mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
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:
parent
f069da8e0e
commit
41d45079c7
25 changed files with 411 additions and 201 deletions
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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[] =>
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 || {};
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue