feat(ui): i18n for usage page batch 2

This commit is contained in:
吹雪 2026-06-11 01:22:58 +09:00
parent e4f613b5df
commit 84c5d6af01
4 changed files with 237 additions and 66 deletions

View file

@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
import { Button, Select, Input, Spin } from "antd";
import ReactMarkdown from "react-markdown";
import { useTranslation } from "react-i18next";
import { modelHubCall, usageAiChatStream, UsageAiToolCallEvent } from "../../networking";
const { TextArea } = Input;
@ -32,6 +33,7 @@ const TOOL_ICONS: Record<string, string> = {
};
const ToolCallDisplay: React.FC<{ step: ToolCallStep }> = ({ step }) => {
const { t } = useTranslation();
const icon = TOOL_ICONS[step.tool_name] || "🔧";
const args = step.arguments;
const dateRange = args.start_date && args.end_date ? `${args.start_date} → ${args.end_date}` : "";
@ -53,7 +55,9 @@ const ToolCallDisplay: React.FC<{ step: ToolCallStep }> = ({ step }) => {
{icon} {step.tool_label}
</div>
{dateRange && <div className="text-gray-500 mt-0.5">{dateRange}</div>}
{filter && <div className="text-gray-500 mt-0.5">Filter: {filter}</div>}
{filter && (
<div className="text-gray-500 mt-0.5">{t("usagePage.usageAiChatPanel.filterLabel", { filter })}</div>
)}
{step.status === "error" && step.error && <div className="text-red-600 mt-0.5">{step.error}</div>}
</div>
</div>
@ -97,6 +101,7 @@ const MarkdownContent: React.FC<{ content: string }> = ({ content }) => (
);
const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, accessToken }) => {
const { t } = useTranslation();
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputText, setInputText] = useState("");
const [isLoading, setIsLoading] = useState(false);
@ -177,7 +182,10 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
(errorMsg: string) => {
setStatusMessage(null);
setActiveToolCalls([]);
setMessages((prev) => [...prev, { role: "assistant", content: `Error: ${errorMsg}` }]);
setMessages((prev) => [
...prev,
{ role: "assistant", content: t("usagePage.usageAiChatPanel.errorPrefix", { message: errorMsg }) },
]);
setStreamingContent("");
},
(status: string) => {
@ -198,8 +206,11 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
if (error?.name === "AbortError" || abortController.signal.aborted) {
return;
}
const errorMsg = error?.message || "Failed to get response. Please try again.";
setMessages((prev) => [...prev, { role: "assistant", content: `Error: ${errorMsg}` }]);
const errorMsg = error?.message || t("usagePage.usageAiChatPanel.fallbackError");
setMessages((prev) => [
...prev,
{ role: "assistant", content: t("usagePage.usageAiChatPanel.errorPrefix", { message: errorMsg }) },
]);
setStreamingContent("");
} finally {
setIsLoading(false);
@ -243,7 +254,7 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
<svg className="w-5 h-5 text-blue-600" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 1l1.5 3.5L13 6l-3.5 1.5L8 11 6.5 7.5 3 6l3.5-1.5L8 1zm4 7l.75 1.75L14.5 10.5l-1.75.75L12 13l-.75-1.75L9.5 10.5l1.75-.75L12 8zM4 9l.75 1.75L6.5 11.5l-1.75.75L4 14l-.75-1.75L1.5 11.5l1.75-.75L4 9z" />
</svg>
<h3 className="text-base font-semibold text-gray-900">Ask AI</h3>
<h3 className="text-base font-semibold text-gray-900">{t("usagePage.usageAiChatPanel.title")}</h3>
</div>
<button
onClick={handleClose}
@ -254,13 +265,13 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
</svg>
</button>
</div>
<p className="text-xs text-gray-500">Ask about your spend, models, keys, and trends</p>
<p className="text-xs text-gray-500">{t("usagePage.usageAiChatPanel.subtitle")}</p>
</div>
{/* Model selector */}
<div className="px-5 py-3 border-b border-gray-100 flex-shrink-0">
<Select
placeholder="Select a model (optional, defaults to gpt-4o-mini)"
placeholder={t("usagePage.usageAiChatPanel.modelPlaceholder")}
value={selectedModel}
onChange={(value) => setSelectedModel(value)}
loading={isLoadingModels}
@ -285,8 +296,8 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
<p className="text-sm font-medium">Ask a question about your usage</p>
<p className="text-xs mt-1">e.g. &quot;Which model costs me the most?&quot;</p>
<p className="text-sm font-medium">{t("usagePage.usageAiChatPanel.emptyStateTitle")}</p>
<p className="text-xs mt-1">{t("usagePage.usageAiChatPanel.emptyStateExample")}</p>
</div>
)}
@ -330,7 +341,7 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
{isLoading && !streamingContent && (
<div className="flex items-center gap-2 px-3 py-2 text-xs text-gray-500">
<Spin size="small" />
<span className="italic">{statusMessage || "Thinking..."}</span>
<span className="italic">{statusMessage || t("usagePage.usageAiChatPanel.thinking")}</span>
</div>
)}
@ -351,13 +362,13 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask about your usage..."
placeholder={t("usagePage.usageAiChatPanel.inputPlaceholder")}
autoSize={{ minRows: 1, maxRows: 3 }}
className="flex-1"
disabled={isLoading}
/>
<Button type="primary" onClick={handleSend} disabled={!inputText.trim() || isLoading} loading={isLoading}>
Send
{t("usagePage.usageAiChatPanel.sendButton")}
</Button>
</div>
<div className="flex justify-between items-center mt-2">
@ -366,9 +377,9 @@ const UsageAIChatPanel: React.FC<UsageAIChatPanelProps> = ({ open, onClose, acce
className="text-xs text-gray-400 hover:text-gray-600 transition-colors"
disabled={messages.length === 0}
>
Clear chat
{t("usagePage.usageAiChatPanel.clearChat")}
</button>
<span className="text-xs text-gray-400">Enter to send</span>
<span className="text-xs text-gray-400">{t("usagePage.usageAiChatPanel.enterToSend")}</span>
</div>
</div>
</div>

View file

@ -8,6 +8,7 @@
import { DownOutlined, ExportOutlined, InfoCircleOutlined, LoadingOutlined, RightOutlined } from "@ant-design/icons";
import { useDebouncedState } from "@tanstack/react-pacer/debouncer";
import { Trans, useTranslation } from "react-i18next";
import {
BarChart,
Card,
@ -58,6 +59,7 @@ interface UsagePageProps {
}
const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
const { t } = useTranslation();
const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
// Aggregated endpoint: try first, fall back to paginated if unavailable
const [aggregatedData, setAggregatedData] = useState<{ results: DailyData[]; metadata: any } | null>(null);
@ -462,16 +464,23 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<div className="flex items-center justify-between">
<span>
<LoadingOutlined spin className="mr-2" />
Currently fetching spend data: fetched {paginatedResult.progress.currentPage} /{" "}
{paginatedResult.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.usagePageView.fetchingSpendData"
values={{
currentPage: paginatedResult.progress.currentPage,
totalPages: paginatedResult.progress.totalPages,
}}
components={{
a: (
<a href={window.location.href} target="_blank" rel="noopener noreferrer">
open a new tab <ExportOutlined />
</a>
),
}}
/>
</span>
<Button type="primary" danger onClick={paginatedResult.cancel}>
Stop
{t("usagePage.usagePageView.stopButton")}
</Button>
</div>
}
@ -484,8 +493,10 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
className="mb-2"
message={
<span>
Showing partial data ({paginatedResult.progress.currentPage}/{paginatedResult.progress.totalPages}{" "}
pages loaded)
{t("usagePage.usagePageView.partialData", {
currentPage: paginatedResult.progress.currentPage,
totalPages: paginatedResult.progress.totalPages,
})}
</span>
}
/>
@ -495,12 +506,12 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<>
{isAdmin && usageView === "global" && (
<div className="mb-4">
<Text className="mb-2">Filter by user</Text>
<Text className="mb-2">{t("usagePage.usagePageView.filterByUser")}</Text>
<Select
showSearch
allowClear
style={{ width: "100%" }}
placeholder="Select user to filter..."
placeholder={t("usagePage.usagePageView.selectUserPlaceholder")}
value={selectedUserId}
onChange={(value) => setSelectedUserId(value ?? null)}
filterOption={false}
@ -508,7 +519,9 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
searchValue={userSearchInput}
onPopupScroll={handleUserPopupScroll}
loading={isLoadingUsers}
notFoundContent={isLoadingUsers ? <LoadingOutlined spin /> : "No users found"}
notFoundContent={
isLoadingUsers ? <LoadingOutlined spin /> : t("usagePage.usagePageView.noUsersFound")
}
options={userOptions}
popupRender={(menu) => (
<>
@ -526,11 +539,11 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<TabGroup>
<div className="flex justify-between items-center">
<TabList variant="solid" className="mt-1">
<Tab>Cost</Tab>
<Tab>Model Activity</Tab>
<Tab>Key Activity</Tab>
<Tab>MCP Server Activity</Tab>
<Tab>Endpoint Activity</Tab>
<Tab>{t("usagePage.usagePageView.tabCost")}</Tab>
<Tab>{t("usagePage.usagePageView.tabModelActivity")}</Tab>
<Tab>{t("usagePage.usagePageView.tabKeyActivity")}</Tab>
<Tab>{t("usagePage.usagePageView.tabMcpServerActivity")}</Tab>
<Tab>{t("usagePage.usagePageView.tabEndpointActivity")}</Tab>
</TabList>
<div className="flex items-center gap-2">
<Button
@ -541,7 +554,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
</svg>
}
>
Ask AI
{t("usagePage.usagePageView.askAiButton")}
</Button>
<Button
onClick={() => setIsGlobalExportModalOpen(true)}
@ -556,7 +569,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
</svg>
}
>
Export Data
{t("usagePage.usagePageView.exportDataButton")}
</Button>
</div>
</div>
@ -568,7 +581,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<Col numColSpan={2}>
<div className="flex items-center gap-4 mt-2 mb-2">
<Text className="text-tremor-default text-tremor-content dark:text-dark-tremor-content text-lg">
Project Spend{" "}
{t("usagePage.usagePageView.projectSpend")}{" "}
{dateValue.from && dateValue.to && (
<>
{dateValue.from.toLocaleDateString("en-US", {
@ -597,24 +610,24 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<Col numColSpan={2}>
<Card>
<Title>Usage Metrics</Title>
<Title>{t("usagePage.usagePageView.usageMetrics")}</Title>
<Grid numItems={5} className="gap-4 mt-4">
<Card>
<Title>Total Requests</Title>
<Title>{t("usagePage.usagePageView.totalRequests")}</Title>
<Text className="text-2xl font-bold mt-2">
{userSpendData.metadata?.total_api_requests?.toLocaleString() || 0}
</Text>
</Card>
<Card>
<Title>Successful Requests</Title>
<Title>{t("usagePage.usagePageView.successfulRequests")}</Title>
<Text className="text-2xl font-bold mt-2 text-green-600">
{userSpendData.metadata?.total_successful_requests?.toLocaleString() || 0}
</Text>
</Card>
<Card>
<div className="flex items-center gap-2">
<Title>Failed Requests</Title>
<Tooltip title="Includes requests that failed to route to a provider, tool usage failures, and other request errors where the provider cannot be determined.">
<Title>{t("usagePage.usagePageView.failedRequests")}</Title>
<Tooltip title={t("usagePage.usagePageView.failedRequestsTooltip")}>
<InfoCircleOutlined className="text-gray-400 hover:text-gray-600" />
</Tooltip>
</div>
@ -623,7 +636,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
</Text>
</Card>
<Card>
<Title>Average Cost per Request</Title>
<Title>{t("usagePage.usagePageView.averageCostPerRequest")}</Title>
<Text className="text-2xl font-bold mt-2">
$
{formatNumberWithCommas(
@ -637,7 +650,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
onClick={() => setShowTokenBreakdown(!showTokenBreakdown)}
>
<div className="flex items-center gap-2">
<Title>Total Tokens</Title>
<Title>{t("usagePage.usagePageView.totalTokens")}</Title>
{showTokenBreakdown ? (
<DownOutlined className="text-gray-400 text-xs" />
) : (
@ -652,25 +665,25 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
{showTokenBreakdown && (
<Grid numItems={4} className="gap-4 mt-4">
<Card>
<Title>Input Tokens</Title>
<Title>{t("usagePage.usagePageView.inputTokens")}</Title>
<Text className="text-2xl font-bold mt-2 text-blue-600">
{(userSpendData.metadata?.total_prompt_tokens || 0).toLocaleString()}
</Text>
</Card>
<Card>
<Title>Output Tokens</Title>
<Title>{t("usagePage.usagePageView.outputTokens")}</Title>
<Text className="text-2xl font-bold mt-2 text-cyan-600">
{userSpendData.metadata?.total_completion_tokens?.toLocaleString() || 0}
</Text>
</Card>
<Card>
<Title>Cache Read Tokens</Title>
<Title>{t("usagePage.usagePageView.cacheReadTokens")}</Title>
<Text className="text-2xl font-bold mt-2 text-green-600">
{userSpendData.metadata?.total_cache_read_input_tokens?.toLocaleString() || 0}
</Text>
</Card>
<Card>
<Title>Cache Write Tokens</Title>
<Title>{t("usagePage.usagePageView.cacheWriteTokens")}</Title>
<Text className="text-2xl font-bold mt-2 text-purple-600">
{userSpendData.metadata?.total_cache_creation_input_tokens?.toLocaleString() || 0}
</Text>
@ -683,7 +696,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
{/* Daily Spend Chart */}
<Col numColSpan={2}>
<Card>
<Title>Daily Spend</Title>
<Title>{t("usagePage.usagePageView.dailySpend")}</Title>
{loading ? (
<ChartLoader isDateChanging={isDateChanging} />
) : (
@ -702,12 +715,28 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<div className="bg-white p-4 shadow-lg rounded-lg border">
<p className="font-bold">{data.date}</p>
<p className="text-cyan-500">
Spend: ${formatNumberWithCommas(data.metrics.spend, 2)}
{t("usagePage.usagePageView.tooltipSpend", {
amount: formatNumberWithCommas(data.metrics.spend, 2),
})}
</p>
<p className="text-gray-600">
{t("usagePage.usagePageView.tooltipRequests", {
count: data.metrics.api_requests,
})}
</p>
<p className="text-gray-600">
{t("usagePage.usagePageView.tooltipSuccessful", {
count: data.metrics.successful_requests,
})}
</p>
<p className="text-gray-600">
{t("usagePage.usagePageView.tooltipFailed", {
count: data.metrics.failed_requests,
})}
</p>
<p className="text-gray-600">
{t("usagePage.usagePageView.tooltipTokens", { count: data.metrics.total_tokens })}
</p>
<p className="text-gray-600">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">Tokens: {data.metrics.total_tokens}</p>
</div>
);
}}
@ -718,7 +747,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
{/* Top API Keys */}
<Col numColSpan={1}>
<Card className="h-full">
<Title>Top Virtual Keys</Title>
<Title>{t("usagePage.usagePageView.topVirtualKeys")}</Title>
<TopKeyView
topKeys={topKeys}
teams={null}
@ -731,7 +760,11 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
{/* Top Models */}
<Col numColSpan={1}>
<Card className="h-full">
<Title>{modelViewType === "groups" ? "Top Public Model Names" : "Top Litellm Models"}</Title>
<Title>
{modelViewType === "groups"
? t("usagePage.usagePageView.topPublicModelNames")
: t("usagePage.usagePageView.topLitellmModels")}
</Title>
<div className="flex justify-between items-center mb-4">
<Segmented
options={[
@ -752,7 +785,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
}`}
onClick={() => setModelViewType("groups")}
>
Public Model Name
{t("usagePage.usagePageView.publicModelName")}
</button>
<button
className={`px-3 py-1 text-sm rounded-md transition-colors ${
@ -762,7 +795,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
}`}
onClick={() => setModelViewType("individual")}
>
Litellm Model Name
{t("usagePage.usagePageView.litellmModelName")}
</button>
</div>
</div>
@ -791,18 +824,30 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<div className="bg-white p-4 shadow-lg rounded-lg border">
<p className="font-bold">{data.key}</p>
<p className="text-cyan-500">
Spend: ${formatNumberWithCommas(data.spend, 2)}
{t("usagePage.usagePageView.tooltipSpend", {
amount: formatNumberWithCommas(data.spend, 2),
})}
</p>
<p className="text-gray-600">
Total Requests: {data.requests.toLocaleString()}
{t("usagePage.usagePageView.tooltipTotalRequests", {
count: data.requests.toLocaleString(),
})}
</p>
<p className="text-green-600">
Successful: {data.successful_requests.toLocaleString()}
{t("usagePage.usagePageView.tooltipSuccessful", {
count: data.successful_requests.toLocaleString(),
})}
</p>
<p className="text-red-600">
Failed: {data.failed_requests.toLocaleString()}
{t("usagePage.usagePageView.tooltipFailed", {
count: data.failed_requests.toLocaleString(),
})}
</p>
<p className="text-gray-600">
{t("usagePage.usagePageView.tooltipTokens", {
count: data.tokens.toLocaleString(),
})}
</p>
<p className="text-gray-600">Tokens: {data.tokens.toLocaleString()}</p>
</div>
);
}}
@ -905,12 +950,13 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
<Alert
banner
type="info"
message="Reusable credentials are automatically tracked as tags"
message={t("usagePage.usagePageView.credentialBannerMessage")}
description={
<Typography.Text>
When a reusable credential is used, it will appear as a tag prefixed with{" "}
<Typography.Text code>Credential: </Typography.Text>
in this view.
<Trans
i18nKey="usagePage.usagePageView.credentialBannerDescription"
components={{ code: <Typography.Text code /> }}
/>
</Typography.Text>
}
closable
@ -979,7 +1025,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
}}
dateRange={dateValue}
selectedFilters={[]}
customTitle="Export Usage Data"
customTitle={t("usagePage.usagePageView.exportUsageDataTitle")}
/>
{/* AI Chat Panel */}

View file

@ -4870,6 +4870,63 @@
}
},
"usagePage": {
"usageAiChatPanel": {
"title": "Ask AI",
"subtitle": "Ask about your spend, models, keys, and trends",
"modelPlaceholder": "Select a model (optional, defaults to gpt-4o-mini)",
"emptyStateTitle": "Ask a question about your usage",
"emptyStateExample": "e.g. \"Which model costs me the most?\"",
"thinking": "Thinking...",
"inputPlaceholder": "Ask about your usage...",
"sendButton": "Send",
"clearChat": "Clear chat",
"enterToSend": "Enter to send",
"filterLabel": "Filter: {{filter}}",
"errorPrefix": "Error: {{message}}",
"fallbackError": "Failed to get response. Please try again."
},
"usagePageView": {
"filterByUser": "Filter by user",
"selectUserPlaceholder": "Select user to filter...",
"noUsersFound": "No users found",
"tabCost": "Cost",
"tabModelActivity": "Model Activity",
"tabKeyActivity": "Key Activity",
"tabMcpServerActivity": "MCP Server Activity",
"tabEndpointActivity": "Endpoint Activity",
"askAiButton": "Ask AI",
"exportDataButton": "Export Data",
"projectSpend": "Project Spend",
"usageMetrics": "Usage Metrics",
"totalRequests": "Total Requests",
"successfulRequests": "Successful Requests",
"failedRequests": "Failed Requests",
"failedRequestsTooltip": "Includes requests that failed to route to a provider, tool usage failures, and other request errors where the provider cannot be determined.",
"averageCostPerRequest": "Average Cost per Request",
"totalTokens": "Total Tokens",
"inputTokens": "Input Tokens",
"outputTokens": "Output Tokens",
"cacheReadTokens": "Cache Read Tokens",
"cacheWriteTokens": "Cache Write Tokens",
"dailySpend": "Daily Spend",
"tooltipSpend": "Spend: ${{amount}}",
"tooltipRequests": "Requests: {{count}}",
"tooltipSuccessful": "Successful: {{count}}",
"tooltipFailed": "Failed: {{count}}",
"tooltipTokens": "Tokens: {{count}}",
"tooltipTotalRequests": "Total Requests: {{count}}",
"topVirtualKeys": "Top Virtual Keys",
"topPublicModelNames": "Top Public Model Names",
"topLitellmModels": "Top Litellm Models",
"publicModelName": "Public Model Name",
"litellmModelName": "Litellm Model Name",
"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)",
"stopButton": "Stop",
"credentialBannerMessage": "Reusable credentials are automatically tracked as tags",
"credentialBannerDescription": "When a reusable credential is used, it will appear as a tag prefixed with <code>Credential: </code> in this view.",
"exportUsageDataTitle": "Export Usage Data"
},
"endpointUsageBarChart": {
"title": "Success vs Failed Requests by Endpoint"
},

View file

@ -4870,6 +4870,63 @@
}
},
"usagePage": {
"usageAiChatPanel": {
"title": "询问 AI",
"subtitle": "询问您的花费、模型、密钥及趋势",
"modelPlaceholder": "选择模型(可选,默认使用 gpt-4o-mini)",
"emptyStateTitle": "就您的用量提问",
"emptyStateExample": "例如:\"哪个模型花费最多?\"",
"thinking": "思考中...",
"inputPlaceholder": "询问您的用量...",
"sendButton": "发送",
"clearChat": "清空对话",
"enterToSend": "按 Enter 发送",
"filterLabel": "筛选:{{filter}}",
"errorPrefix": "错误:{{message}}",
"fallbackError": "获取响应失败,请重试。"
},
"usagePageView": {
"filterByUser": "按用户筛选",
"selectUserPlaceholder": "选择用户以筛选...",
"noUsersFound": "未找到用户",
"tabCost": "花费",
"tabModelActivity": "模型活动",
"tabKeyActivity": "密钥活动",
"tabMcpServerActivity": "MCP 服务器活动",
"tabEndpointActivity": "端点活动",
"askAiButton": "询问 AI",
"exportDataButton": "导出数据",
"projectSpend": "项目花费",
"usageMetrics": "用量指标",
"totalRequests": "总请求数",
"successfulRequests": "成功请求数",
"failedRequests": "失败请求数",
"failedRequestsTooltip": "包含路由到提供商失败的请求、工具调用失败以及无法确定提供商的其他请求错误。",
"averageCostPerRequest": "每次请求平均费用",
"totalTokens": "总 Token 数",
"inputTokens": "输入 Token",
"outputTokens": "输出 Token",
"cacheReadTokens": "缓存读取 Token",
"cacheWriteTokens": "缓存写入 Token",
"dailySpend": "每日花费",
"tooltipSpend": "花费:${{amount}}",
"tooltipRequests": "请求数:{{count}}",
"tooltipSuccessful": "成功:{{count}}",
"tooltipFailed": "失败:{{count}}",
"tooltipTokens": "Token 数:{{count}}",
"tooltipTotalRequests": "总请求数:{{count}}",
"topVirtualKeys": "虚拟密钥排行",
"topPublicModelNames": "公开模型名称排行",
"topLitellmModels": "LiteLLM 模型排行",
"publicModelName": "公开模型名称",
"litellmModelName": "LiteLLM 模型名称",
"fetchingSpendData": "正在获取花费数据:已获取 {{currentPage}} / {{totalPages}} 页。图表会随数据加载定期更新。离开本页将停止并重置加载。如需继续使用界面,请 <a>新标签页打开</a>。",
"partialData": "显示部分数据(已加载 {{currentPage}}/{{totalPages}} 页)",
"stopButton": "停止",
"credentialBannerMessage": "可复用凭据会自动作为标签进行追踪",
"credentialBannerDescription": "当可复用凭据被使用时,将以 <code>Credential: </code> 前缀的标签形式出现在此视图中。",
"exportUsageDataTitle": "导出用量数据"
},
"endpointUsageBarChart": {
"title": "按端点统计成功与失败请求"
},