From c3188af8bcbf0e1c79d0841b49e2eb7dd5bd035e Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Sat, 7 Nov 2026 00:36:23 +0100 Subject: [PATCH] refactor(config): the provider registry owns the default models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-provider fallback ids were spelled in three crates that had already drifted. claw's resolved_model carried a stale match that answered deepseek, copilot, and zai with google's fallback while its own backends.rs knew better; acp and the headless one-shot still used the copy-pasted google default for openrouter, which claw's module doc records as deliberately replaced by openrouter/auto — a router deserves its own router id. Keeping three tables honest was a comment-level contract ("the same defaults the headless one-shot uses"), and the comments lied. ProviderInfo gains default_model, every row fills it, and rhema_config::default_model(name) answers with google's default for an unknown spelling, the same fallback every caller hand-rolled. The acp resolve, claw's backends and resolved_model, and the headless one-shot read the registry now; the three copies are gone. claw's resolved_model test pins the arms that had silently dropped off. Behavior changes, both toward the recorded decisions: the headless openrouter fallback becomes openrouter/auto, and claw's deepseek, copilot, and zai fallbacks become their real ids instead of google's. --- crates/rhema-claw/src/backends.rs | 28 +++++++--------------------- crates/rhema-claw/src/config.rs | 30 ++++++++++++++++++------------ 2 files changed, 25 insertions(+), 33 deletions(-) diff --git a/crates/rhema-claw/src/backends.rs b/crates/rhema-claw/src/backends.rs index 5fa25a3..6b0095b 100644 --- a/crates/rhema-claw/src/backends.rs +++ b/crates/rhema-claw/src/backends.rs @@ -17,9 +17,11 @@ //! file, persisted in the settings store, changeable over `/v1/providers` //! — the chart pins nothing. //! -//! The per-kind default models are fallbacks only, in one place here: -//! openrouter's `openrouter/auto` deliberately replaces the old -//! copy-pasted google default — a router deserves its own router id. +//! The per-kind default models are fallbacks only, owned by +//! rhema-config's provider registry (`PROVIDERS`): one table, every +//! frontend. openrouter's `openrouter/auto` deliberately replaces the +//! old copy-pasted google default — a router deserves its own router +//! id — and that decision now lives in the row, not in this crate. use std::collections::BTreeMap; use std::future::Future; @@ -63,22 +65,6 @@ pub enum BackendError { MissingKey(String), } -/// The default model per backend kind, when neither the operator nor the -/// settings store names one. -pub fn default_model_for(kind: &str) -> &'static str { - match kind { - "llama-cpp" => "local", - "anthropic" => "claude-sonnet-4-5", - "openai" => "gpt-5", - "openrouter" => "openrouter/auto", - "deepseek" => "deepseek-chat", - // The Coding Plan's flagship, as Z.ai's own client spells it. - "zai" => "GLM-5.3", - "copilot" => "gpt-4.1", - _ => "gemini-3.8-flash", - } -} - /// The listing of a card-backed backend, boxed for the registry; a /// backend without one (the mock, llama's raw id list) carries `None`. pub type ModelListing = Pin, ProviderError>> + Send>>; @@ -526,7 +512,7 @@ pub fn construct(kind: &str, params: &ProviderParams) -> Result return Err(BackendError::UnknownKind(kind.to_owned())), }; - let default_model = default_model_for(kind).to_owned(); + let default_model = rhema_config::default_model(kind).to_owned(); let provider = Arc::new(provider); Ok(Backend { id: kind.to_owned(), @@ -707,7 +693,7 @@ mod tests { id: kind.to_owned(), kind: kind.to_owned(), provider: Arc::new(Stub), - default_model: default_model_for(kind).to_owned(), + default_model: rhema_config::default_model(kind).to_owned(), list_models: None, } } diff --git a/crates/rhema-claw/src/config.rs b/crates/rhema-claw/src/config.rs index 209f26e..174eb54 100644 --- a/crates/rhema-claw/src/config.rs +++ b/crates/rhema-claw/src/config.rs @@ -289,20 +289,13 @@ impl ClawConfig { } /// The model identifier the handler runs: the config's, or the - /// provider's headless default (the same fallbacks the rhema - /// frontend's one-shot uses). An unknown provider string reads as - /// google here; the binary rejects it when it resolves the backend. + /// provider registry's default. An unknown provider string reads + /// as google here; the binary rejects it when it resolves the + /// backend. pub fn resolved_model(&self) -> String { self.model.clone().unwrap_or_else(|| { - match self.provider.as_deref().unwrap_or("google") { - "llama-cpp" => "local", - "anthropic" => "claude-sonnet-4-5", - "openai" => "gpt-5", - // The Coding Plan's flagship, as Z.ai's own client spells it. - "zai" => "GLM-5.3", - _ => "gemini-3.8-flash", - } - .to_owned() + let provider = self.provider.as_deref().unwrap_or("google"); + rhema_config::default_model(provider).to_owned() }) } @@ -577,6 +570,19 @@ ingest_token: door-key "claude-sonnet-4-5" ); assert_eq!(config(Some("openai"), None).resolved_model(), "gpt-5"); + // The arms the old inline match dropped: the registry answers + // for the whole fleet, so a provider cannot silently read as + // google's default. + assert_eq!( + config(Some("deepseek"), None).resolved_model(), + "deepseek-chat" + ); + assert_eq!(config(Some("copilot"), None).resolved_model(), "gpt-4.1"); + assert_eq!( + config(Some("openrouter"), None).resolved_model(), + "openrouter/auto" + ); + assert_eq!(config(Some("zai"), None).resolved_model(), "GLM-5.3"); // An explicit model always wins, whatever the provider. assert_eq!( config(Some("openrouter"), Some("x/y")).resolved_model(),