Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/agent_endpoints/endpoints.py: 72%

424 statements  

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

1""" 

2Agent endpoints for registering + discovering agents via LiteLLM. 

3 

4Follows the A2A Spec. 

5 

61. Register an agent via POST `/v1/agents` 

72. Discover agents via GET `/v1/agents` 

83. Get specific agent via GET `/v1/agents/{agent_id}` 

9""" 

10 

11import asyncio 

12import os 

13import uuid 

14from collections.abc import Mapping, Sequence 

15from types import MappingProxyType 

16from typing import Annotated, Final, TypedDict 

17 

18from fastapi import APIRouter, Depends, HTTPException, Query, Request 

19from typing_extensions import ReadOnly, Required, assert_never 

20 

21import litellm 

22from litellm._logging import verbose_proxy_logger 

23from litellm.llms.custom_httpx.http_handler import get_async_httpx_client 

24from litellm.proxy._types import ( 

25 CommonProxyErrors, 

26 LitellmUserRoles, 

27 UserAPIKeyAuth, 

28 user_api_key_has_admin_view, 

29) 

30from litellm.proxy.a2a.agent_card import ( 

31 SUPPORTED_A2A_PROTOCOL_VERSIONS, 

32 merge_agent_card, 

33 normalize_protocol_version, 

34) 

35from litellm.proxy.agent_endpoints.agent_registry import ( 

36 AgentIdWhere, 

37 parse_agent_kill_switch, 

38 parse_agent_litellm_params, 

39 redact_sensitive_agent_litellm_params, 

40) 

41from litellm.proxy.agent_endpoints.agent_search import ( 

42 DEFAULT_AGENT_SEARCH_TOP_K, 

43 AgentSearchEmbeddingFailed, 

44 AgentSearchHits, 

45 AgentSearchNotConfigured, 

46 global_agent_search_index, 

47 search_agents, 

48) 

49from litellm.proxy.agent_endpoints.auth.agent_permission_handler import accessible_agents 

50from litellm.proxy.agent_endpoints.kill_switch import ( 

51 KillSwitchAuditLogWriter, 

52 KillSwitchHttpClient, 

53 build_kill_switch_audit_log, 

54 default_kill_switch_audit_log_writer, 

55 default_kill_switch_http_client, 

56 fire_kill_switch, 

57 redact_kill_switch, 

58) 

59from litellm.proxy.auth.user_api_key_auth import user_api_key_auth 

60from litellm.proxy.common_utils.rbac_utils import check_feature_access_for_user 

61from litellm.proxy.management_endpoints.common_daily_activity import get_daily_activity 

62from litellm.proxy.utils import get_custom_url 

63from litellm.types.agents import ( 

64 AgentCard, 

65 AgentConfig, 

66 AgentKeySummary, 

67 AgentKillSwitchConfig, 

68 AgentKillSwitchResult, 

69 AgentMakePublicResponse, 

70 AgentResponse, 

71 MakeAgentsPublicRequest, 

72 PatchAgentRequest, 

73) 

74from litellm.types.llms.custom_http import httpxSpecialProvider 

75from litellm.types.proxy.management_endpoints.common_daily_activity import ( 

76 DailySpendMetadata, 

77 SpendAnalyticsPaginatedResponse, 

78) 

79 

80 

81def _proxy_base_url(http_request: Request) -> str: 

82 """Return the proxy's public base URL, preferring PROXY_BASE_URL when set.""" 

83 return get_custom_url(str(http_request.base_url), route=None) 

84 

85 

86def _validate_protocol_version(upstream_card: AgentCard | None) -> None: 

87 """Reject an agent card pinning an unsupported A2A protocol version.""" 

88 version: Final = upstream_card.get("protocolVersion") if upstream_card else None 

89 if version is not None and normalize_protocol_version(version) is None: 

90 raise HTTPException( 

91 status_code=400, 

92 detail=( 

93 f"Unsupported protocolVersion '{version}'. " 

94 f"Supported versions: {', '.join(SUPPORTED_A2A_PROTOCOL_VERSIONS)}." 

95 ), 

96 ) 

97 

98 

99def _build_merged_agent_card( 

100 upstream_card: AgentCard | None, 

101 *, 

102 agent_id: str, 

103 http_request: Request, 

104 agent_name: str | None = None, 

105) -> dict[str, object]: 

106 """Apply the LiteLLM-fronting merge to ``upstream_card`` for ``agent_id``.""" 

107 proxy_base: Final = _proxy_base_url(http_request) 

108 _validate_protocol_version(upstream_card) 

109 # Prefer a card-supplied ``name`` (the discovery UI exposes an editable 

110 # "Name (shown to API clients)" field that flows into 

111 # ``agent_card_params.name``) over the internal ``agent_name`` identifier. 

112 # Fall back to ``agent_name`` only when the card itself has no name. 

113 card_name: Final = upstream_card.get("name") if upstream_card else None 

114 return merge_agent_card( 

115 upstream_card, 

116 proxy_url=f"{proxy_base}/a2a/{agent_id}", 

117 proxy_base_url=proxy_base, 

118 name=card_name or agent_name, 

119 ) 

120 

121 

122router: Final = APIRouter() 

123 

124 

125async def _attach_keys_to_agents(agents: Sequence[AgentResponse], prisma_client) -> None: 

126 """Attach each agent's virtual keys, derived from the key table's agent_id 

127 foreign key. Mirrors how spend is joined into the agent response so the UI 

128 never has to cross-reference a full key dump client-side. Only non-secret 

129 fields are exposed (alias, masked key_name, hashed token).""" 

130 from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry 

131 

132 agent_ids: Final = tuple( 

133 alias_id for agent in agents for alias_id in global_agent_registry.ids_for_agent(agent.agent_id) 

134 ) 

135 if not agent_ids: 135 ↛ 136line 135 didn't jump to line 136 because the condition on line 135 was never true

136 return 

137 key_rows: Final = await prisma_client.db.litellm_verificationtoken.find_many( 

138 where={"agent_id": {"in": agent_ids}}, 

139 ) 

140 keys_by_agent: Final[dict[str, list[AgentKeySummary]]] = {} 

141 for row in key_rows: 141 ↛ 142line 141 didn't jump to line 142 because the loop on line 141 never started

142 keys_by_agent.setdefault(row.agent_id, []).append( 

143 AgentKeySummary( 

144 token=row.token, 

145 key_alias=row.key_alias, 

146 key_name=row.key_name, 

147 ) 

148 ) 

149 for agent in agents: 

