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:
Shaikh Taimoor
2026-05-28 20:50:55 -07:00
committed by GitHub
co-authored by Taimoor
parent 1fdb3f76b0
commit dad5f42a38
3 changed files with 101 additions and 8 deletions
@@ -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-4K7Ollama 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-4NMnvidia-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
+1 -1
View File
@@ -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();