From 08a8c8160e6c9717f2f9188af8ddcfb49897abf7 Mon Sep 17 00:00:00 2001 From: oxoxDev <164490987+oxoxDev@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:39:07 +0530 Subject: [PATCH] fix(inference): demote OpenRouter no-tool-use-endpoint 404 + surface it in Subconscious (#3996) (#4003) --- .../inference/provider/config_rejection.rs | 19 +++++++++++ src/openhuman/subconscious/engine.rs | 34 +++++++++++++++++++ src/openhuman/subconscious/engine_tests.rs | 30 ++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/src/openhuman/inference/provider/config_rejection.rs b/src/openhuman/inference/provider/config_rejection.rs index 4765a03ee..17ba2333c 100644 --- a/src/openhuman/inference/provider/config_rejection.rs +++ b/src/openhuman/inference/provider/config_rejection.rs @@ -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 \"\". + // ..."}}`. 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 diff --git a/src/openhuman/subconscious/engine.rs b/src/openhuman/subconscious/engine.rs index f9ba16f94..5ff2bc2d3 100644 --- a/src/openhuman/subconscious/engine.rs +++ b/src/openhuman/subconscious/engine.rs @@ -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 (` 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 diff --git a/src/openhuman/subconscious/engine_tests.rs b/src/openhuman/subconscious/engine_tests.rs index d9d98285a..c21daeaa7 100644 --- a/src/openhuman/subconscious/engine_tests.rs +++ b/src/openhuman/subconscious/engine_tests.rs @@ -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")); +}