From 1f8909882e951c1d50c318dd8a0fcab5f1a48319 Mon Sep 17 00:00:00 2001 From: Rohan Mukherjee Date: Fri, 30 Jan 2026 20:07:00 +0530 Subject: [PATCH] feat: add cloudflare AI gateway as a provider --- docs/providers/cloudflare-ai-gateway.md | 97 +++++++++++++++++++ src/agents/model-auth.ts | 1 + src/cli/program/register.onboard.ts | 5 +- src/commands/auth-choice-options.ts | 11 +++ .../auth-choice.apply.api-providers.ts | 97 +++++++++++++++++++ .../auth-choice.preferred-provider.ts | 1 + src/commands/onboard-auth.config-core.ts | 42 ++++++++ src/commands/onboard-auth.credentials.ts | 22 +++++ src/commands/onboard-auth.ts | 4 + src/commands/onboard-types.ts | 4 + 10 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 docs/providers/cloudflare-ai-gateway.md diff --git a/docs/providers/cloudflare-ai-gateway.md b/docs/providers/cloudflare-ai-gateway.md new file mode 100644 index 000000000..316be198c --- /dev/null +++ b/docs/providers/cloudflare-ai-gateway.md @@ -0,0 +1,97 @@ +--- +title: "Cloudflare AI Gateway" +summary: "Cloudflare AI Gateway setup (analytics, caching, rate limiting)" +read_when: + - You want to use Cloudflare AI Gateway with OpenClaw + - You need analytics, caching, or rate limiting for AI requests +--- +# Cloudflare AI Gateway + +The [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) provides visibility and control over your AI applications with features like analytics, logging, caching, rate limiting, request retries, and model fallback. + +- Provider: `cloudflare-ai-gateway` +- Auth: Account ID + Gateway ID + API Key (optional for unauthenticated gateway) +- API: Anthropic Messages compatible (via provider-specific endpoints) + +## Quick start + +1) Set up your Cloudflare AI Gateway credentials: + +```bash +openclaw onboard --auth-choice cloudflare-ai-gateway-api-key +``` + +You'll be prompted to enter: +- Cloudflare Account ID (found in your Cloudflare dashboard) +- Cloudflare AI Gateway ID (the name you give your gateway) +- Cloudflare AI Gateway API key (optional, only needed for authenticated gateways) + +2) Set a default model: + +```json5 +{ + agents: { + defaults: { + model: { primary: "cloudflare-ai-gateway/anthropic/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 "your-api-key" +``` + +## Setting up your Cloudflare AI Gateway + +1. Log into the [Cloudflare dashboard](https://dash.cloudflare.com/) +2. Go to **AI** > **AI Gateway** +3. Select **Create Gateway** +4. Enter your **Gateway name** (this becomes your gateway ID) +5. (Optional) Enable authentication for added security + +## Features + +### Analytics +View metrics such as the number of requests, tokens, and cost of running your application. + +### Caching +Serve requests directly from Cloudflare's cache for faster requests and cost savings. + +### Rate Limiting +Control how your application scales by limiting the number of requests. + +### Request Retry and Fallback +Improve resilience by defining request retry and model fallbacks in case of an error. + +## Supported Providers + +Cloudflare AI Gateway works with: +- OpenAI +- Anthropic (Claude) +- Google AI Studio (Gemini) +- Azure OpenAI +- AWS Bedrock +- Workers AI +- And many more + +## Environment note + +If the Gateway runs as a daemon (launchd/systemd), make sure credentials are available to that process (for example, in `~/.openclaw/.env` or via `env.shellEnv`). + +You can also set environment variables: +- `CLOUDFLARE_AI_GATEWAY_API_KEY` - Your Cloudflare AI Gateway API key +- Account ID and Gateway ID are stored in the auth profile metadata + +## Learn more + +- [Cloudflare AI Gateway Documentation](https://developers.cloudflare.com/ai-gateway/) +- [Getting Started Guide](https://developers.cloudflare.com/ai-gateway/get-started/) +- [Authentication](https://developers.cloudflare.com/ai-gateway/configuration/authentication/) diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 1445b53f7..bf65c6f63 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -278,6 +278,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", "kimi-code": "KIMICODE_API_KEY", minimax: "MINIMAX_API_KEY", diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index 3f81a5ee8..307591d07 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -52,7 +52,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|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|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 ", @@ -68,6 +68,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 Code API key") .option("--gemini-api-key ", "Gemini API key") diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 5acddf4e3..3d34c46e7 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -96,6 +96,12 @@ const AUTH_CHOICE_GROUP_DEFS: { hint: "API key", choices: ["ai-gateway-api-key"], }, + { + value: "cloudflare-ai-gateway", + label: "Cloudflare AI Gateway", + hint: "Account ID + Gateway ID + API key", + choices: ["cloudflare-ai-gateway-api-key"], + }, { value: "moonshot", label: "Moonshot AI", @@ -146,6 +152,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: "Moonshot AI API key" }); options.push({ value: "kimi-code-api-key", label: "Kimi Code API key" }); options.push({ value: "synthetic-api-key", label: "Synthetic API key" }); diff --git a/src/commands/auth-choice.apply.api-providers.ts b/src/commands/auth-choice.apply.api-providers.ts index fa4fc77e7..17136a6cb 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, @@ -30,6 +32,7 @@ import { applyXiaomiConfig, applyXiaomiProviderConfig, applyZaiConfig, + CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, KIMI_CODE_MODEL_REF, MOONSHOT_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, @@ -37,6 +40,7 @@ import { VENICE_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, XIAOMI_DEFAULT_MODEL_REF, + setCloudflareAiGatewayConfig, setGeminiApiKey, setKimiCodeApiKey, setMoonshotApiKey, @@ -75,6 +79,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 (params.opts.tokenProvider === "kimi-code") { @@ -224,6 +230,97 @@ export async function applyAuthChoiceApiProviders( return { config: nextConfig, agentModelOverride }; } + if (authChoice === "cloudflare-ai-gateway-api-key") { + let hasCredential = false; + + if ( + !hasCredential && + params.opts?.cloudflareAiGatewayAccountId && + params.opts?.cloudflareAiGatewayGatewayId && + params.opts?.cloudflareAiGatewayApiKey + ) { + await setCloudflareAiGatewayConfig( + params.opts.cloudflareAiGatewayAccountId, + params.opts.cloudflareAiGatewayGatewayId, + normalizeApiKeyInput(params.opts.cloudflareAiGatewayApiKey), + params.agentDir, + ); + hasCredential = true; + } + + const envKey = resolveEnvApiKey("cloudflare-ai-gateway"); + if (envKey) { + const useExisting = await params.prompter.confirm({ + message: `Use existing CLOUDFLARE_AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, + initialValue: true, + }); + if (useExisting) { + const accountId = await params.prompter.text({ + message: "Enter Cloudflare Account ID", + validate: (val) => (String(val).trim() ? undefined : "Account ID is required"), + }); + const gatewayId = await params.prompter.text({ + message: "Enter Cloudflare AI Gateway ID", + validate: (val) => (String(val).trim() ? undefined : "Gateway ID is required"), + }); + await setCloudflareAiGatewayConfig( + String(accountId).trim(), + String(gatewayId).trim(), + envKey.apiKey, + params.agentDir, + ); + hasCredential = true; + } + } + if (!hasCredential) { + await params.prompter.note( + [ + "Cloudflare AI Gateway provides analytics, caching, and rate limiting for AI requests.", + "Get your credentials at: https://dash.cloudflare.com/", + ].join("\n"), + "Cloudflare AI Gateway", + ); + const accountId = await params.prompter.text({ + message: "Enter Cloudflare Account ID", + validate: (val) => (String(val).trim() ? undefined : "Account ID is required"), + }); + const gatewayId = await params.prompter.text({ + message: "Enter Cloudflare AI Gateway ID", + validate: (val) => (String(val).trim() ? undefined : "Gateway ID is required"), + }); + const key = await params.prompter.text({ + message: "Enter Cloudflare AI Gateway API key (optional for unauthenticated gateway)", + validate: validateApiKeyInput, + }); + await setCloudflareAiGatewayConfig( + String(accountId).trim(), + String(gatewayId).trim(), + normalizeApiKeyInput(String(key)), + params.agentDir, + ); + } + 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: applyCloudflareAiGatewayConfig, + applyProviderConfig: applyCloudflareAiGatewayProviderConfig, + 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.preferred-provider.ts b/src/commands/auth-choice.preferred-provider.ts index a4d831c92..bb1c5fadc 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", "kimi-code-api-key": "kimi-code", "gemini-api-key": "google", diff --git a/src/commands/onboard-auth.config-core.ts b/src/commands/onboard-auth.config-core.ts index c94eeb51b..2f90369c3 100644 --- a/src/commands/onboard-auth.config-core.ts +++ b/src/commands/onboard-auth.config-core.ts @@ -13,6 +13,7 @@ import { } from "../agents/venice-models.js"; import type { OpenClawConfig } from "../config/config.js"; import { + CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF, XIAOMI_DEFAULT_MODEL_REF, @@ -117,6 +118,47 @@ export function applyVercelAiGatewayConfig(cfg: OpenClawConfig): OpenClawConfig }; } +export function applyCloudflareAiGatewayProviderConfig(cfg: OpenClawConfig): 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", + }; + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + models, + }, + }, + }; +} + +export function applyCloudflareAiGatewayConfig(cfg: OpenClawConfig): OpenClawConfig { + const next = applyCloudflareAiGatewayProviderConfig(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: 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 fbf6dbfb9..ef2f32010 100644 --- a/src/commands/onboard-auth.credentials.ts +++ b/src/commands/onboard-auth.credentials.ts @@ -116,6 +116,7 @@ export const ZAI_DEFAULT_MODEL_REF = "zai/glm-4.7"; export const XIAOMI_DEFAULT_MODEL_REF = "xiaomi/mimo-v2-flash"; 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 const CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF = "cloudflare-ai-gateway/anthropic/claude-sonnet-4-5"; export async function setZaiApiKey(key: string, agentDir?: string) { // Write to resolved agent dir so gateway finds credentials on startup. @@ -177,3 +178,24 @@ export async function setOpencodeZenApiKey(key: string, agentDir?: string) { agentDir: resolveAuthAgentDir(agentDir), }); } + +export async function setCloudflareAiGatewayConfig( + accountId: string, + gatewayId: string, + apiKey: string, + agentDir?: string, +) { + upsertAuthProfile({ + profileId: "cloudflare-ai-gateway:default", + credential: { + type: "api_key", + provider: "cloudflare-ai-gateway", + key: apiKey, + metadata: { + accountId, + gatewayId, + }, + }, + agentDir: resolveAuthAgentDir(agentDir), + }); +} diff --git a/src/commands/onboard-auth.ts b/src/commands/onboard-auth.ts index 612b24865..70950d460 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, @@ -35,8 +37,10 @@ export { applyOpencodeZenProviderConfig, } from "./onboard-auth.config-opencode.js"; export { + CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF, OPENROUTER_DEFAULT_MODEL_REF, setAnthropicApiKey, + setCloudflareAiGatewayConfig, setGeminiApiKey, setKimiCodeApiKey, setMinimaxApiKey, diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index f4154bc6d..eb6a1b9f0 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" | "kimi-code-api-key" | "synthetic-api-key" @@ -64,6 +65,9 @@ export type OnboardOptions = { openaiApiKey?: string; openrouterApiKey?: string; aiGatewayApiKey?: string; + cloudflareAiGatewayAccountId?: string; + cloudflareAiGatewayGatewayId?: string; + cloudflareAiGatewayApiKey?: string; moonshotApiKey?: string; kimiCodeApiKey?: string; geminiApiKey?: string;