From d764b7405d82c44adcca2ccec927b5e29b26ba79 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 18 Dec 2025 15:29:54 +0530 Subject: [PATCH 1/2] Add thinking to reasoning_effort mapping in v1/messages --- .../adapters/transformation.py | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 4c202b9eec0..9cfbf1b6d8d 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -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 From dafd123756214a6d713470658e0d9fa6e3006aef Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 18 Dec 2025 19:54:15 +0530 Subject: [PATCH 2/2] Fix : tool calling with response api bridge --- .../transformation.py | 2 +- .../llms/openai/responses/transformation.py | 2 +- ...responses_transformation_transformation.py | 78 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 612bec239ba..1840f784ae7 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -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} diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index 7ccec074703..fb98c24ce43 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -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) diff --git a/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py index 2ef27396585..9ffe4046027 100644 --- a/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py +++ b/tests/test_litellm/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py @@ -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,