Watch
1
0
Fork
You've already forked souveraine
0

claude: 529 is Anthropic overload, not a permanent failure

529 is Anthropic's own overload signal on this wire, so classify_status
treated it as Permanent and bailed the whole request with zero retries.
The bail string reaches the surface, where it reads as an auth failure.

Adds 529 to the transient arm and a status_hint() clause naming capacity
rather than credentials, on both the retry warning and the final bail.
This commit is contained in:
Fimeg 2026-08-12 10:39:40 -04:00
commit acb2b6c608

View file

@ -424,7 +424,8 @@ impl LlmProvider for ClaudeSubscriptionProvider {
ErrorClass::Transient if attempt + 1 < max_attempts => {
let delay = backoff(attempt);
warn!(
"claude subscription {status} on {model} (attempt {}), retry in {:?}",
"claude subscription {status} on {model}{} (attempt {}), retry in {:?}",
status_hint(status.as_u16()),
attempt + 1,
delay
);
@ -444,8 +445,9 @@ impl LlmProvider for ClaudeSubscriptionProvider {
body: body_text.chars().take(300).collect(),
});
anyhow::bail!(
"claude subscription returned {status} after {} attempt(s) on {model}: {}",
"claude subscription returned {status} after {} attempt(s) on {model}{}: {}",
attempt + 1,
status_hint(status.as_u16()),
&body_text[..body_text.len().min(500)]
);
}
@ -1132,11 +1134,25 @@ enum ErrorClass {
fn classify_status(status: reqwest::StatusCode) -> ErrorClass {
match status.as_u16() {
429 | 500 | 502 | 503 | 504 | 408 => ErrorClass::Transient,
// 529 is Anthropic's own overload signal on this wire, not a
// standard HTTP code — easy to misread as an auth/token failure
// because nothing before this line said otherwise.
429 | 500 | 502 | 503 | 504 | 408 | 529 => ErrorClass::Transient,
_ => ErrorClass::Permanent,
}
}
/// A short, human hint appended to the bail message for status codes whose
/// number alone reads as something it isn't — chiefly 529, which looks like
/// an OAuth/subscription failure but means Anthropic's capacity, not ours.
fn status_hint(status: u16) -> &'static str {
match status {
529 => " (Anthropic overloaded — not a Souveraine auth/token failure)",
429 => " (rate limited)",
_ => "",
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -1144,6 +1160,43 @@ mod tests {
ContentValue, MessageToolCall, MessageToolCallFunction, ToolFunction,
};
fn class_of(code: u16) -> ErrorClass {
classify_status(reqwest::StatusCode::from_u16(code).expect("valid status"))
}
#[test]
fn anthropic_overload_529_retries_instead_of_bailing_on_the_first_try() {
// 529 is Anthropic's own overload signal, not a standard HTTP code.
// Classified Permanent, one overloaded response bailed the entire
// request with zero retries, and that bail string reaches the Panel
// where it reads as a Souveraine auth/token failure.
for code in [408, 429, 500, 502, 503, 504, 529] {
assert!(
matches!(class_of(code), ErrorClass::Transient),
"{code} must be transient — it describes capacity, not our credentials"
);
}
for code in [400, 401, 403, 404, 422] {
assert!(
matches!(class_of(code), ErrorClass::Permanent),
"{code} must stay permanent — retrying it only burns quota"
);
}
}
#[test]
fn the_529_hint_blames_capacity_rather_than_our_credentials() {
let hint = status_hint(529);
assert!(hint.contains("Anthropic overloaded"), "{hint}");
assert!(
hint.contains("not a Souveraine auth"),
"the hint exists to stop a capacity failure reading as an auth failure: {hint}"
);
// Codes that read plainly get no editorial.
assert_eq!(status_hint(500), "");
assert_eq!(status_hint(401), "");
}
#[test]
fn fingerprint_matches_spec_worked_example() {
assert_eq!(compute_fingerprint("hello", "2.1.196"), "68b");