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.
This commit is contained in:
yuneng-jiang 2026-11-05 12:42:14 -08:00 • committed by GitHub
parent f069da8e0e
commit 41d45079c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 411 additions and 201 deletions

View file

@ -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<EndpointUsageBarChartProps> = ({ endpointD
{CATEGORIES.map((category, i) => (
<span key={category} className="flex items-center gap-1.5">
<span aria-hidden="true" className="size-2 rounded-[2px]" style={{ backgroundColor: COLORS[i] }} />
<span>{category}</span>
<span>{t(CATEGORY_LABELS[category])}</span>
<span className="tabular-nums text-foreground">{totals[category].toLocaleString()}</span>
</span>
))}

View file

@ -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<Record<string, string>> = {
"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<EntityUsageProps> = ({
const tileLabel = ({ title, tooltip, expandable }: SummaryTile) => (
<>
<span>{title}</span>
<span>{Object.hasOwn(SUMMARY_TILE_LABELS, title) ? t(SUMMARY_TILE_LABELS[title]) : title}</span>
{tooltip ? (
<Tooltip>
<TooltipTrigger render={<Info className="size-3.5 text-muted-foreground hover:text-foreground" />} />
@ -540,13 +541,13 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
<Segmented
label={t("usage.entityUsage.chart.bucketSize")}
value={granularity}
options={GRANULARITY_OPTIONS}
options={GRANULARITY_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={setGranularity}
/>
<Segmented
label={t("usage.entityUsage.chart.scaleLabel")}
value={scale}
options={SCALE_OPTIONS}
options={SCALE_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={setScale}
/>
</div>

View file

@ -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
<Segmented
label={t("usage.topModelView.viewModeLabel")}
value={modelViewMode}
options={VIEW_OPTIONS}
options={VIEW_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={setModelViewMode}
/>
</div>

View file

@ -20,6 +20,18 @@ interface SpendSummaryMetadata {
total_tokens: number;
}
export const SUMMARY_TILE_LABELS: Readonly<Record<string, ParseKeys>> = {
"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";

View file

@ -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
<Segmented
label={t("usage.errors.rateChart.viewLabel")}
value={rateView}
options={ERROR_RATE_VIEW_OPTIONS}
options={ERROR_RATE_VIEW_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={setRateView}
/>
}
@ -280,7 +280,7 @@ export default function ErrorsTab({ activity, loading, failed, userScoped = fals
<Segmented
label={t("usage.errors.entities.groupByLabel")}
value={kind}
options={ENTITY_OPTIONS}
options={ENTITY_OPTIONS.map(({ value, labelKey }) => ({ 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
}
>
<Table aria-label={t("usage.errors.entities.tableLabel", { entities: ERROR_ENTITY_LABEL[kind].plural })}>
<Table aria-label={t("usage.errors.entities.tableLabel", { entities: t(ERROR_ENTITY_LABEL[kind].plural) })}>
<TableHeader>
<TableRow>
<TableHead>{ERROR_ENTITY_LABEL[kind].singular}</TableHead>
<TableHead>{t(ERROR_ENTITY_LABEL[kind].singular)}</TableHead>
<TableHead className="text-right">{t("usage.errors.entities.columns.requests")}</TableHead>
<TableHead className="text-right">{t("usage.errors.entities.columns.failed")}</TableHead>
<TableHead>{t("usage.errors.entities.columns.errorRate")}</TableHead>

View file

@ -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<ErrorEntityKind, { singular: string; plural: string }> = {
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<ErrorEntityKind, { singular: ParseKeys; plural: ParseKeys }> = {
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<number, ParseKeys> = {
@ -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[] =>

View file

@ -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({
<Segmented
label={t("usage.breakdown.metricLabel")}
value={state.metric}
options={METRIC_OPTIONS}
options={METRIC_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={(metric) => onChange({ ...state, metric })}
/>
{showDimension && (
<Segmented
label={t("usage.breakdown.groupByLabel")}
value={state.dimension}
options={DIMENSION_OPTIONS}
options={DIMENSION_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={(dimension) => onChange({ ...state, dimension })}
/>
)}

View file

@ -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<Record<string, { text: string; className: string }>> = {
@ -42,7 +43,9 @@ const CLIENT_SURFACES: Readonly<Partial<Record<string, typeof Monitor>>> = {
};
function AgentGlyph({ agent }: { agent: Pick<AgentRow, "id" | "label" | "logo"> }) {
if (agent.logo) return <Logo src={agent.logo} label={agent.label} className="size-6 object-contain" />;
const { t } = useTranslation();
const label = agentDisplayLabel(agent.label, t);
if (agent.logo) return <Logo src={agent.logo} label={label} className="size-6 object-contain" />;
const monogram = MONOGRAMS[agent.id];
if (monogram) {
return (
@ -56,7 +59,7 @@ function AgentGlyph({ agent }: { agent: Pick<AgentRow, "id" | "label" | "logo">
</span>
);
}
return <span className="text-sm font-semibold uppercase text-muted-foreground">{agent.label.charAt(0)}</span>;
return <span className="text-sm font-semibold uppercase text-muted-foreground">{label.charAt(0)}</span>;
}
export function AgentMark({ agent }: { agent: Pick<AgentRow, "id" | "label" | "logo"> }) {
@ -103,7 +106,7 @@ function AgentItem({
)}
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-medium text-foreground">{agent.label}</span>
<span className="block truncate text-sm font-medium text-foreground">{agentDisplayLabel(agent.label, t)}</span>
{openRow ? (
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground group-hover:text-foreground">
{t("usage.topAgents.viewActivity")}
@ -130,7 +133,7 @@ function AgentItem({
{openRow ? (
<button
type="button"
aria-label={t("usage.topAgents.viewAgentUsage", { agent: agent.label })}
aria-label={t("usage.topAgents.viewAgentUsage", { agent: agentDisplayLabel(agent.label, t) })}
onClick={openRow}
className={cn(
rowClassName,
@ -208,7 +211,12 @@ export default function TopAgents({
subtitle={subtitle}
testId="top-agents"
action={
<Segmented label={t("usage.topAgents.metricLabel")} value={metric} options={METRICS} onChange={setMetric} />
<Segmented
label={t("usage.topAgents.metricLabel")}
value={metric}
options={METRICS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={setMetric}
/>
}
bodyClassName="px-0 pt-4 pb-0"
>
@ -217,7 +225,7 @@ export default function TopAgents({
<StackedUsageChart
data={series.data}
series={series.keys}
labels={series.labels}
labels={series.labels.map((label) => agentDisplayLabel(label, t))}
colors={series.colors}
xKey="date"
xLabel={(date) => labelForDate(series, date)}

View file

@ -25,13 +25,13 @@ const METRIC_NOUN: Record<UsageMetric, ParseKeys> = {
requests: "usage.breakdownChart.metricNoun.requests",
};
const GRANULARITY_OPTIONS = [
{ value: "day", label: "Daily" },
{ value: "week", label: "Weekly" },
] as const satisfies readonly { value: Granularity; label: string }[];
{ value: "day", labelKey: "usage.breakdownChart.granularityOptions.day" },
{ value: "week", labelKey: "usage.breakdownChart.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.breakdownChart.scaleOptions.linear" },
{ value: "log", labelKey: "usage.breakdownChart.scaleOptions.log" },
] as const satisfies readonly { value: StackedUsageScale; labelKey: ParseKeys }[];
interface UsageBreakdownChartProps {
results: readonly DailyData[];
@ -80,13 +80,13 @@ export default function UsageBreakdownChart({
<Segmented
label={t("usage.breakdownChart.bucketSize")}
value={granularity}
options={GRANULARITY_OPTIONS}
options={GRANULARITY_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={setGranularity}
/>
<Segmented
label={t("usage.breakdownChart.scaleLabel")}
value={scale}
options={SCALE_OPTIONS}
options={SCALE_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={setScale}
/>
</>

View file

@ -11,7 +11,7 @@ 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 { ParseKeys, TFunction } from "i18next";
import type { components } from "@/lib/http/schema";
import i18n from "@/i18n/i18n";
import type { DailyData, SpendMetrics } from "@/components/UsagePage/types";
@ -240,6 +240,13 @@ const FAMILIES: readonly AgentFamily[] = [
},
];
const AGENT_LABEL_KEYS: Readonly<Record<string, ParseKeys>> = {
"Claude (unspecified)": "usage.agentCatalog.labels.claudeUnspecified",
};
export const agentDisplayLabel = (label: string, t: TFunction): string =>
Object.hasOwn(AGENT_LABEL_KEYS, label) ? t(AGENT_LABEL_KEYS[label]) : label;
const USER_AGENT_PREFIX = /^user[- ]agent:\s*/i;
export const isUserAgentTag = (tag: string): boolean => USER_AGENT_PREFIX.test(tag.trim());

View file

@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import i18n from "@/i18n/i18n";
import {
clientUsageFromLogs,
identifyUsageClient,
@ -115,7 +116,7 @@ describe("client usage from request logs", () => {
"should refuse totals for a partial page %j",
(overrides) => {
const result = clientUsageFromLogs(page([log()], overrides), scope);
expect(result.error).toBe(INCOMPLETE_CLIENT_USAGE);
expect(result.error).toBe(i18n.t(INCOMPLETE_CLIENT_USAGE));
expect(result.data.results).toEqual([]);
expect(result.data.metadata?.total_spend).toBe(0);
},

View file

@ -1,3 +1,4 @@
import type { ParseKeys } from "i18next";
import { z } from "zod";
import {
EMPTY_DAILY_ACTIVITY_METADATA,
@ -15,8 +16,7 @@ type SpendMetrics = components["schemas"]["SpendMetrics"];
export { UsageClient } from "../components/overview/agentCatalog";
export const CLIENT_REQUEST_LIMIT = 1000;
export const INCOMPLETE_CLIENT_USAGE =
"Narrow the date range to fewer than 1,000 requests to see complete client usage";
export const INCOMPLETE_CLIENT_USAGE: ParseKeys = "usage.clientUsage.errors.narrowRange";
const count = z.number().int().nonnegative();
const cacheDetailsSchema = z.object({
@ -191,7 +191,7 @@ export function clientUsageFromLogs(response: unknown, scope: ClientUsageScope):
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);
return emptyClientUsage(i18n.t(INCOMPLETE_CLIENT_USAGE));
}
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)) {

View file

@ -20,9 +20,9 @@ const LIMIT_OPTIONS = TOP_KEYS_LIMITS.map((limit) => ({ value: String(limit), la
type ViewMode = "table" | "chart";
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.topKeys.viewOptions.table" },
{ value: "chart", labelKey: "usage.topKeys.viewOptions.chart" },
] as const satisfies readonly { value: ViewMode; labelKey: ParseKeys }[];
const BAR_COLOR = "#2b3fd6";
const QUIET_HEADER = { headerClassName: "font-normal" };
@ -226,7 +226,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
<Segmented
label={t("usage.topKeys.viewLabel")}
value={viewMode}
options={VIEW_OPTIONS}
options={VIEW_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={setViewMode}
/>
</div>

View file

@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import {
ArrowDownToLine,
ArrowUpFromLine,
@ -68,6 +69,7 @@ function ResponseCacheIndicator() {
}
function PromptCacheChips({ usage }: { usage?: TokenUsage }) {
const { t } = useTranslation();
if (usage?.servedFromResponseCache) {
return <ResponseCacheIndicator />;
}
@ -80,7 +82,7 @@ function PromptCacheChips({ usage }: { usage?: TokenUsage }) {
{readTokens > 0 && (
<MetricItem
label="Cache Read"
tooltip={PROMPT_CACHE_READ_TOOLTIP}
tooltip={t(PROMPT_CACHE_READ_TOOLTIP)}
icon={<Database className="size-3" aria-hidden="true" />}
value={String(readTokens)}
/>
@ -89,7 +91,7 @@ function PromptCacheChips({ usage }: { usage?: TokenUsage }) {
{creationTokens > 0 && (
<MetricItem
label="Cache Write"
tooltip={PROMPT_CACHE_CREATION_TOOLTIP}
tooltip={t(PROMPT_CACHE_CREATION_TOOLTIP)}
icon={<DatabaseBackup className="size-3" aria-hidden="true" />}
value={String(creationTokens)}
/>

View file

@ -1,7 +1,7 @@
"use client";
import { ColumnFiltersState, OnChangeFn, PaginationState } from "@tanstack/react-table";
import type { TFunction } from "i18next";
import type { ParseKeys, TFunction } from "i18next";
import { ScrollText } from "lucide-react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
@ -35,46 +35,52 @@ interface AuditLogsTableProps {
const ALL_VALUE = "all";
const ACTION_OPTIONS = [
{ label: "Created", value: "created" },
{ label: "Updated", value: "updated" },
{ label: "Deleted", value: "deleted" },
{ label: "Rotated", value: "rotated" },
{ label: "Kill switch fired", value: "kill_switch_fired" },
] as const;
interface FilterOption {
labelKey: ParseKeys;
value: string;
}
const TABLE_OPTIONS = [
{ label: "Keys", value: "LiteLLM_VerificationToken" },
{ label: "Teams", value: "LiteLLM_TeamTable" },
{ label: "Users", value: "LiteLLM_UserTable" },
{ label: "Organizations", value: "LiteLLM_OrganizationTable" },
{ label: "Models", value: "LiteLLM_ProxyModelTable" },
{ label: "Agents", value: "LiteLLM_AgentsTable" },
] as const;
const ACTION_FILTER_ITEMS = [
{ value: ALL_VALUE, label: "All Actions" },
...ACTION_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
const ACTION_OPTIONS: readonly FilterOption[] = [
{ labelKey: "logs.audit.actionOptions.created", value: "created" },
{ labelKey: "logs.audit.actionOptions.updated", value: "updated" },
{ labelKey: "logs.audit.actionOptions.deleted", value: "deleted" },
{ labelKey: "logs.audit.actionOptions.rotated", value: "rotated" },
{ labelKey: "logs.audit.actionOptions.killSwitchFired", value: "kill_switch_fired" },
];
const TABLE_FILTER_ITEMS = [
{ value: ALL_VALUE, label: "All Tables" },
...TABLE_OPTIONS.map((option) => ({ value: option.value, label: option.label })),
const TABLE_OPTIONS: readonly FilterOption[] = [
{ labelKey: "logs.audit.tableNames.keys", value: "LiteLLM_VerificationToken" },
{ labelKey: "logs.audit.tableNames.teams", value: "LiteLLM_TeamTable" },
{ labelKey: "logs.audit.tableNames.users", value: "LiteLLM_UserTable" },
{ labelKey: "logs.audit.tableNames.organizations", value: "LiteLLM_OrganizationTable" },
{ labelKey: "logs.audit.tableNames.models", value: "LiteLLM_ProxyModelTable" },
{ labelKey: "logs.audit.tableNames.agents", value: "LiteLLM_AgentsTable" },
];
const FILTER_LABELS: Record<string, string> = {
object_id: "Object ID",
changed_by: "Changed By",
team_id: "Team ID",
key_hash: "Key Hash",
action: "Action",
table_name: "Table",
const ACTION_FILTER_ITEMS: readonly FilterOption[] = [
{ value: ALL_VALUE, labelKey: "logs.audit.table.allActions" },
...ACTION_OPTIONS,
];
const TABLE_FILTER_ITEMS: readonly FilterOption[] = [
{ value: ALL_VALUE, labelKey: "logs.audit.table.allTables" },
...TABLE_OPTIONS,
];
const FILTER_LABELS: Record<string, ParseKeys> = {
object_id: "logs.audit.table.objectId",
changed_by: "logs.audit.table.changedBy",
team_id: "logs.audit.table.teamId",
key_hash: "logs.audit.table.keyHash",
action: "logs.audit.table.action",
table_name: "logs.audit.table.table",
};
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;
const action = ACTION_OPTIONS.find((option) => option.value === raw);
return action ? t(action.labelKey) : raw;
}
if (columnId === "table_name") {
return Object.hasOwn(AUDIT_TABLE_NAME_DISPLAY, raw) ? t(AUDIT_TABLE_NAME_DISPLAY[raw]) : raw;
@ -116,6 +122,11 @@ export function AuditLogsTable({
const { t } = useTranslation();
const [filtersOpen, setFiltersOpen] = useState(false);
const columns = useMemo(() => getAuditLogsTableColumns({ onViewLog }, t), [onViewLog, t]);
const filterLabels = useMemo(
() => Object.fromEntries(Object.entries(FILTER_LABELS).map(([id, labelKey]) => [id, t(labelKey)])),
[t],
);
const withLabel = ({ value, labelKey }: FilterOption) => ({ value, label: t(labelKey) });
const hasActiveSearch = Boolean(searchValue?.trim());
return (
@ -145,7 +156,7 @@ export function AuditLogsTable({
onRefresh={onRefresh}
isRefreshing={isRefreshing}
onOpenFilters={() => setFiltersOpen(true)}
filterLabels={FILTER_LABELS}
filterLabels={filterLabels}
formatFilterValue={formatFilterValue}
showViewOptions={false}
/>
@ -188,7 +199,7 @@ export function AuditLogsTable({
</DataTableFilterField>
<DataTableFilterField label={t("logs.audit.table.action")}>
<Select
items={ACTION_FILTER_ITEMS}
items={ACTION_FILTER_ITEMS.map(withLabel)}
value={(get("action") as string) ?? ALL_VALUE}
onValueChange={(value) => set("action", value === ALL_VALUE ? undefined : value)}
>
@ -199,7 +210,7 @@ export function AuditLogsTable({
<SelectItem value={ALL_VALUE}>{t("logs.audit.table.allActions")}</SelectItem>
{ACTION_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
{t(option.labelKey)}
</SelectItem>
))}
</SelectContent>
@ -207,7 +218,7 @@ export function AuditLogsTable({
</DataTableFilterField>
<DataTableFilterField label={t("logs.audit.table.table")}>
<Select
items={TABLE_FILTER_ITEMS}
items={TABLE_FILTER_ITEMS.map(withLabel)}
value={(get("table_name") as string) ?? ALL_VALUE}
onValueChange={(value) => set("table_name", value === ALL_VALUE ? undefined : value)}
>
@ -218,7 +229,7 @@ export function AuditLogsTable({
<SelectItem value={ALL_VALUE}>{t("logs.audit.table.allTables")}</SelectItem>
{TABLE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
{t(option.labelKey)}
</SelectItem>
))}
</SelectContent>

View file

@ -1,15 +1,17 @@
export const ERROR_CODE_OPTIONS: { label: string; value: string }[] = [
{ label: "400 - Bad Request", value: "400" },
{ label: "401 - Invalid Authentication", value: "401" },
{ label: "403 - Permission Denied", value: "403" },
{ label: "404 - Not Found", value: "404" },
{ label: "408 - Request Timeout", value: "408" },
{ label: "422 - Unprocessable Entity", value: "422" },
{ label: "429 - Rate Limited", value: "429" },
{ label: "500 - Internal Server Error", value: "500" },
{ label: "502 - Bad Gateway", value: "502" },
{ label: "503 - Service Unavailable", value: "503" },
{ label: "529 - Overloaded", value: "529" },
import type { ParseKeys } from "i18next";
export const ERROR_CODE_OPTIONS: { labelKey: ParseKeys; value: string }[] = [
{ labelKey: "logs.common.errorCodes.badRequest", value: "400" },
{ labelKey: "logs.common.errorCodes.invalidAuthentication", value: "401" },
{ labelKey: "logs.common.errorCodes.permissionDenied", value: "403" },
{ labelKey: "logs.common.errorCodes.notFound", value: "404" },
{ labelKey: "logs.common.errorCodes.requestTimeout", value: "408" },
{ labelKey: "logs.common.errorCodes.unprocessableEntity", value: "422" },
{ labelKey: "logs.common.errorCodes.rateLimited", value: "429" },
{ labelKey: "logs.common.errorCodes.internalServerError", value: "500" },
{ labelKey: "logs.common.errorCodes.badGateway", value: "502" },
{ labelKey: "logs.common.errorCodes.serviceUnavailable", value: "503" },
{ labelKey: "logs.common.errorCodes.overloaded", value: "529" },
];
/** Call types that represent MCP tool invocations (shared across columns, index, drawer). */
@ -21,16 +23,16 @@ export const AGENT_CALL_TYPES = ["asend_message"];
/** Call types that represent Batch API operations (creation and retrieval, sync and async). */
export const BATCH_CALL_TYPES = ["acreate_batch", "create_batch", "aretrieve_batch", "retrieve_batch"];
export const SPAN_TYPE_LABELS: Record<string, string> = {
llm: "LLM",
agent: "Agent",
mcp: "MCP",
batch: "Batch",
export const SPAN_TYPE_LABELS: Record<string, ParseKeys> = {
llm: "logs.common.spanTypes.llm",
agent: "logs.common.spanTypes.agent",
mcp: "logs.common.spanTypes.mcp",
batch: "logs.common.spanTypes.batch",
};
export const CREDENTIAL_LABELS: Record<string, string> = {
true: "Client OAuth token",
false: "Configured key",
export const CREDENTIAL_LABELS: Record<string, ParseKeys> = {
true: "logs.common.credentials.clientOauthToken",
false: "logs.common.credentials.configuredKey",
};
export const KUBERNETES_POD_ROUTING_LABELS = {
@ -39,11 +41,11 @@ export const KUBERNETES_POD_ROUTING_LABELS = {
session_affinity_retry: "logs.common.podRouting.sessionAffinityRetry",
} as const;
export const QUICK_SELECT_OPTIONS: { label: string; value: number; unit: string }[] = [
{ label: "Last Minute", value: 1, unit: "minutes" },
{ label: "Last 15 Minutes", value: 15, unit: "minutes" },
{ label: "Last Hour", value: 1, unit: "hours" },
{ label: "Last 4 Hours", value: 4, unit: "hours" },
{ label: "Last 24 Hours", value: 24, unit: "hours" },
{ label: "Last 7 Days", value: 7, unit: "days" },
export const QUICK_SELECT_OPTIONS: { labelKey: ParseKeys; value: number; unit: string }[] = [
{ labelKey: "logs.common.quickSelect.lastMinute", value: 1, unit: "minutes" },
{ labelKey: "logs.common.quickSelect.last15Minutes", value: 15, unit: "minutes" },
{ labelKey: "logs.common.quickSelect.lastHour", value: 1, unit: "hours" },
{ labelKey: "logs.common.quickSelect.last4Hours", value: 4, unit: "hours" },
{ labelKey: "logs.common.quickSelect.last24Hours", value: 24, unit: "hours" },
{ labelKey: "logs.common.quickSelect.last7Days", value: 7, unit: "days" },
];

View file

@ -184,7 +184,7 @@ export function LogDetailContent({
)}
{typeof logEntry.metadata?.used_client_oauth_token === "boolean" && (
<DescriptionItem label={t("logs.detail.content.credential")}>
{CREDENTIAL_LABELS[String(logEntry.metadata.used_client_oauth_token)]}
{t(CREDENTIAL_LABELS[String(logEntry.metadata.used_client_oauth_token)])}
</DescriptionItem>
)}
{hasGuardrailData && (
@ -592,7 +592,7 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
label={
<MetricLabel
label={t("logs.detail.metrics.promptCacheReadTokens")}
tooltip={PROMPT_CACHE_READ_TOOLTIP}
tooltip={t(PROMPT_CACHE_READ_TOOLTIP)}
docsUrl={PROMPT_CACHE_DOCS_URL}
/>
}
@ -605,7 +605,7 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
label={
<MetricLabel
label={t("logs.detail.metrics.promptCacheCreationTokens")}
tooltip={PROMPT_CACHE_CREATION_TOOLTIP}
tooltip={t(PROMPT_CACHE_CREATION_TOOLTIP)}
docsUrl={PROMPT_CACHE_DOCS_URL}
/>
}

View file

@ -2,6 +2,7 @@
* Utility functions for parsing and processing tool data from log entries
*/
import i18n from "@/i18n/i18n";
import { LogEntry } from "../../../types";
import { ParsedTool, ToolDefinition, ToolCall } from "./types";
@ -148,7 +149,7 @@ export function parseToolsFromLog(log: LogEntry): ParsedTool[] {
// Parse each tool definition
// Handle both OpenAI format (tool.function.name) and Anthropic format (tool.name + tool.input_schema)
return requestTools.map((tool: any, index: number) => {
const name = tool.function?.name || tool.name || `Tool ${index + 1}`;
const name = tool.function?.name || tool.name || i18n.t("logs.detail.tools.fallbackName", { index: index + 1 });
const description = tool.function?.description || tool.description || "";
const parameters = tool.function?.parameters || tool.input_schema || {};

View file

@ -52,7 +52,7 @@ export function LogsTimeRangePicker({ value, onValueChange }: LogsTimeRangePicke
);
const displayLabel = value.isCustomDate
? getTimeRangeDisplay(true, value.startTime, value.endTime, t)
: selectedOption?.label;
: selectedOption && t(selectedOption.labelKey);
return (
<>
@ -69,7 +69,7 @@ export function LogsTimeRangePicker({ value, onValueChange }: LogsTimeRangePicke
<div className="space-y-1">
{QUICK_SELECT_OPTIONS.map((option) => (
<Button
key={option.label}
key={option.labelKey}
variant="ghost"
className="w-full justify-start font-normal"
onClick={() => {
@ -77,7 +77,7 @@ export function LogsTimeRangePicker({ value, onValueChange }: LogsTimeRangePicke
setOpen(false);
}}
>
{option.label}
{t(option.labelKey)}
</Button>
))}
<div className="my-2 border-t" />

View file

@ -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<string> = 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<SearchSelectOption[]>(
() => ERROR_CODE_OPTIONS.map(({ labelKey, value: code }) => ({ label: t(labelKey), value: code })),
[t],
);
const options = useMemo<SearchSelectOption[]>(() => {
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<SearchSelectOption | null>(() => {
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<SearchSelectOption[]>(() => {
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
<DataTableFilterField label={t("logs.request.filters.spanType")}>
<Select
items={SPAN_TYPE_FILTER_ITEMS}
items={spanTypeItems}
value={valueOf(LOG_FILTER_IDS.SPAN_TYPE) === "" ? ALL_VALUE : valueOf(LOG_FILTER_IDS.SPAN_TYPE)}
onValueChange={(next) =>
set(LOG_FILTER_IDS.SPAN_TYPE, next === null || next === ALL_VALUE ? undefined : next)
@ -367,7 +383,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
<SelectValue placeholder={t("logs.request.filters.allTypes")} />
</SelectTrigger>
<SelectContent>
{SPAN_TYPE_FILTER_ITEMS.map((item) => (
{spanTypeItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
@ -378,7 +394,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
<DataTableFilterField label={t("logs.request.filters.status")}>
<Select
items={STATUS_FILTER_ITEMS}
items={statusItems}
value={valueOf(LOG_FILTER_IDS.STATUS) === "" ? ALL_VALUE : valueOf(LOG_FILTER_IDS.STATUS)}
onValueChange={(next) => set(LOG_FILTER_IDS.STATUS, next === null || next === ALL_VALUE ? undefined : next)}
>
@ -386,7 +402,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
<SelectValue placeholder={t("logs.request.filters.allStatuses")} />
</SelectTrigger>
<SelectContent>
{STATUS_FILTER_ITEMS.map((item) => (
{statusItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
@ -397,7 +413,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
<DataTableFilterField label={t("logs.request.filters.cache")}>
<Select
items={CACHE_FILTER_ITEMS}
items={cacheItems}
value={valueOf(LOG_FILTER_IDS.CACHE_STATUS) === "" ? ALL_VALUE : valueOf(LOG_FILTER_IDS.CACHE_STATUS)}
onValueChange={(next) =>
set(LOG_FILTER_IDS.CACHE_STATUS, next === null || next === ALL_VALUE ? undefined : next)
@ -407,7 +423,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
<SelectValue placeholder={t("logs.request.filters.allRequests")} />
</SelectTrigger>
<SelectContent>
{CACHE_FILTER_ITEMS.map((item) => (
{cacheItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
@ -418,7 +434,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
<DataTableFilterField label={t("logs.request.filters.credential")}>
<Select
items={CREDENTIAL_FILTER_ITEMS}
items={credentialItems}
value={valueOf(LOG_FILTER_IDS.CREDENTIAL) === "" ? ALL_VALUE : valueOf(LOG_FILTER_IDS.CREDENTIAL)}
onValueChange={(next) =>
set(LOG_FILTER_IDS.CREDENTIAL, next === null || next === ALL_VALUE ? undefined : next)
@ -428,7 +444,7 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
<SelectValue placeholder={t("logs.request.filters.allCredentials")} />
</SelectTrigger>
<SelectContent>
{CREDENTIAL_FILTER_ITEMS.map((item) => (
{credentialItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>

View file

@ -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}

View file

@ -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<string, string> = {
[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<string, ParseKeys> = {
[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 {

View file

@ -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<UserAgentActivityProps> = ({
<Segmented
label={t("usage.userAgentActivity.activity.viewLabel")}
value={activityView}
options={VIEW_OPTIONS}
options={VIEW_OPTIONS.map(({ value, labelKey }) => ({ value, label: t(labelKey) }))}
onChange={setActivityView}
/>
</>

View file

@ -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...",

View file

@ -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;