From 7d5a1521a75c90ea721e31fa8342046e2fa7ea28 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 5 May 2026 20:39:23 +0000 Subject: [PATCH] Skip duplicate originals in _build_anthropic_tool_name_maps If the same invalid tool name appeared twice in original_names (e.g. ['foo/bar', 'foo/bar']), the second occurrence overwrote the forward map entry with a freshly-suffixed name (foo_bar_2), leaving foo_bar orphaned in 'used' with no reverse mapping. _sanitize_tool_names_in_request then rewrote both tool entries to foo_bar_2, and Anthropic 400'd on duplicate tool names. Skip the rewrite if forward already has the original mapped. Co-authored-by: Mateo Wang --- litellm/llms/anthropic/chat/transformation.py | 7 +++++++ .../test_anthropic_chat_transformation.py | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 64d885767cf..dc3100f4670 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -192,6 +192,13 @@ def _build_anthropic_tool_name_maps( candidate = _basic_sanitize_anthropic_tool_name(original) if candidate == original: continue + # Skip duplicates of the same original name. Without this guard the + # second pass would assign a fresh suffix and overwrite the forward + # map entry, causing every reference to map to the suffixed name and + # leaving the original sanitized slot orphaned in `used` with no + # reverse mapping. + if original in forward: + continue # Disambiguate against names already chosen this request. unique = candidate n = 1 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 7054c515017..6f67d5417d2 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 @@ -4043,6 +4043,26 @@ def test_build_anthropic_tool_name_maps_reverse_order_collision(): assert "foo_bar" not in reverse +def test_build_anthropic_tool_name_maps_duplicate_originals(): + """REGRESSION: duplicate originals must not corrupt the forward map. + + Previously, the second occurrence of the same invalid name would + rewrite ``forward[original]`` to a suffixed name (``foo_bar_2``), + leaving ``foo_bar`` orphaned in ``used`` with no reverse mapping — + so when ``_sanitize_tool_names_in_request`` applied the forward + map, *both* tool entries got the suffixed name and Anthropic 400'd + on duplicates. + """ + from litellm.llms.anthropic.chat.transformation import ( + _build_anthropic_tool_name_maps, + ) + + forward, reverse = _build_anthropic_tool_name_maps(["foo/bar", "foo/bar"]) + # Same original sanitizes to the same target — no spurious suffix. + assert forward == {"foo/bar": "foo_bar"} + assert reverse == {"foo_bar": "foo/bar"} + + def test_map_openai_params_does_not_pollute_optional_params_with_internal_keys(): """REGRESSION: ``optional_params`` is what becomes the JSON body sent to Anthropic (``data = {**optional_params}``). It MUST NOT carry LiteLLM-