mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 13:11:22 +00:00
feat: add cloudflare ai gateway provider
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ export type ApplyAuthChoiceParams = {
|
||||
opts?: {
|
||||
tokenProvider?: string;
|
||||
token?: string;
|
||||
cloudflareAiGatewayAccountId?: string;
|
||||
cloudflareAiGatewayGatewayId?: string;
|
||||
cloudflareAiGatewayApiKey?: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const PREFERRED_PROVIDER_BY_AUTH_CHOICE: Partial<Record<AuthChoice, string>> = {
|
||||
"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",
|
||||
|
||||
@@ -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<string, { key?: string; metadata?: Record<string, string> }>;
|
||||
};
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string, unknown>)
|
||||
? {
|
||||
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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, { provider?: string; mode?: string }>;
|
||||
};
|
||||
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);
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user