diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentFormKit.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentFormKit.tsx index 3d863036234..70c7d2fdb92 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentFormKit.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentFormKit.tsx @@ -27,6 +27,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component import { Input } from "@/components/ui/input"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"; +import i18n from "@/i18n/i18n"; import type { KeyValueFormValue, KillSwitchConfig, KillSwitchFormValue } from "./kill_switch_config"; export interface AgentSkillFormValue { @@ -282,7 +283,7 @@ export const AgentTagsInput = ({ value, onValueChange, placeholder, - emptyText = "No matching options", + emptyText = i18n.t("agents.formKit.noMatchingOptions"), ...props }: AgentTagsInputProps) => { const anchor = useComboboxAnchor(); @@ -396,7 +397,7 @@ export const AgentMultiSelect = ({ value, onValueChange, placeholder, - emptyText = "No matching options", + emptyText = i18n.t("agents.formKit.noMatchingOptions"), ...props }: AgentMultiSelectProps) => { const anchor = useComboboxAnchor(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityDetails.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityDetails.tsx index 12465c6e861..e891195d137 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityDetails.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityDetails.tsx @@ -1,14 +1,17 @@ import React from "react"; +import type { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; import type { components } from "@/lib/http/schema"; import { useQuery } from "@tanstack/react-query"; import { apiClient } from "@/components/networking"; import { Button } from "@/components/ui/button"; import { readAgentIdentity } from "./agent_identity"; -const authenticationMessage = (error: boolean, lastAuthenticated?: string | null): string => { - if (error) return "Could not load authentication evidence"; - if (lastAuthenticated) return `Last authenticated identity match: ${new Date(lastAuthenticated).toLocaleString()}`; - return "Configured, awaiting an authenticated request"; +const authenticationMessage = (t: TFunction, error: boolean, lastAuthenticated?: string | null): string => { + if (error) return t("agents.identityDetails.evidence.loadFailed"); + if (lastAuthenticated) + return t("agents.identityDetails.evidence.lastMatch", { date: new Date(lastAuthenticated).toLocaleString() }); + return t("agents.identityDetails.evidence.awaiting"); }; export const AgentIdentityDetails = ({ @@ -22,6 +25,7 @@ export const AgentIdentityDetails = ({ accessToken: string | null; isAdmin: boolean; }) => { + const { t } = useTranslation(); const identity = readAgentIdentity(value); const { data, isError, isFetching, refetch } = useQuery({ queryKey: ["agent-identity", agentId, identity], @@ -36,31 +40,39 @@ export const AgentIdentityDetails = ({ }); if (!identity || !isAdmin) return null; - const executionLabel = data?.enabled ? "Enabled" : "Disabled"; + const executionLabel = data?.enabled + ? t("agents.identityDetails.execution.enabled") + : t("agents.identityDetails.execution.disabled"); return ( -
-

Agent Identity: Microsoft Entra ID

+
+

{t("agents.identityDetails.title")}

- Tenant: {identity.tenant_id} + {t("agents.identityDetails.tenant")} {identity.tenant_id}

<>

- Application (Client) ID: {identity.client_id} + {t("agents.identityDetails.clientId")} {identity.client_id} +

+

+ {t("agents.identityDetails.servicePrincipal")}{" "} + {identity.service_principal_id || t("agents.identityDetails.notConfigured")}

-

Enterprise application Object ID: {identity.service_principal_id || "Not configured"}

- Execution: {data ? executionLabel : "Loading"} · Mode: {data?.execution_mode ?? "Loading"} + {t("agents.identityDetails.executionSummary", { + execution: data ? executionLabel : t("agents.identityDetails.loading"), + mode: data?.execution_mode ?? t("agents.identityDetails.loading"), + })}

{data?.identity?.active === false - ? "Identity unbound; execution is disabled" - : authenticationMessage(isError, data?.last_authenticated_at)} -

-

