From ca2fa4f5f616ae0d7be83c4ece4755991225d58b Mon Sep 17 00:00:00 2001 From: Yang Yang Date: Sun, 2 Aug 2026 22:03:40 -0700 Subject: [PATCH] fix(ai): do not treat XAI_API_KEY as SuperGrok availability Paid-key-only setups were marked signed in for xai-oauth, so the shared grok-4.5 default picker preferred SuperGrok over xai/grok-4.5. --- packages/ai/CHANGELOG.md | 3 ++ packages/ai/src/auth-storage.ts | 20 ++++++++- packages/ai/test/xai-login.test.ts | 43 +++++++++++++++++++ packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/test/model-resolver.test.ts | 30 +++++++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 08c36e86c..c9810157d 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -148,6 +148,9 @@ ### Fixed - Fixed an issue where Ollama requests without a user-role message would fail to generate output or silently fail with a misleading error. +### 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. ## [17.2.5] - 2026-08-03 diff --git a/packages/ai/src/auth-storage.ts b/packages/ai/src/auth-storage.ts index 625f6b6d4..538a2ce78 100644 --- a/packages/ai/src/auth-storage.ts +++ b/packages/ai/src/auth-storage.ts @@ -2687,7 +2687,7 @@ export class AuthStorage { if (this.#runtimeOverrides.has(provider)) return true; if (this.#configOverrides.has(provider)) return true; if (this.#getCredentialsForProvider(provider).length > 0) return true; - if (getEnvApiKey(provider)) return true; + if (this.#hasDedicatedEnvAuth(provider)) return true; if (this.#fallbackResolver?.(provider)) return true; return false; } @@ -2711,6 +2711,22 @@ export class AuthStorage { return false; } + /** + * Env auth that belongs to this provider, not a cross-provider alias. + * + * `getEnvApiKey("xai-oauth")` also accepts `XAI_API_KEY` so an explicit + * `xai-oauth/…` stream can still borrow the paid key. Availability and + * origin must not: otherwise an API-key-only setup marks SuperGrok as + * signed in and `pickDefaultAvailableModel` prefers `xai-oauth/grok-4.5` + * over paid `xai/grok-4.5`. + */ + #hasDedicatedEnvAuth(provider: string): boolean { + if (provider === "xai-oauth") { + return Boolean($env.XAI_OAUTH_TOKEN?.trim()); + } + return Boolean(getEnvApiKey(provider)); + } + /** * Classify where a provider's auth comes from, following the same precedence * as {@link AuthStorage.getApiKey}: runtime override → config override → @@ -2727,7 +2743,7 @@ export class AuthStorage { if (stored.some(credential => credential.type === "api_key" && credential.source === "login")) { return { kind: "api_key" }; } - if (getEnvApiKey(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) }; + if (this.#hasDedicatedEnvAuth(provider)) return { kind: "env", envVar: getEnvApiKeyName(provider) }; if (stored.some(credential => credential.type === "api_key")) return { kind: "api_key" }; if (this.#fallbackResolver?.(provider)) return { kind: "fallback" }; return undefined; diff --git a/packages/ai/test/xai-login.test.ts b/packages/ai/test/xai-login.test.ts index 1cdd2ca1d..0d0ad19f4 100644 --- a/packages/ai/test/xai-login.test.ts +++ b/packages/ai/test/xai-login.test.ts @@ -29,6 +29,49 @@ describe("xAI API login wiring", () => { expect(getEnvApiKey("xai")).toBe("xai-env-key"); }); + test("XAI_API_KEY alone does not mark SuperGrok as available", async () => { + const originalOauthToken = Bun.env.XAI_OAUTH_TOKEN; + Bun.env.XAI_API_KEY = "xai-env-key"; + delete Bun.env.XAI_OAUTH_TOKEN; + const store = new SqliteAuthCredentialStore(new Database(":memory:")); + const storage = new AuthStorage(store); + await storage.reload(); + try { + expect(storage.hasAuth("xai")).toBe(true); + expect(storage.hasAuth("xai-oauth")).toBe(false); + expect(storage.getCredentialOrigin("xai")).toEqual({ kind: "env", envVar: "XAI_API_KEY" }); + expect(storage.getCredentialOrigin("xai-oauth")).toBeUndefined(); + } finally { + if (originalOauthToken === undefined) { + delete Bun.env.XAI_OAUTH_TOKEN; + } else { + Bun.env.XAI_OAUTH_TOKEN = originalOauthToken; + } + store.close(); + } + }); + + test("XAI_OAUTH_TOKEN marks SuperGrok available without a paid API key", async () => { + const originalOauthToken = Bun.env.XAI_OAUTH_TOKEN; + delete Bun.env.XAI_API_KEY; + Bun.env.XAI_OAUTH_TOKEN = "xai-oauth-env"; + const store = new SqliteAuthCredentialStore(new Database(":memory:")); + const storage = new AuthStorage(store); + await storage.reload(); + try { + expect(storage.hasAuth("xai")).toBe(false); + expect(storage.hasAuth("xai-oauth")).toBe(true); + expect(storage.getCredentialOrigin("xai-oauth")).toEqual({ kind: "env" }); + } finally { + if (originalOauthToken === undefined) { + delete Bun.env.XAI_OAUTH_TOKEN; + } else { + Bun.env.XAI_OAUTH_TOKEN = originalOauthToken; + } + store.close(); + } + }); + test("AuthStorage.login('xai') validates against /models and stores the pasted key", async () => { const fetchCalls: Array<{ url: string; init: RequestInit | undefined }> = []; const fetchMock: FetchImpl = vi.fn(async (input: string | URL | Request, init?: RequestInit) => { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a4951c68f..2c7bb22cb 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -371,6 +371,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`. ## [17.2.5] - 2026-08-03 diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index f8f665441..6d0e2d86d 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -437,6 +437,36 @@ describe("pickDefaultAvailableModel", () => { expect(result?.provider).toBe("zhipu-coding-plan"); expect(result?.id).toBe("glm-5.1"); }); + + test("prefers SuperGrok over paid xAI when both defaults are present", () => { + const paid = buildModel({ + id: "grok-4.5", + name: "Grok 4.5", + api: "openai-responses", + provider: "xai", + baseUrl: "https://api.x.ai/v1", + reasoning: true, + input: ["text", "image"], + cost: { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 }, + contextWindow: 500000, + maxTokens: 500000, + }); + const oauth = buildModel({ + id: "grok-4.5", + name: "Grok 4.5", + api: "openai-responses", + provider: "xai-oauth", + baseUrl: "https://api.x.ai/v1", + reasoning: true, + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 500000, + maxTokens: 500000, + }); + + expect(pickDefaultAvailableModel([paid, oauth])?.provider).toBe("xai-oauth"); + expect(pickDefaultAvailableModel([paid])?.provider).toBe("xai"); + }); }); describe("parseModelPattern", () => {