mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-18 06:26:16 +00:00
fix: Support response_format parameter in completion -> responses bridge
Fixes #16810
## Problem
When using completion() with models that have mode: "responses" (like o3-pro,
gpt-5-codex), the response_format parameter with JSON schemas was being ignored
or incorrectly handled, causing:
- Large schemas (>512 chars) to fail with "metadata.schema_dict_json: string too long" error
- Structured outputs to be silently dropped
- Users' code to break unexpectedly
## Root Cause
The completion -> responses bridge in
litellm/completion_extras/litellm_responses_transformation/transformation.py
was missing the conversion of response_format (Chat Completion format) to
text.format (Responses API format).
The inverse bridge (responses -> completion) already had this conversion
implemented in commit 29f0ed223a, but the completion -> responses direction
was incomplete.
## Solution
Added _transform_response_format_to_text_format() method that converts:
- response_format with json_schema → text.format with json_schema
- response_format with json_object → text.format with json_object
- response_format with text → text.format with text
Updated transform_request() to detect and convert response_format parameter
before sending to litellm.responses().
## Changes
- Added _transform_response_format_to_text_format() method (lines 592-647)
- Modified transform_request() to handle response_format (lines 199-203)
- Added comprehensive tests to validate the conversion
## Testing
- 5 new unit tests covering all conversion scenarios
- Real API test with OpenAI confirming large schemas (>512 chars) work
- No more metadata.schema_dict_json errors
## Impact
Users can now use completion() with models that have mode: "responses" and:
- Use large JSON schemas without hitting metadata 512 char limit
- Get proper structured outputs
- Have their existing code continue working
This commit is contained in:
@@ -196,6 +196,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
||||
cast(List[Dict[str, Any]], value)
|
||||
)
|
||||
)
|
||||
elif key == "response_format":
|
||||
# Convert response_format to text.format
|
||||
text_format = self._transform_response_format_to_text_format(value)
|
||||
if text_format:
|
||||
responses_api_request["text"] = text_format # type: ignore
|
||||
elif key in ResponsesAPIOptionalRequestParams.__annotations__.keys():
|
||||
responses_api_request[key] = value # type: ignore
|
||||
elif key in ("metadata"):
|
||||
@@ -589,6 +594,63 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
||||
return Reasoning(effort="minimal")
|
||||
return None
|
||||
|
||||
def _transform_response_format_to_text_format(
|
||||
self, response_format: Union[Dict[str, Any], Any]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Transform Chat Completion response_format parameter to Responses API text.format parameter.
|
||||
|
||||
Chat Completion response_format structure:
|
||||
{
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "schema_name",
|
||||
"schema": {...},
|
||||
"strict": True
|
||||
}
|
||||
}
|
||||
|
||||
Responses API text parameter structure:
|
||||
{
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": "schema_name",
|
||||
"schema": {...},
|
||||
"strict": True
|
||||
}
|
||||
}
|
||||
"""
|
||||
if not response_format:
|
||||
return None
|
||||
|
||||
if isinstance(response_format, dict):
|
||||
format_type = response_format.get("type")
|
||||
|
||||
if format_type == "json_schema":
|
||||
json_schema = response_format.get("json_schema", {})
|
||||
return {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"name": json_schema.get("name", "response_schema"),
|
||||
"schema": json_schema.get("schema", {}),
|
||||
"strict": json_schema.get("strict", False),
|
||||
}
|
||||
}
|
||||
elif format_type == "json_object":
|
||||
return {
|
||||
"format": {
|
||||
"type": "json_object"
|
||||
}
|
||||
}
|
||||
elif format_type == "text":
|
||||
return {
|
||||
"format": {
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
def _map_responses_status_to_finish_reason(self, status: Optional[str]) -> str:
|
||||
"""Map responses API status to chat completion finish_reason"""
|
||||
if not status:
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Test for response_format to text.format conversion in completion -> responses bridge
|
||||
"""
|
||||
import pytest
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
|
||||
|
||||
def test_transform_response_format_to_text_format_json_schema():
|
||||
"""Test conversion of response_format with json_schema to text.format"""
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
# Chat Completion format
|
||||
response_format = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "person_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"}
|
||||
},
|
||||
"required": ["name", "age"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"strict": True
|
||||
}
|
||||
}
|
||||
|
||||
# Convert to Responses API format
|
||||
result = handler._transform_response_format_to_text_format(response_format)
|
||||
|
||||
# Verify conversion
|
||||
assert result is not None
|
||||
assert "format" in result
|
||||
assert result["format"]["type"] == "json_schema"
|
||||
assert result["format"]["name"] == "person_schema"
|
||||
assert result["format"]["strict"] is True
|
||||
assert "schema" in result["format"]
|
||||
assert result["format"]["schema"]["type"] == "object"
|
||||
assert "properties" in result["format"]["schema"]
|
||||
|
||||
|
||||
def test_transform_response_format_to_text_format_json_object():
|
||||
"""Test conversion of response_format with json_object to text.format"""
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
response_format = {
|
||||
"type": "json_object"
|
||||
}
|
||||
|
||||
result = handler._transform_response_format_to_text_format(response_format)
|
||||
|
||||
assert result is not None
|
||||
assert "format" in result
|
||||
assert result["format"]["type"] == "json_object"
|
||||
|
||||
|
||||
def test_transform_response_format_to_text_format_text():
|
||||
"""Test conversion of response_format with text to text.format"""
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
response_format = {
|
||||
"type": "text"
|
||||
}
|
||||
|
||||
result = handler._transform_response_format_to_text_format(response_format)
|
||||
|
||||
assert result is not None
|
||||
assert "format" in result
|
||||
assert result["format"]["type"] == "text"
|
||||
|
||||
|
||||
def test_transform_response_format_to_text_format_none():
|
||||
"""Test that None input returns None"""
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
result = handler._transform_response_format_to_text_format(None)
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_transform_request_with_response_format():
|
||||
"""Test that transform_request correctly handles response_format parameter"""
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Extract person info: John Doe, 30 years old"}
|
||||
]
|
||||
|
||||
optional_params = {
|
||||
"response_format": {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "person_schema",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"age": {"type": "integer"}
|
||||
},
|
||||
"required": ["name", "age"],
|
||||
"additionalProperties": False
|
||||
},
|
||||
"strict": True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
litellm_params = {}
|
||||
headers = {}
|
||||
|
||||
# Mock logging object
|
||||
class MockLoggingObj:
|
||||
pass
|
||||
|
||||
litellm_logging_obj = MockLoggingObj()
|
||||
|
||||
result = handler.transform_request(
|
||||
model="o3-pro",
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
headers=headers,
|
||||
litellm_logging_obj=litellm_logging_obj,
|
||||
)
|
||||
|
||||
# Verify that text parameter was set with converted format
|
||||
assert "text" in result
|
||||
assert result["text"] is not None
|
||||
assert "format" in result["text"]
|
||||
assert result["text"]["format"]["type"] == "json_schema"
|
||||
assert result["text"]["format"]["name"] == "person_schema"
|
||||
assert "schema" in result["text"]["format"]
|
||||
Reference in New Issue
Block a user