diff --git a/internal/gateway/chat.go b/internal/gateway/chat.go index 8407da4..71f43a6 100644 --- a/internal/gateway/chat.go +++ b/internal/gateway/chat.go @@ -575,8 +575,14 @@ func recordChatUsage(rec *Req, req *types.ChatRequest, resp *types.UnifiedRespon // 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) + } + if resp.TokenUsage.PromptTokensDetails != nil { + // Upstream reported cache details (even a 0 hit) — tag the row so + // the UI can show 0% rather than “—”. + rec.CacheReported = true } else if resp.TokenUsage.PromptCacheHit > 0 { rec.CacheHit = int64(resp.TokenUsage.PromptCacheHit) + rec.CacheReported = true } if resp.TokenUsage.PromptCacheMiss > 0 { rec.CacheMiss = int64(resp.TokenUsage.PromptCacheMiss) @@ -761,10 +767,14 @@ func (g *Gateway) pumpStream(w http.ResponseWriter, rec *Req, chunks <-chan type 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) + if d := tut.PromptTokensDetails; d != nil { + rec.CacheReported = true + if d.CachedTokens > 0 { + rec.CacheHit = int64(d.CachedTokens) + } } else if tut.PromptCacheHit > 0 { rec.CacheHit = int64(tut.PromptCacheHit) + rec.CacheReported = true } if tut.PromptCacheMiss > 0 { rec.CacheMiss = int64(tut.PromptCacheMiss) diff --git a/internal/gateway/stats.go b/internal/gateway/stats.go index 2cef29e..2f2505f 100644 --- a/internal/gateway/stats.go +++ b/internal/gateway/stats.go @@ -35,6 +35,10 @@ type Req struct { // Both 0 = upstream gave no cache data. CacheHit int64 `json:"cache_hit_tokens,omitempty"` CacheMiss int64 `json:"cache_miss_tokens,omitempty"` + // CacheReported marks that the upstream usage reported cache + // accounting at all (even when the hit count is 0). The WebUI shows + // "0%" instead of "—" for such rows. + CacheReported bool `json:"cache_reported,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 52c1b87..c34898d 100644 --- a/internal/gateway/ui/index.html +++ b/internal/gateway/ui/index.html @@ -1990,10 +1990,11 @@ // 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 ''; + if (!hit && !(r.cache_miss_tokens > 0) && !r.cache_reported) + return ''; const prompt = r.prompt_tokens || 0; const pct = prompt > 0 ? Math.round((hit * 100) / prompt) : 0; - return ` 0 ? "tag-amber" : ""}" title="命中 ${hit} / 未命中 ${r.cache_miss_tokens || 0}">${pct}%`; + return `${pct}%`; } function showModelConfig(srcName, model) { const el = $("#conncfg"); diff --git a/internal/lua/adapters/agentrouter.lua b/internal/lua/adapters/agentrouter.lua index 4decf4b..48cdfe4 100644 --- a/internal/lua/adapters/agentrouter.lua +++ b/internal/lua/adapters/agentrouter.lua @@ -53,7 +53,7 @@ function adapter.transform_response(raw_body) unified.token_usage.completion = resp.usage.completion_tokens or 0 unified.token_usage.total = resp.usage.total_tokens or 0 local hit = 0 - if type(resp.usage.prompt_tokens_details) == "table" and (resp.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(resp.usage.prompt_tokens_details) == "table" and resp.usage.prompt_tokens_details.cached_tokens ~= nil then hit = resp.usage.prompt_tokens_details.cached_tokens unified.token_usage.prompt_tokens_details = { cached_tokens = hit } end @@ -107,7 +107,7 @@ function adapter.transform_stream_chunk(raw_chunk) completion = chunk.usage.completion_tokens or chunk.usage.completion or 0, total = chunk.usage.total_tokens or chunk.usage.total or 0, } - if type(chunk.usage.prompt_tokens_details) == "table" and (chunk.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(chunk.usage.prompt_tokens_details) == "table" and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } elseif (chunk.usage.prompt_cache_hit_tokens or 0) > 0 then uses.prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens diff --git a/internal/lua/adapters/deepseek.lua b/internal/lua/adapters/deepseek.lua index 9f65e71..310c59e 100644 --- a/internal/lua/adapters/deepseek.lua +++ b/internal/lua/adapters/deepseek.lua @@ -51,7 +51,7 @@ function adapter.transform_response(raw_body) unified.token_usage.total = resp.usage.total_tokens or 0 unified.token_usage.prompt_cache_hit_tokens = resp.usage.prompt_cache_hit_tokens or 0 unified.token_usage.prompt_cache_miss_tokens = resp.usage.prompt_cache_miss_tokens or 0 - if resp.usage.prompt_tokens_details and resp.usage.prompt_tokens_details.cached_tokens then + if resp.usage.prompt_tokens_details and resp.usage.prompt_tokens_details.cached_tokens ~= nil then unified.token_usage.prompt_tokens_details = { cached_tokens = resp.usage.prompt_tokens_details.cached_tokens } end end @@ -100,7 +100,7 @@ function adapter.transform_stream_chunk(raw_chunk) prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens or 0, prompt_cache_miss_tokens = chunk.usage.prompt_cache_miss_tokens or 0, } - if chunk.usage.prompt_tokens_details and chunk.usage.prompt_tokens_details.cached_tokens then + if chunk.usage.prompt_tokens_details and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } end end diff --git a/internal/lua/adapters/github.lua b/internal/lua/adapters/github.lua index a7ace2c..0db6d9b 100644 --- a/internal/lua/adapters/github.lua +++ b/internal/lua/adapters/github.lua @@ -34,7 +34,7 @@ function adapter.transform_response(raw_body) unified.token_usage.completion = resp.usage.completion_tokens or 0 unified.token_usage.total = resp.usage.total_tokens or 0 local hit = 0 - if type(resp.usage.prompt_tokens_details) == "table" and (resp.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(resp.usage.prompt_tokens_details) == "table" and resp.usage.prompt_tokens_details.cached_tokens ~= nil then hit = resp.usage.prompt_tokens_details.cached_tokens unified.token_usage.prompt_tokens_details = { cached_tokens = hit } end @@ -86,7 +86,7 @@ function adapter.transform_stream_chunk(raw_chunk) completion = chunk.usage.completion_tokens or chunk.usage.completion or 0, total = chunk.usage.total_tokens or chunk.usage.total or 0, } - if type(chunk.usage.prompt_tokens_details) == "table" and (chunk.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(chunk.usage.prompt_tokens_details) == "table" and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } elseif (chunk.usage.prompt_cache_hit_tokens or 0) > 0 then uses.prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens diff --git a/internal/lua/adapters/groq.lua b/internal/lua/adapters/groq.lua index 88ea0ad..d8858bc 100644 --- a/internal/lua/adapters/groq.lua +++ b/internal/lua/adapters/groq.lua @@ -33,7 +33,7 @@ function adapter.transform_response(raw_body) unified.token_usage.completion = resp.usage.completion_tokens or 0 unified.token_usage.total = resp.usage.total_tokens or 0 local hit = 0 - if type(resp.usage.prompt_tokens_details) == "table" and (resp.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(resp.usage.prompt_tokens_details) == "table" and resp.usage.prompt_tokens_details.cached_tokens ~= nil then hit = resp.usage.prompt_tokens_details.cached_tokens unified.token_usage.prompt_tokens_details = { cached_tokens = hit } end @@ -85,7 +85,7 @@ function adapter.transform_stream_chunk(raw_chunk) completion = chunk.usage.completion_tokens or chunk.usage.completion or 0, total = chunk.usage.total_tokens or chunk.usage.total or 0, } - if type(chunk.usage.prompt_tokens_details) == "table" and (chunk.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(chunk.usage.prompt_tokens_details) == "table" and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } elseif (chunk.usage.prompt_cache_hit_tokens or 0) > 0 then uses.prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens diff --git a/internal/lua/adapters/kimicode.lua b/internal/lua/adapters/kimicode.lua index 2d6d4fa..9d10c54 100644 --- a/internal/lua/adapters/kimicode.lua +++ b/internal/lua/adapters/kimicode.lua @@ -65,7 +65,7 @@ function adapter.transform_response(raw_body) unified.token_usage.completion = resp.usage.completion_tokens or 0 unified.token_usage.total = resp.usage.total_tokens or 0 local hit = 0 - if type(resp.usage.prompt_tokens_details) == "table" and (resp.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(resp.usage.prompt_tokens_details) == "table" and resp.usage.prompt_tokens_details.cached_tokens ~= nil then hit = resp.usage.prompt_tokens_details.cached_tokens unified.token_usage.prompt_tokens_details = { cached_tokens = hit } end @@ -118,7 +118,7 @@ function adapter.transform_stream_chunk(raw_chunk) completion = chunk.usage.completion_tokens or chunk.usage.completion or 0, total = chunk.usage.total_tokens or chunk.usage.total or 0, } - if type(chunk.usage.prompt_tokens_details) == "table" and (chunk.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(chunk.usage.prompt_tokens_details) == "table" and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } elseif (chunk.usage.prompt_cache_hit_tokens or 0) > 0 then uses.prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens diff --git a/internal/lua/adapters/mistral.lua b/internal/lua/adapters/mistral.lua index e33264f..75004b7 100644 --- a/internal/lua/adapters/mistral.lua +++ b/internal/lua/adapters/mistral.lua @@ -33,7 +33,7 @@ function adapter.transform_response(raw_body) unified.token_usage.completion = resp.usage.completion_tokens or 0 unified.token_usage.total = resp.usage.total_tokens or 0 local hit = 0 - if type(resp.usage.prompt_tokens_details) == "table" and (resp.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(resp.usage.prompt_tokens_details) == "table" and resp.usage.prompt_tokens_details.cached_tokens ~= nil then hit = resp.usage.prompt_tokens_details.cached_tokens unified.token_usage.prompt_tokens_details = { cached_tokens = hit } end @@ -85,7 +85,7 @@ function adapter.transform_stream_chunk(raw_chunk) completion = chunk.usage.completion_tokens or chunk.usage.completion or 0, total = chunk.usage.total_tokens or chunk.usage.total or 0, } - if type(chunk.usage.prompt_tokens_details) == "table" and (chunk.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(chunk.usage.prompt_tokens_details) == "table" and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } elseif (chunk.usage.prompt_cache_hit_tokens or 0) > 0 then uses.prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens diff --git a/internal/lua/adapters/openai.lua b/internal/lua/adapters/openai.lua index 83130a4..5538df5 100644 --- a/internal/lua/adapters/openai.lua +++ b/internal/lua/adapters/openai.lua @@ -38,7 +38,7 @@ function adapter.transform_response(raw_body) -- prompt_tokens_details.cached_tokens (falls back to the legacy -- standalone field), so both shapes reach clients. local hit = 0 - if type(resp.usage.prompt_tokens_details) == "table" and (resp.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(resp.usage.prompt_tokens_details) == "table" and resp.usage.prompt_tokens_details.cached_tokens ~= nil then hit = resp.usage.prompt_tokens_details.cached_tokens unified.token_usage.prompt_tokens_details = { cached_tokens = hit } end @@ -93,7 +93,7 @@ function adapter.transform_stream_chunk(raw_chunk) completion = chunk.usage.completion_tokens or chunk.usage.completion or 0, total = chunk.usage.total_tokens or chunk.usage.total or 0, } - if type(chunk.usage.prompt_tokens_details) == "table" and (chunk.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(chunk.usage.prompt_tokens_details) == "table" and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } elseif (chunk.usage.prompt_cache_hit_tokens or 0) > 0 then uses.prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens diff --git a/internal/lua/adapters/opencode.lua b/internal/lua/adapters/opencode.lua index 80acb83..63c07df 100644 --- a/internal/lua/adapters/opencode.lua +++ b/internal/lua/adapters/opencode.lua @@ -101,7 +101,7 @@ function adapter.transform_response(raw_body) unified.token_usage.completion = resp.usage.completion_tokens or 0 unified.token_usage.total = resp.usage.total_tokens or 0 local hit = 0 - if type(resp.usage.prompt_tokens_details) == "table" and (resp.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(resp.usage.prompt_tokens_details) == "table" and resp.usage.prompt_tokens_details.cached_tokens ~= nil then hit = resp.usage.prompt_tokens_details.cached_tokens unified.token_usage.prompt_tokens_details = { cached_tokens = hit } end @@ -158,7 +158,7 @@ function adapter.transform_stream_chunk(raw_chunk) completion = chunk.usage.completion_tokens or chunk.usage.completion or 0, total = chunk.usage.total_tokens or chunk.usage.total or 0, } - if type(chunk.usage.prompt_tokens_details) == "table" and (chunk.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(chunk.usage.prompt_tokens_details) == "table" and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } elseif (chunk.usage.prompt_cache_hit_tokens or 0) > 0 then uses.prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens diff --git a/internal/lua/adapters/sensenova.lua b/internal/lua/adapters/sensenova.lua index db1d2ad..ed37d60 100644 --- a/internal/lua/adapters/sensenova.lua +++ b/internal/lua/adapters/sensenova.lua @@ -38,7 +38,7 @@ function adapter.transform_response(raw_body) unified.token_usage.completion = resp.usage.completion_tokens or 0 unified.token_usage.total = resp.usage.total_tokens or 0 local hit = 0 - if type(resp.usage.prompt_tokens_details) == "table" and (resp.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(resp.usage.prompt_tokens_details) == "table" and resp.usage.prompt_tokens_details.cached_tokens ~= nil then hit = resp.usage.prompt_tokens_details.cached_tokens unified.token_usage.prompt_tokens_details = { cached_tokens = hit } end @@ -79,7 +79,7 @@ function adapter.transform_stream_chunk(raw_chunk) completion = chunk.usage.completion_tokens or chunk.usage.completion or 0, total = chunk.usage.total_tokens or chunk.usage.total or 0, } - if type(chunk.usage.prompt_tokens_details) == "table" and (chunk.usage.prompt_tokens_details.cached_tokens or 0) > 0 then + if type(chunk.usage.prompt_tokens_details) == "table" and chunk.usage.prompt_tokens_details.cached_tokens ~= nil then uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_tokens_details.cached_tokens } elseif (chunk.usage.prompt_cache_hit_tokens or 0) > 0 then uses.prompt_cache_hit_tokens = chunk.usage.prompt_cache_hit_tokens diff --git a/internal/types/types.go b/internal/types/types.go index 34fda89..1f0d7d1 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -99,7 +99,9 @@ type TokenUsage struct { // PromptTokensDetails is the OpenAI v2 prompt_tokens_details object. Only // CachedTokens is emitted (omitempty drops the whole object when zero). type PromptTokensDetails struct { - CachedTokens int `json:"cached_tokens,omitempty"` + // CachedTokens is always emitted (even 0) so clients can distinguish + // "upstream reports cache, this request missed" from "no cache data". + CachedTokens int `json:"cached_tokens"` } // MarshalJSON emits both the legacy short keys (prompt/completion/total, used