fix(inference): demote OpenRouter no-tool-use-endpoint 404 + surface it in Subconscious (#3996) (#4003)

This commit is contained in:
oxoxDev
2026-06-24 10:09:07 -07:00
committed by GitHub
parent 4764493b7d
commit 08a8c8160e
3 changed files with 83 additions and 0 deletions
@@ -251,6 +251,18 @@ pub fn is_provider_config_rejection_message(body: &str) -> bool {
// harmless (`.any()` short-circuits) and kept so each Sentry
// family stays self-documenting.
"does not support tools",
// TAURI-RUST-ADC (~5.9k events / 10 users) — OpenRouter's
// *router-level* phrasing of the same tool-capability user-state.
// When the picked model has no provider endpoint that supports tool
// calling, OpenRouter returns a 404 with `{"error":{"message":"No
// endpoints found that support tool use. Try disabling \"<tool>\".
// ..."}}`. The wording differs from the direct-provider `does not
// support tools` body above ("tool use" vs "tools", prefixed with
// "No endpoints found"), so it needs its own anchor. Same user-state
// class: pick a tool-capable model. Surfaced most often by the
// autonomous Subconscious loop, which additionally halts on this via
// its own capability breaker (`subconscious/engine.rs`).
"no endpoints found that support tool use",
// TAURI-RUST-4P6 (~36.6k events / 2 users) — user picked an
// *embedding* model (Ollama `bge-m3:latest`, OpenHuman's default
// memory-tree embed model) as their chat model. Ollama rejects every
@@ -367,6 +379,13 @@ mod tests {
"J4",
r#"custom_openai streaming API error (404 Not Found): {"error":{"message":"model 'llama3.3' not found","type":"not_found_error","param":null,"code":null}}"#,
),
// TAURI-RUST-ADC — OpenRouter router-level "no tool-use endpoint"
// 404, surfaced by the autonomous Subconscious loop on a
// content-safety model that supports no tools.
(
"ADC",
r#"openrouter API error (404 Not Found): {"error":{"message":"No endpoints found that support tool use. Try disabling \"spawn_async_subagent\". To learn more about provider routing, visit: https://openrouter.ai/docs/guides/routing/provider-selection"}}"#,
),
// TAURI-RUST-4NM — nvidia-nim (and compatible providers) return
// this body when the request body has an empty `"model":""`.
// This is user-configuration state: the provider string had no
+34
View File
@@ -34,6 +34,13 @@ const TICK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30 * 60
/// Per-tool-call timeout injected into the agent config.
const TOOL_CALL_TIMEOUT_SECS: u64 = 5 * 60;
/// Actionable reason surfaced (via `SubconsciousStatus.provider_unavailable_reason`)
/// when a subconscious tick fails because the configured chat model has no
/// tool-use endpoint. The subconscious turn is inherently tool-bearing (it
/// maintains its scratchpad through tools), so a tool-incapable model can never
/// satisfy a tick — this tells the user how to recover. See TAURI-RUST-ADC.
const TOOL_UNSUPPORTED_REASON: &str = "The selected chat model has no tool-use endpoint, so Subconscious can't run. Pick a tool-capable model in Settings > AI.";
/// Pick the `TrustedAutomationSource` variant for a subconscious tick.
///
/// Extracted from the engine's `run_agent` body so the
@@ -285,6 +292,21 @@ impl SubconsciousEngine {
state.total_ticks += 1;
if agent_failed {
state.consecutive_failures += 1;
// Surface an actionable reason when the failure is a permanent
// tool-capability error: the subconscious turn is inherently
// tool-bearing, so the configured chat model can never satisfy it
// and the user must pick a tool-capable model. The hard Sentry
// flood from this error is suppressed at the provider classifier
// (is_provider_config_rejection_message, TAURI-RUST-ADC); here we
// only make the cause visible in Subconscious status.
if let Err(e) = &agent_result {
if is_tool_capability_error(e) {
info!(
"[subconscious] configured chat model has no tool-use endpoint — Subconscious can't run until the model changes (TAURI-RUST-ADC)"
);
state.provider_unavailable_reason = Some(TOOL_UNSUPPORTED_REASON.to_string());
}
}
} else {
state.consecutive_failures = 0;
state.last_tick_at = tick_at;
@@ -464,6 +486,18 @@ fn resolve_subconscious_route(config: &Config) -> SubconsciousProviderRoute {
}
}
/// True when an agent-run error means the configured chat model can't do tool
/// calls at all — a permanent, user-actionable condition (pick a tool-capable
/// model). Matches both the direct-provider body (`<model> does not support
/// tools`) and OpenRouter's router-level phrasing (`No endpoints found that
/// support tool use`, TAURI-RUST-ADC). Kept narrow to tool capability so an
/// unrelated provider error (auth, billing, rate-limit) is not misread as one.
fn is_tool_capability_error(msg: &str) -> bool {
let lower = msg.to_ascii_lowercase();
lower.contains("no endpoints found that support tool use")
|| lower.contains("does not support tools")
}
// ── Pre-LLM memory retrieval ────────────────────────────────────────────────
/// Query the memory tree using scratchpad entries as context, returning
@@ -18,3 +18,33 @@ fn tick_origin_with_external_sync_chunk_uses_tainted_source() {
TrustedAutomationSource::SubconsciousTainted
));
}
// ── Tool-capability error detection (TAURI-RUST-ADC) ────────────────────
#[test]
fn tool_capability_error_matches_openrouter_and_direct_bodies() {
// OpenRouter router-level 404 (the reported ADC body).
assert!(is_tool_capability_error(
r#"agent run: openrouter API error (404 Not Found): {"error":{"message":"No endpoints found that support tool use. Try disabling \"spawn_async_subagent\"."}}"#
));
// Direct-provider "does not support tools" phrasing (TAURI-RUST-35 family).
assert!(is_tool_capability_error(
r#"agent run: cloud API error: {"error":{"message":"qwen2:0.5b does not support tools"}}"#
));
// Case-insensitive.
assert!(is_tool_capability_error(
"NO ENDPOINTS FOUND THAT SUPPORT TOOL USE"
));
}
#[test]
fn tool_capability_error_ignores_unrelated_failures() {
// A different 404, an auth wall, and a generic timeout must NOT match.
assert!(!is_tool_capability_error(
r#"agent run: openrouter API error (404 Not Found): {"error":{"message":"model 'llama3.3' not found"}}"#
));
assert!(!is_tool_capability_error(
"agent run: Backend returned 401 Unauthorized: Invalid token"
));
assert!(!is_tool_capability_error("agent run: request timed out"));
}