- Recent evidence comes from a validated Entra token matching this binding. It is persisted across restarts and - cleared when the binding changes. Tool and model permissions are checked separately. + ? t("agents.identityDetails.evidence.unbound") + : authenticationMessage(t, isError, data?.last_authenticated_at)}

+

{t("agents.identityDetails.evidenceNote")}

- View request logs + {t("agents.identityDetails.viewLogs")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityFields.tsx index 50c60776ff3..d4af3ff3833 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityFields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentIdentityFields.tsx @@ -1,26 +1,30 @@ import React, { useEffect, useState } from "react"; import { useWatch } from "react-hook-form"; +import type { ParseKeys } from "i18next"; +import { Trans, useTranslation } from "react-i18next"; +import i18n from "@/i18n/i18n"; import { apiClient } from "@/components/networking"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { AgentFormField, type AgentFormValues } from "./AgentFormKit"; import { entraTenantFromIssuer, IDENTITY_UUID_PATTERN } from "./agent_identity"; -const PROVIDER_OPTIONS = [ - { value: "none", label: "No explicit identity binding" }, - { value: "microsoft_entra", label: "Microsoft Entra ID" }, +const PROVIDER_OPTIONS: { value: string; labelKey: ParseKeys }[] = [ + { value: "none", labelKey: "agents.identityFields.providers.none" }, + { value: "microsoft_entra", labelKey: "agents.identityFields.providers.microsoftEntra" }, ]; -const EXECUTION_MODE_OPTIONS = [ - { value: "autonomous", label: "Autonomous" }, - { value: "delegated", label: "On behalf of a user" }, - { value: "both", label: "Both" }, +const EXECUTION_MODE_OPTIONS: { value: string; labelKey: ParseKeys }[] = [ + { value: "autonomous", labelKey: "agents.identityFields.executionModes.autonomous" }, + { value: "delegated", labelKey: "agents.identityFields.executionModes.delegated" }, + { value: "both", labelKey: "agents.identityFields.executionModes.both" }, ]; -const EXECUTION_OPTIONS = [ - { value: "enabled", label: "Enabled" }, - { value: "disabled", label: "Disabled" }, +const EXECUTION_OPTIONS: { value: string; labelKey: ParseKeys }[] = [ + { value: "enabled", labelKey: "agents.identityFields.execution.enabled" }, + { value: "disabled", labelKey: "agents.identityFields.execution.disabled" }, ]; export const AgentIdentityFields = ({ accessToken }: { accessToken: string | null }) => { + const { t } = useTranslation(); const provider = useWatch({ name: "identity_provider" }); const mode = useWatch({ name: "execution_mode" }); const showScopes = mode !== "autonomous" && mode !== undefined; @@ -44,7 +48,7 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul } }) .catch(() => { - if (active) setError("Could not load the gateway's trusted identity providers"); + if (active) setError(i18n.t("agents.identityFields.errors.providersLoadFailed")); }); return () => { active = false; @@ -53,18 +57,18 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul return ( <> -
+
-

Agent Identity

-

- Connect an existing identity provider application to this agent. Its name and runtime address can change - independently. -

+

{t("agents.identityFields.title")}

+

{t("agents.identityFields.description")}

- + {({ value, onChange, id }) => ( - + {tenants.map((tenant) => ( @@ -109,26 +113,24 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul

)} {!error && tenants.length === 0 && ( -

- No trusted Entra tenant is available. Configure JWT issuer and audience validation on the gateway first. - Dashboard Microsoft SSO is configured separately. -

+

{t("agents.identityFields.tenant.noneAvailable")}

)} - Find this under{" "} - - Entra App registrations - - , select your agent application, then Overview. No client secret is required here. - + + ), + }} + /> } > {({ value, onChange, ref, ...control }) => ( @@ -141,10 +143,14 @@ export const AgentIdentityFields = ({ accessToken }: { accessToken: string | nul /> )} - + {({ value, onChange, id }) => ( {({ value, onChange, ref, ...control }) => ( )} -

- Users must first sign in through this gateway's Microsoft SSO. Subsequent delegated calls must - satisfy both user and agent permissions. -

+

{t("agents.identityFields.requiredScopes.note")}

)} - + {({ value, onChange, id }) => ( )} -

- LiteLLM verifies the agent's Entra token before matching this identity. Saving these fields - configures the binding; an authenticated request provides verification. Runtime authentication headers are - configured separately. -

+

{t("agents.identityFields.verificationNote")}

)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentKillSwitchDangerZone.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentKillSwitchDangerZone.tsx index 8d68a8f921b..e733a564576 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentKillSwitchDangerZone.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentKillSwitchDangerZone.tsx @@ -1,5 +1,6 @@ import { CircleAlert } from "lucide-react"; import React, { useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; @@ -24,6 +25,7 @@ const AgentKillSwitchDangerZone: React.FC = ({ accessToken, isAdmin, }) => { + const { t } = useTranslation(); const [isConfirmOpen, setIsConfirmOpen] = useState(false); const [confirmationInput, setConfirmationInput] = useState(""); const [isFiring, setIsFiring] = useState(false); @@ -44,9 +46,9 @@ const AgentKillSwitchDangerZone: React.FC = ({ const result = await triggerAgentKillSwitchCall(accessToken, agentId); setLastResult(result); setIsConfirmOpen(false); - toast.success(`Kill switch fired (HTTP ${result.status_code})`); + toast.success(t("agents.dangerZone.toasts.fired", { status: result.status_code })); } catch (error) { - toast.error(error instanceof Error ? error.message : "Failed to fire kill switch"); + toast.error(error instanceof Error ? error.message : t("agents.dangerZone.toasts.fireFailed")); } finally { setIsFiring(false); } @@ -55,38 +57,33 @@ const AgentKillSwitchDangerZone: React.FC = ({ return (

- Danger Zone + {t("agents.dangerZone.title")}

-

Kill switch

+

{t("agents.dangerZone.killSwitch")}

{killSwitch ? ( <> -

- Calls the configured webhook to stop this agent's upstream runtime. This can cause an outage for - everyone using the agent and cannot be undone from LiteLLM -

+

{t("agents.dangerZone.configuredDescription")}

{killSwitch.method ?? "POST"} {killSwitch.url}

) : ( -

- Not configured. Add a kill switch webhook under Settings to enable this action -

+

{t("agents.dangerZone.notConfigured")}

)}
{killSwitch && ( )}
{lastResult && (

- Last result: HTTP {lastResult.status_code} + {t("agents.dangerZone.lastResult", { status: lastResult.status_code })} {lastResult.response_body ? ` ${lastResult.response_body}` : ""}

)} @@ -95,27 +92,32 @@ const AgentKillSwitchDangerZone: React.FC = ({ !open && !isFiring && setIsConfirmOpen(false)}> - Fire kill switch for {agentName}? + {t("agents.dangerZone.confirm.title", { name: agentName })}
- This can cause an outage + {t("agents.dangerZone.confirm.outageTitle")} - LiteLLM will call {killSwitch?.method ?? "POST"} {killSwitch?.url} immediately. Whatever that webhook - does to the agent is outside LiteLLM's control and cannot be reverted here + {t("agents.dangerZone.confirm.outageDescription", { + method: killSwitch?.method ?? "POST", + url: killSwitch?.url, + })}

- Type {agentName} to confirm: + {agentName} }} + />

setConfirmationInput(e.target.value)} placeholder={agentName} @@ -126,7 +128,7 @@ const AgentKillSwitchDangerZone: React.FC = ({
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsPanel.tsx index 3f599c38976..308cefec844 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsPanel.tsx @@ -1,6 +1,7 @@ import { useTerminology } from "@/contexts/TerminologyContext"; import React, { useState, useEffect } from "react"; import { Info, Plus } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { getAgentsList, deleteAgentCall } from "@/components/networking"; import AddAgentForm from "./add_agent_form"; import { isAdminRole } from "@/utils/roles"; @@ -33,6 +34,7 @@ interface AgentsResponse { const AgentsPanel: React.FC = ({ accessToken, userRole, teams }) => { const term = useTerminology(); + const { t } = useTranslation(); const [agentsList, setAgentsList] = useState([]); const [isAddModalVisible, setIsAddModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(true); @@ -122,11 +124,11 @@ const AgentsPanel: React.FC = ({ accessToken, userRole, teams setIsDeleting(true); try { await deleteAgentCall(accessToken, agentToDelete.id); - toast.success(`Agent "${agentToDelete.name}" deleted successfully`); + toast.success(t("agents.panel.toasts.deleted", { name: agentToDelete.name })); await refetchAgents(healthCheckEnabled); } catch (error) { console.error("Error deleting agent:", error); - toast.fromError("Failed to delete agent"); + toast.fromError(t("agents.panel.toasts.deleteFailed")); } finally { setIsDeleting(false); setAgentToDelete(null); @@ -140,25 +142,18 @@ const AgentsPanel: React.FC = ({ accessToken, userRole, teams return (
-

Agents

-

- {term( - "List of A2A-spec agents that are available to be used in your organization. Go to AI Hub, to make agents public.", - )} -

+

{t("agents.panel.title")}

+

{term(t("agents.panel.description"))}

- How do agents authenticate? - - Agents can authenticate with a virtual key or a trusted identity provider using JWT. Configure an identity - binding when adding or editing an agent. JWT authentication does not require a virtual key. - + {t("agents.panel.authAlert.title")} + {t("agents.panel.authAlert.description")} {isAdmin && (
)} @@ -201,15 +196,15 @@ const AgentsPanel: React.FC = ({ accessToken, userRole, teams > - Delete Agent + {t("agents.panel.deleteDialog.title")} - Are you sure you want to delete agent: {agentToDelete.name}? This action cannot be undone. + {t("agents.panel.deleteDialog.description", { name: agentToDelete.name })} - Cancel + {t("agents.panel.deleteDialog.cancel")} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.tsx index d737a9250eb..421207e72d0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTable.tsx @@ -3,6 +3,7 @@ import { SortingState } from "@tanstack/react-table"; import { Bot, CircleCheck, Search as SearchIcon, X } from "lucide-react"; import React, { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Agent } from "@/components/agents/types"; import { DataTable } from "@/components/shared/DataTable"; @@ -27,16 +28,17 @@ interface AgentsTableProps { const DEFAULT_SORTING: SortingState = [{ id: "created_at", desc: true }]; function EmptyState({ isFiltered }: { isFiltered: boolean }) { + const { t } = useTranslation(); return (
-
{isFiltered ? "No matching agents" : "No agents yet"}
+
+ {isFiltered ? t("agents.table.empty.filteredTitle") : t("agents.table.empty.title")} +
- {isFiltered - ? "Adjust the search to see more agents." - : "Add an agent to make it available in your organization."} + {isFiltered ? t("agents.table.empty.filteredDescription") : t("agents.table.empty.description")}
); @@ -52,6 +54,7 @@ const AgentsTable: React.FC = ({ onAgentClick, onDeleteClick, }) => { + const { t } = useTranslation(); const [sorting, setSorting] = useState(DEFAULT_SORTING); const [searchTerm, setSearchTerm] = useState(""); const filteredAgents = useMemo( @@ -79,7 +82,7 @@ const AgentsTable: React.FC = ({ sorting={sorting} onSortingChange={setSorting} isLoading={isLoading} - loadingMessage="Loading agents…" + loadingMessage={t("agents.table.loading")} noDataMessage={ 0} />} size="compact" toolbar={() => ( @@ -89,13 +92,17 @@ const AgentsTable: React.FC = ({ setSearchTerm(e.target.value)} /> {searchTerm && ( - setSearchTerm("")}> + setSearchTerm("")} + > @@ -109,7 +116,7 @@ const AgentsTable: React.FC = ({ - Health Check + {t("agents.table.healthCheck.label")} = ({
} /> - When enabled, only agents with reachable URLs are shown + {t("agents.table.healthCheck.hint")}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx index 002219f5478..a0e8751e682 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/AgentsTableColumns.tsx @@ -1,6 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; +import { useTranslation } from "react-i18next"; import { MoreHorizontal, Trash2 } from "lucide-react"; import { Agent } from "@/components/agents/types"; @@ -14,6 +15,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import i18n from "@/i18n/i18n"; import { cn } from "@/lib/cva.config"; interface AgentRowActionsProps { @@ -22,10 +24,11 @@ interface AgentRowActionsProps { } function AgentRowActions({ agent, onDeleteClick }: AgentRowActionsProps) { + const { t } = useTranslation(); return ( @@ -38,7 +41,7 @@ function AgentRowActions({ agent, onDeleteClick }: AgentRowActionsProps) { onClick={() => onDeleteClick(agent.agent_id, agent.agent_name)} > - Delete + {t("agents.table.actions.delete")} @@ -59,8 +62,8 @@ export const getAgentsTableColumns = ({ { id: "agent_name", accessorKey: "agent_name", - meta: { title: "Agent Name" }, - header: ({ column }) => , + meta: { title: i18n.t("agents.table.columns.name") }, + header: ({ column }) => , size: 200, enableSorting: true, cell: ({ row }) => { @@ -75,8 +78,8 @@ export const getAgentsTableColumns = ({ { id: "agent_id", accessorKey: "agent_id", - meta: { title: "Agent ID" }, - header: ({ column }) => , + meta: { title: i18n.t("agents.table.columns.id") }, + header: ({ column }) => , size: 200, enableSorting: true, cell: ({ row }) => ( @@ -90,22 +93,22 @@ export const getAgentsTableColumns = ({ { id: "spend", accessorKey: "spend", - meta: { title: "Spend (USD)", numeric: true }, - header: ({ column }) => , + meta: { title: i18n.t("agents.table.columns.spend"), numeric: true }, + header: ({ column }) => , size: 130, enableSorting: true, cell: ({ row }) => , }, { id: "model", - meta: { title: "Model" }, - header: "Model", + meta: { title: i18n.t("agents.table.columns.model") }, + header: i18n.t("agents.table.columns.model"), size: 170, enableSorting: false, cell: ({ row }) => { const model = row.original.litellm_params?.model; if (!model) { - return N/A; + return {i18n.t("agents.table.noModel")}; } return ( @@ -122,25 +125,26 @@ export const getAgentsTableColumns = ({ const timestamp = agent.created_at ? new Date(agent.created_at).getTime() : 0; return Number.isNaN(timestamp) ? 0 : timestamp; }, - meta: { title: "Created" }, - header: ({ column }) => , + meta: { title: i18n.t("agents.table.columns.created") }, + header: ({ column }) => , size: 150, enableSorting: true, cell: ({ row }) => , }, { id: "status", - meta: { title: "Status" }, - header: "Status", + meta: { title: i18n.t("agents.table.columns.status") }, + header: i18n.t("agents.table.columns.status"), size: 130, enableSorting: false, cell: ({ row }) => { const hasKeys = (row.original.keys?.length ?? 0) > 0; - if (row.original.jwt_auth_configured) return ; + if (row.original.jwt_auth_configured) + return ; return hasKeys ? ( - + ) : ( - + ); }, }, @@ -149,7 +153,7 @@ export const getAgentsTableColumns = ({ { id: "actions", meta: { className: "text-right", headerClassName: "text-right" }, - header: () => Actions, + header: () => {i18n.t("agents.table.columns.actions")}, size: 64, enableSorting: false, enableHiding: false, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/KillSwitchFormFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/KillSwitchFormFields.tsx index 578d80da6ed..2b9a0edb70d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/KillSwitchFormFields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/KillSwitchFormFields.tsx @@ -1,5 +1,6 @@ import React from "react"; import { useFieldArray, useFormContext, useWatch } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import { Plus, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -7,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Textarea } from "@/components/ui/textarea"; import { Field, FieldTitle } from "@/components/ui/field"; import { PasswordInput } from "@/components/shared/PasswordInput"; +import i18n from "@/i18n/i18n"; import { AgentFormField, AgentFormValues, labelWithHint } from "./AgentFormKit"; import { KILL_SWITCH_AUTH_TYPES, KILL_SWITCH_METHODS, validateKillSwitchBody } from "./kill_switch_config"; @@ -21,6 +23,7 @@ const KeyValueFieldArray = ({ keyPlaceholder: string; valuePlaceholder: string; }) => { + const { t } = useTranslation(); const { control } = useFormContext(); const { fields, append, remove } = useFieldArray({ control, name }); @@ -28,7 +31,10 @@ const KeyValueFieldArray = ({
{fields.map((item, index) => (
- + {({ value, onChange, ref, ...control }) => ( remove(index)} > @@ -109,24 +115,50 @@ const TextField = ({ ); const KillSwitchAuthFields = () => { + const { t } = useTranslation(); const { control } = useFormContext(); const authType = useWatch({ control, name: "kill_switch.auth_type" }); switch (authType) { case "bearer": - return ; + return ( + + ); case "api_key": return ( <> - - + + ); case "basic": return ( <> - - + + ); default: @@ -139,13 +171,13 @@ const KillSwitchFormFields = () => ( - + {({ value, onChange, ref: _ref, ...control }) => ( @@ -208,7 +240,7 @@ const KillSwitchFormFields = () => ( {KILL_SWITCH_AUTH_TYPES.map((option) => ( - {option.label} + {i18n.t(option.labelKey)} ))} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx index 188d3349beb..715f2516e66 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/agents/_components/add_agent_form.tsx @@ -5,6 +5,7 @@ import { FormProvider, useForm, useWatch } from "react-hook-form"; import { toast } from "@/lib/toast"; import { Logo } from "@/components/molecules/logo/Logo"; import { Bot, Check, CircleCheck, Key, LayoutGrid } from "lucide-react"; +import { Trans, useTranslation } from "react-i18next"; import CreatedKeyDisplay from "@/components/shared/CreatedKeyDisplay"; import { Badge } from "@/components/ui/badge"; import { StatusBadge } from "@/components/shared/table_cells/status_badge"; @@ -55,10 +56,17 @@ import MCPToolPermissions from "@/components/mcp_server_management/MCPToolPermis import AccessGroupSelector from "@/components/common_components/AccessGroupSelector"; import GuardrailSelector from "@/components/guardrails/GuardrailSelector"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import i18n from "@/i18n/i18n"; const CUSTOM_AGENT_TYPE = "custom"; -const STEP_TITLES = ["Configure", "Entitlements", "Governance", "Agent Management", "Ready"] as const; +const STEP_TITLES = [ + "agents.addForm.steps.configure", + "agents.addForm.steps.entitlements", + "agents.addForm.steps.governance", + "agents.addForm.steps.agentManagement", + "agents.addForm.steps.ready", +] as const; const stepMarkerClass = (index: number, current: number): string => { if (index < current) return "border-primary text-primary"; @@ -73,11 +81,12 @@ const stepTitleClass = (index: number, current: number): string => { }; const AgentTypeLabel: React.FC<{ agentType: string; info: AgentCreateInfo | undefined }> = ({ agentType, info }) => { + const { t } = useTranslation(); if (agentType === CUSTOM_AGENT_TYPE) { return ( - Custom / Other + {t("agents.addForm.agentType.custom")} ); } @@ -91,7 +100,7 @@ const AgentTypeLabel: React.FC<{ agentType: string; info: AgentCreateInfo | unde }; const StepProgress: React.FC<{ current: number }> = ({ current }) => ( -
    +
      {STEP_TITLES.map((title, index) => (
    1. = ({ current }) => ( > {index < current ? : index + 1} - {title} + {i18n.t(title)} {index < STEP_TITLES.length - 1 &&
    2. ))} @@ -132,6 +141,7 @@ interface AddAgentFormProps { } const AddAgentForm: React.FC = ({ visible, onClose, accessToken, onSuccess, teams }) => { + const { t } = useTranslation(); const { userId, userRole } = useAuthorized(); const form = useForm({ defaultValues: buildInitialValues("a2a") }); const panels = useCollapsiblePanels([AGENT_FORM_CONFIG.basic.key]); @@ -339,12 +349,12 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok const handleCreateAgent = async () => { if (!accessToken) { - toast.error("No access token available"); + toast.error(t("agents.addForm.toasts.noAccessToken")); return; } if (keyAssignOption === "existing_key" && !selectedExistingKey) { - toast.error("Please select an existing key to assign"); + toast.error(t("agents.addForm.toasts.selectExistingKey")); return; } @@ -358,7 +368,7 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok const values = form.getValues(); const built = buildAgentData(values); if (!built) { - toast.error("Failed to build agent data"); + toast.error(t("agents.addForm.toasts.buildFailed")); setIsSubmitting(false); return; } @@ -434,7 +444,11 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok } catch (error) { console.error("Error creating agent:", error); const errorMessage = error instanceof Error ? error.message : String(error); - toast.error(errorMessage ? `Failed to create agent: ${errorMessage}` : "Failed to create agent"); + toast.error( + errorMessage + ? t("agents.addForm.toasts.createFailedWithError", { error: errorMessage }) + : t("agents.addForm.toasts.createFailed"), + ); } finally { setIsSubmitting(false); } @@ -461,17 +475,14 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok 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). -

      +

      {t("agents.addForm.entitlements.description")}

      {({ id, value, onChange }) => ( @@ -479,7 +490,11 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok id={id} value={Array.isArray(value) ? (value as string[]) : []} onValueChange={onChange} - placeholder={loadingModels ? "Loading models..." : "Select models (leave empty for all)"} + placeholder={ + loadingModels + ? t("agents.addForm.entitlements.models.loading") + : t("agents.addForm.entitlements.models.placeholder") + } options={availableModels.map((m) => ({ label: getModelDisplayName(m), value: m }))} /> )} @@ -488,8 +503,8 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok {({ id, value, onChange }) => ( @@ -497,7 +512,11 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok id={id} value={Array.isArray(value) ? (value as string[]) : []} onValueChange={onChange} - placeholder={loadingAgents ? "Loading agents..." : "Select agents (leave empty for all)"} + placeholder={ + loadingAgents + ? t("agents.addForm.entitlements.agents.loading") + : t("agents.addForm.entitlements.agents.placeholder") + } options={availableAgents.map((a) => ({ label: a.agent_name, value: a.agent_id }))} /> )} @@ -506,15 +525,15 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok {({ value, onChange }) => ( )} @@ -524,8 +543,8 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok {({ value, onChange }) => ( @@ -536,7 +555,7 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok accessGroups: (value as McpServerSelection | undefined)?.accessGroups ?? [], }} accessToken={accessToken ?? ""} - placeholder="Select MCP servers or access groups (optional)" + placeholder={t("agents.addForm.entitlements.mcpServers.placeholder")} /> )} @@ -574,15 +593,15 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok const renderObservabilityStep = () => (
      -

      Tracing

      +

      {t("agents.addForm.governance.tracing.title")}

      - Require x-litellm-trace-id on calls TO this agent + {t("agents.addForm.governance.tracing.inbound.label", { header: "x-litellm-trace-id" })}

      - Only accept this agent being invoked with a trace-id (e.g. when used as a sub-agent). + {t("agents.addForm.governance.tracing.inbound.description")}

      @@ -591,10 +610,10 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
      - Require x-litellm-trace-id on calls BY this agent + {t("agents.addForm.governance.tracing.outbound.label", { header: "x-litellm-trace-id" })}

      - Requires LLM/MCP calls made by this agent to include x-litellm-trace-id for session tracking. + {t("agents.addForm.governance.tracing.outbound.description", { header: "x-litellm-trace-id" })}

      = ({ visible, onClose, accessTok
      -

      Budgets & Rate Limits

      +

      {t("agents.addForm.governance.budgets.title")}

      {!requireTraceIdOutbound && (
      - Enable "Require x-litellm-trace-id on calls BY this agent" in Tracing to configure budgets and - rate limits. + {t("agents.addForm.governance.budgets.requiresOutbound", { header: "x-litellm-trace-id" })}
      )} -
      Session Budgets
      +
      + {t("agents.addForm.governance.budgets.session.title")} +
      - Max Iterations + + {t("agents.addForm.governance.budgets.session.maxIterations.label")} + @@ -639,15 +661,19 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok } onBlur={() => setMaxIterations((current) => (current !== null && current < 1 ? 1 : current))} /> -

      Hard cap on LLM calls per session

      +

      + {t("agents.addForm.governance.budgets.session.maxIterations.hint")} +

      - Max Budget Per Session ($) + + {t("agents.addForm.governance.budgets.session.maxBudget.label")} + @@ -657,26 +683,48 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok setMaxBudgetPerSession((current) => (current !== null && current < 0.01 ? 0.01 : current)) } /> -

      Max spend per trace before returning 429

      +

      + {t("agents.addForm.governance.budgets.session.maxBudget.hint")} +

      -
      Agent Rate Limits
      -

      Global rate limits applied across all callers of this agent.

      +
      + {t("agents.addForm.governance.rateLimits.agent.title")} +
      +

      {t("agents.addForm.governance.rateLimits.agent.description")}

      - {rateLimitField("tpm_limit", "TPM Limit", "e.g. 100000")} - {rateLimitField("rpm_limit", "RPM Limit", "e.g. 100")} + {rateLimitField( + "tpm_limit", + t("agents.addForm.governance.rateLimits.agent.tpm.label"), + t("agents.addForm.governance.rateLimits.agent.tpm.placeholder"), + )} + {rateLimitField( + "rpm_limit", + t("agents.addForm.governance.rateLimits.agent.rpm.label"), + t("agents.addForm.governance.rateLimits.agent.rpm.placeholder"), + )}
      -
      Per-Session Rate Limits
      +
      + {t("agents.addForm.governance.rateLimits.session.title")} +

      - Rate limits per session (x-litellm-trace-id). Each session gets its own counters. + {t("agents.addForm.governance.rateLimits.session.description", { header: "x-litellm-trace-id" })}

      - {rateLimitField("session_tpm_limit", "Session TPM Limit", "e.g. 10000")} - {rateLimitField("session_rpm_limit", "Session RPM Limit", "e.g. 20")} + {rateLimitField( + "session_tpm_limit", + t("agents.addForm.governance.rateLimits.session.tpm.label"), + t("agents.addForm.governance.rateLimits.session.tpm.placeholder"), + )} + {rateLimitField( + "session_rpm_limit", + t("agents.addForm.governance.rateLimits.session.rpm.label"), + t("agents.addForm.governance.rateLimits.session.rpm.placeholder"), + )}
      @@ -684,10 +732,8 @@ const AddAgentForm: React.FC = ({ visible, onClose, accessTok
      -

      Guardrails

      -

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

      +

      {t("agents.addForm.governance.guardrails.title")}

      +

      {t("agents.addForm.governance.guardrails.description")}

      {({ value, onChange }) => ( = ({ visible, onClose, accessTok <> - {labelWithHint("Agent Type", "Select the type of agent you want to create")} + {labelWithHint(t("agents.addForm.configure.agentType.label"), t("agents.addForm.configure.agentType.hint"))} )} - + {({ value, onChange, ref, ...control }) => (