import { randomBytes } from 'crypto' import { Readable } from 'stream' import type { Context } from 'koa' import { config } from '../config' export type ApiMode = 'chat_completions' | 'codex_responses' | 'anthropic_messages' | 'bedrock_converse' | 'codex_app_server' export interface ClaudeCodeProxyTargetInput { provider: string model: string baseUrl: string apiKey: string apiMode?: ApiMode } interface ClaudeCodeProxyTarget extends ClaudeCodeProxyTargetInput { key: string routeKey: string token: string updatedAt: number } const targets = new Map() const CLAUDE_PROXY_VISIBLE_MODELS = [ 'claude-haiku-4-5', 'claude-sonnet-4-6', 'claude-opus-4-7', ] function targetKey(provider: string, model: string, apiMode: ApiMode, baseUrl: string): string { return `${provider}\0${model}\0${apiMode}\0${baseUrl}` } function routeKeyFor(provider: string, model: string, apiMode: ApiMode, baseUrl: string): string { return Buffer.from(targetKey(provider, model, apiMode, baseUrl), 'utf-8').toString('base64url') } function localProxyBaseUrl(routeKey: string): string { return `http://127.0.0.1:${config.port}/api/claude-code-proxy/${routeKey}` } export function registerClaudeCodeProxyTarget(input: ClaudeCodeProxyTargetInput): { baseUrl: string; token: string; routeKey: string } { const provider = input.provider.trim() const model = input.model.trim() const baseUrl = input.baseUrl.replace(/\/+$/, '') const apiMode = input.apiMode || 'chat_completions' const key = targetKey(provider, model, apiMode, baseUrl) const existing = targets.get(key) const routeKey = existing?.routeKey || routeKeyFor(provider, model, apiMode, baseUrl) const token = existing?.token || `hwui_${randomBytes(24).toString('base64url')}` targets.set(key, { ...input, provider, model, baseUrl, apiMode, key, routeKey, token, updatedAt: Date.now(), }) return { baseUrl: localProxyBaseUrl(routeKey), token, routeKey } } function findTarget(routeKey: string): ClaudeCodeProxyTarget | null { for (const target of targets.values()) { if (target.routeKey === routeKey) return target } return null } function authToken(ctx: Context): string { const apiKey = ctx.get('x-api-key').trim() if (apiKey) return apiKey const auth = ctx.get('authorization').trim() const match = auth.match(/^Bearer\s+(.+)$/i) return match?.[1]?.trim() || '' } function requireTarget(ctx: Context): ClaudeCodeProxyTarget | null { const target = findTarget(String(ctx.params.key || '')) if (!target) { ctx.status = 404 ctx.body = { type: 'error', error: { type: 'not_found_error', message: 'Claude proxy target not found' } } return null } if (authToken(ctx) !== target.token) { ctx.status = 401 ctx.body = { type: 'error', error: { type: 'authentication_error', message: 'Invalid Claude proxy token' } } return null } return target } function stringifyContent(value: unknown): string { if (typeof value === 'string') return value if (Array.isArray(value)) { return value.map((item) => { if (typeof item === 'string') return item if (item && typeof item === 'object' && 'text' in item) return String((item as any).text || '') return JSON.stringify(item) }).filter(Boolean).join('\n') } if (value == null) return '' return JSON.stringify(value) } function shouldPreserveReasoningContent(target: ClaudeCodeProxyTarget): boolean { const identifier = `${target.provider} ${target.model} ${target.baseUrl}`.toLowerCase() return [ 'deepseek', 'moonshot', 'kimi', 'mimo', 'xiaomimimo', ].some(part => identifier.includes(part)) } function anthropicContentToOpenAiMessages(message: any, preserveReasoningContent = false): any[] { const content = message?.content if (!Array.isArray(content)) { return [{ role: message.role, content: stringifyContent(content) }] } if (message.role === 'assistant') { const textParts: string[] = [] const reasoningParts: string[] = [] const toolCalls: any[] = [] for (const block of content) { if (block?.type === 'text') textParts.push(String(block.text || '')) if (block?.type === 'thinking' && block.thinking) reasoningParts.push(String(block.thinking)) if (block?.type === 'redacted_thinking' && preserveReasoningContent) reasoningParts.push('[redacted thinking]') if (block?.type === 'tool_use') { toolCalls.push({ id: String(block.id || `tool_${toolCalls.length}`), type: 'function', function: { name: String(block.name || 'tool'), arguments: JSON.stringify(block.input || {}), }, }) } } const openAiMessage: any = { role: 'assistant', content: textParts.join('\n') || null, ...(toolCalls.length ? { tool_calls: toolCalls } : {}), } if (preserveReasoningContent && (reasoningParts.length || toolCalls.length)) { openAiMessage.reasoning_content = reasoningParts.join('\n') || 'tool call' } return [openAiMessage] } const messages: any[] = [] const textParts: string[] = [] for (const block of content) { if (block?.type === 'text') textParts.push(String(block.text || '')) if (block?.type === 'tool_result') { if (textParts.length) { messages.push({ role: 'user', content: textParts.splice(0).join('\n') }) } messages.push({ role: 'tool', tool_call_id: String(block.tool_use_id || ''), content: stringifyContent(block.content), }) } } if (textParts.length) messages.push({ role: message.role || 'user', content: textParts.join('\n') }) return messages.length ? messages : [{ role: message.role || 'user', content: '' }] } function anthropicToOpenAiChat(body: any, target: ClaudeCodeProxyTarget, stream = false): any { const messages: any[] = [] const preserveReasoningContent = shouldPreserveReasoningContent(target) const system = body?.system if (system) messages.push({ role: 'system', content: stringifyContent(system) }) for (const message of Array.isArray(body?.messages) ? body.messages : []) { messages.push(...anthropicContentToOpenAiMessages(message, preserveReasoningContent)) } const tools = Array.isArray(body?.tools) ? body.tools.map((tool: any) => ({ type: 'function', function: { name: String(tool.name || ''), description: String(tool.description || ''), parameters: tool.input_schema || { type: 'object', properties: {} }, }, })).filter((tool: any) => tool.function.name) : undefined return { model: target.model, messages, ...(typeof body?.max_tokens === 'number' ? { max_tokens: body.max_tokens } : {}), ...(typeof body?.temperature === 'number' ? { temperature: body.temperature } : {}), ...(tools?.length ? { tools } : {}), stream, } } function anthropicToOpenAiResponsesInput(message: any): any[] { const content = Array.isArray(message?.content) ? message.content : [{ type: 'text', text: stringifyContent(message?.content) }] if (message.role === 'assistant') { const items: any[] = [] const textParts: string[] = [] for (const block of content) { if (block?.type === 'text') textParts.push(String(block.text || '')) if (block?.type === 'tool_use') { if (textParts.length) { items.push({ role: 'assistant', content: textParts.splice(0).join('\n') }) } items.push({ type: 'function_call', call_id: String(block.id || `tool_${items.length}`), name: String(block.name || 'tool'), arguments: JSON.stringify(block.input || {}), }) } } if (textParts.length) items.push({ role: 'assistant', content: textParts.join('\n') }) return items } const items: any[] = [] const textParts: string[] = [] for (const block of content) { if (block?.type === 'text') textParts.push(String(block.text || '')) if (block?.type === 'tool_result') { if (textParts.length) { items.push({ role: 'user', content: textParts.splice(0).join('\n') }) } items.push({ type: 'function_call_output', call_id: String(block.tool_use_id || ''), output: stringifyContent(block.content), }) } } if (textParts.length) items.push({ role: message.role || 'user', content: textParts.join('\n') }) return items.length ? items : [{ role: message.role || 'user', content: '' }] } function anthropicToOpenAiResponses(body: any, target: ClaudeCodeProxyTarget, stream = false): any { const input: any[] = [] for (const message of Array.isArray(body?.messages) ? body.messages : []) { input.push(...anthropicToOpenAiResponsesInput(message)) } const tools = Array.isArray(body?.tools) ? body.tools.map((tool: any) => ({ type: 'function', name: String(tool.name || ''), description: String(tool.description || ''), parameters: tool.input_schema || { type: 'object', properties: {} }, })).filter((tool: any) => tool.name) : undefined return { model: target.model, input, ...(body?.system ? { instructions: stringifyContent(body.system) } : {}), ...(typeof body?.max_tokens === 'number' ? { max_output_tokens: body.max_tokens } : {}), ...(typeof body?.temperature === 'number' ? { temperature: body.temperature } : {}), ...(tools?.length ? { tools } : {}), stream, store: false, } } function safeJsonParse(value: string): any { try { return JSON.parse(value) } catch { return {} } } function mapStopReason(reason: string | null | undefined, hasTools: boolean): string { if (hasTools) return 'tool_use' if (reason === 'length') return 'max_tokens' if (reason === 'content_filter') return 'stop_sequence' return 'end_turn' } function openAiToAnthropicMessage(data: any, target: ClaudeCodeProxyTarget): any { const choice = data?.choices?.[0] || {} const message = choice.message || {} const content: any[] = [] if (shouldPreserveReasoningContent(target) && message.reasoning_content) { content.push({ type: 'thinking', thinking: String(message.reasoning_content) }) } if (message.content) content.push({ type: 'text', text: String(message.content) }) for (const call of Array.isArray(message.tool_calls) ? message.tool_calls : []) { content.push({ type: 'tool_use', id: String(call.id || `toolu_${content.length}`), name: String(call.function?.name || 'tool'), input: safeJsonParse(String(call.function?.arguments || '{}')), }) } const hasTools = content.some(block => block.type === 'tool_use') return { id: String(data?.id || `msg_${Date.now()}`), type: 'message', role: 'assistant', model: target.model, content, stop_reason: mapStopReason(choice.finish_reason, hasTools), stop_sequence: null, usage: { input_tokens: Number(data?.usage?.prompt_tokens || 0), output_tokens: Number(data?.usage?.completion_tokens || 0), }, } } function sseEvent(event: string, data: any): string { return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` } function anthropicMessageToSse(message: any): string { let output = '' output += sseEvent('message_start', { type: 'message_start', message: { ...message, content: [], stop_reason: null, usage: { input_tokens: message.usage.input_tokens, output_tokens: 0 } }, }) message.content.forEach((block: any, index: number) => { if (block.type === 'text') { output += sseEvent('content_block_start', { type: 'content_block_start', index, content_block: { type: 'text', text: '' } }) if (block.text) output += sseEvent('content_block_delta', { type: 'content_block_delta', index, delta: { type: 'text_delta', text: block.text } }) output += sseEvent('content_block_stop', { type: 'content_block_stop', index }) } else if (block.type === 'tool_use') { output += sseEvent('content_block_start', { type: 'content_block_start', index, content_block: { type: 'tool_use', id: block.id, name: block.name, input: {} }, }) output += sseEvent('content_block_delta', { type: 'content_block_delta', index, delta: { type: 'input_json_delta', partial_json: JSON.stringify(block.input || {}) }, }) output += sseEvent('content_block_stop', { type: 'content_block_stop', index }) } }) output += sseEvent('message_delta', { type: 'message_delta', delta: { stop_reason: message.stop_reason, stop_sequence: null }, usage: { output_tokens: message.usage.output_tokens }, }) output += sseEvent('message_stop', { type: 'message_stop' }) return output } function anthropicMessagesUrl(target: ClaudeCodeProxyTarget): string { if (/\/v\d+$/i.test(target.baseUrl)) return `${target.baseUrl}/messages` return `${target.baseUrl}/v1/messages` } async function readProviderJson(res: Response): Promise { const text = await res.text() try { return JSON.parse(text) } catch { return { error: { message: text || `Provider returned HTTP ${res.status}` } } } } function throwProviderError(res: Response, data: any): never { const err = new Error(data?.error?.message || `Provider returned HTTP ${res.status}`) ;(err as any).status = res.status ;(err as any).providerError = data throw err } function anthropicRequestBody(body: any, target: ClaudeCodeProxyTarget): any { return { ...body, model: target.model, } } async function callAnthropicMessages(target: ClaudeCodeProxyTarget, body: any): Promise { if (target.apiMode !== 'anthropic_messages') { const err = new Error(`Claude proxy Anthropic adapter only supports anthropic_messages targets, got ${target.apiMode}`) ;(err as any).status = 501 throw err } const res = await fetch(anthropicMessagesUrl(target), { method: 'POST', headers: { Authorization: `Bearer ${target.apiKey}`, 'x-api-key': target.apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json', }, body: JSON.stringify(anthropicRequestBody(body, target)), }) const data = await readProviderJson(res) if (!res.ok) throwProviderError(res, data) return data } async function callOpenAiChat(target: ClaudeCodeProxyTarget, body: any): Promise { if (target.apiMode !== 'chat_completions') { const err = new Error(`Claude proxy MVP only supports chat_completions targets, got ${target.apiMode}`) ;(err as any).status = 501 throw err } const res = await fetch(`${target.baseUrl}/chat/completions`, { method: 'POST', headers: { Authorization: `Bearer ${target.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(anthropicToOpenAiChat(body, target)), }) const data = await readProviderJson(res) if (!res.ok) throwProviderError(res, data) return data } async function callOpenAiResponses(target: ClaudeCodeProxyTarget, body: any): Promise { if (target.apiMode !== 'codex_responses') { const err = new Error(`Claude proxy responses adapter only supports codex_responses targets, got ${target.apiMode}`) ;(err as any).status = 501 throw err } const res = await fetch(`${target.baseUrl}/responses`, { method: 'POST', headers: { Authorization: `Bearer ${target.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(anthropicToOpenAiResponses(body, target)), }) const data = await readProviderJson(res) if (!res.ok) throwProviderError(res, data) return data } function responseOutputText(item: any): string { if (item?.type === 'output_text') return String(item.text || '') if (item?.type === 'message' && Array.isArray(item.content)) { return item.content .map((part: any) => { if (part?.type === 'output_text' || part?.type === 'text') return String(part.text || '') return '' }) .filter(Boolean) .join('') } return '' } function openAiResponsesToAnthropicMessage(data: any, target: ClaudeCodeProxyTarget): any { const content: any[] = [] const output = Array.isArray(data?.output) ? data.output : [] for (const item of output) { const text = responseOutputText(item) if (text) content.push({ type: 'text', text }) if (item?.type === 'function_call') { content.push({ type: 'tool_use', id: String(item.call_id || item.id || `toolu_${content.length}`), name: String(item.name || 'tool'), input: safeJsonParse(String(item.arguments || '{}')), }) } } if (!content.length && data?.output_text) { content.push({ type: 'text', text: String(data.output_text) }) } const hasTools = content.some(block => block.type === 'tool_use') return { id: String(data?.id || `msg_${Date.now()}`), type: 'message', role: 'assistant', model: target.model, content, stop_reason: hasTools ? 'tool_use' : (data?.status === 'incomplete' ? 'max_tokens' : 'end_turn'), stop_sequence: null, usage: { input_tokens: Number(data?.usage?.input_tokens || 0), output_tokens: Number(data?.usage?.output_tokens || 0), }, } } function getReadableStream(res: Response): AsyncIterable { const body = res.body if (!body) throw new Error('Provider returned an empty stream') return body as any } function parseOpenAiSse(buffer: string): { events: string[]; rest: string } { const events: string[] = [] let cursor = 0 while (true) { const index = buffer.indexOf('\n\n', cursor) if (index < 0) break events.push(buffer.slice(cursor, index)) cursor = index + 2 } return { events, rest: buffer.slice(cursor) } } function extractSseData(event: string): string[] { return event .split(/\r?\n/) .filter(line => line.startsWith('data:')) .map(line => line.slice(5).trimStart()) } function openAiFinishToAnthropic(finishReason: string | null | undefined, sawTool: boolean): string { return mapStopReason(finishReason, sawTool) } async function openAiChatToAnthropicSseStream(target: ClaudeCodeProxyTarget, body: any): Promise { if (target.apiMode !== 'chat_completions') { const err = new Error(`Claude proxy MVP only supports chat_completions targets, got ${target.apiMode}`) ;(err as any).status = 501 throw err } const res = await fetch(`${target.baseUrl}/chat/completions`, { method: 'POST', headers: { Authorization: `Bearer ${target.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(anthropicToOpenAiChat(body, target, true)), }) if (!res.ok) { let data: any const text = await res.text() try { data = JSON.parse(text) } catch { data = { error: { message: text || `Provider returned HTTP ${res.status}` } } } const err = new Error(data?.error?.message || `Provider returned HTTP ${res.status}`) ;(err as any).status = res.status ;(err as any).providerError = data throw err } const stream = getReadableStream(res) const decoder = new TextDecoder() async function* generate() { const messageId = `msg_${Date.now()}` let buffer = '' let thinkingBlockIndex: number | null = null let thinkingBlockStopped = false let textBlockStarted = false let textBlockStopped = false let textBlockIndex: number | null = null let nextIndex = 0 let stopReason: string | null = null let outputTokens = 0 const toolBlocks = new Map() yield sseEvent('message_start', { type: 'message_start', message: { id: messageId, type: 'message', role: 'assistant', model: target.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 }, }, }) const ensureThinkingBlock = function* () { if (thinkingBlockIndex == null) { thinkingBlockIndex = nextIndex++ yield sseEvent('content_block_start', { type: 'content_block_start', index: thinkingBlockIndex, content_block: { type: 'thinking', thinking: '' }, }) } return thinkingBlockIndex } const stopThinkingBlock = function* () { if (thinkingBlockIndex != null && !thinkingBlockStopped) { thinkingBlockStopped = true yield sseEvent('content_block_stop', { type: 'content_block_stop', index: thinkingBlockIndex }) } } const ensureTextBlock = function* () { if (!textBlockStarted) { textBlockStarted = true textBlockIndex = nextIndex yield sseEvent('content_block_start', { type: 'content_block_start', index: textBlockIndex, content_block: { type: 'text', text: '' }, }) nextIndex += 1 } return textBlockIndex ?? 0 } const ensureToolBlock = function* (toolIndex: number, id?: string, name?: string) { let block = toolBlocks.get(toolIndex) if (!block) { block = { blockIndex: nextIndex++, id: id || `toolu_${toolIndex}`, name: name || 'tool', started: false, } toolBlocks.set(toolIndex, block) } else { if (id) block.id = id if (name) block.name = name } if (!block.started && block.name) { block.started = true yield sseEvent('content_block_start', { type: 'content_block_start', index: block.blockIndex, content_block: { type: 'tool_use', id: block.id, name: block.name, input: {} }, }) } return block } for await (const chunk of stream) { buffer += decoder.decode(chunk, { stream: true }) const parsed = parseOpenAiSse(buffer) buffer = parsed.rest for (const event of parsed.events) { for (const dataLine of extractSseData(event)) { if (!dataLine || dataLine === '[DONE]') continue const data = safeJsonParse(dataLine) const choice = data?.choices?.[0] if (!choice) continue const delta = choice.delta || {} if (shouldPreserveReasoningContent(target) && typeof delta.reasoning_content === 'string' && delta.reasoning_content) { const index = yield* ensureThinkingBlock() yield sseEvent('content_block_delta', { type: 'content_block_delta', index, delta: { type: 'thinking_delta', thinking: delta.reasoning_content }, }) } if (typeof delta.content === 'string' && delta.content) { yield* stopThinkingBlock() const index = yield* ensureTextBlock() yield sseEvent('content_block_delta', { type: 'content_block_delta', index, delta: { type: 'text_delta', text: delta.content }, }) } for (const toolCall of Array.isArray(delta.tool_calls) ? delta.tool_calls : []) { yield* stopThinkingBlock() if (textBlockStarted && !textBlockStopped) { textBlockStopped = true yield sseEvent('content_block_stop', { type: 'content_block_stop', index: textBlockIndex ?? 0 }) } const toolIndex = Number(toolCall.index || 0) const block = yield* ensureToolBlock( toolIndex, toolCall.id ? String(toolCall.id) : undefined, toolCall.function?.name ? String(toolCall.function.name) : undefined, ) const argsDelta = toolCall.function?.arguments if (typeof argsDelta === 'string' && argsDelta) { yield sseEvent('content_block_delta', { type: 'content_block_delta', index: block.blockIndex, delta: { type: 'input_json_delta', partial_json: argsDelta }, }) } } if (choice.finish_reason) stopReason = String(choice.finish_reason) if (data?.usage?.completion_tokens) outputTokens = Number(data.usage.completion_tokens) } } } yield* stopThinkingBlock() if (textBlockStarted && !textBlockStopped) { yield sseEvent('content_block_stop', { type: 'content_block_stop', index: textBlockIndex ?? 0 }) } for (const block of toolBlocks.values()) { if (block.started) { yield sseEvent('content_block_stop', { type: 'content_block_stop', index: block.blockIndex }) } } yield sseEvent('message_delta', { type: 'message_delta', delta: { stop_reason: openAiFinishToAnthropic(stopReason, toolBlocks.size > 0), stop_sequence: null }, usage: { output_tokens: outputTokens }, }) yield sseEvent('message_stop', { type: 'message_stop' }) } return Readable.from(generate()) } async function anthropicMessagesSseStream(target: ClaudeCodeProxyTarget, body: any): Promise { if (target.apiMode !== 'anthropic_messages') { const err = new Error(`Claude proxy Anthropic adapter only supports anthropic_messages targets, got ${target.apiMode}`) ;(err as any).status = 501 throw err } const res = await fetch(anthropicMessagesUrl(target), { method: 'POST', headers: { Authorization: `Bearer ${target.apiKey}`, 'x-api-key': target.apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json', }, body: JSON.stringify(anthropicRequestBody(body, target)), }) if (!res.ok) { const data = await readProviderJson(res) throwProviderError(res, data) } return Readable.from(getReadableStream(res)) } async function openAiResponsesToAnthropicSseStream(target: ClaudeCodeProxyTarget, body: any): Promise { if (target.apiMode !== 'codex_responses') { const err = new Error(`Claude proxy responses adapter only supports codex_responses targets, got ${target.apiMode}`) ;(err as any).status = 501 throw err } const res = await fetch(`${target.baseUrl}/responses`, { method: 'POST', headers: { Authorization: `Bearer ${target.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(anthropicToOpenAiResponses(body, target, true)), }) if (!res.ok) { let data: any const text = await res.text() try { data = JSON.parse(text) } catch { data = { error: { message: text || `Provider returned HTTP ${res.status}` } } } const err = new Error(data?.error?.message || `Provider returned HTTP ${res.status}`) ;(err as any).status = res.status ;(err as any).providerError = data throw err } const stream = getReadableStream(res) const decoder = new TextDecoder() async function* generate() { let messageId = `msg_${Date.now()}` let buffer = '' let textBlockIndex: number | null = null let textBlockStopped = false let nextIndex = 0 let stopReason: string | null = null let outputTokens = 0 const toolBlocks = new Map() yield sseEvent('message_start', { type: 'message_start', message: { id: messageId, type: 'message', role: 'assistant', model: target.model, content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 0, output_tokens: 0 }, }, }) const ensureTextBlock = function* () { if (textBlockIndex == null) { textBlockIndex = nextIndex++ yield sseEvent('content_block_start', { type: 'content_block_start', index: textBlockIndex, content_block: { type: 'text', text: '' }, }) } return textBlockIndex } const ensureToolBlock = function* (key: string, id?: string, name?: string) { let block = toolBlocks.get(key) if (!block) { block = { blockIndex: nextIndex++, id: id || key || `toolu_${toolBlocks.size}`, name: name || 'tool', argsDeltaSeen: false, stopped: false, } toolBlocks.set(key, block) yield sseEvent('content_block_start', { type: 'content_block_start', index: block.blockIndex, content_block: { type: 'tool_use', id: block.id, name: block.name, input: {} }, }) } else { if (id) block.id = id if (name && block.name === 'tool') block.name = name } return block } for await (const chunk of stream) { buffer += decoder.decode(chunk, { stream: true }) const parsed = parseOpenAiSse(buffer) buffer = parsed.rest for (const event of parsed.events) { for (const dataLine of extractSseData(event)) { if (!dataLine || dataLine === '[DONE]') continue const data = safeJsonParse(dataLine) const eventType = data?.type if (eventType === 'response.created') { messageId = String(data?.response?.id || messageId) } if (eventType === 'response.output_text.delta') { const deltaText = String(data?.delta || data?.text || '') if (deltaText) { const index = yield* ensureTextBlock() yield sseEvent('content_block_delta', { type: 'content_block_delta', index, delta: { type: 'text_delta', text: deltaText }, }) } } if (eventType === 'response.output_text.done' && textBlockIndex != null && !textBlockStopped) { textBlockStopped = true yield sseEvent('content_block_stop', { type: 'content_block_stop', index: textBlockIndex }) } if (eventType === 'response.output_item.added') { const item = data?.item || data?.output_item if (item?.type === 'function_call') { const key = String(item.call_id || item.id || data.output_index || toolBlocks.size) yield* ensureToolBlock(key, String(item.call_id || item.id || key), item.name ? String(item.name) : undefined) } } if (eventType === 'response.function_call_arguments.delta') { const key = String(data.call_id || data.item_id || data.output_index || toolBlocks.size) const block = yield* ensureToolBlock(key) const argsDelta = String(data.delta || '') if (argsDelta) { block.argsDeltaSeen = true yield sseEvent('content_block_delta', { type: 'content_block_delta', index: block.blockIndex, delta: { type: 'input_json_delta', partial_json: argsDelta }, }) } } if (eventType === 'response.output_item.done') { const item = data?.item || data?.output_item if (item?.type === 'function_call') { const key = String(item.call_id || item.id || data.output_index || toolBlocks.size) const block = yield* ensureToolBlock(key, String(item.call_id || item.id || key), item.name ? String(item.name) : undefined) const args = String(item.arguments || '') if (args && !block.argsDeltaSeen) { yield sseEvent('content_block_delta', { type: 'content_block_delta', index: block.blockIndex, delta: { type: 'input_json_delta', partial_json: args }, }) } if (!block.stopped) { block.stopped = true yield sseEvent('content_block_stop', { type: 'content_block_stop', index: block.blockIndex }) } } } if (eventType === 'response.completed') { const response = data?.response || data outputTokens = Number(response?.usage?.output_tokens || outputTokens) stopReason = response?.status === 'incomplete' ? 'length' : 'stop' } } } } if (textBlockIndex != null && !textBlockStopped) { yield sseEvent('content_block_stop', { type: 'content_block_stop', index: textBlockIndex }) } for (const block of toolBlocks.values()) { if (!block.stopped) { yield sseEvent('content_block_stop', { type: 'content_block_stop', index: block.blockIndex }) } } yield sseEvent('message_delta', { type: 'message_delta', delta: { stop_reason: openAiFinishToAnthropic(stopReason, toolBlocks.size > 0), stop_sequence: null }, usage: { output_tokens: outputTokens }, }) yield sseEvent('message_stop', { type: 'message_stop' }) } return Readable.from(generate()) } export async function claudeProxyModels(ctx: Context) { const target = requireTarget(ctx) if (!target) return const ids = [...new Set([...CLAUDE_PROXY_VISIBLE_MODELS, target.model])] ctx.body = { data: ids.map(id => ({ type: 'model', id, display_name: id, created_at: '2026-01-01T00:00:00Z', })), has_more: false, first_id: ids[0], last_id: ids[ids.length - 1], } } export async function claudeProxyMessages(ctx: Context) { const target = requireTarget(ctx) if (!target) return try { const requestBody = ctx.request.body || {} if ((requestBody as any).stream === true) { const stream = target.apiMode === 'anthropic_messages' ? await anthropicMessagesSseStream(target, requestBody) : target.apiMode === 'codex_responses' ? await openAiResponsesToAnthropicSseStream(target, requestBody) : await openAiChatToAnthropicSseStream(target, requestBody) ctx.set('Content-Type', 'text/event-stream; charset=utf-8') ctx.set('Cache-Control', 'no-cache') ctx.body = stream } else { const message = target.apiMode === 'anthropic_messages' ? await callAnthropicMessages(target, requestBody) : target.apiMode === 'codex_responses' ? openAiResponsesToAnthropicMessage(await callOpenAiResponses(target, requestBody), target) : openAiToAnthropicMessage(await callOpenAiChat(target, requestBody), target) ctx.body = message } } catch (err: any) { ctx.status = err.status || 502 ctx.body = { type: 'error', error: { type: 'api_error', message: err?.message || 'Claude proxy request failed', provider_error: err?.providerError, }, } } }