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 <mateo-berri@users.noreply.github.com>
This commit is contained in:
mateo-berri 2026-05-05 20:39:23 +00:00
parent 56ef1ed003
commit 7d5a1521a7
No known key found for this signature in database
2 changed files with 27 additions and 0 deletions

View file

@ -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

View file

@ -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-