Files
Hermes-ui/packages/server/src/services/codex-proxy.ts
T
ekko 285f623d6f [codex] 修复 Coding Agents 的 Codex 启动和代理隔离 (#1123)
* feat: add coding agent install status

* chore: add latest claude opus model preset

* feat: add coding agent config editing

* Add scoped coding agent launch proxy

* Add Codex proxy plan

* fix coding agents codex launch proxy

* fix codex catalog context test

---------

Co-authored-by: Codex <codex@openai.com>
2026-05-29 19:06:54 +08:00

909 lines
29 KiB
TypeScript

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<string, CodexProxyTarget>()
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<any> {
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<any> {
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<any> {
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<Uint8Array> {
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<Readable> {
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<number, { id: string; name: string; arguments: string; added: boolean }>()
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<Readable> {
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<number, { id: string; name: string; arguments: string; added: boolean }>()
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,
}],
}
}