Classify provider budget errors as quota limits

This commit is contained in:
Ryan Ward
2026-07-28 11:01:07 -05:00
parent a078287f4b
commit 5a8a35926e
3 changed files with 31 additions and 4 deletions
+7 -4
View File
@@ -155,10 +155,13 @@ pub async fn generate_multi_agent_output(
} }
Err(e) => { Err(e) => {
log::error!("[openai] Translator error: {e}"); log::error!("[openai] Translator error: {e}");
let err = Arc::new(crate::server::server_api::AIApiError::Stream { let err = Arc::new(
stream_type: "openai_chat_completions", crate::server::server_api::AIApiError::Stream {
source: anyhow::anyhow!("{e}"), stream_type: "openai_chat_completions",
}); source: anyhow::anyhow!("{e}"),
}
.into_quota_limit_if_provider_budget_exhausted(),
);
let (tx, rx) = async_channel::unbounded(); let (tx, rx) = async_channel::unbounded();
let _ = tx.send(Err(err)).await; let _ = tx.send(Err(err)).await;
Ok(Box::pin(rx)) Ok(Box::pin(rx))
+1
View File
@@ -48,6 +48,7 @@ fn request_params_with_ask_user_question_enabled(ask_user_question_enabled: bool
bedrock_progressive_summary: None, bedrock_progressive_summary: None,
bedrock_tool_result_archive: Vec::new(), bedrock_tool_result_archive: Vec::new(),
bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
global_rules: Vec::new(),
} }
} }
+23
View File
@@ -205,6 +205,29 @@ impl From<serde_json::Error> for AIApiError {
} }
impl AIApiError { impl AIApiError {
/// Turns an upstream provider quota/budget response into a user-facing
/// quota error instead of a generic stream failure.
pub fn into_quota_limit_if_provider_budget_exhausted(self) -> Self {
let AIApiError::Stream { source, .. } = self else {
return self;
};
let message = source.to_string();
let lower = message.to_ascii_lowercase();
if lower.contains("budget has been exceeded")
|| lower.contains("rate limit") && lower.contains("429")
{
return Self::QuotaLimit {
user_display_message: Some(
"The AI provider has reached its rate limit or monthly budget. Please try again later or choose another configured model.".to_string(),
),
};
}
Self::Stream {
stream_type: "provider",
source,
}
}
/// Converts a reqwest error to an AIApiError, using response headers to distinguish /// Converts a reqwest error to an AIApiError, using response headers to distinguish
/// between different types of 429 errors. /// between different types of 429 errors.
fn from_response_error( fn from_response_error(