From eb86d7ec268dc34280336cb193e5a7fe265f2b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 29 Sep 2026 16:10:41 -0400 Subject: [PATCH] fix(shell): turns read the auth scheme and BYOK status from the live catalog reconstruct_full_config and the subagent spawn resolved both from a disk reload that never holds fetched entries, so a fetched Messages model (anthropic, minimax) went out with Authorization: Bearer instead of x-api-key. --- crates/codegen/kigi-shell/src/agent/config.rs | 23 +++++++++++++++++++ .../kigi-shell/src/agent/subagent/mod.rs | 14 ++++++++--- .../session/acp_session_impl/sampler_turn.rs | 14 ++++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/crates/codegen/kigi-shell/src/agent/config.rs b/crates/codegen/kigi-shell/src/agent/config.rs index d1f36e8..0626a95 100644 --- a/crates/codegen/kigi-shell/src/agent/config.rs +++ b/crates/codegen/kigi-shell/src/agent/config.rs @@ -3960,6 +3960,18 @@ pub fn resolve_model_auth_facts(model_id: &str) -> ModelAuthFacts { }, }) } +/// Auth facts of a catalog entry the caller already holds. A fetched entry +/// exists only in the live catalog, never in a disk reload. +pub(crate) fn auth_facts_of(entry: &ModelEntry) -> ModelAuthFacts { + ModelAuthFacts { + byok: if entry.has_own_credentials() { + ModelByok::Byok + } else { + ModelByok::NotByok + }, + auth_scheme: entry.info().auth_scheme, + } +} fn byok_from_lookup(lookup: &ModelLookup) -> ModelByok { match lookup { ModelLookup::ConfigUnavailable => ModelByok::Unknown, @@ -5274,6 +5286,17 @@ reasoning_effort = "low" auth_scheme: Default::default(), } } + /// A fetched entry's scheme and BYOK status come from the entry itself. + #[test] + fn auth_facts_come_from_the_held_entry() { + let mut model = test_model_entry("claude-x", "https://gw.example/v1", None, None, None); + model.info.auth_scheme = AuthScheme::XApiKey; + let facts = auth_facts_of(&model); + assert_eq!(facts.auth_scheme, AuthScheme::XApiKey); + assert_eq!(facts.byok, ModelByok::NotByok); + model.api_key = Some("sk-own".into()); + assert_eq!(auth_facts_of(&model).byok, ModelByok::Byok); + } #[test] fn x_api_key_auth_scheme_flows_from_config_to_sampler() { let mut model = test_model_entry( diff --git a/crates/codegen/kigi-shell/src/agent/subagent/mod.rs b/crates/codegen/kigi-shell/src/agent/subagent/mod.rs index 20047b2..fc8b47f 100644 --- a/crates/codegen/kigi-shell/src/agent/subagent/mod.rs +++ b/crates/codegen/kigi-shell/src/agent/subagent/mod.rs @@ -897,9 +897,17 @@ async fn read_parent_sampling_config( creds.alpha_test_key.as_deref(), &cfg.base_url, ); - let auth_scheme = crate::agent::config::try_resolve_model_credentials(&cfg.model, None) - .map(|r| r.auth_scheme) - .unwrap_or_default(); + let auth_scheme = crate::agent::models::entry_for_slug( + &ctx.available_models, + Some(ctx.model_id.0.as_ref()), + &cfg.model, + ) + .map(|entry| crate::agent::config::auth_facts_of(entry).auth_scheme) + .or_else(|| { + crate::agent::config::try_resolve_model_credentials(&cfg.model, None) + .map(|r| r.auth_scheme) + }) + .unwrap_or_default(); // Claude Pro/Max OAuth Messages adaptation inherits from the parent // model's platform (claude-pro-max → true); every other platform, // and BYOK, → false, so the API-key paths stay byte-identical. diff --git a/crates/codegen/kigi-shell/src/session/acp_session_impl/sampler_turn.rs b/crates/codegen/kigi-shell/src/session/acp_session_impl/sampler_turn.rs index 266d847..6388c2b 100644 --- a/crates/codegen/kigi-shell/src/session/acp_session_impl/sampler_turn.rs +++ b/crates/codegen/kigi-shell/src/session/acp_session_impl/sampler_turn.rs @@ -264,6 +264,16 @@ impl SessionActor { fn aux_model_auth_facts(&self, model_id: &str) -> crate::agent::config::ModelAuthFacts { self.resolve_auth_facts(model_id, false) } + /// Facts from the live catalog entry the session samples against. + fn live_auth_facts(&self, model_id: &str) -> Option { + let models = self.models_manager.models(); + crate::agent::models::entry_for_slug( + &models, + self.selected_catalog_key().as_deref(), + model_id, + ) + .map(crate::agent::config::auth_facts_of) + } /// Shared body of [`Self::model_auth_facts`] / [`Self::aux_model_auth_facts`]. /// `memoize` is the ONLY difference, so the two can never resolve differently. fn resolve_auth_facts( @@ -278,7 +288,9 @@ impl SessionActor { { return *facts; } - let fresh = crate::agent::config::resolve_model_auth_facts(model_id); + let fresh = self + .live_auth_facts(model_id) + .unwrap_or_else(|| crate::agent::config::resolve_model_auth_facts(model_id)); if fresh.byok == ModelByok::Unknown { if let Some((cached_id, facts)) = self.model_auth_facts.borrow().as_ref() && cached_id == model_id