diff --git a/docs/cli/index.md b/docs/cli/index.md index e03257b08e..7f024cf648 100644 --- a/docs/cli/index.md +++ b/docs/cli/index.md @@ -283,7 +283,7 @@ Options: - `--non-interactive` - `--mode ` - `--flow ` -- `--auth-choice ` +- `--auth-choice ` - `--token-provider ` (non-interactive; used with `--auth-choice token`) - `--token ` (non-interactive; used with `--auth-choice token`) - `--token-profile-id ` (non-interactive; default: `:manual`) @@ -291,6 +291,7 @@ Options: - `--anthropic-api-key ` - `--openai-api-key ` - `--openrouter-api-key ` +- `--ai-gateway-api-key ` - `--moonshot-api-key ` - `--gemini-api-key ` - `--zai-api-key ` diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index c8f8511736..12834317f1 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -93,6 +93,13 @@ Clawdbot ships with the pi‑ai catalog. These providers require **no** - CLI: `clawdbot onboard --auth-choice zai-api-key` - Aliases: `z.ai/*` and `z-ai/*` normalize to `zai/*` +### Vercel AI Gateway + +- Provider: `vercel-ai-gateway` +- Auth: `AI_GATEWAY_API_KEY` +- Example model: `vercel-ai-gateway/anthropic/claude-opus-4.5` +- CLI: `clawdbot onboard --auth-choice ai-gateway-api-key` + ### Other built-in providers - OpenRouter: `openrouter` (`OPENROUTER_API_KEY`) diff --git a/docs/providers/index.md b/docs/providers/index.md index 0718a48da1..d89d5c4a52 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -27,6 +27,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/etc.)? See [Chann - [OpenAI (API + Codex)](/providers/openai) - [Anthropic (API + Claude Code CLI)](/providers/anthropic) - [OpenRouter](/providers/openrouter) +- [Vercel AI Gateway](/providers/vercel-ai-gateway) - [Moonshot AI (Kimi)](/providers/moonshot) - [OpenCode Zen](/providers/opencode) - [Z.AI](/providers/zai) diff --git a/docs/providers/models.md b/docs/providers/models.md index 6c729dbd4e..d88747cc38 100644 --- a/docs/providers/models.md +++ b/docs/providers/models.md @@ -25,6 +25,7 @@ model as `provider/model`. - [OpenAI (API + Codex)](/providers/openai) - [Anthropic (API + Claude Code CLI)](/providers/anthropic) - [OpenRouter](/providers/openrouter) +- [Vercel AI Gateway](/providers/vercel-ai-gateway) - [Moonshot AI (Kimi)](/providers/moonshot) - [Synthetic](/providers/synthetic) - [OpenCode Zen](/providers/opencode) diff --git a/docs/providers/vercel-ai-gateway.md b/docs/providers/vercel-ai-gateway.md new file mode 100644 index 0000000000..bd31f0a87b --- /dev/null +++ b/docs/providers/vercel-ai-gateway.md @@ -0,0 +1,49 @@ +--- +summary: "Vercel AI Gateway setup (auth + model selection)" +read_when: + - You want to use Vercel AI Gateway with Clawdbot + - You need the API key env var or CLI auth choice +--- +# Vercel AI Gateway + + +The [Vercel AI Gateway](https://vercel.com/ai-gateway) provides a unified API to access hundreds of models through a single endpoint. + +- Provider: `vercel-ai-gateway` +- Auth: `AI_GATEWAY_API_KEY` +- API: Anthropic Messages compatible + +## Quick start + +1) Set the API key (recommended: store it for the Gateway): + +```bash +clawdbot onboard --auth-choice ai-gateway-api-key +``` + +2) Set a default model: + +```json5 +{ + agents: { + defaults: { + model: { primary: "vercel-ai-gateway/anthropic/claude-opus-4.5" } + } + } +} +``` + +## Non-interactive example + +```bash +clawdbot onboard --non-interactive \ + --mode local \ + --auth-choice ai-gateway-api-key \ + --ai-gateway-api-key "$AI_GATEWAY_API_KEY" +``` + +## Environment note + +If the Gateway runs as a daemon (launchd/systemd), make sure `AI_GATEWAY_API_KEY` +is available to that process (for example, in `~/.clawdbot/.env` or via +`env.shellEnv`). diff --git a/docs/start/wizard.md b/docs/start/wizard.md index 964f23b1b9..333d332a97 100644 --- a/docs/start/wizard.md +++ b/docs/start/wizard.md @@ -45,7 +45,7 @@ The wizard starts with **QuickStart** (defaults) vs **Advanced** (full control). ## What the wizard does **Local mode (default)** walks you through: -- Model/auth (OpenAI Code (Codex) subscription OAuth, Anthropic API key (recommended) or `claude setup-token`, plus MiniMax/GLM/Moonshot options) + - Model/auth (OpenAI Code (Codex) subscription OAuth, Anthropic API key (recommended) or `claude setup-token`, plus MiniMax/GLM/Moonshot/AI Gateway options) - Workspace location + bootstrap files - Gateway settings (port/bind/auth/tailscale) - Providers (Telegram, WhatsApp, Discord, Signal) @@ -88,7 +88,9 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` ( - **OpenAI API key**: uses `OPENAI_API_KEY` if present or prompts for a key, then saves it to `~/.clawdbot/.env` so launchd can read it. - **OpenCode Zen (multi-model proxy)**: prompts for `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`, get it at https://opencode.ai/auth). - **API key**: stores the key for you. - - **MiniMax M2.1**: config is auto-written. + - **Vercel AI Gateway (multi-model proxy)**: prompts for `ai_gateway_api_key`. + - more detail: [vercel ai gateway](/providers/vercel-ai-gateway) + - **minimax m2.1**: config is auto-written. - More detail: [MiniMax](/providers/minimax) - **Synthetic (Anthropic-compatible)**: prompts for `SYNTHETIC_API_KEY`. - More detail: [Synthetic](/providers/synthetic) @@ -209,6 +211,17 @@ clawdbot onboard --non-interactive \ --zai-api-key "$ZAI_API_KEY" \ --gateway-port 18789 \ --gateway-bind loopback + +Vercel AI Gateway example: + +```bash +clawdbot onboard --non-interactive \ + --mode local \ + --auth-choice ai-gateway-api-key \ + --ai-gateway-api-key "$AI_GATEWAY_API_KEY" \ + --gateway-port 18789 \ + --gateway-bind loopback +``` ``` Moonshot example: diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index 5918edede6..ad1bccc901 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -256,4 +256,28 @@ describe("getApiKeyForModel", () => { } } }); + + it("resolves Vercel AI Gateway API key from env", async () => { + const previousGatewayKey = process.env.AI_GATEWAY_API_KEY; + + try { + process.env.AI_GATEWAY_API_KEY = "gateway-test-key"; + + vi.resetModules(); + const { resolveApiKeyForProvider } = await import("./model-auth.js"); + + const resolved = await resolveApiKeyForProvider({ + provider: "vercel-ai-gateway", + store: { version: 1, profiles: {} }, + }); + expect(resolved.apiKey).toBe("gateway-test-key"); + expect(resolved.source).toContain("AI_GATEWAY_API_KEY"); + } finally { + if (previousGatewayKey === undefined) { + delete process.env.AI_GATEWAY_API_KEY; + } else { + process.env.AI_GATEWAY_API_KEY = previousGatewayKey; + } + } + }); }); diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 20e534672c..c93d1a5c83 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -154,6 +154,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { cerebras: "CEREBRAS_API_KEY", xai: "XAI_API_KEY", openrouter: "OPENROUTER_API_KEY", + "vercel-ai-gateway": "AI_GATEWAY_API_KEY", moonshot: "MOONSHOT_API_KEY", minimax: "MINIMAX_API_KEY", synthetic: "SYNTHETIC_API_KEY", diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index d3b9a89129..97cc86c48a 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -51,7 +51,7 @@ export function registerOnboardCommand(program: Command) { .option("--mode ", "Wizard mode: local|remote") .option( "--auth-choice ", - "Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|moonshot-api-key|synthetic-api-key|codex-cli|antigravity|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", + "Auth: setup-token|claude-cli|token|chutes|openai-codex|openai-api-key|openrouter-api-key|ai-gateway-api-key|moonshot-api-key|synthetic-api-key|codex-cli|antigravity|gemini-api-key|zai-api-key|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip", ) .option( "--token-provider ", @@ -66,6 +66,7 @@ export function registerOnboardCommand(program: Command) { .option("--anthropic-api-key ", "Anthropic API key") .option("--openai-api-key ", "OpenAI API key") .option("--openrouter-api-key ", "OpenRouter API key") + .option("--ai-gateway-api-key ", "Vercel AI Gateway API key") .option("--moonshot-api-key ", "Moonshot API key") .option("--gemini-api-key ", "Gemini API key") .option("--zai-api-key ", "Z.AI API key") @@ -113,6 +114,7 @@ export function registerOnboardCommand(program: Command) { anthropicApiKey: opts.anthropicApiKey as string | undefined, openaiApiKey: opts.openaiApiKey as string | undefined, openrouterApiKey: opts.openrouterApiKey as string | undefined, + aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined, moonshotApiKey: opts.moonshotApiKey as string | undefined, geminiApiKey: opts.geminiApiKey as string | undefined, zaiApiKey: opts.zaiApiKey as string | undefined, diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index 2376d6f649..2dd654a622 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -102,6 +102,18 @@ describe("buildAuthChoiceOptions", () => { expect(options.some((opt) => opt.value === "moonshot-api-key")).toBe(true); }); + it("includes Vercel AI Gateway auth choice", () => { + const store: AuthProfileStore = { version: 1, profiles: {} }; + const options = buildAuthChoiceOptions({ + store, + includeSkip: false, + includeClaudeCliIfMissing: true, + platform: "darwin", + }); + + expect(options.some((opt) => opt.value === "ai-gateway-api-key")).toBe(true); + }); + it("includes Synthetic auth choice", () => { const store: AuthProfileStore = { version: 1, profiles: {} }; const options = buildAuthChoiceOptions({ diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 80e585f843..3f83b8aa04 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -14,6 +14,7 @@ export type AuthChoiceGroupId = | "anthropic" | "google" | "openrouter" + | "ai-gateway" | "moonshot" | "zai" | "opencode-zen" @@ -69,6 +70,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["openrouter-api-key"], }, + { + value: "ai-gateway", + label: "Vercel AI Gateway", + hint: "API key", + choices: ["ai-gateway-api-key"], + }, { value: "moonshot", label: "Moonshot AI", @@ -168,6 +175,10 @@ 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: "ai-gateway-api-key", + label: "Vercel AI Gateway API key", + }); options.push({ value: "moonshot-api-key", label: "Moonshot AI API key" }); options.push({ value: "synthetic-api-key", label: "Synthetic API key" }); options.push({ diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index 53d51bcefc..addc7ab295 100644 --- a/src/commands/auth-choice.apply.api-providers.ts +++ b/src/commands/auth-choice.apply.api-providers.ts @@ -21,15 +21,19 @@ import { applyOpenrouterProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, + applyVercelAiGatewayConfig, + applyVercelAiGatewayProviderConfig, applyZaiConfig, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF, + VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, setGeminiApiKey, setMoonshotApiKey, setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, + setVercelAiGatewayApiKey, setZaiApiKey, ZAI_DEFAULT_MODEL_REF, } from "./onboard-auth.js"; @@ -121,6 +125,48 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (params.authChoice === "ai-gateway-api-key") { + let hasCredential = false; + const envKey = resolveEnvApiKey("vercel-ai-gateway"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + await setVercelAiGatewayApiKey(envKey.apiKey, params.agentDir); + hasCredential = true; + } + } + if (!hasCredential) { + const key = await params.prompter.text({ + message: "Enter Vercel AI Gateway API key", + validate: validateApiKeyInput, + }); + await setVercelAiGatewayApiKey(normalizeApiKeyInput(String(key)), params.agentDir); + } + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "vercel-ai-gateway:default", + provider: "vercel-ai-gateway", + mode: "api_key", + }); + { + const applied = await applyDefaultModelChoice({ + config: nextConfig, + setDefaultModel: params.setDefaultModel, + defaultModel: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, + applyDefaultConfig: applyVercelAiGatewayConfig, + applyProviderConfig: applyVercelAiGatewayProviderConfig, + noteDefault: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, + noteAgentModel, + prompter: params.prompter, + }); + nextConfig = applied.config; + agentModelOverride = applied.agentModelOverride ?? agentModelOverride; + } + return { config: nextConfig, agentModelOverride }; + } + if (params.authChoice === "moonshot-api-key") { let hasCredential = false; const envKey = resolveEnvApiKey("moonshot"); diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index 6ffa37cabb..82d6ca2fea 100644 --- a/src/commands/auth-choice.preferred-provider.ts +++ b/src/commands/auth-choice.preferred-provider.ts @@ -11,6 +11,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = { chutes: "chutes", "openai-api-key": "openai", "openrouter-api-key": "openrouter", + "ai-gateway-api-key": "vercel-ai-gateway", "moonshot-api-key": "moonshot", "gemini-api-key": "google", "zai-api-key": "zai", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index 68db4749f2..f36064f3ee 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -5,7 +5,11 @@ import { SYNTHETIC_MODEL_CATALOG, } from "../agents/synthetic-models.js"; import type { ClawdbotConfig } from "../config/config.js"; -import { OPENROUTER_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF } from "./onboard-auth.credentials.js"; +import { + OPENROUTER_DEFAULT_MODEL_REF, + VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, + ZAI_DEFAULT_MODEL_REF, +} from "./onboard-auth.credentials.js"; import { buildMoonshotModelDefinition, MOONSHOT_BASE_URL, @@ -60,6 +64,47 @@ export function applyOpenrouterProviderConfig(cfg: ClawdbotConfig): ClawdbotConf }; } +export function applyVercelAiGatewayProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF] = { + ...models[VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF], + alias: models[VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF]?.alias ?? "Vercel AI Gateway", + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + }; +} + +export function applyVercelAiGatewayConfig(cfg: ClawdbotConfig): ClawdbotConfig { + const next = applyVercelAiGatewayProviderConfig(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: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, + }, + }, + }, + }; +} + export function applyOpenrouterConfig(cfg: ClawdbotConfig): ClawdbotConfig { const next = applyOpenrouterProviderConfig(cfg); const existingModel = next.agents?.defaults?.model; diff --git a/src/commands/onboard-auth.credentials.ts b/src/commands/onboard-auth.credentials.ts index e0e36c9c1e..1d35492bca 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -88,6 +88,8 @@ export async function setSyntheticApiKey(key: string, agentDir?: string) { export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto"; +export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = + "vercel-ai-gateway/anthropic/claude-opus-4.5"; export async function setZaiApiKey(key: string, agentDir?: string) { // Write to resolved agent dir so gateway finds credentials on startup. @@ -114,6 +116,18 @@ export async function setOpenrouterApiKey(key: string, agentDir?: string) { }); } +export async function setVercelAiGatewayApiKey(key: string, agentDir?: string) { + upsertAuthProfile({ + profileId: "vercel-ai-gateway:default", + credential: { + type: "api_key", + provider: "vercel-ai-gateway", + key, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} + export async function setOpencodeZenApiKey(key: string, agentDir?: string) { upsertAuthProfile({ profileId: "opencode:default", diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 8091515ee0..13fe0555e1 100644 --- a/src/commands/onboard-auth.ts +++ b/src/commands/onboard-auth.ts @@ -10,6 +10,8 @@ export { applyOpenrouterProviderConfig, applySyntheticConfig, applySyntheticProviderConfig, + applyVercelAiGatewayConfig, + applyVercelAiGatewayProviderConfig, applyZaiConfig, } from "./onboard-auth.config-core.js"; export { @@ -34,8 +36,10 @@ export { setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, + setVercelAiGatewayApiKey, setZaiApiKey, writeOAuthCredentials, + VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, ZAI_DEFAULT_MODEL_REF, } from "./onboard-auth.credentials.js"; export { diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 65cf83bec3..7351614dd8 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -19,6 +19,7 @@ import { applyOpencodeZenConfig, applyOpenrouterConfig, applySyntheticConfig, + applyVercelAiGatewayConfig, applyZaiConfig, setAnthropicApiKey, setGeminiApiKey, @@ -27,6 +28,7 @@ import { setOpencodeZenApiKey, setOpenrouterApiKey, setSyntheticApiKey, + setVercelAiGatewayApiKey, setZaiApiKey, } from "../../onboard-auth.js"; import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; @@ -191,6 +193,25 @@ export async function applyNonInteractiveAuthChoice(params: { return applyOpenrouterConfig(nextConfig); } + if (authChoice === "ai-gateway-api-key") { + const resolved = await resolveNonInteractiveApiKey({ + provider: "vercel-ai-gateway", + cfg: baseConfig, + flagValue: opts.aiGatewayApiKey, + flagName: "--ai-gateway-api-key", + envVar: "AI_GATEWAY_API_KEY", + runtime, + }); + if (!resolved) return null; + if (resolved.source !== "profile") await setVercelAiGatewayApiKey(resolved.key); + nextConfig = applyAuthProfileConfig(nextConfig, { + profileId: "vercel-ai-gateway:default", + provider: "vercel-ai-gateway", + mode: "api_key", + }); + return applyVercelAiGatewayConfig(nextConfig); + } + if (authChoice === "moonshot-api-key") { const resolved = await resolveNonInteractiveApiKey({ provider: "moonshot", diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index d211da9a89..891c164024 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -12,6 +12,7 @@ export type AuthChoice = | "openai-codex" | "openai-api-key" | "openrouter-api-key" + | "ai-gateway-api-key" | "moonshot-api-key" | "synthetic-api-key" | "codex-cli" @@ -55,6 +56,7 @@ export type OnboardOptions = { anthropicApiKey?: string; openaiApiKey?: string; openrouterApiKey?: string; + aiGatewayApiKey?: string; moonshotApiKey?: string; geminiApiKey?: string; zaiApiKey?: string; diff --git a/src/config/io.ts b/src/config/io.ts index 1926f46bea..b7d1a6f922 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -38,6 +38,7 @@ const SHELL_ENV_EXPECTED_KEYS = [ "GEMINI_API_KEY", "ZAI_API_KEY", "OPENROUTER_API_KEY", + "AI_GATEWAY_API_KEY", "MINIMAX_API_KEY", "SYNTHETIC_API_KEY", "ELEVENLABS_API_KEY",