Merge pull request #23429 from BerriAI/litellm_dev_03_10_2026_p1
Litellm dev 03 10 2026 p1
This commit is contained in:
commit
7aa5bd3ff3
@ -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[]
|
||||
|
||||
@ -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"})
|
||||
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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[]
|
||||
|
||||
@ -547,7 +547,7 @@ function CreateKeyPageContent() {
|
||||
) : page == "policies" ? (
|
||||
<PoliciesPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "agents" ? (
|
||||
<AgentsPanel accessToken={accessToken} userRole={userRole} />
|
||||
<AgentsPanel accessToken={accessToken} userRole={userRole} teams={teams} />
|
||||
) : page == "prompts" ? (
|
||||
<PromptsPanel accessToken={accessToken} userRole={userRole} />
|
||||
) : page == "transform-request" ? (
|
||||
|
||||
@ -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(<EntityUsage {...defaultProps} entityType="team" />);
|
||||
|
||||
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(<EntityUsage {...defaultProps} entityType="tag" />);
|
||||
|
||||
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(<EntityUsage {...defaultProps} entityType="team" />);
|
||||
|
||||
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(<EntityUsage {...defaultProps} entityType="tag" />);
|
||||
|
||||
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(<EntityUsage {...defaultProps} entityType="team" />);
|
||||
|
||||
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(<EntityUsage {...defaultProps} entityType="tag" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockTagDailyActivityCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(mockAgentDailyActivityCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should switch to Agent Activity tab for team entity type", async () => {
|
||||
render(<EntityUsage {...defaultProps} entityType="team" />);
|
||||
|
||||
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,
|
||||
|
||||
@ -100,11 +100,24 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
|
||||
});
|
||||
const { teams } = useTeams();
|
||||
|
||||
const [agentSpendData, setAgentSpendData] = useState<EntitySpendData>({
|
||||
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<string[]>([]);
|
||||
const [topKeysLimit, setTopKeysLimit] = useState<number>(5);
|
||||
const [topModelsLimit, setTopModelsLimit] = useState<number>(5);
|
||||
const [topAgentsLimit, setTopAgentsLimit] = useState<number>(5);
|
||||
|
||||
const fetchSpendData = async () => {
|
||||
if (!accessToken || !dateValue.from || !dateValue.to) return;
|
||||
@ -171,8 +184,21 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ 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<EntityUsageProps> = ({ 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<EntityUsageProps> = ({ accessToken, entityType, enti
|
||||
<TabList variant="solid" className="mt-1">
|
||||
<Tab>Cost</Tab>
|
||||
<Tab>{entityType === "agent" ? "Request / Token Consumption" : "Model Activity"}</Tab>
|
||||
{entityType === "team" && <Tab>Agent Activity</Tab>}
|
||||
<Tab>Key Activity</Tab>
|
||||
<Tab>Endpoint Activity</Tab>
|
||||
</TabList>
|
||||
@ -621,6 +679,20 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* Top Agents - only for team entity type */}
|
||||
{entityType === "team" && (
|
||||
<Col numColSpan={2}>
|
||||
<Card>
|
||||
<Title>Top Agents Driving Spend</Title>
|
||||
<TopModelView
|
||||
topModels={getTopAgents()}
|
||||
topModelsLimit={topAgentsLimit}
|
||||
setTopModelsLimit={setTopAgentsLimit}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{/* Spend by Provider */}
|
||||
<Col numColSpan={2}>
|
||||
<Card>
|
||||
@ -696,6 +768,11 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
|
||||
<TabPanel>
|
||||
<ActivityMetrics modelMetrics={modelMetrics} hidePromptCachingMetrics={entityType === "agent"} />
|
||||
</TabPanel>
|
||||
{entityType === "team" && (
|
||||
<TabPanel>
|
||||
<ActivityMetrics modelMetrics={agentMetrics} />
|
||||
</TabPanel>
|
||||
)}
|
||||
<TabPanel>
|
||||
<ActivityMetrics modelMetrics={keyMetrics} hidePromptCachingMetrics={entityType === "agent"} />
|
||||
</TabPanel>
|
||||
|
||||
@ -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<string, ModelActivityData> => {
|
||||
const modelMetrics: Record<string, ModelActivityData> = {};
|
||||
@ -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,
|
||||
|
||||
@ -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<AgentsPanelProps> = ({ accessToken, userRole }) => {
|
||||
const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole, teams }) => {
|
||||
const [agentsList, setAgentsList] = useState<Agent[]>([]);
|
||||
const [keyInfoMap, setKeyInfoMap] = useState<Record<string, AgentKeyInfo>>({});
|
||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||
@ -282,6 +284,7 @@ const AgentsPanel: React.FC<AgentsPanelProps> = ({ accessToken, userRole }) => {
|
||||
onClose={handleCloseModal}
|
||||
accessToken={accessToken}
|
||||
onSuccess={handleSuccess}
|
||||
teams={teams}
|
||||
/>
|
||||
|
||||
{agentToDelete && (
|
||||
|
||||
@ -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<AddAgentFormProps> = ({
|
||||
@ -36,6 +41,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
onClose,
|
||||
accessToken,
|
||||
onSuccess,
|
||||
teams,
|
||||
}) => {
|
||||
const { userId, userRole } = useAuthorized();
|
||||
const [form] = Form.useForm();
|
||||
@ -45,7 +51,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
const [agentTypeMetadata, setAgentTypeMetadata] = useState<AgentCreateInfo[]>([]);
|
||||
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<string>("");
|
||||
const [newKeyModels, setNewKeyModels] = useState<string[]>([]);
|
||||
@ -54,8 +60,10 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
const [loadingKeys, setLoadingKeys] = useState(false);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
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<string>("");
|
||||
const [createdKeyValue, setCreatedKeyValue] = useState<string | null>(null);
|
||||
const [assignedKeyAlias, setAssignedKeyAlias] = useState<string | null>(null);
|
||||
@ -82,9 +90,9 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
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<AddAgentFormProps> = ({
|
||||
}
|
||||
}, [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<AddAgentFormProps> = ({
|
||||
};
|
||||
}, [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<AddAgentFormProps> = ({
|
||||
// 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<AddAgentFormProps> = ({
|
||||
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<AddAgentFormProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
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<AddAgentFormProps> = ({
|
||||
agentId,
|
||||
newKeyName,
|
||||
newKeyModels,
|
||||
undefined,
|
||||
selectedTeamId,
|
||||
);
|
||||
setCreatedKeyValue(keyResponse.key || null);
|
||||
} else if (keyAssignOption === "existing_key") {
|
||||
@ -264,7 +313,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
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<AddAgentFormProps> = ({
|
||||
onClose();
|
||||
};
|
||||
|
||||
const renderMCPToolsStep = () => (
|
||||
const renderEntitlementsStep = () => (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
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).
|
||||
</p>
|
||||
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">Allowed Models</span>}
|
||||
name="entitlement_models"
|
||||
tooltip="Restrict which models this agent can call. Leave empty to allow all."
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder={loadingModels ? "Loading models..." : "Select models (leave empty for all)"}
|
||||
tokenSeparators={[","]}
|
||||
loading={loadingModels}
|
||||
showSearch
|
||||
options={availableModels.map((m) => ({
|
||||
label: getModelDisplayName(m),
|
||||
value: m,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">Allowed Agents (Sub-Agents)</span>}
|
||||
name="entitlement_agents"
|
||||
tooltip="Restrict which other agents this agent can invoke as sub-agents. Leave empty to allow all."
|
||||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: "100%" }}
|
||||
placeholder={loadingAgents ? "Loading agents..." : "Select agents (leave empty for all)"}
|
||||
loading={loadingAgents}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label as string ?? "").toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
options={availableAgents.map((a) => ({
|
||||
label: a.agent_name,
|
||||
value: a.agent_id,
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider className="my-2" />
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
@ -338,122 +430,137 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
</div>
|
||||
);
|
||||
|
||||
<Collapse ghost className="mt-6" items={[
|
||||
{
|
||||
key: "tracing",
|
||||
label: <span className="text-sm font-medium text-gray-700">Tracing</span>,
|
||||
children: (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Require x-litellm-trace-id on calls TO this agent
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Only accept this agent being invoked with a trace-id (e.g. when used as a sub-agent).
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={requireTraceIdInbound}
|
||||
onChange={setRequireTraceIdInbound}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Require x-litellm-trace-id on calls BY this agent
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Requires LLM/MCP calls made by this agent to include x-litellm-trace-id for session tracking.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={requireTraceIdOutbound}
|
||||
onChange={(checked) => {
|
||||
setRequireTraceIdOutbound(checked);
|
||||
if (!checked) {
|
||||
setMaxIterations(null);
|
||||
setMaxBudgetPerSession(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "budgets_and_rate_limits",
|
||||
label: <span className="text-sm font-medium text-gray-700">Budgets & Rate Limits</span>,
|
||||
children: (
|
||||
<div className="space-y-4">
|
||||
{!requireTraceIdOutbound && (
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
|
||||
Enable "Require x-litellm-trace-id on calls BY this agent" in Tracing to configure budgets and rate limits.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm font-medium text-gray-700">Session Budgets</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-600 block mb-1">Max Iterations</label>
|
||||
<InputNumber
|
||||
className="w-full"
|
||||
min={1}
|
||||
placeholder="e.g. 25"
|
||||
disabled={!requireTraceIdOutbound}
|
||||
value={maxIterations}
|
||||
onChange={(val) => setMaxIterations(val)}
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">Hard cap on LLM calls per session</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-600 block mb-1">Max Budget Per Session ($)</label>
|
||||
<InputNumber
|
||||
className="w-full"
|
||||
min={0.01}
|
||||
step={0.5}
|
||||
placeholder="e.g. 5.00"
|
||||
disabled={!requireTraceIdOutbound}
|
||||
value={maxBudgetPerSession}
|
||||
onChange={(val) => setMaxBudgetPerSession(val)}
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">Max spend per trace before returning 429</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider className="my-2" />
|
||||
|
||||
<div className="text-sm font-medium text-gray-700">Agent Rate Limits</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Global rate limits applied across all callers of this agent.
|
||||
const renderObservabilityStep = () => (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">Tracing</h4>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Require x-litellm-trace-id on calls TO this agent
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Only accept this agent being invoked with a trace-id (e.g. when used as a sub-agent).
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item label="TPM Limit" name="tpm_limit" className="mb-0">
|
||||
<InputNumber className="w-full" min={0} placeholder="e.g. 100000" disabled={!requireTraceIdOutbound} />
|
||||
</Form.Item>
|
||||
<Form.Item label="RPM Limit" name="rpm_limit" className="mb-0">
|
||||
<InputNumber className="w-full" min={0} placeholder="e.g. 100" disabled={!requireTraceIdOutbound} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="text-sm font-medium text-gray-700 mt-4">Per-Session Rate Limits</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Rate limits per session (x-litellm-trace-id). Each session gets its own counters.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item label="Session TPM Limit" name="session_tpm_limit" className="mb-0">
|
||||
<InputNumber className="w-full" min={0} placeholder="e.g. 10000" disabled={!requireTraceIdOutbound} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Session RPM Limit" name="session_rpm_limit" className="mb-0">
|
||||
<InputNumber className="w-full" min={0} placeholder="e.g. 20" disabled={!requireTraceIdOutbound} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]} />
|
||||
<Switch
|
||||
checked={requireTraceIdInbound}
|
||||
onChange={setRequireTraceIdInbound}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Require x-litellm-trace-id on calls BY this agent
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Requires LLM/MCP calls made by this agent to include x-litellm-trace-id for session tracking.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={requireTraceIdOutbound}
|
||||
onChange={(checked) => {
|
||||
setRequireTraceIdOutbound(checked);
|
||||
if (!checked) {
|
||||
setMaxIterations(null);
|
||||
setMaxBudgetPerSession(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider className="my-0" />
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">Budgets & Rate Limits</h4>
|
||||
<div className="space-y-4">
|
||||
{!requireTraceIdOutbound && (
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
|
||||
Enable "Require x-litellm-trace-id on calls BY this agent" in Tracing to configure budgets and rate limits.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm font-medium text-gray-700">Session Budgets</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-600 block mb-1">Max Iterations</label>
|
||||
<InputNumber
|
||||
className="w-full"
|
||||
min={1}
|
||||
placeholder="e.g. 25"
|
||||
disabled={!requireTraceIdOutbound}
|
||||
value={maxIterations}
|
||||
onChange={(val) => setMaxIterations(val)}
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">Hard cap on LLM calls per session</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-600 block mb-1">Max Budget Per Session ($)</label>
|
||||
<InputNumber
|
||||
className="w-full"
|
||||
min={0.01}
|
||||
step={0.5}
|
||||
placeholder="e.g. 5.00"
|
||||
disabled={!requireTraceIdOutbound}
|
||||
value={maxBudgetPerSession}
|
||||
onChange={(val) => setMaxBudgetPerSession(val)}
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">Max spend per trace before returning 429</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider className="my-2" />
|
||||
|
||||
<div className="text-sm font-medium text-gray-700">Agent Rate Limits</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Global rate limits applied across all callers of this agent.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item label="TPM Limit" name="tpm_limit" className="mb-0">
|
||||
<InputNumber className="w-full" min={0} placeholder="e.g. 100000" disabled={!requireTraceIdOutbound} />
|
||||
</Form.Item>
|
||||
<Form.Item label="RPM Limit" name="rpm_limit" className="mb-0">
|
||||
<InputNumber className="w-full" min={0} placeholder="e.g. 100" disabled={!requireTraceIdOutbound} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="text-sm font-medium text-gray-700 mt-4">Per-Session Rate Limits</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
Rate limits per session (x-litellm-trace-id). Each session gets its own counters.
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Form.Item label="Session TPM Limit" name="session_tpm_limit" className="mb-0">
|
||||
<InputNumber className="w-full" min={0} placeholder="e.g. 10000" disabled={!requireTraceIdOutbound} />
|
||||
</Form.Item>
|
||||
<Form.Item label="Session RPM Limit" name="session_rpm_limit" className="mb-0">
|
||||
<InputNumber className="w-full" min={0} placeholder="e.g. 20" disabled={!requireTraceIdOutbound} />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider className="my-0" />
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-3">Guardrails</h4>
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
Apply guardrails to this agent. Selected guardrails will run on all calls made by this agent.
|
||||
</p>
|
||||
<Form.Item name="guardrails" initialValue={[]}>
|
||||
<GuardrailSelector
|
||||
accessToken={accessToken ?? ""}
|
||||
value={form.getFieldValue("guardrails") ?? []}
|
||||
onChange={(selected: string[]) => form.setFieldsValue({ guardrails: selected })}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -610,6 +717,19 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">Assign to Team</span>}
|
||||
name="team_id"
|
||||
tooltip="Optionally assign this agent to a team. The agent and its key will belong to the selected team."
|
||||
>
|
||||
<TeamDropdown
|
||||
teams={teams}
|
||||
loading={!teams}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider className="my-4" />
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Option: Create new key */}
|
||||
<div
|
||||
@ -645,25 +765,6 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
placeholder="e.g. my-agent-key"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-600 block mb-1">
|
||||
Allowed Models <span className="text-gray-400">(optional — leave empty for all models)</span>
|
||||
</label>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: "100%" }}
|
||||
placeholder={loadingModels ? "Loading models..." : "e.g. gpt-4o, claude-3-5-sonnet"}
|
||||
value={newKeyModels}
|
||||
onChange={setNewKeyModels}
|
||||
tokenSeparators={[","]}
|
||||
loading={loadingModels}
|
||||
showSearch
|
||||
options={availableModels.map((m) => ({
|
||||
label: getModelDisplayName(m),
|
||||
value: m,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -783,8 +884,9 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
{/* Step indicator */}
|
||||
<Steps current={currentStep} size="small" className="mb-8">
|
||||
<Step title="Configure" />
|
||||
<Step title="Agent Settings" />
|
||||
<Step title="Assign Key" />
|
||||
<Step title="Entitlements" />
|
||||
<Step title="Governance" />
|
||||
<Step title="Agent Management" />
|
||||
<Step title="Ready" />
|
||||
</Steps>
|
||||
|
||||
@ -793,21 +895,22 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
layout="vertical"
|
||||
initialValues={
|
||||
agentType === "a2a"
|
||||
? { ...getDefaultFormValues(), allowed_mcp_servers_and_groups: { servers: [], accessGroups: [] }, mcp_tool_permissions: {} }
|
||||
: { allowed_mcp_servers_and_groups: { servers: [], accessGroups: [] }, mcp_tool_permissions: {} }
|
||||
? { ...getDefaultFormValues(), allowed_mcp_servers_and_groups: { servers: [], accessGroups: [] }, mcp_tool_permissions: {}, entitlement_models: [], entitlement_agents: [], guardrails: [] }
|
||||
: { allowed_mcp_servers_and_groups: { servers: [], accessGroups: [] }, mcp_tool_permissions: {}, entitlement_models: [], entitlement_agents: [], guardrails: [] }
|
||||
}
|
||||
className="space-y-4"
|
||||
>
|
||||
{currentStep === 0 && renderConfigureStep()}
|
||||
{currentStep === 1 && renderMCPToolsStep()}
|
||||
{currentStep === 2 && renderAssignKeyStep()}
|
||||
{currentStep === 3 && renderReadyStep()}
|
||||
{currentStep === 1 && renderEntitlementsStep()}
|
||||
{currentStep === 2 && renderObservabilityStep()}
|
||||
{currentStep === 3 && renderAssignKeyStep()}
|
||||
{currentStep === 4 && renderReadyStep()}
|
||||
</Form>
|
||||
|
||||
{/* Footer navigation */}
|
||||
<div className="flex items-center justify-between pt-6 border-t border-gray-100 mt-6">
|
||||
<div>
|
||||
{currentStep > 0 && currentStep < 3 && (
|
||||
{currentStep > 0 && currentStep < 4 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
@ -818,7 +921,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{currentStep < 3 && (
|
||||
{currentStep < 4 && (
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
@ -834,11 +937,16 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === 2 && (
|
||||
<Button variant="primary" onClick={handleNext}>
|
||||
Next →
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
<Button variant="primary" loading={isSubmitting} onClick={handleCreateAgent}>
|
||||
{isSubmitting ? "Creating..." : "Create Agent →"}
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === 3 && (
|
||||
{currentStep === 4 && (
|
||||
<Button variant="primary" onClick={handleClose}>
|
||||
Done
|
||||
</Button>
|
||||
|
||||
@ -907,6 +907,7 @@ export const keyCreateForAgentCall = async (
|
||||
keyAlias: string,
|
||||
models: string[],
|
||||
metadata?: Record<string, any>,
|
||||
teamId?: string | null,
|
||||
) => {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/key/generate` : `/key/generate`;
|
||||
const body: Record<string, any> = {
|
||||
@ -914,6 +915,9 @@ export const keyCreateForAgentCall = async (
|
||||
key_alias: keyAlias,
|
||||
models: models.length > 0 ? models : [],
|
||||
};
|
||||
if (teamId) {
|
||||
body.team_id = teamId;
|
||||
}
|
||||
if (metadata && Object.keys(metadata).length > 0) {
|
||||
body.metadata = metadata;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user