150 matched_keys = [ 

151 key_summary 

152 for alias_id in global_agent_registry.ids_for_agent(agent.agent_id) 

153 for key_summary in keys_by_agent.get(alias_id) or () 

154 ] 

155 agent.keys = matched_keys or None 

156 

157 

158def _redact_agent_litellm_params_dict( 

159 litellm_params: Mapping[str, object], 

160) -> dict[str, object]: # mutable-ok: AgentResponse.litellm_params is declared as a plain dict, not Mapping 

161 """Type-narrowing wrapper: a dict in always yields a dict back from 

162 ``redact_sensitive_agent_litellm_params``, which the function's general 

163 (possible-JSON-string, possibly-None) signature can't express.""" 

164 return dict( # mutable-ok: AgentResponse.litellm_params is declared as a plain dict, not Mapping 

165 parse_agent_litellm_params(redact_sensitive_agent_litellm_params(litellm_params)) 

166 ) 

167 

168 

169def _redact_sensitive_agent_fields( 

170 agents: Sequence[AgentResponse], 

171 *, 

172 is_admin: bool, 

173) -> list[AgentResponse]: 

174 """ 

175 Return copies of the given agents with credential-bearing litellm_params 

176 values and kill-switch auth secrets replaced by a fixed marker (never 

177 returned to ANY caller, admin included) and, for non-admin callers, 

178 virtual-key, header and kill-switch fields stripped entirely. The original 

179 objects are not modified. 

180 """ 

181 redacted: Final[list[AgentResponse]] = [] 

182 for agent in agents: 

183 copy = agent.model_copy(deep=True) 

184 if not is_admin: 184 ↛ 185line 184 didn't jump to line 185 because the condition on line 184 was never true

185 copy.static_headers = None 

186 copy.extra_headers = None 

187 copy.keys = None 

188 copy.kill_switch = None 

189 if copy.litellm_params: 

190 copy.litellm_params = _redact_agent_litellm_params_dict(copy.litellm_params) 

191 copy.kill_switch = redact_kill_switch(copy.kill_switch) 

192 redacted.append(copy) 

193 return redacted 

194 

195 

196def _check_agent_management_permission(user_api_key_dict: UserAPIKeyAuth) -> None: 

197 """ 

198 Raises HTTP 403 if the caller does not have permission to create, update, 

199 or delete agents. Only PROXY_ADMIN users are allowed to perform these 

200 write operations. 

201 """ 

202 if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: 202 ↛ 203line 202 didn't jump to line 203 because the condition on line 202 was never true

203 raise HTTPException( 

204 status_code=403, 

205 detail={ 

206 "error": f"Only proxy admins can create, update, or delete agents. Your role={user_api_key_dict.user_role}" 

207 }, 

208 ) 

209 

210 

211AGENT_HEALTH_CHECK_TIMEOUT_SECONDS: Final = float(os.environ.get("LITELLM_AGENT_HEALTH_CHECK_TIMEOUT", "5.0")) 

212AGENT_HEALTH_CHECK_GATHER_TIMEOUT_SECONDS = float(os.environ.get("LITELLM_AGENT_HEALTH_CHECK_GATHER_TIMEOUT", "30.0")) 

213 

214 

215class _AgentHealthResult(TypedDict, total=False): 

216 agent_id: Required[str] 

217 healthy: Required[bool] 

218 error: str 

219 

220 

221async def _check_agent_url_health( 

222 agent: AgentResponse, 

223) -> _AgentHealthResult: 

224 """ 

225 Perform a GET request against the agent's URL and return the health result. 

226 

227 Returns a dict with ``agent_id``, ``healthy`` (bool), and an optional 

228 ``error`` message. 

229 """ 

230 url: Final = (agent.agent_card_params or {}).get("url") 

231 if not url: 231 ↛ 232line 231 didn't jump to line 232 because the condition on line 231 was never true

232 return {"agent_id": agent.agent_id, "healthy": True} 

233 

234 try: 

235 client: Final = get_async_httpx_client( 

236 llm_provider=httpxSpecialProvider.AgentHealthCheck, 

237 params={"timeout": AGENT_HEALTH_CHECK_TIMEOUT_SECONDS}, 

238 ) 

239 response: Final = await client.get(url) 

240 if response.status_code >= 500: 240 ↛ 241line 240 didn't jump to line 241 because the condition on line 240 was never true

241 return { 

242 "agent_id": agent.agent_id, 

243 "healthy": False, 

244 "error": f"HTTP {response.status_code}", 

245 } 

246 return {"agent_id": agent.agent_id, "healthy": True} 

247 except Exception as exc: 

248 return { 

249 "agent_id": agent.agent_id, 

250 "healthy": False, 

251 "error": str(exc), 

252 } 

253 

254 

255class _AgentSearchErrorDetail(TypedDict): 

256 error: ReadOnly[str] 

257 message: ReadOnly[str] 

258 

259 

260def _agent_search_error(status_code: int, error: str, message: str) -> HTTPException: 

261 detail: Final[_AgentSearchErrorDetail] = {"error": error, "message": message} 

262 return HTTPException(status_code=status_code, detail=detail) 

263 

264 

265async def _rank_agents_by_query( 

266 query: str, agents: Sequence[AgentResponse], top_k: int, user_api_key_dict: UserAPIKeyAuth 

267) -> tuple[AgentResponse, ...]: 

268 from litellm.proxy.proxy_server import llm_router, proxy_logging_obj 

269 

270 outcome: Final = await search_agents( 

271 query=query, 

272 agents=agents, 

273 top_k=top_k, 

274 router=llm_router, 

275 embedding_model=litellm.agent_search_embedding_model, 

276 index=global_agent_search_index, 

277 user_api_key_dict=user_api_key_dict, 

278 proxy_logging_obj=proxy_logging_obj, 

279 ) 

280 match outcome: 

281 case AgentSearchHits(hits): 281 ↛ 282line 281 didn't jump to line 282 because the pattern on line 281 never matched

282 return tuple(hit.agent.model_copy(update=MappingProxyType({"search_score": hit.score})) for hit in hits) 

283 case AgentSearchNotConfigured(reason): 283 ↛ 285line 283 didn't jump to line 285 because the pattern on line 283 always matched

284 raise _agent_search_error(400, "agent_search_not_configured", reason) 

285 case AgentSearchEmbeddingFailed(reason): 

286 raise _agent_search_error(503, "agent_search_unavailable", reason) 

287 case _: 

288 assert_never(outcome) 

289 

290 

291@router.get( 

292 "/v1/agents", 

293 tags=["[beta] A2A Agents"], 

294 dependencies=[Depends(user_api_key_auth)], 

295 response_model=list[AgentResponse], 

296) 

297async def get_agents( 

298 request: Request, 

299 health_check: bool = Query( 

300 False, 

301 description="When true, performs a GET request to each agent's URL. Agents with reachable URLs (HTTP status < 500) and agents without a URL are returned; unreachable agents are filtered out.", 

302 ), 

303 query: Annotated[ 

304 str | None, 

305 Query( 

306 min_length=1, 

307 description="Describe the task in natural language to rank the agents you can reach by semantic similarity over their name, description, and skills. Each result carries a search_score. Requires litellm_settings.agent_search_embedding_model.", 

308 ), 

309 ] = None, 

310 top_k: Annotated[ 

311 int, 

312 Query(ge=1, le=100, description="With query: the maximum number of ranked agents to return."), 

313 ] = DEFAULT_AGENT_SEARCH_TOP_K, 

314 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # Used for auth 

315): 

316 """ 

317 Example usage: 

318 ``` 

319 curl -X GET "http://localhost:4000/v1/agents" \ 

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

321 -H "Authorization: Bearer your-key" \ 

322 ``` 

323 

324 Pass `?health_check=true` to filter out agents whose URL is unreachable: 

325 ``` 

326 curl -X GET "http://localhost:4000/v1/agents?health_check=true" \ 

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

328 -H "Authorization: Bearer your-key" \ 

329 ``` 

330 

331 Pass `?query=<task>` to get the best matching agents ranked by semantic similarity: 

332 ``` 

333 curl -X GET "http://localhost:4000/v1/agents?query=translate+a+PDF+document&top_k=5" \ 

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

335 -H "Authorization: Bearer your-key" \ 

336 ``` 

337 

338 Returns: List[AgentResponse] 

339 

340 """ 

341 await check_feature_access_for_user(user_api_key_dict, "agents") 

342 

343 from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry 

344 

345 try: 

346 returned_agents: Sequence[AgentResponse] = await accessible_agents(user_api_key_dict) 

347 

348 # Fetch current spend from DB for all returned agents 

349 from litellm.proxy.proxy_server import prisma_client 

350 

351 if prisma_client is not None: 351 ↛ 373line 351 didn't jump to line 373 because the condition on line 351 was always true

352 agent_ids: Final = tuple( 

353 alias_id 

354 for agent in returned_agents 

355 for alias_id in global_agent_registry.ids_for_agent(agent.agent_id) 

356 ) 

357 if agent_ids: 

358 db_agents: Final = await agents_table(prisma_client).find_many( 

359 where={"agent_id": {"in": agent_ids}}, 

360 ) 

361 spend_map: Final = {a.agent_id: a.spend for a in db_agents} 

362 for agent in returned_agents: 

363 matched_spends = tuple( 

364 spend_map[alias_id] 

365 for alias_id in global_agent_registry.ids_for_agent(agent.agent_id) 

366 if alias_id in spend_map 

367 ) 

368 if matched_spends: 368 ↛ 362line 368 didn't jump to line 362 because the condition on line 368 was always true

369 agent.spend = sum(matched_spends) 

370 await _attach_keys_to_agents(returned_agents, prisma_client) 

371 

372 # add is_public field to each agent - we do it this way, to allow setting config agents as public 

373 for agent in returned_agents: 

374 if agent.litellm_params is None: 374 ↛ 375line 374 didn't jump to line 375 because the condition on line 374 was never true

375 agent.litellm_params = {} 

376 agent.litellm_params["is_public"] = litellm.public_agent_groups is not None and not ( 

377 global_agent_registry.ids_for_agent(agent.agent_id).isdisjoint(litellm.public_agent_groups) 

378 ) 

379 

380 # litellm_params secrets are always redacted; keys/headers stay 

381 # admin-only. 

382 is_admin: Final = ( 

383 user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN 

384 or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value 

385 ) 

386 returned_agents = _redact_sensitive_agent_fields(returned_agents, is_admin=is_admin) 

387 

388 if health_check: 

389 agents_with_url: Final = [agent for agent in returned_agents if (agent.agent_card_params or {}).get("url")] 

390 agents_without_url = [agent for agent in returned_agents if not (agent.agent_card_params or {}).get("url")] 

391 try: 

392 health_results: Sequence[_AgentHealthResult] = await asyncio.wait_for( 

393 asyncio.gather(*[_check_agent_url_health(agent) for agent in agents_with_url]), 

394 timeout=AGENT_HEALTH_CHECK_GATHER_TIMEOUT_SECONDS, 

395 ) 

396 except asyncio.TimeoutError: 

397 verbose_proxy_logger.warning( 

398 "Agent health check gather timed out after %s seconds", 

399 AGENT_HEALTH_CHECK_GATHER_TIMEOUT_SECONDS, 

400 ) 

401 health_results = [ 

402 { 

403 "agent_id": agent.agent_id, 

404 "healthy": False, 

405 "error": "Health check timed out", 

406 } 

407 for agent in agents_with_url 

408 ] 

409 healthy_ids: Final = {result["agent_id"] for result in health_results if result["healthy"]} 

410 returned_agents = [agent for agent in agents_with_url if agent.agent_id in healthy_ids] + agents_without_url 

411 

412 if query is None: 

413 return returned_agents 

414 return await _rank_agents_by_query(query, returned_agents, top_k, user_api_key_dict) 

415 except HTTPException: 

416 raise 

417 except Exception as e: 

418 verbose_proxy_logger.exception("litellm.proxy.agent_endpoints.get_agents(): Exception occurred - %s", e) 

419 raise HTTPException(status_code=500, detail={"error": f"Internal server error: {e}"}) 

420 

421 

422#### CRUD ENDPOINTS FOR AGENTS #### 

423 

424from litellm.proxy.agent_endpoints.agent_registry import ( 

425 agents_table, 

426) 

427from litellm.proxy.agent_endpoints.agent_registry import ( 

428 global_agent_registry as AGENT_REGISTRY, 

429) 

430 

431 

432@router.post( 

433 "/v1/agents", 

434 tags=["[beta] A2A Agents"], 

435 dependencies=[Depends(user_api_key_auth)], 

436 response_model=AgentResponse, 

437) 

438async def create_agent( 

439 request: AgentConfig, 

440 http_request: Request, 

441 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

442): 

