diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index b4d0f82d7b..ce79c2b3d5 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -267,6 +267,7 @@ model LiteLLM_ObjectPermissionTable { vector_stores String[] @default([]) agents String[] @default([]) agent_access_groups String[] @default([]) + models String[] @default([]) blocked_tools String[] @default([]) // Tool names blocked for any key/team/user with this permission teams LiteLLM_TeamTable[] projects LiteLLM_ProjectTable[] diff --git a/litellm/proxy/_experimental/out/chat.html b/litellm/proxy/_experimental/out/chat/index.html similarity index 100% rename from litellm/proxy/_experimental/out/chat.html rename to litellm/proxy/_experimental/out/chat/index.html diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index b85072247f..6e2e6c7e25 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1,59 +1,40 @@ import enum import json from datetime import datetime -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Union +from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Literal, + Optional, Union) import httpx -from pydantic import ( - BaseModel, - ConfigDict, - Field, - Json, - field_validator, - model_validator, -) +from pydantic import (BaseModel, ConfigDict, Field, Json, field_validator, + model_validator) from typing_extensions import Required, TypedDict from litellm._uuid import uuid from litellm.types.integrations.slack_alerting import AlertType -from litellm.types.llms.openai import ( - AllMessageValues, - OpenAIFileObject, - ResponsesAPIResponse, -) -from litellm.types.mcp import ( - MCPAuthType, - MCPCredentials, - MCPTransport, - MCPTransportType, -) +from litellm.types.llms.openai import (AllMessageValues, OpenAIFileObject, + ResponsesAPIResponse) +from litellm.types.mcp import (MCPAuthType, MCPCredentials, MCPTransport, + MCPTransportType) from litellm.types.mcp_server.mcp_server_manager import MCPInfo from litellm.types.router import RouterErrors, UpdateRouterConfig from litellm.types.secret_managers.main import KeyManagementSystem -from litellm.types.utils import ( - CallTypes, - CostBreakdown, - EmbeddingResponse, - GenericBudgetConfigType, - ImageResponse, - LiteLLMBatch, - LiteLLMFineTuningJob, - LiteLLMPydanticObjectBase, - ModelResponse, - ProviderField, - StandardCallbackDynamicParams, - StandardLoggingGuardrailInformation, - StandardLoggingMCPToolCall, - StandardLoggingModelInformation, - StandardLoggingPayloadErrorInformation, - StandardLoggingPayloadStatus, - StandardLoggingVectorStoreRequest, - StandardPassThroughResponseObject, - TextCompletionResponse, -) +from litellm.types.utils import (CallTypes, CostBreakdown, EmbeddingResponse, + GenericBudgetConfigType, ImageResponse, + LiteLLMBatch, LiteLLMFineTuningJob, + LiteLLMPydanticObjectBase, ModelResponse, + ProviderField, StandardCallbackDynamicParams, + StandardLoggingGuardrailInformation, + StandardLoggingMCPToolCall, + StandardLoggingModelInformation, + StandardLoggingPayloadErrorInformation, + StandardLoggingPayloadStatus, + StandardLoggingVectorStoreRequest, + StandardPassThroughResponseObject, + TextCompletionResponse) from litellm.types.videos.main import VideoObject -from .types_utils.utils import get_instance_fn, validate_custom_validate_return_type +from .types_utils.utils import (get_instance_fn, + validate_custom_validate_return_type) if TYPE_CHECKING: from opentelemetry.trace import Span as _Span @@ -857,6 +838,7 @@ class LiteLLM_ObjectPermissionBase(LiteLLMPydanticObjectBase): vector_stores: Optional[List[str]] = None agents: Optional[List[str]] = None agent_access_groups: Optional[List[str]] = None + models: Optional[List[str]] = None class GenerateRequestBase(LiteLLMPydanticObjectBase): @@ -2504,7 +2486,8 @@ class UserAPIKeyAuth( This is used to track number of requests/spend for health check calls. """ - from litellm.constants import LITTELM_INTERNAL_HEALTH_SERVICE_ACCOUNT_NAME + from litellm.constants import \ + LITTELM_INTERNAL_HEALTH_SERVICE_ACCOUNT_NAME return cls( api_key=LITTELM_INTERNAL_HEALTH_SERVICE_ACCOUNT_NAME, @@ -2536,7 +2519,8 @@ class UserAPIKeyAuth( This is used to track actions performed by automated system jobs. """ - from litellm.constants import LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME + from litellm.constants import \ + LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME return cls( api_key=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME, @@ -2942,7 +2926,8 @@ class LiteLLM_AuditLogs(LiteLLMPydanticObjectBase): @model_validator(mode="after") def mask_api_keys(self): - from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker + from litellm.litellm_core_utils.sensitive_data_masker import \ + SensitiveDataMasker masker = SensitiveDataMasker(sensitive_patterns={"key"}) diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 721c3e404d..b68872e2ed 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -267,6 +267,7 @@ model LiteLLM_ObjectPermissionTable { vector_stores String[] @default([]) agents String[] @default([]) agent_access_groups String[] @default([]) + models String[] @default([]) blocked_tools String[] @default([]) // Tool names blocked for any key/team/user with this permission teams LiteLLM_TeamTable[] projects LiteLLM_ProjectTable[] diff --git a/litellm/types/agents.py b/litellm/types/agents.py index 951fbfcabd..efb2e73bfb 100644 --- a/litellm/types/agents.py +++ b/litellm/types/agents.py @@ -172,6 +172,8 @@ class AgentObjectPermission(TypedDict, total=False): mcp_servers: Optional[List[str]] mcp_access_groups: Optional[List[str]] mcp_tool_permissions: Optional[Dict[str, List[str]]] + models: Optional[List[str]] + agents: Optional[List[str]] class AgentConfig(TypedDict, total=False): diff --git a/schema.prisma b/schema.prisma index 8d4bdffb2d..939f1eb0f4 100644 --- a/schema.prisma +++ b/schema.prisma @@ -267,6 +267,7 @@ model LiteLLM_ObjectPermissionTable { vector_stores String[] @default([]) agents String[] @default([]) agent_access_groups String[] @default([]) + models String[] @default([]) blocked_tools String[] @default([]) // Tool names blocked for any key/team/user with this permission teams LiteLLM_TeamTable[] projects LiteLLM_ProjectTable[] diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index bddcb0ab59..5f2921203f 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -547,7 +547,7 @@ function CreateKeyPageContent() { ) : page == "policies" ? ( ) : page == "agents" ? ( - + ) : page == "prompts" ? ( ) : page == "transform-request" ? ( diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx index c29ade5d65..5c23cf71ab 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.test.tsx @@ -125,6 +125,208 @@ describe("EntityUsage", () => { }, }; + const mockAgentSpendData = { + results: [ + { + date: "2025-01-01", + metrics: { + spend: 245.8, + api_requests: 3200, + successful_requests: 3100, + failed_requests: 100, + total_tokens: 1250000, + prompt_tokens: 850000, + completion_tokens: 400000, + cache_read_input_tokens: 50000, + cache_creation_input_tokens: 10000, + }, + breakdown: { + entities: { + "agent-code-review": { + metrics: { + spend: 120.4, + api_requests: 1500, + successful_requests: 1450, + failed_requests: 50, + total_tokens: 620000, + prompt_tokens: 420000, + completion_tokens: 200000, + cache_read_input_tokens: 30000, + cache_creation_input_tokens: 5000, + }, + metadata: { agent_name: "Code Review Agent" }, + api_key_breakdown: {}, + }, + "agent-customer-support": { + metrics: { + spend: 85.2, + api_requests: 1200, + successful_requests: 1170, + failed_requests: 30, + total_tokens: 430000, + prompt_tokens: 290000, + completion_tokens: 140000, + cache_read_input_tokens: 15000, + cache_creation_input_tokens: 3000, + }, + metadata: { agent_name: "Customer Support Agent" }, + api_key_breakdown: {}, + }, + "agent-data-analyst": { + metrics: { + spend: 40.2, + api_requests: 500, + successful_requests: 480, + failed_requests: 20, + total_tokens: 200000, + prompt_tokens: 140000, + completion_tokens: 60000, + cache_read_input_tokens: 5000, + cache_creation_input_tokens: 2000, + }, + metadata: { agent_name: "Data Analyst Agent" }, + api_key_breakdown: {}, + }, + }, + models: { + "gpt-4o": { + metrics: { + spend: 180.0, + api_requests: 2000, + successful_requests: 1950, + failed_requests: 50, + total_tokens: 900000, + prompt_tokens: 600000, + completion_tokens: 300000, + cache_read_input_tokens: 40000, + cache_creation_input_tokens: 8000, + }, + metadata: {}, + api_key_breakdown: {}, + }, + "claude-sonnet-4-20250514": { + metrics: { + spend: 65.8, + api_requests: 1200, + successful_requests: 1150, + failed_requests: 50, + total_tokens: 350000, + prompt_tokens: 250000, + completion_tokens: 100000, + cache_read_input_tokens: 10000, + cache_creation_input_tokens: 2000, + }, + metadata: {}, + api_key_breakdown: {}, + }, + }, + api_keys: {}, + providers: { + openai: { + metrics: { + spend: 180.0, + api_requests: 2000, + successful_requests: 1950, + failed_requests: 50, + total_tokens: 900000, + prompt_tokens: 600000, + completion_tokens: 300000, + cache_read_input_tokens: 40000, + cache_creation_input_tokens: 8000, + }, + }, + anthropic: { + metrics: { + spend: 65.8, + api_requests: 1200, + successful_requests: 1150, + failed_requests: 50, + total_tokens: 350000, + prompt_tokens: 250000, + completion_tokens: 100000, + cache_read_input_tokens: 10000, + cache_creation_input_tokens: 2000, + }, + }, + }, + }, + }, + { + date: "2025-01-02", + metrics: { + spend: 198.5, + api_requests: 2800, + successful_requests: 2720, + failed_requests: 80, + total_tokens: 980000, + prompt_tokens: 670000, + completion_tokens: 310000, + cache_read_input_tokens: 42000, + cache_creation_input_tokens: 9000, + }, + breakdown: { + entities: { + "agent-code-review": { + metrics: { + spend: 95.3, + api_requests: 1300, + successful_requests: 1270, + failed_requests: 30, + total_tokens: 510000, + prompt_tokens: 350000, + completion_tokens: 160000, + cache_read_input_tokens: 25000, + cache_creation_input_tokens: 4000, + }, + metadata: { agent_name: "Code Review Agent" }, + api_key_breakdown: {}, + }, + "agent-customer-support": { + metrics: { + spend: 68.7, + api_requests: 1000, + successful_requests: 970, + failed_requests: 30, + total_tokens: 320000, + prompt_tokens: 220000, + completion_tokens: 100000, + cache_read_input_tokens: 12000, + cache_creation_input_tokens: 3000, + }, + metadata: { agent_name: "Customer Support Agent" }, + api_key_breakdown: {}, + }, + "agent-data-analyst": { + metrics: { + spend: 34.5, + api_requests: 500, + successful_requests: 480, + failed_requests: 20, + total_tokens: 150000, + prompt_tokens: 100000, + completion_tokens: 50000, + cache_read_input_tokens: 5000, + cache_creation_input_tokens: 2000, + }, + metadata: { agent_name: "Data Analyst Agent" }, + api_key_breakdown: {}, + }, + }, + models: {}, + api_keys: {}, + providers: {}, + }, + }, + ], + metadata: { + total_spend: 444.3, + total_api_requests: 6000, + total_successful_requests: 5820, + total_failed_requests: 180, + total_tokens: 2230000, + }, + }; + const defaultProps = { accessToken: "test-token", entityType: "tag" as const, @@ -153,7 +355,7 @@ describe("EntityUsage", () => { mockTeamDailyActivityCall.mockResolvedValue(mockSpendData); mockOrganizationDailyActivityCall.mockResolvedValue(mockSpendData); mockCustomerDailyActivityCall.mockResolvedValue(mockSpendData); - mockAgentDailyActivityCall.mockResolvedValue(mockSpendData); + mockAgentDailyActivityCall.mockResolvedValue(mockAgentSpendData); mockUserDailyActivityCall.mockResolvedValue(mockSpendData); }); @@ -231,7 +433,7 @@ describe("EntityUsage", () => { expect(screen.getByText("Agent Spend Overview")).toBeInTheDocument(); await waitFor(() => { - const spendElements = screen.getAllByText("$100.50"); + const spendElements = screen.getAllByText("$444.30"); expect(spendElements.length).toBeGreaterThan(0); }); }); @@ -385,6 +587,87 @@ describe("EntityUsage", () => { }); }); + it("should display Agent Activity tab for team entity type", async () => { + render(); + + await waitFor(() => { + expect(mockTeamDailyActivityCall).toHaveBeenCalled(); + }); + + expect(screen.getByText("Agent Activity")).toBeInTheDocument(); + }); + + it("should not display Agent Activity tab for non-team entity types", async () => { + render(); + + await waitFor(() => { + expect(mockTagDailyActivityCall).toHaveBeenCalled(); + }); + + expect(screen.queryByText("Agent Activity")).not.toBeInTheDocument(); + }); + + it("should display Top Agents Driving Spend card for team entity type", async () => { + render(); + + await waitFor(() => { + expect(mockTeamDailyActivityCall).toHaveBeenCalled(); + }); + + expect(screen.getByText("Top Agents Driving Spend")).toBeInTheDocument(); + }); + + it("should not display Top Agents Driving Spend card for non-team entity types", async () => { + render(); + + await waitFor(() => { + expect(mockTagDailyActivityCall).toHaveBeenCalled(); + }); + + expect(screen.queryByText("Top Agents Driving Spend")).not.toBeInTheDocument(); + }); + + it("should fetch agent activity data when entity type is team", async () => { + render(); + + await waitFor(() => { + expect(mockAgentDailyActivityCall).toHaveBeenCalledWith( + "test-token", + expect.any(Date), + expect.any(Date), + 1, + null, + ); + }); + }); + + it("should not fetch agent activity data for non-team entity types", async () => { + render(); + + await waitFor(() => { + expect(mockTagDailyActivityCall).toHaveBeenCalled(); + }); + + expect(mockAgentDailyActivityCall).not.toHaveBeenCalled(); + }); + + it("should switch to Agent Activity tab for team entity type", async () => { + render(); + + await waitFor(() => { + expect(mockTeamDailyActivityCall).toHaveBeenCalled(); + }); + + const agentActivityTab = screen.getByText("Agent Activity"); + act(() => { + fireEvent.click(agentActivityTab); + }); + + await waitFor(() => { + expect(screen.getAllByText("Activity Metrics").length).toBeGreaterThan(0); + }); + }); + it("should fallback to entity value when no entityList and no team_alias", async () => { const spendDataWithoutAlias = { ...mockSpendData, diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx index a106910cff..3e0343fdf6 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EntityUsage/EntityUsage.tsx @@ -100,11 +100,24 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti }); const { teams } = useTeams(); + const [agentSpendData, setAgentSpendData] = useState({ + results: [], + metadata: { + total_spend: 0, + total_api_requests: 0, + total_successful_requests: 0, + total_failed_requests: 0, + total_tokens: 0, + }, + }); + const modelMetrics = processActivityData(spendData, "models", teams || []); const keyMetrics = processActivityData(spendData, "api_keys", teams || []); + const agentMetrics = entityType === "team" ? processActivityData(agentSpendData, "entities", teams || []) : {}; const [selectedTags, setSelectedTags] = useState([]); const [topKeysLimit, setTopKeysLimit] = useState(5); const [topModelsLimit, setTopModelsLimit] = useState(5); + const [topAgentsLimit, setTopAgentsLimit] = useState(5); const fetchSpendData = async () => { if (!accessToken || !dateValue.from || !dateValue.to) return; @@ -171,8 +184,21 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti } }; + const fetchAgentSpendData = async () => { + if (!accessToken || !dateValue.from || !dateValue.to || entityType !== "team") return; + const startTime = new Date(dateValue.from); + const endTime = new Date(dateValue.to); + try { + const data = await agentDailyActivityCall(accessToken, startTime, endTime, 1, null); + setAgentSpendData(data); + } catch (e) { + console.error("Failed to fetch agent activity data:", e); + } + }; + useEffect(() => { fetchSpendData(); + fetchAgentSpendData(); }, [accessToken, dateValue, entityId, selectedTags]); const getTopModels = () => { @@ -209,6 +235,37 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti .slice(0, topModelsLimit); }; + const getTopAgents = () => { + const agentSpend: { [key: string]: any } = {}; + agentSpendData.results.forEach((day) => { + Object.entries(day.breakdown.entities || {}).forEach(([agentId, data]) => { + if (!agentSpend[agentId]) { + agentSpend[agentId] = { + spend: 0, + requests: 0, + successful_requests: 0, + failed_requests: 0, + tokens: 0, + agent_name: (data.metadata as any)?.agent_name || agentId, + }; + } + agentSpend[agentId].spend += data.metrics.spend; + agentSpend[agentId].requests += data.metrics.api_requests; + agentSpend[agentId].successful_requests += data.metrics.successful_requests; + agentSpend[agentId].failed_requests += data.metrics.failed_requests; + agentSpend[agentId].tokens += data.metrics.total_tokens; + }); + }); + + return Object.entries(agentSpend) + .map(([agentId, metrics]) => ({ + key: metrics.agent_name, + ...metrics, + })) + .sort((a, b) => b.spend - a.spend) + .slice(0, topAgentsLimit); + }; + const getTopAPIKeys = () => { console.log("debugTags", { spendData }); const keySpend: { [key: string]: KeyMetricWithMetadata } = {}; @@ -408,6 +465,7 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti Cost {entityType === "agent" ? "Request / Token Consumption" : "Model Activity"} + {entityType === "team" && Agent Activity} Key Activity Endpoint Activity @@ -621,6 +679,20 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti + {/* Top Agents - only for team entity type */} + {entityType === "team" && ( + + + Top Agents Driving Spend + + + + )} + {/* Spend by Provider */} @@ -696,6 +768,11 @@ const EntityUsage: React.FC = ({ accessToken, entityType, enti + {entityType === "team" && ( + + + + )} diff --git a/ui/litellm-dashboard/src/components/activity_metrics.tsx b/ui/litellm-dashboard/src/components/activity_metrics.tsx index 2337d41d8d..e805eed8c8 100644 --- a/ui/litellm-dashboard/src/components/activity_metrics.tsx +++ b/ui/litellm-dashboard/src/components/activity_metrics.tsx @@ -362,7 +362,7 @@ export const formatKeyLabel = (modelData: KeyMetricWithMetadata, model: string, // Process data function export const processActivityData = ( dailyActivity: { results: DailyData[] }, - key: "models" | "api_keys" | "mcp_servers", + key: "models" | "api_keys" | "mcp_servers" | "entities", teams: Team[] = [], ): Record => { const modelMetrics: Record = {}; @@ -371,7 +371,11 @@ export const processActivityData = ( Object.entries(day.breakdown[key] || {}).forEach(([model, modelData]) => { if (!modelMetrics[model]) { modelMetrics[model] = { - label: key === "api_keys" ? formatKeyLabel(modelData as KeyMetricWithMetadata, model, teams) : model, + label: key === "api_keys" + ? formatKeyLabel(modelData as KeyMetricWithMetadata, model, teams) + : key === "entities" + ? ((modelData as any).metadata?.agent_name || (modelData as any).metadata?.team_alias || model) + : model, total_requests: 0, total_successful_requests: 0, total_failed_requests: 0, diff --git a/ui/litellm-dashboard/src/components/agents.tsx b/ui/litellm-dashboard/src/components/agents.tsx index 169017ec86..542d890e12 100644 --- a/ui/litellm-dashboard/src/components/agents.tsx +++ b/ui/litellm-dashboard/src/components/agents.tsx @@ -19,19 +19,21 @@ import { isAdminRole } from "@/utils/roles"; import AgentInfoView from "./agents/agent_info"; import NotificationsManager from "./molecules/notifications_manager"; import { Agent, AgentKeyInfo } from "./agents/types"; +import { Team } from "./key_team_helpers/key_list"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton"; interface AgentsPanelProps { accessToken: string | null; userRole?: string; + teams?: Team[] | null; } interface AgentsResponse { agents: Agent[]; } -const AgentsPanel: React.FC = ({ accessToken, userRole }) => { +const AgentsPanel: React.FC = ({ accessToken, userRole, teams }) => { const [agentsList, setAgentsList] = useState([]); const [keyInfoMap, setKeyInfoMap] = useState>({}); const [isAddModalVisible, setIsAddModalVisible] = useState(false); @@ -282,6 +284,7 @@ const AgentsPanel: React.FC = ({ accessToken, userRole }) => { onClose={handleCloseModal} accessToken={accessToken} onSuccess={handleSuccess} + teams={teams} /> {agentToDelete && ( diff --git a/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx b/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx index 0cec0331f4..c5518596b8 100644 --- a/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx +++ b/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx @@ -6,6 +6,7 @@ import CreatedKeyDisplay from "../shared/CreatedKeyDisplay"; import { createAgentCall, getAgentCreateMetadata, + getAgentsList, keyCreateForAgentCall, keyListCall, keyUpdateCall, @@ -14,11 +15,14 @@ import { } from "../networking"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; +import { Team } from "../key_team_helpers/key_list"; +import TeamDropdown from "../common_components/team_dropdown"; import AgentFormFields from "./agent_form_fields"; import DynamicAgentFormFields, { buildDynamicAgentData } from "./dynamic_agent_form_fields"; import { getDefaultFormValues, buildAgentDataFromForm } from "./agent_config"; import MCPServerSelector from "../mcp_server_management/MCPServerSelector"; import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions"; +import GuardrailSelector from "../guardrails/GuardrailSelector"; const { Step } = Steps; @@ -29,6 +33,7 @@ interface AddAgentFormProps { onClose: () => void; accessToken: string | null; onSuccess: () => void; + teams?: Team[] | null; } const AddAgentForm: React.FC = ({ @@ -36,6 +41,7 @@ const AddAgentForm: React.FC = ({ onClose, accessToken, onSuccess, + teams, }) => { const { userId, userRole } = useAuthorized(); const [form] = Form.useForm(); @@ -45,7 +51,7 @@ const AddAgentForm: React.FC = ({ const [agentTypeMetadata, setAgentTypeMetadata] = useState([]); const [loadingMetadata, setLoadingMetadata] = useState(false); - // Step 1: key assignment state + // Step 3: key assignment state const [keyAssignOption, setKeyAssignOption] = useState<"create_new" | "existing_key" | "skip">("create_new"); const [newKeyName, setNewKeyName] = useState(""); const [newKeyModels, setNewKeyModels] = useState([]); @@ -54,8 +60,10 @@ const AddAgentForm: React.FC = ({ const [loadingKeys, setLoadingKeys] = useState(false); const [availableModels, setAvailableModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); + const [availableAgents, setAvailableAgents] = useState<{agent_id: string; agent_name: string}[]>([]); + const [loadingAgents, setLoadingAgents] = useState(false); - // Step 2: results + // Step 4: results const [createdAgentName, setCreatedAgentName] = useState(""); const [createdKeyValue, setCreatedKeyValue] = useState(null); const [assignedKeyAlias, setAssignedKeyAlias] = useState(null); @@ -82,9 +90,9 @@ const AddAgentForm: React.FC = ({ fetchMetadata(); }, []); - // Fetch existing keys when assign key step becomes active (step 2) + // Fetch existing keys when Agent Management step becomes active (step 3) useEffect(() => { - if (currentStep === 2 && accessToken && existingKeys.length === 0) { + if (currentStep === 3 && accessToken && existingKeys.length === 0) { const fetchKeys = async () => { setLoadingKeys(true); try { @@ -100,9 +108,9 @@ const AddAgentForm: React.FC = ({ } }, [currentStep, accessToken]); - // Fetch available models when Assign Key step is active (same list as key generation) + // Fetch available models when Agent Management step is active (same list as key generation) useEffect(() => { - if (currentStep !== 2 || !accessToken || !userId || !userRole) return; + if ((currentStep !== 1 && currentStep !== 3) || !accessToken || !userId || !userRole) return; let cancelled = false; setLoadingModels(true); modelAvailableCall(accessToken, userId, userRole) @@ -125,6 +133,25 @@ const AddAgentForm: React.FC = ({ }; }, [currentStep, accessToken, userId, userRole]); + useEffect(() => { + if (currentStep !== 1 || !accessToken) return; + let cancelled = false; + setLoadingAgents(true); + getAgentsList(accessToken) + .then((response) => { + if (cancelled) return; + const agents = response?.agents ?? []; + setAvailableAgents(agents.map((a: any) => ({ agent_id: a.agent_id, agent_name: a.agent_name }))); + }) + .catch((error) => { + if (!cancelled) console.error("Error fetching agents:", error); + }) + .finally(() => { + if (!cancelled) setLoadingAgents(false); + }); + return () => { cancelled = true; }; + }, [currentStep, accessToken]); + const selectedAgentTypeInfo = agentTypeMetadata.find( (info) => info.agent_type === agentType ); @@ -207,11 +234,14 @@ const AddAgentForm: React.FC = ({ // Build object_permission from MCP Tools step (allowed_mcp_servers_and_groups, mcp_tool_permissions) const mcpServersAndGroups = values.allowed_mcp_servers_and_groups; const mcpToolPermissions = values.mcp_tool_permissions || {}; - if ( - mcpServersAndGroups && - (mcpServersAndGroups.servers?.length > 0 || mcpServersAndGroups.accessGroups?.length > 0) || - Object.keys(mcpToolPermissions).length > 0 - ) { + const entitlementModels = values.entitlement_models || []; + const entitlementAgents = values.entitlement_agents || []; + const hasObjectPermission = + (mcpServersAndGroups?.servers?.length > 0 || mcpServersAndGroups?.accessGroups?.length > 0) || + Object.keys(mcpToolPermissions).length > 0 || + entitlementModels.length > 0 || + entitlementAgents.length > 0; + if (hasObjectPermission) { agentData.object_permission = {}; if (mcpServersAndGroups?.servers?.length > 0) { agentData.object_permission.mcp_servers = mcpServersAndGroups.servers; @@ -222,6 +252,12 @@ const AddAgentForm: React.FC = ({ if (Object.keys(mcpToolPermissions).length > 0) { agentData.object_permission.mcp_tool_permissions = mcpToolPermissions; } + if (entitlementModels.length > 0) { + agentData.object_permission.models = entitlementModels; + } + if (entitlementAgents.length > 0) { + agentData.object_permission.agents = entitlementAgents; + } } // Wire trace-id flags and budget controls into agent litellm_params (before create call) @@ -237,6 +273,17 @@ const AddAgentForm: React.FC = ({ } } + const selectedGuardrails = values.guardrails || []; + if (selectedGuardrails.length > 0) { + if (!agentData.litellm_params) agentData.litellm_params = {}; + agentData.litellm_params.guardrails = selectedGuardrails; + } + + const selectedTeamId = values.team_id || null; + if (selectedTeamId) { + agentData.team_id = selectedTeamId; + } + const agentResponse = await createAgentCall(accessToken, agentData); const agentId: string = agentResponse.agent_id; const agentName: string = agentResponse.agent_name || values.agent_name || agentId; @@ -248,6 +295,8 @@ const AddAgentForm: React.FC = ({ agentId, newKeyName, newKeyModels, + undefined, + selectedTeamId, ); setCreatedKeyValue(keyResponse.key || null); } else if (keyAssignOption === "existing_key") { @@ -264,7 +313,7 @@ const AddAgentForm: React.FC = ({ setAssignedKeyAlias(keyInfo?.key_alias || selectedExistingKey.slice(0, 12) + "…"); } - setCurrentStep(3); + setCurrentStep(4); onSuccess(); } catch (error) { console.error("Error creating agent:", error); @@ -293,11 +342,54 @@ const AddAgentForm: React.FC = ({ onClose(); }; - const renderMCPToolsStep = () => ( + const renderEntitlementsStep = () => (

- Optionally restrict which MCP servers and tools this agent can use. Leave empty to allow all (subject to key/team permissions). + Configure which models, agents, and MCP tools this agent is allowed to use. Leave fields empty to allow all (subject to key/team permissions).

+ + Allowed Models} + name="entitlement_models" + tooltip="Restrict which models this agent can call. Leave empty to allow all." + > + + (option?.label as string ?? "").toLowerCase().includes(input.toLowerCase()) + } + options={availableAgents.map((a) => ({ + label: a.agent_name, + value: a.agent_id, + }))} + /> + + + + @@ -338,122 +430,137 @@ const AddAgentForm: React.FC = ({
)} + + ); - Tracing, - children: ( -
-
-
- - Require x-litellm-trace-id on calls TO this agent - -

- Only accept this agent being invoked with a trace-id (e.g. when used as a sub-agent). -

-
- -
- -
-
- - Require x-litellm-trace-id on calls BY this agent - -

- Requires LLM/MCP calls made by this agent to include x-litellm-trace-id for session tracking. -

-
- { - setRequireTraceIdOutbound(checked); - if (!checked) { - setMaxIterations(null); - setMaxBudgetPerSession(null); - } - }} - /> -
-
- ), - }, - { - key: "budgets_and_rate_limits", - label: Budgets & Rate Limits, - children: ( -
- {!requireTraceIdOutbound && ( -
- Enable "Require x-litellm-trace-id on calls BY this agent" in Tracing to configure budgets and rate limits. -
- )} - -
Session Budgets
-
-
- - setMaxIterations(val)} - /> -

Hard cap on LLM calls per session

-
-
- - setMaxBudgetPerSession(val)} - /> -

