Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Run [OpenClaw](https://github.com/openclaw/openclaw) (formerly Moltbot, formerly
## Requirements

- [Workers Paid plan](https://www.cloudflare.com/plans/developer-platform/) ($5 USD/month) — required for Cloudflare Sandbox containers
- [Anthropic API key](https://console.anthropic.com/) — for Claude access, or you can use AI Gateway's [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/)
- [Anthropic API key/Claude Code Subscription](https://console.anthropic.com/) — for Claude access, or you can use AI Gateway's [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/)

The following Cloudflare features used by this project have free tiers:
- Cloudflare Access (authentication)
Expand Down Expand Up @@ -46,6 +46,9 @@ npm install
# Set your API key (direct Anthropic access)
npx wrangler secret put ANTHROPIC_API_KEY

# Or set your Claude Code token (claude setup-token)
# npx wrangler secret put ANTHROPIC_OAUTH_TOKEN

# Or use AI Gateway instead (see "Optional: Cloudflare AI Gateway" below)
# npx wrangler secret put AI_GATEWAY_API_KEY
# npx wrangler secret put AI_GATEWAY_BASE_URL
Expand Down
24 changes: 0 additions & 24 deletions src/gateway/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,30 +45,6 @@ describe('buildEnvVars', () => {
expect(result.AI_GATEWAY_BASE_URL).toBe('https://gateway.ai.cloudflare.com/v1/123/my-gw/anthropic');
});

it('AI_GATEWAY_* takes precedence over direct provider keys for Anthropic', () => {
const env = createMockEnv({
AI_GATEWAY_API_KEY: 'gateway-key',
AI_GATEWAY_BASE_URL: 'https://gateway.example.com/anthropic',
ANTHROPIC_API_KEY: 'direct-key',
ANTHROPIC_BASE_URL: 'https://api.anthropic.com',
});
const result = buildEnvVars(env);
expect(result.ANTHROPIC_API_KEY).toBe('gateway-key');
expect(result.AI_GATEWAY_BASE_URL).toBe('https://gateway.example.com/anthropic');
});

it('AI_GATEWAY_* takes precedence over direct provider keys for OpenAI', () => {
const env = createMockEnv({
AI_GATEWAY_API_KEY: 'gateway-key',
AI_GATEWAY_BASE_URL: 'https://gateway.example.com/openai',
OPENAI_API_KEY: 'direct-key',
});
const result = buildEnvVars(env);
expect(result.OPENAI_API_KEY).toBe('gateway-key');
expect(result.AI_GATEWAY_BASE_URL).toBe('https://gateway.example.com/openai');
expect(result.OPENAI_BASE_URL).toBe('https://gateway.example.com/openai');
});

it('falls back to ANTHROPIC_* when AI_GATEWAY_* not set', () => {
const env = createMockEnv({
ANTHROPIC_API_KEY: 'direct-key',
Expand Down
31 changes: 20 additions & 11 deletions src/gateway/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,31 @@ export function buildEnvVars(env: MoltbotEnv): Record<string, string> {
const normalizedBaseUrl = env.AI_GATEWAY_BASE_URL?.replace(/\/+$/, '');
const isOpenAIGateway = normalizedBaseUrl?.endsWith('/openai');

// AI Gateway vars take precedence
// Map to the appropriate provider env var based on the gateway endpoint
// If key in request (user provided ANTHROPIC_API_KEY) pass ANTHROPIC_API_KEY as is
if (env.ANTHROPIC_API_KEY) {
envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
}
if (env.ANTHROPIC_OAUTH_TOKEN) {
envVars.ANTHROPIC_OAUTH_TOKEN = env.ANTHROPIC_OAUTH_TOKEN;
}
if (env.OPENAI_API_KEY) {
envVars.OPENAI_API_KEY = env.OPENAI_API_KEY;
}

// AI Gateway will take use auth token from either provider specific headers (x-api-key, Authorization) or cf-aig-authorization header
// If the user wants to use AI Gateway (authenticated)
// 1. If Anthropic/OpenAI key is not passed directly (stored with BYOK or if Unified Billing is used), pass AI_GATEWAY_API_KEY in vendor specific header
// 2. If key is passed directly pass AI_GATEWAY_API_KEY in cf-aig-authorization header
if (env.AI_GATEWAY_API_KEY) {
if (isOpenAIGateway) {
if (isOpenAIGateway && !envVars.OPENAI_API_KEY) {
envVars.OPENAI_API_KEY = env.AI_GATEWAY_API_KEY;
} else {
} else if (!envVars.ANTHROPIC_API_KEY && !envVars.ANTHROPIC_OAUTH_TOKEN) {
envVars.ANTHROPIC_API_KEY = env.AI_GATEWAY_API_KEY;
} else {
envVars.AI_GATEWAY_API_KEY = env.AI_GATEWAY_API_KEY;
}
}

// Fall back to direct provider keys
if (!envVars.ANTHROPIC_API_KEY && env.ANTHROPIC_API_KEY) {
envVars.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
}
if (!envVars.OPENAI_API_KEY && env.OPENAI_API_KEY) {
envVars.OPENAI_API_KEY = env.OPENAI_API_KEY;
}

// Pass base URL (used by start-moltbot.sh to determine provider)
if (normalizedBaseUrl) {
Expand Down Expand Up @@ -58,3 +66,4 @@ export function buildEnvVars(env: MoltbotEnv): Record<string, string> {

return envVars;
}

6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ function validateRequiredEnv(env: MoltbotEnv): string[] {
if (!env.AI_GATEWAY_BASE_URL) {
missing.push('AI_GATEWAY_BASE_URL (required when using AI_GATEWAY_API_KEY)');
}
} else if (!env.ANTHROPIC_API_KEY) {
// Direct Anthropic access requires API key
missing.push('ANTHROPIC_API_KEY or AI_GATEWAY_API_KEY');
} else if (!env.ANTHROPIC_API_KEY && !env.ANTHROPIC_OAUTH_TOKEN) {
// Direct Anthropic access requires API key or OAuth token
missing.push('ANTHROPIC_API_KEY or ANTHROPIC_OAUTH_TOKEN or AI_GATEWAY_API_KEY');
}

return missing;
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface MoltbotEnv {
AI_GATEWAY_BASE_URL?: string; // AI Gateway URL (e.g., https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic)
// Legacy direct provider configuration (fallback)
ANTHROPIC_API_KEY?: string;
ANTHROPIC_OAUTH_TOKEN?: string;
ANTHROPIC_BASE_URL?: string;
OPENAI_API_KEY?: string;
MOLTBOT_GATEWAY_TOKEN?: string; // Gateway token (mapped to CLAWDBOT_GATEWAY_TOKEN for container)
Expand Down
11 changes: 11 additions & 0 deletions start-moltbot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,12 @@ if (process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) {
const baseUrl = (process.env.AI_GATEWAY_BASE_URL || process.env.ANTHROPIC_BASE_URL || '').replace(/\/+$/, '');
const isOpenAI = baseUrl.endsWith('/openai');

const headers = {};

if(process.env.AI_GATEWAY_API_KEY) {
headers['cf-aig-authorization'] = 'Bearer ' + process.env.AI_GATEWAY_API_KEY;
}

if (isOpenAI) {
// Create custom openai provider config with baseUrl override
// Omit apiKey so moltbot falls back to OPENAI_API_KEY env var
Expand All @@ -224,6 +230,7 @@ if (isOpenAI) {
config.models.providers.openai = {
baseUrl: baseUrl,
api: 'openai-responses',
headers,
models: [
{ id: 'gpt-5.2', name: 'GPT-5.2', contextWindow: 200000 },
{ id: 'gpt-5', name: 'GPT-5', contextWindow: 200000 },
Expand All @@ -243,6 +250,7 @@ if (isOpenAI) {
const providerConfig = {
baseUrl: baseUrl,
api: 'anthropic-messages',
headers,
models: [
{ id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5', contextWindow: 200000 },
{ id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5', contextWindow: 200000 },
Expand All @@ -252,6 +260,9 @@ if (isOpenAI) {
// Include API key in provider config if set (required when using custom baseUrl)
if (process.env.ANTHROPIC_API_KEY) {
providerConfig.apiKey = process.env.ANTHROPIC_API_KEY;
} else if (process.env.ANTHROPIC_OAUTH_TOKEN) {
providerConfig.auth = "token";
providerConfig.apiKey = process.env.ANTHROPIC_OAUTH_TOKEN;
}
config.models.providers.anthropic = providerConfig;
// Add models to the allowlist so they appear in /models
Expand Down