diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index c94b8c2eb50..c63890cee8e 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -495,6 +495,7 @@ class LiteLLMAnthropicMessagesAdapter: has_cache_control_in_text = False tool_calls: list[ChatCompletionAssistantToolCall] = [] thinking_blocks: list[ChatCompletionThinkingBlock | ChatCompletionRedactedThinkingBlock] = [] + unsigned_thinking_texts: list[str] = [] if m["role"] == "assistant": if isinstance(m.get("content"), str): assistant_message_str = str(m.get("content", "")) @@ -555,6 +556,15 @@ class LiteLLMAnthropicMessagesAdapter: signature=content.get("signature") or "", ) thinking_blocks.append(thinking_block) + else: + # Unsigned text is NOT dropped: it is + # replayed as the flat reasoning_content field + # below (the provider-visible form), while + # staying out of thinking_blocks, so the + # signature-400 stays avoided. + unsigned_text = str(content.get("thinking") or "") + if unsigned_text: + unsigned_thinking_texts.append(unsigned_text) elif content.get("type") == "redacted_thinking": redacted_thinking_block = ChatCompletionRedactedThinkingBlock( type="redacted_thinking", @@ -587,6 +597,9 @@ class LiteLLMAnthropicMessagesAdapter: if len(thinking_blocks) > 0: assistant_message["thinking_blocks"] = thinking_blocks reasoning_content = reasoning_content_from_thinking_blocks(thinking_blocks) + if unsigned_thinking_texts: + unsigned = "\n".join(unsigned_thinking_texts) + reasoning_content = f"{reasoning_content}\n{unsigned}" if reasoning_content else unsigned if reasoning_content: assistant_message["reasoning_content"] = reasoning_content new_messages.append(assistant_message) diff --git a/tests/test_litellm/llms/together_ai/chat/test_together_ai_chat_transformation.py b/tests/test_litellm/llms/together_ai/chat/test_together_ai_chat_transformation.py index 7eb7dc41d4f..ce8a51b00f6 100644 --- a/tests/test_litellm/llms/together_ai/chat/test_together_ai_chat_transformation.py +++ b/tests/test_litellm/llms/together_ai/chat/test_together_ai_chat_transformation.py @@ -1013,7 +1013,11 @@ def test_anthropic_messages_replays_tool_loop_and_maps_reasoning_to_thinking_blo assert tool_turn["content"] == "Sunny, 18C" blocks = {block["type"]: block for block in response["content"]} - assert blocks["thinking"]["thinking"] == "Tool said sunny." + + # Contract (thinking param absent): the provider's reasoning is NOT + # surfaced as an Anthropic thinking block. The mock response still + # carries reasoning, and this asserts it is suppressed. + assert "thinking" not in blocks assert blocks["text"]["text"] == "Sunny in SF." assert response["stop_reason"] == "end_turn" @@ -1031,6 +1035,9 @@ def test_anthropic_messages_streams_together_tool_call_as_input_json_delta(): captured_requests: list[httpx.Request] = [] client = _sync_client(captured_requests, _sse_response(*PARALLEL_TOOL_CALL_STREAM)) + # Explicit thinking= would be needed to surface provider reasoning + # (the pass-through contract suppresses it when the thinking param is + # absent); this test asserts the streaming translation only. events = _anthropic_sse_events( litellm.anthropic.messages.create( model=f"together_ai/{UNMAPPED_MODEL}", @@ -1067,7 +1074,9 @@ def test_anthropic_messages_streams_together_tool_call_as_input_json_delta(): for event in events if event["type"] == "content_block_delta" and event["delta"]["type"] == "thinking_delta" ) - assert thinking_text == "Need weather and time." + # thinking param absent in the request: provider reasoning is + # suppressed per the contract, so no thinking block may appear. + assert thinking_text == "" assert [event["delta"]["stop_reason"] for event in events if event["type"] == "message_delta"] == ["tool_use"]