From 832a34659e3ea4a97373bdbfef41462f855f5f68 Mon Sep 17 00:00:00 2001 From: Hai Trinh Date: Thu, 10 Sep 2026 22:06:17 +0700 Subject: [PATCH] feat(codex): add GPT Image 2.5, Flare and Sunburst image models - Add gpt-image-1.5, gpt-image-2, gpt-image-2.5, gpt-image-2.5-flare and gpt-image-2.5-sunburst as Codex image models with multi-image support - Add gpt-image-2.5, gpt-image-2.5-flare and gpt-image-2.5-sunburst to the OpenAI provider catalog - Route tool-backed image models through the Codex responses model while passing the selected model to the image_generation tool, pinning tool_choice and deriving generate/edit from the presence of references - Cover the Codex gpt-image-2.5 request shape with a unit test --- open-sse/handlers/imageProviders/codex.js | 26 ++++++++++++-- open-sse/providers/registry/codex.js | 5 +++ open-sse/providers/registry/openai.js | 3 ++ tests/unit/image-generation.test.js | 41 +++++++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/open-sse/handlers/imageProviders/codex.js b/open-sse/handlers/imageProviders/codex.js index 385ec5b6..afaf8e97 100644 --- a/open-sse/handlers/imageProviders/codex.js +++ b/open-sse/handlers/imageProviders/codex.js @@ -9,6 +9,14 @@ const CODEX_USER_AGENT = `codex_cli_rs/${CODEX_CLI_VERSION}`; const CODEX_ORIGINATOR = "codex_cli_rs"; const CODEX_MODEL_SUFFIX = "-image"; const CODEX_REF_DETAIL = "high"; +const CODEX_IMAGES_MAIN_MODEL = "gpt-5.5"; +const CODEX_TOOL_IMAGE_MODELS = new Set([ + "gpt-image-1.5", + "gpt-image-2", + "gpt-image-2.5", + "gpt-image-2.5-flare", + "gpt-image-2.5-sunburst", +]); function decodeAccountId(idToken) { try { @@ -27,6 +35,13 @@ function stripImageSuffix(model) { return model.endsWith(CODEX_MODEL_SUFFIX) ? model.slice(0, -CODEX_MODEL_SUFFIX.length) : model; } +function resolveCodexImageModels(model) { + if (CODEX_TOOL_IMAGE_MODELS.has(model)) { + return { responsesModel: CODEX_IMAGES_MAIN_MODEL, toolModel: model }; + } + return { responsesModel: stripImageSuffix(model), toolModel: null }; +} + function toDataUrl(input) { if (!input || typeof input !== "string") return null; if (/^data:image\//i.test(input) || /^https?:\/\//i.test(input)) return input; @@ -167,21 +182,26 @@ export default { const single = toDataUrl(body.image); if (single) refs.push(single); const detail = body.image_detail || CODEX_REF_DETAIL; + const { responsesModel, toolModel } = resolveCodexImageModels(model); const imgTool = { type: "image_generation", output_format: (body.output_format || "png").toLowerCase() }; + if (toolModel) { + imgTool.action = refs.length > 0 ? "edit" : "generate"; + imgTool.model = toolModel; + } if (body.size && body.size !== "") imgTool.size = body.size; if (body.quality && body.quality !== "") imgTool.quality = body.quality; if (body.background && body.background !== "") imgTool.background = body.background; return { - model: stripImageSuffix(model), + model: responsesModel, instructions: "", input: [{ type: "message", role: "user", content: buildContent(body.prompt, refs, detail) }], tools: [imgTool], - tool_choice: "auto", + tool_choice: toolModel ? { type: "image_generation" } : "auto", parallel_tool_calls: false, prompt_cache_key: randomUUID(), stream: true, store: false, - reasoning: null, + reasoning: toolModel ? { effort: "medium", summary: "auto" } : null, }; }, // Custom: codex parses SSE → either pipe to client or collect b64 diff --git a/open-sse/providers/registry/codex.js b/open-sse/providers/registry/codex.js index bf436a10..9eaafe74 100644 --- a/open-sse/providers/registry/codex.js +++ b/open-sse/providers/registry/codex.js @@ -65,6 +65,11 @@ export default { { id: "gpt-5.4-mini-review", name: "GPT 5.4 Mini Review", upstreamModelId: "gpt-5.4-mini", quotaFamily: "review" }, { id: "gpt-5.3-codex-spark", name: "GPT 5.3 Codex Spark" }, { id: "gpt-5.3-codex-spark-review", name: "GPT 5.3 Codex Spark Review", upstreamModelId: "gpt-5.3-codex-spark", quotaFamily: "review" }, + { id: "gpt-image-2.5", name: "GPT Image 2.5", capabilities: ["text2img","edit","multiImage"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, + { id: "gpt-image-2.5-flare", name: "GPT Image 2.5 Flare", capabilities: ["text2img","edit","multiImage"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, + { id: "gpt-image-2.5-sunburst", name: "GPT Image 2.5 Sunburst", capabilities: ["text2img","edit","multiImage"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, + { id: "gpt-image-2", name: "GPT Image 2", capabilities: ["text2img","edit","multiImage"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, + { id: "gpt-image-1.5", name: "GPT Image 1.5", capabilities: ["text2img","edit","multiImage"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, { id: "gpt-5.6-sol-image", name: "GPT 5.6 Sol Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, { id: "gpt-5.6-terra-image", name: "GPT 5.6 Terra Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, { id: "gpt-5.6-luna-image", name: "GPT 5.6 Luna Image", capabilities: ["text2img","edit"], params: ["size","quality","background","image_detail","output_format"], kind: "image" }, diff --git a/open-sse/providers/registry/openai.js b/open-sse/providers/registry/openai.js index 9a1ca57b..4a5f0eb1 100644 --- a/open-sse/providers/registry/openai.js +++ b/open-sse/providers/registry/openai.js @@ -57,6 +57,9 @@ export default { { id: "whisper-1", name: "Whisper 1", params: ["language","response_format","temperature","prompt"], kind: "stt" }, { id: "gpt-4o-transcribe", name: "GPT-4o Transcribe", params: ["language","response_format","temperature","prompt"], kind: "stt" }, { id: "gpt-4o-mini-transcribe", name: "GPT-4o Mini Transcribe", params: ["language","response_format","temperature","prompt"], kind: "stt" }, + { id: "gpt-image-2.5", name: "GPT Image 2.5", params: ["n","size","quality","response_format"], kind: "image" }, + { id: "gpt-image-2.5-flare", name: "GPT Image 2.5 Flare", params: ["n","size","quality","response_format"], kind: "image" }, + { id: "gpt-image-2.5-sunburst", name: "GPT Image 2.5 Sunburst", params: ["n","size","quality","response_format"], kind: "image" }, { id: "gpt-image-1", name: "GPT Image 1", params: ["n","size","quality","response_format"], kind: "image" }, { id: "dall-e-3", name: "DALL-E 3", params: ["size","quality","style","response_format"], kind: "image" }, { id: "dall-e-2", name: "DALL-E 2", params: ["n","size","response_format"], kind: "image" }, diff --git a/tests/unit/image-generation.test.js b/tests/unit/image-generation.test.js index 4df94afc..6a22cd40 100644 --- a/tests/unit/image-generation.test.js +++ b/tests/unit/image-generation.test.js @@ -367,6 +367,47 @@ describe("handleImageGenerationCore", () => { expect(responseBody.data[0].b64_json).toBe("base64codeximage"); }); + it("generates image with Codex gpt-image-2.5 tool model", async () => { + global.fetch.mockResolvedValueOnce( + new Response( + [ + "event: response.output_item.done", + 'data: {"item":{"type":"image_generation_call","result":"base64codeximage"}}', + "", + "", + ].join("\n"), + { status: 200, headers: { "Content-Type": "text/event-stream" } } + ) + ); + + const result = await handleImageGenerationCore({ + body: { + prompt: "A futuristic city", + size: "1024x1024", + output_format: "png", + }, + modelInfo: { provider: "codex", model: "gpt-image-2.5" }, + credentials: { + accessToken: "codex-token", + providerSpecificData: { chatgptAccountId: "account-123" }, + }, + log: null, + }); + + expect(result.success).toBe(true); + const fetchCall = global.fetch.mock.calls[0]; + const requestBody = JSON.parse(fetchCall[1].body); + expect(requestBody.model).toBe("gpt-5.5"); + expect(requestBody.tools).toEqual([ + { type: "image_generation", output_format: "png", size: "1024x1024", action: "generate", model: "gpt-image-2.5" }, + ]); + expect(requestBody.tool_choice).toEqual({ type: "image_generation" }); + expect(requestBody.reasoning).toEqual({ effort: "medium", summary: "auto" }); + + const responseBody = await result.response.json(); + expect(responseBody.data[0].b64_json).toBe("base64codeximage"); + }); + it("generates image with Cloudflare Workers AI JSON response", async () => { global.fetch.mockResolvedValueOnce( new Response(