diff --git a/crates/cm-llm/src/anthropic.rs b/crates/cm-llm/src/anthropic.rs index 0a5ccfd..7fc26d2 100644 --- a/crates/cm-llm/src/anthropic.rs +++ b/crates/cm-llm/src/anthropic.rs @@ -242,6 +242,14 @@ impl LlmProvider for AnthropicProvider { data["message"]["usage"]["input_tokens"].as_u64().unwrap_or(0) as u32; } "message_delta" => { + // Anthropic reports input_tokens in `message_start`; + // z.ai's Anthropic-compatible endpoint sends 0 there and + // the real count here, beside output_tokens (measured + // 2026-09-14: start `input_tokens: 0`, delta + // `input_tokens: 14`). Prefer the delta's figure when + // it carries one — the judge's input, which is the + // number that empties a plan, read as zero until then. + input_tokens = input_tokens_after_delta(input_tokens, &data["usage"]); if let Some(out) = data["usage"]["output_tokens"].as_u64() { yield LlmEvent::Usage { input_tokens, @@ -266,10 +274,38 @@ impl LlmProvider for AnthropicProvider { } } +/// The input-token figure to report once a `message_delta` has arrived. +/// +/// `start` is what `message_start` said. Anthropic puts the real count there +/// and nothing in the delta; z.ai's Anthropic-compatible endpoint puts 0 there +/// and the real count in the delta's `usage` (measured 2026-09-14). A nonzero +/// delta figure wins; anything else keeps what `message_start` said, so the +/// Anthropic path is unchanged. +fn input_tokens_after_delta(start: u32, delta_usage: &serde_json::Value) -> u32 { + match delta_usage["input_tokens"].as_u64() { + Some(n) if n > 0 => n as u32, + _ => start, + } +} + #[cfg(test)] mod tests { use super::*; + /// z.ai reports input in the delta; Anthropic reports it at the start. + /// Before this the judge's input tokens — the number that empties a + /// plan — read as zero on every GLM verdict. + #[test] + fn input_tokens_come_from_whichever_frame_carries_them() { + use serde_json::json; + // z.ai shape: start says 0, delta says 14. + assert_eq!(input_tokens_after_delta(0, &json!({"input_tokens": 14, "output_tokens": 16})), 14); + // Anthropic shape: start said 812, delta has no input figure. + assert_eq!(input_tokens_after_delta(812, &json!({"output_tokens": 40})), 812); + // A delta that explicitly says 0 must not erase the start's figure. + assert_eq!(input_tokens_after_delta(812, &json!({"input_tokens": 0, "output_tokens": 40})), 812); + } + #[test] fn setup_tokens_are_distinguished_from_api_keys() { assert!(is_setup_token("sk-ant-oat01-abc"));