From db31c0ccca922704185061aac89123c52970ea64 Mon Sep 17 00:00:00 2001 From: George Pickett Date: Thu, 5 Feb 2026 12:25:34 -0800 Subject: [PATCH] feat: add xAI Grok provider support --- src/cli/program/register.onboard.ts | 4 +- src/commands/auth-choice-options.test.ts | 10 ++ src/commands/auth-choice-options.ts | 10 +- src/commands/auth-choice.apply.ts | 3 + src/commands/auth-choice.apply.xai.ts | 86 ++++++++++++++++++ .../auth-choice.preferred-provider.ts | 1 + src/commands/auth-choice.test.ts | 54 +++++++++++ src/commands/onboard-auth.config-core.ts | 69 ++++++++++++++ src/commands/onboard-auth.credentials.ts | 13 +++ src/commands/onboard-auth.models.ts | 24 +++++ src/commands/onboard-auth.ts | 4 + .../onboard-non-interactive.xai.test.ts | 91 +++++++++++++++++++ .../local/auth-choice-inference.ts | 2 + .../local/auth-choice.ts | 25 +++++ src/commands/onboard-types.ts | 2 + 15 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 src/commands/auth-choice.apply.xai.ts create mode 100644 src/commands/onboard-non-interactive.xai.test.ts diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 995afbfdc..35cee3393 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -58,7 +58,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|cloudflare-ai-gateway-api-key|moonshot-api-key|moonshot-api-key-cn|kimi-code-api-key|synthetic-api-key|venice-api-key|gemini-api-key|zai-api-key|xiaomi-api-key|xai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", ) .option( "--token-provider ", @@ -86,6 +86,7 @@ export function registerOnboardCommand(program: Command) { .option("--synthetic-api-key ", "Synthetic API key") .option("--venice-api-key ", "Venice API key") .option("--opencode-zen-api-key ", "OpenCode Zen API key") + .option("--xai-api-key ", "xAI API key") .option("--gateway-port ", "Gateway port") .option("--gateway-bind ", "Gateway bind: loopback|tailnet|lan|auto|custom") .option("--gateway-auth ", "Gateway auth: token|password") @@ -140,6 +141,7 @@ export function registerOnboardCommand(program: Command) { syntheticApiKey: opts.syntheticApiKey as string | undefined, veniceApiKey: opts.veniceApiKey as string | undefined, opencodeZenApiKey: opts.opencodeZenApiKey as string | undefined, + xaiApiKey: opts.xaiApiKey as string | undefined, gatewayPort: typeof gatewayPort === "number" && Number.isFinite(gatewayPort) ? gatewayPort diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index 2ea1cf624..c0608f1ec 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -114,4 +114,14 @@ describe("buildAuthChoiceOptions", () => { expect(options.some((opt) => opt.value === "qwen-portal")).toBe(true); }); + + it("includes xAI auth choice", () => { + const store: AuthProfileStore = { version: 1, profiles: {} }; + const options = buildAuthChoiceOptions({ + store, + includeSkip: false, + }); + + expect(options.some((opt) => opt.value === "xai-api-key")).toBe(true); + }); }); diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index c3a281278..20a37a70f 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -22,7 +22,8 @@ export type AuthChoiceGroupId = | "minimax" | "synthetic" | "venice" - | "qwen"; + | "qwen" + | "xai"; export type AuthChoiceGroup = { value: AuthChoiceGroupId; @@ -37,6 +38,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint?: string; choices: AuthChoice[]; }[] = [ + { + value: "xai", + label: "xAI (Grok)", + hint: "API key", + choices: ["xai-api-key"], + }, { value: "openai", label: "OpenAI", @@ -149,6 +156,7 @@ export function buildAuthChoiceOptions(params: { options.push({ value: "chutes", label: "Chutes (OAuth)" }); options.push({ value: "openai-api-key", label: "OpenAI API key" }); options.push({ value: "openrouter-api-key", label: "OpenRouter API key" }); + options.push({ value: "xai-api-key", label: "xAI (Grok) API key" }); options.push({ value: "ai-gateway-api-key", label: "Vercel AI Gateway API key", diff --git a/src/commands/auth-choice.apply.ts b/src/commands/auth-choice.apply.ts index 53b22fdd4..103e60609 100644 --- a/src/commands/auth-choice.apply.ts +++ b/src/commands/auth-choice.apply.ts @@ -12,6 +12,7 @@ import { applyAuthChoiceMiniMax } from "./auth-choice.apply.minimax.js"; import { applyAuthChoiceOAuth } from "./auth-choice.apply.oauth.js"; import { applyAuthChoiceOpenAI } from "./auth-choice.apply.openai.js"; import { applyAuthChoiceQwenPortal } from "./auth-choice.apply.qwen-portal.js"; +import { applyAuthChoiceXAI } from "./auth-choice.apply.xai.js"; export type ApplyAuthChoiceParams = { authChoice: AuthChoice; @@ -27,6 +28,7 @@ export type ApplyAuthChoiceParams = { cloudflareAiGatewayAccountId?: string; cloudflareAiGatewayGatewayId?: string; cloudflareAiGatewayApiKey?: string; + xaiApiKey?: string; }; }; @@ -49,6 +51,7 @@ export async function applyAuthChoice( applyAuthChoiceGoogleGeminiCli, applyAuthChoiceCopilotProxy, applyAuthChoiceQwenPortal, + applyAuthChoiceXAI, ]; for (const handler of handlers) { diff --git a/src/commands/auth-choice.apply.xai.ts b/src/commands/auth-choice.apply.xai.ts new file mode 100644 index 000000000..197fcae86 --- /dev/null +++ b/src/commands/auth-choice.apply.xai.ts @@ -0,0 +1,86 @@ +import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; +import { resolveEnvApiKey } from "../agents/model-auth.js"; +import { + formatApiKeyPreview, + normalizeApiKeyInput, + validateApiKeyInput, +} from "./auth-choice.api-key.js"; +import { applyDefaultModelChoice } from "./auth-choice.default-model.js"; +import { + applyAuthProfileConfig, + applyXaiConfig, + applyXaiProviderConfig, + setXaiApiKey, + XAI_DEFAULT_MODEL_REF, +} from "./onboard-auth.js"; + +export async function applyAuthChoiceXAI( + params: ApplyAuthChoiceParams, +): Promise { + if (params.authChoice !== "xai-api-key") { + return null; + } + + let nextConfig = params.config; + let agentModelOverride: string | undefined; + const noteAgentModel = async (model: string) => { + if (!params.agentId) { + return; + } + await params.prompter.note( + `Default model set to ${model} for agent "${params.agentId}".`, + "Model configured", + ); + }; + + let hasCredential = false; + const optsKey = params.opts?.xaiApiKey?.trim(); + if (optsKey) { + await setXaiApiKey(normalizeApiKeyInput(optsKey), params.agentDir); + hasCredential = true; + } + + if (!hasCredential) { + const envKey = resolveEnvApiKey("xai"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing XAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setXaiApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + } + + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter xAI API key", + validate: validateApiKeyInput, + }); + await setXaiApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "xai:default", + provider: "xai", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: XAI_DEFAULT_MODEL_REF, + applyDefaultConfig: applyXaiConfig, + applyProviderConfig: applyXaiProviderConfig, + noteDefault: XAI_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + + return { config: nextConfig, agentModelOverride }; +} diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index ac530e169..e78f5ed27 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -30,6 +30,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { "minimax-api-lightning": "minimax", minimax: "lmstudio", "opencode-zen": "opencode", + "xai-api-key": "xai", "qwen-portal": "qwen-portal", "minimax-portal": "minimax-portal", }; diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 61acc9d0d..6079531ca 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -193,6 +193,60 @@ describe("applyAuthChoice", () => { expect(parsed.profiles?.["synthetic:default"]?.key).toBe("sk-synthetic-test"); }); + it("does not override the global default model when selecting xai-api-key without setDefaultModel", async () => { + tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); + process.env.OPENCLAW_STATE_DIR = tempStateDir; + process.env.OPENCLAW_AGENT_DIR = path.join(tempStateDir, "agent"); + process.env.PI_CODING_AGENT_DIR = process.env.OPENCLAW_AGENT_DIR; + + const text = vi.fn().mockResolvedValue("sk-xai-test"); + const select: WizardPrompter["select"] = vi.fn( + async (params) => params.options[0]?.value as never, + ); + const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []); + const prompter: WizardPrompter = { + intro: vi.fn(noopAsync), + outro: vi.fn(noopAsync), + note: vi.fn(noopAsync), + select, + multiselect, + text, + confirm: vi.fn(async () => false), + progress: vi.fn(() => ({ update: noop, stop: noop })), + }; + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn((code: number) => { + throw new Error(`exit:${code}`); + }), + }; + + const result = await applyAuthChoice({ + authChoice: "xai-api-key", + config: { agents: { defaults: { model: { primary: "openai/gpt-4o-mini" } } } }, + prompter, + runtime, + setDefaultModel: false, + agentId: "agent-1", + }); + + expect(text).toHaveBeenCalledWith(expect.objectContaining({ message: "Enter xAI API key" })); + expect(result.config.auth?.profiles?.["xai:default"]).toMatchObject({ + provider: "xai", + mode: "api_key", + }); + expect(result.config.agents?.defaults?.model?.primary).toBe("openai/gpt-4o-mini"); + expect(result.agentModelOverride).toBe("xai/grok-2-latest"); + + const authProfilePath = authProfilePathFor(requireAgentDir()); + const raw = await fs.readFile(authProfilePath, "utf8"); + const parsed = JSON.parse(raw) as { + profiles?: Record; + }; + expect(parsed.profiles?.["xai:default"]?.key).toBe("sk-xai-test"); + }); + it("sets default model when selecting github-copilot", async () => { tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-")); process.env.OPENCLAW_STATE_DIR = tempStateDir; diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 804035a91..13299915d 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -22,14 +22,18 @@ import { VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, XIAOMI_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, + XAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; import { buildMoonshotModelDefinition, + buildXaiModelDefinition, KIMI_CODING_MODEL_REF, MOONSHOT_BASE_URL, MOONSHOT_CN_BASE_URL, MOONSHOT_DEFAULT_MODEL_ID, MOONSHOT_DEFAULT_MODEL_REF, + XAI_BASE_URL, + XAI_DEFAULT_MODEL_ID, } from "./onboard-auth.models.js"; export function applyZaiConfig(cfg: OpenClawConfig): OpenClawConfig { @@ -588,6 +592,71 @@ export function applyVeniceConfig(cfg: OpenClawConfig): OpenClawConfig { }; } +export function applyXaiProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[XAI_DEFAULT_MODEL_REF] = { + ...models[XAI_DEFAULT_MODEL_REF], + alias: models[XAI_DEFAULT_MODEL_REF]?.alias ?? "Grok", + }; + + const providers = { ...cfg.models?.providers }; + const existingProvider = providers.xai; + const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : []; + const defaultModel = buildXaiModelDefinition(); + const hasDefaultModel = existingModels.some((model) => model.id === XAI_DEFAULT_MODEL_ID); + const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel]; + const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record< + string, + unknown + > as { apiKey?: string }; + const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined; + const normalizedApiKey = resolvedApiKey?.trim(); + providers.xai = { + ...existingProviderRest, + baseUrl: XAI_BASE_URL, + api: "openai-completions", + ...(normalizedApiKey ? { apiKey: normalizedApiKey } : {}), + models: mergedModels.length > 0 ? mergedModels : [defaultModel], + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + models: { + mode: cfg.models?.mode ?? "merge", + providers, + }, + }; +} + +export function applyXaiConfig(cfg: OpenClawConfig): OpenClawConfig { + const next = applyXaiProviderConfig(cfg); + const existingModel = next.agents?.defaults?.model; + return { + ...next, + agents: { + ...next.agents, + defaults: { + ...next.agents?.defaults, + model: { + ...(existingModel && "fallbacks" in (existingModel as Record) + ? { + fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks, + } + : undefined), + primary: XAI_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyAuthProfileConfig( cfg: OpenClawConfig, params: { diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index 86980906f..93c819239 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -2,6 +2,7 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai"; import { resolveOpenClawAgentDir } from "../agents/agent-paths.js"; import { upsertAuthProfile } from "../agents/auth-profiles.js"; export { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF } from "../agents/cloudflare-ai-gateway.js"; +export { XAI_DEFAULT_MODEL_REF } from "./onboard-auth.models.js"; const resolveAuthAgentDir = (agentDir?: string) => agentDir ?? resolveOpenClawAgentDir(); @@ -203,3 +204,15 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export async function setXaiApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "xai:default", + credential: { + type: "api_key", + provider: "xai", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.models.ts b/src/commands/onboard-auth.models.ts index a706c9a03..043ba93e7 100644 --- a/src/commands/onboard-auth.models.ts +++ b/src/commands/onboard-auth.models.ts @@ -92,3 +92,27 @@ export function buildMoonshotModelDefinition(): ModelDefinitionConfig { maxTokens: MOONSHOT_DEFAULT_MAX_TOKENS, }; } + +export const XAI_BASE_URL = "https://api.x.ai/v1"; +export const XAI_DEFAULT_MODEL_ID = "grok-2-latest"; +export const XAI_DEFAULT_MODEL_REF = `xai/${XAI_DEFAULT_MODEL_ID}`; +export const XAI_DEFAULT_CONTEXT_WINDOW = 131072; +export const XAI_DEFAULT_MAX_TOKENS = 8192; +export const XAI_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +export function buildXaiModelDefinition(): ModelDefinitionConfig { + return { + id: XAI_DEFAULT_MODEL_ID, + name: "Grok 2", + reasoning: false, + input: ["text"], + cost: XAI_DEFAULT_COST, + contextWindow: XAI_DEFAULT_CONTEXT_WINDOW, + maxTokens: XAI_DEFAULT_MAX_TOKENS, + }; +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 97483e1ed..982570d0d 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -24,6 +24,8 @@ export { applyXiaomiConfig, applyXiaomiProviderConfig, applyZaiConfig, + applyXaiConfig, + applyXaiProviderConfig, } from "./onboard-auth.config-core.js"; export { applyMinimaxApiConfig, @@ -54,10 +56,12 @@ export { setVercelAiGatewayApiKey, setXiaomiApiKey, setZaiApiKey, + setXaiApiKey, writeOAuthCredentials, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, XIAOMI_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, + XAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; export { buildMinimaxApiModelDefinition, diff --git a/src/commands/onboard-non-interactive.xai.test.ts b/src/commands/onboard-non-interactive.xai.test.ts new file mode 100644 index 000000000..bb34fb064 --- /dev/null +++ b/src/commands/onboard-non-interactive.xai.test.ts @@ -0,0 +1,91 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it, vi } from "vitest"; + +describe("onboard (non-interactive): xAI", () => { + it("stores the API key and configures the default model", async () => { + const prev = { + home: process.env.HOME, + stateDir: process.env.OPENCLAW_STATE_DIR, + configPath: process.env.OPENCLAW_CONFIG_PATH, + skipChannels: process.env.OPENCLAW_SKIP_CHANNELS, + skipGmail: process.env.OPENCLAW_SKIP_GMAIL_WATCHER, + skipCron: process.env.OPENCLAW_SKIP_CRON, + skipCanvas: process.env.OPENCLAW_SKIP_CANVAS_HOST, + token: process.env.OPENCLAW_GATEWAY_TOKEN, + password: process.env.OPENCLAW_GATEWAY_PASSWORD, + }; + + process.env.OPENCLAW_SKIP_CHANNELS = "1"; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1"; + process.env.OPENCLAW_SKIP_CRON = "1"; + process.env.OPENCLAW_SKIP_CANVAS_HOST = "1"; + delete process.env.OPENCLAW_GATEWAY_TOKEN; + delete process.env.OPENCLAW_GATEWAY_PASSWORD; + + const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-onboard-xai-")); + process.env.HOME = tempHome; + process.env.OPENCLAW_STATE_DIR = tempHome; + process.env.OPENCLAW_CONFIG_PATH = path.join(tempHome, "openclaw.json"); + vi.resetModules(); + + const runtime = { + log: () => {}, + error: (msg: string) => { + throw new Error(msg); + }, + exit: (code: number) => { + throw new Error(`exit:${code}`); + }, + }; + + try { + const { runNonInteractiveOnboarding } = await import("./onboard-non-interactive.js"); + await runNonInteractiveOnboarding( + { + nonInteractive: true, + authChoice: "xai-api-key", + xaiApiKey: "xai-test-key", + skipHealth: true, + skipChannels: true, + skipSkills: true, + json: true, + }, + runtime, + ); + + const { CONFIG_PATH } = await import("../config/config.js"); + const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8")) as { + auth?: { + profiles?: Record; + }; + agents?: { defaults?: { model?: { primary?: string } } }; + }; + + expect(cfg.auth?.profiles?.["xai:default"]?.provider).toBe("xai"); + expect(cfg.auth?.profiles?.["xai:default"]?.mode).toBe("api_key"); + expect(cfg.agents?.defaults?.model?.primary).toBe("xai/grok-2-latest"); + + const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js"); + const store = ensureAuthProfileStore(); + const profile = store.profiles["xai:default"]; + expect(profile?.type).toBe("api_key"); + if (profile?.type === "api_key") { + expect(profile.provider).toBe("xai"); + expect(profile.key).toBe("xai-test-key"); + } + } finally { + await fs.rm(tempHome, { recursive: true, force: true }); + process.env.HOME = prev.home; + process.env.OPENCLAW_STATE_DIR = prev.stateDir; + process.env.OPENCLAW_CONFIG_PATH = prev.configPath; + process.env.OPENCLAW_SKIP_CHANNELS = prev.skipChannels; + process.env.OPENCLAW_SKIP_GMAIL_WATCHER = prev.skipGmail; + process.env.OPENCLAW_SKIP_CRON = prev.skipCron; + process.env.OPENCLAW_SKIP_CANVAS_HOST = prev.skipCanvas; + process.env.OPENCLAW_GATEWAY_TOKEN = prev.token; + process.env.OPENCLAW_GATEWAY_PASSWORD = prev.password; + } + }, 60_000); +}); diff --git a/src/commands/onboard-non-interactive/local/auth-choice-inference.ts b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts index c747c92d5..1d7eaa77f 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice-inference.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts @@ -22,6 +22,7 @@ type AuthChoiceFlagOptions = Pick< | "xiaomiApiKey" | "minimaxApiKey" | "opencodeZenApiKey" + | "xaiApiKey" >; const AUTH_CHOICE_FLAG_MAP = [ @@ -41,6 +42,7 @@ const AUTH_CHOICE_FLAG_MAP = [ { flag: "veniceApiKey", authChoice: "venice-api-key", label: "--venice-api-key" }, { flag: "zaiApiKey", authChoice: "zai-api-key", label: "--zai-api-key" }, { flag: "xiaomiApiKey", authChoice: "xiaomi-api-key", label: "--xiaomi-api-key" }, + { flag: "xaiApiKey", authChoice: "xai-api-key", label: "--xai-api-key" }, { flag: "minimaxApiKey", authChoice: "minimax-api", label: "--minimax-api-key" }, { flag: "opencodeZenApiKey", authChoice: "opencode-zen", label: "--opencode-zen-api-key" }, ] satisfies ReadonlyArray; diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index d1d4406a4..e1cd61ab1 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -21,6 +21,7 @@ import { applySyntheticConfig, applyVeniceConfig, applyVercelAiGatewayConfig, + applyXaiConfig, applyXiaomiConfig, applyZaiConfig, setAnthropicApiKey, @@ -32,6 +33,7 @@ import { setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, + setXaiApiKey, setVeniceApiKey, setVercelAiGatewayApiKey, setXiaomiApiKey, @@ -218,6 +220,29 @@ export async function applyNonInteractiveAuthChoice(params: { return applyXiaomiConfig(nextConfig); } + if (authChoice === "xai-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "xai", + cfg: baseConfig, + flagValue: opts.xaiApiKey, + flagName: "--xai-api-key", + envVar: "XAI_API_KEY", + runtime, + }); + if (!resolved) { + return null; + } + if (resolved.source !== "profile") { + await setXaiApiKey(resolved.key); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "xai:default", + provider: "xai", + mode: "api_key", + }); + return applyXaiConfig(nextConfig); + } + if (authChoice === "openai-api-key") { const resolved = await resolveNonInteractiveApiKey({ provider: "openai", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index ad0406efd..c64d37046 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -35,6 +35,7 @@ export type AuthChoice = | "github-copilot" | "copilot-proxy" | "qwen-portal" + | "xai-api-key" | "skip"; export type GatewayAuthChoice = "token" | "password"; export type ResetScope = "config" | "config+creds+sessions" | "full"; @@ -79,6 +80,7 @@ export type OnboardOptions = { syntheticApiKey?: string; veniceApiKey?: string; opencodeZenApiKey?: string; + xaiApiKey?: string; gatewayPort?: number; gatewayBind?: GatewayBind; gatewayAuth?: GatewayAuthChoice;