""" Agent endpoints for registering + discovering agents via LiteLLM. Follows the A2A Spec. 1. Register an agent via POST `/v1/agents` 2. Discover agents via GET `/v1/agents` 3. Get specific agent via GET `/v1/agents/{agent_id}` """ import asyncio import os import uuid from typing import Any, Dict, List, Mapping, Optional from fastapi import APIRouter, Depends, HTTPException, Query, Request import litellm from litellm._logging import verbose_proxy_logger from litellm.litellm_core_utils.litellm_logging import _get_masked_values from litellm.llms.custom_httpx.http_handler import get_async_httpx_client from litellm.proxy._types import CommonProxyErrors, LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.a2a.agent_card import merge_agent_card from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.common_utils.rbac_utils import check_feature_access_for_user from litellm.proxy.management_endpoints.common_daily_activity import get_daily_activity from litellm.types.agents import ( AgentConfig, AgentMakePublicResponse, AgentResponse, MakeAgentsPublicRequest, PatchAgentRequest, ) from litellm.types.llms.custom_http import httpxSpecialProvider from litellm.types.proxy.management_endpoints.common_daily_activity import ( DailySpendMetadata, SpendAnalyticsPaginatedResponse, ) def _proxy_base_url(http_request: Request) -> str: """Return the proxy's base URL as seen by the caller, without trailing slash.""" return str(http_request.base_url).rstrip("/") def _build_merged_agent_card( upstream_card: Optional[Mapping[str, Any]], *, agent_id: str, http_request: Request, agent_name: Optional[str] = None, ) -> Dict[str, Any]: """Apply the LiteLLM-fronting merge to ``upstream_card`` for ``agent_id``.""" proxy_base = _proxy_base_url(http_request) # Prefer a card-supplied ``name`` (the discovery UI exposes an editable # "Name (shown to API clients)" field that flows into # ``agent_card_params.name``) over the internal ``agent_name`` identifier. # Fall back to ``agent_name`` only when the card itself has no name. card_name = upstream_card.get("name") if upstream_card else None return merge_agent_card( upstream_card, proxy_url=f"{proxy_base}/a2a/{agent_id}", proxy_base_url=proxy_base, name=card_name or agent_name, ) router = APIRouter() def _redact_sensitive_agent_fields( agents: List[AgentResponse], ) -> List[AgentResponse]: """ Return copies of the given agents with sensitive configuration fields redacted. The original objects are not modified. """ redacted: List[AgentResponse] = [] for agent in agents: copy = agent.model_copy(deep=True) copy.static_headers = None copy.extra_headers = None if copy.litellm_params: copy.litellm_params = _get_masked_values( copy.litellm_params, unmasked_length=4, number_of_asterisks=4, ) redacted.append(copy) return redacted def _check_agent_management_permission(user_api_key_dict: UserAPIKeyAuth) -> None: """ Raises HTTP 403 if the caller does not have permission to create, update, or delete agents. Only PROXY_ADMIN users are allowed to perform these write operations. """ if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: raise HTTPException( status_code=403, detail={ "error": "Only proxy admins can create, update, or delete agents. Your role={}".format( user_api_key_dict.user_role ) }, ) AGENT_HEALTH_CHECK_TIMEOUT_SECONDS = float( os.environ.get("LITELLM_AGENT_HEALTH_CHECK_TIMEOUT", "5.0") ) AGENT_HEALTH_CHECK_GATHER_TIMEOUT_SECONDS = float( os.environ.get("LITELLM_AGENT_HEALTH_CHECK_GATHER_TIMEOUT", "30.0") ) async def _check_agent_url_health( agent: AgentResponse, ) -> Dict[str, Any]: """ Perform a GET request against the agent's URL and return the health result. Returns a dict with ``agent_id``, ``healthy`` (bool), and an optional ``error`` message. """ url = (agent.agent_card_params or {}).get("url") if not url: return {"agent_id": agent.agent_id, "healthy": True} try: client = get_async_httpx_client( llm_provider=httpxSpecialProvider.AgentHealthCheck, params={"timeout": AGENT_HEALTH_CHECK_TIMEOUT_SECONDS}, ) response = await client.get(url) if response.status_code >= 500: return { "agent_id": agent.agent_id, "healthy": False, "error": f"HTTP {response.status_code}", } return {"agent_id": agent.agent_id, "healthy": True} except Exception as exc: return { "agent_id": agent.agent_id, "healthy": False, "error": str(exc), } @router.get( "/v1/agents", tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=List[AgentResponse], ) async def get_agents( request: Request, health_check: bool = Query( False, 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.", ), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # Used for auth ): """ Example usage: ``` curl -X GET "http://localhost:4000/v1/agents" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-key" \ ``` Pass `?health_check=true` to filter out agents whose URL is unreachable: ``` curl -X GET "http://localhost:4000/v1/agents?health_check=true" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-key" \ ``` Returns: List[AgentResponse] """ await check_feature_access_for_user(user_api_key_dict, "agents") from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry from litellm.proxy.agent_endpoints.auth.agent_permission_handler import ( AgentRequestHandler, ) try: returned_agents: List[AgentResponse] = [] # Admin users get all agents if ( user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value ): returned_agents = global_agent_registry.get_agent_list() else: # Get allowed agents from object_permission (key/team level) allowed_agent_ids = await AgentRequestHandler.get_allowed_agents( user_api_key_auth=user_api_key_dict ) # If no restrictions (empty list), return all agents if len(allowed_agent_ids) == 0: returned_agents = global_agent_registry.get_agent_list() else: # Filter agents by allowed IDs all_agents = global_agent_registry.get_agent_list() returned_agents = [ agent for agent in all_agents if agent.agent_id in allowed_agent_ids ] # Fetch current spend from DB for all returned agents from litellm.proxy.proxy_server import prisma_client if prisma_client is not None: agent_ids = [agent.agent_id for agent in returned_agents] if agent_ids: db_agents = await AgentsRepository(prisma_client).table.find_many( where={"agent_id": {"in": agent_ids}}, ) spend_map = {a.agent_id: a.spend for a in db_agents} for agent in returned_agents: if agent.agent_id in spend_map: agent.spend = spend_map[agent.agent_id] # add is_public field to each agent - we do it this way, to allow setting config agents as public for agent in returned_agents: if agent.litellm_params is None: agent.litellm_params = {} agent.litellm_params["is_public"] = ( litellm.public_agent_groups is not None and (agent.agent_id in litellm.public_agent_groups) ) # Redact sensitive fields for non-admin users is_admin = ( user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value ) if not is_admin: returned_agents = _redact_sensitive_agent_fields(returned_agents) if health_check: agents_with_url = [ agent for agent in returned_agents if (agent.agent_card_params or {}).get("url") ] agents_without_url = [ agent for agent in returned_agents if not (agent.agent_card_params or {}).get("url") ] try: health_results = await asyncio.wait_for( asyncio.gather( *[_check_agent_url_health(agent) for agent in agents_with_url] ), timeout=AGENT_HEALTH_CHECK_GATHER_TIMEOUT_SECONDS, ) except asyncio.TimeoutError: verbose_proxy_logger.warning( "Agent health check gather timed out after %s seconds", AGENT_HEALTH_CHECK_GATHER_TIMEOUT_SECONDS, ) health_results = [ { "agent_id": agent.agent_id, "healthy": False, "error": "Health check timed out", } for agent in agents_with_url ] healthy_ids = { result["agent_id"] for result in health_results if result["healthy"] } returned_agents = [ agent for agent in agents_with_url if agent.agent_id in healthy_ids ] + agents_without_url return returned_agents except HTTPException: raise except Exception as e: verbose_proxy_logger.exception( "litellm.proxy.agent_endpoints.get_agents(): Exception occurred - {}".format( str(e) ) ) raise HTTPException( status_code=500, detail={"error": f"Internal server error: {str(e)}"} ) #### CRUD ENDPOINTS FOR AGENTS #### from litellm.proxy.agent_endpoints.agent_registry import ( global_agent_registry as AGENT_REGISTRY, ) from litellm.repositories.table_repositories import AgentsRepository @router.post( "/v1/agents", tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentResponse, ) async def create_agent( request: AgentConfig, http_request: Request, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Create a new agent Example Request: ```bash curl -X POST "http://localhost:4000/v1/agents" \\ -H "Authorization: Bearer " \\ -H "Content-Type: application/json" \\ -d '{ "agent_name": "my-custom-agent", "agent_card_params": { "protocolVersion": "1.0", "name": "Hello World Agent", "description": "Just a hello world agent", "url": "http://localhost:9999/", "version": "1.0.0", "defaultInputModes": ["text"], "defaultOutputModes": ["text"], "capabilities": { "streaming": true }, "skills": [ { "id": "hello_world", "name": "Returns hello world", "description": "just returns hello world", "tags": ["hello world"], "examples": ["hi", "hello world"] } ] }, "litellm_params": { "make_public": true } }' ``` """ await check_feature_access_for_user(user_api_key_dict, "agents") from litellm.proxy.proxy_server import prisma_client _check_agent_management_permission(user_api_key_dict) if prisma_client is None: raise HTTPException(status_code=500, detail="Prisma client not initialized") try: # Get the user ID from the API key auth created_by = user_api_key_dict.user_id or "unknown" # check for naming conflicts existing_agent = AGENT_REGISTRY.get_agent_by_name( agent_name=request.get("agent_name") # type: ignore ) if existing_agent is not None: raise HTTPException( status_code=400, detail=f"Agent with name {request.get('agent_name')} already exists", ) # Apply the LiteLLM-fronting merge only when the admin actually # provided an agent card. Plain chat/LLM agents register without # ``agent_card_params``, and synthesising a default A2A card for them # would advertise capabilities (``supportedInterfaces``, security # schemes, default skills) the agent doesn't actually expose. upstream_card = request.get("agent_card_params") agent_to_create: AgentConfig = request new_agent_id: Optional[str] = None if upstream_card is not None: # Pre-generate the agent_id so the merged card can reference it # in ``supportedInterfaces`` before the DB row exists. new_agent_id = str(uuid.uuid4()) merged_card = _build_merged_agent_card( upstream_card, agent_id=new_agent_id, http_request=http_request, agent_name=request.get("agent_name"), ) agent_to_create = {**request, "agent_card_params": merged_card} # type: ignore[typeddict-item] result = await AGENT_REGISTRY.add_agent_to_db( agent=agent_to_create, prisma_client=prisma_client, created_by=created_by, agent_id=new_agent_id, ) agent_name = result.agent_name agent_id = result.agent_id # Also register in memory try: AGENT_REGISTRY.register_agent(agent_config=result) verbose_proxy_logger.info( f"Successfully registered agent '{agent_name}' (ID: {agent_id}) in memory" ) except Exception as reg_error: verbose_proxy_logger.warning( f"Failed to register agent '{agent_name}' (ID: {agent_id}) in memory: {reg_error}" ) return result except HTTPException: raise except Exception as e: verbose_proxy_logger.exception(f"Error adding agent to db: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get( "/v1/agents/{agent_id}", tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentResponse, ) async def get_agent_by_id( agent_id: str, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Get a specific agent by ID Example Request: ```bash curl -X GET "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000" \\ -H "Authorization: Bearer " ``` """ await check_feature_access_for_user(user_api_key_dict, "agents") is_admin = ( user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value ) if not is_admin: from litellm.proxy.agent_endpoints.auth.agent_permission_handler import ( AgentRequestHandler, ) is_allowed = await AgentRequestHandler.is_agent_allowed( agent_id=agent_id, user_api_key_auth=user_api_key_dict ) if not is_allowed: raise HTTPException( status_code=403, detail=f"Agent '{agent_id}' is not allowed for your key/team. Contact proxy admin for access.", ) from litellm.proxy.proxy_server import prisma_client if prisma_client is None: raise HTTPException(status_code=500, detail="Prisma client not initialized") try: agent = AGENT_REGISTRY.get_agent_by_id(agent_id=agent_id) if agent is None: agent_row = await AgentsRepository(prisma_client).table.find_unique( where={"agent_id": agent_id}, include={"object_permission": True}, ) if agent_row is not None: agent_dict = agent_row.model_dump() if agent_row.object_permission is not None: try: agent_dict["object_permission"] = ( agent_row.object_permission.model_dump() ) except Exception: agent_dict["object_permission"] = ( agent_row.object_permission.dict() ) agent = AgentResponse(**agent_dict) # type: ignore else: # Agent found in memory — refresh spend from DB db_row = await AgentsRepository(prisma_client).table.find_unique( where={"agent_id": agent_id} ) if db_row is not None: agent.spend = db_row.spend if agent is None: raise HTTPException( status_code=404, detail=f"Agent with ID {agent_id} not found" ) # Redact sensitive fields for non-admin users is_admin = ( user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN or user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value ) if not is_admin: agent = _redact_sensitive_agent_fields([agent])[0] return agent except HTTPException: raise except Exception as e: verbose_proxy_logger.exception(f"Error getting agent from db: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.put( "/v1/agents/{agent_id}", tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentResponse, ) async def update_agent( agent_id: str, request: AgentConfig, http_request: Request, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Update an existing agent Example Request: ```bash curl -X PUT "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000" \\ -H "Authorization: Bearer " \\ -H "Content-Type: application/json" \\ -d '{ "agent_name": "updated-agent", "agent_card_params": { "protocolVersion": "1.0", "name": "Updated Agent", "description": "Updated description", "url": "http://localhost:9999/", "version": "1.1.0", "defaultInputModes": ["text"], "defaultOutputModes": ["text"], "capabilities": { "streaming": true }, "skills": [] }, "litellm_params": { "make_public": false } }' ``` """ await check_feature_access_for_user(user_api_key_dict, "agents") from litellm.proxy.proxy_server import prisma_client _check_agent_management_permission(user_api_key_dict) if prisma_client is None: raise HTTPException( status_code=500, detail=CommonProxyErrors.db_not_connected_error.value ) try: # Check if agent exists existing_agent = await AgentsRepository(prisma_client).table.find_unique( where={"agent_id": agent_id} ) if existing_agent is not None: existing_agent = dict(existing_agent) if existing_agent is None: raise HTTPException( status_code=404, detail=f"Agent with ID {agent_id} not found" ) # Get the user ID from the API key auth updated_by = user_api_key_dict.user_id or "unknown" # Re-apply the LiteLLM-fronting merge — an update is a re-registration, # so any new upstream card the admin pasted must go through the same # transformation as initial create. Plain agents without an # ``agent_card_params`` skip the merge so we don't synthesise an A2A # card for them. upstream_card = request.get("agent_card_params") agent_to_update: AgentConfig = request if upstream_card is not None: merged_card = _build_merged_agent_card( upstream_card, agent_id=agent_id, http_request=http_request, agent_name=request.get("agent_name"), ) agent_to_update = {**request, "agent_card_params": merged_card} # type: ignore[typeddict-item] result = await AGENT_REGISTRY.update_agent_in_db( agent_id=agent_id, agent=agent_to_update, prisma_client=prisma_client, updated_by=updated_by, ) # deregister in memory AGENT_REGISTRY.deregister_agent(agent_name=existing_agent.get("agent_name")) # type: ignore # register in memory AGENT_REGISTRY.register_agent(agent_config=result) verbose_proxy_logger.info( f"Successfully updated agent '{existing_agent.get('agent_name')}' (ID: {agent_id}) in memory" ) return result except HTTPException: raise except Exception as e: verbose_proxy_logger.exception(f"Error updating agent: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.patch( "/v1/agents/{agent_id}", tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentResponse, ) async def patch_agent( agent_id: str, request: PatchAgentRequest, http_request: Request, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Update an existing agent Example Request: ```bash curl -X PATCH "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000" \\ -H "Authorization: Bearer " \\ -H "Content-Type: application/json" \\ -d '{ "agent_name": "updated-agent", "agent_card_params": { "protocolVersion": "1.0", "name": "Updated Agent", "description": "Updated description", "url": "http://localhost:9999/", "version": "1.1.0", "defaultInputModes": ["text"], "defaultOutputModes": ["text"], "capabilities": { "streaming": true }, "skills": [] }, "litellm_params": { "make_public": false } }' ``` """ await check_feature_access_for_user(user_api_key_dict, "agents") from litellm.proxy.proxy_server import prisma_client _check_agent_management_permission(user_api_key_dict) if prisma_client is None: raise HTTPException( status_code=500, detail=CommonProxyErrors.db_not_connected_error.value ) try: # Check if agent exists existing_agent = await AgentsRepository(prisma_client).table.find_unique( where={"agent_id": agent_id} ) if existing_agent is not None: existing_agent = dict(existing_agent) if existing_agent is None: raise HTTPException( status_code=404, detail=f"Agent with ID {agent_id} not found" ) # Get the user ID from the API key auth updated_by = user_api_key_dict.user_id or "unknown" # Re-merge only when the patch actually touches agent_card_params; a # patch updating just litellm_params/rate limits (``agent_card_params`` # omitted) shouldn't rewrite the stored card. An explicitly provided # ``agent_card_params`` — even an empty dict — still goes through the # merge so LiteLLM applies its security schemes and supported # interfaces instead of storing a bare card. patch_payload: PatchAgentRequest = request upstream_card = request.get("agent_card_params") if upstream_card is not None: merged_card = _build_merged_agent_card( upstream_card, agent_id=agent_id, http_request=http_request, agent_name=request.get("agent_name"), ) patch_payload = {**request, "agent_card_params": merged_card} # type: ignore[typeddict-item] result = await AGENT_REGISTRY.patch_agent_in_db( agent_id=agent_id, agent=patch_payload, prisma_client=prisma_client, updated_by=updated_by, ) # deregister in memory AGENT_REGISTRY.deregister_agent(agent_name=existing_agent.get("agent_name")) # type: ignore # register in memory AGENT_REGISTRY.register_agent(agent_config=result) verbose_proxy_logger.info( f"Successfully updated agent '{existing_agent.get('agent_name')}' (ID: {agent_id}) in memory" ) return result except HTTPException: raise except Exception as e: verbose_proxy_logger.exception(f"Error updating agent: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete( "/v1/agents/{agent_id}", tags=["Agents"], dependencies=[Depends(user_api_key_auth)], ) async def delete_agent( agent_id: str, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Delete an agent Example Request: ```bash curl -X DELETE "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000" \\ -H "Authorization: Bearer " ``` Example Response: ```json { "message": "Agent 123e4567-e89b-12d3-a456-426614174000 deleted successfully" } ``` """ await check_feature_access_for_user(user_api_key_dict, "agents") from litellm.proxy.proxy_server import prisma_client _check_agent_management_permission(user_api_key_dict) if prisma_client is None: raise HTTPException(status_code=500, detail="Prisma client not initialized") try: # Check if agent exists existing_agent = await AgentsRepository(prisma_client).table.find_unique( where={"agent_id": agent_id} ) if existing_agent is not None: existing_agent = dict[Any, Any](existing_agent) if existing_agent is None: raise HTTPException( status_code=404, detail=f"Agent with ID {agent_id} not found in DB." ) await AGENT_REGISTRY.delete_agent_from_db( agent_id=agent_id, prisma_client=prisma_client ) AGENT_REGISTRY.deregister_agent(agent_name=existing_agent.get("agent_name")) # type: ignore return {"message": f"Agent {agent_id} deleted successfully"} except HTTPException: raise except Exception as e: verbose_proxy_logger.exception(f"Error deleting agent: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post( "/v1/agents/{agent_id}/make_public", tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentMakePublicResponse, ) async def make_agent_public( agent_id: str, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Make an agent publicly discoverable Example Request: ```bash curl -X POST "http://localhost:4000/v1/agents/123e4567-e89b-12d3-a456-426614174000/make_public" \\ -H "Authorization: Bearer " \\ -H "Content-Type: application/json" ``` Example Response: ```json { "agent_id": "123e4567-e89b-12d3-a456-426614174000", "agent_name": "my-custom-agent", "litellm_params": { "make_public": true }, "agent_card_params": {...}, "created_at": "2025-11-15T10:30:00Z", "updated_at": "2025-11-15T10:35:00Z", "created_by": "user123", "updated_by": "user123" } ``` """ from litellm.proxy.proxy_server import prisma_client if prisma_client is None: raise HTTPException( status_code=500, detail=CommonProxyErrors.db_not_connected_error.value ) try: # Update the public model groups import litellm from litellm.proxy.agent_endpoints.agent_registry import ( global_agent_registry as AGENT_REGISTRY, ) from litellm.proxy.proxy_server import proxy_config # Check if user has admin permissions if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: raise HTTPException( status_code=403, detail={ "error": "Only proxy admins can update public model groups. Your role={}".format( user_api_key_dict.user_role ) }, ) agent = AGENT_REGISTRY.get_agent_by_id(agent_id=agent_id) if agent is None: # check if agent exists in DB agent = await AgentsRepository(prisma_client).table.find_unique( where={"agent_id": agent_id} ) if agent is not None: agent = AgentResponse(**agent.model_dump()) # type: ignore if agent is None: raise HTTPException( status_code=404, detail=f"Agent with ID {agent_id} not found" ) if litellm.public_agent_groups is None: litellm.public_agent_groups = [] # handle duplicates if agent.agent_id in litellm.public_agent_groups: raise HTTPException( status_code=400, detail=f"Agent with name {agent.agent_name} already in public agent groups", ) litellm.public_agent_groups.append(agent.agent_id) # Load existing config config = await proxy_config.get_config() # Update config with new settings if "litellm_settings" not in config or config["litellm_settings"] is None: config["litellm_settings"] = {} config["litellm_settings"]["public_agent_groups"] = litellm.public_agent_groups # Save the updated config await proxy_config.save_config(new_config=config) verbose_proxy_logger.debug( f"Updated public agent groups to: {litellm.public_agent_groups} by user: {user_api_key_dict.user_id}" ) return { "message": "Successfully updated public agent groups", "public_agent_groups": litellm.public_agent_groups, "updated_by": user_api_key_dict.user_id, } except HTTPException: raise except Exception as e: verbose_proxy_logger.exception(f"Error making agent public: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post( "/v1/agents/make_public", tags=["[beta] A2A Agents"], dependencies=[Depends(user_api_key_auth)], response_model=AgentMakePublicResponse, ) async def make_agents_public( request: MakeAgentsPublicRequest, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Make multiple agents publicly discoverable Example Request: ```bash curl -X POST "http://localhost:4000/v1/agents/make_public" \\ -H "Authorization: Bearer " \\ -H "Content-Type: application/json" \\ -d '{ "agent_ids": ["123e4567-e89b-12d3-a456-426614174000", "123e4567-e89b-12d3-a456-426614174001"] }' ``` Example Response: ```json { "agent_id": "123e4567-e89b-12d3-a456-426614174000", "agent_name": "my-custom-agent", "litellm_params": { "make_public": true }, "agent_card_params": {...}, "created_at": "2025-11-15T10:30:00Z", "updated_at": "2025-11-15T10:35:00Z", "created_by": "user123", "updated_by": "user123" } ``` """ from litellm.proxy.proxy_server import prisma_client if prisma_client is None: raise HTTPException( status_code=500, detail=CommonProxyErrors.db_not_connected_error.value ) try: # Update the public model groups import litellm from litellm.proxy.agent_endpoints.agent_registry import ( global_agent_registry as AGENT_REGISTRY, ) from litellm.proxy.proxy_server import proxy_config # Load existing config config = await proxy_config.get_config() # Check if user has admin permissions if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN: raise HTTPException( status_code=403, detail={ "error": "Only proxy admins can update public model groups. Your role={}".format( user_api_key_dict.user_role ) }, ) if litellm.public_agent_groups is None: litellm.public_agent_groups = [] for agent_id in request.agent_ids: agent = AGENT_REGISTRY.get_agent_by_id(agent_id=agent_id) if agent is None: # check if agent exists in DB agent = await AgentsRepository(prisma_client).table.find_unique( where={"agent_id": agent_id} ) if agent is not None: agent = AgentResponse(**agent.model_dump()) # type: ignore if agent is None: raise HTTPException( status_code=404, detail=f"Agent with ID {agent_id} not found" ) litellm.public_agent_groups = request.agent_ids # Update config with new settings if "litellm_settings" not in config or config["litellm_settings"] is None: config["litellm_settings"] = {} config["litellm_settings"]["public_agent_groups"] = litellm.public_agent_groups # Save the updated config await proxy_config.save_config(new_config=config) verbose_proxy_logger.debug( f"Updated public agent groups to: {litellm.public_agent_groups} by user: {user_api_key_dict.user_id}" ) return { "message": "Successfully updated public agent groups", "public_agent_groups": litellm.public_agent_groups, "updated_by": user_api_key_dict.user_id, } except HTTPException: raise except Exception as e: verbose_proxy_logger.exception(f"Error making agent public: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get( "/agent/daily/activity", tags=["Agent Management"], dependencies=[Depends(user_api_key_auth)], response_model=SpendAnalyticsPaginatedResponse, ) async def get_agent_daily_activity( agent_ids: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, model: Optional[str] = None, api_key: Optional[str] = None, page: int = 1, page_size: int = 10, exclude_agent_ids: Optional[str] = None, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ Get daily activity for specific agents or all accessible agents. """ await check_feature_access_for_user(user_api_key_dict, "agents") from litellm.proxy.proxy_server import prisma_client if prisma_client is None: raise HTTPException( status_code=500, detail={"error": CommonProxyErrors.db_not_connected_error.value}, ) agent_ids_list = agent_ids.split(",") if agent_ids else None exclude_agent_ids_list: Optional[List[str]] = None if exclude_agent_ids: exclude_agent_ids_list = ( exclude_agent_ids.split(",") if exclude_agent_ids else None ) # Without scoping, an empty `agent_ids` query returned every agent's # spend/token rows on the proxy. Restrict non-admin callers to the # agents they're permitted to invoke (or that they created), and # intersect their explicit `agent_ids` filter with the same allowlist. from litellm.proxy.agent_endpoints.auth.agent_permission_handler import ( AgentRequestHandler, ) from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view where_condition: Dict[str, Any] = {} if not _user_has_admin_view(user_api_key_dict): permitted_agent_ids = await AgentRequestHandler.get_allowed_agents( user_api_key_auth=user_api_key_dict ) # `get_allowed_agents` returns an empty list when the caller's key # and team carry no agent restrictions. For activity scoping that's # not "see everything" — fall back to the agents the caller # created so they cannot enumerate other tenants' agents. # Guard against `user_id is None`: a literal None in Prisma # `where={"created_by": None}` resolves to ``created_by IS NULL`` # and would expose every ownerless agent's rows. if not permitted_agent_ids: if user_api_key_dict.user_id is None: permitted_agent_ids = [] else: owned_records = await AgentsRepository(prisma_client).table.find_many( where={"created_by": user_api_key_dict.user_id} ) permitted_agent_ids = [a.agent_id for a in owned_records] if agent_ids_list: permitted_agent_id_set = set(permitted_agent_ids) agent_ids_list = [ aid for aid in agent_ids_list if aid in permitted_agent_id_set ] else: agent_ids_list = list(permitted_agent_ids) # No accessible agents → return an empty page without querying. if not agent_ids_list: return SpendAnalyticsPaginatedResponse( results=[], metadata=DailySpendMetadata( total_spend=0.0, total_prompt_tokens=0, total_completion_tokens=0, total_tokens=0, total_api_requests=0, total_successful_requests=0, total_failed_requests=0, total_cache_read_input_tokens=0, total_cache_creation_input_tokens=0, page=page, total_pages=0, has_more=False, ), ) if agent_ids_list: where_condition["agent_id"] = {"in": list(agent_ids_list)} agent_records = await AgentsRepository(prisma_client).table.find_many( where=where_condition ) agent_metadata = { agent.agent_id: {"agent_name": agent.agent_name} for agent in agent_records } return await get_daily_activity( prisma_client=prisma_client, table_name="litellm_dailyagentspend", entity_id_field="agent_id", entity_id=agent_ids_list, entity_metadata_field=agent_metadata, exclude_entity_ids=exclude_agent_ids_list, start_date=start_date, end_date=end_date, model=model, api_key=api_key, page=page, page_size=page_size, )