fix(translator): drop Codex image_generation tool in Responses→Chat (#2950)

Codex Desktop injects an image_generation hosted tool into every Responses API
request (even text-only ones). The tool-type validator threw
unsupportedFeature() (400) for it, breaking every Codex Desktop request.

Mirror the tool_search handling: add IMAGE_GENERATION_TOOL_TYPES, allow it past
the validator guard, and drop it from the tools array before forwarding to Chat
Completions.

Closes #2950
This commit is contained in:
diegosouzapw
2026-05-31 01:02:38 -03:00
parent bc6332310d
commit b5d03ed3f2
3 changed files with 64 additions and 2 deletions

View File

@@ -78,6 +78,11 @@
`claude-opus-4.6`) instead of the Responses API, which Copilot does not serve for
non-OpenAI models (returned `[400]`). Native OpenAI `gpt-*` models keep the
Responses API. (#2911)
- **translator/responses:** Codex Desktop injects an `image_generation` hosted
tool into every Responses API request (even text-only ones), which OmniRoute
rejected with `[400] image_generation tool type is not supported`. It is now
treated like `tool_search`: allowed past the tool-type validator and dropped
silently from the tools array before forwarding to Chat Completions. (#2950)
### ✨ New Features

View File

@@ -18,6 +18,9 @@ const WEB_SEARCH_TOOL_TYPES = /^web_search/;
// tool_search is a Responses API built-in sent by newer Codex clients; it has no Chat Completions
// equivalent and must be silently dropped (not rejected with 400).
const TOOL_SEARCH_TOOL_TYPES = /^tool_search/;
// image_generation is a Responses API hosted tool that Codex Desktop injects into every request
// (even text-only ones); it has no Chat Completions equivalent and must be silently dropped (#2950).
const IMAGE_GENERATION_TOOL_TYPES = /^image_generation/;
function toRecord(value: unknown): JsonRecord {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
@@ -85,6 +88,7 @@ export function openaiResponsesToOpenAIRequest(
toolType !== "namespace" &&
!WEB_SEARCH_TOOL_TYPES.test(toolType) &&
!TOOL_SEARCH_TOOL_TYPES.test(toolType) &&
!IMAGE_GENERATION_TOOL_TYPES.test(toolType) &&
!tool.function
) {
throw unsupportedFeature(
@@ -267,8 +271,11 @@ export function openaiResponsesToOpenAIRequest(
.filter((toolValue) => {
const tool = toRecord(toolValue);
const toolType = toString(tool.type);
// tool_search has no Chat Completions equivalent; drop it silently (issue #2766).
return !TOOL_SEARCH_TOOL_TYPES.test(toolType);
// tool_search (#2766) and image_generation (#2950) are Responses API built-ins
// with no Chat Completions equivalent; drop them silently.
return (
!TOOL_SEARCH_TOOL_TYPES.test(toolType) && !IMAGE_GENERATION_TOOL_TYPES.test(toolType)
);
})
.map((toolValue) => {
const tool = toRecord(toolValue);

View File

@@ -747,3 +747,53 @@ test("Responses -> Chat: tool_search is stripped from output tools array (issue
assert.equal(tools[0].type, "function");
assert.equal(tools[0].function.name, "foo");
});
// --- Issue #2950: image_generation built-in should be silently dropped ---
test("Responses -> Chat: image_generation does not throw (issue #2950)", () => {
// Codex Desktop injects an image_generation hosted tool into every Responses
// request, even text-only ones. It has no Chat Completions equivalent and must
// be dropped silently, not rejected with 400.
assert.doesNotThrow(() =>
openaiResponsesToOpenAIRequest(
"gpt-4o",
{
input: [{ role: "user", content: [{ type: "input_text", text: "hi" }] }],
tools: [{ type: "image_generation", output_format: "png" }],
},
false,
null
)
);
});
test("Responses -> Chat: image_generation is stripped from output tools array (issue #2950)", () => {
const result = openaiResponsesToOpenAIRequest(
"gpt-4o",
{
input: [{ role: "user", content: [{ type: "input_text", text: "hello" }] }],
tools: [
{ type: "image_generation", output_format: "png" },
{
type: "function",
name: "foo",
description: "A function",
parameters: { type: "object" },
},
],
},
false,
null
) as Record<string, unknown>;
const tools = result.tools as any[];
assert.ok(Array.isArray(tools), "tools array must be present");
assert.equal(
tools.some((t) => t.type === "image_generation"),
false,
"image_generation must be stripped from output"
);
assert.equal(tools.length, 1, "only the function tool must remain");
assert.equal(tools[0].type, "function");
assert.equal(tools[0].function.name, "foo");
});