feat(ui): i18n for usage page batch 1

This commit is contained in:
吹雪 2026-06-11 01:15:12 +09:00
parent 08f5303270
commit e4f613b5df
9 changed files with 408 additions and 110 deletions

View file

@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { BarChart, Card, Title } from "@tremor/react";
import { CustomLegend, CustomTooltip } from "@/components/common_components/chartUtils";
import { MetricWithMetadata } from "../../../types";
@ -8,6 +9,7 @@ interface EndpointUsageBarChartProps {
}
const EndpointUsageBarChart: React.FC<EndpointUsageBarChartProps> = ({ endpointData }) => {
const { t } = useTranslation();
const dataToUse = endpointData || {};
// Transform endpoint data into chart format
@ -28,7 +30,7 @@ const EndpointUsageBarChart: React.FC<EndpointUsageBarChartProps> = ({ endpointD
return (
<Card>
<div className="flex justify-between items-center">
<Title>Success vs Failed Requests by Endpoint</Title>
<Title>{t("usagePage.endpointUsageBarChart.title")}</Title>
<CustomLegend
categories={["metrics.successful_requests", "metrics.failed_requests"]}
colors={["green", "red"]}

View file

@ -1,5 +1,6 @@
import { Card, LineChart, Title } from "@tremor/react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { DailyData } from "../../../types";
interface EndpointUsageLineChartProps {
@ -43,6 +44,7 @@ function transformDailyDataToChart(dailyData: DailyData[]): Array<Record<string,
}
export function EndpointUsageLineChart({ dailyData, endpointData }: EndpointUsageLineChartProps) {
const { t } = useTranslation();
const chartData = useMemo(() => {
if (!dailyData?.results || dailyData.results.length === 0) {
return [];
@ -64,7 +66,7 @@ export function EndpointUsageLineChart({ dailyData, endpointData }: EndpointUsag
return (
<Card className="mb-6">
<div className="flex items-center justify-between mb-4">
<Title>Endpoint Usage Trends</Title>
<Title>{t("usagePage.endpointUsageLineChart.title")}</Title>
</div>
<LineChart
className="h-80"

View file

@ -1,4 +1,5 @@
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { Trans, useTranslation } from "react-i18next";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import {
BarChart,
@ -101,6 +102,7 @@ const ENTITY_FETCH_FNS: Record<EntityType, (...args: any[]) => Promise<any>> = {
};
const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, entityId, entityList, dateValue }) => {
const { t } = useTranslation();
const { teams } = useTeams();
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [topKeysLimit, setTopKeysLimit] = useState<number>(5);
@ -386,11 +388,11 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
};
const getFilterLabel = (entityType: string) => {
return `Filter by ${entityType}`;
return t("usagePage.entityUsage.filterLabel", { entityType });
};
const getFilterPlaceholder = (entityType: string) => {
return `Select ${entityType} to filter...`;
return t("usagePage.entityUsage.filterPlaceholder", { entityType });
};
const capitalizedEntityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);
@ -406,16 +408,20 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
<div className="flex items-center justify-between">
<span>
<LoadingOutlined spin className="mr-2" />
Currently fetching spend data: fetched {progress.currentPage} / {progress.totalPages} pages. Charts will
update periodically as data loads. Moving off of this page will stop and reset this. To continue using
the UI in the meantime,{" "}
<a href={window.location.href} target="_blank" rel="noopener noreferrer">
open a new tab <ExportOutlined />
</a>
.
<Trans
i18nKey="usagePage.entityUsage.fetchingSpendData"
values={{ currentPage: progress.currentPage, totalPages: progress.totalPages }}
components={{
a: (
<a href={window.location.href} target="_blank" rel="noopener noreferrer">
<ExportOutlined />
</a>
),
}}
/>
</span>
<Button type="primary" danger onClick={cancel}>
Stop
{t("usagePage.entityUsage.stopButton")}
</Button>
</div>
}
@ -428,7 +434,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
className="mb-2"
message={
<span>
Showing partial data ({progress.currentPage}/{progress.totalPages} pages loaded)
{t("usagePage.entityUsage.partialData", {
currentPage: progress.currentPage,
totalPages: progress.totalPages,
})}
</span>
}
/>
@ -442,16 +451,20 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
<div className="flex items-center justify-between">
<span>
<LoadingOutlined spin className="mr-2" />
Currently fetching agent data: fetched {agentProgress.currentPage} / {agentProgress.totalPages} pages.
Charts will update periodically as data loads. Moving off of this page will stop and reset this. To
continue using the UI in the meantime,{" "}
<a href={window.location.href} target="_blank" rel="noopener noreferrer">
open a new tab <ExportOutlined />
</a>
.
<Trans
i18nKey="usagePage.entityUsage.fetchingAgentData"
values={{ currentPage: agentProgress.currentPage, totalPages: agentProgress.totalPages }}
components={{
a: (
<a href={window.location.href} target="_blank" rel="noopener noreferrer">
<ExportOutlined />
</a>
),
}}
/>
</span>
<Button type="primary" danger onClick={agentCancel}>
Stop
{t("usagePage.entityUsage.stopButton")}
</Button>
</div>
}
@ -464,14 +477,17 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
className="mb-2"
message={
<span>
Showing partial agent data ({agentProgress.currentPage}/{agentProgress.totalPages} pages loaded)
{t("usagePage.entityUsage.partialAgentData", {
currentPage: agentProgress.currentPage,
totalPages: agentProgress.totalPages,
})}
</span>
}
/>
)}
{entityType === "team" && (
<div className="mb-4">
<Text className="mb-2">Filter by team</Text>
<Text className="mb-2">{t("usagePage.entityUsage.filterByTeam")}</Text>
<TeamMultiSelect value={selectedTags} onChange={setSelectedTags} />
</div>
)}
@ -490,11 +506,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
/>
<TabGroup>
<TabList variant="solid" className="mt-1">
<Tab>Cost</Tab>
<Tab>{entityType === "agent" ? "Request / Token Consumption" : "Model Activity"}</Tab>
{entityType === "team" ? <Tab>Agent Activity</Tab> : <></>}
<Tab>Key Activity</Tab>
<Tab>Endpoint Activity</Tab>
<Tab>{t("usagePage.entityUsage.tabCost")}</Tab>
<Tab>
{entityType === "agent"
? t("usagePage.entityUsage.tabRequestTokenConsumption")
: t("usagePage.entityUsage.tabModelActivity")}
</Tab>
{entityType === "team" ? <Tab>{t("usagePage.entityUsage.tabAgentActivity")}</Tab> : <></>}
<Tab>{t("usagePage.entityUsage.tabKeyActivity")}</Tab>
<Tab>{t("usagePage.entityUsage.tabEndpointActivity")}</Tab>
</TabList>
<TabPanels>
<TabPanel>
@ -502,34 +522,34 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
{/* Total Spend Card */}
<Col numColSpan={2}>
<Card>
<Title>{capitalizedEntityLabel} Spend Overview</Title>
<Title>{t("usagePage.entityUsage.spendOverview", { entity: capitalizedEntityLabel })}</Title>
<Grid numItems={5} className="gap-4 mt-4">
<Card>
<Title>Total Spend</Title>
<Title>{t("usagePage.entityUsage.totalSpend")}</Title>
<Text className="text-2xl font-bold mt-2">
${formatNumberWithCommas(spendData.metadata.total_spend, 2)}
</Text>
</Card>
<Card>
<Title>Total Requests</Title>
<Title>{t("usagePage.entityUsage.totalRequests")}</Title>
<Text className="text-2xl font-bold mt-2">
{spendData.metadata.total_api_requests.toLocaleString()}
</Text>
</Card>
<Card>
<Title>Successful Requests</Title>
<Title>{t("usagePage.entityUsage.successfulRequests")}</Title>
<Text className="text-2xl font-bold mt-2 text-green-600">
{spendData.metadata.total_successful_requests.toLocaleString()}
</Text>
</Card>
<Card>
<Title>Failed Requests</Title>
<Title>{t("usagePage.entityUsage.failedRequests")}</Title>
<Text className="text-2xl font-bold mt-2 text-red-600">
{spendData.metadata.total_failed_requests.toLocaleString()}
</Text>
</Card>
<Card>
<Title>Total Tokens</Title>
<Title>{t("usagePage.entityUsage.totalTokens")}</Title>
<Text className="text-2xl font-bold mt-2">
{spendData.metadata.total_tokens.toLocaleString()}
</Text>
@ -541,7 +561,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
{/* Daily Spend Chart */}
<Col numColSpan={2}>
<Card>
<Title>Daily Spend</Title>
<Title>{t("usagePage.entityUsage.dailySpend")}</Title>
<BarChart
data={[...spendData.results].sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
@ -559,16 +579,37 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
return (
<div className="bg-white p-4 shadow-lg rounded-lg border">
<p className="font-bold">{data.date}</p>
<p className="text-cyan-500">Total Spend: ${formatNumberWithCommas(data.metrics.spend, 2)}</p>
<p className="text-gray-600">Total Requests: {data.metrics.api_requests}</p>
<p className="text-gray-600">Successful: {data.metrics.successful_requests}</p>
<p className="text-gray-600">Failed: {data.metrics.failed_requests}</p>
<p className="text-gray-600">Total Tokens: {data.metrics.total_tokens}</p>
<p className="text-cyan-500">
{t("usagePage.entityUsage.tooltipTotalSpend", {
amount: formatNumberWithCommas(data.metrics.spend, 2),
})}
</p>
<p className="text-gray-600">
Total {capitalizedEntityLabel}s: {entityCount}
{t("usagePage.entityUsage.tooltipTotalRequests", {
count: data.metrics.api_requests,
})}
</p>
<p className="text-gray-600">
{t("usagePage.entityUsage.tooltipSuccessful", {
count: data.metrics.successful_requests,
})}
</p>
<p className="text-gray-600">
{t("usagePage.entityUsage.tooltipFailed", { count: data.metrics.failed_requests })}
</p>
<p className="text-gray-600">
{t("usagePage.entityUsage.tooltipTotalTokens", { count: data.metrics.total_tokens })}
</p>
<p className="text-gray-600">
{t("usagePage.entityUsage.tooltipTotalEntities", {
entity: capitalizedEntityLabel,
count: entityCount,
})}
</p>
<div className="mt-2 border-t pt-2">
<p className="font-semibold">Spend by {capitalizedEntityLabel}:</p>
<p className="font-semibold">
{t("usagePage.entityUsage.tooltipSpendByEntity", { entity: capitalizedEntityLabel })}
</p>
{Object.entries(data.breakdown.entities || {})
.sort(([, a], [, b]) => {
const spendA = (a as EntityMetrics).metrics.spend;
@ -586,7 +627,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
);
})}
{entityCount > 5 && (
<p className="text-sm text-gray-500 italic">...and {entityCount - 5} more</p>
<p className="text-sm text-gray-500 italic">
{t("usagePage.entityUsage.tooltipAndMore", { count: entityCount - 5 })}
</p>
)}
</div>
</div>
@ -601,16 +644,21 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
<Card>
<div className="flex flex-col space-y-4">
<div className="flex flex-col space-y-2">
<Title>Spend Per {capitalizedEntityLabel}</Title>
<Subtitle className="text-xs">Showing Top 5 by Spend</Subtitle>
<Title>{t("usagePage.entityUsage.spendPerEntity", { entity: capitalizedEntityLabel })}</Title>
<Subtitle className="text-xs">{t("usagePage.entityUsage.showingTop5BySpend")}</Subtitle>
<div className="flex items-center text-sm text-gray-500">
<span>Get Started by Tracking cost per {capitalizedEntityLabel} </span>
<a
href="https://docs.litellm.ai/docs/proxy/enterprise#spend-tracking"
className="text-blue-500 hover:text-blue-700 ml-1"
>
here
</a>
<Trans
i18nKey="usagePage.entityUsage.getStartedTracking"
values={{ entity: capitalizedEntityLabel }}
components={{
a: (
<a
href="https://docs.litellm.ai/docs/proxy/enterprise#spend-tracking"
className="text-blue-500 hover:text-blue-700 ml-1"
/>
),
}}
/>
</div>
</div>
<Grid numItems={2} className="gap-6">
@ -631,13 +679,31 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
return (
<div className="bg-white p-4 shadow-lg rounded-lg border">
<p className="font-bold">{data.metadata.alias}</p>
<p className="text-cyan-500">Spend: ${formatNumberWithCommas(data.metrics.spend, 4)}</p>
<p className="text-gray-600">Requests: {data.metrics.api_requests.toLocaleString()}</p>
<p className="text-green-600">
Successful: {data.metrics.successful_requests.toLocaleString()}
<p className="text-cyan-500">
{t("usagePage.entityUsage.tooltipSpend", {
amount: formatNumberWithCommas(data.metrics.spend, 4),
})}
</p>
<p className="text-gray-600">
{t("usagePage.entityUsage.tooltipRequests", {
count: data.metrics.api_requests.toLocaleString(),
})}
</p>
<p className="text-green-600">
{t("usagePage.entityUsage.tooltipSuccessful", {
count: data.metrics.successful_requests.toLocaleString(),
})}
</p>
<p className="text-red-600">
{t("usagePage.entityUsage.tooltipFailed", {
count: data.metrics.failed_requests.toLocaleString(),
})}
</p>
<p className="text-gray-600">
{t("usagePage.entityUsage.tooltipTokens", {
count: data.metrics.total_tokens.toLocaleString(),
})}
</p>
<p className="text-red-600">Failed: {data.metrics.failed_requests.toLocaleString()}</p>
<p className="text-gray-600">Tokens: {data.metrics.total_tokens.toLocaleString()}</p>
</div>
);
}}
@ -649,10 +715,14 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
<TableHead>
<TableRow>
<TableHeaderCell>{capitalizedEntityLabel}</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
<TableHeaderCell>Tokens</TableHeaderCell>
<TableHeaderCell>{t("usagePage.entityUsage.colSpend")}</TableHeaderCell>
<TableHeaderCell className="text-green-600">
{t("usagePage.entityUsage.colSuccessful")}
</TableHeaderCell>
<TableHeaderCell className="text-red-600">
{t("usagePage.entityUsage.colFailed")}
</TableHeaderCell>
<TableHeaderCell>{t("usagePage.entityUsage.colTokens")}</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
@ -683,7 +753,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
{/* Top API Keys */}
<Col numColSpan={1}>
<Card>
<Title>Top Virtual Keys</Title>
<Title>{t("usagePage.entityUsage.topVirtualKeys")}</Title>
<TopKeyView
topKeys={getTopAPIKeys()}
teams={null}
@ -697,7 +767,11 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
{/* Top Models */}
<Col numColSpan={1}>
<Card>
<Title>{entityType === "agent" ? "Top Agents" : "Top Models"}</Title>
<Title>
{entityType === "agent"
? t("usagePage.entityUsage.topAgents")
: t("usagePage.entityUsage.topModels")}
</Title>
<TopModelView
topModels={getTopModels()}
topModelsLimit={topModelsLimit}
@ -710,7 +784,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
{entityType === "team" && (
<Col numColSpan={2}>
<Card>
<Title>Top Agents Driving Spend</Title>
<Title>{t("usagePage.entityUsage.topAgentsDrivingSpend")}</Title>
<TopModelView
topModels={getTopAgents()}
topModelsLimit={topAgentsLimit}
@ -724,7 +798,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
<Col numColSpan={2}>
<Card>
<div className="flex flex-col space-y-4">
<Title>Provider Usage</Title>
<Title>{t("usagePage.entityUsage.providerUsage")}</Title>
<Grid numItems={2}>
<Col numColSpan={1}>
<DonutChart
@ -740,11 +814,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
<TableHeaderCell>Tokens</TableHeaderCell>
<TableHeaderCell>{t("usagePage.entityUsage.colProvider")}</TableHeaderCell>
<TableHeaderCell>{t("usagePage.entityUsage.colSpend")}</TableHeaderCell>
<TableHeaderCell className="text-green-600">
{t("usagePage.entityUsage.colSuccessful")}
</TableHeaderCell>
<TableHeaderCell className="text-red-600">
{t("usagePage.entityUsage.colFailed")}
</TableHeaderCell>
<TableHeaderCell>{t("usagePage.entityUsage.colTokens")}</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
@ -755,7 +833,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({ accessToken, entityType, enti
{provider.provider && (
<img
src={getProviderLogoAndName(provider.provider).logo}
alt={`${provider.provider} logo`}
alt={t("usagePage.entityUsage.providerLogoAlt", {
provider: provider.provider,
})}
className="w-4 h-4"
onError={(e) => {
const target = e.target as HTMLImageElement;

View file

@ -1,4 +1,5 @@
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { useTranslation } from "react-i18next";
import { InfoCircleOutlined } from "@ant-design/icons";
import {
Card,
@ -35,6 +36,7 @@ interface SpendByProviderProps {
}
const SpendByProvider: React.FC<SpendByProviderProps> = ({ loading, isDateChanging, providerSpend }) => {
const { t } = useTranslation();
const [includeZeroSpend, setIncludeZeroSpend] = useState(false);
const [includeUnknown, setIncludeUnknown] = useState(false);
@ -58,16 +60,16 @@ const SpendByProvider: React.FC<SpendByProviderProps> = ({ loading, isDateChangi
return (
<Card className="h-full">
<div className="flex justify-between items-center mb-4">
<Title>Spend by Provider</Title>
<Title>{t("usagePage.spendByProvider.title")}</Title>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<label className="text-sm text-gray-700">Show Zero Spend</label>
<label className="text-sm text-gray-700">{t("usagePage.spendByProvider.showZeroSpend")}</label>
<Switch checked={includeZeroSpend} onChange={setIncludeZeroSpend} />
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<label className="text-sm text-gray-700">Show Unknown</label>
<Tooltip title="Requests that failed to route to a provider">
<label className="text-sm text-gray-700">{t("usagePage.spendByProvider.showUnknown")}</label>
<Tooltip title={t("usagePage.spendByProvider.unknownTooltip")}>
<InfoCircleOutlined className="text-gray-400 hover:text-gray-600" />
</Tooltip>
</div>
@ -93,11 +95,13 @@ const SpendByProvider: React.FC<SpendByProviderProps> = ({ loading, isDateChangi
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Spend</TableHeaderCell>
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
<TableHeaderCell>Tokens</TableHeaderCell>
<TableHeaderCell>{t("usagePage.spendByProvider.colProvider")}</TableHeaderCell>
<TableHeaderCell>{t("usagePage.spendByProvider.colSpend")}</TableHeaderCell>
<TableHeaderCell className="text-green-600">
{t("usagePage.spendByProvider.colSuccessful")}
</TableHeaderCell>
<TableHeaderCell className="text-red-600">{t("usagePage.spendByProvider.colFailed")}</TableHeaderCell>
<TableHeaderCell>{t("usagePage.spendByProvider.colTokens")}</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>

View file

@ -1,4 +1,5 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useTranslation } from "react-i18next";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/outline";
import { BarChart, Button } from "@tremor/react";
import { Segmented, Tooltip } from "antd";
@ -19,6 +20,7 @@ interface TopKeyViewProps {
}
const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = false, topKeysLimit, setTopKeysLimit }) => {
const { t } = useTranslation();
const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [selectedKey, setSelectedKey] = useState<string | null>(null);
@ -81,7 +83,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
// Define columns for the table view
const baseColumns = [
{
header: "Key ID",
header: t("usagePage.topKeyView.colKeyId"),
accessorKey: "api_key",
cell: (info: any) => (
<div className="overflow-hidden">
@ -99,14 +101,14 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
),
},
{
header: "Key Alias",
header: t("usagePage.topKeyView.colKeyAlias"),
accessorKey: "key_alias",
cell: (info: any) => info.getValue() || "-",
},
];
const tagsColumn = {
header: "Tags",
header: t("usagePage.topKeyView.colTags"),
accessorKey: "tags",
cell: (info: any) => {
const tags = info.getValue() as TagUsage[] | undefined;
@ -130,10 +132,10 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
title={
<div>
<div>
<span className="text-gray-300">Tag Name:</span> {tag.tag}
<span className="text-gray-300">{t("usagePage.topKeyView.tagName")}</span> {tag.tag}
</div>
<div>
<span className="text-gray-300">Spend:</span>{" "}
<span className="text-gray-300">{t("usagePage.topKeyView.tagSpend")}</span>{" "}
{tag.usage > 0 && tag.usage < 0.01 ? "<$0.01" : `$${formatNumberWithCommas(tag.usage, 2)}`}
</div>
</div>
@ -146,7 +148,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
<button
onClick={() => toggleTagsExpansion(apiKey)}
className="ml-1 p-1 hover:bg-gray-200 rounded-full transition-colors"
title={isExpanded ? "Show fewer tags" : "Show all tags"}
title={isExpanded ? t("usagePage.topKeyView.showFewerTags") : t("usagePage.topKeyView.showAllTags")}
>
{isExpanded ? (
<ChevronUpIcon className="h-3 w-3 text-gray-500" />
@ -162,7 +164,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
};
const spendColumn = {
header: "Spend (USD)",
header: t("usagePage.topKeyView.colSpendUsd"),
accessorKey: "spend",
cell: (info: any) => {
const value = info.getValue();
@ -195,13 +197,13 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
onClick={() => setViewMode("table")}
className={`px-3 py-1 text-sm rounded-md ${viewMode === "table" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
>
Table View
{t("usagePage.topKeyView.tableView")}
</button>
<button
onClick={() => setViewMode("chart")}
className={`px-3 py-1 text-sm rounded-md ${viewMode === "chart" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
>
Chart View
{t("usagePage.topKeyView.chartView")}
</button>
</div>
</div>
@ -228,15 +230,15 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
<div className="relative z-50 p-3 bg-black/90 shadow-lg rounded-lg text-white max-w-xs">
<div className="space-y-1.5">
<div className="text-sm">
<span className="text-gray-300">Key Alias: </span>
<span className="text-gray-300">{t("usagePage.topKeyView.tooltipKeyAlias")} </span>
<span className="font-mono text-gray-100 break-all">{item?.key_alias}</span>
</div>
<div className="text-sm">
<span className="text-gray-300">Key ID: </span>
<span className="text-gray-300">{t("usagePage.topKeyView.tooltipKeyId")} </span>
<span className="font-mono text-gray-100 break-all">{item?.api_key}</span>
</div>
<div className="text-sm">
<span className="text-gray-300">Spend: </span>
<span className="text-gray-300">{t("usagePage.topKeyView.tooltipSpend")} </span>
<span className="text-white font-medium">${formatNumberWithCommas(item?.spend, 2)}</span>
</div>
</div>

View file

@ -1,6 +1,7 @@
import { BarChart } from "@tremor/react";
import { Segmented } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { formatNumberWithCommas } from "../../../../utils/dataUtils";
import { DataTable } from "../../../view_logs/table";
@ -19,16 +20,17 @@ interface TopModelViewProps {
}
export default function TopModelView({ topModels, topModelsLimit, setTopModelsLimit }: TopModelViewProps) {
const { t } = useTranslation();
const [modelViewMode, setModelViewMode] = useState<"chart" | "table">("table");
const columns = [
{
header: "Model",
header: t("usagePage.topModelView.colModel"),
accessorKey: "key",
cell: (info: any) => info.getValue() || "-",
},
{
header: "Spend (USD)",
header: t("usagePage.topModelView.colSpendUsd"),
accessorKey: "spend",
cell: (info: any) => {
const value = info.getValue();
@ -36,17 +38,17 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
},
},
{
header: "Successful",
header: t("usagePage.topModelView.colSuccessful"),
accessorKey: "successful_requests",
cell: (info: any) => <span className="text-green-600">{info.getValue()?.toLocaleString() || 0}</span>,
},
{
header: "Failed",
header: t("usagePage.topModelView.colFailed"),
accessorKey: "failed_requests",
cell: (info: any) => <span className="text-red-600">{info.getValue()?.toLocaleString() || 0}</span>,
},
{
header: "Tokens",
header: t("usagePage.topModelView.colTokens"),
accessorKey: "tokens",
cell: (info: any) => info.getValue()?.toLocaleString() || 0,
},
@ -71,13 +73,13 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
onClick={() => setModelViewMode("table")}
className={`px-3 py-1 text-sm rounded-md ${modelViewMode === "table" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
>
Table View
{t("usagePage.topModelView.tableView")}
</button>
<button
onClick={() => setModelViewMode("chart")}
className={`px-3 py-1 text-sm rounded-md ${modelViewMode === "chart" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
>
Chart View
{t("usagePage.topModelView.chartView")}
</button>
</div>
</div>

View file

@ -2,7 +2,9 @@ import { formatNumberWithCommas } from "@/utils/dataUtils";
import { BarChart, Card, Title } from "@tremor/react";
import { Table } from "antd";
import type { ColumnsType } from "antd/es/table";
import React, { useState } from "react";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { TopModelData } from "../types";
interface KeyModelUsageViewProps {
@ -13,33 +15,33 @@ const VISIBLE_ROWS = 5;
// antd Table with size="small" has a row height of ~39px
const ANTD_SMALL_TABLE_ROW_HEIGHT = 39;
const columns: ColumnsType<TopModelData> = [
const getColumns = (t: TFunction): ColumnsType<TopModelData> => [
{
title: "Model",
title: t("usagePage.keyModelUsageView.colModel"),
dataIndex: "model",
key: "model",
render: (value) => value || "-",
},
{
title: "Spend (USD)",
title: t("usagePage.keyModelUsageView.colSpendUsd"),
dataIndex: "spend",
key: "spend",
render: (value) => `$${formatNumberWithCommas(value, 2)}`,
},
{
title: "Successful",
title: t("usagePage.keyModelUsageView.colSuccessful"),
dataIndex: "successful_requests",
key: "successful_requests",
render: (value) => <span className="text-green-600">{value?.toLocaleString() || 0}</span>,
},
{
title: "Failed",
title: t("usagePage.keyModelUsageView.colFailed"),
dataIndex: "failed_requests",
key: "failed_requests",
render: (value) => <span className="text-red-600">{value?.toLocaleString() || 0}</span>,
},
{
title: "Tokens",
title: t("usagePage.keyModelUsageView.colTokens"),
dataIndex: "tokens",
key: "tokens",
render: (value) => value?.toLocaleString() || 0,
@ -47,6 +49,8 @@ const columns: ColumnsType<TopModelData> = [
];
const KeyModelUsageView: React.FC<KeyModelUsageViewProps> = ({ topModels }) => {
const { t } = useTranslation();
const columns = useMemo(() => getColumns(t), [t]);
const [viewMode, setViewMode] = useState<"chart" | "table">("table");
if (topModels.length === 0) {
@ -56,19 +60,19 @@ const KeyModelUsageView: React.FC<KeyModelUsageViewProps> = ({ topModels }) => {
return (
<Card className="mt-4">
<div className="flex justify-between items-center mb-3">
<Title>Model Usage</Title>
<Title>{t("usagePage.keyModelUsageView.title")}</Title>
<div className="flex space-x-2">
<button
onClick={() => setViewMode("table")}
className={`px-3 py-1 text-sm rounded-md ${viewMode === "table" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
>
Table
{t("usagePage.keyModelUsageView.tableView")}
</button>
<button
onClick={() => setViewMode("chart")}
className={`px-3 py-1 text-sm rounded-md ${viewMode === "chart" ? "bg-blue-100 text-blue-700" : "bg-gray-100 text-gray-700"}`}
>
Chart
{t("usagePage.keyModelUsageView.chartView")}
</button>
</div>
</div>

View file

@ -4868,5 +4868,106 @@
"invalidPercentageRange": "Percentage must be between 0% and 1000%",
"invalidFixedAmount": "Fixed amount must be non-negative"
}
},
"usagePage": {
"endpointUsageBarChart": {
"title": "Success vs Failed Requests by Endpoint"
},
"endpointUsageLineChart": {
"title": "Endpoint Usage Trends"
},
"entityUsage": {
"filterLabel": "Filter by {{entityType}}",
"filterPlaceholder": "Select {{entityType}} to filter...",
"fetchingSpendData": "Currently fetching spend data: fetched {{currentPage}} / {{totalPages}} pages. Charts will update periodically as data loads. Moving off of this page will stop and reset this. To continue using the UI in the meantime, <a>open a new tab</a>.",
"partialData": "Showing partial data ({{currentPage}}/{{totalPages}} pages loaded)",
"fetchingAgentData": "Currently fetching agent data: fetched {{currentPage}} / {{totalPages}} pages. Charts will update periodically as data loads. Moving off of this page will stop and reset this. To continue using the UI in the meantime, <a>open a new tab</a>.",
"partialAgentData": "Showing partial agent data ({{currentPage}}/{{totalPages}} pages loaded)",
"filterByTeam": "Filter by team",
"stopButton": "Stop",
"tabCost": "Cost",
"tabRequestTokenConsumption": "Request / Token Consumption",
"tabModelActivity": "Model Activity",
"tabAgentActivity": "Agent Activity",
"tabKeyActivity": "Key Activity",
"tabEndpointActivity": "Endpoint Activity",
"spendOverview": "{{entity}} Spend Overview",
"totalSpend": "Total Spend",
"totalRequests": "Total Requests",
"successfulRequests": "Successful Requests",
"failedRequests": "Failed Requests",
"totalTokens": "Total Tokens",
"dailySpend": "Daily Spend",
"tooltipTotalSpend": "Total Spend: ${{amount}}",
"tooltipTotalRequests": "Total Requests: {{count}}",
"tooltipSuccessful": "Successful: {{count}}",
"tooltipFailed": "Failed: {{count}}",
"tooltipTotalTokens": "Total Tokens: {{count}}",
"tooltipTotalEntities": "Total {{entity}}s: {{count}}",
"tooltipSpendByEntity": "Spend by {{entity}}:",
"tooltipAndMore": "...and {{count}} more",
"spendPerEntity": "Spend Per {{entity}}",
"showingTop5BySpend": "Showing Top 5 by Spend",
"getStartedTracking": "Get Started by Tracking cost per {{entity}} <a>here</a>",
"tooltipSpend": "Spend: ${{amount}}",
"tooltipRequests": "Requests: {{count}}",
"tooltipTokens": "Tokens: {{count}}",
"colSpend": "Spend",
"colSuccessful": "Successful",
"colFailed": "Failed",
"colTokens": "Tokens",
"colProvider": "Provider",
"topVirtualKeys": "Top Virtual Keys",
"topAgents": "Top Agents",
"topModels": "Top Models",
"topAgentsDrivingSpend": "Top Agents Driving Spend",
"providerUsage": "Provider Usage",
"providerLogoAlt": "{{provider}} logo"
},
"spendByProvider": {
"title": "Spend by Provider",
"showZeroSpend": "Show Zero Spend",
"showUnknown": "Show Unknown",
"unknownTooltip": "Requests that failed to route to a provider",
"colProvider": "Provider",
"colSpend": "Spend",
"colSuccessful": "Successful",
"colFailed": "Failed",
"colTokens": "Tokens"
},
"topKeyView": {
"colKeyId": "Key ID",
"colKeyAlias": "Key Alias",
"colTags": "Tags",
"colSpendUsd": "Spend (USD)",
"tagName": "Tag Name:",
"tagSpend": "Spend:",
"showFewerTags": "Show fewer tags",
"showAllTags": "Show all tags",
"tooltipKeyAlias": "Key Alias:",
"tooltipKeyId": "Key ID:",
"tooltipSpend": "Spend:",
"tableView": "Table View",
"chartView": "Chart View"
},
"topModelView": {
"colModel": "Model",
"colSpendUsd": "Spend (USD)",
"colSuccessful": "Successful",
"colFailed": "Failed",
"colTokens": "Tokens",
"tableView": "Table View",
"chartView": "Chart View"
},
"keyModelUsageView": {
"title": "Model Usage",
"colModel": "Model",
"colSpendUsd": "Spend (USD)",
"colSuccessful": "Successful",
"colFailed": "Failed",
"colTokens": "Tokens",
"tableView": "Table",
"chartView": "Chart"
}
}
}

View file

@ -4868,5 +4868,106 @@
"invalidPercentageRange": "百分比必须在 0% 到 1000% 之间",
"invalidFixedAmount": "固定金额不能为负数"
}
},
"usagePage": {
"endpointUsageBarChart": {
"title": "按端点统计成功与失败请求"
},
"endpointUsageLineChart": {
"title": "端点用量趋势"
},
"entityUsage": {
"filterLabel": "按 {{entityType}} 筛选",
"filterPlaceholder": "选择 {{entityType}} 进行筛选...",
"fetchingSpendData": "正在获取花费数据:已获取 {{currentPage}} / {{totalPages}} 页。图表会随数据加载定期更新。离开本页将停止并重置加载。如需继续使用界面,请 <a>新标签页打开</a>。",
"partialData": "显示部分数据(已加载 {{currentPage}}/{{totalPages}} 页)",
"fetchingAgentData": "正在获取 Agent 数据:已获取 {{currentPage}} / {{totalPages}} 页。图表会随数据加载定期更新。离开本页将停止并重置加载。如需继续使用界面,请 <a>新标签页打开</a>。",
"partialAgentData": "显示部分 Agent 数据(已加载 {{currentPage}}/{{totalPages}} 页)",
"filterByTeam": "按团队筛选",
"stopButton": "停止",
"tabCost": "花费",
"tabRequestTokenConsumption": "请求 / Token 消耗",
"tabModelActivity": "模型活动",
"tabAgentActivity": "Agent 活动",
"tabKeyActivity": "密钥活动",
"tabEndpointActivity": "端点活动",
"spendOverview": "{{entity}} 花费概览",
"totalSpend": "总花费",
"totalRequests": "总请求数",
"successfulRequests": "成功请求数",
"failedRequests": "失败请求数",
"totalTokens": "总 Token 数",
"dailySpend": "每日花费",
"tooltipTotalSpend": "总花费:${{amount}}",
"tooltipTotalRequests": "总请求数:{{count}}",
"tooltipSuccessful": "成功:{{count}}",
"tooltipFailed": "失败:{{count}}",
"tooltipTotalTokens": "总 Token 数:{{count}}",
"tooltipTotalEntities": "{{entity}} 总数:{{count}}",
"tooltipSpendByEntity": "按 {{entity}} 统计花费:",
"tooltipAndMore": "...以及另外 {{count}} 个",
"spendPerEntity": "每个 {{entity}} 的花费",
"showingTop5BySpend": "显示花费前 5",
"getStartedTracking": "开始按 {{entity}} 跟踪成本,<a>点击这里</a>",
"tooltipSpend": "花费:${{amount}}",
"tooltipRequests": "请求数:{{count}}",
"tooltipTokens": "Token 数:{{count}}",
"colSpend": "花费",
"colSuccessful": "成功",
"colFailed": "失败",
"colTokens": "Token",
"colProvider": "提供商",
"topVirtualKeys": "热门虚拟密钥",
"topAgents": "热门 Agent",
"topModels": "热门模型",
"topAgentsDrivingSpend": "花费最高的 Agent",
"providerUsage": "提供商用量",
"providerLogoAlt": "{{provider}} 图标"
},
"spendByProvider": {
"title": "按提供商统计花费",
"showZeroSpend": "显示零花费",
"showUnknown": "显示未知",
"unknownTooltip": "路由到提供商失败的请求",
"colProvider": "提供商",
"colSpend": "花费",
"colSuccessful": "成功",
"colFailed": "失败",
"colTokens": "Token"
},
"topKeyView": {
"colKeyId": "密钥 ID",
"colKeyAlias": "密钥别名",
"colTags": "标签",
"colSpendUsd": "花费(USD)",
"tagName": "标签名:",
"tagSpend": "花费:",
"showFewerTags": "显示更少标签",
"showAllTags": "显示全部标签",
"tooltipKeyAlias": "密钥别名:",
"tooltipKeyId": "密钥 ID:",
"tooltipSpend": "花费:",
"tableView": "表格视图",
"chartView": "图表视图"
},
"topModelView": {
"colModel": "模型",
"colSpendUsd": "花费(USD)",
"colSuccessful": "成功",
"colFailed": "失败",
"colTokens": "Token",
"tableView": "表格视图",
"chartView": "图表视图"
},
"keyModelUsageView": {
"title": "模型用量",
"colModel": "模型",
"colSpendUsd": "花费(USD)",
"colSuccessful": "成功",
"colFailed": "失败",
"colTokens": "Token",
"tableView": "表格",
"chartView": "图表"
}
}
}