mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
Merge pull request #18194 from BerriAI/litellm_fix_cli_bugs
Fix: Claude code responses api bridge errors
This commit is contained in:
commit
d1d008fb7e
4 changed files with 120 additions and 3 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue