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}`); });