From 41d45079c7b0b71caac21566f7948c7ad269e339 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 5 Nov 2026 12:42:14 -0800 Subject: [PATCH] refactor(ui): move remaining logs and usage text onto i18n (#50146) Logs and usage option lists, filter chip labels and label maps now hold message keys that their readers translate at render time. Tile titles, the "Claude (unspecified)" agent label and the endpoint chart categories stay English ids and get display maps keyed by them. Lookups keyed by URL or server values check Object.hasOwn first. --- .../components/EndpointUsageBarChart.tsx | 7 +- .../components/EntityUsage/EntityUsage.tsx | 19 +-- .../components/EntityUsage/TopModelView.tsx | 9 +- .../EntityUsage/entityUsageSummary.ts | 12 ++ .../components/errors/ErrorsTab.tsx | 12 +- .../components/errors/errorsData.ts | 16 +-- .../components/overview/BreakdownChart.tsx | 21 +-- .../components/overview/TopAgents.tsx | 30 ++-- .../overview/UsageBreakdownChart.tsx | 16 +-- .../components/overview/agentCatalog.ts | 9 +- .../_components/hooks/clientUsageData.test.ts | 3 +- .../_components/hooks/clientUsageData.ts | 6 +- .../components/EntityUsage/TopKeyView.tsx | 8 +- .../components/chat_ui/ResponseMetrics.tsx | 6 +- .../components/logs/audit/AuditLogsTable.tsx | 83 ++++++----- .../src/components/logs/constants.ts | 56 ++++---- .../logs/detail/LogDetailContent.tsx | 6 +- .../detail/sections/ToolsSection/utils.ts | 3 +- .../logs/request/LogsTableToolbar.tsx | 6 +- .../logs/request/RequestLogsFilters.tsx | 84 ++++++----- .../logs/request/RequestLogsTable.tsx | 16 ++- .../logs/request/useLogFilterLogic.ts | 33 ++--- .../src/components/user_agent_activity.tsx | 9 +- ui/litellm-dashboard/src/i18n/locales/en.json | 134 ++++++++++++++++++ .../src/utils/promptCacheUsage.ts | 8 +- 25 files changed, 411 insertions(+), 201 deletions(-) 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 687571e70be..198d501ba41 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 type { ParseKeys } from "i18next"; import { useTranslation } from "react-i18next"; import { ChartColumnStacked } from "lucide-react"; import { BarChart, CustomTooltip } from "@/components/shared/charts"; @@ -12,6 +13,10 @@ interface EndpointUsageBarChartProps { const SUCCESS_COLOR = "#2b3fd6"; const FAILED_COLOR = "#ef4444"; const CATEGORIES = ["Successful", "Failed"] as const; +const CATEGORY_LABELS: Record<(typeof CATEGORIES)[number], ParseKeys> = { + Successful: "usage.endpointUsage.barChart.successful", + Failed: "usage.endpointUsage.barChart.failed", +}; const COLORS = [SUCCESS_COLOR, FAILED_COLOR] as const; const valueFormatter = (value: number) => value.toLocaleString(); @@ -45,7 +50,7 @@ const EndpointUsageBarChart: React.FC = ({ endpointD {CATEGORIES.map((category, i) => ( ))} 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 96656225b30..f12f59f9c28 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 @@ -7,6 +7,7 @@ import { buildSummaryTiles, hasFlatCost, hasPtuHours, + SUMMARY_TILE_LABELS, type SummaryTile, } from "./entityUsageSummary"; import { MoneyCell } from "@/components/shared/table_cells"; @@ -74,13 +75,13 @@ const TILE_TRENDS: Readonly> = { "Total Tokens": "tokens", }; const GRANULARITY_OPTIONS = [ - { value: "day", label: "Daily" }, - { value: "week", label: "Weekly" }, -] as const satisfies readonly { value: Granularity; label: string }[]; + { value: "day", labelKey: "usage.entityUsage.chart.granularityOptions.day" }, + { value: "week", labelKey: "usage.entityUsage.chart.granularityOptions.week" }, +] as const satisfies readonly { value: Granularity; labelKey: ParseKeys }[]; const SCALE_OPTIONS = [ - { value: "linear", label: "Linear" }, - { value: "log", label: "Log" }, -] as const satisfies readonly { value: StackedUsageScale; label: string }[]; + { value: "linear", labelKey: "usage.entityUsage.chart.scaleOptions.linear" }, + { value: "log", labelKey: "usage.entityUsage.chart.scaleOptions.log" }, +] as const satisfies readonly { value: StackedUsageScale; labelKey: ParseKeys }[]; const QUIET_HEADER = { headerClassName: "font-normal" }; const FLAT_COST_KEY = "flat_cost"; @@ -433,7 +434,7 @@ const EntityUsage: React.FC = ({ const tileLabel = ({ title, tooltip, expandable }: SummaryTile) => ( <> - {title} + {Object.hasOwn(SUMMARY_TILE_LABELS, title) ? t(SUMMARY_TILE_LABELS[title]) : title} {tooltip ? ( } /> @@ -540,13 +541,13 @@ const EntityUsage: React.FC = ({ ({ value, label: t(labelKey) }))} onChange={setGranularity} /> ({ value, label: t(labelKey) }))} onChange={setScale} /> 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 ae98dd835b5..c8e42c9a6aa 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 @@ -2,6 +2,7 @@ import { BarChart } from "@/components/shared/charts"; import { DataTable } from "@/components/shared/DataTable"; import { MoneyCell } from "@/components/shared/table_cells"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; +import type { ParseKeys } from "i18next"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { formatNumberWithCommas } from "@/utils/dataUtils"; @@ -28,9 +29,9 @@ type ViewMode = "chart" | "table"; const LIMIT_OPTIONS = TOP_MODEL_LIMITS.map((limit) => ({ value: String(limit), label: String(limit) })); const VIEW_OPTIONS = [ - { value: "table", label: "Table View" }, - { value: "chart", label: "Chart View" }, -] as const satisfies readonly { value: ViewMode; label: string }[]; + { value: "table", labelKey: "usage.topModelView.viewOptions.table" }, + { value: "chart", labelKey: "usage.topModelView.viewOptions.chart" }, +] as const satisfies readonly { value: ViewMode; labelKey: ParseKeys }[]; const BAR_COLOR = "#2b3fd6"; const QUIET_HEADER = { headerClassName: "font-normal" }; @@ -100,7 +101,7 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi ({ value, label: t(labelKey) }))} onChange={setModelViewMode} /> diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageSummary.ts b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageSummary.ts index 60084f7ef66..bf7c272347e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageSummary.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EntityUsage/entityUsageSummary.ts @@ -20,6 +20,18 @@ interface SpendSummaryMetadata { total_tokens: number; } +export const SUMMARY_TILE_LABELS: Readonly> = { + "PTU Hours": "usage.entitySummary.titles.ptuHours", + "Total Cost": "usage.entitySummary.titles.totalCost", + "Total Spend": "usage.entitySummary.titles.totalSpend", + "Total Requests": "usage.entitySummary.titles.totalRequests", + "Successful Requests": "usage.entitySummary.titles.successfulRequests", + "Failed Requests": "usage.entitySummary.titles.failedRequests", + "Total Tokens": "usage.entitySummary.titles.totalTokens", + "Request Cost": "usage.entitySummary.titles.requestCost", + "Flat Cost": "usage.entitySummary.titles.flatCost", +}; + export const TOTAL_COST_TOOLTIP: ParseKeys = "usage.entitySummary.tooltips.totalCost"; export const REQUEST_COST_TOOLTIP: ParseKeys = "usage.entitySummary.tooltips.requestCost"; 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 8f1cc80a81d..8f62e941a73 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 @@ -35,7 +35,7 @@ interface ErrorsTabProps { userScoped?: boolean; } -const ENTITY_OPTIONS = ERROR_ENTITY_KINDS.map((kind) => ({ value: kind, label: ERROR_ENTITY_LABEL[kind].plural })); +const ENTITY_OPTIONS = ERROR_ENTITY_KINDS.map((kind) => ({ value: kind, labelKey: ERROR_ENTITY_LABEL[kind].plural })); const shortDate = (iso: string): string => new Date(`${iso}T00:00:00`).toLocaleDateString(undefined, { month: "short", day: "numeric" }); @@ -187,7 +187,7 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals ({ value, label: t(labelKey) }))} onChange={setRateView} /> } @@ -280,7 +280,7 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals ({ value, label: t(labelKey) }))} onChange={setKind} /> } @@ -290,14 +290,14 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals height="h-48" empty={ rows.length === 0 - ? t("usage.errors.entities.empty", { entities: ERROR_ENTITY_LABEL[kind].plural.toLowerCase() }) + ? t("usage.errors.entities.empty", { entities: t(ERROR_ENTITY_LABEL[kind].plural).toLowerCase() }) : null } > - +
- {ERROR_ENTITY_LABEL[kind].singular} + {t(ERROR_ENTITY_LABEL[kind].singular)} {t("usage.errors.entities.columns.requests")} {t("usage.errors.entities.columns.failed")} {t("usage.errors.entities.columns.errorRate")} 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 c45524b2add..66b69eacaff 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 @@ -33,11 +33,11 @@ export interface RequestErrorActivity { export const ERROR_ENTITY_KINDS = ["key", "team", "user", "model"] as const; export type ErrorEntityKind = (typeof ERROR_ENTITY_KINDS)[number]; -export const ERROR_ENTITY_LABEL: Record = { - key: { singular: "Virtual key", plural: "Virtual keys" }, - team: { singular: "Team", plural: "Teams" }, - user: { singular: "User", plural: "Users" }, - model: { singular: "Model", plural: "Models" }, +export const ERROR_ENTITY_LABEL: Record = { + key: { singular: "usage.errors.entityLabels.key.singular", plural: "usage.errors.entityLabels.key.plural" }, + team: { singular: "usage.errors.entityLabels.team.singular", plural: "usage.errors.entityLabels.team.plural" }, + user: { singular: "usage.errors.entityLabels.user.singular", plural: "usage.errors.entityLabels.user.plural" }, + model: { singular: "usage.errors.entityLabels.model.singular", plural: "usage.errors.entityLabels.model.plural" }, }; const STATUS_LABELS: Record = { @@ -153,9 +153,9 @@ export const errorSeries = (activity: RequestErrorActivity): ErrorSeries => { export type ErrorRateView = "total" | "status"; -export const ERROR_RATE_VIEW_OPTIONS: readonly { value: ErrorRateView; label: string }[] = [ - { value: "total", label: "Total" }, - { value: "status", label: "By status code" }, +export const ERROR_RATE_VIEW_OPTIONS: readonly { value: ErrorRateView; labelKey: ParseKeys }[] = [ + { value: "total", labelKey: "usage.errors.rateViews.total" }, + { value: "status", labelKey: "usage.errors.rateViews.status" }, ]; export const rateByStatusRows = (series: ErrorSeries): ErrorSeriesRow[] => 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 f0a346dc8da..79c64e6079b 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 type { ParseKeys } from "i18next"; import { useTranslation } from "react-i18next"; import { ArrowDownRight, ArrowUpRight } from "lucide-react"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; @@ -22,16 +23,16 @@ import { Segmented } from "./Primitives"; import { providerForModel } from "./modelProvider"; const METRIC_OPTIONS = [ - { value: "spend", label: "Spend" }, - { value: "tokens", label: "Tokens" }, - { value: "requests", label: "Requests" }, -] as const satisfies readonly { value: UsageMetric; label: string }[]; + { value: "spend", labelKey: "usage.breakdown.metricOptions.spend" }, + { value: "tokens", labelKey: "usage.breakdown.metricOptions.tokens" }, + { value: "requests", labelKey: "usage.breakdown.metricOptions.requests" }, +] as const satisfies readonly { value: UsageMetric; labelKey: ParseKeys }[]; const DIMENSION_OPTIONS = [ - { value: "model_groups", label: "Model" }, - { value: "models", label: "Deployment" }, - { value: "providers", label: "Provider" }, -] as const satisfies readonly { value: BreakdownDimension; label: string }[]; + { value: "model_groups", labelKey: "usage.breakdown.dimensionOptions.modelGroups" }, + { value: "models", labelKey: "usage.breakdown.dimensionOptions.models" }, + { value: "providers", labelKey: "usage.breakdown.dimensionOptions.providers" }, +] as const satisfies readonly { value: BreakdownDimension; labelKey: ParseKeys }[]; export interface BreakdownState { metric: UsageMetric; @@ -67,14 +68,14 @@ export function BreakdownControls({ ({ value, label: t(labelKey) }))} onChange={(metric) => onChange({ ...state, metric })} /> {showDimension && ( ({ value, label: t(labelKey) }))} onChange={(dimension) => onChange({ ...state, dimension })} /> )} 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 68ed202a8dc..d5d69216523 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 type { ParseKeys } from "i18next"; import { useTranslation } from "react-i18next"; import { ArrowRight, Bot, Monitor, Terminal } from "lucide-react"; import { StackedUsageChart } from "@/components/shared/charts"; @@ -18,17 +19,17 @@ import { type UsageMetric, } from "./overviewData"; import { ChartSkeleton, Panel, PANEL_INSET_X, Segmented } from "./Primitives"; -import { agentDailyData, topAgents, type AgentRow, type TagSummaryRow } from "./agentCatalog"; +import { agentDailyData, agentDisplayLabel, topAgents, type AgentRow, type TagSummaryRow } from "./agentCatalog"; const ROWS = 10; const CHART_SERIES = 6; /** Below this share of tokens carrying a User-Agent, say so rather than imply the list is the whole picture. */ const COVERAGE_NOTE_BELOW = 0.5; const METRICS = [ - { value: "tokens", label: "Tokens" }, - { value: "spend", label: "Spend" }, - { value: "requests", label: "Requests" }, -] as const satisfies readonly { value: UsageMetric; label: string }[]; + { value: "tokens", labelKey: "usage.topAgents.metricOptions.tokens" }, + { value: "spend", labelKey: "usage.topAgents.metricOptions.spend" }, + { value: "requests", labelKey: "usage.topAgents.metricOptions.requests" }, +] as const satisfies readonly { value: UsageMetric; labelKey: ParseKeys }[]; /** Brand-tinted monograms for runtimes that have no logo file, so a top spender never shows a blank tile. */ const MONOGRAMS: Readonly> = { @@ -42,7 +43,9 @@ const CLIENT_SURFACES: Readonly>> = { }; function AgentGlyph({ agent }: { agent: Pick }) { - if (agent.logo) return ; + const { t } = useTranslation(); + const label = agentDisplayLabel(agent.label, t); + if (agent.logo) return ; const monogram = MONOGRAMS[agent.id]; if (monogram) { return ( @@ -56,7 +59,7 @@ function AgentGlyph({ agent }: { agent: Pick ); } - return {agent.label.charAt(0)}; + return {label.charAt(0)}; } export function AgentMark({ agent }: { agent: Pick }) { @@ -103,7 +106,7 @@ function AgentItem({ )} - {agent.label} + {agentDisplayLabel(agent.label, t)} {openRow ? ( {t("usage.topAgents.viewActivity")} @@ -130,7 +133,7 @@ function AgentItem({ {openRow ? ( ))}
diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.tsx index f444d21d6e8..26f84386c0c 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsFilters.tsx @@ -1,5 +1,6 @@ "use client"; +import type { ParseKeys } from "i18next"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -27,30 +28,35 @@ import { LOG_FILTER_IDS, type LogsWindow } from "./useLogFilterLogic"; const ALL_VALUE = "all"; -const STATUS_FILTER_ITEMS = [ - { value: ALL_VALUE, label: "All Statuses" }, - { value: "success", label: "Success" }, - { value: "failure", label: "Failure" }, -] as const; +interface FilterItem { + value: string; + labelKey: ParseKeys; +} -const CACHE_FILTER_ITEMS = [ - { value: ALL_VALUE, label: "All Requests" }, - { value: "hit", label: "Cache Hit" }, - { value: "miss", label: "Cache Miss" }, -] as const; +const STATUS_FILTER_ITEMS: readonly FilterItem[] = [ + { value: ALL_VALUE, labelKey: "logs.request.filters.allStatuses" }, + { value: "success", labelKey: "logs.request.filters.statusOptions.success" }, + { value: "failure", labelKey: "logs.request.filters.statusOptions.failure" }, +]; -const CREDENTIAL_FILTER_ITEMS = [ - { value: ALL_VALUE, label: "All Credentials" }, - ...Object.entries(CREDENTIAL_LABELS).map(([value, label]) => ({ value, label })), -] as const; +const CACHE_FILTER_ITEMS: readonly FilterItem[] = [ + { value: ALL_VALUE, labelKey: "logs.request.filters.allRequests" }, + { value: "hit", labelKey: "logs.request.filters.cacheOptions.hit" }, + { value: "miss", labelKey: "logs.request.filters.cacheOptions.miss" }, +]; -const SPAN_TYPE_FILTER_ITEMS = [ - { value: ALL_VALUE, label: "All Types" }, - { value: "llm", label: "LLM" }, - { value: "agent", label: "Agent" }, - { value: "mcp", label: "MCP" }, - { value: "batch", label: "Batch" }, -] as const; +const CREDENTIAL_FILTER_ITEMS: readonly FilterItem[] = [ + { value: ALL_VALUE, labelKey: "logs.request.filters.allCredentials" }, + ...Object.entries(CREDENTIAL_LABELS).map(([value, labelKey]) => ({ value, labelKey })), +]; + +const SPAN_TYPE_FILTER_ITEMS: readonly FilterItem[] = [ + { value: ALL_VALUE, labelKey: "logs.request.filters.allTypes" }, + { value: "llm", labelKey: "logs.common.spanTypes.llm" }, + { value: "agent", labelKey: "logs.common.spanTypes.agent" }, + { value: "mcp", labelKey: "logs.common.spanTypes.mcp" }, + { value: "batch", labelKey: "logs.common.spanTypes.batch" }, +]; const PAGE_SIZE = 50; const SEARCH_INPUT_REASONS: ReadonlySet = new Set(["input-change", "input-clear", "clear-press"]); @@ -278,21 +284,26 @@ function ErrorCodeFilterField({ value, onChange }: { value: string; onChange: (v const { t } = useTranslation(); const [query, setQuery] = useState(""); + const errorCodeOptions = useMemo( + () => ERROR_CODE_OPTIONS.map(({ labelKey, value: code }) => ({ label: t(labelKey), value: code })), + [t], + ); + const options = useMemo(() => { const trimmed = query.trim(); const lowered = trimmed.toLowerCase(); - const matches = ERROR_CODE_OPTIONS.filter((option) => option.label.toLowerCase().includes(lowered)); - const isKnownCode = ERROR_CODE_OPTIONS.some( + const matches = errorCodeOptions.filter((option) => option.label.toLowerCase().includes(lowered)); + const isKnownCode = errorCodeOptions.some( (option) => option.value === trimmed || option.label.toLowerCase() === lowered, ); if (trimmed === "" || isKnownCode) return matches; return [...matches, { label: t("logs.request.filters.customErrorCode", { code: trimmed }), value: trimmed }]; - }, [query, t]); + }, [query, t, errorCodeOptions]); const selected = useMemo(() => { if (value === "") return null; - return ERROR_CODE_OPTIONS.find((option) => option.value === value) ?? { label: value, value }; - }, [value]); + return errorCodeOptions.find((option) => option.value === value) ?? { label: value, value }; + }, [value, errorCodeOptions]); const items = useMemo(() => { if (selected === null) return options; @@ -346,6 +357,11 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF const { t } = useTranslation(); const valueOf = (id: string): string => asString(get(id)); const setter = (id: string) => (next: string | undefined) => set(id, next); + const withLabel = ({ value, labelKey }: FilterItem) => ({ value, label: t(labelKey) }); + const spanTypeItems = SPAN_TYPE_FILTER_ITEMS.map(withLabel); + const statusItems = STATUS_FILTER_ITEMS.map(withLabel); + const cacheItems = CACHE_FILTER_ITEMS.map(withLabel); + const credentialItems = CREDENTIAL_FILTER_ITEMS.map(withLabel); return ( <> @@ -357,7 +373,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF set(LOG_FILTER_IDS.STATUS, next === null || next === ALL_VALUE ? undefined : next)} > @@ -386,7 +402,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF - {STATUS_FILTER_ITEMS.map((item) => ( + {statusItems.map((item) => ( {item.label} @@ -397,7 +413,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF set(LOG_FILTER_IDS.CREDENTIAL, next === null || next === ALL_VALUE ? undefined : next) @@ -428,7 +444,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF - {CREDENTIAL_FILTER_ITEMS.map((item) => ( + {credentialItems.map((item) => ( {item.label} diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsTable.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTable.tsx index 31b494448c2..01314fe3221 100644 --- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsTable.tsx +++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsTable.tsx @@ -1,6 +1,7 @@ "use client"; import type { ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table"; +import type { TFunction } from "i18next"; import { ScrollText } from "lucide-react"; import { useMemo, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; @@ -39,12 +40,12 @@ interface RequestLogsTableProps { toolbarChildren?: ReactNode; } -const formatFilterValue = (columnId: string, value: unknown): string => { +const formatFilterValue = (columnId: string, value: unknown, t: TFunction): string => { if (columnId === LOG_FILTER_IDS.SPAN_TYPE) { - return SPAN_TYPE_LABELS[String(value)] ?? String(value); + return Object.hasOwn(SPAN_TYPE_LABELS, String(value)) ? t(SPAN_TYPE_LABELS[String(value)]) : String(value); } if (columnId === LOG_FILTER_IDS.CREDENTIAL) { - return CREDENTIAL_LABELS[String(value)] ?? String(value); + return Object.hasOwn(CREDENTIAL_LABELS, String(value)) ? t(CREDENTIAL_LABELS[String(value)]) : String(value); } return Array.isArray(value) ? value.join(", ") : String(value); }; @@ -100,6 +101,11 @@ export function RequestLogsTable({ return getRequestLogsTableColumns({ onKeyHashClick, onSessionClick, resolveUserEmail }, t); }, [onKeyHashClick, onSessionClick, emailByUserId, t]); + const filterLabels = useMemo( + () => Object.fromEntries(Object.entries(LOG_FILTER_LABELS).map(([id, labelKey]) => [id, t(labelKey)])), + [t], + ); + const isFiltered = columnFilters.length > 0 || searchValue !== ""; return ( @@ -136,8 +142,8 @@ export function RequestLogsTable({ onRefresh={onRefresh} isRefreshing={isRefreshing} onOpenFilters={() => setFiltersOpen(true)} - filterLabels={LOG_FILTER_LABELS} - formatFilterValue={formatFilterValue} + filterLabels={filterLabels} + formatFilterValue={(columnId, value) => formatFilterValue(columnId, value, t)} showViewOptions={false} > {toolbarChildren} diff --git a/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.ts b/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.ts index 17d7eb4b860..f50780539ac 100644 --- a/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.ts +++ b/ui/litellm-dashboard/src/components/logs/request/useLogFilterLogic.ts @@ -1,4 +1,5 @@ import moment from "moment"; +import type { ParseKeys } from "i18next"; import { keepPreviousData, useQuery, type UseQueryOptions } from "@tanstack/react-query"; import type { ColumnFiltersState, PaginationState, SortingState } from "@tanstack/react-table"; import { uiSpendLogsCall } from "../../networking"; @@ -38,22 +39,22 @@ export const LOG_FILTER_IDS = { SEARCH: "search", } as const; -export const LOG_FILTER_LABELS: Record = { - [LOG_FILTER_IDS.TEAM_ID]: "Team ID", - [LOG_FILTER_IDS.SPAN_TYPE]: "Span Type", - [LOG_FILTER_IDS.STATUS]: "Status", - [LOG_FILTER_IDS.CACHE_STATUS]: "Cache", - [LOG_FILTER_IDS.CREDENTIAL]: "Credential", - [LOG_FILTER_IDS.KEY_ALIAS]: "Key Alias", - [LOG_FILTER_IDS.USER_ID]: "User ID", - [LOG_FILTER_IDS.END_USER]: "End User", - [LOG_FILTER_IDS.ERROR_CODE]: "Error Code", - [LOG_FILTER_IDS.ERROR_MESSAGE]: "Error Message", - [LOG_FILTER_IDS.KEY_HASH]: "Key Hash", - [LOG_FILTER_IDS.SESSION_ID]: "Session ID", - [LOG_FILTER_IDS.MODEL_ID]: "Model", - [LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL]: "Public model / search tool", - [LOG_FILTER_IDS.SEARCH]: "Search", +export const LOG_FILTER_LABELS: Record = { + [LOG_FILTER_IDS.TEAM_ID]: "logs.request.filters.teamId", + [LOG_FILTER_IDS.SPAN_TYPE]: "logs.request.filters.spanType", + [LOG_FILTER_IDS.STATUS]: "logs.request.filters.status", + [LOG_FILTER_IDS.CACHE_STATUS]: "logs.request.filters.cache", + [LOG_FILTER_IDS.CREDENTIAL]: "logs.request.filters.credential", + [LOG_FILTER_IDS.KEY_ALIAS]: "logs.request.filters.keyAlias", + [LOG_FILTER_IDS.USER_ID]: "logs.request.filters.userId", + [LOG_FILTER_IDS.END_USER]: "logs.request.filters.endUser", + [LOG_FILTER_IDS.ERROR_CODE]: "logs.request.filters.errorCode", + [LOG_FILTER_IDS.ERROR_MESSAGE]: "logs.request.filters.errorMessage", + [LOG_FILTER_IDS.KEY_HASH]: "logs.request.filters.keyHash", + [LOG_FILTER_IDS.SESSION_ID]: "logs.request.filters.sessionId", + [LOG_FILTER_IDS.MODEL_ID]: "logs.request.filters.model", + [LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL]: "logs.request.filters.publicModelOrSearchTool", + [LOG_FILTER_IDS.SEARCH]: "logs.request.filters.search", }; export interface LogsWindow { diff --git a/ui/litellm-dashboard/src/components/user_agent_activity.tsx b/ui/litellm-dashboard/src/components/user_agent_activity.tsx index 27e9f7373dc..6904d6b323d 100644 --- a/ui/litellm-dashboard/src/components/user_agent_activity.tsx +++ b/ui/litellm-dashboard/src/components/user_agent_activity.tsx @@ -1,3 +1,4 @@ +import type { ParseKeys } from "i18next"; import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { Bot, Users } from "lucide-react"; @@ -25,9 +26,9 @@ type ActivityView = "active-users" | "per-user"; type ActivePeriod = "dau" | "wau" | "mau"; const VIEW_OPTIONS = [ - { value: "active-users", label: "Active users" }, - { value: "per-user", label: "Per user" }, -] as const satisfies readonly { value: ActivityView; label: string }[]; + { value: "active-users", labelKey: "usage.userAgentActivity.viewOptions.activeUsers" }, + { value: "per-user", labelKey: "usage.userAgentActivity.viewOptions.perUser" }, +] as const satisfies readonly { value: ActivityView; labelKey: ParseKeys }[]; const PERIOD_OPTIONS = [ { value: "dau", label: "DAU" }, @@ -576,7 +577,7 @@ const UserAgentActivity: React.FC = ({ ({ value, label: t(labelKey) }))} onChange={setActivityView} /> diff --git a/ui/litellm-dashboard/src/i18n/locales/en.json b/ui/litellm-dashboard/src/i18n/locales/en.json index f9a7882b2de..2f61c51d819 100644 --- a/ui/litellm-dashboard/src/i18n/locales/en.json +++ b/ui/litellm-dashboard/src/i18n/locales/en.json @@ -12972,6 +12972,37 @@ }, "logs": { "common": { + "errorCodes": { + "badRequest": "400 - Bad Request", + "invalidAuthentication": "401 - Invalid Authentication", + "permissionDenied": "403 - Permission Denied", + "notFound": "404 - Not Found", + "requestTimeout": "408 - Request Timeout", + "unprocessableEntity": "422 - Unprocessable Entity", + "rateLimited": "429 - Rate Limited", + "internalServerError": "500 - Internal Server Error", + "badGateway": "502 - Bad Gateway", + "serviceUnavailable": "503 - Service Unavailable", + "overloaded": "529 - Overloaded" + }, + "spanTypes": { + "llm": "LLM", + "agent": "Agent", + "mcp": "MCP", + "batch": "Batch" + }, + "credentials": { + "clientOauthToken": "Client OAuth token", + "configuredKey": "Configured key" + }, + "quickSelect": { + "lastMinute": "Last Minute", + "last15Minutes": "Last 15 Minutes", + "lastHour": "Last Hour", + "last4Hours": "Last 4 Hours", + "last24Hours": "Last 24 Hours", + "last7Days": "Last 7 Days" + }, "podRouting": { "roundRobin": "Round robin", "sessionAffinity": "Session affinity", @@ -13377,6 +13408,7 @@ "note": "Note: This will only affect new requests after the configuration change." }, "tools": { + "fallbackName": "Tool {{index}}", "title": "Tools", "summary": "{{provided}} provided, {{called}} called", "called": "called", @@ -13509,6 +13541,15 @@ "filtersDescription": "Narrow down request logs" }, "filters": { + "search": "Search", + "statusOptions": { + "success": "Success", + "failure": "Failure" + }, + "cacheOptions": { + "hit": "Cache Hit", + "miss": "Cache Miss" + }, "teamId": "Team ID", "teamPlaceholder": "Search or select a team", "noTeams": "No teams found", @@ -13589,6 +13630,13 @@ } }, "audit": { + "actionOptions": { + "created": "Created", + "updated": "Updated", + "deleted": "Deleted", + "rotated": "Rotated", + "killSwitchFired": "Kill switch fired" + }, "panel": { "enterpriseTitle": "✨ Enterprise Feature.", "enterpriseDescription": "This is a LiteLLM Enterprise feature, and requires a valid key to use.", @@ -13973,6 +14021,14 @@ "weekOf": "Wk of {{date}}" }, "breakdownChart": { + "granularityOptions": { + "day": "Daily", + "week": "Weekly" + }, + "scaleOptions": { + "linear": "Linear", + "log": "Log" + }, "metricNoun": { "spend": "spend", "tokens": "tokens", @@ -13990,12 +14046,27 @@ "loadingTopModels": "Loading top models" }, "breakdown": { + "metricOptions": { + "spend": "Spend", + "tokens": "Tokens", + "requests": "Requests" + }, + "dimensionOptions": { + "modelGroups": "Model", + "models": "Deployment", + "providers": "Provider" + }, "metricLabel": "Metric", "groupByLabel": "Group by", "empty": "No usage in this range", "byProvider": "by {{provider}}" }, "topAgents": { + "metricOptions": { + "tokens": "Tokens", + "spend": "Spend", + "requests": "Requests" + }, "viewActivity": "View activity", "tokens": "{{tokens}} tokens", "viewAgentUsage": "View {{agent}} usage", @@ -14006,6 +14077,9 @@ "metricLabel": "Agent metric" }, "agentCatalog": { + "labels": { + "claudeUnspecified": "Claude (unspecified)" + }, "descriptions": { "claudeDesktop": "Anthropic's desktop assistant", "claudeCode": "Anthropic's coding agent", @@ -14033,6 +14107,28 @@ } }, "errors": { + "entityLabels": { + "key": { + "singular": "Virtual key", + "plural": "Virtual keys" + }, + "team": { + "singular": "Team", + "plural": "Teams" + }, + "user": { + "singular": "User", + "plural": "Users" + }, + "model": { + "singular": "Model", + "plural": "Models" + } + }, + "rateViews": { + "total": "Total", + "status": "By status code" + }, "entities": { "title": "Failures by identity", "subtitle": "Keys, teams, users and models ranked by failed requests", @@ -14165,6 +14261,17 @@ } }, "entitySummary": { + "titles": { + "ptuHours": "PTU Hours", + "totalCost": "Total Cost", + "totalSpend": "Total Spend", + "totalRequests": "Total Requests", + "successfulRequests": "Successful Requests", + "failedRequests": "Failed Requests", + "totalTokens": "Total Tokens", + "requestCost": "Request Cost", + "flatCost": "Flat Cost" + }, "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.", @@ -14174,6 +14281,14 @@ }, "entityUsage": { "chart": { + "granularityOptions": { + "day": "Daily", + "week": "Weekly" + }, + "scaleOptions": { + "linear": "Linear", + "log": "Log" + }, "flatCostSeries": "Flat cost", "metricTitle": { "spend": "Spend", @@ -14259,6 +14374,10 @@ "noUser": "(no user)" }, "topModelView": { + "viewOptions": { + "table": "Table View", + "chart": "Chart View" + }, "columns": { "model": "Model", "spendUsd": "Spend (USD)", @@ -14286,6 +14405,8 @@ "noData": "No endpoint usage data" }, "barChart": { + "successful": "Successful", + "failed": "Failed", "title": "Success vs Failed Requests by Endpoint" }, "lineChart": { @@ -14296,6 +14417,7 @@ "unknownModel": "Unknown model", "unknownProvider": "Unknown provider", "errors": { + "narrowRange": "Narrow the date range to fewer than 1,000 requests to see complete client usage", "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", @@ -14361,6 +14483,10 @@ "loadMoreFailed": "Could not load more keys." }, "topKeys": { + "viewOptions": { + "table": "Table View", + "chart": "Chart View" + }, "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", @@ -14554,6 +14680,10 @@ } }, "userAgentActivity": { + "viewOptions": { + "activeUsers": "Active users", + "perUser": "Per user" + }, "noActiveUsers": "No active users in this period", "weekLabel": "Week {{number}}", "monthLabel": "Month {{number}}", @@ -14695,6 +14825,10 @@ } } }, + "promptCacheUsage": { + "readTooltip": "Input tokens read from the LLM provider's prompt cache (e.g. Anthropic / OpenAI), billed at a discounted rate. Reported by the provider.", + "creationTooltip": "Input tokens written to the LLM provider's prompt cache for reuse by later requests." + }, "organizations": { "view": { "loading": "Loading...", diff --git a/ui/litellm-dashboard/src/utils/promptCacheUsage.ts b/ui/litellm-dashboard/src/utils/promptCacheUsage.ts index f7c25d43e5f..9209e60485c 100644 --- a/ui/litellm-dashboard/src/utils/promptCacheUsage.ts +++ b/ui/litellm-dashboard/src/utils/promptCacheUsage.ts @@ -1,7 +1,7 @@ -export const PROMPT_CACHE_READ_TOOLTIP = - "Input tokens read from the LLM provider's prompt cache (e.g. Anthropic / OpenAI), billed at a discounted rate. Reported by the provider."; -export const PROMPT_CACHE_CREATION_TOOLTIP = - "Input tokens written to the LLM provider's prompt cache for reuse by later requests."; +import type { ParseKeys } from "i18next"; + +export const PROMPT_CACHE_READ_TOOLTIP: ParseKeys = "promptCacheUsage.readTooltip"; +export const PROMPT_CACHE_CREATION_TOOLTIP: ParseKeys = "promptCacheUsage.creationTooltip"; interface CachedTokenDetails { cached_tokens?: number | null;