mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(responses): raise instead of dropping a format with no readable type
Greptile caught a silent path this PR had reintroduced for malformed input: `_convert_response_format_to_text_param` returned None when it could not read a `type`, and because both entry points pop `response_format` from kwargs before conversion, the request then went out completely unconstrained -- the exact silent drop this change exists to remove, just narrowed to bad input. Raise instead. A bare ValueError would be wrong here: exception_type() does not recognise it and surfaces it as APIConnectionError, reporting a malformed argument as a network fault. litellm.BadRequestError passes through intact with the message the caller needs, and matches how the prompt-template helpers report the same kind of bad content block. Also covers text_format, which previously raised a bare KeyError on the same input.
This commit is contained in:
parent
5365c3fdcc
commit
da207f1f4c
2 changed files with 50 additions and 14 deletions
|
|
@ -919,7 +919,7 @@ class ResponsesAPIRequestUtils:
|
|||
@staticmethod
|
||||
def _convert_response_format_to_text_param(
|
||||
response_format: type["BaseModel"] | dict | None,
|
||||
) -> Optional["ResponseText"]:
|
||||
) -> "ResponseText":
|
||||
"""
|
||||
Convert a Chat-Completions style `response_format` (or a Pydantic model) into
|
||||
the Responses API `text` parameter.
|
||||
|
|
@ -933,19 +933,28 @@ class ResponsesAPIRequestUtils:
|
|||
The schema-less formats (`{"type": "json_object"}`, `{"type": "text"}`) carry no
|
||||
extra fields and map straight across.
|
||||
|
||||
Returns:
|
||||
ResponseText object with the converted format, or None if conversion fails
|
||||
Raises:
|
||||
litellm.BadRequestError: if no `type` can be read from the supplied format.
|
||||
Returning None here would drop the caller's format and send the request
|
||||
unconstrained, which is the silent failure this conversion exists to
|
||||
remove. A bare ValueError would surface through exception_type() as
|
||||
APIConnectionError, reporting malformed input as a network fault.
|
||||
"""
|
||||
from litellm.llms.base_llm.base_utils import type_to_response_format_param
|
||||
|
||||
# Normalizes a Pydantic model into a response_format dict; passes a dict through.
|
||||
converted: Final = type_to_response_format_param(response_format)
|
||||
if converted is None:
|
||||
return None
|
||||
|
||||
converted: Final = type_to_response_format_param(response_format) or {}
|
||||
format_type: Final = converted.get("type")
|
||||
if format_type is None:
|
||||
return None
|
||||
raise litellm.BadRequestError(
|
||||
message=(
|
||||
f"Could not read a `type` from the supplied response format: {response_format!r}. "
|
||||
'Expected {"type": "json_schema", "json_schema": {...}}, {"type": "json_object"}, '
|
||||
'{"type": "text"}, or a Pydantic model.'
|
||||
),
|
||||
model=None,
|
||||
llm_provider=None,
|
||||
)
|
||||
if format_type != "json_schema":
|
||||
return {"format": {"type": format_type}}
|
||||
|
||||
|
|
@ -980,16 +989,16 @@ class ResponsesAPIRequestUtils:
|
|||
neither text nor text_format was supplied)
|
||||
|
||||
Returns:
|
||||
ResponseText object with the converted format, or None if conversion fails
|
||||
ResponseText object with the converted format, or None if none was supplied
|
||||
|
||||
Raises:
|
||||
litellm.BadRequestError: if a format was supplied but no `type` could be read from it
|
||||
"""
|
||||
if text is not None:
|
||||
return text
|
||||
for candidate in (text_format, response_format):
|
||||
if candidate is None:
|
||||
continue
|
||||
converted = ResponsesAPIRequestUtils._convert_response_format_to_text_param(candidate)
|
||||
if converted is not None:
|
||||
return converted
|
||||
if candidate is not None:
|
||||
return ResponsesAPIRequestUtils._convert_response_format_to_text_param(candidate)
|
||||
return text
|
||||
|
||||
@staticmethod
|
||||
|
|
|
|||
|
|
@ -172,3 +172,30 @@ def test_text_format_takes_precedence_over_response_format():
|
|||
def test_no_format_leaves_text_unset():
|
||||
"""Absent every spelling, nothing is invented."""
|
||||
assert "text" not in _capture_request_params()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad_format",
|
||||
[
|
||||
{},
|
||||
{"json_schema": {"name": "Flags", "schema": FLAGS_SCHEMA}}, # `type` omitted
|
||||
],
|
||||
)
|
||||
def test_response_format_without_type_raises(bad_format):
|
||||
"""
|
||||
A format with no readable `type` cannot be converted. It must raise rather than
|
||||
return None: response_format is consumed before the request is built, so returning
|
||||
None would send the request unconstrained -- reintroducing, for malformed input,
|
||||
exactly the silent drop this conversion exists to remove.
|
||||
|
||||
The helper raises ValueError; responses() surfaces it through exception_type() as
|
||||
BadRequestError, which is what a caller actually sees.
|
||||
"""
|
||||
with pytest.raises(litellm.BadRequestError, match="Could not read a `type`"):
|
||||
_capture_request_params(response_format=bad_format)
|
||||
|
||||
|
||||
def test_text_format_without_type_raises():
|
||||
"""Same guarantee for the text_format spelling, which previously raised KeyError."""
|
||||
with pytest.raises(litellm.BadRequestError, match="Could not read a `type`"):
|
||||
_capture_request_params(text_format={"json_schema": {"name": "Flags"}})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue