From 9263d7bb2611625861393a7babf1b9a63dfd389f Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 4 Mar 2026 07:38:22 +0000 Subject: [PATCH] refactor(proxy): remove OpenRouter fallback, simplify to Anthropic API + CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes all OpenRouter proxy code from claude-proxy.ts. The fallback chain is now just: Anthropic API (key/OAuth) → Claude CLI. Co-Authored-By: Claude Opus 4.6 --- packages/app/server/claude-proxy.ts | 197 +--------------------------- 1 file changed, 4 insertions(+), 193 deletions(-) diff --git a/packages/app/server/claude-proxy.ts b/packages/app/server/claude-proxy.ts index 9766b4a5..9c7df5ac 100644 --- a/packages/app/server/claude-proxy.ts +++ b/packages/app/server/claude-proxy.ts @@ -176,7 +176,6 @@ async function getAnthropicCredential(): Promise { return undefined } -const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY ?? process.env.VITE_OPENROUTER_API_KEY ?? '' const isOAuthToken = (s: string) => /^sk-ant-oat/.test(s) const SEARCH_WEB_TOOL = { @@ -326,176 +325,6 @@ async function streamViaAnthropicApi( } } -/** Call OpenRouter but emit Anthropic-format SSE (for Claude provider fallback) */ -async function streamViaOpenRouterFallback( - model: string, - system: string | undefined, - messages: { role: string; content: string }[], - res: import('http').ServerResponse, -): Promise { - const orModel = model?.includes('opus') ? 'anthropic/claude-opus-4' - : model?.includes('haiku') ? 'anthropic/claude-haiku-4' - : 'anthropic/claude-sonnet-4' - const orMessages: { role: string; content: string }[] = [] - if (system) orMessages.push({ role: 'system', content: system }) - for (const m of messages) { - orMessages.push({ role: m.role, content: m.content }) - } - - let clientDisconnected = false - res.on('close', () => { clientDisconnected = true }) - - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - 'X-Accel-Buffering': 'no', - }) - - try { - const apiRes = await fetch('https://openrouter.ai/api/v1/chat/completions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, - 'HTTP-Referer': APP_URL, - 'X-Title': 'AIUI', - }, - body: JSON.stringify({ model: orModel, messages: orMessages, stream: true }), - signal: AbortSignal.timeout(120000), - }) - - if (!apiRes.ok) { - const errBody = await apiRes.text() - if (!clientDisconnected) { - res.write(`data: ${JSON.stringify({ type: 'error', error: { message: `OpenRouter ${apiRes.status}: ${errBody.slice(0, 200)}` } })}\n\n`) - res.write('data: [DONE]\n\n') - res.end() - } - return - } - - const reader = apiRes.body?.getReader() - if (!reader) { - if (!clientDisconnected) { - res.write('data: [DONE]\n\n') - res.end() - } - return - } - - const decoder = new TextDecoder() - let buffer = '' - while (true) { - const { done, value } = await reader.read() - if (done || clientDisconnected) break - buffer += decoder.decode(value, { stream: true }) - const lines = buffer.split('\n') - buffer = lines.pop() ?? '' - for (const line of lines) { - if (!line.startsWith('data: ')) continue - const payload = line.slice(6).trim() - if (payload === '[DONE]') continue - try { - const chunk = JSON.parse(payload) - const text = chunk.choices?.[0]?.delta?.content - if (text && !clientDisconnected) { - // Convert to Anthropic SSE format - res.write(`data: ${JSON.stringify({ type: 'content_block_delta', delta: { type: 'text_delta', text } })}\n\n`) - } - } catch { /* skip malformed chunks */ } - } - } - - if (!clientDisconnected) { - res.write('data: [DONE]\n\n') - res.end() - } - } catch (err) { - console.error('[proxy] OpenRouter fallback error:', err) - if (!clientDisconnected) { - res.write(`data: ${JSON.stringify({ type: 'error', error: { message: `OpenRouter error: ${err instanceof Error ? err.message : String(err)}` } })}\n\n`) - res.write('data: [DONE]\n\n') - res.end() - } - } -} - -async function streamOpenRouterProxy( - reqBody: string, - res: import('http').ServerResponse, -): Promise { - if (!OPENROUTER_API_KEY) { - res.writeHead(500, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }) - res.end(JSON.stringify({ error: 'OPENROUTER_API_KEY not configured on server' })) - return - } - - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Access-Control-Allow-Origin': '*', - 'X-Accel-Buffering': 'no', - }) - - let clientDisconnected = false - res.on('close', () => { clientDisconnected = true }) - - try { - const apiRes = await fetch('https://openrouter.ai/api/v1/chat/completions', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, - 'HTTP-Referer': APP_URL, - 'X-Title': 'AIUI', - }, - body: reqBody, - signal: AbortSignal.timeout(120000), - }) - - if (!apiRes.ok) { - const errBody = await apiRes.text() - if (!clientDisconnected) { - res.write(`data: ${JSON.stringify({ error: `OpenRouter API ${apiRes.status}: ${errBody.slice(0, 200)}` })}\n\n`) - res.write('data: [DONE]\n\n') - res.end() - } - return - } - - const reader = apiRes.body?.getReader() - if (!reader) { - if (!clientDisconnected) { - res.write('data: [DONE]\n\n') - res.end() - } - return - } - - const decoder = new TextDecoder() - while (true) { - const { done, value } = await reader.read() - if (done || clientDisconnected) break - const chunk = decoder.decode(value, { stream: true }) - res.write(chunk) - } - - if (!clientDisconnected) { - res.end() - } - } catch (err) { - console.error('[proxy] OpenRouter error:', err) - if (!clientDisconnected) { - res.write(`data: ${JSON.stringify({ error: `OpenRouter proxy error: ${err instanceof Error ? err.message : String(err)}` })}\n\n`) - res.write('data: [DONE]\n\n') - res.end() - } - } -} - const server = createServer((req, res) => { if (req.method === 'OPTIONS') { res.writeHead(204, { @@ -507,7 +336,7 @@ const server = createServer((req, res) => { return } - if (req.method !== 'POST' || (req.url !== '/v1/messages' && req.url !== '/v1/openrouter')) { + if (req.method !== 'POST' || req.url !== '/v1/messages') { res.writeHead(404, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ error: 'Not found' })) return @@ -516,11 +345,6 @@ const server = createServer((req, res) => { let body = '' req.on('data', (chunk) => { body += chunk }) req.on('end', async () => { - if (req.url === '/v1/openrouter') { - console.log('[proxy] → OpenRouter proxy') - streamOpenRouterProxy(body, res) - return - } try { const payload = JSON.parse(body) const { model, messages, system, webSearch } = payload @@ -542,17 +366,8 @@ const server = createServer((req, res) => { return } - // Fallback: OpenRouter (when no Anthropic credential available) - if (OPENROUTER_API_KEY) { - console.log('[proxy] No API credential — falling back to OpenRouter') - const lastMsg = (messages ?? []).slice(-1)[0]?.content?.slice(0, 60) ?? '' - console.log(`[proxy] → OpenRouter fallback "${lastMsg}..."`) - await streamViaOpenRouterFallback(model, system, messages ?? [], res) - return - } - - // Last resort: Claude CLI (works when logged in from terminal) - console.log('[proxy] No API credential and no OpenRouter key — falling back to Claude CLI') + // Fallback: Claude CLI (works when logged in from terminal) + console.log('[proxy] No API credential — falling back to Claude CLI') const modelFlag = model?.includes('opus') ? 'opus' : model?.includes('haiku') ? 'haiku' @@ -615,7 +430,7 @@ const server = createServer((req, res) => { if (fullOutput.length === 0 && !clientDisconnected) { console.warn('[proxy] CLI timeout (30s no output) — killing process') proc.kill('SIGTERM') - res.write(`data: ${JSON.stringify({ type: 'error', error: { message: 'Claude CLI timed out. Set ANTHROPIC_API_KEY or VITE_OPENROUTER_API_KEY in .env.local' } })}\n\n`) + res.write(`data: ${JSON.stringify({ type: 'error', error: { message: 'Claude CLI timed out. Set ANTHROPIC_API_KEY in .env.local or log in with: claude login' } })}\n\n`) res.write('data: [DONE]\n\n') res.end() } @@ -691,14 +506,10 @@ server.listen(PORT, async () => { const mode = isOAuthToken(credential) ? 'OAuth (Max)' : 'API key' console.log(` Auth: ${mode} (token ...${credential.slice(-8)})`) console.log(` Mode: Direct Anthropic API (no CLI needed)`) - } else if (OPENROUTER_API_KEY) { - console.log(` Auth: none found (Anthropic)`) - console.log(` Fallback: OpenRouter (key ...${OPENROUTER_API_KEY.slice(-8)})`) } else { console.log(` Auth: none found`) console.log(` Checked: env vars, ~/.claude/.credentials.json, ~/.claude/settings.json`) console.log(` Fallback: Claude CLI (${CLAUDE_BIN})`) } - console.log(` OpenRouter proxy: ${OPENROUTER_API_KEY ? 'enabled' : 'add OPENROUTER_API_KEY to .env.local'}`) console.log() })