Skip to content
Merged
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
1 change: 1 addition & 0 deletions dashboard/src/config/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/config/providers/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
7 changes: 6 additions & 1 deletion dashboard/src/config/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
}
1 change: 1 addition & 0 deletions dashboard/src/config/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
17 changes: 17 additions & 0 deletions dashboard/src/config/providers/openrouter.ts
Original file line number Diff line number Diff line change
@@ -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-preview', label: 'Gemini 3 Pro Preview' },
{ id: 'deepseek/deepseek-v3.2', label: 'DeepSeek V3.2' },
],
};
1 change: 1 addition & 0 deletions dashboard/src/config/providers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-...")
}
2 changes: 2 additions & 0 deletions dashboard/src/config/providers/venice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -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' },
],
};
4 changes: 2 additions & 2 deletions dashboard/src/secrets/AddKeyForm.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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"
/>
Expand Down
15 changes: 11 additions & 4 deletions proxy/src/routes/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ')) {
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The authorization header extraction on line 29 doesn't check if the header value is a string before calling startsWith. While HTTP headers can be string or string arrays in Node.js/Fastify, calling startsWith on an array would cause a runtime error. Consider adding a type check similar to the x-api-key handling on line 32, for example: 'if (typeof auth === 'string' && auth.startsWith('Bearer '))'.

Suggested change
if (auth?.startsWith('Bearer ')) {
if (typeof auth === 'string' && auth.startsWith('Bearer ')) {

Copilot uses AI. Check for mistakes.
botToken = auth.slice(7);
} else if (req.headers['x-api-key'] && typeof req.headers['x-api-key'] === 'string') {
botToken = req.headers['x-api-key'];
Comment on lines +32 to +33
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code now accepts bot authentication via the x-api-key header (line 32-33), but this header is not being removed before forwarding to the upstream provider in the forwardToUpstream function. The authorization header is removed in upstream.ts line 37, but x-api-key is not. This means if a bot authenticates with x-api-key and the request is forwarded to Anthropic (which also uses x-api-key for authentication), the bot's authentication token would be sent to Anthropic instead of the real API key. The x-api-key header should also be deleted in upstream.ts alongside the authorization header removal.

Copilot uses AI. Check for mistakes.
}
Comment on lines +32 to +34
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new x-api-key authentication path added in lines 32-34 lacks test coverage. The proxy/src/routes/ directory has comprehensive tests for admin.ts but no tests for proxy.ts. Consider adding tests to verify that x-api-key authentication works correctly for bot authentication, especially since this is a security-critical code path.

Copilot uses AI. Check for mistakes.

if (!botToken) {
reply.status(401).send({ error: 'Missing authorization' });
return;
}

const botToken = auth.slice(7);
const tokenHash = hashToken(botToken);

// Lookup bot
Expand Down
8 changes: 7 additions & 1 deletion proxy/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const VENDOR_CONFIGS: Record<string, VendorConfig> = {
},
anthropic: {
host: 'api.anthropic.com',
basePath: '/v1',
basePath: '', // OpenClaw's anthropic-messages API includes /v1 in its path
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The anthropic basePath has been changed from '/v1' to an empty string to accommodate OpenClaw's anthropic-messages API type, which already includes /v1 in its request paths. While this change is coordinated with the getApiTypeForProvider function in templates.ts, it could be a breaking change for any existing Anthropic bots that were created before this update. These existing bots would have been configured with the old basePath and might fail if the paths are constructed differently. Consider documenting this as a breaking change or providing a migration path for existing deployments.

Copilot uses AI. Check for mistakes.
authHeader: 'x-api-key',
authFormat: (key) => key,
},
Expand All @@ -56,4 +56,10 @@ export const VENDOR_CONFIGS: Record<string, VendorConfig> = {
authHeader: 'x-goog-api-key',
authFormat: (key) => key,
},
openrouter: {
host: 'openrouter.ai',
basePath: '/api/v1',
authHeader: 'Authorization',
authFormat: (key) => `Bearer ${key}`,
},
};
46 changes: 46 additions & 0 deletions src/bots/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;

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<string, unknown>;
const models = openclawConfig.models as Record<string, unknown>;
const providers = models.providers as Record<string, unknown>;
const veniceProxy = providers['venice-proxy'] as Record<string, unknown>;

expect(veniceProxy.api).toBe('openai-completions');
});

it('should include persona in workspace files', () => {
const config = createTestConfig({
persona: {
Expand Down
21 changes: 20 additions & 1 deletion src/bots/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,25 @@ 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';
Comment on lines +62 to +63
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getApiTypeForProvider function now includes a mapping for 'google' to 'google-gemini' (line 62-63), but there is no test coverage for this case. While anthropic and venice API types are tested in templates.test.ts lines 154-178 and 180-198, the google provider should also have a test to verify the google-gemini API type is correctly set when using proxy configuration.

Copilot uses AI. Check for mistakes.
case 'venice':
case 'openrouter':
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getApiTypeForProvider function adds support for 'openrouter' (line 65) which maps to 'openai-completions', but there is no test coverage for this new provider. While it shares the same API type as venice, adding a dedicated test would ensure the openrouter provider is correctly handled in proxy configurations, consistent with the test pattern established for other providers in templates.test.ts.

Copilot uses AI. Check for mistakes.
return 'openai-completions'; // OpenAI-compatible APIs
case 'openai':
default:
return 'openai-responses';
}
}

/**
* Generate openclaw.json configuration.
* Follows OpenClaw's expected config structure for gateway mode.
Expand All @@ -72,7 +91,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 }],
},
},
Expand Down
Loading