From b308e9055b7286fdb0acc692373e109e5188cad1 Mon Sep 17 00:00:00 2001 From: Palash Golecha Date: Fri, 30 Jan 2026 19:52:56 -0800 Subject: [PATCH] fix: support authenticated ai gateway with key in request, support anthropic token auth --- README.md | 5 ++++- src/gateway/env.test.ts | 24 ------------------------ src/gateway/env.ts | 31 ++++++++++++++++++++----------- src/index.ts | 6 +++--- src/types.ts | 1 + start-moltbot.sh | 11 +++++++++++ 6 files changed, 39 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 90bf7b7..c55558d 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 diff --git a/src/gateway/env.test.ts b/src/gateway/env.test.ts index 29f033d..87b1a24 100644 --- a/src/gateway/env.test.ts +++ b/src/gateway/env.test.ts @@ -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', diff --git a/src/gateway/env.ts b/src/gateway/env.ts index a57e781..a155f7e 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -13,23 +13,31 @@ export function buildEnvVars(env: MoltbotEnv): Record { 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) { @@ -58,3 +66,4 @@ export function buildEnvVars(env: MoltbotEnv): Record { return envVars; } + diff --git a/src/index.ts b/src/index.ts index 3ee1f5c..d2fa1a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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; diff --git a/src/types.ts b/src/types.ts index bb82c8c..a08bcb5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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) diff --git a/start-moltbot.sh b/start-moltbot.sh index 7e225e8..e2153c7 100644 --- a/start-moltbot.sh +++ b/start-moltbot.sh @@ -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 @@ -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 }, @@ -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 }, @@ -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