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:
Jason
2026-07-11 12:27:39 +08:00
parent 06039540ff
commit f991726ff0
13 changed files with 443 additions and 102 deletions
+52 -6
View File
@@ -18,13 +18,16 @@
/// would happen with the opposite default.
const CACHE_INCLUSIVE_APP_TYPES: &[&str] = &["codex", "gemini"];
pub(crate) const INPUT_TOKEN_SEMANTICS_LEGACY: i64 = 0;
pub(crate) const INPUT_TOKEN_SEMANTICS_TOTAL: i64 = 1;
pub(crate) const INPUT_TOKEN_SEMANTICS_FRESH: i64 = 2;
/// Build an SQL expression that returns the cache-normalized `input_tokens`
/// for a single row in `proxy_request_logs` or `usage_daily_rollups`.
///
/// For rows whose `app_type` is in [`CACHE_INCLUSIVE_APP_TYPES`] and
/// `input_tokens >= cache_read_tokens`, returns
/// `input_tokens - cache_read_tokens`. For all other rows the original
/// `input_tokens` is returned unchanged.
/// Legacy rows subtract cache reads only. New total-inclusive rows subtract
/// both cache reads and writes. Rollups normalized to fresh input are returned
/// unchanged.
///
/// Pass an empty string to reference the columns directly (no alias),
/// or a table alias such as `"l"` to emit `l.input_tokens` style references.
@@ -40,7 +43,15 @@ pub fn fresh_input_sql(alias: &str) -> String {
.collect::<Vec<_>>()
.join(", ");
format!(
"CASE WHEN {prefix}app_type IN ({app_type_list}) AND {prefix}input_tokens >= {prefix}cache_read_tokens \
"CASE \
WHEN {prefix}input_token_semantics = {INPUT_TOKEN_SEMANTICS_FRESH} THEN {prefix}input_tokens \
WHEN {prefix}app_type IN ({app_type_list}) \
AND {prefix}input_token_semantics = {INPUT_TOKEN_SEMANTICS_TOTAL} \
AND {prefix}input_tokens >= ({prefix}cache_read_tokens + {prefix}cache_creation_tokens) \
THEN ({prefix}input_tokens - {prefix}cache_read_tokens - {prefix}cache_creation_tokens) \
WHEN {prefix}app_type IN ({app_type_list}) \
AND {prefix}input_token_semantics = {INPUT_TOKEN_SEMANTICS_LEGACY} \
AND {prefix}input_tokens >= {prefix}cache_read_tokens \
THEN ({prefix}input_tokens - {prefix}cache_read_tokens) \
ELSE {prefix}input_tokens END"
)
@@ -60,7 +71,8 @@ mod tests {
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
cache_creation_tokens INTEGER NOT NULL DEFAULT 0
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
input_token_semantics INTEGER NOT NULL DEFAULT 0
);",
)
.unwrap();
@@ -131,4 +143,38 @@ mod tests {
let value: i64 = conn.query_row(&sql, [], |r| r.get(0)).unwrap();
assert_eq!(value, 100);
}
#[test]
fn fresh_input_subtracts_cache_write_for_total_semantics() {
let conn = setup_conn();
conn.execute(
"INSERT INTO proxy_request_logs (
request_id, app_type, input_tokens, cache_read_tokens,
cache_creation_tokens, input_token_semantics
) VALUES ('codex-total', 'codex', 1000, 300, 200, ?1)",
[INPUT_TOKEN_SEMANTICS_TOTAL],
)
.unwrap();
let expr = fresh_input_sql("l");
let sql = format!("SELECT {expr} FROM proxy_request_logs l");
let value: i64 = conn.query_row(&sql, [], |row| row.get(0)).unwrap();
assert_eq!(value, 500);
}
#[test]
fn fresh_input_keeps_normalized_rollup_value() {
let conn = setup_conn();
conn.execute(
"INSERT INTO proxy_request_logs (
request_id, app_type, input_tokens, cache_read_tokens,
cache_creation_tokens, input_token_semantics
) VALUES ('codex-fresh', 'codex', 500, 300, 200, ?1)",
[INPUT_TOKEN_SEMANTICS_FRESH],
)
.unwrap();
let expr = fresh_input_sql("l");
let sql = format!("SELECT {expr} FROM proxy_request_logs l");
let value: i64 = conn.query_row(&sql, [], |row| row.get(0)).unwrap();
assert_eq!(value, 500);
}
}