Merge pull request #18194 from BerriAI/litellm_fix_cli_bugs

Fix: Claude code responses api bridge errors
This commit is contained in:
Sameer Kankute 2025-12-19 08:38:55 +05:30 • committed by GitHub
commit d1d008fb7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 120 additions and 3 deletions

View file

@ -492,7 +492,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
def _convert_content_str_to_input_text(
self, content: str, role: str
) -> Dict[str, Any]:
if role == "user" or role == "system":
if role == "user" or role == "system" or role == "tool":
return {"type": "input_text", "text": content}
else:
return {"type": "output_text", "text": content}

View file

@ -169,7 +169,7 @@ class LiteLLMAnthropicMessagesAdapter:
"""
Which anthropic params, we need to translate to the openai format.
"""
return ["messages", "metadata", "system", "tool_choice", "tools"]
return ["messages", "metadata", "system", "tool_choice", "tools", "thinking"]
def translate_anthropic_messages_to_openai( # noqa: PLR0915
self,
@ -420,6 +420,35 @@ class LiteLLMAnthropicMessagesAdapter:
return new_messages
def translate_anthropic_thinking_to_openai(
self, thinking: Dict[str, Any]
) -> Optional[str]:
"""
Translate Anthropic's thinking parameter to OpenAI's reasoning_effort.
Anthropic thinking format: {'type': 'enabled'|'disabled', 'budget_tokens': int}
OpenAI reasoning_effort: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'default'
"""
if not isinstance(thinking, dict):
return None
thinking_type = thinking.get("type", "disabled")
if thinking_type == "disabled":
return None
elif thinking_type == "enabled":
budget_tokens = thinking.get("budget_tokens", 0)
if budget_tokens >= 10000:
return "high"
elif budget_tokens >= 5000:
return "medium"
elif budget_tokens >= 2000:
return "low"
else:
return "minimal"
return None
def translate_anthropic_tool_choice_to_openai(
self, tool_choice: AnthropicMessagesToolChoice
) -> ChatCompletionToolChoiceValues:
@ -529,6 +558,16 @@ class LiteLLMAnthropicMessagesAdapter:
tools=cast(List[AllAnthropicToolsValues], tools)
)
## CONVERT THINKING
if "thinking" in anthropic_message_request:
thinking = anthropic_message_request["thinking"]
if thinking:
reasoning_effort = self.translate_anthropic_thinking_to_openai(
thinking=cast(Dict[str, Any], thinking)
)
if reasoning_effort:
new_kwargs["reasoning_effort"] = reasoning_effort
translatable_params = self.translatable_anthropic_params()
for k, v in anthropic_message_request.items():
if k not in translatable_params: # pass remaining params as is

View file

@ -96,8 +96,8 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig):
validated_input.append(item.model_dump(exclude_none=True))
elif isinstance(item, dict):
# Handle reasoning items specifically to filter out status=None
verbose_logger.debug(f"Handling reasoning item: {item}")
if item.get("type") == "reasoning":
verbose_logger.debug(f"Handling reasoning item: {item}")
# Type assertion since we know it's a dict at this point
dict_item = cast(Dict[str, Any], item)
filtered_item = self._handle_reasoning_item(dict_item)

View file

@ -142,6 +142,84 @@ def test_convert_chat_completion_messages_to_responses_api_tool_result_with_imag
print("✓ Tool result with image correctly transformed to Responses API format")
def test_convert_chat_completion_messages_to_responses_api_tool_result_with_text():
"""
Test that tool messages with text content are correctly transformed to Responses API format.
This is a regression test for the issue where tool results were being transformed
with type='output_text' instead of type='input_text', which caused OpenAI's Responses API
to reject the request with "Invalid value: 'output_text'".
Chat Completion format:
{"role": "tool", "tool_call_id": "call_abc123", "content": "15 degrees"}
Responses API format should use input_text, not output_text:
{"type": "function_call_output", "call_id": "call_abc123", "output": [{"type": "input_text", "text": "15 degrees"}]}
"""
from litellm.completion_extras.litellm_responses_transformation.transformation import (
LiteLLMResponsesTransformationHandler,
)
handler = LiteLLMResponsesTransformationHandler()
# Chat Completion format with tool result containing text
messages = [
{
"role": "user",
"content": "What is the weather like in San Francisco?",
},
{
"role": "assistant",
"content": None,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"location": "San Francisco, CA", "unit": "celsius"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "15 degrees",
},
]
response, _ = handler.convert_chat_completion_messages_to_responses_api(messages)
# Find the function_call_output item
function_call_output = None
for item in response:
if item.get("type") == "function_call_output":
function_call_output = item
break
assert (
function_call_output is not None
), "function_call_output not found in response"
assert function_call_output["call_id"] == "call_abc123"
# Check that the output is correctly transformed to use input_text, not output_text
output = function_call_output["output"]
assert isinstance(output, list), "output should be a list"
assert len(output) == 1, "output should have one item"
text_item = output[0]
# Should be transformed to use input_text for tool results in Responses API format
assert (
text_item["type"] == "input_text"
), f"Expected type 'input_text' for tool result, got '{text_item.get('type')}'"
assert (
text_item["text"] == "15 degrees"
), f"Expected text '15 degrees', got '{text_item.get('text')}'"
print("✓ Tool result with text correctly transformed to use input_text for Responses API format")
def test_openai_responses_chunk_parser_reasoning_summary():
from litellm.completion_extras.litellm_responses_transformation.transformation import (
OpenAiResponsesToChatCompletionStreamIterator,