diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index 0279e53cd66..b93dcf66171 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -293,22 +293,18 @@ def _dispatch_nonstreaming_api_request(agent, api_kwargs: dict, *, make_client): def should_use_direct_api_call(agent) -> bool: - """True when the LLM call must run inline instead of on the interrupt worker. + """Whether a cron OpenAI-wire request should skip the interrupt worker. - ``interruptible_api_call`` / ``interruptible_streaming_api_call`` run every - request on a spawned daemon worker so the conversation loop can poll for an - interactive interrupt during the blocking HTTP round-trip. Cron jobs execute - their turn inside the gateway's *nested* thread pools (cron-scheduler → - parallel/sequential pool → per-job pool → ``run_conversation``); stacking the - interrupt worker on top of that wedges before the socket even opens on the - 2nd+ call of a tool-using turn (#62151), while the identical job runs fine - via ``hermes cron tick`` (foreground, no nested gateway pools). Cron has no - interactive interrupt surface — its only stop signal is the scheduler's - inactivity watchdog, which fires from the outer thread — so running inline - removes the deadlock class without giving anything up. This predicate is the - single extension point for any future non-interactive, nested-pool context. + Issue #62151 is specific to OpenRouter's chat-completions path inside the + gateway cron thread stack. Keep native/Codex/Bedrock/MoA transports on their + established workers: their cancellation and client ownership differ, and + the report provides no evidence that those paths share the pre-HTTP wedge. """ - return getattr(agent, "platform", None) == "cron" + return ( + getattr(agent, "platform", None) == "cron" + and getattr(agent, "api_mode", None) == "chat_completions" + and getattr(agent, "provider", None) != "moa" + ) def direct_api_call(agent, api_kwargs: dict): diff --git a/tests/agent/test_cron_inline_api_call_62151.py b/tests/agent/test_cron_inline_api_call_62151.py index 11eea93ee32..44d15949082 100644 --- a/tests/agent/test_cron_inline_api_call_62151.py +++ b/tests/agent/test_cron_inline_api_call_62151.py @@ -40,12 +40,21 @@ def _make_agent(*, platform="cron"): return agent -def test_should_use_direct_api_call_only_for_cron_platform(): +def test_should_use_direct_api_call_only_for_cron_openai_wire(): assert should_use_direct_api_call(_make_agent(platform="cron")) is True assert should_use_direct_api_call(_make_agent(platform="cli")) is False assert should_use_direct_api_call(_make_agent(platform="telegram")) is False assert should_use_direct_api_call(_make_agent(platform=None)) is False + for api_mode in ("codex_responses", "anthropic_messages", "bedrock_converse"): + agent = _make_agent(platform="cron") + agent.api_mode = api_mode + assert should_use_direct_api_call(agent) is False + + moa = _make_agent(platform="cron") + moa.provider = "moa" + assert should_use_direct_api_call(moa) is False + def test_direct_api_call_runs_inline_and_closes_client(): agent = _make_agent() diff --git a/tests/cron/test_cron_direct_api_call_62151.py b/tests/cron/test_cron_direct_api_call_62151.py index 28c02a52eb2..f6c4c19d530 100644 --- a/tests/cron/test_cron_direct_api_call_62151.py +++ b/tests/cron/test_cron_direct_api_call_62151.py @@ -21,6 +21,7 @@ def _make_agent(*, platform="cron"): agent = MagicMock() agent.platform = platform agent.api_mode = "chat_completions" + agent.provider = "openrouter" agent._interrupt_requested = False agent._touch_activity = MagicMock() agent._create_request_openai_client = MagicMock() @@ -28,12 +29,21 @@ def _make_agent(*, platform="cron"): return agent -def test_should_use_direct_api_call_only_for_cron_platform(): +def test_should_use_direct_api_call_only_for_cron_openai_wire(): assert should_use_direct_api_call(_make_agent(platform="cron")) is True assert should_use_direct_api_call(_make_agent(platform="cli")) is False assert should_use_direct_api_call(_make_agent(platform="telegram")) is False assert should_use_direct_api_call(_make_agent(platform=None)) is False + for api_mode in ("codex_responses", "anthropic_messages", "bedrock_converse"): + agent = _make_agent(platform="cron") + agent.api_mode = api_mode + assert should_use_direct_api_call(agent) is False + + moa = _make_agent(platform="cron") + moa.provider = "moa" + assert should_use_direct_api_call(moa) is False + def test_direct_api_call_runs_two_sequential_requests_on_same_thread(): """Mirror the 2nd+ call failure mode: two back-to-back completions.create."""