From 8ac8a1f611602cb60449078560b4c4c84dab8103 Mon Sep 17 00:00:00 2001 From: Ghost Scripter Date: Fri, 22 May 2026 01:21:51 +0530 Subject: [PATCH] fix(observability): demote OpenHuman backend unknown-model 400 (TAURI-RUST-2Z1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenHuman hosted backend now emits the OpenAI-compatible "Model 'X' is not available. Use GET /openai/v1/models …" wire body for user-configured unknown model ids — a body shape the polarity guard in `is_provider_config_rejection_http` had previously assumed only third-party `custom_openai` upstreams would speak. Result: the per-attempt event reached Sentry as a real error (TAURI-RUST-2Z1) even though the underlying condition is pure user-state (the user typed an unknown model id and configured `model_fallbacks` with `custom:`-prefixed variants that the backend also rejected). Drop the polarity guard for that one body shape via a new narrow helper `is_openai_compatible_unknown_model_message` anchored on the `/openai/v1/models` remediation hint. Other config-rejection phrases (DeepSeek `supported api model names are`, Moonshot `invalid temperature`, litellm envelopes, OpenRouter `requires more credits`) stay polarity-gated so a regression where our own backend started emitting them would still reach Sentry. The aggregate sibling TAURI-RUST-2Z2 is already auto-demoted by the message-only classifier in `core::observability::expected_error_kind` (via #2239's `/openai/v1/models` phrase). --- .../inference/provider/config_rejection.rs | 93 ++++++++++++++++++- src/openhuman/inference/provider/mod.rs | 4 +- src/openhuman/inference/provider/ops.rs | 85 ++++++++++++++--- 3 files changed, 164 insertions(+), 18 deletions(-) diff --git a/src/openhuman/inference/provider/config_rejection.rs b/src/openhuman/inference/provider/config_rejection.rs index c64eaefa4..125edc4cc 100644 --- a/src/openhuman/inference/provider/config_rejection.rs +++ b/src/openhuman/inference/provider/config_rejection.rs @@ -44,20 +44,32 @@ //! //! ## Provider-aware polarity (important) //! -//! The phrases below are emitted by **third-party upstream APIs** +//! Most of the phrases below are emitted by **third-party upstream APIs** //! (DeepSeek / OpenRouter / Moonshot). The OpenHuman hosted backend //! resolves tier aliases natively and never emits "supported API model //! names are deepseek-…" or "invalid temperature: only 1 is allowed" — so -//! the phrase set is intrinsically scoped to custom providers. The +//! that phrase set is intrinsically scoped to custom providers. The //! HTTP-layer wrapper [`super::ops::is_provider_config_rejection_http`] -//! additionally guards on `provider != openhuman_backend::PROVIDER_LABEL` -//! so a model-rejection from our **own** backend (which would be a real +//! polarity-guards those phrases on `provider != +//! openhuman_backend::PROVIDER_LABEL` so a model-rejection from our +//! **own** backend that we did not expect (which would be a real //! regression we sent it a bad request) still reaches Sentry. The //! message-only predicate is consumed by //! [`crate::core::observability::expected_error_kind`] for the //! re-reported error that escapes the provider layer and is raised again //! by `agent.run_single` / `web_channel.run_chat_task`. //! +//! **Exception: the OpenAI-compatible "unknown model" shape** (`Model 'X' +//! is not available. Use GET /openai/v1/models …`) is now emitted by the +//! OpenHuman hosted backend too, in response to user-configured model ids +//! that aren't in the backend's registry. Pinned by +//! [`is_openai_compatible_unknown_model_message`]. The HTTP-layer wrapper +//! drops the polarity guard for that specific shape so the same body is +//! treated as user-state regardless of provider — see TAURI-RUST-2Z1 +//! where a user-typed `MiniMax-M2.7-highspeed` model id (plus two +//! `custom:` fallback variants from their own `model_fallbacks` config) +//! was rejected with this wire shape and otherwise reached Sentry. +//! //! Keep the list deliberately tight: a false positive demotes a real //! provider/backend bug to an info log. @@ -142,6 +154,29 @@ pub fn is_provider_config_rejection_message(body: &str) -> bool { PHRASES.iter().any(|phrase| lower.contains(phrase)) } +/// Returns true if a provider error body matches the OpenAI-compatible +/// "unknown model" shape — anchored on the `/openai/v1/models` +/// remediation hint the upstream returns alongside `Model 'X' is not +/// available.`. +/// +/// This is a strict subset of [`is_provider_config_rejection_message`]: +/// the same phrase already lives in that predicate's list. The narrower +/// helper exists so the HTTP-layer wrapper +/// ([`super::ops::is_provider_config_rejection_http`]) can drop its +/// `provider != openhuman_backend::PROVIDER_LABEL` polarity guard for +/// this specific body shape — the OpenHuman hosted backend now emits the +/// same OpenAI-compatible "Model 'X' is not available" wire body in +/// response to user-configured unknown model ids, so the original +/// polarity assumption ("only third-party providers speak this dialect") +/// no longer holds. +/// +/// Drops TAURI-RUST-2Z1 (per-attempt) — the aggregate sibling +/// TAURI-RUST-2Z2 is already covered by the message-only classifier in +/// [`crate::core::observability::expected_error_kind`]. +pub fn is_openai_compatible_unknown_model_message(body: &str) -> bool { + body.to_ascii_lowercase().contains("/openai/v1/models") +} + #[cfg(test)] mod tests { use super::*; @@ -252,4 +287,54 @@ mod tests { ); } } + + #[test] + fn unknown_model_helper_matches_openai_compatible_bodies() { + // TAURI-RUST-2Z1 — the OpenHuman hosted backend now emits the + // OpenAI-compatible "Model 'X' is not available" wire body for + // user-configured unknown model ids. The helper is anchored on + // the `/openai/v1/models` remediation hint so the same body shape + // matches whether it came from a third-party `custom_openai` + // upstream or our own backend. + for body in [ + r#"OpenHuman API error (400 Bad Request): {"success":false,"error":"Model 'MiniMax-M2.7-highspeed' is not available. Use GET /openai/v1/models to list available models."}"#, + r#"OpenHuman API error (400 Bad Request): {"success":false,"error":"Model 'custom:MiniMax-M2.7' is not available. Use GET /openai/v1/models to list available models."}"#, + "Model 'deepseek-v4-pro' is not available. Use GET /openai/v1/models to list available models.", + ] { + assert!( + is_openai_compatible_unknown_model_message(body), + "TAURI-RUST-2Z1 body must classify as openai-compatible unknown model: {body:?}" + ); + // Sanity: must remain a member of the broader phrase set so + // the message-only classifier in + // `crate::core::observability::expected_error_kind` keeps + // demoting the aggregate (TAURI-RUST-2Z2). + assert!( + is_provider_config_rejection_message(body), + "broader classifier must continue to match: {body:?}" + ); + } + } + + #[test] + fn unknown_model_helper_rejects_other_config_rejection_phrases() { + // Polarity exception must stay narrow: other config-rejection + // shapes (DeepSeek `supported api model names are`, Moonshot + // `invalid temperature`, OpenRouter `requires more credits`, …) + // must still go through the provider-polarity guard so a + // hypothetical regression where our own backend emits one of + // those phrases reaches Sentry. + for body in [ + "The supported API model names are deepseek-v4-pro or deepseek-v4-flash, but you passed reasoning-v1.", + "invalid temperature: only 1 is allowed for this model", + "The model `gpt-5.5` does not exist or you do not have access to it.", + r#"{"error":{"message":"model not found","code":"model_not_found"}}"#, + "This request requires more credits, or fewer max_tokens.", + ] { + assert!( + !is_openai_compatible_unknown_model_message(body), + "{body:?} must NOT match the narrow openai-compatible-unknown-model helper" + ); + } + } } diff --git a/src/openhuman/inference/provider/mod.rs b/src/openhuman/inference/provider/mod.rs index f47f71e2d..b4744aa43 100644 --- a/src/openhuman/inference/provider/mod.rs +++ b/src/openhuman/inference/provider/mod.rs @@ -28,6 +28,8 @@ pub use traits::{ }; pub use billing_error::is_budget_exhausted_message; -pub use config_rejection::is_provider_config_rejection_message; +pub use config_rejection::{ + is_openai_compatible_unknown_model_message, is_provider_config_rejection_message, +}; pub use factory::{create_chat_provider, provider_for_role}; pub use ops::*; diff --git a/src/openhuman/inference/provider/ops.rs b/src/openhuman/inference/provider/ops.rs index 29e818584..5ff95a1c1 100644 --- a/src/openhuman/inference/provider/ops.rs +++ b/src/openhuman/inference/provider/ops.rs @@ -478,21 +478,44 @@ pub(super) fn log_provider_access_policy_denied_http_403( /// abstract tier leaked to a custom provider, model-specific temperature /// constraint) that should be demoted from Sentry to an info log. /// -/// Provider-aware (inverted polarity vs. the 401/403 backend rule): the -/// same body from the OpenHuman **backend** stays Sentry-actionable — -/// that would mean we sent our own backend a bad request (a regression, -/// e.g. #2079). Only client errors from a *custom / third-party* -/// provider are user-config state. Restricted to the observed shapes -/// (400 invalid-param / unknown-model, 404 model-does-not-exist, 422 -/// unprocessable); 408/429 are transient and handled separately. +/// Provider-aware (inverted polarity vs. the 401/403 backend rule): for +/// most config-rejection phrases the same body from the OpenHuman +/// **backend** stays Sentry-actionable — that would mean we sent our own +/// backend a bad request (a regression, e.g. #2079). Restricted to the +/// observed shapes (400 invalid-param / unknown-model, 404 +/// model-does-not-exist, 422 unprocessable); 408/429 are transient and +/// handled separately. +/// +/// **Exception: OpenAI-compatible "unknown model"** (`Model 'X' is not +/// available. Use GET /openai/v1/models …`). The OpenHuman backend now +/// emits this exact body for user-configured unknown model ids, so it is +/// user-state regardless of provider — the polarity guard is dropped for +/// this specific shape (TAURI-RUST-2Z1). See +/// [`super::is_openai_compatible_unknown_model_message`]. pub(super) fn is_provider_config_rejection_http( status: reqwest::StatusCode, provider: &str, body: &str, ) -> bool { - matches!(status.as_u16(), 400 | 404 | 422) - && provider != openhuman_backend::PROVIDER_LABEL - && super::is_provider_config_rejection_message(body) + if !matches!(status.as_u16(), 400 | 404 | 422) { + return false; + } + if !super::is_provider_config_rejection_message(body) { + return false; + } + // OpenAI-compatible "unknown model" body is user-state regardless of + // provider — both third-party `custom_openai` upstreams and our own + // OpenHuman backend now emit it for user-configured model ids that + // aren't in the registry (TAURI-RUST-2Z1). + if super::is_openai_compatible_unknown_model_message(body) { + return true; + } + // Remaining config-rejection phrases (DeepSeek `supported api model + // names are`, Moonshot `invalid temperature`, litellm envelopes, …) + // are intrinsically scoped to third-party providers — keep the + // polarity guard so a regression where our own backend emits one of + // those still reaches Sentry. + provider != openhuman_backend::PROVIDER_LABEL } pub(super) fn log_provider_config_rejection( @@ -1375,14 +1398,50 @@ mod tests { #[test] fn openhuman_backend_same_body_is_not_suppressed() { - // Inverted polarity: a model-rejection from our OWN backend - // means we sent it a bad request — a real regression that must - // still reach Sentry. (Mirror of the 401/403 backend rule.) + // Inverted polarity: for tier-leak / temperature / litellm / + // OpenRouter-style phrases, the OpenHuman backend never + // emits them, so the same body from our OWN backend would + // mean we sent it a bad request — a real regression that + // must still reach Sentry. (Mirror of the 401/403 backend + // rule.) assert!(!is_provider_config_rejection_http( reqwest::StatusCode::BAD_REQUEST, openhuman_backend::PROVIDER_LABEL, TIER_LEAK_BODY, )); + assert!(!is_provider_config_rejection_http( + reqwest::StatusCode::BAD_REQUEST, + openhuman_backend::PROVIDER_LABEL, + TEMP_BODY, + )); + } + + #[test] + fn openhuman_backend_openai_compatible_unknown_model_is_suppressed() { + // TAURI-RUST-2Z1 — the OpenHuman backend DOES emit the + // OpenAI-compatible "Model 'X' is not available. Use GET + // /openai/v1/models …" wire body for user-configured unknown + // model ids (here `MiniMax-M2.7-highspeed` and two + // `custom:`-prefixed fallback variants from the user's own + // `model_fallbacks` config). That's user-state, not a + // regression — drop the polarity guard for this specific + // shape so the per-attempt event stops reaching Sentry. + // (The aggregate sibling TAURI-RUST-2Z2 is already covered by + // `expected_error_kind` via the broader message-only + // classifier.) + for body in [ + r#"OpenHuman API error (400 Bad Request): {"success":false,"error":"Model 'MiniMax-M2.7-highspeed' is not available. Use GET /openai/v1/models to list available models."}"#, + r#"OpenHuman API error (400 Bad Request): {"success":false,"error":"Model 'custom:MiniMax-M2.7' is not available. Use GET /openai/v1/models to list available models."}"#, + ] { + assert!( + is_provider_config_rejection_http( + reqwest::StatusCode::BAD_REQUEST, + openhuman_backend::PROVIDER_LABEL, + body, + ), + "TAURI-RUST-2Z1 body must be suppressed for openhuman backend: {body:?}" + ); + } } #[test]