From d881a482206a4ac506f2f36cf43af85f4f2e77ba Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Fri, 17 Apr 2026 11:29:20 +0530 Subject: [PATCH] fix(anthropic): strip Gemini thought suffix from streaming tool_use id Gemini tool call ids embed thought signatures as call_*__thought__*; the Anthropic /v1/messages SSE adapter now exposes a clean id and moves the signature to provider_specific_fields.signature for round-trip. Fixes #25836. Made-with: Cursor --- .../adapters/transformation.py | 30 +++++++++--- ...al_pass_through_adapters_transformation.py | 47 +++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 924205b1593..bc2def4e09d 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -72,6 +72,9 @@ from openai.types.chat.chat_completion_chunk import Choice as OpenAIStreamingCho from litellm.litellm_core_utils.prompt_templates.common_utils import ( parse_tool_call_arguments, ) +from litellm.litellm_core_utils.prompt_templates.factory import ( + THOUGHT_SIGNATURE_SEPARATOR, +) from litellm.types.llms.anthropic import ( ANTHROPIC_HOSTED_TOOLS, AllAnthropicToolsValues, @@ -1366,7 +1369,7 @@ class LiteLLMAnthropicMessagesAdapter: "ContentBlockContentBlockDict", ]: from litellm._uuid import uuid - from litellm.types.llms.anthropic import TextBlock, ToolUseBlock + from litellm.types.llms.anthropic import TextBlock for choice in choices: if ( @@ -1374,12 +1377,25 @@ class LiteLLMAnthropicMessagesAdapter: and len(choice.delta.tool_calls) > 0 and choice.delta.tool_calls[0].function is not None ): - return "tool_use", ToolUseBlock( - type="tool_use", - id=choice.delta.tool_calls[0].id or str(uuid.uuid4()), - name=choice.delta.tool_calls[0].function.name or "", - input={}, # type: ignore[typeddict-item] - ) + raw_id = choice.delta.tool_calls[0].id or str(uuid.uuid4()) + tool_name = choice.delta.tool_calls[0].function.name or "" + base_id = raw_id + thought_sig: Optional[str] = None + if THOUGHT_SIGNATURE_SEPARATOR in raw_id: + parts = raw_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1) + base_id = parts[0] + thought_sig = parts[1] if len(parts) > 1 else None + tool_block: Dict[str, Any] = { + "type": "tool_use", + "id": base_id, + "name": tool_name, + "input": {}, + } + if thought_sig: + tool_block["provider_specific_fields"] = { + "signature": thought_sig, + } + return "tool_use", cast("ContentBlockContentBlockDict", tool_block) elif choice.delta.content is not None and len(choice.delta.content) > 0: return "text", TextBlock(type="text", text="") elif isinstance(choice, StreamingChoices) and hasattr( diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 197aa9ab905..2edd8745446 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -7,6 +7,9 @@ import pytest sys.path.insert(0, os.path.abspath("../../../../..")) +from litellm.litellm_core_utils.prompt_templates.factory import ( + THOUGHT_SIGNATURE_SEPARATOR, +) from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import ( OPENAI_MAX_TOOL_NAME_LENGTH, LiteLLMAnthropicMessagesAdapter, @@ -74,6 +77,50 @@ def test_translate_streaming_openai_chunk_to_anthropic_content_block(): } +def test_translate_streaming_openai_chunk_strips_gemini_thought_from_tool_call_id(): + """Gemini embeds thought signatures in OpenAI tool ids; Anthropic SSE should expose a clean id.""" + base = "call_3e9417b7925e49aca9a71dc1885e" + sig = "CiIBDDnWx" + combined = f"{base}{THOUGHT_SIGNATURE_SEPARATOR}{sig}" + choices = [ + StreamingChoices( + finish_reason=None, + index=0, + delta=Delta( + provider_specific_fields=None, + content=None, + role="assistant", + function_call=None, + tool_calls=[ + ChatCompletionDeltaToolCall( + id=combined, + function=Function( + arguments='{"a": 17, "b": 25}', name="add_numbers" + ), + type="function", + index=0, + ) + ], + audio=None, + ), + logprobs=None, + ) + ] + + ( + block_type, + content_block_start, + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block( + choices=choices + ) + + assert block_type == "tool_use" + assert content_block_start["id"] == base + assert content_block_start["name"] == "add_numbers" + assert content_block_start["input"] == {} + assert content_block_start["provider_specific_fields"]["signature"] == sig + + def test_translate_streaming_openai_chunk_to_anthropic_thinking_content_block(): choices = [ StreamingChoices(