mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-10 17:10:18 -10:00
fix(models): a provider default resolves without a registry row (#6043, SHA-6443)
`deepseek-flash` is the Deepseek descriptor default and was absent from the ModelRegistry, so `codewhale model resolve deepseek-flash` failed with "not available from provider deepseek" while the model was the documented default. The same gap existed for every provider whose declared default never got a hand-maintained row. The descriptor owns its declared default: resolve now falls back to it — for the provider it declared, never borrowing another provider's model. Ollama keeps the old fail-closed path because its descriptor default is the placeholder "unknown" and the real default comes from the live catalog. New test `every_provider_default_resolves_for_its_own_provider` iterates ProviderKind::all(); pre-fix it failed with 11 rows (deepseek-flash, zen gpt-5.6, qianfan ernie, codex gpt-5.6, zen, telecomjs, modelstudio, edenai, concentrate, codewhale, custom). Also seeds `deepseek-flash` in the TUI model registry (the list documents that it mirrors DEFAULT_*_MODEL) and adds it to the DeepSeek web-search capability arm. Evidence: - before the fallback, the new test listed deepseek-flash first among 11 unresolved defaults; live `model resolve deepseek-flash` errored - cargo test -p codewhale-agent --locked test result: ok. 70 passed; 0 failed (was 2 passed; 1 failed on HEAD for explicit_deepseek_selection_retains_its_provider_owned_default, already red) - cargo test -p codewhale-cli --locked --test model_resolve_provenance test result: ok. 15 passed; 0 failed - cargo test -p codewhale-tui --lib --locked -- model_registry test result: ok. 10 passed; 0 failed - cargo test -p codewhale-config --locked -- capabilities test result: ok. 7 passed; 0 failed; 1 passed (two targets)
This commit is contained in:
1 parent
a591e2c690
commit
5ad9cdbbd0
3 files changed
+92
-18
No files matched your search
+85
-17
@@ -1541,6 +1541,22 @@ impl ModelRegistry {
|
||||
fallback_chain,
|
||||
});
|
||||
}
|
||||
// A provider's own declared default is available from that
|
||||
// provider by definition — the descriptor owns that fact (#6443:
|
||||
// `deepseek-flash` is the Deepseek default and resolved nowhere).
|
||||
// Registry rows canonicalize aliases and carry capability
|
||||
// metadata; they must not gate the name the provider declares.
|
||||
let declared_default = provider.provider().default_model();
|
||||
if !declared_default.trim().is_empty()
|
||||
&& name.trim().eq_ignore_ascii_case(declared_default.trim())
|
||||
{
|
||||
return Ok(ModelResolution {
|
||||
requested: Some(name.to_string()),
|
||||
resolved: Self::descriptor_default_model(provider, declared_default),
|
||||
used_fallback: false,
|
||||
fallback_chain,
|
||||
});
|
||||
}
|
||||
if !self.models.iter().any(|model| model.provider == provider) {
|
||||
return Err(ModelResolutionError::ProviderHasNoModels {
|
||||
provider,
|
||||
@@ -1554,12 +1570,6 @@ impl ModelRegistry {
|
||||
}
|
||||
|
||||
fallback_chain.push(format!("provider_default:{}", provider.as_str()));
|
||||
if !self.models.iter().any(|model| model.provider == provider) {
|
||||
return Err(ModelResolutionError::ProviderHasNoModels {
|
||||
provider,
|
||||
requested: None,
|
||||
});
|
||||
}
|
||||
let default_model = provider.provider().default_model();
|
||||
if let Some(model) = self
|
||||
.models
|
||||
@@ -1574,12 +1584,45 @@ impl ModelRegistry {
|
||||
fallback_chain,
|
||||
});
|
||||
}
|
||||
// Same rule as the explicit branch: the descriptor's declared default
|
||||
// resolves for its own provider even without a registry row (#6443).
|
||||
// Ollama is the exception: its descriptor default is the placeholder
|
||||
// `unknown`, and the real default comes from the live local catalog
|
||||
// (Y-2), so a placeholder must never resolve as a model.
|
||||
if !default_model.trim().is_empty() && provider != ProviderKind::Ollama {
|
||||
return Ok(ModelResolution {
|
||||
requested: None,
|
||||
resolved: Self::descriptor_default_model(provider, default_model),
|
||||
used_fallback: true,
|
||||
fallback_chain,
|
||||
});
|
||||
}
|
||||
if !self.models.iter().any(|model| model.provider == provider) {
|
||||
return Err(ModelResolutionError::ProviderHasNoModels {
|
||||
provider,
|
||||
requested: None,
|
||||
});
|
||||
}
|
||||
|
||||
Err(ModelResolutionError::ProviderDefaultUnavailable {
|
||||
provider,
|
||||
default_model: default_model.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The [`ModelInfo`] a provider's declared default resolves to when the
|
||||
/// registry carries no explicit row for it. The descriptor owns the
|
||||
/// identity; capability metadata stays conservative rather than
|
||||
/// fabricating a capability the registry never recorded.
|
||||
fn descriptor_default_model(provider: ProviderKind, id: &str) -> ModelInfo {
|
||||
ModelInfo {
|
||||
id: id.trim().to_string(),
|
||||
provider,
|
||||
aliases: Vec::new(),
|
||||
supports_tools: true,
|
||||
supports_reasoning: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize(value: &str) -> String {
|
||||
@@ -1825,7 +1868,10 @@ mod tests {
|
||||
let resolved = registry.resolve_ok(None, Some(ProviderKind::Deepseek));
|
||||
|
||||
assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
|
||||
assert_eq!(resolved.resolved.id, "deepseek-v4-pro");
|
||||
// The descriptor's declared default, not the first cloud row: the
|
||||
// registry's Deepseek rows start at v4-pro, and borrowing that here is
|
||||
// exactly the mismatch #6443 fixed.
|
||||
assert_eq!(resolved.resolved.id, "deepseek-flash");
|
||||
assert!(resolved.used_fallback);
|
||||
assert_eq!(resolved.fallback_chain, ["provider_default:deepseek"]);
|
||||
}
|
||||
@@ -1845,7 +1891,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_provider_default_row_fails_instead_of_borrowing_another_model() {
|
||||
fn provider_default_without_a_registry_row_resolves_to_its_own_id() {
|
||||
// SHA-6443: the descriptor owns its declared default. A registry that
|
||||
// carries unrelated rows must still resolve the provider's own
|
||||
// default — and must never borrow another provider's model.
|
||||
let registry = ModelRegistry::new(vec![ModelInfo {
|
||||
id: "not-the-openai-default".to_string(),
|
||||
provider: ProviderKind::Openai,
|
||||
@@ -1854,16 +1903,12 @@ mod tests {
|
||||
supports_reasoning: true,
|
||||
}]);
|
||||
|
||||
let error = registry
|
||||
let resolved = registry
|
||||
.resolve(None, Some(ProviderKind::Openai))
|
||||
.expect_err("a missing provider default must fail closed");
|
||||
assert_eq!(
|
||||
error,
|
||||
ModelResolutionError::ProviderDefaultUnavailable {
|
||||
provider: ProviderKind::Openai,
|
||||
default_model: "gpt-5.6".to_string(),
|
||||
}
|
||||
);
|
||||
.expect("the provider's declared default resolves for that provider");
|
||||
assert_eq!(resolved.resolved.id, "gpt-5.6");
|
||||
assert_eq!(resolved.resolved.provider, ProviderKind::Openai);
|
||||
assert!(resolved.used_fallback);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2801,4 +2846,27 @@ mod tests {
|
||||
);
|
||||
assert_eq!(model_family(""), ModelFamily::Inferencer);
|
||||
}
|
||||
|
||||
/// SHA-6443: a provider's declared default must be a model its own
|
||||
/// registry can resolve. A default the registry rejects fails a test
|
||||
/// here, not a founder's `model resolve`.
|
||||
#[test]
|
||||
fn every_provider_default_resolves_for_its_own_provider() {
|
||||
let registry = ModelRegistry::default();
|
||||
let mut failures = Vec::new();
|
||||
for kind in ProviderKind::all() {
|
||||
let default = kind.provider().default_model();
|
||||
if default.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
if let Err(error) = registry.resolve(Some(default), Some(*kind)) {
|
||||
failures.push(format!("{} ({kind:?}): {error}", default));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"provider defaults must resolve for their own provider:\n{}",
|
||||
failures.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,10 @@ pub(crate) fn documented_server_side_web_search(
|
||||
),
|
||||
"deepseek" => matches!(
|
||||
wire_model_id.as_str(),
|
||||
"deepseek-v4-flash" | "deepseek-v4-pro" | "deepseek-v4-flash-vision-exp"
|
||||
"deepseek-flash"
|
||||
| "deepseek-v4-flash"
|
||||
| "deepseek-v4-pro"
|
||||
| "deepseek-v4-flash-vision-exp"
|
||||
),
|
||||
"moonshot" => matches!(wire_model_id.as_str(), "kimi-k3" | "kimi-k2.6"),
|
||||
_ => false,
|
||||
|
||||
@@ -129,6 +129,9 @@ const SEED_MODEL_IDS: &[(&str, ModelProvider)] = &[
|
||||
// / Atlascloud / Novita / Fireworks / Siliconflow / SGLang / vLLM /
|
||||
// Huggingface / Together / Volcengine / WanjieArk / Ollama defaults) ---
|
||||
("deepseek-v4-pro", ModelProvider::DeepSeek),
|
||||
// The official V4.1 id and the config default: it must be seeded here so
|
||||
// this list keeps mirroring the DEFAULT_* constants it documents.
|
||||
("deepseek-flash", ModelProvider::DeepSeek),
|
||||
("deepseek-v4-flash", ModelProvider::DeepSeek),
|
||||
("deepseek-v4-flash-vision-exp", ModelProvider::DeepSeek),
|
||||
("deepseek-ai/deepseek-v4-pro", ModelProvider::DeepSeek),
|
||||
|
||||
Reference in new issue
Block a user