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
1,266 changes: 581 additions & 685 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,8 @@
},
"engines": {
"node": ">=20"
},
"dependencies": {
"@aws-sdk/client-bedrock-runtime": "^3.1003.0"
}
}
20 changes: 20 additions & 0 deletions src/api-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const PROVIDER_ENDPOINTS: Record<string, string> = {
moonshot: "https://api.moonshot.cn/v1",
nvidia: "https://integrate.api.nvidia.com/v1",
openrouter: "https://openrouter.ai/api/v1",
"amazon-bedrock": "https://bedrock-runtime.eu-west-1.amazonaws.com",
};

/**
Expand All @@ -47,6 +48,7 @@ const ENV_VAR_MAP: Record<string, string> = {
moonshot: "MOONSHOT_API_KEY",
nvidia: "NVIDIA_API_KEY",
openrouter: "OPENROUTER_API_KEY",
"amazon-bedrock": "AWS_ACCESS_KEY_ID",
};

export type ProviderConfig = {
Expand Down Expand Up @@ -101,6 +103,15 @@ export function loadApiKeys(pluginConfig?: Record<string, unknown>): ApiKeysConf
}
}

// 4. AWS Bedrock — uses IAM credentials, not a single API key
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
if (!config.providers["amazon-bedrock"]) {
config.providers["amazon-bedrock"] = { apiKey: "iam-sigv4" };
} else if (!config.providers["amazon-bedrock"].apiKey) {
config.providers["amazon-bedrock"].apiKey = "iam-sigv4";
}
}

return config;
}

Expand Down Expand Up @@ -151,6 +162,15 @@ export function resolveProviderAccess(
): { apiKey: string; baseUrl: string; provider: string; viaOpenRouter: boolean } | undefined {
const provider = getProviderFromModel(modelId);

// AWS Bedrock — uses SDK with IAM credentials, not HTTP+API key
if (provider === "amazon-bedrock") {
const bedrockConfig = config.providers["amazon-bedrock"];
if (bedrockConfig?.apiKey) {
return { apiKey: "iam-sigv4", baseUrl: "bedrock-sdk", provider: "amazon-bedrock", viaOpenRouter: false };
}
// Fall through to OpenRouter if no IAM credentials
}

// Anthropic + Google need format conversion (tools, streaming, etc.)
// Always route through OpenRouter if available — it handles conversion automatically
const needsConversion = provider === "anthropic" || provider === "google";
Expand Down
28 changes: 14 additions & 14 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import { startProxy, getProxyPort } from "./proxy.js";
import { loadApiKeys, getConfiguredProviders, hasOpenRouter, getAccessibleProviders } from "./api-keys.js";
import { VERSION } from "./version.js";
import { clog, cerr } from "./log.js";

function printHelp(): void {
console.log(`
Expand Down Expand Up @@ -67,37 +68,36 @@ async function main(): Promise<void> {
const configured = getConfiguredProviders(apiKeys);

if (configured.length === 0) {
console.error("[ClawRouter] No API keys configured!");
console.error("[ClawRouter] Quickest: export OPENROUTER_API_KEY=sk-or-... (one key → all models)");
console.error("[ClawRouter] Or set individual keys: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.");
console.error("[ClawRouter] Or edit ~/.openclaw/clawrouter/config.json");
cerr("[ClawRouter] No API keys configured!");
cerr("[ClawRouter] Quickest: export OPENROUTER_API_KEY=sk-or-... (one key → all models)");
cerr("[ClawRouter] Or set individual keys: OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.");
cerr("[ClawRouter] Or edit ~/.openclaw/clawrouter/config.json");
process.exit(1);
}

const accessible = getAccessibleProviders(apiKeys);
const orFallback = hasOpenRouter(apiKeys);
console.log(`[ClawRouter] Configured providers: ${configured.join(", ")}${orFallback ? " (OpenRouter covers all)" : ""}`);
console.log(`[ClawRouter] Accessible providers: ${accessible.join(", ")} (${accessible.length} total)`);
clog(`[ClawRouter] Configured providers: ${configured.join(", ")}${orFallback ? " (OpenRouter covers all)" : ""}`);
clog(`[ClawRouter] Accessible providers: ${accessible.join(", ")} (${accessible.length} total)`);

const proxy = await startProxy({
apiKeys,
port: args.port,
onReady: (port) => {
console.log(`[ClawRouter] Proxy listening on http://127.0.0.1:${port}`);
console.log(`[ClawRouter] Health check: http://127.0.0.1:${port}/health`);
clog(`[ClawRouter] Proxy listening on http://127.0.0.1:${port}`);
clog(`[ClawRouter] Health check: http://127.0.0.1:${port}/health`);
},
onError: (error) => console.error(`[ClawRouter] Error: ${error.message}`),
onError: (error) => cerr(`[ClawRouter] Error: ${error.message}`),
onRouted: (decision) => {
const cost = decision.costEstimate.toFixed(4);
const saved = (decision.savings * 100).toFixed(0);
console.log(`[ClawRouter] [${decision.tier}] ${decision.model} ~$${cost} (saved ${saved}%)`);
clog(`[ClawRouter] [${decision.tier}] ${decision.model} ~$${cost}`);
},
});

console.log(`[ClawRouter] Ready - Ctrl+C to stop`);
clog(`[ClawRouter] Ready - Ctrl+C to stop`);

const shutdown = async (signal: string) => {
console.log(`\n[ClawRouter] Received ${signal}, shutting down...`);
clog(`\n[ClawRouter] Received ${signal}, shutting down...`);
try { await proxy.close(); process.exit(0); } catch { process.exit(1); }
};

Expand All @@ -106,4 +106,4 @@ async function main(): Promise<void> {
await new Promise(() => {});
}

main().catch((err) => { console.error(`[ClawRouter] Fatal: ${err.message}`); process.exit(1); });
main().catch((err) => { cerr(`[ClawRouter] Fatal: ${err.message}`); process.exit(1); });
13 changes: 13 additions & 0 deletions src/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Timestamped console logging helpers.
*/

