From cc5944c76eb9be4fb369f4d385bc248a7c552f68 Mon Sep 17 00:00:00 2001 From: interplanetarychris Date: Tue, 3 Feb 2026 15:21:56 -0800 Subject: [PATCH 1/4] fix: accept x-api-key header for Anthropic-style auth OpenClaw sends different auth headers based on API type: - openai-completions: Authorization: Bearer - anthropic-messages: x-api-key: The keyring-proxy now accepts both authentication methods, fixing 401 "Missing authorization" errors for Anthropic and Venice providers. Co-Authored-By: Claude Opus 4.5 --- proxy/src/routes/proxy.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/proxy/src/routes/proxy.ts b/proxy/src/routes/proxy.ts index ec4bf06..c9dfa20 100644 --- a/proxy/src/routes/proxy.ts +++ b/proxy/src/routes/proxy.ts @@ -22,14 +22,21 @@ export function registerProxyRoutes( } const vendorConfig = VENDOR_CONFIGS[vendor]; - // Extract and validate bot token + // Extract bot token from either Authorization header or x-api-key + // This supports both OpenAI-style (Bearer token) and Anthropic-style (x-api-key) auth + let botToken: string | undefined; + const auth = req.headers.authorization; - if (!auth?.startsWith('Bearer ')) { + if (auth?.startsWith('Bearer ')) { + botToken = auth.slice(7); + } else if (req.headers['x-api-key'] && typeof req.headers['x-api-key'] === 'string') { + botToken = req.headers['x-api-key']; + } + + if (!botToken) { reply.status(401).send({ error: 'Missing authorization' }); return; } - - const botToken = auth.slice(7); const tokenHash = hashToken(botToken); // Lookup bot From c40fe4c095e390cc224295b5ab35101bdeb33b03 Mon Sep 17 00:00:00 2001 From: interplanetarychris Date: Tue, 3 Feb 2026 15:28:00 -0800 Subject: [PATCH 2/4] fix: use empty basePath for Anthropic vendor OpenClaw's anthropic-messages API includes /v1 in its request path, so the vendor basePath should be empty to avoid double /v1/v1 in the forwarded URL. Co-Authored-By: Claude Opus 4.5 --- proxy/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/types.ts b/proxy/src/types.ts index b55737e..1622d7c 100644 --- a/proxy/src/types.ts +++ b/proxy/src/types.ts @@ -40,7 +40,7 @@ export const VENDOR_CONFIGS: Record = { }, anthropic: { host: 'api.anthropic.com', - basePath: '/v1', + basePath: '', // OpenClaw's anthropic-messages API includes /v1 in its path authHeader: 'x-api-key', authFormat: (key) => key, }, From a3277e0f0df2f3f4c479fcd367c9f0704099a0af Mon Sep 17 00:00:00 2001 From: interplanetarychris Date: Tue, 3 Feb 2026 15:32:04 -0800 Subject: [PATCH 3/4] fix: use correct API type per provider when generating bot config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BotMaker was hardcoding 'openai-responses' API type for all providers. Now correctly maps: - openai → openai-responses - anthropic → anthropic-messages - venice → openai-completions (OpenAI-compatible) - google → google-gemini Added tests for anthropic and venice proxy configurations. Co-Authored-By: Claude Opus 4.5 --- src/bots/templates.test.ts | 46 ++++++++++++++++++++++++++++++++++++++ src/bots/templates.ts | 20 ++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/bots/templates.test.ts b/src/bots/templates.test.ts index e1f6c8a..b80534b 100644 --- a/src/bots/templates.test.ts +++ b/src/bots/templates.test.ts @@ -151,6 +151,52 @@ describe('templates', () => { expect(model.primary).toBe('anthropic/claude-3-opus'); }); + it('should use anthropic-messages API type for anthropic with proxy', () => { + const config = createTestConfig({ + aiProvider: 'anthropic', + model: 'claude-3-opus', + proxy: { + baseUrl: 'http://proxy:9101/v1/anthropic', + token: 'proxy-token-123', + }, + }); + createBotWorkspace(testDir, config); + + const openclawPath = join(testDir, 'bots', config.botHostname, 'openclaw.json'); + const openclawConfig = JSON.parse(readFileSync(openclawPath, 'utf-8')) as Record; + + expect(openclawConfig.models).toEqual({ + providers: { + 'anthropic-proxy': { + baseUrl: 'http://proxy:9101/v1/anthropic', + apiKey: 'proxy-token-123', + api: 'anthropic-messages', + models: [{ id: 'claude-3-opus', name: 'claude-3-opus' }], + }, + }, + }); + }); + + it('should use openai-completions API type for venice with proxy', () => { + const config = createTestConfig({ + aiProvider: 'venice', + model: 'llama-3.3-70b', + proxy: { + baseUrl: 'http://proxy:9101/v1/venice', + token: 'proxy-token-123', + }, + }); + createBotWorkspace(testDir, config); + + const openclawPath = join(testDir, 'bots', config.botHostname, 'openclaw.json'); + const openclawConfig = JSON.parse(readFileSync(openclawPath, 'utf-8')) as Record; + const models = openclawConfig.models as Record; + const providers = models.providers as Record; + const veniceProxy = providers['venice-proxy'] as Record; + + expect(veniceProxy.api).toBe('openai-completions'); + }); + it('should include persona in workspace files', () => { const config = createTestConfig({ persona: { diff --git a/src/bots/templates.ts b/src/bots/templates.ts index 92b724e..a849cc7 100644 --- a/src/bots/templates.ts +++ b/src/bots/templates.ts @@ -51,6 +51,24 @@ export interface BotWorkspaceConfig { proxy?: ProxyConfig; } +/** + * Map AI provider to OpenClaw API type. + * Each provider uses a different API format that OpenClaw must know about. + */ +function getApiTypeForProvider(provider: string): string { + switch (provider) { + case 'anthropic': + return 'anthropic-messages'; + case 'google': + return 'google-gemini'; + case 'venice': + return 'openai-completions'; // Venice uses OpenAI-compatible API + case 'openai': + default: + return 'openai-responses'; + } +} + /** * Generate openclaw.json configuration. * Follows OpenClaw's expected config structure for gateway mode. @@ -72,7 +90,7 @@ function generateOpenclawConfig(config: BotWorkspaceConfig): object { [`${config.aiProvider}-proxy`]: { baseUrl: config.proxy.baseUrl, apiKey: config.proxy.token, - api: 'openai-responses', // Required for custom providers + api: getApiTypeForProvider(config.aiProvider), models: [{ id: config.model, name: config.model }], }, }, From 8ddbc72e23ee181ad247527fdfbc0c1e7baffca9 Mon Sep 17 00:00:00 2001 From: interplanetarychris Date: Tue, 3 Feb 2026 16:23:40 -0800 Subject: [PATCH 4/4] feat: add OpenRouter provider and improve multi-provider support - Add OpenRouter as a new provider with curated model list - Add keyHint field to provider configs for API key format hints - Dynamic API key placeholder in AddKeyForm based on selected provider - Update Venice model list with current recommended models Provider key hints: - OpenAI: sk-... - Anthropic: sk-ant-... - Google: AIza... - Venice: VENICE-INFERENCE-KEY-... - OpenRouter: sk-or-... Co-Authored-By: Claude Opus 4.5 --- dashboard/src/config/providers/anthropic.ts | 1 + dashboard/src/config/providers/google.ts | 1 + dashboard/src/config/providers/index.ts | 7 ++++++- dashboard/src/config/providers/openai.ts | 1 + dashboard/src/config/providers/openrouter.ts | 17 +++++++++++++++++ dashboard/src/config/providers/types.ts | 1 + dashboard/src/config/providers/venice.ts | 2 ++ dashboard/src/secrets/AddKeyForm.tsx | 4 ++-- proxy/src/types.ts | 6 ++++++ src/bots/templates.ts | 3 ++- 10 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 dashboard/src/config/providers/openrouter.ts diff --git a/dashboard/src/config/providers/anthropic.ts b/dashboard/src/config/providers/anthropic.ts index 79278fe..5ab2b7e 100644 --- a/dashboard/src/config/providers/anthropic.ts +++ b/dashboard/src/config/providers/anthropic.ts @@ -4,6 +4,7 @@ export const anthropic: ProviderConfig = { id: 'anthropic', label: 'Anthropic', baseUrl: 'https://api.anthropic.com/v1', + keyHint: 'sk-ant-...', defaultModel: 'claude-opus-4-5', models: [ { id: 'claude-opus-4-5', label: 'Claude Opus 4.5' }, diff --git a/dashboard/src/config/providers/google.ts b/dashboard/src/config/providers/google.ts index 68dd635..8469263 100644 --- a/dashboard/src/config/providers/google.ts +++ b/dashboard/src/config/providers/google.ts @@ -4,6 +4,7 @@ export const google: ProviderConfig = { id: 'google', label: 'Google', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', + keyHint: 'AIza...', defaultModel: 'gemini-3-pro-preview', models: [ { id: 'gemini-3-pro-preview', label: 'Gemini 3 Pro Preview' }, diff --git a/dashboard/src/config/providers/index.ts b/dashboard/src/config/providers/index.ts index a93bd9e..f6333d1 100644 --- a/dashboard/src/config/providers/index.ts +++ b/dashboard/src/config/providers/index.ts @@ -3,10 +3,11 @@ import { openai } from './openai'; import { anthropic } from './anthropic'; import { google } from './google'; import { venice } from './venice'; +import { openrouter } from './openrouter'; export type { ProviderConfig, ModelInfo }; -export const PROVIDERS: ProviderConfig[] = [openai, anthropic, google, venice]; +export const PROVIDERS: ProviderConfig[] = [openai, anthropic, google, venice, openrouter]; export const AI_PROVIDERS = PROVIDERS.map((p) => ({ value: p.id, @@ -29,3 +30,7 @@ export function getDefaultModel(providerId: string): string { const provider = getProvider(providerId); return provider?.defaultModel ?? provider?.models[0]?.id ?? ''; } + +export function getKeyHint(providerId: string): string { + return getProvider(providerId)?.keyHint ?? 'API key'; +} diff --git a/dashboard/src/config/providers/openai.ts b/dashboard/src/config/providers/openai.ts index 1b3d4af..fef241e 100644 --- a/dashboard/src/config/providers/openai.ts +++ b/dashboard/src/config/providers/openai.ts @@ -4,6 +4,7 @@ export const openai: ProviderConfig = { id: 'openai', label: 'OpenAI', baseUrl: 'https://api.openai.com/v1', + keyHint: 'sk-...', defaultModel: 'gpt-5.2', models: [ { id: 'gpt-5.2', label: 'GPT-5.2' }, diff --git a/dashboard/src/config/providers/openrouter.ts b/dashboard/src/config/providers/openrouter.ts new file mode 100644 index 0000000..17dc8f4 --- /dev/null +++ b/dashboard/src/config/providers/openrouter.ts @@ -0,0 +1,17 @@ +import type { ProviderConfig } from './types'; + +export const openrouter: ProviderConfig = { + id: 'openrouter', + label: 'OpenRouter', + baseUrl: 'https://openrouter.ai/api/v1', + keyHint: 'sk-or-...', + defaultModel: 'anthropic/claude-sonnet-4-5', + models: [ + { id: 'openrouter/auto', label: 'Auto (best for task)' }, + { id: 'anthropic/claude-opus-4-5', label: 'Claude Opus 4.5' }, + { id: 'anthropic/claude-sonnet-4-5', label: 'Claude Sonnet 4.5' }, + { id: 'openai/gpt-5.2', label: 'GPT-5.2' }, + { id: 'google/gemini-3-pro', label: 'Gemini 3 Pro' }, + { id: 'deepseek/deepseek-v3.2', label: 'DeepSeek V3.2' }, + ], +}; diff --git a/dashboard/src/config/providers/types.ts b/dashboard/src/config/providers/types.ts index 6cc65d1..ebed86e 100644 --- a/dashboard/src/config/providers/types.ts +++ b/dashboard/src/config/providers/types.ts @@ -10,4 +10,5 @@ export interface ProviderConfig { baseUrl: string; models: ModelInfo[]; defaultModel: string; + keyHint?: string; // Placeholder hint for API key format (e.g., "sk-ant-...") } diff --git a/dashboard/src/config/providers/venice.ts b/dashboard/src/config/providers/venice.ts index 4b8c469..4c28b35 100644 --- a/dashboard/src/config/providers/venice.ts +++ b/dashboard/src/config/providers/venice.ts @@ -4,6 +4,7 @@ export const venice: ProviderConfig = { id: 'venice', label: 'Venice', baseUrl: 'https://api.venice.ai/api/v1', + keyHint: 'VENICE-INFERENCE-KEY-...', defaultModel: 'llama-3.3-70b', models: [ { id: 'llama-3.3-70b', label: 'Llama 3.3 70B' }, @@ -12,5 +13,6 @@ export const venice: ProviderConfig = { { id: 'deepseek-v3.2', label: 'DeepSeek V3.2' }, { id: 'zai-org-glm-4.7', label: 'ZAI GLM 4.7' }, { id: 'mistral-31-24b', label: 'Mistral 31 24B' }, + { id: 'kimi-k2-5', label: 'Kimi K2.5' }, ], }; diff --git a/dashboard/src/secrets/AddKeyForm.tsx b/dashboard/src/secrets/AddKeyForm.tsx index e2ef526..eec98b1 100644 --- a/dashboard/src/secrets/AddKeyForm.tsx +++ b/dashboard/src/secrets/AddKeyForm.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import type { AddKeyInput } from '../types'; -import { PROVIDERS } from '../config/providers'; +import { PROVIDERS, getKeyHint } from '../config/providers'; import './AddKeyForm.css'; interface AddKeyFormProps { @@ -73,7 +73,7 @@ export function AddKeyForm({ onSubmit, onCancel, loading }: AddKeyFormProps) { type="password" value={secret} onChange={(e) => { setSecret(e.target.value); }} - placeholder="sk-..." + placeholder={getKeyHint(vendor)} disabled={loading} autoComplete="off" /> diff --git a/proxy/src/types.ts b/proxy/src/types.ts index 1622d7c..edb9d07 100644 --- a/proxy/src/types.ts +++ b/proxy/src/types.ts @@ -56,4 +56,10 @@ export const VENDOR_CONFIGS: Record = { authHeader: 'x-goog-api-key', authFormat: (key) => key, }, + openrouter: { + host: 'openrouter.ai', + basePath: '/api/v1', + authHeader: 'Authorization', + authFormat: (key) => `Bearer ${key}`, + }, }; diff --git a/src/bots/templates.ts b/src/bots/templates.ts index a849cc7..a791a43 100644 --- a/src/bots/templates.ts +++ b/src/bots/templates.ts @@ -62,7 +62,8 @@ function getApiTypeForProvider(provider: string): string { case 'google': return 'google-gemini'; case 'venice': - return 'openai-completions'; // Venice uses OpenAI-compatible API + case 'openrouter': + return 'openai-completions'; // OpenAI-compatible APIs case 'openai': default: return 'openai-responses';