fix(config): an explicit [model.X] api_backend survives same-slug propagation

The slug-match step treated an entry whose api_backend equals the default
as unset and overwrote it with a same-slug sibling's backend, so
`api_backend = "chat_completions"` silently became messages/responses.
Config keys whose override sets api_backend are recorded and skipped by
the donor step; an unset backend still inherits.

Ported from upstream 1.0.22 (grok-build agent/config.rs
explicit_api_backend_keys).

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
This commit is contained in:
雷电芽衣 committed 2026-09-13 07:13:47 -04:00
1 parent 017a570d7d
commit 00a5e318bb
1 file changed
+49 -2
+49 -2
View File
@@ -2601,6 +2601,7 @@ pub(crate) fn resolve_model_list(
} }
resolved = prefetched; resolved = prefetched;
} }
let mut explicit_api_backend_keys = std::collections::HashSet::new();
for (key, model_override) in &cfg.config_models { for (key, model_override) in &cfg.config_models {
let had_base = resolved.contains_key(key); let had_base = resolved.contains_key(key);
let base = resolved.shift_remove(key); let base = resolved.shift_remove(key);
@@ -2617,6 +2618,9 @@ pub(crate) fn resolve_model_list(
); );
} }
} }
if model_override.api_backend.is_some() {
explicit_api_backend_keys.insert(key.as_str());
}
let entry = model_override.apply(key, base, &cfg.endpoints); let entry = model_override.apply(key, base, &cfg.endpoints);
tracing::debug!( tracing::debug!(
model_key = % key, base_url = % entry.info.base_url, has_api_key = entry model_key = % key, base_url = % entry.info.base_url, has_api_key = entry
@@ -2638,7 +2642,7 @@ pub(crate) fn resolve_model_list(
) )
}) })
.collect(); .collect();
for entry in resolved.values_mut() { for (key, entry) in resolved.iter_mut() {
if let Some((donor_cw, donor_backend)) = donors.get(&entry.info.model) { if let Some((donor_cw, donor_backend)) = donors.get(&entry.info.model) {
if entry.info.context_window.get() == default_cw { if entry.info.context_window.get() == default_cw {
tracing::debug!( tracing::debug!(
@@ -2648,7 +2652,9 @@ pub(crate) fn resolve_model_list(
); );
entry.info.context_window = *donor_cw; entry.info.context_window = *donor_cw;
} }
if entry.info.api_backend == ApiBackend::default() // An explicit `[model.X] api_backend` wins even when it is the default.
if !explicit_api_backend_keys.contains(key.as_str())
&& entry.info.api_backend == ApiBackend::default()
&& *donor_backend != ApiBackend::default() && *donor_backend != ApiBackend::default()
{ {
entry.info.api_backend.clone_from(donor_backend); entry.info.api_backend.clone_from(donor_backend);
@@ -9067,6 +9073,47 @@ default = "kigi-4.5"
sibling 'kigi-build' (same model slug), not stay at {default_cw}" sibling 'kigi-build' (same model slug), not stay at {default_cw}"
); );
} }
/// An explicit `[model.X] api_backend` survives a same-slug sibling even when it is the default.
#[test]
fn slug_propagation_keeps_an_explicit_default_api_backend() {
// Only kigi-messages has a non-default context window: the slug's single donor.
let default_cw = DEFAULT_CONTEXT_WINDOW;
let raw: toml::Value = toml::from_str(&format!(
r#"
[model.kigi-messages]
model = "kigi-4.5"
context_window = 500000
base_url = "https://test.example.com/v1"
api_backend = "messages"
[model.kigi-chat]
model = "kigi-4.5"
context_window = {default_cw}
base_url = "https://test.example.com/v1"
api_backend = "chat_completions"
[model.kigi-plain]
model = "kigi-4.5"
context_window = {default_cw}
base_url = "https://test.example.com/v1"
"#
))
.unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
let resolved = resolve_model_list(&cfg, None, &Default::default());
let backend = |key: &str| resolved.get(key).expect(key).info.api_backend.clone();
assert_eq!(
backend("kigi-chat"),
ApiBackend::ChatCompletions,
"explicit default backend must not be overwritten by the sibling"
);
assert_eq!(
backend("kigi-plain"),
ApiBackend::Messages,
"unset backend still inherits"
);
assert_eq!(backend("kigi-messages"), ApiBackend::Messages);
}
/// Slug propagation should carry over api_backend but NOT agent_type. /// Slug propagation should carry over api_backend but NOT agent_type.
#[test] #[test]
fn slug_propagation_inherits_api_backend_but_not_agent_type() { fn slug_propagation_inherits_api_backend_but_not_agent_type() {