diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 456417a4d69..7ca49eb5a99 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -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" ) { diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index a1caa3361bf..5719995dd68 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -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