mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-13 02:22:50 +00:00
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.
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user