Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/search_endpoints/search_tool_management.py: 65%

196 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1""" 

2CRUD ENDPOINTS FOR SEARCH TOOLS 

3""" 

4 

5from collections.abc import Awaitable, Callable 

6from datetime import datetime 

7from typing import Any, Final, TypeAlias 

8 

9from fastapi import APIRouter, Depends, HTTPException 

10from pydantic import BaseModel 

11 

12from litellm._logging import verbose_proxy_logger 

13from litellm.constants import UI_SESSION_TOKEN_TEAM_ID 

14from litellm.proxy._types import ( 

15 LiteLLM_TeamTable, 

16 LitellmUserRoles, 

17 UserAPIKeyAuth, 

18) 

19from litellm.proxy.auth.user_api_key_auth import user_api_key_auth 

20from litellm.proxy.search_endpoints.search_tool_registry import SearchToolRegistry 

21from litellm.types.search import ( 

22 ListSearchToolsResponse, 

23 SearchTool, 

24 SearchToolInfoResponse, 

25) 

26from litellm.types.utils import SearchProviders 

27 

28#### SEARCH TOOLS ENDPOINTS #### 

29 

30router: Final = APIRouter() 

31SEARCH_TOOL_REGISTRY: Final = SearchToolRegistry() 

32 

33 

34def _convert_datetime_to_str(value: datetime | str | None) -> str | None: 

35 """ 

36 Convert datetime object to ISO format string. 

37 

38 Args: 

39 value: datetime object, string, or None 

40 

41 Returns: 

42 ISO format string or original value if already string or None 

43 """ 

44 if value is None: 44 ↛ 45line 44 didn't jump to line 45 because the condition on line 44 was never true

45 return None 

46 if isinstance(value, datetime): 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true

47 return value.isoformat() 

48 return value 

49 

50 

51TeamObjectLookup: TypeAlias = Callable[[str, UserAPIKeyAuth], Awaitable[LiteLLM_TeamTable]] 

52 

53 

54async def _refresh_router_search_tools() -> None: 

55 """Push the search tools table into this worker's router. 

56 

57 Best-effort: the row is already committed, so a refresh failure must not surface as a 500 and 

58 push the caller into a retry that creates duplicates. 

59 """ 

60 from litellm.proxy.proxy_server import proxy_config 

61 

62 try: 

63 await proxy_config.reload_search_tools_from_db() 

64 except Exception as e: # noqa: BLE001 # the row is committed; no refresh failure may reach the caller 

65 verbose_proxy_logger.exception("Search tool router refresh failed after a management write: %s", e) 

66 

67 

68async def _team_object_from_db(team_id: str, user_api_key_dict: UserAPIKeyAuth) -> LiteLLM_TeamTable: 

69 from litellm.proxy.auth.auth_checks import get_team_object 

70 from litellm.proxy.proxy_server import ( 

71 prisma_client, 

72 proxy_logging_obj, 

73 user_api_key_cache, 

74 ) 

75 

76 return await get_team_object( 

77 team_id=team_id, 

78 prisma_client=prisma_client, 

79 user_api_key_cache=user_api_key_cache, 

80 parent_otel_span=user_api_key_dict.parent_otel_span, 

81 proxy_logging_obj=proxy_logging_obj, 

82 ) 

83 

84 

85def _allowlist_team_id(user_api_key_dict: UserAPIKeyAuth) -> str | None: 

86 """ 

87 The team whose object_permission allowlist scopes this caller, or None when there is none. 

88 

89 Every Admin UI session key is stamped with UI_SESSION_TOKEN_TEAM_ID, a reserved sentinel that 

90 never has a row in LiteLLM_TeamTable (`/team/new` rejects it as a real team id), so looking it 

91 up would raise 404 instead of resolving a team. It carries no allowlist of its own, so the 

92 caller is scoped by its key-level allowlist alone. Any other team id is looked up for real and 

93 a failed lookup still surfaces. 

94 """ 

95 team_id: Final = user_api_key_dict.team_id 

96 if not team_id or team_id == UI_SESSION_TOKEN_TEAM_ID: 

97 return None 

98 return team_id 

99 

100 

101async def _filter_visible_search_tools( 

102 search_tools: list[SearchToolInfoResponse], 

103 user_api_key_dict: UserAPIKeyAuth, 

104 lookup_team_object: TeamObjectLookup = _team_object_from_db, 

105) -> list[SearchToolInfoResponse]: 

