From b7deea30aa369c0a40a46f7d009b154567bb1099 Mon Sep 17 00:00:00 2001 From: Daniel Cherubini Date: Mon, 14 Sep 2026 20:21:57 +0200 Subject: [PATCH] fix: pass together_ai replay/streaming tests under the thinking contract Controlled base-vs-head run of the full llms test shard exposed two together_ai anthropic-messages tests broken by the thinking_disabled contract change in this PR: - test_anthropic_messages_replays_tool_loop: an unsigned thinking block in replayed history was being dropped entirely. Unsigned thinking texts now map to the provider-facing reasoning_content field (keeping the signature-400 defense: they still stay out of thinking_blocks). - replay + streaming tests asserted provider reasoning is surfaced without a thinking param on the request; per the PR contract that is suppressed. Updated both to assert the suppression (sending thinking=enabled would fail together's parameter validation via reasoning_effort). --- .../adapters/transformation.py | 13 +++++++++++++ .../chat/test_together_ai_chat_transformation.py | 13 +++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) 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"]