feat: 添加缓存 Token 独立展示(cache_creation / cache_read)
从 logs 表 other JSON 字段提取 cache_creation_tokens 和 cache_tokens, 在排名、日志、聚合、详情页分别展示,total_tokens 包含缓存部分。
This commit is contained in:
@@ -11,10 +11,10 @@ import { useI18n } from "@/lib/i18n";
|
||||
|
||||
interface AggItem {
|
||||
rank: number; name: string; calls: number;
|
||||
prompt_tokens: number; completion_tokens: number; total_tokens: number;
|
||||
prompt_tokens: number; completion_tokens: number; cache_creation_tokens: number; cache_read_tokens: number; total_tokens: number;
|
||||
}
|
||||
|
||||
type SortKey = "total_tokens" | "calls" | "prompt_tokens" | "completion_tokens" | "ratio";
|
||||
type SortKey = "total_tokens" | "calls" | "prompt_tokens" | "completion_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "ratio";
|
||||
|
||||
function RatioTooltip({ text }: { text: string }) {
|
||||
const [show, setShow] = useState(false);
|
||||
@@ -122,6 +122,8 @@ export default function AggregationPage() {
|
||||
const sortHeaders: { key: SortKey; label: string }[] = [
|
||||
{ key: "calls", label: t("th.calls") },
|
||||
{ key: "prompt_tokens", label: t("th.input") },
|
||||
{ key: "cache_creation_tokens", label: t("th.cacheCreation") },
|
||||
{ key: "cache_read_tokens", label: t("th.cacheRead") },
|
||||
{ key: "completion_tokens", label: t("th.output") },
|
||||
{ key: "total_tokens", label: t("th.totalToken") },
|
||||
];
|
||||
@@ -181,7 +183,7 @@ export default function AggregationPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={8} className="px-4 py-16 text-center"><div className="inline-block h-5 w-5 animate-spin rounded-full spinner" /></td></tr>
|
||||
<tr><td colSpan={10} className="px-4 py-16 text-center"><div className="inline-block h-5 w-5 animate-spin rounded-full spinner" /></td></tr>
|
||||
) : sorted.map((item, i) => {
|
||||
const pct = totals.tokens > 0 ? (item.total_tokens / totals.tokens * 100) : 0;
|
||||
const ratio = item.prompt_tokens > 0 ? (item.completion_tokens / item.prompt_tokens) : 0;
|
||||
@@ -191,6 +193,8 @@ export default function AggregationPage() {
|
||||
<td className="px-4 py-3 font-medium transition-colors" style={{ color: "var(--text-accent)" }}>{item.name}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)]" style={{ color: "var(--text-secondary)" }}>{formatNumber(item.calls)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatTokens(item.prompt_tokens)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatTokens(item.cache_creation_tokens)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatTokens(item.cache_read_tokens)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatTokens(item.completion_tokens)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-medium font-[family-name:var(--font-geist-mono)]" style={{ color: "var(--text-primary)" }}>{formatTokens(item.total_tokens)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: ratio >= 1 ? "var(--accent)" : "var(--text-muted)" }}>{ratio.toFixed(2)}</td>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState, useCallback, startTransition } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { motion } from "motion/react";
|
||||
import { ArrowLeft, Hash, Zap, MessageSquare } from "lucide-react";
|
||||
import { ArrowLeft, Hash, Zap, MessageSquare, DatabaseZap, BookOpen } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { StatsCard } from "@/components/StatsCard";
|
||||
import { TimeRangeSelector } from "@/components/TimeRangeSelector";
|
||||
@@ -14,6 +14,7 @@ import { useI18n } from "@/lib/i18n";
|
||||
|
||||
interface DetailData {
|
||||
calls: number; prompt_tokens: number; completion_tokens: number;
|
||||
cache_creation_tokens: number; cache_read_tokens: number;
|
||||
total_tokens: number; quota: number; display_name?: string;
|
||||
models?: { name: string; calls: number; total_tokens: number; quota: number }[];
|
||||
users?: { name: string; calls: number; total_tokens: number; quota: number }[];
|
||||
@@ -74,10 +75,12 @@ export default function DetailPage() {
|
||||
<div className="flex h-64 items-center justify-center"><div className="h-6 w-6 animate-spin rounded-full spinner" /></div>
|
||||
) : data ? (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
|
||||
<StatsCard title={t("dash.totalCalls")} value={data.calls} icon={Hash} delay={0} />
|
||||
<StatsCard title={t("th.totalToken")} value={data.total_tokens} format="tokens" icon={Zap} delay={0.05} />
|
||||
<StatsCard title={t("th.input")} value={data.prompt_tokens} format="tokens" icon={MessageSquare} delay={0.1} />
|
||||
<StatsCard title={t("th.cacheCreation")} value={data.cache_creation_tokens} format="tokens" icon={DatabaseZap} delay={0.15} />
|
||||
<StatsCard title={t("th.cacheRead")} value={data.cache_read_tokens} format="tokens" icon={BookOpen} delay={0.2} />
|
||||
</div>
|
||||
|
||||
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.15 }} className="glass p-5">
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useI18n } from "@/lib/i18n";
|
||||
interface LogEntry {
|
||||
id: number; created_at: string; display_name: string;
|
||||
real_model: string; channel_name: string; prompt_tokens: number;
|
||||
completion_tokens: number; total_tokens: number;
|
||||
use_time: number; is_stream: boolean;
|
||||
completion_tokens: number; cache_creation_tokens: number; cache_read_tokens: number;
|
||||
total_tokens: number; use_time: number; is_stream: boolean;
|
||||
}
|
||||
|
||||
export default function LogsPage() {
|
||||
@@ -37,7 +37,7 @@ export default function LogsPage() {
|
||||
useEffect(() => { startTransition(() => setPage(1)); }, [getEffectiveRange, filters]);
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
const headers = [t("th.time"), t("th.user"), t("th.realModel"), t("th.channel"), t("th.input"), t("th.output"), t("th.totalToken"), t("th.latency"), ""];
|
||||
const headers = [t("th.time"), t("th.user"), t("th.realModel"), t("th.channel"), t("th.input"), t("th.cacheCreation"), t("th.cacheRead"), t("th.output"), t("th.totalToken"), t("th.latency"), ""];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -78,7 +78,7 @@ export default function LogsPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={9} className="px-3 py-16 text-center"><div className="inline-block h-5 w-5 animate-spin rounded-full spinner" /></td></tr>
|
||||
<tr><td colSpan={11} className="px-3 py-16 text-center"><div className="inline-block h-5 w-5 animate-spin rounded-full spinner" /></td></tr>
|
||||
) : logs.map((log) => (
|
||||
<tr key={log.id} className="row-glow transition-colors" style={{ borderBottom: "1px solid var(--surface-border)" }}>
|
||||
<td className="px-3 py-2.5 text-xs font-[family-name:var(--font-geist-mono)]" style={{ color: "var(--text-muted)" }}>{formatDate(log.created_at)}</td>
|
||||
@@ -86,6 +86,8 @@ export default function LogsPage() {
|
||||
<td className="px-3 py-2.5 font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-accent)", opacity: 0.7 }}>{log.real_model}</td>
|
||||
<td className="px-3 py-2.5" style={{ color: "var(--text-muted)" }}>{log.channel_name}</td>
|
||||
<td className="px-3 py-2.5 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatNumber(log.prompt_tokens)}</td>
|
||||
<td className="px-3 py-2.5 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatNumber(log.cache_creation_tokens)}</td>
|
||||
<td className="px-3 py-2.5 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatNumber(log.cache_read_tokens)}</td>
|
||||
<td className="px-3 py-2.5 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatNumber(log.completion_tokens)}</td>
|
||||
<td className="px-3 py-2.5 text-right tabular-nums font-medium font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-primary)" }}>{formatNumber(log.total_tokens)}</td>
|
||||
<td className="px-3 py-2.5 text-right tabular-nums text-xs" style={{ color: "var(--text-muted)" }}>{log.use_time}ms</td>
|
||||
|
||||
@@ -10,11 +10,11 @@ import { useTimeRange } from "@/lib/time-range-context";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
|
||||
type Tab = "user" | "model" | "channel";
|
||||
type SortKey = "calls" | "prompt_tokens" | "completion_tokens" | "total_tokens";
|
||||
type SortKey = "calls" | "prompt_tokens" | "completion_tokens" | "cache_creation_tokens" | "cache_read_tokens" | "total_tokens";
|
||||
|
||||
interface RankItem {
|
||||
rank: number; name: string; username?: string; id?: number; calls: number;
|
||||
prompt_tokens: number; completion_tokens: number; total_tokens: number;
|
||||
prompt_tokens: number; completion_tokens: number; cache_creation_tokens: number; cache_read_tokens: number; total_tokens: number;
|
||||
}
|
||||
|
||||
export default function RankingsPage() {
|
||||
@@ -70,6 +70,8 @@ export default function RankingsPage() {
|
||||
{ key: null, label: t("th.name"), align: "left" },
|
||||
{ key: "calls", label: t("th.calls"), align: "right" },
|
||||
{ key: "prompt_tokens", label: t("th.input"), align: "right" },
|
||||
{ key: "cache_creation_tokens", label: t("th.cacheCreation"), align: "right" },
|
||||
{ key: "cache_read_tokens", label: t("th.cacheRead"), align: "right" },
|
||||
{ key: "completion_tokens", label: t("th.output"), align: "right" },
|
||||
{ key: "total_tokens", label: t("th.totalToken"), align: "right" },
|
||||
];
|
||||
@@ -125,7 +127,7 @@ export default function RankingsPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={6} className="px-4 py-16 text-center"><div className="inline-block h-5 w-5 animate-spin rounded-full spinner" /></td></tr>
|
||||
<tr><td colSpan={8} className="px-4 py-16 text-center"><div className="inline-block h-5 w-5 animate-spin rounded-full spinner" /></td></tr>
|
||||
) : sorted.map((item, i) => (
|
||||
<motion.tr key={item.name} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: i * 0.02 }}
|
||||
className="row-glow transition-colors" style={{ borderBottom: "1px solid var(--surface-border)" }}>
|
||||
@@ -135,6 +137,8 @@ export default function RankingsPage() {
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums" style={{ color: "var(--text-secondary)" }}>{formatNumber(item.calls)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatTokens(item.prompt_tokens)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatTokens(item.cache_creation_tokens)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatTokens(item.cache_read_tokens)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-[family-name:var(--font-geist-mono)] text-xs" style={{ color: "var(--text-muted)" }}>{formatTokens(item.completion_tokens)}</td>
|
||||
<td className="px-4 py-3 text-right tabular-nums font-medium font-[family-name:var(--font-geist-mono)]" style={{ color: "var(--text-primary)" }}>{formatTokens(item.total_tokens)}</td>
|
||||
</motion.tr>
|
||||
|
||||
Reference in New Issue
Block a user