From ba44fe4a6f8c1ae6a00d6078def40b721a3ce348 Mon Sep 17 00:00:00 2001 From: Zhu Lei Date: Thu, 15 Oct 2026 18:00:22 +0800 Subject: [PATCH] fix(anthropic): map legacy function_call finish to tool_use in messages adapter --- .../pass_through/adapters/transformation.py | 29 +++++++++++++++++-- ...al_pass_through_adapters_transformation.py | 29 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/litellm/llms/anthropic/pass_through/adapters/transformation.py b/litellm/llms/anthropic/pass_through/adapters/transformation.py index defb1fa8f38..4e3190fd96b 100644 --- a/litellm/llms/anthropic/pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/pass_through/adapters/transformation.py @@ -200,7 +200,7 @@ from litellm.types.llms.openai import ( ChatCompletionUserMessage, ToolMessageContentPart, ) -from litellm.types.utils import Choices, ModelResponse, StreamingChoices, Usage +from litellm.types.utils import Choices, FunctionCall, ModelResponse, StreamingChoices, Usage from litellm.utils import supports_mid_conversation_system from .streaming_iterator import AnthropicSSEStream, AnthropicStreamWrapper @@ -1431,15 +1431,40 @@ class LiteLLMAnthropicMessagesAdapter: if provider_specific_fields: tool_use_block.provider_specific_fields = provider_specific_fields new_content.append(tool_use_block.model_dump(exclude_none=True)) + if choice.message.function_call is not None: + new_content.append( + self._legacy_function_call_block(choice.message.function_call, tool_name_mapping).model_dump( + exclude_none=True + ) + ) return new_content + @staticmethod + def _legacy_function_call_block( + function_call: FunctionCall, tool_name_mapping: Mapping[str, str] | None + ) -> AnthropicResponseContentBlockToolUse: + from litellm._uuid import uuid + + raw_name: Final = function_call.name or "" + name: Final = tool_name_mapping.get(raw_name, raw_name) if tool_name_mapping else raw_name + return AnthropicResponseContentBlockToolUse( + type="tool_use", + id=normalize_anthropic_tool_use_id(str(uuid.uuid4())), + name=name, + input=parse_tool_call_arguments( + function_call.arguments, + tool_name=name, + context="Anthropic pass-through adapter", + ), + ) + def _translate_openai_finish_reason_to_anthropic(self, openai_finish_reason: str) -> AnthropicFinishReason: if openai_finish_reason == "stop": return "end_turn" elif openai_finish_reason == "length": return "max_tokens" - elif openai_finish_reason == "tool_calls": + elif openai_finish_reason in ("tool_calls", "function_call"): return "tool_use" elif openai_finish_reason in ["content_filter", "refusal"]: return "refusal" 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 2f0a7ad070b..6ba53d5ebe3 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 @@ -5235,3 +5235,32 @@ def test_eager_input_streaming_tool_reaches_bedrock_converse_as_beta(): assert data["additionalModelRequestFields"]["anthropic_beta"] == ["fine-grained-tool-streaming-2025-05-14"] assert data["toolConfig"]["tools"][0]["toolSpec"]["inputSchema"]["json"] == EAGER_INPUT_SCHEMA + + +def test_translate_openai_function_call_response_to_anthropic_tool_use(): + """A legacy OpenAI function_call finish maps to an Anthropic tool_use turn.""" + response: Final = ModelResponse( + id="chatcmpl-function-call", + model="legacy-function-model", + choices=[ + Choices( + index=0, + finish_reason="function_call", + message=Message( + role="assistant", + content=None, + function_call={"name": "get_weather", "arguments": '{"location": "Boston"}'}, + ), + ) + ], + usage=Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15), + ) + + result: Final = LiteLLMAnthropicMessagesAdapter().translate_openai_response_to_anthropic(response) + + assert result["stop_reason"] == "tool_use" + [block] = result["content"] or [] + assert block["type"] == "tool_use" + assert block["name"] == "get_weather" + assert block["input"] == {"location": "Boston"} + assert block["id"] and all(c.isalnum() or c in "-_" for c in block["id"])