const express = require('express'); const path = require('path'); const fs = require('fs'); const os = require('os'); const { ProxyAgent, setGlobalDispatcher } = require('undici'); const proxyUrl = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; if (proxyUrl) { setGlobalDispatcher(new ProxyAgent(proxyUrl)); console.log(`✓ Using proxy: ${proxyUrl}`); } const app = express(); app.use(express.json({ limit: '10mb' })); app.use(express.static(path.join(__dirname, 'public'))); const MODELS = { sonnet: 'claude-sonnet-4-6', haiku: 'claude-haiku-4-5-20251001', }; function getAccessToken() { const configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'); const credsPath = path.join(configDir, '.credentials.json'); try { const creds = JSON.parse(fs.readFileSync(credsPath, 'utf8')); return creds?.claudeAiOauth?.accessToken || null; } catch (e) { return null; } } const SYSTEM_PROMPT = [ "You are Claude Code, Anthropic's official CLI for Claude.", "", "Output the shortest possible correct answer in the user's language. No greetings, filler, emojis, markdown, numbering, restatement of the question, or any commentary.", "", "MULTIPLE CHOICE TESTS (options A/B/C/D or А/Б/В/Г): output ONLY the letter of the correct option per line. Nothing else. No question numbers, no option text, no punctuation. One letter per question, each on its own line, in order.", "", "OPEN QUESTIONS: respond with just the bare fact/value/term — typically 1-5 words, no sentence. Only use a full sentence if the question genuinely demands one.", "", "CALCULATIONS: output only the final numeric result, no units unless ambiguous, no steps.", "", "Respond with the answer only. Nothing else.", ].join("\n"); async function callClaude(modelId, text, accessToken) { return fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}`, 'anthropic-version': '2023-06-01', 'anthropic-beta': 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14', 'x-app': 'cli', 'User-Agent': 'claude-cli/2.1.114', }, body: JSON.stringify({ model: modelId, max_tokens: 8192, stream: true, system: SYSTEM_PROMPT, messages: [{ role: 'user', content: text }], }), }); } app.post('/api/chat', async (req, res) => { const { text, model = 'sonnet' } = req.body; if (!text?.trim()) return res.status(400).json({ error: 'text required' }); const accessToken = getAccessToken(); if (!accessToken) { return res.status(500).json({ error: 'No credentials in ' + (process.env.CLAUDE_CONFIG_DIR || '~/.claude') }); } res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders(); const heartbeat = setInterval(() => res.write(': ping\n\n'), 15000); const primaryId = MODELS[model] || MODELS.sonnet; const fallbackId = MODELS.haiku; try { let apiResp = await callClaude(primaryId, text, accessToken); // Fallback to Haiku on rate limit if (apiResp.status === 429 && primaryId !== fallbackId) { apiResp = await callClaude(fallbackId, text, accessToken); } if (!apiResp.ok) { const errText = await apiResp.text(); res.write(`data: ${JSON.stringify({ error: `${apiResp.status}: ${errText.substring(0, 500)}` })}\n\n`); return; } const reader = apiResp.body.getReader(); const decoder = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); const events = buf.split('\n\n'); buf = events.pop(); for (const ev of events) { const dataLine = ev.split('\n').find(l => l.startsWith('data: ')); if (!dataLine) continue; const payload = dataLine.slice(6); if (payload === '[DONE]') continue; try { const obj = JSON.parse(payload); if (obj.type === 'content_block_delta' && obj.delta?.type === 'text_delta') { res.write(`data: ${JSON.stringify({ text: obj.delta.text })}\n\n`); } } catch (e) {} } } } catch (err) { console.error('[api error]', err.message); res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`); } finally { clearInterval(heartbeat); res.write('data: [DONE]\n\n'); res.end(); } }); app.listen(8108, () => console.log('sechenov :8108'));