mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(openai): preserve reasoning_effort summary field for Responses API
When reasoning_effort is passed as a dict with additional fields like 'summary' or 'generate_summary', preserve the full dict format instead of normalizing it to a string. This ensures that when requests are routed to the OpenAI Responses API, all reasoning parameters are correctly included. The normalization to string format now only happens for simple dicts with just the 'effort' key, which is appropriate for the Chat Completions API. Fixes issue where summary field was being dropped when routing gpt-5.4+ requests with tools + reasoning to Responses API. Made-with: Cursor
This commit is contained in:
parent
b28d6eca67
commit
8cf80a14d9
3 changed files with 158 additions and 7 deletions
|
|
@ -151,17 +151,34 @@ class OpenAIGPT5Config(OpenAIGPTConfig):
|
|||
)
|
||||
|
||||
# Normalize reasoning_effort: chat completion API expects a string, not a dict
|
||||
# (e.g. {'effort': 'high', 'summary': 'detailed'} -> 'high')
|
||||
# (e.g. {'effort': 'high'} -> 'high')
|
||||
# BUT: preserve dict format if it has additional fields like 'summary' for Responses API
|
||||
raw_reasoning_effort = (
|
||||
non_default_params.get("reasoning_effort")
|
||||
or optional_params.get("reasoning_effort")
|
||||
)
|
||||
normalized = _normalize_reasoning_effort_for_chat_completion(raw_reasoning_effort)
|
||||
if raw_reasoning_effort is not None and normalized is not None:
|
||||
if "reasoning_effort" in non_default_params:
|
||||
non_default_params["reasoning_effort"] = normalized
|
||||
if "reasoning_effort" in optional_params:
|
||||
optional_params["reasoning_effort"] = normalized
|
||||
|
||||
# Only normalize if it's a simple dict with just 'effort' key
|
||||
# Preserve dict format if it has additional fields (e.g., 'summary') for Responses API
|
||||
should_normalize = False
|
||||
if isinstance(raw_reasoning_effort, dict):
|
||||
# Only normalize if dict has only 'effort' key (or is empty)
|
||||
if set(raw_reasoning_effort.keys()) == {"effort"} or len(raw_reasoning_effort) == 0:
|
||||
should_normalize = True
|
||||
elif isinstance(raw_reasoning_effort, str):
|
||||
# String format is already normalized
|
||||
should_normalize = False
|
||||
|
||||
if should_normalize:
|
||||
normalized = _normalize_reasoning_effort_for_chat_completion(raw_reasoning_effort)
|
||||
if raw_reasoning_effort is not None and normalized is not None:
|
||||
if "reasoning_effort" in non_default_params:
|
||||
non_default_params["reasoning_effort"] = normalized
|
||||
if "reasoning_effort" in optional_params:
|
||||
optional_params["reasoning_effort"] = normalized
|
||||
else:
|
||||
# Keep the original format (string or dict with additional fields)
|
||||
normalized = raw_reasoning_effort
|
||||
|
||||
reasoning_effort = normalized or raw_reasoning_effort
|
||||
if reasoning_effort is not None and reasoning_effort == "xhigh":
|
||||
|
|
|
|||
|
|
@ -1778,3 +1778,35 @@ def test_parallel_tool_calls_comprehensive_streaming_integration():
|
|||
)
|
||||
|
||||
print("✓ Parallel tool calls with split argument deltas stream correctly end-to-end")
|
||||
|
||||
|
||||
def test_map_optional_params_preserves_reasoning_summary():
|
||||
"""Test that reasoning_effort dict with summary field is preserved.
|
||||
|
||||
Regression test for: User reported that summary field was being dropped
|
||||
when routing to Responses API. The dict format should be fully preserved.
|
||||
"""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams
|
||||
|
||||
handler = LiteLLMResponsesTransformationHandler()
|
||||
|
||||
optional_params = {
|
||||
"stream": False,
|
||||
"tools": [{"type": "function", "function": {"name": "test_tool"}}],
|
||||
"tool_choice": "auto",
|
||||
"reasoning_effort": {"effort": "high", "summary": "detailed"},
|
||||
}
|
||||
|
||||
responses_api_request = ResponsesAPIOptionalRequestParams()
|
||||
handler._map_optional_params_to_responses_api_request(
|
||||
optional_params, responses_api_request
|
||||
)
|
||||
|
||||
# Verify reasoning_effort dict with summary was fully preserved
|
||||
assert "reasoning" in responses_api_request
|
||||
assert responses_api_request["reasoning"] == {"effort": "high", "summary": "detailed"}
|
||||
assert responses_api_request["reasoning"]["effort"] == "high"
|
||||
assert responses_api_request["reasoning"]["summary"] == "detailed"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from litellm.llms.openai.chat.gpt_transformation import (
|
|||
OpenAIChatCompletionStreamingHandler,
|
||||
OpenAIGPTConfig,
|
||||
)
|
||||
from litellm.llms.openai.chat.gpt_5_transformation import OpenAIGPT5Config
|
||||
|
||||
|
||||
class TestOpenAIGPTConfig:
|
||||
|
|
@ -324,3 +325,104 @@ class TestPromptCacheParams:
|
|||
)
|
||||
assert optional_params.get("prompt_cache_key") == "my-cache-key"
|
||||
assert optional_params.get("prompt_cache_retention") == "24h"
|
||||
|
||||
|
||||
class TestGPT5ReasoningEffortPreservation:
|
||||
"""Tests for GPT-5 reasoning_effort dict preservation for Responses API."""
|
||||
|
||||
def setup_method(self):
|
||||
self.config = OpenAIGPT5Config()
|
||||
|
||||
def test_reasoning_effort_string_preserved(self):
|
||||
"""Test that reasoning_effort as string is preserved."""
|
||||
non_default_params = {"reasoning_effort": "high"}
|
||||
optional_params = {}
|
||||
|
||||
self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# String format should be preserved
|
||||
assert non_default_params.get("reasoning_effort") == "high"
|
||||
|
||||
def test_reasoning_effort_dict_with_only_effort_normalized(self):
|
||||
"""Test that reasoning_effort dict with only 'effort' key is normalized to string."""
|
||||
non_default_params = {"reasoning_effort": {"effort": "high"}}
|
||||
optional_params = {}
|
||||
|
||||
self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# Dict with only 'effort' should be normalized to string
|
||||
assert non_default_params.get("reasoning_effort") == "high"
|
||||
|
||||
def test_reasoning_effort_dict_with_summary_preserved(self):
|
||||
"""Test that reasoning_effort dict with 'summary' field is preserved for Responses API.
|
||||
|
||||
Regression test for: User reported that summary field was being dropped when
|
||||
routing to Responses API. The dict format with additional fields should be
|
||||
preserved so it can be properly handled by the Responses API transformation.
|
||||
"""
|
||||
non_default_params = {"reasoning_effort": {"effort": "high", "summary": "detailed"}}
|
||||
optional_params = {}
|
||||
|
||||
self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# Dict with additional fields should be preserved
|
||||
assert non_default_params.get("reasoning_effort") == {"effort": "high", "summary": "detailed"}
|
||||
assert isinstance(non_default_params.get("reasoning_effort"), dict)
|
||||
assert non_default_params["reasoning_effort"]["effort"] == "high"
|
||||
assert non_default_params["reasoning_effort"]["summary"] == "detailed"
|
||||
|
||||
def test_reasoning_effort_dict_with_generate_summary_preserved(self):
|
||||
"""Test that reasoning_effort dict with 'generate_summary' field is preserved."""
|
||||
non_default_params = {"reasoning_effort": {"effort": "medium", "generate_summary": "auto"}}
|
||||
optional_params = {}
|
||||
|
||||
self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# Dict with additional fields should be preserved
|
||||
assert non_default_params.get("reasoning_effort") == {"effort": "medium", "generate_summary": "auto"}
|
||||
assert isinstance(non_default_params.get("reasoning_effort"), dict)
|
||||
|
||||
def test_reasoning_effort_dict_with_all_fields_preserved(self):
|
||||
"""Test that reasoning_effort dict with all fields is preserved."""
|
||||
non_default_params = {
|
||||
"reasoning_effort": {
|
||||
"effort": "high",
|
||||
"summary": "detailed",
|
||||
"generate_summary": "concise"
|
||||
}
|
||||
}
|
||||
optional_params = {}
|
||||
|
||||
self.config.map_openai_params(
|
||||
non_default_params=non_default_params,
|
||||
optional_params=optional_params,
|
||||
model="gpt-5.4",
|
||||
drop_params=False,
|
||||
)
|
||||
|
||||
# Dict with all fields should be preserved
|
||||
reasoning = non_default_params.get("reasoning_effort")
|
||||
assert isinstance(reasoning, dict)
|
||||
assert reasoning["effort"] == "high"
|
||||
assert reasoning["summary"] == "detailed"
|
||||
assert reasoning["generate_summary"] == "concise"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue