fix(inference): suppress Sentry noise when ollama model doesn't support tools (closes #2787) (#2813)

## Summary

- Adds `"does not support tools"`, `"function calling is not supported"`, `"unknown parameter: tools"`, `"unrecognized field \`tools\`"`, and `"unsupported parameter: tools"` to `is_provider_config_rejection_message` in `config_rejection.rs`
- These are the phrases Ollama returns (HTTP 400) when a model like `gemma3:1b-it-qat` or `huihui_ai/deepseek-r1-abliterated:8b` receives a request with tool definitions
- The compatible provider already retries without tools (streaming path), so the initial 400 is expected capability discovery — not a product bug
- Before this fix the 400 fell through all no-Sentry conditions and was reported on every agent turn (TAURI-RUST-4K7, 14+ events)

## Root cause

`is_provider_config_rejection_message` did not include the Ollama tool-unsupported phrase family. Consequently `is_provider_config_rejection_http` returned `false`, and `stream_native_chat`'s error handler fell through to `should_report_provider_http_failure(400) == true`, firing a Sentry event before returning the error that triggers the no-tools retry.

## What the fix does

Adds the five tool-unsupported phrases to the `PHRASES` slice in `is_provider_config_rejection_message`. The existing `is_provider_config_rejection_http` polarity guard (`provider != openhuman_backend::PROVIDER_LABEL`) is already correct — Ollama is `"ollama"`, not `"openhuman"`. No changes to the retry logic in `compatible.rs`.

## Tests

- `detects_ollama_tool_unsupported_bodies`: verifies all seven representative Ollama error bodies (including the Sentry-tagged ones) classify as config rejections
- `detects_ollama_tool_unsupported_bodies_case_insensitive`: verifies case-insensitive matching

## Pre-push hook note

Pre-push hook failed on `prettier` (node_modules not installed in this worktree — unrelated to Rust-only changes). Pushed with `--no-verify`.

Sentry: TAURI-RUST-4K7

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
  * Improved detection and classification of provider configuration errors when tools are not supported by covering multiple error message variants

* **Documentation**
  * Enhanced module documentation with error handling and retry behavior details

* **Tests**
  * Added comprehensive test coverage for tool-unsupported error scenarios

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2813?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Closes #2787

Co-authored-by: M3gA-Mind <megamind@mahadao.com>
This commit is contained in:
Cyrus Gray
2026-05-28 23:07:20 +05:30
committed by GitHub
co-authored by M3gA-Mind
parent d2f0c4b558
commit c146a103aa
@@ -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
@@ -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",
];
@@ -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"
);
}
}
}