106 """ 

107 Drop search tools the caller is not authorized to invoke, applying the same 

108 key/team object_permission allowlists enforced on /search. Admins see all tools. 

109 """ 

110 if user_api_key_dict.user_role in ( 110 ↛ 116line 110 didn't jump to line 116 because the condition on line 110 was always true

111 LitellmUserRoles.PROXY_ADMIN, 

112 LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY, 

113 ): 

114 return search_tools 

115 

116 from litellm.proxy.auth.auth_checks import can_user_view_search_tool 

117 

118 allowlist_team_id: Final = _allowlist_team_id(user_api_key_dict) 

119 team_object: Final[LiteLLM_TeamTable | None] = ( 

120 await lookup_team_object(allowlist_team_id, user_api_key_dict) if allowlist_team_id else None 

121 ) 

122 

123 visible: Final[list[SearchToolInfoResponse]] = [] 

124 for tool in search_tools: 

125 tool_name = tool.get("search_tool_name") 

126 if tool_name and await can_user_view_search_tool( 

127 search_tool_name=tool_name, 

128 valid_token=user_api_key_dict, 

129 team_object=team_object, 

130 ): 

131 visible.append(tool) 

132 return visible 

133 

134 

135@router.get( 

136 "/search_tools/list", 

137 tags=["Search Tools"], 

138 dependencies=[Depends(user_api_key_auth)], 

139 response_model=ListSearchToolsResponse, 

140) 

141async def list_search_tools( 

142 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

143): 

144 """ 

145 List all search tools that are available in the database and config file. 

146 

147 Example Request: 

148 ```bash 

149 curl -X GET "http://localhost:4000/search_tools/list" -H "Authorization: Bearer <your_api_key>" 

150 ``` 

151 

152 Example Response: 

153 ```json 

154 { 

155 "search_tools": [ 

156 { 

157 "search_tool_id": "123e4567-e89b-12d3-a456-426614174000", 

158 "search_tool_name": "litellm-search", 

159 "litellm_params": { 

160 "search_provider": "perplexity", 

161 "api_key": "sk-***", 

162 "api_base": "https://api.perplexity.ai" 

163 }, 

164 "search_tool_info": { 

165 "description": "Perplexity search tool" 

166 }, 

167 "created_at": "2023-11-09T12:34:56.789Z", 

168 "updated_at": "2023-11-09T12:34:56.789Z", 

169 "is_from_config": false 

170 }, 

171 { 

172 "search_tool_name": "config-search-tool", 

173 "litellm_params": { 

174 "search_provider": "tavily", 

175 "api_key": "tvly-***" 

176 }, 

177 "is_from_config": true 

178 } 

179 ] 

180 } 

181 ``` 

182 """ 

183 from litellm.litellm_core_utils.litellm_logging import _get_masked_values 

184 from litellm.proxy.proxy_server import prisma_client, proxy_config 

185 

186 if prisma_client is None: 186 ↛ 187line 186 didn't jump to line 187 because the condition on line 186 was never true

187 raise HTTPException(status_code=500, detail="Prisma client not initialized") 

188 

189 try: 

190 search_tools_from_db = await SEARCH_TOOL_REGISTRY.get_all_search_tools_from_db(prisma_client=prisma_client) 

191 

192 db_tool_names: Final = {tool.get("search_tool_name") for tool in search_tools_from_db} 

193 

194 search_tool_configs: list[SearchToolInfoResponse] = [] 

195 

196 config_search_tools = [] 

197 

198 try: 

199 config: Final = await proxy_config.get_config() 

200 parsed_tools: Final = proxy_config.parse_search_tools(config) 

201 if parsed_tools: 201 ↛ 202line 201 didn't jump to line 202 because the condition on line 201 was never true

202 config_search_tools = parsed_tools 

203 except Exception as e: 

204 verbose_proxy_logger.debug("Could not get config-defined search tools: %s", e) 

205 

206 for config_search_tool in config_search_tools: 206 ↛ 207line 206 didn't jump to line 207 because the loop on line 206 never started

207 tool_name = config_search_tool.get("search_tool_name") 

208 if tool_name: 

209 litellm_params_dict = dict(config_search_tool.get("litellm_params", {})) 

210 masked_litellm_params_dict = _get_masked_values( 

211 litellm_params_dict, 

212 unmasked_length=4, 

213 number_of_asterisks=4, 

214 ) 

