fix(responses): match guardrail tools by ordinal in one pass

Sort the chat-tool keys once and number duplicates with groupby instead of
rescanning every preceding key per position, so the guardrail merge stays
O(n log n) on client-supplied tool lists. Drop the comment that restated the
unsupported-tool warning in the Responses-to-chat transformation.
This commit is contained in:
mateo-berri 2026-09-02 12:57:01 -07:00
parent d7ee215c57
commit dc12e4c2b4
3 changed files with 24 additions and 6 deletions

View file

@ -1,5 +1,5 @@
from collections.abc import Iterable, Mapping, Sequence
from itertools import accumulate, chain
from itertools import accumulate, chain, groupby
from types import MappingProxyType
from typing import Final, TypeAlias
@ -47,7 +47,11 @@ def _chat_tool_key(tool: Tool) -> str:
def _indexed_keys(tools: Sequence[Tool]) -> tuple[IndexedKey, ...]:
keys: Final = tuple(_chat_tool_key(tool) for tool in tools)
return tuple((key, keys[:position].count(key)) for position, key in enumerate(keys))
positions_by_key: Final = groupby(sorted(range(len(keys)), key=keys.__getitem__), key=keys.__getitem__)
ordinal_by_position: Final = MappingProxyType(
{position: ordinal for _, positions in positions_by_key for ordinal, position in enumerate(positions)}
)
return tuple((key, ordinal_by_position[position]) for position, key in enumerate(keys))
def _namespace_members(namespace: Tool) -> tuple[Tool, ...]:

View file

@ -1903,10 +1903,6 @@ class LiteLLMCompletionResponsesConfig:
converted: Final = convert_custom_tool_to_function_tool(tool)
return ResponsesToolChatForm(chat_tools=() if converted is None else (converted,), web_search_options=None)
if tool_type in ("computer_use", "image_generation", "shell"):
# Drop unsupported Responses-API-only tool types that have no
# Chat Completions equivalent. Passing them through verbatim
# causes providers to reject the request with "'function' is a
# required property".
verbose_logger.warning(
"Dropping Responses API tool of type '%s': it has no Chat Completions "
"equivalent and the target provider would reject the request.",

View file

@ -56,6 +56,24 @@ def test_duplicate_function_names_are_matched_by_ordinal():
assert list(merged) == [original[0]]
def test_interleaved_duplicate_names_keep_their_own_ordinals():
original = [
_function("dup", "a"),
_function("other", "x"),
_function("dup", "b"),
_function("dup", "c"),
_function("other", "y"),
]
groups = _groups(original)
flat = _flat(groups)
edited = {**flat[3], "function": {**flat[3]["function"], "description": "changed"}}
merged = merge_guardrailed_tools(original, groups, [*flat[:3], edited, flat[4]])
assert list(merged) == [*original[:3], {**_function("dup", "changed"), "strict": False}, original[4]]
assert all(merged[position] is original[position] for position in (0, 1, 2, 4))
def test_edited_mcp_tool_is_rewritten():
original = [{"type": "mcp", "server_label": "deepwiki", "server_url": "https://mcp.deepwiki.com/mcp"}]
groups = _groups(original)