fix: Support text.format parameter in Responses API for providers without native ResponsesAPIConfig (#16023)

Fixes #15995

When using the Responses API with providers that don't have a native
ResponsesAPIConfig implementation (like Gemini, Anthropic, Cohere, etc.),
the text.format parameter was being ignored. This happened because these
providers fall back to using the LiteLLMCompletionTransformationHandler,
which converts Responses API requests to Chat Completion API requests.

Changes:
- Added 'text' to the list of supported parameters in
  LiteLLMCompletionResponsesConfig.get_supported_openai_params()
- Added transformation logic to convert text.format (Responses API format)
  to response_format (Chat Completion API format) in
  transform_responses_api_request_to_chat_completion_request()
- Created _transform_text_format_to_response_format() method to handle
  the conversion between the two format structures

The transformation supports:
- json_schema: Converts to Chat Completion's json_schema format with
  proper nested structure
- json_object: Converts to Chat Completion's json_object mode
- text: Returns None (default text format)

This fix benefits all providers that use the fallback transformation
handler, ensuring consistent behavior with OpenAI's native Responses API
implementation.
This commit is contained in:
Rodolfo Nobrega de Resende
2025-10-28 22:06:48 -03:00
committed by GitHub
parent b0a2e08a60
commit 29f0ed223a
@@ -87,6 +87,7 @@ class LiteLLMCompletionResponsesConfig:
"previous_response_id",
"stream",
"temperature",
"text",
"tool_choice",
"tools",
"top_p",
@@ -109,6 +110,14 @@ class LiteLLMCompletionResponsesConfig:
tools, web_search_options = LiteLLMCompletionResponsesConfig.transform_responses_api_tools_to_chat_completion_tools(
responses_api_request.get("tools") or [] # type: ignore
)
response_format = None
text_param = responses_api_request.get("text")
if text_param:
response_format = LiteLLMCompletionResponsesConfig._transform_text_format_to_response_format(
text_param
)
litellm_completion_request: dict = {
"messages": LiteLLMCompletionResponsesConfig.transform_responses_api_input_to_messages(
input=input,
@@ -126,6 +135,7 @@ class LiteLLMCompletionResponsesConfig:
"metadata": kwargs.get("metadata"),
"service_tier": kwargs.get("service_tier"),
"web_search_options": web_search_options,
"response_format": response_format,
# litellm specific params
"custom_llm_provider": custom_llm_provider,
"extra_headers": extra_headers,
@@ -441,14 +451,14 @@ class LiteLLMCompletionResponsesConfig:
),
index=0,
)
# Create an assistant message with the tool call
chat_completion_response_message = ChatCompletionResponseMessage(
tool_calls=[tool_call],
role="assistant",
content=None, # Function calls don't have content
)
return [chat_completion_response_message]
@staticmethod
@@ -626,19 +636,19 @@ class LiteLLMCompletionResponsesConfig:
) -> ResponsesAPIStatus:
"""
Map chat completion finish_reason to responses API status.
Chat completion finish_reason values include: "stop", "length", "tool_calls", "content_filter", "function_call"
Responses API status values are: "completed", "failed", "in_progress", "cancelled", "queued", "incomplete"
Args:
finish_reason: The finish_reason from a chat completion response
Returns:
The corresponding responses API status value (one of ResponsesAPIStatus)
"""
if finish_reason is None:
return "completed"
# Map finish reasons to status
if finish_reason in ["stop", "tool_calls", "function_call"]:
return "completed"
@@ -664,7 +674,7 @@ class LiteLLMCompletionResponsesConfig:
choices: List[Choices] = getattr(chat_completion_response, "choices", [])
if choices and len(choices) > 0:
finish_reason = choices[0].finish_reason
responses_api_response: ResponsesAPIResponse = ResponsesAPIResponse(
id=chat_completion_response.id,
created_at=chat_completion_response.created,
@@ -845,7 +855,7 @@ class LiteLLMCompletionResponsesConfig:
transformed_annotations = LiteLLMCompletionResponsesConfig._transform_chat_completion_annotations_to_response_output_annotations(
annotations=annotations
)
return OutputText(
type="output_text",
text=message.content,
@@ -895,15 +905,68 @@ class LiteLLMCompletionResponsesConfig:
output_tokens=0,
total_tokens=0,
)
response_usage = ResponseAPIUsage(
input_tokens=usage.prompt_tokens,
output_tokens=usage.completion_tokens,
total_tokens=usage.total_tokens,
)
# Preserve cost field if it exists (for streaming usage with cost calculation)
if hasattr(usage, "cost") and usage.cost is not None:
setattr(response_usage, "cost", usage.cost)
return response_usage
@staticmethod
def _transform_text_format_to_response_format(
text_param: Union[Dict[str, Any], Any],
) -> Optional[Dict[str, Any]]:
"""
Transform Responses API text.format parameter to Chat Completion response_format parameter.
Responses API text parameter structure:
{
"format": {
"type": "json_schema",
"name": "schema_name",
"schema": {...},
"strict": True
}
}
Chat Completion response_format structure:
{
"type": "json_schema",
"json_schema": {
"name": "schema_name",
"schema": {...},
"strict": True
}
}
"""
if not text_param:
return None
if isinstance(text_param, dict):
format_param = text_param.get("format")
if format_param and isinstance(format_param, dict):
format_type = format_param.get("type")
if format_type == "json_schema":
return {
"type": "json_schema",
"json_schema": {
"name": format_param.get("name", "response_schema"),
"schema": format_param.get("schema", {}),
"strict": format_param.get("strict", False),
}
}
elif format_type == "json_object":
return {
"type": "json_object"
}
elif format_type == "text":
return None
return None