feat: auto-fallback to Haiku when Sonnet is rate-limited

This commit is contained in:
aedes
2026-04-21 14:36:44 +03:00
parent 321390ea0a
commit 2b7f8fda0b

View File

@@ -30,26 +30,10 @@ function getAccessToken() {
} }
} }
app.post('/api/chat', async (req, res) => { 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.";
const { text, model = 'sonnet' } = req.body;
if (!text?.trim()) return res.status(400).json({ error: 'text required' });
const modelId = MODELS[model] || MODELS.sonnet; async function callClaude(modelId, text, accessToken) {
const accessToken = getAccessToken(); return fetch('https://api.anthropic.com/v1/messages', {
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);
try {
const apiResp = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -63,10 +47,38 @@ app.post('/api/chat', async (req, res) => {
model: modelId, model: modelId,
max_tokens: 8192, max_tokens: 8192,
stream: true, 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.", system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: text }], 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) { if (!apiResp.ok) {
const errText = await apiResp.text(); const errText = await apiResp.text();