diff --git a/docs/tools/image-generation.md b/docs/tools/image-generation.md index db62be1aa700..596e27fd4a7e 100644 --- a/docs/tools/image-generation.md +++ b/docs/tools/image-generation.md @@ -310,7 +310,8 @@ ComfyUI support 1. transparent outputs require `outputFormat` `png` or `webp` and a transparency-capable OpenAI image model. OpenClaw routes default `gpt-image-2` transparent-background requests to `gpt-image-1.5`. - `openai.outputCompression` applies to JPEG/WebP outputs. + `openai.outputCompression` applies to JPEG/WebP outputs and is ignored + for PNG outputs. The top-level `background` hint is provider-neutral and currently maps to the same OpenAI `background` request field when the OpenAI provider diff --git a/extensions/openai/image-generation-provider.test.ts b/extensions/openai/image-generation-provider.test.ts index dd2081ca87c6..41fc8f13c6e0 100644 --- a/extensions/openai/image-generation-provider.test.ts +++ b/extensions/openai/image-generation-provider.test.ts @@ -605,6 +605,28 @@ describe("openai image generation provider", () => { expect(result.images[0]?.fileName).toBe("image-1.jpg"); }); + it("omits output compression for PNG direct generations", async () => { + mockGeneratedPngResponse(); + + const provider = buildOpenAIImageGenerationProvider(); + await provider.generateImage({ + provider: "openai", + model: "gpt-image-2", + prompt: "Transparent PNG", + cfg: {}, + outputFormat: "png", + providerOptions: { + openai: { + outputCompression: 60, + }, + }, + }); + + const body = jsonRequestCall().body as Record; + expect(body.output_format).toBe("png"); + expect(body.output_compression).toBeUndefined(); + }); + it("routes transparent default-model requests to the OpenAI image model that supports alpha", async () => { mockGeneratedPngResponse(); @@ -1080,6 +1102,30 @@ describe("openai image generation provider", () => { expect(result.model).toBe("gpt-image-1.5"); }); + it("omits output compression for PNG Codex image requests", async () => { + mockCodexAuthOnly(); + mockCodexImageStream({ imageData: "codex-png-image" }); + + const provider = buildOpenAIImageGenerationProvider(); + await provider.generateImage({ + provider: "openai", + model: "gpt-image-2", + prompt: "Draw a transparent Codex badge", + cfg: {}, + authStore: { version: 1, profiles: {} }, + outputFormat: "png", + providerOptions: { + openai: { + outputCompression: 55, + }, + }, + }); + + const body = jsonRequestCall().body as { tools?: Array> }; + expect(body.tools?.[0]?.output_format).toBe("png"); + expect(body.tools?.[0]?.output_compression).toBeUndefined(); + }); + it("uses configured Codex OAuth directly instead of probing an available OpenAI API key", async () => { resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => { if (params?.provider === "openai") { diff --git a/extensions/openai/image-generation-provider.ts b/extensions/openai/image-generation-provider.ts index 317355d63305..d9f9e0695c5a 100644 --- a/extensions/openai/image-generation-provider.ts +++ b/extensions/openai/image-generation-provider.ts @@ -183,20 +183,33 @@ function resolveOutputMime(outputFormat?: ImageGenerationOutputFormat): { return { mimeType: DEFAULT_OUTPUT_MIME, extension: DEFAULT_OUTPUT_EXTENSION }; } +type OpenAIImageRequest = Parameters[0]; +type OpenAIImageOptions = NonNullable["openai"]; + +function resolveOpenAIImageOutputCompression( + req: OpenAIImageRequest, + openai: OpenAIImageOptions, +): number | undefined { + if (openai?.outputCompression === undefined) { + return undefined; + } + const outputFormat = req.outputFormat ?? "png"; + return outputFormat === "jpeg" || outputFormat === "webp" ? openai.outputCompression : undefined; +} + function appendOpenAIImageOptions( target: Record | FormData, req: Parameters[0], ): void { const openai = req.providerOptions?.openai; const background = openai?.background ?? req.background; + const outputCompression = resolveOpenAIImageOutputCompression(req, openai); const entries: Record = { ...(req.quality !== undefined ? { quality: req.quality } : {}), ...(req.outputFormat !== undefined ? { output_format: req.outputFormat } : {}), ...(background !== undefined ? { background } : {}), ...(openai?.moderation !== undefined ? { moderation: openai.moderation } : {}), - ...(openai?.outputCompression !== undefined - ? { output_compression: openai.outputCompression } - : {}), + ...(outputCompression !== undefined ? { output_compression: outputCompression } : {}), ...(openai?.user !== undefined ? { user: openai.user } : {}), }; for (const [key, value] of Object.entries(entries)) { @@ -637,6 +650,7 @@ async function generateOpenAICodexImage(params: { const timeoutMs = resolveOpenAIImageTimeoutMs(req.timeoutMs); const openai = req.providerOptions?.openai; const background = openai?.background ?? req.background; + const outputCompression = resolveOpenAIImageOutputCompression(req, openai); headers.set("Content-Type", "application/json"); const content: Array> = [ { type: "input_text", text: req.prompt }, @@ -668,9 +682,7 @@ async function generateOpenAICodexImage(params: { ...(req.quality !== undefined ? { quality: req.quality } : {}), ...(req.outputFormat !== undefined ? { output_format: req.outputFormat } : {}), ...(background !== undefined ? { background } : {}), - ...(openai?.outputCompression !== undefined - ? { output_compression: openai.outputCompression } - : {}), + ...(outputCompression !== undefined ? { output_compression: outputCompression } : {}), }, ], tool_choice: { type: "image_generation" },