fix(proxy): populate tool registry for anthropic_messages traffic

LiteLLM_ToolTable and LiteLLM_SpendLogToolIndex stayed empty for
/v1/messages requests. _enqueue_tool_registry_upsert only read
tools[].function.name from the request, missing the Anthropic format
where the name is top-level, and gated response extraction on
hasattr(completion_response, "choices"), which is False for
AnthropicMessagesResponse since it is a TypedDict. Also extract
top-level request tool names and tool_use content blocks from
dict-shaped responses.

Fixes #27840.
This commit is contained in:
Filippo Mattia Menghi 2026-06-10 10:39:38 +02:00
parent 8f54ab2493
commit 4bf46cb18e
2 changed files with 69 additions and 5 deletions

View file

@ -245,12 +245,15 @@ class DBSpendUpdateWriter:
Extract tool names from the LLM request and response and enqueue them
for upsert into LiteLLM_ToolTable via ToolDiscoveryQueue.
Handles four sources:
Handles five sources:
- MCP tools: standard_logging_object.mcp_tool_call_metadata.namespaced_tool_name
- Response tool_calls (OpenAI / Anthropic pass-through converted to OpenAI format):
completion_response.choices[].message.tool_calls[].function.name
- Request tools array (OpenAI format): kwargs["tools"][].function.name
- Request tools array (Anthropic /messages format): kwargs["passthrough_logging_payload"]
- Response tool_use blocks (anthropic_messages route, dict response):
completion_response["content"][].name where type == "tool_use"
- Request tools array (OpenAI format: tools[].function.name,
Anthropic /messages format: tools[].name)
- Request tools array (Anthropic pass-through): kwargs["passthrough_logging_payload"]
["request_body"]["tools"][].name
"""
try:
@ -290,13 +293,16 @@ class DBSpendUpdateWriter:
if tool_name:
_enqueue(tool_name, origin=mcp_server_name or "user_defined")
# --- Tools from request body (OpenAI format: tools[].function.name) ---
# --- Tools from request body (OpenAI format: tools[].function.name,
# Anthropic /messages format: tools[].name) ---
request_tools = kwargs.get("tools") or []
for tool_def in request_tools:
if not isinstance(tool_def, dict):
continue
fn = tool_def.get("function") or {}
name = fn.get("name") if isinstance(fn, dict) else None
name = (
fn.get("name") if isinstance(fn, dict) else None
) or tool_def.get("name")
if name:
_enqueue(name)
@ -333,6 +339,15 @@ class DBSpendUpdateWriter:
tool_name = getattr(fn, "name", None)
if tool_name:
_enqueue(tool_name)
# --- Response tool_use blocks (anthropic_messages route returns
# AnthropicMessagesResponse, a TypedDict / plain dict) ---
elif isinstance(completion_response, dict):
for block in completion_response.get("content") or []:
if isinstance(block, dict) and block.get("type") == "tool_use":
name = block.get("name")
if name:
_enqueue(name)
except Exception as e:
verbose_proxy_logger.debug(
"_enqueue_tool_registry_upsert error (non-blocking): %s", e

View file

@ -1656,3 +1656,52 @@ async def test_commit_spend_updates_iterates_in_sorted_order(
)
assert captured_where_values == expected_order
def test_enqueue_tool_registry_upsert_anthropic_format_request_tools():
writer = DBSpendUpdateWriter()
writer.tool_discovery_queue.add_update = MagicMock()
writer._enqueue_tool_registry_upsert(
kwargs={
"tools": [
{"name": "get_weather", "input_schema": {"type": "object"}},
{"name": "search_docs", "input_schema": {"type": "object"}},
]
},
completion_response=None,
)
enqueued = [
c.args[0]["tool_name"]
for c in writer.tool_discovery_queue.add_update.call_args_list
]
assert enqueued == ["get_weather", "search_docs"]
def test_enqueue_tool_registry_upsert_dict_response_tool_use_blocks():
writer = DBSpendUpdateWriter()
writer.tool_discovery_queue.add_update = MagicMock()
writer._enqueue_tool_registry_upsert(
kwargs={},
completion_response={
"id": "msg_123",
"role": "assistant",
"content": [
{"type": "text", "text": "Let me check the weather."},
{
"type": "tool_use",
"id": "toolu_01",
"name": "get_weather",
"input": {"city": "San Francisco"},
},
],
},
)
enqueued = [
c.args[0]["tool_name"]
for c in writer.tool_discovery_queue.add_update.call_args_list
]
assert enqueued == ["get_weather"]