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;