Files
clawmates/crates/cm-llm/src/anthropic.rs
T
Omar SobhandClaude Opus 5 736b6a9a82
deploy / test (push) Successful in 5m14s
deploy / build (push) Successful in 5m59s
fix(llm): read input tokens from the frame that carries them
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
2026-09-13 22:55:03 -05:00

363 lines
15 KiB
Rust

//! Anthropic Messages API provider (cloud target).
use async_stream::try_stream;
use eventsource_stream::Eventsource;
use futures::StreamExt;
use serde_json::{json, Value};
use crate::provider::{
ChatRequest, ChatRole, ContentPart, EventStream, LlmError, LlmEvent, LlmProvider, StopReason,
};
/// The Claude Code identity line Anthropic requires as the first system block
/// when authenticating with a subscription OAuth token.
const OAUTH_SYSTEM_PREAMBLE: &str = "You are Claude Code, Anthropic's official CLI for Claude.";
/// Setup tokens minted by `claude setup-token` carry this prefix. API keys are
/// `sk-ant-api…`, so the shape is enough to pick the auth scheme.
fn is_setup_token(credential: &str) -> bool {
credential.trim().starts_with("sk-ant-oat")
}
pub struct AnthropicProvider {
client: reqwest::Client,
base_url: String,
api_key: String,
}
impl AnthropicProvider {
pub fn new(api_key: String) -> AnthropicProvider {
AnthropicProvider::with_base_url(api_key, "https://api.anthropic.com".into())
}
/// Base URL override exists for self-hosted proxies and tests.
pub fn with_base_url(api_key: String, base_url: String) -> AnthropicProvider {
AnthropicProvider {
client: reqwest::Client::new(),
base_url,
api_key,
}
}
/// Whether this provider is authenticating with a subscription token
/// rather than an API key. Callers that report cost attribution care.
pub fn is_subscription(&self) -> bool {
is_setup_token(&self.api_key)
}
/// The Claude Code identity as its OWN system block, with the caller's
/// prompt as a second block.
///
/// This used to concatenate the two into one string, and **every call that
/// set a system prompt failed**. On the OAuth path Anthropic requires the
/// identity to be the first system BLOCK; a single string that merely
/// begins with it is rejected — and the rejection arrives as
/// `429 {"type":"rate_limit_error","message":"Error"}`, which reads as
/// throttling and is not. Measured on one token, seconds apart:
///
/// ```text
/// "PREAMBLE" (string) -> 200
/// "PREAMBLE\n\nJudge the …" (string) -> 429
/// ["PREAMBLE"] (blocks) -> 200
/// ["PREAMBLE", "Judge the …"] (blocks) -> 200
/// ```
///
/// while the account itself reported `5h utilization 0.07, status allowed`.
/// Every judge verdict, preflight probe and refiner call on a subscription
/// token was failing 100% of the time and being logged as "no capacity",
/// including a `done_when` verdict that failed closed and burned one of a
/// phase's three passes.
///
/// Idempotent: a caller whose prompt already opens with the identity does
/// not get it twice.
fn oauth_system_blocks(system: &str) -> Value {
let mut blocks = vec![json!({"type": "text", "text": OAUTH_SYSTEM_PREAMBLE})];
let rest = system
.trim_start()
.strip_prefix(OAUTH_SYSTEM_PREAMBLE)
.unwrap_or(system)
.trim();
if !rest.is_empty() {
blocks.push(json!({"type": "text", "text": rest}));
}
Value::Array(blocks)
}
fn wire_messages(request: &ChatRequest) -> Vec<Value> {
request
.messages
.iter()
.map(|message| {
let role = match message.role {
ChatRole::User => "user",
ChatRole::Assistant => "assistant",
};
let content: Vec<Value> = message
.parts
.iter()
.map(|part| match part {
ContentPart::Text { text } => json!({"type": "text", "text": text}),
ContentPart::ToolUse { id, name, input } => {
json!({"type": "tool_use", "id": id,
"name": crate::wire_tool_name(name), "input": input})
}
ContentPart::ToolResult {
tool_use_id,
content,
} => json!({
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": content.to_string(),
}),
})
.collect();
json!({"role": role, "content": content})
})
.collect()
}
}
fn stop_reason(wire: &str) -> StopReason {
match wire {
"tool_use" => StopReason::ToolUse,
"max_tokens" => StopReason::MaxTokens,
_ => StopReason::EndTurn,
}
}
#[async_trait::async_trait]
impl LlmProvider for AnthropicProvider {
async fn stream(&self, request: ChatRequest) -> Result<EventStream, LlmError> {
let mut tools: Vec<Value> = request
.tools
.iter()
.map(|t| {
json!({
"name": crate::wire_tool_name(&t.name),
"description": t.description,
"input_schema": t.input_schema,
})
})
.collect();
if request.web_search {
// Anthropic server-side web search — the model searches the web itself.
tools.push(json!({"type": "web_search_20250305", "name": "web_search", "max_uses": 5}));
}
// A subscription token authenticates as Claude Code: bearer auth, the
// Claude Code beta set, and a system prompt whose first line is the
// Claude Code identity. Sending it as `x-api-key` returns 401.
let oauth = is_setup_token(&self.api_key);
// Blocks on the OAuth path, plain string on the API-key path — the
// API-key path never had this constraint and must keep working.
let system = if oauth {
Self::oauth_system_blocks(&request.system)
} else {
Value::String(request.system.clone())
};
let body = json!({
"model": request.model,
"max_tokens": request.max_tokens,
"system": system,
"messages": AnthropicProvider::wire_messages(&request),
"tools": tools,
"stream": true,
});
let mut req = self
.client
.post(format!("{}/v1/messages", self.base_url))
.header("anthropic-version", "2023-06-01");
req = if oauth {
req.header("authorization", format!("Bearer {}", self.api_key))
.header(
"anthropic-beta",
"claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14",
)
} else {
req.header("x-api-key", &self.api_key)
};
let response = req
.json(&body)
.send()
.await
.map_err(|e| LlmError::Transport(e.to_string()))?;
if !response.status().is_success() {
let status = response.status();
let detail = response.text().await.unwrap_or_default();
return Err(LlmError::Api(format!("{status}: {detail}")));
}
let mut sse = response.bytes_stream().eventsource();
let stream = try_stream! {
// tool_use input arrives as accumulated partial JSON between
// content_block_start and content_block_stop.
let mut pending_tool: Option<(String, String, String)> = None; // id, name, json
let mut input_tokens: u32 = 0;
while let Some(event) = sse.next().await {
let event = event.map_err(|e| LlmError::Transport(e.to_string()))?;
let data: Value = serde_json::from_str(&event.data)
.map_err(|e| LlmError::Wire(format!("{e}: {}", event.data)))?;
match data["type"].as_str().unwrap_or_default() {
"content_block_start" => {
let block = &data["content_block"];
if block["type"] == "tool_use" {
pending_tool = Some((
block["id"].as_str().unwrap_or_default().to_owned(),
crate::internal_tool_name(
block["name"].as_str().unwrap_or_default(),
),
String::new(),
));
}
}
"content_block_delta" => {
let delta = &data["delta"];
match delta["type"].as_str().unwrap_or_default() {
"text_delta" => {
if let Some(text) = delta["text"].as_str() {
yield LlmEvent::TextDelta(text.to_owned());
}
}
"input_json_delta" => {
if let Some((_, _, buf)) = pending_tool.as_mut() {
buf.push_str(delta["partial_json"].as_str().unwrap_or_default());
}
}
_ => {}
}
}
"content_block_stop" => {
if let Some((id, name, buf)) = pending_tool.take() {
let input: Value = if buf.is_empty() {
json!({})
} else {
serde_json::from_str(&buf)
.map_err(|e| LlmError::Wire(format!("tool input: {e}")))?
};
yield LlmEvent::ToolUse { id, name, input };
}
}
"message_start" => {
input_tokens =
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,
output_tokens: out as u32,
};
}
if let Some(reason) = data["delta"]["stop_reason"].as_str() {
yield LlmEvent::Stop(stop_reason(reason));
}
}
"error" => {
Err(LlmError::Api(data["error"]["message"]
.as_str()
.unwrap_or("unknown")
.to_owned()))?;
}
_ => {}
}
}
};
Ok(Box::pin(stream))
}
}
/// 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"));
assert!(is_setup_token(" sk-ant-oat01-abc "), "trims first");
assert!(!is_setup_token("sk-ant-api03-abc"));
assert!(!is_setup_token(""));
}
/// The identity must be its own FIRST block, and the caller's prompt a
/// SECOND one. Concatenating them into a single string is what made every
/// system-prompt-setting call return `429 rate_limit_error` on a token
/// whose account was at 7% utilization.
#[test]
fn the_oauth_system_is_blocks_with_the_identity_first() {
let v = AnthropicProvider::oauth_system_blocks("Judge the condition.");
let blocks = v.as_array().expect("system must be an ARRAY, not a string");
assert_eq!(blocks.len(), 2);
assert_eq!(blocks[0]["type"], "text");
assert_eq!(blocks[0]["text"], OAUTH_SYSTEM_PREAMBLE);
assert_eq!(blocks[1]["text"], "Judge the condition.");
}
/// Idempotent: a caller that already opens with the identity must not send
/// it twice — duplicate identity is token waste on every single call.
#[test]
fn the_identity_is_never_duplicated() {
let already = format!("{OAUTH_SYSTEM_PREAMBLE}\n\nJudge the condition.");
let v = AnthropicProvider::oauth_system_blocks(&already);
let blocks = v.as_array().unwrap();
assert_eq!(blocks.len(), 2, "{blocks:?}");
assert_eq!(blocks[0]["text"], OAUTH_SYSTEM_PREAMBLE);
assert_eq!(blocks[1]["text"], "Judge the condition.");
let text = serde_json::to_string(&v).unwrap();
assert_eq!(text.matches(OAUTH_SYSTEM_PREAMBLE).count(), 1);
}
/// An empty caller prompt yields the identity ALONE — never a trailing
/// empty block, which the API rejects.
#[test]
fn an_empty_system_prompt_yields_the_identity_alone() {
for empty in ["", " ", OAUTH_SYSTEM_PREAMBLE] {
let v = AnthropicProvider::oauth_system_blocks(empty);
let blocks = v.as_array().unwrap();
assert_eq!(blocks.len(), 1, "{empty:?} -> {blocks:?}");
assert_eq!(blocks[0]["text"], OAUTH_SYSTEM_PREAMBLE);
}
}
#[test]
fn is_subscription_reports_the_credential_kind() {
assert!(AnthropicProvider::new("sk-ant-oat01-x".into()).is_subscription());
assert!(!AnthropicProvider::new("sk-ant-api03-x".into()).is_subscription());
}
}