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
« 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.
4Follows the A2A Spec.
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"""
11import asyncio
12import os
13import uuid
14from collections.abc import Mapping, Sequence
15from types import MappingProxyType
16from typing import Annotated, Final, TypedDict
18from fastapi import APIRouter, Depends, HTTPException, Query, Request
19from typing_extensions import ReadOnly, Required, assert_never
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)
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)
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 )
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 )
122router: Final = APIRouter()
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
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
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 )
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
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 )
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"))
215class _AgentHealthResult(TypedDict, total=False):
216 agent_id: Required[str]
217 healthy: Required[bool]
218 error: str
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.
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}
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 }
255class _AgentSearchErrorDetail(TypedDict):
256 error: ReadOnly[str]
257 message: ReadOnly[str]
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)
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
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)
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 ```
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 ```
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 ```
338 Returns: List[AgentResponse]
340 """
341 await check_feature_access_for_user(user_api_key_dict, "agents")
343 from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry
345 try:
346 returned_agents: Sequence[AgentResponse] = await accessible_agents(user_api_key_dict)
348 # Fetch current spend from DB for all returned agents
349 from litellm.proxy.proxy_server import prisma_client
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)
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 )
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)
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
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}"})
422#### CRUD ENDPOINTS FOR AGENTS ####
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)
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
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")
482 from litellm.proxy.proxy_server import prisma_client
484 _check_agent_management_permission(user_api_key_dict)
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")
489 try:
490 # Get the user ID from the API key auth
491 created_by: Final = user_api_key_dict.user_id or "unknown"
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 )
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}
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 )
528 agent_name: Final = result.agent_name
529 agent_id: Final = result.agent_id
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 )
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]
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))
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
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")
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 )
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 )
584 from litellm.proxy.proxy_server import prisma_client
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")
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
610 if agent is None:
611 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found")
613 await _attach_keys_to_agents([agent], prisma_client)
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]
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))
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
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")
674 from litellm.proxy.proxy_server import prisma_client
676 _check_agent_management_permission(user_api_key_dict)
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)
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)
687 if existing_agent is None:
688 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found")
690 # Get the user ID from the API key auth
691 updated_by: Final = user_api_key_dict.user_id or "unknown"
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}
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 )
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)
721 verbose_proxy_logger.info(
722 "Successfully updated agent '%s' (ID: %s) in memory", existing_agent.get("agent_name"), agent_id
723 )
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))
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
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")
776 from litellm.proxy.proxy_server import prisma_client
778 _check_agent_management_permission(user_api_key_dict)
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)
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)
789 if existing_agent is None:
790 raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found")
792 # Get the user ID from the API key auth
793 updated_by: Final = user_api_key_dict.user_id or "unknown"
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}
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 )
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)
824 verbose_proxy_logger.info(
825 "Successfully updated agent '%s' (ID: %s) in memory", existing_agent.get("agent_name"), agent_id
826 )
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))
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
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 ```
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")
863 from litellm.proxy.proxy_server import prisma_client
865 _check_agent_management_permission(user_api_key_dict)
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")
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)
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.")
879 await AGENT_REGISTRY.delete_agent_from_db(agent_id=agent_id, prisma_client=prisma_client)
881 AGENT_REGISTRY.deregister_agent(agent_name=existing_agent.get("agent_name"))
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))
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.
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.
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
919 await check_feature_access_for_user(user_api_key_dict, "agents")
920 _check_agent_management_permission(user_api_key_dict)
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")
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
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
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)
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
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
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 ```
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
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)
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
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 )
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())
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")
1027 config: Final = await proxy_config.get_config()
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]
1037 if "litellm_settings" not in config or config["litellm_settings"] is None:
1038 config["litellm_settings"] = {}
1040 config["litellm_settings"]["public_agent_groups"] = updated_public_agent_groups
1042 await proxy_config.save_config(new_config=config)
1044 litellm.public_agent_groups = updated_public_agent_groups
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 )
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))
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
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 ```
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
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)
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
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 )
1125 if litellm.public_agent_groups is None:
1126 litellm.public_agent_groups = []
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())
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")
1139 litellm.public_agent_groups = request.agent_ids
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"] = {}
1145 config["litellm_settings"]["public_agent_groups"] = litellm.public_agent_groups
1147 # Save the updated config
1148 await proxy_config.save_config(new_config=config)
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 )
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))
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")
1188 from litellm.proxy.proxy_server import prisma_client
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 )
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
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
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]
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)
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 )
1258 if agent_ids_list:
1259 where_condition["agent_id"] = {"in": list(agent_ids_list)}
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 }
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 )