mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(inference): fail-fast on empty model for cloud providers, demote nvidia-nim Sentry noise (#2811)
Co-authored-by: Taimoor <astikkosapparel009@gmail.com>
This commit is contained in:
co-authored by
Taimoor
parent
1fdb3f76b0
commit
dad5f42a38
@@ -171,17 +171,44 @@ pub fn is_provider_config_rejection_message(body: &str) -> bool {
|
||||
// this is the `type` field used by litellm/Anthropic-style
|
||||
// envelopes for the same class of user-state error.
|
||||
"not_found_error",
|
||||
// TAURI-RUST-4K7 — Ollama models that don't support tool calling
|
||||
// (e.g. gemma3:1b-it-qat, huihui_ai/deepseek-r1-abliterated:8b)
|
||||
// return HTTP 400 with one of these phrases. The compatible
|
||||
// provider (`compatible.rs`) detects the error and retries
|
||||
// without tools, so the 400 is expected capability-discovery
|
||||
// rather than a product bug. Suppress Sentry to avoid noise from
|
||||
// the first-attempt rejection that precedes the successful retry.
|
||||
// TAURI-RUST-4NM — nvidia-nim (and some other providers) return
|
||||
// `{"error":{"message":"model field is required","type":"invalid_request_error","code":"missing_required_field"}}`
|
||||
// when the `model` key is absent or empty in the request body.
|
||||
// This is a user-configuration error (provider string has no model
|
||||
// component, e.g. `nvidia-nim:` with empty model), not a product
|
||||
// regression. Demote from Sentry; the factory now validates this
|
||||
// up-front so in practice this phrase should no longer appear.
|
||||
"model field is required",
|
||||
// TAURI-RUST-4XK — Ollama 403 when the requested model requires a
|
||||
// paid Ollama subscription. Body carries the upgrade URL. User must
|
||||
// switch to a free model or upgrade their Ollama account.
|
||||
"requires a subscription",
|
||||
// TAURI-RUST-2G / TAURI-RUST-2F — DeepSeek / compatible providers
|
||||
// that use extended thinking reject tool-call turns when the
|
||||
// `reasoning_content` block from a prior assistant turn is not
|
||||
// threaded back. This is user-config state (model requires the
|
||||
// caller to replay the thinking block; the frontend replay logic in
|
||||
// `turn.rs` handles it for subsequent turns, so the first-turn 400
|
||||
// is expected capability-discovery, not a regression).
|
||||
"in the thinking mode must be passed back",
|
||||
// TAURI-RUST-35 / TAURI-RUST-4K7 / TAURI-RUST-4Z0 — Ollama models
|
||||
// (e.g. gemma3, phi3, deepseek-r1) that do not support function
|
||||
// calling return HTTP 400 with this phrase. The compatible provider
|
||||
// retries without tools on 400, so the initial rejection is expected
|
||||
// capability-discovery. Sentry noise suppressed here.
|
||||
"does not support tools",
|
||||
// TAURI-RUST-4K7-d — alternative phrasing used by some Ollama model
|
||||
// versions for the same tool-unsupported condition.
|
||||
"function calling is not supported",
|
||||
// TAURI-RUST-4K7-e — litellm / OpenAI-compatible proxies reject the
|
||||
// `tools` key in the request body when the backing model does not
|
||||
// support tool use (e.g. local Ollama via LiteLLM gateway).
|
||||
"unknown parameter: tools",
|
||||
// TAURI-RUST-4K7-f — Ollama native API surface rejects the field
|
||||
// outright when the model has no function-calling capability.
|
||||
"unrecognized field `tools`",
|
||||
// TAURI-RUST-4K7-g — another litellm / proxy variant of the same
|
||||
// tool-unsupported condition.
|
||||
"unsupported parameter: tools",
|
||||
// TAURI-RUST-4NM — nvidia-nim (and compatible providers) return
|
||||
// `{"error":{"message":"model field is required","code":"missing_required_field"}}`
|
||||
@@ -565,6 +592,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_nvidia_nim_missing_model_body() {
|
||||
// TAURI-RUST-4NM — nvidia-nim rejects requests with model="" with
|
||||
// `{"error":{"message":"model field is required",...}}`.
|
||||
let body = r#"nvidia-nim API error (400 Bad Request): {"error":{"message":"model field is required","type":"invalid_request_error","code":"missing_required_field"}}"#;
|
||||
assert!(
|
||||
is_provider_config_rejection_message(body),
|
||||
"TAURI-RUST-4NM body must classify as provider config-rejection: {body:?}"
|
||||
);
|
||||
// Also verify the bare phrase on its own (defense-in-depth path).
|
||||
assert!(is_provider_config_rejection_message(
|
||||
"model field is required"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_model_helper_rejects_other_config_rejection_phrases() {
|
||||
// Polarity exception must stay narrow: other config-rejection
|
||||
|
||||
@@ -788,7 +788,7 @@ fn make_cloud_provider_by_slug(
|
||||
slug,
|
||||
);
|
||||
anyhow::bail!(
|
||||
"[chat-factory] role '{}' resolved to an empty model id for slug '{}'. \
|
||||
"[chat-factory] no model configured: role '{}' resolved to an empty model id for slug '{}'. \
|
||||
Include a model in the provider string (e.g. '{slug}:<model-id>') or \
|
||||
set default_model on the cloud_providers entry for slug '{slug}'.",
|
||||
role,
|
||||
|
||||
@@ -369,6 +369,57 @@ fn empty_model_in_ollama_rejected() {
|
||||
assert!(err.to_string().contains("empty model"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloud_provider_with_no_model_and_no_default_rejected() {
|
||||
// TAURI-RUST-4NM — nvidia-nim (and others) reject `model=""` with
|
||||
// "model field is required". The factory must catch this up-front with
|
||||
// a clear, actionable message instead of leaking an empty model to the API.
|
||||
let mut config = Config::default();
|
||||
config.cloud_providers.push(CloudProviderCreds {
|
||||
id: "p_nim".to_string(),
|
||||
slug: "nvidia-nim".to_string(),
|
||||
label: "NVIDIA NIM".to_string(),
|
||||
endpoint: "https://integrate.api.nvidia.com/v1".to_string(),
|
||||
auth_style: AuthStyle::Bearer,
|
||||
default_model: None, // no fallback model configured
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let err = match create_chat_provider_from_string("reasoning", "nvidia-nim:", &config) {
|
||||
Ok(_) => panic!("empty model must fail"),
|
||||
Err(e) => e,
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("no model configured"),
|
||||
"expected 'no model configured' in error, got: {msg}"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("nvidia-nim"),
|
||||
"error must name the slug; got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloud_provider_default_model_used_when_model_part_is_empty() {
|
||||
// When provider string is "nvidia-nim:" (empty model) but the entry
|
||||
// has a default_model, the factory must use the default — not error.
|
||||
let mut config = Config::default();
|
||||
config.cloud_providers.push(CloudProviderCreds {
|
||||
id: "p_nim".to_string(),
|
||||
slug: "nvidia-nim".to_string(),
|
||||
label: "NVIDIA NIM".to_string(),
|
||||
endpoint: "https://integrate.api.nvidia.com/v1".to_string(),
|
||||
auth_style: AuthStyle::Bearer,
|
||||
default_model: Some("meta/llama-3.1-8b-instruct".to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let (_, model) = create_chat_provider_from_string("reasoning", "nvidia-nim:", &config)
|
||||
.expect("empty model with default_model must succeed");
|
||||
assert_eq!(model, "meta/llama-3.1-8b-instruct");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_slug_for_openai_gives_clear_error() {
|
||||
let config = Config::default();
|
||||
|
||||
Reference in New Issue
Block a user