feat(xai): prefilled OAuth device link, SuperGrok login label, trimmed model list (#6734)
- Use verification_uri_complete from the xAI device-code response so the login link opens with the user code prefilled (https-validated) - Add OAuthAuth.loginLabel and show "Sign in with SuperGrok or X Premium" in the xAI auth-type selector (threaded through lazyOAuth) - Remove grok-3, grok-3-fast, grok-4.20 variants and grok-code-fast-1 from the built-in xAI catalog and default to grok-4.5 Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
c2c32febb0
commit
a01baaaea7
@@ -31,7 +31,7 @@ export function envApiKeyAuth(name: string, envVars: readonly string[]): ApiKeyA
|
|||||||
* of bundles by loading through a bundler-opaque dynamic import (variable
|
* of bundles by loading through a bundler-opaque dynamic import (variable
|
||||||
* specifier, see the bedrock lazy wrapper).
|
* specifier, see the bedrock lazy wrapper).
|
||||||
*/
|
*/
|
||||||
export function lazyOAuth(input: { name: string; load: () => Promise<OAuthAuth> }): OAuthAuth {
|
export function lazyOAuth(input: { name: string; loginLabel?: string; load: () => Promise<OAuthAuth> }): OAuthAuth {
|
||||||
let promise: Promise<OAuthAuth> | undefined;
|
let promise: Promise<OAuthAuth> | undefined;
|
||||||
const loaded = () => {
|
const loaded = () => {
|
||||||
promise ??= input.load();
|
promise ??= input.load();
|
||||||
@@ -39,6 +39,7 @@ export function lazyOAuth(input: { name: string; load: () => Promise<OAuthAuth>
|
|||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
name: input.name,
|
name: input.name,
|
||||||
|
loginLabel: input.loginLabel,
|
||||||
login: async (interaction) => (await loaded()).login(interaction),
|
login: async (interaction) => (await loaded()).login(interaction),
|
||||||
refresh: async (credential) => (await loaded()).refresh(credential),
|
refresh: async (credential) => (await loaded()).refresh(credential),
|
||||||
toAuth: async (credential) => (await loaded()).toAuth(credential),
|
toAuth: async (credential) => (await loaded()).toAuth(credential),
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type XaiDeviceCode = {
|
|||||||
deviceCode: string;
|
deviceCode: string;
|
||||||
userCode: string;
|
userCode: string;
|
||||||
verificationUri: string;
|
verificationUri: string;
|
||||||
|
verificationUriComplete?: string;
|
||||||
intervalSeconds?: number;
|
intervalSeconds?: number;
|
||||||
expiresInSeconds: number;
|
expiresInSeconds: number;
|
||||||
};
|
};
|
||||||
@@ -110,10 +111,15 @@ function parseDeviceCode(body: JsonObject): XaiDeviceCode {
|
|||||||
const interval = body.interval;
|
const interval = body.interval;
|
||||||
const intervalSeconds =
|
const intervalSeconds =
|
||||||
typeof interval === "number" && Number.isFinite(interval) && interval > 0 ? interval : undefined;
|
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 {
|
return {
|
||||||
deviceCode: requiredString(body, "device_code"),
|
deviceCode: requiredString(body, "device_code"),
|
||||||
userCode: requiredString(body, "user_code"),
|
userCode: requiredString(body, "user_code"),
|
||||||
verificationUri: validateVerificationUri(requiredString(body, "verification_uri")),
|
verificationUri: validateVerificationUri(requiredString(body, "verification_uri")),
|
||||||
|
verificationUriComplete,
|
||||||
intervalSeconds,
|
intervalSeconds,
|
||||||
expiresInSeconds: positiveNumber(body, "expires_in"),
|
expiresInSeconds: positiveNumber(body, "expires_in"),
|
||||||
};
|
};
|
||||||
@@ -197,7 +203,7 @@ async function loginXai(interaction: AuthInteraction): Promise<OAuthCredential>
|
|||||||
interaction.notify({
|
interaction.notify({
|
||||||
type: "device_code",
|
type: "device_code",
|
||||||
userCode: device.userCode,
|
userCode: device.userCode,
|
||||||
verificationUri: device.verificationUri,
|
verificationUri: device.verificationUriComplete ?? device.verificationUri,
|
||||||
intervalSeconds: device.intervalSeconds,
|
intervalSeconds: device.intervalSeconds,
|
||||||
expiresInSeconds: device.expiresInSeconds,
|
expiresInSeconds: device.expiresInSeconds,
|
||||||
});
|
});
|
||||||
@@ -222,6 +228,7 @@ async function refreshXaiToken(refreshToken: string, signal?: AbortSignal): Prom
|
|||||||
|
|
||||||
export const xaiOAuth: OAuthAuth = {
|
export const xaiOAuth: OAuthAuth = {
|
||||||
name: "xAI (Grok/X subscription)",
|
name: "xAI (Grok/X subscription)",
|
||||||
|
loginLabel: "Sign in with SuperGrok or X Premium",
|
||||||
login: loginXai,
|
login: loginXai,
|
||||||
refresh: (credential, signal) => refreshXaiToken(credential.refresh, signal),
|
refresh: (credential, signal) => refreshXaiToken(credential.refresh, signal),
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,9 @@ export interface OAuthAuth {
|
|||||||
/** Display name, e.g. "Anthropic (Claude Pro/Max)". */
|
/** Display name, e.g. "Anthropic (Claude Pro/Max)". */
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
/** Selector label for the subscription login option, e.g. "Sign in with SuperGrok or X Premium". */
|
||||||
|
loginLabel?: string;
|
||||||
|
|
||||||
login(interaction: AuthInteraction): Promise<OAuthCredential>;
|
login(interaction: AuthInteraction): Promise<OAuthCredential>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,78 +4,6 @@
|
|||||||
import type { Model } from "../types.ts";
|
import type { Model } from "../types.ts";
|
||||||
|
|
||||||
export const XAI_MODELS = {
|
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": {
|
"grok-4.3": {
|
||||||
id: "grok-4.3",
|
id: "grok-4.3",
|
||||||
name: "Grok 4.3",
|
name: "Grok 4.3",
|
||||||
@@ -131,22 +59,4 @@ export const XAI_MODELS = {
|
|||||||
contextWindow: 256000,
|
contextWindow: 256000,
|
||||||
maxTokens: 256000,
|
maxTokens: 256000,
|
||||||
} satisfies Model<"openai-completions">,
|
} 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;
|
} as const;
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ export function xaiProvider(): Provider<"openai-completions" | "openai-responses
|
|||||||
baseUrl: "https://api.x.ai/v1",
|
baseUrl: "https://api.x.ai/v1",
|
||||||
auth: {
|
auth: {
|
||||||
apiKey: envApiKeyAuth("xAI API key", ["XAI_API_KEY"]),
|
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),
|
models: Object.values(XAI_MODELS),
|
||||||
api: {
|
api: {
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ describe("Context overflow error handling", () => {
|
|||||||
|
|
||||||
describe.skipIf(!process.env.XAI_API_KEY)("xAI", () => {
|
describe.skipIf(!process.env.XAI_API_KEY)("xAI", () => {
|
||||||
it("grok-3-fast - should detect overflow via isContextOverflow", async () => {
|
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!);
|
const result = await testContextOverflow(model, process.env.XAI_API_KEY!);
|
||||||
logResult(result);
|
logResult(result);
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ describe("AI Providers Empty Message Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider Empty Messages", () => {
|
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 () => {
|
it("should handle empty content array", { retry: 3, timeout: 30000 }, async () => {
|
||||||
await testEmptyMessage(llm);
|
await testEmptyMessage(llm);
|
||||||
|
|||||||
@@ -543,7 +543,7 @@ describe("Generate E2E Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider (grok-code-fast-1 via OpenAI Completions)", () => {
|
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 () => {
|
it("should complete basic text generation", { retry: 3 }, async () => {
|
||||||
await basicTextGeneration(llm);
|
await basicTextGeneration(llm);
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ describe("Token Statistics on Abort", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider", () => {
|
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 () => {
|
it("should include token stats when aborted mid-stream", { retry: 3, timeout: 30000 }, async () => {
|
||||||
await testTokensOnAbort(llm);
|
await testTokensOnAbort(llm);
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ describe("Tool Call Without Result Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider", () => {
|
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 () => {
|
it("should filter out tool calls without corresponding tool results", { retry: 3, timeout: 30000 }, async () => {
|
||||||
await testToolCallWithoutResult(model);
|
await testToolCallWithoutResult(model);
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ describe("totalTokens field", () => {
|
|||||||
"grok-3-fast - should return totalTokens equal to sum of components",
|
"grok-3-fast - should return totalTokens equal to sum of components",
|
||||||
{ retry: 3, timeout: 60000 },
|
{ retry: 3, timeout: 60000 },
|
||||||
async () => {
|
async () => {
|
||||||
const llm = getModel("xai", "grok-3-fast");
|
const llm = getModel("xai", "grok-4.3");
|
||||||
|
|
||||||
console.log(`\nxAI / ${llm.id}:`);
|
console.log(`\nxAI / ${llm.id}:`);
|
||||||
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.XAI_API_KEY });
|
const { first, second } = await testTotalTokensWithCache(llm, { apiKey: process.env.XAI_API_KEY });
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ describe("AI Providers Unicode Surrogate Pair Tests", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider Unicode Handling", () => {
|
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 () => {
|
it("should handle emoji in tool results", { retry: 3, timeout: 30000 }, async () => {
|
||||||
await testEmojiInToolResults(llm);
|
await testEmojiInToolResults(llm);
|
||||||
|
|||||||
@@ -173,6 +173,51 @@ describe("xAI OAuth device flow", () => {
|
|||||||
expect(pollTimes).toEqual([startTime.getTime() + 5000]);
|
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"])(
|
it.each(["http://accounts.x.ai/oauth2/device", "file:///etc/passwd", "not a url"])(
|
||||||
"rejects a non-https verification URI: %s",
|
"rejects a non-https verification URI: %s",
|
||||||
async (verificationUri) => {
|
async (verificationUri) => {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
|||||||
"github-copilot": "gpt-5.4",
|
"github-copilot": "gpt-5.4",
|
||||||
openrouter: "moonshotai/kimi-k2.6",
|
openrouter: "moonshotai/kimi-k2.6",
|
||||||
"vercel-ai-gateway": "zai/glm-5.1",
|
"vercel-ai-gateway": "zai/glm-5.1",
|
||||||
xai: "grok-4.20-0309-reasoning",
|
xai: "grok-4.5",
|
||||||
groq: "openai/gpt-oss-120b",
|
groq: "openai/gpt-oss-120b",
|
||||||
cerebras: "zai-glm-4.7",
|
cerebras: "zai-glm-4.7",
|
||||||
zai: "glm-5.1",
|
zai: "glm-5.1",
|
||||||
|
|||||||
@@ -4877,7 +4877,10 @@ export class InteractiveMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showLoginAuthTypeSelector(providerOptions?: AuthSelectorProvider[]): void {
|
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 apiKeyLabel = "Sign in with an API key";
|
||||||
const availableAuthTypes = providerOptions
|
const availableAuthTypes = providerOptions
|
||||||
? new Set(providerOptions.map((provider) => provider.authType))
|
? new Set(providerOptions.map((provider) => provider.authType))
|
||||||
|
|||||||
Reference in New Issue
Block a user