// translate.js —— Responses API ⇄ Chat Completions 协议转换。 // 让只支持 chat/completions 的中转(DeepSeek/GLM/Kimi/通义等)也能给 Codex(用 responses)用。 // 参考实现:sybil-solutions/codex-shim、BerriAI/litellm。 const crypto = require("crypto"); function genId(prefix) { return prefix + Date.now().toString(36) + crypto.randomBytes(4).toString("hex"); } // content parts / string → 纯文本 function partsToText(content) { if (typeof content === "string") return content; if (Array.isArray(content)) { return content .map((p) => { if (typeof p === "string") return p; if (p && (p.type === "input_text" || p.type === "output_text" || p.type === "text")) return p.text || ""; return ""; }) .join(""); } return ""; } // Responses 的 tool(扁平)→ Chat Completions 的 tool(嵌套在 function 下) function toChatTool(t) { if (!t) return null; if (t.type === "function" && t.function) return t; // 已是嵌套 if (t.type === "function" || t.name) { return { type: "function", function: { name: t.name, description: t.description || "", parameters: t.parameters || { type: "object", properties: {} }, }, }; } return null; // 托管型工具(web_search 等)无 chat 等价,先跳过 } function toChatToolChoice(tc) { if (typeof tc === "string") return tc; if (tc && tc.type === "function" && tc.name) return { type: "function", function: { name: tc.name } }; return tc; } // —— 请求:Responses → Chat Completions —— function responsesToChat(req, overrideModel) { const messages = []; if (req.instructions) messages.push({ role: "system", content: req.instructions }); const pending = []; const flush = () => { if (pending.length) { messages.push({ role: "assistant", content: null, tool_calls: pending.splice(0) }); } }; const input = req.input; if (typeof input === "string") { messages.push({ role: "user", content: input }); } else if (Array.isArray(input)) { for (const item of input) { const t = item.type; if (t === "message" || (item.role && !t)) { flush(); const role = item.role === "developer" ? "system" : item.role; messages.push({ role, content: partsToText(item.content) }); } else if (t === "function_call") { const callId = item.call_id || item.id || "call_0"; pending.push({ id: callId, type: "function", function: { name: item.name || "", arguments: item.arguments || "" } }); } else if (t === "function_call_output") { flush(); const out = typeof item.output === "string" ? item.output : JSON.stringify(item.output); messages.push({ role: "tool", tool_call_id: item.call_id, content: out }); } // reasoning item → 丢弃(chat 上游用不到) } } flush(); const out = { model: overrideModel || req.model, messages, stream: req.stream !== false }; if (Array.isArray(req.tools)) { const tools = req.tools.map(toChatTool).filter(Boolean); if (tools.length) out.tools = tools; } if (req.tool_choice) out.tool_choice = toChatToolChoice(req.tool_choice); if (req.max_output_tokens) out.max_tokens = req.max_output_tokens; if (req.temperature != null) out.temperature = req.temperature; if (req.top_p != null) out.top_p = req.top_p; if (req.parallel_tool_calls != null) out.parallel_tool_calls = req.parallel_tool_calls; const eff = req.reasoning && req.reasoning.effort; if (eff) out.reasoning_effort = eff; if (out.stream) out.stream_options = { include_usage: true }; return out; } function mapUsage(u) { if (!u) return null; return { input_tokens: u.prompt_tokens || 0, input_tokens_details: { cached_tokens: (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || 0 }, output_tokens: u.completion_tokens || 0, output_tokens_details: { reasoning_tokens: 0 }, total_tokens: u.total_tokens || 0, }; } // —— 响应:Chat Completions 流 → Responses 流事件(有状态转换器)—— class ChatToResponses { constructor(model) { this.model = model; this.responseId = genId("resp_"); this.msgId = genId("msg_"); this.seq = 0; this.outIndex = 0; this.msgOpen = false; this.msgClosed = false; this.msgText = ""; this.msgOutputIndex = null; this.tools = {}; // index → {id,name,arguments,outputIndex,closed} this.usage = null; this.started = false; // 推理模型(DeepSeek-R1/v4、GLM、Kimi)的思考内容 this.reasonOpen = false; this.reasonClosed = false; this.reasonText = ""; this.reasonId = null; this.reasonOutputIndex = null; } openReason() { if (this.reasonOpen) return ""; this.reasonOpen = true; this.reasonId = genId("rs_"); this.reasonOutputIndex = this.outIndex++; let s = this.ev("response.output_item.added", { output_index: this.reasonOutputIndex, item: { id: this.reasonId, type: "reasoning", summary: [] }, }); s += this.ev("response.reasoning_summary_part.added", { item_id: this.reasonId, output_index: this.reasonOutputIndex, summary_index: 0, part: { type: "summary_text", text: "" }, }); return s; } closeReason() { if (!this.reasonOpen || this.reasonClosed) return ""; this.reasonClosed = true; let s = this.ev("response.reasoning_summary_text.done", { item_id: this.reasonId, output_index: this.reasonOutputIndex, summary_index: 0, text: this.reasonText, }); s += this.ev("response.reasoning_summary_part.done", { item_id: this.reasonId, output_index: this.reasonOutputIndex, summary_index: 0, part: { type: "summary_text", text: this.reasonText }, }); s += this.ev("response.output_item.done", { output_index: this.reasonOutputIndex, item: { id: this.reasonId, type: "reasoning", summary: [{ type: "summary_text", text: this.reasonText }] }, }); return s; } ev(type, obj) { const payload = Object.assign({ type, sequence_number: this.seq++ }, obj); return `event: ${type}\ndata: ${JSON.stringify(payload)}\n\n`; } baseResponse(status, output) { return { id: this.responseId, object: "response", created_at: Math.floor(Date.now() / 1000), status, error: null, incomplete_details: null, instructions: null, model: this.model, output, parallel_tool_calls: true, previous_response_id: null, reasoning: { effort: null, summary: null }, store: false, temperature: 1.0, text: { format: { type: "text" } }, tool_choice: "auto", tools: [], top_p: 1.0, truncation: "disabled", usage: this.usage, metadata: {}, }; } start() { const r = this.baseResponse("in_progress", []); return this.ev("response.created", { response: r }) + this.ev("response.in_progress", { response: r }); } openMsg() { if (this.msgOpen) return ""; this.msgOpen = true; this.msgOutputIndex = this.outIndex++; let s = this.ev("response.output_item.added", { output_index: this.msgOutputIndex, item: { id: this.msgId, type: "message", status: "in_progress", role: "assistant", content: [] }, }); s += this.ev("response.content_part.added", { item_id: this.msgId, output_index: this.msgOutputIndex, content_index: 0, part: { type: "output_text", text: "", annotations: [] }, }); return s; } closeMsg() { if (!this.msgOpen || this.msgClosed) return ""; this.msgClosed = true; let s = this.ev("response.output_text.done", { item_id: this.msgId, output_index: this.msgOutputIndex, content_index: 0, text: this.msgText, logprobs: [], }); s += this.ev("response.content_part.done", { item_id: this.msgId, output_index: this.msgOutputIndex, content_index: 0, part: { type: "output_text", text: this.msgText, annotations: [] }, }); s += this.ev("response.output_item.done", { output_index: this.msgOutputIndex, item: { id: this.msgId, type: "message", status: "completed", role: "assistant", content: [{ type: "output_text", text: this.msgText, annotations: [] }], }, }); return s; } onToolDelta(tc) { let out = ""; const index = tc.index != null ? tc.index : 0; const fn = tc.function || {}; let st = this.tools[index]; if (!st) { out += this.closeReason(); // 先关推理 out += this.closeMsg(); // 再关文本消息,才开工具项 const callId = tc.id || "call_" + index; st = { id: callId, name: fn.name || "", arguments: "", outputIndex: this.outIndex++, closed: false }; this.tools[index] = st; out += this.ev("response.output_item.added", { output_index: st.outputIndex, item: { id: st.id, type: "function_call", status: "in_progress", call_id: st.id, name: st.name, arguments: "" }, }); } else if (fn.name) { st.name += fn.name; } if (fn.arguments) { st.arguments += fn.arguments; out += this.ev("response.function_call_arguments.delta", { item_id: st.id, output_index: st.outputIndex, delta: fn.arguments, }); } return out; } closeTool(st) { if (st.closed) return ""; st.closed = true; let s = this.ev("response.function_call_arguments.done", { item_id: st.id, output_index: st.outputIndex, arguments: st.arguments, }); s += this.ev("response.output_item.done", { output_index: st.outputIndex, item: { id: st.id, type: "function_call", status: "completed", call_id: st.id, name: st.name, arguments: st.arguments }, }); return s; } // 喂一个 chat 流 chunk(已解析的 JSON),返回要写给 Codex 的 Responses 事件串 onChunk(obj) { let out = ""; if (!this.started) { out += this.start(); this.started = true; } if (obj.usage) this.usage = mapUsage(obj.usage); const choice = obj.choices && obj.choices[0]; if (!choice) return out; const delta = choice.delta || {}; // 推理内容(reasoning_content)→ 思考事件 const rc = delta.reasoning_content || delta.reasoning; if (rc) { out += this.openReason(); this.reasonText += rc; out += this.ev("response.reasoning_summary_text.delta", { item_id: this.reasonId, output_index: this.reasonOutputIndex, summary_index: 0, delta: rc, }); } if (delta.content) { out += this.closeReason(); // 有正文了,先收尾推理 out += this.openMsg(); this.msgText += delta.content; out += this.ev("response.output_text.delta", { item_id: this.msgId, output_index: this.msgOutputIndex, content_index: 0, delta: delta.content, logprobs: [], }); } if (Array.isArray(delta.tool_calls)) { for (const tc of delta.tool_calls) out += this.onToolDelta(tc); } return out; } finish() { let out = ""; if (!this.started) { out += this.start(); this.started = true; } out += this.closeReason(); out += this.closeMsg(); const sts = Object.values(this.tools).sort((a, b) => a.outputIndex - b.outputIndex); for (const st of sts) out += this.closeTool(st); const output = []; if (this.reasonOpen) output.push({ id: this.reasonId, type: "reasoning", summary: [{ type: "summary_text", text: this.reasonText }] }); if (this.msgOpen) output.push({ id: this.msgId, type: "message", status: "completed", role: "assistant", content: [{ type: "output_text", text: this.msgText, annotations: [] }], }); for (const st of sts) output.push({ id: st.id, type: "function_call", status: "completed", call_id: st.id, name: st.name, arguments: st.arguments }); out += this.ev("response.completed", { response: this.baseResponse("completed", output) }); return out; } } module.exports = { responsesToChat, ChatToResponses };