Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 83 additions & 26 deletions src/openhuman/inference/provider/config_rejection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@
//! from a user OAuth/scope gap)
//! - `"not_found_error"` (J2 / J5 / J4 — litellm-compatible envelope
//! `type` field carrying "model 'X' not found")
//! - `"does not support tools"` / `"function calling is not supported"` /
//! `"unknown parameter: tools"` / `"unrecognized field \`tools\`"` /
//! `"unsupported parameter: tools"` (TAURI-RUST-4K7 — Ollama models such
//! as `gemma3:1b-it-qat` and `huihui_ai/deepseek-r1-abliterated:8b`
//! reject tool-enabled requests with HTTP 400. The compatible provider
//! already retries without tools, so the initial 400 is not a
//! bug — it's expected discovery of the model's capability boundary.
//! Sentry noise suppressed here; the retry path in `compatible.rs` runs
//! unchanged.)
//!
//! These are **deterministic user-configuration state**, not bugs the
//! maintainers can act on: the user pointed OpenHuman at a custom
Expand Down Expand Up @@ -148,39 +157,27 @@ 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.
"does not support tools",
"function calling is not supported",
"unknown parameter: tools",
"unrecognized field `tools`",
"unsupported parameter: tools",
// TAURI-RUST-4NM — nvidia-nim (and compatible providers) return
// `{"error":{"message":"model field is required","code":"missing_required_field"}}`
// when the request body contains an empty `"model":""` field.
// This is deterministic user-configuration state: the user's
// provider string had no model id and the config entry has no
// default_model. Factory now bails early, but guard the Sentry
// signal for in-flight requests from older configs.
// Note: match on the message phrase only — "missing_required_field"
// is too generic and would incorrectly suppress unrelated errors.
"model field is required",
// 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
// doesn't echo the prior assistant's `reasoning_content` field.
// Body shape (backtick-quoted JSON literal in the upstream body):
// `{"error":{"message":"The `reasoning_content` in the thinking
// mode must be passed back to the API.",...}}`. The
// provider-contract gap is on our side, but until the thinking-
// mode round-tripping ships in the inference layer, every affected
// turn fires a fresh Sentry event — and the UI already surfaces
// the actionable error to the user. Anchor on the unique
// `thinking mode must be passed back` substring so the match
// doesn't depend on the upstream's backtick-quoting around
// `reasoning_content` (some provider versions ship without them).
// thinking-mode model rejects a follow-up turn that doesn't echo
// the prior assistant's `reasoning_content` field.
"thinking mode must be passed back",
// TAURI-RUST-4XK (~649 events) — Ollama Cloud subscription gate.
// Body: `{"error":"this model requires a subscription, upgrade for
// access: https://ollama.com/upgrade (ref: <uuid>)"}` on a 403
// Forbidden from `compatible::OpenAiCompatibleProvider` with
// `name = "ollama"`. User-state: the model picked in Settings is
// a paid-tier Ollama Cloud model the user's account doesn't
// cover. The UI surfaces an actionable upgrade link in the
// remediation message itself.
"requires a subscription, upgrade for access",
];

Expand Down Expand Up @@ -391,4 +388,64 @@ mod tests {
);
}
}

/// 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 several tool-rejection phrases.
/// The compatible provider retries without tools, so the 400 is expected
/// capability-discovery rather than a product bug. These phrases must be
/// classified as config-rejections so Sentry is not flooded on every turn.
#[test]
fn detects_ollama_tool_unsupported_bodies() {
for (sentry_id, body) in [
(
"4K7-a",
r#"{"error":"gemma3:1b-it-qat does not support tools"}"#,
),
(
"4K7-b",
r#"{"error":"huihui_ai/deepseek-r1-abliterated:8b does not support tools"}"#,
),
(
"4K7-c",
r#"ollama streaming API error (400 Bad Request): {"error":"phi3:mini does not support tools"}"#,
),
(
"4K7-d",
r#"{"error":"function calling is not supported by this model"}"#,
),
(
"4K7-e",
r#"{"error":{"message":"unknown parameter: tools","type":"invalid_request_error"}}"#,
),
(
"4K7-f",
r#"{"error":"unrecognized field `tools` in request body"}"#,
),
(
"4K7-g",
r#"{"error":{"message":"unsupported parameter: tools","type":"invalid_request_error"}}"#,
),
] {
assert!(
is_provider_config_rejection_message(body),
"TAURI-RUST-{sentry_id} body must classify as provider config-rejection (tool-unsupported): {body:?}"
);
}
}

#[test]
fn detects_ollama_tool_unsupported_bodies_case_insensitive() {
// Ollama error messages should match regardless of casing.
for body in [
"Model 'gemma3:1b-it-qat' DOES NOT SUPPORT TOOLS",
"Function Calling Is Not Supported By This Model",
"Unknown Parameter: Tools",
] {
assert!(
is_provider_config_rejection_message(body),
"{body:?} must classify as config-rejection regardless of case"
);
}
}
}
Loading