Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions agent/conversation_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import threading
import time
import uuid
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Callable

from agent.codex_responses_adapter import _summarize_user_message_for_log
from agent.display import KawaiiSpinner
Expand Down Expand Up @@ -374,7 +374,7 @@ def run_conversation(
system_message: str = None,
conversation_history: List[Dict[str, Any]] = None,
task_id: str = None,
stream_callback: Optional[callable] = None,
stream_callback: Optional[Callable[..., None]] = None,
persist_user_message: Optional[str] = None,
) -> Dict[str, Any]:
"""
Expand Down
20 changes: 11 additions & 9 deletions agent/image_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
| ``text``, default ``auto``) and the active model's capability metadata.

In ``auto`` mode:
- If the user has explicitly configured ``auxiliary.vision.provider``
- If the active model reports ``supports_vision=True`` in its
models.dev metadata (or via the ``supports_vision`` config override),
we attach natively — the main model can see the pixels directly.
- Otherwise, if the user has explicitly configured ``auxiliary.vision.provider``
(i.e. not ``auto`` and not empty), we assume they want the text pipeline
regardless of the main model — they've opted in to a specific vision
backend for a reason (cost, quality, local-only, etc.).
- Otherwise, if the active model reports ``supports_vision=True`` in its
models.dev metadata, we attach natively.
as a fallback (they've opted in to a specific vision backend for a reason:
cost, quality, local-only, etc.).
- Otherwise (non-vision model, no explicit override), we fall back to text.

This keeps ``vision_analyze`` surfaced as a tool in every session — skills
Expand Down Expand Up @@ -337,12 +338,13 @@ def decide_image_input_mode(
return "text"

# auto
if _explicit_aux_vision_override(cfg):
return "text"

supports = _lookup_supports_vision(provider, model, cfg)
if supports is True:
return "native"
return "native" # main model can see images

if _explicit_aux_vision_override(cfg):
return "text" # main model can't see -> use aux if configured

return "text"


Expand Down
2 changes: 1 addition & 1 deletion gateway/stream_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def __init__(
chat_id: str,
config: Optional[StreamConsumerConfig] = None,
metadata: Optional[dict] = None,
on_new_message: Optional[callable] = None,
on_new_message: Optional[Callable[..., None]] = None,
initial_reply_to_id: Optional[str] = None,
):
self.adapter = adapter
Expand Down
4 changes: 2 additions & 2 deletions hermes_cli/dingtalk_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import sys
import time
import logging
from typing import Optional, Tuple
from typing import Optional, Tuple, Callable

import requests

Expand Down Expand Up @@ -107,7 +107,7 @@ def wait_for_registration_success(
device_code: str,
interval: int = 3,
expires_in: int = 7200,
on_waiting: Optional[callable] = None,
on_waiting: Optional[Callable[..., None]] = None,
) -> Tuple[str, str]:
"""Block until the registration succeeds or times out.

Expand Down
4 changes: 2 additions & 2 deletions plugins/disk-cleanup/disk_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Callable

try:
from hermes_constants import get_hermes_home
Expand Down Expand Up @@ -397,7 +397,7 @@ def quick() -> Dict[str, Any]:
# ---------------------------------------------------------------------------

def deep(
confirm: Optional[callable] = None,
confirm: Optional[Callable[[Any], bool]] = None,
) -> Dict[str, Any]:
"""Deep cleanup.

Expand Down
6 changes: 3 additions & 3 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
import time
import threading
import uuid
from typing import List, Dict, Any, Optional
from typing import List, Dict, Any, Optional, Callable
# NOTE: `from openai import OpenAI` is deliberately NOT at module top — the
# SDK pulls ~240 ms of imports. We expose `OpenAI` as a thin proxy object
# that imports the SDK on first call/isinstance check. This preserves:
Expand Down Expand Up @@ -5095,14 +5095,14 @@ def run_conversation(
system_message: str = None,
conversation_history: List[Dict[str, Any]] = None,
task_id: str = None,
stream_callback: Optional[callable] = None,
stream_callback: Optional[Callable[..., None]] = None,
persist_user_message: Optional[str] = None,
) -> Dict[str, Any]:
"""Forwarder — see ``agent.conversation_loop.run_conversation``."""
from agent.conversation_loop import run_conversation
return run_conversation(self, user_message, system_message, conversation_history, task_id, stream_callback, persist_user_message)

def chat(self, message: str, stream_callback: Optional[callable] = None) -> str:
def chat(self, message: str, stream_callback: Optional[Callable[..., None]] = None) -> str:
"""
Simple chat interface that returns just the final response.

Expand Down
19 changes: 15 additions & 4 deletions tests/agent/test_image_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,15 +279,26 @@ def test_auto_text_for_custom_with_no_override(self):
with patch("agent.models_dev.get_model_capabilities", return_value=None):
assert decide_image_input_mode("custom", "unknown", {}) == "text"

def test_explicit_aux_vision_override_still_wins(self):
# If the user has configured a dedicated vision aux backend, respect
# it even when supports_vision: true is also set.
def test_explicit_aux_vision_override_only_applies_when_main_model_not_vision(self):
# Aux override is a fallback: when the main model supports vision,
# images go native regardless of aux config. The aux override only
# applies when the main model is non-vision.
cfg = {
"model": {"supports_vision": True},
"auxiliary": {"vision": {"provider": "openrouter", "model": "gemini-2.5-pro"}},
}
with patch("agent.models_dev.get_model_capabilities", return_value=None):
assert decide_image_input_mode("custom", "qwen3.6-35b", cfg) == "text"
assert decide_image_input_mode("custom", "qwen3.6-35b", cfg) == "native"

def test_aux_vision_override_wins_when_main_model_not_vision(self):
# When the main model does NOT support vision, the explicit aux
# override still forces text routing.
cfg = {
"model": {"supports_vision": False},
"auxiliary": {"vision": {"provider": "openrouter", "model": "gemini-2.5-pro"}},
}
with patch("agent.models_dev.get_model_capabilities", return_value=None):
assert decide_image_input_mode("custom", "text-only-model", cfg) == "text"


# ─── build_native_content_parts ──────────────────────────────────────────────
Expand Down
4 changes: 2 additions & 2 deletions tools/delegate_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
ThreadPoolExecutor,
TimeoutError as FuturesTimeoutError,
)
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Callable

from toolsets import TOOLSETS

Expand Down Expand Up @@ -725,7 +725,7 @@ def _build_child_progress_callback(
depth: Optional[int] = None,
model: Optional[str] = None,
toolsets: Optional[List[str]] = None,
) -> Optional[callable]:
) -> Optional[Callable[..., None]]:
"""Build a callback that relays child agent tool calls to the parent display.

Two display paths:
Expand Down