feat(xai): add device code oauth login

This commit is contained in:
FullerStackDev
2026-05-18 23:50:33 -06:00
committed by Ayaan Zaidi
parent ddeaebfc68
commit 896fd13b1c
6 changed files with 464 additions and 10 deletions

View File

@@ -12,11 +12,12 @@ OpenClaw ships a bundled `xai` provider plugin for Grok models.
<Steps>
<Step title="Choose auth">
Use either an API key from the [xAI console](https://console.x.ai/) or
xAI OAuth browser sign-in with an eligible xAI account. OAuth does not
require an xAI API key, and OpenClaw does not require the Grok Build app.
xAI may still label the consent app as Grok Build because OpenClaw uses
xAI's shared OAuth client.
Use either an API key from the [xAI console](https://console.x.ai/),
xAI OAuth browser sign-in with an eligible xAI account, or xAI device-code
sign-in for remote/VPS hosts where a localhost browser callback is awkward.
OAuth does not require an xAI API key, and OpenClaw does not require the
Grok Build app. xAI may still label the consent app as Grok Build because
OpenClaw uses xAI's shared OAuth client.
</Step>
<Step title="Sign in">
Set `XAI_API_KEY`, run the API-key wizard, or start the OAuth flow:
@@ -24,7 +25,9 @@ OpenClaw ships a bundled `xai` provider plugin for Grok models.
```bash
openclaw onboard --auth-choice xai-api-key
openclaw onboard --auth-choice xai-oauth
openclaw onboard --auth-choice xai-device-code
openclaw models auth login --provider xai --method oauth
openclaw models auth login --provider xai --device-code
```
</Step>
@@ -40,7 +43,8 @@ OpenClaw ships a bundled `xai` provider plugin for Grok models.
<Note>
OpenClaw uses the xAI Responses API as the bundled xAI transport. The same
credential from `openclaw onboard --auth-choice xai-api-key` or
`openclaw onboard --auth-choice xai-oauth` can also power first-class
`openclaw onboard --auth-choice xai-oauth` /
`openclaw onboard --auth-choice xai-device-code` can also power first-class
`x_search`, remote `code_execution`, and xAI image/video generation.
Speech and transcription currently require `XAI_API_KEY` or provider config.
`XAI_API_KEY` or plugin web-search config can power Grok-backed `web_search` too.
@@ -51,6 +55,12 @@ and, by default, `x_search` through an operator xAI Responses proxy.
`code_execution` tuning lives under `plugins.entries.xai.config.codeExecution`.
</Note>
<Tip>
Use `xai-device-code` when signing in from SSH, Docker, or a VPS. OpenClaw
prints an xAI URL and short code; finish sign-in in any local browser while the
remote process polls xAI for the completed token exchange.
</Tip>
## Built-in catalog
OpenClaw includes the current xAI chat models out of the box, ordered newest

View File

@@ -60,6 +60,15 @@ function requireEntry<T extends { id?: string }>(entries: T[], id: string): T {
}
describe("xai provider plugin", () => {
it("exposes OAuth and device-code auth choices", async () => {
const provider = await registerSingleProviderPlugin(plugin);
expect(provider.auth?.map((method) => method.id)).toEqual(["api-key", "oauth", "device-code"]);
const deviceCode = provider.auth?.find((method) => method.id === "device-code");
expect(deviceCode?.kind).toBe("device_code");
expect(deviceCode?.wizard?.choiceId).toBe("xai-device-code");
});
it("registers xAI speech providers for batch and streaming STT", async () => {
const { mediaProviders, realtimeTranscriptionProviders } = await registerProviderPlugin({
plugin,

View File

@@ -31,7 +31,11 @@ import {
buildMissingXSearchApiKeyPayload,
createXSearchToolDefinition,
} from "./x-search-tool-shared.js";
import { createXaiOAuthAuthMethod, refreshXaiOAuthCredential } from "./xai-oauth.js";
import {
createXaiDeviceCodeAuthMethod,
createXaiOAuthAuthMethod,
refreshXaiOAuthCredential,
} from "./xai-oauth.js";
const PROVIDER_ID = "xai";
type CodeExecutionModule = typeof import("./code-execution.js");
@@ -183,7 +187,7 @@ export default defineSingleProviderPluginEntry({
},
},
],
extraAuth: [createXaiOAuthAuthMethod()],
extraAuth: [createXaiOAuthAuthMethod(), createXaiDeviceCodeAuthMethod()],
catalog: {
buildProvider: buildXaiProvider,
},

View File

@@ -62,6 +62,17 @@
"groupLabel": "xAI (Grok)",
"groupHint": "API key or browser OAuth",
"onboardingFeatured": true
},
{
"provider": "xai",
"method": "device-code",
"choiceId": "xai-device-code",
"choiceLabel": "xAI device code",
"choiceHint": "Remote-friendly browser sign-in without a localhost callback",
"groupId": "xai",
"groupLabel": "xAI (Grok)",
"groupHint": "API key or browser OAuth",
"onboardingFeatured": true
}
],
"uiHints": {

View File

@@ -1,9 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
buildXaiOAuthAuthorizationCodeTokenBody,
buildXaiOAuthAuthorizeUrl,
fetchXaiOAuthDiscovery,
isTrustedXaiOAuthEndpoint,
loginXaiDeviceCode,
refreshXaiOAuthCredential,
XAI_OAUTH_CALLBACK_CORS_ORIGIN_ALLOWLIST,
XAI_OAUTH_CALLBACK_PORT,
@@ -20,7 +21,26 @@ function jsonResponse(value: unknown, init?: ResponseInit): Response {
});
}
function createJwt(payload: Record<string, unknown>): string {
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
const body = Buffer.from(JSON.stringify(payload)).toString("base64url");
return `${header}.${body}.signature`;
}
function requireStringBody(init: RequestInit | undefined): string {
if (typeof init?.body !== "string") {
throw new Error("expected request body to be a string");
}
return init.body;
}
describe("xAI OAuth", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.unstubAllEnvs();
vi.useRealTimers();
});
it("accepts only trusted xAI OAuth endpoints", () => {
expect(isTrustedXaiOAuthEndpoint("https://auth.x.ai/oauth2/token")).toBe(true);
expect(isTrustedXaiOAuthEndpoint("https://accounts.x.ai/oauth2/token")).toBe(true);
@@ -80,12 +100,14 @@ describe("xAI OAuth", () => {
const fetchImpl = vi.fn(async () =>
jsonResponse({
authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code",
token_endpoint: "https://auth.x.ai/oauth2/token",
}),
) as unknown as typeof fetch;
await expect(fetchXaiOAuthDiscovery({ fetchImpl })).resolves.toEqual({
authorizationEndpoint: "https://auth.x.ai/oauth2/authorize",
deviceAuthorizationEndpoint: "https://auth.x.ai/oauth2/device/code",
tokenEndpoint: "https://auth.x.ai/oauth2/token",
});
@@ -99,6 +121,7 @@ describe("xAI OAuth", () => {
const poisonedFetch = vi.fn(async () =>
jsonResponse({
authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code",
token_endpoint: "https://evil.test/oauth2/token",
}),
) as unknown as typeof fetch;
@@ -143,4 +166,98 @@ describe("xAI OAuth", () => {
expect(refreshed.expires).toBe(121_000);
vi.unstubAllEnvs();
});
it("logs in with xAI device code without a localhost callback", async () => {
vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
const progress = {
update: vi.fn(),
stop: vi.fn(),
};
const fetchImpl = vi
.fn<typeof fetch>()
.mockResolvedValueOnce(
jsonResponse({
authorization_endpoint: "https://auth.x.ai/oauth2/authorize",
device_authorization_endpoint: "https://auth.x.ai/oauth2/device/code",
token_endpoint: "https://auth.x.ai/oauth2/token",
}),
)
.mockResolvedValueOnce(
jsonResponse({
device_code: "device-code-1",
user_code: "ABCD-1234",
verification_uri: "https://accounts.x.ai/oauth2/device",
verification_uri_complete: "https://accounts.x.ai/oauth2/device?user_code=ABCD-1234",
expires_in: 900,
interval: 5,
}),
)
.mockResolvedValueOnce(
jsonResponse({
access_token: createJwt({ exp: 4, sub: "acct-1" }),
refresh_token: "refresh-1",
id_token: createJwt({
sub: "acct-1",
email: "dev@example.com",
name: "Dev User",
}),
expires_in: 120,
}),
);
vi.stubGlobal("fetch", fetchImpl);
const ctx = {
config: {},
isRemote: true,
openUrl: vi.fn(async () => {}),
prompter: {
progress: vi.fn(() => progress),
note: vi.fn(async () => {}),
},
runtime: {
log: vi.fn(),
},
oauth: {},
};
const result = await loginXaiDeviceCode(ctx as never);
expect(ctx.openUrl).not.toHaveBeenCalled();
expect(ctx.prompter.note).toHaveBeenCalledWith(
expect.stringContaining("ABCD-1234"),
"xAI device code",
);
const remoteLog = ctx.runtime.log.mock.calls[0]?.[0];
expect(remoteLog).toContain("https://accounts.x.ai/oauth2/device");
expect(remoteLog).not.toContain("ABCD-1234");
const deviceRequest = fetchImpl.mock.calls[1]?.[1];
expect(deviceRequest?.method).toBe("POST");
const deviceBody = requireStringBody(deviceRequest);
expect(deviceBody).toContain(`client_id=${encodeURIComponent(XAI_OAUTH_CLIENT_ID)}`);
expect(deviceBody).toContain(`scope=${encodeURIComponent(XAI_OAUTH_SCOPE)}`);
const tokenRequest = fetchImpl.mock.calls[2]?.[1];
expect(tokenRequest?.method).toBe("POST");
const tokenBody = requireStringBody(tokenRequest);
expect(tokenBody).toContain(
"grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code",
);
expect(tokenBody).toContain("device_code=device-code-1");
const credential = result.profiles[0]?.credential as Record<string, unknown> | undefined;
expect(credential).toMatchObject({
type: "oauth",
provider: "xai",
refresh: "refresh-1",
email: "dev@example.com",
displayName: "Dev User",
tokenEndpoint: "https://auth.x.ai/oauth2/token",
deviceAuthorizationEndpoint: "https://auth.x.ai/oauth2/device/code",
issuer: "https://auth.x.ai",
authFlow: "device-code",
accountId: "acct-1",
});
expect(credential?.access).toEqual(expect.any(String));
expect(progress.update).toHaveBeenCalledWith("Waiting for xAI device authorization...");
expect(progress.stop).toHaveBeenCalledWith("xAI device code complete");
});
});

View File

@@ -15,6 +15,8 @@ import { xaiUserAgent } from "./src/xai-user-agent.js";
const PROVIDER_ID = "xai";
export const XAI_OAUTH_METHOD_ID = "oauth";
export const XAI_OAUTH_CHOICE_ID = "xai-oauth";
export const XAI_DEVICE_CODE_METHOD_ID = "device-code";
export const XAI_DEVICE_CODE_CHOICE_ID = "xai-device-code";
export const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
export const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
export const XAI_OAUTH_ISSUER = "https://auth.x.ai";
@@ -29,9 +31,14 @@ export const XAI_OAUTH_CALLBACK_CORS_ORIGIN_ALLOWLIST = ["auth.x.ai", "accounts.
const XAI_OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
const XAI_OAUTH_FETCH_TIMEOUT_MS = 30 * 1000;
const XAI_DEVICE_CODE_DEFAULT_INTERVAL_MS = 5 * 1000;
const XAI_DEVICE_CODE_MIN_INTERVAL_MS = 1 * 1000;
const XAI_DEVICE_CODE_SLOW_DOWN_INCREMENT_MS = 5 * 1000;
const XAI_DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
type XaiOAuthDiscovery = {
authorizationEndpoint: string;
deviceAuthorizationEndpoint: string;
tokenEndpoint: string;
};
@@ -53,6 +60,20 @@ type XaiOAuthFetchOptions = {
now?: () => number;
};
type XaiDeviceCodeResponse = {
deviceCode: string;
userCode: string;
verificationUri: string;
verificationUriComplete?: string;
expiresInMs: number;
intervalMs: number;
};
type XaiOAuthErrorResponse = {
error?: string;
errorDescription?: string;
};
function getFetchImpl(fetchImpl?: typeof fetch): typeof fetch {
return fetchImpl ?? fetch;
}
@@ -110,8 +131,13 @@ export async function fetchXaiOAuthDiscovery(
});
const json = readStringRecord(await readJsonResponse(response, "xAI OAuth discovery"));
const authorizationEndpoint = json.authorization_endpoint;
const deviceAuthorizationEndpoint = json.device_authorization_endpoint;
const tokenEndpoint = json.token_endpoint;
if (typeof authorizationEndpoint !== "string" || typeof tokenEndpoint !== "string") {
if (
typeof authorizationEndpoint !== "string" ||
typeof deviceAuthorizationEndpoint !== "string" ||
typeof tokenEndpoint !== "string"
) {
throw new Error("xAI OAuth discovery response is missing endpoints");
}
return {
@@ -119,6 +145,10 @@ export async function fetchXaiOAuthDiscovery(
authorizationEndpoint,
"authorization endpoint",
),
deviceAuthorizationEndpoint: requireTrustedXaiOAuthEndpoint(
deviceAuthorizationEndpoint,
"device authorization endpoint",
),
tokenEndpoint: requireTrustedXaiOAuthEndpoint(tokenEndpoint, "token endpoint"),
};
}
@@ -175,6 +205,19 @@ function normalizeExpires(value: unknown, now: () => number): number | undefined
return now() + seconds * 1000;
}
function normalizePositiveSecondsToMs(value: unknown): number | undefined {
const seconds =
typeof value === "number"
? value
: typeof value === "string"
? Number.parseFloat(value)
: Number.NaN;
if (!Number.isFinite(seconds) || seconds <= 0) {
return undefined;
}
return Math.trunc(seconds * 1000);
}
function parseXaiOAuthTokenResponse(
value: unknown,
now: () => number,
@@ -222,6 +265,28 @@ function deriveExpiresFromJwt(token: string | undefined): number | undefined {
return exp * 1000;
}
function parseXaiOAuthErrorResponse(value: unknown): XaiOAuthErrorResponse {
const json = readStringRecord(value);
const error = typeof json.error === "string" ? json.error : undefined;
const errorDescription =
typeof json.error_description === "string" ? json.error_description : undefined;
return {
...(error ? { error } : {}),
...(errorDescription ? { errorDescription } : {}),
};
}
function formatXaiOAuthError(params: { context: string; status: number; body: unknown }): string {
const error = parseXaiOAuthErrorResponse(params.body);
if (error.error && error.errorDescription) {
return `${params.context} failed (${params.status}): ${error.error} (${error.errorDescription})`;
}
if (error.error) {
return `${params.context} failed (${params.status}): ${error.error}`;
}
return `${params.context} failed (${params.status})`;
}
async function exchangeXaiOAuthToken(
params: {
tokenEndpoint: string;
@@ -250,6 +315,147 @@ async function exchangeXaiOAuthToken(
);
}
async function requestXaiDeviceCode(
params: {
deviceAuthorizationEndpoint: string;
} & XaiOAuthFetchOptions,
): Promise<XaiDeviceCodeResponse> {
const response = await getFetchImpl(params.fetchImpl)(
requireTrustedXaiOAuthEndpoint(
params.deviceAuthorizationEndpoint,
"device authorization endpoint",
),
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
"User-Agent": xaiUserAgent(),
},
body: toFormUrlEncoded({
client_id: XAI_OAUTH_CLIENT_ID,
scope: XAI_OAUTH_SCOPE,
}),
signal: AbortSignal.timeout(XAI_OAUTH_FETCH_TIMEOUT_MS),
},
);
const json = readStringRecord(await readJsonResponse(response, "xAI device code request"));
const deviceCode = json.device_code;
const userCode = json.user_code;
const verificationUri = json.verification_uri;
const verificationUriComplete = json.verification_uri_complete;
if (
typeof deviceCode !== "string" ||
deviceCode.trim().length === 0 ||
typeof userCode !== "string" ||
userCode.trim().length === 0 ||
typeof verificationUri !== "string" ||
verificationUri.trim().length === 0
) {
throw new Error(
"xAI device code response is missing device_code, user_code, or verification_uri",
);
}
const trustedVerificationUri = requireTrustedXaiOAuthEndpoint(
verificationUri,
"device verification URI",
);
const trustedVerificationUriComplete =
typeof verificationUriComplete === "string" && verificationUriComplete.trim().length > 0
? requireTrustedXaiOAuthEndpoint(verificationUriComplete, "complete device verification URI")
: undefined;
return {
deviceCode,
userCode,
verificationUri: trustedVerificationUri,
...(trustedVerificationUriComplete
? { verificationUriComplete: trustedVerificationUriComplete }
: {}),
expiresInMs: normalizePositiveSecondsToMs(json.expires_in) ?? XAI_OAUTH_TIMEOUT_MS,
intervalMs: normalizePositiveSecondsToMs(json.interval) ?? XAI_DEVICE_CODE_DEFAULT_INTERVAL_MS,
};
}
function resolveNextXaiDeviceCodePollDelayMs(intervalMs: number, deadlineMs: number): number {
const remainingMs = Math.max(0, deadlineMs - Date.now());
return Math.min(Math.max(intervalMs, XAI_DEVICE_CODE_MIN_INTERVAL_MS), remainingMs);
}
async function pollXaiDeviceCodeToken(
params: {
tokenEndpoint: string;
deviceCode: string;
expiresInMs: number;
intervalMs: number;
} & XaiOAuthFetchOptions,
): Promise<XaiOAuthTokenResponse> {
const fetchImpl = getFetchImpl(params.fetchImpl);
const deadlineMs = Date.now() + params.expiresInMs;
let intervalMs = params.intervalMs;
while (Date.now() < deadlineMs) {
const response = await fetchImpl(
requireTrustedXaiOAuthEndpoint(params.tokenEndpoint, "token endpoint"),
{
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
"User-Agent": xaiUserAgent(),
},
body: toFormUrlEncoded({
grant_type: XAI_DEVICE_CODE_GRANT_TYPE,
client_id: XAI_OAUTH_CLIENT_ID,
device_code: params.deviceCode,
}),
signal: AbortSignal.timeout(XAI_OAUTH_FETCH_TIMEOUT_MS),
},
);
let body: unknown;
try {
body = await response.json();
} catch {
body = null;
}
if (response.ok) {
return parseXaiOAuthTokenResponse(body, params.now ?? Date.now, {
requireRefreshToken: true,
});
}
const error = parseXaiOAuthErrorResponse(body).error;
if (error === "authorization_pending") {
await new Promise((resolve) =>
setTimeout(resolve, resolveNextXaiDeviceCodePollDelayMs(intervalMs, deadlineMs)),
);
continue;
}
if (error === "slow_down") {
intervalMs += XAI_DEVICE_CODE_SLOW_DOWN_INCREMENT_MS;
await new Promise((resolve) =>
setTimeout(resolve, resolveNextXaiDeviceCodePollDelayMs(intervalMs, deadlineMs)),
);
continue;
}
if (error === "access_denied" || error === "authorization_denied") {
throw new Error("xAI device authorization was denied");
}
if (error === "expired_token") {
throw new Error("xAI device code expired. Re-run the login.");
}
throw new Error(
formatXaiOAuthError({
context: "xAI device token exchange",
status: response.status,
body,
}),
);
}
throw new Error("xAI device authorization timed out");
}
function decodeJwtPayload(token: string | undefined): Record<string, unknown> {
if (!token) {
return {};
@@ -364,6 +570,84 @@ export async function loginXaiOAuth(ctx: ProviderAuthContext): Promise<ProviderA
}
}
async function noteXaiDeviceCode(
ctx: ProviderAuthContext,
deviceCode: XaiDeviceCodeResponse,
): Promise<void> {
const expiresInMinutes = Math.max(1, Math.round(deviceCode.expiresInMs / 60_000));
await ctx.prompter.note(
[
ctx.isRemote
? "Open this URL in your LOCAL browser and enter the code below."
: "Open this URL in your browser and enter the code below.",
`URL: ${deviceCode.verificationUriComplete ?? deviceCode.verificationUri}`,
`Code: ${deviceCode.userCode}`,
`Code expires in ${expiresInMinutes} minutes. Never share it.`,
].join("\n"),
"xAI device code",
);
}
export async function loginXaiDeviceCode(ctx: ProviderAuthContext): Promise<ProviderAuthResult> {
const progress = ctx.prompter.progress("Starting xAI device code flow...");
try {
const discovery = await fetchXaiOAuthDiscovery();
progress.update("Requesting xAI device code...");
const deviceCode = await requestXaiDeviceCode({
deviceAuthorizationEndpoint: discovery.deviceAuthorizationEndpoint,
});
await noteXaiDeviceCode(ctx, deviceCode);
const browserUrl = deviceCode.verificationUriComplete ?? deviceCode.verificationUri;
const logUrl = deviceCode.verificationUri;
if (ctx.isRemote) {
ctx.runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${logUrl}\n`);
} else {
try {
await ctx.openUrl(browserUrl);
ctx.runtime.log(`Open: ${logUrl}`);
} catch {
ctx.runtime.log(`Open manually: ${logUrl}`);
}
}
progress.update("Waiting for xAI device authorization...");
const tokens = await pollXaiDeviceCodeToken({
tokenEndpoint: discovery.tokenEndpoint,
deviceCode: deviceCode.deviceCode,
expiresInMs: deviceCode.expiresInMs,
intervalMs: deviceCode.intervalMs,
});
const identity = resolveXaiOAuthIdentity(tokens);
progress.stop("xAI device code complete");
return buildOauthProviderAuthResult({
providerId: PROVIDER_ID,
defaultModel: XAI_DEFAULT_MODEL_REF,
access: tokens.accessToken,
refresh: tokens.refreshToken,
expires: tokens.expires,
email: identity.email,
displayName: identity.displayName,
profileName: identity.email ?? identity.accountId,
configPatch: applyXaiConfig(ctx.config),
credentialExtra: {
tokenEndpoint: discovery.tokenEndpoint,
deviceAuthorizationEndpoint: discovery.deviceAuthorizationEndpoint,
issuer: XAI_OAUTH_ISSUER,
authFlow: "device-code",
...(tokens.idToken ? { idToken: tokens.idToken } : {}),
...(identity.accountId ? { accountId: identity.accountId } : {}),
},
notes: [
"xAI device code login uses your xAI account entitlement without requiring a localhost callback.",
"xAI may label the consent app as Grok Build because OpenClaw uses xAI's shared OAuth client.",
],
});
} catch (err) {
progress.stop("xAI device code failed");
throw new Error(`xAI device code failed: ${formatErrorMessage(err)}`, { cause: err });
}
}
export async function refreshXaiOAuthCredential(
credential: OAuthCredential,
options: XaiOAuthFetchOptions = {},
@@ -420,3 +704,22 @@ export function createXaiOAuthAuthMethod(): ProviderAuthMethod {
run: async (ctx) => loginXaiOAuth(ctx),
};
}
export function createXaiDeviceCodeAuthMethod(): ProviderAuthMethod {
return {
id: XAI_DEVICE_CODE_METHOD_ID,
label: "xAI device code",
hint: "Remote-friendly browser sign-in without a localhost callback",
kind: "device_code",
wizard: {
choiceId: XAI_DEVICE_CODE_CHOICE_ID,
choiceLabel: "xAI device code",
choiceHint: "Remote-friendly browser sign-in without a localhost callback",
groupId: PROVIDER_ID,
groupLabel: "xAI (Grok)",
groupHint: "API key or browser OAuth",
methodId: XAI_DEVICE_CODE_METHOD_ID,
},
run: async (ctx) => loginXaiDeviceCode(ctx),
};
}