feat(providers): add xAI (Grok) + migrate house BYOK env to KIGI_API_KEY

13th provider (16th registry variant). Also reconciles a naming collision
the matrix flagged: this fork is house-branded "xai" (cf. xai.dev metadata,
KIGI_CODE_XAI_API_KEY legacy env), so XAI_API_KEY + method xai.api_key were
the GENERIC house BYOK, not x.ai/Grok. The provider table wants xai/XAI_API_KEY
for Grok.

Resolution (user-approved): XAI_API_KEY now keys the x.ai/Grok provider; the
house BYOK primary env moves to KIGI_API_KEY, keeping XAI_API_KEY and
KIGI_CODE_XAI_API_KEY as back-compat fallbacks (read_xai_api_key_env checks
KIGI_API_KEY first). The xai.api_key method id is unchanged (persisted-session
compat); the platform method id is the bare "xai", distinct from it.

xAI spec: api.x.ai/v1, Bearer, OpenAI listing + ChatCompletions, Passthrough
(docs confirm stream_options.include_usage accepted). /v1/models is minimal
(ids only) and requires auth, so it doubles as the key validator (401 on bad
key, no override) and metadata comes from models.dev enrichment. Live ids
match the models.dev "xai" keys byte-for-byte, so restrict_to_enriched keeps
the 5 tool-calling chat models (grok-4.5/4.3/4.20-0309-*/build-0.1) and drops
the grok-imagine-* generators + the non-tool multi-agent model. Snapshot
regenerated to include the xai provider (was stale; gen script already listed
it in TARGETS).

Env migration is comprehensive to avoid keying the xai platform (which would
trigger a live api.x.ai fetch) or leaving house-key reads stranded: routed
the trace CLI resolver + acp_agent/auth.json bridge + paste-key ext handler
through the new primary; moved all leader/pager/e2e harness setters to
KIGI_API_KEY; made every house-key isolation test unset KIGI_API_KEY too;
updated user-facing hints to name KIGI_API_KEY.

Tests: e2e proves enrichment-supplied context (wire carries none), non-vacuous
tool_call restriction, bare-id round-trip under xai/, Passthrough; validation
tests hit /models (401 reject, 200 accept); house_env_var_takes_precedence_over_xai
pins the new precedence. Registry at 16; picker 17 rows; 4 auth arrays + xai.

