Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/guardrails/tool_name_extraction.py: 61%
46 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""
2Extract tool names from request body by route/call type.
4Used by auth (check_tools_allowlist) and ToolPolicyGuardrail so tool-format
5knowledge lives in one place. Uses guardrail translation handlers where available,
6with standalone extractors for generate_content and MCP.
7"""
9from typing import Any, Final
11from litellm.litellm_core_utils.api_route_to_call_types import get_call_types_for_route
12from litellm.llms import load_guardrail_translation_mappings
13from litellm.types.utils import CallTypes
15# Call types that have no guardrail translation handler; we use standalone extractors
16STANDALONE_EXTRACTORS: Final[dict[str, Any]] = {}
19def _extract_generate_content_tool_names(data: dict) -> list[str]:
20 """Google generateContent: tools[].functionDeclarations[].name"""
21 names: Final[list[str]] = []
22 for tool in data.get("tools") or []: 22 ↛ 23line 22 didn't jump to line 23 because the loop on line 22 never started
23 if not isinstance(tool, dict):
24 continue
25 for decl in tool.get("functionDeclarations") or []:
26 if isinstance(decl, dict) and decl.get("name"):
27 names.append(str(decl["name"]))
28 return names
31def _extract_mcp_tool_names(data: dict) -> list[str]:
32 """MCP call_tool: name or mcp_tool_name in body"""
33 names: Final[list[str]] = []
34 name: Final = data.get("name") or data.get("mcp_tool_name")
35 if name:
36 names.append(str(name))
37 return names
40def _register_standalone_extractors() -> None:
41 if STANDALONE_EXTRACTORS:
42 return
43 STANDALONE_EXTRACTORS[CallTypes.generate_content.value] = _extract_generate_content_tool_names
44 STANDALONE_EXTRACTORS[CallTypes.agenerate_content.value] = _extract_generate_content_tool_names
45 STANDALONE_EXTRACTORS[CallTypes.call_mcp_tool.value] = _extract_mcp_tool_names
48# Tool-capable call types (routes that can send tools in the request)
49TOOL_CAPABLE_CALL_TYPES: Final = frozenset(
50 {
51 CallTypes.completion.value,
52 CallTypes.acompletion.value,
53 CallTypes.responses.value,
54 CallTypes.aresponses.value,
55 CallTypes.anthropic_messages.value,
56 CallTypes.generate_content.value,
57 CallTypes.agenerate_content.value,
58 CallTypes.call_mcp_tool.value,
59 }
60)
63def extract_request_tool_names(route: str, data: dict) -> list[str]:
64 """
65 Extract tool names from the request body for the given route.
66 Uses guardrail translation handlers when available, else standalone extractors
67 for generate_content and MCP. Returns [] for non-tool-capable routes or when
68 no tools are present.
69 """
70 call_types: Final = get_call_types_for_route(route)
71 if not call_types: 71 ↛ 72line 71 didn't jump to line 72 because the condition on line 71 was never true
72 return []
73 _register_standalone_extractors()
74 mappings: Final = load_guardrail_translation_mappings()
75 for call_type in call_types:
76 if not isinstance(call_type, CallTypes): 76 ↛ 77line 76 didn't jump to line 77 because the condition on line 76 was never true
77 continue
78 if call_type.value not in TOOL_CAPABLE_CALL_TYPES: 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true
79 continue
80 if call_type.value in STANDALONE_EXTRACTORS:
81 return STANDALONE_EXTRACTORS[call_type.value](data)
82 handler_cls = mappings.get(call_type)
83 if handler_cls is not None: 83 ↛ 75line 83 didn't jump to line 75 because the condition on line 83 was always true
84 names = handler_cls().extract_request_tool_names(data)
85 if names: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true
86 return names
87 return []