opencode-anthropic-proxy/claude-proxy.mjs

178 lines
5.1 KiB
JavaScript

import http from "node:http";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const PORT = process.env.CLAUDE_PROXY_PORT || 3000;
const MODEL_MAP = {
"claude-opus-4-7": "opus",
"claude-sonnet-4-6": "sonnet",
"claude-haiku-4-5": "haiku",
};
const TOOL_MAP = {
bash: "Bash",
read: "Read",
write: "Write",
edit: "Edit",
glob: "Glob",
grep: "Grep",
webfetch: "WebFetch",
websearch: "WebSearch",
question: "Question",
task: "Task",
todowrite: "TodoWrite",
skill: "Skill",
};
function parseBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString()));
} catch (e) {
reject(e);
}
});
req.on("error", reject);
});
}
function buildMessages(body) {
const messages = body.messages || [];
const parts = [];
for (const msg of messages) {
if (msg.role === "system") {
parts.push(`System: ${msg.content}`);
} else if (msg.role === "user") {
parts.push(`User: ${msg.content}`);
} else if (msg.role === "assistant") {
parts.push(`Assistant: ${msg.content}`);
}
}
return parts.join("\n\n");
}
function extractTools(body) {
const tools = body.tools || [];
const claudeTools = [];
for (const tool of tools) {
const name = tool.function?.name || tool.name;
if (name) {
const mapped = TOOL_MAP[name.toLowerCase()] || name;
claudeTools.push(mapped);
}
}
return claudeTools;
}
async function callClaude(prompt, model, allowedTools) {
const modelWithoutPrefix = model.includes("/") ? model.split("/").pop() : model;
const modelAlias = MODEL_MAP[modelWithoutPrefix] || modelWithoutPrefix;
const args = ["-p", prompt, "--model", modelAlias, "--no-session-persistence", "--output-format", "json", "--permission-mode", "bypassPermissions"];
if (allowedTools.length > 0) {
args.push("--allowedTools", ...allowedTools);
}
const { stdout } = await execFileAsync("/home/kenny/.local/bin/claude", args, {
timeout: 300000,
maxBuffer: 1024 * 1024 * 10,
});
return JSON.parse(stdout);
}
async function handleRequest(req, res) {
if (req.method === "GET" && req.url === "/v1/models") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
object: "list",
data: Object.keys(MODEL_MAP).map((id) => ({
id,
object: "model",
created: Math.floor(Date.now() / 1000),
owned_by: "anthropic",
})),
}));
return;
}
if (req.method !== "POST" || !req.url.startsWith("/v1/chat/completions")) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
return;
}
try {
const body = await parseBody(req);
const prompt = buildMessages(body);
const model = body.model || "claude-sonnet-4-6";
const stream = body.stream || false;
const allowedTools = extractTools(body);
const result = await callClaude(prompt, model, allowedTools);
const response = {
id: `chatcmpl-${Date.now()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model,
choices: [
{
index: 0,
message: {
role: "assistant",
content: result.result || result.content || JSON.stringify(result),
},
finish_reason: "stop",
},
],
usage: {
prompt_tokens: result.usage?.input_tokens || 0,
completion_tokens: result.usage?.output_tokens || 0,
total_tokens: (result.usage?.input_tokens || 0) + (result.usage?.output_tokens || 0),
},
};
if (stream) {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
const chunk = {
id: response.id,
object: "chat.completion.chunk",
created: response.created,
model: response.model,
choices: [{ index: 0, delta: { role: "assistant", content: response.choices[0].message.content }, finish_reason: null }],
};
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
const endChunk = {
id: response.id,
object: "chat.completion.chunk",
created: response.created,
model: response.model,
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
};
res.write(`data: ${JSON.stringify(endChunk)}\n\n`);
res.write("data: [DONE]\n\n");
res.end();
} else {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(response));
}
} catch (err) {
console.error(`[${new Date().toISOString()}] Proxy error:`, err);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
}
}
const server = http.createServer(handleRequest);
server.listen(PORT, () => {
console.log(`Claude proxy listening on http://localhost:${PORT}`);
});