This commit is contained in:
Atharva-Kanherkar 2026-11-05 10:47:11 +00:00 • committed by GitHub
commit 1fec22b40d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 64 additions and 3 deletions

View file

@ -4696,11 +4696,11 @@ class PythonCore(DeprecatedRouterMembers):
"""What a fallback continues from once a Responses stream broke after text, raising the primary's error
when the request constrains its output or the stream began a tool call, since a continued JSON document or
tool call would not parse, and when a content filter cut it, so no other model finishes filtered text."""
if source_iterator.started_tool_call:
PythonCore._raise_original_mid_stream_error(e)
if _mid_stream_error_before_output(e):
return None
if source_iterator.started_tool_call or isinstance(
e.original_exception, litellm_exceptions.ContentPolicyViolationError
):
if isinstance(e.original_exception, litellm_exceptions.ContentPolicyViolationError):
PythonCore._raise_original_mid_stream_error(e)
cut_deployment_id: Final = (get_hidden_params(source_iterator) or {}).get("model_id")
cut_deployment: Final = (

View file

@ -1643,6 +1643,67 @@ async def test_responses_fallback_surfaces_the_primary_error_once_the_cut_stream
router.discard()
def _openai_stream_cut_after_a_tool_call_without_text(failure: Mapping[str, object]) -> httpx.Response:
call: Final = {
"type": "function_call", "id": "fc_primary", "call_id": "call_primary",
"name": "lookup", "arguments": '{"city": "Paris"}', "status": "completed",
}
return _sse(
{"type": "response.created", "sequence_number": 0, "response": {
"id": "resp_primary", "object": "response", "created_at": 0, "status": "in_progress",
"model": "gpt-6-astra", "output": [],
}},
{
"type": "response.output_item.added", "sequence_number": 1, "output_index": 0,
"item": {**call, "arguments": "", "status": "in_progress"},
},
{
"type": "response.function_call_arguments.delta", "sequence_number": 2,
"item_id": "fc_primary", "output_index": 0, "delta": call["arguments"],
},
{"type": "response.output_item.done", "sequence_number": 3, "output_index": 0, "item": call},
{**failure, "sequence_number": 4},
)
@pytest.mark.asyncio
@pytest.mark.parametrize(
"failure",
[
{"type": "error", "error": {"type": "server_error", "code": "internal_error", "message": "interrupted"}},
{"type": "response.failed", "response": {
"id": "resp_primary", "object": "response", "created_at": 0, "status": "failed",
"model": "gpt-6-astra", "output": [],
"error": {"code": "server_error", "message": "interrupted"},
}},
],
ids=["error", "response_failed"],
)
async def test_responses_fallback_surfaces_the_primary_error_once_the_cut_stream_delivered_a_tool_call_without_text(
failure: Mapping[str, object], respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
respx_mock.post("https://api.openai.com/v1/responses").mock(
return_value=_openai_stream_cut_after_a_tool_call_without_text(failure)
)
fallback: Final = respx_mock.post("https://api.anthropic.com/v1/messages").mock(
return_value=_anthropic_stream("It is sunny in Paris.")
)
router: Final = _primary_with_fallback("anthropic/claude-opus-5-5")
try:
stream: Final = await router.aresponses(
model="primary",
input="What is the weather in Paris?",
stream=True,
tools=[{"type": "function", "name": "lookup", "parameters": {"type": "object"}}],
)
with pytest.raises(litellm.InternalServerError, match="interrupted"):
[event async for event in stream]
assert fallback.call_count == 0
finally:
router.discard()
def _openai_stream_cut_after_event(preamble: str, event: Mapping[str, object]) -> httpx.Response:
return _sse(
{