import { randomBytes } from 'crypto' import { Readable } from 'stream' import type { Context } from 'koa' import { config } from '../config' import type { ApiMode } from './claude-code-proxy' export interface CodexProxyTargetInput { profile: string provider: string model: string baseUrl: string apiKey: string apiMode?: ApiMode } interface CodexProxyTarget extends CodexProxyTargetInput { key: string routeKey: string token: string updatedAt: number } const targets = new Map() function targetKey(profile: string, provider: string, model: string, apiMode: ApiMode, baseUrl: string): string { return `${profile}\0${provider}\0${model}\0${apiMode}\0${baseUrl}` } function routeKeyFor(profile: string, provider: string, model: string, apiMode: ApiMode, baseUrl: string): string { return Buffer.from(targetKey(profile, provider, model, apiMode, baseUrl), 'utf-8').toString('base64url') } function localProxyBaseUrl(routeKey: string): string { return `http://127.0.0.1:${config.port}/api/codex-proxy/${routeKey}/v1` } export function registerCodexProxyTarget(input: CodexProxyTargetInput): { baseUrl: string; token: string; routeKey: string } { const profile = input.profile.trim() const provider = input.provider.trim() const model = input.model.trim() const baseUrl = input.baseUrl.replace(/\/+$/, '') const apiMode = input.apiMode || 'chat_completions' const key = targetKey(profile, provider, model, apiMode, baseUrl) const existing = targets.get(key) const routeKey = existing?.routeKey || routeKeyFor(profile, provider, model, apiMode, baseUrl) const token = existing?.token || `hwui_${randomBytes(24).toString('base64url')}` targets.set(key, { ...input, profile, provider, model, baseUrl, apiMode, key, routeKey, token, updatedAt: Date.now(), }) return { baseUrl: localProxyBaseUrl(routeKey), token, routeKey } } function findTarget(routeKey: string): CodexProxyTarget | 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): CodexProxyTarget | null { const target = findTarget(String(ctx.params.key || '')) if (!target) { ctx.status = 404 ctx.body = { error: { type: 'not_found_error', message: 'Codex proxy target not found' } } return null } if (authToken(ctx) !== target.token) { ctx.status = 401 ctx.body = { error: { type: 'authentication_error', message: 'Invalid Codex 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') { const block = item as any if (typeof block.text === 'string') return block.text if (typeof block.output === 'string') return block.output } return JSON.stringify(item) }).filter(Boolean).join('\n') } if (value == null) return '' return JSON.stringify(value) } function responseContentToText(content: unknown): string { if (typeof content === 'string') return content if (!Array.isArray(content)) return stringifyContent(content) return content.map((part: any) => { if (typeof part === 'string') return part if (part?.type === 'input_text' || part?.type === 'output_text' || part?.type === 'text') { return String(part.text || '') } return stringifyContent(part) }).filter(Boolean).join('\n') } function responsesInputToChatMessages(body: any): any[] { const messages: any[] = [] if (body?.instructions) { messages.push({ role: 'system', content: stringifyContent(body.instructions) }) } const input = body?.input if (typeof input === 'string') { messages.push({ role: 'user', content: input }) return messages } for (const item of Array.isArray(input) ? input : []) { if (!item || typeof item !== 'object') continue if (item.type === 'function_call') { const callId = String(item.call_id || item.id || `call_${messages.length}`) messages.push({ role: 'assistant', content: null, tool_calls: [{ id: callId, type: 'function', function: { name: String(item.name || 'tool'), arguments: String(item.arguments || '{}'), }, }], }) continue } if (item.type === 'function_call_output') { messages.push({ role: 'tool', tool_call_id: String(item.call_id || ''), content: stringifyContent(item.output), }) continue } if (item.role) { messages.push({ role: chatRoleForResponsesRole(item.role), content: responseContentToText(item.content), }) } } return messages.length ? messages : [{ role: 'user', content: '' }] } function chatRoleForResponsesRole(role: unknown): string { const value = String(role || '').trim() if (value === 'developer') return 'system' if (value === 'system' || value === 'user' || value === 'assistant' || value === 'tool') return value return 'user' } function responsesToolsToChatTools(tools: unknown): any[] | undefined { if (!Array.isArray(tools)) return undefined const mapped = tools.map((tool: any) => { if (tool?.type !== 'function') return null return { type: 'function', function: { name: String(tool.name || ''), description: String(tool.description || ''), parameters: tool.parameters || { type: 'object', properties: {} }, }, } }).filter((tool: any) => tool?.function?.name) return mapped.length ? mapped : undefined } function responsesToOpenAiChat(body: any, target: CodexProxyTarget, stream = false): any { const tools = responsesToolsToChatTools(body?.tools) return { model: target.model, messages: responsesInputToChatMessages(body), ...(typeof body?.max_output_tokens === 'number' ? { max_tokens: body.max_output_tokens } : {}), ...(typeof body?.temperature === 'number' ? { temperature: body.temperature } : {}), ...(typeof body?.top_p === 'number' ? { top_p: body.top_p } : {}), ...(tools?.length ? { tools } : {}), stream, } } function responsesRoleToAnthropicRole(role: unknown): 'user' | 'assistant' { return String(role || '') === 'assistant' ? 'assistant' : 'user' } function responsesContentToAnthropicContent(content: unknown, role: 'user' | 'assistant'): any[] { const parts = Array.isArray(content) ? content : [{ type: role === 'assistant' ? 'output_text' : 'input_text', text: stringifyContent(content) }] const mapped = parts.map((part: any) => { if (typeof part === 'string') return { type: 'text', text: part } if (part?.type === 'input_text' || part?.type === 'output_text' || part?.type === 'text') { return { type: 'text', text: String(part.text || '') } } return null }).filter(Boolean) return mapped.length ? mapped : [{ type: 'text', text: '' }] } function responsesInputToAnthropicMessages(body: any): any[] { const messages: any[] = [] const input = body?.input if (typeof input === 'string') return [{ role: 'user', content: [{ type: 'text', text: input }] }] for (const item of Array.isArray(input) ? input : []) { if (!item || typeof item !== 'object') continue if (item.type === 'function_call') { messages.push({ role: 'assistant', content: [{ type: 'tool_use', id: String(item.call_id || item.id || `toolu_${messages.length}`), name: String(item.name || 'tool'), input: safeJsonParse(String(item.arguments || '{}')), }], }) continue } if (item.type === 'function_call_output') { messages.push({ role: 'user', content: [{ type: 'tool_result', tool_use_id: String(item.call_id || ''), content: stringifyContent(item.output), }], }) continue } if (item.role) { const role = responsesRoleToAnthropicRole(item.role) messages.push({ role, content: responsesContentToAnthropicContent(item.content, role), }) } } return messages.length ? messages : [{ role: 'user', content: [{ type: 'text', text: '' }] }] } function responsesToolsToAnthropicTools(tools: unknown): any[] | undefined { if (!Array.isArray(tools)) return undefined const mapped = tools.map((tool: any) => { if (tool?.type !== 'function') return null return { name: String(tool.name || ''), description: String(tool.description || ''), input_schema: tool.parameters || { type: 'object', properties: {} }, } }).filter((tool: any) => tool?.name) return mapped.length ? mapped : undefined } function responsesToAnthropicMessages(body: any, target: CodexProxyTarget, stream = false): any { const tools = responsesToolsToAnthropicTools(body?.tools) return { model: target.model, messages: responsesInputToAnthropicMessages(body), ...(body?.instructions ? { system: stringifyContent(body.instructions) } : {}), ...(typeof body?.max_output_tokens === 'number' ? { max_tokens: body.max_output_tokens } : { max_tokens: 4096 }), ...(typeof body?.temperature === 'number' ? { temperature: body.temperature } : {}), ...(typeof body?.top_p === 'number' ? { top_p: body.top_p } : {}), ...(tools?.length ? { tools } : {}), stream, } } function chatCompletionsUrl(target: CodexProxyTarget): string { if (/\/v\d+$/i.test(target.baseUrl)) return `${target.baseUrl}/chat/completions` return `${target.baseUrl}/v1/chat/completions` } function anthropicMessagesUrl(target: CodexProxyTarget): 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 responseId(data: any): string { return String(data?.id || `resp_${Date.now()}`) } function usageFromChat(data: any) { return { input_tokens: Number(data?.usage?.prompt_tokens || 0), output_tokens: Number(data?.usage?.completion_tokens || 0), total_tokens: Number(data?.usage?.total_tokens || 0), } } function usageFromAnthropic(data: any) { const inputTokens = Number(data?.usage?.input_tokens || 0) const outputTokens = Number(data?.usage?.output_tokens || 0) return { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: inputTokens + outputTokens, } } function openAiChatToResponses(data: any, target: CodexProxyTarget): any { const choice = data?.choices?.[0] || {} const message = choice.message || {} const output: any[] = [] if (message.content) { output.push({ type: 'message', id: `msg_${responseId(data)}`, status: 'completed', role: 'assistant', content: [{ type: 'output_text', text: String(message.content), annotations: [] }], }) } for (const call of Array.isArray(message.tool_calls) ? message.tool_calls : []) { output.push({ type: 'function_call', id: String(call.id || `fc_${output.length}`), call_id: String(call.id || `call_${output.length}`), name: String(call.function?.name || 'tool'), arguments: String(call.function?.arguments || '{}'), }) } return { id: responseId(data), object: 'response', created_at: Number(data?.created || Math.floor(Date.now() / 1000)), status: 'completed', model: target.model, output, usage: usageFromChat(data), } } function anthropicMessageToResponses(data: any, target: CodexProxyTarget): any { const output: any[] = [] const textParts: string[] = [] for (const block of Array.isArray(data?.content) ? data.content : []) { if (block?.type === 'text' && block.text) textParts.push(String(block.text)) if (block?.type === 'tool_use') { output.push({ type: 'function_call', id: String(block.id || `fc_${output.length}`), call_id: String(block.id || `call_${output.length}`), name: String(block.name || 'tool'), arguments: JSON.stringify(block.input || {}), }) } } if (textParts.length) { output.unshift({ type: 'message', id: `msg_${responseId(data)}`, status: 'completed', role: 'assistant', content: [{ type: 'output_text', text: textParts.join('\n'), annotations: [] }], }) } return { id: responseId(data), object: 'response', created_at: Math.floor(Date.now() / 1000), status: 'completed', model: target.model, output, usage: usageFromAnthropic(data), } } async function callOpenAiChat(target: CodexProxyTarget, body: any): Promise { if (target.apiMode !== 'chat_completions') { const err = new Error(`Codex proxy only supports chat_completions targets, got ${target.apiMode}`) ;(err as any).status = 501 throw err } const res = await fetch(chatCompletionsUrl(target), { method: 'POST', headers: { Authorization: `Bearer ${target.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(responsesToOpenAiChat(body, target)), }) const data = await readProviderJson(res) if (!res.ok) throwProviderError(res, data) return data } async function callAnthropicMessages(target: CodexProxyTarget, body: any): Promise { if (target.apiMode !== 'anthropic_messages') { const err = new Error(`Codex 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(responsesToAnthropicMessages(body, target)), }) const data = await readProviderJson(res) if (!res.ok) throwProviderError(res, data) return data } function sseEvent(event: string, data: any): string { return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` } function safeJsonParse(value: string): any { try { return JSON.parse(value) } catch { return {} } } 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()) } async function openAiChatToResponsesSseStream(target: CodexProxyTarget, body: any): Promise { if (target.apiMode !== 'chat_completions') { const err = new Error(`Codex proxy only supports chat_completions targets, got ${target.apiMode}`) ;(err as any).status = 501 throw err } const res = await fetch(chatCompletionsUrl(target), { method: 'POST', headers: { Authorization: `Bearer ${target.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(responsesToOpenAiChat(body, target, true)), }) if (!res.ok) { const data = await readProviderJson(res) throwProviderError(res, data) } const stream = getReadableStream(res) const decoder = new TextDecoder() async function* generate() { const id = `resp_${Date.now()}` const messageId = `msg_${id}` let buffer = '' let textStarted = false let text = '' const toolCalls = new Map() yield sseEvent('response.created', { type: 'response.created', response: { id, object: 'response', status: 'in_progress', model: target.model, output: [] }, }) 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 (typeof delta.content === 'string' && delta.content) { if (!textStarted) { textStarted = true yield sseEvent('response.output_item.added', { type: 'response.output_item.added', output_index: 0, item: { type: 'message', id: messageId, status: 'in_progress', role: 'assistant', content: [], }, }) yield sseEvent('response.content_part.added', { type: 'response.content_part.added', item_id: messageId, output_index: 0, content_index: 0, part: { type: 'output_text', text: '', annotations: [] }, }) } text += delta.content yield sseEvent('response.output_text.delta', { type: 'response.output_text.delta', item_id: messageId, output_index: 0, content_index: 0, delta: delta.content, }) } for (const toolCall of Array.isArray(delta.tool_calls) ? delta.tool_calls : []) { const index = Number(toolCall.index || 0) let call = toolCalls.get(index) if (!call) { call = { id: String(toolCall.id || `call_${index}`), name: String(toolCall.function?.name || 'tool'), arguments: '', added: false, } toolCalls.set(index, call) } if (toolCall.id) call.id = String(toolCall.id) if (toolCall.function?.name) call.name = String(toolCall.function.name) if (!call.added && call.name) { call.added = true yield sseEvent('response.output_item.added', { type: 'response.output_item.added', output_index: textStarted ? index + 1 : index, item: { type: 'function_call', id: call.id, call_id: call.id, name: call.name, arguments: '', }, }) } const argsDelta = toolCall.function?.arguments if (typeof argsDelta === 'string' && argsDelta) { call.arguments += argsDelta yield sseEvent('response.function_call_arguments.delta', { type: 'response.function_call_arguments.delta', item_id: call.id, output_index: textStarted ? index + 1 : index, delta: argsDelta, }) } } } } } const output: any[] = [] if (textStarted) { const messageItem = { type: 'message', id: messageId, status: 'completed', role: 'assistant', content: [{ type: 'output_text', text, annotations: [] }], } output.push(messageItem) yield sseEvent('response.output_text.done', { type: 'response.output_text.done', item_id: messageId, output_index: 0, content_index: 0, text, }) yield sseEvent('response.content_part.done', { type: 'response.content_part.done', item_id: messageId, output_index: 0, content_index: 0, part: { type: 'output_text', text, annotations: [] }, }) yield sseEvent('response.output_item.done', { type: 'response.output_item.done', output_index: 0, item: messageItem, }) } for (const [index, call] of toolCalls.entries()) { const outputIndex = textStarted ? index + 1 : index const callItem = { type: 'function_call', id: call.id, call_id: call.id, name: call.name, arguments: call.arguments || '{}', } output.push(callItem) yield sseEvent('response.output_item.done', { type: 'response.output_item.done', output_index: outputIndex, item: callItem, }) } yield sseEvent('response.completed', { type: 'response.completed', response: { id, object: 'response', status: 'completed', model: target.model, output, }, }) } return Readable.from(generate()) } function extractSseEventName(event: string): string { return event .split(/\r?\n/) .find(line => line.startsWith('event:')) ?.slice(6) .trim() || '' } async function anthropicMessagesToResponsesSseStream(target: CodexProxyTarget, body: any): Promise { if (target.apiMode !== 'anthropic_messages') { const err = new Error(`Codex 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(responsesToAnthropicMessages(body, target, true)), }) if (!res.ok) { const data = await readProviderJson(res) throwProviderError(res, data) } const stream = getReadableStream(res) const decoder = new TextDecoder() async function* generate() { let id = `resp_${Date.now()}` let messageId = `msg_${id}` let buffer = '' let textStarted = false let text = '' const toolBlocks = new Map() yield sseEvent('response.created', { type: 'response.created', response: { id, object: 'response', status: 'in_progress', model: target.model, output: [] }, }) const ensureText = function* () { if (!textStarted) { textStarted = true yield sseEvent('response.output_item.added', { type: 'response.output_item.added', output_index: 0, item: { type: 'message', id: messageId, status: 'in_progress', role: 'assistant', content: [] }, }) yield sseEvent('response.content_part.added', { type: 'response.content_part.added', item_id: messageId, output_index: 0, content_index: 0, part: { type: 'output_text', text: '', annotations: [] }, }) } } const ensureTool = function* (index: number, idValue?: string, name?: string) { let block = toolBlocks.get(index) if (!block) { block = { id: idValue || `toolu_${index}`, name: name || 'tool', arguments: '', added: false } toolBlocks.set(index, block) } if (idValue) block.id = idValue if (name) block.name = name if (!block.added) { block.added = true yield sseEvent('response.output_item.added', { type: 'response.output_item.added', output_index: textStarted ? index + 1 : index, item: { type: 'function_call', id: block.id, call_id: block.id, name: block.name, arguments: '' }, }) } 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) { const eventName = extractSseEventName(event) for (const dataLine of extractSseData(event)) { if (!dataLine || dataLine === '[DONE]') continue const data = safeJsonParse(dataLine) if (eventName === 'message_start' || data?.type === 'message_start') { id = String(data?.message?.id || id) messageId = `msg_${id}` } if (eventName === 'content_block_start' || data?.type === 'content_block_start') { const contentBlock = data?.content_block || {} if (contentBlock.type === 'tool_use') { yield* ensureTool(Number(data.index || 0), String(contentBlock.id || ''), String(contentBlock.name || 'tool')) } } if (eventName === 'content_block_delta' || data?.type === 'content_block_delta') { const delta = data?.delta || {} if (delta.type === 'text_delta' && delta.text) { yield* ensureText() text += String(delta.text) yield sseEvent('response.output_text.delta', { type: 'response.output_text.delta', item_id: messageId, output_index: 0, content_index: 0, delta: String(delta.text), }) } if (delta.type === 'input_json_delta' && delta.partial_json) { const index = Number(data.index || 0) const block = yield* ensureTool(index) const argsDelta = String(delta.partial_json) block.arguments += argsDelta yield sseEvent('response.function_call_arguments.delta', { type: 'response.function_call_arguments.delta', item_id: block.id, output_index: textStarted ? index + 1 : index, delta: argsDelta, }) } } } } } const output: any[] = [] if (textStarted) { const messageItem = { type: 'message', id: messageId, status: 'completed', role: 'assistant', content: [{ type: 'output_text', text, annotations: [] }], } output.push(messageItem) yield sseEvent('response.output_text.done', { type: 'response.output_text.done', item_id: messageId, output_index: 0, content_index: 0, text, }) yield sseEvent('response.content_part.done', { type: 'response.content_part.done', item_id: messageId, output_index: 0, content_index: 0, part: { type: 'output_text', text, annotations: [] }, }) yield sseEvent('response.output_item.done', { type: 'response.output_item.done', output_index: 0, item: messageItem, }) } for (const [index, block] of toolBlocks.entries()) { const outputIndex = textStarted ? index + 1 : index const item = { type: 'function_call', id: block.id, call_id: block.id, name: block.name, arguments: block.arguments || '{}', } output.push(item) yield sseEvent('response.output_item.done', { type: 'response.output_item.done', output_index: outputIndex, item, }) } yield sseEvent('response.completed', { type: 'response.completed', response: { id, object: 'response', status: 'completed', model: target.model, output }, }) } return Readable.from(generate()) } export async function codexProxyResponses(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 anthropicMessagesToResponsesSseStream(target, requestBody) : await openAiChatToResponsesSseStream(target, requestBody) ctx.set('Content-Type', 'text/event-stream; charset=utf-8') ctx.set('Cache-Control', 'no-cache') ctx.body = stream } else { ctx.body = target.apiMode === 'anthropic_messages' ? anthropicMessageToResponses(await callAnthropicMessages(target, requestBody), target) : openAiChatToResponses(await callOpenAiChat(target, requestBody), target) } } catch (err: any) { ctx.status = err.status || 502 ctx.body = { error: { type: 'api_error', message: err?.message || 'Codex proxy request failed', provider_error: err?.providerError, }, } } } export async function codexProxyModels(ctx: Context) { const target = requireTarget(ctx) if (!target) return ctx.body = { object: 'list', data: [{ id: target.model, object: 'model', created: 0, owned_by: target.provider, }], } }