fix: 补齐 Claude 流式响应的 content_block_start/stop 事件序列
修复通过 /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 流式转换逻辑
This commit is contained in:
parent
694f0c9099
commit
5d8ad5a925
2 changed files with 62 additions and 9 deletions
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue