Skip to content
Closed
42 changes: 42 additions & 0 deletions tests/tools/test_vision_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,48 @@ def test_falls_back_to_default_model(self):
# (the centralized call_llm router picks the default)
assert model is None

def test_config_yaml_model_takes_priority_over_env(self):
"""config.yaml auxiliary.vision.model should be preferred over env var."""
with (
patch(
"tools.vision_tools.vision_analyze_tool", new_callable=AsyncMock
) as mock_tool,
patch(
"hermes_cli.config.load_config",
return_value={"auxiliary": {"vision": {"model": "qwen3.7-plus"}}},
),
patch.dict(os.environ, {"AUXILIARY_VISION_MODEL": "env-model"}),
):
mock_tool.return_value = json.dumps({"result": "ok"})
coro = _handle_vision_analyze(
{"image_url": "https://example.com/img.png", "question": "test"}
)
coro.close()
call_args = mock_tool.call_args
model = call_args[0][2] # third positional arg
assert model == "qwen3.7-plus"

def test_env_var_used_when_config_missing_model(self):
"""Env var should be used when config.yaml has no auxiliary.vision.model."""
with (
patch(
"tools.vision_tools.vision_analyze_tool", new_callable=AsyncMock
) as mock_tool,
patch(
"hermes_cli.config.load_config",
return_value={"auxiliary": {"vision": {}}},
),
patch.dict(os.environ, {"AUXILIARY_VISION_MODEL": "fallback-model"}),
):
mock_tool.return_value = json.dumps({"result": "ok"})
coro = _handle_vision_analyze(
{"image_url": "https://example.com/img.png", "question": "test"}
)
coro.close()
call_args = mock_tool.call_args
model = call_args[0][2]
assert model == "fallback-model"

def test_empty_args_graceful(self):
"""Missing keys should default to empty strings, not raise."""
with patch(
Expand Down
27 changes: 25 additions & 2 deletions tools/vision_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -1213,7 +1213,18 @@ def _handle_vision_analyze(args: Dict[str, Any], **kw: Any) -> Awaitable[str]:
"Fully describe and explain everything about this image, then answer the "
f"following question:\n\n{question}"
)
model = os.getenv("AUXILIARY_VISION_MODEL", "").strip() or None
# Prefer config.yaml auxiliary.vision.model; env var is a legacy override.
model = None
try:
from hermes_cli.config import cfg_get, load_config
_cfg = load_config()
_vmodel = cfg_get(_cfg, "auxiliary", "vision", "model")
if _vmodel:
model = str(_vmodel).strip() or None
except Exception:
pass
if not model:
model = os.getenv("AUXILIARY_VISION_MODEL", "").strip() or None
return vision_analyze_tool(image_url, full_prompt, model)


Expand Down Expand Up @@ -1576,7 +1587,19 @@ def _handle_video_analyze(args: Dict[str, Any], **kw: Any) -> Awaitable[str]:
"including visual content, motion, audio cues, text overlays, and scene "
f"transitions. Then answer the following question:\n\n{question}"
)
model = os.getenv("AUXILIARY_VIDEO_MODEL", "").strip() or os.getenv("AUXILIARY_VISION_MODEL", "").strip() or None
# Prefer config.yaml auxiliary.video.model (falling back to vision);
# env vars are a legacy override.
model = None
try:
from hermes_cli.config import cfg_get, load_config
_cfg = load_config()
_vmodel = cfg_get(_cfg, "auxiliary", "video", "model") or cfg_get(_cfg, "auxiliary", "vision", "model")
if _vmodel:
model = str(_vmodel).strip() or None
except Exception:
pass
if not model:
model = os.getenv("AUXILIARY_VIDEO_MODEL", "").strip() or os.getenv("AUXILIARY_VISION_MODEL", "").strip() or None
return video_analyze_tool(video_url, full_prompt, model)


Expand Down
Loading