From 1b6d8745e15a95a517f5d63a4e16a8daef57ee70 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 12 Jun 2026 06:23:48 +0000 Subject: [PATCH] fix(translation): openai-dialect responses without a wire body error loudly (critic-openai M3) serialize_response fell through to the anthropic assembly when an openai-dialect ChatResponse carried no wire body, silently serving a wrong-shaped body if a future openai-dialect parser forgets to set ChatResponse.wire (azure/azure_ai now extend exactly this path). The function returns Body | TranslationError; both pipeline call sites lift the error into the Result, and a regression test pins the loud error. --- litellm/translation/engine/pipeline.py | 20 ++++++++++++++----- .../inbound/openai_chat/response.py | 19 ++++++++++++++++-- .../test_differential_openai_response.py | 20 +++++++++++++++++++ 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/litellm/translation/engine/pipeline.py b/litellm/translation/engine/pipeline.py index 8fc26b11a9..8b45bcc7d5 100644 --- a/litellm/translation/engine/pipeline.py +++ b/litellm/translation/engine/pipeline.py @@ -180,11 +180,20 @@ def translate_chat_response( ) ) dialect = response_dialect(provider) - return parser(raw_response, request).map( - lambda response: serialize_response(response, deps, dialect) + return parser(raw_response, request).bind( + lambda response: _serialized_body(response, deps, dialect) ) +def _serialized_body( + response: ChatResponse, deps: TranslationDeps, dialect: ResponseDialect +) -> TranslateResult: + body = serialize_response(response, deps, dialect) + if isinstance(body, TranslationError): + return Error(body) + return Ok(body) + + @dataclass(frozen=True) class PreparedRequest: """A request that passed the fail-closed translation; from here on the @@ -268,9 +277,10 @@ async def send_prepared( ) match parser(response.body, prepared.request): case Result(tag="ok", ok=chat_response): - return Ok( - serialize_response(chat_response, deps, response_dialect(provider)) - ) + body = serialize_response(chat_response, deps, response_dialect(provider)) + if isinstance(body, TranslationError): + return Error(ExecuteError.of_translation(body)) + return Ok(body) case Result(error=response_err): return Error(ExecuteError.of_translation(response_err)) diff --git a/litellm/translation/inbound/openai_chat/response.py b/litellm/translation/inbound/openai_chat/response.py index 74590814dc..02f682bc7f 100644 --- a/litellm/translation/inbound/openai_chat/response.py +++ b/litellm/translation/inbound/openai_chat/response.py @@ -29,6 +29,7 @@ from expression import Option from expression.collections import Block from ...deps import TranslationDeps +from ...errors import BoundaryError, TranslationError from ...ir import Body, ChatResponse, ContentBlock, PlainJson, ResponseUsage ResponseDialect = Literal["anthropic", "bedrock_converse", "openai", "gemini"] @@ -38,13 +39,27 @@ def serialize_response( response: ChatResponse, deps: TranslationDeps, dialect: ResponseDialect = "anthropic", -) -> Body: +) -> Body | TranslationError: if dialect == "openai": match response.wire: case Option(tag="some", some=blob) if isinstance(blob.value, dict): return blob.value case _: - pass # no wire body: fall through to the anthropic assembly + # The openai dialect has no assembly of its own: the provider + # parser MUST ride the normalized body on ChatResponse.wire. + # A registered openai-dialect parser that does not is a + # wiring bug; serving the anthropic assembly here would put a + # wrong-shaped body on the wire silently (critic-openai M3). + return TranslationError.of_boundary( + BoundaryError.of( + Block.of_seq( + [ + "openai-dialect response carries no wire body;" + " the provider parser must set ChatResponse.wire" + ] + ) + ) + ) if dialect == "gemini": return _gemini_body(response) text = "".join(block.text.text for block in response.content if block.tag == "text") diff --git a/tests/test_litellm/translation/test_differential_openai_response.py b/tests/test_litellm/translation/test_differential_openai_response.py index 0a50732a7f..16e9b14ba6 100644 --- a/tests/test_litellm/translation/test_differential_openai_response.py +++ b/tests/test_litellm/translation/test_differential_openai_response.py @@ -345,3 +345,23 @@ def test_unreachable_response_shape_is_a_typed_error(name: str) -> None: result = parse_response(copy.deepcopy(raw), parsed.ok) assert result.is_error(), f"{name} unexpectedly parsed" assert reason_fragment in result.error.summary, result.error.summary + + +def test_openai_dialect_without_wire_body_is_a_loud_error() -> None: + """An openai-dialect response whose parser failed to set + ChatResponse.wire must be a typed error, never a silently served + anthropic-shaped body (critic-openai M3).""" + import dataclasses + + from expression import Nothing + + from litellm.translation.errors import TranslationError + + parsed = parse_request(copy.deepcopy(_REQUEST)) + assert parsed.is_ok() + response = parse_response(copy.deepcopy(_RESPONSES["text"]), parsed.ok) + assert response.is_ok() + wireless = dataclasses.replace(response.ok, wire=Nothing) + body = serialize_response(wireless, build_translation_deps(), "openai") + assert isinstance(body, TranslationError) + assert "wire body" in body.summary