443 """ 

444 Create a new agent 

445 

446 Example Request: 

447 ```bash 

448 curl -X POST "http://localhost:4000/v1/agents" \\ 

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

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

451 -d '{ 

452 "agent_name": "my-custom-agent", 

453 "agent_card_params": { 

454 "protocolVersion": "1.0", 

455 "name": "Hello World Agent", 

456 "description": "Just a hello world agent", 

457 "url": "http://localhost:9999/", 

458 "version": "1.0.0", 

459 "defaultInputModes": ["text"], 

460 "defaultOutputModes": ["text"], 

461 "capabilities": { 

462 "streaming": true 

463 }, 

464 "skills": [ 

465 { 

466 "id": "hello_world", 

467 "name": "Returns hello world", 

468 "description": "just returns hello world", 

469 "tags": ["hello world"], 

470 "examples": ["hi", "hello world"] 

471 } 

472 ] 

473 }, 

474 "litellm_params": { 

475 "make_public": true 

476 } 

477 }' 

478 ``` 

479 """ 

480 await check_feature_access_for_user(user_api_key_dict, "agents") 

481 

482 from litellm.proxy.proxy_server import prisma_client 

483 

484 _check_agent_management_permission(user_api_key_dict) 

485 

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

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

488 

489 try: 

490 # Get the user ID from the API key auth 

491 created_by: Final = user_api_key_dict.user_id or "unknown" 

492 

493 # check for naming conflicts 

494 existing_agent: Final = AGENT_REGISTRY.get_agent_by_name(agent_name=request.get("agent_name")) 

495 if existing_agent is not None: 

496 raise HTTPException( 

497 status_code=400, 

498 detail=f"Agent with name {request.get('agent_name')} already exists", 

499 ) 

500 

501 # Apply the LiteLLM-fronting merge only when the admin actually 

502 # provided an agent card. Plain chat/LLM agents register without 

503 # ``agent_card_params``, and synthesising a default A2A card for them 

504 # would advertise capabilities (``supportedInterfaces``, security 

505 # schemes, default skills) the agent doesn't actually expose. 

506 upstream_card: Final = request.get("agent_card_params") 

507 agent_to_create: AgentConfig = request 

508 new_agent_id: str | None = None 

509 if upstream_card is not None: 509 ↛ 521line 509 didn't jump to line 521 because the condition on line 509 was always true

510 # Pre-generate the agent_id so the merged card can reference it 

511 # in ``supportedInterfaces`` before the DB row exists. 

512 new_agent_id = str(uuid.uuid4()) 

513 merged_card: Final = _build_merged_agent_card( 

514 upstream_card, 

515 agent_id=new_agent_id, 

516 http_request=http_request, 

517 agent_name=request.get("agent_name"), 

518 ) 

519 agent_to_create = {**request, "agent_card_params": merged_card} 

520 

521 result: Final = await AGENT_REGISTRY.add_agent_to_db( 

522 agent=agent_to_create, 

523 prisma_client=prisma_client, 

524 created_by=created_by, 

525 agent_id=new_agent_id, 

526 ) 

527 

528 agent_name: Final = result.agent_name 

529 agent_id: Final = result.agent_id 

530 

531 # Also register in memory 

532 try: 

533 AGENT_REGISTRY.register_agent(agent_config=result) 

534 verbose_proxy_logger.info("Successfully registered agent '%s' (ID: %s) in memory", agent_name, agent_id) 

535 except Exception as reg_error: 

536 verbose_proxy_logger.warning( 

537 "Failed to register agent '%s' (ID: %s) in memory: %s", agent_name, agent_id, reg_error 

538 ) 

539 

540 # The caller is a proxy admin (enforced above); litellm_params 

541 # secrets are still never echoed back in the response. 

542 return _redact_sensitive_agent_fields((result,), is_admin=True)[0] 

543 

544 except HTTPException: 

545 raise 

546 except Exception as e: 

547 verbose_proxy_logger.exception("Error adding agent to db: %s", e) 

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

549 

550 

551@router.get( 

552 "/v1/agents/{agent_id}", 

553 tags=["[beta] A2A Agents"], 

554 dependencies=[Depends(user_api_key_auth)], 

555 response_model=AgentResponse, 

556) 

557async def get_agent_by_id( 

558 agent_id: str, 

559 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

560): 

561 """ 

562 Get a specific agent by ID 

563 

564 Example Request: 

565 ```bash 

566 curl -X GET "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000" \\ 

567 -H "Authorization: Bearer <your_api_key>" 

568 ``` 

569 """ 

570 await check_feature_access_for_user(user_api_key_dict, "agents") 

571 

572 if not user_api_key_has_admin_view(user_api_key_dict): 572 ↛ 573line 572 didn't jump to line 573 because the condition on line 572 was never true

573 from litellm.proxy.agent_endpoints.auth.agent_permission_handler import ( 

574 AgentRequestHandler, 

575 ) 

576 

577 is_allowed = await AgentRequestHandler.is_agent_allowed(agent_id=agent_id, user_api_key_auth=user_api_key_dict) 

578 if not is_allowed: 

579 raise HTTPException( 

580 status_code=403, 

581 detail=f"Agent '{agent_id}' is not allowed for your key/team. Contact proxy admin for access.", 

582 ) 

583 

584 from litellm.proxy.proxy_server import prisma_client 

585 

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

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

588 

589 try: 

590 agent = AGENT_REGISTRY.get_agent_by_id(agent_id=agent_id) 

591 if agent is None: 

592 agent_row: Final = await agents_table(prisma_client).find_unique( 

593 where={"agent_id": agent_id}, 

594 include={"object_permission": True}, 

595 ) 

596 if agent_row is not None: 596 ↛ 597line 596 didn't jump to line 597 because the condition on line 596 was never true

597 agent_dict: Final = agent_row.model_dump() 

598 if agent_row.object_permission is not None: 

599 try: 

600 agent_dict["object_permission"] = agent_row.object_permission.model_dump() 

601 except Exception: 

602 agent_dict["object_permission"] = agent_row.object_permission.dict() 

603 agent = AgentResponse(**agent_dict) 

604 else: 

605 # Agent found in memory — refresh spend from DB 

606 db_row: Final = await agents_table(prisma_client).find_unique(where={"agent_id": agent_id}) 

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

608 agent.spend = db_row.spend 

609 

610 if agent is None: 

611 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found") 

612 

613 await _attach_keys_to_agents([agent], prisma_client) 

614 

615 # litellm_params secrets are always redacted; keys/headers stay 

616 # admin-only. 

617 is_admin = ( 

618 user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN 

619 or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value 

620 ) 

621 agent = _redact_sensitive_agent_fields((agent,), is_admin=is_admin)[0] 