215 config_tool_info = config_search_tool.get("search_tool_info") 

216 

217 search_tool_configs.append( 

218 SearchToolInfoResponse( 

219 search_tool_id=None, 

220 search_tool_name=tool_name, 

221 litellm_params=masked_litellm_params_dict, 

222 search_tool_info=(dict(config_tool_info) if config_tool_info else None), 

223 created_at=None, 

224 updated_at=None, 

225 is_from_config=True, 

226 ) 

227 ) 

228 

229 search_tool_configs = [ 

230 tool for tool in search_tool_configs if tool.get("search_tool_name") not in db_tool_names 

231 ] 

232 

233 for db_search_tool in search_tools_from_db: 

234 litellm_params_dict = dict(db_search_tool.get("litellm_params", {})) 

235 masked_litellm_params_dict = _get_masked_values( 

236 litellm_params_dict, 

237 unmasked_length=4, 

238 number_of_asterisks=4, 

239 ) 

240 

241 search_tool_configs.append( 

242 SearchToolInfoResponse( 

243 search_tool_id=db_search_tool.get("search_tool_id"), 

244 search_tool_name=db_search_tool.get("search_tool_name", ""), 

245 litellm_params=masked_litellm_params_dict, 

246 search_tool_info=db_search_tool.get("search_tool_info"), 

247 created_at=_convert_datetime_to_str(db_search_tool.get("created_at")), 

248 updated_at=_convert_datetime_to_str(db_search_tool.get("updated_at")), 

249 is_from_config=False, 

250 ) 

251 ) 

252 

253 visible_search_tools: Final = await _filter_visible_search_tools(search_tool_configs, user_api_key_dict) 

254 

255 return ListSearchToolsResponse(search_tools=visible_search_tools) 

256 except HTTPException: 

257 raise 

258 except Exception as e: 

259 verbose_proxy_logger.exception("Error getting search tools: %s", e) 

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

261 

262 

263class CreateSearchToolRequest(BaseModel): 

264 search_tool: SearchTool 

265 

266 

267@router.post( 

268 "/search_tools", 

269 tags=["Search Tools"], 

270 dependencies=[Depends(user_api_key_auth)], 

271) 

272async def create_search_tool(request: CreateSearchToolRequest): 

273 """ 

274 Create a new search tool. 

275 

276 Example Request: 

277 ```bash 

278 curl -X POST "http://localhost:4000/search_tools" \\ 

279 -H "Authorization: Bearer <your_api_key>" \\ 

280 -H "Content-Type: application/json" \\ 

281 -d '{ 

282 "search_tool": { 

283 "search_tool_name": "litellm-search", 

284 "litellm_params": { 

285 "search_provider": "perplexity", 

286 "api_key": "sk-..." 

287 }, 

288 "search_tool_info": { 

289 "description": "Perplexity search tool" 

290 } 

291 } 

292 }' 

293 ``` 

294 

295 Example Response: 

296 ```json 

297 { 

298 "search_tool_id": "123e4567-e89b-12d3-a456-426614174000", 

299 "search_tool_name": "litellm-search", 

300 "litellm_params": { 

301 "search_provider": "perplexity", 

302 "api_key": "sk-..." 

303 }, 

304 "search_tool_info": { 

305 "description": "Perplexity search tool" 

306 }, 

307 "created_at": "2023-11-09T12:34:56.789Z", 

308 "updated_at": "2023-11-09T12:34:56.789Z" 

309 } 

310 ``` 

311 """ 

312 from litellm.proxy.proxy_server import prisma_client 

313 

314 if prisma_client is None: 314 ↛ 315line 314 didn't jump to line 315 because the condition on line 314 was never true

315 raise HTTPException(status_code=500, detail="Prisma client not initialized") 

316 

317 try: 

318 result: Final = await SEARCH_TOOL_REGISTRY.add_search_tool_to_db( 

319 search_tool=request.search_tool, prisma_client=prisma_client 

320 ) 

321 

322 await _refresh_router_search_tools() 

323 

324 verbose_proxy_logger.debug( 

325 "Successfully added search tool '%s' to database.", 

326 result.get("search_tool_name"), 

327 ) 

328 

329 return result 

330 except Exception as e: 

331 verbose_proxy_logger.exception("Error adding search tool to db: %s", e) 

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

333 

334 

335class UpdateSearchToolRequest(BaseModel): 

336 search_tool: SearchTool 

337 

338 

339@router.put( 

340 "/search_tools/{search_tool_id}", 

341 tags=["Search Tools"], 

342 dependencies=[Depends(user_api_key_auth)], 

343) 

344async def update_search_tool(search_tool_id: str, request: UpdateSearchToolRequest): 

345 """ 

346 Update an existing search tool. 

347 

348 Example Request: 

349 ```bash 

350 curl -X PUT "http://localhost:4000/search_tools/123e4567-e89b-12d3-a456-426614174000" \\ 

351 -H "Authorization: Bearer <your_api_key>" \\ 

352 -H "Content-Type: application/json" \\ 

353 -d '{ 

354 "search_tool": { 

355 "search_tool_name": "updated-search", 

356 "litellm_params": { 

357 "search_provider": "perplexity", 

358 "api_key": "sk-new-key" 

359 }, 

360 "search_tool_info": { 

361 "description": "Updated search tool" 

362 } 

363 } 

364 }' 

365 ``` 

366 

367 Example Response: 

368 ```json 

369 { 

370 "search_tool_id": "123e4567-e89b-12d3-a456-426614174000", 

371 "search_tool_name": "updated-search", 

372 "litellm_params": { 

373 "search_provider": "perplexity", 

374 "api_key": "sk-new-key" 

375 }, 

376 "search_tool_info": { 

377 "description": "Updated search tool" 

378 }, 

379 "created_at": "2023-11-09T12:34:56.789Z", 

380 "updated_at": "2023-11-09T13:45:12.345Z" 

381 } 

382 ``` 

383 """ 

384 from litellm.proxy.proxy_server import prisma_client 

385 

386 if prisma_client is None: 386 ↛ 387line 386 didn't jump to line 387 because the condition on line 386 was never true

387 raise HTTPException(status_code=500, detail="Prisma client not initialized") 

388 

389 try: 

390 # Check if search tool exists 

391 existing_tool: Final = await SEARCH_TOOL_REGISTRY.get_search_tool_by_id_from_db( 

392 search_tool_id=search_tool_id, prisma_client=prisma_client 

393 ) 

394 

395 if existing_tool is None: 395 ↛ 396line 395 didn't jump to line 396 because the condition on line 395 was never true

396 raise HTTPException( 

397 status_code=404, 

398 detail=f"Search tool with ID {search_tool_id} not found", 

399 ) 

400 

401 result: Final = await SEARCH_TOOL_REGISTRY.update_search_tool_in_db( 

402 search_tool_id=search_tool_id, 

403 search_tool=request.search_tool, 

404 prisma_client=prisma_client, 

405 ) 

406 

407 await _refresh_router_search_tools() 

408 

409 verbose_proxy_logger.debug( 

410 "Successfully updated search tool '%s' in database.", 

411 result.get("search_tool_name"), 

412 ) 

413 

414 return result 

415 except HTTPException as e: 

416 raise e 

417 except Exception as e: 

418 verbose_proxy_logger.exception("Error updating search tool: %s", e) 

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

420 

421 

422@router.delete( 

423 "/search_tools/{search_tool_id}", 

424 tags=["Search Tools"], 

425 dependencies=[Depends(user_api_key_auth)], 

426) 

427async def delete_search_tool(search_tool_id: str): 

428 """ 

429 Delete a search tool. 

430 

431 Example Request: 

432 ```bash 

433 curl -X DELETE "http://localhost:4000/search_tools/123e4567-e89b-12d3-a456-426614174000" \\ 

434 -H "Authorization: Bearer <your_api_key>" 

435 ``` 

436 

437 Example Response: 

438 ```json 

439 { 

440 "message": "Search tool 123e4567-e89b-12d3-a456-426614174000 deleted successfully", 

441 "search_tool_name": "litellm-search" 

442 } 

443 ``` 

444 """ 

445 from litellm.proxy.proxy_server import prisma_client 

446 

447 if prisma_client is None: 447 ↛ 448line 447 didn't jump to line 448 because the condition on line 447 was never true

448 raise HTTPException(status_code=500, detail="Prisma client not initialized") 

449 

450 try: 

451 # Check if search tool exists 

452 existing_tool: Final = await SEARCH_TOOL_REGISTRY.get_search_tool_by_id_from_db( 

453 search_tool_id=search_tool_id, prisma_client=prisma_client 

454 ) 

455 

456 if existing_tool is None: 456 ↛ 457line 456 didn't jump to line 457 because the condition on line 456 was never true

457 raise HTTPException( 

458 status_code=404, 

459 detail=f"Search tool with ID {search_tool_id} not found", 

460 ) 

461 

462 result: Final = await SEARCH_TOOL_REGISTRY.delete_search_tool_from_db( 

463 search_tool_id=search_tool_id, prisma_client=prisma_client 

464 ) 

465 

466 await _refresh_router_search_tools() 

467 

468 verbose_proxy_logger.debug("Successfully deleted search tool from database.") 

469 

470 return result 

471 except HTTPException as e: 

472 raise e 

473 except Exception as e: 

474 verbose_proxy_logger.exception("Error deleting search tool: %s", e) 

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

476 

477 

478@router.get( 

479 "/search_tools/{search_tool_id}", 

480 tags=["Search Tools"], 

481 dependencies=[Depends(user_api_key_auth)], 

482) 

483async def get_search_tool_info(search_tool_id: str): 

484 """ 

485 Get detailed information about a specific search tool by ID. 

486 

487 Example Request: 

488 ```bash 

489 curl -X GET "http://localhost:4000/search_tools/123e4567-e89b-12d3-a456-426614174000" \\ 

490 -H "Authorization: Bearer <your_api_key>" 

491 ``` 

492 

493 Example Response: 

494 ```json 

495 { 

496 "search_tool_id": "123e4567-e89b-12d3-a456-426614174000", 

497 "search_tool_name": "litellm-search", 

498 "litellm_params": { 

499 "search_provider": "perplexity", 

500 "api_key": "sk-***" 

501 }, 

502 "search_tool_info": { 

503 "description": "Perplexity search tool" 

504 }, 

505 "created_at": "2023-11-09T12:34:56.789Z", 

506 "updated_at": "2023-11-09T12:34:56.789Z" 

507 } 

508 ``` 

509 """ 

510 from litellm.litellm_core_utils.litellm_logging import _get_masked_values 

511 from litellm.proxy.proxy_server import prisma_client 

512 

513 if prisma_client is None: 513 ↛ 514line 513 didn't jump to line 514 because the condition on line 513 was never true

514 raise HTTPException(status_code=500, detail="Prisma client not initialized") 

515 

516 try: 

517 result: Final = await SEARCH_TOOL_REGISTRY.get_search_tool_by_id_from_db( 

518 search_tool_id=search_tool_id, prisma_client=prisma_client 

519 ) 

520 

521 if result is None: 

522 raise HTTPException( 

523 status_code=404, 

524 detail=f"Search tool with ID {search_tool_id} not found", 

525 ) 

526 

527 # Mask sensitive data 

528 litellm_params_dict: Final = dict(result.get("litellm_params", {})) 

529 masked_litellm_params_dict: Final = _get_masked_values( 

530 litellm_params_dict, 

531 unmasked_length=4, 

532 number_of_asterisks=4, 

533 ) 

534 

535 return SearchToolInfoResponse( 

536 search_tool_id=result.get("search_tool_id"), 

537 search_tool_name=result.get("search_tool_name", ""), 

538 litellm_params=masked_litellm_params_dict, 

539 search_tool_info=result.get("search_tool_info"), 

540 created_at=_convert_datetime_to_str(result.get("created_at")), 

541 updated_at=_convert_datetime_to_str(result.get("updated_at")), 

542 is_from_config=False, # This endpoint only returns DB tools 

543 ) 

544 except HTTPException as e: 

545 raise e 

546 except Exception as e: 

547 verbose_proxy_logger.exception("Error getting search tool info: %s", e) 

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

549 

550 

551class TestSearchToolConnectionRequest(BaseModel): 

552 litellm_params: dict[str, Any] 

553 

554 

555@router.post( 

556 "/search_tools/test_connection", 

557 tags=["Search Tools"], 

558 dependencies=[Depends(user_api_key_auth)], 

559) 

560async def test_search_tool_connection(request: TestSearchToolConnectionRequest): 

