diff --git a/crates/zesdex-backend/src/app/subagent/engine.rs b/crates/zesdex-backend/src/app/subagent/engine.rs index a2fcf8d..cac5e66 100644 --- a/crates/zesdex-backend/src/app/subagent/engine.rs +++ b/crates/zesdex-backend/src/app/subagent/engine.rs @@ -25,19 +25,12 @@ fn step_retry_delay(attempt: u32) -> Duration { /// Heuristic to decide whether the error is worth retrying. fn should_retry_subagent_step(err_str: &str) -> bool { - let lower = err_str.to_lowercase(); // Never retry auth/billing failures - if err_str.contains("API error 401") - || err_str.contains("API error 402") - || err_str.contains("API error 403") - || lower.contains("unauthorized") - || lower.contains("forbidden") - || lower.contains("authentication failed") - { + if crate::service::provider::is_auth_error(err_str) { return false; } // Never retry abort or user cancellation - if lower.contains("aborted") { + if err_str.to_lowercase().contains("aborted") { return false; } // Everything else (timeout, 5xx, rate-limit, network blip) is retryable diff --git a/crates/zesdex-backend/src/app/workflow/engine/mod.rs b/crates/zesdex-backend/src/app/workflow/engine/mod.rs index 2f97979..b882c72 100644 --- a/crates/zesdex-backend/src/app/workflow/engine/mod.rs +++ b/crates/zesdex-backend/src/app/workflow/engine/mod.rs @@ -421,9 +421,7 @@ fn spawn_single_agent(sp: SpawnCtx<'_>) -> anyhow::Result { } Err(e) => { let err_str = e.to_string(); - let is_auth = err_str.contains("API error 401") - || err_str.contains("API error 402") - || err_str.contains("API error 403"); + let is_auth = crate::service::provider::is_auth_error(&err_str); // Auth errors are permanent — don't retry. if is_auth || attempt >= 2 { let _ = done_tx.send(Err(e)); diff --git a/crates/zesdex-backend/src/service/provider.rs b/crates/zesdex-backend/src/service/provider.rs index f5e138a..5722d99 100644 --- a/crates/zesdex-backend/src/service/provider.rs +++ b/crates/zesdex-backend/src/service/provider.rs @@ -57,7 +57,7 @@ fn backoff_duration(attempt: u32) -> Duration { /// request builders below, plus well-known auth keywords in case the body /// contains them. This is intentionally tighter than `contains("401")`, /// which could false-positive on a URL port, model name, or body text. -fn is_auth_error(err_str: &str) -> bool { +pub fn is_auth_error(err_str: &str) -> bool { let err_lower = err_str.to_lowercase(); // Structured HTTP status patterns (err_str.contains("API error 401")