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,
|
id: codexChunk.response.id,
|
||||||
type: "message",
|
type: "message",
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
model: model,
|
model: model,
|
||||||
usage: { input_tokens: 0, output_tokens: 0 }
|
usage: { input_tokens: 0, output_tokens: 0 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1133,7 +1133,9 @@ export class CodexConverter extends BaseConverter {
|
||||||
model: model,
|
model: model,
|
||||||
createdAt: Math.floor(Date.now() / 1000),
|
createdAt: Math.floor(Date.now() / 1000),
|
||||||
responseID: resId,
|
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);
|
const state = this.streamParams.get(resId);
|
||||||
|
|
@ -1146,6 +1148,7 @@ export class CodexConverter extends BaseConverter {
|
||||||
id: state.responseID,
|
id: state.responseID,
|
||||||
type: "message",
|
type: "message",
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
|
content: [],
|
||||||
model: state.model,
|
model: state.model,
|
||||||
usage: { input_tokens: 0, output_tokens: 0 }
|
usage: { input_tokens: 0, output_tokens: 0 }
|
||||||
}
|
}
|
||||||
|
|
@ -1153,23 +1156,67 @@ export class CodexConverter extends BaseConverter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'response.reasoning_summary_text.delta') {
|
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",
|
type: "content_block_delta",
|
||||||
index: state.blockIndex,
|
index: state.blockIndex,
|
||||||
delta: { type: "thinking_delta", thinking: chunk.delta }
|
delta: { type: "thinking_delta", thinking: chunk.delta }
|
||||||
};
|
});
|
||||||
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'response.output_text.delta') {
|
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",
|
type: "content_block_delta",
|
||||||
index: state.blockIndex,
|
index: state.blockIndex,
|
||||||
delta: { type: "text_delta", text: chunk.delta }
|
delta: { type: "text_delta", text: chunk.delta }
|
||||||
};
|
});
|
||||||
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'response.output_item.done' && chunk.item?.type === 'function_call') {
|
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",
|
type: "content_block_start",
|
||||||
index: state.blockIndex,
|
index: state.blockIndex,
|
||||||
|
|
@ -1192,13 +1239,18 @@ export class CodexConverter extends BaseConverter {
|
||||||
type: "content_block_stop",
|
type: "content_block_stop",
|
||||||
index: state.blockIndex
|
index: state.blockIndex
|
||||||
}
|
}
|
||||||
];
|
);
|
||||||
state.blockIndex++;
|
state.blockIndex++;
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'response.completed') {
|
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",
|
type: "message_delta",
|
||||||
delta: { stop_reason: "end_turn" },
|
delta: { stop_reason: "end_turn" },
|
||||||
|
|
@ -1208,7 +1260,7 @@ export class CodexConverter extends BaseConverter {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ type: "message_stop" }
|
{ type: "message_stop" }
|
||||||
];
|
);
|
||||||
this.streamParams.delete(resId);
|
this.streamParams.delete(resId);
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue