diff --git a/src/openhuman/inference/ops.rs b/src/openhuman/inference/ops.rs index 5f6387252..6c7ab7205 100644 --- a/src/openhuman/inference/ops.rs +++ b/src/openhuman/inference/ops.rs @@ -9,10 +9,26 @@ use crate::openhuman::inference::{device, presets, sentiment, SentimentResult}; use crate::openhuman::inference::{LocalAiEmbeddingResult, LocalAiStatus}; use crate::rpc::RpcOutcome; use serde_json::{json, Value}; -use tracing::{debug, error}; +use tracing::{debug, error, warn}; const LOG_PREFIX: &str = "[inference::ops]"; +/// User picked a provider id (slug) that isn't registered in the cloud +/// provider list — e.g. selecting `"ollama"` as a cloud provider when it's +/// actually a local runtime. Matches the literal phrase emitted at +/// `src/openhuman/inference/provider/ops.rs:54` +/// (`"no cloud provider with id or slug '{}' found"`). +/// +/// Used by [`inference_list_models`] to demote this user-config case to +/// `warn!` so it stops escalating to Sentry (TAURI-RUST-X, ~5740 events). +/// The matcher is anchored on the exact phrase so unrelated sibling +/// failures (TAURI-RUST-12 JSON parse, TAURI-RUST-2W reqwest builder, +/// TAURI-RUST-JP local ollama_admin transport) still surface as real +/// errors. +fn is_unknown_provider_user_config(err: &str) -> bool { + err.contains("no cloud provider with id or slug") +} + #[derive(Debug, Clone, serde::Serialize)] pub struct InferenceTestProviderModelResult { pub reply: String, @@ -245,7 +261,29 @@ pub async fn inference_list_models(provider_id: &str) -> Result debug!("{LOG_PREFIX} list_models:ok"), - Err(err) => error!(error = %err, "{LOG_PREFIX} list_models:error"), + Err(err) => { + if is_unknown_provider_user_config(err) { + // User selected a provider id that isn't a registered + // cloud provider (e.g. picking "ollama", a local runtime). + // Demote to `warn!` so it stays in local logs but doesn't + // escalate to Sentry. Targets TAURI-RUST-X (~5740 events). + warn!( + provider_id, + error = %err, + "{LOG_PREFIX} list_models:unknown-provider (user-config)" + ); + } else { + // Real error — embed `{err}` in the format string so + // Sentry's event title carries the actionable cause + // instead of the opaque `list_models:error` shape that + // made TAURI-RUST-X untriageable. + error!( + provider_id, + error = %err, + "{LOG_PREFIX} list_models:error: {err}" + ); + } + } } result } diff --git a/src/openhuman/inference/ops_tests.rs b/src/openhuman/inference/ops_tests.rs index 7a8cb7187..f3b4235b8 100644 --- a/src/openhuman/inference/ops_tests.rs +++ b/src/openhuman/inference/ops_tests.rs @@ -258,3 +258,61 @@ async fn inference_openai_oauth_disconnect_returns_removed_flag() { assert_eq!(outcome.value["disconnected"], true); assert_eq!(outcome.logs, vec!["openai oauth disconnected"]); } + +// ── is_unknown_provider_user_config (TAURI-RUST-X) ─────────────────────── +// +// `inference_list_models` calls `providers::ops::list_configured_models`, +// which surfaces a `String` error when the user-selected provider id isn't +// registered in the cloud-provider list (e.g. picking "ollama" — a local +// runtime — as a cloud provider). The error string is emitted at +// `src/openhuman/inference/provider/ops.rs:54`. Before this fix the emit +// site at `inference/ops.rs:248` escalated every such error to `error!`, +// which sentry-tracing ships to Sentry as `"[inference::ops] +// list_models:error"` — 5740+ events with the underlying error hidden in +// a tracing field where no Sentry classifier can reach it. The helper +// gate keeps the demote anchored so unrelated failures (HTTP / JSON / IO) +// still escalate. + +#[test] +fn is_unknown_provider_user_config_matches_canonical_emit_site_string() { + // Verbatim shape from `provider/ops.rs:54`: + // format!("no cloud provider with id or slug '{}' found", provider_id) + // Latest TAURI-RUST-X event (Sentry id 95) carried provider_id="ollama"; + // every well-formed provider id slug must trigger the demote. + assert!(is_unknown_provider_user_config( + "no cloud provider with id or slug 'ollama' found" + )); + assert!(is_unknown_provider_user_config( + "no cloud provider with id or slug 'made-up-custom-id' found" + )); + assert!(is_unknown_provider_user_config( + "no cloud provider with id or slug '' found" + )); +} + +#[test] +fn is_unknown_provider_user_config_rejects_other_list_models_failures() { + // Defense in depth: the sibling list_models Sentry issues + // (TAURI-RUST-12 JSON parse, TAURI-RUST-2W HTTP builder, etc.) are + // real bugs that MUST still escalate to Sentry. The matcher must stay + // strictly anchored on the "no cloud provider with id or slug" phrase + // so it can't accidentally silence them. + for raw in [ + // TAURI-RUST-12 (362 events) — provider/ops.rs JSON decode failure + "[providers][list_models] failed to parse JSON: error decoding response", + // TAURI-RUST-2W (100 events) — provider/ops.rs reqwest builder failure + "[providers][list_models] HTTP request failed: builder error", + // TAURI-RUST-JP (8 events) — local_ai ollama_admin transport failure + "[local_ai:ollama_admin] list_models: request send failed", + // Generic shapes from elsewhere in the call chain + "request timed out after 30s", + "permission denied accessing config", + "no cloud provider configured for slug 'openai' (role 'chat')", + "", + ] { + assert!( + !is_unknown_provider_user_config(raw), + "must NOT demote real error: {raw:?}" + ); + } +}