feat: record cache hit/miss per request in audit trail and WebUI

- Req: add CacheHit and CacheMiss fields (carrying upstream cache
  accounting from either prompt_tokens_details.cached_tokens or legacy
  prompt_cache_hit_tokens)
- recordChatUsage (non-streaming): copy cache fields from resp.TokenUsage
- pumpStream (streaming): write lastUsage cache fields back onto rec at
  stream end, so streaming requests carry cache data too
- CSV export: add first_byte_ms, cache_hit_tokens, cache_miss_tokens
  columns alongside the existing latency/prompt/completion
- WebUI request-records table: add a Cache column showing hit% per row
  (green/amber tag with tooltip hit/miss breakdown; em-dash when the
  upstream reported no cache data)
This commit is contained in:
dev
2026-08-25 09:36:21 +08:00
parent 18c2385c61
commit 24609289e8
4 changed files with 44 additions and 3 deletions

View File

@ -1971,18 +1971,30 @@
const slice = records.slice().reverse().slice(0, 300);
el.innerHTML =
`<table><tr><th>${t("thTime")}</th><th class="num">${t("thStatus")}</th><th>${t("thKey")}</th><th>${t("thType")}</th><th>${t("thModel")}</th><th>${t("thSrc")}</th>
<th class="num">${t("thPrompt")}</th><th class="num">${t("thCompl")}</th><th class="num">${t("thLatMs")}</th></tr>` +
<th class="num">${t("thPrompt")}</th><th class="num">${t("thCompl")}</th><th class="num">${t("thCache") || "缓存"}</th><th class="num">${t("thLatMs")}</th></tr>` +
slice
.map(
(r) => `<tr>
<td class="t-tag">${fmtTime(r.time)}</td>
<td class="num">${r.ok ? `<span class="tag tag-green">${r.status || 200}</span>` : `<span class="tag tag-red" title="${esc(r.error || "")}">${r.status || 500}</span>`}</td>
<td>${esc(keyNames[r.key] ? keyNames[r.key] + " · " + r.key : r.key)}</td><td class="t-tag">${esc(r.type)}</td><td>${esc(r.model)}</td><td>${esc(r.source || "")}</td>
<td class="num">${fmtTok(r.prompt_tokens)}</td><td class="num">${fmtTok(r.completion_tokens)}</td><td class="num">${fmtMs(r.latency_ms)}</td></tr>`,
<td class="num">${fmtTok(r.prompt_tokens)}</td><td class="num">${fmtTok(r.completion_tokens)}</td>
<td class="num">${cacheCell(r)}</td>
<td class="num">${fmtMs(r.latency_ms)}</td></tr>`,
)
.join("") +
"</table>";
}
// cacheCell renders the per-request cache-hit column: a percentage when
// the upstream reported cache accounting, otherwise an em dash.
function cacheCell(r) {
const hit = r.cache_hit_tokens || 0;
if (!hit && !(r.cache_miss_tokens > 0)) return '<span class="muted">—</span>';
const prompt = r.prompt_tokens || 0;
const pct = prompt > 0 ? Math.round((hit * 100) / prompt) : 0;
return `<span class="tag ${pct >= 50 ? "tag-green" : pct > 0 ? "tag-amber" : ""}" title="命中 ${hit} / 未命中 ${r.cache_miss_tokens || 0}">${pct}%</span>`;
}
function showModelConfig(srcName, model) {
const el = $("#conncfg");
if (!el) return;