Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/adapters/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,14 +705,14 @@ function preferConfiguredHostedTools(
}

let input = body.input;
const strippedAdditionalToolsIndices = new Set<number>();
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 };
});
Expand All @@ -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 }] }
Expand Down
28 changes: 28 additions & 0 deletions tests/openai-responses-passthrough.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Comment on lines +1241 to +1248

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the restored payload, not only request construction.

This test can pass if the adapter strips all three client declarations and restores none, or restores image_generation to the wrong container. Capture the built request and assert that this input restores exactly one hosted declaration in the first stripped additional_tools container. Assert that the second and third containers do not receive a duplicate.

As per path instructions, the adapter behavior change must have a focused regression test that checks the changed behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/openai-responses-passthrough.test.ts` around lines 1241 - 1248, Update
the test around adapter.buildRequest to capture the returned request, then
assert the restored payload contains exactly one hosted image_generation
declaration in the first stripped additional_tools container. Also assert the
second and third additional_tools containers do not contain a duplicate, while
preserving the existing no-throw check.

Source: Path instructions

} finally {
Math.min = originalMin;
}
});

test("configured model rewrites a custom image-gen selector", () => {
const adapter = createResponsesPassthroughAdapter({
...keyedProvider,
Expand Down
Loading