feat: add cloudflare ai gateway provider
parent
19ecdce275
commit
5b0851ebd8
|
|
@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||||
|
|
||||||
### Changes
|
### 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.
|
- 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.
|
- 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.
|
- Cron: add announce delivery mode for isolated jobs (CLI + Control UI) and delivery mode config.
|
||||||
|
|
|
||||||
|
|
@ -535,5 +535,5 @@ Thanks to all clawtributors:
|
||||||
<a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/search?q=wolfred"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="wolfred" title="wolfred"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/YangHuang2280"><img src="https://avatars.githubusercontent.com/u/201681634?v=4&s=48" width="48" height="48" alt="YangHuang2280" title="YangHuang2280"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/yevhen"><img src="https://avatars.githubusercontent.com/u/107726?v=4&s=48" width="48" height="48" alt="yevhen" title="yevhen"/></a> <a href="https://github.com/YiWang24"><img src="https://avatars.githubusercontent.com/u/176262341?v=4&s=48" width="48" height="48" alt="YiWang24" title="YiWang24"/></a> <a href="https://github.com/search?q=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></a>
|
<a href="https://github.com/voidserf"><img src="https://avatars.githubusercontent.com/u/477673?v=4&s=48" width="48" height="48" alt="voidserf" title="voidserf"/></a> <a href="https://github.com/search?q=Vultr-Clawd%20Admin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Vultr-Clawd Admin" title="Vultr-Clawd Admin"/></a> <a href="https://github.com/search?q=Wimmie"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Wimmie" title="Wimmie"/></a> <a href="https://github.com/search?q=wolfred"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="wolfred" title="wolfred"/></a> <a href="https://github.com/wstock"><img src="https://avatars.githubusercontent.com/u/1394687?v=4&s=48" width="48" height="48" alt="wstock" title="wstock"/></a> <a href="https://github.com/YangHuang2280"><img src="https://avatars.githubusercontent.com/u/201681634?v=4&s=48" width="48" height="48" alt="YangHuang2280" title="YangHuang2280"/></a> <a href="https://github.com/yazinsai"><img src="https://avatars.githubusercontent.com/u/1846034?v=4&s=48" width="48" height="48" alt="yazinsai" title="yazinsai"/></a> <a href="https://github.com/yevhen"><img src="https://avatars.githubusercontent.com/u/107726?v=4&s=48" width="48" height="48" alt="yevhen" title="yevhen"/></a> <a href="https://github.com/YiWang24"><img src="https://avatars.githubusercontent.com/u/176262341?v=4&s=48" width="48" height="48" alt="YiWang24" title="YiWang24"/></a> <a href="https://github.com/search?q=ymat19"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ymat19" title="ymat19"/></a>
|
||||||
<a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/zackerthescar"><img src="https://avatars.githubusercontent.com/u/38077284?v=4&s=48" width="48" height="48" alt="zackerthescar" title="zackerthescar"/></a> <a href="https://github.com/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a>
|
<a href="https://github.com/search?q=Zach%20Knickerbocker"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Zach Knickerbocker" title="Zach Knickerbocker"/></a> <a href="https://github.com/zackerthescar"><img src="https://avatars.githubusercontent.com/u/38077284?v=4&s=48" width="48" height="48" alt="zackerthescar" title="zackerthescar"/></a> <a href="https://github.com/0xJonHoldsCrypto"><img src="https://avatars.githubusercontent.com/u/81202085?v=4&s=48" width="48" height="48" alt="0xJonHoldsCrypto" title="0xJonHoldsCrypto"/></a> <a href="https://github.com/aaronn"><img src="https://avatars.githubusercontent.com/u/1653630?v=4&s=48" width="48" height="48" alt="aaronn" title="aaronn"/></a> <a href="https://github.com/Alphonse-arianee"><img src="https://avatars.githubusercontent.com/u/254457365?v=4&s=48" width="48" height="48" alt="Alphonse-arianee" title="Alphonse-arianee"/></a> <a href="https://github.com/atalovesyou"><img src="https://avatars.githubusercontent.com/u/3534502?v=4&s=48" width="48" height="48" alt="atalovesyou" title="atalovesyou"/></a> <a href="https://github.com/search?q=Azade"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Azade" title="Azade"/></a> <a href="https://github.com/carlulsoe"><img src="https://avatars.githubusercontent.com/u/34673973?v=4&s=48" width="48" height="48" alt="carlulsoe" title="carlulsoe"/></a> <a href="https://github.com/search?q=ddyo"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="ddyo" title="ddyo"/></a> <a href="https://github.com/search?q=Erik"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Erik" title="Erik"/></a>
|
||||||
<a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a>
|
<a href="https://github.com/latitudeki5223"><img src="https://avatars.githubusercontent.com/u/119656367?v=4&s=48" width="48" height="48" alt="latitudeki5223" title="latitudeki5223"/></a> <a href="https://github.com/search?q=Manuel%20Maly"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Manuel Maly" title="Manuel Maly"/></a> <a href="https://github.com/search?q=Mourad%20Boustani"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Mourad Boustani" title="Mourad Boustani"/></a> <a href="https://github.com/odrobnik"><img src="https://avatars.githubusercontent.com/u/333270?v=4&s=48" width="48" height="48" alt="odrobnik" title="odrobnik"/></a> <a href="https://github.com/pcty-nextgen-ios-builder"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="pcty-nextgen-ios-builder" title="pcty-nextgen-ios-builder"/></a> <a href="https://github.com/search?q=Quentin"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Quentin" title="Quentin"/></a> <a href="https://github.com/search?q=Randy%20Torres"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Randy Torres" title="Randy Torres"/></a> <a href="https://github.com/rhjoh"><img src="https://avatars.githubusercontent.com/u/105699450?v=4&s=48" width="48" height="48" alt="rhjoh" title="rhjoh"/></a> <a href="https://github.com/search?q=Rolf%20Fredheim"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="Rolf Fredheim" title="Rolf Fredheim"/></a> <a href="https://github.com/ronak-guliani"><img src="https://avatars.githubusercontent.com/u/23518228?v=4&s=48" width="48" height="48" alt="ronak-guliani" title="ronak-guliani"/></a>
|
||||||
<a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a>
|
<a href="https://github.com/search?q=William%20Stock"><img src="assets/avatar-placeholder.svg" width="48" height="48" alt="William Stock" title="William Stock"/></a> <a href="https://github.com/roerohan"><img src="https://avatars.githubusercontent.com/u/42958812?v=4&s=48" width="48" height="48" alt="roerohan" title="roerohan"/></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -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/<account_id>/<gateway_id>/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 <cloudflare-ai-gateway-token>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`).
|
||||||
|
|
@ -40,6 +40,7 @@ See [Venice AI](/providers/venice).
|
||||||
- [Qwen (OAuth)](/providers/qwen)
|
- [Qwen (OAuth)](/providers/qwen)
|
||||||
- [OpenRouter](/providers/openrouter)
|
- [OpenRouter](/providers/openrouter)
|
||||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
|
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||||
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
|
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
|
||||||
- [OpenCode Zen](/providers/opencode)
|
- [OpenCode Zen](/providers/opencode)
|
||||||
- [Amazon Bedrock](/bedrock)
|
- [Amazon Bedrock](/bedrock)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ See [Venice AI](/providers/venice).
|
||||||
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
- [Anthropic (API + Claude Code CLI)](/providers/anthropic)
|
||||||
- [OpenRouter](/providers/openrouter)
|
- [OpenRouter](/providers/openrouter)
|
||||||
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
- [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
||||||
|
- [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway)
|
||||||
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
|
- [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot)
|
||||||
- [Synthetic](/providers/synthetic)
|
- [Synthetic](/providers/synthetic)
|
||||||
- [OpenCode Zen](/providers/opencode)
|
- [OpenCode Zen](/providers/opencode)
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
|
||||||
- **API key**: stores the key for you.
|
- **API key**: stores the key for you.
|
||||||
- **Vercel AI Gateway (multi-model proxy)**: prompts for `AI_GATEWAY_API_KEY`.
|
- **Vercel AI Gateway (multi-model proxy)**: prompts for `AI_GATEWAY_API_KEY`.
|
||||||
- More detail: [Vercel AI Gateway](/providers/vercel-ai-gateway)
|
- 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.
|
- **MiniMax M2.1**: config is auto-written.
|
||||||
- More detail: [MiniMax](/providers/minimax)
|
- More detail: [MiniMax](/providers/minimax)
|
||||||
- **Synthetic (Anthropic-compatible)**: prompts for `SYNTHETIC_API_KEY`.
|
- **Synthetic (Anthropic-compatible)**: prompts for `SYNTHETIC_API_KEY`.
|
||||||
|
|
@ -239,6 +241,19 @@ openclaw onboard --non-interactive \
|
||||||
--gateway-bind loopback
|
--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:
|
Moonshot example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,11 @@ export async function resolveApiKeyForProfile(params: {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cred.type === "api_key") {
|
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") {
|
if (cred.type === "token") {
|
||||||
const token = cred.token?.trim();
|
const token = cred.token?.trim();
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ import type { OpenClawConfig } from "../../config/config.js";
|
||||||
export type ApiKeyCredential = {
|
export type ApiKeyCredential = {
|
||||||
type: "api_key";
|
type: "api_key";
|
||||||
provider: string;
|
provider: string;
|
||||||
key: string;
|
key?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
/** Optional provider-specific metadata (e.g., account IDs, gateway IDs). */
|
||||||
|
metadata?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TokenCredential = {
|
export type TokenCredential = {
|
||||||
|
|
|
||||||
|
|
@ -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`;
|
||||||
|
}
|
||||||
|
|
@ -293,6 +293,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null {
|
||||||
xai: "XAI_API_KEY",
|
xai: "XAI_API_KEY",
|
||||||
openrouter: "OPENROUTER_API_KEY",
|
openrouter: "OPENROUTER_API_KEY",
|
||||||
"vercel-ai-gateway": "AI_GATEWAY_API_KEY",
|
"vercel-ai-gateway": "AI_GATEWAY_API_KEY",
|
||||||
|
"cloudflare-ai-gateway": "CLOUDFLARE_AI_GATEWAY_API_KEY",
|
||||||
moonshot: "MOONSHOT_API_KEY",
|
moonshot: "MOONSHOT_API_KEY",
|
||||||
minimax: "MINIMAX_API_KEY",
|
minimax: "MINIMAX_API_KEY",
|
||||||
xiaomi: "XIAOMI_API_KEY",
|
xiaomi: "XIAOMI_API_KEY",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ import {
|
||||||
} from "../providers/github-copilot-token.js";
|
} from "../providers/github-copilot-token.js";
|
||||||
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
|
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
|
||||||
import { discoverBedrockModels } from "./bedrock-discovery.js";
|
import { discoverBedrockModels } from "./bedrock-discovery.js";
|
||||||
|
import {
|
||||||
|
buildCloudflareAiGatewayModelDefinition,
|
||||||
|
resolveCloudflareAiGatewayBaseUrl,
|
||||||
|
} from "./cloudflare-ai-gateway.js";
|
||||||
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
|
import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js";
|
||||||
import {
|
import {
|
||||||
buildSyntheticModelDefinition,
|
buildSyntheticModelDefinition,
|
||||||
|
|
@ -453,6 +457,34 @@ export async function resolveImplicitProviders(params: {
|
||||||
providers.xiaomi = { ...buildXiaomiProvider(), apiKey: xiaomiKey };
|
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
|
// Ollama provider - only add if explicitly configured
|
||||||
const ollamaKey =
|
const ollamaKey =
|
||||||
resolveEnvApiKeyVarName("ollama") ??
|
resolveEnvApiKeyVarName("ollama") ??
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ function resolveModelAuthLabel(params: {
|
||||||
if (profile.type === "token") {
|
if (profile.type === "token") {
|
||||||
return `token ${formatApiKeySnippet(profile.token)}${label ? ` (${label})` : ""}`;
|
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);
|
const envKey = resolveEnvApiKey(providerKey);
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function resolveModelAuthLabel(
|
||||||
const snippet = formatApiKeySnippet(profile.token);
|
const snippet = formatApiKeySnippet(profile.token);
|
||||||
return `token ${snippet}${label ? ` (${label})` : ""}`;
|
return `token ${snippet}${label ? ` (${label})` : ""}`;
|
||||||
}
|
}
|
||||||
const snippet = formatApiKeySnippet(profile.key);
|
const snippet = formatApiKeySnippet(profile.key ?? "");
|
||||||
return `api-key ${snippet}${label ? ` (${label})` : ""}`;
|
return `api-key ${snippet}${label ? ` (${label})` : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ export const resolveAuthLabel = async (
|
||||||
|
|
||||||
if (profile.type === "api_key") {
|
if (profile.type === "api_key") {
|
||||||
return {
|
return {
|
||||||
label: `${profileId} api-key ${maskApiKey(profile.key)}${more}`,
|
label: `${profileId} api-key ${maskApiKey(profile.key ?? "")}${more}`,
|
||||||
source: "",
|
source: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +154,7 @@ export const resolveAuthLabel = async (
|
||||||
}
|
}
|
||||||
if (profile.type === "api_key") {
|
if (profile.type === "api_key") {
|
||||||
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
const suffix = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
||||||
return `${profileId}=${maskApiKey(profile.key)}${suffix}`;
|
return `${profileId}=${maskApiKey(profile.key ?? "")}${suffix}`;
|
||||||
}
|
}
|
||||||
if (profile.type === "token") {
|
if (profile.type === "token") {
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export function registerOnboardCommand(program: Command) {
|
||||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||||
.option(
|
.option(
|
||||||
"--auth-choice <choice>",
|
"--auth-choice <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(
|
.option(
|
||||||
"--token-provider <id>",
|
"--token-provider <id>",
|
||||||
|
|
@ -74,6 +74,9 @@ export function registerOnboardCommand(program: Command) {
|
||||||
.option("--openai-api-key <key>", "OpenAI API key")
|
.option("--openai-api-key <key>", "OpenAI API key")
|
||||||
.option("--openrouter-api-key <key>", "OpenRouter API key")
|
.option("--openrouter-api-key <key>", "OpenRouter API key")
|
||||||
.option("--ai-gateway-api-key <key>", "Vercel AI Gateway API key")
|
.option("--ai-gateway-api-key <key>", "Vercel AI Gateway API key")
|
||||||
|
.option("--cloudflare-ai-gateway-account-id <id>", "Cloudflare Account ID")
|
||||||
|
.option("--cloudflare-ai-gateway-gateway-id <id>", "Cloudflare AI Gateway ID")
|
||||||
|
.option("--cloudflare-ai-gateway-api-key <key>", "Cloudflare AI Gateway API key")
|
||||||
.option("--moonshot-api-key <key>", "Moonshot API key")
|
.option("--moonshot-api-key <key>", "Moonshot API key")
|
||||||
.option("--kimi-code-api-key <key>", "Kimi Coding API key")
|
.option("--kimi-code-api-key <key>", "Kimi Coding API key")
|
||||||
.option("--gemini-api-key <key>", "Gemini API key")
|
.option("--gemini-api-key <key>", "Gemini API key")
|
||||||
|
|
@ -125,6 +128,9 @@ export function registerOnboardCommand(program: Command) {
|
||||||
openaiApiKey: opts.openaiApiKey as string | undefined,
|
openaiApiKey: opts.openaiApiKey as string | undefined,
|
||||||
openrouterApiKey: opts.openrouterApiKey as string | undefined,
|
openrouterApiKey: opts.openrouterApiKey as string | undefined,
|
||||||
aiGatewayApiKey: opts.aiGatewayApiKey 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,
|
moonshotApiKey: opts.moonshotApiKey as string | undefined,
|
||||||
kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined,
|
kimiCodeApiKey: opts.kimiCodeApiKey as string | undefined,
|
||||||
geminiApiKey: opts.geminiApiKey as string | undefined,
|
geminiApiKey: opts.geminiApiKey as string | undefined,
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,16 @@ describe("buildAuthChoiceOptions", () => {
|
||||||
expect(options.some((opt) => opt.value === "ai-gateway-api-key")).toBe(true);
|
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", () => {
|
it("includes Synthetic auth choice", () => {
|
||||||
const store: AuthProfileStore = { version: 1, profiles: {} };
|
const store: AuthProfileStore = { version: 1, profiles: {} };
|
||||||
const options = buildAuthChoiceOptions({
|
const options = buildAuthChoiceOptions({
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export type AuthChoiceGroupId =
|
||||||
| "copilot"
|
| "copilot"
|
||||||
| "openrouter"
|
| "openrouter"
|
||||||
| "ai-gateway"
|
| "ai-gateway"
|
||||||
|
| "cloudflare-ai-gateway"
|
||||||
| "moonshot"
|
| "moonshot"
|
||||||
| "zai"
|
| "zai"
|
||||||
| "xiaomi"
|
| "xiaomi"
|
||||||
|
|
@ -120,6 +121,12 @@ const AUTH_CHOICE_GROUP_DEFS: {
|
||||||
hint: "Privacy-focused (uncensored models)",
|
hint: "Privacy-focused (uncensored models)",
|
||||||
choices: ["venice-api-key"],
|
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: {
|
export function buildAuthChoiceOptions(params: {
|
||||||
|
|
@ -146,6 +153,11 @@ export function buildAuthChoiceOptions(params: {
|
||||||
value: "ai-gateway-api-key",
|
value: "ai-gateway-api-key",
|
||||||
label: "Vercel 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({
|
options.push({
|
||||||
value: "moonshot-api-key",
|
value: "moonshot-api-key",
|
||||||
label: "Kimi API key (.ai)",
|
label: "Kimi API key (.ai)",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import {
|
||||||
} from "./google-gemini-model-default.js";
|
} from "./google-gemini-model-default.js";
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
|
applyCloudflareAiGatewayConfig,
|
||||||
|
applyCloudflareAiGatewayProviderConfig,
|
||||||
applyKimiCodeConfig,
|
applyKimiCodeConfig,
|
||||||
applyKimiCodeProviderConfig,
|
applyKimiCodeProviderConfig,
|
||||||
applyMoonshotConfig,
|
applyMoonshotConfig,
|
||||||
|
|
@ -32,6 +34,7 @@ import {
|
||||||
applyXiaomiConfig,
|
applyXiaomiConfig,
|
||||||
applyXiaomiProviderConfig,
|
applyXiaomiProviderConfig,
|
||||||
applyZaiConfig,
|
applyZaiConfig,
|
||||||
|
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||||
KIMI_CODING_MODEL_REF,
|
KIMI_CODING_MODEL_REF,
|
||||||
MOONSHOT_DEFAULT_MODEL_REF,
|
MOONSHOT_DEFAULT_MODEL_REF,
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
|
|
@ -39,6 +42,7 @@ import {
|
||||||
VENICE_DEFAULT_MODEL_REF,
|
VENICE_DEFAULT_MODEL_REF,
|
||||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||||
XIAOMI_DEFAULT_MODEL_REF,
|
XIAOMI_DEFAULT_MODEL_REF,
|
||||||
|
setCloudflareAiGatewayConfig,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
setKimiCodingApiKey,
|
setKimiCodingApiKey,
|
||||||
setMoonshotApiKey,
|
setMoonshotApiKey,
|
||||||
|
|
@ -79,6 +83,8 @@ export async function applyAuthChoiceApiProviders(
|
||||||
authChoice = "openrouter-api-key";
|
authChoice = "openrouter-api-key";
|
||||||
} else if (params.opts.tokenProvider === "vercel-ai-gateway") {
|
} else if (params.opts.tokenProvider === "vercel-ai-gateway") {
|
||||||
authChoice = "ai-gateway-api-key";
|
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") {
|
} else if (params.opts.tokenProvider === "moonshot") {
|
||||||
authChoice = "moonshot-api-key";
|
authChoice = "moonshot-api-key";
|
||||||
} else if (
|
} else if (
|
||||||
|
|
@ -231,6 +237,105 @@ export async function applyAuthChoiceApiProviders(
|
||||||
return { config: nextConfig, agentModelOverride };
|
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") {
|
if (authChoice === "moonshot-api-key") {
|
||||||
let hasCredential = false;
|
let hasCredential = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@ export type ApplyAuthChoiceParams = {
|
||||||
opts?: {
|
opts?: {
|
||||||
tokenProvider?: string;
|
tokenProvider?: string;
|
||||||
token?: 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",
|
"openai-api-key": "openai",
|
||||||
"openrouter-api-key": "openrouter",
|
"openrouter-api-key": "openrouter",
|
||||||
"ai-gateway-api-key": "vercel-ai-gateway",
|
"ai-gateway-api-key": "vercel-ai-gateway",
|
||||||
|
"cloudflare-ai-gateway-api-key": "cloudflare-ai-gateway",
|
||||||
"moonshot-api-key": "moonshot",
|
"moonshot-api-key": "moonshot",
|
||||||
"moonshot-api-key-cn": "moonshot",
|
"moonshot-api-key-cn": "moonshot",
|
||||||
"kimi-code-api-key": "kimi-coding",
|
"kimi-code-api-key": "kimi-coding",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ describe("applyAuthChoice", () => {
|
||||||
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
|
||||||
const previousOpenrouterKey = process.env.OPENROUTER_API_KEY;
|
const previousOpenrouterKey = process.env.OPENROUTER_API_KEY;
|
||||||
const previousAiGatewayKey = process.env.AI_GATEWAY_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 previousSshTty = process.env.SSH_TTY;
|
||||||
const previousChutesClientId = process.env.CHUTES_CLIENT_ID;
|
const previousChutesClientId = process.env.CHUTES_CLIENT_ID;
|
||||||
let tempStateDir: string | null = null;
|
let tempStateDir: string | null = null;
|
||||||
|
|
@ -69,6 +70,11 @@ describe("applyAuthChoice", () => {
|
||||||
} else {
|
} else {
|
||||||
process.env.AI_GATEWAY_API_KEY = previousAiGatewayKey;
|
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) {
|
if (previousSshTty === undefined) {
|
||||||
delete process.env.SSH_TTY;
|
delete process.env.SSH_TTY;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -405,6 +411,76 @@ describe("applyAuthChoice", () => {
|
||||||
delete process.env.AI_GATEWAY_API_KEY;
|
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 () => {
|
it("writes Chutes OAuth credentials when selecting chutes (remote/manual)", async () => {
|
||||||
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
|
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-"));
|
||||||
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
process.env.OPENCLAW_STATE_DIR = tempStateDir;
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ export function resolveProviderAuthOverview(params: {
|
||||||
return `${profileId}=missing`;
|
return `${profileId}=missing`;
|
||||||
}
|
}
|
||||||
if (profile.type === "api_key") {
|
if (profile.type === "api_key") {
|
||||||
return withUnusableSuffix(`${profileId}=${maskApiKey(profile.key)}`, profileId);
|
return withUnusableSuffix(`${profileId}=${maskApiKey(profile.key ?? "")}`, profileId);
|
||||||
}
|
}
|
||||||
if (profile.type === "token") {
|
if (profile.type === "token") {
|
||||||
return withUnusableSuffix(`${profileId}=token:${maskApiKey(profile.token)}`, profileId);
|
return withUnusableSuffix(`${profileId}=token:${maskApiKey(profile.token)}`, profileId);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
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 { buildXiaomiProvider, XIAOMI_DEFAULT_MODEL_ID } from "../agents/models-config.providers.js";
|
||||||
import {
|
import {
|
||||||
buildSyntheticModelDefinition,
|
buildSyntheticModelDefinition,
|
||||||
|
|
@ -13,6 +17,7 @@ import {
|
||||||
VENICE_MODEL_CATALOG,
|
VENICE_MODEL_CATALOG,
|
||||||
} from "../agents/venice-models.js";
|
} from "../agents/venice-models.js";
|
||||||
import {
|
import {
|
||||||
|
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||||
XIAOMI_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 {
|
export function applyVercelAiGatewayConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||||
const next = applyVercelAiGatewayProviderConfig(cfg);
|
const next = applyVercelAiGatewayProviderConfig(cfg);
|
||||||
const existingModel = next.agents?.defaults?.model;
|
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 {
|
export function applyOpenrouterConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||||
const next = applyOpenrouterProviderConfig(cfg);
|
const next = applyOpenrouterProviderConfig(cfg);
|
||||||
const existingModel = next.agents?.defaults?.model;
|
const existingModel = next.agents?.defaults?.model;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
||||||
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
||||||
import { upsertAuthProfile } from "../agents/auth-profiles.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();
|
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) {
|
export async function setVercelAiGatewayApiKey(key: string, agentDir?: string) {
|
||||||
upsertAuthProfile({
|
upsertAuthProfile({
|
||||||
profileId: "vercel-ai-gateway:default",
|
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 { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js";
|
||||||
export {
|
export {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
|
applyCloudflareAiGatewayConfig,
|
||||||
|
applyCloudflareAiGatewayProviderConfig,
|
||||||
applyKimiCodeConfig,
|
applyKimiCodeConfig,
|
||||||
applyKimiCodeProviderConfig,
|
applyKimiCodeProviderConfig,
|
||||||
applyMoonshotConfig,
|
applyMoonshotConfig,
|
||||||
|
|
@ -37,8 +39,10 @@ export {
|
||||||
applyOpencodeZenProviderConfig,
|
applyOpencodeZenProviderConfig,
|
||||||
} from "./onboard-auth.config-opencode.js";
|
} from "./onboard-auth.config-opencode.js";
|
||||||
export {
|
export {
|
||||||
|
CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF,
|
||||||
OPENROUTER_DEFAULT_MODEL_REF,
|
OPENROUTER_DEFAULT_MODEL_REF,
|
||||||
setAnthropicApiKey,
|
setAnthropicApiKey,
|
||||||
|
setCloudflareAiGatewayConfig,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
setKimiCodingApiKey,
|
setKimiCodingApiKey,
|
||||||
setMinimaxApiKey,
|
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 { applyGoogleGeminiModelDefault } from "../../google-gemini-model-default.js";
|
||||||
import {
|
import {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
|
applyCloudflareAiGatewayConfig,
|
||||||
applyKimiCodeConfig,
|
applyKimiCodeConfig,
|
||||||
applyMinimaxApiConfig,
|
applyMinimaxApiConfig,
|
||||||
applyMinimaxConfig,
|
applyMinimaxConfig,
|
||||||
|
|
@ -23,6 +24,7 @@ import {
|
||||||
applyXiaomiConfig,
|
applyXiaomiConfig,
|
||||||
applyZaiConfig,
|
applyZaiConfig,
|
||||||
setAnthropicApiKey,
|
setAnthropicApiKey,
|
||||||
|
setCloudflareAiGatewayConfig,
|
||||||
setGeminiApiKey,
|
setGeminiApiKey,
|
||||||
setKimiCodingApiKey,
|
setKimiCodingApiKey,
|
||||||
setMinimaxApiKey,
|
setMinimaxApiKey,
|
||||||
|
|
@ -281,6 +283,44 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||||
return applyVercelAiGatewayConfig(nextConfig);
|
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") {
|
if (authChoice === "moonshot-api-key") {
|
||||||
const resolved = await resolveNonInteractiveApiKey({
|
const resolved = await resolveNonInteractiveApiKey({
|
||||||
provider: "moonshot",
|
provider: "moonshot",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export type AuthChoice =
|
||||||
| "openai-api-key"
|
| "openai-api-key"
|
||||||
| "openrouter-api-key"
|
| "openrouter-api-key"
|
||||||
| "ai-gateway-api-key"
|
| "ai-gateway-api-key"
|
||||||
|
| "cloudflare-ai-gateway-api-key"
|
||||||
| "moonshot-api-key"
|
| "moonshot-api-key"
|
||||||
| "moonshot-api-key-cn"
|
| "moonshot-api-key-cn"
|
||||||
| "kimi-code-api-key"
|
| "kimi-code-api-key"
|
||||||
|
|
@ -66,6 +67,9 @@ export type OnboardOptions = {
|
||||||
openaiApiKey?: string;
|
openaiApiKey?: string;
|
||||||
openrouterApiKey?: string;
|
openrouterApiKey?: string;
|
||||||
aiGatewayApiKey?: string;
|
aiGatewayApiKey?: string;
|
||||||
|
cloudflareAiGatewayAccountId?: string;
|
||||||
|
cloudflareAiGatewayGatewayId?: string;
|
||||||
|
cloudflareAiGatewayApiKey?: string;
|
||||||
moonshotApiKey?: string;
|
moonshotApiKey?: string;
|
||||||
kimiCodeApiKey?: string;
|
kimiCodeApiKey?: string;
|
||||||
geminiApiKey?: string;
|
geminiApiKey?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue