mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(chatgpt): keep text and accept string input for the Codex backend
Two request-side defects stop chatgpt/* serving structured output or a string
input.
The allowlist at the end of transform_responses_api_request omits "text". The
chat-to-responses bridge translates response_format into text.format, so the
allowlist discards strict schemas silently and the backend answers with prose
instead of JSON. The backend does honour text.format when it receives it.
Separately the backend rejects a bare string input with
{"detail": "Input must be a list"}, while the Responses API itself accepts
either shape, so the string needs wrapping before it is sent.
Both are request-shaping only, so neither depends on the streaming work in
#31332 or #34095, though those two block the same call and have to be in place
to observe this one end to end.
The "text" case was reported in #24356 and closed by the stale bot without a
fix.
Signed-off-by: Alexander Chernov <alexander@chernov.it>
This commit is contained in:
parent
f005afa146
commit
9aaf01d8ea
2 changed files with 98 additions and 0 deletions
|
|
@ -66,6 +66,10 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
litellm_params: GenericLiteLLMParams,
|
||||
headers: dict,
|
||||
) -> dict:
|
||||
# The Responses API accepts a string or a list, but this backend
|
||||
# rejects a string with {"detail": "Input must be a list"}.
|
||||
if isinstance(input, str):
|
||||
input = [{"role": "user", "content": input}]
|
||||
request: Final = super().transform_responses_api_request(
|
||||
model,
|
||||
input,
|
||||
|
|
@ -99,6 +103,9 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig):
|
|||
"reasoning",
|
||||
"previous_response_id",
|
||||
"truncation",
|
||||
# The chat-to-responses bridge translates response_format into
|
||||
# "text"; dropping it here discards strict schemas silently.
|
||||
"text",
|
||||
}
|
||||
|
||||
return {k: v for k, v in request.items() if k in allowed_keys}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,97 @@ class TestChatGPTResponsesAPITransformation:
|
|||
"function": {"name": "hello"},
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"chatgpt/gpt-5.4",
|
||||
"chatgpt/gpt-5.3-codex",
|
||||
],
|
||||
)
|
||||
def test_chatgpt_preserves_text_for_structured_output(self, model_name):
|
||||
"""text carries the schema, so dropping it loses structured output.
|
||||
|
||||
The chat-to-responses bridge turns response_format into text.format,
|
||||
so an allowlist without "text" silently discards strict schemas and
|
||||
the backend answers with prose.
|
||||
"""
|
||||
config = ChatGPTResponsesAPIConfig()
|
||||
text_param = {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "ExtractedEntities",
|
||||
"strict": True,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
request = config.transform_responses_api_request(
|
||||
model=model_name,
|
||||
input="hi",
|
||||
response_api_optional_request_params={"text": text_param},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert request["text"] == text_param
|
||||
assert request["text"]["format"]["type"] == "json_schema"
|
||||
assert request["text"]["format"]["strict"] is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"chatgpt/gpt-5.4",
|
||||
"chatgpt/gpt-5.3-codex",
|
||||
],
|
||||
)
|
||||
def test_chatgpt_coerces_string_input_to_list(self, model_name):
|
||||
"""The backend rejects a string input with "Input must be a list".
|
||||
|
||||
The Responses API itself accepts either, so the string has to be
|
||||
wrapped before it reaches this backend.
|
||||
"""
|
||||
config = ChatGPTResponsesAPIConfig()
|
||||
|
||||
request = config.transform_responses_api_request(
|
||||
model=model_name,
|
||||
input="say hi",
|
||||
response_api_optional_request_params={},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert isinstance(request["input"], list)
|
||||
assert request["input"] == [{"role": "user", "content": "say hi"}]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"model_name",
|
||||
[
|
||||
"chatgpt/gpt-5.4",
|
||||
],
|
||||
)
|
||||
def test_chatgpt_leaves_list_input_untouched(self, model_name):
|
||||
"""Only a bare string needs wrapping; a list must pass through."""
|
||||
config = ChatGPTResponsesAPIConfig()
|
||||
original = [
|
||||
{"role": "system", "content": "be brief"},
|
||||
{"role": "user", "content": "say hi"},
|
||||
]
|
||||
|
||||
request = config.transform_responses_api_request(
|
||||
model=model_name,
|
||||
input=list(original),
|
||||
response_api_optional_request_params={},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert request["input"] == original
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model_name", "response_model"),
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue