-
Notifications
You must be signed in to change notification settings - Fork 2.6k
fix(providers): drop budget-exhausted 400s from Sentry (#3M, #12, #13) #1633
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0e18cfa
a9a0477
1a4b572
4594544
a397972
30e837c
99aad44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| /// Returns true if a 400 response body indicates the user is out of | ||
| /// budget / has insufficient balance / over their plan. These are | ||
| /// deterministic user-state errors — already surfaced in the UI as a | ||
| /// toast — and must not flow to Sentry as errors. | ||
| /// | ||
| /// Match is case-insensitive against any of the known phrases. Keep the | ||
| /// list deliberately tight: false positives demote real backend bugs. | ||
| pub fn is_budget_exhausted_message(body: &str) -> bool { | ||
| const PHRASES: &[&str] = &[ | ||
| "insufficient budget", | ||
| "budget exceeded", | ||
| "add credits", | ||
| "insufficient balance", | ||
| ]; | ||
|
|
||
| let lower = body.to_ascii_lowercase(); | ||
| PHRASES.iter().any(|phrase| lower.contains(phrase)) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn detects_known_budget_exhaustion_phrases() { | ||
| for body in [ | ||
| "Insufficient budget", | ||
| "Budget exceeded", | ||
| "Insufficient balance", | ||
| "Add credits to continue", | ||
| ] { | ||
| assert!( | ||
| is_budget_exhausted_message(body), | ||
| "{body:?} must be classified as budget-exhausted user-state" | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn detection_is_case_insensitive() { | ||
| assert!(is_budget_exhausted_message("INSUFFICIENT BUDGET")); | ||
| assert!(is_budget_exhausted_message("budget EXCEEDED — ADD credits")); | ||
| assert!(is_budget_exhausted_message("Insufficient BALANCE")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn ignores_non_budget_messages() { | ||
| for body in [ | ||
| "Bad request: missing field", | ||
| "Invalid request: model not found", | ||
| "HTTP 400 Bad Request", | ||
| "", | ||
| ] { | ||
| assert!( | ||
| !is_budget_exhausted_message(body), | ||
| "{body:?} must not be classified as budget-exhausted" | ||
| ); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -400,7 +400,14 @@ impl OpenAiCompatibleProvider { | |
| let error = response.text().await?; | ||
| let sanitized = super::sanitize_api_error(&error); | ||
| let message = format!("{} Responses API error: {sanitized}", self.name); | ||
| if super::should_report_provider_http_failure(status) { | ||
| if super::is_budget_exhausted_http_400(status, &error) { | ||
| super::log_budget_exhausted_http_400( | ||
| "responses_api", | ||
| self.name.as_str(), | ||
| Some(model), | ||
| status, | ||
| ); | ||
| } else if super::should_report_provider_http_failure(status) { | ||
| crate::core::observability::report_error( | ||
| message.as_str(), | ||
| "llm_provider", | ||
|
|
@@ -736,7 +743,14 @@ impl OpenAiCompatibleProvider { | |
| "{} streaming API error ({}): {}", | ||
| self.name, status, sanitized | ||
| ); | ||
| if super::should_report_provider_http_failure(status) { | ||
| if super::is_budget_exhausted_http_400(status, &body) { | ||
| super::log_budget_exhausted_http_400( | ||
| "streaming_chat", | ||
| self.name.as_str(), | ||
| Some(native_request.model.as_str()), | ||
| status, | ||
| ); | ||
| } else if super::should_report_provider_http_failure(status) { | ||
|
Comment on lines
+746
to
+753
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Skip the non-streaming fallback for deterministic budget 400s. This branch now knows the failure is a terminal user-state error, but 🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Acknowledged — this is a valid follow-up but out of scope for the noise-drop PR. The typed sentinel error would require a provider-error taxonomy refactor (
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Would you like me to open a follow-up GitHub issue to track the streaming-fallback short-circuit work ( 🧠 Learnings used |
||
| crate::core::observability::report_error( | ||
| message.as_str(), | ||
| "llm_provider", | ||
|
|
@@ -1190,7 +1204,14 @@ impl Provider for OpenAiCompatibleProvider { | |
|
|
||
| let status_str = status.as_u16().to_string(); | ||
| let message = format!("{} API error ({status}): {sanitized}", self.name); | ||
| if super::should_report_provider_http_failure(status) { | ||
| if super::is_budget_exhausted_http_400(status, &error) { | ||
| super::log_budget_exhausted_http_400( | ||
| "chat_completions", | ||
| self.name.as_str(), | ||
| Some(model), | ||
| status, | ||
| ); | ||
| } else if super::should_report_provider_http_failure(status) { | ||
| crate::core::observability::report_error( | ||
| message.as_str(), | ||
| "llm_provider", | ||
|
|
@@ -1574,7 +1595,14 @@ impl Provider for OpenAiCompatibleProvider { | |
|
|
||
| let status_str = status.as_u16().to_string(); | ||
| let message = format!("{} API error ({status}): {sanitized}", self.name); | ||
| if super::should_report_provider_http_failure(status) { | ||
| if super::is_budget_exhausted_http_400(status, &error) { | ||
| super::log_budget_exhausted_http_400( | ||
| "native_chat", | ||
| self.name.as_str(), | ||
| Some(model), | ||
| status, | ||
| ); | ||
| } else if super::should_report_provider_http_failure(status) { | ||
| crate::core::observability::report_error( | ||
| message.as_str(), | ||
| "llm_provider", | ||
|
|
@@ -1701,7 +1729,14 @@ impl Provider for OpenAiCompatibleProvider { | |
| }; | ||
| let sanitized_error = super::sanitize_api_error(&raw_error); | ||
| let message = format!("{}: {}", status, sanitized_error); | ||
| if super::should_report_provider_http_failure(status) { | ||
| if super::is_budget_exhausted_http_400(status, &raw_error) { | ||
| super::log_budget_exhausted_http_400( | ||
| "stream_chat", | ||
| provider_name.as_str(), | ||
| Some(model_owned.as_str()), | ||
| status, | ||
| ); | ||
| } else if super::should_report_provider_http_failure(status) { | ||
| crate::core::observability::report_error( | ||
| message.as_str(), | ||
| "llm_provider", | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.