From 2b7f8fda0b65f859498b68ed7ef3996f3ef34b06 Mon Sep 17 00:00:00 2001 From: aedes Date: Tue, 21 Apr 2026 14:36:44 +0300 Subject: [PATCH] feat: auto-fallback to Haiku when Sonnet is rate-limited --- server.js | 52 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/server.js b/server.js index d572f24..ed042c0 100644 --- a/server.js +++ b/server.js @@ -30,13 +30,34 @@ function getAccessToken() { } } +const SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI for Claude.\n\nAnswer in the same language as the user. Give the shortest possible correct answer. No greetings, no filler, no emojis, no markdown formatting unless the user explicitly asks for code. Do not restate the question. Do not add caveats or disclaimers. Respond with just the answer — nothing else."; + +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 modelId = MODELS[model] || MODELS.sonnet; const accessToken = getAccessToken(); - if (!accessToken) { return res.status(500).json({ error: 'No credentials in ' + (process.env.CLAUDE_CONFIG_DIR || '~/.claude') }); } @@ -48,25 +69,16 @@ app.post('/api/chat', async (req, res) => { const heartbeat = setInterval(() => res.write(': ping\n\n'), 15000); + const primaryId = MODELS[model] || MODELS.sonnet; + const fallbackId = MODELS.haiku; + try { - const apiResp = await 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: "You are Claude Code, Anthropic's official CLI for Claude.\n\nAnswer in the same language as the user. Give the shortest possible correct answer. No greetings, no filler, no emojis, no markdown formatting unless the user explicitly asks for code. Do not restate the question. Do not add caveats or disclaimers. Respond with just the answer — nothing else.", - messages: [{ role: 'user', content: text }], - }), - }); + 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();