feat(web): 用量明细展示缓存读/写与上游实际模型,补 1h 缓存写与思考口径
API:/api/v1/usage/logs 与 /api/v1/admin/usage 返回 cache_creation_tokens、 reasoning_tokens、cache_write_1h_tokens、served_model(types.ts 同步可选字段)。 前端: - 模型列在被模型映射/聚合商改写时显示「→ 上游实际模型」,长名截断并可悬浮看全; - 表格里 Token 列第二行显示缓存读/写(与成本直接相关),1h 缓存写与思考 token 放悬浮提示——两者直接上表格会把 Token 列撑到 255px,1280 宽下把「时间/原始」 挤出视野(实测 1146px vs 容器 1006px);移动卡两行都显示; - 用量表协议列由 chat/completions 简化为 chat(表头已标明「API 格式」), 时间列改月-日 时:分、完整时间进 title;调整后桌面端表格宽度 1006px, 与容器一致、无横向滚动; - 顺带修复控制台用量页 Button 未导入(上一页/下一页此前渲染不出来)。 验证:本地 mock 上游 + 临时 sqlite 实例与真实数据实测;1280 桌面与 390 窄屏 截图核对(表格 tableW == 容器宽度);vue-tsc、vitest、eslint、vite build 与 go vet、go test ./... 全绿。
This commit is contained in:
1 parent
04dfb4285d
commit
d34d9bd2e6
5 files changed
+135
-18
No files matched your search
@@ -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,
|
"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],
|
"channel_id": l.ChannelID, "channel": chNames[l.ChannelID],
|
||||||
"input_tokens": l.InputTokens, "output_tokens": l.OutputTokens,
|
"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,
|
"latency_ms": l.LatencyMS, "status": l.Status, "error_code": l.ErrorCode,
|
||||||
"raw_request": l.RawRequest, "raw_response": l.RawResponse,
|
"raw_request": l.RawRequest, "raw_response": l.RawResponse,
|
||||||
"created_at": l.CreatedAt,
|
"created_at": l.CreatedAt,
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ func (h *Handler) UsageLogs(c *gin.Context) {
|
|||||||
out = append(out, gin.H{
|
out = append(out, gin.H{
|
||||||
"id": l.ID, "request_id": l.RequestID, "model": l.ModelName, "protocol": l.Protocol,
|
"id": l.ID, "request_id": l.RequestID, "model": l.ModelName, "protocol": l.Protocol,
|
||||||
"input_tokens": l.InputTokens, "output_tokens": l.OutputTokens,
|
"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,
|
"latency_ms": l.LatencyMS, "status": l.Status, "error_code": l.ErrorCode,
|
||||||
"created_at": l.CreatedAt,
|
"created_at": l.CreatedAt,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -112,6 +112,13 @@ export interface UsageLog {
|
|||||||
input_tokens: number
|
input_tokens: number
|
||||||
output_tokens: number
|
output_tokens: number
|
||||||
cache_read_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
|
cost: number
|
||||||
latency_ms: number
|
latency_ms: number
|
||||||
status: string
|
status: string
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { http, errMsg } from '@/api/client'
|
import { http, errMsg } from '@/api/client'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { protocolShort } from '@/lib/protocol'
|
import { fmtCost, fmtNum, fmtTime } from '@/lib/format'
|
||||||
import { fmtCost, fmtTime } from '@/lib/format'
|
|
||||||
import Badge from '@/components/ui/Badge.vue'
|
import Badge from '@/components/ui/Badge.vue'
|
||||||
import Button from '@/components/ui/Button.vue'
|
import Button from '@/components/ui/Button.vue'
|
||||||
import Modal from '@/components/ui/Modal.vue'
|
import Modal from '@/components/ui/Modal.vue'
|
||||||
@@ -22,6 +21,50 @@ const pageSize = 15
|
|||||||
const viewing = ref<UsageLog | null>(null)
|
const viewing = ref<UsageLog | null>(null)
|
||||||
const viewTab = ref<'request' | 'response'>('request')
|
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) {
|
function openRaw(l: UsageLog) {
|
||||||
viewing.value = l
|
viewing.value = l
|
||||||
viewTab.value = 'request'
|
viewTab.value = 'request'
|
||||||
@@ -140,6 +183,7 @@ onMounted(load)
|
|||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate font-mono text-sm text-ink">{{ l.model }}</p>
|
<p class="truncate font-mono text-sm text-ink">{{ l.model }}</p>
|
||||||
|
<p v-if="servedModel(l)" class="truncate font-mono text-xs text-muted">→ {{ servedModel(l) }}</p>
|
||||||
<p class="mt-0.5 truncate text-xs text-muted">{{ l.channel }} · {{ l.user }}</p>
|
<p class="mt-0.5 truncate text-xs text-muted">{{ l.channel }} · {{ l.user }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 flex-col items-end gap-1">
|
<div class="flex shrink-0 flex-col items-end gap-1">
|
||||||
@@ -148,8 +192,10 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 border-t border-edge pt-2 text-xs text-muted">
|
<div class="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 border-t border-edge pt-2 text-xs text-muted">
|
||||||
<span class="font-mono">{{ protocolShort(l.protocol) }}</span>
|
<span class="font-mono">{{ l.protocol }}</span>
|
||||||
<span class="mono-num">{{ l.input_tokens }}/{{ l.output_tokens }} tok</span>
|
<span class="mono-num">{{ l.input_tokens }}/{{ l.output_tokens }} tok</span>
|
||||||
|
<span v-if="cacheLine(l)" class="mono-num">缓存 {{ cacheLine(l) }}</span>
|
||||||
|
<span v-if="cacheExtra(l)" class="mono-num">{{ cacheExtra(l) }}</span>
|
||||||
<span class="mono-num">{{ l.latency_ms }}ms</span>
|
<span class="mono-num">{{ l.latency_ms }}ms</span>
|
||||||
<span class="mono-num w-full">{{ fmtTime(l.created_at) }}</span>
|
<span class="mono-num w-full">{{ fmtTime(l.created_at) }}</span>
|
||||||
<Button v-if="l.raw_request" size="sm" variant="ghost" class="ml-auto" @click="openRaw(l)">查看原始</Button>
|
<Button v-if="l.raw_request" size="sm" variant="ghost" class="ml-auto" @click="openRaw(l)">查看原始</Button>
|
||||||
@@ -160,9 +206,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<!-- 桌面端:表格 -->
|
<!-- 桌面端:表格 -->
|
||||||
<div class="hidden overflow-x-auto lg:block">
|
<div class="hidden overflow-x-auto lg:block">
|
||||||
<table class="w-full text-sm min-w-[760px]">
|
<table class="w-full text-sm min-w-[900px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-edge text-left text-xs text-muted">
|
<tr class="whitespace-nowrap border-b border-edge text-left text-xs text-muted">
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">用户</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">用户</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">模型</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">模型</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">渠道</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">渠道</th>
|
||||||
@@ -178,16 +224,29 @@ onMounted(load)
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="l in logs" :key="l.id" class="table-row">
|
<tr v-for="l in logs" :key="l.id" class="table-row">
|
||||||
<td class="px-4 py-2.5 text-xs text-ink">{{ l.user }}</td>
|
<td class="px-4 py-2.5 text-xs text-ink">{{ l.user }}</td>
|
||||||
<td class="px-4 py-2.5 font-mono text-xs text-ink">{{ l.model }}</td>
|
<td class="px-4 py-2.5 font-mono text-xs text-ink">
|
||||||
|
{{ l.model }}
|
||||||
|
<p v-if="servedModel(l)" :title="servedModel(l)" class="max-w-[118px] truncate text-[11px] text-muted">
|
||||||
|
→ {{ servedModel(l) }}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ l.channel || '-' }}</td>
|
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ l.channel || '-' }}</td>
|
||||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ protocolShort(l.protocol) }}</td>
|
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ l.protocol }}</td>
|
||||||
<td class="px-4 py-2.5 mono-num text-xs text-muted">{{ l.input_tokens }}/{{ l.output_tokens }}</td>
|
<td
|
||||||
|
class="whitespace-nowrap px-4 py-2.5 mono-num text-xs text-muted"
|
||||||
|
:title="tokenTitle(l)"
|
||||||
|
>
|
||||||
|
<span>{{ l.input_tokens }}/{{ l.output_tokens }}</span>
|
||||||
|
<p v-if="cacheLine(l)" class="text-[11px]">缓存 {{ cacheLine(l) }}</p>
|
||||||
|
</td>
|
||||||
<td class="px-4 py-2.5 mono-num text-xs text-ink">{{ fmtCost(l.cost) }}</td>
|
<td class="px-4 py-2.5 mono-num text-xs text-ink">{{ fmtCost(l.cost) }}</td>
|
||||||
<td class="px-4 py-2.5 mono-num text-xs text-muted">{{ l.latency_ms }}ms</td>
|
<td class="px-4 py-2.5 mono-num text-xs text-muted">{{ l.latency_ms }}ms</td>
|
||||||
<td class="px-4 py-2.5">
|
<td class="px-4 py-2.5">
|
||||||
<Badge :variant="l.status === 'success' ? 'ok' : l.status === 'canceled' ? 'neutral' : 'err'">{{ l.status }}</Badge>
|
<Badge :variant="l.status === 'success' ? 'ok' : l.status === 'canceled' ? 'neutral' : 'err'">{{ l.status }}</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ fmtTime(l.created_at) }}</td>
|
<td class="whitespace-nowrap px-4 py-2.5 font-mono text-xs text-muted" :title="fmtTime(l.created_at)">
|
||||||
|
{{ fmtClock(l.created_at) }}
|
||||||
|
</td>
|
||||||
<td class="px-4 py-2.5">
|
<td class="px-4 py-2.5">
|
||||||
<Button v-if="l.raw_request" size="sm" variant="ghost" @click="openRaw(l)">查看</Button>
|
<Button v-if="l.raw_request" size="sm" variant="ghost" @click="openRaw(l)">查看</Button>
|
||||||
<span v-else class="text-xs text-muted">-</span>
|
<span v-else class="text-xs text-muted">-</span>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { onMounted, ref } from 'vue'
|
|||||||
import { http, errMsg } from '@/api/client'
|
import { http, errMsg } from '@/api/client'
|
||||||
import { useToastStore } from '@/stores/toast'
|
import { useToastStore } from '@/stores/toast'
|
||||||
import { fmtNum, fmtCost, fmtTime } from '@/lib/format'
|
import { fmtNum, fmtCost, fmtTime } from '@/lib/format'
|
||||||
import { protocolShort } from '@/lib/protocol'
|
|
||||||
import Badge from '@/components/ui/Badge.vue'
|
import Badge from '@/components/ui/Badge.vue'
|
||||||
|
import Button from '@/components/ui/Button.vue'
|
||||||
import TrendChart from '@/components/ui/TrendChart.vue'
|
import TrendChart from '@/components/ui/TrendChart.vue'
|
||||||
import type { UsageLog } from '@/types'
|
import type { UsageLog } from '@/types'
|
||||||
|
|
||||||
@@ -35,6 +35,39 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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(' · ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 switchGroup(g: 'day' | 'model') {
|
function switchGroup(g: 'day' | 'model') {
|
||||||
group.value = g
|
group.value = g
|
||||||
load()
|
load()
|
||||||
@@ -114,7 +147,8 @@ onMounted(load)
|
|||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate font-mono text-sm text-ink">{{ l.model }}</p>
|
<p class="truncate font-mono text-sm text-ink">{{ l.model }}</p>
|
||||||
<p class="mt-0.5 font-mono text-xs text-muted">{{ protocolShort(l.protocol) }}</p>
|
<p v-if="servedModel(l)" class="truncate font-mono text-xs text-muted">→ {{ servedModel(l) }}</p>
|
||||||
|
<p class="mt-0.5 font-mono text-xs text-muted">{{ l.protocol }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 flex-col items-end gap-1">
|
<div class="flex shrink-0 flex-col items-end gap-1">
|
||||||
<Badge :variant="l.status === 'success' ? 'ok' : l.status === 'canceled' ? 'neutral' : 'err'">{{ l.status }}</Badge>
|
<Badge :variant="l.status === 'success' ? 'ok' : l.status === 'canceled' ? 'neutral' : 'err'">{{ l.status }}</Badge>
|
||||||
@@ -123,6 +157,8 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 border-t border-edge pt-2 text-xs text-muted">
|
<div class="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 border-t border-edge pt-2 text-xs text-muted">
|
||||||
<span class="mono-num">{{ l.input_tokens }}/{{ l.output_tokens }} tok</span>
|
<span class="mono-num">{{ l.input_tokens }}/{{ l.output_tokens }} tok</span>
|
||||||
|
<span v-if="cacheLine(l)" class="mono-num">缓存 {{ cacheLine(l) }}</span>
|
||||||
|
<span v-if="cacheExtra(l)" class="mono-num">{{ cacheExtra(l) }}</span>
|
||||||
<span class="mono-num">{{ l.latency_ms }}ms</span>
|
<span class="mono-num">{{ l.latency_ms }}ms</span>
|
||||||
<span class="mono-num w-full">{{ fmtTime(l.created_at) }}</span>
|
<span class="mono-num w-full">{{ fmtTime(l.created_at) }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,9 +168,9 @@ onMounted(load)
|
|||||||
|
|
||||||
<!-- 桌面端:表格 -->
|
<!-- 桌面端:表格 -->
|
||||||
<div class="hidden overflow-x-auto lg:block">
|
<div class="hidden overflow-x-auto lg:block">
|
||||||
<table class="w-full text-sm min-w-[640px]">
|
<table class="w-full text-sm min-w-[720px]">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-edge text-left text-xs text-muted">
|
<tr class="whitespace-nowrap border-b border-edge text-left text-xs text-muted">
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">模型</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">模型</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">协议</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">协议</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">Token 入/出</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">Token 入/出</th>
|
||||||
@@ -146,9 +182,20 @@ onMounted(load)
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="l in logs" :key="l.id" class="table-row">
|
<tr v-for="l in logs" :key="l.id" class="table-row">
|
||||||
<td class="px-4 py-2.5 font-mono text-xs text-ink">{{ l.model }}</td>
|
<td class="px-4 py-2.5 font-mono text-xs text-ink">
|
||||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ protocolShort(l.protocol) }}</td>
|
{{ l.model }}
|
||||||
<td class="px-4 py-2.5 mono-num text-xs text-muted">{{ l.input_tokens }}/{{ l.output_tokens }}</td>
|
<p v-if="servedModel(l)" :title="servedModel(l)" class="max-w-[118px] truncate text-[11px] text-muted">
|
||||||
|
→ {{ servedModel(l) }}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ l.protocol }}</td>
|
||||||
|
<td
|
||||||
|
class="whitespace-nowrap px-4 py-2.5 mono-num text-xs text-muted"
|
||||||
|
:title="tokenTitle(l)"
|
||||||
|
>
|
||||||
|
<span>{{ l.input_tokens }}/{{ l.output_tokens }}</span>
|
||||||
|
<p v-if="cacheLine(l)" class="text-[11px]">缓存 {{ cacheLine(l) }}</p>
|
||||||
|
</td>
|
||||||
<td class="px-4 py-2.5 mono-num text-xs text-ink">{{ fmtCost(l.cost) }}</td>
|
<td class="px-4 py-2.5 mono-num text-xs text-ink">{{ fmtCost(l.cost) }}</td>
|
||||||
<td class="px-4 py-2.5 mono-num text-xs text-muted">{{ l.latency_ms }}ms</td>
|
<td class="px-4 py-2.5 mono-num text-xs text-muted">{{ l.latency_ms }}ms</td>
|
||||||
<td class="px-4 py-2.5"><Badge :variant="l.status === 'success' ? 'ok' : l.status === 'canceled' ? 'neutral' : 'err'">{{ l.status }}</Badge></td>
|
<td class="px-4 py-2.5"><Badge :variant="l.status === 'success' ? 'ok' : l.status === 'canceled' ? 'neutral' : 'err'">{{ l.status }}</Badge></td>
|
||||||
|
|||||||
Reference in new issue
Block a user