mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-29 09:37:37 +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:
@@ -125,19 +125,14 @@ pub(crate) fn map_anthropic_stop_reason_to_status(
|
||||
|
||||
/// Builds Responses usage from Anthropic usage.
|
||||
///
|
||||
/// Anthropic's `input_tokens` is the "cache-excluded" fresh input; OpenAI/Responses'
|
||||
/// `input_tokens` includes cache hits. To keep downstream metering correct, this
|
||||
/// adds them (symmetric to the subtraction done for the Claude side in
|
||||
/// `transform_responses`):
|
||||
/// input_tokens = input + cache_read
|
||||
/// Anthropic's `input_tokens` is the cache-excluded fresh input. OpenAI Responses
|
||||
/// reports total input and exposes cache reads/writes as subsets:
|
||||
/// input_tokens = fresh + cache_read + cache_creation
|
||||
/// input_tokens_details.cached_tokens = cache_read
|
||||
/// input_tokens_details.cache_write_tokens = cache_creation
|
||||
///
|
||||
/// Note: **do not** fold `cache_creation` into `input_tokens`. The Codex billing
|
||||
/// calculator (usage/calculator.rs) only subtracts `cache_read` for codex
|
||||
/// (`billable = input - cache_read`), and separately lists cache-creation cost via
|
||||
/// `cache_creation_input_tokens`; if creation were also added into
|
||||
/// `input_tokens`, it would be double-charged at both the input price and the
|
||||
/// cache-creation price.
|
||||
/// The internal billing parser subtracts both subsets before charging the normal
|
||||
/// input rate, then prices cache reads and writes separately.
|
||||
pub(crate) fn build_responses_usage_from_anthropic(usage: Option<&Value>) -> Value {
|
||||
let u = match usage {
|
||||
Some(v) if v.is_object() => v,
|
||||
@@ -166,10 +161,10 @@ pub(crate) fn build_responses_usage_from_anthropic(usage: Option<&Value>) -> Val
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0);
|
||||
|
||||
let input_tokens = fresh_input.saturating_add(cache_read);
|
||||
let total_tokens = input_tokens
|
||||
.saturating_add(cache_creation)
|
||||
.saturating_add(output);
|
||||
let input_tokens = fresh_input
|
||||
.saturating_add(cache_read)
|
||||
.saturating_add(cache_creation);
|
||||
let total_tokens = input_tokens.saturating_add(output);
|
||||
|
||||
let mut result = json!({
|
||||
"input_tokens": input_tokens,
|
||||
@@ -177,10 +172,14 @@ pub(crate) fn build_responses_usage_from_anthropic(usage: Option<&Value>) -> Val
|
||||
"total_tokens": total_tokens,
|
||||
"output_tokens_details": { "reasoning_tokens": reasoning }
|
||||
});
|
||||
if cache_read > 0 {
|
||||
result["input_tokens_details"] = json!({ "cached_tokens": cache_read });
|
||||
if cache_read > 0 || cache_creation > 0 {
|
||||
result["input_tokens_details"] = json!({
|
||||
"cached_tokens": cache_read,
|
||||
"cache_write_tokens": cache_creation
|
||||
});
|
||||
}
|
||||
// Explicitly pass through cache_creation so the downstream usage parser (from_codex_response) attributes billing correctly.
|
||||
// Keep the legacy top-level alias for one compatibility window. New code reads
|
||||
// the official nested cache_write_tokens field first.
|
||||
if cache_creation > 0 {
|
||||
result["cache_creation_input_tokens"] = json!(cache_creation);
|
||||
}
|
||||
@@ -2078,18 +2077,20 @@ mod tests {
|
||||
}
|
||||
});
|
||||
let result = anthropic_response_to_responses(input).unwrap();
|
||||
// input_tokens = fresh + cache_read = 20 + 60 = 80 (excluding cache_creation).
|
||||
// The Codex billing calculator only subtracts cache_read from input (→ billable=fresh=20),
|
||||
// and separately lists cache-creation cost via cache_creation_input_tokens; folding creation into input would double-charge.
|
||||
assert_eq!(result["usage"]["input_tokens"], 80);
|
||||
// Responses input_tokens is the inclusive total: fresh + read + write.
|
||||
assert_eq!(result["usage"]["input_tokens"], 100);
|
||||
assert_eq!(result["usage"]["output_tokens"], 5);
|
||||
assert_eq!(
|
||||
result["usage"]["output_tokens_details"]["reasoning_tokens"],
|
||||
3
|
||||
);
|
||||
// total still includes everything: 80 + cache_creation 20 + output 5 = 105
|
||||
// total includes input total + output exactly once.
|
||||
assert_eq!(result["usage"]["total_tokens"], 105);
|
||||
assert_eq!(result["usage"]["input_tokens_details"]["cached_tokens"], 60);
|
||||
assert_eq!(
|
||||
result["usage"]["input_tokens_details"]["cache_write_tokens"],
|
||||
20
|
||||
);
|
||||
// cache_creation is passed through explicitly for downstream billing attribution (counted only once)
|
||||
assert_eq!(result["usage"]["cache_creation_input_tokens"], 20);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user