diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json
index e0d553ac384..d9efd3808ab 100644
--- a/ui/litellm-dashboard/.eslintrc.json
+++ b/ui/litellm-dashboard/.eslintrc.json
@@ -52,6 +52,7 @@
"src/components/common_components/AutoRotationView.tsx",
"src/components/agents/agent_card.tsx",
"src/components/agents/agent_table.tsx",
+ "src/components/agents/add_agent_form.tsx",
"src/components/view_users/columns.tsx",
"src/components/Projects/ProjectsPage.tsx",
"src/components/pass_through_settings.tsx",
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 891e9c574d8..826bd123a81 100644
--- a/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx
+++ b/ui/litellm-dashboard/src/components/agents/add_agent_form.tsx
@@ -1,9 +1,47 @@
import React, { useState, useEffect } from "react";
-import { Modal, Form, Select, Input, Steps, Radio, Tag, Divider, Switch, InputNumber, Collapse } from "antd";
-// eslint-disable-next-line litellm-ui/no-banned-ui-imports
+import {
+ Controller,
+ FormProvider,
+ useForm,
+ UseFormReturn,
+} from "react-hook-form";
import MessageManager from "@/components/molecules/message_manager";
-import { Button } from "@tremor/react";
-import { CheckCircle2 as CheckCircleFilled, Key as KeyOutlined, Bot as RobotOutlined, LayoutGrid as AppstoreOutlined, Info as InfoCircleOutlined } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Separator } from "@/components/ui/separator";
+import { Switch } from "@/components/ui/switch";
+import { Textarea } from "@/components/ui/textarea";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import {
+ Check,
+ CheckCircle2,
+ Info,
+ Key as KeyIcon,
+ LayoutGrid,
+ Bot,
+ X,
+} from "lucide-react";
import CreatedKeyDisplay from "../shared/CreatedKeyDisplay";
import {
createAgentCall,
@@ -26,8 +64,6 @@ import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
import GuardrailSelector from "../guardrails/GuardrailSelector";
-const { Step } = Steps;
-
const CUSTOM_AGENT_TYPE = "custom";
interface AddAgentFormProps {
@@ -38,45 +74,310 @@ interface AddAgentFormProps {
teams?: Team[] | null;
}
+const STEP_LABELS = [
+ "Configure",
+ "Entitlements",
+ "Governance",
+ "Agent Management",
+ "Ready",
+];
+
+const InfoTip: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+
+
+
+
+ {children}
+
+
+);
+
+function Stepper({ current, steps }: { current: number; steps: string[] }) {
+ return (
+
+ {steps.map((label, i) => {
+ const active = i === current;
+ const completed = i < current;
+ return (
+
+
+ {completed ? : i + 1}
+
+
+ {label}
+
+ {i < steps.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+ );
+}
+
+/**
+ * Shadcn-Select-backed multi-select with chip list under the trigger. Shares
+ * the pattern with AccessGroupBaseForm.tsx.
+ */
+function ChipMultiSelect({
+ value,
+ onChange,
+ options,
+ placeholder,
+ emptyText = "No options",
+ loading = false,
+}: {
+ value: string[];
+ onChange: (next: string[]) => void;
+ options: { label: string; value: string }[];
+ placeholder: string;
+ emptyText?: string;
+ loading?: boolean;
+}) {
+ const selected = value ?? [];
+ const remaining = options.filter((o) => !selected.includes(o.value));
+ return (
+
+
{
+ if (v) onChange([...selected, v]);
+ }}
+ >
+
+
+
+
+ {loading ? (
+
+ Loading…
+
+ ) : remaining.length === 0 ? (
+
+ {emptyText}
+
+ ) : (
+ remaining.map((opt) => (
+
+ {opt.label}
+
+ ))
+ )}
+
+
+ {selected.length > 0 && (
+
+ {selected.map((v) => {
+ const opt = options.find((o) => o.value === v);
+ return (
+
+ {opt?.label ?? v}
+ onChange(selected.filter((s) => s !== v))}
+ className="inline-flex items-center justify-center rounded-full hover:bg-muted-foreground/20"
+ aria-label={`Remove ${opt?.label ?? v}`}
+ >
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+/**
+ * Free-form tag entry (models entitlement accepts arbitrary strings).
+ * Variant that also accepts an options list for suggestions.
+ */
+function FreeformTagsWithOptions({
+ value,
+ onChange,
+ options,
+ placeholder,
+ loading = false,
+}: {
+ value: string[];
+ onChange: (next: string[]) => void;
+ options: { label: string; value: string }[];
+ placeholder: string;
+ loading?: boolean;
+}) {
+ const [draft, setDraft] = useState("");
+ const selected = value ?? [];
+ const remaining = options.filter((o) => !selected.includes(o.value));
+ return (
+
+
+
{
+ const v = e.target.value;
+ if (v.includes(",")) {
+ const parts = v
+ .split(",")
+ .map((s) => s.trim())
+ .filter((s) => s && !selected.includes(s));
+ if (parts.length > 0) onChange([...selected, ...parts]);
+ setDraft("");
+ } else {
+ setDraft(v);
+ }
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ const v = draft.trim();
+ if (v && !selected.includes(v)) {
+ onChange([...selected, v]);
+ }
+ setDraft("");
+ }
+ }}
+ onBlur={() => {
+ const v = draft.trim();
+ if (v && !selected.includes(v)) {
+ onChange([...selected, v]);
+ }
+ setDraft("");
+ }}
+ />
+
{
+ if (v) onChange([...selected, v]);
+ }}
+ >
+
+
+
+
+ {loading ? (
+
+ Loading…
+
+ ) : remaining.length === 0 ? (
+
+ No options
+
+ ) : (
+ remaining.map((opt) => (
+
+ {opt.label}
+
+ ))
+ )}
+
+
+
+ {selected.length > 0 && (
+
+ {selected.map((v) => {
+ const opt = options.find((o) => o.value === v);
+ return (
+
+ {opt?.label ?? v}
+ onChange(selected.filter((s) => s !== v))}
+ className="inline-flex items-center justify-center rounded-full hover:bg-muted-foreground/20"
+ aria-label={`Remove ${opt?.label ?? v}`}
+ >
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+type FormValues = Record;
+
+const getInitialFormValues = () => ({
+ ...getDefaultFormValues(),
+ allowed_mcp_servers_and_groups: { servers: [], accessGroups: [] },
+ mcp_tool_permissions: {},
+ entitlement_models: [],
+ entitlement_agents: [],
+ guardrails: [],
+});
+
const AddAgentForm: React.FC = ({
visible,
onClose,
accessToken,
onSuccess,
- teams,
}) => {
const { userId, userRole } = useAuthorized();
- const [form] = Form.useForm();
+ const form: UseFormReturn = useForm({
+ defaultValues: getInitialFormValues(),
+ });
+ const { register, control, watch, trigger, getValues, reset, formState } =
+ form;
const [currentStep, setCurrentStep] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [agentType, setAgentType] = useState("a2a");
const [agentTypeMetadata, setAgentTypeMetadata] = useState([]);
- const [loadingMetadata, setLoadingMetadata] = useState(false);
+ const [, setLoadingMetadata] = useState(false);
- // Step 3: key assignment state
const [keyAssignOption, setKeyAssignOption] = useState<"create_new" | "existing_key" | "skip">("create_new");
const [newKeyName, setNewKeyName] = useState("");
- const [newKeyModels, setNewKeyModels] = useState([]);
+ const [newKeyModels] = useState([]);
const [existingKeys, setExistingKeys] = useState([]);
const [selectedExistingKey, setSelectedExistingKey] = useState(null);
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 [availableAgents, setAvailableAgents] = useState<
+ { agent_id: string; agent_name: string }[]
+ >([]);
const [loadingAgents, setLoadingAgents] = useState(false);
- // Step 4: results
const [createdAgentName, setCreatedAgentName] = useState("");
const [createdKeyValue, setCreatedKeyValue] = useState(null);
const [assignedKeyAlias, setAssignedKeyAlias] = useState(null);
- // Tracing & guardrails state
const [requireTraceIdInbound, setRequireTraceIdInbound] = useState(false);
const [requireTraceIdOutbound, setRequireTraceIdOutbound] = useState(false);
const [maxIterations, setMaxIterations] = useState(null);
const [maxBudgetPerSession, setMaxBudgetPerSession] = useState(null);
- // Fetch agent type metadata on mount
useEffect(() => {
const fetchMetadata = async () => {
setLoadingMetadata(true);
@@ -92,7 +393,6 @@ const AddAgentForm: React.FC = ({
fetchMetadata();
}, []);
- // Fetch existing keys when Agent Management step becomes active (step 3)
useEffect(() => {
if (currentStep === 3 && accessToken && existingKeys.length === 0) {
const fetchKeys = async () => {
@@ -108,9 +408,8 @@ const AddAgentForm: React.FC = ({
};
fetchKeys();
}
- }, [currentStep, accessToken]);
+ }, [currentStep, accessToken, existingKeys.length]);
- // Fetch available models when Agent Management step is active (same list as key generation)
useEffect(() => {
if ((currentStep !== 1 && currentStep !== 3) || !accessToken || !userId || !userRole) return;
let cancelled = false;
@@ -155,29 +454,26 @@ const AddAgentForm: React.FC = ({
}, [currentStep, accessToken]);
const selectedAgentTypeInfo = agentTypeMetadata.find(
- (info) => info.agent_type === agentType
+ (info) => info.agent_type === agentType,
);
const handleNext = async () => {
- try {
- if (currentStep === 0) {
- await form.validateFields(["agent_name"]);
- const agentName = form.getFieldValue("agent_name");
- if (agentName && !newKeyName) {
- setNewKeyName(`${agentName}-key`);
- }
+ if (currentStep === 0) {
+ const ok = await trigger(["agent_name"]);
+ if (!ok) return;
+ const agentName = getValues("agent_name");
+ if (agentName && !newKeyName) {
+ setNewKeyName(`${agentName}-key`);
}
- setCurrentStep((s) => s + 1);
- } catch {
- // validation failed — stay on current step
}
+ setCurrentStep((s) => s + 1);
};
const handleBack = () => {
setCurrentStep((s) => Math.max(0, s - 1));
};
- const buildAgentData = (values: any) => {
+ const buildAgentData = (values: FormValues): any => {
if (agentType === CUSTOM_AGENT_TYPE) {
return {
agent_name: values.agent_name,
@@ -206,6 +502,7 @@ const AddAgentForm: React.FC = ({
for (const field of selectedAgentTypeInfo.credential_fields) {
const value = values[field.key];
if (value && field.include_in_litellm_params !== false) {
+ if (!agentData.litellm_params) agentData.litellm_params = {};
agentData.litellm_params[field.key] = value;
}
}
@@ -224,8 +521,12 @@ const AddAgentForm: React.FC = ({
setIsSubmitting(true);
try {
- await form.validateFields();
- const values = { ...form.getFieldsValue(true) };
+ const ok = await trigger();
+ if (!ok) {
+ setIsSubmitting(false);
+ return;
+ }
+ const values = { ...getValues() };
const agentData = buildAgentData(values);
if (!agentData) {
MessageManager.error("Failed to build agent data");
@@ -233,7 +534,6 @@ const AddAgentForm: React.FC = ({
return;
}
- // 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 || {};
const entitlementModels = values.entitlement_models || [];
@@ -262,7 +562,6 @@ const AddAgentForm: React.FC = ({
}
}
- // Wire trace-id flags and budget controls into agent litellm_params (before create call)
if (requireTraceIdInbound || requireTraceIdOutbound) {
if (!agentData.litellm_params) agentData.litellm_params = {};
if (requireTraceIdInbound) {
@@ -327,12 +626,11 @@ const AddAgentForm: React.FC = ({
};
const handleClose = () => {
- form.resetFields();
+ reset(getInitialFormValues());
setAgentType("a2a");
setCurrentStep(0);
setKeyAssignOption("create_new");
setNewKeyName("");
- setNewKeyModels([]);
setSelectedExistingKey(null);
setCreatedAgentName("");
setCreatedKeyValue(null);
@@ -344,129 +642,319 @@ const AddAgentForm: React.FC = ({
onClose();
};
+ const handleAgentTypeChange = (value: string) => {
+ setAgentType(value);
+ reset(getInitialFormValues());
+ };
+
+ const isCustomAgent = agentType === CUSTOM_AGENT_TYPE;
+ const selectedLogo = isCustomAgent
+ ? null
+ : selectedAgentTypeInfo?.logo_url ||
+ agentTypeMetadata.find((a) => a.agent_type === "a2a")?.logo_url;
+
+ const agentNameError = (formState.errors as any)?.agent_name;
+
+ const renderConfigureStep = () => (
+ <>
+
+
+ Agent Type *
+ Select the type of agent you want to create
+
+
+
+
+
+
+ {agentTypeMetadata.map((info) => (
+
+
+
+
+
+ {info.agent_type_display_name}
+
+ {info.description && (
+
+ {info.description}
+
+ )}
+
+
+
+ ))}
+
+
+ Not listed?
+
+
+
+
+
+
+
+ Custom / Other
+
+
+ GENERIC
+
+
+
+ For agents that don't follow a standard protocol —
+ just needs a virtual key
+
+
+
+
+
+
+
+
+
+
+ {agentType === CUSTOM_AGENT_TYPE ? (
+
+
+
+ Agent Name *
+
+
+ {agentNameError && (
+
+ {agentNameError.message as string}
+
+ )}
+
+
+ Description
+
+
+
+ ) : agentType === "a2a" ? (
+
+ ) : selectedAgentTypeInfo?.use_a2a_form_fields ? (
+ <>
+
+ {selectedAgentTypeInfo.credential_fields.length > 0 && (
+
+
+ {selectedAgentTypeInfo.agent_type_display_name} Settings
+
+
+ {selectedAgentTypeInfo.credential_fields.map((field) => {
+ const fieldError = (formState.errors as any)?.[field.key];
+ const inputId = `credential-${field.key}`;
+ return (
+
+
+ {field.label}
+ {field.required && (
+ *
+ )}
+ {field.tooltip ? (
+ {field.tooltip}
+ ) : null}
+
+
+ {fieldError && (
+
+ {fieldError.message as string}
+
+ )}
+
+ );
+ })}
+
+
+ )}
+ >
+ ) : selectedAgentTypeInfo ? (
+
+ ) : null}
+
+ >
+ );
+
const renderEntitlementsStep = () => (
-
- Configure which models, agents, and MCP tools this agent is allowed to use. Leave fields 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."
- >
- ({
- label: getModelDisplayName(m),
- value: m,
- }))}
- />
-
-
-
Allowed Agents (Sub-Agents)}
- name="entitlement_agents"
- tooltip="Restrict which other agents this agent can invoke as sub-agents. 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,
- }))}
- />
-
-
-
-
-
- Allowed MCP Servers{" "}
-
-
- }
- name="allowed_mcp_servers_and_groups"
- initialValue={{ servers: [], accessGroups: [] }}
- >
-
- form.setFieldValue("allowed_mcp_servers_and_groups", val)
- }
- value={form.getFieldValue("allowed_mcp_servers_and_groups") || { servers: [], accessGroups: [] }}
- accessToken={accessToken ?? ""}
- placeholder="Select MCP servers or access groups (optional)"
- />
-
-
-
-
-
- prev.allowed_mcp_servers_and_groups !== curr.allowed_mcp_servers_and_groups ||
- prev.mcp_tool_permissions !== curr.mcp_tool_permissions
- }
- >
- {() => (
-
-
) => form.setFieldsValue({ mcp_tool_permissions: toolPerms })}
+
+
+ Allowed Models
+
+ Restrict which models this agent can call. Leave empty to allow
+ all.
+
+
+ (
+ ({
+ label: getModelDisplayName(m),
+ value: m,
+ }))}
+ placeholder={
+ loadingModels
+ ? "Loading models..."
+ : "Type or pick models (leave empty for all)"
+ }
+ loading={loadingModels}
/>
-
- )}
-
+ )}
+ />
+
+
+
+
+ Allowed Agents (Sub-Agents)
+
+ Restrict which other agents this agent can invoke as sub-agents.
+ Leave empty to allow all.
+
+
+ (
+ ({
+ label: a.agent_name,
+ value: a.agent_id,
+ }))}
+ placeholder={
+ loadingAgents
+ ? "Loading agents..."
+ : "Select agents (leave empty for all)"
+ }
+ loading={loadingAgents}
+ />
+ )}
+ />
+
+
+
+
+
+
+ Allowed MCP Servers
+
+ Select which MCP servers or access groups this agent can access
+
+
+ (
+
+ field.onChange(val)
+ }
+ value={field.value || { servers: [], accessGroups: [] }}
+ accessToken={accessToken ?? ""}
+ placeholder="Select MCP servers or access groups (optional)"
+ />
+ )}
+ />
+
+
+ {
+ const selectedServers =
+ watch("allowed_mcp_servers_and_groups")?.servers ?? [];
+ return (
+
+ ) =>
+ field.onChange(toolPerms)
+ }
+ />
+
+ );
+ }}
+ />
);
const renderObservabilityStep = () => (
-
Tracing
+
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).
+
+ 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.
+
+ Requires LLM/MCP calls made by this agent to include
+ x-litellm-trace-id for session tracking.
{
+ onCheckedChange={(checked) => {
setRequireTraceIdOutbound(checked);
if (!checked) {
setMaxIterations(null);
@@ -478,351 +966,319 @@ const AddAgentForm: React.FC = ({
-
+
-
Budgets & Rate Limits
+
+ Budgets & Rate Limits
+
{!requireTraceIdOutbound && (
- Enable "Require x-litellm-trace-id on calls BY this agent" in Tracing to configure budgets and rate limits.
+ Enable "Require x-litellm-trace-id on calls BY this
+ agent" in Tracing to configure budgets and rate limits.
)}
-
Session Budgets
+
+ Session Budgets
+
-
Max Iterations
-
+ Max Iterations
+
+ setMaxIterations(val)}
+ value={maxIterations ?? ""}
+ onChange={(e) =>
+ setMaxIterations(
+ e.target.value === "" ? null : Number(e.target.value),
+ )
+ }
/>
- Hard cap on LLM calls per session
+
+ Hard cap on LLM calls per session
+
-
Max Budget Per Session ($)
-
+ Max Budget Per Session ($)
+
+ setMaxBudgetPerSession(val)}
+ value={maxBudgetPerSession ?? ""}
+ onChange={(e) =>
+ setMaxBudgetPerSession(
+ e.target.value === "" ? null : Number(e.target.value),
+ )
+ }
/>
- Max spend per trace before returning 429
+
+ Max spend per trace before returning 429
+
-
+
-
Agent Rate Limits
-
+
+ Agent Rate Limits
+
+
Global rate limits applied across all callers of this agent.
-
-
-
-
-
-
+
+ TPM Limit
+
+
+
+ RPM Limit
+
+
-
Per-Session Rate Limits
-
- Rate limits per session (x-litellm-trace-id). Each session gets its own counters.
+
+ Per-Session Rate Limits
+
+
+ Rate limits per session (x-litellm-trace-id). Each session gets
+ its own counters.
-
-
-
-
-
-
+
+ Session TPM Limit
+
+
+
+ Session RPM Limit
+
+
-
+
-
Guardrails
-
- Apply guardrails to this agent. Selected guardrails will run on all calls made by this agent.
+
+ Guardrails
+
+
+ Apply guardrails to this agent. Selected guardrails will run on all
+ calls made by this agent.
-
- form.setFieldsValue({ guardrails: selected })}
- />
-
+
(
+ field.onChange(selected)}
+ />
+ )}
+ />
);
- const handleAgentTypeChange = (value: string) => {
- setAgentType(value);
- form.resetFields();
- };
-
- const isCustomAgent = agentType === CUSTOM_AGENT_TYPE;
- const selectedLogo = isCustomAgent
- ? null
- : selectedAgentTypeInfo?.logo_url ||
- agentTypeMetadata.find((a) => a.agent_type === "a2a")?.logo_url;
-
- const renderConfigureStep = () => (
- <>
-
Agent Type}
- required
- tooltip="Select the type of agent you want to create"
- >
- (
- <>
- {menu}
-
-
-
- Not listed?
-
-
handleAgentTypeChange(CUSTOM_AGENT_TYPE)}
- >
-
-
-
- Custom / Other
- GENERIC
-
-
- For agents that don't follow a standard protocol — just needs a virtual key
-
-
-
-
- >
- )}
- >
- {agentTypeMetadata.map((info) => (
-
-
- {info.agent_type_display_name}
-
- }
- >
-
-
-
-
{info.agent_type_display_name}
- {info.description && (
-
{info.description}
- )}
-
-
-
- ))}
-
-
-
-
- {agentType === CUSTOM_AGENT_TYPE ? (
-
-
-
-
-
-
-
-
- ) : agentType === "a2a" ? (
-
- ) : selectedAgentTypeInfo?.use_a2a_form_fields ? (
- <>
-
- {selectedAgentTypeInfo.credential_fields.length > 0 && (
-
-
- {selectedAgentTypeInfo.agent_type_display_name} Settings
-
- {selectedAgentTypeInfo.credential_fields.map((field) => (
-
- {field.field_type === "password" ? (
-
- ) : (
-
- )}
-
- ))}
-
- )}
- >
- ) : selectedAgentTypeInfo ? (
-
- ) : null}
-
-
- >
- );
-
const renderAssignKeyStep = () => {
- const agentName = form.getFieldValue("agent_name") || "your-agent";
+ const agentName = watch("agent_name") || "your-agent";
return (
- {/* Agent name chip */}
- } color="purple" className="px-3 py-1 text-sm">
+
+
{agentName}
-
+
-
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."
+
+
+ Assign to Team
+
+ Optionally assign this agent to a team. The agent and its key
+ will belong to the selected team.
+
+
+ (
+
+ )}
+ />
+
+
+
+
+
+ setKeyAssignOption(v as "create_new" | "existing_key" | "skip")
+ }
>
-
-
+
+
setKeyAssignOption("create_new")}
+ >
+
+
+
+
+
+
+
+ Create a new key for this agent
+
+
+
+ A dedicated key scoped to this agent.
+
+ {keyAssignOption === "create_new" && (
+
e.stopPropagation()}
+ >
+
+
+ Key Name
+
+ setNewKeyName(e.target.value)}
+ placeholder="e.g. my-agent-key"
+ />
+
+
+ )}
+
+
+
+ Recommended
+
+
+
-
-
-
- {/* Option: Create new key */}
-
setKeyAssignOption("create_new")}
- >
-
-
-
setKeyAssignOption("create_new")}
+ setKeyAssignOption("existing_key")}
+ >
+
+
-
- Create a new key for this agent
+
+
+ Assign an existing key
+
-
- A dedicated key scoped to this agent.
+
+ Re-assign a key you already have to this agent.
- {keyAssignOption === "create_new" && (
-
e.stopPropagation()}>
-
- Key Name
- setNewKeyName(e.target.value)}
- placeholder="e.g. my-agent-key"
- />
-
+ {keyAssignOption === "existing_key" && (
+
e.stopPropagation()}
+ >
+ setSelectedExistingKey(v)}
+ >
+
+
+
+
+ {existingKeys.map((k) => (
+
+ {k.key_alias || k.token?.slice(0, 12) + "…"}
+
+ ))}
+
+
)}
-
Recommended
-
- {/* Option: Assign existing key */}
- setKeyAssignOption("existing_key")}
- >
-
-
setKeyAssignOption("existing_key")}
- />
-
-
-
- Assign an existing key
-
-
- Re-assign a key you already have to this agent.
-
- {keyAssignOption === "existing_key" && (
-
e.stopPropagation()}>
- setSelectedExistingKey(value)}
- filterOption={(input, option) =>
- (option?.label as string ?? "").toLowerCase().includes(input.toLowerCase())
- }
- options={existingKeys.map((k) => ({
- label: k.key_alias || k.token?.slice(0, 12) + "…",
- value: k.token,
- }))}
- />
-
- )}
-
-
-
-
+
setKeyAssignOption("skip")}
>
Skip for now — I'll assign a key later
@@ -834,12 +1290,21 @@ const AddAgentForm: React.FC = ({
const renderReadyStep = () => (
-
-
Agent Created!
+
+
+ Agent Created!
+
- } color="purple" className="px-3 py-1 text-sm">
+
+
{createdAgentName}
-
+
{createdKeyValue && (
@@ -847,12 +1312,13 @@ const AddAgentForm: React.FC
= ({
)}
{assignedKeyAlias && (
-
- Key {assignedKeyAlias} has been assigned to this agent.
+
+ Key {assignedKeyAlias} has been
+ assigned to this agent.
)}
{!createdKeyValue && !assignedKeyAlias && keyAssignOption === "skip" && (
-
+
No key assigned. You can create one from the Virtual Keys page.
)}
@@ -860,100 +1326,76 @@ const AddAgentForm: React.FC
= ({
);
return (
-
- {selectedLogo && currentStep < 1 && (
-
- )}
- Add New Agent
-
- }
+ {
+ if (!open) handleClose();
}}
>
-
- {/* Step indicator */}
-
-
-
-
-
-
-
+
+
+
+
+ {selectedLogo && currentStep < 1 && (
+
+ )}
+
+ Add New Agent
+
+
+
+
+
+
-
+
+
+
- {/* Footer navigation */}
-
-
- {currentStep > 0 && currentStep < 4 && (
-
- ← Back
-
- )}
-
-
- {currentStep < 4 && (
-
- Cancel
-
- )}
- {currentStep === 0 && (
-
- Next →
-
- )}
- {currentStep === 1 && (
-
- Next →
-
- )}
- {currentStep === 2 && (
-
- Next →
-
- )}
- {currentStep === 3 && (
-
- {isSubmitting ? "Creating..." : "Create Agent →"}
-
- )}
- {currentStep === 4 && (
-
- Done
-
- )}
+
+
+ {currentStep > 0 && currentStep < 4 && (
+
+ ← Back
+
+ )}
+
+
+ {currentStep < 4 && (
+
+ Cancel
+
+ )}
+ {currentStep === 0 && (
+ Next →
+ )}
+ {currentStep === 1 && (
+ Next →
+ )}
+ {currentStep === 2 && (
+ Next →
+ )}
+ {currentStep === 3 && (
+
+ {isSubmitting ? "Creating..." : "Create Agent →"}
+
+ )}
+ {currentStep === 4 && Done }
+
-
-
+
+
);
};
diff --git a/ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx b/ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx
index d9bfa70f590..9f835c595d6 100644
--- a/ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx
+++ b/ui/litellm-dashboard/src/components/agents/agent_form_fields.tsx
@@ -1,7 +1,16 @@
-import React from "react";
-import { Form, Switch, Collapse, Select } from "antd";
+import React, { useState } from "react";
+import { Controller, useFieldArray, useFormContext } from "react-hook-form";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from "@/components/ui/accordion";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
@@ -9,13 +18,10 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
-import { Info, MinusCircle, Plus } from "lucide-react";
+import { Info, MinusCircle, Plus, X } from "lucide-react";
import { AGENT_FORM_CONFIG, SKILL_FIELD_CONFIG } from "./agent_config";
-
import CostConfigFields from "./cost_config_fields";
-const { Panel } = Collapse;
-
const InfoTip: React.FC<{ children: React.ReactNode }> = ({ children }) => (
@@ -32,286 +38,573 @@ interface AgentFormFieldsProps {
visiblePanels?: string[];
}
+interface FieldProps {
+ name: string;
+ label: string;
+ required?: boolean;
+ tooltip?: string;
+ placeholder?: string;
+ type?: string;
+ rows?: number;
+}
+
+function TextFieldItem({
+ name,
+ label,
+ required,
+ tooltip,
+ placeholder,
+ type,
+ rows,
+}: FieldProps) {
+ const { register, formState } = useFormContext();
+ const error = (formState.errors as any)?.[name];
+ const inputId = `agent-field-${name}`;
+ return (
+
+
+ {label}
+ {required && * }
+ {tooltip ? {tooltip} : null}
+
+ {type === "textarea" ? (
+
+ ) : (
+
+ )}
+ {error && (
+
{error.message as string}
+ )}
+
+ );
+}
+
+function SwitchFieldItem({ name, label }: { name: string; label: string }) {
+ const { control } = useFormContext();
+ return (
+
+ {label}
+ (
+
+ )}
+ />
+
+ );
+}
+
+/**
+ * Comma-separated tag field. Stored as string[] in the form, rendered as a
+ * single text input that splits on commas, consistent with the prior antd
+ * `getValueFromEvent` behavior.
+ */
+function CommaListField({
+ name,
+ label,
+ required,
+ placeholder,
+}: {
+ name: string;
+ label: string;
+ required?: boolean;
+ placeholder?: string;
+}) {
+ const { control, formState } = useFormContext();
+ const error = (formState.errors as any)?.[name.split(".")[0]];
+ return (
+
+
+ {label}
+ {required && * }
+
+
+ Array.isArray(v) && v.length > 0 && v.some((s) => s?.trim())
+ ? true
+ : "Required",
+ }
+ : undefined
+ }
+ render={({ field }) => (
+ {
+ const raw = e.target.value;
+ field.onChange(
+ raw
+ .split(",")
+ .map((s) => s.trim())
+ .filter((s) => s !== "" || required !== true),
+ );
+ }}
+ />
+ )}
+ />
+ {error && (
+
+ {(error as any).message as string}
+
+ )}
+
+ );
+}
+
+function SkillsList() {
+ const { control } = useFormContext();
+ const { fields, append, remove } = useFieldArray({
+ control,
+ name: "skills",
+ });
+ return (
+
+ {fields.map((f, index) => (
+
+
+
+
+
+
+ remove(index)}
+ >
+
+ Remove Skill
+
+
+ ))}
+
+ append({ id: "", name: "", description: "", tags: [], examples: [] })
+ }
+ className="w-full border-dashed"
+ >
+
+ Add Skill
+
+
+ );
+}
+
+function StaticHeadersList() {
+ const { control, register } = useFormContext();
+ const { fields, append, remove } = useFieldArray({
+ control,
+ name: "static_headers",
+ });
+ return (
+
+ );
+}
+
+/**
+ * Free-form tag input (replacement for `antd.Select mode="tags"`). Stores a
+ * string[] in form state; users type a value and press Enter/comma to add.
+ */
+function FreeformTagsField({
+ name,
+ placeholder,
+}: {
+ name: string;
+ placeholder?: string;
+}) {
+ const { control } = useFormContext();
+ const [draft, setDraft] = useState("");
+ return (
+ {
+ const values: string[] = Array.isArray(field.value) ? field.value : [];
+ const commitDraft = () => {
+ const parts = draft
+ .split(",")
+ .map((s) => s.trim())
+ .filter((s) => s && !values.includes(s));
+ if (parts.length > 0) {
+ field.onChange([...values, ...parts]);
+ }
+ setDraft("");
+ };
+ return (
+
+
{
+ const v = e.target.value;
+ if (v.includes(",")) {
+ const parts = v
+ .split(",")
+ .map((s) => s.trim())
+ .filter((s) => s && !values.includes(s));
+ if (parts.length > 0) {
+ field.onChange([...values, ...parts]);
+ }
+ setDraft("");
+ } else {
+ setDraft(v);
+ }
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ commitDraft();
+ }
+ }}
+ onBlur={commitDraft}
+ />
+ {values.length > 0 && (
+
+ {values.map((v) => (
+
+ {v}
+
+ field.onChange(values.filter((x) => x !== v))
+ }
+ className="inline-flex items-center justify-center rounded-full hover:bg-muted-foreground/20"
+ aria-label={`Remove ${v}`}
+ >
+
+
+
+ ))}
+
+ )}
+
+ );
+ }}
+ />
+ );
+}
+
/**
* Reusable form fields component for agent forms
* Uses shared configuration from agent_config.ts
*/
-const AgentFormFields: React.FC = ({ showAgentName = true, visiblePanels }) => {
- const shouldShow = (key: string) => !visiblePanels || visiblePanels.includes(key);
+const AgentFormFields: React.FC = ({
+ showAgentName = true,
+ visiblePanels,
+}) => {
+ const shouldShow = (key: string) =>
+ !visiblePanels || visiblePanels.includes(key);
+
+ const defaultOpen: string[] = ["basic"];
+
return (
- <>
+
{showAgentName && (
-
-
-
+ placeholder="e.g., customer-support-agent"
+ />
)}
-
+
{/* Basic Information */}
{shouldShow(AGENT_FORM_CONFIG.basic.key) && (
-
- {AGENT_FORM_CONFIG.basic.fields.map((field) => (
-
- {field.type === 'textarea' ? (
-
- ) : (
-
- )}
-
- ))}
-
+
+
+ {AGENT_FORM_CONFIG.basic.title} (Required)
+
+
+
+ {AGENT_FORM_CONFIG.basic.fields.map((field) => (
+
+ ))}
+
+
+
)}
{/* Skills */}
{shouldShow(AGENT_FORM_CONFIG.skills.key) && (
-
-
- {(fields, { add, remove }) => (
- <>
- {fields.map((field) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
- e.target.value.split(',').map((s: string) => s.trim())}
- getValueProps={(value) => ({ value: Array.isArray(value) ? value.join(', ') : value })}
- >
-
-
-
- e.target.value.split(',').map((s: string) => s.trim()).filter((s: string) => s)}
- getValueProps={(value) => ({ value: Array.isArray(value) ? value.join(', ') : '' })}
- >
-
-
-
- remove(field.name)}
- >
-
- Remove Skill
-
-
- ))}
- add()}
- className="w-full border-dashed"
- >
-
- Add Skill
-
- >
- )}
-
-
+
+
+ {AGENT_FORM_CONFIG.skills.title} (Required)
+
+
+
+
+
)}
{/* Capabilities */}
{shouldShow(AGENT_FORM_CONFIG.capabilities.key) && (
-
- {AGENT_FORM_CONFIG.capabilities.fields.map((field) => (
-
-
-
- ))}
-
+
+
+ {AGENT_FORM_CONFIG.capabilities.title}
+
+
+
+ {AGENT_FORM_CONFIG.capabilities.fields.map((field) => (
+
+ ))}
+
+
+
)}
{/* Optional Settings */}
{shouldShow(AGENT_FORM_CONFIG.optional.key) && (
-
- {AGENT_FORM_CONFIG.optional.fields.map((field) => (
-
- {field.type === 'switch' ? : }
-
- ))}
-
+
+
+ {AGENT_FORM_CONFIG.optional.title}
+
+
+
+ {AGENT_FORM_CONFIG.optional.fields.map((field) =>
+ field.type === "switch" ? (
+
+ ) : (
+
+ ),
+ )}
+
+
+
)}
{/* Cost Configuration */}
{shouldShow(AGENT_FORM_CONFIG.cost.key) && (
-
-
-
+
+
+ {AGENT_FORM_CONFIG.cost.title}
+
+
+
+
+
)}
{/* LiteLLM Parameters */}
{shouldShow(AGENT_FORM_CONFIG.litellm.key) && (
-
- {AGENT_FORM_CONFIG.litellm.fields.map((field) => (
-
- {field.type === 'switch' ? : }
-
- ))}
-
+
+
+ {AGENT_FORM_CONFIG.litellm.title}
+
+
+
+ {AGENT_FORM_CONFIG.litellm.fields.map((field) =>
+ field.type === "switch" ? (
+
+ ) : (
+
+ ),
+ )}
+
+
+
)}
{/* Authentication Headers */}
{shouldShow("auth_headers") && (
-
- {/* Static Headers */}
-
- Static Headers{" "}
-
- Headers always sent to the backend agent, regardless of the
- client request. Admin-configured, static wins on conflict.
-
-
- }
+
-
- {(fields, { add, remove }) => (
- <>
- {fields.map(({ key, name, ...restField }) => (
-
-
-
-
-
-
-
- remove(name)}
- className="text-destructive"
- aria-label="Remove header"
- >
-
-
-
- ))}
- add()}
- className="w-full border-dashed"
- >
-
- Add Static Header
-
- >
- )}
-
-
+
+ Authentication Headers
+
+
+
+
+
+ Static Headers
+
+ Headers always sent to the backend agent, regardless of
+ the client request. Admin-configured, static wins on
+ conflict.
+
+
+
+
- {/* Extra Headers (dynamic forwarding) */}
-
- Forward Client Headers{" "}
-
- Header names to extract from the client's request and
- forward to the agent. Type a name and press Enter.
-
-
- }
- name="extra_headers"
- >
-
-
-
+
+
+ Forward Client Headers
+
+ Header names to extract from the client's request
+ and forward to the agent. Type a name and press Enter.
+
+
+
+
+
+
+
)}
-
- >
+
+
);
};
export default AgentFormFields;
-
diff --git a/ui/litellm-dashboard/src/components/agents/agent_info.tsx b/ui/litellm-dashboard/src/components/agents/agent_info.tsx
index 44dfa91b2e4..9f112c648ea 100644
--- a/ui/litellm-dashboard/src/components/agents/agent_info.tsx
+++ b/ui/litellm-dashboard/src/components/agents/agent_info.tsx
@@ -1,9 +1,10 @@
import React, { useState, useEffect, useCallback } from "react";
-// eslint-disable-next-line litellm-ui/no-banned-ui-imports
-import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
-import { Form, Input, InputNumber, Descriptions } from "antd";
+import { FormProvider, useForm } from "react-hook-form";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowLeft, Loader2 } from "lucide-react";
import MessageManager from "@/components/molecules/message_manager";
import { getAgentInfo, patchAgentCall, getAgentCreateMetadata, AgentCreateInfo } from "../networking";
@@ -21,6 +22,31 @@ interface AgentInfoViewProps {
isAdmin: boolean;
}
+function DescRow({
+ label,
+ children,
+}: {
+ label: React.ReactNode;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {label}
+
+ {children}
+
+ );
+}
+
+function DescList({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
+
const AgentInfoView: React.FC = ({
agentId,
onClose,
@@ -31,7 +57,8 @@ const AgentInfoView: React.FC = ({
const [isLoading, setIsLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [isSaving, setIsSaving] = useState(false);
- const [form] = Form.useForm();
+ const form = useForm({ defaultValues: {} });
+ const { register, reset, handleSubmit } = form;
const [agentTypeMetadata, setAgentTypeMetadata] = useState([]);
const [detectedAgentType, setDetectedAgentType] = useState("a2a");
@@ -59,15 +86,15 @@ const AgentInfoView: React.FC = ({
setDetectedAgentType(agentType);
if (agentType === "a2a") {
- form.setFieldsValue(parseAgentForForm(data));
+ reset(parseAgentForForm(data));
} else {
const typeInfo = agentTypeMetadata.find(
(t) => t.agent_type === agentType,
);
if (typeInfo) {
- form.setFieldsValue(parseDynamicAgentForForm(data, typeInfo));
+ reset(parseDynamicAgentForForm(data, typeInfo));
} else {
- form.setFieldsValue(parseAgentForForm(data));
+ reset(parseAgentForForm(data));
}
}
} catch (error) {
@@ -76,13 +103,12 @@ const AgentInfoView: React.FC = ({
} finally {
setIsLoading(false);
}
- }, [accessToken, agentId, agentTypeMetadata, form]);
+ }, [accessToken, agentId, agentTypeMetadata, reset]);
useEffect(() => {
fetchAgentInfo();
}, [fetchAgentInfo]);
- // Re-parse form when metadata is loaded
useEffect(() => {
if (agent && agentTypeMetadata.length > 0) {
const agentType = detectAgentType(agent);
@@ -91,36 +117,32 @@ const AgentInfoView: React.FC = ({
(t) => t.agent_type === agentType,
);
if (typeInfo) {
- form.setFieldsValue(parseDynamicAgentForForm(agent, typeInfo));
+ reset(parseDynamicAgentForForm(agent, typeInfo));
}
}
}
- }, [agentTypeMetadata, agent, form]);
+ }, [agentTypeMetadata, agent, reset]);
const selectedAgentTypeInfo = agentTypeMetadata.find(
(t) => t.agent_type === detectedAgentType,
);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleUpdate = async (values: any) => {
if (!accessToken || !agent) return;
setIsSaving(true);
try {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
let updateData: any;
-
if (detectedAgentType === "a2a") {
updateData = buildAgentDataFromForm(values, agent);
} else if (selectedAgentTypeInfo) {
updateData = buildDynamicAgentData(values, selectedAgentTypeInfo);
- // Preserve the agent_name from form
updateData.agent_name = values.agent_name;
} else {
updateData = buildAgentDataFromForm(values, agent);
}
-
+
await patchAgentCall(accessToken, agentId, updateData);
MessageManager.success("Agent updated successfully");
setIsEditing(false);
@@ -154,7 +176,6 @@ const AgentInfoView: React.FC = ({
);
}
- // Format date helper function
const formatDate = (dateString?: string) => {
if (!dateString) return "-";
const date = new Date(dateString);
@@ -174,104 +195,135 @@ const AgentInfoView: React.FC = ({
{agent.agent_id}
-
-
- Overview
- {isAdmin ? Settings : <>>}
-
+
+
+ Overview
+ {isAdmin && Settings }
+
-
- {/* Overview Panel */}
-
-
- {agent.agent_id}
- {agent.agent_name}
- {agent.agent_card_params?.name || "-"}
- {agent.agent_card_params?.description || "-"}
- {agent.agent_card_params?.url || "-"}
- {agent.agent_card_params?.version || "-"}
- {agent.agent_card_params?.protocolVersion || "-"}
-
- {agent.agent_card_params?.capabilities?.streaming ? "Yes" : "No"}
-
- {agent.agent_card_params?.capabilities?.pushNotifications && (
- Yes
- )}
- {agent.agent_card_params?.capabilities?.stateTransitionHistory && (
- Yes
- )}
-
- {agent.agent_card_params?.skills?.length || 0} configured
-
- {agent.litellm_params?.model && (
- {agent.litellm_params.model}
- )}
- {agent.litellm_params?.make_public !== undefined && (
- {agent.litellm_params.make_public ? "Yes" : "No"}
- )}
- {agent.agent_card_params?.iconUrl && (
- {agent.agent_card_params.iconUrl}
- )}
- {agent.agent_card_params?.documentationUrl && (
- {agent.agent_card_params.documentationUrl}
- )}
- {agent.tpm_limit ?? "Unlimited"}
- {agent.rpm_limit ?? "Unlimited"}
- {agent.session_tpm_limit ?? "Unlimited"}
- {agent.session_rpm_limit ?? "Unlimited"}
- {formatDate(agent.created_at)}
- {formatDate(agent.updated_at)}
-
+
+
+ {agent.agent_id}
+ {agent.agent_name}
+
+ {agent.agent_card_params?.name || "-"}
+
+
+ {agent.agent_card_params?.description || "-"}
+
+
+ {agent.agent_card_params?.url || "-"}
+
+
+ {agent.agent_card_params?.version || "-"}
+
+
+ {agent.agent_card_params?.protocolVersion || "-"}
+
+
+ {agent.agent_card_params?.capabilities?.streaming ? "Yes" : "No"}
+
+ {agent.agent_card_params?.capabilities?.pushNotifications && (
+ Yes
+ )}
+ {agent.agent_card_params?.capabilities?.stateTransitionHistory && (
+ Yes
+ )}
+
+ {agent.agent_card_params?.skills?.length || 0} configured
+
+ {agent.litellm_params?.model && (
+ {agent.litellm_params.model}
+ )}
+ {agent.litellm_params?.make_public !== undefined && (
+
+ {agent.litellm_params.make_public ? "Yes" : "No"}
+
+ )}
+ {agent.agent_card_params?.iconUrl && (
+
+ {agent.agent_card_params.iconUrl}
+
+ )}
+ {agent.agent_card_params?.documentationUrl && (
+
+ {agent.agent_card_params.documentationUrl}
+
+ )}
+
+ {agent.tpm_limit ?? "Unlimited"}
+
+
+ {agent.rpm_limit ?? "Unlimited"}
+
+
+ {agent.session_tpm_limit ?? "Unlimited"}
+
+
+ {agent.session_rpm_limit ?? "Unlimited"}
+
+ {formatDate(agent.created_at)}
+ {formatDate(agent.updated_at)}
+
- {agent.object_permission &&
- (agent.object_permission.mcp_servers?.length ||
- agent.object_permission.mcp_access_groups?.length ||
- (agent.object_permission.mcp_tool_permissions &&
- Object.keys(agent.object_permission.mcp_tool_permissions).length > 0)) && (
+ {agent.object_permission &&
+ (agent.object_permission.mcp_servers?.length ||
+ agent.object_permission.mcp_access_groups?.length ||
+ (agent.object_permission.mcp_tool_permissions &&
+ Object.keys(agent.object_permission.mcp_tool_permissions)
+ .length > 0)) && (
-
MCP Tool Permissions
-
- {agent.object_permission.mcp_servers && agent.object_permission.mcp_servers.length > 0 && (
-
- {agent.object_permission.mcp_servers.join(", ")}
-
- )}
- {agent.object_permission.mcp_access_groups &&
- agent.object_permission.mcp_access_groups.length > 0 && (
-
- {agent.object_permission.mcp_access_groups.join(", ")}
-
- )}
- {agent.object_permission.mcp_tool_permissions &&
- Object.keys(agent.object_permission.mcp_tool_permissions).length > 0 && (
-
-
- {Object.entries(agent.object_permission.mcp_tool_permissions).map(
- ([serverId, tools]) => (
+
+ MCP Tool Permissions
+
+
+
+ {agent.object_permission.mcp_servers &&
+ agent.object_permission.mcp_servers.length > 0 && (
+
+ {agent.object_permission.mcp_servers.join(", ")}
+
+ )}
+ {agent.object_permission.mcp_access_groups &&
+ agent.object_permission.mcp_access_groups.length > 0 && (
+
+ {agent.object_permission.mcp_access_groups.join(", ")}
+
+ )}
+ {agent.object_permission.mcp_tool_permissions &&
+ Object.keys(agent.object_permission.mcp_tool_permissions)
+ .length > 0 && (
+
+
+ {Object.entries(
+ agent.object_permission.mcp_tool_permissions,
+ ).map(([serverId, tools]) => (
{serverId}: {" "}
- {Array.isArray(tools) ? tools.join(", ") : String(tools)}
+ {Array.isArray(tools)
+ ? tools.join(", ")
+ : String(tools)}
- )
- )}
-
-
- )}
-
+ ))}
+
+
+ )}
+
+
)}
-
+
- {agent.agent_card_params?.skills &&
- agent.agent_card_params.skills.length > 0 && (
-
-
Skills
-
+ {agent.agent_card_params?.skills &&
+ agent.agent_card_params.skills.length > 0 && (
+
+
Skills
+
+
{agent.agent_card_params.skills.map(
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
(skill: any, index: number) => (
-
@@ -297,62 +349,106 @@ const AgentInfoView: React.FC = ({
)}
-
+
),
)}
-
+
- )}
-
+
+ )}
+
- {/* Settings Panel (only for admins) */}
- {isAdmin && (
-
-
-
-
Agent Settings
- {!isEditing && (
- setIsEditing(true)}>
- Edit Settings
-
- )}
-
+ {isAdmin && (
+
+
+
+
Agent Settings
+ {!isEditing && (
+ setIsEditing(true)}>
+ Edit Settings
+
+ )}
+
- {isEditing ? (
-
-
-
+
+ Agent ID
+
+
{detectedAgentType === "a2a" ? (
) : selectedAgentTypeInfo ? (
-
+
) : (
-
+
)}
Rate Limits
-
-
-
-
-
-
+
+ TPM Limit
+
+
+
+ RPM Limit
+
+
-
-
-
-
-
-
+
+
+ Session TPM Limit
+
+
+
+
+
+ Session RPM Limit
+
+
+
@@ -370,21 +466,19 @@ const AgentInfoView: React.FC
= ({
{isSaving ? "Saving..." : "Save Changes"}
-
- ) : (
-
- Click "Edit Settings" to modify agent
- configuration.
-
- )}
-
-
- )}
-
-
+
+
+ ) : (
+
+ Click "Edit Settings" to modify agent configuration.
+
+ )}
+
+
+ )}
+
);
};
export default AgentInfoView;
-
diff --git a/ui/litellm-dashboard/src/components/agents/cost_config_fields.tsx b/ui/litellm-dashboard/src/components/agents/cost_config_fields.tsx
index c66e0f0e014..d54f4c60e61 100644
--- a/ui/litellm-dashboard/src/components/agents/cost_config_fields.tsx
+++ b/ui/litellm-dashboard/src/components/agents/cost_config_fields.tsx
@@ -1,26 +1,47 @@
import React from "react";
-import { Form } from "antd";
+import { useFormContext } from "react-hook-form";
import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { Info } from "lucide-react";
import { AGENT_FORM_CONFIG } from "./agent_config";
+const InfoTip: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+
+
+
+
+ {children}
+
+
+);
+
const CostConfigFields: React.FC = () => {
+ const { register } = useFormContext();
return (
- <>
+
);
};
diff --git a/ui/litellm-dashboard/src/components/agents/dynamic_agent_form_fields.tsx b/ui/litellm-dashboard/src/components/agents/dynamic_agent_form_fields.tsx
index 108309255cd..fb2792851a8 100644
--- a/ui/litellm-dashboard/src/components/agents/dynamic_agent_form_fields.tsx
+++ b/ui/litellm-dashboard/src/components/agents/dynamic_agent_form_fields.tsx
@@ -1,5 +1,5 @@
-import React from "react";
-import { Form, Input as AntInput, Select } from "antd";
+import React, { useEffect } from "react";
+import { Controller, useFormContext } from "react-hook-form";
import {
Accordion,
AccordionContent,
@@ -7,7 +7,22 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { Info } from "lucide-react";
import { AgentCreateInfo, AgentCredentialFieldMetadata } from "../networking";
import { AGENT_FORM_CONFIG } from "./agent_config";
import CostConfigFields from "./cost_config_fields";
@@ -16,6 +31,17 @@ interface DynamicAgentFormFieldsProps {
agentTypeInfo: AgentCreateInfo;
}
+const InfoTip: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+
+
+
+
+ {children}
+
+
+);
+
/**
* Form fields for dynamic agent types (e.g., LangGraph).
* Renders common fields (agent name, display name, description) plus
@@ -24,66 +50,146 @@ interface DynamicAgentFormFieldsProps {
const DynamicAgentFormFields: React.FC
= ({
agentTypeInfo,
}) => {
- return (
- <>
-
-
-
+ const { register, control, setValue, formState, getValues } =
+ useFormContext();
-
+ useEffect(() => {
+ for (const field of agentTypeInfo.credential_fields) {
+ if (field.default_value !== undefined) {
+ const current = getValues(field.key);
+ if (current === undefined || current === null || current === "") {
+ setValue(field.key, field.default_value);
+ }
+ }
+ }
+ }, [agentTypeInfo, setValue, getValues]);
+
+ const agentNameError = (formState.errors as any)?.agent_name;
+
+ return (
+
+
+
+ Agent Name *
+ Unique identifier for the agent
+
+
+ {agentNameError && (
+
+ {agentNameError.message as string}
+
+ )}
+
+
+
+
+ Description
+ Brief description of what this agent does
+
-
+
{agentTypeInfo.credential_fields.map(
- (field: AgentCredentialFieldMetadata) => (
-
- {field.field_type === "password" ? (
-
- ) : field.field_type === "textarea" ? (
-
- ) : field.field_type === "select" && field.options ? (
-
- {field.options.map((opt) => (
-
- {opt}
-
- ))}
-
- ) : (
-
- )}
-
- ),
+ (field: AgentCredentialFieldMetadata) => {
+ const error = (formState.errors as any)?.[field.key];
+ const inputId = `credential-${field.key}`;
+ return (
+
+ );
+ },
)}
@@ -99,7 +205,7 @@ const DynamicAgentFormFields: React.FC = ({
- >
+
);
};
@@ -182,4 +288,3 @@ export const buildDynamicAgentData = (
};
export default DynamicAgentFormFields;
-