diff --git a/app/aggregation/page.tsx b/app/aggregation/page.tsx
index 5e94570..0d54257 100644
--- a/app/aggregation/page.tsx
+++ b/app/aggregation/page.tsx
@@ -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() {
{loading ? (
- |
+ |
) : 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() {
{item.name} |
{formatNumber(item.calls)} |
{formatTokens(item.prompt_tokens)} |
+ {formatTokens(item.cache_creation_tokens)} |
+ {formatTokens(item.cache_read_tokens)} |
{formatTokens(item.completion_tokens)} |
{formatTokens(item.total_tokens)} |
= 1 ? "var(--accent)" : "var(--text-muted)" }}>{ratio.toFixed(2)} |
diff --git a/app/detail/[...slug]/page.tsx b/app/detail/[...slug]/page.tsx
index bc54c96..e84c082 100644
--- a/app/detail/[...slug]/page.tsx
+++ b/app/detail/[...slug]/page.tsx
@@ -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() {
) : data ? (
<>
-
+
+
+
diff --git a/app/logs/page.tsx b/app/logs/page.tsx
index bc19bc0..ac2e577 100644
--- a/app/logs/page.tsx
+++ b/app/logs/page.tsx
@@ -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 (
@@ -78,7 +78,7 @@ export default function LogsPage() {
{loading ? (
- |
+ |
) : logs.map((log) => (
| {formatDate(log.created_at)} |
@@ -86,6 +86,8 @@ export default function LogsPage() {
{log.real_model} |
{log.channel_name} |
{formatNumber(log.prompt_tokens)} |
+ {formatNumber(log.cache_creation_tokens)} |
+ {formatNumber(log.cache_read_tokens)} |
{formatNumber(log.completion_tokens)} |
{formatNumber(log.total_tokens)} |
{log.use_time}ms |
diff --git a/app/rankings/page.tsx b/app/rankings/page.tsx
index 1efb36c..758f5fd 100644
--- a/app/rankings/page.tsx
+++ b/app/rankings/page.tsx
@@ -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() {
{loading ? (
- |
+ |
) : sorted.map((item, i) => (
@@ -135,6 +137,8 @@ export default function RankingsPage() {
{formatNumber(item.calls)} |
{formatTokens(item.prompt_tokens)} |
+ {formatTokens(item.cache_creation_tokens)} |
+ {formatTokens(item.cache_read_tokens)} |
{formatTokens(item.completion_tokens)} |
{formatTokens(item.total_tokens)} |
diff --git a/lib/i18n.tsx b/lib/i18n.tsx
index 68f572d..3d1deb6 100644
--- a/lib/i18n.tsx
+++ b/lib/i18n.tsx
@@ -53,6 +53,8 @@ const translations = {
"th.calls": "调用次数",
"th.input": "输入",
"th.output": "输出",
+ "th.cacheCreation": "缓存创建",
+ "th.cacheRead": "缓存读取",
"th.totalToken": "总 Token",
"th.time": "时间",
"th.realModel": "真实模型",
@@ -171,6 +173,8 @@ const translations = {
"th.calls": "Calls",
"th.input": "Input",
"th.output": "Output",
+ "th.cacheCreation": "Cache Write",
+ "th.cacheRead": "Cache Read",
"th.totalToken": "Total Token",
"th.time": "Time",
"th.realModel": "Real Model",
diff --git a/lib/queries.ts b/lib/queries.ts
index ab13983..4a8ad87 100644
--- a/lib/queries.ts
+++ b/lib/queries.ts
@@ -6,6 +6,16 @@ const REAL_MODEL = `COALESCE(
THEN other::jsonb->>'upstream_model_name' END,
model_name)`;
+const CACHE_CREATION = `COALESCE(
+ CASE WHEN other IS NOT NULL AND other != '' AND other::jsonb ? 'cache_creation_tokens'
+ THEN (other::jsonb->>'cache_creation_tokens')::bigint END,
+ 0)`;
+
+const CACHE_READ = `COALESCE(
+ CASE WHEN other IS NOT NULL AND other != '' AND other::jsonb ? 'cache_tokens'
+ THEN (other::jsonb->>'cache_tokens')::bigint END,
+ 0)`;
+
// ── 数据时间边界 ────────────────────────────────────────────────
export async function getDateRange(): Promise<{ minDate: string; maxDate: string }> {
@@ -43,6 +53,8 @@ export interface OverviewData {
total_tokens: number;
total_prompt: number;
total_completion: number;
+ total_cache_creation: number;
+ total_cache_read: number;
total_quota: number;
active_users: number;
active_models: number;
@@ -62,6 +74,8 @@ export async function getOverview(
COALESCE(SUM(prompt_tokens + completion_tokens), 0)::bigint as total_tokens,
COALESCE(SUM(prompt_tokens), 0)::bigint as total_prompt,
COALESCE(SUM(completion_tokens), 0)::bigint as total_completion,
+ COALESCE(SUM(${CACHE_CREATION}), 0)::bigint as total_cache_creation,
+ COALESCE(SUM(${CACHE_READ}), 0)::bigint as total_cache_read,
COALESCE(SUM(quota), 0)::bigint as total_quota,
COUNT(DISTINCT user_id)::int as active_users,
COUNT(DISTINCT ${REAL_MODEL})::int as active_models,
@@ -72,9 +86,11 @@ export async function getOverview(
const r = rows[0];
return {
total_calls: Number(r.total_calls),
- total_tokens: Number(r.total_tokens),
+ total_tokens: Number(r.total_tokens) + Number(r.total_cache_creation) + Number(r.total_cache_read),
total_prompt: Number(r.total_prompt),
total_completion: Number(r.total_completion),
+ total_cache_creation: Number(r.total_cache_creation),
+ total_cache_read: Number(r.total_cache_read),
total_quota: Number(r.total_quota),
active_users: Number(r.active_users),
active_models: Number(r.active_models),
@@ -89,6 +105,8 @@ export interface TrendPoint {
calls: number;
prompt_tokens: number;
completion_tokens: number;
+ cache_creation_tokens: number;
+ cache_read_tokens: number;
total_tokens: number;
quota: number;
}
@@ -112,6 +130,8 @@ export async function getTrends(
COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens), 0)::bigint as prompt_tokens,
COALESCE(SUM(completion_tokens), 0)::bigint as completion_tokens,
+ COALESCE(SUM(${CACHE_CREATION}), 0)::bigint as cache_creation_tokens,
+ COALESCE(SUM(${CACHE_READ}), 0)::bigint as cache_read_tokens,
COALESCE(SUM(quota), 0)::bigint as quota
FROM logs WHERE ${where}
GROUP BY date ORDER BY date`,
@@ -123,7 +143,9 @@ export async function getTrends(
calls: Number(r.calls),
prompt_tokens: Number(r.prompt_tokens),
completion_tokens: Number(r.completion_tokens),
- total_tokens: Number(r.prompt_tokens) + Number(r.completion_tokens),
+ cache_creation_tokens: Number(r.cache_creation_tokens),
+ cache_read_tokens: Number(r.cache_read_tokens),
+ total_tokens: Number(r.prompt_tokens) + Number(r.completion_tokens) + Number(r.cache_creation_tokens) + Number(r.cache_read_tokens),
quota: Number(r.quota),
}));
}
@@ -137,6 +159,8 @@ export interface RankingItem {
calls: number;
prompt_tokens: number;
completion_tokens: number;
+ cache_creation_tokens: number;
+ cache_read_tokens: number;
total_tokens: number;
quota: number;
quota_usd: number;
@@ -172,10 +196,12 @@ export async function getUserRanking(
COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens), 0)::bigint as prompt,
COALESCE(SUM(completion_tokens), 0)::bigint as completion,
+ COALESCE(SUM(${CACHE_CREATION}), 0)::bigint as cache_creation,
+ COALESCE(SUM(${CACHE_READ}), 0)::bigint as cache_read,
COALESCE(SUM(quota), 0)::bigint as quota
FROM logs WHERE ${where}
GROUP BY user_id, username
- ORDER BY COALESCE(SUM(prompt_tokens),0) + COALESCE(SUM(completion_tokens),0) DESC
+ ORDER BY COALESCE(SUM(prompt_tokens),0) + COALESCE(SUM(completion_tokens),0) + COALESCE(SUM(${CACHE_CREATION}),0) + COALESCE(SUM(${CACHE_READ}),0) DESC
LIMIT $${params.length}`,
params
);
@@ -188,7 +214,9 @@ export async function getUserRanking(
calls: Number(r.calls),
prompt_tokens: Number(r.prompt),
completion_tokens: Number(r.completion),
- total_tokens: Number(r.prompt) + Number(r.completion),
+ cache_creation_tokens: Number(r.cache_creation),
+ cache_read_tokens: Number(r.cache_read),
+ total_tokens: Number(r.prompt) + Number(r.completion) + Number(r.cache_creation) + Number(r.cache_read),
quota: Number(r.quota),
quota_usd: Number(r.quota) / 500000,
}));
@@ -208,10 +236,12 @@ export async function getModelRanking(
COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens), 0)::bigint as prompt,
COALESCE(SUM(completion_tokens), 0)::bigint as completion,
+ COALESCE(SUM(${CACHE_CREATION}), 0)::bigint as cache_creation,
+ COALESCE(SUM(${CACHE_READ}), 0)::bigint as cache_read,
COALESCE(SUM(quota), 0)::bigint as quota
FROM logs WHERE ${where}
GROUP BY model
- ORDER BY COALESCE(SUM(prompt_tokens),0) + COALESCE(SUM(completion_tokens),0) DESC
+ ORDER BY COALESCE(SUM(prompt_tokens),0) + COALESCE(SUM(completion_tokens),0) + COALESCE(SUM(${CACHE_CREATION}),0) + COALESCE(SUM(${CACHE_READ}),0) DESC
LIMIT $${params.length}`,
params
);
@@ -222,7 +252,9 @@ export async function getModelRanking(
calls: Number(r.calls),
prompt_tokens: Number(r.prompt),
completion_tokens: Number(r.completion),
- total_tokens: Number(r.prompt) + Number(r.completion),
+ cache_creation_tokens: Number(r.cache_creation),
+ cache_read_tokens: Number(r.cache_read),
+ total_tokens: Number(r.prompt) + Number(r.completion) + Number(r.cache_creation) + Number(r.cache_read),
quota: Number(r.quota),
quota_usd: Number(r.quota) / 500000,
}));
@@ -244,10 +276,12 @@ export async function getChannelRanking(
COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens), 0)::bigint as prompt,
COALESCE(SUM(completion_tokens), 0)::bigint as completion,
+ COALESCE(SUM(${CACHE_CREATION}), 0)::bigint as cache_creation,
+ COALESCE(SUM(${CACHE_READ}), 0)::bigint as cache_read,
COALESCE(SUM(quota), 0)::bigint as quota
FROM logs WHERE ${where}
GROUP BY channel_id
- ORDER BY COALESCE(SUM(prompt_tokens),0) + COALESCE(SUM(completion_tokens),0) DESC
+ ORDER BY COALESCE(SUM(prompt_tokens),0) + COALESCE(SUM(completion_tokens),0) + COALESCE(SUM(${CACHE_CREATION}),0) + COALESCE(SUM(${CACHE_READ}),0) DESC
LIMIT $${params.length}`,
params
);
@@ -259,7 +293,9 @@ export async function getChannelRanking(
calls: Number(r.calls),
prompt_tokens: Number(r.prompt),
completion_tokens: Number(r.completion),
- total_tokens: Number(r.prompt) + Number(r.completion),
+ cache_creation_tokens: Number(r.cache_creation),
+ cache_read_tokens: Number(r.cache_read),
+ total_tokens: Number(r.prompt) + Number(r.completion) + Number(r.cache_creation) + Number(r.cache_read),
quota: Number(r.quota),
quota_usd: Number(r.quota) / 500000,
}));
@@ -288,6 +324,8 @@ export async function getUserDetail(
`SELECT COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens),0)::bigint as prompt,
COALESCE(SUM(completion_tokens),0)::bigint as completion,
+ COALESCE(SUM(${CACHE_CREATION}),0)::bigint as cache_creation,
+ COALESCE(SUM(${CACHE_READ}),0)::bigint as cache_read,
COALESCE(SUM(quota),0)::bigint as quota
FROM logs WHERE ${where} AND username = $${params.length}`,
params
@@ -302,6 +340,8 @@ export async function getUserDetail(
`SELECT ${REAL_MODEL} as model,
COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens + completion_tokens),0)::bigint as tokens,
+ COALESCE(SUM(${CACHE_CREATION}),0)::bigint as cache_creation,
+ COALESCE(SUM(${CACHE_READ}),0)::bigint as cache_read,
COALESCE(SUM(quota),0)::bigint as quota
FROM logs WHERE ${where2} AND username = $${params2.length}
GROUP BY model
@@ -323,12 +363,14 @@ export async function getUserDetail(
calls: Number(o.calls),
prompt_tokens: Number(o.prompt),
completion_tokens: Number(o.completion),
- total_tokens: Number(o.prompt) + Number(o.completion),
+ cache_creation_tokens: Number(o.cache_creation),
+ cache_read_tokens: Number(o.cache_read),
+ total_tokens: Number(o.prompt) + Number(o.completion) + Number(o.cache_creation) + Number(o.cache_read),
quota: Number(o.quota),
models: models.map((m) => ({
name: m.model,
calls: Number(m.calls),
- total_tokens: Number(m.tokens),
+ total_tokens: Number(m.tokens) + Number(m.cache_creation) + Number(m.cache_read),
quota: Number(m.quota),
})),
};
@@ -347,6 +389,8 @@ export async function getModelDetail(
`SELECT COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens),0)::bigint as prompt,
COALESCE(SUM(completion_tokens),0)::bigint as completion,
+ COALESCE(SUM(${CACHE_CREATION}),0)::bigint as cache_creation,
+ COALESCE(SUM(${CACHE_READ}),0)::bigint as cache_read,
COALESCE(SUM(quota),0)::bigint as quota
FROM logs WHERE ${where} AND ${REAL_MODEL} = $${params.length}`,
params
@@ -361,6 +405,8 @@ export async function getModelDetail(
`SELECT user_id, username,
COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens + completion_tokens),0)::bigint as tokens,
+ COALESCE(SUM(${CACHE_CREATION}),0)::bigint as cache_creation,
+ COALESCE(SUM(${CACHE_READ}),0)::bigint as cache_read,
COALESCE(SUM(quota),0)::bigint as quota
FROM logs WHERE ${where2} AND ${REAL_MODEL} = $${params2.length}
GROUP BY user_id, username
@@ -373,12 +419,14 @@ export async function getModelDetail(
calls: Number(o.calls),
prompt_tokens: Number(o.prompt),
completion_tokens: Number(o.completion),
- total_tokens: Number(o.prompt) + Number(o.completion),
+ cache_creation_tokens: Number(o.cache_creation),
+ cache_read_tokens: Number(o.cache_read),
+ total_tokens: Number(o.prompt) + Number(o.completion) + Number(o.cache_creation) + Number(o.cache_read),
quota: Number(o.quota),
users: users.map((u) => ({
name: displayNames[u.user_id] || u.username,
calls: Number(u.calls),
- total_tokens: Number(u.tokens),
+ total_tokens: Number(u.tokens) + Number(u.cache_creation) + Number(u.cache_read),
quota: Number(u.quota),
})),
};
@@ -399,6 +447,8 @@ export async function getChannelDetail(
`SELECT COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens),0)::bigint as prompt,
COALESCE(SUM(completion_tokens),0)::bigint as completion,
+ COALESCE(SUM(${CACHE_CREATION}),0)::bigint as cache_creation,
+ COALESCE(SUM(${CACHE_READ}),0)::bigint as cache_read,
COALESCE(SUM(quota),0)::bigint as quota
FROM logs WHERE ${where} AND channel_id = $${params.length}`,
params
@@ -412,6 +462,8 @@ export async function getChannelDetail(
`SELECT ${REAL_MODEL} as model,
COUNT(*)::int as calls,
COALESCE(SUM(prompt_tokens + completion_tokens),0)::bigint as tokens,
+ COALESCE(SUM(${CACHE_CREATION}),0)::bigint as cache_creation,
+ COALESCE(SUM(${CACHE_READ}),0)::bigint as cache_read,
COALESCE(SUM(quota),0)::bigint as quota
FROM logs WHERE ${where2} AND channel_id = $${params2.length}
GROUP BY model
@@ -425,12 +477,14 @@ export async function getChannelDetail(
calls: Number(o.calls),
prompt_tokens: Number(o.prompt),
completion_tokens: Number(o.completion),
- total_tokens: Number(o.prompt) + Number(o.completion),
+ cache_creation_tokens: Number(o.cache_creation),
+ cache_read_tokens: Number(o.cache_read),
+ total_tokens: Number(o.prompt) + Number(o.completion) + Number(o.cache_creation) + Number(o.cache_read),
quota: Number(o.quota),
models: models.map((m) => ({
name: m.model,
calls: Number(m.calls),
- total_tokens: Number(m.tokens),
+ total_tokens: Number(m.tokens) + Number(m.cache_creation) + Number(m.cache_read),
quota: Number(m.quota),
})),
};
@@ -449,6 +503,8 @@ export interface LogEntry {
channel_id: number;
prompt_tokens: number;
completion_tokens: number;
+ cache_creation_tokens: number;
+ cache_read_tokens: number;
total_tokens: number;
quota: number;
quota_usd: number;
@@ -513,6 +569,8 @@ export async function getLogs(options: {
`SELECT id, created_at, user_id, username, model_name,
${REAL_MODEL} as real_model,
channel_id, prompt_tokens, completion_tokens, quota,
+ ${CACHE_CREATION} as cache_creation,
+ ${CACHE_READ} as cache_read,
use_time, is_stream, token_name
FROM logs WHERE ${where}
ORDER BY id DESC
@@ -535,7 +593,9 @@ export async function getLogs(options: {
channel_id: Number(r.channel_id),
prompt_tokens: Number(r.prompt_tokens),
completion_tokens: Number(r.completion_tokens),
- total_tokens: Number(r.prompt_tokens) + Number(r.completion_tokens),
+ cache_creation_tokens: Number(r.cache_creation),
+ cache_read_tokens: Number(r.cache_read),
+ total_tokens: Number(r.prompt_tokens) + Number(r.completion_tokens) + Number(r.cache_creation) + Number(r.cache_read),
quota: Number(r.quota),
quota_usd: Number(r.quota) / 500000,
use_time: Number(r.use_time),