622 

623 return agent 

624 except HTTPException: 

625 raise 

626 except Exception as e: 

627 verbose_proxy_logger.exception("Error getting agent from db: %s", e) 

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

629 

630 

631@router.put( 

632 "/v1/agents/{agent_id}", 

633 tags=["[beta] A2A Agents"], 

634 dependencies=[Depends(user_api_key_auth)], 

635 response_model=AgentResponse, 

636) 

637async def update_agent( 

638 agent_id: str, 

639 request: AgentConfig, 

640 http_request: Request, 

641 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

642): 

643 """ 

644 Update an existing agent 

645 

646 Example Request: 

647 ```bash 

648 curl -X PUT "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000" \\ 

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

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

651 -d '{ 

652 "agent_name": "updated-agent", 

653 "agent_card_params": { 

654 "protocolVersion": "1.0", 

655 "name": "Updated Agent", 

656 "description": "Updated description", 

657 "url": "http://localhost:9999/", 

658 "version": "1.1.0", 

659 "defaultInputModes": ["text"], 

660 "defaultOutputModes": ["text"], 

661 "capabilities": { 

662 "streaming": true 

663 }, 

664 "skills": [] 

665 }, 

666 "litellm_params": { 

667 "make_public": false 

668 } 

669 }' 

670 ``` 

671 """ 

672 await check_feature_access_for_user(user_api_key_dict, "agents") 

673 

674 from litellm.proxy.proxy_server import prisma_client 

675 

676 _check_agent_management_permission(user_api_key_dict) 

677 

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

679 raise HTTPException(status_code=500, detail=CommonProxyErrors.db_not_connected_error.value) 

680 

681 try: 

682 # Check if agent exists 

683 existing_agent = await agents_table(prisma_client).find_unique(where={"agent_id": agent_id}) 

684 if existing_agent is not None: 

685 existing_agent = dict(existing_agent) 

686 

687 if existing_agent is None: 

688 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found") 

689 

690 # Get the user ID from the API key auth 

691 updated_by: Final = user_api_key_dict.user_id or "unknown" 

692 

693 # Re-apply the LiteLLM-fronting merge — an update is a re-registration, 

694 # so any new upstream card the admin pasted must go through the same 

695 # transformation as initial create. Plain agents without an 

696 # ``agent_card_params`` skip the merge so we don't synthesise an A2A 

697 # card for them. 

698 upstream_card: Final = request.get("agent_card_params") 

699 agent_to_update: AgentConfig = request 

700 if upstream_card is not None: 700 ↛ 709line 700 didn't jump to line 709 because the condition on line 700 was always true

701 merged_card: Final = _build_merged_agent_card( 

702 upstream_card, 

703 agent_id=agent_id, 

704 http_request=http_request, 

705 agent_name=request.get("agent_name"), 

706 ) 

707 agent_to_update = {**request, "agent_card_params": merged_card} 

708 

709 result: Final = await AGENT_REGISTRY.update_agent_in_db( 

710 agent_id=agent_id, 

711 agent=agent_to_update, 

712 prisma_client=prisma_client, 

713 updated_by=updated_by, 

714 ) 

715 

716 # deregister in memory 

717 AGENT_REGISTRY.deregister_agent(agent_name=existing_agent.get("agent_name")) 

718 # register in memory 

719 AGENT_REGISTRY.register_agent(agent_config=result) 

720 

721 verbose_proxy_logger.info( 

722 "Successfully updated agent '%s' (ID: %s) in memory", existing_agent.get("agent_name"), agent_id 

723 ) 

724 

725 return _redact_sensitive_agent_fields((result,), is_admin=True)[0] 

726 except HTTPException: 

727 raise 

728 except Exception as e: 

729 verbose_proxy_logger.exception("Error updating agent: %s", e) 

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

731 

732 

733@router.patch( 

734 "/v1/agents/{agent_id}", 

735 tags=["[beta] A2A Agents"], 

736 dependencies=[Depends(user_api_key_auth)], 

737 response_model=AgentResponse, 

738) 

739async def patch_agent( 

740 agent_id: str, 

741 request: PatchAgentRequest, 

742 http_request: Request, 

743 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

744): 

745 """ 

746 Update an existing agent 

747 

748 Example Request: 

749 ```bash 

750 curl -X PATCH "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000" \\ 

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

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

753 -d '{ 

754 "agent_name": "updated-agent", 

755 "agent_card_params": { 

756 "protocolVersion": "1.0", 

757 "name": "Updated Agent", 

758 "description": "Updated description", 

759 "url": "http://localhost:9999/", 

760 "version": "1.1.0", 

761 "defaultInputModes": ["text"], 

762 "defaultOutputModes": ["text"], 

763 "capabilities": { 

764 "streaming": true 

765 }, 

766 "skills": [] 

767 }, 

768 "litellm_params": { 

769 "make_public": false 

770 } 

771 }' 

772 ``` 

773 """ 

774 await check_feature_access_for_user(user_api_key_dict, "agents") 

775 

776 from litellm.proxy.proxy_server import prisma_client 

777 

778 _check_agent_management_permission(user_api_key_dict) 

779 

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

781 raise HTTPException(status_code=500, detail=CommonProxyErrors.db_not_connected_error.value) 

782 

783 try: 

784 # Check if agent exists 

785 existing_agent = await agents_table(prisma_client).find_unique(where={"agent_id": agent_id}) 

786 if existing_agent is not None: 

787 existing_agent = dict(existing_agent) 

788 

789 if existing_agent is None: 

790 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found") 

791 

792 # Get the user ID from the API key auth 

793 updated_by: Final = user_api_key_dict.user_id or "unknown" 

794 

795 # Re-merge only when the patch actually touches agent_card_params; a 

796 # patch updating just litellm_params/rate limits (``agent_card_params`` 

797 # omitted) shouldn't rewrite the stored card. An explicitly provided 

798 # ``agent_card_params`` — even an empty dict — still goes through the 

799 # merge so LiteLLM applies its security schemes and supported 

800 # interfaces instead of storing a bare card. 

801 patch_payload: PatchAgentRequest = request 

802 upstream_card: Final = request.get("agent_card_params") 

803 if upstream_card is not None: 

804 merged_card: Final = _build_merged_agent_card( 

805 upstream_card, 

806 agent_id=agent_id, 

807 http_request=http_request, 

808 agent_name=request.get("agent_name"), 

809 ) 

810 patch_payload = {**request, "agent_card_params": merged_card} 

811 

