refactor(proxy): remove OpenRouter fallback, simplify to Anthropic API + CLI

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 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-04 07:38:22 +00:00
co-authored by Claude Opus 4.6
parent f1db573f10
commit 9263d7bb26
+4 -193
View File
@@ -176,7 +176,6 @@ async function getAnthropicCredential(): Promise<string | undefined> {
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<void> {
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<void> {
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()
})