mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(observability): demote OpenHuman backend unknown-model 400 (TAURI-RUST-2Z1) (#2464)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Steven Enamakel
parent
a2d1b122ce
commit
fe37b132ee
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, BYOK_INCOMPLETE_SENTINEL};
|
||||
pub use ops::*;
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user