feat(openai): default images to gpt-image-2

This commit is contained in:
Peter Steinberger
2026-04-21 21:49:16 +01:00
parent 0b1a35363e
commit aa94501f5f
16 changed files with 164 additions and 48 deletions

View File

@@ -6,7 +6,7 @@ import {
export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.4";
export const OPENAI_CODEX_DEFAULT_MODEL = "openai-codex/gpt-5.4";
export const OPENAI_DEFAULT_IMAGE_MODEL = "gpt-image-1";
export const OPENAI_DEFAULT_IMAGE_MODEL = "gpt-image-2";
export const OPENAI_DEFAULT_TTS_MODEL = "gpt-4o-mini-tts";
export const OPENAI_DEFAULT_TTS_VOICE = "alloy";
export const OPENAI_DEFAULT_AUDIO_TRANSCRIPTION_MODEL = "gpt-4o-transcribe";

View File

@@ -48,13 +48,23 @@ describe("openai image generation provider", () => {
vi.unstubAllEnvs();
});
it("advertises the current OpenAI image model and 2K/4K size hints", () => {
const provider = buildOpenAIImageGenerationProvider();
expect(provider.defaultModel).toBe("gpt-image-2");
expect(provider.models).toEqual(["gpt-image-2"]);
expect(provider.capabilities.geometry?.sizes).toEqual(
expect.arrayContaining(["2048x2048", "3840x2160", "2160x3840"]),
);
});
it("does not auto-allow local baseUrl overrides for image requests", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "Draw a QA lighthouse",
cfg: {
models: {
@@ -82,13 +92,40 @@ describe("openai image generation provider", () => {
expect(result.images).toHaveLength(1);
});
it("forwards generation count and custom size overrides", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Create two landscape campaign variants",
cfg: {},
count: 2,
size: "3840x2160",
});
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.openai.com/v1/images/generations",
body: {
model: "gpt-image-2",
prompt: "Create two landscape campaign variants",
n: 2,
size: "3840x2160",
},
}),
);
expect(result.images).toHaveLength(1);
});
it("allows loopback image requests for the synthetic mock-openai provider", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "mock-openai",
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "Draw a QA lighthouse",
cfg: {
models: {
@@ -123,7 +160,7 @@ describe("openai image generation provider", () => {
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "Draw a QA lighthouse",
cfg: {
models: {
@@ -150,21 +187,28 @@ describe("openai image generation provider", () => {
expect(result.images).toHaveLength(1);
});
it("uses JSON image_url edits for input-image requests", async () => {
it("forwards edit count, custom size, and multiple input images", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "Change only the background to pale blue",
cfg: {},
count: 2,
size: "1024x1536",
inputImages: [
{
buffer: Buffer.from("png-bytes"),
mimeType: "image/png",
fileName: "reference.png",
},
{
buffer: Buffer.from("jpeg-bytes"),
mimeType: "image/jpeg",
fileName: "style.jpg",
},
],
});
@@ -172,12 +216,17 @@ describe("openai image generation provider", () => {
expect.objectContaining({
url: "https://api.openai.com/v1/images/edits",
body: expect.objectContaining({
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "Change only the background to pale blue",
n: 2,
size: "1024x1536",
images: [
{
image_url: "data:image/png;base64,cG5nLWJ5dGVz",
},
{
image_url: "data:image/jpeg;base64,anBlZy1ieXRlcw==",
},
],
}),
}),

View File

@@ -13,7 +13,15 @@ import { resolveConfiguredOpenAIBaseUrl, toOpenAIDataUrl } from "./shared.js";
const DEFAULT_OPENAI_IMAGE_BASE_URL = "https://api.openai.com/v1";
const DEFAULT_OUTPUT_MIME = "image/png";
const DEFAULT_SIZE = "1024x1024";
const OPENAI_SUPPORTED_SIZES = ["1024x1024", "1024x1536", "1536x1024"] as const;
const OPENAI_SUPPORTED_SIZES = [
"1024x1024",
"1536x1024",
"1024x1536",
"2048x2048",
"2048x1152",
"3840x2160",
"2160x3840",
] as const;
const OPENAI_MAX_INPUT_IMAGES = 5;
const MOCK_OPENAI_PROVIDER_ID = "mock-openai";

View File

@@ -141,10 +141,12 @@ describe("openai plugin", () => {
const authStore = { version: 1, profiles: {} };
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "draw a cat",
cfg: {},
authStore,
count: 2,
size: "2048x2048",
});
expect(resolveApiKeySpy).toHaveBeenCalledWith(
@@ -157,10 +159,10 @@ describe("openai plugin", () => {
expect.objectContaining({
url: "https://api.openai.com/v1/images/generations",
body: {
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "draw a cat",
n: 1,
size: "1024x1024",
n: 2,
size: "2048x2048",
},
}),
);
@@ -178,7 +180,7 @@ describe("openai plugin", () => {
revisedPrompt: "revised",
},
],
model: "gpt-image-1",
model: "gpt-image-2",
});
});
@@ -193,10 +195,12 @@ describe("openai plugin", () => {
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "Edit this image",
cfg: {},
authStore,
count: 2,
size: "1536x1024",
inputImages: [
{ buffer: Buffer.from("x"), mimeType: "image/png" },
{ buffer: Buffer.from("y"), mimeType: "image/jpeg", fileName: "ref.jpg" },
@@ -213,10 +217,10 @@ describe("openai plugin", () => {
expect.objectContaining({
url: "https://api.openai.com/v1/images/edits",
body: {
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "Edit this image",
n: 1,
size: "1024x1024",
n: 2,
size: "1536x1024",
images: [
{
image_url: "data:image/png;base64,eA==",
@@ -236,7 +240,7 @@ describe("openai plugin", () => {
fileName: "image-1.png",
},
],
model: "gpt-image-1",
model: "gpt-image-2",
});
});
@@ -253,7 +257,7 @@ describe("openai plugin", () => {
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-1",
model: "gpt-image-2",
prompt: "draw a cat",
cfg: {
models: {

View File

@@ -17,7 +17,7 @@ import plugin from "./index.js";
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? "";
const LIVE_MODEL_ID = process.env.OPENCLAW_LIVE_OPENAI_PLUGIN_MODEL?.trim() || "gpt-5.4-nano";
const LIVE_IMAGE_MODEL = process.env.OPENCLAW_LIVE_OPENAI_IMAGE_MODEL?.trim() || "gpt-image-1";
const LIVE_IMAGE_MODEL = process.env.OPENCLAW_LIVE_OPENAI_IMAGE_MODEL?.trim() || "gpt-image-2";
const LIVE_VISION_MODEL = process.env.OPENCLAW_LIVE_OPENAI_VISION_MODEL?.trim() || "gpt-4.1-mini";
const liveEnabled = OPENAI_API_KEY.trim().length > 0 && process.env.OPENCLAW_LIVE_TEST === "1";
const describeLive = liveEnabled ? describe : describe.skip;
@@ -262,8 +262,9 @@ describeLive("openai plugin live", () => {
cfg,
agentDir,
authStore: EMPTY_AUTH_STORE,
timeoutMs: 45_000,
size: "1024x1024",
timeoutMs: 180_000,
count: 1,
size: "1536x1024",
});
expect(generated.model).toBe(LIVE_IMAGE_MODEL);
@@ -273,7 +274,7 @@ describeLive("openai plugin live", () => {
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
}
}, 60_000);
}, 240_000);
it("edits a reference image through the registered image provider", async () => {
const { imageProviders } = await registerOpenAIPlugin();
@@ -291,8 +292,9 @@ describeLive("openai plugin live", () => {
cfg,
agentDir,
authStore: EMPTY_AUTH_STORE,
timeoutMs: 45_000,
size: "1024x1024",
timeoutMs: 180_000,
count: 1,
size: "1024x1536",
inputImages: [
{
buffer: createReferencePng(),
@@ -309,7 +311,7 @@ describeLive("openai plugin live", () => {
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
}
}, 60_000);
}, 240_000);
it("describes a deterministic image through the registered media provider", async () => {
const { mediaProviders } = await registerOpenAIPlugin();