812 result: Final = await AGENT_REGISTRY.patch_agent_in_db( 

813 agent_id=agent_id, 

814 agent=patch_payload, 

815 prisma_client=prisma_client, 

816 updated_by=updated_by, 

817 ) 

818 

819 # deregister in memory 

820 AGENT_REGISTRY.deregister_agent(agent_name=existing_agent.get("agent_name")) 

821 # register in memory 

822 AGENT_REGISTRY.register_agent(agent_config=result) 

823 

824 verbose_proxy_logger.info( 

825 "Successfully updated agent '%s' (ID: %s) in memory", existing_agent.get("agent_name"), agent_id 

826 ) 

827 

828 return _redact_sensitive_agent_fields((result,), is_admin=True)[0] 

829 except HTTPException: 

830 raise 

831 except Exception as e: 

832 verbose_proxy_logger.exception("Error updating agent: %s", e) 

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

834 

835 

836@router.delete( 

837 "/v1/agents/{agent_id}", 

838 tags=["Agents"], 

839 dependencies=[Depends(user_api_key_auth)], 

840) 

841async def delete_agent( 

842 agent_id: str, 

843 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

844): 

845 """ 

846 Delete an agent 

847 

848 Example Request: 

849 ```bash 

850 curl -X DELETE "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000" \\ 

851 -H "Authorization: Bearer <your_api_key>" 

852 ``` 

853 

854 Example Response: 

855 ```json 

856 { 

857 "message": "Agent 123e4567-e89b-12d3-a456-426614174000 deleted successfully" 

858 } 

859 ``` 

860 """ 

861 await check_feature_access_for_user(user_api_key_dict, "agents") 

862 

863 from litellm.proxy.proxy_server import prisma_client 

864 

865 _check_agent_management_permission(user_api_key_dict) 

866 

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

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

869 

870 try: 

871 # Check if agent exists 

872 existing_agent = await agents_table(prisma_client).find_unique(where={"agent_id": agent_id}) 

873 if existing_agent is not None: 873 ↛ 876line 873 didn't jump to line 876 because the condition on line 873 was always true

874 existing_agent = dict[str, object](existing_agent) 

875 

876 if existing_agent is None: 876 ↛ 877line 876 didn't jump to line 877 because the condition on line 876 was never true

877 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found in DB.") 

878 

879 await AGENT_REGISTRY.delete_agent_from_db(agent_id=agent_id, prisma_client=prisma_client) 

880 

881 AGENT_REGISTRY.deregister_agent(agent_name=existing_agent.get("agent_name")) 

882 

883 return {"message": f"Agent {agent_id} deleted successfully"} 

884 except HTTPException: 

885 raise 

886 except Exception as e: 

887 verbose_proxy_logger.exception("Error deleting agent: %s", e) 

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

889 

890 

891@router.post( 

892 "/v1/agents/{agent_id}/kill_switch", 

893 tags=["[beta] A2A Agents"], # mutable-ok: fastapi types tags as list[str | Enum] 

894 dependencies=(Depends(user_api_key_auth),), 

895 response_model=AgentKillSwitchResult, 

896) 

897async def trigger_agent_kill_switch( 

898 agent_id: str, 

899 user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], 

900 http_client: Annotated[KillSwitchHttpClient, Depends(default_kill_switch_http_client)], 

901 audit_log_writer: Annotated[KillSwitchAuditLogWriter, Depends(default_kill_switch_audit_log_writer)], 

902): 

903 """ 

904 Fire the agent's configured kill switch webhook. Proxy admin only. 

905 

906 LiteLLM only makes the configured HTTP call and reports what came back; it 

907 does not change the agent's state in LiteLLM. Returns 200 when the webhook 

908 answered 2xx, 502 with the same result body otherwise. Every attempt is 

909 written to the audit log as a `kill_switch_fired` row against the agent. 

910 

911 Example Request: 

912 ```bash 

913 curl -X POST "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000/kill_switch" \\ 

914 -H "Authorization: Bearer <your_api_key>" 

915 ``` 

916 """ 

917 from litellm.proxy.proxy_server import litellm_proxy_admin_name 

918 

919 await check_feature_access_for_user(user_api_key_dict, "agents") 

920 _check_agent_management_permission(user_api_key_dict) 

921 

922 resolved: Final = await _resolve_agent_kill_switch(agent_id) 

923 if resolved is None: 

924 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found") 

925 resolved_agent_id, config = resolved 

926 if config is None: 926 ↛ 929line 926 didn't jump to line 929 because the condition on line 926 was always true

927 raise HTTPException(status_code=400, detail=f"Agent with ID {agent_id} has no kill_switch configured") 

928 

929 result: Final = await fire_kill_switch(agent_id=resolved_agent_id, config=config, http_client=http_client) 

930 await audit_log_writer( 

931 build_kill_switch_audit_log( 

932 result=result, 

933 user_api_key_dict=user_api_key_dict, 

934 litellm_proxy_admin_name=litellm_proxy_admin_name, 

935 ) 

936 ) 

937 if not result.succeeded: 

938 raise HTTPException(status_code=502, detail=result.model_dump()) 

939 return result 

940 

941 

942async def _resolve_agent_kill_switch(agent_id: str) -> tuple[str, AgentKillSwitchConfig | None] | None: 

943 """The DB row wins over this replica's in-memory registry so a trigger never fires a webhook another 

944 replica has since changed; config.yaml agents have no row and fall back to the registry.""" 

945 from litellm.proxy.proxy_server import prisma_client 

946 

947 if prisma_client is not None: 947 ↛ 953line 947 didn't jump to line 953 because the condition on line 947 was always true

948 where: Final[AgentIdWhere] = {"agent_id": agent_id} 

949 row: Final = await agents_table(prisma_client).find_unique(where=where) 

950 if row is not None: 

951 return row.agent_id, parse_agent_kill_switch(row.kill_switch) 

952 

953 agent: Final = AGENT_REGISTRY.get_agent_by_id(agent_id=agent_id) 

954 if agent is None: 954 ↛ 956line 954 didn't jump to line 956 because the condition on line 954 was always true

955 return None 

956 return agent.agent_id, agent.kill_switch 

957 

958 

959@router.post( 

960 "/v1/agents/{agent_id}/make_public", 

961 tags=["[beta] A2A Agents"], 

962 dependencies=[Depends(user_api_key_auth)], 

963 response_model=AgentMakePublicResponse, 

964) 

965async def make_agent_public( 

966 agent_id: str, 

967 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

968): 

