From 6b827e1b88a394c0dcbe9563acee76d58f8b7c7f Mon Sep 17 00:00:00 2001 From: xavix-yo Date: Fri, 7 Aug 2026 15:30:45 +0100 Subject: [PATCH] fix(llm): resolve catalog capabilities for reasoning-mode queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reasoning_mode_for (the add/edit form's reasoning-knob endpoint) evaluated rules against an empty capability set, so a declared provider whose modes are capability-gated never offered the knob; only id-glob rules (deepseek, openai, anthropic) could match. It now resolves the model's capabilities from the provider catalog first. DeclaredProvider also gains llm_model_info (find in the listing) — until now only anthropic/ollama overrode it, which is why a declared model's context_length never refreshed from the catalog either (maybe_refresh_meta always got None). And DeepInfra's entry learns a second mode: models tagged 'reasoning' but not 'reasoning_effort' (R1, DeepSeek-V4-Flash/Pro) accept the plain effort levels per DeepInfra's docs — graded steps stay behind the reasoning_effort tag. --- crates/skald-core/src/llm/manager.rs | 8 +++++++- crates/skald-core/src/llm/providers/declared.rs | 11 +++++++++++ providers.yaml | 8 ++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/crates/skald-core/src/llm/manager.rs b/crates/skald-core/src/llm/manager.rs index e606a45..f9ff1e2 100644 --- a/crates/skald-core/src/llm/manager.rs +++ b/crates/skald-core/src/llm/manager.rs @@ -356,7 +356,13 @@ impl LlmManager { pub async fn reasoning_mode_for(&self, provider_id: i64, model_id: &str) -> Option { let record = self.state.read().await.providers.get(&provider_id).cloned()?; let provider = self.registry.get(&record.provider)?; - provider.reasoning_mode(model_id, &[]) + // Capability-gated modes need the model's real capabilities: resolve + // them from the provider's catalog. Empty when unlisted — id-glob + // rules still match. + let caps = self.fetch_model_info(provider_id, model_id).await + .map(|m| m.capabilities) + .unwrap_or_default(); + provider.reasoning_mode(model_id, &caps) } pub async fn list_models_info(&self) -> Vec { diff --git a/crates/skald-core/src/llm/providers/declared.rs b/crates/skald-core/src/llm/providers/declared.rs index 2af369e..b4f3d8d 100644 --- a/crates/skald-core/src/llm/providers/declared.rs +++ b/crates/skald-core/src/llm/providers/declared.rs @@ -588,6 +588,17 @@ impl ApiProvider for DeclaredProvider { Ok(Some(self.list_models(record).await?)) } + async fn llm_model_info( + &self, + record: &LlmProviderRecord, + model_id: &str, + ) -> Result> { + if self.spec.models.is_none() { + return Ok(None); + } + Ok(self.list_models(record).await?.into_iter().find(|m| m.id == model_id)) + } + fn reasoning_mode(&self, model_id: &str, capabilities: &[String]) -> Option { let spec = self.spec.reasoning.as_ref()?; let rule = spec diff --git a/providers.yaml b/providers.yaml index 4430b07..f588d5f 100644 --- a/providers.yaml +++ b/providers.yaml @@ -190,13 +190,17 @@ providers: base_capabilities: [function_calling] reasoning: # Flat reasoning_effort (none/minimal/low/medium/high/xhigh/max); - # "none" disables reasoning where the model supports it. Models tagged - # `reasoning` but not `reasoning_effort` always think — no knob. + # "none" disables reasoning where the model supports it. request: { kind: effort, remap: { disabled: none } } modes: - when: { capability: reasoning_effort } values: [disabled, minimal, low, medium, high, xhigh, max] default: high + # Models tagged only `reasoning` (R1, DeepSeek-V4-…) accept the plain + # levels — graded steps are a `reasoning_effort`-tag affair. + - when: { capability: reasoning } + values: [disabled, low, medium, high] + default: high - id: lm_studio name: "LM Studio"