diff --git a/app/src/ai/agent/api/impl.rs b/app/src/ai/agent/api/impl.rs index d3884f28..77649927 100644 --- a/app/src/ai/agent/api/impl.rs +++ b/app/src/ai/agent/api/impl.rs @@ -155,10 +155,13 @@ pub async fn generate_multi_agent_output( } Err(e) => { log::error!("[openai] Translator error: {e}"); - let err = Arc::new(crate::server::server_api::AIApiError::Stream { - stream_type: "openai_chat_completions", - source: anyhow::anyhow!("{e}"), - }); + let err = Arc::new( + crate::server::server_api::AIApiError::Stream { + stream_type: "openai_chat_completions", + source: anyhow::anyhow!("{e}"), + } + .into_quota_limit_if_provider_budget_exhausted(), + ); let (tx, rx) = async_channel::unbounded(); let _ = tx.send(Err(err)).await; Ok(Box::pin(rx)) diff --git a/app/src/ai/agent/api/impl_tests.rs b/app/src/ai/agent/api/impl_tests.rs index 9dce7aeb..cd1e8b41 100644 --- a/app/src/ai/agent/api/impl_tests.rs +++ b/app/src/ai/agent/api/impl_tests.rs @@ -48,6 +48,7 @@ fn request_params_with_ask_user_question_enabled(ask_user_question_enabled: bool bedrock_progressive_summary: None, bedrock_tool_result_archive: Vec::new(), bedrock_messages_sent: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), + global_rules: Vec::new(), } } diff --git a/app/src/server/server_api.rs b/app/src/server/server_api.rs index 4280f9c4..210055c0 100644 --- a/app/src/server/server_api.rs +++ b/app/src/server/server_api.rs @@ -205,6 +205,29 @@ impl From for 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 /// between different types of 429 errors. fn from_response_error(