From ea31fad18890804e6d81e4ef75e4e9fb3d044042 Mon Sep 17 00:00:00 2001 From: xavix-yo Date: Fri, 7 Aug 2026 14:55:58 +0100 Subject: [PATCH] fix(llm): send the provider model id on the wire, not the alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LlmManager keys its model registry by llm_models.name (the user-facing alias), and the kernel sent ModelHandle.id as the request's model field — so the alias, not llm_models.model_id, went on the wire. A model worked only while the alias was left equal to the model id; renaming it made every provider reject the call (DeepInfra 404 model_not_found, DeepSeek 400 invalid_request_error). ModelHandle gains an optional wire_id: the model identifier to put on the wire when the selector's id is a bookkeeping key. The kernel and the compaction summary call both send handle.wire_model(); SkaldSelector sets wire_id from LlmEntry.model (llm_models.model_id). Everything else keeps keying on the alias: the chat's model pin, health reporting, fallback exclusion and request logging are untouched. --- crates/agent-loop/src/compaction.rs | 2 +- crates/agent-loop/src/kernel.rs | 2 +- crates/agent-loop/src/model.rs | 19 ++++++++++++++++--- crates/agent-loop/src/testing.rs | 7 ++++--- .../skald-core/src/loop_adapters/selector.rs | 10 +++++++++- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/crates/agent-loop/src/compaction.rs b/crates/agent-loop/src/compaction.rs index 155c4d2..3f527a1 100644 --- a/crates/agent-loop/src/compaction.rs +++ b/crates/agent-loop/src/compaction.rs @@ -284,7 +284,7 @@ impl Compaction { let request = ModelRequest { messages: vec![json!({ "role": "user", "content": body })], tools: Vec::new(), - model: handle.id.clone(), + model: handle.wire_model().to_string(), max_tokens: None, temperature: self.temperature, request_id: uuid_like(), diff --git a/crates/agent-loop/src/kernel.rs b/crates/agent-loop/src/kernel.rs index 9498135..fa09eb6 100644 --- a/crates/agent-loop/src/kernel.rs +++ b/crates/agent-loop/src/kernel.rs @@ -149,7 +149,7 @@ pub(crate) async fn run( let req = ModelRequest { messages: messages.clone(), tools: defs.clone(), - model: handle.id.clone(), + model: handle.wire_model().to_string(), max_tokens: None, temperature: None, request_id: mint_request_id(), diff --git a/crates/agent-loop/src/model.rs b/crates/agent-loop/src/model.rs index 89e2b34..31c8575 100644 --- a/crates/agent-loop/src/model.rs +++ b/crates/agent-loop/src/model.rs @@ -262,6 +262,18 @@ pub struct ModelHandle { pub id: ModelId, pub model: Arc, pub info: ModelInfo, + /// Wire model name when it differs from `id`: a selector whose `id` is a + /// bookkeeping key (Skald: the user-facing alias keying its model + /// registry) sets this to the provider's API model id. `None` ⇒ `id` + /// goes on the wire. + pub wire_id: Option, +} + +impl ModelHandle { + /// The model identifier to put on the wire. + pub fn wire_model(&self) -> &str { + self.wire_id.as_deref().unwrap_or(&self.id) + } } // ── ModelHint ──────────────────────────────────────────────────────────────── @@ -347,9 +359,10 @@ pub trait NamedModel: Model + 'static { Self: Sized, { ModelHandle { - id: self.default_model().to_string(), - model: Arc::new(self), - info: ModelInfo::default(), + id: self.default_model().to_string(), + model: Arc::new(self), + info: ModelInfo::default(), + wire_id: None, } } } diff --git a/crates/agent-loop/src/testing.rs b/crates/agent-loop/src/testing.rs index 8a1f62a..776aacc 100644 --- a/crates/agent-loop/src/testing.rs +++ b/crates/agent-loop/src/testing.rs @@ -118,9 +118,10 @@ impl Model for FakeModel { /// `requests()` afterwards). pub fn handle(fake: &std::sync::Arc, id: &str) -> crate::model::ModelHandle { crate::model::ModelHandle { - id: id.to_string(), - model: fake.clone(), - info: crate::model::ModelInfo::default(), + id: id.to_string(), + model: fake.clone(), + info: crate::model::ModelInfo::default(), + wire_id: None, } } diff --git a/crates/skald-core/src/loop_adapters/selector.rs b/crates/skald-core/src/loop_adapters/selector.rs index cab927d..027b0ee 100644 --- a/crates/skald-core/src/loop_adapters/selector.rs +++ b/crates/skald-core/src/loop_adapters/selector.rs @@ -91,7 +91,12 @@ impl ModelSelector for SkaldSelector { }; let model = self.instrument(&name, &entry); Ok(ModelHandle { - id: name, + // `id` is the registry alias: health, fallback exclusion and the + // chat's model pin all key on it. `wire_id` is what the provider + // API must see (`llm_models.model_id`) — an alias renamed in the + // UI must never change the request's model field. + id: name, + wire_id: Some(entry.model.clone()), model, info: model_info_of(&entry), }) @@ -157,7 +162,10 @@ mod tests { let sel = SkaldSelector::new(manager, None); let h = sel.select(&ModelHint::name("weak-model"), &[]).await.unwrap(); + // The handle keys on the alias; the wire carries the provider model id. assert_eq!(h.id, "weak-model"); + assert_eq!(h.wire_id.as_deref(), Some("weak-id")); + assert_eq!(h.wire_model(), "weak-id"); assert!(sel.select(&ModelHint::name("nope"), &[]).await.is_err());