From f4fda2dfc030bf23f11a1f964f3a4b5b01282b04 Mon Sep 17 00:00:00 2001 From: Steven Enamakel's Droid Date: Mon, 1 Jun 2026 04:17:02 +0000 Subject: [PATCH 1/3] fix(inference): surface actionable error when Managed route fails with no credits (#3088) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenHuman managed backend signals no-credits via a 400 carrying "Insufficient budget" / "Insufficient balance" (billing_error.rs canonical phrases). The web-channel budget detector (is_inference_budget_exceeded_error) only matched the regex set ("budget.*exceed", "top up", "add.*credits", "out of credits") — it missed those phrases, so the error fell through to the generic "Something went wrong" branch. Users with Ollama enabled but routing still on Managed saw an opaque provider error and entered an infinite "Thinking…" state with no way to self-diagnose. Changes: - Widen is_inference_budget_exceeded_error to also delegate to billing_error::is_budget_exhausted_message (the canonical detector) so the 400 "Insufficient budget" path is caught. - Extend classify_inference_error's budget_exhausted branch to call is_inference_budget_exceeded_error as a fallback, catching any budget-signal that doesn't carry a 402 status. - Rewrite the user-facing budget-exceeded copy to guide the user: top up credits OR switch routing to "Use your own model" in Settings. - Add tests for the new detection paths and the updated copy. --- .../channels/providers/web_errors.rs | 32 +++++++++++++-- src/openhuman/channels/providers/web_tests.rs | 40 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/openhuman/channels/providers/web_errors.rs b/src/openhuman/channels/providers/web_errors.rs index dc5cdc2cae7..9fcc399f711 100644 --- a/src/openhuman/channels/providers/web_errors.rs +++ b/src/openhuman/channels/providers/web_errors.rs @@ -17,13 +17,33 @@ pub(crate) fn is_inference_budget_exceeded_error(message: &str) -> bool { let normalized = BUDGET_ERROR_NORMALIZE_RE .replace_all(&message.trim().to_ascii_lowercase(), " ") .into_owned(); - BUDGET_ERROR_PATTERNS + if BUDGET_ERROR_PATTERNS .iter() .any(|pattern| pattern.is_match(&normalized)) + { + return true; + } + // Align with the canonical OpenHuman-backend budget detector + // (`billing_error::is_budget_exhausted_message`) so the managed + // no-credits response — a 400 carrying "Insufficient budget" / + // "Insufficient balance" — surfaces the actionable budget message + // below instead of the generic "Something went wrong" apology + // (issue #3088). Without this, an Ollama user with zero credits and + // routing still on Managed sees an opaque "provider error" and has no + // way to self-diagnose that they must top up or switch routing. + crate::openhuman::inference::provider::is_budget_exhausted_message(message) } pub(crate) fn inference_budget_exceeded_user_message() -> &'static str { - "I don't have any budget available right now. Please top up your credits or choose a plan to continue." + // Keep the literal "top up" / "credits" tokens (asserted by + // `budget_exceeded_copy_mentions_top_up`) and add the self-diagnosis + // path for issue #3088: a user who enabled a local model but left + // routing on Managed needs to know they can switch to their own model + // rather than being stuck. We guide, never auto-switch — the user's + // routing choice in Settings is respected. + "You're out of credits, so I can't run the managed (cloud) model right now. \ + You can top up your credits or pick a plan to continue — or, if you've enabled a \ + local model like Ollama, switch routing to \"Use your own model\" in Settings → LLM." } pub(crate) fn generic_inference_error_user_message() -> &'static str { @@ -379,6 +399,12 @@ pub(crate) fn classify_inference_error(err: &str) -> ClassifiedError { } else if lower.contains("402") || lower.contains("payment required") || lower.contains("insufficient balance") + // Issue #3088: the OpenHuman managed backend reports no-credits as a + // 400 with "Insufficient budget" (not a 402), which previously fell + // through to the generic "Something went wrong" branch. Catch the + // canonical budget phrases here so the user gets the actionable + // top-up / switch-to-your-own-model guidance instead. + || is_inference_budget_exceeded_error(err) { // `openhuman_billing` means OpenHuman's own credit/quota system — // a 402 carrying the "openhuman" envelope (or no envelope at all, @@ -392,7 +418,7 @@ pub(crate) fn classify_inference_error(err: &str) -> ClassifiedError { }; ClassifiedError { error_type: "budget_exhausted", - message: with_provider_detail("Insufficient credits. Please top up to continue.", err), + message: with_provider_detail(inference_budget_exceeded_user_message(), err), source, retryable: false, retry_after_ms: None, diff --git a/src/openhuman/channels/providers/web_tests.rs b/src/openhuman/channels/providers/web_tests.rs index e2d20652843..989073e5a4c 100644 --- a/src/openhuman/channels/providers/web_tests.rs +++ b/src/openhuman/channels/providers/web_tests.rs @@ -134,6 +134,16 @@ fn detects_backend_budget_exhaustion_error() { assert!(is_inference_budget_exceeded_error( "provider error: budget exceeded, please add credits" )); + // Issue #3088: the OpenHuman managed backend reports no-credits as a + // 400 carrying these canonical phrases (see `billing_error.rs`). They + // were previously NOT recognised here, so the error fell through to the + // generic "Something went wrong" branch. They must now match. + assert!(is_inference_budget_exceeded_error( + "openhuman API error (400 Bad Request): Insufficient budget" + )); + assert!(is_inference_budget_exceeded_error( + "openhuman API error (400 Bad Request): Insufficient balance" + )); assert!(!is_inference_budget_exceeded_error( "OpenHuman API error (500): Internal server error" )); @@ -144,6 +154,36 @@ fn budget_exceeded_copy_mentions_top_up() { let message = inference_budget_exceeded_user_message(); assert!(message.contains("top up")); assert!(message.contains("credits")); + // Issue #3088: the copy must guide the user to the self-service fix — + // switching routing to their own local model — so an Ollama user with + // no credits can self-diagnose. We guide, never auto-switch. + assert!(message.contains("Use your own model")); + assert!(message.contains("Settings")); +} + +#[test] +fn classify_inference_error_managed_insufficient_budget_400_is_budget_exhausted() { + // Issue #3088: a managed (OpenHuman backend) no-credits failure arrives + // as a 400 with "Insufficient budget" — NOT a 402. It previously fell + // through to the generic `inference` branch ("Something went wrong"), + // leaving the user unable to self-diagnose. It must now classify as + // budget_exhausted with actionable, non-retryable copy. + let raw = "openhuman API error (400 Bad Request): Insufficient budget"; + let classified = classify_inference_error(raw); + assert_eq!(classified.error_type, "budget_exhausted"); + assert_eq!( + classified.source, "openhuman_billing", + "the OpenHuman backend's own credit system is the origin" + ); + assert!( + !classified.retryable, + "out of credits — retrying the same prompt won't help" + ); + assert!( + classified.message.contains("Use your own model"), + "must guide the user to switch routing: {}", + classified.message + ); } #[test] From b8de90ac80e30a21b24fbf0e92dfde5741f0d364 Mon Sep 17 00:00:00 2001 From: Steven Enamakel's Droid Date: Mon, 1 Jun 2026 05:15:06 +0000 Subject: [PATCH 2/3] test: update E2E assertion for budget classifier after #3088 fix The coverage E2E test asserted that "inference budget exceeded: monthly limit reached" classified as generic `inference`. After the #3088 fix widened budget detection, this string correctly classifies as `budget_exhausted` with source `openhuman_billing`. Update the assertion to match the new (correct) behavior. --- tests/channels_provider_leftovers_raw_coverage_e2e.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/channels_provider_leftovers_raw_coverage_e2e.rs b/tests/channels_provider_leftovers_raw_coverage_e2e.rs index 84b3008323d..496b8455aa2 100644 --- a/tests/channels_provider_leftovers_raw_coverage_e2e.rs +++ b/tests/channels_provider_leftovers_raw_coverage_e2e.rs @@ -318,11 +318,14 @@ async fn web_round19_covers_classifier_variants_and_cancel_cleanup() { assert_eq!(auth.source, "config"); assert!(!auth.retryable); + // Issue #3088: budget-signal strings now classify as `budget_exhausted` + // instead of falling through to the generic `inference` branch — the + // user gets an actionable "top up or switch routing" message. let budget = web_test_support::classify_error_for_test( "inference budget exceeded: monthly limit reached", ); - assert_eq!(budget.error_type, "inference"); - assert_eq!(budget.source, "provider"); + assert_eq!(budget.error_type, "budget_exhausted"); + assert_eq!(budget.source, "openhuman_billing"); let network = web_test_support::classify_error_for_test( "request error: dns error while trying to connect", From 9234cc9c63974dd3a39147b219b42102328171a7 Mon Sep 17 00:00:00 2001 From: Steven Enamakel's Droid Date: Mon, 1 Jun 2026 15:11:34 +0000 Subject: [PATCH 3/3] fix: match exact UI label in budget-exceeded copy per review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the user-facing message to reference "Use Your Own Models" (matching the i18n key settings.ai.routing.useYourOwn) and "Settings → AI Configuration" (matching settings.ai) instead of the previous approximation "Use your own model" / "Settings → LLM". --- src/openhuman/channels/providers/web_errors.rs | 2 +- src/openhuman/channels/providers/web_tests.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/openhuman/channels/providers/web_errors.rs b/src/openhuman/channels/providers/web_errors.rs index 9fcc399f711..58f89819be0 100644 --- a/src/openhuman/channels/providers/web_errors.rs +++ b/src/openhuman/channels/providers/web_errors.rs @@ -43,7 +43,7 @@ pub(crate) fn inference_budget_exceeded_user_message() -> &'static str { // routing choice in Settings is respected. "You're out of credits, so I can't run the managed (cloud) model right now. \ You can top up your credits or pick a plan to continue — or, if you've enabled a \ - local model like Ollama, switch routing to \"Use your own model\" in Settings → LLM." + local model like Ollama, switch routing to \"Use Your Own Models\" in Settings → AI Configuration." } pub(crate) fn generic_inference_error_user_message() -> &'static str { diff --git a/src/openhuman/channels/providers/web_tests.rs b/src/openhuman/channels/providers/web_tests.rs index 989073e5a4c..2f2b1cfd9b4 100644 --- a/src/openhuman/channels/providers/web_tests.rs +++ b/src/openhuman/channels/providers/web_tests.rs @@ -157,7 +157,7 @@ fn budget_exceeded_copy_mentions_top_up() { // Issue #3088: the copy must guide the user to the self-service fix — // switching routing to their own local model — so an Ollama user with // no credits can self-diagnose. We guide, never auto-switch. - assert!(message.contains("Use your own model")); + assert!(message.contains("Use Your Own Models")); assert!(message.contains("Settings")); } @@ -180,7 +180,7 @@ fn classify_inference_error_managed_insufficient_budget_400_is_budget_exhausted( "out of credits — retrying the same prompt won't help" ); assert!( - classified.message.contains("Use your own model"), + classified.message.contains("Use Your Own Models"), "must guide the user to switch routing: {}", classified.message );