feat(analytics): 增加总令牌数支持并优化相关逻辑

This commit is contained in:
Jeff Gu committed 2026-04-13 14:31:28 +08:00
1 parent 0258367a64
commit 141da9dcf0
7 files changed
+47 -47

No files matched your search

@@ -1,13 +1,6 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { AnalyticsBreakdownItem } from "@/types";
import { cn, formatCurrency } from "@/lib/utils";
const formatCompactNumber = (value: number): string => {
return new Intl.NumberFormat("zh-CN", {
notation: value >= 1000 ? "compact" : "standard",
maximumFractionDigits: value >= 1000 ? 1 : 0,
}).format(value);
};
import { cn, formatCompactNumber, formatCurrency } from "@/lib/utils";
const formatPercent = (value: number): string => `${(value * 100).toFixed(1)}%`;
@@ -33,9 +26,7 @@ export function BreakdownChartCard({
displayDecimals,
}: BreakdownChartCardProps) {
const visibleItems = items.slice(0, 6);
const maxCost = Math.max(...visibleItems.map((item) => item.totalCost), 0);
const useCostScale = maxCost > 0;
const maxScaleValue = useCostScale ? Math.max(maxCost, 1) : Math.max(...visibleItems.map((item) => item.requests), 1);
const maxRequests = Math.max(...visibleItems.map((item) => item.requests), 1);
return (
<Card className="overflow-hidden border-0">
@@ -61,8 +52,7 @@ export function BreakdownChartCard({
</div>
) : (
visibleItems.map((item, index) => {
const scaleValue = useCostScale ? item.totalCost : item.requests;
const width = scaleValue > 0 ? Math.max((scaleValue / maxScaleValue) * 100, 6) : 0;
const width = item.requests > 0 ? Math.max((item.requests / maxRequests) * 100, 6) : 0;
return (
<div key={`${title}-${item.label}-${index}`}>
@@ -81,17 +71,15 @@ export function BreakdownChartCard({
{item.label}
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span>请求 {formatCompactNumber(item.requests)}</span>
<span>成功率 {formatPercent(item.successRate)}</span>
<span>输出 {formatCompactNumber(item.completionTokens)}</span>
<span>消耗 {formatCompactNumber(item.totalTokens)}</span>
<span>费用 {formatCurrency(item.totalCost, displayDecimals)}</span>
</div>
</div>
</div>
</div>
<div className="text-right">
<div className="font-semibold">
{useCostScale ? formatCurrency(item.totalCost, displayDecimals) : formatCompactNumber(item.requests)}
</div>
<div className="font-semibold">{formatCompactNumber(item.requests)}</div>
<div className="text-xs text-muted-foreground">{Math.round(item.avgLatencyMs)} ms</div>
</div>
</div>
@@ -1,5 +1,5 @@
import { AnalyticsRange, AnalyticsTrendPoint } from "@/types";
import { formatCurrency, parseUtcTimestamp } from "@/lib/utils";
import { formatCompactNumber, formatCurrency, parseUtcTimestamp } from "@/lib/utils";
import { Card } from "../ui/card";
const formatAxisLabel = (value: string, range: AnalyticsRange): string => {
@@ -20,13 +20,6 @@ const formatAxisLabel = (value: string, range: AnalyticsRange): string => {
});
};
const formatCompactNumber = (value: number): string => {
return new Intl.NumberFormat("zh-CN", {
notation: value >= 1000 ? "compact" : "standard",
maximumFractionDigits: value >= 1000 ? 1 : 0,
}).format(value);
};
export function TrendBarChart({
points,
range,
+30
View File
@@ -43,6 +43,36 @@ export function formatQuota(value: number, displayDecimals = DEFAULT_BILLING_DIS
return formatCurrency(value, displayDecimals)
}
export function formatCompactNumber(value: number): string {
const absValue = Math.abs(value)
const formatWithSuffix = (divisor: number, suffix: string): string => {
const scaled = value / divisor
const formatted = new Intl.NumberFormat('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: Math.abs(scaled) >= 100 ? 0 : 1,
}).format(scaled)
return `${formatted}${suffix}`
}
if (absValue >= 1_000_000_000) {
return formatWithSuffix(1_000_000_000, 'B')
}
if (absValue >= 1_000_000) {
return formatWithSuffix(1_000_000, 'M')
}
if (absValue >= 1_000) {
return formatWithSuffix(1_000, 'K')
}
return new Intl.NumberFormat('en-US', {
maximumFractionDigits: 0,
}).format(value)
}
export function copyToClipboard(text: string): Promise<void> {
return navigator.clipboard.writeText(text)
}
+5 -12
View File
@@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button";
import { Select } from "@/components/ui/select";
import { useBillingConfig } from "@/hooks/use-billing-config";
import { readScopedCache, writeScopedCache } from "@/lib/local-cache";
import { cn, formatCurrency } from "@/lib/utils";
import { cn, formatCompactNumber, formatCurrency } from "@/lib/utils";
import { Activity, CircleDollarSign, Clock3, DatabaseZap, RefreshCw, ShieldCheck } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
@@ -36,40 +36,33 @@ const BREAKDOWN_CHARTS: Array<{
{
dimension: "model",
title: "模型排行",
description: "看清哪些模型是主要成本与流量入口。",
description: "看清哪些模型承载最多请求,并同步观察质量与成本。",
barClassName: "from-amber-500 to-orange-400",
badgeClassName: "bg-amber-500/12 text-amber-700 dark:text-amber-400",
},
{
dimension: "channel",
title: "渠道排行",
description: "对比不同渠道承载的请求与成本消耗。",
description: "对比不同渠道的请求承载能力与稳定性表现。",
barClassName: "from-emerald-500 to-teal-400",
badgeClassName: "bg-emerald-500/12 text-emerald-600 dark:text-emerald-400",
},
{
dimension: "token",
title: "令牌排行",
description: "按成本优先排序,识别最活跃的访问凭证。",
description: "按请求量识别最活跃的访问凭证。",
barClassName: "from-sky-500 to-cyan-400",
badgeClassName: "bg-sky-500/12 text-sky-600 dark:text-sky-400",
},
{
dimension: "provider",
title: "服务商排行",
description: "衡量各上游服务商的占比与稳定性表现。",
description: "衡量各上游服务商承接的请求占比与稳定性表现。",
barClassName: "from-rose-500 to-pink-400",
badgeClassName: "bg-rose-500/12 text-rose-600 dark:text-rose-400",
},
];
const formatCompactNumber = (value: number): string => {
return new Intl.NumberFormat("zh-CN", {
notation: value >= 1000 ? "compact" : "standard",
maximumFractionDigits: value >= 1000 ? 1 : 0,
}).format(value);
};
const formatPercent = (value: number): string => `${(value * 100).toFixed(1)}%`;
const formatDuration = (value: number): string => {
+1 -8
View File
@@ -11,7 +11,7 @@ import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import { useBillingConfig } from "@/hooks/use-billing-config";
import { readScopedCache, writeScopedCache } from "@/lib/local-cache";
import { cn, formatCurrency, parseUtcTimestamp } from "@/lib/utils";
import { cn, formatCompactNumber, formatCurrency, parseUtcTimestamp } from "@/lib/utils";
import { Eye, RefreshCw, Search } from "lucide-react";
type UsageLogFilterState = {
@@ -119,13 +119,6 @@ const formatDuration = (value: number): string => {
return `${Math.round(value)} ms`;
};
const formatCompactNumber = (value: number): string => {
return new Intl.NumberFormat("zh-CN", {
notation: value >= 1000 ? "compact" : "standard",
maximumFractionDigits: value >= 1000 ? 1 : 0,
}).format(value);
};
const getUsageLogWindow = (range: AnalyticsRange): { start: Date; end: Date } => {
const end = new Date();
const start = new Date(end.getTime() - USAGE_LOG_RANGE_DURATION_MS[range]);
+1
View File
@@ -109,6 +109,7 @@ export interface AnalyticsBreakdownItem {
failures: number
successRate: number
totalCost: number
totalTokens: number
promptTokens: number
completionTokens: number
avgLatencyMs: number
+3 -1
View File
@@ -723,6 +723,7 @@ SELECT
sum(_sample_interval) AS requests,
sum(${DOUBLE_FIELDS.successFlag} * _sample_interval) AS successes,
sum(${normalizedTotalCostExpression} * _sample_interval) AS total_cost,
sum(${DOUBLE_FIELDS.totalTokens} * _sample_interval) AS total_tokens,
sum(${DOUBLE_FIELDS.promptTokens} * _sample_interval) AS prompt_tokens,
sum(${DOUBLE_FIELDS.completionTokens} * _sample_interval) AS completion_tokens,
sum(${DOUBLE_FIELDS.latencyMs} * _sample_interval) / sum(_sample_interval) AS avg_latency_ms
@@ -730,7 +731,7 @@ FROM ${dataset}
WHERE ${buildRangeWhereClause(config)}
AND ${dimensionField} != ''
GROUP BY label
ORDER BY total_cost DESC, requests DESC
ORDER BY requests DESC, total_cost DESC
LIMIT 12
`.trim());
@@ -748,6 +749,7 @@ LIMIT 12
failures: Math.max(0, requests - successes),
successRate: requests > 0 ? successes / requests : 0,
totalCost: toNumber(row.total_cost),
totalTokens: toNumber(row.total_tokens),
promptTokens: toNumber(row.prompt_tokens),
completionTokens: toNumber(row.completion_tokens),
avgLatencyMs: toNumber(row.avg_latency_ms),