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
|
||||
* 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;
|
||||
const loaded = () => {
|
||||
promise ??= input.load();
|
||||
@@ -39,6 +39,7 @@ export function lazyOAuth(input: { name: string; load: () => Promise<OAuthAuth>
|
||||
};
|
||||
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),
|
||||
|
||||
@@ -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<OAuthCredential>
|
||||
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),
|
||||
|
||||
|
||||
@@ -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<OAuthCredential>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
|
||||
"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",
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user