diff --git a/agent/transports/chat_completions.py b/agent/transports/chat_completions.py index 0d2a3fd519..d23af22306 100644 --- a/agent/transports/chat_completions.py +++ b/agent/transports/chat_completions.py @@ -110,6 +110,32 @@ def _add_prompt_cache_key( api_kwargs["prompt_cache_key"] = cache_key +_ROUTER_TIMEOUT_SHIM = "Connect timeout, please try again later." + + +def _has_positive_completion_tokens(usage: Any) -> bool: + """Return whether a response usage object proves text was generated.""" + for field in ("completion_tokens", "output_tokens"): + value = usage.get(field) if isinstance(usage, dict) else getattr(usage, field, None) + if isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0: + return True + return False + + +def _is_router_timeout_shim(response: Any) -> bool: + """Recognize a router failure encoded as a successful ChatCompletion.""" + choices = getattr(response, "choices", None) + if not isinstance(choices, list) or len(choices) != 1: + return False + message = getattr(choices[0], "message", None) + content = getattr(message, "content", None) + if not isinstance(content, str) or content.strip() != _ROUTER_TIMEOUT_SHIM: + return False + if getattr(message, "tool_calls", None): + return False + return not _has_positive_completion_tokens(getattr(response, "usage", None)) + + def _reasoning_config_for_model(model: str, reasoning_config: dict | None) -> dict | None: """Clamp Hermes' extended effort set (``ultra``) to the OpenAI-compat wire vocabulary. @@ -603,8 +629,10 @@ class ChatCompletionsTransport(ProviderTransport): ) def validate_response(self, response: Any) -> bool: - """Check that response has valid choices.""" - return bool(response is not None and getattr(response, "choices", None)) + """Check that response has valid choices and is not a router failure shim.""" + if response is None or not getattr(response, "choices", None): + return False + return not _is_router_timeout_shim(response) def extract_cache_stats(self, response: Any) -> dict[str, int] | None: """Cache stats from prompt_tokens_details (OpenRouter/OpenAI) or DeepSeek's top-level prompt_cache_hit_tokens.""" diff --git a/tests/agent/transports/test_chat_completions.py b/tests/agent/transports/test_chat_completions.py index 4a4541d0e2..a21980d6be 100644 --- a/tests/agent/transports/test_chat_completions.py +++ b/tests/agent/transports/test_chat_completions.py @@ -584,6 +584,36 @@ class TestChatCompletionsValidate: + @pytest.mark.parametrize("usage", [None, SimpleNamespace(completion_tokens=0)]) + def test_rejects_known_router_timeout_shim_without_generated_tokens(self, transport, usage): + """#68396: an HTTP-200 router timeout shim with no generated tokens is not a completion.""" + response = SimpleNamespace( + choices=[SimpleNamespace(message=SimpleNamespace( + content="Connect timeout, please try again later.", + tool_calls=None, + ))], + usage=usage, + ) + + assert transport.validate_response(response) is False + + @pytest.mark.parametrize( + ("content", "tool_calls", "usage"), + [ + ("Connect timeout, please try again later.", None, SimpleNamespace(completion_tokens=1)), + ("The router said: Connect timeout, please try again later.", None, None), + ("Connect timeout, please try again later.", [SimpleNamespace()], None), + ], + ) + def test_accepts_non_shim_timeout_text(self, transport, content, tool_calls, usage): + """Positive controls (#68396): generated tokens, embedded phrase, or tool calls stay valid.""" + response = SimpleNamespace( + choices=[SimpleNamespace(message=SimpleNamespace(content=content, tool_calls=tool_calls))], + usage=usage, + ) + + assert transport.validate_response(response) is True + def test_valid(self, transport): r = SimpleNamespace(choices=[SimpleNamespace(message=SimpleNamespace(content="hi"))]) assert transport.validate_response(r) is True