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:
Chesars
2025-11-19 17:07:46 -03:00
parent 1f8fe007a1
commit 3e58fe42b7
2 changed files with 198 additions and 0 deletions
@@ -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:
@@ -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"]