From 3b1190336594d164dc9176db4094cdef52d7f2e4 Mon Sep 17 00:00:00 2001 From: Fimeg Date: Mon, 17 Aug 2026 08:42:19 -0400 Subject: [PATCH] bifrost: stop sleeping on a spent quota window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit retry-after was honoured unclamped, so an 11997s header on 2026-08-16 parked the subconscious pass for 3h20m with no cancel path. A 429 asking for more than 60s is a spent window, not a burst, and it does not clear inside the retry loop — fail it and name the number. claude_subscription answers the same header by rotating logins; bifrost has nothing to rotate to, so an absent header keeps the ordinary backoff. --- src/bridge/bifrost.rs | 82 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/src/bridge/bifrost.rs b/src/bridge/bifrost.rs index efa8491..f31f7be 100644 --- a/src/bridge/bifrost.rs +++ b/src/bridge/bifrost.rs @@ -456,6 +456,33 @@ fn retry_cap(body: &str, max_retries: u32) -> u32 { } } +/// Longest `retry-after` still worth sleeping on rather than failing the call. +/// +/// A window this long is a spent quota, not a burst limit, and it does not +/// clear inside a retry loop. Measured 2026-08-16: an `11997` header put the +/// subconscious pass to sleep for 3h20m with no cancel path and nothing on the +/// wire to say it was waiting rather than dead. +/// +/// `claude_subscription.rs` learned this first and answers it by rotating to +/// another login, so there an *absent* `retry-after` also counts as exhausted. +/// Bifrost has nothing to rotate to, so an absent header keeps the ordinary +/// jittered backoff — only an explicitly long one fails fast. +const BURST_RETRY_CEILING_SECS: u64 = 60; + +fn is_exhausted_window(status: reqwest::StatusCode, retry_after: Option) -> bool { + status.as_u16() == 429 + && retry_after.is_some_and(|d| d.as_secs() > BURST_RETRY_CEILING_SECS) +} + +/// Name the window in the error, so a failure says how long the provider +/// wanted rather than only that it refused. +fn reset_hint(retry_after: Option) -> String { + match retry_after { + Some(d) => format!(" (provider asked for {}s)", d.as_secs()), + None => String::new(), + } +} + fn classify_status(status: reqwest::StatusCode, body: &str) -> ErrorClass { match status.as_u16() { 429 => { @@ -731,7 +758,10 @@ impl BifrostClient { .context("Failed to read Bifrost error body")?; match classify_status(status, &body_text) { - ErrorClass::Transient if attempt < retry_cap(&body_text, policy.max_retries) => { + ErrorClass::Transient + if attempt < retry_cap(&body_text, policy.max_retries) + && !is_exhausted_window(status, retry_after) => + { let delay = retry_after.unwrap_or_else(|| jittered_delay(attempt, policy)); warn!( "Bifrost {} on {} (attempt {}), retrying in {:?}", @@ -756,10 +786,11 @@ impl BifrostClient { body: body_text[..body_text.len().min(300)].to_string(), }); anyhow::bail!( - "Bifrost returned {} after {} attempt(s) on {}: {}", + "Bifrost returned {} after {} attempt(s) on {}{}: {}", status, attempt + 1, model, + reset_hint(retry_after), &body_text[..body_text.len().min(500)] ); } @@ -844,6 +875,53 @@ impl LlmProvider for BifrostClient { mod tests { use super::*; + #[test] + fn exhausted_window_rejects_the_measured_11997s_header() { + // The header that put the subconscious to sleep for 3h20m on + // 2026-08-16. It must not be slept on. + let long = Some(Duration::from_secs(11997)); + assert!(is_exhausted_window(reqwest::StatusCode::TOO_MANY_REQUESTS, long)); + } + + #[test] + fn exhausted_window_still_sleeps_on_a_real_burst_limit() { + let short = Some(Duration::from_secs(30)); + assert!(!is_exhausted_window( + reqwest::StatusCode::TOO_MANY_REQUESTS, + short + )); + // Exactly at the ceiling is still a burst, not a spent window. + let at_ceiling = Some(Duration::from_secs(BURST_RETRY_CEILING_SECS)); + assert!(!is_exhausted_window( + reqwest::StatusCode::TOO_MANY_REQUESTS, + at_ceiling + )); + } + + #[test] + fn exhausted_window_ignores_absent_headers_and_other_statuses() { + // Bifrost has no account to rotate to, so a 429 with no header keeps + // the ordinary jittered backoff — this is where it diverges from + // `claude_subscription::is_exhausted_window` on purpose. + assert!(!is_exhausted_window( + reqwest::StatusCode::TOO_MANY_REQUESTS, + None + )); + assert!(!is_exhausted_window( + reqwest::StatusCode::SERVICE_UNAVAILABLE, + Some(Duration::from_secs(11997)) + )); + } + + #[test] + fn reset_hint_names_the_window_or_says_nothing() { + assert_eq!( + reset_hint(Some(Duration::from_secs(11997))), + " (provider asked for 11997s)" + ); + assert_eq!(reset_hint(None), ""); + } + #[test] fn test_client_creation() { let client = BifrostClient::new(