mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 23:56:02 +08:00
fix(usage): account for cache-write tokens across schema versions
Parse cache_write_tokens from OpenAI usage details and preserve cache creation data across Chat, Responses, and Anthropic conversion paths. Add explicit input-token semantics to request logs and rollups so legacy rows subtract cache reads only while new total-inclusive rows subtract both cache reads and writes. Migrate v12 databases, normalize rollups to fresh input, and cover historical backfill behavior with regression tests.
This commit is contained in:
@@ -9,6 +9,24 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
fn openai_cache_read_tokens(usage: &Value) -> u32 {
|
||||
usage
|
||||
.get("cache_read_input_tokens")
|
||||
.or_else(|| usage.pointer("/input_tokens_details/cached_tokens"))
|
||||
.or_else(|| usage.pointer("/prompt_tokens_details/cached_tokens"))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0) as u32
|
||||
}
|
||||
|
||||
fn openai_cache_write_tokens(usage: &Value) -> u32 {
|
||||
usage
|
||||
.get("cache_creation_input_tokens")
|
||||
.or_else(|| usage.pointer("/input_tokens_details/cache_write_tokens"))
|
||||
.or_else(|| usage.pointer("/prompt_tokens_details/cache_write_tokens"))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(0) as u32
|
||||
}
|
||||
|
||||
/// Session 日志 request_id 前缀,与 `session_usage.rs` 中的格式保持一致
|
||||
pub const SESSION_REQUEST_ID_PREFIX: &str = "session:";
|
||||
|
||||
@@ -250,25 +268,14 @@ impl TokenUsage {
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let cached_tokens = usage
|
||||
.get("cache_read_input_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.or_else(|| {
|
||||
usage
|
||||
.get("input_tokens_details")
|
||||
.and_then(|d| d.get("cached_tokens"))
|
||||
.and_then(|v| v.as_u64())
|
||||
})
|
||||
.unwrap_or(0) as u32;
|
||||
let cached_tokens = openai_cache_read_tokens(usage);
|
||||
let cache_write_tokens = openai_cache_write_tokens(usage);
|
||||
|
||||
Some(Self {
|
||||
input_tokens: input_tokens? as u32,
|
||||
output_tokens: output_tokens? as u32,
|
||||
cache_read_tokens: cached_tokens,
|
||||
cache_creation_tokens: usage
|
||||
.get("cache_creation_input_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u32,
|
||||
cache_creation_tokens: cache_write_tokens,
|
||||
model,
|
||||
message_id: None,
|
||||
})
|
||||
@@ -285,19 +292,13 @@ impl TokenUsage {
|
||||
let output_tokens = usage.get("output_tokens")?.as_u64()? as u32;
|
||||
|
||||
// 获取 cached_tokens (可能在 cache_read_input_tokens 或 input_tokens_details 中)
|
||||
let cached_tokens = usage
|
||||
.get("cache_read_input_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.or_else(|| {
|
||||
usage
|
||||
.get("input_tokens_details")
|
||||
.and_then(|d| d.get("cached_tokens"))
|
||||
.and_then(|v| v.as_u64())
|
||||
})
|
||||
.unwrap_or(0) as u32;
|
||||
let cached_tokens = openai_cache_read_tokens(usage);
|
||||
let cache_write_tokens = openai_cache_write_tokens(usage);
|
||||
|
||||
// 调整 input_tokens: 减去 cached_tokens
|
||||
let adjusted_input = input_tokens.saturating_sub(cached_tokens);
|
||||
// 调整 input_tokens: OpenAI total input 同时包含 cache read/write 两桶。
|
||||
let adjusted_input = input_tokens
|
||||
.saturating_sub(cached_tokens)
|
||||
.saturating_sub(cache_write_tokens);
|
||||
|
||||
// 提取响应中的模型名称
|
||||
let model = body
|
||||
@@ -309,10 +310,7 @@ impl TokenUsage {
|
||||
input_tokens: adjusted_input,
|
||||
output_tokens,
|
||||
cache_read_tokens: cached_tokens,
|
||||
cache_creation_tokens: usage
|
||||
.get("cache_creation_input_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u32,
|
||||
cache_creation_tokens: cache_write_tokens,
|
||||
model,
|
||||
message_id: None,
|
||||
})
|
||||
@@ -391,11 +389,8 @@ impl TokenUsage {
|
||||
let completion_tokens = usage.get("completion_tokens").and_then(|v| v.as_u64())?;
|
||||
|
||||
// 获取 cached_tokens (可能在 prompt_tokens_details 中)
|
||||
let cached_tokens = usage
|
||||
.get("prompt_tokens_details")
|
||||
.and_then(|d| d.get("cached_tokens"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
let cached_tokens = openai_cache_read_tokens(usage);
|
||||
let cache_write_tokens = openai_cache_write_tokens(usage);
|
||||
|
||||
// 提取响应中的模型名称
|
||||
let model = body
|
||||
@@ -407,7 +402,7 @@ impl TokenUsage {
|
||||
input_tokens: prompt_tokens as u32,
|
||||
output_tokens: completion_tokens as u32,
|
||||
cache_read_tokens: cached_tokens,
|
||||
cache_creation_tokens: 0,
|
||||
cache_creation_tokens: cache_write_tokens,
|
||||
model,
|
||||
message_id: None,
|
||||
})
|
||||
@@ -797,6 +792,30 @@ mod tests {
|
||||
assert_eq!(usage.cache_read_tokens, 300);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codex_response_parsing_cache_write_tokens_in_details() {
|
||||
let response = json!({
|
||||
"usage": {
|
||||
"input_tokens": 1000,
|
||||
"output_tokens": 500,
|
||||
"input_tokens_details": {
|
||||
"cached_tokens": 300,
|
||||
"cache_write_tokens": 200
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let usage = TokenUsage::from_codex_response(&response).unwrap();
|
||||
assert_eq!(usage.input_tokens, 1000);
|
||||
assert_eq!(usage.cache_read_tokens, 300);
|
||||
assert_eq!(usage.cache_creation_tokens, 200);
|
||||
|
||||
let adjusted = TokenUsage::from_codex_response_adjusted(&response).unwrap();
|
||||
assert_eq!(adjusted.input_tokens, 500);
|
||||
assert_eq!(adjusted.cache_read_tokens, 300);
|
||||
assert_eq!(adjusted.cache_creation_tokens, 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_codex_response_adjusted() {
|
||||
let response = json!({
|
||||
|
||||
Reference in New Issue
Block a user