From 595230177846b2182cef0dd834fc73661d2416c5 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Fri, 7 Aug 2026 20:12:37 +0900 Subject: [PATCH] fix: avoid spreading hosted-tool indices --- src/adapters/openai-responses.ts | 11 ++++----- tests/openai-responses-passthrough.test.ts | 28 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/adapters/openai-responses.ts b/src/adapters/openai-responses.ts index 5ed43ba381..8201644b12 100644 --- a/src/adapters/openai-responses.ts +++ b/src/adapters/openai-responses.ts @@ -705,14 +705,14 @@ function preferConfiguredHostedTools( } let input = body.input; - const strippedAdditionalToolsIndices = new Set(); + let firstStrippedAdditionalToolsIndex: number | undefined; if (Array.isArray(body.input)) { let nestedChanged = false; const mappedInput = body.input.map((item, index) => { if (!isPlainObject(item) || item.type !== "additional_tools" || !Array.isArray(item.tools)) return item; const nestedTools = stripGroup(item.tools); if (nestedTools === item.tools) return item; - strippedAdditionalToolsIndices.add(index); + firstStrippedAdditionalToolsIndex ??= index; nestedChanged = true; return { ...item, tools: nestedTools }; }); @@ -731,17 +731,16 @@ function preferConfiguredHostedTools( || (Array.isArray(input) && input.some(item => isPlainObject(item) && item.type === "additional_tools" && hasHostedImageGenTool(item.tools))); - if ((strippedTopLevelImageGenTool || strippedAdditionalToolsIndices.size > 0) && !hasHostedImageGenDeclaration) { + if ((strippedTopLevelImageGenTool || firstStrippedAdditionalToolsIndex !== undefined) && !hasHostedImageGenDeclaration) { if (strippedTopLevelImageGenTool && Array.isArray(tools)) { tools = [...tools, { type: HOSTED_IMAGE_GENERATION_TOOL }]; - } else if (strippedAdditionalToolsIndices.size > 0 && Array.isArray(input)) { + } else if (firstStrippedAdditionalToolsIndex !== undefined && Array.isArray(input)) { // Restore into the FIRST stripped container only. Tool declarations are // request-scoped, not container-scoped — the containers are separate carriers for // one tool set, so a single hosted declaration covers the request. An earlier // revision restored into every stripped container and put `image_generation` on // the wire twice; review caught it. - const firstStripped = Math.min(...strippedAdditionalToolsIndices); - input = input.map((item, index) => index === firstStripped + input = input.map((item, index) => index === firstStrippedAdditionalToolsIndex && isPlainObject(item) && Array.isArray(item.tools) ? { ...item, tools: [...item.tools, { type: HOSTED_IMAGE_GENERATION_TOOL }] } diff --git a/tests/openai-responses-passthrough.test.ts b/tests/openai-responses-passthrough.test.ts index 59f58237ba..0f52b9a3a7 100644 --- a/tests/openai-responses-passthrough.test.ts +++ b/tests/openai-responses-passthrough.test.ts @@ -1223,6 +1223,34 @@ describe("OpenAI Responses hosted-tool name conflicts", () => { expect(containers[0].tools).toContainEqual({ type: "image_generation" }); }); + test("additional_tools restoration does not spread stripped indices into Math.min", () => { + const adapter = createResponsesPassthroughAdapter({ + ...keyedProvider, + modelPreferHostedTools: { "provider-image-model": ["image_generation"] }, + }); + const input = Array.from({ length: 3 }, () => ({ + type: "additional_tools", + tools: [{ type: "namespace", name: "image_gen", tools: [] }], + })); + const originalMin = Math.min; + Math.min = (...values: number[]) => { + if (values.length > 2) throw new RangeError("too many arguments"); + return originalMin(...values); + }; + + try { + expect(() => adapter.buildRequest({ + modelId: "provider-image-model", + context: { messages: [] }, + stream: true, + options: {}, + _rawBody: { model: "provider-image-model", input }, + }, meta)).not.toThrow(); + } finally { + Math.min = originalMin; + } + }); + test("configured model rewrites a custom image-gen selector", () => { const adapter = createResponsesPassthroughAdapter({ ...keyedProvider,