From 736b6a9a82cad72c1a7a2716d34c25fcc4ce4e42 Mon Sep 17 00:00:00 2001 From: Omar Sobh Date: Sun, 13 Sep 2026 22:55:03 -0500 Subject: [PATCH] fix(llm): read input tokens from the frame that carries them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first judge-spend rows recorded by 248948c came back with input_tokens = 0 on both passes of mission 01a09dfc — 7 and 9 requests, 5940 and 2109 output tokens, and nothing on the side that actually empties the plan. Probed z.ai's Anthropic-compatible stream directly: `message_start` carries `"input_tokens": 0`, and the real figure arrives in `message_delta.usage` beside output_tokens. Anthropic proper does it the other way round, which is the shape the parser was written for. A nonzero figure in the delta now wins; otherwise the start's figure stands, so the Anthropic path is byte-for-byte unchanged. The decision is a pure function with the three shapes as its test — including a delta that says 0, which must not erase what the start said. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz --- crates/cm-llm/src/anthropic.rs | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) 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"));