refactor(config): the provider registry owns the default models

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.
This commit is contained in:
vhaudiquet committed 2026-11-08 17:53:04 +01:00
1 parent df4cf87a20
commit c3188af8bc
2 files changed
+25 -33

No files matched your search

+7 -21
View File
@@ -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<Box<dyn Future<Output = Result<Vec<ModelCard>, ProviderError>> + Send>>;
@@ -526,7 +512,7 @@ pub fn construct(kind: &str, params: &ProviderParams) -> Result<Backend, Backend
}
_ => 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,
}
}
+18 -12
View File
@@ -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(),