mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for usage page batch 2
This commit is contained in:
parent
e4f613b5df
commit
84c5d6af01
4 changed files with 237 additions and 66 deletions
|
|
@ -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. "Which model costs me the most?"</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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": "按端点统计成功与失败请求"
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue