#!/usr/bin/env node // claude-sub-proxy.js — OpenAI-compatible proxy routing through Claude Code CLI (Pro subscription) // Accepts OpenAI chat completions format, routes through claude CLI, returns OpenAI format // Usage: CLAUDE_CODE_OAUTH_TOKEN= node claude-sub-proxy.js import { createServer } from 'http'; import { spawn } from 'child_process'; const PORT = 8782; const CLAUDE_BIN = '/home/openclaw/.npm-global/bin/claude'; // Build a plain-text prompt from OpenAI messages array function buildPrompt(messages, system) { const parts = []; if (system) parts.push(`[SYSTEM]\n${system}\n[/SYSTEM]`); for (const msg of messages) { const role = msg.role === 'assistant' ? 'Assistant' : 'Human'; const content = Array.isArray(msg.content) ? msg.content.map(b => typeof b === 'string' ? b : (b.text || '')).join('') : String(msg.content || ''); parts.push(`${role}: ${content}`); } return parts.join('\n\n'); } // Strip provider prefix e.g. "sub-claude/claude-sonnet-4-5" → "claude-sonnet-4-5" function cleanModel(model) { return (model || 'claude-sonnet-4-5').replace(/^[^/]+\//, ''); } function runClaude(prompt, model) { return new Promise((resolve, reject) => { const env = { ...process.env }; delete env.ANTHROPIC_API_KEY; // force CLAUDE_CODE_OAUTH_TOKEN auth const child = spawn(CLAUDE_BIN, [ '-p', prompt, '--output-format', 'json', '--model', model, '--dangerously-skip-permissions', '--no-session-persistence', ], { env, timeout: 120000, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = '', stderr = ''; child.stdout.on('data', d => stdout += d); child.stderr.on('data', d => stderr += d); child.on('close', code => { if (code !== 0) return reject(new Error(`claude exit ${code}: ${stderr.slice(0, 300)}`)); if (!stdout.trim()) return reject(new Error(`empty output. stderr: ${stderr.slice(0, 200)}`)); try { resolve(JSON.parse(stdout)); } catch { reject(new Error(`bad JSON: ${stdout.slice(0, 200)}`)); } }); child.on('error', reject); }); } const server = createServer(async (req, res) => { // Health check if (req.method === 'GET' && req.url === '/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, proxy: 'claude-sub-proxy' })); return; } if (req.method !== 'POST') { res.writeHead(405); res.end(); return; } let body = ''; req.on('data', d => body += d); req.on('end', async () => { try { const parsed = JSON.parse(body); const model = cleanModel(parsed.model); const messages = parsed.messages || []; const system = typeof parsed.system === 'string' ? parsed.system : undefined; const prompt = buildPrompt(messages, system); console.log(`[${new Date().toISOString()}] → ${model} (${prompt.length} chars)`); const result = await runClaude(prompt, model); if (result.is_error) throw new Error(result.result || 'Claude error'); console.log(`[${new Date().toISOString()}] ✅ cost=$${result.total_cost_usd?.toFixed(5) || '?'}`); // Return OpenAI chat completions format res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ id: `chatcmpl_proxy_${Date.now()}`, object: 'chat.completion', created: Math.floor(Date.now() / 1000), model: parsed.model || model, choices: [{ index: 0, message: { role: 'assistant', content: result.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), }, })); } catch (err) { console.error(`[${new Date().toISOString()}] ❌`, err.message); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: err.message, type: 'proxy_error' } })); } }); }); server.listen(PORT, '127.0.0.1', () => { console.log(`✅ claude-sub-proxy on http://127.0.0.1:${PORT} (OpenAI-compatible)`); console.log(` Routes → Claude Code CLI (${CLAUDE_BIN}) using Pro subscription`); });