diff --git a/server/internal/api/admin_stats.go b/server/internal/api/admin_stats.go index 98714c0..264c10c 100644 --- a/server/internal/api/admin_stats.go +++ b/server/internal/api/admin_stats.go @@ -257,7 +257,9 @@ func (h *Handler) AdminUsage(c *gin.Context) { "id": l.ID, "user": userNames[l.UserID], "user_id": l.UserID, "model": l.ModelName, "protocol": l.Protocol, "channel_id": l.ChannelID, "channel": chNames[l.ChannelID], "input_tokens": l.InputTokens, "output_tokens": l.OutputTokens, - "cache_read_tokens": l.CacheReadTokens, "cost": l.Cost, + "cache_read_tokens": l.CacheReadTokens, "cache_creation_tokens": l.CacheCreationTokens, + "reasoning_tokens": l.ReasoningTokens, "cache_write_1h_tokens": l.CacheWrite1hTokens, + "served_model": l.ServedModel, "cost": l.Cost, "latency_ms": l.LatencyMS, "status": l.Status, "error_code": l.ErrorCode, "raw_request": l.RawRequest, "raw_response": l.RawResponse, "created_at": l.CreatedAt, diff --git a/server/internal/api/usage.go b/server/internal/api/usage.go index cc2615d..5de908f 100644 --- a/server/internal/api/usage.go +++ b/server/internal/api/usage.go @@ -105,7 +105,9 @@ func (h *Handler) UsageLogs(c *gin.Context) { out = append(out, gin.H{ "id": l.ID, "request_id": l.RequestID, "model": l.ModelName, "protocol": l.Protocol, "input_tokens": l.InputTokens, "output_tokens": l.OutputTokens, - "cache_read_tokens": l.CacheReadTokens, "cost": l.Cost, + "cache_read_tokens": l.CacheReadTokens, "cache_creation_tokens": l.CacheCreationTokens, + "reasoning_tokens": l.ReasoningTokens, "cache_write_1h_tokens": l.CacheWrite1hTokens, + "served_model": l.ServedModel, "cost": l.Cost, "latency_ms": l.LatencyMS, "status": l.Status, "error_code": l.ErrorCode, "created_at": l.CreatedAt, }) diff --git a/web/src/types.ts b/web/src/types.ts index 68b91e9..14891de 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -112,6 +112,13 @@ export interface UsageLog { input_tokens: number output_tokens: number cache_read_tokens: number + cache_creation_tokens?: number + /** 思考 token:上游已计入 output_tokens,仅展示口径 */ + reasoning_tokens?: number + /** Anthropic 1h 缓存写(计价 2× 输入);含在 cache_creation_tokens 内 */ + cache_write_1h_tokens?: number + /** 上游实际应答的模型名;模型映射/聚合商场景下与 model 不同 */ + served_model?: string cost: number latency_ms: number status: string diff --git a/web/src/views/admin/UsageView.vue b/web/src/views/admin/UsageView.vue index 47f077b..c0f2f10 100644 --- a/web/src/views/admin/UsageView.vue +++ b/web/src/views/admin/UsageView.vue @@ -2,8 +2,7 @@ import { onMounted, ref } from 'vue' import { http, errMsg } from '@/api/client' import { useToastStore } from '@/stores/toast' -import { protocolShort } from '@/lib/protocol' -import { fmtCost, fmtTime } from '@/lib/format' +import { fmtCost, fmtNum, fmtTime } from '@/lib/format' import Badge from '@/components/ui/Badge.vue' import Button from '@/components/ui/Button.vue' import Modal from '@/components/ui/Modal.vue' @@ -22,6 +21,50 @@ const pageSize = 15 const viewing = ref(null) const viewTab = ref<'request' | 'response'>('request') +// cacheLine 缓存读/写的紧凑摘要(换行会碎成多行,只放两个数);无缓存流量返回空串。 +function cacheLine(l: UsageLog): string { + const read = l.cache_read_tokens || 0 + const write = l.cache_creation_tokens || 0 + const parts: string[] = [] + if (read) parts.push(`读${fmtNum(read)}`) + if (write) parts.push(`写${fmtNum(write)}`) + return parts.join(' ') +} + +// cacheExtra 第二行:1h 缓存写(计费 2× 输入)与思考 token,两者都没有则返回空串。 +function cacheExtra(l: UsageLog): string { + const oneHour = l.cache_write_1h_tokens || 0 + const thinking = l.reasoning_tokens || 0 + const parts: string[] = [] + if (oneHour) parts.push(`1h${fmtNum(oneHour)}`) + if (thinking) parts.push(`思考${fmtNum(thinking)}`) + return parts.join(' · ') +} + +// fmtClock 表格里的紧凑时间(月-日 时:分):完整时间在悬浮提示里, +// 否则这一列在 1280 宽下会把「原始」列挤出视野。 +function fmtClock(s?: string | null): string { + if (!s) return '-' + const d = new Date(s) + if (Number.isNaN(d.getTime())) return '-' + return d.toLocaleString('zh-CN', { + month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, + }) +} + +// tokenTitle Token 列的悬浮提示:表格里只放缓存读/写(与成本直接相关), +// 1h 缓存写与思考 token 放提示里,避免把列撑宽挤出其它列。 +function tokenTitle(l: UsageLog): string { + const extra = cacheExtra(l) + const base = `输入 ${l.input_tokens} / 输出 ${l.output_tokens}` + return extra ? `${base} · ${extra}` : base +} + +// servedModel 上游实际应答的模型:仅在模型映射/聚合商改写时与请求模型不同才展示。 +function servedModel(l: UsageLog): string { + return l.served_model && l.served_model !== l.model ? l.served_model : '' +} + function openRaw(l: UsageLog) { viewing.value = l viewTab.value = 'request' @@ -140,6 +183,7 @@ onMounted(load)

{{ l.model }}

+

→ {{ servedModel(l) }}

{{ l.channel }} · {{ l.user }}

@@ -148,8 +192,10 @@ onMounted(load)
- {{ protocolShort(l.protocol) }} + {{ l.protocol }} {{ l.input_tokens }}/{{ l.output_tokens }} tok + 缓存 {{ cacheLine(l) }} + {{ cacheExtra(l) }} {{ l.latency_ms }}ms {{ fmtTime(l.created_at) }} @@ -160,9 +206,9 @@ onMounted(load)