From 4bf46cb18edb69b286b1bca0c4cf8c43b03244fb Mon Sep 17 00:00:00 2001 From: Filippo Mattia Menghi Date: Wed, 10 Jun 2026 10:39:38 +0200 Subject: [PATCH] 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. --- litellm/proxy/db/db_spend_update_writer.py | 25 ++++++++-- .../proxy/db/test_db_spend_update_writer.py | 49 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index e7f14df5294..67c54a01f08 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -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 diff --git a/tests/test_litellm/proxy/db/test_db_spend_update_writer.py b/tests/test_litellm/proxy/db/test_db_spend_update_writer.py index 79e6494eab0..a1006ac8980 100644 --- a/tests/test_litellm/proxy/db/test_db_spend_update_writer.py +++ b/tests/test_litellm/proxy/db/test_db_spend_update_writer.py @@ -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"]