From 61a84e9fdbea537f4cd596d5ea16dfb1a1753ada Mon Sep 17 00:00:00 2001 From: Cesar Garcia <128240629+Chesars@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:51:40 -0300 Subject: [PATCH] fix(anthropic-adapter): truncate tool names exceeding OpenAI's 64-char limit (#20107) When using LiteLLM's Anthropic /v1/messages endpoint to route requests to OpenAI models, requests fail if any tool name exceeds OpenAI's 64-character limit. Anthropic API has no such limit, causing compatibility issues. Changes: - Add truncate_tool_name() function using {55-char-prefix}_{8-char-hash} format - Modify translate_anthropic_tools_to_openai() to truncate and return mapping - Modify translate_anthropic_tool_choice_to_openai() to truncate tool name - Restore original tool names in responses using the mapping - Support tool name restoration in streaming responses - Add backwards-compatible API (existing methods still work) The fix only applies when routing Anthropic requests to OpenAI models. Native Anthropic/Claude requests pass through unchanged. --- .../adapters/handler.py | 27 ++- .../adapters/streaming_iterator.py | 17 +- .../adapters/transformation.py | 186 ++++++++++++++++-- ...al_pass_through_adapters_transformation.py | 184 ++++++++++++++++- 4 files changed, 383 insertions(+), 31 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py index 8fa7bb7e65..a17eba75b3 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py @@ -6,6 +6,7 @@ from typing import ( Dict, List, Optional, + Tuple, Union, cast, ) @@ -47,8 +48,14 @@ class LiteLLMMessagesToCompletionTransformationHandler: top_p: Optional[float] = None, output_format: Optional[Dict] = None, extra_kwargs: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - """Prepare kwargs for litellm.completion/acompletion""" + ) -> Tuple[Dict[str, Any], Dict[str, str]]: + """Prepare kwargs for litellm.completion/acompletion. + + Returns: + Tuple of (completion_kwargs, tool_name_mapping) + - tool_name_mapping maps truncated tool names back to original names + for tools that exceeded OpenAI's 64-char limit + """ from litellm.litellm_core_utils.litellm_logging import ( Logging as LiteLLMLoggingObject, ) @@ -80,7 +87,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: if output_format: request_data["output_format"] = output_format - openai_request = ANTHROPIC_ADAPTER.translate_completion_input_params( + openai_request, tool_name_mapping = ANTHROPIC_ADAPTER.translate_completion_input_params_with_tool_mapping( request_data ) @@ -116,7 +123,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: ): completion_kwargs[key] = value - return completion_kwargs + return completion_kwargs, tool_name_mapping @staticmethod async def async_anthropic_messages_handler( @@ -137,7 +144,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: **kwargs, ) -> Union[AnthropicMessagesResponse, AsyncIterator]: """Handle non-Anthropic models asynchronously using the adapter""" - completion_kwargs = ( + completion_kwargs, tool_name_mapping = ( LiteLLMMessagesToCompletionTransformationHandler._prepare_completion_kwargs( max_tokens=max_tokens, messages=messages, @@ -164,6 +171,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: ANTHROPIC_ADAPTER.translate_completion_output_params_streaming( completion_response, model=model, + tool_name_mapping=tool_name_mapping, ) ) if transformed_stream is not None: @@ -172,7 +180,8 @@ class LiteLLMMessagesToCompletionTransformationHandler: else: anthropic_response = ( ANTHROPIC_ADAPTER.translate_completion_output_params( - cast(ModelResponse, completion_response) + cast(ModelResponse, completion_response), + tool_name_mapping=tool_name_mapping, ) ) if anthropic_response is not None: @@ -222,7 +231,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: **kwargs, ) - completion_kwargs = ( + completion_kwargs, tool_name_mapping = ( LiteLLMMessagesToCompletionTransformationHandler._prepare_completion_kwargs( max_tokens=max_tokens, messages=messages, @@ -249,6 +258,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: ANTHROPIC_ADAPTER.translate_completion_output_params_streaming( completion_response, model=model, + tool_name_mapping=tool_name_mapping, ) ) if transformed_stream is not None: @@ -257,7 +267,8 @@ class LiteLLMMessagesToCompletionTransformationHandler: else: anthropic_response = ( ANTHROPIC_ADAPTER.translate_completion_output_params( - cast(ModelResponse, completion_response) + cast(ModelResponse, completion_response), + tool_name_mapping=tool_name_mapping, ) ) if anthropic_response is not None: diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py index 24524233dd..aa2f0cc08f 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py @@ -3,7 +3,7 @@ import json import traceback from collections import deque -from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator, Literal, Optional +from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, Iterator, Literal, Optional from litellm import verbose_logger from litellm._uuid import uuid @@ -44,9 +44,16 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): pending_new_content_block: bool = False chunk_queue: deque = deque() # Queue for buffering multiple chunks - def __init__(self, completion_stream: Any, model: str): + def __init__( + self, + completion_stream: Any, + model: str, + tool_name_mapping: Optional[Dict[str, str]] = None, + ): super().__init__(completion_stream) self.model = model + # Mapping of truncated tool names to original names (for OpenAI's 64-char limit) + self.tool_name_mapping = tool_name_mapping or {} def _create_initial_usage_delta(self) -> UsageDelta: """ @@ -401,6 +408,12 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper): choices=chunk.choices # type: ignore ) + # Restore original tool name if it was truncated for OpenAI's 64-char limit + if block_type == "tool_use" and content_block_start.get("name"): + truncated_name = content_block_start.get("name", "") + original_name = self.tool_name_mapping.get(truncated_name, truncated_name) + content_block_start["name"] = original_name + if block_type != self.current_content_block_type: self.current_content_block_type = block_type self.current_content_block_start = content_block_start diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 0a64c7be4c..444f821c20 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1,3 +1,4 @@ +import hashlib import json from typing import ( TYPE_CHECKING, @@ -12,6 +13,54 @@ from typing import ( cast, ) +# OpenAI has a 64-character limit for function/tool names +# Anthropic does not have this limit, so we need to truncate long names +OPENAI_MAX_TOOL_NAME_LENGTH = 64 +TOOL_NAME_HASH_LENGTH = 8 +TOOL_NAME_PREFIX_LENGTH = OPENAI_MAX_TOOL_NAME_LENGTH - TOOL_NAME_HASH_LENGTH - 1 # 55 + + +def truncate_tool_name(name: str) -> str: + """ + 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. + + Args: + name: The original tool name + + Returns: + The original name if <= 64 chars, otherwise truncated with hash + """ + if len(name) <= OPENAI_MAX_TOOL_NAME_LENGTH: + return name + + # Create deterministic hash from full name to avoid collisions + name_hash = hashlib.sha256(name.encode()).hexdigest()[:TOOL_NAME_HASH_LENGTH] + return f"{name[:TOOL_NAME_PREFIX_LENGTH]}_{name_hash}" + + +def create_tool_name_mapping( + 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] = {} + for tool in tools: + original_name = tool.get("name", "") + truncated_name = truncate_tool_name(original_name) + if truncated_name != original_name: + mapping[truncated_name] = original_name + return mapping + from openai.types.chat.chat_completion_chunk import Choice as OpenAIStreamingChoice from litellm.litellm_core_utils.prompt_templates.common_utils import ( @@ -77,8 +126,29 @@ class AnthropicAdapter: self, kwargs ) -> Optional[ChatCompletionRequest]: """ + Translate Anthropic request params to OpenAI format. + - translate params, where needed - pass rest, as is + + Note: Use translate_completion_input_params_with_tool_mapping() if you need + the tool name mapping for restoring original names in responses. + """ + result, _ = self.translate_completion_input_params_with_tool_mapping(kwargs) + return result + + 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. + + This method handles truncation of tool names that exceed OpenAI's 64-character + limit. The mapping allows restoring original names when translating responses. + + Returns: + Tuple of (openai_request, tool_name_mapping) + - tool_name_mapping maps truncated tool names back to original names """ ######################################################### @@ -102,26 +172,51 @@ class AnthropicAdapter: model=model, messages=messages, **kwargs ) - translated_body = ( + translated_body, tool_name_mapping = ( LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai( anthropic_message_request=request_body ) ) - return translated_body + return translated_body, tool_name_mapping def translate_completion_output_params( - self, response: ModelResponse + self, + response: ModelResponse, + tool_name_mapping: Optional[Dict[str, str]] = None, ) -> Optional[AnthropicMessagesResponse]: + """ + 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 + response=response, + tool_name_mapping=tool_name_mapping, ) def translate_completion_output_params_streaming( - self, completion_stream: Any, model: str + 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. + + 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, model=model + completion_stream=completion_stream, + model=model, + tool_name_mapping=tool_name_mapping, ) # Return the SSE-wrapped version for proper event formatting return anthropic_wrapper.async_anthropic_sse_wrapper() @@ -417,8 +512,10 @@ class LiteLLMAnthropicMessagesAdapter: has_cache_control_in_text = True assistant_content_list.append(text_block) elif content.get("type") == "tool_use": + # Truncate tool name for OpenAI's 64-char limit + tool_name = truncate_tool_name(content.get("name", "")) function_chunk: ChatCompletionToolCallFunctionChunk = { - "name": content.get("name", ""), + "name": tool_name, "arguments": json.dumps(content.get("input", {})), } signature = ( @@ -587,8 +684,11 @@ class LiteLLMAnthropicMessagesAdapter: elif tool_choice["type"] == "auto": return "auto" elif tool_choice["type"] == "tool": + # Truncate tool name if it exceeds OpenAI's 64-char limit + original_name = tool_choice.get("name", "") + truncated_name = truncate_tool_name(original_name) tc_function_param = ChatCompletionToolChoiceFunctionParam( - name=tool_choice.get("name", "") + name=truncated_name ) return ChatCompletionToolChoiceObjectParam( type="function", function=tc_function_param @@ -600,12 +700,28 @@ class LiteLLMAnthropicMessagesAdapter: def translate_anthropic_tools_to_openai( self, tools: List[AllAnthropicToolsValues], model: Optional[str] = None - ) -> List[ChatCompletionToolParam]: + ) -> 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] = {} mapped_tool_params = ["name", "input_schema", "description", "cache_control"] for tool in tools: + original_name = tool["name"] + truncated_name = truncate_tool_name(original_name) + + # Store mapping if name was truncated + if truncated_name != original_name: + tool_name_mapping[truncated_name] = original_name + function_chunk = ChatCompletionToolParamFunctionChunk( - name=tool["name"], + name=truncated_name, ) if "input_schema" in tool: function_chunk["parameters"] = tool["input_schema"] # type: ignore @@ -619,7 +735,7 @@ class LiteLLMAnthropicMessagesAdapter: self._add_cache_control_if_applicable(tool, tool_param, model) new_tools.append(tool_param) # type: ignore[arg-type] - return new_tools # type: ignore[return-value] + return new_tools, tool_name_mapping # type: ignore[return-value] def translate_anthropic_output_format_to_openai( self, output_format: Any @@ -694,12 +810,18 @@ class LiteLLMAnthropicMessagesAdapter: def translate_anthropic_to_openai( self, anthropic_message_request: AnthropicMessagesRequest - ) -> ChatCompletionRequest: + ) -> 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] = {} ## CONVERT ANTHROPIC MESSAGES TO OPENAI messages_list: List[ @@ -750,7 +872,7 @@ class LiteLLMAnthropicMessagesAdapter: if "tools" in anthropic_message_request: tools = anthropic_message_request["tools"] if tools: - new_kwargs["tools"] = self.translate_anthropic_tools_to_openai( + new_kwargs["tools"], tool_name_mapping = self.translate_anthropic_tools_to_openai( tools=cast(List[AllAnthropicToolsValues], tools), model=new_kwargs.get("model"), ) @@ -784,7 +906,7 @@ class LiteLLMAnthropicMessagesAdapter: if k not in translatable_params: # pass remaining params as is new_kwargs[k] = v # type: ignore - return new_kwargs + return new_kwargs, tool_name_mapping def _translate_anthropic_image_to_openai(self, image_source: dict) -> Optional[str]: """ @@ -813,7 +935,11 @@ class LiteLLMAnthropicMessagesAdapter: return None - def _translate_openai_content_to_anthropic(self, choices: List[Choices]) -> List[ + def _translate_openai_content_to_anthropic( + self, + choices: List[Choices], + tool_name_mapping: Optional[Dict[str, str]] = None, + ) -> List[ Union[ AnthropicResponseContentBlockText, AnthropicResponseContentBlockToolUse, @@ -895,13 +1021,21 @@ class LiteLLMAnthropicMessagesAdapter: if signature: provider_specific_fields["signature"] = signature + # Restore original tool name if it was truncated + truncated_name = tool_call.function.name or "" + original_name = ( + tool_name_mapping.get(truncated_name, truncated_name) + if tool_name_mapping + else truncated_name + ) + tool_use_block = AnthropicResponseContentBlockToolUse( type="tool_use", id=tool_call.id, - name=tool_call.function.name or "", + name=original_name, input=parse_tool_call_arguments( tool_call.function.arguments, - tool_name=tool_call.function.name, + tool_name=original_name, context="Anthropic pass-through adapter", ), ) @@ -926,10 +1060,24 @@ class LiteLLMAnthropicMessagesAdapter: return "end_turn" def translate_openai_response_to_anthropic( - self, response: ModelResponse + self, + response: ModelResponse, + tool_name_mapping: Optional[Dict[str, str]] = None, ) -> AnthropicMessagesResponse: + """ + 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(choices=response.choices) # type: ignore + anthropic_content = self._translate_openai_content_to_anthropic( + choices=response.choices, # type: ignore + tool_name_mapping=tool_name_mapping, + ) ## extract finish reason anthropic_finish_reason = self._translate_openai_finish_reason_to_anthropic( openai_finish_reason=response.choices[0].finish_reason # 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 1c790f7006..bcb0059fd1 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 @@ -8,7 +8,10 @@ sys.path.insert(0, os.path.abspath("../../../../..")) from litellm.llms.anthropic.experimental_pass_through.adapters.transformation import ( + OPENAI_MAX_TOOL_NAME_LENGTH, LiteLLMAnthropicMessagesAdapter, + create_tool_name_mapping, + truncate_tool_name, ) from litellm.types.llms.anthropic import ( AnthopicMessagesAssistantMessageParam, @@ -1388,12 +1391,13 @@ def test_cache_control_preserved_in_tools_for_claude(): ] adapter = LiteLLMAnthropicMessagesAdapter() - result = adapter.translate_anthropic_tools_to_openai( + result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai( tools=tools, model=CACHE_CONTROL_BEDROCK_CONVERSE_MODEL ) assert len(result) == 1 assert result[0]["cache_control"] == {"type": "ephemeral"} + assert tool_name_mapping == {} # No truncation needed for short names def test_cache_control_not_preserved_in_tools_for_non_claude(): @@ -1408,7 +1412,7 @@ def test_cache_control_not_preserved_in_tools_for_non_claude(): ] adapter = LiteLLMAnthropicMessagesAdapter() - result = adapter.translate_anthropic_tools_to_openai( + result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai( tools=tools, model=CACHE_CONTROL_NON_ANTHROPIC_MODEL ) @@ -1527,3 +1531,179 @@ def test_translate_openai_response_to_anthropic_with_reasoning_content_only(): assert cast(Any, anthropic_content[1]).text == "There are **3** \"r\"s in the word strawberry." assert anthropic_response.get("stop_reason") == "end_turn" + assert tool_name_mapping == {} # No truncation needed for short names + + +# ===================================================================== +# Tool Name Truncation Tests (Issue #17904) +# OpenAI has a 64-character limit for function/tool names +# ===================================================================== + + +def test_truncate_tool_name_short_name(): + """Short tool names should not be truncated.""" + short_name = "get_weather" + result = truncate_tool_name(short_name) + assert result == short_name + assert len(result) <= OPENAI_MAX_TOOL_NAME_LENGTH + + +def test_truncate_tool_name_exactly_64_chars(): + """Tool names exactly 64 chars should not be truncated.""" + name_64_chars = "a" * 64 + result = truncate_tool_name(name_64_chars) + assert result == name_64_chars + assert len(result) == 64 + + +def test_truncate_tool_name_long_name(): + """Long tool names should be truncated with hash suffix.""" + long_name = "computer_tool_with_very_long_name_that_exceeds_openai_64_character_limit_and_keeps_going" + result = truncate_tool_name(long_name) + + assert len(result) == OPENAI_MAX_TOOL_NAME_LENGTH + assert result != long_name + # Should have format: {55-char-prefix}_{8-char-hash} + assert "_" in result + parts = result.rsplit("_", 1) + assert len(parts[0]) == 55 + assert len(parts[1]) == 8 + + +def test_truncate_tool_name_deterministic(): + """Truncation should be deterministic (same input = same output).""" + long_name = "a_very_long_tool_name_that_needs_to_be_truncated_for_openai_compatibility_reasons" + result1 = truncate_tool_name(long_name) + result2 = truncate_tool_name(long_name) + assert result1 == result2 + + +def test_truncate_tool_name_avoids_collisions(): + """Similar long names should produce different truncated names.""" + name1 = "process_user_data_with_validation_and_error_handling_for_production_environment" + name2 = "process_user_data_with_validation_and_error_handling_for_staging_environment" + + result1 = truncate_tool_name(name1) + result2 = truncate_tool_name(name2) + + assert result1 != result2 # Different hashes prevent collision + + +def test_create_tool_name_mapping_no_long_names(): + """Mapping should be empty when no names need truncation.""" + tools = [ + {"name": "get_weather"}, + {"name": "search_web"}, + ] + mapping = create_tool_name_mapping(tools) + assert mapping == {} + + +def test_create_tool_name_mapping_with_long_names(): + """Mapping should contain entries for truncated names.""" + long_name = "a_very_long_tool_name_that_exceeds_the_64_character_limit_imposed_by_openai" + tools = [ + {"name": "short_name"}, + {"name": long_name}, + ] + mapping = create_tool_name_mapping(tools) + + assert len(mapping) == 1 + truncated = truncate_tool_name(long_name) + assert truncated in mapping + assert mapping[truncated] == long_name + + +def test_translate_anthropic_tools_with_long_names(): + """Tools with long names should be truncated and mapped.""" + long_name = "computer_tool_with_very_long_descriptive_name_that_exceeds_openai_limit_completely" + tools = [ + { + "name": long_name, + "description": "A tool with a very long name", + "input_schema": {"type": "object", "properties": {}}, + } + ] + + adapter = LiteLLMAnthropicMessagesAdapter() + result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai( + tools=tools, model="gpt-4" + ) + + assert len(result) == 1 + # The tool name should be truncated + truncated_name = result[0]["function"]["name"] + assert len(truncated_name) <= 64 + assert truncated_name != long_name + # Mapping should have the reverse lookup + assert truncated_name in tool_name_mapping + assert tool_name_mapping[truncated_name] == long_name + + +def test_translate_anthropic_tools_mixed_names(): + """Mix of short and long names should work correctly.""" + short_name = "get_weather" + long_name = "process_complex_data_transformation_with_validation_and_error_handling_pipeline" + tools = [ + {"name": short_name, "input_schema": {"type": "object"}}, + {"name": long_name, "input_schema": {"type": "object"}}, + ] + + adapter = LiteLLMAnthropicMessagesAdapter() + result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai( + tools=tools, model="gpt-4" + ) + + assert len(result) == 2 + # Short name unchanged + assert result[0]["function"]["name"] == short_name + # Long name truncated + assert result[1]["function"]["name"] != long_name + assert len(result[1]["function"]["name"]) <= 64 + # Only long name in mapping + assert len(tool_name_mapping) == 1 + + +def test_translate_openai_response_restores_tool_names(): + """Tool names in responses should be restored to original.""" + original_name = "a_very_long_tool_name_that_needs_truncation_for_openai_api_compatibility" + truncated_name = truncate_tool_name(original_name) + tool_name_mapping = {truncated_name: original_name} + + # Create a mock OpenAI response with the truncated name + response = ModelResponse( + id="test-id", + choices=[ + Choices( + index=0, + finish_reason="tool_calls", + message=Message( + role="assistant", + content=None, + tool_calls=[ + ChatCompletionAssistantToolCall( + id="call_123", + type="function", + function=Function( + name=truncated_name, + arguments='{"arg": "value"}', + ), + ) + ], + ), + ) + ], + model="gpt-4", + usage=Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15), + ) + + adapter = LiteLLMAnthropicMessagesAdapter() + result = adapter.translate_openai_response_to_anthropic( + response=response, tool_name_mapping=tool_name_mapping + ) + + # Find the tool_use block in the response + tool_use_blocks = [c for c in result["content"] if getattr(c, "type", None) == "tool_use"] + assert len(tool_use_blocks) == 1 + # Name should be restored to original + assert getattr(tool_use_blocks[0], "name", None) == original_name