Guard OpenAI image compression for PNG outputs (#85776)

* Guard OpenAI image compression for PNG outputs

* Fix OpenAI image compression type narrowing

* docs(changelog): note OpenAI PNG compression fix

* Revert "docs(changelog): note OpenAI PNG compression fix"

This reverts commit b11e4bff01.

---------

Co-authored-by: airlin <airlin@airlins-Mac-mini.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
AirLin
2026-05-24 12:01:55 +08:00
committed by GitHub
parent 1d1a7c26d8
commit d0751111a4
3 changed files with 66 additions and 7 deletions
+2 -1
View File
@@ -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
@@ -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<string, unknown>;
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<Record<string, unknown>> };
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") {
+18 -6
View File
@@ -183,20 +183,33 @@ function resolveOutputMime(outputFormat?: ImageGenerationOutputFormat): {
return { mimeType: DEFAULT_OUTPUT_MIME, extension: DEFAULT_OUTPUT_EXTENSION };
}
type OpenAIImageRequest = Parameters<ImageGenerationProvider["generateImage"]>[0];
type OpenAIImageOptions = NonNullable<OpenAIImageRequest["providerOptions"]>["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<string, unknown> | FormData,
req: Parameters<ImageGenerationProvider["generateImage"]>[0],
): void {
const openai = req.providerOptions?.openai;
const background = openai?.background ?? req.background;
const outputCompression = resolveOpenAIImageOutputCompression(req, openai);
const entries: Record<string, unknown> = {
...(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<Record<string, unknown>> = [
{ 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" },