diff --git a/helpers/litellm_transport.py b/helpers/litellm_transport.py index 9298ede59..9e7a290f9 100644 --- a/helpers/litellm_transport.py +++ b/helpers/litellm_transport.py @@ -1574,10 +1574,9 @@ def _looks_like_responses_request_rejected(text: str) -> bool: "expected object, received string", "expected string, received array", "zod", - "invalid request", - "invalid type", - "failed to deserialize", - "validation error", + "failed to deserialize input", + "failed to deserialize response", + "failed to deserialize responses", ) ) diff --git a/helpers/litellm_transport.py.dox.md b/helpers/litellm_transport.py.dox.md index da8836150..938af1432 100644 --- a/helpers/litellm_transport.py.dox.md +++ b/helpers/litellm_transport.py.dox.md @@ -26,7 +26,7 @@ - Strip Agent Zero internal kwargs before sending requests to LiteLLM. - Do not send orphan tool controls when no tools are present; strict OpenAI-compatible servers can reject empty `tools` arrays. - Prefer Responses API when configured, but fallback to Chat Completions when the provider does not support Responses. -- Fall back to Chat Completions when a Responses request is rejected before any output by a Bad Request/validation error that indicates the provider cannot parse the Responses request shape. +- Fall back to Chat Completions when a Responses request is rejected before any output by an endpoint-specific or shape-specific Bad Request indicating the provider cannot parse Responses payloads. - Preserve provider-state metadata when Responses API calls succeed, and fall back to local replay when provider state is unsupported. - Keep prompt-cache markers only for providers that accept them. diff --git a/tests/test_stream_tool_early_stop.py b/tests/test_stream_tool_early_stop.py index 791e70e78..a1109948f 100644 --- a/tests/test_stream_tool_early_stop.py +++ b/tests/test_stream_tool_early_stop.py @@ -611,6 +611,48 @@ async def test_unified_call_falls_back_when_responses_bad_request_rejects_shape( assert calls == ["responses", "chat"] +@pytest.mark.asyncio +async def test_unified_call_raises_generic_responses_bad_request(monkeypatch): + class BadRequestError(Exception): + status_code = 400 + + calls: list[str] = [] + + async def fake_aresponses(*args, **kwargs): + calls.append("responses") + raise BadRequestError( + "BadRequestError: validation error: invalid request: max_tokens is too high" + ) + + async def fake_acompletion(*args, **kwargs): + calls.append("chat") + raise AssertionError("generic 400 should not fallback to chat") + + async def fake_rate_limiter(*args, **kwargs): + return None + + monkeypatch.setattr(litellm_transport, "aresponses", fake_aresponses) + monkeypatch.setattr(litellm_transport, "acompletion", fake_acompletion) + monkeypatch.setattr(models, "apply_rate_limiter", fake_rate_limiter) + + wrapper = models.LiteLLMChatWrapper( + model="test-model", + provider="openai", + model_config=None, + ) + + async def response_callback(chunk: str, full: str): + return None + + with pytest.raises(BadRequestError): + await wrapper.unified_call( + messages=[], + response_callback=response_callback, + ) + + assert calls == ["responses"] + + @pytest.mark.asyncio async def test_unified_call_preserves_cache_control_with_chat_for_non_native_responses( monkeypatch,