969 """ 

970 Make an agent publicly discoverable 

971 

972 Example Request: 

973 ```bash 

974 curl -X POST "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000/make_public" \\ 

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

976 -H "Content-Type: application/json" 

977 ``` 

978 

979 Example Response: 

980 ```json 

981 { 

982 "agent_id": "123e4567-e89b-12d3-a456-426614174000", 

983 "agent_name": "my-custom-agent", 

984 "litellm_params": { 

985 "make_public": true 

986 }, 

987 "agent_card_params": {...}, 

988 "created_at": "2025-11-15T10:30:00Z", 

989 "updated_at": "2025-11-15T10:35:00Z", 

990 "created_by": "user123", 

991 "updated_by": "user123" 

992 } 

993 ``` 

994 """ 

995 from litellm.proxy.proxy_server import prisma_client 

996 

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

998 raise HTTPException(status_code=500, detail=CommonProxyErrors.db_not_connected_error.value) 

999 

1000 try: 

1001 # Update the public model groups 

1002 import litellm 

1003 from litellm.proxy.agent_endpoints.agent_registry import ( 

1004 global_agent_registry as AGENT_REGISTRY, 

1005 ) 

1006 from litellm.proxy.proxy_server import proxy_config 

1007 

1008 # Check if user has admin permissions 

1009 if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: 1009 ↛ 1010line 1009 didn't jump to line 1010 because the condition on line 1009 was never true

1010 raise HTTPException( 

1011 status_code=403, 

1012 detail={ 

1013 "error": f"Only proxy admins can update public model groups. Your role={user_api_key_dict.user_role}" 

1014 }, 

1015 ) 

1016 

1017 agent = AGENT_REGISTRY.get_agent_by_id(agent_id=agent_id) 

1018 if agent is None: 1018 ↛ 1027line 1018 didn't jump to line 1027 because the condition on line 1018 was always true

1019 # check if agent exists in DB 

1020 agent = await agents_table(prisma_client).find_unique(where={"agent_id": agent_id}) 

1021 if agent is not None: 1021 ↛ 1022line 1021 didn't jump to line 1022 because the condition on line 1021 was never true

1022 agent = AgentResponse(**agent.model_dump()) 

1023 

1024 if agent is None: 1024 ↛ 1027line 1024 didn't jump to line 1027 because the condition on line 1024 was always true

1025 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found") 

1026 

1027 config: Final = await proxy_config.get_config() 

1028 

1029 current_public_agent_groups: Final = list(litellm.public_agent_groups or []) 

1030 if not AGENT_REGISTRY.ids_for_agent(agent.agent_id).isdisjoint(current_public_agent_groups): 

1031 raise HTTPException( 

1032 status_code=400, 

1033 detail=f"Agent with name {agent.agent_name} already in public agent groups", 

1034 ) 

1035 updated_public_agent_groups: Final = [*current_public_agent_groups, agent.agent_id] 

1036 

1037 if "litellm_settings" not in config or config["litellm_settings"] is None: 

1038 config["litellm_settings"] = {} 

1039 

1040 config["litellm_settings"]["public_agent_groups"] = updated_public_agent_groups 

1041 

1042 await proxy_config.save_config(new_config=config) 

1043 

1044 litellm.public_agent_groups = updated_public_agent_groups 

1045 

1046 verbose_proxy_logger.debug( 

1047 "Updated public agent groups to: %s by user: %s", updated_public_agent_groups, user_api_key_dict.user_id 

1048 ) 

1049 

1050 return { 

1051 "message": "Successfully updated public agent groups", 

1052 "public_agent_groups": updated_public_agent_groups, 

1053 "updated_by": user_api_key_dict.user_id, 

1054 } 

1055 except HTTPException: 

1056 raise 

1057 except Exception as e: 

1058 verbose_proxy_logger.exception("Error making agent public: %s", e) 

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

1060 

1061 

1062@router.post( 

1063 "/v1/agents/make_public", 

1064 tags=["[beta] A2A Agents"], 

1065 dependencies=[Depends(user_api_key_auth)], 

1066 response_model=AgentMakePublicResponse, 

1067) 

1068async def make_agents_public( 

1069 request: MakeAgentsPublicRequest, 

1070 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

1071): 

1072 """ 

1073 Make multiple agents publicly discoverable 

1074 

1075 Example Request: 

1076 ```bash 

1077 curl -X POST "http://localhost:4000/v1/agents/make_public" \\ 

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

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

1080 -d '{ 

1081 "agent_ids": ["123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001"] 

1082 }' 

1083 ``` 

1084 

1085 Example Response: 

1086 ```json 

1087 { 

1088 "agent_id": "123e4567-e89b-12d3-a456-426614174000", 

1089 "agent_name": "my-custom-agent", 

1090 "litellm_params": { 

1091 "make_public": true 

1092 }, 

1093 "agent_card_params": {...}, 

1094 "created_at": "2025-11-15T10:30:00Z", 

1095 "updated_at": "2025-11-15T10:35:00Z", 

1096 "created_by": "user123", 

1097 "updated_by": "user123" 

1098 } 

1099 ``` 

1100 """ 

1101 from litellm.proxy.proxy_server import prisma_client 

1102 

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

1104 raise HTTPException(status_code=500, detail=CommonProxyErrors.db_not_connected_error.value) 

1105 

1106 try: 

1107 # Update the public model groups 

1108 import litellm 

1109 from litellm.proxy.agent_endpoints.agent_registry import ( 

1110 global_agent_registry as AGENT_REGISTRY, 

1111 ) 

1112 from litellm.proxy.proxy_server import proxy_config 

1113 

1114 # Load existing config 

1115 config: Final = await proxy_config.get_config() 

1116 # Check if user has admin permissions 

1117 if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: 1117 ↛ 1118line 1117 didn't jump to line 1118 because the condition on line 1117 was never true

1118 raise HTTPException( 

1119 status_code=403, 

1120 detail={ 

1121 "error": f"Only proxy admins can update public model groups. Your role={user_api_key_dict.user_role}" 

1122 }, 

1123 ) 

1124 

1125 if litellm.public_agent_groups is None: 

1126 litellm.public_agent_groups = [] 

1127 

1128 for agent_id in request.agent_ids: 

1129 agent = AGENT_REGISTRY.get_agent_by_id(agent_id=agent_id) 

1130 if agent is None: 1130 ↛ 1128line 1130 didn't jump to line 1128 because the condition on line 1130 was always true

1131 # check if agent exists in DB 

1132 agent = await agents_table(prisma_client).find_unique(where={"agent_id": agent_id}) 

1133 if agent is not None: 1133 ↛ 1134line 1133 didn't jump to line 1134 because the condition on line 1133 was never true

