diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index a8bbab8bfd..17c0d2dc51 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -702,8 +702,18 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthes }; const reorderBatchOutputs = (items: unknown[]): unknown[] => { const ordered: unknown[] = []; + const claimedOutputIndexes = new Set(); + const outputIndexesByKey = new Map(); + for (let outputIndex = 0; outputIndex < items.length; outputIndex += 1) { + const outputKey = outputKeyOf(items[outputIndex]); + if (outputKey === null) continue; + const indexes = outputIndexesByKey.get(outputKey); + if (indexes) indexes.push(outputIndex); + else outputIndexesByKey.set(outputKey, [outputIndex]); + } let index = 0; while (index < items.length) { + if (claimedOutputIndexes.has(index)) { index += 1; continue; } const key = callKeyOf(items[index]); if (key === null) { ordered.push(items[index]); index += 1; continue; } const batch: unknown[] = []; @@ -722,20 +732,19 @@ function repairOrphanedInputItems(body: unknown, dropReasoning: boolean, synthes index = cursor; continue; } - const remainder: unknown[] = []; - const batchOutputs: Array<{ key: string; item: unknown }> = []; - for (let probe = cursor; probe < items.length; probe += 1) { - const outputKey = outputKeyOf(items[probe]); - if (outputKey !== null && batchKeys.includes(outputKey)) { - batchOutputs.push({ key: outputKey, item: items[probe] }); - } else { - remainder.push(items[probe]); + const batchOutputs: unknown[] = []; + const emittedKeys = new Set(); + for (const batchKey of batchKeys) { + if (emittedKeys.has(batchKey)) continue; + emittedKeys.add(batchKey); + for (const outputIndex of outputIndexesByKey.get(batchKey) ?? []) { + if (outputIndex < cursor || claimedOutputIndexes.has(outputIndex)) continue; + claimedOutputIndexes.add(outputIndex); + batchOutputs.push(items[outputIndex]); } } - batchOutputs.sort((left, right) => batchKeys.indexOf(left.key) - batchKeys.indexOf(right.key)); - ordered.push(...batch, ...batchOutputs.map(output => output.item)); - ordered.push(...reorderBatchOutputs(remainder)); - return ordered; + ordered.push(...batch, ...batchOutputs); + index = cursor; } return ordered; }; diff --git a/tests/responses-stateless-dangling-call-repair.test.ts b/tests/responses-stateless-dangling-call-repair.test.ts index 8ef39259d3..965aefda29 100644 --- a/tests/responses-stateless-dangling-call-repair.test.ts +++ b/tests/responses-stateless-dangling-call-repair.test.ts @@ -125,6 +125,23 @@ describe("stateless Responses wire repairs orphaned tool calls", () => { expect(String((input[4] as { output: unknown }).output)).toContain("no tool result was recorded"); }); + test("repairs many separated dangling calls without recursive reprocessing", async () => { + const callCount = 2_000; + const requestInput = Array.from({ length: callCount }, (_, index) => [ + { type: "function_call", id: `fc_${index}`, call_id: `call_${index}`, name: "exec_command", arguments: "{}" }, + { type: "message", role: "user", content: [{ type: "input_text", text: `separator ${index}` }] }, + ]).flat(); + + const { body } = await drive(requestInput); + const input = body.input as Array>; + expect(input).toHaveLength(callCount * 3); + expect(input[0]).toMatchObject({ type: "function_call", call_id: "call_0" }); + expect(input[1]).toMatchObject({ type: "function_call_output", call_id: "call_0" }); + expect(input.at(-3)).toMatchObject({ type: "function_call", call_id: `call_${callCount - 1}` }); + expect(input.at(-2)).toMatchObject({ type: "function_call_output", call_id: `call_${callCount - 1}` }); + expect(input.at(-1)).toMatchObject({ type: "message" }); + }); + test("leaves intact call/output pairs untouched", async () => { const { body } = await drive([ { type: "function_call", id: "fc_ok", call_id: "call_ok", name: "exec_command", arguments: "{}" },