From ec89daad62a234a09f138eb33ba86cde77a3b469 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 25 Aug 2026 09:52:55 +0800 Subject: [PATCH] fix(adapters): pass through cache tokens in the remaining 8 adapters Live testing proved both sensenova and zen DO return cache fields: - zen laguna-s-2.1-free: usage.prompt_tokens_details.cached_tokens = 32 (real hit), plus cache_write_tokens/audio_tokens - sensenova glm-5.2: prompt_tokens_details.cached_tokens present (0 on short prompts) The previous round only patched deepseek/openai/anthropic/gemini.lua; sensenova/opencode (localzen!) and the other adapters still dropped them. - sensenova/opencode/groq/mistral/github/kimicode: stream + response cache passthrough (same pattern as openai.lua) - agentrouter: response passthrough + NEW stream usage forwarding (it previously dropped the terminal usage-only chunk entirely) - ollama skipped intentionally: its native API has no cache fields Verified end-to-end through the gateway: localzen/laguna-s-2.1-free now returns prompt_tokens_details.cached_tokens=32 to clients, and the request record carries cache_hit_tokens (both chat and stream paths). --- internal/lua/adapters/agentrouter.lua | 40 ++++++++++++++++++++++++++- internal/lua/adapters/github.lua | 19 +++++++++++++ internal/lua/adapters/groq.lua | 19 +++++++++++++ internal/lua/adapters/kimicode.lua | 19 +++++++++++++ internal/lua/adapters/mistral.lua | 19 +++++++++++++ internal/lua/adapters/opencode.lua | 19 +++++++++++++ internal/lua/adapters/sensenova.lua | 19 +++++++++++++ 7 files changed, 153 insertions(+), 1 deletion(-) diff --git a/internal/lua/adapters/agentrouter.lua b/internal/lua/adapters/agentrouter.lua index 549fbbe..4decf4b 100644 --- a/internal/lua/adapters/agentrouter.lua +++ b/internal/lua/adapters/agentrouter.lua @@ -52,6 +52,18 @@ function adapter.transform_response(raw_body) unified.token_usage.prompt = resp.usage.prompt_tokens or 0 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 + hit = resp.usage.prompt_tokens_details.cached_tokens + unified.token_usage.prompt_tokens_details = { cached_tokens = hit } + end + if (resp.usage.prompt_cache_hit_tokens or 0) > 0 then + unified.token_usage.prompt_cache_hit_tokens = resp.usage.prompt_cache_hit_tokens + unified.token_usage.prompt_cache_miss_tokens = resp.usage.prompt_cache_miss_tokens or 0 + if hit == 0 then + unified.token_usage.prompt_tokens_details = { cached_tokens = resp.usage.prompt_cache_hit_tokens } + end + end end if type(resp.choices) == "table" and #resp.choices > 0 then @@ -86,7 +98,30 @@ function adapter.transform_stream_chunk(raw_chunk) local ok, chunk = pcall(json.decode, raw_chunk) if not ok then return "" end - if not chunk.choices or #chunk.choices == 0 then return "" end + -- usage-only terminal chunk (empty choices + usage): forward it so the + -- gateway emits exact token counts (and cache fields when present). + local uses = nil + if type(chunk.usage) == "table" then + uses = { + prompt = chunk.usage.prompt_tokens or chunk.usage.prompt or 0, + 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 + 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 + uses.prompt_cache_miss_tokens = chunk.usage.prompt_cache_miss_tokens or 0 + uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_cache_hit_tokens } + end + end + + if not chunk.choices or #chunk.choices == 0 then + if uses ~= nil then + return json.encode({ usage = uses, done = false }) + end + return "" + end local delta = chunk.choices[1].delta or {} local fr = chunk.choices[1].finish_reason @@ -105,6 +140,9 @@ function adapter.transform_stream_chunk(raw_chunk) if delta.tool_calls then unified.tool_calls = delta.tool_calls end + if uses ~= nil then + unified.usage = uses + end return json.encode(unified) end diff --git a/internal/lua/adapters/github.lua b/internal/lua/adapters/github.lua index e425f80..a7ace2c 100644 --- a/internal/lua/adapters/github.lua +++ b/internal/lua/adapters/github.lua @@ -33,6 +33,18 @@ function adapter.transform_response(raw_body) unified.token_usage.prompt = resp.usage.prompt_tokens or 0 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 + hit = resp.usage.prompt_tokens_details.cached_tokens + unified.token_usage.prompt_tokens_details = { cached_tokens = hit } + end + if (resp.usage.prompt_cache_hit_tokens or 0) > 0 then + unified.token_usage.prompt_cache_hit_tokens = resp.usage.prompt_cache_hit_tokens + unified.token_usage.prompt_cache_miss_tokens = resp.usage.prompt_cache_miss_tokens or 0 + if hit == 0 then + unified.token_usage.prompt_tokens_details = { cached_tokens = resp.usage.prompt_cache_hit_tokens } + end + end end if type(resp.choices) == "table" and #resp.choices > 0 then @@ -74,6 +86,13 @@ 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 + 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 + uses.prompt_cache_miss_tokens = chunk.usage.prompt_cache_miss_tokens or 0 + uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_cache_hit_tokens } + end end if not chunk.choices or #chunk.choices == 0 then diff --git a/internal/lua/adapters/groq.lua b/internal/lua/adapters/groq.lua index 88d7956..88ea0ad 100644 --- a/internal/lua/adapters/groq.lua +++ b/internal/lua/adapters/groq.lua @@ -32,6 +32,18 @@ function adapter.transform_response(raw_body) unified.token_usage.prompt = resp.usage.prompt_tokens or 0 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 + hit = resp.usage.prompt_tokens_details.cached_tokens + unified.token_usage.prompt_tokens_details = { cached_tokens = hit } + end + if (resp.usage.prompt_cache_hit_tokens or 0) > 0 then + unified.token_usage.prompt_cache_hit_tokens = resp.usage.prompt_cache_hit_tokens + unified.token_usage.prompt_cache_miss_tokens = resp.usage.prompt_cache_miss_tokens or 0 + if hit == 0 then + unified.token_usage.prompt_tokens_details = { cached_tokens = resp.usage.prompt_cache_hit_tokens } + end + end end if type(resp.choices) == "table" and #resp.choices > 0 then @@ -73,6 +85,13 @@ 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 + 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 + uses.prompt_cache_miss_tokens = chunk.usage.prompt_cache_miss_tokens or 0 + uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_cache_hit_tokens } + end end if not chunk.choices or #chunk.choices == 0 then diff --git a/internal/lua/adapters/kimicode.lua b/internal/lua/adapters/kimicode.lua index 07a06ec..2d6d4fa 100644 --- a/internal/lua/adapters/kimicode.lua +++ b/internal/lua/adapters/kimicode.lua @@ -64,6 +64,18 @@ function adapter.transform_response(raw_body) unified.token_usage.prompt = resp.usage.prompt_tokens or 0 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 + hit = resp.usage.prompt_tokens_details.cached_tokens + unified.token_usage.prompt_tokens_details = { cached_tokens = hit } + end + if (resp.usage.prompt_cache_hit_tokens or 0) > 0 then + unified.token_usage.prompt_cache_hit_tokens = resp.usage.prompt_cache_hit_tokens + unified.token_usage.prompt_cache_miss_tokens = resp.usage.prompt_cache_miss_tokens or 0 + if hit == 0 then + unified.token_usage.prompt_tokens_details = { cached_tokens = resp.usage.prompt_cache_hit_tokens } + end + end end if type(resp.choices) == "table" and #resp.choices > 0 then local ch = resp.choices[1] @@ -106,6 +118,13 @@ 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 + 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 + uses.prompt_cache_miss_tokens = chunk.usage.prompt_cache_miss_tokens or 0 + uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_cache_hit_tokens } + end end if not chunk.choices or #chunk.choices == 0 then diff --git a/internal/lua/adapters/mistral.lua b/internal/lua/adapters/mistral.lua index ba5bc89..e33264f 100644 --- a/internal/lua/adapters/mistral.lua +++ b/internal/lua/adapters/mistral.lua @@ -32,6 +32,18 @@ function adapter.transform_response(raw_body) unified.token_usage.prompt = resp.usage.prompt_tokens or 0 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 + hit = resp.usage.prompt_tokens_details.cached_tokens + unified.token_usage.prompt_tokens_details = { cached_tokens = hit } + end + if (resp.usage.prompt_cache_hit_tokens or 0) > 0 then + unified.token_usage.prompt_cache_hit_tokens = resp.usage.prompt_cache_hit_tokens + unified.token_usage.prompt_cache_miss_tokens = resp.usage.prompt_cache_miss_tokens or 0 + if hit == 0 then + unified.token_usage.prompt_tokens_details = { cached_tokens = resp.usage.prompt_cache_hit_tokens } + end + end end if type(resp.choices) == "table" and #resp.choices > 0 then @@ -73,6 +85,13 @@ 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 + 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 + uses.prompt_cache_miss_tokens = chunk.usage.prompt_cache_miss_tokens or 0 + uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_cache_hit_tokens } + end end if not chunk.choices or #chunk.choices == 0 then diff --git a/internal/lua/adapters/opencode.lua b/internal/lua/adapters/opencode.lua index e442268..80acb83 100644 --- a/internal/lua/adapters/opencode.lua +++ b/internal/lua/adapters/opencode.lua @@ -100,6 +100,18 @@ function adapter.transform_response(raw_body) unified.token_usage.prompt = resp.usage.prompt_tokens or 0 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 + hit = resp.usage.prompt_tokens_details.cached_tokens + unified.token_usage.prompt_tokens_details = { cached_tokens = hit } + end + if (resp.usage.prompt_cache_hit_tokens or 0) > 0 then + unified.token_usage.prompt_cache_hit_tokens = resp.usage.prompt_cache_hit_tokens + unified.token_usage.prompt_cache_miss_tokens = resp.usage.prompt_cache_miss_tokens or 0 + if hit == 0 then + unified.token_usage.prompt_tokens_details = { cached_tokens = resp.usage.prompt_cache_hit_tokens } + end + end end if type(resp.choices) == "table" and #resp.choices > 0 then @@ -146,6 +158,13 @@ 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 + 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 + uses.prompt_cache_miss_tokens = chunk.usage.prompt_cache_miss_tokens or 0 + uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_cache_hit_tokens } + end end if not chunk.choices or #chunk.choices == 0 then diff --git a/internal/lua/adapters/sensenova.lua b/internal/lua/adapters/sensenova.lua index 735a68a..db1d2ad 100644 --- a/internal/lua/adapters/sensenova.lua +++ b/internal/lua/adapters/sensenova.lua @@ -37,6 +37,18 @@ function adapter.transform_response(raw_body) unified.token_usage.prompt = resp.usage.prompt_tokens or 0 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 + hit = resp.usage.prompt_tokens_details.cached_tokens + unified.token_usage.prompt_tokens_details = { cached_tokens = hit } + end + if (resp.usage.prompt_cache_hit_tokens or 0) > 0 then + unified.token_usage.prompt_cache_hit_tokens = resp.usage.prompt_cache_hit_tokens + unified.token_usage.prompt_cache_miss_tokens = resp.usage.prompt_cache_miss_tokens or 0 + if hit == 0 then + unified.token_usage.prompt_tokens_details = { cached_tokens = resp.usage.prompt_cache_hit_tokens } + end + end end if type(resp.choices) == "table" and #resp.choices > 0 then @@ -67,6 +79,13 @@ 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 + 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 + uses.prompt_cache_miss_tokens = chunk.usage.prompt_cache_miss_tokens or 0 + uses.prompt_tokens_details = { cached_tokens = chunk.usage.prompt_cache_hit_tokens } + end end if not chunk.choices or #chunk.choices == 0 then