From 2dae3568500ed86a71c872629f22baf3e1becc3f Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Wed, 22 Apr 2026 23:24:10 +0200 Subject: [PATCH 1/9] fix: preserve reasoning_content in anthropic to openai tool-call pass-through --- .../experimental_pass_through/adapters/transformation.py | 9 ++++++++- 1 file changed, 8 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 08797889192..23aed56e65f 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -559,6 +559,7 @@ class LiteLLMAnthropicMessagesAdapter: ) # For content blocks with cache_control has_cache_control_in_text = False tool_calls: List[ChatCompletionAssistantToolCall] = [] + reasoning_content_parts: List[str] = [] thinking_blocks: List[ Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock] ] = [] @@ -616,12 +617,15 @@ class LiteLLMAnthropicMessagesAdapter: ) tool_calls.append(tool_call) elif content.get("type") == "thinking": + thinking_text = content.get("thinking") or "" thinking_block = ChatCompletionThinkingBlock( type="thinking", - thinking=content.get("thinking") or "", + thinking=thinking_text, signature=content.get("signature") or "", cache_control=content.get("cache_control", {}), ) + if thinking_text: + reasoning_content_parts.append(thinking_text) thinking_blocks.append(thinking_block) elif content.get("type") == "redacted_thinking": redacted_thinking_block = ( @@ -659,6 +663,9 @@ class LiteLLMAnthropicMessagesAdapter: ) if len(tool_calls) > 0: assistant_message["tool_calls"] = tool_calls # type: ignore + assistant_message["reasoning_content"] = ( + "".join(reasoning_content_parts) if len(reasoning_content_parts) > 0 else " " + ) # type: ignore if len(thinking_blocks) > 0: assistant_message["thinking_blocks"] = thinking_blocks # type: ignore new_messages.append(assistant_message) From 7bf38c6312b19551331c3b698c1b6631f552db78 Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:16:00 +0200 Subject: [PATCH 2/9] fix(anthropic): strip leaked think tags from messages --- litellm/llms/anthropic/chat/transformation.py | 3 ++ .../adapters/transformation.py | 8 +++-- .../messages/handler.py | 34 ++++++++++--------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 31b9bad3395..8263ea4cf01 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1667,6 +1667,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if thinking_content is not None: reasoning_content += thinking_content + if "" in text_content: + text_content = text_content.split("", 1)[1].lstrip() + return ( text_content, citations, diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 23aed56e65f..e3c62dc9ea7 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1266,10 +1266,14 @@ class LiteLLMAnthropicMessagesAdapter: ) # Handle text content - if choice.message.content is not None: + text_content = choice.message.content + if isinstance(text_content, str) and "" in text_content: + text_content = text_content.split("", 1)[1].lstrip() + + if text_content: new_content.append( AnthropicResponseContentBlockText( - type="text", text=choice.message.content + type="text", text=text_content ).model_dump() ) # Handle tool calls (in parallel to text content) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index 0c59e812e0b..583f0e8b01b 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -454,20 +454,22 @@ def anthropic_messages_handler( "display": "summarized", } - return base_llm_http_handler.anthropic_messages_handler( - model=model, - messages=messages, - anthropic_messages_provider_config=anthropic_messages_provider_config, - anthropic_messages_optional_request_params=dict( - anthropic_messages_optional_request_params - ), - _is_async=is_async, - client=client, - custom_llm_provider=custom_llm_provider, - litellm_params=litellm_params, - logging_obj=litellm_logging_obj, - api_key=api_key, - api_base=api_base, - stream=stream, - kwargs=kwargs, + return _sanitize_think_tag_text_blocks( + base_llm_http_handler.anthropic_messages_handler( + model=model, + messages=messages, + anthropic_messages_provider_config=anthropic_messages_provider_config, + anthropic_messages_optional_request_params=dict( + anthropic_messages_optional_request_params + ), + _is_async=is_async, + client=client, + custom_llm_provider=custom_llm_provider, + litellm_params=litellm_params, + logging_obj=litellm_logging_obj, + api_key=api_key, + api_base=api_base, + stream=stream, + kwargs=kwargs, + ) ) From af42ce72095fb90aa2a5dfc9dd4e53b3c7931110 Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:29:01 +0200 Subject: [PATCH 3/9] test(anthropic): add think-tag regression coverage --- .../test_anthropic_chat_transformation.py | 35 ++++++++++++++ ...al_pass_through_adapters_transformation.py | 39 +++++++++++++++ ...erimental_pass_through_messages_handler.py | 48 +++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index e1fe4befb57..867f6370961 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -511,6 +511,41 @@ def test_multiple_web_search_tool_results(): assert web_search_results[1]["tool_use_id"] == "srvtoolu_search2" +def test_extract_response_content_strips_leaked_think_tags_from_text_blocks(): + config = AnthropicConfig() + + completion_response = { + "content": [ + { + "type": "text", + "text": "I need to call the tool first.\n\n\ntool-loop-ok", + }, + { + "type": "tool_use", + "id": "toolu_01XYZ789", + "name": "echo_status", + "input": {"status": "ok"}, + }, + ] + } + + ( + text, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) + + assert text == "tool-loop-ok" + assert tool_calls is not None + assert len(tool_calls) == 1 + assert tool_calls[0]["function"]["name"] == "echo_status" + + def test_add_code_execution_tool(): config = AnthropicConfig() 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 42efde90926..8b7e5c3f05b 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 @@ -472,6 +472,45 @@ def test_translate_openai_response_to_anthropic_text_and_tool_calls(): assert anthropic_response.get("stop_reason") == "tool_use" +def test_translate_openai_response_to_anthropic_strips_leaked_think_tags(): + openai_response = ModelResponse( + id="resp_text_tool_sanitized", + model="gpt-4o-mini", + choices=[ + Choices( + finish_reason="tool_calls", + message=Message( + role="assistant", + content="I need to call the tool first.\n\n\ntool-loop-ok", + tool_calls=[ + ChatCompletionAssistantToolCall( + id="call_tool_combo", + type="function", + function=Function( + name="echo_status", arguments='{"status": "ok"}' + ), + ) + ], + ), + ) + ], + usage=Usage(prompt_tokens=5, completion_tokens=2), + ) + + adapter = LiteLLMAnthropicMessagesAdapter() + anthropic_response = adapter.translate_openai_response_to_anthropic( + response=openai_response + ) + + anthropic_content = anthropic_response.get("content") + assert anthropic_content is not None + assert len(anthropic_content) == 2 + assert anthropic_content[0]["type"] == "text" + assert anthropic_content[0]["text"] == "tool-loop-ok" + assert anthropic_content[1]["type"] == "tool_use" + assert anthropic_content[1]["name"] == "echo_status" + + def test_translate_streaming_openai_chunk_to_anthropic_with_partial_json(): """Test that partial tool arguments are correctly handled as input_json_delta.""" choices = [ diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index 33628e1d19d..598513451e7 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -499,3 +499,51 @@ class TestThinkingSummaryPreservation: assert result == { "reasoning_effort": {"effort": "medium", "summary": "concise"} } + + +def test_anthropic_messages_handler_strips_leaked_think_tags_from_completion_path(): + from litellm.llms.anthropic.experimental_pass_through.messages.handler import ( + anthropic_messages_handler, + ) + from litellm.types.llms.anthropic_messages.anthropic_response import ( + AnthropicMessagesResponse, + ) + + leaked_response = AnthropicMessagesResponse( + id="msg_test", + type="message", + role="assistant", + content=[ + { + "type": "text", + "text": "I need to call the tool first.\n\n\ntool-loop-ok", + }, + { + "type": "tool_use", + "id": "toolu_01XYZ789", + "name": "echo_status", + "input": {"status": "ok"}, + }, + ], + model="custom-provider/test-model", + stop_reason="tool_use", + usage={"input_tokens": 10, "output_tokens": 20}, + ) + + with patch( + "litellm.llms.anthropic.experimental_pass_through.messages.handler.LiteLLMMessagesToCompletionTransformationHandler.anthropic_messages_handler", + return_value=leaked_response, + ) as mock_completion_handler: + result = anthropic_messages_handler( + max_tokens=100, + messages=[{"role": "user", "content": "Hello"}], + model="my-custom-model", + custom_llm_provider="my-custom-llm", + api_key="test-api-key", + ) + + mock_completion_handler.assert_called_once() + assert result["content"][0]["type"] == "text" + assert result["content"][0]["text"] == "tool-loop-ok" + assert result["content"][1]["type"] == "tool_use" + assert result["content"][1]["name"] == "echo_status" From 4eabe4367e5bdef8089fc32da11286be4d4c0f1d Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Thu, 23 Apr 2026 01:13:49 +0200 Subject: [PATCH 4/9] fix(anthropic): drop empty reasoning sentinel --- .../adapters/transformation.py | 4 ++- ...al_pass_through_adapters_transformation.py | 28 +++++++++++++++++++ 2 files changed, 31 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 e3c62dc9ea7..5c8e26b8855 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -664,7 +664,9 @@ class LiteLLMAnthropicMessagesAdapter: if len(tool_calls) > 0: assistant_message["tool_calls"] = tool_calls # type: ignore assistant_message["reasoning_content"] = ( - "".join(reasoning_content_parts) if len(reasoning_content_parts) > 0 else " " + "".join(reasoning_content_parts) + if len(reasoning_content_parts) > 0 + else None ) # type: ignore if len(thinking_blocks) > 0: assistant_message["thinking_blocks"] = thinking_blocks # type: ignore 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 8b7e5c3f05b..7556d842f7f 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 @@ -1007,6 +1007,34 @@ def test_translate_anthropic_messages_to_openai_tool_use_with_signature(): ) +def test_translate_anthropic_messages_to_openai_tool_use_without_thinking_blocks(): + anthropic_messages = [ + AnthropicMessagesUserMessageParam( + role="user", + content=[{"type": "text", "text": "Use the weather tool."}], + ), + AnthopicMessagesAssistantMessageParam( + role="assistant", + content=[ + { + "type": "tool_use", + "id": "toolu_01234", + "name": "get_weather", + "input": {"location": "Boston"}, + } + ], + ), + ] + + adapter = LiteLLMAnthropicMessagesAdapter() + result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages) + + assert len(result) == 2 + assert result[1]["role"] == "assistant" + assert len(result[1]["tool_calls"]) == 1 + assert result[1].get("reasoning_content") is None + + 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 From 256c85a733cf5ad5d480884bfa7f7a0ac3ac6b33 Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:53:34 +0200 Subject: [PATCH 5/9] fix(anthropic): address review feedback on think sanitization --- .../adapters/transformation.py | 282 ++++++++---------- .../messages/handler.py | 35 +++ ...erimental_pass_through_messages_handler.py | 36 +++ 3 files changed, 201 insertions(+), 152 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 5c8e26b8855..c18d8e855e5 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1,16 +1,11 @@ import copy import hashlib import json +from collections.abc import AsyncIterator from typing import ( TYPE_CHECKING, Any, - AsyncIterator, - Dict, - List, Literal, - Optional, - Tuple, - Union, cast, ) @@ -26,8 +21,7 @@ TOOL_NAME_PREFIX_LENGTH = OPENAI_MAX_TOOL_NAME_LENGTH - TOOL_NAME_HASH_LENGTH - def truncate_tool_name(name: str) -> str: - """ - Truncate tool names that exceed OpenAI's 64-character limit. + """Truncate tool names that exceed OpenAI's 64-character limit. Uses format: {55-char-prefix}_{8-char-hash} to avoid collisions when multiple tools have similar long names. @@ -37,6 +31,7 @@ def truncate_tool_name(name: str) -> str: Returns: The original name if <= 64 chars, otherwise truncated with hash + """ if len(name) <= OPENAI_MAX_TOOL_NAME_LENGTH: return name @@ -47,18 +42,18 @@ def truncate_tool_name(name: str) -> str: def create_tool_name_mapping( - tools: List[Dict[str, Any]], -) -> Dict[str, str]: - """ - Create a mapping of truncated tool names to original names. + tools: list[dict[str, Any]], +) -> dict[str, str]: + """Create a mapping of truncated tool names to original names. Args: tools: List of tool definitions with 'name' field Returns: Dict mapping truncated names to original names (only for truncated tools) + """ - mapping: Dict[str, str] = {} + mapping: dict[str, str] = {} for tool in tools: original_name = tool.get("name", "") truncated_name = truncate_tool_name(original_name) @@ -67,8 +62,6 @@ def create_tool_name_mapping( return mapping -from openai.types.chat.chat_completion_chunk import Choice as OpenAIStreamingChoice - from litellm.litellm_core_utils.prompt_templates.common_utils import ( parse_tool_call_arguments, ) @@ -121,6 +114,7 @@ from litellm.types.llms.openai import ( ChatCompletionUserMessage, ) from litellm.types.utils import Choices, ModelResponse, StreamingChoices, Usage +from openai.types.chat.chat_completion_chunk import Choice as OpenAIStreamingChoice from .streaming_iterator import AnthropicStreamWrapper @@ -132,11 +126,8 @@ class AnthropicAdapter: def __init__(self) -> None: pass - def translate_completion_input_params( - self, kwargs - ) -> Optional[ChatCompletionRequest]: - """ - Translate Anthropic request params to OpenAI format. + def translate_completion_input_params(self, kwargs) -> ChatCompletionRequest | None: + """Translate Anthropic request params to OpenAI format. - translate params, where needed - pass rest, as is @@ -149,9 +140,8 @@ class AnthropicAdapter: def translate_completion_input_params_with_tool_mapping( self, kwargs - ) -> Tuple[Optional[ChatCompletionRequest], Dict[str, str]]: - """ - Translate Anthropic request params to OpenAI format, returning tool name mapping. + ) -> tuple[ChatCompletionRequest | None, dict[str, str]]: + """Translate Anthropic request params to OpenAI format, returning tool name mapping. This method handles truncation of tool names that exceed OpenAI's 64-character limit. The mapping allows restoring original names when translating responses. @@ -159,8 +149,8 @@ class AnthropicAdapter: Returns: Tuple of (openai_request, tool_name_mapping) - tool_name_mapping maps truncated tool names back to original names - """ + """ ######################################################### # Validate required params ######################################################### @@ -194,16 +184,16 @@ class AnthropicAdapter: def translate_completion_output_params( self, response: ModelResponse, - tool_name_mapping: Optional[Dict[str, str]] = None, - ) -> Optional[AnthropicMessagesResponse]: - """ - Translate OpenAI response to Anthropic format. + tool_name_mapping: dict[str, str] | None = None, + ) -> AnthropicMessagesResponse | None: + """Translate OpenAI response to Anthropic format. Args: response: The OpenAI ModelResponse tool_name_mapping: Optional mapping of truncated tool names to original names. Used to restore original names for tools that exceeded OpenAI's 64-char limit. + """ return LiteLLMAnthropicMessagesAdapter().translate_openai_response_to_anthropic( response=response, @@ -214,15 +204,15 @@ class AnthropicAdapter: self, completion_stream: Any, model: str, - tool_name_mapping: Optional[Dict[str, str]] = None, - ) -> Union[AsyncIterator[bytes], None]: - """ - Translate OpenAI streaming response to Anthropic format. + tool_name_mapping: dict[str, str] | None = None, + ) -> AsyncIterator[bytes] | None: + """Translate OpenAI streaming response to Anthropic format. Args: completion_stream: The OpenAI streaming response model: The model name tool_name_mapping: Optional mapping of truncated tool names to original names. + """ anthropic_wrapper = AnthropicStreamWrapper( completion_stream=completion_stream, @@ -239,9 +229,8 @@ class LiteLLMAnthropicMessagesAdapter: ### FOR [BETA] `/v1/messages` endpoint support - def _extract_signature_from_tool_call(self, tool_call: Any) -> Optional[str]: - """ - Extract signature from a tool call's provider_specific_fields. + def _extract_signature_from_tool_call(self, tool_call: Any) -> str | None: + """Extract signature from a tool call's provider_specific_fields. Only checks provider_specific_fields, not thinking blocks. """ signature = None @@ -264,11 +253,9 @@ class LiteLLMAnthropicMessagesAdapter: return signature def _extract_signature_from_tool_use_content( - self, content: Dict[str, Any] - ) -> Optional[str]: - """ - Extract signature from a tool_use content block's provider_specific_fields. - """ + self, content: dict[str, Any] + ) -> str | None: + """Extract signature from a tool_use content block's provider_specific_fields.""" provider_specific_fields = content.get("provider_specific_fields", {}) if provider_specific_fields: return provider_specific_fields.get("signature") @@ -278,10 +265,9 @@ class LiteLLMAnthropicMessagesAdapter: self, source: Any, target: Any, - model: Optional[str], + model: str | None, ) -> None: - """ - Extract cache_control from source and add to target if it should be preserved. + """Extract cache_control from source and add to target if it should be preserved. This method accepts Any type to support both regular dicts and TypedDict objects. TypedDict objects (like ChatCompletionTextObject, ChatCompletionImageObject, etc.) @@ -292,6 +278,7 @@ class LiteLLMAnthropicMessagesAdapter: source: Dict or TypedDict containing potential cache_control field target: Dict or TypedDict to add cache_control to model: Model name to check if cache_control should be preserved + """ # TypedDict objects are dicts at runtime, so .get() works cache_control = ( @@ -306,12 +293,10 @@ class LiteLLMAnthropicMessagesAdapter: target["cache_control"] = cache_control # type: ignore[typeddict-item] else: # Fallback for non-dict objects (shouldn't happen in practice) - cast(Dict[str, Any], target)["cache_control"] = cache_control + cast(dict[str, Any], target)["cache_control"] = cache_control - def translatable_anthropic_params(self) -> List: - """ - Which anthropic params, we need to translate to the openai format. - """ + def translatable_anthropic_params(self) -> list: + """Which anthropic params, we need to translate to the openai format.""" return [ "messages", "metadata", @@ -323,9 +308,8 @@ class LiteLLMAnthropicMessagesAdapter: "output_config", ] - def _is_web_search_tool(self, tool: Dict[str, Any]) -> bool: - """ - Check if a tool is an Anthropic web search tool. + def _is_web_search_tool(self, tool: dict[str, Any]) -> bool: + """Check if a tool is an Anthropic web search tool. Anthropic web search tools have: - type starting with "web_search" (e.g., "web_search_20260209") @@ -336,6 +320,7 @@ class LiteLLMAnthropicMessagesAdapter: Returns: True if this is a web search tool + """ tool_type = tool.get("type", "") tool_name = tool.get("name", "") @@ -345,20 +330,17 @@ class LiteLLMAnthropicMessagesAdapter: def translate_anthropic_messages_to_openai( # noqa: PLR0915 self, - messages: List[ - Union[ - AnthropicMessagesUserMessageParam, - AnthopicMessagesAssistantMessageParam, - ] + messages: list[ + AnthropicMessagesUserMessageParam | AnthopicMessagesAssistantMessageParam ], - model: Optional[str] = None, - ) -> List: - new_messages: List[AllMessageValues] = [] + model: str | None = None, + ) -> list: + new_messages: list[AllMessageValues] = [] for m in messages: - user_message: Optional[ChatCompletionUserMessage] = None - tool_message_list: List[ChatCompletionToolMessage] = [] - new_user_content_list: List[ - Union[ChatCompletionTextObject, ChatCompletionImageObject] + user_message: ChatCompletionUserMessage | None = None + tool_message_list: list[ChatCompletionToolMessage] = [] + new_user_content_list: list[ + ChatCompletionTextObject | ChatCompletionImageObject ] = [] ## USER MESSAGE ## if m["role"] == "user": @@ -493,11 +475,9 @@ class LiteLLMAnthropicMessagesAdapter: else: # For multiple content items, combine into a single tool message # with list content to preserve all items while having one tool_use_id - combined_content_parts: List[ - Union[ - ChatCompletionTextObject, - ChatCompletionImageObject, - ] + combined_content_parts: list[ + ChatCompletionTextObject + | ChatCompletionImageObject ] = [] for c in content_items: if isinstance(c, str): @@ -553,15 +533,15 @@ class LiteLLMAnthropicMessagesAdapter: new_messages.append({"role": "user", "content": new_user_content_list}) # type: ignore ## ASSISTANT MESSAGE ## - assistant_message_str: Optional[str] = None - assistant_content_list: List[Dict[str, Any]] = ( - [] - ) # For content blocks with cache_control + assistant_message_str: str | None = None + assistant_content_list: list[ + dict[str, Any] + ] = [] # For content blocks with cache_control has_cache_control_in_text = False - tool_calls: List[ChatCompletionAssistantToolCall] = [] - reasoning_content_parts: List[str] = [] - thinking_blocks: List[ - Union[ChatCompletionThinkingBlock, ChatCompletionRedactedThinkingBlock] + tool_calls: list[ChatCompletionAssistantToolCall] = [] + reasoning_content_parts: list[str] = [] + thinking_blocks: list[ + ChatCompletionThinkingBlock | ChatCompletionRedactedThinkingBlock ] = [] if m["role"] == "assistant": if isinstance(m.get("content"), str): @@ -572,7 +552,7 @@ class LiteLLMAnthropicMessagesAdapter: assistant_message_str = str(content) elif isinstance(content, dict): if content.get("type") == "text": - text_block: Dict[str, Any] = { + text_block: dict[str, Any] = { "type": "text", "text": content.get("text", ""), } @@ -591,12 +571,12 @@ class LiteLLMAnthropicMessagesAdapter: } signature = ( self._extract_signature_from_tool_use_content( - cast(Dict[str, Any], content) + cast(dict[str, Any], content) ) ) if signature: - provider_specific_fields: Dict[str, Any] = ( + provider_specific_fields: dict[str, Any] = ( function_chunk.get("provider_specific_fields") or {} ) @@ -663,10 +643,14 @@ class LiteLLMAnthropicMessagesAdapter: ) if len(tool_calls) > 0: assistant_message["tool_calls"] = tool_calls # type: ignore + reasoning_content = ( + "".join(reasoning_content_parts) + if len(reasoning_content_parts) > 0 + else None + ) + if reasoning_content is not None or len(tool_calls) > 0: assistant_message["reasoning_content"] = ( - "".join(reasoning_content_parts) - if len(reasoning_content_parts) > 0 - else None + reasoning_content ) # type: ignore if len(thinking_blocks) > 0: assistant_message["thinking_blocks"] = thinking_blocks # type: ignore @@ -676,10 +660,9 @@ class LiteLLMAnthropicMessagesAdapter: @staticmethod def translate_anthropic_thinking_to_reasoning_effort( - thinking: Dict[str, Any] - ) -> Optional[str]: - """ - Translate Anthropic's thinking parameter to OpenAI's reasoning_effort. + thinking: dict[str, Any], + ) -> str | None: + """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' @@ -717,8 +700,7 @@ class LiteLLMAnthropicMessagesAdapter: @staticmethod def is_anthropic_claude_model(model: str) -> bool: - """ - Check if the model is an Anthropic Claude model that supports the thinking parameter. + """Check if the model is an Anthropic Claude model that supports the thinking parameter. Returns True for: - anthropic/* models @@ -730,11 +712,10 @@ class LiteLLMAnthropicMessagesAdapter: @staticmethod def translate_thinking_for_model( - thinking: Dict[str, Any], + thinking: dict[str, Any], model: str, - ) -> Dict[str, Any]: - """ - Translate Anthropic thinking parameter based on the target model. + ) -> dict[str, Any]: + """Translate Anthropic thinking parameter based on the target model. For Claude/Anthropic models: returns {'thinking': } - Preserves exact budget_tokens value @@ -748,6 +729,7 @@ class LiteLLMAnthropicMessagesAdapter: Returns: Dict with either 'thinking' or 'reasoning_effort' key + """ if LiteLLMAnthropicMessagesAdapter.is_anthropic_claude_model(model): return {"thinking": thinking} @@ -798,22 +780,22 @@ class LiteLLMAnthropicMessagesAdapter: return "none" else: raise ValueError( - "Incompatible tool choice param submitted - {}".format(tool_choice) + f"Incompatible tool choice param submitted - {tool_choice}" ) def translate_anthropic_tools_to_openai( - self, tools: List[AllAnthropicToolsValues], model: Optional[str] = None - ) -> Tuple[List[ChatCompletionToolParam], Dict[str, str]]: - """ - Translate Anthropic tools to OpenAI format. + self, tools: list[AllAnthropicToolsValues], model: str | None = None + ) -> tuple[list[ChatCompletionToolParam], dict[str, str]]: + """Translate Anthropic tools to OpenAI format. Returns: Tuple of (translated_tools, tool_name_mapping) - tool_name_mapping maps truncated names back to original names for tools that exceeded OpenAI's 64-char limit + """ - new_tools: List[ChatCompletionToolParam] = [] - tool_name_mapping: Dict[str, str] = {} + new_tools: list[ChatCompletionToolParam] = [] + tool_name_mapping: dict[str, str] = {} mapped_tool_params = ["name", "input_schema", "description", "cache_control"] for idx, tool in enumerate(tools): @@ -858,9 +840,8 @@ class LiteLLMAnthropicMessagesAdapter: def translate_anthropic_output_format_to_openai( self, output_format: Any - ) -> Optional[Dict[str, Any]]: - """ - Translate Anthropic's output_format to OpenAI's response_format. + ) -> dict[str, Any] | None: + """Translate Anthropic's output_format to OpenAI's response_format. Anthropic output_format: {"type": "json_schema", "schema": {...}} OpenAI response_format: {"type": "json_schema", "json_schema": {"name": "...", "schema": {...}}} @@ -870,6 +851,7 @@ class LiteLLMAnthropicMessagesAdapter: Returns: OpenAI-compatible response_format dict, or None if invalid + """ if not isinstance(output_format, dict): return None @@ -899,8 +881,7 @@ class LiteLLMAnthropicMessagesAdapter: @staticmethod def _add_additional_properties_false(schema: dict) -> None: - """ - Recursively ensure object schemas comply with OpenAI strict mode. + """Recursively ensure object schemas comply with OpenAI strict mode. OpenAI's strict mode requires: 1. 'additionalProperties': false at every object nesting level @@ -939,7 +920,7 @@ class LiteLLMAnthropicMessagesAdapter: def _add_system_message_to_messages( self, - new_messages: List[AllMessageValues], + new_messages: list[AllMessageValues], anthropic_message_request: AnthropicMessagesRequest, ) -> None: """Add system message to messages list if present in request.""" @@ -956,11 +937,11 @@ class LiteLLMAnthropicMessagesAdapter: ) elif isinstance(system_content, list): # Convert Anthropic system content blocks to OpenAI format - openai_system_content: List[Dict[str, Any]] = [] + openai_system_content: list[dict[str, Any]] = [] model_name = anthropic_message_request.get("model", "") for block in system_content: if isinstance(block, dict) and block.get("type") == "text": - text_block: Dict[str, Any] = { + text_block: dict[str, Any] = { "type": "text", "text": block.get("text", ""), } @@ -969,7 +950,9 @@ class LiteLLMAnthropicMessagesAdapter: if openai_system_content: new_messages.insert( 0, - ChatCompletionSystemMessage(role="system", content=openai_system_content), # type: ignore + ChatCompletionSystemMessage( + role="system", content=openai_system_content + ), # type: ignore ) def _translate_metadata_to_openai( @@ -1006,7 +989,7 @@ class LiteLLMAnthropicMessagesAdapter: self, anthropic_message_request: AnthropicMessagesRequest, new_kwargs: ChatCompletionRequest, - ) -> Dict[str, str]: + ) -> dict[str, str]: """Translate tools and extract web_search_options when needed.""" if "tools" not in anthropic_message_request: return {} @@ -1015,10 +998,10 @@ class LiteLLMAnthropicMessagesAdapter: if not tools: return {} - web_search_tools: List[AllAnthropicToolsValues] = [] - regular_tools: List[AllAnthropicToolsValues] = [] + web_search_tools: list[AllAnthropicToolsValues] = [] + regular_tools: list[AllAnthropicToolsValues] = [] for tool in tools: - cast_tool = cast(Dict[str, Any], tool) + cast_tool = cast(dict[str, Any], tool) if self._is_web_search_tool(cast_tool): web_search_tools.append(cast(AllAnthropicToolsValues, tool)) else: @@ -1056,7 +1039,7 @@ class LiteLLMAnthropicMessagesAdapter: return reasoning_effort = self.translate_anthropic_thinking_to_reasoning_effort( - cast(Dict[str, Any], thinking) + cast(dict[str, Any], thinking) ) if not reasoning_effort: return @@ -1118,30 +1101,26 @@ class LiteLLMAnthropicMessagesAdapter: def translate_anthropic_to_openai( self, anthropic_message_request: AnthropicMessagesRequest - ) -> Tuple[ChatCompletionRequest, Dict[str, str]]: - """ - This is used by the beta Anthropic Adapter, for translating anthropic `/v1/messages` requests to the openai format. + ) -> tuple[ChatCompletionRequest, dict[str, str]]: + """This is used by the beta Anthropic Adapter, for translating anthropic `/v1/messages` requests to the openai format. Returns: Tuple of (openai_request, tool_name_mapping) - tool_name_mapping maps truncated tool names back to original names for tools that exceeded OpenAI's 64-char limit + """ # Debug: Processing Anthropic message request - new_messages: List[AllMessageValues] = [] - tool_name_mapping: Dict[str, str] = {} + new_messages: list[AllMessageValues] = [] + tool_name_mapping: dict[str, str] = {} ## CONVERT ANTHROPIC MESSAGES TO OPENAI - messages_list: List[ - Union[ - AnthropicMessagesUserMessageParam, AnthopicMessagesAssistantMessageParam - ] + messages_list: list[ + AnthropicMessagesUserMessageParam | AnthopicMessagesAssistantMessageParam ] = cast( - List[ - Union[ - AnthropicMessagesUserMessageParam, - AnthopicMessagesAssistantMessageParam, - ] + list[ + AnthropicMessagesUserMessageParam + | AnthopicMessagesAssistantMessageParam ], anthropic_message_request["messages"], ) @@ -1188,9 +1167,8 @@ class LiteLLMAnthropicMessagesAdapter: return new_kwargs, tool_name_mapping - def _translate_anthropic_image_to_openai(self, image_source: dict) -> Optional[str]: - """ - Translate Anthropic image source format to OpenAI-compatible image URL. + def _translate_anthropic_image_to_openai(self, image_source: dict) -> str | None: + """Translate Anthropic image source format to OpenAI-compatible image URL. Anthropic supports two image source formats: 1. Base64: {"type": "base64", "media_type": "image/jpeg", "data": "..."} @@ -1217,10 +1195,10 @@ class LiteLLMAnthropicMessagesAdapter: def _translate_openai_content_to_anthropic( self, - choices: List[Choices], - tool_name_mapping: Optional[Dict[str, str]] = None, - ) -> List[Dict[str, Any]]: - new_content: List[Dict[str, Any]] = [] + choices: list[Choices], + tool_name_mapping: dict[str, str] | None = None, + ) -> list[dict[str, Any]]: + new_content: list[dict[str, Any]] = [] for choice in choices: # Handle thinking blocks first if ( @@ -1332,16 +1310,16 @@ class LiteLLMAnthropicMessagesAdapter: def translate_openai_response_to_anthropic( self, response: ModelResponse, - tool_name_mapping: Optional[Dict[str, str]] = None, + tool_name_mapping: dict[str, str] | None = None, ) -> AnthropicMessagesResponse: - """ - Translate OpenAI response to Anthropic format. + """Translate OpenAI response to Anthropic format. Args: response: The OpenAI ModelResponse tool_name_mapping: Optional mapping of truncated tool names to original names. Used to restore original names for tools that exceeded OpenAI's 64-char limit. + """ ## translate content block anthropic_content = self._translate_openai_content_to_anthropic( @@ -1353,7 +1331,7 @@ class LiteLLMAnthropicMessagesAdapter: openai_finish_reason=response.choices[0].finish_reason # type: ignore ) # extract usage - usage: Usage = getattr(response, "usage") + usage: Usage = response.usage uncached_input_tokens = usage.prompt_tokens or 0 cached_tokens = 0 if hasattr(usage, "prompt_tokens_details") and usage.prompt_tokens_details: @@ -1390,8 +1368,8 @@ class LiteLLMAnthropicMessagesAdapter: return translated_obj def _translate_streaming_openai_chunk_to_anthropic_content_block( - self, choices: List[Union[OpenAIStreamingChoice, StreamingChoices]] - ) -> Tuple[ + self, choices: list[OpenAIStreamingChoice | StreamingChoices] + ) -> tuple[ Literal["text", "tool_use", "thinking"], "ContentBlockContentBlockDict", ]: @@ -1450,20 +1428,18 @@ class LiteLLMAnthropicMessagesAdapter: return "text", TextBlock(type="text", text="") def _translate_streaming_openai_chunk_to_anthropic( - self, choices: List[Union[OpenAIStreamingChoice, StreamingChoices]] - ) -> Tuple[ + self, choices: list[OpenAIStreamingChoice | StreamingChoices] + ) -> tuple[ Literal["text_delta", "input_json_delta", "thinking_delta", "signature_delta"], - Union[ - ContentTextBlockDelta, - ContentJsonBlockDelta, - ContentThinkingBlockDelta, - ContentThinkingSignatureBlockDelta, - ], + ContentTextBlockDelta + | ContentJsonBlockDelta + | ContentThinkingBlockDelta + | ContentThinkingSignatureBlockDelta, ]: text: str = "" reasoning_content: str = "" reasoning_signature: str = "" - partial_json: Optional[str] = None + partial_json: str | None = None for choice in choices: if choice.delta.content is not None and len(choice.delta.content) > 0: text += choice.delta.content @@ -1520,7 +1496,7 @@ class LiteLLMAnthropicMessagesAdapter: def translate_streaming_openai_response_to_anthropic( self, response: ModelResponse, current_content_block_index: int - ) -> Union[ContentBlockDelta, MessageBlockDelta]: + ) -> ContentBlockDelta | MessageBlockDelta: ## base case - final chunk w/ finish reason if response.choices[0].finish_reason is not None: delta = MessageDelta( @@ -1529,7 +1505,7 @@ class LiteLLMAnthropicMessagesAdapter: ), ) if getattr(response, "usage", None) is not None: - litellm_usage_chunk: Optional[Usage] = response.usage # type: ignore + litellm_usage_chunk: Usage | None = response.usage # type: ignore elif ( hasattr(response, "_hidden_params") and "usage" in response._hidden_params @@ -1570,7 +1546,9 @@ class LiteLLMAnthropicMessagesAdapter: else: usage_delta = UsageDelta(input_tokens=0, output_tokens=0) return MessageBlockDelta( - type="message_delta", delta=delta, usage=usage_delta # type: ignore + type="message_delta", + delta=delta, + usage=usage_delta, # type: ignore ) ( type_of_content, diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index 583f0e8b01b..a4d611d3568 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -47,6 +47,41 @@ def _should_route_to_responses_api(custom_llm_provider: Optional[str]) -> bool: return custom_llm_provider in _RESPONSES_API_PROVIDERS +def _sanitize_think_tag_text_blocks( + response: Union[AnthropicMessagesResponse, AsyncIterator] +) -> Union[AnthropicMessagesResponse, AsyncIterator]: + if not isinstance(response, dict): + return response + + content = response.get("content") + if not isinstance(content, list): + return response + + sanitized_content: List[Dict[str, Any]] = [] + for block in content: + if not isinstance(block, dict): + sanitized_content.append(block) + continue + + if block.get("type") != "text": + sanitized_content.append(block) + continue + + text = block.get("text") + if not isinstance(text, str) or "" not in text: + sanitized_content.append(block) + continue + + cleaned_text = text.split("", 1)[1].lstrip() + sanitized_block = dict(block) + sanitized_block["text"] = cleaned_text + sanitized_content.append(sanitized_block) + + sanitized_response = dict(response) + sanitized_response["content"] = sanitized_content + return cast(AnthropicMessagesResponse, sanitized_response) + + ####### ENVIRONMENT VARIABLES ################### # Initialize any necessary instances or variables here base_llm_http_handler = BaseLLMHTTPHandler() diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index 598513451e7..e9c7adc0462 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -547,3 +547,39 @@ def test_anthropic_messages_handler_strips_leaked_think_tags_from_completion_pat assert result["content"][0]["text"] == "tool-loop-ok" assert result["content"][1]["type"] == "tool_use" assert result["content"][1]["name"] == "echo_status" + + +def test_sanitize_think_tag_text_blocks_preserves_empty_blocks_without_mutation(): + from litellm.llms.anthropic.experimental_pass_through.messages.handler import ( + _sanitize_think_tag_text_blocks, + ) + from litellm.types.llms.anthropic_messages.anthropic_response import ( + AnthropicMessagesResponse, + ) + + response = AnthropicMessagesResponse( + id="msg_test_empty", + type="message", + role="assistant", + content=[ + {"type": "text", "text": "Thinking...\n"}, + { + "type": "tool_use", + "id": "toolu_01EMPTY", + "name": "echo_status", + "input": {"status": "ok"}, + }, + ], + model="custom-provider/test-model", + stop_reason="tool_use", + usage={"input_tokens": 10, "output_tokens": 20}, + ) + + sanitized = _sanitize_think_tag_text_blocks(response) + + assert sanitized is not response + assert response["content"][0]["text"] == "Thinking...\n" + assert sanitized["content"][0]["type"] == "text" + assert sanitized["content"][0]["text"] == "" + assert len(sanitized["content"]) == 2 + assert sanitized["content"][1]["type"] == "tool_use" From 2a1d9866458adf1ec336b3629d162ab87790869f Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:18:02 +0200 Subject: [PATCH 6/9] fix(anthropic): sanitize think tags across pass-through handlers --- .../experimental_pass_through/messages/handler.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index a4d611d3568..6c255a69690 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -323,7 +323,7 @@ async def anthropic_messages( response = await init_response else: response = init_response - return response + return _sanitize_think_tag_text_blocks(response) def validate_anthropic_api_metadata(metadata: Optional[Dict] = None) -> Optional[Dict]: @@ -458,10 +458,12 @@ def anthropic_messages_handler( **kwargs, ) if _should_route_to_responses_api(custom_llm_provider): - return LiteLLMMessagesToResponsesAPIHandler.anthropic_messages_handler( - **_shared_kwargs + return _sanitize_think_tag_text_blocks( + LiteLLMMessagesToResponsesAPIHandler.anthropic_messages_handler( + **_shared_kwargs + ) ) - return ( + return _sanitize_think_tag_text_blocks( LiteLLMMessagesToCompletionTransformationHandler.anthropic_messages_handler( **_shared_kwargs ) From e5c1daf3d2ace5eca8980b9d49fcc2c688fc07a2 Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:33:59 +0200 Subject: [PATCH 7/9] style(anthropic): format adapter transformation --- .../experimental_pass_through/adapters/transformation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index c18d8e855e5..cfe52007c58 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -534,9 +534,9 @@ class LiteLLMAnthropicMessagesAdapter: ## ASSISTANT MESSAGE ## assistant_message_str: str | None = None - assistant_content_list: list[ - dict[str, Any] - ] = [] # For content blocks with cache_control + assistant_content_list: list[dict[str, Any]] = ( + [] + ) # For content blocks with cache_control has_cache_control_in_text = False tool_calls: list[ChatCompletionAssistantToolCall] = [] reasoning_content_parts: list[str] = [] From 9ec472bb97edad2fe1cee94ef3907b9cee164dba Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:48:58 +0200 Subject: [PATCH 8/9] fix(anthropic): satisfy lint on staging re-raise --- litellm/llms/anthropic/chat/transformation.py | 9 +++++++-- .../experimental_pass_through/adapters/transformation.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 8263ea4cf01..3cd19fad1fb 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1573,6 +1573,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) return _message + @staticmethod + def _strip_leaked_think_prefix(text_content: str) -> str: + if "" not in text_content: + return text_content + return text_content.split("", 1)[1].lstrip() + def extract_response_content(self, completion_response: dict) -> Tuple[ str, Optional[List[Any]], @@ -1667,8 +1673,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): if thinking_content is not None: reasoning_content += thinking_content - if "" in text_content: - text_content = text_content.split("", 1)[1].lstrip() + text_content = self._strip_leaked_think_prefix(text_content) return ( text_content, diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index cfe52007c58..8e0f15e54ea 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1385,12 +1385,12 @@ class LiteLLMAnthropicMessagesAdapter: 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 + thought_sig: str | None = 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] = { + tool_block: dict[str, Any] = { "type": "tool_use", "id": base_id, "name": tool_name, From 4aa6f02beba78299245b4ccade08f1166e102e14 Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:18:53 +0200 Subject: [PATCH 9/9] fix(anthropic): resolve mypy lint errors in pass-through handler and adapter --- .../adapters/transformation.py | 2 +- .../experimental_pass_through/messages/handler.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 8e0f15e54ea..818627918ce 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1331,7 +1331,7 @@ class LiteLLMAnthropicMessagesAdapter: openai_finish_reason=response.choices[0].finish_reason # type: ignore ) # extract usage - usage: Usage = response.usage + usage: Usage = response.usage # type: ignore[attr-defined] uncached_input_tokens = usage.prompt_tokens or 0 cached_tokens = 0 if hasattr(usage, "prompt_tokens_details") and usage.prompt_tokens_details: diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index 6c255a69690..74223048a33 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -48,8 +48,10 @@ def _should_route_to_responses_api(custom_llm_provider: Optional[str]) -> bool: def _sanitize_think_tag_text_blocks( - response: Union[AnthropicMessagesResponse, AsyncIterator] -) -> Union[AnthropicMessagesResponse, AsyncIterator]: + response: Union[ + AnthropicMessagesResponse, AsyncIterator[Any], Coroutine[Any, Any, Any] + ], +) -> Union[AnthropicMessagesResponse, AsyncIterator[Any], Coroutine[Any, Any, Any]]: if not isinstance(response, dict): return response @@ -57,7 +59,7 @@ def _sanitize_think_tag_text_blocks( if not isinstance(content, list): return response - sanitized_content: List[Dict[str, Any]] = [] + sanitized_content: List[Any] = [] for block in content: if not isinstance(block, dict): sanitized_content.append(block) @@ -221,7 +223,7 @@ async def anthropic_messages( client: Optional[AsyncHTTPHandler] = None, custom_llm_provider: Optional[str] = None, **kwargs, -) -> Union[AnthropicMessagesResponse, AsyncIterator]: +) -> Union[AnthropicMessagesResponse, AsyncIterator[Any], Coroutine[Any, Any, Any]]: """ Async: Make llm api request in Anthropic /messages API spec """