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:
co-authored by
Claude Opus 4.6
parent
f1db573f10
commit
9263d7bb26
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user