AIClient-2-API/src/converters/strategies/CodexConverter.js

1330 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Codex 转换器
* 处理 OpenAI 协议与 Codex 协议之间的转换
*/
import { v4 as uuidv4 } from 'uuid';
import { BaseConverter } from '../BaseConverter.js';
import { MODEL_PROTOCOL_PREFIX } from '../../utils/common.js';
import {
generateResponseCreated,
generateResponseInProgress,
generateOutputItemAdded,
generateContentPartAdded,
generateOutputTextDone,
generateContentPartDone,
generateOutputItemDone,
generateResponseCompleted
} from '../../providers/openai/openai-responses-core.mjs';
export class CodexConverter extends BaseConverter {
constructor() {
super('codex');
this.toolNameMap = new Map(); // 工具名称缩短映射: original -> short
this.reverseToolNameMap = new Map(); // 反向映射: short -> original
this.streamParams = new Map(); // 用于存储流式状态key 为响应 ID 或临时标识
}
/**
* 转换请求
*/
convertRequest(data, targetProtocol) {
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
}
/**
* 转换响应
*/
convertResponse(data, targetProtocol, model) {
switch (targetProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIResponse(data, model);
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
return this.toOpenAIResponsesResponse(data, model);
case MODEL_PROTOCOL_PREFIX.GEMINI:
return this.toGeminiResponse(data, model);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeResponse(data, model);
case MODEL_PROTOCOL_PREFIX.CODEX:
return data; // Codex to Codex
default:
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
}
}
/**
* 转换流式响应块
*/
convertStreamChunk(chunk, targetProtocol, model, requestId) {
switch (targetProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIStreamChunk(chunk, model);
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
return this.toOpenAIResponsesStreamChunk(chunk, model);
case MODEL_PROTOCOL_PREFIX.GEMINI:
return this.toGeminiStreamChunk(chunk, model);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeStreamChunk(chunk, model, requestId);
case MODEL_PROTOCOL_PREFIX.CODEX:
return chunk; // Codex to Codex
default:
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
}
}
/**
* 转换模型列表
*/
convertModelList(data, targetProtocol) {
return data;
}
/**
* OpenAI Responses → Codex 请求转换
*/
toOpenAIResponsesToCodexRequest(responsesRequest) {
let codexRequest = { ...responsesRequest };
// 保留监控相关字段
if (responsesRequest._monitorRequestId) {
codexRequest._monitorRequestId = responsesRequest._monitorRequestId;
}
if (responsesRequest._requestBaseUrl) {
codexRequest._requestBaseUrl = responsesRequest._requestBaseUrl;
}
// 处理 input 字段,如果它是字符串,则转换为消息数组
if (codexRequest.input && typeof codexRequest.input === 'string') {
const inputText = codexRequest.input;
codexRequest.input = [{
type: "message",
role: "user",
content: [{
type: "input_text",
text: inputText
}]
}];
}
// 设置Codex特定的字段
codexRequest.stream = true;
codexRequest.store = false;
codexRequest.parallel_tool_calls = true;
codexRequest.include = ['reasoning.encrypted_content'];
codexRequest.service_tier = responsesRequest.service_tier || 'default';
if (codexRequest.service_tier !== 'priority') {
delete codexRequest.service_tier;
}
// 删除Codex不支持的字段
delete codexRequest.max_output_tokens;
delete codexRequest.max_completion_tokens;
delete codexRequest.temperature;
delete codexRequest.top_p;
delete codexRequest.user;
// 添加 reasoning 配置
codexRequest.reasoning = {
"effort": responsesRequest.reasoning_effort || responsesRequest.reasoning?.effort || "medium",
"summary": responsesRequest.reasoning?.summary || "auto"
};
// 确保 input 数组中的每个项都有 type: "message",并将系统角色转换为开发者角色
if (codexRequest.input && Array.isArray(codexRequest.input)) {
codexRequest.input = codexRequest.input.filter(item => {
// 如果 instructions 已存在,过滤掉 input 中的 system/developer 消息以避免重复
if (codexRequest.instructions && (item.role === 'system' || item.role === 'developer')) {
return false;
}
return true;
}).map(item => {
// 如果没有 type 或者 type 不是 message则添加 type: "message"
if (!item.type || item.type !== 'message') {
item = { type: "message", ...item };
}
// 将系统角色转换为开发者角色
if (item.role === 'system') {
item = { ...item, role: 'developer' };
}
return item;
});
}
return codexRequest;
}
/**
* OpenAI → Codex 请求转换
*/
toOpenAIRequestToCodexRequest(data) {
// 构建工具名称映射
this.buildToolNameMap(data.tools || []);
const codexRequest = {
model: data.model,
instructions: this.buildInstructions(data),
input: this.convertMessages((data.messages || []).filter(m => m.role !== 'system' && m.role !== 'developer')),
stream: true,
store: false,
metadata: data.metadata || {},
reasoning: {
effort: data.reasoning_effort || data.reasoning?.effort || 'medium',
summary: data.reasoning?.summary || 'auto'
},
parallel_tool_calls: true,
include: ['reasoning.encrypted_content']
};
// 保留监控相关字段
if (data._monitorRequestId) {
codexRequest._monitorRequestId = data._monitorRequestId;
}
if (data._requestBaseUrl) {
codexRequest._requestBaseUrl = data._requestBaseUrl;
}
codexRequest.service_tier = data.service_tier || 'default';
if (codexRequest.service_tier !== 'priority') {
delete codexRequest.service_tier;
}
// 处理 OpenAI Responses 特有的 instructions 和 input 字段(如果存在)
if (data.instructions && !codexRequest.instructions) {
codexRequest.instructions = data.instructions;
}
if (data.input && Array.isArray(data.input) && codexRequest.input.length === 0) {
// 如果是 OpenAI Responses 格式的 input
for (const item of data.input) {
if (item.type === 'message' && item.role !== 'system' && item.role !== 'developer') {
codexRequest.input.push({
type: 'message',
role: item.role === 'system' ? 'developer' : item.role,
content: Array.isArray(item.content) ? item.content.map(c => ({
type: item.role === 'assistant' ? 'output_text' : 'input_text',
text: c.text
})) : [{
type: item.role === 'assistant' ? 'output_text' : 'input_text',
text: item.content
}]
});
}
}
}
if (data.tools && data.tools.length > 0) {
codexRequest.tools = this.convertTools(data.tools);
}
if (data.tool_choice) {
codexRequest.tool_choice = this.convertToolChoice(data.tool_choice);
}
if (data.response_format || data.text?.verbosity) {
const textObj = {};
if (data.response_format) {
const converted = this.convertResponseFormat(data.response_format);
if (converted) {
textObj.format = converted;
}
}
if (data.text?.verbosity) {
textObj.verbosity = data.text.verbosity;
}
if (Object.keys(textObj).length > 0) {
codexRequest.text = textObj;
}
}
// 在 input 开头注入特殊指令(如果配置允许)
// 这里我们默认开启,因为这是为了确保 Codex 遵循指令
if (codexRequest.input.length > 0 && codexRequest.instructions) {
const firstMsg = codexRequest.input[0];
const specialInstruction = "EXECUTE ACCORDING TO THE FOLLOWING INSTRUCTIONS!!!";
const firstText = firstMsg.content?.[0]?.text;
if (firstMsg.role === 'user' && firstText !== specialInstruction) {
codexRequest.input.unshift({
type: "message",
role: "user",
content: [{
type: "input_text",
text: specialInstruction
}]
});
}
}
return codexRequest;
}
/**
* 构建指令
*/
buildInstructions(data) {
// 首先检查显式的 instructions 字段 (OpenAI Responses)
if (data.instructions) return data.instructions;
const systemMessages = (data.messages || []).filter(m => m.role === 'system');
if (systemMessages.length > 0) {
return systemMessages.map(m => {
if (typeof m.content === 'string') {
return m.content;
} else if (Array.isArray(m.content)) {
const textPart = m.content.find(part => part.type === 'text');
return textPart ? textPart.text : '';
}
return '';
}).join('\n').trim();
}
return '';
}
/**
* 转换消息
*/
convertMessages(messages) {
const input = [];
for (const msg of messages) {
const role = msg.role;
if (role === 'tool' || role === 'tool_result') {
input.push({
type: 'function_call_output',
call_id: msg.tool_call_id || msg.tool_use_id,
output: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
});
} else {
const codexMsg = {
type: 'message',
role: role === 'system' ? 'developer' : (role === 'model' ? 'assistant' : role),
content: this.convertMessageContent(msg.content, role)
};
if (codexMsg.content.length > 0) {
input.push(codexMsg);
}
if ((role === 'assistant' || role === 'model') && msg.tool_calls) {
for (const toolCall of msg.tool_calls) {
if (toolCall.type === 'function' || toolCall.function) {
const func = toolCall.function || toolCall;
const originalName = func.name;
const shortName = this.toolNameMap.get(originalName) || this.shortenToolName(originalName);
input.push({
type: 'function_call',
call_id: toolCall.id,
name: shortName,
arguments: typeof func.arguments === 'string' ? func.arguments : JSON.stringify(func.arguments)
});
}
}
}
// 处理 Claude 格式的 tool_use
if (role === 'assistant' && Array.isArray(msg.content)) {
for (const part of msg.content) {
if (part.type === 'tool_use') {
const originalName = part.name;
const shortName = this.toolNameMap.get(originalName) || this.shortenToolName(originalName);
input.push({
type: 'function_call',
call_id: part.id,
name: shortName,
arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input)
});
}
}
}
}
}
return input;
}
/**
* 转换消息内容
*/
convertMessageContent(content, role) {
if (!content) return [];
const isAssistant = role === 'assistant' || role === 'model';
if (typeof content === 'string') {
return [{
type: isAssistant ? 'output_text' : 'input_text',
text: content
}];
}
if (Array.isArray(content)) {
return content.map(part => {
if (typeof part === 'string') {
return {
type: isAssistant ? 'output_text' : 'input_text',
text: part
};
}
if (part.type === 'text') {
return {
type: isAssistant ? 'output_text' : 'input_text',
text: part.text
};
} else if ((part.type === 'image_url' || part.type === 'image') && !isAssistant) {
let url = '';
if (part.image_url) {
url = typeof part.image_url === 'string' ? part.image_url : part.image_url.url;
} else if (part.source && part.source.type === 'base64') {
url = `data:${part.source.media_type};base64,${part.source.data}`;
}
return url ? {
type: 'input_image',
image_url: url
} : null;
}
return null;
}).filter(Boolean);
}
return [];
}
/**
* 构建工具名称映射
*/
buildToolNameMap(tools) {
this.toolNameMap.clear();
this.reverseToolNameMap.clear();
const names = [];
for (const t of tools) {
if (t.type === 'function' && t.function?.name) {
names.push(t.function.name);
} else if (t.name) {
names.push(t.name);
}
}
if (names.length === 0) return;
const limit = 64;
const used = new Set();
const baseCandidate = (n) => {
if (n.length <= limit) return n;
if (n.startsWith('mcp__')) {
const idx = n.lastIndexOf('__');
if (idx > 0) {
let cand = 'mcp__' + n.slice(idx + 2);
return cand.length > limit ? cand.slice(0, limit) : cand;
}
}
return n.slice(0, limit);
};
for (const n of names) {
let cand = baseCandidate(n);
let uniq = cand;
if (used.has(uniq)) {
for (let i = 1; ; i++) {
const suffix = '_' + i;
const allowed = limit - suffix.length;
const base = cand.slice(0, Math.max(0, allowed));
const tmp = base + suffix;
if (!used.has(tmp)) {
uniq = tmp;
break;
}
}
}
used.add(uniq);
this.toolNameMap.set(n, uniq);
this.reverseToolNameMap.set(uniq, n);
}
}
/**
* 转换工具
*/
convertTools(tools) {
return tools.map(tool => {
// 处理 Claude 的 web_search
if (tool.type === "web_search_20250305") {
return { type: "web_search" };
}
if (tool.type !== 'function' && !tool.name) {
return tool;
}
const func = tool.function || tool;
const originalName = func.name;
const shortName = this.toolNameMap.get(originalName) || this.shortenToolName(originalName);
const result = {
type: 'function',
name: shortName,
description: func.description,
parameters: func.parameters || func.input_schema || { type: 'object', properties: {} },
strict: func.strict !== undefined ? func.strict : false
};
// 清理参数
if (result.parameters && result.parameters.$schema) {
delete result.parameters.$schema;
}
return result;
});
}
/**
* 转换 tool_choice
*/
convertToolChoice(toolChoice) {
if (typeof toolChoice === 'string') {
return toolChoice;
}
if (toolChoice.type === 'function') {
const name = toolChoice.function?.name;
const shortName = name ? (this.toolNameMap.get(name) || this.shortenToolName(name)) : '';
return {
type: 'function',
name: shortName
};
}
return toolChoice;
}
/**
* 缩短工具名称
*/
shortenToolName(name) {
const limit = 64;
if (name.length <= limit) return name;
if (name.startsWith('mcp__')) {
const idx = name.lastIndexOf('__');
if (idx > 0) {
let cand = 'mcp__' + name.slice(idx + 2);
return cand.length > limit ? cand.slice(0, limit) : cand;
}
}
return name.slice(0, limit);
}
/**
* 获取原始工具名称
*/
getOriginalToolName(shortName) {
return this.reverseToolNameMap.get(shortName) || shortName;
}
/**
* 转换响应格式
*/
convertResponseFormat(responseFormat) {
if (responseFormat.type === 'json_schema') {
return {
type: 'json_schema',
name: responseFormat.json_schema?.name || 'response',
schema: responseFormat.json_schema?.schema || {}
};
} else if (responseFormat.type === 'json_object') {
return null;
}
return responseFormat;
}
/**
* Codex → OpenAI 响应转换(非流式)
*/
toOpenAIResponse(rawJSON, model) {
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
if (root.type !== 'response.completed') {
return null;
}
const response = root.response;
const unixTimestamp = response.created_at || Math.floor(Date.now() / 1000);
const openaiResponse = {
id: response.id || `chatcmpl-${Date.now()}`,
object: 'chat.completion',
created: unixTimestamp,
model: response.model || model,
choices: [{
index: 0,
message: {
role: 'assistant',
content: null,
reasoning_content: null,
tool_calls: null
},
finish_reason: null,
native_finish_reason: null
}],
usage: {
prompt_tokens: response.usage?.input_tokens || 0,
completion_tokens: response.usage?.output_tokens || 0,
total_tokens: response.usage?.total_tokens || 0
}
};
if (response.usage?.output_tokens_details?.reasoning_tokens) {
openaiResponse.usage.completion_tokens_details = {
reasoning_tokens: response.usage.output_tokens_details.reasoning_tokens
};
}
const output = response.output || [];
let contentText = '';
let reasoningText = '';
const toolCalls = [];
for (const item of output) {
switch (item.type) {
case 'reasoning':
if (Array.isArray(item.summary)) {
const summaryItem = item.summary.find(s => s.type === 'summary_text');
if (summaryItem) reasoningText = summaryItem.text;
}
break;
case 'message':
if (Array.isArray(item.content)) {
const contentItem = item.content.find(c => c.type === 'output_text');
if (contentItem) contentText = contentItem.text;
}
break;
case 'function_call':
toolCalls.push({
id: item.call_id || `call_${Date.now()}_${toolCalls.length}`,
type: 'function',
function: {
name: this.getOriginalToolName(item.name),
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments)
}
});
break;
}
}
if (contentText) openaiResponse.choices[0].message.content = contentText;
if (reasoningText) openaiResponse.choices[0].message.reasoning_content = reasoningText;
if (toolCalls.length > 0) openaiResponse.choices[0].message.tool_calls = toolCalls;
if (response.status === 'completed') {
openaiResponse.choices[0].finish_reason = toolCalls.length > 0 ? 'tool_calls' : 'stop';
openaiResponse.choices[0].native_finish_reason = 'stop';
}
return openaiResponse;
}
/**
* Codex → OpenAI Responses 响应转换
*/
toOpenAIResponsesResponse(rawJSON, model) {
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
if (root.type !== 'response.completed') {
return null;
}
const response = root.response;
const unixTimestamp = response.created_at || Math.floor(Date.now() / 1000);
const output = [];
if (response.output && Array.isArray(response.output)) {
for (const item of response.output) {
if (item.type === 'reasoning') {
let reasoningText = '';
if (Array.isArray(item.summary)) {
const summaryItem = item.summary.find(s => s.type === 'summary_text');
if (summaryItem) reasoningText = summaryItem.text;
}
if (reasoningText) {
output.push({
id: `msg_${uuidv4().replace(/-/g, '')}`,
type: "message",
role: "assistant",
status: "completed",
content: [{
type: "reasoning",
text: reasoningText
}]
});
}
} else if (item.type === 'message') {
let contentText = '';
if (Array.isArray(item.content)) {
const contentItem = item.content.find(c => c.type === 'output_text');
if (contentItem) contentText = contentItem.text;
}
if (contentText) {
output.push({
id: `msg_${uuidv4().replace(/-/g, '')}`,
type: "message",
role: "assistant",
status: "completed",
content: [{
type: "output_text",
text: contentText,
annotations: []
}]
});
}
} else if (item.type === 'function_call') {
output.push({
id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
type: "function_call",
name: this.getOriginalToolName(item.name),
arguments: typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments),
status: "completed"
});
}
}
}
return {
id: response.id || `resp_${uuidv4().replace(/-/g, '')}`,
object: "response",
created_at: unixTimestamp,
model: response.model || model,
status: "completed",
output: output,
incomplete_details: response.incomplete_details || null,
usage: {
input_tokens: response.usage?.input_tokens || 0,
output_tokens: response.usage?.output_tokens || 0,
total_tokens: response.usage?.total_tokens || 0,
output_tokens_details: {
reasoning_tokens: response.usage?.output_tokens_details?.reasoning_tokens || 0
}
}
};
}
/**
* Codex → Gemini 响应转换
*/
toGeminiResponse(rawJSON, model) {
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
if (root.type !== 'response.completed') {
return null;
}
const response = root.response;
const parts = [];
if (response.output && Array.isArray(response.output)) {
for (const item of response.output) {
if (item.type === 'reasoning') {
let reasoningText = '';
if (Array.isArray(item.summary)) {
const summaryItem = item.summary.find(s => s.type === 'summary_text');
if (summaryItem) reasoningText = summaryItem.text;
}
if (reasoningText) {
parts.push({ text: reasoningText, thought: true });
}
} else if (item.type === 'message') {
let contentText = '';
if (Array.isArray(item.content)) {
const contentItem = item.content.find(c => c.type === 'output_text');
if (contentItem) contentText = contentItem.text;
}
if (contentText) {
parts.push({ text: contentText });
}
} else if (item.type === 'function_call') {
parts.push({
functionCall: {
name: this.getOriginalToolName(item.name),
args: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
}
});
}
}
}
return {
candidates: [{
content: {
role: "model",
parts: parts
},
finishReason: "STOP"
}],
usageMetadata: {
promptTokenCount: response.usage?.input_tokens || 0,
candidatesTokenCount: response.usage?.output_tokens || 0,
totalTokenCount: response.usage?.total_tokens || 0
},
modelVersion: response.model || model,
responseId: response.id
};
}
/**
* Codex → Claude 响应转换
*/
toClaudeResponse(rawJSON, model) {
const root = typeof rawJSON === 'string' ? JSON.parse(rawJSON) : rawJSON;
if (root.type !== 'response.completed') {
return null;
}
const response = root.response;
const content = [];
let stopReason = "end_turn";
if (response.output && Array.isArray(response.output)) {
for (const item of response.output) {
if (item.type === 'reasoning') {
let reasoningText = '';
if (Array.isArray(item.summary)) {
const summaryItem = item.summary.find(s => s.type === 'summary_text');
if (summaryItem) reasoningText = summaryItem.text;
}
if (reasoningText) {
content.push({ type: "thinking", thinking: reasoningText });
}
} else if (item.type === 'message') {
let contentText = '';
if (Array.isArray(item.content)) {
const contentItem = item.content.find(c => c.type === 'output_text');
if (contentItem) contentText = contentItem.text;
}
if (contentText) {
content.push({ type: "text", text: contentText });
}
} else if (item.type === 'function_call') {
stopReason = "tool_use";
content.push({
type: "tool_use",
id: item.call_id || `call_${uuidv4().replace(/-/g, '')}`,
name: this.getOriginalToolName(item.name),
input: typeof item.arguments === 'string' ? JSON.parse(item.arguments) : item.arguments
});
}
}
}
return {
id: response.id || `msg_${uuidv4().replace(/-/g, '')}`,
type: "message",
role: "assistant",
model: response.model || model,
content: content,
stop_reason: stopReason,
usage: {
input_tokens: response.usage?.input_tokens || 0,
output_tokens: response.usage?.output_tokens || 0
}
};
}
/**
* Codex → OpenAI 流式响应块转换
*/
toOpenAIStreamChunk(chunk, model) {
const type = chunk.type;
// 使用固定的 key 来存储当前流的状态
const stateKey = 'openai_stream_current';
if (!this.streamParams.has(stateKey)) {
this.streamParams.set(stateKey, {
model: model,
createdAt: Math.floor(Date.now() / 1000),
responseID: chunk.response?.id || `chatcmpl-${Date.now()}`,
functionCallIndex: 0, // 初始值为 0第一个 function_call 的 index 为 0
isFirstChunk: true // 标记是否是第一个内容 chunk
});
}
const state = this.streamParams.get(stateKey);
// 构建模板时使用当前状态中的值
const buildTemplate = () => ({
id: state.responseID,
object: 'chat.completion.chunk',
created: state.createdAt,
model: state.model,
choices: [{
index: 0,
delta: {
role: 'assistant',
content: null,
reasoning_content: null,
tool_calls: null
},
finish_reason: null,
native_finish_reason: null
}]
});
if (type === 'response.created') {
// 更新状态中的 responseID
state.responseID = chunk.response.id;
state.createdAt = chunk.response.created_at || state.createdAt;
state.model = chunk.response.model || state.model;
// 重置 functionCallIndex确保每个新请求从 0 开始
state.functionCallIndex = 0;
state.isFirstChunk = true;
// response.created 不发送 chunk等待第一个内容 chunk
return null;
}
if (type === 'response.reasoning_summary_text.delta') {
const results = [];
// 如果是第一个内容 chunk先发送带 role 的 chunk
if (state.isFirstChunk) {
const firstTemplate = buildTemplate();
firstTemplate.choices[0].delta = {
role: 'assistant',
content: null,
reasoning_content: chunk.delta,
tool_calls: null
};
results.push(firstTemplate);
state.isFirstChunk = false;
} else {
const template = buildTemplate();
template.choices[0].delta = {
role: 'assistant',
content: null,
reasoning_content: chunk.delta,
tool_calls: null
};
results.push(template);
}
return results.length === 1 ? results[0] : results;
}
if (type === 'response.reasoning_summary_text.done') {
const template = buildTemplate();
template.choices[0].delta = {
role: 'assistant',
content: null,
reasoning_content: '\n\n',
tool_calls: null
};
return template;
}
if (type === 'response.output_text.delta') {
const results = [];
// 如果是第一个内容 chunk先发送带 role 的 chunk
if (state.isFirstChunk) {
const firstTemplate = buildTemplate();
firstTemplate.choices[0].delta = {
role: 'assistant',
content: chunk.delta,
reasoning_content: null,
tool_calls: null
};
results.push(firstTemplate);
state.isFirstChunk = false;
} else {
const template = buildTemplate();
template.choices[0].delta = {
role: 'assistant',
content: chunk.delta,
reasoning_content: null,
tool_calls: null
};
results.push(template);
}
return results.length === 1 ? results[0] : results;
}
if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
const currentIndex = state.functionCallIndex;
state.functionCallIndex++; // 递增,为下一个 function_call 准备
const template = buildTemplate();
template.choices[0].delta = {
role: 'assistant',
content: null,
reasoning_content: null,
tool_calls: [{
index: currentIndex,
id: chunk.item.call_id,
type: 'function',
function: {
name: this.getOriginalToolName(chunk.item.name),
arguments: typeof chunk.item.arguments === 'string' ? chunk.item.arguments : JSON.stringify(chunk.item.arguments)
}
}]
};
return template;
}
if (type === 'response.completed') {
const template = buildTemplate();
const finishReason = state.functionCallIndex > 0 ? 'tool_calls' : 'stop';
template.choices[0].delta = {
role: null,
content: null,
reasoning_content: null,
tool_calls: null
};
template.choices[0].finish_reason = finishReason;
template.choices[0].native_finish_reason = finishReason;
template.usage = {
prompt_tokens: chunk.response.usage?.input_tokens || 0,
completion_tokens: chunk.response.usage?.output_tokens || 0,
total_tokens: chunk.response.usage?.total_tokens || 0
};
if (chunk.response.usage?.output_tokens_details?.reasoning_tokens) {
template.usage.completion_tokens_details = {
reasoning_tokens: chunk.response.usage.output_tokens_details.reasoning_tokens
};
}
// 完成后清理状态
this.streamParams.delete(stateKey);
return template;
}
return null;
}
/**
* Codex → OpenAI Responses 流式响应转换
*/
toOpenAIResponsesStreamChunk(chunk, model) {
if(true){
return chunk;
}
const type = chunk.type;
const resId = chunk.response?.id || 'default';
if (!this.streamParams.has(resId)) {
this.streamParams.set(resId, {
model: model,
createdAt: Math.floor(Date.now() / 1000),
responseID: resId,
functionCallIndex: -1,
eventsSent: new Set()
});
}
const state = this.streamParams.get(resId);
const events = [];
if (type === 'response.created') {
state.responseID = chunk.response.id;
state.model = chunk.response.model || state.model;
events.push(
generateResponseCreated(state.responseID, state.model),
generateResponseInProgress(state.responseID)
);
return events;
}
if (type === 'response.reasoning_summary_text.delta') {
events.push({
type: "response.reasoning_summary_text.delta",
response_id: state.responseID,
delta: chunk.delta
});
return events;
}
if (type === 'response.output_text.delta') {
if (!state.eventsSent.has('output_item_added')) {
events.push(generateOutputItemAdded(state.responseID));
state.eventsSent.add('output_item_added');
}
if (!state.eventsSent.has('content_part_added')) {
events.push(generateContentPartAdded(state.responseID));
state.eventsSent.add('content_part_added');
}
events.push({
type: "response.output_text.delta",
response_id: state.responseID,
delta: chunk.delta
});
return events;
}
if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
events.push({
type: "response.output_item.added",
response_id: state.responseID,
item: {
id: chunk.item.call_id,
type: "function_call",
name: this.getOriginalToolName(chunk.item.name),
arguments: typeof chunk.item.arguments === 'string' ? chunk.item.arguments : JSON.stringify(chunk.item.arguments),
status: "completed"
}
});
events.push({
type: "response.output_item.done",
response_id: state.responseID,
item_id: chunk.item.call_id
});
return events;
}
if (type === 'response.completed') {
events.push(
generateOutputTextDone(state.responseID),
generateContentPartDone(state.responseID),
generateOutputItemDone(state.responseID)
);
const completedEvent = generateResponseCompleted(state.responseID);
completedEvent.response.usage = {
input_tokens: chunk.response.usage?.input_tokens || 0,
output_tokens: chunk.response.usage?.output_tokens || 0,
total_tokens: chunk.response.usage?.total_tokens || 0
};
events.push(completedEvent);
this.streamParams.delete(resId);
return events;
}
return null;
}
/**
* Codex → Gemini 流式响应转换
*/
toGeminiStreamChunk(chunk, model) {
const type = chunk.type;
const resId = chunk.response?.id || 'default';
if (!this.streamParams.has(resId)) {
this.streamParams.set(resId, {
model: model,
createdAt: Math.floor(Date.now() / 1000),
responseID: resId
});
}
const state = this.streamParams.get(resId);
const template = {
candidates: [{
content: {
role: "model",
parts: []
}
}],
modelVersion: state.model,
responseId: state.responseID
};
if (type === 'response.reasoning_summary_text.delta') {
template.candidates[0].content.parts.push({ text: chunk.delta, thought: true });
return template;
}
if (type === 'response.output_text.delta') {
template.candidates[0].content.parts.push({ text: chunk.delta });
return template;
}
if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
template.candidates[0].content.parts.push({
functionCall: {
name: this.getOriginalToolName(chunk.item.name),
args: typeof chunk.item.arguments === 'string' ? JSON.parse(chunk.item.arguments) : chunk.item.arguments
}
});
return template;
}
if (type === 'response.completed') {
template.candidates[0].finishReason = "STOP";
template.usageMetadata = {
promptTokenCount: chunk.response.usage?.input_tokens || 0,
candidatesTokenCount: chunk.response.usage?.output_tokens || 0,
totalTokenCount: chunk.response.usage?.total_tokens || 0
};
this.streamParams.delete(resId);
return template;
}
return null;
}
/**
* Codex → Claude 流式响应转换
*/
toClaudeStreamChunk(chunk, model, requestId) {
const type = chunk.type;
// 使用 requestId 作为流状态的隔离 key并发安全
// 每个请求在 handleStreamRequest 中生成唯一 requestId
// 确保同一单例 converter 上的并发流状态完全独立。
const stateKey = requestId || chunk.response?.id || 'default';
// response.created 携带 response.id用它来初始化该请求的流状态
if (type === 'response.created') {
const resId = chunk.response.id;
this.streamParams.set(stateKey, {
model: model,
createdAt: Math.floor(Date.now() / 1000),
responseID: resId,
blockIndex: 0,
blockStarted: false,
currentBlockType: null,
});
const state = this.streamParams.get(stateKey);
return {
type: "message_start",
message: {
id: state.responseID,
type: "message",
role: "assistant",
content: [],
model: state.model,
usage: { input_tokens: 0, output_tokens: 0 }
}
};
}
if (!this.streamParams.has(stateKey)) {
// 如果还没有状态(比如没有收到 response.created 就收到了其他事件),
// 用 chunk 中能拿到的信息初始化
this.streamParams.set(stateKey, {
model: model,
createdAt: Math.floor(Date.now() / 1000),
responseID: chunk.response?.id || stateKey,
blockIndex: 0,
blockStarted: false,
currentBlockType: null,
});
}
const state = this.streamParams.get(stateKey);
// response.output_item.added 不产生 Claude 输出
if (type === 'response.output_item.added') {
return null;
}
if (type === 'response.created') {
// 已在上方处理,不应到达此处
return null;
}
if (type === 'response.reasoning_summary_text.delta') {
const events = [];
// If switching from a different block type, close the previous block first
if (state.blockStarted && state.currentBlockType !== 'thinking') {
events.push({ type: "content_block_stop", index: state.blockIndex });
state.blockIndex++;
state.blockStarted = false;
}
// Emit content_block_start on first delta for this thinking block
if (!state.blockStarted) {
events.push({
type: "content_block_start",
index: state.blockIndex,
content_block: { type: "thinking", thinking: "" }
});
state.blockStarted = true;
state.currentBlockType = 'thinking';
}
events.push({
type: "content_block_delta",
index: state.blockIndex,
delta: { type: "thinking_delta", thinking: chunk.delta }
});
return events;
}
if (type === 'response.output_text.delta') {
const events = [];
// If switching from a different block type, close the previous block first
if (state.blockStarted && state.currentBlockType !== 'text') {
events.push({ type: "content_block_stop", index: state.blockIndex });
state.blockIndex++;
state.blockStarted = false;
}
// Emit content_block_start on first delta for this text block
if (!state.blockStarted) {
events.push({
type: "content_block_start",
index: state.blockIndex,
content_block: { type: "text", text: "" }
});
state.blockStarted = true;
state.currentBlockType = 'text';
}
events.push({
type: "content_block_delta",
index: state.blockIndex,
delta: { type: "text_delta", text: chunk.delta }
});
return events;
}
if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
const events = [];
// Close any open text/thinking block before tool_use
if (state.blockStarted) {
events.push({ type: "content_block_stop", index: state.blockIndex });
state.blockIndex++;
state.blockStarted = false;
state.currentBlockType = null;
}
events.push(
{
type: "content_block_start",
index: state.blockIndex,
content_block: {
type: "tool_use",
id: chunk.item.call_id,
name: this.getOriginalToolName(chunk.item.name),
input: {}
}
},
{
type: "content_block_delta",
index: state.blockIndex,
delta: {
type: "input_json_delta",
partial_json: typeof chunk.item.arguments === 'string' ? chunk.item.arguments : JSON.stringify(chunk.item.arguments)
}
},
{
type: "content_block_stop",
index: state.blockIndex
}
);
state.blockIndex++;
return events;
}
if (type === 'response.completed') {
const events = [];
// Close any open content block before ending the message
if (state.blockStarted) {
events.push({ type: "content_block_stop", index: state.blockIndex });
}
events.push(
{
type: "message_delta",
delta: { stop_reason: "end_turn" },
usage: {
input_tokens: chunk.response.usage?.input_tokens || 0,
output_tokens: chunk.response.usage?.output_tokens || 0
}
},
{ type: "message_stop" }
);
// 清理该请求的流状态
this.streamParams.delete(stateKey);
return events;
}
return null;
}
}