export function clog(...args: unknown[]): void {
const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
console.log(`[${ts}]`, ...args);
}

export function cerr(...args: unknown[]): void {
const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
console.error(`[${ts}]`, ...args);
}
44 changes: 44 additions & 0 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,50 @@ export const BLOCKRUN_MODELS: BlockRunModel[] = [
contextWindow: 262144,
maxOutput: 16384,
},

// AWS Bedrock - Claude models via cross-region inference
{
id: "amazon-bedrock/eu.anthropic.claude-opus-4-6-v1",
name: "Bedrock Claude Opus 4.6",
inputPrice: 15.0,
outputPrice: 75.0,
contextWindow: 200000,
maxOutput: 32000,
reasoning: true,
vision: true,
agentic: true,
},
{
id: "amazon-bedrock/eu.anthropic.claude-sonnet-4-6",
name: "Bedrock Claude Sonnet 4.6",
inputPrice: 3.0,
outputPrice: 15.0,
contextWindow: 200000,
maxOutput: 64000,
reasoning: true,
vision: true,
agentic: true,
},
{
id: "amazon-bedrock/eu.anthropic.claude-sonnet-4-20250514-v1:0",
name: "Bedrock Claude Sonnet 4",
inputPrice: 3.0,
outputPrice: 15.0,
contextWindow: 200000,
maxOutput: 64000,
reasoning: true,
agentic: true,
},
{
id: "amazon-bedrock/eu.anthropic.claude-haiku-4-5-20251001-v1:0",
name: "Bedrock Claude Haiku 4.5",
inputPrice: 1.0,
outputPrice: 5.0,
contextWindow: 200000,
maxOutput: 8192,
vision: true,
agentic: true,
},
];

/**
Expand Down
5 changes: 3 additions & 2 deletions src/openrouter-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

import { BLOCKRUN_MODELS } from "./models.js";
import { clog, cerr } from "./log.js";

type OpenRouterModel = { id: string; name?: string };

Expand Down Expand Up @@ -90,7 +91,7 @@ export async function refreshOpenRouterModels(apiKey: string): Promise<void> {
);
if (mapped.length > 0) {
for (const [from, to] of mapped) {
console.log(`[ClawRouter] ${from} → ${to}`);
clog(`[ClawRouter] ${from} → ${to}`);
}
}
}
Expand Down Expand Up @@ -118,6 +119,6 @@ export function isOpenRouterCacheReady(): boolean {
export function ensureOpenRouterCache(apiKey: string): void {
if (isOpenRouterCacheReady()) return;
refreshOpenRouterModels(apiKey).catch((err) => {
console.error(`[ClawRouter] Background OpenRouter cache refresh failed: ${err.message}`);
cerr(`[ClawRouter] Background OpenRouter cache refresh failed: ${err.message}`);
});
}
Loading