From 5d8ad5a92545a069cceeceb1b671dd5fd90b318d Mon Sep 17 00:00:00 2001 From: Cishoon Date: Wed, 4 Mar 2026 14:40:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A1=A5=E9=BD=90=20Claude=20=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E5=93=8D=E5=BA=94=E7=9A=84=20content=5Fblock=5Fstart/?= =?UTF-8?q?stop=20=E4=BA=8B=E4=BB=B6=E5=BA=8F=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复通过 /v1/messages 对接 Claude 客户端时「长时间无输出,最后一次性出现大量内容」的问题。 问题原因: - CodexConverter 在文本增量场景只发送了 content_block_delta, 缺少对应文本块的 content_block_start 和 content_block_stop 事件 - Claude 兼容客户端无法将 delta 识别为可渲染增量 修复内容: - 为 text/thinking 输出分支补齐完整的 Claude SSE 事件序列: 首个 delta 前发送 content_block_start,块结束时发送 content_block_stop - 新增 blockStarted/currentBlockType 状态跟踪,正确处理块类型切换 - 在 tool_use 和 response.completed 前关闭已打开的内容块 - message_start 事件补充 content: [] 字段 影响范围:CodexConverter、ClaudeConverter 流式转换逻辑 --- src/converters/strategies/ClaudeConverter.js | 1 + src/converters/strategies/CodexConverter.js | 70 +++++++++++++++++--- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/converters/strategies/ClaudeConverter.js b/src/converters/strategies/ClaudeConverter.js index e4c07b9..b9bc5fc 100644 --- a/src/converters/strategies/ClaudeConverter.js +++ b/src/converters/strategies/ClaudeConverter.js @@ -2163,6 +2163,7 @@ export class ClaudeConverter extends BaseConverter { id: codexChunk.response.id, type: "message", role: "assistant", + content: [], model: model, usage: { input_tokens: 0, output_tokens: 0 } } diff --git a/src/converters/strategies/CodexConverter.js b/src/converters/strategies/CodexConverter.js index 31ad6be..23e49c0 100644 --- a/src/converters/strategies/CodexConverter.js +++ b/src/converters/strategies/CodexConverter.js @@ -1133,7 +1133,9 @@ export class CodexConverter extends BaseConverter { model: model, createdAt: Math.floor(Date.now() / 1000), responseID: resId, - blockIndex: 0 + blockIndex: 0, + blockStarted: false, // track whether content_block_start has been sent for current block + currentBlockType: null // 'thinking' or 'text' }); } const state = this.streamParams.get(resId); @@ -1146,6 +1148,7 @@ export class CodexConverter extends BaseConverter { id: state.responseID, type: "message", role: "assistant", + content: [], model: state.model, usage: { input_tokens: 0, output_tokens: 0 } } @@ -1153,23 +1156,67 @@ export class CodexConverter extends BaseConverter { } if (type === 'response.reasoning_summary_text.delta') { - return { + 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') { - return { + 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 = [ + 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, @@ -1192,13 +1239,18 @@ export class CodexConverter extends BaseConverter { type: "content_block_stop", index: state.blockIndex } - ]; + ); state.blockIndex++; return events; } if (type === 'response.completed') { - const events = [ + 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" }, @@ -1208,7 +1260,7 @@ export class CodexConverter extends BaseConverter { } }, { type: "message_stop" } - ]; + ); this.streamParams.delete(resId); return events; }