Review (16 findings, all fixed): caught a missed else-branch env clear in the
paste-key handler (would leak the house key past a clear) and a non-hermetic
credential-priority test; both fixed.
This commit is contained in:
2026-07-21 16:23:08 -04:00
parent 193d16f6d5
commit ebf11057f8
25 changed files with 297 additions and 56 deletions
File diff suppressed because one or more lines are too long
+42 -2
View File
@@ -597,6 +597,41 @@ const VERCEL_SPEC: PlatformSpec = PlatformSpec {
restrict_to_enriched: true, restrict_to_enriched: true,
}; };
pub const XAI_BASE_URL_ENV: &str = "KIGI_XAI_BASE_URL";
const XAI_SPEC: PlatformSpec = PlatformSpec {
id: "xai",
display_name: "xAI (Grok)",
base_url: BaseUrlSource::EnvOr {
env: XAI_BASE_URL_ENV,
default: "https://api.x.ai/v1",
},
uses_oauth: false,
allowed_model_prefixes: None,
// NOTE: `XAI_API_KEY` is ALSO read as a legacy fallback by the house BYOK
// path (`read_xai_api_key_env`), whose primary env is now `KIGI_API_KEY`.
// Here it is the x.ai/Grok provider key (its canonical ecosystem name).
api_key_envs: &["XAI_API_KEY"],
vendor: "xAI",
console_host: Some("console.x.ai"),
login_label: Some("xAI (Grok) (API key)"),
models_dev_id: Some("xai"),
// /v1/models is minimal (ids only; rich metadata lives on the non-standard
// /v1/language-models), so take context/limits from models.dev enrichment.
// The /v1/models ids match the models.dev "xai" keys byte-for-byte
// (grok-4.5, grok-4.20-0309-reasoning, ...); restrict to tool-calling chat
// models to drop the grok-imagine-* image/video generators.
wire_serves_metadata: false,
wire_api: PlatformWireApi::ChatCompletions,
listing: ListingDialect::OpenAi,
chat_compat: PlatformChatCompat::Passthrough,
key_header: PlatformKeyHeader::Bearer,
// /v1/models requires auth (401 without a key), so it doubles as the key
// validator; no separate public endpoint needed.
key_validation_path: None,
strip_listing_id_prefix: None,
restrict_to_enriched: true,
};
/// The platform registry. Platforms are compiled-in spec rows; there is no /// The platform registry. Platforms are compiled-in spec rows; there is no
/// dynamic provider registration (PRD F2). /// dynamic provider registration (PRD F2).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
@@ -631,12 +666,14 @@ pub enum PlatformId {
Nvidia, Nvidia,
/// Vercel AI Gateway (API key, wire-listed with models.dev enrichment). /// Vercel AI Gateway (API key, wire-listed with models.dev enrichment).
Vercel, Vercel,
/// xAI Grok platform (API key, OpenAI-compatible ChatCompletions).
Xai,
} }
impl PlatformId { impl PlatformId {
/// All platforms, in catalog precedence order: the subscription channel /// All platforms, in catalog precedence order: the subscription channel
/// first so "default model = first list item" favors it when present. /// first so "default model = first list item" favors it when present.
pub const ALL: [PlatformId; 15] = [ pub const ALL: [PlatformId; 16] = [
Self::KimiCode, Self::KimiCode,
Self::MoonshotCn, Self::MoonshotCn,
Self::MoonshotAi, Self::MoonshotAi,
@@ -652,6 +689,7 @@ impl PlatformId {
Self::Cerebras, Self::Cerebras,
Self::Nvidia, Self::Nvidia,
Self::Vercel, Self::Vercel,
Self::Xai,
]; ];
/// The registry row backing this platform (single source of per-platform /// The registry row backing this platform (single source of per-platform
@@ -673,6 +711,7 @@ impl PlatformId {
Self::Cerebras => &CEREBRAS_SPEC, Self::Cerebras => &CEREBRAS_SPEC,
Self::Nvidia => &NVIDIA_SPEC, Self::Nvidia => &NVIDIA_SPEC,
Self::Vercel => &VERCEL_SPEC, Self::Vercel => &VERCEL_SPEC,
Self::Xai => &XAI_SPEC,
} }
} }
@@ -1428,9 +1467,10 @@ mod tests {
PlatformId::Cerebras => 12, PlatformId::Cerebras => 12,
PlatformId::Nvidia => 13, PlatformId::Nvidia => 13,
PlatformId::Vercel => 14, PlatformId::Vercel => 14,
PlatformId::Xai => 15,
} }
} }
const VARIANT_COUNT: usize = 15; // update together with `ordinal` const VARIANT_COUNT: usize = 16; // update together with `ordinal`
let mut seen: Vec<usize> = PlatformId::ALL.iter().map(|&p| ordinal(p)).collect(); let mut seen: Vec<usize> = PlatformId::ALL.iter().map(|&p| ordinal(p)).collect();
seen.sort_unstable(); seen.sort_unstable();
seen.dedup(); seen.dedup();
@@ -97,7 +97,7 @@ impl ContentController {
// pins them to the mock so no PTY test can reach a live endpoint. // pins them to the mock so no PTY test can reach a live endpoint.
("KIGI_MOONSHOT_CN_BASE_URL".into(), self.url()), ("KIGI_MOONSHOT_CN_BASE_URL".into(), self.url()),
("KIGI_MOONSHOT_AI_BASE_URL".into(), self.url()), ("KIGI_MOONSHOT_AI_BASE_URL".into(), self.url()),
("XAI_API_KEY".into(), "test-key-for-ci".into()), ("KIGI_API_KEY".into(), "test-key-for-ci".into()),
("KIGI_TELEMETRY_ENABLED".into(), "false".into()), ("KIGI_TELEMETRY_ENABLED".into(), "false".into()),
("KIGI_FEEDBACK_ENABLED".into(), "false".into()), ("KIGI_FEEDBACK_ENABLED".into(), "false".into()),
("KIGI_TRACE_UPLOAD".into(), "false".into()), ("KIGI_TRACE_UPLOAD".into(), "false".into()),
@@ -287,7 +287,7 @@ mod tests {
assert_eq!(get("KIGI_API_BASE_URL"), Some(content.url())); assert_eq!(get("KIGI_API_BASE_URL"), Some(content.url()));
assert_eq!(get("KIGI_MOONSHOT_CN_BASE_URL"), Some(content.url())); assert_eq!(get("KIGI_MOONSHOT_CN_BASE_URL"), Some(content.url()));
assert_eq!(get("KIGI_MOONSHOT_AI_BASE_URL"), Some(content.url())); assert_eq!(get("KIGI_MOONSHOT_AI_BASE_URL"), Some(content.url()));
assert_eq!(get("XAI_API_KEY").as_deref(), Some("test-key-for-ci")); assert_eq!(get("KIGI_API_KEY").as_deref(), Some("test-key-for-ci"));
assert_eq!(get("KIGI_TELEMETRY_ENABLED").as_deref(), Some("false")); assert_eq!(get("KIGI_TELEMETRY_ENABLED").as_deref(), Some("false"));
assert_eq!(get("KIGI_FEEDBACK_ENABLED").as_deref(), Some("false")); assert_eq!(get("KIGI_FEEDBACK_ENABLED").as_deref(), Some("false"));
assert_eq!(get("KIGI_TRACE_UPLOAD").as_deref(), Some("false")); assert_eq!(get("KIGI_TRACE_UPLOAD").as_deref(), Some("false"));
@@ -96,11 +96,12 @@ pub fn seed_fake_oauth(content: &ContentController, user: &str) {
.expect("seed fake oauth auth.json"); .expect("seed fake oauth auth.json");
} }
/// [`ContentController::env_for_pager`] minus `XAI_API_KEY`, so the entry /// [`ContentController::env_for_pager`] minus the house BYOK key
/// written by [`seed_fake_oauth`] is the active credential. /// (`KIGI_API_KEY`), so the entry written by [`seed_fake_oauth`] is the active
/// credential.
pub fn oauth_env_for_pager(content: &ContentController) -> Vec<(String, String)> { pub fn oauth_env_for_pager(content: &ContentController) -> Vec<(String, String)> {
let mut env = content.env_for_pager(); let mut env = content.env_for_pager();
env.retain(|(k, _)| k != "XAI_API_KEY"); env.retain(|(k, _)| k != "KIGI_API_KEY");
env env
} }
@@ -19,24 +19,36 @@ pub(crate) fn new_shared_auth_method_id(initial: Option<acp::AuthMethodId>) -> S
)) ))
} }
/// Env var that, when set, advertises `xai.api_key` as a viable auth method. /// Primary env var that, when set, advertises `xai.api_key` as a viable auth
/// method. NOTE: `xai.api_key` is the *house* bring-your-own-key method (the
/// upstream product is house-branded "xai"), unrelated to the x.ai/Grok
/// provider. That collision is why the primary env moved here to `KIGI_API_KEY`
/// — `XAI_API_KEY` is now the x.ai/Grok provider key (see `XAI_SPEC`).
/// ///
/// Kept as a constant so test code and the production check stay in sync. /// Kept as a constant so test code and the production check stay in sync.
pub const HOUSE_API_KEY_ENV_VAR: &str = "KIGI_API_KEY";
/// Back-compat fallback env: `XAI_API_KEY` was the house BYOK key before it
/// became the x.ai/Grok provider key. Still honored so existing house-BYOK
/// deployments keep working (they share the key with the Grok provider).
pub const XAI_API_KEY_ENV_VAR: &str = "XAI_API_KEY"; pub const XAI_API_KEY_ENV_VAR: &str = "XAI_API_KEY";
/// Legacy env var name. Checked as a fallback when `XAI_API_KEY` is not set, /// Legacy env var name (pre-`XAI_API_KEY`). Checked last so the oldest
/// so existing deployments that use the old name keep working. /// deployments keep working.
pub const LEGACY_XAI_API_KEY_ENV_VAR: &str = "KIGI_CODE_XAI_API_KEY"; pub const LEGACY_XAI_API_KEY_ENV_VAR: &str = "KIGI_CODE_XAI_API_KEY";
/// Read the API key from the environment. /// Read the house BYOK API key from the environment.
/// ///
/// Checks `XAI_API_KEY` first, then falls back to the legacy /// Checks `KIGI_API_KEY` first, then the back-compat `XAI_API_KEY`, then the
/// `KIGI_CODE_XAI_API_KEY` for backward compatibility. /// legacy `KIGI_CODE_XAI_API_KEY`.
pub fn read_xai_api_key_env() -> Result<String, std::env::VarError> { pub fn read_xai_api_key_env() -> Result<String, std::env::VarError> {
std::env::var(XAI_API_KEY_ENV_VAR).or_else(|_| std::env::var(LEGACY_XAI_API_KEY_ENV_VAR)) std::env::var(HOUSE_API_KEY_ENV_VAR)
.or_else(|_| std::env::var(XAI_API_KEY_ENV_VAR))
.or_else(|_| std::env::var(LEGACY_XAI_API_KEY_ENV_VAR))
} }
/// Returns `true` if either `XAI_API_KEY` or `KIGI_CODE_XAI_API_KEY` is set. /// Returns `true` if any house BYOK env is set: `KIGI_API_KEY` (primary) or the
/// back-compat `XAI_API_KEY` / `KIGI_CODE_XAI_API_KEY`.
pub fn has_xai_api_key_env() -> bool { pub fn has_xai_api_key_env() -> bool {
read_xai_api_key_env().is_ok() read_xai_api_key_env().is_ok()
} }
@@ -271,7 +283,7 @@ pub fn session_token_auth_gate(
pub const AUTH_ERROR_SESSION_EXPIRED: &str = pub const AUTH_ERROR_SESSION_EXPIRED: &str =
"Session expired. Run `kigi login` to re-authenticate."; "Session expired. Run `kigi login` to re-authenticate.";
pub const AUTH_ERROR_API_KEY: &str = "Authentication failed. Run `kigi login`, set XAI_API_KEY, or add api_key to ~/.kigi/config.toml."; pub const AUTH_ERROR_API_KEY: &str = "Authentication failed. Run `kigi login`, set KIGI_API_KEY, or add api_key to ~/.kigi/config.toml.";
/// Next ACP method id when `cached_token` cannot proceed (missing / expired): /// Next ACP method id when `cached_token` cannot proceed (missing / expired):
/// prefer non-interactive `xai.api_key` when advertiseable, else the /// prefer non-interactive `xai.api_key` when advertiseable, else the
@@ -292,7 +304,7 @@ pub fn xai_api_key_auth_method() -> acp::AuthMethod {
"xai.api_key".to_string(), "xai.api_key".to_string(),
) )
.description(Some(format!( .description(Some(format!(
"{XAI_API_KEY_ENV_VAR} or api_key/env_key in config.toml" "{HOUSE_API_KEY_ENV_VAR} or api_key/env_key in config.toml"
))), ))),
) )
} }
@@ -607,7 +619,8 @@ mod tests {
"together", "together",
"cerebras", "cerebras",
"nvidia", "nvidia",
"vercel-ai-gateway" "vercel-ai-gateway",
"xai"
] ]
); );
assert_eq!(default_id(&built), Some(XAI_API_KEY_METHOD_ID)); assert_eq!(default_id(&built), Some(XAI_API_KEY_METHOD_ID));
@@ -645,7 +658,8 @@ mod tests {
"together", "together",
"cerebras", "cerebras",
"nvidia", "nvidia",
"vercel-ai-gateway" "vercel-ai-gateway",
"xai"
] ]
); );
assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID)); assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID));
@@ -676,7 +690,8 @@ mod tests {
"together", "together",
"cerebras", "cerebras",
"nvidia", "nvidia",
"vercel-ai-gateway" "vercel-ai-gateway",
"xai"
] ]
); );
assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID)); assert_eq!(default_id(&built), Some(CACHED_TOKEN_AUTH_METHOD_ID));
@@ -710,7 +725,8 @@ mod tests {
"together", "together",
"cerebras", "cerebras",
"nvidia", "nvidia",
"vercel-ai-gateway" "vercel-ai-gateway",
"xai"
] ]
); );
assert_eq!(default_id(&built), None); assert_eq!(default_id(&built), None);
@@ -759,21 +775,34 @@ mod tests {
#[test] #[test]
#[serial] #[serial]
fn legacy_env_var_fallback_advertises_xai_api_key() { fn legacy_env_var_fallback_advertises_xai_api_key() {
let _house = EnvGuard::unset(HOUSE_API_KEY_ENV_VAR);
let _unset = EnvGuard::unset(XAI_API_KEY_ENV_VAR); let _unset = EnvGuard::unset(XAI_API_KEY_ENV_VAR);
let _set = EnvGuard::set(LEGACY_XAI_API_KEY_ENV_VAR, "legacy-key"); let _set = EnvGuard::set(LEGACY_XAI_API_KEY_ENV_VAR, "legacy-key");
assert!(has_xai_api_key_env()); assert!(has_xai_api_key_env());
assert_eq!(read_xai_api_key_env().unwrap(), "legacy-key"); assert_eq!(read_xai_api_key_env().unwrap(), "legacy-key");
} }
/// The new env var takes precedence over the legacy one. /// `XAI_API_KEY` takes precedence over the older legacy env var.
#[test] #[test]
#[serial] #[serial]
fn new_env_var_takes_precedence_over_legacy() { fn xai_env_var_takes_precedence_over_legacy() {
let _house = EnvGuard::unset(HOUSE_API_KEY_ENV_VAR);
let _new = EnvGuard::set(XAI_API_KEY_ENV_VAR, "new-key"); let _new = EnvGuard::set(XAI_API_KEY_ENV_VAR, "new-key");
let _legacy = EnvGuard::set(LEGACY_XAI_API_KEY_ENV_VAR, "legacy-key"); let _legacy = EnvGuard::set(LEGACY_XAI_API_KEY_ENV_VAR, "legacy-key");
assert_eq!(read_xai_api_key_env().unwrap(), "new-key"); assert_eq!(read_xai_api_key_env().unwrap(), "new-key");
} }
/// After the migration, `KIGI_API_KEY` is the house BYOK primary and wins
/// over the back-compat `XAI_API_KEY` (now the x.ai/Grok provider key).
#[test]
#[serial]
fn house_env_var_takes_precedence_over_xai() {
let _house = EnvGuard::set(HOUSE_API_KEY_ENV_VAR, "house-key");
let _xai = EnvGuard::set(XAI_API_KEY_ENV_VAR, "grok-key");
let _legacy = EnvGuard::set(LEGACY_XAI_API_KEY_ENV_VAR, "legacy-key");
assert_eq!(read_xai_api_key_env().unwrap(), "house-key");
}
/// Moonshot authenticate with no configured key: actionable error naming /// Moonshot authenticate with no configured key: actionable error naming
/// the platform, the login screen, and the platform-scoped env var. No /// the platform, the login screen, and the platform-scoped env var. No
/// HTTP is attempted (`key: None` short-circuits). /// HTTP is attempted (`key: None` short-circuits).
@@ -931,4 +960,50 @@ mod tests {
.await .await
.expect("200 from /key must validate the key"); .expect("200 from /key must validate the key");
} }
/// xAI has no validation-path override: `/v1/models` itself requires auth
/// (401 without a valid key), so it doubles as the validator. A bad key is
/// rejected.
#[tokio::test]
#[serial]
async fn xai_validates_against_models_and_rejects_bad_key() {
use wiremock::matchers::{method, path};
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(method("GET"))
.and(path("/models"))
.respond_with(wiremock::ResponseTemplate::new(401))
.expect(1)
.mount(&server)
.await;
let _base = EnvGuard::set(kigi_models::XAI_BASE_URL_ENV, &server.uri());
let err = authenticate_platform_api_key(kigi_models::PlatformId::Xai, Some("xai-bad"))
.await
.expect_err("a 401 from /models must reject the key");
assert_eq!(
err.message,
"Invalid API key for xai \u{2014} check your key on console.x.ai"
);
}
/// A valid xAI key: `/models` returns 200 for the Bearer header → accepted.
#[tokio::test]
#[serial]
async fn xai_valid_key_succeeds_via_models() {
use wiremock::matchers::{header, method, path};
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(method("GET"))
.and(path("/models"))
.and(header("Authorization", "Bearer xai-good"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(serde_json::json!({ "data": [] })),
)
.expect(1)
.mount(&server)
.await;
let _base = EnvGuard::set(kigi_models::XAI_BASE_URL_ENV, &server.uri());
authenticate_platform_api_key(kigi_models::PlatformId::Xai, Some("xai-good"))
.await
.expect("200 from /models must validate the key");
}
} }
+13 -2
View File
@@ -4893,7 +4893,9 @@ reasoning_effort = "low"
#[test] #[test]
#[serial] #[serial]
fn resolve_credentials_empty_env_key_falls_through_to_global_key() { fn resolve_credentials_empty_env_key_falls_through_to_global_key() {
use crate::agent::auth_method::{LEGACY_XAI_API_KEY_ENV_VAR, XAI_API_KEY_ENV_VAR}; use crate::agent::auth_method::{
HOUSE_API_KEY_ENV_VAR, LEGACY_XAI_API_KEY_ENV_VAR, XAI_API_KEY_ENV_VAR,
};
use kigi_chat_state::AuthType; use kigi_chat_state::AuthType;
use kigi_test_support::EnvGuard; use kigi_test_support::EnvGuard;
let sentinel = "xai-global-sentinel-key"; let sentinel = "xai-global-sentinel-key";
@@ -4901,6 +4903,9 @@ reasoning_effort = "low"
let alias = "KIGI_TEST_EMPTY_ENV_GLOBAL_ALIAS"; let alias = "KIGI_TEST_EMPTY_ENV_GLOBAL_ALIAS";
let _primary = EnvGuard::set(primary, ""); let _primary = EnvGuard::set(primary, "");
let _alias = EnvGuard::set(alias, ""); let _alias = EnvGuard::set(alias, "");
// Unset the new house primary so the back-compat XAI_API_KEY is the
// global key under test (not shadowed by an ambient KIGI_API_KEY).
let _house = EnvGuard::unset(HOUSE_API_KEY_ENV_VAR);
let _global = EnvGuard::set(XAI_API_KEY_ENV_VAR, sentinel); let _global = EnvGuard::set(XAI_API_KEY_ENV_VAR, sentinel);
let _legacy = EnvGuard::unset(LEGACY_XAI_API_KEY_ENV_VAR); let _legacy = EnvGuard::unset(LEGACY_XAI_API_KEY_ENV_VAR);
let mut model = test_model_entry("m", "https://inference.example/v1", None, None, None); let mut model = test_model_entry("m", "https://inference.example/v1", None, None, None);
@@ -6454,7 +6459,13 @@ reasoning_effort = "low"
None, None,
None, None,
); );
unsafe { std::env::set_var("XAI_API_KEY", "env-key") }; // Clear the house primary + legacy so the XAI_API_KEY set below is the
// env key resolved by read_xai_api_key_env (KIGI_API_KEY wins otherwise).
unsafe {
std::env::remove_var("KIGI_API_KEY");
std::env::remove_var("KIGI_CODE_XAI_API_KEY");
std::env::set_var("XAI_API_KEY", "env-key");
}
let sampling = resolve_sampling(&model_with_key, Some("session-key")); let sampling = resolve_sampling(&model_with_key, Some("session-key"));
assert_eq!( assert_eq!(
sampling.api_key.as_deref(), sampling.api_key.as_deref(),
@@ -132,7 +132,7 @@ fn fetch_custom_endpoint_models_blocking(
.ok_or(std::env::VarError::NotPresent) .ok_or(std::env::VarError::NotPresent)
}) })
.map_err(|_| { .map_err(|_| {
BackendError::Auth("No API key for custom models endpoint. Set XAI_API_KEY.".into()) BackendError::Auth("No API key for custom models endpoint. Set KIGI_API_KEY.".into())
})?; })?;
let request = client let request = client
.get(&url) .get(&url)
@@ -1994,6 +1994,96 @@ mod tests {
); );
} }
/// xAI-cycle e2e: /v1/models is minimal (bare grok ids, no context), so
/// enrichment supplies context/limits; the restriction drops the
/// non-tool-calling grok-imagine-* generators; bare id round-trips under
/// the `xai/` key; Passthrough dialect.
#[tokio::test(flavor = "multi_thread")]
#[serial_test::serial]
async fn xai_enriches_restricts_and_maps_passthrough() {
let platform_server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/models"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(
// xAI /v1/models is minimal: id/object only, NO context field —
// so a non-zero context below can only come from enrichment.
serde_json::json!({ "data": [
{ "id": "grok-4.5", "object": "model" },
{ "id": "grok-imagine-image", "object": "model" }
]}),
))
.expect(1)
.mount(&platform_server)
.await;
let modelsdev_server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api.json"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(
serde_json::json!({ "xai": { "models": {
"grok-4.5": {
"limit": {"context": 500000, "output": 128000},
"tool_call": true
},
// present in enrichment too, but not tool-calling → dropped.
"grok-imagine-image": { "limit": {"context": 8000} }
}}}),
))
.expect(1)
.mount(&modelsdev_server)
.await;
let cache_dir = tempfile::tempdir().unwrap();
let _base =
kigi_test_support::EnvGuard::set(kigi_models::XAI_BASE_URL_ENV, platform_server.uri());
let _mdev = kigi_test_support::EnvGuard::set(
crate::agent::enrichment_fetch::MODELS_DEV_URL_ENV,
format!("{}/api.json", modelsdev_server.uri()),
);
let _mdev_cache = kigi_test_support::EnvGuard::set(
crate::agent::enrichment_fetch::MODELS_DEV_CACHE_DIR_ENV,
cache_dir.path(),
);
let endpoints = crate::agent::config::EndpointsConfig::default();
let keys = crate::agent::models::PlatformApiKeys::test_single(
kigi_models::PlatformId::Xai,
"xai-1",
);
let result = tokio::task::spawn_blocking(move || {
fetch_platform_models_blocking(&endpoints, None, &keys)
})
.await
.unwrap()
.expect("fetch must succeed");
assert_eq!(
result
.models
.iter()
.map(|m| m.id.as_deref().unwrap_or_default())
.collect::<Vec<_>>(),
vec!["xai/grok-4.5"],
"grok-imagine-image (not tool-calling) dropped; bare id kept under the xai key"
);
let entry = &result.models[0];
assert_eq!(
entry.context_window.get(),
500_000,
"context comes from enrichment (the wire listing carries none)"
);
assert_eq!(entry.max_completion_tokens, Some(128_000));
assert_eq!(entry.model, "grok-4.5", "the bare grok id rides the wire");
let model_entry = crate::agent::config::ModelEntry::from_config_entry(entry);
let creds = crate::agent::config::ResolvedCredentials {
api_key: Some("xai-1".into()),
base_url: entry.base_url.clone(),
auth_type: kigi_chat_state::AuthType::ApiKey,
auth_scheme: Default::default(),
};
let cfg = crate::agent::config::sampling_config_for_model(&model_entry, creds, None);
assert_eq!(
cfg.chat_compat,
kigi_sampling_types::ChatCompat::Passthrough
);
}
#[test] #[test]
fn get_env_keys_parses_strings_and_rejects_non_strings() { fn get_env_keys_parses_strings_and_rejects_non_strings() {
use crate::agent::config::EnvKeys; use crate::agent::config::EnvKeys;
@@ -165,7 +165,7 @@ impl acp::Agent for MvpAgent {
&crate::util::kigi_home::kigi_home(), &crate::util::kigi_home::kigi_home(),
) )
{ {
unsafe { std::env::set_var("XAI_API_KEY", &api_key) }; unsafe { std::env::set_var("KIGI_API_KEY", &api_key) };
tracing::info!("auth: loaded API key from auth.json (xai::api_key scope)"); tracing::info!("auth: loaded API key from auth.json (xai::api_key scope)");
kigi_log::unified_log::info( kigi_log::unified_log::info(
"auth: loaded API key from auth.json (xai::api_key scope)", "auth: loaded API key from auth.json (xai::api_key scope)",
@@ -357,7 +357,7 @@ impl acp::Agent for MvpAgent {
return Err( return Err(
acp::Error::auth_required() acp::Error::auth_required()
.data( .data(
"Set XAI_API_KEY or add api_key/env_key to config.toml.", "Set KIGI_API_KEY or add api_key/env_key to config.toml.",
), ),
); );
} }
@@ -1904,10 +1904,12 @@ async fn cached_token_fallthrough_prefers_api_key_for_deployment_key() {
#[serial_test::serial] #[serial_test::serial]
async fn cached_token_fallthrough_falls_to_kigi_com_without_credentials() { async fn cached_token_fallthrough_falls_to_kigi_com_without_credentials() {
use crate::agent::auth_method::{ use crate::agent::auth_method::{
KIMI_CODE_METHOD_ID, LEGACY_XAI_API_KEY_ENV_VAR, XAI_API_KEY_ENV_VAR, HOUSE_API_KEY_ENV_VAR, KIMI_CODE_METHOD_ID, LEGACY_XAI_API_KEY_ENV_VAR,
XAI_API_KEY_ENV_VAR,
}; };
use kigi_test_support::EnvGuard; use kigi_test_support::EnvGuard;
let _lockdown = EnvGuard::unset("KIGI_DISABLE_API_KEY_AUTH"); let _lockdown = EnvGuard::unset("KIGI_DISABLE_API_KEY_AUTH");
let _house = EnvGuard::unset(HOUSE_API_KEY_ENV_VAR);
let _new = EnvGuard::unset(XAI_API_KEY_ENV_VAR); let _new = EnvGuard::unset(XAI_API_KEY_ENV_VAR);
let _legacy = EnvGuard::unset(LEGACY_XAI_API_KEY_ENV_VAR); let _legacy = EnvGuard::unset(LEGACY_XAI_API_KEY_ENV_VAR);
let agent = build_minimal_agent_for_tests(); let agent = build_minimal_agent_for_tests();
+5 -2
View File
@@ -92,7 +92,9 @@ pub async fn list_models(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::agent::auth_method::{LEGACY_XAI_API_KEY_ENV_VAR, XAI_API_KEY_ENV_VAR}; use crate::agent::auth_method::{
HOUSE_API_KEY_ENV_VAR, LEGACY_XAI_API_KEY_ENV_VAR, XAI_API_KEY_ENV_VAR,
};
use crate::agent::config::Config; use crate::agent::config::Config;
use crate::auth::{AuthMode, KimiAuth}; use crate::auth::{AuthMode, KimiAuth};
use kigi_test_support::EnvGuard; use kigi_test_support::EnvGuard;
@@ -102,10 +104,11 @@ mod tests {
/// ///
/// Uses `KIGI_AUTH_PATH` (not `KIGI_SHARE_DIR`) so a OnceLock-cached real home /// Uses `KIGI_AUTH_PATH` (not `KIGI_SHARE_DIR`) so a OnceLock-cached real home
/// with `auth.json` cannot leak into these tests. /// with `auth.json` cannot leak into these tests.
fn isolate_auth_sources() -> (tempfile::TempDir, [EnvGuard; 7]) { fn isolate_auth_sources() -> (tempfile::TempDir, [EnvGuard; 8]) {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let auth_path = dir.path().join("no-auth.json"); let auth_path = dir.path().join("no-auth.json");
let guards = [ let guards = [
EnvGuard::unset(HOUSE_API_KEY_ENV_VAR),
EnvGuard::unset(XAI_API_KEY_ENV_VAR), EnvGuard::unset(XAI_API_KEY_ENV_VAR),
EnvGuard::unset(LEGACY_XAI_API_KEY_ENV_VAR), EnvGuard::unset(LEGACY_XAI_API_KEY_ENV_VAR),
EnvGuard::unset("KIGI_AUTH"), EnvGuard::unset("KIGI_AUTH"),
@@ -56,18 +56,18 @@ fn handle_set_api_key(args: &acp::ExtRequest) -> ExtResult {
crate::auth::clear_api_key(&kigi_home) crate::auth::clear_api_key(&kigi_home)
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?; .map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
// SAFETY: ext_method is single-threaded per agent // SAFETY: ext_method is single-threaded per agent
unsafe { std::env::remove_var("XAI_API_KEY") }; unsafe { std::env::remove_var("KIGI_API_KEY") };
} else { } else {
crate::auth::store_api_key(&kigi_home, k) crate::auth::store_api_key(&kigi_home, k)
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?; .map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
// SAFETY: ext_method is single-threaded per agent // SAFETY: ext_method is single-threaded per agent
unsafe { std::env::set_var("XAI_API_KEY", k) }; unsafe { std::env::set_var("KIGI_API_KEY", k) };
} }
} else { } else {
crate::auth::clear_api_key(&kigi_home) crate::auth::clear_api_key(&kigi_home)
.map_err(|e| acp::Error::internal_error().data(e.to_string()))?; .map_err(|e| acp::Error::internal_error().data(e.to_string()))?;
// SAFETY: ext_method is single-threaded per agent // SAFETY: ext_method is single-threaded per agent
unsafe { std::env::remove_var("XAI_API_KEY") }; unsafe { std::env::remove_var("KIGI_API_KEY") };
} }
ExtMethodResult::success(serde_json::json!({ "ok": true })) ExtMethodResult::success(serde_json::json!({ "ok": true }))
.to_ext_response() .to_ext_response()
@@ -76,7 +76,7 @@ pub fn map_sampling_err_to_acp(err: SamplingError) -> acp::Error {
&& crate::agent::auth_method::has_xai_api_key_env() && crate::agent::auth_method::has_xai_api_key_env()
{ {
format!( format!(
"{message}\n\nYou have an API key set (XAI_API_KEY). \ "{message}\n\nYou have an API key set (KIGI_API_KEY). \
Your cached OAuth session is being used instead. \ Your cached OAuth session is being used instead. \
To use your API key, run `kigi logout` or type /logout in the TUI." To use your API key, run `kigi logout` or type /logout in the TUI."
) )
@@ -441,24 +441,32 @@ mod tests {
); );
} }
/// Helper: run a closure with XAI_API_KEY temporarily set (or cleared). /// Helper: run a closure with the house BYOK key (KIGI_API_KEY) temporarily
/// Cleans up even if the closure panics. /// set (or cleared). Clears every house-key env (KIGI_API_KEY plus the
/// back-compat XAI_API_KEY / KIGI_CODE_XAI_API_KEY) so the "no key" case is
/// hermetic. Cleans up even if the closure panics.
fn with_api_key_env<F: FnOnce()>(key: Option<&str>, f: F) { fn with_api_key_env<F: FnOnce()>(key: Option<&str>, f: F) {
let prev_house = std::env::var("KIGI_API_KEY").ok();
let prev = std::env::var("XAI_API_KEY").ok(); let prev = std::env::var("XAI_API_KEY").ok();
let prev_legacy = std::env::var("KIGI_CODE_XAI_API_KEY").ok(); let prev_legacy = std::env::var("KIGI_CODE_XAI_API_KEY").ok();
// SAFETY: serial_test ensures no concurrent env mutation. // SAFETY: serial_test ensures no concurrent env mutation.
unsafe { unsafe {
std::env::remove_var("KIGI_API_KEY");
std::env::remove_var("XAI_API_KEY"); std::env::remove_var("XAI_API_KEY");
std::env::remove_var("KIGI_CODE_XAI_API_KEY"); std::env::remove_var("KIGI_CODE_XAI_API_KEY");
if let Some(k) = key { if let Some(k) = key {
std::env::set_var("XAI_API_KEY", k); std::env::set_var("KIGI_API_KEY", k);
} }
} }
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
// Restore original state. // Restore original state.
unsafe { unsafe {
std::env::remove_var("KIGI_API_KEY");
std::env::remove_var("XAI_API_KEY"); std::env::remove_var("XAI_API_KEY");
std::env::remove_var("KIGI_CODE_XAI_API_KEY"); std::env::remove_var("KIGI_CODE_XAI_API_KEY");
if let Some(v) = prev_house {
std::env::set_var("KIGI_API_KEY", v);
}
if let Some(v) = prev { if let Some(v) = prev {
std::env::set_var("XAI_API_KEY", v); std::env::set_var("XAI_API_KEY", v);
} }
@@ -1044,7 +1044,9 @@ pub async fn resolve_api_key(explicit: Option<&str>, kigi_home: &Path) -> Result
if let Some(k) = explicit.map(str::trim).filter(|s| !s.is_empty()) { if let Some(k) = explicit.map(str::trim).filter(|s| !s.is_empty()) {
return Ok(k.to_owned()); return Ok(k.to_owned());
} }
let env_key = std::env::var("XAI_API_KEY").ok(); // Canonical house-key resolution: KIGI_API_KEY, then the back-compat
// XAI_API_KEY / KIGI_CODE_XAI_API_KEY (see `read_xai_api_key_env`).
let env_key = crate::agent::auth_method::read_xai_api_key_env().ok();
if let Some(k) = env_key.as_deref().map(str::trim).filter(|s| !s.is_empty()) { if let Some(k) = env_key.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
return Ok(k.to_owned()); return Ok(k.to_owned());
} }
@@ -1052,7 +1054,7 @@ pub async fn resolve_api_key(explicit: Option<&str>, kigi_home: &Path) -> Result
return Ok(key); return Ok(key);
} }
Err(anyhow!( Err(anyhow!(
"no API key: pass --api-key, set XAI_API_KEY, or run `kigi login` to populate \ "no API key: pass --api-key, set KIGI_API_KEY, or run `kigi login` to populate \
<kigi-home>/auth.json. An expired OIDC token is auto-refreshed when a refresh_token \ <kigi-home>/auth.json. An expired OIDC token is auto-refreshed when a refresh_token \
is present; if not, re-login is required." is present; if not, re-login is required."
)) ))
@@ -2344,7 +2346,7 @@ mod tests {
.await; .await;
assert!( assert!(
msg.contains("--api-key") msg.contains("--api-key")
&& msg.contains("XAI_API_KEY") && msg.contains("KIGI_API_KEY")
&& msg.contains("kigi login") && msg.contains("kigi login")
&& msg.contains("auth.json"), && msg.contains("auth.json"),
"error names all three sources: {msg}", "error names all three sources: {msg}",
@@ -2445,7 +2447,9 @@ mod tests {
/// refresher that could mint credentials independent of the /// refresher that could mint credentials independent of the
/// fixture. /// fixture.
const ISOLATED_ENV_KEYS: &[&str] = &[ const ISOLATED_ENV_KEYS: &[&str] = &[
"KIGI_API_KEY",
"XAI_API_KEY", "XAI_API_KEY",
"KIGI_CODE_XAI_API_KEY",
"KIGI_AUTH", "KIGI_AUTH",
"KIGI_AUTH_PATH", "KIGI_AUTH_PATH",
"KIGI_AUTH_PROVIDER_COMMAND", "KIGI_AUTH_PROVIDER_COMMAND",
@@ -433,7 +433,7 @@ fn git_rebase_refresh_storm_e2e() {
std::env::set_var("KIGI_SHARE_DIR", kigi_home.path()); std::env::set_var("KIGI_SHARE_DIR", kigi_home.path());
std::env::set_var("KIGI_CODE_BASE_URL", server.url()); std::env::set_var("KIGI_CODE_BASE_URL", server.url());
std::env::set_var("KIGI_API_BASE_URL", server.url()); std::env::set_var("KIGI_API_BASE_URL", server.url());
std::env::set_var("XAI_API_KEY", "test-key-for-ci"); std::env::set_var("KIGI_API_KEY", "test-key-for-ci");
std::env::set_var("KIGI_TELEMETRY_ENABLED", "false"); std::env::set_var("KIGI_TELEMETRY_ENABLED", "false");
std::env::set_var("KIGI_FEEDBACK_ENABLED", "false"); std::env::set_var("KIGI_FEEDBACK_ENABLED", "false");
std::env::set_var("KIGI_TRACE_UPLOAD", "false"); std::env::set_var("KIGI_TRACE_UPLOAD", "false");
@@ -627,7 +627,7 @@ async fn full_session_load_e2e() {
std::env::set_var("KIGI_INSTRUMENTATION_LOG", &instr_log); std::env::set_var("KIGI_INSTRUMENTATION_LOG", &instr_log);
std::env::set_var("KIGI_CODE_BASE_URL", server.url()); std::env::set_var("KIGI_CODE_BASE_URL", server.url());
std::env::set_var("KIGI_API_BASE_URL", server.url()); std::env::set_var("KIGI_API_BASE_URL", server.url());
std::env::set_var("XAI_API_KEY", "test-key-for-ci"); std::env::set_var("KIGI_API_KEY", "test-key-for-ci");
std::env::set_var("KIGI_TELEMETRY_ENABLED", "false"); std::env::set_var("KIGI_TELEMETRY_ENABLED", "false");
std::env::set_var("KIGI_FEEDBACK_ENABLED", "false"); std::env::set_var("KIGI_FEEDBACK_ENABLED", "false");
std::env::set_var("KIGI_TRACE_UPLOAD", "false"); std::env::set_var("KIGI_TRACE_UPLOAD", "false");
@@ -138,7 +138,7 @@ async fn leader_soak_churning_clients_no_leaks_no_zombies() {
std::env::set_var("KIGI_SHARE_DIR", kigi_home.path()); std::env::set_var("KIGI_SHARE_DIR", kigi_home.path());
std::env::set_var("KIGI_CODE_BASE_URL", server.url()); std::env::set_var("KIGI_CODE_BASE_URL", server.url());
std::env::set_var("KIGI_API_BASE_URL", server.url()); std::env::set_var("KIGI_API_BASE_URL", server.url());
std::env::set_var("XAI_API_KEY", "test-key-for-ci"); std::env::set_var("KIGI_API_KEY", "test-key-for-ci");
std::env::set_var("KIGI_TELEMETRY_ENABLED", "false"); std::env::set_var("KIGI_TELEMETRY_ENABLED", "false");
std::env::set_var("KIGI_FEEDBACK_ENABLED", "false"); std::env::set_var("KIGI_FEEDBACK_ENABLED", "false");
std::env::set_var("KIGI_TRACE_UPLOAD", "false"); std::env::set_var("KIGI_TRACE_UPLOAD", "false");
+1 -1
View File
@@ -166,7 +166,7 @@ pub fn test_env_cmd_tokio(
.env("KIGI_SHARE_DIR", home.join(".kigi")) .env("KIGI_SHARE_DIR", home.join(".kigi"))
.env("KIGI_CODE_BASE_URL", mock_url) .env("KIGI_CODE_BASE_URL", mock_url)
.env("KIGI_API_BASE_URL", mock_url) .env("KIGI_API_BASE_URL", mock_url)
.env("XAI_API_KEY", "test-key-for-ci") .env("KIGI_API_KEY", "test-key-for-ci")
.env("KIGI_TELEMETRY_ENABLED", "false") .env("KIGI_TELEMETRY_ENABLED", "false")
.env("KIGI_FEEDBACK_ENABLED", "false") .env("KIGI_FEEDBACK_ENABLED", "false")
.env("KIGI_TRACE_UPLOAD", "false") .env("KIGI_TRACE_UPLOAD", "false")
@@ -143,7 +143,7 @@ impl LeaderStdioClient {
.env("KIGI_LEADER_SOCKET", home.join(".kigi").join("leader.sock")) .env("KIGI_LEADER_SOCKET", home.join(".kigi").join("leader.sock"))
.env("KIGI_CODE_BASE_URL", server.url()) .env("KIGI_CODE_BASE_URL", server.url())
.env("KIGI_API_BASE_URL", server.url()) .env("KIGI_API_BASE_URL", server.url())
.env("XAI_API_KEY", "test-key-for-ci") .env("KIGI_API_KEY", "test-key-for-ci")
.env("KIGI_TELEMETRY_ENABLED", "false") .env("KIGI_TELEMETRY_ENABLED", "false")
.env("KIGI_FEEDBACK_ENABLED", "false") .env("KIGI_FEEDBACK_ENABLED", "false")
.env("KIGI_TRACE_UPLOAD", "false") .env("KIGI_TRACE_UPLOAD", "false")
+10 -3
View File
@@ -6894,7 +6894,7 @@ pub(crate) mod tests {
#[test] #[test]
fn pending_menu_items_lists_interactive_methods_plus_quit() { fn pending_menu_items_lists_interactive_methods_plus_quit() {
let items = pending_menu_items(&fresh_user_auth_methods(), None); let items = pending_menu_items(&fresh_user_auth_methods(), None);
assert_eq!(items.len(), 16, "15 login rows + Quit, got {items:?}"); assert_eq!(items.len(), 17, "16 login rows + Quit, got {items:?}");
assert!( assert!(
matches!(&items[0], PendingMenuItem::Login { label } if label == "Kimi Code (OAuth)"), matches!(&items[0], PendingMenuItem::Login { label } if label == "Kimi Code (OAuth)"),
"row 0 must be the OAuth login, got {:?}", "row 0 must be the OAuth login, got {:?}",
@@ -6999,7 +6999,14 @@ pub(crate) mod tests {
label: "Vercel AI Gateway (API key)".into(), label: "Vercel AI Gateway (API key)".into(),
} }
); );
assert_eq!(items[15], PendingMenuItem::Quit); assert_eq!(
items[15],
PendingMenuItem::ApiKey {
target: PlatformLogin(kigi_shell::models::PlatformId::Xai),
label: "xAI (Grok) (API key)".into(),
}
);
assert_eq!(items[16], PendingMenuItem::Quit);
// The non-interactive methods must never appear as rows. // The non-interactive methods must never appear as rows.
let byok = kigi_shell::agent::auth_method::build_auth_methods( let byok = kigi_shell::agent::auth_method::build_auth_methods(
kigi_shell::agent::auth_method::AuthMethodsBuildInputs { kigi_shell::agent::auth_method::AuthMethodsBuildInputs {
@@ -7010,7 +7017,7 @@ pub(crate) mod tests {
); );
assert_eq!( assert_eq!(
pending_menu_items(&byok.methods, None).len(), pending_menu_items(&byok.methods, None).len(),
16, 17,
"xai.api_key / cached_token must not add rows" "xai.api_key / cached_token must not add rows"
); );
} }
@@ -319,7 +319,7 @@ impl PagerLeaderCluster {
crate::test_util::EnvVarGuard::set("KIGI_SHARE_DIR", kigi_home.path()), crate::test_util::EnvVarGuard::set("KIGI_SHARE_DIR", kigi_home.path()),
crate::test_util::EnvVarGuard::set("KIGI_CODE_BASE_URL", server.url()), crate::test_util::EnvVarGuard::set("KIGI_CODE_BASE_URL", server.url()),
crate::test_util::EnvVarGuard::set("KIGI_API_BASE_URL", server.url()), crate::test_util::EnvVarGuard::set("KIGI_API_BASE_URL", server.url()),
crate::test_util::EnvVarGuard::set("XAI_API_KEY", "test-key-for-ci"), crate::test_util::EnvVarGuard::set("KIGI_API_KEY", "test-key-for-ci"),
crate::test_util::EnvVarGuard::set("KIGI_TELEMETRY_ENABLED", "false"), crate::test_util::EnvVarGuard::set("KIGI_TELEMETRY_ENABLED", "false"),
crate::test_util::EnvVarGuard::set("KIGI_FEEDBACK_ENABLED", "false"), crate::test_util::EnvVarGuard::set("KIGI_FEEDBACK_ENABLED", "false"),
crate::test_util::EnvVarGuard::set("KIGI_TRACE_UPLOAD", "false"), crate::test_util::EnvVarGuard::set("KIGI_TRACE_UPLOAD", "false"),
+1 -1
View File
@@ -9,7 +9,7 @@ use crate::client_identity::{PAGER_CLIENT_TYPE, PAGER_CLIENT_VERSION};
pub async fn list_available_models(agent_config: &AgentConfig) -> Result<()> { pub async fn list_available_models(agent_config: &AgentConfig) -> Result<()> {
match AuthStatus::resolve(agent_config) { match AuthStatus::resolve(agent_config) {
AuthStatus::ApiKey => println!("You are using XAI_API_KEY."), AuthStatus::ApiKey => println!("You are using KIGI_API_KEY."),
AuthStatus::LoggedIn(host) => println!("You are logged in with {}.", host), AuthStatus::LoggedIn(host) => println!("You are logged in with {}.", host),
AuthStatus::ModelCredentials(model) => { AuthStatus::ModelCredentials(model) => {
println!("Model '{model}' is using its own API key."); println!("Model '{model}' is using its own API key.");
@@ -51,7 +51,7 @@ async fn campaign_leader_mode_remote_dismiss_on_model_pick() {
let socket = kigi_home.join("leader-e2e.sock"); let socket = kigi_home.join("leader-e2e.sock");
let socket = socket.to_str().expect("socket path is utf-8").to_owned(); let socket = socket.to_str().expect("socket path is utf-8").to_owned();
// Session (OAuth) auth, not the harness's default XAI_API_KEY: the // Session (OAuth) auth, not the harness's default KIGI_API_KEY: the
// settings fetch requires `auth_manager.auth()` — in ApiKey/BYOK mode the // settings fetch requires `auth_manager.auth()` — in ApiKey/BYOK mode the
// pager never requests `/v1/settings`, so a remote campaign would be // pager never requests `/v1/settings`, so a remote campaign would be
// structurally unreachable (see `spawn_polling_session`'s doc). // structurally unreachable (see `spawn_polling_session`'s doc).
@@ -78,7 +78,7 @@ fn prepare_sandbox(home: &Path, gate_on: bool) -> Vec<(String, String)> {
("NO_COLOR".into(), "0".into()), ("NO_COLOR".into(), "0".into()),
("TERM_PROGRAM".into(), "".into()), ("TERM_PROGRAM".into(), "".into()),
("TMUX".into(), "".into()), ("TMUX".into(), "".into()),
// Do not set XAI_API_KEY — prefer OIDC entry in auth.json (pty_e2e pattern). // Do not set KIGI_API_KEY — prefer OIDC entry in auth.json (pty_e2e pattern).
]; ];
// Pin the feature gate explicitly so the cycle is deterministic regardless // Pin the feature gate explicitly so the cycle is deterministic regardless
// of the developer's shell. `KIGI_AUTO_PERMISSION_MODE` is the highest gate // of the developer's shell. `KIGI_AUTO_PERMISSION_MODE` is the highest gate
@@ -47,7 +47,7 @@ async fn campaign_remote_settings_nudge_and_dismiss() {
) )
.expect("write config.toml"); .expect("write config.toml");
// Session (OAuth) auth, not the harness's default XAI_API_KEY: the // Session (OAuth) auth, not the harness's default KIGI_API_KEY: the
// settings fetch requires `auth_manager.auth()` — in ApiKey/BYOK mode the // settings fetch requires `auth_manager.auth()` — in ApiKey/BYOK mode the
// pager never requests `/v1/settings`, so a remote campaign would be // pager never requests `/v1/settings`, so a remote campaign would be
// structurally unreachable (see `spawn_polling_session`'s doc). // structurally unreachable (see `spawn_polling_session`'s doc).
@@ -138,7 +138,7 @@ pub(crate) fn tall_response(sentinel: &str, rows: usize) -> String {
/// Spawn a pager with fake session (OAuth) auth, then drive it into a live /// Spawn a pager with fake session (OAuth) auth, then drive it into a live
/// session (welcome → prompt → mock response). Session auth matters: the /// session (welcome → prompt → mock response). Session auth matters: the
/// settings poll requires `auth_manager.auth()`, and the harness's default /// settings poll requires `auth_manager.auth()`, and the harness's default
/// `XAI_API_KEY` (ApiKey/BYOK mode, no auth.json entry) would never fetch /// `KIGI_API_KEY` (ApiKey/BYOK mode, no auth.json entry) would never fetch
/// `/v1/settings`. Call `content.set_response(..)` BEFORE this so the entry /// `/v1/settings`. Call `content.set_response(..)` BEFORE this so the entry
/// prompt streams. /// prompt streams.
pub(crate) fn spawn_polling_session(content: &ContentController, oauth_user: &str) -> PtyHarness { pub(crate) fn spawn_polling_session(content: &ContentController, oauth_user: &str) -> PtyHarness {