Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/search_endpoints/endpoints.py: 37%
73 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#### Search Endpoints #####
3from typing import Final
5import orjson
6from fastapi import APIRouter, Depends, HTTPException, Request, Response
7from fastapi.responses import ORJSONResponse
9from litellm._logging import verbose_proxy_logger
10from litellm.proxy._types import *
11from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth
12from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
14router: Final = APIRouter()
17@router.post(
18 "/v1/search/{search_tool_name}",
19 dependencies=[Depends(user_api_key_auth)],
20 response_class=ORJSONResponse,
21 tags=["search"],
22)
23@router.post(
24 "/search/{search_tool_name}",
25 dependencies=[Depends(user_api_key_auth)],
26 response_class=ORJSONResponse,
27 tags=["search"],
28)
29@router.post(
30 "/v1/search",
31 dependencies=[Depends(user_api_key_auth)],
32 response_class=ORJSONResponse,
33 tags=["search"],
34)
35@router.post(
36 "/search",
37 dependencies=[Depends(user_api_key_auth)],
38 response_class=ORJSONResponse,
39 tags=["search"],
40)
41async def search(
42 request: Request,
43 fastapi_response: Response,
44 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
45 search_tool_name: str | None = None,
46):
47 """
48 Search endpoint for performing web searches.
50 Follows the Perplexity Search API spec:
51 https://docs.perplexity.ai/api-reference/search-post
53 The search_tool_name can be passed either:
54 1. In the URL path: /v1/search/{search_tool_name}
55 2. In the request body: {"search_tool_name": "..."}
57 Example with search_tool_name in URL (recommended - keeps body Perplexity-compatible):
58 ```bash
59 curl -X POST "http://localhost:4000/v1/search/litellm-search" \
60 -H "Authorization: Bearer sk-1234" \
61 -H "Content-Type: application/json" \
62 -d '{
63 "query": "latest AI developments 2024",
64 "max_results": 5,
65 "search_domain_filter": ["arxiv.org", "nature.com"],
66 "country": "US"
67 }'
68 ```
70 Example with search_tool_name in body:
71 ```bash
72 curl -X POST "http://localhost:4000/v1/search" \
73 -H "Authorization: Bearer sk-1234" \
74 -H "Content-Type: application/json" \
75 -d '{
76 "search_tool_name": "litellm-search",
77 "query": "latest AI developments 2024",
78 "max_results": 5,
79 "search_domain_filter": ["arxiv.org", "nature.com"],
80 "country": "US"
81 }'
82 ```
84 Request Body Parameters (when search_tool_name not in URL):
85 - search_tool_name (str, required if not in URL): Name of the search tool configured in router
86 - query (str or list[str], required): Search query
87 - max_results (int, optional): Maximum number of results (1-20), default 10
88 - search_domain_filter (list[str], optional): List of domains to filter (max 20)
89 - max_tokens_per_page (int, optional): Max tokens per page, default 1024
90 - country (str, optional): Country code filter (e.g., 'US', 'GB', 'DE')
92 When using URL path parameter, only Perplexity-compatible parameters are needed in body:
93 - query (str or list[str], required): Search query
94 - max_results (int, optional): Maximum number of results (1-20), default 10
95 - search_domain_filter (list[str], optional): List of domains to filter (max 20)
96 - max_tokens_per_page (int, optional): Max tokens per page, default 1024
97 - country (str, optional): Country code filter (e.g., 'US', 'GB', 'DE')
99 Response follows Perplexity Search API format:
100 ```json
101 {
102 "object": "search",
103 "results": [
104 {
105 "title": "Result title",
106 "url": "https://example.com",
107 "snippet": "Result snippet...",
108 "date": "2024-01-01",
109 "last_updated": "2024-01-01"
110 }
111 ]
112 }
113 ```
114 """
115 from litellm.proxy.proxy_server import (
116 general_settings,
117 llm_router,
118 proxy_config,
119 proxy_logging_obj,
120 select_data_generator,
121 user_api_base,
122 user_max_tokens,
123 user_model,
124 user_request_timeout,
125 user_temperature,
126 version,
127 )
129 # Read request body
130 body: Final = await request.body()
131 data: Final = orjson.loads(body)
133 # If search_tool_name is provided in URL path, use it (takes precedence over body)
134 if search_tool_name is not None:
135 data["search_tool_name"] = search_tool_name
137 if "search_tool_name" in data and data["search_tool_name"]:
138 data["model"] = data["search_tool_name"]
139 search_tool_name_value: Final = data["search_tool_name"]
141 # Authorization check: verify key can access this search tool
142 from litellm.proxy.auth.auth_checks import (
143 can_key_call_search_tool,
144 can_team_call_search_tool,
145 get_team_object,
146 )
148 try:
149 # Check key-level access
150 await can_key_call_search_tool(
151 search_tool_name=search_tool_name_value,
152 valid_token=user_api_key_dict,
153 )
155 # Check team-level access if key is associated with a team
156 if user_api_key_dict.team_id:
157 from litellm.proxy.proxy_server import (
158 prisma_client,
159 proxy_logging_obj,
160 user_api_key_cache,
161 )
163 team_object: Final = await get_team_object(
164 team_id=user_api_key_dict.team_id,
165 prisma_client=prisma_client,
166 user_api_key_cache=user_api_key_cache,
167 parent_otel_span=user_api_key_dict.parent_otel_span,
168 proxy_logging_obj=proxy_logging_obj,
169 )
170 await can_team_call_search_tool(
171 search_tool_name=search_tool_name_value,
172 team_object=team_object,
173 )
174 except Exception as e:
175 verbose_proxy_logger.error("Search tool authorization failed for %s: %s", search_tool_name_value, e)
176 raise
178 if llm_router is not None and hasattr(llm_router, "search_tools"):
179 verbose_proxy_logger.debug(
180 "Search endpoint - Looking for search_tool_name: %s. Available search tools in router: %s. Total search tools: %s",
181 search_tool_name_value,
182 [tool.get("search_tool_name") for tool in llm_router.search_tools],
183 len(llm_router.search_tools),
184 )
186 matching_tools: Final = [
187 tool for tool in llm_router.search_tools if tool.get("search_tool_name") == search_tool_name_value
188 ]
190 if matching_tools:
191 search_tool: Final = matching_tools[0]
192 search_provider: Final = search_tool.get("litellm_params", {}).get("search_provider")
194 if search_provider:
195 data["custom_llm_provider"] = search_provider
197 if "metadata" not in data:
198 data["metadata"] = {}
199 data["metadata"]["model_group"] = search_tool_name_value
201 # Ensure team context is available to search router credential resolution.
202 # add_litellm_data_to_request() also injects these values, but this keeps
203 # search endpoint behavior explicit and resilient for direct router paths.
204 if "metadata" not in data or not isinstance(data.get("metadata"), dict):
205 data["metadata"] = {}
206 if getattr(user_api_key_dict, "team_metadata", None) is not None:
207 data["metadata"]["user_api_key_team_metadata"] = user_api_key_dict.team_metadata
208 if getattr(user_api_key_dict, "team_id", None) is not None:
209 data["metadata"]["user_api_key_team_id"] = user_api_key_dict.team_id
211 # Process request using ProxyBaseLLMRequestProcessing
212 processor: Final = ProxyBaseLLMRequestProcessing(data=data)
213 try:
214 return await processor.base_process_llm_request(
215 request=request,
216 fastapi_response=fastapi_response,
217 user_api_key_dict=user_api_key_dict,
218 route_type="asearch",
219 proxy_logging_obj=proxy_logging_obj,
220 llm_router=llm_router,
221 general_settings=general_settings,
222 proxy_config=proxy_config,
223 select_data_generator=select_data_generator,
224 model=None,
225 user_model=user_model,
226 user_temperature=user_temperature,
227 user_request_timeout=user_request_timeout,
228 user_max_tokens=user_max_tokens,
229 user_api_base=user_api_base,
230 version=version,
231 )
232 except Exception as e:
233 raise await processor._handle_llm_api_exception(
234 e=e,
235 user_api_key_dict=user_api_key_dict,
236 proxy_logging_obj=proxy_logging_obj,
237 version=version,
238 )
241@router.get(
242 "/v1/search/tools",
243 dependencies=[Depends(user_api_key_auth)],
244 response_class=ORJSONResponse,
245 tags=["search"],
246)
247@router.get(
248 "/search/tools",
249 dependencies=[Depends(user_api_key_auth)],
250 response_class=ORJSONResponse,
251 tags=["search"],
252)
253async def list_search_tools(
254 request: Request,
255 fastapi_response: Response,
256 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
257):
258 """
259 List all available search tools configured in the router.
261 This endpoint returns the search tools that are currently loaded and available
262 for use with the /v1/search endpoint.
264 Example:
265 ```bash
266 curl -X GET "http://localhost:4000/v1/search/tools" \
267 -H "Authorization: Bearer sk-1234"
268 ```
270 Response:
271 ```json
272 {
273 "object": "list",
274 "data": [
275 {
276 "search_tool_name": "litellm-search",
277 "search_provider": "perplexity",
278 "description": "Perplexity search tool"
279 }
280 ]
281 }
282 ```
283 """
284 from litellm.proxy.proxy_server import llm_router
286 try:
287 search_tools_list: Final = []
289 if llm_router is not None and hasattr(llm_router, "search_tools"): 289 ↛ 304line 289 didn't jump to line 304 because the condition on line 289 was always true
290 for tool in llm_router.search_tools:
291 tool_info = {
292 "search_tool_name": tool.get("search_tool_name"),
293 "search_provider": tool.get("litellm_params", {}).get("search_provider"),
294 }
296 # Add description if available
297 if "search_tool_info" in tool and tool["search_tool_info"]:
298 description = tool["search_tool_info"].get("description")
299 if description: 299 ↛ 300line 299 didn't jump to line 300 because the condition on line 299 was never true
300 tool_info["description"] = description
302 search_tools_list.append(tool_info)
304 return {"object": "list", "data": search_tools_list}
305 except Exception as e:
306 from litellm._logging import verbose_proxy_logger
308 verbose_proxy_logger.exception("Error listing search tools: %s", e)
309 raise HTTPException(status_code=500, detail=str(e))