Max spend per trace before returning 429

-
-
- - - -
Agent Rate Limits
-

- Global rate limits applied across all callers of this agent. + const renderObservabilityStep = () => ( +

+
+

Tracing

+
+
+
+ + Require x-litellm-trace-id on calls TO this agent + +

+ Only accept this agent being invoked with a trace-id (e.g. when used as a sub-agent).

-
- - - - - - -
- -
Per-Session Rate Limits
-

- Rate limits per session (x-litellm-trace-id). Each session gets its own counters. -

-
- - - - - - -
- ), - }, - ]} /> + +
+ +
+
+ + Require x-litellm-trace-id on calls BY this agent + +

+ Requires LLM/MCP calls made by this agent to include x-litellm-trace-id for session tracking. +

+
+ { + setRequireTraceIdOutbound(checked); + if (!checked) { + setMaxIterations(null); + setMaxBudgetPerSession(null); + } + }} + /> +
+
+
+ + + +
+

Budgets & Rate Limits

+
+ {!requireTraceIdOutbound && ( +
+ Enable "Require x-litellm-trace-id on calls BY this agent" in Tracing to configure budgets and rate limits. +
+ )} + +
Session Budgets
+
+
+ + setMaxIterations(val)} + /> +

Hard cap on LLM calls per session

+
+
+ + setMaxBudgetPerSession(val)} + /> +

Max spend per trace before returning 429

+
+
+ + + +
Agent Rate Limits
+

+ Global rate limits applied across all callers of this agent. +

+
+ + + + + + +
+ +
Per-Session Rate Limits
+

+ Rate limits per session (x-litellm-trace-id). Each session gets its own counters. +

+
+ + + + + + +
+
+
+ + + +
+

Guardrails

+

+ Apply guardrails to this agent. Selected guardrails will run on all calls made by this agent. +

+ + form.setFieldsValue({ guardrails: selected })} + /> + +
); @@ -610,6 +717,19 @@ const AddAgentForm: React.FC = ({
+ Assign to Team} + name="team_id" + tooltip="Optionally assign this agent to a team. The agent and its key will belong to the selected team." + > + + + + +
{/* Option: Create new key */}
= ({ placeholder="e.g. my-agent-key" />
-
- -