diff --git a/internal/gateway/api.go b/internal/gateway/api.go index 417f396..360ba11 100644 --- a/internal/gateway/api.go +++ b/internal/gateway/api.go @@ -171,7 +171,7 @@ func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) { for _, k := range g.core.ListKeys() { names[keyID(k.Key)] = k.Name } - _ = cw.Write([]string{"time", "key", "key_name", "type", "model", "source", "status", "ok", "prompt_tokens", "completion_tokens", "latency_ms", "error"}) + _ = cw.Write([]string{"time", "key", "key_name", "type", "model", "source", "status", "ok", "prompt_tokens", "completion_tokens", "latency_ms", "first_byte_ms", "cache_hit_tokens", "cache_miss_tokens", "error"}) for _, rec := range g.stats.AuditRecords(from, to, key) { _ = cw.Write([]string{ time.UnixMilli(rec.Time).Format(time.RFC3339), @@ -185,6 +185,9 @@ func (g *Gateway) handleStatsAPI(w http.ResponseWriter, r *http.Request) { strconv.FormatInt(rec.Prompt, 10), strconv.FormatInt(rec.Compl, 10), strconv.FormatInt(rec.LatMs, 10), + strconv.FormatInt(rec.FirstByteMs, 10), + strconv.FormatInt(rec.CacheHit, 10), + strconv.FormatInt(rec.CacheMiss, 10), rec.Err, }) } diff --git a/internal/gateway/chat.go b/internal/gateway/chat.go index 429d0df..8407da4 100644 --- a/internal/gateway/chat.go +++ b/internal/gateway/chat.go @@ -570,6 +570,17 @@ func recordChatUsage(rec *Req, req *types.ChatRequest, resp *types.UnifiedRespon if rec.Compl == 0 { rec.Compl = estimateTextTokens(resp.Content, resp.ReasoningContent, resp.ToolCalls) } + // Cache accounting: whichever source format the adapter normalized into + // (prompt_tokens_details.cached_tokens or legacy prompt_cache_hit_tokens), + // read it back so the request record carries the hit/miss split. + if d := resp.TokenUsage.PromptTokensDetails; d != nil && d.CachedTokens > 0 { + rec.CacheHit = int64(d.CachedTokens) + } else if resp.TokenUsage.PromptCacheHit > 0 { + rec.CacheHit = int64(resp.TokenUsage.PromptCacheHit) + } + if resp.TokenUsage.PromptCacheMiss > 0 { + rec.CacheMiss = int64(resp.TokenUsage.PromptCacheMiss) + } } // writeChatCompletion renders a unified response as an OpenAI @@ -748,6 +759,16 @@ func (g *Gateway) pumpStream(w http.ResponseWriter, rec *Req, chunks <-chan type var tut *types.TokenUsage if lastUsage != nil { tut = lastUsage + // Write the upstream's cache accounting back onto the request record + // so the audit trail carries the hit/miss split for streaming too. + if d := tut.PromptTokensDetails; d != nil && d.CachedTokens > 0 { + rec.CacheHit = int64(d.CachedTokens) + } else if tut.PromptCacheHit > 0 { + rec.CacheHit = int64(tut.PromptCacheHit) + } + if tut.PromptCacheMiss > 0 { + rec.CacheMiss = int64(tut.PromptCacheMiss) + } } else if rec.Prompt+rec.Compl > 0 { u := types.TokenUsage{ Prompt: int(rec.Prompt), diff --git a/internal/gateway/stats.go b/internal/gateway/stats.go index 789e833..2cef29e 100644 --- a/internal/gateway/stats.go +++ b/internal/gateway/stats.go @@ -30,6 +30,11 @@ type Req struct { // to the first SSE chunk sent to the client); for non-streaming it // equals LatMs. 0 when unmeasured (legacy records). FirstByteMs int64 `json:"first_byte_ms,omitempty"` + // CacheHit / CacheMiss carry the upstream prompt-cache accounting + // (DeepSeek-style hit/miss tokens) when the upstream reports it. + // Both 0 = upstream gave no cache data. + CacheHit int64 `json:"cache_hit_tokens,omitempty"` + CacheMiss int64 `json:"cache_miss_tokens,omitempty"` OK bool `json:"ok"` // Status http status code Status int `json:"status"` diff --git a/internal/gateway/ui/index.html b/internal/gateway/ui/index.html index 8ffb2ef..52c1b87 100644 --- a/internal/gateway/ui/index.html +++ b/internal/gateway/ui/index.html @@ -1971,18 +1971,30 @@ const slice = records.slice().reverse().slice(0, 300); el.innerHTML = `
| ${t("thTime")} | ${t("thStatus")} | ${t("thKey")} | ${t("thType")} | ${t("thModel")} | ${t("thSrc")} | -${t("thPrompt")} | ${t("thCompl")} | ${t("thLatMs")} | ${t("thPrompt")} | ${t("thCompl")} | ${t("thCache") || "缓存"} | ${t("thLatMs")} | ` + slice .map( (r) => `
|---|---|---|---|---|---|---|---|---|
| ${fmtTime(r.time)} | ${r.ok ? `${r.status || 200}` : `${r.status || 500}`} | ${esc(keyNames[r.key] ? keyNames[r.key] + " · " + r.key : r.key)} | ${esc(r.type)} | ${esc(r.model)} | ${esc(r.source || "")} | -${fmtTok(r.prompt_tokens)} | ${fmtTok(r.completion_tokens)} | ${fmtMs(r.latency_ms)} | ${fmtTok(r.prompt_tokens)} | ${fmtTok(r.completion_tokens)} | +${cacheCell(r)} | +${fmtMs(r.latency_ms)} | `, ) .join("") + "