fix(openai): prevent double emission of text/reasoning in native and codex handlers (#10888)

This commit is contained in:
Hannes Rudolph 2026-01-21 23:22:29 -07:00 committed by GitHub
parent 21bd7609a9
commit 1feefb6f43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 36 additions and 20 deletions

View file

@ -908,17 +908,25 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion
}
}
if (item.type === "text" && item.text) {
yield { type: "text", text: item.text }
} else if (item.type === "reasoning" && item.text) {
yield { type: "reasoning", text: item.text }
} else if (item.type === "message" && Array.isArray(item.content)) {
for (const content of item.content) {
if ((content?.type === "text" || content?.type === "output_text") && content?.text) {
yield { type: "text", text: content.text }
// For "added" events, yield text/reasoning content (streaming path)
// For "done" events, do NOT yield text/reasoning - it's already been streamed via deltas
// and would cause double-emission (A, B, C, ABC).
if (event.type === "response.output_item.added") {
if (item.type === "text" && item.text) {
yield { type: "text", text: item.text }
} else if (item.type === "reasoning" && item.text) {
yield { type: "reasoning", text: item.text }
} else if (item.type === "message" && Array.isArray(item.content)) {
for (const content of item.content) {
if ((content?.type === "text" || content?.type === "output_text") && content?.text) {
yield { type: "text", text: content.text }
}
}
}
} else if (
}
// Only handle tool/function calls from done events (to ensure arguments are complete)
if (
(item.type === "function_call" || item.type === "tool_call") &&
event.type === "response.output_item.done"
) {

View file

@ -1223,20 +1223,28 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio
}
}
if (item.type === "text" && item.text) {
yield { type: "text", text: item.text }
} else if (item.type === "reasoning" && item.text) {
yield { type: "reasoning", text: item.text }
} else if (item.type === "message" && Array.isArray(item.content)) {
for (const content of item.content) {
// Some implementations send 'text'; others send 'output_text'
if ((content?.type === "text" || content?.type === "output_text") && content?.text) {
yield { type: "text", text: content.text }
// For "added" events, yield text/reasoning content (streaming path)
// For "done" events, do NOT yield text/reasoning - it's already been streamed via deltas
// and would cause double-emission (A, B, C, ABC).
if (event.type === "response.output_item.added") {
if (item.type === "text" && item.text) {
yield { type: "text", text: item.text }
} else if (item.type === "reasoning" && item.text) {
yield { type: "reasoning", text: item.text }
} else if (item.type === "message" && Array.isArray(item.content)) {
for (const content of item.content) {
// Some implementations send 'text'; others send 'output_text'
if ((content?.type === "text" || content?.type === "output_text") && content?.text) {
yield { type: "text", text: content.text }
}
}
}
} else if (
}
// Only handle tool/function calls from done events (to ensure arguments are complete)
if (
(item.type === "function_call" || item.type === "tool_call") &&
event.type === "response.output_item.done" // Only handle done events for tool calls to ensure arguments are complete
event.type === "response.output_item.done"
) {
// Handle complete tool/function call item
// Emit as tool_call for backward compatibility with non-streaming tool handling