fix(llm): the subscription 429s were a malformed request, not a rate limit
On the OAuth path Anthropic requires the Claude Code identity to be its own
first system BLOCK. We concatenated it with the caller's prompt into a single
string, so EVERY server-side call that set a system prompt was 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:
"PREAMBLE" (string) -> 200
"PREAMBLE\n\nJudge the …" (string) -> 429
"PREAMBLE" (string) -> 200 (control)
["PREAMBLE"] (blocks) -> 200
["PREAMBLE", "Judge the …"] (blocks) -> 200
while the account reported `5h utilization 0.07, 7d 0.11, overage 0.0`, every
window `allowed`. A Max 20x subscription at 7% was being read as out of
capacity.
What this was breaking, silently, for as long as it has been there:
- every `done_when` verdict on the subscription judge. Mission 01a00bbb
pass 2 returned "could not evaluate the completion condition this pass"
and BURNED one of the phase's three passes on it.
- the boot preflight, which reported `claude-opus-4-8 throttled (configured,
no capacity now)` on every start — a diagnostic that was itself the bug.
- mission_refiner, phase_summarizer, swarm planning.
The `claude` CLI was unaffected throughout, because it sends its system prompt
as blocks. That divergence is what made this look like an account problem: the
agents worked while everything server-side "throttled".
After the fix the preflight reports opus-5, sonnet-5 and haiku all `ok`.
The API-key path keeps sending a plain string — it never had this constraint.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
69c294addc
commit
b31a79f650
@@ -45,16 +45,42 @@ impl AnthropicProvider {
|
||||
is_setup_token(&self.api_key)
|
||||
}
|
||||
|
||||
/// Prepend the Claude Code identity line, unless the caller's system
|
||||
/// prompt already opens with it (so repeated wrapping can't stack).
|
||||
fn with_oauth_preamble(system: &str) -> String {
|
||||
if system.trim_start().starts_with(OAUTH_SYSTEM_PREAMBLE) {
|
||||
return system.to_string();
|
||||
/// 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}));
|
||||
}
|
||||
if system.trim().is_empty() {
|
||||
return OAUTH_SYSTEM_PREAMBLE.to_string();
|
||||
}
|
||||
format!("{OAUTH_SYSTEM_PREAMBLE}\n\n{system}")
|
||||
Value::Array(blocks)
|
||||
}
|
||||
|
||||
fn wire_messages(request: &ChatRequest) -> Vec<Value> {
|
||||
@@ -121,10 +147,12 @@ impl LlmProvider for AnthropicProvider {
|
||||
// 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::with_oauth_preamble(&request.system)
|
||||
Self::oauth_system_blocks(&request.system)
|
||||
} else {
|
||||
request.system.clone()
|
||||
Value::String(request.system.clone())
|
||||
};
|
||||
let body = json!({
|
||||
"model": request.model,
|
||||
@@ -250,25 +278,44 @@ mod tests {
|
||||
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 oauth_preamble_is_prepended_once() {
|
||||
let once = AnthropicProvider::with_oauth_preamble("Judge the condition.");
|
||||
assert!(once.starts_with(OAUTH_SYSTEM_PREAMBLE));
|
||||
assert!(once.ends_with("Judge the condition."));
|
||||
// Re-wrapping must not stack the identity line — the API rejects a
|
||||
// system prompt that doesn't *start* with it, and duplicating it is
|
||||
// pure token waste on a path whose whole point is being cheap.
|
||||
let twice = AnthropicProvider::with_oauth_preamble(&once);
|
||||
assert_eq!(once, twice);
|
||||
assert_eq!(twice.matches(OAUTH_SYSTEM_PREAMBLE).count(), 1);
|
||||
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 oauth_preamble_handles_an_empty_system_prompt() {
|
||||
assert_eq!(
|
||||
AnthropicProvider::with_oauth_preamble(" "),
|
||||
OAUTH_SYSTEM_PREAMBLE
|
||||
);
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user