561 """ 

562 Test connection to a search provider with the given configuration. 

563  

564 Makes a simple test search query to verify the API key and configuration are valid. 

565 

566 Example Request: 

567 ```bash 

568 curl -X POST "http://localhost:4000/search_tools/test_connection" \\ 

569 -H "Authorization: Bearer <your_api_key>" \\ 

570 -H "Content-Type: application/json" \\ 

571 -d '{ 

572 "litellm_params": { 

573 "search_provider": "perplexity", 

574 "api_key": "sk-..." 

575 } 

576 }' 

577 ``` 

578 

579 Example Response (Success): 

580 ```json 

581 { 

582 "status": "success", 

583 "message": "Successfully connected to perplexity search provider", 

584 "test_query": "test", 

585 "results_count": 5 

586 } 

587 ``` 

588 

589 Example Response (Failure): 

590 ```json 

591 { 

592 "status": "error", 

593 "message": "Authentication failed: Invalid API key", 

594 "error_type": "AuthenticationError" 

595 } 

596 ``` 

597 """ 

598 try: 

599 from litellm.search import asearch 

600 

601 # Extract params from request 

602 litellm_params: Final = request.litellm_params 

603 search_provider: Final = litellm_params.get("search_provider") 

604 api_key: Final = litellm_params.get("api_key") 

605 api_base: Final = litellm_params.get("api_base") 

606 

607 if not search_provider: 607 ↛ 610line 607 didn't jump to line 610 because the condition on line 607 was always true

608 raise HTTPException(status_code=400, detail="search_provider is required in litellm_params") 

609 

610 verbose_proxy_logger.debug("Testing connection to search provider: %s", search_provider) 

611 

612 # Make a simple test search query with max_results=1 to minimize cost 

613 test_query: Final = "test" 

614 response: Final = await asearch( 

615 query=test_query, 

616 search_provider=search_provider, 

617 api_key=api_key, 

618 api_base=api_base, 

619 max_results=1, # Minimize results to reduce cost 

620 timeout=10.0, # 10 second timeout for test 

621 ) 

622 

623 verbose_proxy_logger.debug("Successfully tested connection to %s search provider", search_provider) 

624 

625 return { 

626 "status": "success", 

627 "message": f"Successfully connected to {search_provider} search provider", 

628 "test_query": test_query, 

629 "results_count": (len(response.results) if response and response.results else 0), 

630 } 

631 

632 except Exception as e: 

633 error_message: Final = str(e) 

634 error_type: Final = type(e).__name__ 

635 

636 verbose_proxy_logger.exception("Failed to connect to search provider: %s", error_message) 

637 

638 # Return error details in a structured format 

639 return { 

640 "status": "error", 

641 "message": error_message, 

642 "error_type": error_type, 

643 } 

644 

645 

646@router.get( 

647 "/search_tools/ui/available_providers", 

648 tags=["Search Tools"], 

649 dependencies=[Depends(user_api_key_auth)], 

650) 

651async def get_available_search_providers(): 

652 """ 

653 Get the list of available search providers with their configuration fields. 

654  

655 Auto-discovers search providers and their UI-friendly names from transformation configs. 

656 

657 Example Request: 

658 ```bash 

659 curl -X GET "http://localhost:4000/search_tools/ui/available_providers" \\ 

660 -H "Authorization: Bearer <your_api_key>" 

661 ``` 

662 

663 Example Response: 

664 ```json 

665 { 

666 "providers": [ 

667 { 

668 "provider_name": "perplexity", 

669 "ui_friendly_name": "Perplexity" 

670 }, 

671 { 

672 "provider_name": "tavily", 

673 "ui_friendly_name": "Tavily" 

674 } 

675 ] 

676 } 

677 ``` 

678 """ 

679 try: 

680 from litellm.utils import ProviderConfigManager 

681 

682 available_providers: Final = [] 

683 

684 # Auto-discover providers from SearchProviders enum 

685 for provider in SearchProviders: 

686 try: 

687 # Get the config class for this provider 

688 config = ProviderConfigManager.get_provider_search_config(provider=provider) 

689 

690 if config is not None: 690 ↛ 685line 690 didn't jump to line 685 because the condition on line 690 was always true

691 # Get the UI-friendly name from the config class 

692 ui_name = config.ui_friendly_name() 

693 

694 available_providers.append( 

695 { 

696 "provider_name": provider.value, 

697 "ui_friendly_name": ui_name, 

698 } 

699 ) 

700 except Exception as e: 

701 verbose_proxy_logger.debug("Could not get config for search provider %s: %s", provider.value, e) 

702 continue 

703 

704 return {"providers": available_providers} 

705 except Exception as e: 

706 verbose_proxy_logger.exception("Error getting available search providers: %s", e) 

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