1134 agent = AgentResponse(**agent.model_dump()) 

1135 

1136 if agent is None: 1136 ↛ 1128line 1136 didn't jump to line 1128 because the condition on line 1136 was always true

1137 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found") 

1138 

1139 litellm.public_agent_groups = request.agent_ids 

1140 

1141 # Update config with new settings 

1142 if "litellm_settings" not in config or config["litellm_settings"] is None: 1142 ↛ 1143line 1142 didn't jump to line 1143 because the condition on line 1142 was never true

1143 config["litellm_settings"] = {} 

1144 

1145 config["litellm_settings"]["public_agent_groups"] = litellm.public_agent_groups 

1146 

1147 # Save the updated config 

1148 await proxy_config.save_config(new_config=config) 

1149 

1150 verbose_proxy_logger.debug( 

1151 "Updated public agent groups to: %s by user: %s", litellm.public_agent_groups, user_api_key_dict.user_id 

1152 ) 

1153 

1154 return { 

1155 "message": "Successfully updated public agent groups", 

1156 "public_agent_groups": litellm.public_agent_groups, 

1157 "updated_by": user_api_key_dict.user_id, 

1158 } 

1159 except HTTPException: 

1160 raise 

1161 except Exception as e: 

1162 verbose_proxy_logger.exception("Error making agent public: %s", e) 

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

1164 

1165 

1166@router.get( 

1167 "/agent/daily/activity", 

1168 tags=["Agent Management"], 

1169 dependencies=[Depends(user_api_key_auth)], 

1170 response_model=SpendAnalyticsPaginatedResponse, 

1171) 

1172async def get_agent_daily_activity( 

1173 agent_ids: str | None = None, 

1174 start_date: str | None = None, 

1175 end_date: str | None = None, 

1176 model: str | None = None, 

1177 api_key: str | None = None, 

1178 page: int = 1, 

1179 page_size: int = 10, 

1180 exclude_agent_ids: str | None = None, 

1181 user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), 

1182): 

1183 """ 

1184 Get daily activity for specific agents or all accessible agents. 

1185 """ 

1186 await check_feature_access_for_user(user_api_key_dict, "agents") 

1187 

1188 from litellm.proxy.proxy_server import prisma_client 

1189 

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

1191 raise HTTPException( 

1192 status_code=500, 

1193 detail={"error": CommonProxyErrors.db_not_connected_error.value}, 

1194 ) 

1195 

1196 agent_ids_list = agent_ids.split(",") if agent_ids else None 

1197 exclude_agent_ids_list: list[str] | None = None 

1198 if exclude_agent_ids: 

1199 exclude_agent_ids_list = exclude_agent_ids.split(",") if exclude_agent_ids else None 

1200 

1201 # Without scoping, an empty `agent_ids` query returned every agent's 

1202 # spend/token rows on the proxy. Restrict non-admin callers to the 

1203 # agents they're permitted to invoke (or that they created), and 

1204 # intersect their explicit `agent_ids` filter with the same allowlist. 

1205 from litellm.proxy.agent_endpoints.auth.agent_permission_handler import ( 

1206 AgentRequestHandler, 

1207 RestrictedAgentAccess, 

1208 UnrestrictedAgentAccess, 

1209 ) 

1210 from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view 

1211 

1212 where_condition: Final[dict[str, object]] = {} 

1213 if not _user_has_admin_view(user_api_key_dict): 1213 ↛ 1214line 1213 didn't jump to line 1214 because the condition on line 1213 was never true

1214 permitted_agent_ids: list[str] = [] 

1215 # An unrestricted caller is not "see everything" for activity scoping. Fall 

1216 # back to the agents the caller created so they cannot enumerate other 

1217 # tenants' agents. 

1218 # Guard against `user_id is None`: a literal None in Prisma 

1219 # `where={"created_by": None}` resolves to ``created_by IS NULL`` 

1220 # and would expose every ownerless agent's rows. 

1221 match await AgentRequestHandler.resolve_agent_access(user_api_key_auth=user_api_key_dict): 

1222 case RestrictedAgentAccess(allowed_agent_ids): 

1223 permitted_agent_ids = list(allowed_agent_ids) 

1224 case UnrestrictedAgentAccess(): 

1225 if user_api_key_dict.user_id is not None: 

1226 owned_records: Final = await agents_table(prisma_client).find_many( 

1227 where={"created_by": user_api_key_dict.user_id} 

1228 ) 

1229 permitted_agent_ids = [a.agent_id for a in owned_records] 

1230 

1231 if agent_ids_list: 

1232 permitted_agent_id_set: Final = set(permitted_agent_ids) 

1233 agent_ids_list = [aid for aid in agent_ids_list if aid in permitted_agent_id_set] 

1234 else: 

1235 agent_ids_list = list(permitted_agent_ids) 

1236 

1237 # No accessible agents → return an empty page without querying. 

1238 if not agent_ids_list: 

1239 return SpendAnalyticsPaginatedResponse( 

1240 results=[], 

1241 metadata=DailySpendMetadata( 

1242 total_spend=0.0, 

1243 total_prompt_tokens=0, 

1244 total_completion_tokens=0, 

1245 total_tokens=0, 

1246 total_api_requests=0, 

1247 total_successful_requests=0, 

1248 total_failed_requests=0, 

1249 total_cache_read_input_tokens=0, 

1250 total_cache_creation_input_tokens=0, 

1251 total_compression_saved_tokens=0, 

1252 page=page, 

1253 total_pages=0, 

1254 has_more=False, 

1255 ), 

1256 ) 

1257 

1258 if agent_ids_list: 

1259 where_condition["agent_id"] = {"in": list(agent_ids_list)} 

1260 

1261 agent_records: Final = await agents_table(prisma_client).find_many(where=where_condition) 

1262 agent_metadata: Final[Mapping[str, dict[str, object]]] = { 

1263 agent.agent_id: {"agent_name": agent.agent_name} for agent in agent_records 

1264 } 

1265 

1266 return await get_daily_activity( 

1267 prisma_client=prisma_client, 

1268 table_name="litellm_dailyagentspend", 

1269 entity_id_field="agent_id", 

1270 entity_id=agent_ids_list, 

1271 entity_metadata_field=agent_metadata, 

1272 exclude_entity_ids=exclude_agent_ids_list, 

1273 start_date=start_date, 

1274 end_date=end_date, 

1275 model=model, 

1276 api_key=api_key, 

1277 page=page, 

1278 page_size=page_size, 

1279 )