From dbcd9827f60957e682928210e13b5a6915bf47ae Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Sat, 8 Aug 2026 14:25:00 +0300 Subject: [PATCH] fix(pipeline): capability filtering widens the chain instead of killing the request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenCode (like every coding agent) sends tool definitions with each request. The router rated the prompt low → the low-tier chain contained only the local model → the tools filter emptied the chain → hard failure: "No healthy model available that satisfies required capabilities", with the agent stuck in a retry loop. On an AUTO route whose filtered chain comes up empty, the pipeline now widens to any pool model satisfying the required capabilities — preferring the routing table's own medium→high order (an agentic request deserves a capable model), then the rest of the pool — and logs the widening. Pinned requests still fail loudly as before. Verified with OpenCode's exact request shape: cortiq-auto + tools at tier low now answers from deepseek in 1.2s instead of erroring. --- src/pipeline.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/pipeline.rs b/src/pipeline.rs index 741a0c0..e979372 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -313,6 +313,42 @@ impl Pipeline { } } } + // Capability widening: a tier chain built of tools-less local models + // must not kill an agent request. When the filtered chain is empty on + // an AUTO route, widen to any pool model that satisfies the required + // capabilities — preferring the routing table's own medium→high order + // (an agentic request is worth a capable model), then the rest of the + // pool. Coding agents (OpenCode, Cline, …) always send tools; without + // this every low-tier verdict was a hard failure. + if final_candidates.is_empty() { + let mut widened: Vec = Vec::new(); + for tier in ["medium", "high"] { + for id in live.routing.candidates(tier) { + if !widened.contains(&id) { + widened.push(id); + } + } + } + for id in live.registry.ids() { + if !widened.contains(id) { + widened.push(id.clone()); + } + } + for id in widened { + if let Some(prov) = live.registry.get(&id) { + if needs_tools && !prov.caps().tools { + continue; + } + final_candidates.push(id); + } + } + if !final_candidates.is_empty() { + tracing::info!( + chosen = %final_candidates[0], + "capability widening: tier chain had no model with the required capabilities" + ); + } + } if final_candidates.is_empty() { return Err(GatewayError::UpstreamUnavailable( "No healthy model available that satisfies required capabilities (e.g. tools)"