diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index a4a30f3021a..df70c086a46 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1081,6 +1081,11 @@ "count": 1 } }, + "src/app/(dashboard)/usage/_components/components/UsagePageView.tsx": { + "max-lines": { + "count": 1 + } + }, "src/app/(dashboard)/users/_components/BulkEditUsers.tsx": { "prefer-const": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx index a9672b088ba..47cd5dd377b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import useCan from "@/app/(dashboard)/hooks/useCan"; import DeletedKeysPage from "@/components/DeletedKeysPage/DeletedKeysPage"; @@ -13,6 +14,7 @@ import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; type LogsTab = "request logs" | "audit logs" | "deleted keys" | "deleted teams"; export default function LogsPage() { + const { t } = useTranslation(); const { accessToken, userRole, userId, token, premiumUser } = useAuthorized(); const [activeTab, setActiveTab] = useState("request logs"); const canViewAuditLogs = useCan("viewAuditLogs"); @@ -23,7 +25,12 @@ export default function LogsPage() { if (credentialsPending || identityPending) { return ( -
+
); @@ -33,10 +40,12 @@ export default function LogsPage() { setActiveTab(value)}> - Request Logs - {canViewAuditLogs && Audit Logs} - Deleted Keys - {canViewDeletedTeams && Deleted Teams} + {t("logs.page.tabs.requestLogs")} + {canViewAuditLogs && {t("logs.page.tabs.auditLogs")}} + {t("logs.page.tabs.deletedKeys")} + {canViewDeletedTeams && ( + {t("logs.page.tabs.deletedTeams")} + )} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/telemetry/_components/TelemetryEnterpriseDialog.tsx b/ui/litellm-dashboard/src/app/(dashboard)/telemetry/_components/TelemetryEnterpriseDialog.tsx index 11b9fc0b302..eef706071ad 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/telemetry/_components/TelemetryEnterpriseDialog.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/telemetry/_components/TelemetryEnterpriseDialog.tsx @@ -1,5 +1,6 @@ "use client"; +import { useTranslation } from "react-i18next"; import { Dialog, DialogContent, @@ -18,17 +19,18 @@ export function TelemetryEnterpriseDialog({ onOpenChange: (open: boolean) => void; premiumUser: boolean; }) { + const { t } = useTranslation(); return ( onOpenChange(next)}> - {premiumUser ? "Contact your enterprise representative" : "Telemetry is an enterprise-only feature"} + {premiumUser ? t("telemetry.enterpriseDialog.premiumTitle") : t("telemetry.enterpriseDialog.title")} {premiumUser - ? "Telemetry can't be turned on from this page. Reach out to your LiteLLM enterprise representative to turn it on for this proxy." - : "Telemetry is only available on LiteLLM Enterprise, and this proxy isn't on an enterprise license."} + ? t("telemetry.enterpriseDialog.premiumDescription") + : t("telemetry.enterpriseDialog.description")} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/telemetry/_components/TelemetrySettingsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/telemetry/_components/TelemetrySettingsPage.tsx index b99bce385f3..43bc56123f7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/telemetry/_components/TelemetrySettingsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/telemetry/_components/TelemetrySettingsPage.tsx @@ -1,6 +1,8 @@ "use client"; +import type { ParseKeys, TFunction } from "i18next"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Switch } from "@/components/ui/switch"; import { FieldLabel } from "@/components/ui/field"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; @@ -21,21 +23,21 @@ import { TelemetryEnterpriseDialog } from "./TelemetryEnterpriseDialog"; const TABS = ["proxy", "ui"] as const; -const destinationText = (settings: TelemetrySettings): string => { - const every = `every ${settings.flush_interval_seconds} s`; +const destinationText = (settings: TelemetrySettings, t: TFunction): string => { + const every = t("telemetry.destinationText.every", { seconds: settings.flush_interval_seconds }); switch (settings.destination) { case "http": - return `One JSON report per worker ${every} and on shutdown, by HTTP POST to LITELLM_TELEMETRY_ENDPOINT. A failed send is retried next window`; + return t("telemetry.destinationText.http", { every }); case "local_table": - return `One report per worker ${every} and on shutdown, stored in this proxy's LiteLLM_TelemetryReport table for ${settings.retention_days} days. Nothing leaves the proxy. Admins can export from GET /telemetry/reports`; + return t("telemetry.destinationText.localTable", { every, count: settings.retention_days }); case null: - return "No destination is chosen, so nothing is recorded"; + return t("telemetry.destinationText.none"); } }; -const DESTINATION_LABELS: Record = { - local_table: "Local table (LiteLLM_TelemetryReport in this proxy's database)", - http: "HTTP (POST to LITELLM_TELEMETRY_ENDPOINT)", +const DESTINATION_LABELS: Record = { + local_table: "telemetry.destination.localTable", + http: "telemetry.destination.http", }; type SettingsUpdate = ReturnType; @@ -57,15 +59,14 @@ function DestinationPicker({ update: SettingsUpdate; requestEnable: () => void; }) { + const { t } = useTranslation(); const unchangeable = !settings.editable || settings.destination_set_by_environment; const locked = unchangeable || update.isPending || !isOn(settings); const missing = settings.destination === null && enabledGroups(settings).size > 0; return (
-

Destination

- {missing && ( -

Telemetry is on but no destination is chosen, so nothing is recorded

- )} +

{t("telemetry.destination.title")}

+ {missing &&

{t("telemetry.destination.missing")}

} - {DESTINATION_LABELS[destination]} + {t(DESTINATION_LABELS[destination])} ))} {settings.destination_set_by_environment && ( -

Set by LITELLM_TELEMETRY_DESTINATION

+

{t("telemetry.destination.setByEnvironment")}

)}
); @@ -104,6 +105,7 @@ function GroupList({ update: SettingsUpdate; requestEnable: () => void; }) { + const { t } = useTranslation(); const requires: Requires = new Map(settings.groups.map((info) => [info.group, info.requires ?? null])); const enabled = enabledGroups(settings); const depths = relativeDepths(groups, requires); @@ -129,18 +131,27 @@ function GroupList({ > onToggle(group, on)} />
-
{copy.title}
- {copy.slices !== undefined &&

Slices by: {copy.slices}

} +
{t(copy.title)}
+ {copy.slices !== undefined && ( +

+ {t("telemetry.groupList.slicesBy")} + {t(copy.slices)} +

+ )}

- {copy.slices !== undefined ? "Counts" : "Sends"}: {copy.counts} + {copy.slices !== undefined ? t("telemetry.groupList.counts") : t("telemetry.groupList.sends")}:{" "} + {t(copy.counts)} +

+

+ {t("telemetry.groupList.helpsCatch")} + {t(copy.helps)}

-

Helps catch: {copy.helps}

); @@ -159,32 +170,32 @@ function TelemetrySwitch({ update: SettingsUpdate; requestEnable: () => void; }) { + const { t } = useTranslation(); const on = isOn(settings); return (
next ? requestEnable() : update.mutate({ groups: [], destination: savedDestination(settings) }) } /> - {on ? "On" : "Off"} + {on ? t("telemetry.switch.on") : t("telemetry.switch.off")}
); } function ReportPreview({ settings }: { settings: TelemetrySettings }) { + const { t } = useTranslation(); return (

- {settings.report_is_sample ? "Sample report" : "Last report sent from this worker"} + {settings.report_is_sample ? t("telemetry.report.sampleTitle") : t("telemetry.report.lastTitle")}

- {settings.report_is_sample - ? "Made-up traffic through the real filter, with the groups that are on (all groups while telemetry is off)" - : "Exactly what this worker last sent or stored"} + {settings.report_is_sample ? t("telemetry.report.sampleDescription") : t("telemetry.report.lastDescription")}

         {JSON.stringify(settings.report, null, 2)}
@@ -194,31 +205,35 @@ function ReportPreview({ settings }: { settings: TelemetrySettings }) {
 }
 
 export default function TelemetrySettingsPage() {
+  const { t } = useTranslation();
   const [tab, setTab] = useUrlTab(TABS, "proxy");
   const { data: settings, isLoading, error } = useTelemetrySettings();
   const update = useUpdateTelemetrySettings();
   const { premiumUser } = useAuthorized();
   const [enableDialogOpen, setEnableDialogOpen] = useState(false);
   const requestEnable = () => setEnableDialogOpen(true);
-  if (isLoading) return Loading telemetry settings...;
+  if (isLoading) return {t("telemetry.page.loading")};
   if (settings === undefined)
-    return Could not load telemetry settings: {error?.message};
+    return (
+      
+        {t("telemetry.page.loadFailed")}
+        {error?.message}
+      
+    );
   return (
     
        setTab(value as (typeof TABS)[number])}>
         
           
             
-            Telemetry
+            {t("telemetry.page.title")}
           
-          
-            Choose what privacy-safe, aggregated usage data this proxy shares
-          
+          {t("telemetry.page.description")}
           
             
             
-              Proxy requests
-              Admin UI
+              {t("telemetry.page.tabs.proxy")}
+              {t("telemetry.page.tabs.ui")}
             
           
         
@@ -227,20 +242,16 @@ export default function TelemetrySettingsPage() {
         
         
           
-          

- Sent only while Page navigation is on, to this proxy at POST /telemetry/ui_events, and counted into the same - report as proxy traffic -

+

{t("telemetry.page.uiNote")}

-

How and when it is sent

-

{destinationText(settings)}. Changes apply from the next window

+

{t("telemetry.howSent.title")}

- Off by default. Each switch adds aggregated counts, never prompts, responses, keys, ids or header values. - LITELLM_TELEMETRY_DISABLED=true turns everything off + {destinationText(settings, t)}. {t("telemetry.howSent.changesApply")}

+

{t("telemetry.howSent.offByDefault")}

> = { heartbeat: { - title: "Heartbeat", - counts: "Random instance id, LiteLLM version, report window, which groups are on", - helps: "Which versions are running", + title: "telemetry.groups.heartbeat.title", + counts: "telemetry.groups.heartbeat.counts", + helps: "telemetry.groups.heartbeat.helps", }, request_success: { - title: "Request success", - slices: "endpoint, LiteLLM status, provider status, stream, handled by Rust, LiteLLM cache hit", - counts: "requests, provider attempts, latency to headers and to first byte", - helps: "Error-rate and latency regressions, Rust vs Python", + title: "telemetry.groups.requestSuccess.title", + slices: "telemetry.groups.requestSuccess.slices", + counts: "telemetry.groups.requestSuccess.counts", + helps: "telemetry.groups.requestSuccess.helps", }, token_info: { - title: "Token info", - slices: "provider prompt-cache hit", - counts: "input, output and cache-read token sums", - helps: "Broken prompt caching or token counting", + title: "telemetry.groups.tokenInfo.title", + slices: "telemetry.groups.tokenInfo.slices", + counts: "telemetry.groups.tokenInfo.counts", + helps: "telemetry.groups.tokenInfo.helps", }, request_taxonomy: { - title: "Request taxonomy", - slices: "provider, salted deployment hash", - counts: "one row per provider attempt with its status", - helps: "Which provider is failing, retries and fallbacks", + title: "telemetry.groups.requestTaxonomy.title", + slices: "telemetry.groups.requestTaxonomy.slices", + counts: "telemetry.groups.requestTaxonomy.counts", + helps: "telemetry.groups.requestTaxonomy.helps", }, event_details: { - title: "Event details", - counts: "message block counts and types, allowlisted header names (no values), provider time to first token", - helps: "Which request shapes and clients fail", + title: "telemetry.groups.eventDetails.title", + counts: "telemetry.groups.eventDetails.counts", + helps: "telemetry.groups.eventDetails.helps", }, instance_configuration: { - title: "Instance configuration", - counts: "names of allowlisted config keys that are set (no values)", - helps: "Which features an upgrade must keep working", + title: "telemetry.groups.instanceConfiguration.title", + counts: "telemetry.groups.instanceConfiguration.counts", + helps: "telemetry.groups.instanceConfiguration.helps", }, page_navigation: { - title: "Page navigation", - counts: "Admin UI page views and tab switches by route name (no ids or typed text)", - helps: "Which pages and tabs get used", + title: "telemetry.groups.pageNavigation.title", + counts: "telemetry.groups.pageNavigation.counts", + helps: "telemetry.groups.pageNavigation.helps", }, }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ClientUsagePanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ClientUsagePanel.tsx index 3ca8f2e038d..0db68d5e207 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ClientUsagePanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ClientUsagePanel.tsx @@ -1,6 +1,8 @@ "use client"; import { useMemo } from "react"; +import type { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; import { ArrowLeft, Coins } from "lucide-react"; import { useTagSummary } from "@/app/(dashboard)/hooks/tags/useTagSummary"; import AdvancedDatePicker from "@/components/shared/advanced_date_picker"; @@ -12,6 +14,7 @@ import { stackedUsageColor } from "@/components/shared/charts"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Skeleton } from "@/components/ui/skeleton"; +import i18n from "@/i18n/i18n"; import { EMPTY_DAILY_ACTIVITY_METADATA, toDailyData } from "@/components/UsagePage/dailyActivityApi"; import type { DailyData } from "@/components/UsagePage/types"; import claudeLogo from "../../../../../../public/assets/logos/claude.svg"; @@ -53,7 +56,8 @@ interface ClientUsagePanelProps { } const dateLabel = (date: Date | null) => date?.toLocaleDateString("en-US", { month: "short", day: "numeric" }) ?? "—"; -const countLabel = (value: number | null) => (value === null ? "Unavailable" : value.toLocaleString()); +const countLabel = (value: number | null, t: TFunction) => + value === null ? t("usage.clientPanel.tokenBreakdown.unavailable") : value.toLocaleString(); export default function ClientUsagePanel({ client, @@ -63,7 +67,7 @@ export default function ClientUsagePanel({ scopeError: accessError = null, teams = [], members = [], - defaultScopeLabel = "All users", + defaultScopeLabel = i18n.t("usage.clientPanel.scope.allUsers"), onTeamChange, onUserChange, onClose, @@ -80,6 +84,7 @@ export default function ClientUsagePanel({ summaryFailed = false, onRetry, }: ClientUsagePanelProps) { + const { t } = useTranslation(); const label = client === "claude-desktop" ? "Claude Desktop" : "Claude Code"; const scopedRequest = Boolean(userId || teamId) && !accessError; const personRequest = { @@ -93,11 +98,12 @@ export default function ClientUsagePanel({ const personUsage = useClientUsage(personRequest); const allUsersEnabled = canChooseUser && !scopedRequest && !accessError; const teamLabel = teams.find((team) => team.value === teamId)?.label ?? teamId; - const personLabel = members.find((member) => member.value === userId)?.label ?? "Selected user"; + const personLabel = + members.find((member) => member.value === userId)?.label ?? t("usage.clientPanel.scope.selectedUser"); const selectedScopeLabel = (() => { - if (teamId) return `${teamLabel} · ${userId ? personLabel : "All members"}`; - if (!canChooseUser) return "My usage"; - return userId ? "Selected user" : "All users"; + if (teamId) return `${teamLabel} · ${userId ? personLabel : t("usage.clientPanel.scope.allMembers")}`; + if (!canChooseUser) return t("usage.clientPanel.scope.myUsage"); + return userId ? t("usage.clientPanel.scope.selectedUser") : t("usage.clientPanel.scope.allUsers"); })(); const summaryQuery = useTagSummary(startTime, endTime, allUsersEnabled && agent === null); const activeAgent = useMemo( @@ -115,9 +121,9 @@ export default function ClientUsagePanel({ const personTotals = overviewTotals(personUsage.data.metadata ?? EMPTY_DAILY_ACTIVITY_METADATA, null); const pending = !accessError && (scopedRequest ? personUsage.loading : (loading || summaryQuery.isLoading) && activeAgent === null); - const summaryError = summaryQuery.isError || summaryFailed ? "Could not load client totals" : null; + const summaryError = summaryQuery.isError || summaryFailed ? t("usage.clientPanel.errors.totalsFailed") : null; const scopeError = - accessError ?? (!canChooseUser && !scopedRequest ? "Sign in to view your client usage" : summaryError); + accessError ?? (!canChooseUser && !scopedRequest ? t("usage.clientPanel.errors.signIn") : summaryError); const error = accessError ?? (scopedRequest ? personUsage.error : scopeError); const summary = scopedRequest ? personTotals : agentDetail?.summary; const rawBreakdown = scopedRequest @@ -152,7 +158,7 @@ export default function ClientUsagePanel({
@@ -176,11 +182,11 @@ export default function ClientUsagePanel({ {teams.length > 0 && onTeamChange && (
({ ...member, value: `user:${member.value}` })), ]} allowClear={false} @@ -213,11 +219,11 @@ export default function ClientUsagePanel({
{userId && ( )}
@@ -225,13 +231,13 @@ export default function ClientUsagePanel({
)} {pending && ( -
+
)} {error && (
-

Could not load client usage

+

{t("usage.clientPanel.errors.loadFailed")}

{error}

{canRetry && ( )}
)} {ready && !summary && ( -

No {label} usage recorded in this date range.

+

+ {t("usage.clientPanel.noUsage", { client: label })} +

)} {ready && summary && ( <>
- - - + + +
{summary.requests === 0 ? (

- No {label} requests in the selected range. Try another date range. - {(canChooseUser || teamId) && " You can also choose another person."} + {t("usage.clientPanel.noRequests", { client: label })} + {(canChooseUser || teamId) && t("usage.clientPanel.chooseAnotherPerson")}

) : ( <> @@ -283,7 +294,7 @@ export default function ClientUsagePanel({ {chartError}

)} @@ -291,10 +302,8 @@ export default function ClientUsagePanel({ )}

- {scopedRequest - ? "Based on retained request logs within the exact selected time range." - : "All-user totals use UTC daily aggregates for the selected dates. Filtering by person uses the exact selected times. Cache values are reported counts."}{" "} - Spend is the cost recorded by LiteLLM. + {scopedRequest ? t("usage.clientPanel.footnote.scoped") : t("usage.clientPanel.footnote.allUsers")}{" "} + {t("usage.clientPanel.footnote.spend")}

)} @@ -313,20 +322,26 @@ function SummaryStat({ label, value }: { label: string; value: string }) { } function TokenExplanation({ total, breakdown }: { total: number; breakdown: ClientAgentDetail["tokenBreakdown"] }) { + const { t } = useTranslation(); const { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } = breakdown; const freshInput = inputTokens !== null && cacheReadTokens !== null && cacheWriteTokens !== null ? inputTokens - cacheReadTokens - cacheWriteTokens : null; const rows = [ - { label: "Cache reads", value: cacheReadTokens, color: stackedUsageColor(0) }, - { label: "Cache writes", value: cacheWriteTokens, color: stackedUsageColor(1) }, - { label: "Uncached input", value: freshInput, color: stackedUsageColor(2) }, - { label: "Output", value: outputTokens, color: stackedUsageColor(3) }, + { label: t("usage.clientPanel.tokenBreakdown.cacheReads"), value: cacheReadTokens, color: stackedUsageColor(0) }, + { label: t("usage.clientPanel.tokenBreakdown.cacheWrites"), value: cacheWriteTokens, color: stackedUsageColor(1) }, + { label: t("usage.clientPanel.tokenBreakdown.uncachedInput"), value: freshInput, color: stackedUsageColor(2) }, + { label: t("usage.clientPanel.tokenBreakdown.output"), value: outputTokens, color: stackedUsageColor(3) }, ]; const complete = rows.every((row) => row.value !== null); return ( - + {complete && total > 0 && ( ))} -

- Total tokens include input, cached input and output. Cached input is priced differently, so token count alone - does not explain spend. Compare the same person and dates when checking the count shown by a client. -

+

{t("usage.clientPanel.tokenBreakdown.explanation")}

); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ClientUsageSelect.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ClientUsageSelect.tsx index d0b0c7750d6..cab3b7572a3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ClientUsageSelect.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ClientUsageSelect.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import { SearchSelect } from "@/components/shared/SearchSelect"; import { parseUsageClient, UsageClient } from "./overview/agentCatalog"; @@ -13,10 +14,11 @@ export default function ClientUsageSelect({ value: UsageClient | null; onChange: (client: UsageClient | null) => void; }) { + const { t } = useTranslation(); return ( onChange(parseUsageClient(client))} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx index b55ea0513c7..687571e70be 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { ChartColumnStacked } from "lucide-react"; import { BarChart, CustomTooltip } from "@/components/shared/charts"; import { MetricWithMetadata } from "@/components/UsagePage/types"; @@ -16,6 +17,7 @@ const COLORS = [SUCCESS_COLOR, FAILED_COLOR] as const; const valueFormatter = (value: number) => value.toLocaleString(); const EndpointUsageBarChart: React.FC = ({ endpointData }) => { + const { t } = useTranslation(); // Transform endpoint data into chart format const chartData = React.useMemo(() => { return Object.entries(endpointData || {}).map(([endpoint, data]) => ({ @@ -37,7 +39,7 @@ const EndpointUsageBarChart: React.FC = ({ endpointD return ( {CATEGORIES.map((category, i) => ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx index cd35034fe1f..c390298fa6f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx @@ -1,4 +1,5 @@ import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { ChartLine } from "lucide-react"; import { LineChart, stackedUsageColor } from "@/components/shared/charts"; import { DailyData } from "@/components/UsagePage/types"; @@ -44,6 +45,7 @@ function transformDailyDataToChart(dailyData: DailyData[]): Array { if (!dailyData?.results || dailyData.results.length === 0) { return []; @@ -62,7 +64,7 @@ export function EndpointUsageLineChart({ dailyData }: EndpointUsageLineChartProp const colors = useMemo(() => categories.map((_, i) => stackedUsageColor(i)), [categories]); return ( - + = ({ endpointData }) => { + const { t } = useTranslation(); const calculateSuccessRate = (successful: number, total: number): number => { if (total === 0) return 0; return (successful / total) * 100; @@ -42,12 +44,12 @@ const EndpointUsageTable: React.FC = ({ endpointData }) const columns: ColumnDef[] = [ { - header: "Endpoint", + header: t("usage.endpointUsage.table.columns.endpoint"), accessorKey: "endpoint", cell: ({ row }) => {row.original.endpoint}, }, { - header: "Successful / Failed", + header: t("usage.endpointUsage.table.columns.successfulFailed"), id: "requests", cell: ({ row }) => { const record = row.original; @@ -59,7 +61,11 @@ const EndpointUsageTable: React.FC = ({ endpointData }) return (
- + 0 ? "h-1 bg-destructive/70" : "h-1"}> @@ -77,13 +83,13 @@ const EndpointUsageTable: React.FC = ({ endpointData }) }, }, { - header: "Total Request", + header: t("usage.endpointUsage.table.columns.totalRequest"), accessorKey: "api_requests", meta: { numeric: true }, cell: ({ row }) => {row.original.api_requests.toLocaleString()}, }, { - header: "Success Rate", + header: t("usage.endpointUsage.table.columns.successRate"), accessorKey: "successRate", meta: { numeric: true }, cell: ({ row }) => { @@ -102,13 +108,13 @@ const EndpointUsageTable: React.FC = ({ endpointData }) }, }, { - header: "Total Tokens", + header: t("usage.endpointUsage.table.columns.totalTokens"), accessorKey: "total_tokens", meta: { numeric: true }, cell: ({ row }) => {row.original.total_tokens.toLocaleString()}, }, { - header: "Spend", + header: t("usage.endpointUsage.table.columns.spend"), accessorKey: "spend", meta: { numeric: true }, cell: ({ row }) => , @@ -118,10 +124,13 @@ const EndpointUsageTable: React.FC = ({ endpointData }) return ( - {dataSource.length.toLocaleString()} {dataSource.length === 1 ? "endpoint" : "endpoints"} + {dataSource.length.toLocaleString()}{" "} + {dataSource.length === 1 + ? t("usage.endpointUsage.table.endpointSingular") + : t("usage.endpointUsage.table.endpointPlural")} } > @@ -129,7 +138,7 @@ const EndpointUsageTable: React.FC = ({ endpointData }) columns={columns} data={dataSource} getRowId={(row) => row.key} - noDataMessage="No endpoint usage data" + noDataMessage={t("usage.endpointUsage.table.noData")} size="compact" /> diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx index ca170dc718a..96656225b30 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx @@ -20,6 +20,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/cva.config"; import React, { type ReactNode, useCallback, useMemo, useState } from "react"; +import type { ParseKeys, TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; import TeamMultiSelect from "@/components/common_components/team_multi_select"; import UserDropdown from "@/components/common_components/UserDropdown"; import CustomerMultiSelect from "@/components/common_components/CustomerMultiSelect"; @@ -57,9 +59,13 @@ import TeamUserSpendCard from "./TeamUserSpendCard"; import { ProviderSpendBreakdown } from "./SpendByProvider"; const BRAND = "#2b3fd6"; -const FLAT_COST_SERIES = "Flat cost"; +const FLAT_COST_SERIES: ParseKeys = "usage.entityUsage.chart.flatCostSeries"; const FLAT_COST_COLOR = "#8b5cf6"; -const METRIC_TITLE: Record = { spend: "Spend", tokens: "Tokens", requests: "Requests" }; +const METRIC_TITLE: Record = { + spend: "usage.entityUsage.chart.metricTitle.spend", + tokens: "usage.entityUsage.chart.metricTitle.tokens", + requests: "usage.entityUsage.chart.metricTitle.requests", +}; /** Summary tiles that carry a sparkline, keyed by tile title, valued by the dailyTotals field. */ const TILE_TRENDS: Readonly> = { "Total Spend": "spend", @@ -79,12 +85,12 @@ const QUIET_HEADER = { headerClassName: "font-normal" }; const FLAT_COST_KEY = "flat_cost"; /** Stacks reserved-capacity flat cost on top of the per-model spend, so each bar is the day's full cost. */ -const withFlatCost = (series: Series, results: readonly DailyData[]): Series => { +const withFlatCost = (series: Series, results: readonly DailyData[], t: TFunction): Series => { const flatByDate = new Map(results.map((day) => [day.date, day.metrics.flat_cost ?? 0])); return { data: series.data.map((day) => ({ ...day, [FLAT_COST_KEY]: flatByDate.get(day.date) ?? 0 })), keys: [...series.keys, FLAT_COST_KEY], - labels: [...series.labels, FLAT_COST_SERIES], + labels: [...series.labels, t(FLAT_COST_SERIES)], colors: [...series.colors, FLAT_COST_COLOR], }; }; @@ -144,6 +150,7 @@ const EntityUsage: React.FC = ({ selectedEntities: selectedTags, onSelectedEntitiesChange: setSelectedTags, }) => { + const { t } = useTranslation(); const { teams } = useTeams(); const teamList = useMemo(() => teams ?? [], [teams]); const [modelViewType, setModelViewType] = useState("groups"); @@ -341,11 +348,11 @@ const EntityUsage: React.FC = ({ ); const getFilterLabel = (entityType: string) => { - return `Filter by ${entityType}`; + return t("usage.entityUsage.filterLabel", { entity: entityType }); }; const getFilterPlaceholder = (entityType: string) => { - return `Select ${entityType} to filter...`; + return t("usage.entityUsage.filterPlaceholder", { entity: entityType }); }; const entityFilterSlots: Partial> = { @@ -374,9 +381,9 @@ const EntityUsage: React.FC = ({ const dailyTrend = useMemo(() => dailyTotals(spendData.results), [spendData.results]); const stackFlatCost = showFlatCost && breakdown.metric === "spend"; const chartSeries = useMemo(() => { - const series = stackFlatCost ? withFlatCost(dailySeries, spendData.results) : dailySeries; + const series = stackFlatCost ? withFlatCost(dailySeries, spendData.results, t) : dailySeries; return bucketSeries(series, granularity); - }, [stackFlatCost, dailySeries, spendData.results, granularity]); + }, [stackFlatCost, dailySeries, spendData.results, granularity, t]); const chartTotals = useMemo(() => bucketTotals(chartSeries), [chartSeries]); const entityBreakdownColumns = useMemo[]>( @@ -388,37 +395,37 @@ const EntityUsage: React.FC = ({ cell: ({ row }) => {row.original.metadata.alias}, }, { - header: "Share", + header: t("usage.entityUsage.breakdownTable.share"), id: "share", meta: { ...QUIET_HEADER, className: "w-40" }, cell: ({ row }) => , }, { - header: "Spend", + header: t("usage.entityUsage.breakdownTable.spend"), accessorKey: "metrics.spend", meta: { numeric: true, ...QUIET_HEADER }, cell: ({ row }) => , }, { - header: "Successful", + header: t("usage.entityUsage.breakdownTable.successful"), accessorKey: "metrics.successful_requests", meta: { numeric: true, className: "text-success", ...QUIET_HEADER }, cell: ({ row }) => row.original.metrics.successful_requests.toLocaleString(), }, { - header: "Failed", + header: t("usage.entityUsage.breakdownTable.failed"), accessorKey: "metrics.failed_requests", meta: { numeric: true, className: "text-destructive", ...QUIET_HEADER }, cell: ({ row }) => row.original.metrics.failed_requests.toLocaleString(), }, { - header: "Tokens", + header: t("usage.entityUsage.breakdownTable.tokens"), accessorKey: "metrics.total_tokens", meta: { numeric: true, ...QUIET_HEADER }, cell: ({ row }) => row.original.metrics.total_tokens.toLocaleString(), }, ], - [capitalizedEntityLabel, maxEntitySpend], + [capitalizedEntityLabel, maxEntitySpend, t], ); const chev = "size-3 text-muted-foreground"; @@ -471,10 +478,11 @@ const EntityUsage: React.FC = ({ ); }; - const breakdownTiles = showFlatCost && showCostBreakdown ? buildCostBreakdownTiles(spendData.metadata) : []; - const summaryTiles = buildSummaryTiles(spendData.metadata, showFlatCost, showPtuHours); + const breakdownTiles = showFlatCost && showCostBreakdown ? buildCostBreakdownTiles(spendData.metadata, t) : []; + const summaryTiles = buildSummaryTiles(spendData.metadata, showFlatCost, showPtuHours, t); - const modelViewTitle = modelViewType === "groups" ? "Top Public Model Names" : "Top Litellm Models"; + const modelViewTitle = + modelViewType === "groups" ? t("usage.entityUsage.topPublicModelNames") : t("usage.entityUsage.topLitellmModels"); const chartFormat = (value: number) => formatMetricValue(value, breakdown.metric); const costPanel = loading ? ( @@ -482,7 +490,9 @@ const EntityUsage: React.FC = ({ ) : (
-

{capitalizedEntityLabel} Spend Overview

+

+ {t("usage.entityUsage.spendOverview", { entity: capitalizedEntityLabel })} +

= ({

- {METRIC_TITLE[breakdown.metric]} by {modelViewType === "groups" ? "model" : "deployment"} + {t("usage.entityUsage.chart.title", { + metric: t(METRIC_TITLE[breakdown.metric]), + dimension: + modelViewType === "groups" + ? t("usage.entityUsage.chart.dimension.model") + : t("usage.entityUsage.chart.dimension.deployment"), + })}

- {granularity === "day" ? "Daily" : "Weekly"} {breakdown.metric}, top 8 stacked - {stackFlatCost ? ", flat cost on top" : ""} + {t("usage.entityUsage.chart.subtitle", { + granularity: + granularity === "day" + ? t("usage.entityUsage.chart.granularity.daily") + : t("usage.entityUsage.chart.granularity.weekly"), + metric: breakdown.metric, + })} + {stackFlatCost ? t("usage.entityUsage.chart.flatCostOnTop") : ""}

- +
@@ -552,13 +579,13 @@ const EntityUsage: React.FC = ({ - Track cost per {entityType} + {t("usage.entityUsage.trackCostPer", { entity: entityType })} @@ -583,7 +610,7 @@ const EntityUsage: React.FC = ({ )}
- + = ({ } > = ({
{showAgentBreakdown && ( - + {agentLoading ? ( ) : ( @@ -619,17 +646,20 @@ const EntityUsage: React.FC = ({ )} - +
); const tabs: readonly { key: string; label: string; content: ReactNode }[] = [ - { key: "cost", label: "Cost", content: costPanel }, + { key: "cost", label: t("usage.entityUsage.tabs.cost"), content: costPanel }, { key: "models", - label: entityType === "agent" ? "Request / Token Consumption" : "Model Activity", + label: + entityType === "agent" + ? t("usage.entityUsage.tabs.requestTokenConsumption") + : t("usage.entityUsage.tabs.modelActivity"), content: ( <>
@@ -647,14 +677,14 @@ const EntityUsage: React.FC = ({ ? [ { key: "agents", - label: "Agent Activity", + label: t("usage.entityUsage.tabs.agentActivity"), content: , }, ] : []), { key: "keys", - label: "Key Activity", + label: t("usage.entityUsage.tabs.keyActivity"), content: ( = ({ /> ), }, - { key: "endpoints", label: "Endpoint Activity", content: }, + { + key: "endpoints", + label: t("usage.entityUsage.tabs.endpointActivity"), + content: , + }, ]; return (
- {failed && } + {failed && } {showAgentBreakdown && agentFailed && ( - + )}
[] = [ { - header: "Provider", + header: () => i18n.t("usage.providerSpend.columns.provider"), accessorKey: "provider", meta: QUIET_HEADER, cell: ({ row }) => ( @@ -47,33 +49,33 @@ const columns: ColumnDef[] = [ ), }, { - header: "Spend", + header: () => i18n.t("usage.providerSpend.columns.spend"), accessorKey: "spend", meta: { numeric: true, ...QUIET_HEADER }, cell: ({ row }) => row.original.subscription_covered ? (
{row.original.spend > 0 && } - Included in subscription + {i18n.t("usage.providerSpend.includedInSubscription")}
) : ( ), }, { - header: "Successful", + header: () => i18n.t("usage.providerSpend.columns.successful"), accessorKey: "successful_requests", meta: { numeric: true, className: "text-success", ...QUIET_HEADER }, cell: ({ row }) => row.original.successful_requests.toLocaleString(), }, { - header: "Failed", + header: () => i18n.t("usage.providerSpend.columns.failed"), accessorKey: "failed_requests", meta: { numeric: true, className: "text-destructive", ...QUIET_HEADER }, cell: ({ row }) => row.original.failed_requests.toLocaleString(), }, { - header: "Tokens", + header: () => i18n.t("usage.providerSpend.columns.tokens"), accessorKey: "tokens", meta: { numeric: true, ...QUIET_HEADER }, cell: ({ row }) => row.original.tokens.toLocaleString(), @@ -86,6 +88,7 @@ const columns: ColumnDef[] = [ * usage views. */ export function ProviderSpendBreakdown({ rows }: { rows: readonly ProviderSpendData[] }) { + const { t } = useTranslation(); const sorted = [...rows].sort((a, b) => b.spend - a.spend); const total = sorted.reduce((sum, row) => sum + row.spend, 0); const shareOf = (spend: number) => (total > 0 ? (spend / total) * 100 : 0); @@ -96,7 +99,7 @@ export function ProviderSpendBreakdown({ rows }: { rows: readonly ProviderSpendD {visible.length > 0 && (
- Total provider spend + {t("usage.providerSpend.totalProviderSpend")} ${formatNumberWithCommas(total, 2)} @@ -127,7 +130,7 @@ export function ProviderSpendBreakdown({ rows }: { rows: readonly ProviderSpendD columns={columns} data={sorted} getRowId={(row) => row.provider} - noDataMessage="No provider usage data" + noDataMessage={t("usage.providerSpend.noData")} size="compact" />
@@ -163,6 +166,7 @@ function ToggleField({ } const SpendByProvider: React.FC = ({ loading, isDateChanging, providerSpend }) => { + const { t } = useTranslation(); const [includeZeroSpend, setIncludeZeroSpend] = useState(false); const [includeUnknown, setIncludeUnknown] = useState(false); @@ -190,16 +194,20 @@ const SpendByProvider: React.FC = ({ loading, isDateChangi return ( - +
} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamUserSpendCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamUserSpendCard.tsx index a5e0bba09cd..f8cba576053 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamUserSpendCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TeamUserSpendCard.tsx @@ -2,11 +2,13 @@ import { useQuery } from "@tanstack/react-query"; import type { ColumnDef } from "@tanstack/react-table"; import { Download, Users } from "lucide-react"; import React, { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { teamSpendByUserCall } from "@/components/networking"; import { DataTable } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; import { Button } from "@/components/ui/button"; +import i18n from "@/i18n/i18n"; import { Panel } from "../overview/Primitives"; @@ -31,40 +33,46 @@ interface TeamUserSpendCardProps { const QUIET_HEADER = { headerClassName: "font-normal" }; const columns: ColumnDef[] = [ - { header: "Team", accessorFn: teamLabel, id: "team", meta: QUIET_HEADER, cell: ({ row }) => teamLabel(row.original) }, { - header: "User", + header: () => i18n.t("usage.teamUserSpend.columns.team"), + accessorFn: teamLabel, + id: "team", + meta: QUIET_HEADER, + cell: ({ row }) => teamLabel(row.original), + }, + { + header: () => i18n.t("usage.teamUserSpend.columns.user"), accessorFn: userLabel, id: "user", meta: QUIET_HEADER, cell: ({ row }) => {userLabel(row.original)}, }, { - header: "Spend", + header: () => i18n.t("usage.teamUserSpend.columns.spend"), accessorKey: "spend", meta: { numeric: true, ...QUIET_HEADER }, cell: ({ row }) => , }, { - header: "Requests", + header: () => i18n.t("usage.teamUserSpend.columns.requests"), accessorKey: "api_requests", meta: { numeric: true, ...QUIET_HEADER }, cell: ({ row }) => row.original.api_requests.toLocaleString(), }, { - header: "Successful", + header: () => i18n.t("usage.teamUserSpend.columns.successful"), accessorKey: "successful_requests", meta: { numeric: true, className: "text-success", ...QUIET_HEADER }, cell: ({ row }) => row.original.successful_requests.toLocaleString(), }, { - header: "Failed", + header: () => i18n.t("usage.teamUserSpend.columns.failed"), accessorKey: "failed_requests", meta: { numeric: true, className: "text-destructive", ...QUIET_HEADER }, cell: ({ row }) => row.original.failed_requests.toLocaleString(), }, { - header: "Tokens", + header: () => i18n.t("usage.teamUserSpend.columns.tokens"), accessorKey: "total_tokens", meta: { numeric: true, ...QUIET_HEADER }, cell: ({ row }) => row.original.total_tokens.toLocaleString(), @@ -72,6 +80,7 @@ const columns: ColumnDef[] = [ ]; const TeamUserSpendCard: React.FC = ({ accessToken, startTime, endTime, teamIds }) => { + const { t } = useTranslation(); const hasTeams = teamIds.length > 0; const { data, isLoading, error } = useQuery({ queryKey: ["teamSpendByUser", startTime?.toISOString(), endTime?.toISOString(), teamIds], @@ -84,7 +93,7 @@ const TeamUserSpendCard: React.FC = ({ accessToken, star return ( = ({ accessToken, star onClick={() => data && downloadCsv(buildTeamUserSpendCsv(data), teamUserSpendCsvFileName(data))} > - Download CSV + {t("usage.teamUserSpend.downloadCsv")} } > -

- Attributed per request from spend logs, so it includes JWT/SSO traffic that does not use a virtual key -

+

{t("usage.teamUserSpend.attributionNote")}

diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TopModelView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TopModelView.tsx index 0d9544f17c0..ae98dd835b5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TopModelView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/TopModelView.tsx @@ -3,6 +3,7 @@ import { DataTable } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { Segmented } from "../overview/Primitives"; import { providerForModel } from "../overview/modelProvider"; @@ -50,35 +51,36 @@ function ModelName({ name }: { name: string | undefined }) { } export default function TopModelView({ topModels, topModelsLimit, setTopModelsLimit }: TopModelViewProps) { + const { t } = useTranslation(); const [modelViewMode, setModelViewMode] = useState("table"); const columns = [ { - header: "Model", + header: t("usage.topModelView.columns.model"), accessorKey: "key", meta: QUIET_HEADER, cell: (info: any) => , }, { - header: "Spend (USD)", + header: t("usage.topModelView.columns.spendUsd"), accessorKey: "spend", meta: { numeric: true, ...QUIET_HEADER }, cell: (info: any) => , }, { - header: "Successful", + header: t("usage.topModelView.columns.successful"), accessorKey: "successful_requests", meta: { numeric: true, ...QUIET_HEADER }, cell: (info: any) => {info.getValue()?.toLocaleString() || 0}, }, { - header: "Failed", + header: t("usage.topModelView.columns.failed"), accessorKey: "failed_requests", meta: { numeric: true, ...QUIET_HEADER }, cell: (info: any) => {info.getValue()?.toLocaleString() || 0}, }, { - header: "Tokens", + header: t("usage.topModelView.columns.tokens"), accessorKey: "tokens", meta: { numeric: true, ...QUIET_HEADER }, cell: (info: any) => info.getValue()?.toLocaleString() || 0, @@ -90,13 +92,13 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi <>
setTopModelsLimit(Number(limit))} /> (metadata.total_flat_cost ?? 0) > 0; @@ -38,6 +36,7 @@ export const buildSummaryTiles = ( metadata: SpendSummaryMetadata, showFlatCost: boolean, showPtuHours: boolean = false, + t: TFunction = i18n.t.bind(i18n), ): SummaryTile[] => { const flatCost = metadata.total_flat_cost ?? 0; const ptuHoursTile: SummaryTile[] = showPtuHours @@ -45,7 +44,7 @@ export const buildSummaryTiles = ( { title: "PTU Hours", value: formatNumberWithCommas(metadata.total_ptu_hours ?? 0, 2), - tooltip: PTU_HOURS_TOOLTIP, + tooltip: t(PTU_HOURS_TOOLTIP), }, ] : []; @@ -54,7 +53,7 @@ export const buildSummaryTiles = ( ? { title: "Total Cost", value: `$${formatNumberWithCommas(metadata.total_spend + flatCost, 2)}`, - tooltip: TOTAL_COST_TOOLTIP, + tooltip: t(TOTAL_COST_TOOLTIP), expandable: true, } : { title: "Total Spend", value: `$${formatNumberWithCommas(metadata.total_spend, 2)}` }, @@ -70,17 +69,20 @@ export const buildSummaryTiles = ( ]; }; -export const buildCostBreakdownTiles = (metadata: SpendSummaryMetadata): SummaryTile[] => [ +export const buildCostBreakdownTiles = ( + metadata: SpendSummaryMetadata, + t: TFunction = i18n.t.bind(i18n), +): SummaryTile[] => [ { title: "Request Cost", value: `$${formatNumberWithCommas(metadata.total_spend, 2)}`, className: "text-info", - tooltip: REQUEST_COST_TOOLTIP, + tooltip: t(REQUEST_COST_TOOLTIP), }, { title: "Flat Cost", value: `$${formatNumberWithCommas(metadata.total_flat_cost ?? 0, 2)}`, className: "text-violet-600", - tooltip: FLAT_COST_TOOLTIP, + tooltip: t(FLAT_COST_TOOLTIP), }, ]; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamUserSpend.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamUserSpend.ts index d0b47a4e5c0..1dd80ec45f9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamUserSpend.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/teamUserSpend.ts @@ -1,14 +1,16 @@ +import type { ParseKeys } from "i18next"; import Papa from "papaparse"; import type { TeamUserSpendResponse } from "@/components/networking"; +import i18n from "@/i18n/i18n"; export type TeamUserSpendRow = TeamUserSpendResponse["results"][number]; -export const NO_USER_LABEL = "(no user)"; +export const NO_USER_LABEL: ParseKeys = "usage.teamUserSpend.noUser"; export const userLabel = (row: TeamUserSpendRow): string => { const identity = row.user_email || row.user_alias; - return identity || row.user_id || NO_USER_LABEL; + return identity || row.user_id || i18n.t(NO_USER_LABEL); }; export const teamLabel = (row: TeamUserSpendRow): string => row.team_alias || row.team_id; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ModelViewToggle.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ModelViewToggle.tsx index 60a841936aa..e02eabd9db5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ModelViewToggle.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/ModelViewToggle.tsx @@ -1,8 +1,11 @@ +import type { ParseKeys } from "i18next"; +import { useTranslation } from "react-i18next"; + export type ModelViewType = "groups" | "individual"; -const MODEL_VIEW_OPTIONS: readonly { value: ModelViewType; label: string }[] = [ - { value: "groups", label: "Public Model Name" }, - { value: "individual", label: "Litellm Model Name" }, +const MODEL_VIEW_OPTIONS: readonly { value: ModelViewType; label: ParseKeys }[] = [ + { value: "groups", label: "usage.modelViewToggle.publicModelName" }, + { value: "individual", label: "usage.modelViewToggle.litellmModelName" }, ]; interface ModelViewToggleProps { @@ -11,6 +14,7 @@ interface ModelViewToggleProps { } export default function ModelViewToggle({ value, onChange }: ModelViewToggleProps) { + const { t } = useTranslation(); return (
{MODEL_VIEW_OPTIONS.map((option) => ( @@ -21,7 +25,7 @@ export default function ModelViewToggle({ value, onChange }: ModelViewToggleProp }`} onClick={() => onChange(option.value)} > - {option.label} + {t(option.label)} ))}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/SubscriptionCostCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/SubscriptionCostCard.tsx index deee30a2db5..4946d86ade1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/SubscriptionCostCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/SubscriptionCostCard.tsx @@ -1,6 +1,7 @@ import type { ColumnDef } from "@tanstack/react-table"; import { Info } from "lucide-react"; import React from "react"; +import { Trans, useTranslation } from "react-i18next"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import { ChartLoader } from "@/components/shared/chart_loader"; import { DataTable } from "@/components/shared/DataTable"; @@ -19,6 +20,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import i18n from "@/i18n/i18n"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import { ATTRIBUTION_RULE, fixedCostByCurrency, formatBillingPeriod, formatFee } from "./formatting"; import type { SubscriptionAccountUsage, SubscriptionUsageResponse } from "./types"; @@ -41,8 +43,9 @@ const accountRowId = (account: SubscriptionAccountUsage): string => `${account.custom_llm_provider}:${account.account_id ?? "unresolved"}`; const AccountCell = ({ account }: { account: SubscriptionAccountUsage }) => { + const { t } = useTranslation(); if (account.account_id === null) { - return Account not signed in on the proxy; + return {t("usage.subscriptionCost.card.accountNotSignedIn")}; } return (
@@ -58,33 +61,38 @@ const ActionsCell = ({ onEditFee, onRemoveFee, }: { account: SubscriptionAccountUsage } & FeeActions) => { + const { t } = useTranslation(); if (account.account_id === null) return null; if (account.fee === null) { return ( ); } return (
- }>Remove + }> + {t("usage.subscriptionCost.card.remove")} + - Remove this monthly fee? + {t("usage.subscriptionCost.removeDialog.title")} - {account.account_id} goes back to "Fixed cost not tracked". Its requests stay included in the - subscription + {account.account_id} }} + /> - Cancel + {t("usage.subscriptionCost.removeDialog.cancel")} onRemoveFee(account)}> - Remove fee + {t("usage.subscriptionCost.removeDialog.confirm")} @@ -95,7 +103,7 @@ const ActionsCell = ({ const baseColumns: ColumnDef[] = [ { - header: "Provider", + header: () => i18n.t("usage.subscriptionCost.columns.provider"), accessorKey: "custom_llm_provider", cell: ({ row }) => (
@@ -105,32 +113,35 @@ const baseColumns: ColumnDef[] = [ ), }, { - header: "Account", + header: () => i18n.t("usage.subscriptionCost.columns.account"), accessorKey: "account_id", cell: ({ row }) => , }, { - header: "Deployments", + header: () => i18n.t("usage.subscriptionCost.columns.deployments"), id: "deployments", - cell: ({ row }) => (row.original.deployments.length === 0 ? "None" : row.original.deployments.join(", ")), + cell: ({ row }) => + row.original.deployments.length === 0 + ? i18n.t("usage.subscriptionCost.card.noDeployments") + : row.original.deployments.join(", "), }, { - header: "Monthly fee", + header: () => i18n.t("usage.subscriptionCost.columns.monthlyFee"), id: "monthly_fee", meta: { numeric: true }, cell: ({ row }) => row.original.fee === null ? ( - Fixed cost not tracked + {i18n.t("usage.subscriptionCost.card.notTracked")} ) : ( formatFee(row.original.fee.monthly_fee, row.original.fee.currency) ), }, { - header: "Billing periods in range", + header: () => i18n.t("usage.subscriptionCost.columns.billingPeriods"), id: "billing_periods", cell: ({ row }) => row.original.billing_periods.length === 0 ? ( - None in range + {i18n.t("usage.subscriptionCost.card.noPeriodsInRange")} ) : (
{row.original.billing_periods.map((period) => ( @@ -140,7 +151,7 @@ const baseColumns: ColumnDef[] = [ ), }, { - header: "Fixed cost in range", + header: () => i18n.t("usage.subscriptionCost.columns.fixedCost"), id: "fixed_cost", meta: { numeric: true }, cell: ({ row }) => @@ -168,6 +179,7 @@ const SubscriptionCostCard: React.FC = ({ onEditFee, onRemoveFee, }) => { + const { t } = useTranslation(); if (!failed && (usage === null || usage.accounts.length === 0)) return null; const columns = canEdit ? [...baseColumns, actionsColumn({ onSetFee, onEditFee, onRemoveFee })] : baseColumns; const accounts = usage?.accounts ?? []; @@ -177,15 +189,15 @@ const SubscriptionCostCard: React.FC = ({ - Subscription fixed costs + {t("usage.subscriptionCost.card.title")} } /> - {ATTRIBUTION_RULE} + {t(ATTRIBUTION_RULE)} - {failed &&

Could not load subscription accounts for this range.

} + {failed &&

{t("usage.subscriptionCost.card.loadFailed")}

} {!failed && loading && } {!failed && !loading && (
@@ -193,24 +205,24 @@ const SubscriptionCostCard: React.FC = ({ columns={columns} data={accounts} getRowId={accountRowId} - noDataMessage="No subscription accounts" + noDataMessage={t("usage.subscriptionCost.card.noAccounts")} size="compact" />
{totals.length === 0 ? ( - Fixed cost in range: none + {t("usage.subscriptionCost.card.fixedCostNone")} ) : ( totals.map(({ currency, total }) => ( - Fixed cost in range: {formatFee(total, currency)} + {t("usage.subscriptionCost.card.fixedCostTotal", { total: formatFee(total, currency) })} )) )} - Usage-billed spend in range, all providers: ${formatNumberWithCommas(meteredSpend, 2)} + {t("usage.subscriptionCost.card.meteredSpend", { spend: formatNumberWithCommas(meteredSpend, 2) })}
-

{ATTRIBUTION_RULE}

+

{t(ATTRIBUTION_RULE)}

)}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/SubscriptionFeeModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/SubscriptionFeeModal.tsx index 1c211a5280c..0afe3af230a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/SubscriptionFeeModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/SubscriptionFeeModal.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { z } from "zod"; import { formatDate } from "@/components/networking"; import { FormField } from "@/components/shared/form/FormField"; @@ -15,20 +16,25 @@ import { } from "@/components/ui/dialog"; import { FieldGroup } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; +import i18n from "@/i18n/i18n"; import { useZodForm } from "@/lib/forms/useZodForm"; import type { SubscriptionAccountUsage, SubscriptionFeeFormValues } from "./types"; const feeShape = { monthly_fee: z .string() - .min(1, "Enter a monthly fee") - .refine((value) => Number(value) > 0, "Monthly fee must be greater than 0"), + .min(1, { error: () => i18n.t("usage.subscriptionCost.feeModal.errors.feeRequired") }) + .refine((value) => Number(value) > 0, { + error: () => i18n.t("usage.subscriptionCost.feeModal.errors.feePositive"), + }), currency: z .string() .trim() .toUpperCase() - .regex(/^[A-Z]{3}$/, "Currency must be a three-letter code"), - billing_period_start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Pick a billing period start date"), + .regex(/^[A-Z]{3}$/, { error: () => i18n.t("usage.subscriptionCost.feeModal.errors.currencyCode") }), + billing_period_start: z + .string() + .regex(/^\d{4}-\d{2}-\d{2}$/, { error: () => i18n.t("usage.subscriptionCost.feeModal.errors.periodStart") }), label: z.string().trim(), }; @@ -51,6 +57,7 @@ interface SubscriptionFeeModalProps { } const SubscriptionFeeModal: React.FC = ({ visible, account, onCancel, onSubmit }) => { + const { t } = useTranslation(); const form = useZodForm(feeSchema, { defaultValues: formInputFor(account) }); const { reset } = form; @@ -77,32 +84,53 @@ const SubscriptionFeeModal: React.FC = ({ visible, ac !open && handleCancel()}> - {account?.fee ? "Edit monthly fee" : "Set monthly fee"} + + {account?.fee + ? t("usage.subscriptionCost.feeModal.editTitle") + : t("usage.subscriptionCost.feeModal.setTitle")} + - {account ? `${account.custom_llm_provider} account ${account.account_id ?? ""}` : ""} + {account + ? t("usage.subscriptionCost.feeModal.description", { + provider: account.custom_llm_provider, + accountId: account.account_id ?? "", + }) + : ""}
- + {({ ref, ...field }) => } - + {({ ref, ...field }) => } - + {({ ref, ...field }) => } - + {({ ref, ...field }) => }
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/formatting.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/formatting.ts index b1d4e0a48ef..5c0abf2592b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/formatting.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/formatting.ts @@ -1,7 +1,7 @@ +import type { ParseKeys } from "i18next"; import type { BillingPeriod } from "./types"; -export const ATTRIBUTION_RULE = - "A subscription fee is counted once for each monthly billing period whose start date falls inside the selected dates. Fixed costs sit beside metered spend and are never added to it, to key or team spend, or to budgets."; +export const ATTRIBUTION_RULE: ParseKeys = "usage.subscriptionCost.attributionRule"; export const formatFee = (amount: number, currency: string): string => `${amount.toFixed(2)} ${currency}`; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/useSubscriptionFeeActions.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/useSubscriptionFeeActions.ts index dd2b2f01a30..e5ad5b88ae7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/useSubscriptionFeeActions.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/SubscriptionCost/useSubscriptionFeeActions.ts @@ -4,6 +4,7 @@ import { subscriptionAccountDeleteCall, subscriptionAccountUpdateCall, } from "@/components/networking"; +import i18n from "@/i18n/i18n"; import { toast } from "@/lib/toast"; import type { SubscriptionAccountUpdateRequest, @@ -62,7 +63,7 @@ export function useSubscriptionFeeActions({ accessToken, refetch }: Options): Re if (request === null) return; try { await request; - toast.success("Monthly fee saved"); + toast.success(i18n.t("usage.subscriptionCost.toasts.feeSaved")); setFeeModalAccount(null); await refetch(); } catch (error) { @@ -77,7 +78,7 @@ export function useSubscriptionFeeActions({ accessToken, refetch }: Options): Re if (!accessToken || account.fee === null) return; try { await subscriptionAccountDeleteCall(accessToken, account.fee.subscription_account_id); - toast.success("Monthly fee removed"); + toast.success(i18n.t("usage.subscriptionCost.toasts.feeRemoved")); await refetch(); } catch (error) { toast.fromError(error); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx index 78159778d87..cd3e5b2d0ef 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsagePageView.tsx @@ -10,6 +10,7 @@ import { Download, Link2, Search, X } from "lucide-react"; import { useQueryStates } from "nuqs"; import type { DateRangePickerValue } from "@/components/shared/date_picker_types"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { StackedUsageChart } from "@/components/shared/charts"; import { Alert, AlertAction, AlertDescription, AlertTitle } from "@/components/shared/Alert"; @@ -23,6 +24,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import useIsOrgAdmin from "@/app/(dashboard)/hooks/useIsOrgAdmin"; import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser"; import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; +import i18n from "@/i18n/i18n"; import { toast } from "@/lib/toast"; import { hasCapability } from "@/utils/capabilities"; import { copyToClipboard } from "@/utils/dataUtils"; @@ -113,6 +115,7 @@ interface UsagePageProps { } const UsagePage: React.FC = ({ teams, organizations, teamsLoading = false, defaultTimeRange }) => { + const { t } = useTranslation(); const { accessToken, userRole, userId: userID, premiumUser, isViewOnly, isLoading: authLoading } = useAuthorized(); const [gatewayActivityData, setGatewayActivityData] = useState(null); @@ -194,8 +197,7 @@ const UsagePage: React.FC = ({ teams, organizations, teamsLoadin if (!accessSettled) return; const cleanup = cleanUsageUrl(urlParams, { allowedViews, isAdmin, canViewTeamClients }); if (!cleanup) return; - if (cleanup.deniedAccess) - toast.warning("You don't have access to that usage view", { id: "usage-url-access-denied" }); + if (cleanup.deniedAccess) toast.warning(i18n.t("usage.pageView.accessDenied"), { id: "usage-url-access-denied" }); void setUrlParams(cleanup.patch); }, [accessSettled, urlParams, allowedViews, isAdmin, canViewTeamClients, setUrlParams]); @@ -482,15 +484,18 @@ const UsagePage: React.FC = ({ teams, organizations, teamsLoadin onValueChange={handleDateChange} presetShortLabel={presetShortLabel} /> -
{aggregatedFailed && ( - + )}
@@ -505,26 +510,26 @@ const UsagePage: React.FC = ({ teams, organizations, teamsLoadin
- Overview + {t("usage.pageView.tabs.overview")} - Model Activity + {t("usage.pageView.tabs.modelActivity")} - Key Activity + {t("usage.pageView.tabs.keyActivity")} - User Activity + {t("usage.pageView.tabs.userActivity")} - MCP Server Activity + {t("usage.pageView.tabs.mcpServerActivity")} - Endpoint Activity + {t("usage.pageView.tabs.endpointActivity")} {isAdmin && ( - Errors + {t("usage.pageView.tabs.errors")} )} @@ -532,7 +537,7 @@ const UsagePage: React.FC = ({ teams, organizations, teamsLoadin
@@ -566,17 +571,20 @@ const UsagePage: React.FC = ({ teams, organizations, teamsLoadin gatewayActivity && gatewayActivity.by_route.length > 0 ? ( ({ route: row.route, - Successful: row.successful_requests, - Failed: row.failed_requests, + [t("usage.pageView.gatewayByEndpoint.successful")]: row.successful_requests, + [t("usage.pageView.gatewayByEndpoint.failed")]: row.failed_requests, }))} - series={["Successful", "Failed"]} + series={[ + t("usage.pageView.gatewayByEndpoint.successful"), + t("usage.pageView.gatewayByEndpoint.failed"), + ]} colors={["#2b3fd6", "#ef4444"]} xKey="route" format={(value) => value.toLocaleString()} @@ -610,8 +618,8 @@ const UsagePage: React.FC = ({ teams, organizations, teamsLoadin setModelQuery(event.target.value)} /> @@ -619,7 +627,7 @@ const UsagePage: React.FC = ({ teams, organizations, teamsLoadin setModelQuery("")} > @@ -633,7 +641,7 @@ const UsagePage: React.FC = ({ teams, organizations, teamsLoadin Object.keys(modelMetrics).length > 0 && Object.keys(filteredModelMetrics).length === 0 ? (

- No models match "{trimmedModelQuery}" in this date range + {t("usage.pageView.modelSearch.noMatch", { query: trimmedModelQuery })}

) : ( = ({ teams, organizations, teamsLoadin <> {showCredentialBanner && ( - Reusable credentials are automatically tracked as tags + {t("usage.pageView.credentialBanner.title")} - When a reusable credential is used, it will appear as a tag prefixed with{" "} - Credential: - in this view. + Credential: , + }} + />
); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx index 05d9e9a69de..768799f6d52 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UsageViewSelect/UsageViewSelect.tsx @@ -1,8 +1,11 @@ import { useTerminology } from "@/contexts/TerminologyContext"; +import type { ParseKeys } from "i18next"; +import { useTranslation } from "react-i18next"; import { BarChart3, Bot, Building2, Globe, LineChart, ShoppingCart, Tags, User, Users } from "lucide-react"; import React from "react"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import i18n from "@/i18n/i18n"; import { hasCapability, type Capability } from "@/utils/capabilities"; import { all_admin_roles } from "@/utils/roles"; export type UsageOption = @@ -27,80 +30,80 @@ export interface UsageViewSelectProps { } interface OptionConfig { value: UsageOption; - label: string; - description: string; + label: ParseKeys; + description: ParseKeys; icon: React.ReactNode; capability?: Capability; adminOnly?: boolean; - showForAdmin?: string; - showForNonAdmin?: string; - descriptionForAdmin?: string; - descriptionForNonAdmin?: string; + showForAdmin?: ParseKeys; + showForNonAdmin?: ParseKeys; + descriptionForAdmin?: ParseKeys; + descriptionForNonAdmin?: ParseKeys; badgeText?: string; } const OPTIONS: OptionConfig[] = [ { value: "global", - label: "Global Usage", - showForAdmin: "Global Usage", - showForNonAdmin: "Your Usage", - description: "View usage across all resources", - descriptionForAdmin: "View usage across all resources", - descriptionForNonAdmin: "View your usage", + label: "usage.viewSelect.options.global.label", + showForAdmin: "usage.viewSelect.options.global.adminLabel", + showForNonAdmin: "usage.viewSelect.options.global.nonAdminLabel", + description: "usage.viewSelect.options.global.description", + descriptionForAdmin: "usage.viewSelect.options.global.adminDescription", + descriptionForNonAdmin: "usage.viewSelect.options.global.nonAdminDescription", icon: , }, { value: "my-usage", - label: "Your Usage", - description: "View your own usage", + label: "usage.viewSelect.options.myUsage.label", + description: "usage.viewSelect.options.myUsage.description", icon: , adminOnly: true, }, { value: "organization", - label: "Organization Usage", - description: "View usage across all organizations", + label: "usage.viewSelect.options.organization.label", + description: "usage.viewSelect.options.organization.description", icon: , capability: "viewOrganizationUsage", }, { value: "team", - label: "Team Usage", - description: "View usage by team", + label: "usage.viewSelect.options.team.label", + description: "usage.viewSelect.options.team.description", icon: , }, { value: "customer", - label: "Customer Usage", - description: "View usage by customer accounts", + label: "usage.viewSelect.options.customer.label", + description: "usage.viewSelect.options.customer.description", icon: , adminOnly: true, }, { value: "tag", - label: "Tag Usage", - description: "View usage grouped by tags", + label: "usage.viewSelect.options.tag.label", + description: "usage.viewSelect.options.tag.description", icon: , adminOnly: true, }, { value: "agent", - label: "Agent Usage (A2A)", - description: "View usage by AI agents", + label: "usage.viewSelect.options.agent.label", + description: "usage.viewSelect.options.agent.description", icon: , capability: "viewAgentUsage", }, { value: "user", - label: "User Usage", - description: "View usage by individual users", + label: "usage.viewSelect.options.user.label", + description: "usage.viewSelect.options.user.description", icon: , adminOnly: true, }, { value: "user-agent-activity", - label: "User Agent Activity", - description: "View detailed user agent activity logs", + label: "usage.viewSelect.options.userAgentActivity.label", + description: "usage.viewSelect.options.userAgentActivity.description", icon: , adminOnly: true, }, @@ -132,10 +135,11 @@ export const UsageViewSelect: React.FC = ({ userRole, canViewTagUsage = false, isOrgAdmin = false, - title = "Usage", - description = "Select the usage data you want to view", + title = i18n.t("usage.viewSelect.title"), + description = i18n.t("usage.viewSelect.description"), "data-id": dataId, }) => { + const { t } = useTranslation(); const term = useTerminology(); const isAdmin = all_admin_roles.includes(userRole ?? ""); const getFilteredOptions = () => { @@ -150,8 +154,8 @@ export const UsageViewSelect: React.FC = ({ } return { value: option.value, - label, - description: term(desc), + label: t(label), + description: term(t(desc)), icon: option.icon, badgeText: option.badgeText, }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UserRecordLink.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UserRecordLink.tsx index 492a0f75686..5a9157f1cc1 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UserRecordLink.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/UserRecordLink.tsx @@ -1,15 +1,17 @@ import { SquareArrowOutUpRight } from "lucide-react"; import Link from "next/link"; +import { useTranslation } from "react-i18next"; import { userRecordHref } from "../usageUrlState"; export default function UserRecordLink({ userId }: { userId: string }) { + const { t } = useTranslation(); return ( - View user record + {t("usage.userRecordLink.view")} ); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/ErrorsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/ErrorsTab.tsx index b587b3b8675..8f1cc80a81d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/ErrorsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/ErrorsTab.tsx @@ -1,6 +1,8 @@ "use client"; import React, { useMemo, useState } from "react"; +import type { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; import Link from "next/link"; import { AlertCircle, ArrowUpRight } from "lucide-react"; import { LineChart, STACKED_USAGE_PALETTE, StackedUsageChart } from "@/components/shared/charts"; @@ -76,13 +78,14 @@ function PanelBody({ return <>{children}; } -function rateChartEmpty(rateView: ErrorRateView, series: ErrorSeries, noTraffic: boolean): string | null { - if (noTraffic || series.rows.length === 0) return "No requests in this range"; - if (rateView === "status" && series.codes.length === 0) return "No failed requests in this range"; +function rateChartEmpty(rateView: ErrorRateView, series: ErrorSeries, noTraffic: boolean, t: TFunction): string | null { + if (noTraffic || series.rows.length === 0) return t("usage.errors.empty.noRequests"); + if (rateView === "status" && series.codes.length === 0) return t("usage.errors.empty.noFailedRequests"); return null; } export default function ErrorsTab({ activity, loading, failed, userScoped = false }: ErrorsTabProps) { + const { t } = useTranslation(); const [kind, setKind] = useState("key"); const [rateView, setRateView] = useState("total"); const summary = useMemo(() => (activity ? errorSummary(activity) : null), [activity]); @@ -99,14 +102,14 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals [series, rateView], ); const noTraffic = summary !== null && summary.requests === 0; - const rateEmpty = rateChartEmpty(rateView, series, noTraffic); + const rateEmpty = rateChartEmpty(rateView, series, noTraffic, t); if (userScoped) { return ( - Errors are deployment-wide - Clear the user filter in Global Usage to see failure analytics + {t("usage.errors.userScoped.title")} + {t("usage.errors.userScoped.description")} ); } @@ -114,10 +117,8 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals return ( - Error activity could not be loaded - - The proxy did not answer the failed-request query. Reload the page to try again. - + {t("usage.errors.failed.title")} + {t("usage.errors.failed.description")} ); } @@ -125,30 +126,53 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals return (
- + 0 ? "destructive" : undefined} - hint={summary ? `${count(summary.failed)} of ${count(summary.requests)} requests` : undefined} + hint={ + summary + ? t("usage.errors.stats.failedOfTotal", { + failed: count(summary.failed), + total: count(summary.requests), + }) + : undefined + } /> - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -157,9 +181,16 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals
} + title={t("usage.errors.rateChart.title")} + subtitle={t("usage.errors.rateChart.subtitle")} + action={ + + } > - + row.failed === 0) ? "No failed requests in this range" : null} + empty={series.rows.every((row) => row.failed === 0) ? t("usage.errors.empty.noFailedRequests") : null} > failedByDate.get(date)} - totalLabel="failed" + totalLabel={t("usage.errors.statusChart.totalLabel")} className="h-64" />
    {series.codes.map((code, index) => (
  • @@ -214,13 +245,13 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals
- + -
    +
      {statusRows.map((row) => (
    • } + title={t("usage.errors.entities.title")} + subtitle={t("usage.errors.entities.subtitle")} + action={ + + } > - +
      {ERROR_ENTITY_LABEL[kind].singular} - Requests - Failed - Error rate - Top status + {t("usage.errors.entities.columns.requests")} + {t("usage.errors.entities.columns.failed")} + {t("usage.errors.entities.columns.errorRate")} + {t("usage.errors.entities.columns.topStatus")} @@ -292,7 +330,7 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals
      diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.ts index 9afd0707d06..c45524b2add 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/errors/errorsData.ts @@ -1,3 +1,6 @@ +import type { ParseKeys } from "i18next"; +import i18n from "@/i18n/i18n"; + export interface RequestErrorDailyEntry { date: string; successful_requests: number; @@ -37,30 +40,30 @@ export const ERROR_ENTITY_LABEL: Record = { - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 408: "Request Timeout", - 413: "Payload Too Large", - 422: "Unprocessable Entity", - 429: "Rate Limited", - 500: "Internal Server Error", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 529: "Overloaded", +const STATUS_LABELS: Record = { + 400: "usage.errors.statusLabels.badRequest", + 401: "usage.errors.statusLabels.unauthorized", + 402: "usage.errors.statusLabels.paymentRequired", + 403: "usage.errors.statusLabels.forbidden", + 404: "usage.errors.statusLabels.notFound", + 408: "usage.errors.statusLabels.requestTimeout", + 413: "usage.errors.statusLabels.payloadTooLarge", + 422: "usage.errors.statusLabels.unprocessableEntity", + 429: "usage.errors.statusLabels.rateLimited", + 500: "usage.errors.statusLabels.internalServerError", + 502: "usage.errors.statusLabels.badGateway", + 503: "usage.errors.statusLabels.serviceUnavailable", + 504: "usage.errors.statusLabels.gatewayTimeout", + 529: "usage.errors.statusLabels.overloaded", }; export const statusCodeLabel = (code: number): string => { - if (code === 0) return "Not recorded"; + if (code === 0) return i18n.t("usage.errors.statusLabels.notRecorded"); const known = STATUS_LABELS[code]; - if (known) return known; - if (code >= 500) return "Server Error"; - if (code >= 400) return "Client Error"; - return "Error"; + if (known && Object.hasOwn(STATUS_LABELS, code)) return i18n.t(known); + if (code >= 500) return i18n.t("usage.errors.statusLabels.serverError"); + if (code >= 400) return i18n.t("usage.errors.statusLabels.clientError"); + return i18n.t("usage.errors.statusLabels.error"); }; export const errorRate = (failed: number, total: number): number | null => (total > 0 ? (failed / total) * 100 : null); @@ -111,9 +114,9 @@ export interface ErrorSeries { } export const ERROR_SERIES_MAX_CODES = 6; -export const ERROR_SERIES_OTHER = "Other"; +export const ERROR_SERIES_OTHER: ParseKeys = "usage.errors.series.other"; -const seriesKey = (code: number): string => (code === 0 ? "No status" : String(code)); +const seriesKey = (code: number): string => (code === 0 ? i18n.t("usage.errors.series.noStatus") : String(code)); export const errorSeries = (activity: RequestErrorActivity): ErrorSeries => { const ranked = [...activity.by_status_code] @@ -134,7 +137,7 @@ export const errorSeries = (activity: RequestErrorActivity): ErrorSeries => { ); const recorded = Object.values(counts).reduce((sum, count) => sum + count, 0); const other: Record = needsOther - ? { [ERROR_SERIES_OTHER]: Math.max(0, day.failed_requests - recorded) } + ? { [i18n.t(ERROR_SERIES_OTHER)]: Math.max(0, day.failed_requests - recorded) } : {}; return { date: day.date, @@ -145,7 +148,7 @@ export const errorSeries = (activity: RequestErrorActivity): ErrorSeries => { ...other, }; }); - return { rows, codes: needsOther ? [...codes, ERROR_SERIES_OTHER] : codes }; + return { rows, codes: needsOther ? [...codes, i18n.t(ERROR_SERIES_OTHER)] : codes }; }; export type ErrorRateView = "total" | "status"; @@ -214,6 +217,6 @@ export const entityRows = (activity: RequestErrorActivity, kind: ErrorEntityKind topStatus: entry.top_status_code === null ? null - : `${entry.top_status_code === 0 ? "No status" : entry.top_status_code} · ${entry.top_status_code_requests.toLocaleString()}`, + : `${entry.top_status_code === 0 ? i18n.t("usage.errors.series.noStatus") : entry.top_status_code} · ${entry.top_status_code_requests.toLocaleString()}`, })); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/BreakdownChart.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/BreakdownChart.tsx index df34876f351..f0a346dc8da 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/BreakdownChart.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/BreakdownChart.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { ArrowDownRight, ArrowUpRight } from "lucide-react"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import type { DailyData } from "@/components/UsagePage/types"; @@ -60,17 +61,18 @@ export function BreakdownControls({ onChange: (next: BreakdownState) => void; showDimension?: boolean; }) { + const { t } = useTranslation(); return (
      onChange({ ...state, metric })} /> {showDimension && ( onChange({ ...state, dimension })} @@ -134,6 +136,7 @@ export function Leaderboard({ dense?: boolean; colorByKey?: ReadonlyMap; }) { + const { t } = useTranslation(); const colorOf = (name: string) => { if (colorByKey) return colorForModel(name, colorByKey); const index = series.labels.indexOf(name); @@ -142,7 +145,7 @@ export function Leaderboard({ const value = (row: LeaderRow) => rankValue(row, metric); const rows = ranking.slice(0, limit); if (rows.length === 0) { - return

      No usage in this range

      ; + return

      {t("usage.breakdown.empty")}

      ; } const half = columns === 2 ? Math.ceil(rows.length / 2) : rows.length; const groups = columns === 2 ? [rows.slice(0, half), rows.slice(half)] : [rows]; @@ -168,7 +171,9 @@ export function Leaderboard({ {row.key} {providerForModel(row.key) && ( - by {providerForModel(row.key)} + + {t("usage.breakdown.byProvider", { provider: providerForModel(row.key) })} + )} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/TopAgents.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/TopAgents.tsx index 5724fc04eb9..68ed202a8dc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/TopAgents.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/TopAgents.tsx @@ -2,6 +2,7 @@ import { useTerminology } from "@/contexts/TerminologyContext"; import React, { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { ArrowRight, Bot, Monitor, Terminal } from "lucide-react"; import { StackedUsageChart } from "@/components/shared/charts"; import { Logo } from "@/components/molecules/logo/Logo"; @@ -85,6 +86,7 @@ function AgentItem({ color: string | undefined; onOpen?: (agent: AgentRow) => void; }) { + const { t } = useTranslation(); const term = useTerminology(); const openRow = onOpen && agent.tags.length > 0 ? () => onOpen(agent) : undefined; const content = ( @@ -104,7 +106,7 @@ function AgentItem({ {agent.label} {openRow ? ( - View activity + {t("usage.topAgents.viewActivity")} ) : ( @@ -112,7 +114,9 @@ function AgentItem({ )} - {formatCompact(agent.tokens)} tokens + + {t("usage.topAgents.tokens", { tokens: formatCompact(agent.tokens) })} + {formatCompactUsd(agent.spend)} · {share.toFixed(1)}% @@ -126,7 +130,7 @@ function AgentItem({ {openRow ? (
      -

      Top models

      +

      {t("usage.breakdownChart.topModels")}

      {loading ? ( -
      +
      ) : ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageOverview.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageOverview.tsx index d21a89bbaaa..296b9221d6c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageOverview.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageOverview.tsx @@ -7,6 +7,7 @@ */ import { type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; import type { DailyData } from "@/components/UsagePage/types"; import { KeyRound, Users } from "lucide-react"; import type { OverviewTotals } from "./overviewData"; @@ -41,6 +42,7 @@ export default function UsageOverview({ providerBreakdown, budget, }: UsageOverviewProps) { + const { t } = useTranslation(); return (
      + {topKeys} - + {topUsers} {gatewayByEndpoint} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageStatStrip.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageStatStrip.tsx index 5d9f8d6e0e3..15c33d8653c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageStatStrip.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/UsageStatStrip.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useMemo } from "react"; +import { Trans, useTranslation } from "react-i18next"; import type { DailyData } from "@/components/UsagePage/types"; import { cn } from "@/lib/cva.config"; import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from "@/components/ui/popover"; @@ -25,90 +26,110 @@ export default function UsageStatStrip({ requestCountsPending, budget, }: UsageStatStripProps) { + const { t } = useTranslation(); const daily = useMemo(() => dailyTotals(results), [results]); return (
      }> }> - + {!requestCountsPending && (
      - {totals.successful.toLocaleString()} ok + {totals.successful.toLocaleString()}, + }} + /> - 0 ? "text-destructive tabular-nums" : "tabular-nums"}> - {totals.failed.toLocaleString()} - {" "} - failed + 0 ? "text-destructive tabular-nums" : "tabular-nums"}> + {totals.failed.toLocaleString()} + + ), + }} + />
      )}
      }> {!loading && ( - View token breakdown + {t("usage.statStrip.viewTokenBreakdown")} - What counts toward total tokens? + {t("usage.statStrip.tokenPopover.title")}
      -
      Input tokens
      +
      {t("usage.statStrip.tokenPopover.inputTokens")}
      {totals.inputTokens.toLocaleString()}
      -
      Cache reads
      +
      {t("usage.statStrip.tokenPopover.cacheReads")}
      {totals.cacheReadTokens.toLocaleString()}
      -
      Cache writes
      +
      {t("usage.statStrip.tokenPopover.cacheWrites")}
      {totals.cacheWriteTokens.toLocaleString()}
      -
      Output tokens
      +
      {t("usage.statStrip.tokenPopover.outputTokens")}
      {totals.outputTokens.toLocaleString()}
      -
      Total tokens
      +
      {t("usage.statStrip.tokenPopover.totalTokens")}
      {totals.tokens.toLocaleString()}
      -

      - Input includes cache reads and writes. Cached input has separate pricing, so total tokens alone cannot - explain the cost. -

      -

      - A client may show a different subset of tokens. Compare the same person, client and time range before - comparing counts. -

      +

      {t("usage.statStrip.tokenPopover.pricingNote")}

      +

      {t("usage.statStrip.tokenPopover.clientNote")}

      )}
      - +
      diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/agentCatalog.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/agentCatalog.ts index e4fe94791fb..3fa4cb5d1db 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/agentCatalog.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/agentCatalog.ts @@ -11,7 +11,9 @@ import openaiAgentsLogo from "../../../../../../../public/assets/logos/openai-ag import opencodeLogo from "../../../../../../../public/assets/moyai/logos/opencode.svg"; import pydanticAiLogo from "../../../../../../../public/assets/logos/pydantic-ai-color.svg"; import xaiLogo from "../../../../../../../public/assets/logos/xai.svg"; +import type { ParseKeys } from "i18next"; import type { components } from "@/lib/http/schema"; +import i18n from "@/i18n/i18n"; import type { DailyData, SpendMetrics } from "@/components/UsagePage/types"; /** One row of `GET /tag/summary`. */ @@ -47,7 +49,7 @@ export interface AgentRow { interface AgentFamily { id: string; label: string; - description: string; + description: ParseKeys; kind: AgentKind; logo: string | null; /** Matched against the user-agent with its `User-Agent: ` prefix removed, case-insensitively. */ @@ -63,7 +65,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: UsageClient.ClaudeDesktop, label: "Claude Desktop", - description: "Anthropic's desktop assistant", + description: "usage.agentCatalog.descriptions.claudeDesktop", kind: "agent", logo: claudeLogo.src, match: /^claude[-_ ]desktop\b|^claude-cli\/[^\s]+\s+\((?:[^)]*,\s*)?claude-desktop-3p\s*[,)]/i, @@ -71,7 +73,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: UsageClient.ClaudeCode, label: "Claude Code", - description: "Anthropic's coding agent", + description: "usage.agentCatalog.descriptions.claudeCode", kind: "agent", logo: claudeLogo.src, match: /^(claude-code|claude_code|claude-user)\b|^claude-cli\/[^\s]+\s+\((?:[^)]*,\s*)?cli\s*[,)]/i, @@ -79,7 +81,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "claude-unspecified", label: "Claude (unspecified)", - description: "Client not identified", + description: "usage.agentCatalog.descriptions.claudeUnspecified", kind: "agent", logo: claudeLogo.src, match: /^claude-cli\b/i, @@ -87,7 +89,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "codex", label: "Codex", - description: "OpenAI's coding agent", + description: "usage.agentCatalog.descriptions.codex", kind: "agent", logo: openaiLogo.src, match: /^(codex[ _-]|codex$)/i, @@ -95,7 +97,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "opencode", label: "OpenCode", - description: "Open-source terminal coding agent", + description: "usage.agentCatalog.descriptions.opencode", kind: "agent", logo: opencodeLogo.src, match: /^opencode\b/i, @@ -103,7 +105,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "cursor", label: "Cursor", - description: "AI code editor", + description: "usage.agentCatalog.descriptions.cursor", kind: "agent", logo: cursorLogo.src, match: /^cursor\b/i, @@ -111,7 +113,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "copilot", label: "GitHub Copilot", - description: "GitHub's coding assistant", + description: "usage.agentCatalog.descriptions.copilot", kind: "agent", logo: copilotLogo.src, match: /^(github-?copilot|copilot)\b/i, @@ -119,16 +121,23 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "hermes", label: "Hermes Agent", - description: "Open-source agent", + description: "usage.agentCatalog.descriptions.hermes", kind: "agent", logo: hermesLogo.src, match: /^hermes\b/i, }, - { id: "grok", label: "Grok", description: "xAI's assistant", kind: "agent", logo: xaiLogo.src, match: /^grok\b/i }, + { + id: "grok", + label: "Grok", + description: "usage.agentCatalog.descriptions.grok", + kind: "agent", + logo: xaiLogo.src, + match: /^grok\b/i, + }, { id: "cline", label: "Cline", - description: "Open-source coding agent", + description: "usage.agentCatalog.descriptions.cline", kind: "agent", logo: null, match: /^cline\b/i, @@ -136,7 +145,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "kilo-code", label: "Kilo Code", - description: "Open-source coding agent", + description: "usage.agentCatalog.descriptions.kiloCode", kind: "agent", logo: null, match: /^kilo[- ]?code\b/i, @@ -144,7 +153,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "aider", label: "Aider", - description: "AI pair programming in the terminal", + description: "usage.agentCatalog.descriptions.aider", kind: "agent", logo: null, match: /^aider\b/i, @@ -152,7 +161,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "openai-agents", label: "OpenAI Agents", - description: "OpenAI Agents SDK", + description: "usage.agentCatalog.descriptions.openaiAgents", kind: "sdk", logo: openaiAgentsLogo.src, match: /^agents$/i, @@ -160,7 +169,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "langchain", label: "LangChain", - description: "Agent framework", + description: "usage.agentCatalog.descriptions.langchain", kind: "sdk", logo: langchainLogo.src, match: /^(langchain|langgraph|deepagents)/i, @@ -168,7 +177,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "pydantic-ai", label: "Pydantic AI", - description: "Agent framework", + description: "usage.agentCatalog.descriptions.pydanticAi", kind: "sdk", logo: pydanticAiLogo.src, match: /^pydantic-ai\b/i, @@ -176,7 +185,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "github-actions", label: "GitHub Actions", - description: "CI workflows", + description: "usage.agentCatalog.descriptions.githubActions", kind: "sdk", logo: githubLogo.src, match: /^github-actions\b/i, @@ -184,7 +193,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "litellm", label: "LiteLLM SDK", - description: "litellm Python SDK", + description: "usage.agentCatalog.descriptions.litellmSdk", kind: "sdk", logo: litellmLogo.src, match: /^litellm\b/i, @@ -192,7 +201,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "openai-sdk", label: "OpenAI SDK", - description: "OpenAI client library", + description: "usage.agentCatalog.descriptions.openaiSdk", kind: "sdk", logo: openaiLogo.src, match: /^(openai|asyncopenai)\b/i, @@ -200,7 +209,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "anthropic-sdk", label: "Anthropic SDK", - description: "Anthropic client library", + description: "usage.agentCatalog.descriptions.anthropicSdk", kind: "sdk", logo: anthropicLogo.src, match: /^anthropic\b/i, @@ -208,7 +217,7 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "python", label: "Python", - description: "Scripts and HTTP clients", + description: "usage.agentCatalog.descriptions.python", kind: "sdk", logo: null, match: /^(python|httpx|aiohttp|urllib|requests)/i, @@ -216,12 +225,19 @@ const FAMILIES: readonly AgentFamily[] = [ { id: "node", label: "Node.js", - description: "Scripts and HTTP clients", + description: "usage.agentCatalog.descriptions.node", kind: "sdk", logo: null, match: /^(node|bun|deno|axios|undici)\b/i, }, - { id: "curl", label: "curl", description: "Command line", kind: "sdk", logo: null, match: /^curl\b/i }, + { + id: "curl", + label: "curl", + description: "usage.agentCatalog.descriptions.curl", + kind: "sdk", + logo: null, + match: /^curl\b/i, + }, ]; const USER_AGENT_PREFIX = /^user[- ]agent:\s*/i; @@ -258,7 +274,7 @@ const agentFor = (product: string, rows: readonly TagSummaryRow[]): AgentRow => return { id: family?.id ?? `ua:${product.toLowerCase()}`, label: family?.label ?? product, - description: family?.description ?? "Custom client", + description: i18n.t(family?.description ?? "usage.agentCatalog.descriptions.customClient"), kind: family?.kind ?? "sdk", logo: family?.logo ?? null, spend: rows.reduce((sum, row) => sum + (row.total_spend || 0), 0), diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/overviewData.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/overviewData.ts index 9d9e8f9fa32..54f4e5dd1de 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/overviewData.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/overview/overviewData.ts @@ -1,4 +1,6 @@ +import type { ParseKeys } from "i18next"; import type { DailyData, SpendMetrics } from "@/components/UsagePage/types"; +import i18n from "@/i18n/i18n"; import { stackedUsageColor } from "@/components/shared/charts"; import { EMPTY_DAILY_ACTIVITY_METADATA, type DailyActivityMetadata } from "@/components/UsagePage/dailyActivityApi"; import type { GatewayActivity } from "../gatewayActivity"; @@ -65,7 +67,7 @@ export const colorForModel = (model: string, colorByModel: ReadonlyMap { /** ISO date, the chart's category key: unique across years, unlike its display label. */ @@ -136,7 +138,7 @@ export const seriesBy = ( return { day, values, other: remainder > 1e-6 ? remainder : 0 }; }); const hasOther = days.some(({ other }) => other > 0) || leaders.length === 0; - const labels = hasOther ? [...leaders, OTHER_SERIES] : leaders; + const labels = hasOther ? [...leaders, i18n.t(OTHER_SERIES)] : leaders; const keys = labels.map((_, index) => seriesKey(index)); const data = days.map(({ day, values, other }) => ({ date: day.date, @@ -167,7 +169,7 @@ export const bucketSeries = (series: Series, granularity: Granularity): Series = const members = series.data.filter((day) => bucketStart(day.date) === start); return { date: start, - label: `Wk of ${formatDayLabel(start)}`, + label: i18n.t("usage.overview.weekOf", { date: formatDayLabel(start) }), ...Object.fromEntries( series.keys.map((key) => [key, members.reduce((sum, day) => sum + Number(day[key] ?? 0), 0)]), ), diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientAgentModelsData.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientAgentModelsData.ts index 61640a09de2..78386891544 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientAgentModelsData.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientAgentModelsData.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import i18n from "@/i18n/i18n"; import type { DailyData, MetricWithMetadata, SpendMetrics } from "@/components/UsagePage/types"; import type { AgentRow } from "../components/overview/agentCatalog"; import { clientAgentDetail } from "../components/overview/clientAgentDetail"; @@ -61,18 +62,18 @@ const breakdownFor = ( export const clientAgentModelsFromExport = (payload: unknown, agent: AgentRow): ClientAgentModelsResult => { const parsed = z.array(exportRow).safeParse(payload); - if (!parsed.success) return { results: [], error: "The proxy returned incomplete model usage data" }; + if (!parsed.success) return { results: [], error: i18n.t("usage.clientModels.errors.incompleteData") }; const tags = new Set(agent.tags); const rows = parsed.data.filter((row) => tags.has(row.entity_id)); const identities = rows.map((row) => JSON.stringify([row.date, row.entity_id, row.model])); if (new Set(identities).size !== identities.length) { - return { results: [], error: "The proxy returned duplicate model usage data. Try again." }; + return { results: [], error: i18n.t("usage.clientModels.errors.duplicateData") }; } const results = grouped(rows, (row) => row.date).map(([date, members]) => ({ date, metrics: metricsFor(members), breakdown: { - models: breakdownFor(members, (row) => row.model || "Unknown model"), + models: breakdownFor(members, (row) => row.model || i18n.t("usage.clientUsage.unknownModel")), model_groups: {}, entities: breakdownFor(members, (row) => row.entity_id), providers: {}, @@ -83,7 +84,7 @@ export const clientAgentModelsFromExport = (payload: unknown, agent: AgentRow): if (!clientAgentDetail(agent, results).dailyMatchesSummary) { return { results: [], - error: "Model usage does not match these client totals yet. Refresh or choose a shorter range.", + error: i18n.t("usage.clientModels.errors.totalsMismatch"), }; } return { results, error: null }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientUsageData.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientUsageData.ts index 41e655d3e6e..d75fe6f4f64 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientUsageData.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientUsageData.ts @@ -4,6 +4,7 @@ import { EMPTY_DAILY_ACTIVITY_RESPONSE, type DailyActivityAggregatedResponse, } from "@/components/UsagePage/dailyActivityApi"; +import i18n from "@/i18n/i18n"; import type { components } from "@/lib/http/schema"; import { extractPromptCacheTokens } from "@/utils/promptCacheUsage"; import { isUserAgentTag, usageClientFromUserAgent, UsageClient } from "../components/overview/agentCatalog"; @@ -165,7 +166,7 @@ const breakdownFor = ( const localDay = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; -const modelName = (row: ClientLog) => row.model || "Unknown model"; +const modelName = (row: ClientLog) => row.model || i18n.t("usage.clientUsage.unknownModel"); const dailyData = (rows: readonly ClientLog[]): DailyData[] => [...new Set(rows.map((row) => localDay(row.startTime)))].sort().map((date) => { const day = rows.filter((row) => localDay(row.startTime) === date); @@ -175,7 +176,7 @@ const dailyData = (rows: readonly ClientLog[]): DailyData[] => breakdown: { models: breakdownFor(day, modelName), model_groups: breakdownFor(day, (row) => row.model_group || modelName(row)), - providers: breakdownFor(day, (row) => row.custom_llm_provider || "Unknown provider"), + providers: breakdownFor(day, (row) => row.custom_llm_provider || i18n.t("usage.clientUsage.unknownProvider")), api_keys: {}, mcp_servers: {}, entities: {}, @@ -185,19 +186,19 @@ const dailyData = (rows: readonly ClientLog[]): DailyData[] => }); export function clientUsageFromLogs(response: unknown, scope: ClientUsageScope): ClientUsageResult { - if (!scope.userId && !scope.teamId) return emptyClientUsage("Select a user or team to view client usage"); + if (!scope.userId && !scope.teamId) return emptyClientUsage(i18n.t("usage.clientUsage.errors.selectUserOrTeam")); const parsed = pageSchema.safeParse(response); - if (!parsed.success) return emptyClientUsage("The proxy returned incomplete client usage data"); + if (!parsed.success) return emptyClientUsage(i18n.t("usage.clientUsage.errors.incompleteData")); const page = parsed.data; if (page.total_is_capped || page.total_pages > 1 || page.total > CLIENT_REQUEST_LIMIT) { return emptyClientUsage(INCOMPLETE_CLIENT_USAGE); } - if (page.data.length !== page.total) return emptyClientUsage("The proxy returned incomplete client usage data"); + if (page.data.length !== page.total) return emptyClientUsage(i18n.t("usage.clientUsage.errors.incompleteData")); if (scope.userId && page.data.some((row) => row.user !== scope.userId)) { - return emptyClientUsage("The proxy returned usage outside the selected user"); + return emptyClientUsage(i18n.t("usage.clientUsage.errors.outsideUser")); } if (scope.teamId && page.data.some((row) => row.team_id !== scope.teamId)) { - return emptyClientUsage("The proxy returned usage outside the selected team"); + return emptyClientUsage(i18n.t("usage.clientUsage.errors.outsideTeam")); } const uniqueRows = [...new Map(page.data.map((row) => [row.request_id, row])).values()]; const rows = uniqueRows.filter( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientUsageScope.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientUsageScope.ts index 702365fc22b..ef7f96ad2d8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientUsageScope.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/clientUsageScope.ts @@ -1,4 +1,5 @@ import type { Member } from "@/components/networking"; +import i18n from "@/i18n/i18n"; import { hasProxyWideSpendView, isUserTeamAdminForSingleTeam } from "@/utils/roles"; export interface ClientUsageTeam { @@ -76,23 +77,23 @@ export const resolveClientUsageScope = ({ canChooseGlobalUser: proxyAdmin && !ownUsage && !requestedTeamId, error: null, }; - if (!userId) return { ...base, error: "Sign in to view client usage" }; + if (!userId) return { ...base, error: i18n.t("usage.clientUsage.errors.signIn") }; if (ownUsage) return { ...base, scope: { userId, teamId: null } }; if (requestedTeamId) { const selectedTeam = availableTeams.find((team) => team.team_id === requestedTeamId); - if (!selectedTeam) return { ...base, error: "Choose a team you administer" }; + if (!selectedTeam) return { ...base, error: i18n.t("usage.clientUsage.errors.chooseAdministeredTeam") }; const members = memberOptions(selectedTeam); const memberId = requestedUserId || null; if (memberId && !members.some((member) => member.value === memberId)) { - return { ...base, members, error: "Choose a member of the selected team" }; + return { ...base, members, error: i18n.t("usage.clientUsage.errors.chooseTeamMember") }; } return { ...base, members, scope: { teamId: selectedTeam.team_id, userId: memberId } }; } if (proxyAdmin) return { ...base, scope: { teamId: null, userId: requestedUserId || null } }; if (requestedUserId && requestedUserId !== userId) { - return { ...base, error: "Choose an administered team before viewing another member's usage" }; + return { ...base, error: i18n.t("usage.clientUsage.errors.chooseTeamBeforeMember") }; } return { ...base, scope: { teamId: null, userId } }; }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/useClientAgentModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/useClientAgentModels.ts index 9c97072c5ef..b1c95bf04fc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/useClientAgentModels.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/useClientAgentModels.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { dailyActivityExportCall } from "@/components/networking"; +import i18n from "@/i18n/i18n"; import type { DailyActivityRequest } from "@/components/UsagePage/dailyActivityApi"; import type { AgentRow } from "../components/overview/agentCatalog"; import { clientAgentModelsFromExport, type ClientAgentModelsResult } from "./clientAgentModelsData"; @@ -18,15 +19,18 @@ const loadModels = async (request: DailyActivityRequest, agent: AgentRow): Promi try { const blob = await dailyActivityExportCall("tag", request, "daily_with_models", "json"); if (blob.size > MAX_CLIENT_MODEL_EXPORT_BYTES) { - return { results: [], error: "Choose a shorter date range to load the model breakdown" }; + return { results: [], error: i18n.t("usage.clientModels.errors.tooLarge") }; } const payload: unknown = JSON.parse(await blob.text()); return clientAgentModelsFromExport(payload, agent); } catch (error: unknown) { if (error instanceof SyntaxError) { - return { results: [], error: "Could not load complete model usage. Try again." }; + return { results: [], error: i18n.t("usage.clientModels.errors.invalidJson") }; } - return { results: [], error: error instanceof Error ? error.message : "Could not load the model breakdown" }; + return { + results: [], + error: error instanceof Error ? error.message : i18n.t("usage.clientModels.errors.loadFailed"), + }; } }; @@ -61,7 +65,7 @@ export function useClientAgentModels({ accessToken, startTime, endTime, agent, e return { results: current?.results ?? [], loading: active && validRange && current === null, - error: active && !validRange ? "Select a valid date range" : current?.error ?? null, + error: active && !validRange ? i18n.t("usage.clientUsage.errors.invalidRange") : current?.error ?? null, reload, }; } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/useClientUsage.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/useClientUsage.ts index 42d30d46018..f1f8b7e76d4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/useClientUsage.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/hooks/useClientUsage.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useState } from "react"; import { uiSpendLogsCall } from "@/components/networking"; +import i18n from "@/i18n/i18n"; import { CLIENT_REQUEST_LIMIT, clientUsageFromLogs, @@ -24,9 +25,10 @@ export function useClientUsage({ accessToken, startTime, endTime, userId, teamId const end = endTime?.getTime() ?? Number.NaN; const scopeError = (() => { if (client === null) return null; - if (!userId && !teamId) return "Select a user or team to view client usage"; - if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) return "Select a valid date range"; - if (!accessToken) return "Sign in to view client usage"; + if (!userId && !teamId) return i18n.t("usage.clientUsage.errors.selectUserOrTeam"); + if (!Number.isFinite(start) || !Number.isFinite(end) || start > end) + return i18n.t("usage.clientUsage.errors.invalidRange"); + if (!accessToken) return i18n.t("usage.clientUsage.errors.signIn"); return null; })(); const enabled = client !== null && scopeError === null; @@ -63,7 +65,9 @@ export function useClientUsage({ accessToken, startTime, endTime, userId, teamId if (!cancelled) { setSettled({ key, - result: emptyClientUsage(error instanceof Error ? error.message : "Could not load client usage"), + result: emptyClientUsage( + error instanceof Error ? error.message : i18n.t("usage.clientUsage.errors.loadFailed"), + ), }); } }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx index 39fe65f1cb5..e0b65615737 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx @@ -6,11 +6,13 @@ import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useQueryStates } from "nuqs"; +import { useTranslation } from "react-i18next"; import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; import { Skeleton } from "@/components/ui/skeleton"; import { dateRangeFromParams, usageUrlParsers } from "./_components/usageUrlState"; export default function UsagePage() { + const { t } = useTranslation(); useAuthorized(); const { data: teams, isLoading: teamsLoading } = useTeams(); const { data: organizations } = useOrganizations(); @@ -18,7 +20,7 @@ export default function UsagePage() { const [params] = useQueryStates(usageUrlParsers); if (settingsLoading && !dateRangeFromParams(params)) { return ( -
      +
      ); diff --git a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx index 2686806c95b..82ae88f2a2a 100644 --- a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx +++ b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysPage.tsx @@ -1,6 +1,7 @@ "use client"; import { PageContent } from "@/components/shared/Page"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { PaginationState } from "@tanstack/react-table"; import { Info } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; @@ -9,6 +10,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { DeletedKeysTable } from "./DeletedKeysTable/DeletedKeysTable"; export default function DeletedKeysPage() { + const { t } = useTranslation(); const { premiumUser } = useAuthorized(); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 50 }); @@ -19,10 +21,8 @@ export default function DeletedKeysPage() { {!premiumUser && ( - Coming soon to Enterprise - - Deleted key auditing is graduating from beta into our Enterprise audit & compliance suite. - + {t("logs.deletedKeys.page.comingSoonTitle")} + {t("logs.deletedKeys.page.comingSoonDescription")} )}
      -
      No deleted keys found
      -
      Keys deleted from this proxy will show up here.
      +
      {t("logs.deletedKeys.table.emptyTitle")}
      +
      {t("logs.deletedKeys.table.emptyDescription")}
      ); } @@ -38,9 +40,10 @@ export function DeletedKeysTable({ pagination, onPaginationChange, }: DeletedKeysTableProps) { + const { t } = useTranslation(); const [sorting, setSorting] = useState(DEFAULT_SORTING); - const columns = useMemo(() => getDeletedKeysTableColumns(), []); + const columns = useMemo(() => getDeletedKeysTableColumns(t), [t]); return ( } size="compact" /> diff --git a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTableColumns.tsx b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTableColumns.tsx index 32185f867b4..1eccd99ba2a 100644 --- a/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/DeletedKeysPage/DeletedKeysTable/DeletedKeysTableColumns.tsx @@ -1,6 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; +import type { TFunction } from "i18next"; import { DataTableSortHeader } from "@/components/shared/DataTable"; import { DateCell, IdCell, IdentityCell, MoneyCell } from "@/components/shared/table_cells"; @@ -29,12 +30,12 @@ function UserLinkCell({ userId }: { userId: string | null | undefined }) { ); } -export const getDeletedKeysTableColumns = (): ColumnDef[] => [ +export const getDeletedKeysTableColumns = (t: TFunction): ColumnDef[] => [ { id: "token", accessorKey: "token", - meta: { title: "Key ID" }, - header: "Key ID", + meta: { title: t("logs.deletedKeys.columns.keyId") }, + header: t("logs.deletedKeys.columns.keyId"), size: 150, enableSorting: false, cell: ({ row }) => , @@ -42,8 +43,8 @@ export const getDeletedKeysTableColumns = (): ColumnDef[] => { id: "key_alias", accessorKey: "key_alias", - meta: { title: "Key Alias" }, - header: "Key Alias", + meta: { title: t("logs.deletedKeys.columns.keyAlias") }, + header: t("logs.deletedKeys.columns.keyAlias"), size: 150, enableSorting: false, cell: ({ row }) => { @@ -61,8 +62,8 @@ export const getDeletedKeysTableColumns = (): ColumnDef[] => { id: "team_alias", accessorKey: "team_alias", - meta: { title: "Team Alias" }, - header: "Team Alias", + meta: { title: t("logs.deletedKeys.columns.teamAlias") }, + header: t("logs.deletedKeys.columns.teamAlias"), size: 120, enableSorting: false, cell: ({ row }) => , @@ -70,8 +71,8 @@ export const getDeletedKeysTableColumns = (): ColumnDef[] => { id: "spend", accessorKey: "spend", - meta: { title: "Spend (USD)", numeric: true }, - header: ({ column }) => , + meta: { title: t("logs.deletedKeys.columns.spendUsd"), numeric: true }, + header: ({ column }) => , size: 100, enableSorting: true, cell: ({ row }) => , @@ -79,17 +80,24 @@ export const getDeletedKeysTableColumns = (): ColumnDef[] => { id: "max_budget", accessorKey: "max_budget", - meta: { title: "Budget (USD)", numeric: true }, - header: "Budget (USD)", + meta: { title: t("logs.deletedKeys.columns.budgetUsd"), numeric: true }, + header: t("logs.deletedKeys.columns.budgetUsd"), size: 110, enableSorting: false, - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, { id: "user_email", accessorKey: "user_email", - meta: { title: "User Email" }, - header: "User Email", + meta: { title: t("logs.deletedKeys.columns.userEmail") }, + header: t("logs.deletedKeys.columns.userEmail"), size: 160, enableSorting: false, cell: ({ row }) => , @@ -97,8 +105,8 @@ export const getDeletedKeysTableColumns = (): ColumnDef[] => { id: "user_id", accessorKey: "user_id", - meta: { title: "User ID" }, - header: "User ID", + meta: { title: t("logs.deletedKeys.columns.userId") }, + header: t("logs.deletedKeys.columns.userId"), size: 120, enableSorting: false, cell: ({ row }) => , @@ -106,8 +114,8 @@ export const getDeletedKeysTableColumns = (): ColumnDef[] => { id: "created_at", accessorKey: "created_at", - meta: { title: "Created At" }, - header: ({ column }) => , + meta: { title: t("logs.deletedKeys.columns.createdAt") }, + header: ({ column }) => , size: 120, enableSorting: true, cell: ({ row }) => , @@ -115,8 +123,8 @@ export const getDeletedKeysTableColumns = (): ColumnDef[] => { id: "created_by", accessorKey: "created_by", - meta: { title: "Created By" }, - header: "Created By", + meta: { title: t("logs.deletedKeys.columns.createdBy") }, + header: t("logs.deletedKeys.columns.createdBy"), size: 120, enableSorting: false, cell: ({ row }) => , @@ -124,8 +132,8 @@ export const getDeletedKeysTableColumns = (): ColumnDef[] => { id: "deleted_at", accessorKey: "deleted_at", - meta: { title: "Deleted At" }, - header: ({ column }) => , + meta: { title: t("logs.deletedKeys.columns.deletedAt") }, + header: ({ column }) => , size: 120, enableSorting: true, cell: ({ row }) => , @@ -133,8 +141,8 @@ export const getDeletedKeysTableColumns = (): ColumnDef[] => { id: "deleted_by", accessorKey: "deleted_by", - meta: { title: "Deleted By" }, - header: "Deleted By", + meta: { title: t("logs.deletedKeys.columns.deletedBy") }, + header: t("logs.deletedKeys.columns.deletedBy"), size: 120, enableSorting: false, cell: ({ row }) => , diff --git a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsPage.tsx b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsPage.tsx index a491fe138f6..5241d012762 100644 --- a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsPage.tsx +++ b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsPage.tsx @@ -3,6 +3,7 @@ import { PaginationState } from "@tanstack/react-table"; import { Info } from "lucide-react"; import { PageContent } from "@/components/shared/Page"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; import { DEFAULT_PAGE_SIZE_OPTIONS } from "@/components/shared/DataTable"; import { useDeletedTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; @@ -10,6 +11,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { DeletedTeamsTable } from "./DeletedTeamsTable/DeletedTeamsTable"; export default function DeletedTeamsPage() { + const { t } = useTranslation(); const { premiumUser } = useAuthorized(); const [pagination, setPagination] = useState({ pageIndex: 0, @@ -22,10 +24,8 @@ export default function DeletedTeamsPage() { {!premiumUser && ( - Coming soon to Enterprise - - Deleted team auditing is graduating from beta into our Enterprise audit & compliance suite. - + {t("logs.deletedTeams.page.comingSoonTitle")} + {t("logs.deletedTeams.page.comingSoonDescription")} )}
      -
      No deleted teams found
      -
      Teams deleted from this proxy will show up here.
      +
      {t("logs.deletedTeams.table.emptyTitle")}
      +
      {t("logs.deletedTeams.table.emptyDescription")}
      ); } @@ -38,9 +40,10 @@ export function DeletedTeamsTable({ onPaginationChange, rowCount, }: DeletedTeamsTableProps) { + const { t } = useTranslation(); const [sorting, setSorting] = useState(DEFAULT_SORTING); - const columns = useMemo(() => getDeletedTeamsTableColumns(), []); + const columns = useMemo(() => getDeletedTeamsTableColumns(t), [t]); return ( } size="compact" /> diff --git a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTableColumns.tsx b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTableColumns.tsx index 172f0417027..23ad9c86a1d 100644 --- a/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/DeletedTeamsPage/DeletedTeamsTable/DeletedTeamsTableColumns.tsx @@ -1,6 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; +import type { TFunction } from "i18next"; import { DataTableSortHeader } from "@/components/shared/DataTable"; import { DateCell, IdCell, IdentityCell, ModelsCell, MoneyCell } from "@/components/shared/table_cells"; @@ -18,12 +19,12 @@ function EntityCell({ value, href }: { value: string | null | undefined; href: s ); } -export const getDeletedTeamsTableColumns = (): ColumnDef[] => [ +export const getDeletedTeamsTableColumns = (t: TFunction): ColumnDef[] => [ { id: "team_alias", accessorKey: "team_alias", - meta: { title: "Team Name" }, - header: "Team Name", + meta: { title: t("logs.deletedTeams.columns.teamName") }, + header: t("logs.deletedTeams.columns.teamName"), size: 150, enableSorting: false, cell: ({ row }) => { @@ -41,8 +42,8 @@ export const getDeletedTeamsTableColumns = (): ColumnDef[] => [ { id: "team_id", accessorKey: "team_id", - meta: { title: "Team ID" }, - header: "Team ID", + meta: { title: t("logs.deletedTeams.columns.teamId") }, + header: t("logs.deletedTeams.columns.teamId"), size: 150, enableSorting: false, cell: ({ row }) => , @@ -50,8 +51,8 @@ export const getDeletedTeamsTableColumns = (): ColumnDef[] => [ { id: "created_at", accessorKey: "created_at", - meta: { title: "Created" }, - header: ({ column }) => , + meta: { title: t("logs.deletedTeams.columns.created") }, + header: ({ column }) => , size: 120, enableSorting: true, cell: ({ row }) => , @@ -59,8 +60,8 @@ export const getDeletedTeamsTableColumns = (): ColumnDef[] => [ { id: "spend", accessorKey: "spend", - meta: { title: "Spend (USD)", numeric: true }, - header: ({ column }) => , + meta: { title: t("logs.deletedTeams.columns.spendUsd"), numeric: true }, + header: ({ column }) => , size: 100, enableSorting: true, cell: ({ row }) => , @@ -68,17 +69,24 @@ export const getDeletedTeamsTableColumns = (): ColumnDef[] => [ { id: "max_budget", accessorKey: "max_budget", - meta: { title: "Budget (USD)", numeric: true }, - header: "Budget (USD)", + meta: { title: t("logs.deletedTeams.columns.budgetUsd"), numeric: true }, + header: t("logs.deletedTeams.columns.budgetUsd"), size: 110, enableSorting: false, - cell: ({ row }) => , + cell: ({ row }) => ( + + ), }, { id: "models", accessorKey: "models", - meta: { title: "Models", skeleton: "chips" }, - header: "Models", + meta: { title: t("logs.deletedTeams.columns.models"), skeleton: "chips" }, + header: t("logs.deletedTeams.columns.models"), size: 200, enableSorting: false, cell: ({ row }) => , @@ -86,8 +94,8 @@ export const getDeletedTeamsTableColumns = (): ColumnDef[] => [ { id: "organization_id", accessorKey: "organization_id", - meta: { title: "Organization" }, - header: "Organization", + meta: { title: t("logs.deletedTeams.columns.organization") }, + header: t("logs.deletedTeams.columns.organization"), size: 150, enableSorting: false, cell: ({ row }) => { @@ -98,8 +106,8 @@ export const getDeletedTeamsTableColumns = (): ColumnDef[] => [ { id: "deleted_at", accessorKey: "deleted_at", - meta: { title: "Deleted At" }, - header: ({ column }) => , + meta: { title: t("logs.deletedTeams.columns.deletedAt") }, + header: ({ column }) => , size: 120, enableSorting: true, cell: ({ row }) => , @@ -107,8 +115,8 @@ export const getDeletedTeamsTableColumns = (): ColumnDef[] => [ { id: "deleted_by", accessorKey: "deleted_by", - meta: { title: "Deleted By" }, - header: "Deleted By", + meta: { title: t("logs.deletedTeams.columns.deletedBy") }, + header: t("logs.deletedTeams.columns.deletedBy"), size: 120, enableSorting: false, cell: ({ row }) => { diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx index 7044e3a7f31..16e4c0b34ad 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx @@ -1,5 +1,6 @@ import { Loader2 } from "lucide-react"; import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { toast } from "@/lib/toast"; @@ -18,23 +19,24 @@ const EntityUsageExportModal: React.FC = ({ selectedFilters = [], customTitle, }) => { + const { t } = useTranslation(); const [exportFormat, setExportFormat] = useState("csv"); const [exportType, setExportType] = useState("daily"); const [isExporting, setIsExporting] = useState(false); const entityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1); - const modalTitle = customTitle || `Export ${entityLabel} Usage`; + const modalTitle = customTitle || t("usage.export.modal.title", { entity: entityLabel }); const handleExport = async () => { setIsExporting(true); try { const blob = await onExport(exportType, exportFormat); downloadBlob(blob, exportFilename(entityType, exportType, exportFormat, dateRange)); - toast.success(`${entityLabel} usage data exported successfully as ${exportFormat.toUpperCase()}`); + toast.success(t("usage.export.modal.success", { entity: entityLabel, format: exportFormat.toUpperCase() })); onClose(); } catch (error) { console.error("Error exporting data:", error); - toast.fromError("Failed to export data"); + toast.fromError(t("usage.export.modal.failed")); } finally { setIsExporting(false); } @@ -57,11 +59,13 @@ const EntityUsageExportModal: React.FC = ({
      diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.tsx index d031387feaf..8c28fad837d 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.tsx @@ -1,4 +1,6 @@ +import type { ParseKeys } from "i18next"; import React from "react"; +import { useTranslation } from "react-i18next"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import type { ExportFormat } from "./types"; @@ -7,23 +9,24 @@ interface ExportFormatSelectorProps { onChange: (value: ExportFormat) => void; } -const FORMAT_LABELS: Record = { - csv: "CSV (Excel, Google Sheets)", - json: "JSON (includes metadata)", +const FORMAT_LABELS: Record = { + csv: "usage.export.format.csv", + json: "usage.export.format.json", }; const ExportFormatSelector: React.FC = ({ value, onChange }) => { + const { t } = useTranslation(); return (
      - + value && setExportType(value)}> - + @@ -277,11 +284,13 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC {existingSettings && ( - Existing CloudZero Configuration + {t("usage.cloudzero.existing.title")} - API Key: {existingSettings.api_key_masked} + {t("usage.cloudzero.existing.apiKey")} + {existingSettings.api_key_masked}
      - Connection ID: {existingSettings.connection_id} + {t("usage.cloudzero.existing.connectionId")} + {existingSettings.connection_id}
      )} @@ -289,15 +298,27 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC {!existingSettings && (
      event.preventDefault()}> - + {({ ref, ...field }) => ( - + )} - + {({ ref, ...field }) => ( - + )} @@ -312,17 +333,15 @@ const CloudZeroExportModal: React.FC = ({ isOpen, onC {exportType === "csv" && ( - CSV Export - - Export your usage data as a CSV file for analysis in spreadsheet applications. - + {t("usage.cloudzero.csv.title")} + {t("usage.cloudzero.csv.description")} )} {/* Action Buttons */}
      diff --git a/ui/litellm-dashboard/src/components/common_components/AutoRotationView.tsx b/ui/litellm-dashboard/src/components/common_components/AutoRotationView.tsx index 6140738f0c9..87fd7b53f2c 100644 --- a/ui/litellm-dashboard/src/components/common_components/AutoRotationView.tsx +++ b/ui/litellm-dashboard/src/components/common_components/AutoRotationView.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { StatusBadge } from "@/components/shared/table_cells"; import { RefreshIcon, ClockIcon } from "@heroicons/react/outline"; @@ -21,6 +22,7 @@ const AutoRotationView: React.FC = ({ variant = "card", className = "", }) => { + const { t } = useTranslation(); const formatTimestamp = (timestamp: string | Date) => { const date = new Date(timestamp); const dateStr = date.toLocaleDateString("en-US", { @@ -33,7 +35,7 @@ const AutoRotationView: React.FC = ({ minute: "2-digit", hour12: true, }); - return `${dateStr} at ${timeStr}`; + return t("autoRotationView.timestamp", { date: dateStr, time: timeStr }); }; const content = ( @@ -41,12 +43,17 @@ const AutoRotationView: React.FC = ({
      -

      Auto-Rotation

      - +

      {t("autoRotationView.title")}

      + {autoRotate && rotationInterval && ( <>

      •

      -

      Every {rotationInterval}

      +

      + {t("autoRotationView.every", { interval: rotationInterval })} +

      )}
      @@ -58,7 +65,7 @@ const AutoRotationView: React.FC = ({
      -

      Last Rotation

      +

      {t("autoRotationView.lastRotation")}

      {formatTimestamp(lastRotationAt)}

      @@ -68,7 +75,7 @@ const AutoRotationView: React.FC = ({
      -

      Next Scheduled Rotation

      +

      {t("autoRotationView.nextScheduledRotation")}

      {formatTimestamp(nextRotationAt || keyRotationAt || "")}

      @@ -79,7 +86,7 @@ const AutoRotationView: React.FC = ({ {autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
      -

      No rotation history available

      +

      {t("autoRotationView.noHistory")}

      )}
      @@ -88,7 +95,7 @@ const AutoRotationView: React.FC = ({ {!autoRotate && !lastRotationAt && !keyRotationAt && !nextRotationAt && (
      -

      Auto-rotation is not enabled for this key

      +

      {t("autoRotationView.notEnabled")}

      )}
      @@ -99,8 +106,8 @@ const AutoRotationView: React.FC = ({
      -

      Auto-Rotation

      -

      Automatic key rotation settings and status for this key

      +

      {t("autoRotationView.title")}

      +

      {t("autoRotationView.description")}

      {content} @@ -110,7 +117,7 @@ const AutoRotationView: React.FC = ({ return (
      -

      Auto-Rotation

      +

      {t("autoRotationView.title")}

      {content}
      ); diff --git a/ui/litellm-dashboard/src/components/common_components/CustomerMultiSelect.tsx b/ui/litellm-dashboard/src/components/common_components/CustomerMultiSelect.tsx index ffebd736998..6def41aa38f 100644 --- a/ui/litellm-dashboard/src/components/common_components/CustomerMultiSelect.tsx +++ b/ui/litellm-dashboard/src/components/common_components/CustomerMultiSelect.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useCustomerLookups, useCustomers } from "@/app/(dashboard)/hooks/customers/useCustomers"; import { PaginatedMultiSelect } from "@/components/shared/PaginatedMultiSelect"; import type { SearchSelectOption } from "@/components/shared/SearchSelect"; @@ -10,6 +11,7 @@ interface CustomerMultiSelectProps { } export default function CustomerMultiSelect({ value, onChange, disabled }: CustomerMultiSelectProps) { + const { t } = useTranslation(); const [search, setSearch] = useState(""); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } = useCustomers(20, search); const loadedCustomers = useMemo(() => (data?.pages ?? []).flat(), [data]); @@ -27,9 +29,9 @@ export default function CustomerMultiSelect({ value, onChange, disabled }: Custo ).values(), ); const lookupError = selectedCustomers.some((result) => result.isError) - ? "Unable to load selected customers" + ? t("usage.customerSelect.lookupError") : undefined; - const errorText = isError ? "Unable to load customers" : lookupError; + const errorText = isError ? t("usage.customerSelect.loadError") : lookupError; return (
      @@ -42,11 +44,11 @@ export default function CustomerMultiSelect({ value, onChange, disabled }: Custo hasNextPage={hasNextPage} isLoading={isLoading} isFetchingNextPage={isFetchingNextPage} - placeholder="Search customers by ID or alias..." - emptyText="No customers found" + placeholder={t("usage.customerSelect.placeholder")} + emptyText={t("usage.customerSelect.empty")} errorText={errorText} - loadingText="Loading customers..." - clearAllLabel="Clear all customers" + loadingText={t("usage.customerSelect.loading")} + clearAllLabel={t("usage.customerSelect.clearAll")} disabled={disabled} /> {errorText && ( diff --git a/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx b/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx index 99046cfa84d..3a5fcb9d7c8 100644 --- a/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx +++ b/ui/litellm-dashboard/src/components/common_components/LabeledField.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import CopyButton from "@/components/shared/CopyButton"; import { EntityLink } from "@/components/shared/EntityLink"; import { cx } from "@/lib/cva.config"; @@ -24,6 +25,7 @@ export default function LabeledField({ copyable = false, defaultUserIdCheck = false, }: LabeledFieldProps) { + const { t } = useTranslation(); const isEmpty = !value; const isDefaultUser = defaultUserIdCheck && value === DEFAULT_PROXY_ADMIN_USER_ID; const displayValue = isEmpty ? "-" : value; @@ -43,7 +45,7 @@ export default function LabeledField({ {displayValue} )} - {isCopyable && } + {isCopyable && } ); return ( diff --git a/ui/litellm-dashboard/src/components/common_components/RouterSettingsSummary.tsx b/ui/litellm-dashboard/src/components/common_components/RouterSettingsSummary.tsx index 0d0b6a19503..f99a154fe3d 100644 --- a/ui/litellm-dashboard/src/components/common_components/RouterSettingsSummary.tsx +++ b/ui/litellm-dashboard/src/components/common_components/RouterSettingsSummary.tsx @@ -1,4 +1,6 @@ +import { useTranslation } from "react-i18next"; import { Badge } from "@/components/ui/badge"; +import i18n from "@/i18n/i18n"; import { hasRouterSettings } from "./routerSettingsPayload"; interface RouterSettingsSummaryProps { @@ -15,8 +17,9 @@ const fallbackEntries = (fallbacks: unknown): Array<[string, string[]]> => { export default function RouterSettingsSummary({ routerSettings, - emptyText = "No router settings configured", + emptyText = i18n.t("routerSettingsSummary.empty"), }: RouterSettingsSummaryProps) { + const { t } = useTranslation(); if (!hasRouterSettings(routerSettings)) { return
      {emptyText}
      ; } @@ -28,18 +31,27 @@ export default function RouterSettingsSummary({
      {settings.routing_strategy != null && (
      - Routing Strategy: {String(settings.routing_strategy)} + {t("routerSettingsSummary.routingStrategy")}{" "} + {String(settings.routing_strategy)}
      )} - {settings.num_retries != null &&
      Number of Retries: {String(settings.num_retries)}
      } - {settings.allowed_fails != null &&
      Allowed Failures: {String(settings.allowed_fails)}
      } - {settings.cooldown_time != null &&
      Cooldown Time: {String(settings.cooldown_time)}s
      } - {settings.timeout != null &&
      Timeout: {String(settings.timeout)}s
      } - {settings.retry_after != null &&
      Retry After: {String(settings.retry_after)}s
      } - {Boolean(settings.enable_tag_filtering) &&
      Tag Filtering: Enabled
      } + {settings.num_retries != null && ( +
      {t("routerSettingsSummary.numRetries", { value: String(settings.num_retries) })}
      + )} + {settings.allowed_fails != null && ( +
      {t("routerSettingsSummary.allowedFailures", { value: String(settings.allowed_fails) })}
      + )} + {settings.cooldown_time != null && ( +
      {t("routerSettingsSummary.cooldownTime", { value: String(settings.cooldown_time) })}
      + )} + {settings.timeout != null &&
      {t("routerSettingsSummary.timeout", { value: String(settings.timeout) })}
      } + {settings.retry_after != null && ( +
      {t("routerSettingsSummary.retryAfter", { value: String(settings.retry_after) })}
      + )} + {Boolean(settings.enable_tag_filtering) &&
      {t("routerSettingsSummary.tagFilteringEnabled")}
      } {fallbacks.length > 0 && (
      -
      Fallbacks:
      +
      {t("routerSettingsSummary.fallbacks")}
      {fallbacks.map(([model, targets]) => (
      diff --git a/ui/litellm-dashboard/src/components/guardrails/GuardrailSelector.tsx b/ui/litellm-dashboard/src/components/guardrails/GuardrailSelector.tsx index 830dd4c9b1f..424eed160fa 100644 --- a/ui/litellm-dashboard/src/components/guardrails/GuardrailSelector.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/GuardrailSelector.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Guardrail } from "./types"; import { getGuardrailsList } from "../networking"; import { MultiSelect } from "@/components/shared/MultiSelect"; @@ -12,6 +13,7 @@ interface GuardrailSelectorProps { } const GuardrailSelector: React.FC = ({ onChange, value, className, accessToken, disabled }) => { + const { t } = useTranslation(); const [guardrails, setGuardrails] = useState([]); const [loading, setLoading] = useState(false); @@ -43,7 +45,7 @@ const GuardrailSelector: React.FC = ({ onChange, value,
      { // Find the display name for the callback const callbackDisplayName = Object.entries(callback_map).find(([_, value]) => value === callbackName)?.[0]; @@ -45,11 +47,11 @@ export function LoggingSettingsView({ const getEventTypeLabel = (eventType: string) => { switch (eventType) { case "success": - return "Success Only"; + return t("loggingSettingsView.eventTypes.success"); case "failure": - return "Failure Only"; + return t("loggingSettingsView.eventTypes.failure"); case "success_and_failure": - return "Success & Failure"; + return t("loggingSettingsView.eventTypes.successAndFailure"); default: return eventType; } @@ -61,7 +63,7 @@ export function LoggingSettingsView({
      - Logging Integrations + {t("loggingSettingsView.loggingIntegrations")} {loggingConfigs.length}
      @@ -84,7 +86,9 @@ export function LoggingSettingsView({
      {displayName} - {Object.keys(config.callback_vars).length} parameters configured + {t("loggingSettingsView.parametersConfigured", { + count: Object.keys(config.callback_vars).length, + })}
      @@ -98,7 +102,7 @@ export function LoggingSettingsView({ ) : (
      - No logging integrations configured + {t("loggingSettingsView.noLoggingIntegrations")}
      )}
      @@ -107,7 +111,7 @@ export function LoggingSettingsView({
      - Disabled Callbacks + {t("loggingSettingsView.disabledCallbacks")} {disabledCallbacks.length}
      @@ -130,10 +134,10 @@ export function LoggingSettingsView({ />
      {displayName} - Disabled for this key + {t("loggingSettingsView.disabledForKey")}
      - Disabled + {t("loggingSettingsView.disabled")}
      ); })} @@ -141,7 +145,7 @@ export function LoggingSettingsView({ ) : (
      - No callbacks disabled + {t("loggingSettingsView.noCallbacksDisabled")}
      )}
      @@ -153,10 +157,8 @@ export function LoggingSettingsView({
      - Logging Settings - - Active logging integrations and disabled callbacks for this key - + {t("loggingSettingsView.title")} + {t("loggingSettingsView.description")}
      {content} @@ -166,7 +168,7 @@ export function LoggingSettingsView({ return (
      - Logging Settings + {t("loggingSettingsView.title")} {content}
      ); diff --git a/ui/litellm-dashboard/src/components/logs/audit/AuditLogDrawer/AuditLogDrawer.tsx b/ui/litellm-dashboard/src/components/logs/audit/AuditLogDrawer/AuditLogDrawer.tsx index eaddf1e507f..7632511ba54 100644 --- a/ui/litellm-dashboard/src/components/logs/audit/AuditLogDrawer/AuditLogDrawer.tsx +++ b/ui/litellm-dashboard/src/components/logs/audit/AuditLogDrawer/AuditLogDrawer.tsx @@ -1,6 +1,7 @@ import { copyToClipboard } from "@/utils/dataUtils"; import { Check, Copy } from "lucide-react"; import { useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; import moment from "moment"; import { AuditLogEntry, AUDIT_TABLE_NAME_DISPLAY } from "../AuditLogsTableColumns"; import DefaultProxyAdminTag from "../../../common_components/DefaultProxyAdminTag"; @@ -25,6 +26,7 @@ const ACTION_TONE: Record = { }; function CopyableJsonBlock({ label, value }: { label: string; value: Record }) { + const { t } = useTranslation(); const [copied, setCopied] = useState(false); const handleCopy = useCallback(async () => { @@ -38,7 +40,13 @@ function CopyableJsonBlock({ label, value }: { label: string; value: Record
      {label} -
      @@ -59,6 +67,7 @@ function MetadataRow({ label, value }: { label: string; value: React.ReactNode } } function DiffSection({ log }: { log: AuditLogEntry }) { + const { t } = useTranslation(); const { action, table_name, before_value, updated_values } = log; const isKeyTable = table_name === "LiteLLM_VerificationToken"; const isUpdateAction = action === "updated" || action === "rotated"; @@ -96,8 +105,10 @@ function DiffSection({ log }: { log: AuditLogEntry }) { } }); - displayBefore = Object.keys(changedBefore).length > 0 ? changedBefore : { note: "No differing fields detected" }; - displayAfter = Object.keys(changedAfter).length > 0 ? changedAfter : { note: "No differing fields detected" }; + displayBefore = + Object.keys(changedBefore).length > 0 ? changedBefore : { note: t("logs.audit.drawer.noDifferingFields") }; + displayAfter = + Object.keys(changedAfter).length > 0 ? changedAfter : { note: t("logs.audit.drawer.noDifferingFields") }; } const renderValue = (label: string, value: Record | null | undefined) => { @@ -125,17 +136,19 @@ function DiffSection({ log }: { log: AuditLogEntry }) {
      {value.token !== undefined && (

      - Token: {value.token ?? "N/A"} + {t("logs.audit.drawer.token")} {value.token ?? "N/A"}

      )} {value.spend !== undefined && (

      - Spend: ${Number(value.spend).toFixed(6)} + {t("logs.audit.drawer.spend")} $ + {Number(value.spend).toFixed(6)}

      )} {value.max_budget !== undefined && (

      - Max Budget: ${Number(value.max_budget).toFixed(6)} + {t("logs.audit.drawer.maxBudget")} $ + {Number(value.max_budget).toFixed(6)}

      )}
      @@ -149,21 +162,24 @@ function DiffSection({ log }: { log: AuditLogEntry }) { return (
      - {renderValue("Before", displayBefore)} - {renderValue("After", displayAfter)} + {renderValue(t("logs.audit.drawer.before"), displayBefore)} + {renderValue(t("logs.audit.drawer.after"), displayAfter)}
      ); } export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) { + const { t } = useTranslation(); if (!log) return null; - const tableDisplay = AUDIT_TABLE_NAME_DISPLAY[log.table_name] ?? log.table_name; + const tableDisplay = Object.hasOwn(AUDIT_TABLE_NAME_DISPLAY, log.table_name) + ? t(AUDIT_TABLE_NAME_DISPLAY[log.table_name]) + : log.table_name; return ( !nextOpen && onClose()}> - Audit log details + {t("logs.audit.drawer.title")}
      @@ -174,25 +190,30 @@ export function AuditLogDrawer({ open, onClose, log }: AuditLogDrawerProps) {
      -

      Details

      - +

      + {t("logs.audit.drawer.details")} +

      + {log.object_id} - + } /> - } /> } + /> + {log.changed_by_api_key} - + ) : ( "—" diff --git a/ui/litellm-dashboard/src/components/logs/audit/AuditLogsPanel.tsx b/ui/litellm-dashboard/src/components/logs/audit/AuditLogsPanel.tsx index f174f45a791..6326feb1638 100644 --- a/ui/litellm-dashboard/src/components/logs/audit/AuditLogsPanel.tsx +++ b/ui/litellm-dashboard/src/components/logs/audit/AuditLogsPanel.tsx @@ -1,4 +1,5 @@ import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useDebouncedValue } from "@tanstack/react-pacer/debouncer"; import { useQuery, keepPreviousData } from "@tanstack/react-query"; import { ColumnFiltersState, OnChangeFn, PaginationState } from "@tanstack/react-table"; @@ -39,6 +40,7 @@ export default function AuditLogsPanel({ isActive, premiumUser, }: AuditLogsProps) { + const { t } = useTranslation(); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: PAGE_SIZE }); const [columnFilters, setColumnFilters] = useState([]); const [searchInput, setSearchInput] = useState(""); @@ -101,16 +103,14 @@ export default function AuditLogsPanel({ if (!premiumUser) { return (
      -

      ✨ Enterprise Feature.

      -

      - This is a LiteLLM Enterprise feature, and requires a valid key to use. -

      +

      {t("logs.audit.panel.enterpriseTitle")}

      +

      {t("logs.audit.panel.enterpriseDescription")}

      - Here's a preview of what Audit Logs offer: + {t("logs.audit.panel.previewIntro")}

      Audit Logs Preview = { table_name: "Table", }; -const formatFilterValue = (columnId: string, value: unknown): string => { +const formatFilterValue = (columnId: string, value: unknown, t: TFunction = i18n.t.bind(i18n)): string => { const raw = String(value); if (columnId === "action") { return ACTION_OPTIONS.find((option) => option.value === raw)?.label ?? raw; } if (columnId === "table_name") { - return AUDIT_TABLE_NAME_DISPLAY[raw] ?? raw; + return Object.hasOwn(AUDIT_TABLE_NAME_DISPLAY, raw) ? t(AUDIT_TABLE_NAME_DISPLAY[raw]) : raw; } return raw; }; function AuditLogsEmptyState({ filtered }: { filtered: boolean }) { + const { t } = useTranslation(); return (
      - {filtered ? "No matching audit logs" : "No audit logs yet"} + {filtered ? t("logs.audit.table.emptyFilteredTitle") : t("logs.audit.table.emptyTitle")}
      - {filtered - ? "No audit log entries match your filters." - : "Administrative changes to keys, teams, users, and models will appear here."} + {filtered ? t("logs.audit.table.emptyFilteredDescription") : t("logs.audit.table.emptyDescription")}
      ); @@ -111,8 +113,9 @@ export function AuditLogsTable({ onRefresh, onViewLog, }: AuditLogsTableProps) { + const { t } = useTranslation(); const [filtersOpen, setFiltersOpen] = useState(false); - const columns = useMemo(() => getAuditLogsTableColumns({ onViewLog }), [onViewLog]); + const columns = useMemo(() => getAuditLogsTableColumns({ onViewLog }, t), [onViewLog, t]); const hasActiveSearch = Boolean(searchValue?.trim()); return ( @@ -129,7 +132,7 @@ export function AuditLogsTable({ columnFilters={columnFilters} onColumnFiltersChange={onColumnFiltersChange} isLoading={isLoading} - loadingMessage="Loading audit logs…" + loadingMessage={t("logs.audit.table.loading")} noDataMessage={ 0 || hasActiveSearch} />} size="compact" toolbar={(table) => ( @@ -138,7 +141,7 @@ export function AuditLogsTable({ table={table} searchValue={searchValue} onSearchChange={onSearchChange} - searchPlaceholder="Search audit logs by ID…" + searchPlaceholder={t("logs.audit.table.searchPlaceholder")} onRefresh={onRefresh} isRefreshing={isRefreshing} onOpenFilters={() => setFiltersOpen(true)} @@ -150,50 +153,50 @@ export function AuditLogsTable({ table={table} open={filtersOpen} onOpenChange={setFiltersOpen} - title="Filters" - description="Narrow down audit log entries" + title={t("logs.audit.table.filtersTitle")} + description={t("logs.audit.table.filtersDescription")} > {({ get, set }) => ( <> - + set("object_id", event.target.value)} - placeholder="Enter object ID…" + placeholder={t("logs.audit.table.objectIdPlaceholder")} /> - + set("changed_by", event.target.value)} - placeholder="Enter user ID…" + placeholder={t("logs.audit.table.changedByPlaceholder")} /> - + set("team_id", event.target.value)} - placeholder="Enter team ID…" + placeholder={t("logs.audit.table.teamIdPlaceholder")} /> - + set("key_hash", event.target.value)} - placeholder="Enter key hash…" + placeholder={t("logs.audit.table.keyHashPlaceholder")} /> - + - +
      - Criterion - Weight - Score + {t("logs.detail.eval.columns.criterion")} + {t("logs.detail.eval.columns.weight")} + {t("logs.detail.eval.columns.score")} }> - Weighted + {t("logs.detail.eval.columns.weighted")} - - Score × Weight — how much each criterion contributes to the final score - + {t("logs.detail.eval.columns.weightedTooltip")} - Comment + {t("logs.detail.eval.columns.comment")} @@ -189,7 +189,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) { - Total + {t("logs.detail.eval.total")} @@ -206,7 +206,7 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
      ) : ( - Score: {entry.overall_score?.toFixed(1)} — no per-criterion breakdown available. + {t("logs.detail.eval.noBreakdown", { score: entry.overall_score?.toFixed(1) })} )} diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/BedrockGuardrailDetails.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/BedrockGuardrailDetails.tsx index 899d4cfe762..33730bd1b37 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/BedrockGuardrailDetails.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/BedrockGuardrailDetails.tsx @@ -1,4 +1,6 @@ import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import i18n from "@/i18n/i18n"; export type BedrockGuardrailAction = "NONE" | "GUARDRAIL_INTERVENED"; @@ -114,7 +116,8 @@ const chip = (text: React.ReactNode, tone: ChipTone = "slate") => { return {text}; }; -const boolPill = (b?: boolean) => (b ? chip("detected", "red") : chip("not detected", "slate")); +const boolPill = (b?: boolean) => + b ? chip(i18n.t("logs.detail.bedrock.detected"), "red") : chip(i18n.t("logs.detail.bedrock.notDetected"), "slate"); interface SectionProps { title: string; @@ -169,6 +172,7 @@ const Divider: React.FC = () =>
      ; /** ====== Main component ====== */ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailResponse }> = ({ response }) => { + const { t } = useTranslation(); if (!response) return null; const outputs: BedrockOutputContent[] = (response.outputs ?? response.output ?? []) as BedrockOutputContent[]; @@ -179,12 +183,18 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
      {response.guardrailCoverage?.textCharacters && chip( - `text guarded ${response.guardrailCoverage.textCharacters.guarded ?? 0}/${response.guardrailCoverage.textCharacters.total ?? 0}`, + t("logs.detail.bedrock.textGuarded", { + guarded: response.guardrailCoverage.textCharacters.guarded ?? 0, + total: response.guardrailCoverage.textCharacters.total ?? 0, + }), "blue", )} {response.guardrailCoverage?.images && chip( - `images guarded ${response.guardrailCoverage.images.guarded ?? 0}/${response.guardrailCoverage.images.total ?? 0}`, + t("logs.detail.bedrock.imagesGuarded", { + guarded: response.guardrailCoverage.images.guarded ?? 0, + total: response.guardrailCoverage.images.total ?? 0, + }), "blue", )}
      @@ -208,17 +218,17 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
      - {chip(response.action ?? "N/A", actionTone)} - {response.actionReason && {response.actionReason}} + {chip(response.action ?? "N/A", actionTone)} + {response.actionReason && {response.actionReason}} {response.blockedResponse && ( - + {response.blockedResponse} )}
      - {coverageChips} - {usagePills} + {coverageChips} + {usagePills}
      @@ -226,11 +236,13 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo {outputs.length > 0 && ( <> -

      Outputs

      +

      {t("logs.detail.bedrock.outputs")}

      {outputs.map((o, i) => (
      -
      {o.text ?? (non-text output)}
      +
      + {o.text ?? {t("logs.detail.bedrock.nonTextOutput")}} +
      ))}
      @@ -256,12 +268,17 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo return (
      {assess.invocationMetrics?.guardrailProcessingLatency != null && - chip(`${assess.invocationMetrics.guardrailProcessingLatency} ms`, "amber")} + chip( + t("logs.detail.bedrock.latencyMs", { + value: assess.invocationMetrics.guardrailProcessingLatency, + }), + "amber", + )} {policyBadges}
      } @@ -269,9 +286,9 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo {/* Word policy */} {assess.wordPolicy && (
      -
      Word Policy
      +
      {t("logs.detail.bedrock.wordPolicy")}
      {(assess.wordPolicy.customWords?.length ?? 0) > 0 && ( -
      +
      {assess.wordPolicy.customWords!.map((w, i) => (
      @@ -286,7 +303,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
      )} {(assess.wordPolicy.managedWordLists?.length ?? 0) > 0 && ( -
      +
      {assess.wordPolicy.managedWordLists!.map((w, i) => (
      @@ -307,16 +324,16 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo {/* Content policy */} {assess.contentPolicy?.filters?.length ? (
      -
      Content Policy
      +
      {t("logs.detail.bedrock.contentPolicy")}
      - - - - - + + + + + @@ -338,16 +355,16 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo {/* Contextual grounding */} {assess.contextualGroundingPolicy?.filters?.length ? (
      -
      Contextual Grounding
      +
      {t("logs.detail.bedrock.contextualGrounding")}
      TypeActionDetectedStrengthConfidence{t("logs.detail.bedrock.columns.type")}{t("logs.detail.bedrock.columns.action")}{t("logs.detail.bedrock.columns.detected")}{t("logs.detail.bedrock.columns.strength")}{t("logs.detail.bedrock.columns.confidence")}
      - - - - - + + + + + @@ -369,9 +386,9 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo {/* Sensitive Information */} {assess.sensitiveInformationPolicy && (
      -
      Sensitive Information
      +
      {t("logs.detail.bedrock.sensitiveInformation")}
      {(assess.sensitiveInformationPolicy.piiEntities?.length ?? 0) > 0 && ( -
      +
      {assess.sensitiveInformationPolicy.piiEntities!.map((p, i) => (
      @@ -387,7 +404,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
      )} {(assess.sensitiveInformationPolicy.regexes?.length ?? 0) > 0 && ( -
      +
      {assess.sensitiveInformationPolicy.regexes!.map((r, i) => (
      -
      Topic Policy
      +
      {t("logs.detail.bedrock.topicPolicy")}
      {assess.topicPolicy.topics!.map((t, i) => (
      @@ -432,31 +449,35 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo {/* Invocation metrics */} {assess.invocationMetrics && ( -
      +
      - {assess.invocationMetrics.guardrailProcessingLatency ?? "—"} - + + {assess.invocationMetrics.guardrailProcessingLatency ?? "—"} + +
      {assess.invocationMetrics.guardrailCoverage?.textCharacters && chip( - `text ${assess.invocationMetrics.guardrailCoverage.textCharacters.guarded ?? 0}/${ - assess.invocationMetrics.guardrailCoverage.textCharacters.total ?? 0 - }`, + t("logs.detail.bedrock.textCoverage", { + guarded: assess.invocationMetrics.guardrailCoverage.textCharacters.guarded ?? 0, + total: assess.invocationMetrics.guardrailCoverage.textCharacters.total ?? 0, + }), "blue", )} {assess.invocationMetrics.guardrailCoverage?.images && chip( - `images ${assess.invocationMetrics.guardrailCoverage.images.guarded ?? 0}/${ - assess.invocationMetrics.guardrailCoverage.images.total ?? 0 - }`, + t("logs.detail.bedrock.imagesCoverage", { + guarded: assess.invocationMetrics.guardrailCoverage.images.guarded ?? 0, + total: assess.invocationMetrics.guardrailCoverage.images.total ?? 0, + }), "blue", )}
      - +
      {assess.invocationMetrics.usage && Object.entries(assess.invocationMetrics.usage).map(([k, v]) => @@ -478,7 +499,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo {/* Automated reasoning (fallback render) */} {assess.automatedReasoningPolicy?.findings?.length ? ( -
      +
      {assess.automatedReasoningPolicy.findings!.map((f, i) => (
      @@ -495,7 +516,7 @@ export const BedrockGuardrailDetails: React.FC<{ response: BedrockGuardrailRespo
             ) : null}
       
             {/* Raw JSON (for debugging / completeness) */}
      -      
      +
      {JSON.stringify(response, null, 2)}
      diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/CompliancePanel.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/CompliancePanel.tsx index 06450f2b7d2..85f870f6c6a 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/CompliancePanel.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/CompliancePanel.tsx @@ -1,4 +1,6 @@ import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import i18n from "@/i18n/i18n"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { checkEuAiActCompliance, @@ -54,6 +56,7 @@ const ComplianceCard = ({ loading: boolean; error: string | null; }) => { + const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); return ( @@ -88,12 +91,12 @@ const ComplianceCard = ({ : "bg-destructive/15 text-destructive border border-destructive/20" }`} > - {data.compliant ? "COMPLIANT" : "NON-COMPLIANT"} + {data.compliant ? t("logs.detail.compliance.compliant") : t("logs.detail.compliance.nonCompliant")} )} {error && ( - UNAVAILABLE + {t("logs.detail.compliance.unavailable")} )} - {loading &&

      Checking compliance...

      } + {loading &&

      {t("logs.detail.compliance.checking")}

      } {error &&

      {error}

      } {data && (
      @@ -137,6 +140,7 @@ const ComplianceCard = ({ // -- Main Component -- const CompliancePanel: React.FC = ({ accessToken, logEntry }) => { + const { t } = useTranslation(); const [euAiActData, setEuAiActData] = useState(null); const [gdprData, setGdprData] = useState(null); const [euAiActLoading, setEuAiActLoading] = useState(false); @@ -159,25 +163,35 @@ const CompliancePanel: React.FC = ({ accessToken, logEntry setEuAiActError(null); checkEuAiActCompliance(accessToken, payload) .then(setEuAiActData) - .catch((err) => setEuAiActError(err.message || "Failed to check EU AI Act compliance")) + .catch((err) => setEuAiActError(err.message || i18n.t("logs.detail.compliance.euAiActFailed"))) .finally(() => setEuAiActLoading(false)); setGdprLoading(true); setGdprError(null); checkGdprCompliance(accessToken, payload) .then(setGdprData) - .catch((err) => setGdprError(err.message || "Failed to check GDPR compliance")) + .catch((err) => setGdprError(err.message || i18n.t("logs.detail.compliance.gdprFailed"))) .finally(() => setGdprLoading(false)); }, [accessToken, logEntry]); return (

      - Regulatory Compliance + {t("logs.detail.compliance.title")}

      - - + +
      ); diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/ContentFilterDetails.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/ContentFilterDetails.tsx index 424d9fd6827..13c585210da 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/ContentFilterDetails.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/ContentFilterDetails.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; export interface ContentFilterDetection { type: "pattern" | "blocked_word" | "category_keyword"; @@ -74,13 +75,14 @@ const KV: React.FC = ({ label, children, mono }) => ( ); const ContentFilterDetails: React.FC = ({ response }) => { + const { t } = useTranslation(); // Handle case where response is a string (error message) or null if (!response || typeof response === "string") { if (typeof response === "string" && response) { return (
      -
      Error
      +
      {t("logs.detail.contentFilter.error")}

      {response}

      @@ -95,7 +97,7 @@ const ContentFilterDetails: React.FC = ({ response }) if (detections.length === 0) { return (
      -
      No detections found
      +
      {t("logs.detail.contentFilter.noDetections")}
      ); } @@ -118,23 +120,26 @@ const ContentFilterDetails: React.FC = ({ response })
      - + {totalDetections} - +
      - {blockedCount > 0 && chip(`${blockedCount} blocked`, "red")} - {maskedCount > 0 && chip(`${maskedCount} masked`, "blue")} - {blockedCount === 0 && maskedCount === 0 && chip("passed", "green")} + {blockedCount > 0 && chip(t("logs.detail.contentFilter.blockedCount", { count: blockedCount }), "red")} + {maskedCount > 0 && chip(t("logs.detail.contentFilter.maskedCount", { count: maskedCount }), "blue")} + {blockedCount === 0 && maskedCount === 0 && chip(t("logs.detail.contentFilter.passed"), "green")}
      - +
      - {patterns.length > 0 && chip(`${patterns.length} patterns`, "slate")} - {blockedWords.length > 0 && chip(`${blockedWords.length} keywords`, "slate")} - {categoryKeywords.length > 0 && chip(`${categoryKeywords.length} categories`, "slate")} + {patterns.length > 0 && + chip(t("logs.detail.contentFilter.patternsCount", { count: patterns.length }), "slate")} + {blockedWords.length > 0 && + chip(t("logs.detail.contentFilter.keywordsCount", { count: blockedWords.length }), "slate")} + {categoryKeywords.length > 0 && + chip(t("logs.detail.contentFilter.categoriesCount", { count: categoryKeywords.length }), "slate")}
      @@ -143,16 +148,18 @@ const ContentFilterDetails: React.FC = ({ response }) {/* Patterns Section */} {patterns.length > 0 && ( -
      +
      {patterns.map((detection, idx) => (
      - {detection.pattern_name || "unknown"} + {detection.pattern_name || "unknown"}
      - {chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")} + + {chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")} +
      @@ -163,19 +170,23 @@ const ContentFilterDetails: React.FC = ({ response }) {/* Blocked Words Section */} {blockedWords.length > 0 && ( -
      +
      {blockedWords.map((detection, idx) => (
      - + {detection.keyword || "unknown"} - {detection.description && {detection.description}} + {detection.description && ( + {detection.description} + )}
      - {chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")} + + {chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")} +
      @@ -186,18 +197,22 @@ const ContentFilterDetails: React.FC = ({ response }) {/* Category Keywords Section */} {categoryKeywords.length > 0 && ( -
      +
      {categoryKeywords.map((detection, idx) => (
      - {detection.category || "unknown"} - + {detection.category || "unknown"} + {detection.keyword || "unknown"} {detection.severity && ( - + {chip( detection.severity, detection.severity === "high" ? "red" : detection.severity === "medium" ? "amber" : "slate", @@ -206,7 +221,9 @@ const ContentFilterDetails: React.FC = ({ response }) )}
      - {chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")} + + {chip(detection.action, detection.action === "BLOCK" ? "red" : "blue")} +
      @@ -216,7 +233,7 @@ const ContentFilterDetails: React.FC = ({ response }) )} {/* Raw JSON (for debugging) */} -
      +
      {JSON.stringify(detections, null, 2)}
      diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/GuardrailViewer.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/GuardrailViewer.tsx index 0c5ab90f454..7c9a5be9662 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/GuardrailViewer.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/GuardrailViewer.tsx @@ -1,4 +1,6 @@ import React, { useState, useMemo } from "react"; +import type { ParseKeys, TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import PresidioDetectedEntities from "./PresidioDetectedEntities"; import BedrockGuardrailDetails, { @@ -121,10 +123,10 @@ const formatMode = (mode: GuardrailInformation["guardrail_mode"]): string => { return s.replace(/_/g, "-").toUpperCase(); }; -const formatDurationMs = (seconds: number | null): string => { +const formatDurationMs = (seconds: number | null, t: TFunction): string => { if (seconds == null) return "—"; const ms = Math.round(seconds * 1000); - return `${ms}ms`; + return t("logs.detail.guardrails.durationMs", { ms }); }; const getTotalMasked = (entry: GuardrailInformation): number => { @@ -146,11 +148,11 @@ const getEntryOutcome = (entry: GuardrailInformation): EntryOutcome => { const isEntrySuccess = (entry: GuardrailInformation): boolean => getEntryOutcome(entry) === "passed"; -const OUTCOME_LABEL: Record = { - passed: "PASSED", - flagged: "FLAGGED", - failed: "FAILED", - not_run: "NOT RUN", +const OUTCOME_LABEL: Record = { + passed: "logs.detail.guardrails.outcome.passed", + flagged: "logs.detail.guardrails.outcome.flagged", + failed: "logs.detail.guardrails.outcome.failed", + not_run: "logs.detail.guardrails.outcome.notRun", }; const OUTCOME_BADGE_CLASS: Record = { @@ -286,19 +288,22 @@ const DownloadIcon = () => ( // ── Sub-components ────────────────────────────────────────────────────────── const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) => { + const { t } = useTranslation(); if (!matchDetails || matchDetails.length === 0) return null; return (
      -
      Match Details ({matchDetails.length})
      +
      + {t("logs.detail.guardrails.matchDetails.title", { count: matchDetails.length })} +
      TypeActionDetectedScoreThreshold{t("logs.detail.bedrock.columns.type")}{t("logs.detail.bedrock.columns.action")}{t("logs.detail.bedrock.columns.detected")}{t("logs.detail.bedrock.columns.score")}{t("logs.detail.bedrock.columns.threshold")}
      - - - - + + + + @@ -333,6 +338,7 @@ const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) => }; const GenericGuardrailResponse = ({ response }: { response: any }) => { + const { t } = useTranslation(); const [showRaw, setShowRaw] = useState(false); return (
      @@ -343,7 +349,7 @@ const GenericGuardrailResponse = ({ response }: { response: any }) => { >
      -
      Raw Guardrail Response
      +
      {t("logs.detail.guardrails.rawResponse")}
      {showRaw && ( @@ -382,6 +388,7 @@ const orderWithinPhase = (group: GuardrailInformation[]): GuardrailInformation[] }; const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => { + const { t } = useTranslation(); const sorted = useMemo(() => entries.filter(belongsOnLifecycle), [entries]); const timeline = useMemo(() => { @@ -394,7 +401,11 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => { const items: TimelineEntry[] = []; // Request received - items.push({ type: "request", label: "Request received", offsetMs: baseTime === null ? null : 0 }); + items.push({ + type: "request", + label: t("logs.detail.guardrails.lifecycle.requestReceived"), + offsetMs: baseTime === null ? null : 0, + }); // Pre-call guardrails — use modeMatches so array modes (e.g. ["pre_call", "post_call"]) // place the entry in every matching bucket. @@ -407,7 +418,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => { for (const e of preCalls) { items.push({ type: "guardrail", - label: `Pre-call guardrail: ${getDisplayName(e)}`, + label: t("logs.detail.guardrails.lifecycle.preCall", { name: getDisplayName(e) }), offsetMs: offsetOf(e), outcome: getEntryOutcome(e), }); @@ -422,7 +433,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => { items.push({ type: "llm", - label: "LLM call", + label: t("logs.detail.guardrails.lifecycle.llmCall"), offsetMs: llmEndTime === null || baseTime === null ? null : Math.round((llmEndTime - baseTime) * 1000), }); @@ -430,7 +441,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => { for (const e of duringCalls) { items.push({ type: "guardrail", - label: `During-call guardrail: ${getDisplayName(e)}`, + label: t("logs.detail.guardrails.lifecycle.duringCall", { name: getDisplayName(e) }), offsetMs: offsetOf(e), outcome: getEntryOutcome(e), }); @@ -440,7 +451,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => { for (const e of postCalls) { items.push({ type: "guardrail", - label: `Post-call guardrail: ${getDisplayName(e)}`, + label: t("logs.detail.guardrails.lifecycle.postCall", { name: getDisplayName(e) }), offsetMs: offsetOf(e), outcome: getEntryOutcome(e), }); @@ -449,14 +460,20 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => { // Response returned const maxEnd = timed.length > 0 ? Math.max(...timed.map((e) => e.end_time)) : null; const responseOffsetMs = maxEnd === null || baseTime === null ? null : Math.round((maxEnd - baseTime) * 1000) + 1; - items.push({ type: "response", label: "Response returned", offsetMs: responseOffsetMs }); + items.push({ + type: "response", + label: t("logs.detail.guardrails.lifecycle.responseReturned"), + offsetMs: responseOffsetMs, + }); return items; - }, [sorted]); + }, [sorted, t]); return (
      -

      Request Lifecycle

      +

      + {t("logs.detail.guardrails.lifecycle.title")} +

      {timeline.map((item, idx) => (
      @@ -484,11 +501,11 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => { - {OUTCOME_LABEL[item.outcome]} + {t(OUTCOME_LABEL[item.outcome])} )} - {item.offsetMs === null ? "—" : `T+${item.offsetMs}ms`} + {item.offsetMs === null ? "—" : t("logs.detail.guardrails.lifecycle.offset", { ms: item.offsetMs })}
      @@ -509,11 +526,12 @@ const formatGuardrailCost = (cost: number): string => { }; const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { + const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); const outcome = getEntryOutcome(entry); const totalMasked = getTotalMasked(entry); const displayName = getDisplayName(entry); - const durationStr = formatDurationMs(entry.duration); + const durationStr = formatDurationMs(entry.duration, t); const modeStr = formatMode(entry.guardrail_mode); const riskScore = getRiskScore(entry); const textRecords = entry.guardrail_usage?.["text_records"]; @@ -532,9 +550,9 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { // Match count string: "X/Y matched" or "X matched" const matchCountStr = entry.patterns_checked != null - ? `${totalMasked}/${entry.patterns_checked} matched` + ? t("logs.detail.guardrails.card.matchedOfChecked", { matched: totalMasked, checked: entry.patterns_checked }) : totalMasked > 0 - ? `${totalMasked} matched` + ? t("logs.detail.guardrails.card.matched", { count: totalMasked }) : null; return ( @@ -560,7 +578,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { - {OUTCOME_LABEL[outcome]} + {t(OUTCOME_LABEL[outcome])} {matchCountStr && ( @@ -577,7 +595,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { {entry.confidence_score != null && ( - {(entry.confidence_score * 100).toFixed(0)}% conf + {t("logs.detail.guardrails.card.confidence", { value: (entry.confidence_score * 100).toFixed(0) })} )} @@ -591,16 +609,19 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { /> } > - Risk {riskScore}/10 + {t("logs.detail.guardrails.card.risk", { score: riskScore })} - {`Risk score: ${riskScore}/10`} + {t("logs.detail.guardrails.card.riskTooltip", { score: riskScore })} )} {textRecords != null && ( - {textRecords.toLocaleString()} text record{textRecords === 1 ? "" : "s"} + {t("logs.detail.guardrails.card.textRecords", { + count: textRecords, + value: textRecords.toLocaleString(), + })} )} @@ -616,8 +637,8 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { {entry.guardrail_cost_in_spend === false - ? "Estimated guardrail cost (reported only; not counted against spend or budgets)" - : "Guardrail cost"} + ? t("logs.detail.guardrails.card.costEstimated") + : t("logs.detail.guardrails.card.cost")} @@ -642,28 +663,38 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { {/* Classification details for llm-judge */} {entry.classification && (
      -
      Classification
      +
      + {t("logs.detail.guardrails.classification.title")} +
      {entry.classification.category && (
      - Category: + + {t("logs.detail.guardrails.classification.category")} + {entry.classification.category}
      )} {entry.classification.article_reference && (
      - Reference: + + {t("logs.detail.guardrails.classification.reference")} + {entry.classification.article_reference}
      )} {entry.classification.confidence != null && (
      - Confidence: + + {t("logs.detail.guardrails.classification.confidence")} + {(entry.classification.confidence * 100).toFixed(0)}%
      )} {entry.classification.reason && (
      - Reason: + + {t("logs.detail.guardrails.classification.reason")} + {entry.classification.reason}
      )} @@ -678,7 +709,9 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { {/* Masked entity summary */} {totalMasked > 0 && (
      -
      Masked Entities
      +
      + {t("logs.detail.guardrails.card.maskedEntities")} +
      {Object.entries(entry.masked_entity_count || {}).map(([entityType, count]) => ( @@ -721,6 +754,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { // ── Main Component ────────────────────────────────────────────────────────── const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps) => { + const { t } = useTranslation(); const guardrailEntries = useMemo(() => { return Array.isArray(data) ? data.filter((entry): entry is GuardrailInformation => Boolean(entry)) @@ -763,10 +797,10 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
      -

      Guardrails & Policy Compliance

      +

      {t("logs.detail.guardrails.title")}

      - {evaluatedCount} guardrail{evaluatedCount !== 1 ? "s" : ""} evaluated + {t("logs.detail.guardrails.evaluated", { count: evaluatedCount })} | ) : null} - {passedCount} Passed + {t("logs.detail.guardrails.passedCount", { count: passedCount })} {flaggedCount > 0 && ( - {flaggedCount} Flagged + {t("logs.detail.guardrails.flaggedCount", { count: flaggedCount })} )} {notRunCount > 0 && ( - {notRunCount} Not run + {t("logs.detail.guardrails.notRunCount", { count: notRunCount })} )}
      @@ -805,7 +839,9 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
      -
      Total: {totalOverheadMs}ms overhead
      +
      + {t("logs.detail.guardrails.totalOverhead", { ms: totalOverheadMs })} +
      @@ -835,7 +871,7 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps) {/* Evaluation Details */}

      - Evaluation Details + {t("logs.detail.guardrails.evaluationDetails")}

      {guardrailEntries.map((entry, index) => ( diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/PresidioDetectedEntities.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/PresidioDetectedEntities.tsx index d4b456fec11..331a8aceb9b 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/PresidioDetectedEntities.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/GuardrailViewer/PresidioDetectedEntities.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; interface RecognitionMetadata { recognizer_name: string; @@ -26,6 +27,7 @@ const getScoreColor = (score: number): string => { const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) => { const [entityListExpanded, setEntityListExpanded] = useState(true); const [expandedEntities, setExpandedEntities] = useState>({}); + const { t } = useTranslation(); const toggleEntity = (index: number) => { setExpandedEntities((prev) => ({ @@ -47,7 +49,7 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) = > -

      Detected Entities ({entities.length})

      +

      {t("logs.detail.presidio.title", { count: entities.length })}

      {entityListExpanded && ( @@ -71,10 +73,12 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) = {entity.entity_type} - Score: {entity.score.toFixed(2)} + + {t("logs.detail.presidio.score", { value: entity.score.toFixed(2) })} +
      - Position: {entity.start}-{entity.end} + {t("logs.detail.presidio.position", { start: entity.start, end: entity.end })}
      @@ -83,17 +87,15 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) =
      - Entity Type: + {t("logs.detail.presidio.entityType")} {entity.entity_type}
      - Position: - - Characters {entity.start}-{entity.end} - + {t("logs.detail.presidio.positionLabel")} + {t("logs.detail.presidio.characters", { start: entity.start, end: entity.end })}
      - Confidence: + {t("logs.detail.presidio.confidence")} {entity.score.toFixed(2)}
      @@ -102,11 +104,11 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) = {entity.recognition_metadata && ( <>
      - Recognizer: + {t("logs.detail.presidio.recognizer")} {entity.recognition_metadata.recognizer_name}
      - Identifier: + {t("logs.detail.presidio.identifier")} {entity.recognition_metadata.recognizer_identifier} @@ -115,7 +117,7 @@ const PresidioDetectedEntities = ({ entities }: PresidioDetectedEntitiesProps) = )} {entity.analysis_explanation && (
      - Explanation: + {t("logs.detail.presidio.explanation")} {entity.analysis_explanation}
      )} diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/FormattedToolView.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/FormattedToolView.tsx index 8a56312572c..86355543d85 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/FormattedToolView.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/FormattedToolView.tsx @@ -2,6 +2,7 @@ * Formatted view of tool definition with parameters table and call data */ +import { useTranslation } from "react-i18next"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { ParsedTool, ParameterRow } from "./types"; @@ -10,6 +11,7 @@ interface FormattedToolViewProps { } export function FormattedToolView({ tool }: FormattedToolViewProps) { + const { t } = useTranslation(); // Parse parameters for table display const parameterRows: ParameterRow[] = Object.entries(tool.parameters?.properties || {}).map( ([name, schema]: [string, any]) => ({ @@ -33,13 +35,13 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) { {/* Parameters Table */} {parameterRows.length > 0 && (
      - Parameters + {t("logs.detail.tools.parameters")}
      TypeMethodActionDetail{t("logs.detail.guardrails.matchDetails.type")}{t("logs.detail.guardrails.matchDetails.method")}{t("logs.detail.guardrails.matchDetails.action")}{t("logs.detail.guardrails.matchDetails.detail")}
      - Parameter - Type - Description + {t("logs.detail.tools.columns.parameter")} + {t("logs.detail.tools.columns.type")} + {t("logs.detail.tools.columns.description")} @@ -67,7 +69,7 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) { {/* If tool was called, show the arguments used */} {tool.called && tool.callData && (
      - Called With + {t("logs.detail.tools.calledWith")}
                     {JSON.stringify(tool.callData.arguments, null, 2)}
      diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolExpandedContent.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolExpandedContent.tsx
      index 2a6a6143bb6..6c4d404e324 100644
      --- a/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolExpandedContent.tsx
      +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolExpandedContent.tsx
      @@ -3,6 +3,7 @@
        */
       
       import { useState } from "react";
      +import { useTranslation } from "react-i18next";
       import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
       import { ParsedTool } from "./types";
       import { FormattedToolView } from "./FormattedToolView";
      @@ -15,6 +16,7 @@ interface ToolExpandedContentProps {
       }
       
       export function ToolExpandedContent({ tool }: ToolExpandedContentProps) {
      +  const { t } = useTranslation();
         const [viewMode, setViewMode] = useState("formatted");
       
         return (
      @@ -27,11 +29,11 @@ export function ToolExpandedContent({ tool }: ToolExpandedContentProps) {
                 marginBottom: 12,
               }}
             >
      -        Description
      +        {t("logs.detail.tools.description")}
                setViewMode(value as ViewMode)}>
                 
      -            Formatted
      -            JSON
      +            {t("logs.detail.tools.formatted")}
      +            {t("logs.detail.tools.json")}
                 
               
             
      diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolItem.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolItem.tsx index 26b39579859..779290d4245 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolItem.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolItem.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { ChevronDown, ChevronRight, Wrench } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/cva.config"; import { ParsedTool } from "./types"; @@ -14,6 +15,7 @@ interface ToolItemProps { } export function ToolItem({ tool }: ToolItemProps) { + const { t } = useTranslation(); const [expanded, setExpanded] = useState(false); return ( @@ -34,7 +36,9 @@ export function ToolItem({ tool }: ToolItemProps) {
      - {tool.called ? "called" : "not called"} + + {tool.called ? t("logs.detail.tools.called") : t("logs.detail.tools.notCalled")} + {expanded ? ( ) : ( diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolsSection.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolsSection.tsx index a5ee65ceaa1..07147bf30ab 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolsSection.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/ToolsSection/ToolsSection.tsx @@ -5,6 +5,7 @@ import { useState } from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { LogEntry } from "../../../types"; import { parseToolsFromLog } from "./utils"; @@ -15,6 +16,7 @@ interface ToolsSectionProps { } export function ToolsSection({ log }: ToolsSectionProps) { + const { t } = useTranslation(); const [open, setOpen] = useState(false); const tools = parseToolsFromLog(log); @@ -42,9 +44,9 @@ export function ToolsSection({ log }: ToolsSectionProps) { )}
      -

      Tools

      +

      {t("logs.detail.tools.title")}

      - {totalTools} provided, {calledTools} called + {t("logs.detail.tools.summary", { provided: totalTools, called: calledTools })} • {toolNamePreview} diff --git a/ui/litellm-dashboard/src/components/logs/detail/sections/VectorStoreViewer.tsx b/ui/litellm-dashboard/src/components/logs/detail/sections/VectorStoreViewer.tsx index 6b560acb90c..346930bffc9 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/sections/VectorStoreViewer.tsx +++ b/ui/litellm-dashboard/src/components/logs/detail/sections/VectorStoreViewer.tsx @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; +import { Trans, useTranslation } from "react-i18next"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { getProviderLogoAndName } from "../../../provider_info_helpers"; @@ -32,6 +33,7 @@ interface VectorStoreViewerProps { } export function VectorStoreViewer({ data }: VectorStoreViewerProps) { + const { t } = useTranslation(); const [open, setOpen] = useState(true); const [expandedResults, setExpandedResults] = useState>({}); @@ -46,7 +48,7 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) { const calculateDuration = (start: number, end: number): string => { const duration = (end - start) * 1000; // convert to milliseconds - return `${duration.toFixed(2)}ms`; + return t("logs.detail.vectorStore.durationMs", { value: duration.toFixed(2) }); }; const toggleResult = (index: number, resultIndex: number) => { @@ -66,7 +68,7 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) { ) : ( )} -

      Vector Store Requests

      +

      {t("logs.detail.vectorStore.title")}

      @@ -76,21 +78,27 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
      - Query: + {t("logs.detail.vectorStore.query")} {request.query}
      - Vector Store ID: + {t("logs.detail.vectorStore.vectorStoreId")} {request.vector_store_id}
      - Provider: + {t("logs.detail.vectorStore.provider")} {(() => { const { logo, displayName } = getProviderLogoAndName(request.custom_llm_provider); return ( <> - {logo && {`${displayName}} + {logo && ( + {t("logs.detail.vectorStore.logoAlt", + )} {displayName} ); @@ -100,22 +108,22 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
      - Start Time: + {t("logs.detail.vectorStore.startTime")} {formatTime(request.start_time)}
      - End Time: + {t("logs.detail.vectorStore.endTime")} {formatTime(request.end_time)}
      - Duration: + {t("logs.detail.vectorStore.duration")} {calculateDuration(request.start_time, request.end_time)}
      -

      Search Results

      +

      {t("logs.detail.vectorStore.searchResults")}

      {request.vector_store_search_response.data.map((result, resultIndex) => { const isExpanded = expandedResults[`${index}-${resultIndex}`] || false; @@ -135,9 +143,14 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
      - Result {resultIndex + 1} + + {t("logs.detail.vectorStore.result", { index: resultIndex + 1 })} + - Score: {result.score.toFixed(4)} + {result.score.toFixed(4)} }} + />
      diff --git a/ui/litellm-dashboard/src/components/logs/detail/utils.ts b/ui/litellm-dashboard/src/components/logs/detail/utils.ts index d313d07361c..9d454102ceb 100644 --- a/ui/litellm-dashboard/src/components/logs/detail/utils.ts +++ b/ui/litellm-dashboard/src/components/logs/detail/utils.ts @@ -1,3 +1,5 @@ +import type { TFunction } from "i18next"; + /** * Utility functions for LogDetailsDrawer component. * These functions handle data formatting, validation, and guardrail calculations. @@ -87,10 +89,10 @@ export function calculateTotalMaskedEntities(entries: any[]): number { * @param entries - Array of guardrail entries * @returns Display string for guardrail label */ -export function getGuardrailLabel(entries: any[]): string { +export function getGuardrailLabel(entries: any[], t: TFunction): string { if (entries.length === 0) return "-"; if (entries.length === 1) return entries[0]?.guardrail_name ?? "-"; - return `${entries.length} guardrails`; + return t("logs.detail.guardrailLabel.count", { count: entries.length }); } /** diff --git a/ui/litellm-dashboard/src/components/logs/request/LogsTableToolbar.tsx b/ui/litellm-dashboard/src/components/logs/request/LogsTableToolbar.tsx index 4705a9057f5..6dafed6fa0a 100644 --- a/ui/litellm-dashboard/src/components/logs/request/LogsTableToolbar.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/LogsTableToolbar.tsx @@ -3,6 +3,7 @@ import moment from "moment"; import { CalendarDays } from "lucide-react"; import { type ComponentProps, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -43,13 +44,14 @@ interface LogsTimeRangePickerProps { } export function LogsTimeRangePicker({ value, onValueChange }: LogsTimeRangePickerProps) { + const { t } = useTranslation(); const [open, setOpen] = useState(false); const selectedOption = QUICK_SELECT_OPTIONS.find( (option) => option.value === value.interval.value && option.unit === value.interval.unit, ); const displayLabel = value.isCustomDate - ? getTimeRangeDisplay(true, value.startTime, value.endTime) + ? getTimeRangeDisplay(true, value.startTime, value.endTime, t) : selectedOption?.label; return ( @@ -84,7 +86,7 @@ export function LogsTimeRangePicker({ value, onValueChange }: LogsTimeRangePicke className="w-full justify-start font-normal" onClick={() => onValueChange({ ...value, isCustomDate: !value.isCustomDate })} > - Custom Range + {t("logs.request.toolbar.customRange")}
      @@ -98,7 +100,7 @@ export function LogsTimeRangePicker({ value, onValueChange }: LogsTimeRangePicke value={value.startTime} onChange={(event) => onValueChange({ ...value, startTime: event.target.value })} /> - to + {t("logs.request.toolbar.rangeSeparator")} void; teams: Team[]; }) { + const { t } = useTranslation(); const options = useMemo( () => teams.map((team) => ({ @@ -77,13 +79,13 @@ function TeamFilterField({ ); return ( - + onChange(next ?? undefined)} - placeholder="Search or select a team" - emptyText="No teams found" + placeholder={t("logs.request.filters.teamPlaceholder")} + emptyText={t("logs.request.filters.noTeams")} /> ); @@ -98,6 +100,7 @@ function KeyAliasFilterField({ onChange: (value: string | undefined) => void; teamId: string; }) { + const { t } = useTranslation(); const [search, setSearch] = useState(""); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteKeyAliases( PAGE_SIZE, @@ -117,7 +120,7 @@ function KeyAliasFilterField({ }, [data]); return ( - + ); } function ModelFilterField({ value, onChange }: { value: string; onChange: (value: string | undefined) => void }) { + const { t } = useTranslation(); const [search, setSearch] = useState(""); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteModelInfo( PAGE_SIZE, @@ -149,13 +153,19 @@ function ModelFilterField({ value, onChange }: { value: string; onChange: (value const modelName = model.model_name ?? ""; if (!modelId || seen.has(modelId)) return []; seen.add(modelId); - return [{ label: modelName || modelId, value: modelId, sublabel: `Model ID: ${modelId}` }]; + return [ + { + label: modelName || modelId, + value: modelId, + sublabel: t("logs.request.filters.modelIdSublabel", { modelId }), + }, + ]; }), ); - }, [data]); + }, [data, t]); return ( - + ); @@ -181,6 +191,7 @@ function UserIdFilterField({ onChange: (value: string | undefined) => void; logsWindow: LogsWindow; }) { + const { t } = useTranslation(); const [search, setSearch] = useState(""); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteSpendLogUsers( logsWindow, @@ -200,7 +211,7 @@ function UserIdFilterField({ }, [data]); return ( - + ); @@ -226,6 +237,7 @@ function EndUserFilterField({ onChange: (value: string | undefined) => void; logsWindow: LogsWindow; }) { + const { t } = useTranslation(); const [search, setSearch] = useState(""); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteSpendLogEndUsers( logsWindow, @@ -245,7 +257,7 @@ function EndUserFilterField({ }, [data]); return ( - + ); } function ErrorCodeFilterField({ value, onChange }: { value: string; onChange: (value: string | undefined) => void }) { + const { t } = useTranslation(); const [query, setQuery] = useState(""); const options = useMemo(() => { @@ -273,8 +286,8 @@ function ErrorCodeFilterField({ value, onChange }: { value: string; onChange: (v (option) => option.value === trimmed || option.label.toLowerCase() === lowered, ); if (trimmed === "" || isKnownCode) return matches; - return [...matches, { label: `Use custom code: ${trimmed}`, value: trimmed }]; - }, [query]); + return [...matches, { label: t("logs.request.filters.customErrorCode", { code: trimmed }), value: trimmed }]; + }, [query, t]); const selected = useMemo(() => { if (value === "") return null; @@ -288,7 +301,7 @@ function ErrorCodeFilterField({ value, onChange }: { value: string; onChange: (v }, [options, selected]); return ( - + event.currentTarget.select()} - placeholder="Select or type an error code" + placeholder={t("logs.request.filters.errorCodePlaceholder")} showClear={value !== ""} className="w-full" /> - No error codes found + {t("logs.request.filters.noErrorCodes")} {(item: SearchSelectOption) => ( @@ -330,6 +343,7 @@ interface RequestLogsFiltersProps { } export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsFiltersProps) { + const { t } = useTranslation(); const valueOf = (id: string): string => asString(get(id)); const setter = (id: string) => (next: string | undefined) => set(id, next); @@ -341,7 +355,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF teams={teams} /> - + - + - + - + set(LOG_FILTER_IDS.ERROR_MESSAGE, emptyToUndefined(event.target.value))} - placeholder="Enter error message…" + placeholder={t("logs.request.filters.errorMessagePlaceholder")} /> - + set(LOG_FILTER_IDS.KEY_HASH, emptyToUndefined(event.target.value))} - placeholder="Enter key hash…" + placeholder={t("logs.request.filters.keyHashPlaceholder")} /> - + set(LOG_FILTER_IDS.SESSION_ID, emptyToUndefined(event.target.value))} - placeholder="Enter session ID…" + placeholder={t("logs.request.filters.sessionIdPlaceholder")} /> - + set(LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL, emptyToUndefined(event.target.value))} - placeholder="Enter public model or search tool…" + placeholder={t("logs.request.filters.publicModelOrSearchToolPlaceholder")} /> diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx index e29c48b94c0..8e54c37180f 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx @@ -5,6 +5,7 @@ import { useQuery, type UseQueryOptions } from "@tanstack/react-query"; import type { ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table"; import moment from "moment"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { DEFAULT_PAGE_SIZE_OPTIONS } from "@/components/shared/DataTable"; import { Button } from "@/components/ui/button"; @@ -47,6 +48,7 @@ interface RequestLogsPanelProps { } export default function RequestLogsPanel({ accessToken, token, userRole, userID, isActive }: RequestLogsPanelProps) { + const { t } = useTranslation(); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: PAGE_SIZE }); const [sorting, setSorting] = useState(DEFAULT_LOGS_SORTING); const [columnFilters, setColumnFilters] = useState([]); @@ -290,7 +292,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID, keyData={selectedKeyInfo} teams={allTeams ?? []} onClose={() => setSelectedKeyIdInfoView(null)} - backButtonText="Back to Logs" + backButtonText={t("logs.request.panel.backToLogs")} /> ); } @@ -321,19 +323,23 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID, toolbarChildren={ - + {isLiveTail && pagination.pageIndex === 0 && ( - Refreshing every 15s + {t("logs.request.panel.refreshingEvery15s")} )} } diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsTable.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTable.tsx index d3d6b43e2e3..31b494448c2 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsTable.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTable.tsx @@ -3,6 +3,7 @@ import type { ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table"; import { ScrollText } from "lucide-react"; import { useMemo, useState, type ReactNode } from "react"; +import { useTranslation } from "react-i18next"; import { useUserEmailLookup } from "@/app/(dashboard)/hooks/users/useUsers"; import { DataTable, DataTableFilterDrawer, DataTableToolbar } from "@/components/shared/DataTable"; @@ -49,16 +50,17 @@ const formatFilterValue = (columnId: string, value: unknown): string => { }; function RequestLogsEmptyState({ filtered }: { filtered: boolean }) { + const { t } = useTranslation(); return (
      -
      {filtered ? "No matching requests" : "No requests yet"}
      +
      + {filtered ? t("logs.request.table.emptyFilteredTitle") : t("logs.request.table.emptyTitle")} +
      - {filtered - ? "No requests match your filters for this time range." - : "Requests proxied through LiteLLM will appear here."} + {filtered ? t("logs.request.table.emptyFilteredDescription") : t("logs.request.table.emptyDescription")}
      ); @@ -87,6 +89,7 @@ export function RequestLogsTable({ logsWindow, toolbarChildren, }: RequestLogsTableProps) { + const { t } = useTranslation(); const [filtersOpen, setFiltersOpen] = useState(false); const userIds = useMemo(() => data.flatMap((log) => (log.user ? [log.user] : [])), [data]); @@ -94,8 +97,8 @@ export function RequestLogsTable({ const columns = useMemo(() => { const resolveUserEmail = (userId: string) => emailByUserId?.[userId]; - return getRequestLogsTableColumns({ onKeyHashClick, onSessionClick, resolveUserEmail }); - }, [onKeyHashClick, onSessionClick, emailByUserId]); + return getRequestLogsTableColumns({ onKeyHashClick, onSessionClick, resolveUserEmail }, t); + }, [onKeyHashClick, onSessionClick, emailByUserId, t]); const isFiltered = columnFilters.length > 0 || searchValue !== ""; @@ -119,7 +122,7 @@ export function RequestLogsTable({ isError={isError} errorMessage={errorMessage} onRetry={onRefresh} - loadingMessage="Loading request logs…" + loadingMessage={t("logs.request.table.loading")} noDataMessage={} size="compact" onRowClick={onRowClick} @@ -129,7 +132,7 @@ export function RequestLogsTable({ table={table} searchValue={searchValue} onSearchChange={onSearchChange} - searchPlaceholder="Search logs by ID…" + searchPlaceholder={t("logs.request.table.searchPlaceholder")} onRefresh={onRefresh} isRefreshing={isRefreshing} onOpenFilters={() => setFiltersOpen(true)} @@ -143,8 +146,8 @@ export function RequestLogsTable({ table={table} open={filtersOpen} onOpenChange={setFiltersOpen} - title="Filters" - description="Narrow down request logs" + title={t("logs.request.table.filtersTitle")} + description={t("logs.request.table.filtersDescription")} > {({ get, set }) => } diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.tsx index b3d44361d0a..87eeaa58709 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTableColumns.tsx @@ -1,10 +1,13 @@ "use client"; import type { ColumnDef } from "@tanstack/react-table"; +import type { TFunction } from "i18next"; import { Copy } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { DataTableSortHeader } from "@/components/shared/DataTable"; import { CellTooltip, DateCell, IdCell, MoneyCell, StatusBadge } from "@/components/shared/table_cells"; +import i18n from "@/i18n/i18n"; import { copyToClipboard, getSpendString } from "@/utils/dataUtils"; import { getProviderLogoAndName } from "../../provider_info_helpers"; @@ -32,6 +35,7 @@ const readMcpLogoUrl = (metadata: Record | undefined): string | }; function RequestIdWithCallIdTooltip({ requestId, callId }: { requestId: string; callId: string }) { + const { t } = useTranslation(); return ( {requestId} @@ -39,7 +43,7 @@ function RequestIdWithCallIdTooltip({ requestId, callId }: { requestId: string; x-litellm-call-id: {callId}
      ); }, @@ -359,7 +391,7 @@ export const getRequestLogsTableColumns = ({ { id: "user", accessorKey: "user", - header: "Internal User", + header: t("logs.request.columns.internalUser"), size: 150, enableSorting: false, cell: ({ row }) => ( @@ -375,7 +407,7 @@ export const getRequestLogsTableColumns = ({ { id: "end_user", accessorKey: "end_user", - header: "End User", + header: t("logs.request.columns.endUser"), size: 140, enableSorting: false, cell: ({ row }) => , @@ -383,7 +415,7 @@ export const getRequestLogsTableColumns = ({ { id: "request_tags", accessorKey: "request_tags", - header: "Tags", + header: t("logs.request.columns.tags"), size: 150, enableSorting: false, meta: { skeleton: "chips" }, diff --git a/ui/litellm-dashboard/src/components/logs/request/TypeBadges.tsx b/ui/litellm-dashboard/src/components/logs/request/TypeBadges.tsx index db64bfbbe73..e539b7c606f 100644 --- a/ui/litellm-dashboard/src/components/logs/request/TypeBadges.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/TypeBadges.tsx @@ -3,6 +3,8 @@ * Used in the request logs table and session type column. */ +import { useTranslation } from "react-i18next"; + export const SparkleIcon = ({ size = 12 }: { size?: number }) => ( ( ); -export const AgentBadge = ({ count }: { count?: number }) => ( - - - {count != null ? count : "Agent"} - -); +export const AgentBadge = ({ count }: { count?: number }) => { + const { t } = useTranslation(); + return ( + + + {count != null ? count : t("logs.request.badges.agent")} + + ); +}; -export const BatchBadge = () => ( - - - Batch - -); +export const BatchBadge = () => { + const { t } = useTranslation(); + return ( + + + {t("logs.request.badges.batch")} + + ); +}; diff --git a/ui/litellm-dashboard/src/components/logs/request/timeRange.ts b/ui/litellm-dashboard/src/components/logs/request/timeRange.ts index 556908f4b5e..053512867f2 100644 --- a/ui/litellm-dashboard/src/components/logs/request/timeRange.ts +++ b/ui/litellm-dashboard/src/components/logs/request/timeRange.ts @@ -1,7 +1,15 @@ +import type { TFunction } from "i18next"; import moment from "moment"; +import i18n from "@/i18n/i18n"; + // Add this function to format the time range display -export const getTimeRangeDisplay = (isCustomDate: boolean, startTime: string, endTime: string) => { +export const getTimeRangeDisplay = ( + isCustomDate: boolean, + startTime: string, + endTime: string, + t: TFunction = i18n.t.bind(i18n), +) => { if (isCustomDate) { return `${moment(startTime).format("MMM D, h:mm A")} - ${moment(endTime).format("MMM D, h:mm A")}`; } @@ -11,13 +19,13 @@ export const getTimeRangeDisplay = (isCustomDate: boolean, startTime: string, en const diffMinutes = now.diff(start, "minutes"); // Use exact ranges to prevent drift - if (diffMinutes >= 0 && diffMinutes < 2) return "Last 1 Minute"; - if (diffMinutes >= 2 && diffMinutes < 16) return "Last 15 Minutes"; - if (diffMinutes >= 16 && diffMinutes < 61) return "Last Hour"; + if (diffMinutes >= 0 && diffMinutes < 2) return t("logs.request.timeRange.lastMinute"); + if (diffMinutes >= 2 && diffMinutes < 16) return t("logs.request.timeRange.last15Minutes"); + if (diffMinutes >= 16 && diffMinutes < 61) return t("logs.request.timeRange.lastHour"); const diffHours = now.diff(start, "hours"); - if (diffHours >= 1 && diffHours < 5) return "Last 4 Hours"; - if (diffHours >= 5 && diffHours < 25) return "Last 24 Hours"; - if (diffHours >= 25 && diffHours < 169) return "Last 7 Days"; + if (diffHours >= 1 && diffHours < 5) return t("logs.request.timeRange.last4Hours"); + if (diffHours >= 5 && diffHours < 25) return t("logs.request.timeRange.last24Hours"); + if (diffHours >= 25 && diffHours < 169) return t("logs.request.timeRange.last7Days"); return `${start.format("MMM D")} - ${now.format("MMM D")}`; }; diff --git a/ui/litellm-dashboard/src/components/per_user_usage.tsx b/ui/litellm-dashboard/src/components/per_user_usage.tsx index a7c6a5cf504..14c29d80359 100644 --- a/ui/litellm-dashboard/src/components/per_user_usage.tsx +++ b/ui/litellm-dashboard/src/components/per_user_usage.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; import type { ColumnDef, OnChangeFn, PaginationState } from "@tanstack/react-table"; import { BarChart } from "@/components/shared/charts"; import { DataTable } from "@/components/shared/DataTable"; @@ -31,6 +32,7 @@ interface PerUserUsageProps { } const PerUserUsage: React.FC = ({ accessToken, selectedTags, formatAbbreviatedNumber }) => { + const { t } = useTranslation(); // Maximum number of user agent categories to show in charts to prevent color palette overflow const MAX_USER_AGENTS = 8; const [perUserData, setPerUserData] = useState({ @@ -79,40 +81,40 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, const columns: ColumnDef[] = [ { - header: "User ID", + header: t("usage.perUser.columns.userId"), accessorKey: "user_id", cell: ({ row }) => {row.original.user_id}, }, { - header: "User Email", + header: t("usage.perUser.columns.userEmail"), accessorKey: "user_email", - cell: ({ row }) => row.original.user_email || "N/A", + cell: ({ row }) => row.original.user_email || t("usage.perUser.notAvailable"), }, { - header: "User Agent", + header: t("usage.perUser.columns.userAgent"), accessorKey: "user_agent", - cell: ({ row }) => row.original.user_agent || "Unknown", + cell: ({ row }) => row.original.user_agent || t("usage.perUser.unknownAgent"), }, { - header: "Success Generations", + header: t("usage.perUser.columns.successGenerations"), accessorKey: "successful_requests", meta: { numeric: true }, cell: ({ row }) => formatAbbreviatedNumber(row.original.successful_requests), }, { - header: "Total Tokens", + header: t("usage.perUser.columns.totalTokens"), accessorKey: "total_tokens", meta: { numeric: true }, cell: ({ row }) => formatAbbreviatedNumber(row.original.total_tokens), }, { - header: "Failed Requests", + header: t("usage.perUser.columns.failedRequests"), accessorKey: "failed_requests", meta: { numeric: true }, cell: ({ row }) => formatAbbreviatedNumber(row.original.failed_requests), }, { - header: "Total Cost", + header: t("usage.perUser.columns.totalCost"), accessorKey: "spend", meta: { numeric: true }, cell: ({ row }) => `$${formatAbbreviatedNumber(row.original.spend, 4)}`, @@ -121,16 +123,16 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, return (
      -

      Per User Usage

      -

      Individual developer usage metrics

      +

      {t("usage.perUser.title")}

      +

      {t("usage.perUser.subtitle")}

      - User Details + {t("usage.perUser.tabs.details")} - Usage Distribution + {t("usage.perUser.tabs.distribution")} @@ -144,7 +146,7 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, pagination={pagination} onPaginationChange={handlePaginationChange} rowCount={perUserData.total_count} - noDataMessage="No per-user usage data" + noDataMessage={t("usage.perUser.noData")} size="compact" /> @@ -152,8 +154,8 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, {/* Tab 2: Usage Distribution Histogram */}
      -

      User Usage Distribution

      -

      Number of users by successful request frequency

      +

      {t("usage.perUser.distribution.title")}

      +

      {t("usage.perUser.distribution.subtitle")}

      = ({ accessToken, selectedTags, // Get top user agents by frequency first const userAgentCounts = new Map(); perUserData.results.forEach((item: PerUserMetrics) => { - const agent = item.user_agent || "Unknown"; + const agent = item.user_agent || t("usage.perUser.unknownAgent"); userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1); }); @@ -172,18 +174,36 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, // Categorize users by successful request count and user agent const categories = { - "1-9 requests": { range: [1, 9], agents: {} as Record }, - "10-99 requests": { range: [10, 99], agents: {} as Record }, - "100-999 requests": { range: [100, 999], agents: {} as Record }, - "1K-9.9K requests": { range: [1000, 9999], agents: {} as Record }, - "10K-99.9K requests": { range: [10000, 99999], agents: {} as Record }, - "100K+ requests": { range: [100000, Infinity], agents: {} as Record }, + [t("usage.perUser.distribution.buckets.range1To9")]: { + range: [1, 9], + agents: {} as Record, + }, + [t("usage.perUser.distribution.buckets.range10To99")]: { + range: [10, 99], + agents: {} as Record, + }, + [t("usage.perUser.distribution.buckets.range100To999")]: { + range: [100, 999], + agents: {} as Record, + }, + [t("usage.perUser.distribution.buckets.range1kTo9k")]: { + range: [1000, 9999], + agents: {} as Record, + }, + [t("usage.perUser.distribution.buckets.range10kTo99k")]: { + range: [10000, 99999], + agents: {} as Record, + }, + [t("usage.perUser.distribution.buckets.range100kPlus")]: { + range: [100000, Infinity], + agents: {} as Record, + }, }; // Count users in each category by user agent (only for top user agents) perUserData.results.forEach((item: PerUserMetrics) => { const successCount = item.successful_requests; - const userAgent = item.user_agent || "Unknown"; + const userAgent = item.user_agent || t("usage.perUser.unknownAgent"); // Only process if this is one of the top user agents if (topUserAgents.includes(userAgent)) { @@ -215,7 +235,7 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, // Count user agents by frequency and get top ones const userAgentCounts = new Map(); perUserData.results.forEach((item: PerUserMetrics) => { - const agent = item.user_agent || "Unknown"; + const agent = item.user_agent || t("usage.perUser.unknownAgent"); userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1); }); @@ -226,7 +246,7 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, .map(([agent]) => agent); })()} colors={["blue", "green", "orange", "red", "purple", "yellow", "pink", "indigo"]} - valueFormatter={(value: number) => `${value} users`} + valueFormatter={(value: number) => t("usage.perUser.distribution.userCount", { count: value })} yAxisWidth={80} showLegend={true} stack={true} diff --git a/ui/litellm-dashboard/src/components/policies/PolicySelector.tsx b/ui/litellm-dashboard/src/components/policies/PolicySelector.tsx index 135aa0bc49f..d25a79090ad 100644 --- a/ui/litellm-dashboard/src/components/policies/PolicySelector.tsx +++ b/ui/litellm-dashboard/src/components/policies/PolicySelector.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import useCan from "@/app/(dashboard)/hooks/useCan"; import { Policy } from "./types"; import { getPoliciesList } from "../networking"; @@ -52,6 +53,7 @@ const PolicySelector: React.FC = ({ disabled, onPoliciesLoaded, }) => { + const { t } = useTranslation(); const canViewPolicies = useCan("viewPolicies"); const [policies, setPolicies] = useState([]); const [loading, setLoading] = useState(false); @@ -89,9 +91,7 @@ const PolicySelector: React.FC = ({
      = ({ isDateChanging = false }) => ( -
      -
      - -
      - - {isDateChanging ? "Processing date selection..." : "Loading chart data..."} - - - {isDateChanging ? "This will only take a moment" : "Fetching your data"} - +export const ChartLoader: React.FC = ({ isDateChanging = false }) => { + const { t } = useTranslation(); + return ( +
      +
      + +
      + + {isDateChanging ? t("usage.chartLoader.processingDate") : t("usage.chartLoader.loading")} + + + {isDateChanging ? t("usage.chartLoader.processingDateHint") : t("usage.chartLoader.loadingHint")} + +
      -
      -); + ); +}; export default ChartLoader; diff --git a/ui/litellm-dashboard/src/components/user_agent_activity.tsx b/ui/litellm-dashboard/src/components/user_agent_activity.tsx index 37b320e2b5c..27e9f7373dc 100644 --- a/ui/litellm-dashboard/src/components/user_agent_activity.tsx +++ b/ui/litellm-dashboard/src/components/user_agent_activity.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { Bot, Users } from "lucide-react"; import { Combobox, @@ -92,11 +93,12 @@ interface ActiveUserChart { } function ActiveUsersChart({ chart }: { chart: ActiveUserChart }) { + const { t } = useTranslation(); if (chart.loading) return ; if (chart.tags.length === 0) { return (
      - No active users in this period + {t("usage.userAgentActivity.noActiveUsers")}
      ); } @@ -136,6 +138,7 @@ const UserAgentActivity: React.FC = ({ selectedTags: selectedTagsProp, onSelectedTagsChange, }) => { + const { t } = useTranslation(); const anchor = useComboboxAnchor(); // Maximum number of categories to show in charts to prevent color palette overflow const MAX_CATEGORIES = 10; @@ -363,7 +366,7 @@ const UserAgentActivity: React.FC = ({ // Generate all 7 weeks (Week 1 through Week 7) for (let weekNum = 1; weekNum <= 7; weekNum++) { - const weekEntry: any = { week: `Week ${weekNum}` }; + const weekEntry: any = { week: t("usage.userAgentActivity.weekLabel", { number: weekNum }) }; // Initialize all user agents to 0 allWauTags.forEach((tag) => { @@ -380,7 +383,7 @@ const UserAgentActivity: React.FC = ({ // Extract week number from the date field (e.g., "Week 1 (Jul 27)" -> "Week 1") const weekMatch = item.date.match(/Week (\d+)/); if (weekMatch) { - const weekLabel = `Week ${weekMatch[1]}`; + const weekLabel = t("usage.userAgentActivity.weekLabel", { number: weekMatch[1] }); const weekEntry = chartData.find((d) => d.week === weekLabel); if (weekEntry) { weekEntry[userAgent] = item.active_users; @@ -399,7 +402,7 @@ const UserAgentActivity: React.FC = ({ // Generate all 7 months (Month 1 through Month 7) for (let monthNum = 1; monthNum <= 7; monthNum++) { - const monthEntry: any = { month: `Month ${monthNum}` }; + const monthEntry: any = { month: t("usage.userAgentActivity.monthLabel", { number: monthNum }) }; // Initialize all user agents to 0 allMauTags.forEach((tag) => { @@ -416,7 +419,7 @@ const UserAgentActivity: React.FC = ({ // Extract month number from the date field (e.g., "Month 1 (Jul)" -> "Month 1") const monthMatch = item.date.match(/Month (\d+)/); if (monthMatch) { - const monthLabel = `Month ${monthMatch[1]}`; + const monthLabel = t("usage.userAgentActivity.monthLabel", { number: monthMatch[1] }); const monthEntry = chartData.find((d) => d.month === monthLabel); if (monthEntry) { monthEntry[userAgent] = item.active_users; @@ -432,7 +435,7 @@ const UserAgentActivity: React.FC = ({ const activeUserCharts: readonly ActiveUserChart[] = [ { value: "dau", - title: "Daily Active Users - Last 7 Days", + title: t("usage.userAgentActivity.charts.dau"), loading: dauLoading, data: dailyChartData, index: "date", @@ -440,7 +443,7 @@ const UserAgentActivity: React.FC = ({ }, { value: "wau", - title: "Weekly Active Users - Last 7 Weeks", + title: t("usage.userAgentActivity.charts.wau"), loading: wauLoading, data: weeklyChartData, index: "week", @@ -448,7 +451,7 @@ const UserAgentActivity: React.FC = ({ }, { value: "mau", - title: "Monthly Active Users - Last 7 Months", + title: t("usage.userAgentActivity.charts.mau"), loading: mauLoading, data: monthlyChartData, index: "month", @@ -462,11 +465,13 @@ const UserAgentActivity: React.FC = ({
      - + } className="min-w-0 flex-1" aria-busy={tagsLoading}> @@ -478,11 +483,14 @@ const UserAgentActivity: React.FC = ({ )) } - - {selectedTags.length > 0 && } + + {selectedTags.length > 0 && } - No user agents found + {t("usage.userAgentActivity.filter.empty")} {(tag: string) => { const userAgent = extractUserAgent(tag); @@ -528,7 +536,11 @@ const UserAgentActivity: React.FC = ({ {Array.from({ length: Math.max(0, SUMMARY_SLOTS - summaryRows.length) }).map((_, index) => ( No Data} + name={ +

      + {t("usage.userAgentActivity.summary.noData")} +

      + } successRequests="-" totalTokens="-" totalCost="-" @@ -541,14 +553,32 @@ const UserAgentActivity: React.FC = ({ {/* DAU/WAU/MAU vs Per User Usage. Every view stays mounted so switching does not reset its state. */} {activityView === "active-users" && ( - + )} - + } > @@ -583,10 +613,11 @@ function SummaryCell({ totalTokens: string; totalCost: string; }) { + const { t } = useTranslation(); const rows = [ - { label: "Success Requests", value: successRequests }, - { label: "Total Tokens", value: totalTokens }, - { label: "Total Cost", value: totalCost }, + { label: t("usage.userAgentActivity.summary.successRequests"), value: successRequests }, + { label: t("usage.userAgentActivity.summary.totalTokens"), value: totalTokens }, + { label: t("usage.userAgentActivity.summary.totalCost"), value: totalCost }, ]; return (
      diff --git a/ui/litellm-dashboard/src/i18n/locales/en.json b/ui/litellm-dashboard/src/i18n/locales/en.json index 442d50189ea..357c65436d3 100644 --- a/ui/litellm-dashboard/src/i18n/locales/en.json +++ b/ui/litellm-dashboard/src/i18n/locales/en.json @@ -9660,6 +9660,1731 @@ "empty": "No MCP servers found" } }, + "logs": { + "common": { + "podRouting": { + "roundRobin": "Round robin", + "sessionAffinity": "Session affinity", + "sessionAffinityRetry": "Session affinity (retry)" + } + }, + "detail": { + "drawer": { + "title": "Request details", + "titleWithId": "Request {{id}} details", + "session": "Session", + "trace": "Trace", + "copyTraceId": "Copy trace id", + "requestCount": "{{count}} req", + "cachedCount": "{{hits}}/{{total}} cached", + "truncated": "Showing most recent {{shown}} of {{total}}", + "sortDuration": "Duration", + "sortStartTime": "Start time", + "tokens": "{{count}} tok", + "failure": "Failure", + "success": "Success" + }, + "header": { + "backTo": "Back to {{label}}", + "requestId": "Request ID", + "provider": "Provider", + "copied": "Copied!", + "copyLabel": "Copy {{label}}", + "escToClose": "ESC to close", + "environment": "Env: {{environment}}" + }, + "sidebarToggle": { + "expand": "Expand trace sidebar", + "collapse": "Collapse trace sidebar" + }, + "classifyTag": { + "title": "Tier classification call made by the auto-router, not a request the caller sent", + "label": "Classify" + }, + "content": { + "errorFallback": "An error occurred", + "requestFailed": "Request Failed", + "requestDetails": "Request Details", + "model": "Model", + "provider": "Provider", + "callType": "Call Type", + "user": "User", + "modelId": "Model ID", + "apiBase": "API Base", + "pod": "Pod", + "podCount": "{{count}} pods", + "ipAddress": "IP Address", + "credential": "Credential", + "guardrail": "Guardrail", + "loadingDetails": "Loading request & response data...", + "copied": "Copied!", + "errorCode": "Error Code:", + "errorMessage": "Message:", + "tags": "Tags", + "maskedCount": "{{count}} masked" + }, + "metrics": { + "title": "Metrics", + "inputTokens": "Input Tokens", + "outputTokens": "Output Tokens", + "tokens": "Tokens", + "reasoningTokens": "Reasoning Tokens", + "cost": "Cost", + "duration": "Duration", + "timeToFirstToken": "Time to First Token", + "responseCache": "Response Cache", + "responseCacheTooltip": "Whether this request was served from LiteLLM's response cache (e.g. Redis / in-memory), skipping the LLM provider call entirely. This is separate from provider prompt caching; a Miss here does not mean prompt caching failed.", + "hit": "Hit", + "miss": "Miss", + "cacheKey": "Cache Key", + "cacheKeyTooltip": "The key LiteLLM computed for this request in the response cache. Requests with the same cache key share a cached response; a different key means the request content did not match any cached entry.", + "promptCacheReadTokens": "Prompt Cache Read Tokens", + "promptCacheCreationTokens": "Prompt Cache Creation Tokens", + "overhead": "LiteLLM Overhead", + "overheadValue": "{{value}} ms", + "retries": "Retries", + "noRetries": "None", + "startTime": "Start Time", + "endTime": "End Time", + "infoLabel": "{{label}} info", + "docs": "Docs" + }, + "batch": { + "title": "Batch Results", + "batchId": "Batch ID", + "successful": "Successful Requests", + "failed": "Failed Requests", + "models": "Models" + }, + "requestResponse": { + "title": "Request & Response", + "pretty": "Pretty", + "json": "JSON", + "request": "Request", + "response": "Response", + "copyJson": "Copy JSON", + "responseUnavailable": "Response data not available" + }, + "guardrailJumpLink": { + "evaluated_one": "{{count}} guardrail evaluated", + "evaluated_other": "{{count}} guardrails evaluated", + "notRun": ", {{count}} not run" + }, + "guardrailLabel": { + "count": "{{count}} guardrails" + }, + "metadata": { + "title": "Metadata", + "copy": "Copy Metadata" + }, + "routing": { + "title": "Routing", + "tier": "Tier", + "requestType": "Request type", + "decidedBy": "Decided by", + "classifierModel": "Classifier model", + "confidence": "Confidence", + "probabilities": "Probabilities", + "classifierCost": "Classifier cost", + "score": "Score", + "routedTo": "Routed to", + "escalated": "Escalated", + "signals": "Signals", + "calibration": "Calibration", + "routerType": { + "complexity": "Auto-Router v2", + "adaptive": "Adaptive router", + "quality": "Quality router" + }, + "scoreRange": { + "below": "below {{value}}", + "between": "{{from}} to {{to}}", + "atOrAbove": "at or above {{value}}" + }, + "cause": { + "planModeExitTool": "Plan-mode floor (exit_plan_mode tool)", + "planModeKeyword": "Plan-mode floor: \"{{keyword}}\"", + "planMode": "Plan-mode floor", + "housekeepingKeyword": "Client housekeeping call: \"{{keyword}}\"", + "housekeeping": "Client housekeeping call, classifier skipped", + "simpleMediumBoundary": "the Simple to Medium boundary", + "reasoningOverride": "Heuristic, {{tier}} override (2 or more reasoning markers, score of at least {{floor}})", + "heuristicScorer": "Heuristic scorer", + "heuristicV2": "Heuristic v2", + "capabilityClassifier": "Capability", + "capabilityClassifierFallback": "Capable tier, Capability classifier failed", + "llmV2Classifier": "FUSE v2", + "llmV2Fallback": "Capable tier, FUSE v2 classifier failed", + "heuristicFirstShortCircuit": "Heuristic scorer, classifier skipped", + "hybridShortCircuit": "Heuristic scorer, score clear of every boundary", + "classifierPlugin": "Custom classifier plugin", + "semanticKeywordMatch": "Semantic keyword match", + "sessionAffinityPin": "Pinned to session", + "sessionAffinityEscalation": "Escalated from session pin", + "userTurnContinuation": "Continuation turn, classifier skipped", + "modalityEscalation": "Escalated for image input", + "modalityPinOverride": "Overrode session pin for image input", + "qualityTier": "Quality tier mapping", + "bandit": "Adaptive bandit", + "defaultFallback": "Default model, no route matched", + "classifierFallback": "Fallback tier, classifier failed", + "defaultModelFallback": "Default model, classifier failed", + "llmClassifierModel": "LLM classifier ({{model}})", + "llmClassifier": "LLM classifier", + "ossClassifier": "OSS classifier", + "keywordMatchKeyword": "Keyword match: \"{{keyword}}\"", + "keywordMatch": "Keyword match", + "unknown": "Unknown" + }, + "escalation": { + "yesKeyword": "Yes, keyword \"{{keyword}}\"", + "yes": "Yes", + "requestedKeyword": "Requested via \"{{keyword}}\"; already at the highest tier", + "requested": "Requested; already at the highest tier" + }, + "capability": { + "title": "Capability estimates", + "subtitle": "Efficient model solve chance", + "raw": "Raw", + "calibrated": "Calibrated", + "threshold": "Threshold", + "boundary": "Boundary", + "rule": "Rule" + }, + "fuseV2": { + "title": "FUSE v2 estimates", + "efficientRaw": "Efficient (raw)", + "capableRaw": "Capable (raw)", + "efficientCalibrated": "Efficient (calibrated)", + "capableCalibrated": "Capable (calibrated)", + "appliedGap": "Applied gap", + "allowedGap": "Allowed gap", + "percentagePoints": " percentage points" + }, + "heuristicV2": { + "title": "Heuristic v2 estimates", + "successByTier": "Success by tier", + "threshold": "Threshold", + "predictedTier": "Predicted tier" + } + }, + "guardrails": { + "title": "Guardrails & Policy Compliance", + "evaluated_one": "{{count}} guardrail evaluated", + "evaluated_other": "{{count}} guardrails evaluated", + "passedCount": "{{count}} Passed", + "flaggedCount": "{{count}} Flagged", + "notRunCount": "{{count}} Not run", + "totalOverhead": "Total: {{ms}}ms overhead", + "exportLog": "Export Compliance Log", + "evaluationDetails": "Evaluation Details", + "durationMs": "{{ms}}ms", + "rawResponse": "Raw Guardrail Response", + "outcome": { + "passed": "PASSED", + "flagged": "FLAGGED", + "failed": "FAILED", + "notRun": "NOT RUN" + }, + "matchDetails": { + "title": "Match Details ({{count}})", + "type": "Type", + "method": "Method", + "action": "Action", + "detail": "Detail" + }, + "lifecycle": { + "title": "Request Lifecycle", + "requestReceived": "Request received", + "preCall": "Pre-call guardrail: {{name}}", + "llmCall": "LLM call", + "duringCall": "During-call guardrail: {{name}}", + "postCall": "Post-call guardrail: {{name}}", + "responseReturned": "Response returned", + "offset": "T+{{ms}}ms" + }, + "card": { + "matchedOfChecked": "{{matched}}/{{checked}} matched", + "matched": "{{count}} matched", + "confidence": "{{value}}% conf", + "risk": "Risk {{score}}/10", + "riskTooltip": "Risk score: {{score}}/10", + "textRecords_one": "{{value}} text record", + "textRecords_other": "{{value}} text records", + "costEstimated": "Estimated guardrail cost (reported only; not counted against spend or budgets)", + "cost": "Guardrail cost", + "maskedEntities": "Masked Entities" + }, + "classification": { + "title": "Classification", + "category": "Category:", + "reference": "Reference:", + "confidence": "Confidence:", + "reason": "Reason:" + } + }, + "bedrock": { + "detected": "detected", + "notDetected": "not detected", + "textGuarded": "text guarded {{guarded}}/{{total}}", + "imagesGuarded": "images guarded {{guarded}}/{{total}}", + "action": "Action:", + "actionReason": "Action Reason:", + "blockedResponse": "Blocked Response:", + "coverage": "Coverage:", + "usage": "Usage:", + "outputs": "Outputs", + "nonTextOutput": "(non-text output)", + "assessment": "Assessment #{{index}}", + "latencyMs": "{{value}} ms", + "wordPolicy": "Word Policy", + "customWords": "Custom Words", + "managedWordLists": "Managed Word Lists", + "contentPolicy": "Content Policy", + "columns": { + "type": "Type", + "action": "Action", + "detected": "Detected", + "strength": "Strength", + "confidence": "Confidence", + "score": "Score", + "threshold": "Threshold" + }, + "contextualGrounding": "Contextual Grounding", + "sensitiveInformation": "Sensitive Information", + "piiEntities": "PII Entities", + "customRegexes": "Custom Regexes", + "topicPolicy": "Topic Policy", + "invocationMetrics": "Invocation Metrics", + "latency": "Latency (ms)", + "textCoverage": "text {{guarded}}/{{total}}", + "imagesCoverage": "images {{guarded}}/{{total}}", + "automatedReasoningFindings": "Automated Reasoning Findings", + "rawResponse": "Raw Bedrock Guardrail Response" + }, + "contentFilter": { + "error": "Error", + "noDetections": "No detections found", + "totalDetections": "Total Detections:", + "actions": "Actions:", + "blockedCount": "{{count}} blocked", + "maskedCount": "{{count}} masked", + "passed": "passed", + "byType": "By Type:", + "patternsCount": "{{count}} patterns", + "keywordsCount": "{{count}} keywords", + "categoriesCount": "{{count}} categories", + "patternsMatched": "Patterns Matched", + "pattern": "Pattern:", + "action": "Action:", + "blockedWords": "Blocked Words Detected", + "keyword": "Keyword:", + "description": "Description:", + "categoryKeywords": "Category Keywords Detected", + "category": "Category:", + "severity": "Severity:", + "rawData": "Raw Detection Data" + }, + "presidio": { + "title": "Detected Entities ({{count}})", + "score": "Score: {{value}}", + "position": "Position: {{start}}-{{end}}", + "entityType": "Entity Type:", + "positionLabel": "Position:", + "characters": "Characters {{start}}-{{end}}", + "confidence": "Confidence:", + "recognizer": "Recognizer:", + "identifier": "Identifier:", + "explanation": "Explanation:" + }, + "compliance": { + "title": "Regulatory Compliance", + "euAiAct": "EU AI Act", + "gdpr": "GDPR", + "compliant": "COMPLIANT", + "nonCompliant": "NON-COMPLIANT", + "unavailable": "UNAVAILABLE", + "checking": "Checking compliance...", + "euAiActFailed": "Failed to check EU AI Act compliance", + "gdprFailed": "Failed to check GDPR compliance" + }, + "eval": { + "title": "LLM Judge Results", + "passed": "PASSED", + "failed": "FAILED", + "threshold": " (threshold: {{value}})", + "scoreTooltip": "Weighted average of all criterion scores. Each criterion has a weight (%) set when the eval was created — higher-weight criteria count more toward the final score.", + "judge": "Judge: {{model}}", + "iteration": "Iter: {{value}}", + "judgeError": "Judge error: {{error}}", + "columns": { + "criterion": "Criterion", + "weight": "Weight", + "score": "Score", + "weighted": "Weighted", + "weightedTooltip": "Score × Weight — how much each criterion contributes to the final score", + "comment": "Comment" + }, + "total": "Total", + "noBreakdown": "Score: {{score}} — no per-criterion breakdown available." + }, + "costBreakdown": { + "title": "Cost Breakdown", + "total": "Total:", + "cached": " (Cached)", + "inputCost": "Input Cost:", + "tokens": "({{value}} tokens)", + "cacheReadCost": "Prompt Cache Read Cost:", + "cacheWriteCost": "Prompt Cache Write Cost:", + "promptTokens": "({{value}} prompt tokens)", + "outputCost": "Output Cost:", + "completionTokens": "({{value}} completion tokens)", + "toolUsageCost": "Tool Usage Cost:", + "originalCost": "Original LLM Cost:", + "discountPercent": "Discount ({{percent}}):", + "discountAmount": "Discount Amount:", + "marginPercent": "Margin ({{percent}}):", + "margin": "Margin:", + "finalCost": "Final Calculated Cost:" + }, + "vectorStore": { + "title": "Vector Store Requests", + "query": "Query:", + "vectorStoreId": "Vector Store ID:", + "provider": "Provider:", + "logoAlt": "{{name}} logo", + "startTime": "Start Time:", + "endTime": "End Time:", + "duration": "Duration:", + "durationMs": "{{value}}ms", + "searchResults": "Search Results", + "result": "Result {{index}}", + "score": "Score: " + }, + "configInfo": { + "title": "Request/Response Data Not Available", + "description": "To view request and response details, enable prompt storage in your LiteLLM configuration by adding the following to your proxy_config.yaml file, or toggle the setting in Admin Settings → Logging Settings.", + "note": "Note: This will only affect new requests after the configuration change." + }, + "tools": { + "title": "Tools", + "summary": "{{provided}} provided, {{called}} called", + "called": "called", + "notCalled": "not called", + "description": "Description", + "formatted": "Formatted", + "json": "JSON", + "parameters": "Parameters", + "columns": { + "parameter": "Parameter", + "type": "Type", + "description": "Description" + }, + "calledWith": "Called With" + }, + "realtime": { + "noEvents": "No recognized realtime events found", + "session": "Session", + "turnCount_one": "{{count}} turn", + "turnCount_other": "{{count}} turns", + "config": { + "model": "Model", + "voice": "Voice", + "temperature": "Temperature", + "maxOutputTokens": "Max Output Tokens", + "inputAudioFormat": "Input Audio Format", + "outputAudioFormat": "Output Audio Format", + "turnDetection": "Turn Detection", + "tools": "Tools", + "toolCount": "{{count}} tool(s)" + }, + "instructions": "Instructions", + "turnTokens": "{{input}} in / {{output}} out tokens", + "conversation": "conv: {{id}}...", + "input": "Input", + "output": "Output", + "tokenBreakdown": "{{label}} Token Breakdown" + }, + "sectionHeader": { + "input": "Input", + "output": "Output", + "tokens": "Tokens: {{value}}", + "cost": "Cost: ${{value}}", + "turns": "Turns: {{count}}", + "copyInput": "Copy input", + "copyOutput": "Copy output", + "copy": "Copy" + }, + "inputCard": { + "copied": "Input copied" + }, + "outputCard": { + "copied": "Output copied", + "noResponse": "No response data available" + }, + "historyTree": { + "label_one": "HISTORY ({{count}} message)", + "label_other": "HISTORY ({{count}} messages)" + }, + "collapsibleMessage": { + "charCount": "({{value}} chars)" + }, + "toolCall": { + "badge": "function" + }, + "tokenFlow": { + "summary": "{{total}} ({{prompt}} prompt tokens + {{completion}} completion tokens)" + }, + "truncatedValue": { + "copy": "Copy" + }, + "jsonViewer": { + "noData": "No data" + }, + "classifierAudit": { + "input": { + "title": "Classifier input", + "description": "Provider request payload. A cached call or disabled message logging may have no capture." + }, + "originating": { + "title": "Originating request, credentials masked", + "description": "Comparison only. This source request was not appended to the classifier input." + }, + "response": { + "title": "Classifier response", + "description": "The returned verdict and any explanation supplied by the classifier. Later routing rules may change the tier." + }, + "copy": "Copy {{title}}", + "truncated": "This stored copy is truncated. The complete payload is unavailable from the configured log storage.", + "notCaptured": "Not captured or message logging disabled" + } + }, + "page": { + "loading": "Loading", + "tabs": { + "requestLogs": "Request Logs", + "auditLogs": "Audit Logs", + "deletedKeys": "Deleted Keys", + "deletedTeams": "Deleted Teams" + } + }, + "request": { + "panel": { + "backToLogs": "Back to Logs", + "liveTail": "Live Tail", + "refreshingEvery15s": "Refreshing every 15s", + "hideHealthChecks": "Hide Health Checks", + "resetFilters": "Reset Filters" + }, + "toolbar": { + "customRange": "Custom Range", + "rangeSeparator": "to" + }, + "timeRange": { + "lastMinute": "Last 1 Minute", + "last15Minutes": "Last 15 Minutes", + "lastHour": "Last Hour", + "last4Hours": "Last 4 Hours", + "last24Hours": "Last 24 Hours", + "last7Days": "Last 7 Days" + }, + "table": { + "emptyTitle": "No requests yet", + "emptyFilteredTitle": "No matching requests", + "emptyDescription": "Requests proxied through LiteLLM will appear here.", + "emptyFilteredDescription": "No requests match your filters for this time range.", + "loading": "Loading request logs…", + "searchPlaceholder": "Search logs by ID…", + "filtersTitle": "Filters", + "filtersDescription": "Narrow down request logs" + }, + "filters": { + "teamId": "Team ID", + "teamPlaceholder": "Search or select a team", + "noTeams": "No teams found", + "keyAlias": "Key Alias", + "keyAliasPlaceholder": "Search a key alias", + "noKeyAliases": "No key aliases found", + "modelIdSublabel": "Model ID: {{modelId}}", + "model": "Model", + "modelPlaceholder": "Search a model", + "noModels": "No models found", + "userId": "User ID", + "userPlaceholder": "Search an internal user", + "noUsers": "No users found", + "endUser": "End User", + "endUserPlaceholder": "Search an end user", + "noEndUsers": "No end users in this time range", + "customErrorCode": "Use custom code: {{code}}", + "errorCode": "Error Code", + "errorCodePlaceholder": "Select or type an error code", + "noErrorCodes": "No error codes found", + "spanType": "Span Type", + "allTypes": "All Types", + "status": "Status", + "allStatuses": "All Statuses", + "cache": "Cache", + "allRequests": "All Requests", + "credential": "Credential", + "allCredentials": "All Credentials", + "errorMessage": "Error Message", + "errorMessagePlaceholder": "Enter error message…", + "keyHash": "Key Hash", + "keyHashPlaceholder": "Enter key hash…", + "sessionId": "Session ID", + "sessionIdPlaceholder": "Enter session ID…", + "publicModelOrSearchTool": "Public model / search tool", + "publicModelOrSearchToolPlaceholder": "Enter public model or search tool…" + }, + "columns": { + "copyCallId": "Copy x-litellm-call-id", + "time": "Time", + "type": "Type", + "sessionLlmCount_one": "{{count}} LLM", + "sessionLlmCount_other": "{{count}} LLM", + "sessionAgentCount_one": "{{count}} Agent", + "sessionAgentCount_other": "{{count}} Agent", + "sessionMcpCount_one": "{{count}} MCP", + "sessionMcpCount_other": "{{count}} MCP", + "sessionCacheHitCount_one": "{{count}} cache hit", + "sessionCacheHitCount_other": "{{count}} cache hit", + "status": "Status", + "batchSucceeded": "{{successful}}/{{total}} succeeded", + "batchFailed": "{{failed}} of {{total}} batch requests failed", + "success": "Success", + "failure": "Failure", + "sessionId": "Session ID", + "requestId": "Request ID", + "batchTooltip": "Batch {{batchId}} (row: {{requestId}})", + "batchCost": "batch cost", + "cost": "Cost", + "sessionTotal": "session total", + "mcpSpend_one": "incl. {{spend}} from {{count}} MCP", + "mcpSpend_other": "incl. {{spend}} from {{count}} MCP", + "duration": "Duration (s)", + "milliseconds": "{{value}}ms", + "ttft": "TTFT (s)", + "teamName": "Team Name", + "keyHash": "Key Hash", + "keyAlias": "Key Alias", + "model": "Model", + "tokens": "Tokens", + "internalUser": "Internal User", + "endUser": "End User", + "tags": "Tags" + }, + "badges": { + "agent": "Agent", + "batch": "Batch" + } + }, + "audit": { + "panel": { + "enterpriseTitle": "✨ Enterprise Feature.", + "enterpriseDescription": "This is a LiteLLM Enterprise feature, and requires a valid key to use.", + "previewIntro": "Here's a preview of what Audit Logs offer:", + "previewAlt": "Audit Logs Preview" + }, + "table": { + "emptyTitle": "No audit logs yet", + "emptyFilteredTitle": "No matching audit logs", + "emptyDescription": "Administrative changes to keys, teams, users, and models will appear here.", + "emptyFilteredDescription": "No audit log entries match your filters.", + "loading": "Loading audit logs…", + "searchPlaceholder": "Search audit logs by ID…", + "filtersTitle": "Filters", + "filtersDescription": "Narrow down audit log entries", + "objectId": "Object ID", + "objectIdPlaceholder": "Enter object ID…", + "changedBy": "Changed By", + "changedByPlaceholder": "Enter user ID…", + "teamId": "Team ID", + "teamIdPlaceholder": "Enter team ID…", + "keyHash": "Key Hash", + "keyHashPlaceholder": "Enter key hash…", + "action": "Action", + "allActions": "All Actions", + "table": "Table", + "allTables": "All Tables" + }, + "columns": { + "timestamp": "Timestamp", + "action": "Action", + "table": "Table", + "objectId": "Object ID", + "changedBy": "Changed By", + "apiKeyHash": "API Key (Hash)" + }, + "tableNames": { + "keys": "Keys", + "teams": "Teams", + "users": "Users", + "organizations": "Organizations", + "models": "Models", + "agents": "Agents" + }, + "drawer": { + "title": "Audit log details", + "copyJson": "Copy JSON", + "noDifferingFields": "No differing fields detected", + "token": "Token:", + "spend": "Spend:", + "maxBudget": "Max Budget:", + "before": "Before", + "after": "After", + "details": "Details", + "table": "Table", + "objectId": "Object ID", + "copyObjectId": "Copy object ID", + "changedBy": "Changed By", + "apiKeyHash": "API Key (Hash)", + "copyApiKeyHash": "Copy API key hash" + } + }, + "deletedKeys": { + "page": { + "comingSoonTitle": "Coming soon to Enterprise", + "comingSoonDescription": "Deleted key auditing is graduating from beta into our Enterprise audit & compliance suite." + }, + "table": { + "emptyTitle": "No deleted keys found", + "emptyDescription": "Keys deleted from this proxy will show up here.", + "loading": "Loading deleted keys…" + }, + "columns": { + "keyId": "Key ID", + "keyAlias": "Key Alias", + "teamAlias": "Team Alias", + "spendUsd": "Spend (USD)", + "budgetUsd": "Budget (USD)", + "unlimited": "Unlimited", + "userEmail": "User Email", + "userId": "User ID", + "createdAt": "Created At", + "createdBy": "Created By", + "deletedAt": "Deleted At", + "deletedBy": "Deleted By" + } + }, + "deletedTeams": { + "page": { + "comingSoonTitle": "Coming soon to Enterprise", + "comingSoonDescription": "Deleted team auditing is graduating from beta into our Enterprise audit & compliance suite." + }, + "table": { + "emptyTitle": "No deleted teams found", + "emptyDescription": "Teams deleted from this proxy will show up here.", + "loading": "Loading deleted teams…" + }, + "columns": { + "teamName": "Team Name", + "teamId": "Team ID", + "created": "Created", + "spendUsd": "Spend (USD)", + "budgetUsd": "Budget (USD)", + "unlimited": "Unlimited", + "models": "Models", + "organization": "Organization", + "deletedAt": "Deleted At", + "deletedBy": "Deleted By" + } + } + }, + "activityMetrics": { + "topKeys": { + "title": "Top Virtual Keys by Spend", + "loading": "Loading top keys...", + "failed": "Could not load top keys.", + "retry": "Retry", + "team": "Team: {{teamId}}", + "user": "User: {{user}}", + "usage": "{{requests}} requests | {{tokens}} tokens" + }, + "stats": { + "totalRequests": "Total Requests", + "totalSuccessfulRequests": "Total Successful Requests", + "totalTokens": "Total Tokens", + "totalSpend": "Total Spend", + "avgResponseTime": "Avg Response Time", + "avgTokensPerRequest": "{{value}} avg per successful request", + "spendPerRequest": "${{value}} per successful request", + "timedRequests": "over {{value}} timed successful requests" + }, + "panels": { + "spendPerDay": "Spend per day", + "totalTokens": "Total Tokens", + "requestsPerDay": "Requests per day", + "avgResponseTimePerDay": "Avg Response Time per day", + "successVsFailed": "Success vs Failed Requests", + "promptCaching": "Prompt Caching Metrics", + "cacheRead": "Cache Read: {{tokens}} tokens", + "cacheCreation": "Cache Creation: {{tokens}} tokens", + "totalTokensOverTime": "Total Tokens Over Time", + "totalRequestsOverTime": "Total Requests Over Time" + }, + "summary": { + "overallUsage": "Overall Usage", + "unknownItem": "Unknown Item", + "requests": "{{requests}} requests", + "avgResponse": "{{value}} avg response" + } + }, + "loggingSettingsView": { + "eventTypes": { + "success": "Success Only", + "failure": "Failure Only", + "successAndFailure": "Success & Failure" + }, + "loggingIntegrations": "Logging Integrations", + "parametersConfigured_one": "{{count}} parameters configured", + "parametersConfigured_other": "{{count}} parameters configured", + "noLoggingIntegrations": "No logging integrations configured", + "disabledCallbacks": "Disabled Callbacks", + "disabledForKey": "Disabled for this key", + "disabled": "Disabled", + "noCallbacksDisabled": "No callbacks disabled", + "title": "Logging Settings", + "description": "Active logging integrations and disabled callbacks for this key" + }, + "autoRotationView": { + "timestamp": "{{date}} at {{time}}", + "title": "Auto-Rotation", + "enabled": "Enabled", + "disabled": "Disabled", + "every": "Every {{interval}}", + "lastRotation": "Last Rotation", + "nextScheduledRotation": "Next Scheduled Rotation", + "noHistory": "No rotation history available", + "notEnabled": "Auto-rotation is not enabled for this key", + "description": "Automatic key rotation settings and status for this key" + }, + "routerSettingsSummary": { + "empty": "No router settings configured", + "routingStrategy": "Routing Strategy:", + "numRetries": "Number of Retries: {{value}}", + "allowedFailures": "Allowed Failures: {{value}}", + "cooldownTime": "Cooldown Time: {{value}}s", + "timeout": "Timeout: {{value}}s", + "retryAfter": "Retry After: {{value}}s", + "tagFilteringEnabled": "Tag Filtering: Enabled", + "fallbacks": "Fallbacks:" + }, + "labeledField": { + "copy": "Copy {{label}}" + }, + "guardrailSelector": { + "premiumOnly": "Setting guardrails is a premium feature.", + "placeholder": "Select guardrails" + }, + "policySelector": { + "premiumOnly": "Setting policies is a premium feature.", + "placeholder": "Select policies (production or published versions)" + }, + "usage": { + "pageView": { + "accessDenied": "You don't have access to that usage view", + "linkCopied": "Link copied", + "copyShareLink": "Copy Share Link", + "spendLoadFailed": "Failed to load spend data", + "tabs": { + "overview": "Overview", + "modelActivity": "Model Activity", + "keyActivity": "Key Activity", + "userActivity": "User Activity", + "mcpServerActivity": "MCP Server Activity", + "endpointActivity": "Endpoint Activity", + "errors": "Errors" + }, + "exportData": "Export Data", + "gatewayByEndpoint": { + "title": "Gateway Requests by Endpoint", + "subtitle": "Successful and failed requests per route", + "successful": "Successful", + "failed": "Failed" + }, + "modelSearch": { + "label": "Search models", + "placeholder": "Search by model name", + "clear": "Clear model search", + "noMatch": "No models match \"{{query}}\" in this date range" + }, + "credentialBanner": { + "title": "Reusable credentials are automatically tracked as tags", + "description": "When a reusable credential is used, it will appear as a tag prefixed with in this view.", + "close": "Close" + }, + "clientScope": { + "allUsers": "All users", + "myUsage": "My usage" + }, + "exportTitle": "Export Usage Data" + }, + "page": { + "loadingSettings": "Loading usage settings" + }, + "clientSelect": { + "label": "View client usage", + "placeholder": "View client usage" + }, + "modelViewToggle": { + "publicModelName": "Public Model Name", + "litellmModelName": "Litellm Model Name" + }, + "userRecordLink": { + "view": "View user record" + }, + "clientPanel": { + "tokenBreakdown": { + "unavailable": "Unavailable", + "cacheReads": "Cache reads", + "cacheWrites": "Cache writes", + "uncachedInput": "Uncached input", + "output": "Output", + "title": "Token breakdown", + "subtitle": "{{total}} total tokens", + "cacheShare": "{{percent}}% of these tokens are cache reads.", + "explanation": "Total tokens include input, cached input and output. Cached input is priced differently, so token count alone does not explain spend. Compare the same person and dates when checking the count shown by a client." + }, + "scope": { + "allUsers": "All users", + "selectedUser": "Selected user", + "allMembers": "All members", + "myUsage": "My usage", + "label": "Usage scope", + "ariaLabel": "Usage scope" + }, + "errors": { + "totalsFailed": "Could not load client totals", + "signIn": "Sign in to view your client usage", + "loadFailed": "Could not load client usage" + }, + "back": "Back to Usage", + "member": { + "label": "Team member", + "ariaLabel": "Team member", + "allMembers": "All members" + }, + "person": { + "label": "Filter by person", + "allUsers": "All users" + }, + "loading": "Loading client usage", + "tryAgain": "Try again", + "noUsage": "No {{client}} usage recorded in this date range.", + "summary": { + "spend": "Spend", + "totalTokens": "Total tokens", + "requests": "Requests" + }, + "noRequests": "No {{client}} requests in the selected range. Try another date range.", + "chooseAnotherPerson": " You can also choose another person.", + "retryModels": "Retry model breakdown", + "footnote": { + "scoped": "Based on retained request logs within the exact selected time range.", + "allUsers": "All-user totals use UTC daily aggregates for the selected dates. Filtering by person uses the exact selected times. Cache values are reported counts.", + "spend": "Spend is the cost recorded by LiteLLM." + } + }, + "viewSelect": { + "title": "Usage", + "description": "Select the usage data you want to view", + "options": { + "global": { + "label": "Global Usage", + "adminLabel": "Global Usage", + "nonAdminLabel": "Your Usage", + "description": "View usage across all resources", + "adminDescription": "View usage across all resources", + "nonAdminDescription": "View your usage" + }, + "myUsage": { + "label": "Your Usage", + "description": "View your own usage" + }, + "organization": { + "label": "Organization Usage", + "description": "View usage across all organizations" + }, + "team": { + "label": "Team Usage", + "description": "View usage by team" + }, + "customer": { + "label": "Customer Usage", + "description": "View usage by customer accounts" + }, + "tag": { + "label": "Tag Usage", + "description": "View usage grouped by tags" + }, + "agent": { + "label": "Agent Usage (A2A)", + "description": "View usage by AI agents" + }, + "user": { + "label": "User Usage", + "description": "View usage by individual users" + }, + "userAgentActivity": { + "label": "User Agent Activity", + "description": "View detailed user agent activity logs" + } + } + }, + "statStrip": { + "spend": "Spend", + "budgetHint": "of {{budget}} budget", + "noBudget": "No budget set", + "totalRequests": "Total Requests", + "successfulCount": " ok", + "failedCount": " failed", + "totalTokens": "Total Tokens", + "exactTokens": "{{tokens}} tokens", + "cacheReadHint": "{{tokens}} cache read", + "viewTokenBreakdown": "View token breakdown", + "tokenPopover": { + "title": "What counts toward total tokens?", + "inputTokens": "Input tokens", + "cacheReads": "Cache reads", + "cacheWrites": "Cache writes", + "outputTokens": "Output tokens", + "totalTokens": "Total tokens", + "pricingNote": "Input includes cache reads and writes. Cached input has separate pricing, so total tokens alone cannot explain the cost.", + "clientNote": "A client may show a different subset of tokens. Compare the same person, client and time range before comparing counts." + }, + "avgCostPerRequest": "Avg cost / request", + "avgLatency": "Avg latency", + "successRateHint": "{{rate}}% success rate" + }, + "overview": { + "topVirtualKeys": "Top Virtual Keys", + "topUsersBySpend": "Top Users by Spend", + "otherSeries": "Other", + "weekOf": "Wk of {{date}}" + }, + "breakdownChart": { + "metricNoun": { + "spend": "spend", + "tokens": "tokens", + "requests": "requests" + }, + "granularity": { + "daily": "Daily", + "weekly": "Weekly" + }, + "title": "{{granularity}} usage", + "subtitle": "{{granularity}} {{metric}} by model (top 8, rest grouped as Other)", + "bucketSize": "Bucket size", + "scaleLabel": "Scale", + "topModels": "Top models", + "loadingTopModels": "Loading top models" + }, + "breakdown": { + "metricLabel": "Metric", + "groupByLabel": "Group by", + "empty": "No usage in this range", + "byProvider": "by {{provider}}" + }, + "topAgents": { + "viewActivity": "View activity", + "tokens": "{{tokens}} tokens", + "viewAgentUsage": "View {{agent}} usage", + "empty": "No agent traffic in this range", + "subtitleLowCoverage": "Daily usage by agent. {{percent}}% of tokens came with a User-Agent; the rest is unattributed", + "subtitle": "Daily usage by agent, from each request's User-Agent", + "title": "Top agents", + "metricLabel": "Agent metric" + }, + "agentCatalog": { + "descriptions": { + "claudeDesktop": "Anthropic's desktop assistant", + "claudeCode": "Anthropic's coding agent", + "claudeUnspecified": "Client not identified", + "codex": "OpenAI's coding agent", + "opencode": "Open-source terminal coding agent", + "cursor": "AI code editor", + "copilot": "GitHub's coding assistant", + "hermes": "Open-source agent", + "grok": "xAI's assistant", + "aider": "AI pair programming in the terminal", + "openaiAgents": "OpenAI Agents SDK", + "githubActions": "CI workflows", + "litellmSdk": "litellm Python SDK", + "openaiSdk": "OpenAI client library", + "anthropicSdk": "Anthropic client library", + "curl": "Command line", + "cline": "Open-source coding agent", + "kiloCode": "Open-source coding agent", + "langchain": "Agent framework", + "pydanticAi": "Agent framework", + "python": "Scripts and HTTP clients", + "node": "Scripts and HTTP clients", + "customClient": "Custom client" + } + }, + "errors": { + "entities": { + "title": "Failures by identity", + "subtitle": "Keys, teams, users and models ranked by failed requests", + "groupByLabel": "Group by", + "empty": "No {{entities}} with failed requests in this range", + "tableLabel": "{{entities}} ranked by failed requests", + "columns": { + "requests": "Requests", + "failed": "Failed", + "errorRate": "Error rate", + "topStatus": "Top status" + } + }, + "statusLabels": { + "badRequest": "Bad Request", + "unauthorized": "Unauthorized", + "paymentRequired": "Payment Required", + "forbidden": "Forbidden", + "notFound": "Not Found", + "requestTimeout": "Request Timeout", + "payloadTooLarge": "Payload Too Large", + "unprocessableEntity": "Unprocessable Entity", + "rateLimited": "Rate Limited", + "internalServerError": "Internal Server Error", + "badGateway": "Bad Gateway", + "serviceUnavailable": "Service Unavailable", + "gatewayTimeout": "Gateway Timeout", + "overloaded": "Overloaded", + "notRecorded": "Not recorded", + "serverError": "Server Error", + "clientError": "Client Error", + "error": "Error" + }, + "series": { + "other": "Other", + "noStatus": "No status" + }, + "empty": { + "noRequests": "No requests in this range", + "noFailedRequests": "No failed requests in this range" + }, + "userScoped": { + "title": "Errors are deployment-wide", + "description": "Clear the user filter in Global Usage to see failure analytics" + }, + "failed": { + "title": "Error activity could not be loaded", + "description": "The proxy did not answer the failed-request query. Reload the page to try again." + }, + "stats": { + "errorRate": "Error rate", + "failedOfTotal": "{{failed}} of {{total}} requests", + "failedRequests": "Failed requests", + "clientErrors": "Client errors (4xx)", + "serverErrors": "Server errors (5xx)", + "rateLimited": "Rate limited (429)", + "authFailures": "Auth failures (401/403)" + }, + "rateChart": { + "title": "Error rate over time", + "subtitle": "Failed requests as a share of all requests, per day", + "viewLabel": "Show" + }, + "statusChart": { + "title": "Failed requests by status code", + "subtitle": "Per day, stacked by HTTP status", + "totalLabel": "failed", + "legendLabel": "Status codes in the chart" + }, + "statusList": { + "title": "By status code", + "subtitle": "Share of all failed requests", + "listLabel": "Failed requests by status code" + }, + "inspectInLogs": "Inspect failed requests in Logs" + }, + "subscriptionCost": { + "attributionRule": "A subscription fee is counted once for each monthly billing period whose start date falls inside the selected dates. Fixed costs sit beside metered spend and are never added to it, to key or team spend, or to budgets.", + "card": { + "accountNotSignedIn": "Account not signed in on the proxy", + "setFee": "Set monthly fee", + "edit": "Edit", + "remove": "Remove", + "noDeployments": "None", + "notTracked": "Fixed cost not tracked", + "noPeriodsInRange": "None in range", + "title": "Subscription fixed costs", + "loadFailed": "Could not load subscription accounts for this range.", + "noAccounts": "No subscription accounts", + "fixedCostNone": "Fixed cost in range: none", + "fixedCostTotal": "Fixed cost in range: {{total}}", + "meteredSpend": "Usage-billed spend in range, all providers: ${{spend}}" + }, + "removeDialog": { + "title": "Remove this monthly fee?", + "description": " goes back to \"Fixed cost not tracked\". Its requests stay included in the subscription", + "cancel": "Cancel", + "confirm": "Remove fee" + }, + "columns": { + "provider": "Provider", + "account": "Account", + "deployments": "Deployments", + "monthlyFee": "Monthly fee", + "billingPeriods": "Billing periods in range", + "fixedCost": "Fixed cost in range" + }, + "feeModal": { + "errors": { + "feeRequired": "Enter a monthly fee", + "feePositive": "Monthly fee must be greater than 0", + "currencyCode": "Currency must be a three-letter code", + "periodStart": "Pick a billing period start date" + }, + "editTitle": "Edit monthly fee", + "setTitle": "Set monthly fee", + "description": "{{provider}} account {{accountId}}", + "fields": { + "monthlyFee": "Monthly fee", + "currency": "Currency", + "billingPeriodStart": "Billing period start", + "label": "Label (optional)" + }, + "cancel": "Cancel", + "save": "Save" + }, + "toasts": { + "feeSaved": "Monthly fee saved", + "feeRemoved": "Monthly fee removed" + } + }, + "entitySummary": { + "tooltips": { + "totalCost": "Request cost plus flat cost for reserved capacity. Select this tile to see the breakdown.", + "requestCost": "Usage-based cost of the requests this entity sent during the selected period, priced per token.", + "flatCost": "Reserved provisioned throughput, billed per hour whether or not requests are sent. Reported here only; it does not count toward team, key, user, or organization budgets.", + "ptuHours": "Provisioned throughput consumed, in PTU-hours: the tokens sent to PTU deployments converted through the Azure sizing table for each model. Divide it by the PTUs reserved times the hours in the period to see how much of the reservation was used." + } + }, + "entityUsage": { + "chart": { + "flatCostSeries": "Flat cost", + "metricTitle": { + "spend": "Spend", + "tokens": "Tokens", + "requests": "Requests" + }, + "granularity": { + "daily": "Daily", + "weekly": "Weekly" + }, + "title": "{{metric}} by {{dimension}}", + "dimension": { + "model": "model", + "deployment": "deployment" + }, + "subtitle": "{{granularity}} {{metric}}, top 8 stacked", + "flatCostOnTop": ", flat cost on top", + "bucketSize": "Bucket size", + "scaleLabel": "Scale" + }, + "filterLabel": "Filter by {{entity}}", + "filterPlaceholder": "Select {{entity}} to filter...", + "breakdownTable": { + "share": "Share", + "spend": "Spend", + "successful": "Successful", + "failed": "Failed", + "tokens": "Tokens" + }, + "topPublicModelNames": "Top Public Model Names", + "topLitellmModels": "Top Litellm Models", + "spendOverview": "{{entity}} Spend Overview", + "spendPerEntity": "Spend Per {{entity}}", + "trackCostPer": "Track cost per {{entity}}", + "noSpendData": "No {{entity}} spend data", + "topVirtualKeys": "Top Virtual Keys", + "topAgents": "Top Agents", + "topAgentsDrivingSpend": "Top Agents Driving Spend", + "providerUsage": "Provider Usage", + "tabs": { + "cost": "Cost", + "requestTokenConsumption": "Request / Token Consumption", + "modelActivity": "Model Activity", + "agentActivity": "Agent Activity", + "keyActivity": "Key Activity", + "endpointActivity": "Endpoint Activity" + }, + "spendLoadFailed": "Failed to load spend data", + "agentLoadFailed": "Failed to load agent data" + }, + "providerSpend": { + "columns": { + "provider": "Provider", + "spend": "Spend", + "successful": "Successful", + "failed": "Failed", + "tokens": "Tokens" + }, + "includedInSubscription": "Included in subscription", + "totalProviderSpend": "Total provider spend", + "noData": "No provider usage data", + "title": "Spend by Provider", + "showZeroSpend": "Show Zero Spend", + "showUnknown": "Show Unknown", + "showUnknownHint": "Requests that failed to route to a provider" + }, + "teamUserSpend": { + "columns": { + "team": "Team", + "user": "User", + "spend": "Spend", + "requests": "Requests", + "successful": "Successful", + "failed": "Failed", + "tokens": "Tokens" + }, + "title": "Spend Per User Within Team", + "downloadCsv": "Download CSV", + "attributionNote": "Attributed per request from spend logs, so it includes JWT/SSO traffic that does not use a virtual key", + "loadFailed": "Spend per user failed to load", + "selectTeam": "Select a team to see spend per user", + "noUserSpend": "No user spend in this range", + "noUser": "(no user)" + }, + "topModelView": { + "columns": { + "model": "Model", + "spendUsd": "Spend (USD)", + "successful": "Successful", + "failed": "Failed", + "tokens": "Tokens" + }, + "limitLabel": "Number of models to show", + "viewModeLabel": "Top model view mode" + }, + "endpointUsage": { + "table": { + "columns": { + "endpoint": "Endpoint", + "successfulFailed": "Successful / Failed", + "totalRequest": "Total Request", + "successRate": "Success Rate", + "totalTokens": "Total Tokens", + "spend": "Spend" + }, + "successfulRequests": "Successful requests", + "title": "Endpoints", + "endpointSingular": "endpoint", + "endpointPlural": "endpoints", + "noData": "No endpoint usage data" + }, + "barChart": { + "title": "Success vs Failed Requests by Endpoint" + }, + "lineChart": { + "title": "Endpoint Usage Trends" + } + }, + "clientUsage": { + "unknownModel": "Unknown model", + "unknownProvider": "Unknown provider", + "errors": { + "selectUserOrTeam": "Select a user or team to view client usage", + "incompleteData": "The proxy returned incomplete client usage data", + "outsideUser": "The proxy returned usage outside the selected user", + "outsideTeam": "The proxy returned usage outside the selected team", + "signIn": "Sign in to view client usage", + "chooseAdministeredTeam": "Choose a team you administer", + "chooseTeamMember": "Choose a member of the selected team", + "chooseTeamBeforeMember": "Choose an administered team before viewing another member's usage", + "invalidRange": "Select a valid date range", + "loadFailed": "Could not load client usage" + } + }, + "clientModels": { + "errors": { + "incompleteData": "The proxy returned incomplete model usage data", + "duplicateData": "The proxy returned duplicate model usage data. Try again.", + "totalsMismatch": "Model usage does not match these client totals yet. Refresh or choose a shorter range.", + "tooLarge": "Choose a shorter date range to load the model breakdown", + "invalidJson": "Could not load complete model usage. Try again.", + "loadFailed": "Could not load the model breakdown" + } + }, + "keyActivity": { + "searching": "Searching...", + "matchingKeys_one": "{{value}} matching keys", + "matchingKeys_other": "{{value}} matching keys", + "keyCount_one": "{{value}} keys", + "keyCount_other": "{{value}} keys", + "loadingDetails": "Loading key details...", + "detailFailed": "Could not load key details.", + "retry": "Retry", + "noDailyDetails": "No daily details available", + "loadingChartData": "Loading chart data...", + "overallUsage": "Overall Usage", + "stats": { + "spend": "Spend", + "requests": "Requests", + "ok": "ok", + "failed": "failed", + "tokens": "Tokens", + "tokensHint": "{{input}} in · {{output}} out", + "avgCost": "Avg cost / request", + "cacheReadHint": "{{value}} cache read tokens" + }, + "searchFailed": "Search failed", + "firstPageFailed": "Could not load keys for this range.", + "loadingKeys": "Loading keys...", + "requestsSrOnly": "requests", + "retrySearch": "Retry search", + "searchKeysFailed": "Could not search keys. ", + "noMatches": "No keys match \"\" in this date range", + "searchLabel": "Search keys", + "searchPlaceholder": "Search alias, hash, user ID, or email", + "clearSearch": "Clear key search", + "panelTitle": "Virtual keys", + "columns": { + "key": "Key", + "spend": "Spend", + "requests": "Requests" + }, + "searchAllFailed": "Could not search all keys. ", + "loadingMore": "Loading more keys...", + "loadMoreFailed": "Could not load more keys." + }, + "topKeys": { + "keyNotInDatabase": "This key is no longer in the database (deleted, or a CLI/SSO session key), so its details can't be opened", + "columns": { + "keyId": "Key ID", + "keyAlias": "Key Alias", + "user": "User", + "tags": "Tags", + "spend": "Spend (USD)" + }, + "tagTooltip": { + "name": "Tag Name:", + "spend": "Spend:" + }, + "showFewerTags": "Show fewer tags", + "showAllTags": "Show all tags", + "limitLabel": "Number of top keys to show", + "viewLabel": "View", + "chartTooltip": { + "keyAlias": "Key Alias: ", + "keyId": "Key ID: ", + "spend": "Spend: " + }, + "close": "Close" + }, + "userActivity": { + "firstPageFailed": "Could not load users for this range.", + "retry": "Retry", + "loading": "Loading users...", + "empty": "No user activity in this date range", + "columns": { + "user": "User", + "spend": "Spend", + "requests": "Requests", + "successful": "Successful", + "failed": "Failed", + "totalTokens": "Total tokens" + }, + "userCount_one": " users", + "userCount_other": " users", + "loadingMore": "Loading more users...", + "loadMoreFailed": "Could not load more users.", + "loadMore": "Load more", + "panelTitle": "User activity", + "noUser": "(no user)" + }, + "topUsers": { + "failed": "Could not load top users.", + "retry": "Retry", + "loading": "Loading users...", + "empty": "No user activity in this date range", + "columns": { + "user": "User", + "spend": "Spend", + "requests": "Requests" + } + }, + "keyModelUsage": { + "columns": { + "model": "Model", + "spend": "Spend (USD)", + "successful": "Successful", + "failed": "Failed", + "tokens": "Tokens" + }, + "title": "Model Usage", + "table": "Table", + "chart": "Chart" + }, + "valueFormatters": { + "milliseconds": "{{value}}ms", + "seconds": "{{value}}s" + }, + "export": { + "modal": { + "title": "Export {{entity}} Usage", + "success": "{{entity}} usage data exported successfully as {{format}}", + "failed": "Failed to export data", + "cancel": "Cancel", + "exporting": "Exporting...", + "exportFormat": "Export {{format}}" + }, + "format": { + "label": "Format", + "csv": "CSV (Excel, Google Sheets)", + "json": "JSON (includes metadata)" + }, + "summary": { + "filterCount_one": "{{count}} filter", + "filterCount_other": "{{count}} filters" + }, + "type": { + "label": "Export type", + "daily": { + "title": "Day-by-day breakdown by {{entityType}}", + "description": "Daily metrics for each {{entityType}}" + }, + "dailyWithKeys": { + "title": "Day-by-day breakdown by {{entityType}} and key", + "description": "Daily metrics for each {{entityType}}, split by API key" + }, + "dailyWithModels": { + "title": "Day-by-day by {{entityType}} and model", + "description": "Daily metrics split by model" + }, + "dailyWithUsers": { + "title": "Day-by-day breakdown by {{entityType}} and user", + "description": "Daily metrics for each {{entityType}}, split by key owner" + } + }, + "header": { + "noOptionsPlaceholder": "No {{entityType}}s with usage in this range", + "noOptionsFound": "No options found", + "clear": "Clear {{label}}", + "filters": "filters", + "exportData": "Export Data" + } + }, + "cloudzero": { + "validation": { + "apiKeyRequired": "Please enter your CloudZero API key", + "connectionIdRequired": "Please enter the CloudZero connection ID" + }, + "toasts": { + "loadFailedWithReason": "Failed to load existing settings: {{error}}", + "unknownError": "Unknown error", + "loadFailed": "Failed to load existing settings", + "noAccessToken": "No access token available", + "saved": "CloudZero settings saved successfully", + "saveFailed": "Failed to save CloudZero settings", + "exported": "Export to CloudZero completed successfully", + "exportFailed": "Failed to export to CloudZero", + "csvComingSoon": "CSV export functionality coming soon!", + "csvFailed": "Failed to export CSV" + }, + "destinations": { + "cloudzero": "Export to CloudZero", + "csv": "Export to CSV" + }, + "title": "Export Data", + "destinationLabel": "Export Destination", + "existing": { + "title": "Existing CloudZero Configuration", + "apiKey": "API Key: ", + "connectionId": "Connection ID: " + }, + "form": { + "apiKeyLabel": "CloudZero API Key", + "apiKeyPlaceholder": "Enter your CloudZero API key", + "connectionIdLabel": "Connection ID", + "connectionIdPlaceholder": "Enter CloudZero connection ID" + }, + "csv": { + "title": "CSV Export", + "description": "Export your usage data as a CSV file for analysis in spreadsheet applications." + }, + "cancel": "Cancel", + "exportToCloudZero": "Export to CloudZero", + "exportCsv": "Export CSV" + }, + "perUser": { + "unknownAgent": "Unknown", + "notAvailable": "N/A", + "columns": { + "userId": "User ID", + "userEmail": "User Email", + "userAgent": "User Agent", + "successGenerations": "Success Generations", + "totalTokens": "Total Tokens", + "failedRequests": "Failed Requests", + "totalCost": "Total Cost" + }, + "title": "Per User Usage", + "subtitle": "Individual developer usage metrics", + "tabs": { + "details": "User Details", + "distribution": "Usage Distribution" + }, + "noData": "No per-user usage data", + "distribution": { + "title": "User Usage Distribution", + "subtitle": "Number of users by successful request frequency", + "buckets": { + "range1To9": "1-9 requests", + "range10To99": "10-99 requests", + "range100To999": "100-999 requests", + "range1kTo9k": "1K-9.9K requests", + "range10kTo99k": "10K-99.9K requests", + "range100kPlus": "100K+ requests" + }, + "userCount_one": "{{count}} users", + "userCount_other": "{{count}} users" + } + }, + "userAgentActivity": { + "noActiveUsers": "No active users in this period", + "weekLabel": "Week {{number}}", + "monthLabel": "Month {{number}}", + "charts": { + "dau": "Daily Active Users - Last 7 Days", + "wau": "Weekly Active Users - Last 7 Weeks", + "mau": "Monthly Active Users - Last 7 Months" + }, + "summary": { + "title": "Summary by User Agent", + "subtitle": "Performance metrics for different user agents", + "noData": "No Data", + "successRequests": "Success Requests", + "totalTokens": "Total Tokens", + "totalCost": "Total Cost" + }, + "filter": { + "label": "Filter by User Agents", + "placeholder": "All User Agents", + "clear": "Clear user agent filter", + "empty": "No user agents found" + }, + "activity": { + "activeUsersTitle": "DAU, WAU & MAU per Agent", + "perUserTitle": "Per User Usage (Last 30 Days)", + "activeUsersSubtitle": "Active users across different time periods", + "perUserSubtitle": "Usage per end user", + "periodLabel": "Active users period", + "viewLabel": "Activity view" + } + }, + "customerSelect": { + "lookupError": "Unable to load selected customers", + "loadError": "Unable to load customers", + "placeholder": "Search customers by ID or alias...", + "empty": "No customers found", + "loading": "Loading customers...", + "clearAll": "Clear all customers" + }, + "chartLoader": { + "processingDate": "Processing date selection...", + "loading": "Loading chart data...", + "processingDateHint": "This will only take a moment", + "loadingHint": "Fetching your data" + } + }, + "telemetry": { + "destinationText": { + "every": "every {{seconds}} s", + "http": "One JSON report per worker {{every}} and on shutdown, by HTTP POST to LITELLM_TELEMETRY_ENDPOINT. A failed send is retried next window", + "localTable_one": "One report per worker {{every}} and on shutdown, stored in this proxy's LiteLLM_TelemetryReport table for {{count}} days. Nothing leaves the proxy. Admins can export from GET /telemetry/reports", + "localTable_other": "One report per worker {{every}} and on shutdown, stored in this proxy's LiteLLM_TelemetryReport table for {{count}} days. Nothing leaves the proxy. Admins can export from GET /telemetry/reports", + "none": "No destination is chosen, so nothing is recorded" + }, + "destination": { + "localTable": "Local table (LiteLLM_TelemetryReport in this proxy's database)", + "http": "HTTP (POST to LITELLM_TELEMETRY_ENDPOINT)", + "title": "Destination", + "missing": "Telemetry is on but no destination is chosen, so nothing is recorded", + "setByEnvironment": "Set by LITELLM_TELEMETRY_DESTINATION" + }, + "groupList": { + "slicesBy": "Slices by: ", + "counts": "Counts", + "sends": "Sends", + "helpsCatch": "Helps catch: " + }, + "switch": { + "label": "Telemetry", + "on": "On", + "off": "Off" + }, + "report": { + "sampleTitle": "Sample report", + "lastTitle": "Last report sent from this worker", + "sampleDescription": "Made-up traffic through the real filter, with the groups that are on (all groups while telemetry is off)", + "lastDescription": "Exactly what this worker last sent or stored" + }, + "page": { + "loading": "Loading telemetry settings...", + "loadFailed": "Could not load telemetry settings: ", + "title": "Telemetry", + "description": "Choose what privacy-safe, aggregated usage data this proxy shares", + "tabs": { + "proxy": "Proxy requests", + "ui": "Admin UI" + }, + "uiNote": "Sent only while Page navigation is on, to this proxy at POST /telemetry/ui_events, and counted into the same report as proxy traffic" + }, + "howSent": { + "title": "How and when it is sent", + "changesApply": "Changes apply from the next window", + "offByDefault": "Off by default. Each switch adds aggregated counts, never prompts, responses, keys, ids or header values. LITELLM_TELEMETRY_DISABLED=true turns everything off" + }, + "enterpriseDialog": { + "premiumTitle": "Contact your enterprise representative", + "title": "Telemetry is an enterprise-only feature", + "premiumDescription": "Telemetry can't be turned on from this page. Reach out to your LiteLLM enterprise representative to turn it on for this proxy.", + "description": "Telemetry is only available on LiteLLM Enterprise, and this proxy isn't on an enterprise license." + }, + "groups": { + "heartbeat": { + "title": "Heartbeat", + "counts": "Random instance id, LiteLLM version, report window, which groups are on", + "helps": "Which versions are running" + }, + "requestSuccess": { + "title": "Request success", + "slices": "endpoint, LiteLLM status, provider status, stream, handled by Rust, LiteLLM cache hit", + "counts": "requests, provider attempts, latency to headers and to first byte", + "helps": "Error-rate and latency regressions, Rust vs Python" + }, + "tokenInfo": { + "title": "Token info", + "slices": "provider prompt-cache hit", + "counts": "input, output and cache-read token sums", + "helps": "Broken prompt caching or token counting" + }, + "requestTaxonomy": { + "title": "Request taxonomy", + "slices": "provider, salted deployment hash", + "counts": "one row per provider attempt with its status", + "helps": "Which provider is failing, retries and fallbacks" + }, + "eventDetails": { + "title": "Event details", + "counts": "message block counts and types, allowlisted header names (no values), provider time to first token", + "helps": "Which request shapes and clients fail" + }, + "instanceConfiguration": { + "title": "Instance configuration", + "counts": "names of allowlisted config keys that are set (no values)", + "helps": "Which features an upgrade must keep working" + }, + "pageNavigation": { + "title": "Page navigation", + "counts": "Admin UI page views and tab switches by route name (no ids or typed text)", + "helps": "Which pages and tabs get used" + } + } + }, "organizations": { "view": { "loading": "Loading...",