Tighten Responses bad request fallback

Keep fallback for endpoint-specific and Venice-style Responses payload rejections, but stop treating generic Bad Request validation text as proof that Responses is unsupported.

Add a regression that generic pre-output Responses 400s raise without trying Chat Completions.
This commit is contained in:
Alessandro 2026-06-25 21:51:13 +02:00
parent eab40dc121
commit 5c914bc49e
3 changed files with 46 additions and 5 deletions

View file

@ -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",
)
)

View file

@ -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.

View file

@ -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,