diff --git a/packages/ai/src/auth/helpers.ts b/packages/ai/src/auth/helpers.ts index 968b029b..5c64535f 100644 --- a/packages/ai/src/auth/helpers.ts +++ b/packages/ai/src/auth/helpers.ts @@ -31,7 +31,7 @@ export function envApiKeyAuth(name: string, envVars: readonly string[]): ApiKeyA * of bundles by loading through a bundler-opaque dynamic import (variable * specifier, see the bedrock lazy wrapper). */ -export function lazyOAuth(input: { name: string; load: () => Promise }): OAuthAuth { +export function lazyOAuth(input: { name: string; loginLabel?: string; load: () => Promise }): OAuthAuth { let promise: Promise | undefined; const loaded = () => { promise ??= input.load(); @@ -39,6 +39,7 @@ export function lazyOAuth(input: { name: string; load: () => Promise }; return { name: input.name, + loginLabel: input.loginLabel, login: async (interaction) => (await loaded()).login(interaction), refresh: async (credential) => (await loaded()).refresh(credential), toAuth: async (credential) => (await loaded()).toAuth(credential), diff --git a/packages/ai/src/auth/oauth/xai.ts b/packages/ai/src/auth/oauth/xai.ts index 4c508b6b..5432043d 100644 --- a/packages/ai/src/auth/oauth/xai.ts +++ b/packages/ai/src/auth/oauth/xai.ts @@ -25,6 +25,7 @@ type XaiDeviceCode = { deviceCode: string; userCode: string; verificationUri: string; + verificationUriComplete?: string; intervalSeconds?: number; expiresInSeconds: number; }; @@ -110,10 +111,15 @@ function parseDeviceCode(body: JsonObject): XaiDeviceCode { const interval = body.interval; const intervalSeconds = typeof interval === "number" && Number.isFinite(interval) && interval > 0 ? interval : undefined; + const verificationUriComplete = + typeof body.verification_uri_complete === "string" && body.verification_uri_complete.length > 0 + ? validateVerificationUri(body.verification_uri_complete) + : undefined; return { deviceCode: requiredString(body, "device_code"), userCode: requiredString(body, "user_code"), verificationUri: validateVerificationUri(requiredString(body, "verification_uri")), + verificationUriComplete, intervalSeconds, expiresInSeconds: positiveNumber(body, "expires_in"), }; @@ -197,7 +203,7 @@ async function loginXai(interaction: AuthInteraction): Promise interaction.notify({ type: "device_code", userCode: device.userCode, - verificationUri: device.verificationUri, + verificationUri: device.verificationUriComplete ?? device.verificationUri, intervalSeconds: device.intervalSeconds, expiresInSeconds: device.expiresInSeconds, }); @@ -222,6 +228,7 @@ async function refreshXaiToken(refreshToken: string, signal?: AbortSignal): Prom export const xaiOAuth: OAuthAuth = { name: "xAI (Grok/X subscription)", + loginLabel: "Sign in with SuperGrok or X Premium", login: loginXai, refresh: (credential, signal) => refreshXaiToken(credential.refresh, signal), diff --git a/packages/ai/src/auth/types.ts b/packages/ai/src/auth/types.ts index a4f3190d..076388f3 100644 --- a/packages/ai/src/auth/types.ts +++ b/packages/ai/src/auth/types.ts @@ -190,6 +190,9 @@ export interface OAuthAuth { /** Display name, e.g. "Anthropic (Claude Pro/Max)". */ name: string; + /** Selector label for the subscription login option, e.g. "Sign in with SuperGrok or X Premium". */ + loginLabel?: string; + login(interaction: AuthInteraction): Promise; /** diff --git a/packages/ai/src/providers/xai.models.ts b/packages/ai/src/providers/xai.models.ts index 2c781afe..29be7785 100644 --- a/packages/ai/src/providers/xai.models.ts +++ b/packages/ai/src/providers/xai.models.ts @@ -4,78 +4,6 @@ import type { Model } from "../types.ts"; export const XAI_MODELS = { - "grok-3": { - id: "grok-3", - name: "Grok 3", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: false, - input: ["text"], - cost: { - input: 3, - output: 15, - cacheRead: 0.75, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-3-fast": { - id: "grok-3-fast", - name: "Grok 3 Fast", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: false, - input: ["text"], - cost: { - input: 5, - output: 25, - cacheRead: 1.25, - cacheWrite: 0, - }, - contextWindow: 131072, - maxTokens: 8192, - } satisfies Model<"openai-completions">, - "grok-4.20-0309-non-reasoning": { - id: "grok-4.20-0309-non-reasoning", - name: "Grok 4.20 (Non-Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: false, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, - "grok-4.20-0309-reasoning": { - id: "grok-4.20-0309-reasoning", - name: "Grok 4.20 (Reasoning)", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: true, - input: ["text", "image"], - cost: { - input: 1.25, - output: 2.5, - cacheRead: 0.2, - cacheWrite: 0, - }, - contextWindow: 1000000, - maxTokens: 30000, - } satisfies Model<"openai-completions">, "grok-4.3": { id: "grok-4.3", name: "Grok 4.3", @@ -131,22 +59,4 @@ export const XAI_MODELS = { contextWindow: 256000, maxTokens: 256000, } satisfies Model<"openai-completions">, - "grok-code-fast-1": { - id: "grok-code-fast-1", - name: "Grok Code Fast 1", - api: "openai-completions", - provider: "xai", - baseUrl: "https://api.x.ai/v1", - compat: {"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false}, - reasoning: false, - input: ["text"], - cost: { - input: 0.2, - output: 1.5, - cacheRead: 0.02, - cacheWrite: 0, - }, - contextWindow: 32768, - maxTokens: 8192, - } satisfies Model<"openai-completions">, } as const; diff --git a/packages/ai/src/providers/xai.ts b/packages/ai/src/providers/xai.ts index 3a679a2f..6c26bb5d 100644 --- a/packages/ai/src/providers/xai.ts +++ b/packages/ai/src/providers/xai.ts @@ -12,7 +12,11 @@ export function xaiProvider(): Provider<"openai-completions" | "openai-responses baseUrl: "https://api.x.ai/v1", auth: { apiKey: envApiKeyAuth("xAI API key", ["XAI_API_KEY"]), - oauth: lazyOAuth({ name: "xAI (Grok/X subscription)", load: loadXaiOAuth }), + oauth: lazyOAuth({ + name: "xAI (Grok/X subscription)", + loginLabel: "Sign in with SuperGrok or X Premium", + load: loadXaiOAuth, + }), }, models: Object.values(XAI_MODELS), api: { diff --git a/packages/ai/test/context-overflow.test.ts b/packages/ai/test/context-overflow.test.ts index 68305949..b4b93b21 100644 --- a/packages/ai/test/context-overflow.test.ts +++ b/packages/ai/test/context-overflow.test.ts @@ -263,7 +263,7 @@ describe("Context overflow error handling", () => { describe.skipIf(!process.env.XAI_API_KEY)("xAI", () => { it("grok-3-fast - should detect overflow via isContextOverflow", async () => { - const model = getModel("xai", "grok-3-fast"); + const model = getModel("xai", "grok-4.3"); const result = await testContextOverflow(model, process.env.XAI_API_KEY!); logResult(result); diff --git a/packages/ai/test/empty.test.ts b/packages/ai/test/empty.test.ts index 86c25aa4..44c2e793 100644 --- a/packages/ai/test/empty.test.ts +++ b/packages/ai/test/empty.test.ts @@ -247,7 +247,7 @@ describe("AI Providers Empty Message Tests", () => { }); describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider Empty Messages", () => { - const llm = getModel("xai", "grok-3"); + const llm = getModel("xai", "grok-4.3"); it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => { await testEmptyMessage(llm); diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index fbc95f50..eb5a30dd 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -543,7 +543,7 @@ describe("Generate E2E Tests", () => { }); describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider (grok-code-fast-1 via OpenAI Completions)", () => { - const llm = getModel("xai", "grok-code-fast-1"); + const llm = getModel("xai", "grok-4.3"); it("should complete basic text generation", { retry: 3 }, async () => { await basicTextGeneration(llm); diff --git a/packages/ai/test/tokens.test.ts b/packages/ai/test/tokens.test.ts index e99c7134..42e6932d 100644 --- a/packages/ai/test/tokens.test.ts +++ b/packages/ai/test/tokens.test.ts @@ -132,7 +132,7 @@ describe("Token Statistics on Abort", () => { }); describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider", () => { - const llm = getModel("xai", "grok-3-fast"); + const llm = getModel("xai", "grok-4.3"); it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => { await testTokensOnAbort(llm); diff --git a/packages/ai/test/tool-call-without-result.test.ts b/packages/ai/test/tool-call-without-result.test.ts index 198c48c5..f1e8cbfb 100644 --- a/packages/ai/test/tool-call-without-result.test.ts +++ b/packages/ai/test/tool-call-without-result.test.ts @@ -143,7 +143,7 @@ describe("Tool Call Without Result Tests", () => { }); describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider", () => { - const model = getModel("xai", "grok-3-fast"); + const model = getModel("xai", "grok-4.3"); it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => { await testToolCallWithoutResult(model); diff --git a/packages/ai/test/total-tokens.test.ts b/packages/ai/test/total-tokens.test.ts index d07fc5ad..47f2cd18 100644 --- a/packages/ai/test/total-tokens.test.ts +++ b/packages/ai/test/total-tokens.test.ts @@ -248,7 +248,7 @@ describe("totalTokens field", () => { "grok-3-fast - should return totalTokens equal to sum of components", { retry: 3, timeout: 60000 }, async () => { - const llm = getModel("xai", "grok-3-fast"); + const llm = getModel("xai", "grok-4.3"); console.log(`\nxAI / ${llm.id}:`); const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.XAI_API_KEY }); diff --git a/packages/ai/test/unicode-surrogate.test.ts b/packages/ai/test/unicode-surrogate.test.ts index f4ea7450..d623019c 100644 --- a/packages/ai/test/unicode-surrogate.test.ts +++ b/packages/ai/test/unicode-surrogate.test.ts @@ -450,7 +450,7 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => { }); describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider Unicode Handling", () => { - const llm = getModel("xai", "grok-3"); + const llm = getModel("xai", "grok-4.3"); it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => { await testEmojiInToolResults(llm); diff --git a/packages/ai/test/xai-oauth.test.ts b/packages/ai/test/xai-oauth.test.ts index fb71a621..6e707ef8 100644 --- a/packages/ai/test/xai-oauth.test.ts +++ b/packages/ai/test/xai-oauth.test.ts @@ -173,6 +173,51 @@ describe("xAI OAuth device flow", () => { expect(pollTimes).toEqual([startTime.getTime() + 5000]); }); + it("prefers verification_uri_complete when the server provides it", async () => { + vi.useFakeTimers(); + vi.stubGlobal( + "fetch", + vi.fn(async (input: unknown) => { + if (requestUrl(input) === "https://auth.x.ai/oauth2/device/code") { + return jsonResponse( + deviceCodeResponse({ + verification_uri_complete: "https://accounts.x.ai/oauth2/device?user_code=ABCD-1234", + }), + ); + } + return jsonResponse(tokenResponse()); + }), + ); + + const deviceCodes: DeviceCodeInfo[] = []; + const loginPromise = loginXaiForTest({ onDeviceCode: (info) => deviceCodes.push(info) }); + await vi.advanceTimersByTimeAsync(5000); + await loginPromise; + expect(deviceCodes).toEqual([ + { + userCode: "ABCD-1234", + verificationUri: "https://accounts.x.ai/oauth2/device?user_code=ABCD-1234", + intervalSeconds: 5, + expiresInSeconds: 900, + }, + ]); + }); + + it("rejects a non-https verification_uri_complete", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + jsonResponse( + deviceCodeResponse({ + verification_uri_complete: "http://accounts.x.ai/oauth2/device?user_code=ABCD-1234", + }), + ), + ), + ); + + await expect(loginXaiForTest({ onDeviceCode: () => {} })).rejects.toThrow("Untrusted verification URI"); + }); + it.each(["http://accounts.x.ai/oauth2/device", "file:///etc/passwd", "not a url"])( "rejects a non-https verification URI: %s", async (verificationUri) => { diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 3c6f0557..1f4dceaf 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -26,7 +26,7 @@ export const defaultModelPerProvider: Record = { "github-copilot": "gpt-5.4", openrouter: "moonshotai/kimi-k2.6", "vercel-ai-gateway": "zai/glm-5.1", - xai: "grok-4.20-0309-reasoning", + xai: "grok-4.5", groq: "openai/gpt-oss-120b", cerebras: "zai-glm-4.7", zai: "glm-5.1", diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index f9545f33..caddb919 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -4877,7 +4877,10 @@ export class InteractiveMode { } private showLoginAuthTypeSelector(providerOptions?: AuthSelectorProvider[]): void { - const subscriptionLabel = "Sign in with an account"; + const oauthProvider = providerOptions?.find((provider) => provider.authType === "oauth"); + const oauthLoginLabel = + oauthProvider?.method && "loginLabel" in oauthProvider.method ? oauthProvider.method.loginLabel : undefined; + const subscriptionLabel = oauthLoginLabel ?? "Sign in with an account"; const apiKeyLabel = "Sign in with an API key"; const availableAuthTypes = providerOptions ? new Set(providerOptions.map((provider) => provider.authType))