diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index f033a0240a..b85252b142 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -725,8 +725,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 bucket = outputIndexesByKey.get(outputKey); + if (bucket) bucket.indexes.push(outputIndex); + else outputIndexesByKey.set(outputKey, { indexes: [outputIndex], offset: 0 }); + } 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[] = []; @@ -745,20 +755,24 @@ 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[] = []; + for (const batchKey of batchKeys) { + const bucket = outputIndexesByKey.get(batchKey); + if (!bucket) continue; + while (bucket.offset < bucket.indexes.length && bucket.indexes[bucket.offset]! < cursor) { + bucket.offset += 1; + } + while (bucket.offset < bucket.indexes.length) { + const outputIndex = bucket.indexes[bucket.offset]!; + bucket.offset += 1; + if (claimedOutputIndexes.has(outputIndex)) continue; + claimedOutputIndexes.add(outputIndex); + batchOutputs.push(items[outputIndex]); + break; } } - 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..ede2a8d119 100644 --- a/tests/responses-stateless-dangling-call-repair.test.ts +++ b/tests/responses-stateless-dangling-call-repair.test.ts @@ -125,6 +125,45 @@ 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 = 20_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("consumes repeated output-key indexes once without dropping repaired items", async () => { + const callCount = 2_000; + const requestInput = Array.from({ length: callCount }, (_, index) => [ + { type: "function_call", id: `fc_repeat_${index}`, call_id: "call_repeat", 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.filter(item => item.type === "function_call")).toHaveLength(callCount); + expect(input.filter(item => item.type === "function_call_output")).toHaveLength(callCount); + expect(input.filter(item => item.type === "message")).toHaveLength(callCount); + for (let index = 0; index < callCount; index += 1) { + expect(input.slice(index * 3, index * 3 + 3)).toMatchObject([ + { type: "function_call", call_id: "call_repeat" }, + { type: "function_call_output", call_id: "call_repeat" }, + { type: "message", content: [{ type: "input_text", text: `separator ${index}` }] }, + ]); + } + }); + 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: "{}" },