From ec3ba8ae7dc6ecae8fb49d1f5ad7900887704beb Mon Sep 17 00:00:00 2001 From: Simon Pan <263902872+chsimonpan@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:47:45 +0800 Subject: [PATCH] fix(agent-core): parse provider cache token usage into accounting OpenAI-compatible chat (stream + non-stream + SSE reassembly) now reads usage.prompt_tokens_details.cached_tokens, and the Responses API parser reads usage.input_tokens_details.cached_tokens, mapping both into usage_key::CACHE_READ_TOKENS. Previously only the Anthropic native provider populated cache counters, so session_token_usage and session_llm_usage_spans recorded 0 cache_read/cache_write for all OpenAI-protocol models even when the upstream cache was hitting. Pre-commit hook ran. Total eslint: 6, total circular: 0 --- .../src/core/providers/openai_compat/client.rs | 8 ++++++++ .../core/providers/openai_compat/streaming/chat.rs | 5 +++++ .../providers/openai_compat/streaming/sse_stream.rs | 10 ++++++++++ .../src/core/providers/openai_compat/types.rs | 11 +++++++++++ .../src/core/providers/responses_common/parser.rs | 9 +++++++++ .../src/core/providers/responses_common/types.rs | 10 ++++++++++ 6 files changed, 53 insertions(+) diff --git a/src-tauri/crates/agent-core/src/core/providers/openai_compat/client.rs b/src-tauri/crates/agent-core/src/core/providers/openai_compat/client.rs index 6ae573c48..508a19952 100644 --- a/src-tauri/crates/agent-core/src/core/providers/openai_compat/client.rs +++ b/src-tauri/crates/agent-core/src/core/providers/openai_compat/client.rs @@ -249,6 +249,14 @@ impl OpenAICompatClient { api_usage.completion_tokens, ); usage.insert(usage_key::TOTAL_TOKENS.to_string(), api_usage.total_tokens); + if let Some(ref details) = api_usage.prompt_tokens_details { + if details.cached_tokens > 0 { + usage.insert( + usage_key::CACHE_READ_TOKENS.to_string(), + details.cached_tokens, + ); + } + } } } diff --git a/src-tauri/crates/agent-core/src/core/providers/openai_compat/streaming/chat.rs b/src-tauri/crates/agent-core/src/core/providers/openai_compat/streaming/chat.rs index ddd69267c..57256ab6b 100644 --- a/src-tauri/crates/agent-core/src/core/providers/openai_compat/streaming/chat.rs +++ b/src-tauri/crates/agent-core/src/core/providers/openai_compat/streaming/chat.rs @@ -178,6 +178,11 @@ pub(super) async fn run_chat( usage.insert("prompt_tokens".to_string(), api_usage.prompt_tokens); usage.insert("completion_tokens".to_string(), api_usage.completion_tokens); usage.insert("total_tokens".to_string(), api_usage.total_tokens); + if let Some(ref details) = api_usage.prompt_tokens_details { + if details.cached_tokens > 0 { + usage.insert("cache_read_tokens".to_string(), details.cached_tokens); + } + } } let tool_calls = choice diff --git a/src-tauri/crates/agent-core/src/core/providers/openai_compat/streaming/sse_stream.rs b/src-tauri/crates/agent-core/src/core/providers/openai_compat/streaming/sse_stream.rs index dcfb38903..274ad225d 100644 --- a/src-tauri/crates/agent-core/src/core/providers/openai_compat/streaming/sse_stream.rs +++ b/src-tauri/crates/agent-core/src/core/providers/openai_compat/streaming/sse_stream.rs @@ -521,6 +521,16 @@ pub(super) async fn run_chat_streaming( final_usage.insert("prompt_tokens".to_string(), usage.prompt_tokens); final_usage.insert("completion_tokens".to_string(), usage.completion_tokens); final_usage.insert("total_tokens".to_string(), usage.total_tokens); + if let Some(ref details) = usage.prompt_tokens_details { + if details.cached_tokens > 0 { + debug!( + "[streaming-usage] OpenAI cached_tokens={}", + details.cached_tokens + ); + final_usage + .insert("cache_read_tokens".to_string(), details.cached_tokens); + } + } } } if stream_done { diff --git a/src-tauri/crates/agent-core/src/core/providers/openai_compat/types.rs b/src-tauri/crates/agent-core/src/core/providers/openai_compat/types.rs index a404b6640..7708e3d7a 100644 --- a/src-tauri/crates/agent-core/src/core/providers/openai_compat/types.rs +++ b/src-tauri/crates/agent-core/src/core/providers/openai_compat/types.rs @@ -172,6 +172,17 @@ pub(super) struct Usage { pub completion_tokens: i64, #[serde(default)] pub total_tokens: i64, + /// OpenAI prompt caching: `prompt_tokens_details.cached_tokens` reports + /// how many prompt tokens were served from the provider prompt cache. + #[serde(default)] + pub prompt_tokens_details: Option, +} + +/// `usage.prompt_tokens_details` from OpenAI-compatible responses. +#[derive(Debug, Deserialize)] +pub(super) struct PromptTokensDetails { + #[serde(default)] + pub cached_tokens: i64, } /// Error response from the API. diff --git a/src-tauri/crates/agent-core/src/core/providers/responses_common/parser.rs b/src-tauri/crates/agent-core/src/core/providers/responses_common/parser.rs index 537fdb44d..c905801dc 100644 --- a/src-tauri/crates/agent-core/src/core/providers/responses_common/parser.rs +++ b/src-tauri/crates/agent-core/src/core/providers/responses_common/parser.rs @@ -132,6 +132,14 @@ pub fn parse_response(resp: ResponsesResponse) -> Result 0 { + usage.insert( + usage_key::CACHE_READ_TOKENS.to_string(), + details.cached_tokens, + ); + } + } } Ok(LLMResponse { @@ -164,6 +172,7 @@ mod tests { })], usage: Some(ResponsesUsage { input_tokens: Some(10), + input_tokens_details: None, output_tokens: Some(5), total_tokens: Some(15), }), diff --git a/src-tauri/crates/agent-core/src/core/providers/responses_common/types.rs b/src-tauri/crates/agent-core/src/core/providers/responses_common/types.rs index fb9ef9251..d885824e4 100644 --- a/src-tauri/crates/agent-core/src/core/providers/responses_common/types.rs +++ b/src-tauri/crates/agent-core/src/core/providers/responses_common/types.rs @@ -114,6 +114,16 @@ pub struct ResponsesUsage { pub input_tokens: Option, pub output_tokens: Option, pub total_tokens: Option, + /// `usage.input_tokens_details.cached_tokens` — prompt-cache hits. + #[serde(default)] + pub input_tokens_details: Option, +} + +/// `usage.input_tokens_details` from the Responses API. +#[derive(Debug, Deserialize)] +pub struct ResponsesInputTokensDetails { + #[serde(default)] + pub cached_tokens: i64, } /// Error from the Responses API.