From d14c028aeec4f36b277d075b1c1493a624ea27c8 Mon Sep 17 00:00:00 2001 From: Yang Yang Date: Sun, 9 Aug 2026 23:12:07 -0700 Subject: [PATCH] fix(ai): allow explicit xai-oauth selectors with XAI_API_KEY Keep hasAuth() dedicated so SuperGrok is not auto-selected from a paid key. Explicit preflight uses hasResolvableAuth() so xai-oauth/grok-4.5 can still borrow XAI_API_KEY. --- packages/ai/CHANGELOG.md | 2 +- packages/ai/src/auth-storage.ts | 18 ++++++++++++++++-- packages/ai/test/xai-login.test.ts | 3 +++ packages/coding-agent/CHANGELOG.md | 2 +- .../coding-agent/src/config/model-registry.ts | 9 +++++++-- 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 4bda16769..feedcbda3 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -4,7 +4,7 @@ ### Fixed -- Stopped treating `XAI_API_KEY` as SuperGrok (`xai-oauth`) sign-in for availability, so paid-key-only setups default to `xai/grok-4.5` instead of the zero-cost SuperGrok catalog path. +- Stopped treating `XAI_API_KEY` as SuperGrok (`xai-oauth`) sign-in for availability, so paid-key-only setups default to `xai/grok-4.5` instead of the zero-cost SuperGrok catalog path. Explicit `xai-oauth/…` selectors still accept the paid key via the existing env fallback. - Omitted unsupported `reasoning.summary` on paid xAI Responses requests (`xai/grok-4.5`), matching SuperGrok, so a thinking level no longer serializes `summary: "auto"`. - Omitted presence/frequency penalties on all first-party xAI Responses models, including non-reasoning ids such as `xai/grok-2`. diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts index 538a2ce78..4748f28b1 100644 --- a/packages/ai/src/auth-storage.ts +++ b/packages/ai/src/auth-storage.ts @@ -2680,8 +2680,10 @@ export class AuthStorage { } /** - * Check if any form of auth is configured for a provider. - * Unlike getApiKey(), this doesn't refresh OAuth tokens. + * Dedicated auth for default-model availability (picker / `getAvailable`). + * Unlike {@link getApiKey}, this does not refresh OAuth tokens, and unlike + * {@link hasResolvableAuth} it ignores cross-provider env aliases so + * `XAI_API_KEY` does not auto-select SuperGrok (`xai-oauth`). */ hasAuth(provider: string): boolean { if (this.#runtimeOverrides.has(provider)) return true; @@ -2692,6 +2694,18 @@ export class AuthStorage { return false; } + /** + * Whether a request could resolve a key for this provider, including + * cross-provider env aliases (`xai-oauth` borrowing `XAI_API_KEY`). + * Use this for explicit model preflight (`xai-oauth/grok-4.5`); use + * {@link hasAuth} for auto-availability so the default picker stays on + * paid `xai` when only `XAI_API_KEY` is set. + */ + hasResolvableAuth(provider: string): boolean { + if (this.hasAuth(provider)) return true; + return Boolean(getEnvApiKey(provider)); + } + /** * True iff a dedicated, non-env credential source is configured for this * provider — i.e. anything in the cascade EXCEPT `getEnvApiKey(provider)`. diff --git a/packages/ai/test/xai-login.test.ts b/packages/ai/test/xai-login.test.ts index 0d0ad19f4..3571b531a 100644 --- a/packages/ai/test/xai-login.test.ts +++ b/packages/ai/test/xai-login.test.ts @@ -39,6 +39,9 @@ describe("xAI API login wiring", () => { try { expect(storage.hasAuth("xai")).toBe(true); expect(storage.hasAuth("xai-oauth")).toBe(false); + expect(storage.hasResolvableAuth("xai")).toBe(true); + expect(storage.hasResolvableAuth("xai-oauth")).toBe(true); + expect(getEnvApiKey("xai-oauth")).toBe("xai-env-key"); expect(storage.getCredentialOrigin("xai")).toEqual({ kind: "env", envVar: "XAI_API_KEY" }); expect(storage.getCredentialOrigin("xai-oauth")).toBeUndefined(); } finally { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 829b041b2..f61f4fc3d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -9,7 +9,7 @@ - Changed the default model for SuperGrok OAuth (`xai-oauth`) from `grok-4.3` to `grok-4.5`. - Included `reasoning.encrypted_content` in Responses `include` for paid xAI and SuperGrok OAuth models. - Replayed encrypted xAI reasoning on follow-up Responses turns for `xai` and `xai-oauth`. -- Kept automatic model selection on paid `xai/grok-4.5` when only `XAI_API_KEY` is set, instead of preferring SuperGrok `xai-oauth/grok-4.5`. +- Kept automatic model selection on paid `xai/grok-4.5` when only `XAI_API_KEY` is set, instead of preferring SuperGrok `xai-oauth/grok-4.5`. Explicit `xai-oauth/grok-4.5` still works with that paid key. - Stopped sending presence/frequency penalties and stop sequences to xAI reasoning models such as `grok-4.5`, which reject them. ## [17.3.4] - 2026-08-14 diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index f2dd6f1b5..50aa6e713 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -1667,17 +1667,22 @@ export class ModelRegistry { * * Side-effect-free and synchronous: a command-backed key (`!cmd`) counts as * configured by its presence alone — the program is NOT executed — and OAuth - * tokens are NOT refreshed (`authStorage.hasAuth`). This is what keeps the + * tokens are NOT refreshed (`authStorage.hasResolvableAuth`). This is what keeps the * model-switch pre-flight off the event loop's hot path; the real key * (command execution + OAuth refresh) is resolved lazily per request via * {@link ModelRegistry.resolver}. + * + * Cross-provider env aliases count here (`xai-oauth` can borrow `XAI_API_KEY`) + * so an explicit `xai-oauth/…` selector does not fail with "No API key". + * Default-model availability still uses {@link AuthStorage.hasAuth}, which + * ignores that alias so SuperGrok is not auto-selected from a paid key. */ hasConfiguredAuth(model: Model): boolean { const keyConfig = this.#customProviderApiKeys.get(model.provider); return ( isCommandConfigValue(keyConfig) || this.#keylessProviders.has(model.provider) || - this.authStorage.hasAuth(model.provider) + this.authStorage.hasResolvableAuth(model.provider) ); }