mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-26 17:38:36 +00:00
fix(gateway): route inbound-image decision off the event loop
`_prepare_inbound_message_text` (async) called `_decide_image_input_mode` inline for every inbound image. That decision is synchronous and does blocking network I/O on the way to a capability answer: - `agent.models_dev.fetch_models_dev` — an HTTP GET to models.dev (15s timeout) whenever the 1-hour in-memory cache is cold or models.dev is slow. - `agent.model_metadata.query_ollama_supports_vision` — HTTP probes (`detect_local_server_type` + `/api/show`) against a local Ollama server when the active provider fronts one. Running that inline blocks the gateway event loop for up to the request timeout — so a single user attaching an image freezes EVERY session on that gateway (no other messages processed, no heartbeats) until the fetch/probe returns or times out. This is the same off-the-loop class as the cron-fire verifier and the async_is_safe_url work. Wrap the call in `asyncio.to_thread` so the blocking capability lookup runs on a worker thread and the loop stays responsive. The decision result and routing are unchanged. Test: a gateway image-routing runtime test asserts the capability lookup runs off the main (event-loop) thread; it runs on the main thread before the fix.
This commit is contained in:
parent
06c729706f
commit
95b09d3f78
2 changed files with 52 additions and 1 deletions
|
|
@ -10840,7 +10840,13 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if image_paths:
|
||||
# Decide routing: native (attach pixels) vs text (vision_analyze
|
||||
# pre-run + prepend description). See agent/image_routing.py.
|
||||
_img_mode = self._decide_image_input_mode(
|
||||
# Offload to a worker thread: the decision does blocking network
|
||||
# I/O — a models.dev fetch on cache miss, and the Ollama
|
||||
# ``/api/show`` capability probe for local servers — whose
|
||||
# request timeout would otherwise stall the whole gateway event
|
||||
# loop (every session) while a single image is routed.
|
||||
_img_mode = await asyncio.to_thread(
|
||||
self._decide_image_input_mode,
|
||||
source=source,
|
||||
session_key=session_key,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -143,3 +143,48 @@ async def test_prepare_image_routing_falls_back_to_text_for_text_only_session_ov
|
|||
session_key = runner._session_key_for_source(source)
|
||||
assert result == "[vision summary]\n\nlook"
|
||||
assert runner._pending_native_image_paths_by_session.get(session_key) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_image_routing_runs_off_the_event_loop(monkeypatch):
|
||||
"""The image-routing decision does blocking network I/O — a models.dev fetch
|
||||
on cache miss, and the Ollama ``/api/show`` capability probe for local
|
||||
servers — so it must run on a worker thread. Run inline on the gateway
|
||||
event loop it would freeze *every* session for up to the request timeout
|
||||
while a single image is routed.
|
||||
"""
|
||||
import threading
|
||||
|
||||
runner = _make_runner()
|
||||
source = _source()
|
||||
event = _image_event()
|
||||
cfg = _auto_config()
|
||||
|
||||
monkeypatch.setattr("gateway.run._load_gateway_config", lambda: cfg)
|
||||
monkeypatch.setattr("hermes_cli.config.load_config", lambda: cfg)
|
||||
monkeypatch.setattr("agent.auxiliary_client._read_main_provider", lambda: "xiaomi")
|
||||
monkeypatch.setattr("agent.auxiliary_client._read_main_model", lambda: "mimo-v2.5-pro")
|
||||
monkeypatch.setattr(
|
||||
runner,
|
||||
"_resolve_session_agent_runtime",
|
||||
lambda **_: ("gpt-5.5", {"provider": "openai-codex"}),
|
||||
)
|
||||
|
||||
main_thread = threading.current_thread()
|
||||
seen: dict = {}
|
||||
|
||||
def recording_supports(provider, model, config):
|
||||
# Stands in for the real, blocking capability lookup and records the
|
||||
# thread it executes on.
|
||||
seen["thread"] = threading.current_thread()
|
||||
return True # vision-capable → native routing (skips _enrich_message_with_vision)
|
||||
|
||||
monkeypatch.setattr("agent.image_routing._lookup_supports_vision", recording_supports)
|
||||
|
||||
await runner._prepare_inbound_message_text(event=event, source=source, history=[])
|
||||
|
||||
assert seen.get("thread") is not None, "capability lookup was never reached"
|
||||
assert seen["thread"] is not main_thread, (
|
||||
"the blocking image-routing decision must be offloaded off the gateway "
|
||||
"event loop, not run inline on it"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue