AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
111
scripts/claude-sub-proxy.js
Normal file
111
scripts/claude-sub-proxy.js
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/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=<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`);
|
||||
});
|
||||
Reference in New Issue
Block a user