From 2fce0b6fe27fc05b5df55cccf35a91c7d9dbe339 Mon Sep 17 00:00:00 2001 From: Li-john1021 <215793081+Li-john1021@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:01:52 +0800 Subject: [PATCH 1/8] fix(anthropic): round-trip Gemini thought_signature via tool_use.id `/v1/messages` was parking Gemini thought signatures only in non-standard provider_specific_fields. Spec-compliant clients (Claude Code) drop those fields, so every later turn lost the real signature. Embed a URL-safe encoding of the signature in tool_use.id (Anthropic's charset), keep provider_specific_fields for custom clients, and recover the signature from either source on the request path. Fixes #46369 --- litellm/llms/anthropic/common_utils.py | 53 +++++++ .../pass_through/adapters/transformation.py | 75 +++++++--- ...al_pass_through_adapters_transformation.py | 134 ++++++++++++++++-- .../anthropic/test_anthropic_common_utils.py | 15 ++ 4 files changed, 249 insertions(+), 28 deletions(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 47a82aa097c..74f40b52f39 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -2,6 +2,7 @@ This file contains common utils for anthropic calls. """ +import base64 import copy import json import re @@ -1776,6 +1777,58 @@ def normalize_anthropic_tool_use_id(raw_id: str) -> str: return sanitized or "tool_use_id" +def encode_thought_signature_for_anthropic_tool_id(signature: str) -> str: + """ + Encode an opaque Gemini thought signature so it may be embedded in an + Anthropic ``tool_use.id``. + + Anthropic requires ``^[a-zA-Z0-9_-]+$``. Raw Base64 signatures contain + ``+``, ``/``, and ``=``, which clients and ``normalize_anthropic_tool_use_id`` + would strip or corrupt. URL-safe Base64 of the signature *bytes* (no + padding) stays inside that charset and is reversible. + """ + return base64.urlsafe_b64encode(signature.encode("utf-8")).decode("ascii").rstrip("=") + + +def decode_thought_signature_from_anthropic_tool_id(encoded: str) -> str: + """Inverse of ``encode_thought_signature_for_anthropic_tool_id``.""" + pad = (-len(encoded)) % 4 + return base64.urlsafe_b64decode(encoded + ("=" * pad)).decode("utf-8") + + +def format_anthropic_tool_use_id_with_signature(raw_id: str, thought_signature: str | None) -> str: + """ + Build an Anthropic-safe ``tool_use.id`` that still carries a Gemini + ``thought_signature`` for clients that drop non-standard fields. + + Spec-compliant Anthropic clients (Claude Code, typed SDKs) echo ``id`` but + discard ``provider_specific_fields``. Embedding a URL-safe encoding of the + signature after ``__thought__`` lets those clients round-trip the real + signature the same way OpenAI-compatible clients do via ``tool_calls[].id``. + """ + base_id = normalize_anthropic_tool_use_id(raw_id) + if not thought_signature: + return base_id + return f"{base_id}{THOUGHT_SIGNATURE_SEPARATOR}{encode_thought_signature_for_anthropic_tool_id(thought_signature)}" + + +def split_anthropic_tool_use_id_signature(tool_use_id: str) -> tuple[str, str | None]: + """ + Split an Anthropic ``tool_use.id`` / ``tool_result.tool_use_id`` into the + sanitized base id and, when present, the decoded Gemini thought signature. + """ + if THOUGHT_SIGNATURE_SEPARATOR not in tool_use_id: + return tool_use_id, None + base_id, encoded = tool_use_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1) + if not encoded: + return base_id, None + try: + return base_id, decode_thought_signature_from_anthropic_tool_id(encoded) + except (ValueError, UnicodeDecodeError): + # Legacy / non-encoded suffixes: pass through so callers can still try. + return base_id, encoded + + def _sanitize_tool_use_id_content_block(block: object) -> object: if not isinstance(block, dict): return block diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index d2a0849ffef..bc127a42e40 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -131,8 +131,10 @@ from litellm.litellm_core_utils.reasoning_effort_utils import ( ) from litellm.llms.anthropic.common_utils import ( eager_input_streaming_flag, + format_anthropic_tool_use_id_with_signature, is_empty_unsigned_thinking_block, normalize_anthropic_tool_use_id, + split_anthropic_tool_use_id_signature, strip_encrypted_reasoning_blocks_from_anthropic_messages, ) from litellm.llms.anthropic.pass_through.context_management import ( @@ -357,27 +359,52 @@ class LiteLLMAnthropicMessagesAdapter: def _extract_signature_from_tool_call(self, tool_call: object) -> str | None: """ - Extract signature from a tool call's provider_specific_fields. - Only checks provider_specific_fields, not thinking blocks. + Extract a Gemini thought signature from an OpenAI-shaped tool call. + + Prefer ``provider_specific_fields``, then a raw signature embedded in + ``tool_call.id`` after ``__thought__`` (OpenAI-compatible clients echo + the id verbatim). Do not URL-decode here — that encoding is only used + on Anthropic ``tool_use.id`` values. """ fields: Final = _optional_attr(tool_call, "provider_specific_fields") if fields: - return _thought_signature(fields) + signature = _thought_signature(fields) + if signature: + return signature function_fields: Final = _optional_attr(_optional_attr(tool_call, "function"), "provider_specific_fields") if function_fields: - return _thought_signature(function_fields) + signature = _thought_signature(function_fields) + if signature: + return signature + + raw_id: Final = _optional_attr(tool_call, "id") + if isinstance(raw_id, str) and THOUGHT_SIGNATURE_SEPARATOR in raw_id: + parts = raw_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1) + if len(parts) == 2 and parts[1]: + return parts[1] return None def _extract_signature_from_tool_use_content(self, content: Mapping[str, object]) -> str | None: """ - Extract signature from a tool_use content block's provider_specific_fields. + Extract a Gemini thought signature from a tool_use content block. + + Prefer the non-standard ``provider_specific_fields.signature`` when a + client preserved it, then recover a signature embedded in ``id`` + (URL-safe encoded so the id stays inside Anthropic's charset). Clients + that drop unknown fields, such as Claude Code, only echo ``id``. """ provider_specific_fields: Final = _as_string_mapping(content.get("provider_specific_fields", {})) if provider_specific_fields: signature: Final = provider_specific_fields.get("signature") - return signature if isinstance(signature, str) else None + if isinstance(signature, str) and signature: + return signature + raw_id: Final = content.get("id") + if isinstance(raw_id, str): + _, embedded = split_anthropic_tool_use_id_signature(raw_id) + if embedded: + return embedded return None def _add_cache_control_if_applicable( @@ -525,9 +552,15 @@ class LiteLLMAnthropicMessagesAdapter: self._add_cache_control_if_applicable(content, doc_obj, model) new_user_content_list.append(doc_obj) elif content.get("type") == "tool_result": + raw_tool_result_id = content.get("tool_use_id", "") + tool_result_id = ( + split_anthropic_tool_use_id_signature(raw_tool_result_id)[0] + if isinstance(raw_tool_result_id, str) + else raw_tool_result_id + ) tool_result = ChatCompletionToolMessage( role="tool", - tool_call_id=content.get("tool_use_id", ""), + tool_call_id=tool_result_id, content=self._tool_result_content(content.get("content")), ) self._add_cache_control_if_applicable(content, tool_result, model) @@ -583,8 +616,14 @@ class LiteLLMAnthropicMessagesAdapter: provider_specific_fields["thought_signature"] = signature function_chunk["provider_specific_fields"] = provider_specific_fields + raw_tool_use_id = content.get("id", "") + tool_call_id = ( + split_anthropic_tool_use_id_signature(raw_tool_use_id)[0] + if isinstance(raw_tool_use_id, str) + else raw_tool_use_id + ) tool_call = ChatCompletionAssistantToolCall( - id=content.get("id", ""), + id=tool_call_id, type="function", function=function_chunk, ) @@ -1410,12 +1449,14 @@ class LiteLLMAnthropicMessagesAdapter: tool_name_mapping.get(truncated_name, truncated_name) if tool_name_mapping else truncated_name ) - # Strip Gemini thought-signature suffix and normalize id chars - # (e.g. ``functions.Bash:0`` from cross-provider clients). + # Anthropic ids cannot carry raw Base64 (``+ / =``). Re-embed + # the signature as URL-safe Base64 after ``__thought__`` so + # spec-compliant clients that drop provider_specific_fields + # still echo it back on the next turn (#46369). raw_id = tool_call.id or "" tool_use_block = AnthropicResponseContentBlockToolUse( type="tool_use", - id=normalize_anthropic_tool_use_id(raw_id), + id=format_anthropic_tool_use_id_with_signature(raw_id, signature), name=original_name, input=parse_tool_call_arguments( tool_call.function.arguments, @@ -1635,15 +1676,13 @@ class LiteLLMAnthropicMessagesAdapter: and len(choice.delta.tool_calls) > 0 and choice.delta.tool_calls[0].function is not None ): - raw_id = choice.delta.tool_calls[0].id or str(uuid.uuid4()) - tool_name = choice.delta.tool_calls[0].function.name or "" - thought_sig: str | None = None - if THOUGHT_SIGNATURE_SEPARATOR in raw_id: - parts = raw_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1) - thought_sig = parts[1] if len(parts) > 1 else None + delta_tool_call = choice.delta.tool_calls[0] + raw_id = delta_tool_call.id or str(uuid.uuid4()) + tool_name = delta_tool_call.function.name or "" + thought_sig = self._extract_signature_from_tool_call(delta_tool_call) tool_block: dict[str, object] = { "type": "tool_use", - "id": normalize_anthropic_tool_use_id(raw_id), + "id": format_anthropic_tool_use_id_with_signature(raw_id, thought_sig), "name": tool_name, "input": {}, } diff --git a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index 2eee8549c0b..25dd1aac33c 100644 --- a/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/unit/llms/anthropic/pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -16,6 +16,10 @@ from litellm.litellm_core_utils.prompt_templates.factory import ( THOUGHT_SIGNATURE_SEPARATOR, _bedrock_converse_messages_pt, ) +from litellm.llms.anthropic.common_utils import ( + format_anthropic_tool_use_id_with_signature, + split_anthropic_tool_use_id_signature, +) from litellm.llms.anthropic.pass_through.adapters.transformation import ( OPENAI_MAX_TOOL_NAME_LENGTH, AnthropicAdapter, @@ -201,8 +205,24 @@ 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.""" +def test_format_anthropic_tool_use_id_with_signature_stays_in_anthropic_charset(): + import re + + base = "functions.Bash:0" + sig = "CiIBDDnWx+/a==" + formatted = format_anthropic_tool_use_id_with_signature(base, sig) + assert re.fullmatch(r"[a-zA-Z0-9_-]+", formatted) + assert split_anthropic_tool_use_id_signature(formatted) == ("functions_Bash_0", sig) + assert format_anthropic_tool_use_id_with_signature(base, None) == "functions_Bash_0" + + +def test_translate_streaming_openai_chunk_embeds_gemini_thought_in_anthropic_tool_use_id(): + """Gemini embeds thought signatures in OpenAI tool ids; Anthropic SSE must keep a round-trippable id. + + Raw Base64 uses ``+ / =``, which violate Anthropic's ``^[a-zA-Z0-9_-]+$`` + pattern, so the adapter URL-safe-encodes the signature into ``tool_use.id`` + (and still sets provider_specific_fields for clients that preserve extras). + """ base = "call_3e9417b7925e49aca9a71dc1885e" sig = "CiIBDDnWx" combined = f"{base}{THOUGHT_SIGNATURE_SEPARATOR}{sig}" @@ -237,10 +257,12 @@ def test_translate_streaming_openai_chunk_strips_gemini_thought_from_tool_call_i ) = 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["id"] == format_anthropic_tool_use_id_with_signature(base, sig) + assert THOUGHT_SIGNATURE_SEPARATOR in content_block_start["id"] assert content_block_start["name"] == "add_numbers" assert content_block_start["input"] == {} assert content_block_start["provider_specific_fields"]["signature"] == sig + assert split_anthropic_tool_use_id_signature(content_block_start["id"]) == (base, sig) def test_translate_streaming_openai_chunk_to_anthropic_thinking_content_block(): @@ -1169,12 +1191,14 @@ def test_translate_openai_content_to_anthropic_text_and_tool_calls(): assert result[1]["input"] == {"location": "Boston"} -def test_translate_openai_content_to_anthropic_strips_gemini_thought_from_tool_call_id(): +def test_translate_openai_content_to_anthropic_embeds_gemini_thought_in_tool_use_id(): """ - Non-streaming path must strip the Gemini thought-signature suffix from - tool_call.id, same as the streaming path. The base64 signature contains - `+ / =` which violate Anthropic's `^[a-zA-Z0-9_-]+$` tool_use.id pattern - and 400 when the history is replayed to an Anthropic-native provider. + Non-streaming path must re-embed the Gemini thought signature into + ``tool_use.id`` with a URL-safe encoding. Spec-compliant Anthropic clients + drop ``provider_specific_fields``, so a clean id alone loses the signature + and every later turn falls back to the dummy (#46369). Raw Base64 still + cannot sit in the id verbatim (``+ / =``), and native Anthropic sanitization + continues to strip ``__thought__`` suffixes before Anthropic-native replay. """ base = "call_3e9417b7925e49aca9a71dc1885e" sig = "CiIBDDnWx+/a==" @@ -1205,11 +1229,12 @@ def test_translate_openai_content_to_anthropic_strips_gemini_thought_from_tool_c assert len(result) == 1 assert result[0]["type"] == "tool_use" - assert result[0]["id"] == base - assert THOUGHT_SIGNATURE_SEPARATOR not in result[0]["id"] + assert result[0]["id"] == format_anthropic_tool_use_id_with_signature(base, sig) + assert THOUGHT_SIGNATURE_SEPARATOR in result[0]["id"] assert result[0]["name"] == "get_weather" assert result[0]["input"] == {"location": "Boston"} assert result[0]["provider_specific_fields"] == {"signature": sig} + assert split_anthropic_tool_use_id_signature(result[0]["id"]) == (base, sig) def test_translate_openai_content_to_anthropic_sanitizes_colon_dot_tool_call_ids(): @@ -1840,6 +1865,95 @@ def test_translate_anthropic_messages_to_openai_tool_use_with_signature(): ) +def test_translate_anthropic_messages_to_openai_recovers_signature_from_tool_use_id(): + """Claude Code drops provider_specific_fields; the signature must ride in tool_use.id (#46369).""" + test_signature = "CiIBDDnWx+/a==" + anthropic_id = format_anthropic_tool_use_id_with_signature( + "call_386f67af31f9415781bc35071405", test_signature + ) + + anthropic_messages = [ + AnthropicMessagesUserMessageParam( + role="user", + content=[{"type": "text", "text": "What's the weather like in London?"}], + ), + AnthopicMessagesAssistantMessageParam( + role="assistant", + content=[ + { + "type": "tool_use", + "id": anthropic_id, + "name": "get_weather", + "input": {"location": "London"}, + } + ], + ), + AnthropicMessagesUserMessageParam( + role="user", + content=[ + { + "type": "tool_result", + "tool_use_id": anthropic_id, + "content": "rainy", + } + ], + ), + ] + + adapter = LiteLLMAnthropicMessagesAdapter() + result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages) + + assert result[1]["tool_calls"][0]["id"] == "call_386f67af31f9415781bc35071405" + assert ( + result[1]["tool_calls"][0]["function"]["provider_specific_fields"]["thought_signature"] + == test_signature + ) + assert result[2]["tool_call_id"] == "call_386f67af31f9415781bc35071405" + + +def test_messages_adapter_thought_signature_round_trip_without_provider_fields(): + """OpenAI→Anthropic→OpenAI keeps the real signature when clients only echo id/name/input.""" + base = "call_roundtrip_abc" + sig = "EpYECpMEAdHtim9iBECdK1l5uVIIXoZZ+/==" + function = Function(name="Bash", arguments='{"command": "ls"}') + function.provider_specific_fields = {"thought_signature": sig} + openai_choices = [ + Choices( + message=Message( + role="assistant", + content=None, + tool_calls=[ + ChatCompletionAssistantToolCall( + id=f"{base}{THOUGHT_SIGNATURE_SEPARATOR}{sig}", + type="function", + function=function, + ) + ], + ) + ) + ] + + adapter = LiteLLMAnthropicMessagesAdapter() + anthropic_content = adapter._translate_openai_content_to_anthropic(choices=openai_choices) + # Simulate a spec-compliant client: keep only standard Anthropic fields. + echoed = { + "type": anthropic_content[0]["type"], + "id": anthropic_content[0]["id"], + "name": anthropic_content[0]["name"], + "input": anthropic_content[0]["input"], + } + assert "provider_specific_fields" not in echoed + assert split_anthropic_tool_use_id_signature(echoed["id"]) == (base, sig) + + result = adapter.translate_anthropic_messages_to_openai( + messages=[ + AnthopicMessagesAssistantMessageParam(role="assistant", content=[echoed]), + ] + ) + assert result[0]["tool_calls"][0]["function"]["provider_specific_fields"]["thought_signature"] == sig + assert result[0]["tool_calls"][0]["id"] == base + + def test_translate_anthropic_messages_to_openai_tool_result_with_multiple_content_items(): """ Test that tool_result with multiple content items creates a single tool message diff --git a/tests/unit/llms/anthropic/test_anthropic_common_utils.py b/tests/unit/llms/anthropic/test_anthropic_common_utils.py index 40307c9e09e..3847752488e 100644 --- a/tests/unit/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/unit/llms/anthropic/test_anthropic_common_utils.py @@ -1972,6 +1972,21 @@ class TestAnthropicThinkingSignatureSelfHeal: sig = "CiIBDDnWx+/a==" assert normalize_anthropic_tool_use_id(f"{base}{THOUGHT_SIGNATURE_SEPARATOR}{sig}") == base + def test_format_anthropic_tool_use_id_with_signature_round_trips(self): + import re + + from litellm.llms.anthropic.common_utils import ( + format_anthropic_tool_use_id_with_signature, + split_anthropic_tool_use_id_signature, + ) + + base = "functions.Bash:0" + sig = "CiIBDDnWx+/a==" + formatted = format_anthropic_tool_use_id_with_signature(base, sig) + assert re.fullmatch(r"[a-zA-Z0-9_-]+", formatted) + assert split_anthropic_tool_use_id_signature(formatted) == ("functions_Bash_0", sig) + assert format_anthropic_tool_use_id_with_signature(base, None) == "functions_Bash_0" + def test_anthropic_messages_config_http_retry_helpers(self): import httpx From 8d182ccae487f7d6af5a35cb3f03a35633b3f124 Mon Sep 17 00:00:00 2001 From: Li-john1021 <215793081+Li-john1021@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:32 +0800 Subject: [PATCH 2/8] fix(anthropic): drop unused normalize_anthropic_tool_use_id import Ruff F401 on the /v1/messages thought_signature round-trip change. --- litellm/llms/anthropic/pass_through/adapters/transformation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index bc127a42e40..c0fbea3414b 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -133,7 +133,6 @@ from litellm.llms.anthropic.common_utils import ( eager_input_streaming_flag, format_anthropic_tool_use_id_with_signature, is_empty_unsigned_thinking_block, - normalize_anthropic_tool_use_id, split_anthropic_tool_use_id_signature, strip_encrypted_reasoning_blocks_from_anthropic_messages, ) From f1adc665d2b1a468a8ab509cd9df2e3fe2464fca Mon Sep 17 00:00:00 2001 From: Li-john1021 <215793081+Li-john1021@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:59:21 +0800 Subject: [PATCH 3/8] fix(anthropic): annotate thought-signature helpers with Final (LIT010) Satisfy type_discipline_gate LIT010 on the five assignments this PR introduced: pad, base_id, signature/function_signature, and parts. --- litellm/llms/anthropic/common_utils.py | 4 ++-- .../anthropic/pass_through/adapters/transformation.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 74f40b52f39..9a58d85238a 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -1792,7 +1792,7 @@ def encode_thought_signature_for_anthropic_tool_id(signature: str) -> str: def decode_thought_signature_from_anthropic_tool_id(encoded: str) -> str: """Inverse of ``encode_thought_signature_for_anthropic_tool_id``.""" - pad = (-len(encoded)) % 4 + pad: Final = (-len(encoded)) % 4 return base64.urlsafe_b64decode(encoded + ("=" * pad)).decode("utf-8") @@ -1806,7 +1806,7 @@ def format_anthropic_tool_use_id_with_signature(raw_id: str, thought_signature: signature after ``__thought__`` lets those clients round-trip the real signature the same way OpenAI-compatible clients do via ``tool_calls[].id``. """ - base_id = normalize_anthropic_tool_use_id(raw_id) + base_id: Final = normalize_anthropic_tool_use_id(raw_id) if not thought_signature: return base_id return f"{base_id}{THOUGHT_SIGNATURE_SEPARATOR}{encode_thought_signature_for_anthropic_tool_id(thought_signature)}" diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index c0fbea3414b..459469cda5c 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -367,19 +367,19 @@ class LiteLLMAnthropicMessagesAdapter: """ fields: Final = _optional_attr(tool_call, "provider_specific_fields") if fields: - signature = _thought_signature(fields) + signature: Final = _thought_signature(fields) if signature: return signature function_fields: Final = _optional_attr(_optional_attr(tool_call, "function"), "provider_specific_fields") if function_fields: - signature = _thought_signature(function_fields) - if signature: - return signature + function_signature: Final = _thought_signature(function_fields) + if function_signature: + return function_signature raw_id: Final = _optional_attr(tool_call, "id") if isinstance(raw_id, str) and THOUGHT_SIGNATURE_SEPARATOR in raw_id: - parts = raw_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1) + parts: Final = raw_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1) if len(parts) == 2 and parts[1]: return parts[1] From e4194cded24680709d7ab21306edb9c84825bdfb Mon Sep 17 00:00:00 2001 From: Li-john1021 <215793081+Li-john1021@users.noreply.github.com> Date: Thu, 15 Oct 2026 09:34:33 +0800 Subject: [PATCH 4/8] fix(anthropic): quiet basedpyright deltas on thought_signature path Drop the four Unknown-typed locals introduced when stripping embedded signatures from tool_use / tool_result ids (use a typed helper instead), and keep streaming `.function` on the narrowed tool_calls[0] expression so reportOptionalMemberAccess stays at the merge-base ceiling. --- litellm/llms/anthropic/common_utils.py | 4 +- .../pass_through/adapters/transformation.py | 54 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 9a58d85238a..ff623d644d8 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -1792,7 +1792,7 @@ def encode_thought_signature_for_anthropic_tool_id(signature: str) -> str: def decode_thought_signature_from_anthropic_tool_id(encoded: str) -> str: """Inverse of ``encode_thought_signature_for_anthropic_tool_id``.""" - pad: Final = (-len(encoded)) % 4 + pad: Final[int] = (-len(encoded)) % 4 return base64.urlsafe_b64decode(encoded + ("=" * pad)).decode("utf-8") @@ -1806,7 +1806,7 @@ def format_anthropic_tool_use_id_with_signature(raw_id: str, thought_signature: signature after ``__thought__`` lets those clients round-trip the real signature the same way OpenAI-compatible clients do via ``tool_calls[].id``. """ - base_id: Final = normalize_anthropic_tool_use_id(raw_id) + base_id: Final[str] = normalize_anthropic_tool_use_id(raw_id) if not thought_signature: return base_id return f"{base_id}{THOUGHT_SIGNATURE_SEPARATOR}{encode_thought_signature_for_anthropic_tool_id(thought_signature)}" diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 459469cda5c..4c2fa0dc490 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -136,6 +136,7 @@ from litellm.llms.anthropic.common_utils import ( split_anthropic_tool_use_id_signature, strip_encrypted_reasoning_blocks_from_anthropic_messages, ) + from litellm.llms.anthropic.pass_through.context_management import ( PolyfillResult, ) @@ -347,6 +348,13 @@ class AnthropicAdapter: return anthropic_wrapper.anthropic_sse_wrapper() +def _base_tool_use_id(raw_id: object) -> str: + """Strip an embedded thought signature from a tool_use / tool_result id.""" + if isinstance(raw_id, str): + return split_anthropic_tool_use_id_signature(raw_id)[0] + return "" + + _BlockT: Final = TypeVar("_BlockT", bound=Mapping[str, object]) @@ -365,21 +373,21 @@ class LiteLLMAnthropicMessagesAdapter: the id verbatim). Do not URL-decode here — that encoding is only used on Anthropic ``tool_use.id`` values. """ - fields: Final = _optional_attr(tool_call, "provider_specific_fields") + fields: Final[object] = _optional_attr(tool_call, "provider_specific_fields") if fields: - signature: Final = _thought_signature(fields) + signature: Final[str | None] = _thought_signature(fields) if signature: return signature - function_fields: Final = _optional_attr(_optional_attr(tool_call, "function"), "provider_specific_fields") + function_fields: Final[object] = _optional_attr(_optional_attr(tool_call, "function"), "provider_specific_fields") if function_fields: - function_signature: Final = _thought_signature(function_fields) + function_signature: Final[str | None] = _thought_signature(function_fields) if function_signature: return function_signature - raw_id: Final = _optional_attr(tool_call, "id") + raw_id: Final[object] = _optional_attr(tool_call, "id") if isinstance(raw_id, str) and THOUGHT_SIGNATURE_SEPARATOR in raw_id: - parts: Final = raw_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1) + parts: Final[list[str]] = raw_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1) if len(parts) == 2 and parts[1]: return parts[1] @@ -394,12 +402,14 @@ class LiteLLMAnthropicMessagesAdapter: (URL-safe encoded so the id stays inside Anthropic's charset). Clients that drop unknown fields, such as Claude Code, only echo ``id``. """ - provider_specific_fields: Final = _as_string_mapping(content.get("provider_specific_fields", {})) + provider_specific_fields: Final[Mapping[str, object] | None] = _as_string_mapping( + content.get("provider_specific_fields", {}) + ) if provider_specific_fields: - signature: Final = provider_specific_fields.get("signature") + signature: Final[object] = provider_specific_fields.get("signature") if isinstance(signature, str) and signature: return signature - raw_id: Final = content.get("id") + raw_id: Final[object] = content.get("id") if isinstance(raw_id, str): _, embedded = split_anthropic_tool_use_id_signature(raw_id) if embedded: @@ -551,15 +561,9 @@ class LiteLLMAnthropicMessagesAdapter: self._add_cache_control_if_applicable(content, doc_obj, model) new_user_content_list.append(doc_obj) elif content.get("type") == "tool_result": - raw_tool_result_id = content.get("tool_use_id", "") - tool_result_id = ( - split_anthropic_tool_use_id_signature(raw_tool_result_id)[0] - if isinstance(raw_tool_result_id, str) - else raw_tool_result_id - ) tool_result = ChatCompletionToolMessage( role="tool", - tool_call_id=tool_result_id, + tool_call_id=_base_tool_use_id(content.get("tool_use_id", "")), content=self._tool_result_content(content.get("content")), ) self._add_cache_control_if_applicable(content, tool_result, model) @@ -615,14 +619,8 @@ class LiteLLMAnthropicMessagesAdapter: provider_specific_fields["thought_signature"] = signature function_chunk["provider_specific_fields"] = provider_specific_fields - raw_tool_use_id = content.get("id", "") - tool_call_id = ( - split_anthropic_tool_use_id_signature(raw_tool_use_id)[0] - if isinstance(raw_tool_use_id, str) - else raw_tool_use_id - ) tool_call = ChatCompletionAssistantToolCall( - id=tool_call_id, + id=_base_tool_use_id(content.get("id", "")), type="function", function=function_chunk, ) @@ -1675,10 +1673,12 @@ class LiteLLMAnthropicMessagesAdapter: and len(choice.delta.tool_calls) > 0 and choice.delta.tool_calls[0].function is not None ): - delta_tool_call = choice.delta.tool_calls[0] - raw_id = delta_tool_call.id or str(uuid.uuid4()) - tool_name = delta_tool_call.function.name or "" - thought_sig = self._extract_signature_from_tool_call(delta_tool_call) + raw_id = choice.delta.tool_calls[0].id or str(uuid.uuid4()) + # Keep `.function` on the same narrowed expression as the guard + # (`choice.delta.tool_calls[0].function is not None`) so + # reportOptionalMemberAccess stays flat. + tool_name = choice.delta.tool_calls[0].function.name or "" + thought_sig = self._extract_signature_from_tool_call(choice.delta.tool_calls[0]) tool_block: dict[str, object] = { "type": "tool_use", "id": format_anthropic_tool_use_id_with_signature(raw_id, thought_sig), From b823c781234240a0bc788b5148746794a89f22ae Mon Sep 17 00:00:00 2001 From: Li-john1021 <215793081+Li-john1021@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:03:12 +0800 Subject: [PATCH 5/8] style: ruff format transformation.py Satisfy CI ruff format --check on the thought_signature helper line. --- .../llms/anthropic/pass_through/adapters/transformation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 4c2fa0dc490..aac51cbe388 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -379,7 +379,9 @@ class LiteLLMAnthropicMessagesAdapter: if signature: return signature - function_fields: Final[object] = _optional_attr(_optional_attr(tool_call, "function"), "provider_specific_fields") + function_fields: Final[object] = _optional_attr( + _optional_attr(tool_call, "function"), "provider_specific_fields" + ) if function_fields: function_signature: Final[str | None] = _thought_signature(function_fields) if function_signature: From fdcd0244f5af9fb263f0f3452aab8147fe04eebc Mon Sep 17 00:00:00 2001 From: Li-john1021 <215793081+Li-john1021@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:24:08 +0800 Subject: [PATCH 6/8] style: sort imports in anthropic transformation adapter --- litellm/llms/anthropic/pass_through/adapters/transformation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index aac51cbe388..ccdd92cc493 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -136,7 +136,6 @@ from litellm.llms.anthropic.common_utils import ( split_anthropic_tool_use_id_signature, strip_encrypted_reasoning_blocks_from_anthropic_messages, ) - from litellm.llms.anthropic.pass_through.context_management import ( PolyfillResult, ) From ba77ca902b2baaa9b313b4323c84ec506b533b32 Mon Sep 17 00:00:00 2001 From: Li-john1021 <215793081+Li-john1021@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:58:19 +0800 Subject: [PATCH 7/8] style: shrink transformation helper to clear LIT001 ceiling --- .../llms/anthropic/pass_through/adapters/transformation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index ccdd92cc493..9340f9741d9 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -349,9 +349,7 @@ class AnthropicAdapter: def _base_tool_use_id(raw_id: object) -> str: """Strip an embedded thought signature from a tool_use / tool_result id.""" - if isinstance(raw_id, str): - return split_anthropic_tool_use_id_signature(raw_id)[0] - return "" + return split_anthropic_tool_use_id_signature(raw_id)[0] if isinstance(raw_id, str) else "" _BlockT: Final = TypeVar("_BlockT", bound=Mapping[str, object]) From 56ff4548e2d2cb63f2929f6c3ba7089d22901e81 Mon Sep 17 00:00:00 2001 From: Li-john1021 <215793081+Li-john1021@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:29:50 +0800 Subject: [PATCH 8/8] fix(anthropic): drop list annotation in thought-signature id parse LIT001 ceiling was one over merge-base because Final[list[str]] on the split of tool_call.id. Use str.partition instead so the signature path adds no mutable collection annotation. --- .../llms/anthropic/pass_through/adapters/transformation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index 9340f9741d9..86c0866330e 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -386,9 +386,9 @@ class LiteLLMAnthropicMessagesAdapter: raw_id: Final[object] = _optional_attr(tool_call, "id") if isinstance(raw_id, str) and THOUGHT_SIGNATURE_SEPARATOR in raw_id: - parts: Final[list[str]] = raw_id.split(THOUGHT_SIGNATURE_SEPARATOR, 1) - if len(parts) == 2 and parts[1]: - return parts[1] + _, _, embedded = raw_id.partition(THOUGHT_SIGNATURE_SEPARATOR) + if embedded: + return embedded return None