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

1#### Search Endpoints ##### 

2 

3from typing import Final 

4 

5import orjson 

6from fastapi import APIRouter, Depends, HTTPException, Request, Response 

7from fastapi.responses import ORJSONResponse 

8 

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 

13 

14router: Final = APIRouter() 

15 

16 

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. 

49  

50 Follows the Perplexity Search API spec: 

51 https://docs.perplexity.ai/api-reference/search-post 

52  

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": "..."} 

56  

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 ``` 

69  

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 ``` 

83  

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') 

91  

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') 

98  

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 ) 

128 

129 # Read request body 

130 body: Final = await request.body() 

131 data: Final = orjson.loads(body) 

132 

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 

136 

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"] 

140 

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 ) 

147 

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 ) 

154 

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 ) 

162 

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 

177 

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 ) 

185 

186 matching_tools: Final = [ 

187 tool for tool in llm_router.search_tools if tool.get("search_tool_name") == search_tool_name_value 

188 ] 

189 

190 if matching_tools: 

191 search_tool: Final = matching_tools[0] 

192 search_provider: Final = search_tool.get("litellm_params", {}).get("search_provider") 

193 

194 if search_provider: 

195 data["custom_llm_provider"] = search_provider 

196 

197 if "metadata" not in data: 

198 data["metadata"] = {} 

199 data["metadata"]["model_group"] = search_tool_name_value 

200 

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 

210 

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 ) 

239 

240 

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. 

260  

261 This endpoint returns the search tools that are currently loaded and available 

262 for use with the /v1/search endpoint. 

263  

264 Example: 

265 ```bash 

266 curl -X GET "http://localhost:4000/v1/search/tools" \ 

267 -H "Authorization: Bearer sk-1234" 

268 ``` 

269  

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 

285 

286 try: 

287 search_tools_list: Final = [] 

288 

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 } 

295 

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 

301 

302 search_tools_list.append(tool_info) 

303 

304 return {"object": "list", "data": search_tools_list} 

305 except Exception as e: 

306 from litellm._logging import verbose_proxy_logger 

307 

308 verbose_proxy_logger.exception("Error listing search tools: %s", e) 

309 raise HTTPException(status_code=500, detail=str(e))