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;
}
let mut explicit_api_backend_keys = std::collections::HashSet::new();
for (key, model_override) in &cfg.config_models {
let had_base = resolved.contains_key(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);
tracing::debug!(
model_key = % key, base_url = % entry.info.base_url, has_api_key = entry
@@ -2638,7 +2642,7 @@ pub(crate) fn resolve_model_list(
)
})
.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 entry.info.context_window.get() == default_cw {
tracing::debug!(
@@ -2648,7 +2652,9 @@ pub(crate) fn resolve_model_list(
);
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()
{
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}"
);
}
/// 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.
#[test]
fn slug_propagation_inherits_api_backend_but_not_agent_type() {