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),