From 5b0851ebd82cd7ec3372d070ab4d73fda81bd959 Mon Sep 17 00:00:00 2001
From: Peter Steinberger
Date: Wed, 4 Feb 2026 04:10:13 -0800
Subject: [PATCH] feat: add cloudflare ai gateway provider
---
CHANGELOG.md | 1 +
README.md | 2 +-
docs/providers/cloudflare-ai-gateway.md | 71 ++++++++++++
docs/providers/index.md | 1 +
docs/providers/models.md | 1 +
docs/start/wizard.md | 15 +++
src/agents/auth-profiles/oauth.ts | 6 +-
src/agents/auth-profiles/types.ts | 4 +-
src/agents/cloudflare-ai-gateway.ts | 44 ++++++++
src/agents/model-auth.ts | 1 +
src/agents/models-config.providers.ts | 32 ++++++
src/agents/tools/session-status-tool.ts | 2 +-
src/auto-reply/reply/commands-status.ts | 2 +-
.../reply/directive-handling.auth.ts | 4 +-
src/cli/program/register.onboard.ts | 8 +-
src/commands/auth-choice-options.test.ts | 10 ++
src/commands/auth-choice-options.ts | 12 ++
.../auth-choice.apply.api-providers.ts | 105 ++++++++++++++++++
src/commands/auth-choice.apply.ts | 3 +
.../auth-choice.preferred-provider.ts | 1 +
src/commands/auth-choice.test.ts | 76 +++++++++++++
src/commands/models/list.auth-overview.ts | 2 +-
src/commands/onboard-auth.config-core.ts | 97 ++++++++++++++++
src/commands/onboard-auth.credentials.ts | 25 +++++
src/commands/onboard-auth.ts | 4 +
...-interactive.cloudflare-ai-gateway.test.ts | 99 +++++++++++++++++
.../local/auth-choice.ts | 40 +++++++
src/commands/onboard-types.ts | 4 +
28 files changed, 663 insertions(+), 9 deletions(-)
create mode 100644 docs/providers/cloudflare-ai-gateway.md
create mode 100644 src/agents/cloudflare-ai-gateway.ts
create mode 100644 src/commands/onboard-non-interactive.cloudflare-ai-gateway.test.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 669c7984be..1a862fd82c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
### Changes
+- Onboarding: add Cloudflare AI Gateway provider setup and docs. (#7914) Thanks @roerohan.
- Onboarding: add Moonshot (.cn) auth choice and keep the China base URL when preserving defaults. (#7180) Thanks @waynelwz.
- Docs: clarify tmux send-keys for TUI by splitting text and Enter. (#7737) Thanks @Wangnov.
- Cron: add announce delivery mode for isolated jobs (CLI + Control UI) and delivery mode config.
diff --git a/README.md b/README.md
index 7e24435689..bebf5fcfd7 100644
--- a/README.md
+++ b/README.md
@@ -535,5 +535,5 @@ Thanks to all clawtributors:
-
+
diff --git a/docs/providers/cloudflare-ai-gateway.md b/docs/providers/cloudflare-ai-gateway.md
new file mode 100644
index 0000000000..392a611e70
--- /dev/null
+++ b/docs/providers/cloudflare-ai-gateway.md
@@ -0,0 +1,71 @@
+---
+title: "Cloudflare AI Gateway"
+summary: "Cloudflare AI Gateway setup (auth + model selection)"
+read_when:
+ - You want to use Cloudflare AI Gateway with OpenClaw
+ - You need the account ID, gateway ID, or API key env var
+---
+
+# Cloudflare AI Gateway
+
+Cloudflare AI Gateway sits in front of provider APIs and lets you add analytics, caching, and controls. For Anthropic, OpenClaw uses the Anthropic Messages API through your Gateway endpoint.
+
+- Provider: `cloudflare-ai-gateway`
+- Base URL: `https://gateway.ai.cloudflare.com/v1///anthropic`
+- Default model: `cloudflare-ai-gateway/claude-sonnet-4-5`
+- API key: `CLOUDFLARE_AI_GATEWAY_API_KEY` (your provider API key for requests through the Gateway)
+
+For Anthropic models, use your Anthropic API key.
+
+## Quick start
+
+1. Set the provider API key and Gateway details:
+
+```bash
+openclaw onboard --auth-choice cloudflare-ai-gateway-api-key
+```
+
+2. Set a default model:
+
+```json5
+{
+ agents: {
+ defaults: {
+ model: { primary: "cloudflare-ai-gateway/claude-sonnet-4-5" },
+ },
+ },
+}
+```
+
+## Non-interactive example
+
+```bash
+openclaw onboard --non-interactive \
+ --mode local \
+ --auth-choice cloudflare-ai-gateway-api-key \
+ --cloudflare-ai-gateway-account-id "your-account-id" \
+ --cloudflare-ai-gateway-gateway-id "your-gateway-id" \
+ --cloudflare-ai-gateway-api-key "$CLOUDFLARE_AI_GATEWAY_API_KEY"
+```
+
+## Authenticated gateways
+
+If you enabled Gateway authentication in Cloudflare, add the `cf-aig-authorization` header (this is in addition to your provider API key).
+
+```json5
+{
+ models: {
+ providers: {
+ "cloudflare-ai-gateway": {
+ headers: {
+ "cf-aig-authorization": "Bearer ",
+ },
+ },
+ },
+ },
+}
+```
+
+## Environment note
+
+If the Gateway runs as a daemon (launchd/systemd), make sure `CLOUDFLARE_AI_GATEWAY_API_KEY` is available to that process (for example, in `~/.openclaw/.env` or via `env.shellEnv`).
diff --git a/docs/providers/index.md b/docs/providers/index.md
index 6009dba15b..cc1dad7ee5 100644
--- a/docs/providers/index.md
+++ b/docs/providers/index.md
@@ -40,6 +40,7 @@ See [Venice AI](/providers/venice).
- [Qwen (OAuth)](/providers/qwen)
- [OpenRouter](/providers/openrouter)
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
+- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
- [OpenCode Zen](/providers/opencode)
- [Amazon Bedrock](/bedrock)
diff --git a/docs/providers/models.md b/docs/providers/models.md
index ad6e424b05..64c7d865ec 100644
--- a/docs/providers/models.md
+++ b/docs/providers/models.md
@@ -37,6 +37,7 @@ See [Venice AI](/providers/venice).
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
- [OpenRouter](/providers/openrouter)
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
+- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
- [Synthetic](/providers/synthetic)
- [OpenCode Zen](/providers/opencode)
diff --git a/docs/start/wizard.md b/docs/start/wizard.md
index 501d686a81..1269344fe8 100644
--- a/docs/start/wizard.md
+++ b/docs/start/wizard.md
@@ -95,6 +95,8 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
- **API key**: stores the key for you.
- **Vercel AI Gateway (multi-model proxy)**: prompts for `AI_GATEWAY_API_KEY`.
- More detail: [Vercel AI Gateway](/providers/vercel-ai-gateway)
+ - **Cloudflare AI Gateway**: prompts for Account ID, Gateway ID, and `CLOUDFLARE_AI_GATEWAY_API_KEY`.
+ - More detail: [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
- **MiniMax M2.1**: config is auto-written.
- More detail: [MiniMax](/providers/minimax)
- **Synthetic (Anthropic-compatible)**: prompts for `SYNTHETIC_API_KEY`.
@@ -239,6 +241,19 @@ openclaw onboard --non-interactive \
--gateway-bind loopback
```
+Cloudflare AI Gateway example:
+
+```bash
+openclaw onboard --non-interactive \
+ --mode local \
+ --auth-choice cloudflare-ai-gateway-api-key \
+ --cloudflare-ai-gateway-account-id "your-account-id" \
+ --cloudflare-ai-gateway-gateway-id "your-gateway-id" \
+ --cloudflare-ai-gateway-api-key "$CLOUDFLARE_AI_GATEWAY_API_KEY" \
+ --gateway-port 18789 \
+ --gateway-bind loopback
+```
+
Moonshot example:
```bash
diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts
index 064b72f549..4fff5a3012 100644
--- a/src/agents/auth-profiles/oauth.ts
+++ b/src/agents/auth-profiles/oauth.ts
@@ -169,7 +169,11 @@ export async function resolveApiKeyForProfile(params: {
}
if (cred.type === "api_key") {
- return { apiKey: cred.key, provider: cred.provider, email: cred.email };
+ const key = cred.key?.trim();
+ if (!key) {
+ return null;
+ }
+ return { apiKey: key, provider: cred.provider, email: cred.email };
}
if (cred.type === "token") {
const token = cred.token?.trim();
diff --git a/src/agents/auth-profiles/types.ts b/src/agents/auth-profiles/types.ts
index 4d08d301d8..f4a0a4e860 100644
--- a/src/agents/auth-profiles/types.ts
+++ b/src/agents/auth-profiles/types.ts
@@ -4,8 +4,10 @@ import type { OpenClawConfig } from "../../config/config.js";
export type ApiKeyCredential = {
type: "api_key";
provider: string;
- key: string;
+ key?: string;
email?: string;
+ /** Optional provider-specific metadata (e.g., account IDs, gateway IDs). */
+ metadata?: Record;
};
export type TokenCredential = {
diff --git a/src/agents/cloudflare-ai-gateway.ts b/src/agents/cloudflare-ai-gateway.ts
new file mode 100644
index 0000000000..77ed2fdc93
--- /dev/null
+++ b/src/agents/cloudflare-ai-gateway.ts
@@ -0,0 +1,44 @@
+import type { ModelDefinitionConfig } from "../config/types.js";
+
+export const CLOUDFLARE_AI_GATEWAY_PROVIDER_ID = "cloudflare-ai-gateway";
+export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID = "claude-sonnet-4-5";
+export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF = `${CLOUDFLARE_AI_GATEWAY_PROVIDER_ID}/${CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID}`;
+
+export const CLOUDFLARE_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW = 200_000;
+export const CLOUDFLARE_AI_GATEWAY_DEFAULT_MAX_TOKENS = 64_000;
+export const CLOUDFLARE_AI_GATEWAY_DEFAULT_COST = {
+ input: 3,
+ output: 15,
+ cacheRead: 0.3,
+ cacheWrite: 3.75,
+};
+
+export function buildCloudflareAiGatewayModelDefinition(params?: {
+ id?: string;
+ name?: string;
+ reasoning?: boolean;
+ input?: Array<"text" | "image">;
+}): ModelDefinitionConfig {
+ const id = params?.id?.trim() || CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_ID;
+ return {
+ id,
+ name: params?.name ?? "Claude Sonnet 4.5",
+ reasoning: params?.reasoning ?? true,
+ input: params?.input ?? ["text", "image"],
+ cost: CLOUDFLARE_AI_GATEWAY_DEFAULT_COST,
+ contextWindow: CLOUDFLARE_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW,
+ maxTokens: CLOUDFLARE_AI_GATEWAY_DEFAULT_MAX_TOKENS,
+ };
+}
+
+export function resolveCloudflareAiGatewayBaseUrl(params: {
+ accountId: string;
+ gatewayId: string;
+}): string {
+ const accountId = params.accountId.trim();
+ const gatewayId = params.gatewayId.trim();
+ if (!accountId || !gatewayId) {
+ return "";
+ }
+ return `https://gateway.ai.cloudflare.com/v1/${accountId}/${gatewayId}/anthropic`;
+}
diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts
index 4a4b5702cc..ba85e213cc 100644
--- a/src/agents/model-auth.ts
+++ b/src/agents/model-auth.ts
@@ -293,6 +293,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
xai: "XAI_API_KEY",
openrouter: "OPENROUTER_API_KEY",
"vercel-ai-gateway": "AI_GATEWAY_API_KEY",
+ "cloudflare-ai-gateway": "CLOUDFLARE_AI_GATEWAY_API_KEY",
moonshot: "MOONSHOT_API_KEY",
minimax: "MINIMAX_API_KEY",
xiaomi: "XIAOMI_API_KEY",
diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts
index 6ad93813dd..e49b150c76 100644
--- a/src/agents/models-config.providers.ts
+++ b/src/agents/models-config.providers.ts
@@ -6,6 +6,10 @@ import {
} from "../providers/github-copilot-token.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { discoverBedrockModels } from "./bedrock-discovery.js";
+import {
+ buildCloudflareAiGatewayModelDefinition,
+ resolveCloudflareAiGatewayBaseUrl,
+} from "./cloudflare-ai-gateway.js";
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
import {
buildSyntheticModelDefinition,
@@ -453,6 +457,34 @@ export async function resolveImplicitProviders(params: {
providers.xiaomi = { ...buildXiaomiProvider(), apiKey: xiaomiKey };
}
+ const cloudflareProfiles = listProfilesForProvider(authStore, "cloudflare-ai-gateway");
+ for (const profileId of cloudflareProfiles) {
+ const cred = authStore.profiles[profileId];
+ if (cred?.type !== "api_key") {
+ continue;
+ }
+ const accountId = cred.metadata?.accountId?.trim();
+ const gatewayId = cred.metadata?.gatewayId?.trim();
+ if (!accountId || !gatewayId) {
+ continue;
+ }
+ const baseUrl = resolveCloudflareAiGatewayBaseUrl({ accountId, gatewayId });
+ if (!baseUrl) {
+ continue;
+ }
+ const apiKey = resolveEnvApiKeyVarName("cloudflare-ai-gateway") ?? cred.key?.trim() ?? "";
+ if (!apiKey) {
+ continue;
+ }
+ providers["cloudflare-ai-gateway"] = {
+ baseUrl,
+ api: "anthropic-messages",
+ apiKey,
+ models: [buildCloudflareAiGatewayModelDefinition()],
+ };
+ break;
+ }
+
// Ollama provider - only add if explicitly configured
const ollamaKey =
resolveEnvApiKeyVarName("ollama") ??
diff --git a/src/agents/tools/session-status-tool.ts b/src/agents/tools/session-status-tool.ts
index 71547a8340..2eded36e96 100644
--- a/src/agents/tools/session-status-tool.ts
+++ b/src/agents/tools/session-status-tool.ts
@@ -104,7 +104,7 @@ function resolveModelAuthLabel(params: {
if (profile.type === "token") {
return `token ${formatApiKeySnippet(profile.token)}${label ? ` (${label})` : ""}`;
}
- return `api-key ${formatApiKeySnippet(profile.key)}${label ? ` (${label})` : ""}`;
+ return `api-key ${formatApiKeySnippet(profile.key ?? "")}${label ? ` (${label})` : ""}`;
}
const envKey = resolveEnvApiKey(providerKey);
diff --git a/src/auto-reply/reply/commands-status.ts b/src/auto-reply/reply/commands-status.ts
index d9a5176a25..1695ba627f 100644
--- a/src/auto-reply/reply/commands-status.ts
+++ b/src/auto-reply/reply/commands-status.ts
@@ -80,7 +80,7 @@ function resolveModelAuthLabel(
const snippet = formatApiKeySnippet(profile.token);
return `token ${snippet}${label ? ` (${label})` : ""}`;
}
- const snippet = formatApiKeySnippet(profile.key);
+ const snippet = formatApiKeySnippet(profile.key ?? "");
return `api-key ${snippet}${label ? ` (${label})` : ""}`;
}
diff --git a/src/auto-reply/reply/directive-handling.auth.ts b/src/auto-reply/reply/directive-handling.auth.ts
index 1b9ae92f8b..4b25d86b69 100644
--- a/src/auto-reply/reply/directive-handling.auth.ts
+++ b/src/auto-reply/reply/directive-handling.auth.ts
@@ -93,7 +93,7 @@ export const resolveAuthLabel = async (
if (profile.type === "api_key") {
return {
- label: `${profileId} api-key ${maskApiKey(profile.key)}${more}`,
+ label: `${profileId} api-key ${maskApiKey(profile.key ?? "")}${more}`,
source: "",
};
}
@@ -154,7 +154,7 @@ export const resolveAuthLabel = async (
}
if (profile.type === "api_key") {
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
- return `${profileId}=${maskApiKey(profile.key)}${suffix}`;
+ return `${profileId}=${maskApiKey(profile.key ?? "")}${suffix}`;
}
if (profile.type === "token") {
if (
diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts
index 9c9c1634ce..995afbfdcb 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|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|apiKey|minimax-api|minimax-api-lightning|opencode-zen|skip",
)
.option(
"--token-provider ",
@@ -74,6 +74,9 @@ export function registerOnboardCommand(program: Command) {
.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("--cloudflare-ai-gateway-account-id ", "Cloudflare Account ID")
+ .option("--cloudflare-ai-gateway-gateway-id ", "Cloudflare AI Gateway ID")
+ .option("--cloudflare-ai-gateway-api-key ", "Cloudflare AI Gateway API key")
.option("--moonshot-api-key ", "Moonshot API key")
.option("--kimi-code-api-key ", "Kimi Coding API key")
.option("--gemini-api-key ", "Gemini API key")
@@ -125,6 +128,9 @@ export function registerOnboardCommand(program: Command) {
openaiApiKey: opts.openaiApiKey as string | undefined,
openrouterApiKey: opts.openrouterApiKey as string | undefined,
aiGatewayApiKey: opts.aiGatewayApiKey as string | undefined,
+ cloudflareAiGatewayAccountId: opts.cloudflareAiGatewayAccountId as string | undefined,
+ cloudflareAiGatewayGatewayId: opts.cloudflareAiGatewayGatewayId as string | undefined,
+ cloudflareAiGatewayApiKey: opts.cloudflareAiGatewayApiKey as string | undefined,
moonshotApiKey: opts.moonshotApiKey as string | undefined,
kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined,
geminiApiKey: opts.geminiApiKey as string | undefined,
diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts
index 2f1e08faa9..2ea1cf6247 100644
--- a/src/commands/auth-choice-options.test.ts
+++ b/src/commands/auth-choice-options.test.ts
@@ -75,6 +75,16 @@ describe("buildAuthChoiceOptions", () => {
expect(options.some((opt) => opt.value === "ai-gateway-api-key")).toBe(true);
});
+ it("includes Cloudflare AI Gateway auth choice", () => {
+ const store: AuthProfileStore = { version: 1, profiles: {} };
+ const options = buildAuthChoiceOptions({
+ store,
+ includeSkip: false,
+ });
+
+ expect(options.some((opt) => opt.value === "cloudflare-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 aa78c7fe9b..c3a281278c 100644
--- a/src/commands/auth-choice-options.ts
+++ b/src/commands/auth-choice-options.ts
@@ -14,6 +14,7 @@ export type AuthChoiceGroupId =
| "copilot"
| "openrouter"
| "ai-gateway"
+ | "cloudflare-ai-gateway"
| "moonshot"
| "zai"
| "xiaomi"
@@ -120,6 +121,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
hint: "Privacy-focused (uncensored models)",
choices: ["venice-api-key"],
},
+ {
+ value: "cloudflare-ai-gateway",
+ label: "Cloudflare AI Gateway",
+ hint: "Account ID + Gateway ID + API key",
+ choices: ["cloudflare-ai-gateway-api-key"],
+ },
];
export function buildAuthChoiceOptions(params: {
@@ -146,6 +153,11 @@ export function buildAuthChoiceOptions(params: {
value: "ai-gateway-api-key",
label: "Vercel AI Gateway API key",
});
+ options.push({
+ value: "cloudflare-ai-gateway-api-key",
+ label: "Cloudflare AI Gateway",
+ hint: "Account ID + Gateway ID + API key",
+ });
options.push({
value: "moonshot-api-key",
label: "Kimi API key (.ai)",
diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts
index ea6095c9cb..6396b6e397 100644
--- a/src/commands/auth-choice.apply.api-providers.ts
+++ b/src/commands/auth-choice.apply.api-providers.ts
@@ -13,6 +13,8 @@ import {
} from "./google-gemini-model-default.js";
import {
applyAuthProfileConfig,
+ applyCloudflareAiGatewayConfig,
+ applyCloudflareAiGatewayProviderConfig,
applyKimiCodeConfig,
applyKimiCodeProviderConfig,
applyMoonshotConfig,
@@ -32,6 +34,7 @@ import {
applyXiaomiConfig,
applyXiaomiProviderConfig,
applyZaiConfig,
+ CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
KIMI_CODING_MODEL_REF,
MOONSHOT_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
@@ -39,6 +42,7 @@ import {
VENICE_DEFAULT_MODEL_REF,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
XIAOMI_DEFAULT_MODEL_REF,
+ setCloudflareAiGatewayConfig,
setGeminiApiKey,
setKimiCodingApiKey,
setMoonshotApiKey,
@@ -79,6 +83,8 @@ export async function applyAuthChoiceApiProviders(
authChoice = "openrouter-api-key";
} else if (params.opts.tokenProvider === "vercel-ai-gateway") {
authChoice = "ai-gateway-api-key";
+ } else if (params.opts.tokenProvider === "cloudflare-ai-gateway") {
+ authChoice = "cloudflare-ai-gateway-api-key";
} else if (params.opts.tokenProvider === "moonshot") {
authChoice = "moonshot-api-key";
} else if (
@@ -231,6 +237,105 @@ export async function applyAuthChoiceApiProviders(
return { config: nextConfig, agentModelOverride };
}
+ if (authChoice === "cloudflare-ai-gateway-api-key") {
+ let hasCredential = false;
+ let accountId = params.opts?.cloudflareAiGatewayAccountId?.trim() ?? "";
+ let gatewayId = params.opts?.cloudflareAiGatewayGatewayId?.trim() ?? "";
+
+ const ensureAccountGateway = async () => {
+ if (!accountId) {
+ const value = await params.prompter.text({
+ message: "Enter Cloudflare Account ID",
+ validate: (val) => (String(val).trim() ? undefined : "Account ID is required"),
+ });
+ accountId = String(value).trim();
+ }
+ if (!gatewayId) {
+ const value = await params.prompter.text({
+ message: "Enter Cloudflare AI Gateway ID",
+ validate: (val) => (String(val).trim() ? undefined : "Gateway ID is required"),
+ });
+ gatewayId = String(value).trim();
+ }
+ };
+
+ const optsApiKey = normalizeApiKeyInput(params.opts?.cloudflareAiGatewayApiKey ?? "");
+ if (!hasCredential && accountId && gatewayId && optsApiKey) {
+ await setCloudflareAiGatewayConfig(accountId, gatewayId, optsApiKey, params.agentDir);
+ hasCredential = true;
+ }
+
+ const envKey = resolveEnvApiKey("cloudflare-ai-gateway");
+ if (!hasCredential && envKey) {
+ const useExisting = await params.prompter.confirm({
+ message: `Use existing CLOUDFLARE_AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
+ initialValue: true,
+ });
+ if (useExisting) {
+ await ensureAccountGateway();
+ await setCloudflareAiGatewayConfig(
+ accountId,
+ gatewayId,
+ normalizeApiKeyInput(envKey.apiKey),
+ params.agentDir,
+ );
+ hasCredential = true;
+ }
+ }
+
+ if (!hasCredential && optsApiKey) {
+ await ensureAccountGateway();
+ await setCloudflareAiGatewayConfig(accountId, gatewayId, optsApiKey, params.agentDir);
+ hasCredential = true;
+ }
+
+ if (!hasCredential) {
+ await ensureAccountGateway();
+ const key = await params.prompter.text({
+ message: "Enter Cloudflare AI Gateway API key",
+ validate: validateApiKeyInput,
+ });
+ await setCloudflareAiGatewayConfig(
+ accountId,
+ gatewayId,
+ normalizeApiKeyInput(String(key)),
+ params.agentDir,
+ );
+ hasCredential = true;
+ }
+
+ if (hasCredential) {
+ nextConfig = applyAuthProfileConfig(nextConfig, {
+ profileId: "cloudflare-ai-gateway:default",
+ provider: "cloudflare-ai-gateway",
+ mode: "api_key",
+ });
+ }
+ {
+ const applied = await applyDefaultModelChoice({
+ config: nextConfig,
+ setDefaultModel: params.setDefaultModel,
+ defaultModel: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
+ applyDefaultConfig: (cfg) =>
+ applyCloudflareAiGatewayConfig(cfg, {
+ accountId: accountId || params.opts?.cloudflareAiGatewayAccountId,
+ gatewayId: gatewayId || params.opts?.cloudflareAiGatewayGatewayId,
+ }),
+ applyProviderConfig: (cfg) =>
+ applyCloudflareAiGatewayProviderConfig(cfg, {
+ accountId: accountId || params.opts?.cloudflareAiGatewayAccountId,
+ gatewayId: gatewayId || params.opts?.cloudflareAiGatewayGatewayId,
+ }),
+ noteDefault: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
+ noteAgentModel,
+ prompter: params.prompter,
+ });
+ nextConfig = applied.config;
+ agentModelOverride = applied.agentModelOverride ?? agentModelOverride;
+ }
+ return { config: nextConfig, agentModelOverride };
+ }
+
if (authChoice === "moonshot-api-key") {
let hasCredential = false;
diff --git a/src/commands/auth-choice.apply.ts b/src/commands/auth-choice.apply.ts
index 37dc0f272e..53b22fdd47 100644
--- a/src/commands/auth-choice.apply.ts
+++ b/src/commands/auth-choice.apply.ts
@@ -24,6 +24,9 @@ export type ApplyAuthChoiceParams = {
opts?: {
tokenProvider?: string;
token?: string;
+ cloudflareAiGatewayAccountId?: string;
+ cloudflareAiGatewayGatewayId?: string;
+ cloudflareAiGatewayApiKey?: string;
};
};
diff --git a/src/commands/auth-choice.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts
index 14dcf30b28..ac530e169f 100644
--- a/src/commands/auth-choice.preferred-provider.ts
+++ b/src/commands/auth-choice.preferred-provider.ts
@@ -12,6 +12,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial> = {
"openai-api-key": "openai",
"openrouter-api-key": "openrouter",
"ai-gateway-api-key": "vercel-ai-gateway",
+ "cloudflare-ai-gateway-api-key": "cloudflare-ai-gateway",
"moonshot-api-key": "moonshot",
"moonshot-api-key-cn": "moonshot",
"kimi-code-api-key": "kimi-coding",
diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts
index c034b6144a..b13972f7b7 100644
--- a/src/commands/auth-choice.test.ts
+++ b/src/commands/auth-choice.test.ts
@@ -33,6 +33,7 @@ describe("applyAuthChoice", () => {
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
const previousOpenrouterKey = process.env.OPENROUTER_API_KEY;
const previousAiGatewayKey = process.env.AI_GATEWAY_API_KEY;
+ const previousCloudflareGatewayKey = process.env.CLOUDFLARE_AI_GATEWAY_API_KEY;
const previousSshTty = process.env.SSH_TTY;
const previousChutesClientId = process.env.CHUTES_CLIENT_ID;
let tempStateDir: string | null = null;
@@ -69,6 +70,11 @@ describe("applyAuthChoice", () => {
} else {
process.env.AI_GATEWAY_API_KEY = previousAiGatewayKey;
}
+ if (previousCloudflareGatewayKey === undefined) {
+ delete process.env.CLOUDFLARE_AI_GATEWAY_API_KEY;
+ } else {
+ process.env.CLOUDFLARE_AI_GATEWAY_API_KEY = previousCloudflareGatewayKey;
+ }
if (previousSshTty === undefined) {
delete process.env.SSH_TTY;
} else {
@@ -405,6 +411,76 @@ describe("applyAuthChoice", () => {
delete process.env.AI_GATEWAY_API_KEY;
});
+ it("uses existing CLOUDFLARE_AI_GATEWAY_API_KEY when selecting cloudflare-ai-gateway-api-key", 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;
+ process.env.CLOUDFLARE_AI_GATEWAY_API_KEY = "cf-gateway-test-key";
+
+ const text = vi
+ .fn()
+ .mockResolvedValueOnce("cf-account-id")
+ .mockResolvedValueOnce("cf-gateway-id");
+ const select: WizardPrompter["select"] = vi.fn(
+ async (params) => params.options[0]?.value as never,
+ );
+ const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
+ const confirm = vi.fn(async () => true);
+ const prompter: WizardPrompter = {
+ intro: vi.fn(noopAsync),
+ outro: vi.fn(noopAsync),
+ note: vi.fn(noopAsync),
+ select,
+ multiselect,
+ text,
+ confirm,
+ 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: "cloudflare-ai-gateway-api-key",
+ config: {},
+ prompter,
+ runtime,
+ setDefaultModel: true,
+ });
+
+ expect(confirm).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: expect.stringContaining("CLOUDFLARE_AI_GATEWAY_API_KEY"),
+ }),
+ );
+ expect(text).toHaveBeenCalledTimes(2);
+ expect(result.config.auth?.profiles?.["cloudflare-ai-gateway:default"]).toMatchObject({
+ provider: "cloudflare-ai-gateway",
+ mode: "api_key",
+ });
+ expect(result.config.agents?.defaults?.model?.primary).toBe(
+ "cloudflare-ai-gateway/claude-sonnet-4-5",
+ );
+
+ const authProfilePath = authProfilePathFor(requireAgentDir());
+ const raw = await fs.readFile(authProfilePath, "utf8");
+ const parsed = JSON.parse(raw) as {
+ profiles?: Record }>;
+ };
+ expect(parsed.profiles?.["cloudflare-ai-gateway:default"]?.key).toBe("cf-gateway-test-key");
+ expect(parsed.profiles?.["cloudflare-ai-gateway:default"]?.metadata).toEqual({
+ accountId: "cf-account-id",
+ gatewayId: "cf-gateway-id",
+ });
+
+ delete process.env.CLOUDFLARE_AI_GATEWAY_API_KEY;
+ });
+
it("writes Chutes OAuth credentials when selecting chutes (remote/manual)", async () => {
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
process.env.OPENCLAW_STATE_DIR = tempStateDir;
diff --git a/src/commands/models/list.auth-overview.ts b/src/commands/models/list.auth-overview.ts
index 7bd4014e2b..90c8a0defa 100644
--- a/src/commands/models/list.auth-overview.ts
+++ b/src/commands/models/list.auth-overview.ts
@@ -40,7 +40,7 @@ export function resolveProviderAuthOverview(params: {
return `${profileId}=missing`;
}
if (profile.type === "api_key") {
- return withUnusableSuffix(`${profileId}=${maskApiKey(profile.key)}`, profileId);
+ return withUnusableSuffix(`${profileId}=${maskApiKey(profile.key ?? "")}`, profileId);
}
if (profile.type === "token") {
return withUnusableSuffix(`${profileId}=token:${maskApiKey(profile.token)}`, profileId);
diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts
index 892d44224f..804035a918 100644
--- a/src/commands/onboard-auth.config-core.ts
+++ b/src/commands/onboard-auth.config-core.ts
@@ -1,4 +1,8 @@
import type { OpenClawConfig } from "../config/config.js";
+import {
+ buildCloudflareAiGatewayModelDefinition,
+ resolveCloudflareAiGatewayBaseUrl,
+} from "../agents/cloudflare-ai-gateway.js";
import { buildXiaomiProvider, XIAOMI_DEFAULT_MODEL_ID } from "../agents/models-config.providers.js";
import {
buildSyntheticModelDefinition,
@@ -13,6 +17,7 @@ import {
VENICE_MODEL_CATALOG,
} from "../agents/venice-models.js";
import {
+ CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
XIAOMI_DEFAULT_MODEL_REF,
@@ -93,6 +98,73 @@ export function applyVercelAiGatewayProviderConfig(cfg: OpenClawConfig): OpenCla
};
}
+export function applyCloudflareAiGatewayProviderConfig(
+ cfg: OpenClawConfig,
+ params?: { accountId?: string; gatewayId?: string },
+): OpenClawConfig {
+ const models = { ...cfg.agents?.defaults?.models };
+ models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF] = {
+ ...models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF],
+ alias: models[CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF]?.alias ?? "Cloudflare AI Gateway",
+ };
+
+ const providers = { ...cfg.models?.providers };
+ const existingProvider = providers["cloudflare-ai-gateway"];
+ const existingModels = Array.isArray(existingProvider?.models) ? existingProvider.models : [];
+ const defaultModel = buildCloudflareAiGatewayModelDefinition();
+ const hasDefaultModel = existingModels.some((model) => model.id === defaultModel.id);
+ const mergedModels = hasDefaultModel ? existingModels : [...existingModels, defaultModel];
+ const baseUrl =
+ params?.accountId && params?.gatewayId
+ ? resolveCloudflareAiGatewayBaseUrl({
+ accountId: params.accountId,
+ gatewayId: params.gatewayId,
+ })
+ : existingProvider?.baseUrl;
+
+ if (!baseUrl) {
+ return {
+ ...cfg,
+ agents: {
+ ...cfg.agents,
+ defaults: {
+ ...cfg.agents?.defaults,
+ models,
+ },
+ },
+ };
+ }
+
+ const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
+ string,
+ unknown
+ > as { apiKey?: string };
+ const resolvedApiKey = typeof existingApiKey === "string" ? existingApiKey : undefined;
+ const normalizedApiKey = resolvedApiKey?.trim();
+ providers["cloudflare-ai-gateway"] = {
+ ...existingProviderRest,
+ baseUrl,
+ api: "anthropic-messages",
+ ...(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 applyVercelAiGatewayConfig(cfg: OpenClawConfig): OpenClawConfig {
const next = applyVercelAiGatewayProviderConfig(cfg);
const existingModel = next.agents?.defaults?.model;
@@ -115,6 +187,31 @@ export function applyVercelAiGatewayConfig(cfg: OpenClawConfig): OpenClawConfig
};
}
+export function applyCloudflareAiGatewayConfig(
+ cfg: OpenClawConfig,
+ params?: { accountId?: string; gatewayId?: string },
+): OpenClawConfig {
+ const next = applyCloudflareAiGatewayProviderConfig(cfg, params);
+ 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: CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
+ },
+ },
+ },
+ };
+}
+
export function applyOpenrouterConfig(cfg: OpenClawConfig): OpenClawConfig {
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 1790925d7b..8d2dca121e 100644
--- a/src/commands/onboard-auth.credentials.ts
+++ b/src/commands/onboard-auth.credentials.ts
@@ -1,6 +1,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";
const resolveAuthAgentDir = (agentDir?: string) => agentDir ?? resolveOpenClawAgentDir();
@@ -155,6 +156,30 @@ export async function setOpenrouterApiKey(key: string, agentDir?: string) {
});
}
+export async function setCloudflareAiGatewayConfig(
+ accountId: string,
+ gatewayId: string,
+ apiKey: string,
+ agentDir?: string,
+) {
+ const normalizedAccountId = accountId.trim();
+ const normalizedGatewayId = gatewayId.trim();
+ const normalizedKey = apiKey.trim();
+ upsertAuthProfile({
+ profileId: "cloudflare-ai-gateway:default",
+ credential: {
+ type: "api_key",
+ provider: "cloudflare-ai-gateway",
+ key: normalizedKey,
+ metadata: {
+ accountId: normalizedAccountId,
+ gatewayId: normalizedGatewayId,
+ },
+ },
+ agentDir: resolveAuthAgentDir(agentDir),
+ });
+}
+
export async function setVercelAiGatewayApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "vercel-ai-gateway:default",
diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts
index e3fc17b822..97483e1ed5 100644
--- a/src/commands/onboard-auth.ts
+++ b/src/commands/onboard-auth.ts
@@ -5,6 +5,8 @@ export {
export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js";
export {
applyAuthProfileConfig,
+ applyCloudflareAiGatewayConfig,
+ applyCloudflareAiGatewayProviderConfig,
applyKimiCodeConfig,
applyKimiCodeProviderConfig,
applyMoonshotConfig,
@@ -37,8 +39,10 @@ export {
applyOpencodeZenProviderConfig,
} from "./onboard-auth.config-opencode.js";
export {
+ CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
OPENROUTER_DEFAULT_MODEL_REF,
setAnthropicApiKey,
+ setCloudflareAiGatewayConfig,
setGeminiApiKey,
setKimiCodingApiKey,
setMinimaxApiKey,
diff --git a/src/commands/onboard-non-interactive.cloudflare-ai-gateway.test.ts b/src/commands/onboard-non-interactive.cloudflare-ai-gateway.test.ts
new file mode 100644
index 0000000000..c3cc5667e8
--- /dev/null
+++ b/src/commands/onboard-non-interactive.cloudflare-ai-gateway.test.ts
@@ -0,0 +1,99 @@
+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): Cloudflare AI Gateway", () => {
+ 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-cf-gateway-"));
+ 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: "cloudflare-ai-gateway-api-key",
+ cloudflareAiGatewayAccountId: "cf-account-id",
+ cloudflareAiGatewayGatewayId: "cf-gateway-id",
+ cloudflareAiGatewayApiKey: "cf-gateway-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?.["cloudflare-ai-gateway:default"]?.provider).toBe(
+ "cloudflare-ai-gateway",
+ );
+ expect(cfg.auth?.profiles?.["cloudflare-ai-gateway:default"]?.mode).toBe("api_key");
+ expect(cfg.agents?.defaults?.model?.primary).toBe("cloudflare-ai-gateway/claude-sonnet-4-5");
+
+ const { ensureAuthProfileStore } = await import("../agents/auth-profiles.js");
+ const store = ensureAuthProfileStore();
+ const profile = store.profiles["cloudflare-ai-gateway:default"];
+ expect(profile?.type).toBe("api_key");
+ if (profile?.type === "api_key") {
+ expect(profile.provider).toBe("cloudflare-ai-gateway");
+ expect(profile.key).toBe("cf-gateway-test-key");
+ expect(profile.metadata).toEqual({
+ accountId: "cf-account-id",
+ gatewayId: "cf-gateway-id",
+ });
+ }
+ } 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.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts
index c38002d651..9b69f1dfda 100644
--- a/src/commands/onboard-non-interactive/local/auth-choice.ts
+++ b/src/commands/onboard-non-interactive/local/auth-choice.ts
@@ -10,6 +10,7 @@ import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-tok
import { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js";
import {
applyAuthProfileConfig,
+ applyCloudflareAiGatewayConfig,
applyKimiCodeConfig,
applyMinimaxApiConfig,
applyMinimaxConfig,
@@ -23,6 +24,7 @@ import {
applyXiaomiConfig,
applyZaiConfig,
setAnthropicApiKey,
+ setCloudflareAiGatewayConfig,
setGeminiApiKey,
setKimiCodingApiKey,
setMinimaxApiKey,
@@ -281,6 +283,44 @@ export async function applyNonInteractiveAuthChoice(params: {
return applyVercelAiGatewayConfig(nextConfig);
}
+ if (authChoice === "cloudflare-ai-gateway-api-key") {
+ const accountId = opts.cloudflareAiGatewayAccountId?.trim() ?? "";
+ const gatewayId = opts.cloudflareAiGatewayGatewayId?.trim() ?? "";
+ if (!accountId || !gatewayId) {
+ runtime.error(
+ [
+ 'Auth choice "cloudflare-ai-gateway-api-key" requires Account ID and Gateway ID.',
+ "Use --cloudflare-ai-gateway-account-id and --cloudflare-ai-gateway-gateway-id.",
+ ].join("\n"),
+ );
+ runtime.exit(1);
+ return null;
+ }
+ const resolved = await resolveNonInteractiveApiKey({
+ provider: "cloudflare-ai-gateway",
+ cfg: baseConfig,
+ flagValue: opts.cloudflareAiGatewayApiKey,
+ flagName: "--cloudflare-ai-gateway-api-key",
+ envVar: "CLOUDFLARE_AI_GATEWAY_API_KEY",
+ runtime,
+ });
+ if (!resolved) {
+ return null;
+ }
+ if (resolved.source !== "profile") {
+ await setCloudflareAiGatewayConfig(accountId, gatewayId, resolved.key);
+ }
+ nextConfig = applyAuthProfileConfig(nextConfig, {
+ profileId: "cloudflare-ai-gateway:default",
+ provider: "cloudflare-ai-gateway",
+ mode: "api_key",
+ });
+ return applyCloudflareAiGatewayConfig(nextConfig, {
+ accountId,
+ gatewayId,
+ });
+ }
+
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 2111e4ff17..ad0406efd1 100644
--- a/src/commands/onboard-types.ts
+++ b/src/commands/onboard-types.ts
@@ -13,6 +13,7 @@ export type AuthChoice =
| "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"
@@ -66,6 +67,9 @@ export type OnboardOptions = {
openaiApiKey?: string;
openrouterApiKey?: string;
aiGatewayApiKey?: string;
+ cloudflareAiGatewayAccountId?: string;
+ cloudflareAiGatewayGatewayId?: string;
+ cloudflareAiGatewayApiKey?: string;
moonshotApiKey?: string;
kimiCodeApiKey?: string;
geminiApiKey?: string;