diff --git a/src/openhuman/inference/provider/config_rejection.rs b/src/openhuman/inference/provider/config_rejection.rs index df69b159f..cd159f047 100644 --- a/src/openhuman/inference/provider/config_rejection.rs +++ b/src/openhuman/inference/provider/config_rejection.rs @@ -172,6 +172,17 @@ pub fn is_provider_config_rejection_message(body: &str) -> bool { // cover. The UI surfaces an actionable upgrade link in the // remediation message itself. "requires a subscription, upgrade for access", + // TAURI-RUST-1V — reliable-provider chain rolls up exhausted + // fallbacks into `All providers/models failed. Attempts:\n…\nThe + // model `` may not be available on your provider. Configure + // a fallback chain via `reliability.model_fallbacks` in …`. + // Emitted at `src/openhuman/inference/provider/reliable.rs:332`. + // The remediation is "fix your `model_fallbacks` config" — pure + // user-config, nothing Sentry can act on. Anchor on the canonical + // remediation phrase so this doesn't collide with unrelated + // mentions of model availability (`reliable.rs:332` is the sole + // producer in-tree). + "may not be available on your provider", ]; let lower = body.to_ascii_lowercase(); @@ -324,6 +335,41 @@ mod tests { } } + #[test] + fn detects_reliable_chain_exhaustion_rollup() { + // TAURI-RUST-1V — `reliable.rs:325` rolls every attempt into + // `All providers/models failed. Attempts:\n…\nThe model `` + // may not be available on your provider. Configure a fallback + // chain via `reliability.model_fallbacks` in …`. The wrapped err + // bubbles to `memory_sync::composio::bus` which previously + // emitted it as a raw `tracing::error!` — 10.7k events / 14d on + // self-hosted Sentry. The remediation lives entirely in the + // user's `reliability.model_fallbacks` config; Sentry has no + // remediation path. + let rollup = "All providers/models failed. Attempts:\n\ + provider=openhuman model=gemini-3-flash-preview attempt 1/3: \ + non_retryable; error=custom_openai API error (404 Not Found): \ + ...\n\ + The model `gemini-3-flash-preview` may not be available on \ + your provider. Configure a fallback chain via \ + `reliability.model_fallbacks` in your config to route around \ + unavailable models."; + assert!( + is_provider_config_rejection_message(rollup), + "TAURI-RUST-1V multi-line rollup must classify as provider config-rejection" + ); + + // Single-line `reliable.rs:332` emission (without the outer + // rollup wrapper) also matches — defensive against callers that + // surface only the inner remediation message. + let bare = "The model `chat-v1` may not be available on your provider. \ + Configure a fallback chain via `reliability.model_fallbacks` in …"; + assert!( + is_provider_config_rejection_message(bare), + "bare `may not be available on your provider` phrase must classify" + ); + } + #[test] fn unknown_model_helper_matches_openai_compatible_bodies() { // TAURI-RUST-2Z1 — the OpenHuman hosted backend now emits the diff --git a/src/openhuman/memory_sync/composio/bus.rs b/src/openhuman/memory_sync/composio/bus.rs index c22a8e474..4436b094d 100644 --- a/src/openhuman/memory_sync/composio/bus.rs +++ b/src/openhuman/memory_sync/composio/bus.rs @@ -352,10 +352,26 @@ impl EventHandler for ComposioTriggerSubscriber { ); } Err(e) => { - tracing::error!( - label = %envelope.display_label, - error = %e, - "[composio][triage] run_triage failed" + // Route through the central observability classifier + // so user-config / budget-exhausted / provider-state + // rollups from `reliable.rs` (e.g. `The model + // \`\` may not be available on your provider …`) + // get demoted to info-level breadcrumbs instead of + // surfacing as raw Sentry errors. Previously this + // call used `tracing::error!` directly and bypassed + // the classifier — 10.7k events / 14d on self-hosted + // Sentry TAURI-RUST-1V, dominated by + // ProviderConfigRejection-class rollups whose inner + // attempts the provider layer already demoted. + let detail = format!( + "[composio][triage] run_triage failed (label={}): {e:#}", + envelope.display_label + ); + crate::core::observability::report_error_or_expected( + detail.as_str(), + "composio", + "trigger_triage", + &[("label", envelope.display_label.as_str())], ); } }