mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
This commit is contained in:
@@ -304,6 +304,41 @@ fn classify_inference_error_surfaces_provider_config_rejection_actionably() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_inference_error_chat_factory_empty_model_is_actionable_config() {
|
||||
// TAURI-RUST-GKV — the factory's #2784 empty-model bail is a LOCAL
|
||||
// string (no provider JSON body). Before the shared classifier learned
|
||||
// its anchor it fell through to the generic `inference` catch-all:
|
||||
// vague "something went wrong" copy AND retryable=true (a useless Retry
|
||||
// button that re-fires the per-message Sentry flood). It must now route
|
||||
// through the config-rejection arm to the actionable Settings → LLM
|
||||
// copy, classified non-retryable so the FE hides Retry.
|
||||
let raw = "[chat-factory] no model configured: role 'chat' resolved to an empty model id \
|
||||
for slug 'nvidia'. Include a model in the provider string (e.g. \
|
||||
'nvidia:<model-id>') or set default_model on the cloud_providers entry for \
|
||||
slug 'nvidia'.";
|
||||
let ClassifiedError {
|
||||
error_type: category,
|
||||
message,
|
||||
source,
|
||||
retryable,
|
||||
..
|
||||
} = classify_inference_error(raw);
|
||||
assert_eq!(
|
||||
category, "model_unavailable",
|
||||
"empty-model config rejection must classify as model_unavailable, not generic: {category}"
|
||||
);
|
||||
assert_eq!(source, "config", "must be sourced as user config: {source}");
|
||||
assert!(
|
||||
!retryable,
|
||||
"empty-model config rejection must be non-retryable (hide Retry)"
|
||||
);
|
||||
assert!(
|
||||
message.contains("Settings → LLM"),
|
||||
"must give the actionable remediation copy: {message}"
|
||||
);
|
||||
}
|
||||
|
||||
// ── #2364: rate-limit classification + retry-after surfacing ────
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -166,6 +166,21 @@ pub fn is_provider_config_rejection_message(body: &str) -> bool {
|
||||
// `{"error":{"message":"model field is required","code":"missing_required_field"}}`
|
||||
// when the request body contains an empty `"model":""` field.
|
||||
"model field is required",
|
||||
// TAURI-RUST-GKV (~2.3k events / 1 user) — the LOCAL form of the
|
||||
// 4NM empty-model state, caught one layer earlier. The #2784 guard
|
||||
// in `factory::make_cloud_provider_by_slug` bails BEFORE any
|
||||
// provider HTTP call when a `<slug>` provider string carries no
|
||||
// model and the `cloud_providers` entry has no `default_model`:
|
||||
// "[chat-factory] no model configured: role '<r>' resolved to an
|
||||
// empty model id for slug '<s>'. Include a model in the provider
|
||||
// string (e.g. '<s>:<model-id>') or set default_model …".
|
||||
// User-state — the remediation is "add/pick a model in Settings →
|
||||
// LLM", which the user-facing copy surfaces; Sentry has no
|
||||
// remediation. Anchored on the role/slug-interpolation-free
|
||||
// substring, which is also `factory::NO_MODEL_CONFIGURED_ANCHOR`; a
|
||||
// round-trip test in `factory_tests.rs` couples the two so a
|
||||
// wording drift fails CI instead of silently re-flooding Sentry.
|
||||
"resolved to an empty model id",
|
||||
// TAURI-RUST-2G (~2684 events) / TAURI-RUST-2F (~950 events) —
|
||||
// thinking-mode model (DeepSeek-R1 / Moonshot K2-thinking on
|
||||
// `provider=cloud` custom_openai) rejects a follow-up turn that
|
||||
@@ -604,6 +619,36 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_chat_factory_empty_model_local_bail() {
|
||||
// TAURI-RUST-GKV — the #2784 factory guard
|
||||
// (`make_cloud_provider_by_slug`) catches the empty-model state
|
||||
// BEFORE the provider HTTP call (the local form of 4NM) and bails
|
||||
// with this body (role/slug interpolated). Verbatim from Sentry
|
||||
// issue 18482 (role='chat', slug='nvidia').
|
||||
let body = "[chat-factory] no model configured: role 'chat' resolved to an empty model id \
|
||||
for slug 'nvidia'. Include a model in the provider string (e.g. \
|
||||
'nvidia:<model-id>') or set default_model on the cloud_providers entry for \
|
||||
slug 'nvidia'.";
|
||||
assert!(
|
||||
is_provider_config_rejection_message(body),
|
||||
"TAURI-RUST-GKV empty-model bail must classify as provider config-rejection: {body:?}"
|
||||
);
|
||||
// Bare anchor on its own (the literal shared with
|
||||
// `factory::NO_MODEL_CONFIGURED_ANCHOR`).
|
||||
assert!(is_provider_config_rejection_message(
|
||||
"resolved to an empty model id"
|
||||
));
|
||||
// Negative: a near-miss model-resolution error that does NOT carry
|
||||
// the anchor (or any other phrase) must stay Sentry-actionable.
|
||||
assert!(
|
||||
!is_provider_config_rejection_message(
|
||||
"could not resolve the model registry for slug 'nvidia'"
|
||||
),
|
||||
"unrelated model-resolution error must not classify on the GKV anchor"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_model_helper_rejects_other_config_rejection_phrases() {
|
||||
// Polarity exception must stay narrow: other config-rejection
|
||||
|
||||
@@ -64,6 +64,16 @@ pub const CLAUDE_AGENT_SDK_PROVIDER: &str = "claude_agent_sdk";
|
||||
/// instead of silently routing through the managed OpenHuman backend.
|
||||
pub const BYOK_INCOMPLETE_SENTINEL: &str = "__byok_incomplete__";
|
||||
|
||||
/// Interpolation-free substring of the empty-model bail emitted by
|
||||
/// [`make_cloud_provider_by_slug`] when a `<slug>` provider string carries
|
||||
/// no model and the `cloud_providers` entry has no `default_model` (the
|
||||
/// #2784 guard). The Sentry-demotion + user-copy classifier
|
||||
/// [`super::is_provider_config_rejection_message`] keys on this exact literal,
|
||||
/// and a round-trip test in `factory_tests.rs` asserts the bail body still
|
||||
/// contains it — so a wording drift fails CI instead of silently re-flooding
|
||||
/// Sentry (TAURI-RUST-GKV).
|
||||
pub(crate) const NO_MODEL_CONFIGURED_ANCHOR: &str = "resolved to an empty model id";
|
||||
|
||||
fn is_abstract_tier_model(model: &str) -> bool {
|
||||
use crate::openhuman::config::{
|
||||
MODEL_AGENTIC_V1, MODEL_CHAT_V1, MODEL_CODING_V1, MODEL_REASONING_QUICK_V1,
|
||||
|
||||
@@ -453,6 +453,23 @@ fn cloud_provider_with_no_model_and_no_default_rejected() {
|
||||
msg.contains("nvidia-nim"),
|
||||
"error must name the slug; got: {msg}"
|
||||
);
|
||||
|
||||
// TAURI-RUST-GKV coupling — the SAME bail body that floods Sentry must:
|
||||
// (a) still contain the classifier anchor const, and
|
||||
// (b) be recognised by the shared config-rejection classifier
|
||||
// (which both demotes the Sentry event AND drives the actionable
|
||||
// user-facing copy in `classify_inference_error`).
|
||||
// If the bail wording drifts off the anchor, (a) fails; if the
|
||||
// classifier phrase drifts, (b) fails — CI catches either direction, so
|
||||
// the demotion can never silently regress into an error flood.
|
||||
assert!(
|
||||
msg.contains(super::NO_MODEL_CONFIGURED_ANCHOR),
|
||||
"bail body must contain NO_MODEL_CONFIGURED_ANCHOR; got: {msg}"
|
||||
);
|
||||
assert!(
|
||||
crate::openhuman::inference::provider::is_provider_config_rejection_message(&msg),
|
||||
"empty-model bail must classify as provider config-rejection: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user