fix(llm): read input tokens from the frame that carries them
deploy / test (push) Successful in 5m14s
deploy / build (push) Successful in 5m59s

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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
This commit is contained in:
Omar Sobh
2026-09-13 22:55:03 -05:00
co-authored by Claude Opus 5
parent 248948cc84
commit 736b6a9a82
+36
View File
@@ -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"));