Files
openclaw/extensions/openai/image-generation-provider.test.ts
Peter Steinberger e04caa6aee fix(openai): support flexible GPT Image 2 dimensions (#118476)
* fix(openai): support flexible GPT Image 2 dimensions

* docs(openai): explain flexible GPT Image 2 dimensions
2026-08-02 22:40:07 -07:00

2753 lines
87 KiB
TypeScript

// Openai tests cover image generation provider plugin behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js";
const {
ensureAuthProfileStoreMock,
isProviderApiKeyConfiguredMock,
listProfilesForProviderMock,
resolveApiKeyForProviderMock,
postJsonRequestMock,
postMultipartRequestMock,
assertOkOrThrowHttpErrorMock,
resolveProviderHttpRequestConfigMock,
sanitizeConfiguredModelProviderRequestMock,
logInfoMock,
} = vi.hoisted(() => ({
ensureAuthProfileStoreMock: vi.fn(() => ({ version: 1, profiles: {} })),
isProviderApiKeyConfiguredMock: vi.fn<
(params: { provider: string; agentDir?: string }) => boolean
>(() => false),
listProfilesForProviderMock: vi.fn(
(store: { profiles?: Record<string, { provider?: string }> }, provider: string) =>
Object.entries(store.profiles ?? {})
.filter(([, profile]) => profile.provider === provider)
.map(([profileId]) => profileId),
),
resolveApiKeyForProviderMock: vi.fn(
async (_params?: {
provider?: string;
}): Promise<{ apiKey?: string; source?: string; mode?: string }> => ({
apiKey: "openai-key",
}),
),
postJsonRequestMock: vi.fn(),
postMultipartRequestMock: vi.fn(),
assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
resolveProviderHttpRequestConfigMock: vi.fn((params) => {
const headers = new Headers(params.defaultHeaders);
new Headers(params.headers).forEach((value, key) => headers.set(key, value));
return {
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
allowPrivateNetwork: Boolean(
params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork,
),
headers,
dispatcherPolicy: undefined,
};
}),
sanitizeConfiguredModelProviderRequestMock: vi.fn((request) => request),
logInfoMock: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/provider-auth", async (importOriginal) => ({
...(await importOriginal<typeof import("openclaw/plugin-sdk/provider-auth")>()),
ensureAuthProfileStore: ensureAuthProfileStoreMock,
isProviderApiKeyConfigured: isProviderApiKeyConfiguredMock,
listProfilesForProvider: listProfilesForProviderMock,
}));
vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
resolveApiKeyForProvider: resolveApiKeyForProviderMock,
}));
vi.mock("openclaw/plugin-sdk/provider-http", () => ({
assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
postJsonRequest: postJsonRequestMock,
postMultipartRequest: postMultipartRequestMock,
// Pass-through: bounded-reader enforcement is tested via bounded-reader unit tests.
readProviderJsonResponse: async (response: { json(): Promise<unknown> }) => response.json(),
resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
sanitizeConfiguredModelProviderRequest: sanitizeConfiguredModelProviderRequestMock,
}));
vi.mock("openclaw/plugin-sdk/logging-core", () => ({
createSubsystemLogger: vi.fn(() => ({
info: logInfoMock,
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
})),
}));
function mockGeneratedPngResponse() {
const response = {
json: async () => ({
data: [{ b64_json: Buffer.from("png-bytes").toString("base64") }],
}),
};
postJsonRequestMock.mockResolvedValue({
response,
release: vi.fn(async () => {}),
});
postMultipartRequestMock.mockResolvedValue({
response,
release: vi.fn(async () => {}),
});
}
function mockCodexImageStream(params: { imageData?: string; revisedPrompt?: string } = {}) {
const image = Buffer.from(params.imageData ?? "codex-png-bytes").toString("base64");
const events = [
{
type: "response.output_item.done",
item: {
type: "image_generation_call",
result: image,
...(params.revisedPrompt ? { revised_prompt: params.revisedPrompt } : {}),
},
},
{
type: "response.completed",
response: {
usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 },
tool_usage: { image_gen: { total_tokens: 30 } },
},
},
];
const body = events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("");
postJsonRequestMock.mockImplementation(async () => ({
response: new Response(body),
release: vi.fn(async () => {}),
}));
}
function mockCodexCompletedImageStream(
params: {
imageData?: string;
revisedPrompt?: string;
} = {},
) {
const image = Buffer.from(params.imageData ?? "codex-completed-png-bytes").toString("base64");
const events = [
{
type: "response.completed",
response: {
output: [
{
type: "image_generation_call",
result: image,
...(params.revisedPrompt ? { revised_prompt: params.revisedPrompt } : {}),
},
],
usage: { input_tokens: 11, output_tokens: 22, total_tokens: 33 },
},
},
];
const body = events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("");
postJsonRequestMock.mockImplementation(async () => ({
response: new Response(body),
release: vi.fn(async () => {}),
}));
}
function mockCodexRawStream(body: string) {
postJsonRequestMock.mockImplementation(async () => ({
response: new Response(body),
release: vi.fn(async () => {}),
}));
}
function mockCodexAuthOnly() {
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return { apiKey: "codex-key", source: "profile:openai:default", mode: "oauth" };
}
return {};
});
}
function createCodexOAuthAuthStore() {
return {
version: 1 as const,
profiles: {
"openai:default": {
type: "oauth" as const,
provider: "openai",
access: "codex-access",
refresh: "codex-refresh",
expires: Date.now() + 60_000,
},
},
};
}
function createCodexApiKeyAuthStore() {
return {
version: 1 as const,
profiles: {
"openai:manual": {
type: "api_key" as const,
provider: "openai",
key: "codex-api-key",
},
},
};
}
function createCodexTokenAuthStore() {
return {
version: 1 as const,
profiles: {
"openai:token": {
type: "token" as const,
provider: "openai",
token: "codex-token",
},
},
};
}
function createMixedCodexAuthStore() {
return {
version: 1 as const,
profiles: {
...createCodexTokenAuthStore().profiles,
...createCodexApiKeyAuthStore().profiles,
},
};
}
function createMixedOpenAIAuthStore() {
return {
version: 1 as const,
profiles: {
"openai:chatgpt": {
type: "oauth" as const,
provider: "openai",
access: "codex-access",
refresh: "codex-refresh",
expires: Date.now() + 60_000,
},
"openai:default": {
type: "api_key" as const,
provider: "openai",
key: "openai-key",
},
},
};
}
type MockWithCalls = {
mock: {
calls: readonly (readonly unknown[])[];
};
};
type HttpConfigCall = {
allowPrivateNetwork?: boolean;
api?: string;
baseUrl?: string;
capability?: string;
defaultBaseUrl?: string;
defaultHeaders?: Record<string, string>;
provider?: string;
request?: unknown;
};
type RequestCall = {
allowPrivateNetwork?: boolean;
body?: unknown;
dispatcherPolicy?: unknown;
fetchFn?: typeof fetch;
headers?: Headers;
ssrfPolicy?: unknown;
timeoutMs?: number;
url?: string;
};
type AuthResolutionCall = {
cfg?: {
models?: {
providers?: {
openai?: {
auth?: string;
};
};
};
};
credentialPrecedence?: string;
provider?: string;
store?: unknown;
};
function mockCallArg(mock: MockWithCalls, callIndex = 0, argIndex = 0): unknown {
const call = mock.mock.calls[callIndex];
if (!call) {
throw new Error(`Expected mock call ${callIndex}`);
}
if (call.length <= argIndex) {
throw new Error(`Expected mock call ${callIndex} argument ${argIndex}`);
}
return call[argIndex];
}
function jsonRequestCall(callIndex = 0): RequestCall {
return mockCallArg(postJsonRequestMock, callIndex) as RequestCall;
}
function multipartRequestCall(callIndex = 0): RequestCall {
return mockCallArg(postMultipartRequestMock, callIndex) as RequestCall;
}
function httpConfigCall(callIndex = 0): HttpConfigCall {
return mockCallArg(resolveProviderHttpRequestConfigMock, callIndex) as HttpConfigCall;
}
function authResolutionCall(callIndex = 0): AuthResolutionCall {
return mockCallArg(resolveApiKeyForProviderMock, callIndex) as AuthResolutionCall;
}
function expectNoJsonRequestUrl(expectedUrl: string) {
expect(
postJsonRequestMock.mock.calls.some(([call]) => (call as RequestCall).url === expectedUrl),
).toBe(false);
}
function expectNoJsonRequestUrlContaining(expectedFragment: string) {
expect(
postJsonRequestMock.mock.calls.some(
([call]) => (call as RequestCall).url?.includes(expectedFragment) === true,
),
).toBe(false);
}
describe("openai image generation provider", () => {
afterEach(() => {
ensureAuthProfileStoreMock.mockReset();
ensureAuthProfileStoreMock.mockReturnValue({ version: 1, profiles: {} });
isProviderApiKeyConfiguredMock.mockReset();
isProviderApiKeyConfiguredMock.mockReturnValue(false);
listProfilesForProviderMock.mockClear();
resolveApiKeyForProviderMock.mockReset();
resolveApiKeyForProviderMock.mockResolvedValue({ apiKey: "openai-key" });
postJsonRequestMock.mockReset();
postMultipartRequestMock.mockReset();
assertOkOrThrowHttpErrorMock.mockClear();
resolveProviderHttpRequestConfigMock.mockClear();
sanitizeConfiguredModelProviderRequestMock.mockClear();
logInfoMock.mockClear();
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",
"gpt-image-1.5",
"gpt-image-1",
"gpt-image-1-mini",
]);
expect(provider.capabilities.geometry?.sizes).toContain("2048x2048");
expect(provider.capabilities.geometry?.sizes).toContain("3840x2160");
expect(provider.capabilities.geometry?.sizes).toContain("2160x3840");
expect(provider.capabilities.geometry?.sizesByModel).toEqual({
"gpt-image-2": [],
"gpt-image-2-2026-04-21": [],
});
expect(provider.capabilities.output).toEqual({
formats: ["png", "jpeg", "webp"],
qualities: ["low", "medium", "high", "auto"],
backgrounds: ["transparent", "opaque", "auto"],
});
});
it("reports configured when either OpenAI API key auth or Codex OAuth auth is available", () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockReturnValue(true);
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(true);
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "openai",
agentDir: "/tmp/agent",
});
isProviderApiKeyConfiguredMock.mockClear();
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue(createCodexOAuthAuthStore());
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(true);
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "openai",
agentDir: "/tmp/agent",
});
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "openai",
agentDir: "/tmp/agent",
});
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue({ version: 1, profiles: {} });
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(false);
});
it("does not report Codex OAuth image auth as configured for custom OpenAI endpoints", () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockReturnValue(false);
expect(
provider.isConfigured?.({
agentDir: "/tmp/agent",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example.test/v1",
models: [],
},
},
},
},
}),
).toBe(false);
});
it("reports configured from a config apiKey (gateway-routed openai) with no env/profile creds", () => {
const provider = buildOpenAIImageGenerationProvider();
// Config-only auth: a provider apiKey in config, with no env var and no
// auth profile.
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue({ version: 1, profiles: {} });
expect(
provider.isConfigured?.({
agentDir: "/tmp/agent",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://gateway.example.test/openai/v1",
apiKey: "gateway-token",
models: [],
},
},
},
},
}),
).toBe(true);
});
it.each([
["empty", ""],
["whitespace-only", " "],
])("treats a %s config apiKey as not configured", (_label, apiKey) => {
const provider = buildOpenAIImageGenerationProvider();
// Blank placeholders resolve to no usable credential in the generate
// path, so readiness must not count them either.
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue({ version: 1, profiles: {} });
expect(
provider.isConfigured?.({
agentDir: "/tmp/agent",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://gateway.example.test/openai/v1",
apiKey,
models: [],
},
},
},
},
}),
).toBe(false);
});
it("reports ChatGPT OAuth image auth as configured for ChatGPT routes", () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue(createCodexOAuthAuthStore());
expect(
provider.isConfigured?.({
agentDir: "/tmp/agent",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://chatgpt.com/backend-api/codex",
models: [],
},
},
},
},
}),
).toBe(true);
expect(
provider.isConfigured?.({
agentDir: "/tmp/agent",
cfg: {
models: {
providers: {
openai: {
api: "openai-chatgpt-responses",
baseUrl: "https://openai-compatible.example.test/v1",
models: [],
},
},
},
},
}),
).toBe(true);
});
it("does not report OpenAI OAuth image auth as configured for custom OpenAI endpoints", () => {
const provider = buildOpenAIImageGenerationProvider();
vi.stubEnv("OPENAI_API_KEY", "");
isProviderApiKeyConfiguredMock.mockReturnValue(false);
ensureAuthProfileStoreMock.mockReturnValue({
version: 1,
profiles: {
"openai:chatgpt": {
type: "oauth",
provider: "openai",
access: "chatgpt-access",
refresh: "chatgpt-refresh",
expires: Date.now() + 60_000,
},
},
});
expect(
provider.isConfigured?.({
agentDir: "/tmp/agent",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example.test/v1",
models: [],
},
},
},
},
}),
).toBe(false);
});
it("does not report Codex OAuth image auth as configured for non-exact public OpenAI URLs", () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockReturnValue(false);
expect(
provider.isConfigured?.({
agentDir: "/tmp/agent",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1?proxy=1",
models: [],
},
},
},
},
}),
).toBe(false);
});
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-2",
prompt: "Draw a QA lighthouse",
cfg: {
models: {
providers: {
openai: {
baseUrl: "http://127.0.0.1:44080/v1",
models: [],
},
},
},
},
});
expect(httpConfigCall().baseUrl).toBe("http://127.0.0.1:44080/v1");
expect(jsonRequestCall().url).toBe("http://127.0.0.1:44080/v1/images/generations");
expect(jsonRequestCall().allowPrivateNetwork).toBe(false);
expect(result.images).toHaveLength(1);
});
it("allows OpenAI-compatible private image endpoints when browser SSRF policy opts in", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "flux2-klein",
prompt: "A simple, clean illustration of a red apple with a green leaf",
cfg: {
browser: {
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true,
},
},
models: {
providers: {
openai: {
baseUrl: "http://192.168.1.15:8082/v1",
apiKey: "local-noauth",
models: [],
},
},
},
},
});
expect(httpConfigCall().baseUrl).toBe("http://192.168.1.15:8082/v1");
expect(httpConfigCall().allowPrivateNetwork).toBe(true);
expect(jsonRequestCall().url).toBe("http://192.168.1.15:8082/v1/images/generations");
expect(jsonRequestCall().allowPrivateNetwork).toBe(true);
expect(result.images).toHaveLength(1);
});
it("propagates request SSRF policy to JSON image requests", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "test",
cfg: {},
ssrfPolicy: { allowRfc2544BenchmarkRange: true },
});
expect(jsonRequestCall().ssrfPolicy).toEqual({ allowRfc2544BenchmarkRange: true });
});
it("wraps malformed successful OpenAI image responses", async () => {
postJsonRequestMock.mockResolvedValue({
response: { json: async () => ({ data: { b64_json: "not-an-array" } }) },
release: vi.fn(async () => {}),
});
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "bad shape",
cfg: {},
}),
).rejects.toThrow("OpenAI image generation response malformed");
});
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",
});
const request = jsonRequestCall();
expect(request.url).toBe("https://api.openai.com/v1/images/generations");
expect(request.body).toEqual({
model: "gpt-image-2",
prompt: "Create two landscape campaign variants",
n: 2,
size: "3840x2160",
});
expect(result.images).toHaveLength(1);
});
it.each([
{ model: "gpt-image-2", size: "1536x864" },
{ model: "gpt-image-2", size: "1024x640" },
{ model: "gpt-image-2", size: "2304x2304" },
{ model: "gpt-image-2", size: "2560x2560" },
{ model: "gpt-image-2", size: "2880x2880" },
{ model: "gpt-image-2", size: "3072x2176" },
{ model: "gpt-image-2-2026-04-21", size: "864x1536" },
])("preserves flexible $model generation dimensions $size", async ({ model, size }) => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model,
prompt: "Preserve the requested image dimensions",
cfg: {},
size,
});
expect(jsonRequestCall().body).toEqual({
model,
prompt: "Preserve the requested image dimensions",
n: 1,
size,
});
expect(result.metadata).toBeUndefined();
});
it.each(["16x16", "1024x624", "1025x1024", "3088x1024", "4096x2048", "2896x2896"])(
"normalizes unsupported flexible-model dimensions %s",
async (size) => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Normalize unsupported image dimensions",
cfg: {},
size,
});
const normalizedSize = (jsonRequestCall().body as { size: string }).size;
expect(normalizedSize).not.toBe(size);
expect(provider.capabilities.geometry?.sizes).toContain(normalizedSize);
expect(result.metadata).toEqual({ requestedSize: size, normalizedSize });
},
);
it("normalizes legacy gpt-image-1 sizes before native OpenAI generation", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
prompt: "Create a wide Matrix QA image",
cfg: {},
size: "2048x1152",
});
const request = jsonRequestCall();
const body = request.body as Record<string, unknown>;
expect(request.url).toBe("https://api.openai.com/v1/images/generations");
expect(body.model).toBe("gpt-image-1");
expect(body.size).toBe("1536x1024");
expect(result.metadata).toEqual({
requestedSize: "2048x1152",
normalizedSize: "1536x1024",
});
});
it("does not normalize model-specific sizes for custom OpenAI-compatible endpoints", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
prompt: "Create a wide local-provider image",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example.com/v1",
models: [],
},
},
},
},
size: "2048x1152",
});
const request = jsonRequestCall();
const body = request.body as Record<string, unknown>;
expect(request.url).toBe("https://openai-compatible.example.com/v1/images/generations");
expect(body.model).toBe("gpt-image-1");
expect(body.size).toBe("2048x1152");
expect(result.metadata).toBeUndefined();
});
it("falls back to the provider baseUrl when the model catalog is omitted", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
// Plugin-scoped runtime snapshots can carry a built-in provider overlay
// before its model catalog is present.
const cfg = {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example.com/v1",
},
},
},
} as unknown as OpenClawConfig;
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Create an image through a provider overlay",
cfg,
});
expect(httpConfigCall().baseUrl).toBe("https://openai-compatible.example.com/v1");
expect(jsonRequestCall().url).toBe(
"https://openai-compatible.example.com/v1/images/generations",
);
expect(result.images).toHaveLength(1);
});
it("forwards output and OpenAI-only options on direct generations", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Cheap JPEG preview",
cfg: {},
quality: "low",
outputFormat: "jpeg",
providerOptions: {
openai: {
background: "opaque",
moderation: "low",
outputCompression: 60,
user: "end-user-42",
},
},
});
const request = jsonRequestCall();
expect(request.url).toBe("https://api.openai.com/v1/images/generations");
expect(request.body).toEqual({
model: "gpt-image-2",
prompt: "Cheap JPEG preview",
n: 1,
size: "1024x1024",
quality: "low",
output_format: "jpeg",
background: "opaque",
moderation: "low",
output_compression: 60,
user: "end-user-42",
});
expect(result.images[0]?.mimeType).toBe("image/jpeg");
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();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Transparent sticker",
cfg: {},
outputFormat: "png",
background: "transparent",
});
const request = jsonRequestCall();
const body = request.body as Record<string, unknown>;
expect(request.url).toBe("https://api.openai.com/v1/images/generations");
expect(body.model).toBe("gpt-image-1.5");
expect(body.output_format).toBe("png");
expect(body.background).toBe("transparent");
expect(result.model).toBe("gpt-image-1.5");
});
it("does not reroute transparent requests for custom OpenAI-compatible endpoints", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Transparent custom endpoint sticker",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example.com/v1",
models: [],
},
},
},
},
outputFormat: "png",
providerOptions: {
openai: {
background: "transparent",
},
},
});
const request = jsonRequestCall();
const body = request.body as Record<string, unknown>;
expect(request.url).toBe("https://openai-compatible.example.com/v1/images/generations");
expect(body.model).toBe("gpt-image-2");
expect(body.output_format).toBe("png");
expect(body.background).toBe("transparent");
});
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-2",
prompt: "Draw a QA lighthouse",
cfg: {
models: {
providers: {
openai: {
baseUrl: "http://127.0.0.1:44080/v1",
models: [],
},
},
},
},
});
expect(httpConfigCall().allowPrivateNetwork).toBe(true);
expect(jsonRequestCall().url).toBe("http://127.0.0.1:44080/v1/images/generations");
expect(jsonRequestCall().allowPrivateNetwork).toBe(true);
expect(result.images).toHaveLength(1);
});
it("allows loopback image requests for openai only inside the QA harness envelope", async () => {
mockGeneratedPngResponse();
vi.stubEnv("OPENCLAW_QA_ALLOW_LOCAL_IMAGE_PROVIDER", "1");
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw a QA lighthouse",
cfg: {
models: {
providers: {
openai: {
baseUrl: "http://127.0.0.1:44080/v1",
models: [],
},
},
},
},
});
expect(httpConfigCall().allowPrivateNetwork).toBe(true);
expect(jsonRequestCall().allowPrivateNetwork).toBe(true);
expect(result.images).toHaveLength(1);
});
it("uses a model-specific QA image endpoint without changing the text provider route", async () => {
mockGeneratedPngResponse();
vi.stubEnv("OPENCLAW_QA_ALLOW_LOCAL_IMAGE_PROVIDER", "1");
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-1",
prompt: "Draw a QA lighthouse",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
models: [
{
id: "gpt-image-1",
name: "gpt-image-1",
api: "openai-responses",
baseUrl: "http://127.0.0.1:44080/v1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128_000,
maxTokens: 4096,
},
],
},
},
},
},
});
expect(httpConfigCall().baseUrl).toBe("http://127.0.0.1:44080/v1");
expect(jsonRequestCall().url).toBe("http://127.0.0.1:44080/v1/images/generations");
expect(jsonRequestCall().allowPrivateNetwork).toBe(true);
});
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-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",
},
],
});
const editCallArgs = multipartRequestCall() as RequestCall & {
headers: Headers;
body: FormData;
};
expect(editCallArgs.url).toBe("https://api.openai.com/v1/images/edits");
expect(editCallArgs.body).toBeInstanceOf(FormData);
expect(editCallArgs.allowPrivateNetwork).toBe(false);
expect(editCallArgs.dispatcherPolicy).toBeUndefined();
expect(editCallArgs.fetchFn).toBe(fetch);
expect(editCallArgs.headers.has("Content-Type")).toBe(false);
const form = editCallArgs.body;
expect(form.get("model")).toBe("gpt-image-2");
expect(form.get("prompt")).toBe("Change only the background to pale blue");
expect(form.get("n")).toBe("2");
expect(form.get("size")).toBe("1024x1536");
const images = form.getAll("image[]") as File[];
expect(images).toHaveLength(2);
expect(images[0]?.name).toBe("reference.png");
expect(images[0]?.type).toBe("image/png");
expect(images[1]?.name).toBe("image-2.jpg");
expect(images[1]?.type).toBe("image/jpeg");
expectNoJsonRequestUrl("https://api.openai.com/v1/images/edits");
expect(result.images).toHaveLength(1);
});
it.each([
{ model: "gpt-image-2", size: "1536x864" },
{ model: "gpt-image-2", size: "2560x2560" },
{ model: "gpt-image-2-2026-04-21", size: "864x1536" },
])("preserves flexible $model multipart-edit dimensions $size", async ({ model, size }) => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model,
prompt: "Preserve the requested edited image dimensions",
cfg: {},
size,
inputImages: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }],
});
const request = multipartRequestCall() as RequestCall & { body: FormData };
expect(request.url).toBe("https://api.openai.com/v1/images/edits");
expect(request.body.get("model")).toBe(model);
expect(request.body.get("size")).toBe(size);
expect(result.metadata).toBeUndefined();
});
it("forwards supported OpenAI options on multipart edits", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Edit as WebP",
cfg: {},
inputImages: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }],
quality: "high",
outputFormat: "webp",
providerOptions: {
openai: {
background: "transparent",
outputCompression: 75,
user: "end-user-99",
},
},
});
const editCallArgs = multipartRequestCall() as RequestCall & {
body: FormData;
};
const form = editCallArgs.body;
expect(form.get("quality")).toBe("high");
expect(form.get("output_format")).toBe("webp");
expect(form.get("background")).toBe("transparent");
expect(form.get("moderation")).toBeNull();
expect(form.get("output_compression")).toBe("75");
expect(form.get("user")).toBe("end-user-99");
expect(result.images[0]?.mimeType).toBe("image/webp");
expect(result.images[0]?.fileName).toBe("image-1.webp");
});
it.each(["low", "auto"] as const)(
"forwards %s moderation on multipart image edits",
async (moderation) => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Edit with supported moderation",
cfg: {},
inputImages: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }],
providerOptions: {
openai: { moderation },
},
});
const request = multipartRequestCall() as RequestCall & {
body: FormData;
};
expect(postMultipartRequestMock).toHaveBeenCalledOnce();
expect(request.url).toBe("https://api.openai.com/v1/images/edits");
expect(request.body).toBeInstanceOf(FormData);
expect(request.body.get("moderation")).toBe(moderation);
expect(postJsonRequestMock).not.toHaveBeenCalled();
expect(result.images).toHaveLength(1);
expect(result.images[0]?.mimeType).toBe("image/png");
},
);
it("falls back to Codex OAuth image generation through Responses streaming", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-image", revisedPrompt: "revised codex prompt" });
const provider = buildOpenAIImageGenerationProvider();
const authStore = { version: 1, profiles: {} };
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw a Codex lighthouse",
cfg: {},
authStore,
count: 1,
size: "1024x1536",
quality: "low",
outputFormat: "jpeg",
providerOptions: {
openai: {
background: "opaque",
outputCompression: 55,
},
},
});
expect(authResolutionCall(0).provider).toBe("openai");
expect(authResolutionCall(0).store).toBe(authStore);
const configCall = httpConfigCall();
expect(configCall.defaultBaseUrl).toBe("https://chatgpt.com/backend-api/codex");
expect(configCall.defaultHeaders).toEqual({
Authorization: "Bearer codex-key",
Accept: "text/event-stream",
});
expect(configCall.provider).toBe("openai");
expect(configCall.api).toBe("openai-chatgpt-responses");
expect(configCall.capability).toBe("image");
const request = jsonRequestCall();
const body = request.body as Record<string, unknown>;
expect(request.url).toBe("https://chatgpt.com/backend-api/codex/responses");
expect(request.timeoutMs).toBe(180_000);
expect(body.model).toBe("gpt-5.6-sol");
expect(body.instructions).toBe("You are an image generation assistant.");
expect(body.stream).toBe(true);
expect(body.store).toBe(false);
expect(body.tools).toEqual([
{
type: "image_generation",
model: "gpt-image-2",
size: "1024x1536",
quality: "low",
output_format: "jpeg",
background: "opaque",
output_compression: 55,
},
]);
expect(body.tool_choice).toEqual({ type: "image_generation" });
expect(postMultipartRequestMock).not.toHaveBeenCalled();
expect(logInfoMock).toHaveBeenCalledWith(
"image auth selected: provider=openai mode=oauth transport=codex-responses requestedModel=gpt-image-2 responsesModel=gpt-5.6-sol timeoutMs=180000",
);
expect(result.images).toEqual([
{
buffer: Buffer.from("codex-image"),
mimeType: "image/jpeg",
fileName: "image-1.jpg",
revisedPrompt: "revised codex prompt",
},
]);
expect(result.metadata).toEqual({
responses: [
{
usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 },
toolUsage: { image_gen: { total_tokens: 30 } },
},
],
});
});
it("cancels oversized Codex OAuth image response streams", async () => {
mockCodexAuthOnly();
let canceled = false;
let chunkSent = false;
const release = vi.fn(async () => {});
const stream = new ReadableStream<Uint8Array>({
pull(controller) {
if (chunkSent) {
return;
}
chunkSent = true;
controller.enqueue(new Uint8Array(64 * 1024 * 1024 + 1));
},
cancel() {
canceled = true;
},
});
postJsonRequestMock.mockResolvedValue({
response: new Response(stream),
release,
});
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw an oversized Codex lighthouse",
cfg: {},
authStore: createCodexOAuthAuthStore(),
}),
).rejects.toThrow("OpenAI Codex image generation response exceeded size limit");
expect(canceled).toBe(true);
expect(release).toHaveBeenCalledTimes(1);
});
it("does not treat Codex API key profiles as configured Codex OAuth image auth", async () => {
mockGeneratedPngResponse();
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return { apiKey: "openai-key", source: "profile:openai", mode: "api-key" };
}
throw new Error(`Unexpected auth provider ${params?.provider ?? ""}`);
});
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw with OpenAI auth despite a Codex API key profile",
cfg: {},
authStore: createCodexApiKeyAuthStore(),
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations");
expect(httpConfigCall().provider).toBe("openai");
expect(logInfoMock).not.toHaveBeenCalledWith(
expect.stringContaining("transport=codex-responses"),
);
});
it("uses native OpenAI image requests when only Codex API key auth is available", async () => {
mockGeneratedPngResponse();
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return {
apiKey: "codex-api-key",
source: "profile:openai:manual",
mode: "api-key",
};
}
return {};
});
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw through Codex API key auth",
cfg: {},
authStore: createCodexApiKeyAuthStore(),
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
const configCall = httpConfigCall();
expect(configCall.defaultBaseUrl).toBe("https://api.openai.com/v1");
expect(configCall.defaultHeaders).toEqual({
Authorization: "Bearer codex-api-key",
});
expect(configCall.provider).toBe("openai");
expect(configCall.api).toBeUndefined();
expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations");
expect(postMultipartRequestMock).not.toHaveBeenCalled();
expect(logInfoMock).not.toHaveBeenCalledWith(
expect.stringContaining("transport=codex-responses"),
);
});
it("uses native OpenAI image requests when mixed Codex profiles resolve to an API key", async () => {
mockGeneratedPngResponse();
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return {
apiKey: "codex-api-key",
source: "profile:openai:manual",
mode: "api-key",
};
}
throw new Error(`Unexpected auth provider ${params?.provider ?? ""}`);
});
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw through resolved Codex API key auth",
cfg: {},
authStore: createMixedCodexAuthStore(),
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(httpConfigCall().defaultBaseUrl).toBe("https://api.openai.com/v1");
expect(httpConfigCall().defaultHeaders).toEqual({
Authorization: "Bearer codex-api-key",
});
expect(httpConfigCall().provider).toBe("openai");
expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations");
expect(logInfoMock).not.toHaveBeenCalledWith(
expect.stringContaining("transport=codex-responses"),
);
});
it("keeps Codex token auth on the Codex image transport", async () => {
mockCodexImageStream({ imageData: "codex-token-image" });
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return {
apiKey: "codex-token",
source: "profile:openai:token",
mode: "token",
};
}
return {};
});
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw through Codex token auth",
cfg: {},
authStore: createCodexTokenAuthStore(),
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(httpConfigCall().defaultBaseUrl).toBe("https://chatgpt.com/backend-api/codex");
expect(httpConfigCall().provider).toBe("openai");
expect(httpConfigCall().api).toBe("openai-chatgpt-responses");
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
expect(postMultipartRequestMock).not.toHaveBeenCalled();
expect(result.images[0]?.buffer).toEqual(Buffer.from("codex-token-image"));
});
it("uses configured Codex token auth before probing an available OpenAI API key", async () => {
mockCodexImageStream({ imageData: "codex-token-image" });
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return {
apiKey: "codex-token",
source: "profile:openai:token",
mode: "token",
};
}
return {};
});
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw using configured Codex token auth",
cfg: {},
authStore: createCodexTokenAuthStore(),
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
});
it("routes transparent default-model Codex OAuth requests to the alpha-capable image model", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-transparent-image" });
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw a transparent Codex sticker",
cfg: {},
authStore: { version: 1, profiles: {} },
outputFormat: "png",
providerOptions: {
openai: {
background: "transparent",
},
},
});
const request = jsonRequestCall();
const body = request.body as { tools?: Array<Record<string, unknown>> };
expect(request.url).toBe("https://chatgpt.com/backend-api/codex/responses");
expect(body.tools?.[0]?.type).toBe("image_generation");
expect(body.tools?.[0]?.model).toBe("gpt-image-1.5");
expect(body.tools?.[0]?.output_format).toBe("png");
expect(body.tools?.[0]?.background).toBe("transparent");
expect(result.model).toBe("gpt-image-1.5");
});
it("forwards OpenAI moderation into Codex Responses image-generation tool requests", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-moderated-image" });
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw a low-moderation Codex preview",
cfg: {},
authStore: { version: 1, profiles: {} },
providerOptions: {
openai: {
moderation: "low",
},
},
});
const request = jsonRequestCall();
const body = request.body as { tools?: Array<Record<string, unknown>> };
expect(request.url).toBe("https://chatgpt.com/backend-api/codex/responses");
expect(body.tools?.[0]?.type).toBe("image_generation");
expect(body.tools?.[0]?.moderation).toBe("low");
});
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") {
return { apiKey: "codex-key", source: "profile:openai:default", mode: "oauth" };
}
return {};
});
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
const authStore = createCodexOAuthAuthStore();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw using configured Codex auth",
cfg: {},
authStore,
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(authResolutionCall().store).toBe(authStore);
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
expect(logInfoMock).toHaveBeenCalledWith(
"image auth selected: provider=openai mode=oauth transport=codex-responses requestedModel=gpt-image-2 responsesModel=gpt-5.6-sol timeoutMs=180000",
);
expect(result.images[0]?.buffer).toEqual(Buffer.from("codex-image"));
});
it("keeps explicit OpenAI API-key image config on native image requests", async () => {
resolveApiKeyForProviderMock.mockImplementation(async (params?: AuthResolutionCall) => {
if (params?.cfg?.models?.providers?.openai?.auth === "api-key") {
return { apiKey: "configured-openai-key", source: "models.json", mode: "api-key" };
}
return { apiKey: "chatgpt-oauth-token", source: "profile:openai:chatgpt", mode: "oauth" };
});
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
const authStore = createMixedOpenAIAuthStore();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw using configured OpenAI image auth",
cfg: {
models: {
providers: {
openai: {
apiKey: "sk-configured",
baseUrl: "https://api.openai.com/v1",
models: [],
},
},
},
},
authStore,
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(authResolutionCall().store).toBe(authStore);
expect(authResolutionCall().credentialPrecedence).toBe("env-first");
expect(authResolutionCall().cfg?.models?.providers?.openai?.auth).toBe("api-key");
expect(httpConfigCall().defaultBaseUrl).toBe("https://api.openai.com/v1");
expect(httpConfigCall().defaultHeaders).toEqual({
Authorization: "Bearer configured-openai-key",
});
expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations");
expect(logInfoMock).not.toHaveBeenCalled();
});
it("does not fall back to Codex OAuth for custom OpenAI-compatible image endpoints", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw through a custom endpoint",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://openai-compatible.example.test/v1",
models: [],
},
},
},
},
}),
).rejects.toThrow("OpenAI API key missing");
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
it("does not fall back to Codex OAuth for non-exact public OpenAI URLs", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw through public OpenAI with query params",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1?proxy=1",
models: [],
},
},
},
},
}),
).rejects.toThrow("OpenAI API key missing");
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
it("does not fall back to Codex OAuth when direct OpenAI auth resolution fails unexpectedly", async () => {
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
throw new Error("Keychain unavailable");
}
return {};
});
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw after an auth error",
cfg: {},
}),
).rejects.toThrow("Keychain unavailable");
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
it("sanitizes Codex OAuth image auth log values", async () => {
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return {
apiKey: "codex-key",
source: "profile:openai:default",
mode: "oauth\nfake\u202eignored",
};
}
return {};
});
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2\r\nforged=true\u2028next",
prompt: "Draw using configured Codex auth",
cfg: {},
authStore: createCodexOAuthAuthStore(),
});
expect(logInfoMock).toHaveBeenCalledWith(
"image auth selected: provider=openai mode=oauth fakeignored transport=codex-responses requestedModel=gpt-image-2 forged=true next responsesModel=gpt-5.6-sol timeoutMs=180000",
);
});
it("does not split a surrogate pair when truncating Codex OAuth image auth log values", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: `${"a".repeat(255)}😀tail`,
prompt: "Draw using configured Codex auth",
cfg: {},
authStore: createCodexOAuthAuthStore(),
});
expect(logInfoMock).toHaveBeenCalledWith(
`image auth selected: provider=openai mode=oauth transport=codex-responses requestedModel=${"a".repeat(255)}... responsesModel=gpt-5.6-sol timeoutMs=180000`,
);
});
it("parses Codex completed response output image payloads", async () => {
mockCodexAuthOnly();
mockCodexCompletedImageStream({
imageData: "codex-completed-image",
revisedPrompt: "completed prompt",
});
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw from completed output",
cfg: {},
});
expect(result.images).toEqual([
{
buffer: Buffer.from("codex-completed-image"),
mimeType: "image/png",
fileName: "image-1.png",
revisedPrompt: "completed prompt",
},
]);
expect(result.metadata).toEqual({
responses: [
{
usage: { input_tokens: 11, output_tokens: 22, total_tokens: 33 },
toolUsage: undefined,
},
],
});
});
it.each([
{
name: "data fields without optional whitespace",
frame: (event: string) => `data:${event}\n\n`,
},
{
name: "one event split across multiple data fields",
frame: (event: string) => `data:${event.replace(',"item":', ',\ndata:"item":')}\n\n`,
},
])("parses Codex SSE $name", async ({ frame }) => {
mockCodexAuthOnly();
const item = JSON.stringify({
type: "response.output_item.done",
item: {
type: "image_generation_call",
result: Buffer.from("framed-image").toString("base64"),
},
});
const completed = JSON.stringify({ type: "response.completed", response: { output: [] } });
mockCodexRawStream(`${frame(item)}data:${completed}\n\n`);
const result = await buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw an image from a standards-compliant SSE frame",
cfg: {},
});
expect(result.images[0]?.buffer).toEqual(Buffer.from("framed-image"));
});
it("surfaces the authoritative nested Codex response failure", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
`data: ${JSON.stringify({
type: "response.failed",
response: {
status: "failed",
error: { code: "rate_limit_exceeded", message: "quota was exhausted" },
},
})}\n\n`,
);
await expect(
buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw an image after the provider rejects the turn",
cfg: {},
}),
).rejects.toThrow("quota was exhausted");
});
it("rejects incomplete Codex image turns with their provider reason", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
`data: ${JSON.stringify({
type: "response.incomplete",
response: { status: "incomplete", incomplete_details: { reason: "max_output_tokens" } },
})}\n\n`,
);
await expect(
buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw an image before the model runs out of output tokens",
cfg: {},
}),
).rejects.toThrow(/incomplete.*max_output_tokens/i);
});
it("rejects Codex image output when the stream closes before its terminal response", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
`data: ${JSON.stringify({
type: "response.output_item.done",
item: {
type: "image_generation_call",
result: Buffer.from("unconfirmed-image").toString("base64"),
},
})}\n\n`,
);
await expect(
buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw an image on an interrupted stream",
cfg: {},
}),
).rejects.toThrow(/closed before response\.completed/i);
});
it("rejects completed Codex turns that contain no generated image", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
`data: ${JSON.stringify({ type: "response.completed", response: { output: [] } })}\n\n`,
);
await expect(
buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw an image instead of silently returning no assets",
cfg: {},
}),
).rejects.toThrow(/did not produce an image/i);
});
it("uses the completed Codex response as the authoritative image snapshot", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
[
{
type: "response.output_item.done",
item: {
type: "image_generation_call",
result: Buffer.from("stale-image").toString("base64"),
},
},
{
type: "response.completed",
response: {
output: [
{
type: "image_generation_call",
result: Buffer.from("final-image").toString("base64"),
},
],
},
},
]
.map((event) => `data: ${JSON.stringify(event)}\n\n`)
.join(""),
);
const result = await buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw an image from the final provider-owned snapshot",
cfg: {},
});
expect(result.images[0]?.buffer).toEqual(Buffer.from("final-image"));
});
it.each([
{ name: "completed without an image", status: "completed", result: null },
{ name: "failed without an image", status: "failed", result: null },
{
name: "failed with stale image data",
status: "failed",
result: Buffer.from("failed-image").toString("base64"),
},
{
name: "in progress with stale image data",
status: "in_progress",
result: Buffer.from("in-progress-image").toString("base64"),
},
{
name: "generating with stale image data",
status: "generating",
result: Buffer.from("generating-image").toString("base64"),
},
])("rejects an authoritative completed image call $name", async ({ status, result }) => {
mockCodexAuthOnly();
mockCodexRawStream(
[
{
type: "response.output_item.done",
item: {
type: "image_generation_call",
status: "completed",
result: Buffer.from("stale-interim-image").toString("base64"),
},
},
{
type: "response.completed",
response: {
output: [{ type: "image_generation_call", status, result }],
},
},
]
.map((event) => `data: ${JSON.stringify(event)}\n\n`)
.join(""),
);
await expect(
buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Honor the authoritative completed image status instead of a stale interim image",
cfg: {},
}),
).rejects.toThrow(/did not produce an image|image call did not complete/i);
});
it("rejects a failed interim image when the completed snapshot omits image calls", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
[
{
type: "response.output_item.done",
item: {
type: "image_generation_call",
status: "failed",
result: Buffer.from("failed-interim-image").toString("base64"),
},
},
{ type: "response.completed", response: { output: [] } },
]
.map((event) => `data: ${JSON.stringify(event)}\n\n`)
.join(""),
);
await expect(
buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Reject a failed compatibility image instead of returning it",
cfg: {},
}),
).rejects.toThrow(/image call did not complete/i);
});
it("ignores malformed interim image data when the completed snapshot provides the final image", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
[
{
type: "response.output_item.done",
item: { type: "image_generation_call", result: "not valid base64" },
},
{
type: "response.completed",
response: {
output: [
{
type: "image_generation_call",
result: Buffer.from("final-image").toString("base64"),
},
],
},
},
]
.map((event) => `data: ${JSON.stringify(event)}\n\n`)
.join(""),
);
const result = await buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Use the completed image instead of an invalid interim snapshot",
cfg: {},
});
expect(result.images[0]?.buffer).toEqual(Buffer.from("final-image"));
});
it("does not let malformed interim image data hide an authoritative provider failure", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
[
{
type: "response.output_item.done",
item: { type: "image_generation_call", result: "not valid base64" },
},
{
type: "response.failed",
response: { error: { message: "provider account is over quota" } },
},
]
.map((event) => `data: ${JSON.stringify(event)}\n\n`)
.join(""),
);
await expect(
buildOpenAIImageGenerationProvider().generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Return the real provider failure instead of an interim decoding error",
cfg: {},
}),
).rejects.toThrow("provider account is over quota");
});
it.each([
{
name: "invalid alphabet from output item",
event: {
type: "response.output_item.done",
item: { type: "image_generation_call", result: "aGVs!bG8=" },
},
},
{
name: "invalid alphabet from completed output",
event: {
type: "response.completed",
response: {
output: [{ type: "image_generation_call", result: "aGVs!bG8=" }],
},
},
},
{
name: "missing canonical padding",
event: {
type: "response.output_item.done",
item: { type: "image_generation_call", result: "aGVsbG8" },
},
},
{
name: "internal whitespace",
event: {
type: "response.output_item.done",
item: { type: "image_generation_call", result: "aGVs bG8=" },
},
},
{
name: "non-zero trailing bits",
event: {
type: "response.output_item.done",
item: { type: "image_generation_call", result: "Zh==" },
},
},
])("rejects malformed Codex image base64 from $name events", async ({ event }) => {
mockCodexAuthOnly();
const terminal =
event.type === "response.completed"
? ""
: `data: ${JSON.stringify({ type: "response.completed", response: { output: [] } })}\n\n`;
mockCodexRawStream(`data: ${JSON.stringify(event)}\n\n${terminal}`);
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw from malformed image data",
cfg: {},
}),
).rejects.toThrow("OpenAI Codex image generation returned malformed base64 image data");
});
it("accepts Codex-compatible surrounding Unicode whitespace", async () => {
mockCodexAuthOnly();
mockCodexRawStream(
`data: ${JSON.stringify({
type: "response.output_item.done",
item: { type: "image_generation_call", result: "\u0085aGVsbG8=\u0085" },
})}\n\ndata: ${JSON.stringify({ type: "response.completed", response: { output: [] } })}\n\n`,
);
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw from valid image data",
cfg: {},
});
expect(result.images[0]?.buffer).toEqual(Buffer.from("hello"));
});
it("honors configured Codex transport overrides for OAuth image generation", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
const authStore = createCodexOAuthAuthStore();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw through a configured Codex endpoint",
cfg: {
models: {
providers: {
openai: {
baseUrl: "http://127.0.0.1:44220/backend-api/codex",
api: "openai-chatgpt-responses",
request: { allowPrivateNetwork: true },
models: [],
},
},
},
},
authStore,
ssrfPolicy: { allowRfc2544BenchmarkRange: true },
});
expect(sanitizeConfiguredModelProviderRequestMock).toHaveBeenCalledWith({
allowPrivateNetwork: true,
});
expect(httpConfigCall().baseUrl).toBe("http://127.0.0.1:44220/backend-api/codex");
expect(httpConfigCall().request).toEqual({ allowPrivateNetwork: true });
expect(jsonRequestCall().url).toBe("http://127.0.0.1:44220/backend-api/codex/responses");
expect(jsonRequestCall().allowPrivateNetwork).toBe(true);
expect(jsonRequestCall().ssrfPolicy).toEqual({ allowRfc2544BenchmarkRange: true });
expect(result.images[0]?.buffer).toEqual(Buffer.from("codex-image"));
});
it.each([
"https://chatgpt.com/backend-api",
"https://chatgpt.com/backend-api/",
"https://chatgpt.com/backend-api/v1",
"https://chatgpt.com/backend-api/codex/v1",
])("canonicalizes configured Codex OAuth image baseUrl %s", async (configuredBaseUrl) => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw through a legacy configured Codex endpoint",
cfg: {
models: {
providers: {
openai: {
baseUrl: configuredBaseUrl,
api: "openai-chatgpt-responses",
models: [],
},
},
},
},
authStore: createCodexOAuthAuthStore(),
});
expect(httpConfigCall().baseUrl).toBe("https://chatgpt.com/backend-api/codex");
expect(httpConfigCall().provider).toBe("openai");
expect(httpConfigCall().api).toBe("openai-chatgpt-responses");
expect(httpConfigCall().capability).toBe("image");
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
});
it("uses direct OpenAI auth when custom OpenAI image config is explicit", async () => {
mockGeneratedPngResponse();
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return { apiKey: "openai-key", source: "models.json", mode: "api-key" };
}
if (params?.provider === "openai") {
return { apiKey: "codex-key", source: "profile:openai:default", mode: "oauth" };
}
return {};
});
const provider = buildOpenAIImageGenerationProvider();
const authStore = createCodexOAuthAuthStore();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw using explicit direct config",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: "OPENAI_API_KEY",
models: [],
},
},
},
},
authStore,
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(authResolutionCall().store).toBe(authStore);
expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations");
});
it("keeps explicit OpenAI request settings on native image requests", async () => {
mockGeneratedPngResponse();
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai") {
return { apiKey: "openai-key", source: "models.json", mode: "api-key" };
}
if (params?.provider === "openai") {
return { apiKey: "codex-key", source: "profile:openai:default", mode: "oauth" };
}
return {};
});
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw using explicit request settings",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
api: "openai-responses",
headers: { "X-Test-OpenAI": "direct" },
request: { allowPrivateNetwork: true },
models: [],
},
},
},
},
authStore: createCodexOAuthAuthStore(),
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(httpConfigCall().api).toBe("openai-responses");
expect(httpConfigCall().request).toEqual({ allowPrivateNetwork: true });
expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations");
expect(jsonRequestCall().headers?.get("X-Test-OpenAI")).toBe("direct");
expectNoJsonRequestUrl("https://chatgpt.com/backend-api/codex/responses");
});
it("keeps mixed OpenAI OAuth/API-key image auth on the selected OAuth transport", async () => {
mockCodexImageStream();
resolveApiKeyForProviderMock.mockResolvedValue({
apiKey: "codex-key",
source: "profile:openai:chatgpt",
mode: "oauth",
});
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw using the selected ChatGPT profile",
cfg: {
auth: {
profiles: {
"openai:chatgpt": {
provider: "openai",
mode: "oauth",
},
"openai:default": {
provider: "openai",
mode: "api_key",
},
},
order: {
openai: ["openai:chatgpt", "openai:default"],
},
},
},
authStore: createMixedOpenAIAuthStore(),
});
expect(resolveApiKeyForProviderMock).toHaveBeenCalledTimes(1);
expect(authResolutionCall().provider).toBe("openai");
expect(httpConfigCall().provider).toBe("openai");
expect(httpConfigCall().api).toBe("openai-chatgpt-responses");
expect(jsonRequestCall().url).toBe("https://chatgpt.com/backend-api/codex/responses");
});
it("sends Codex reference images as Responses input images", async () => {
mockCodexAuthOnly();
mockCodexImageStream();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Use the reference image",
cfg: {},
inputImages: [
{ buffer: Buffer.from("png-bytes"), mimeType: "image/png", fileName: "ref.png" },
],
});
const body = postJsonRequestMock.mock.calls[0]?.[0].body as {
input: Array<{ content: Array<Record<string, string>> }>;
};
expect(body.input[0]?.content).toEqual([
{ type: "input_text", text: "Use the reference image" },
{
type: "input_image",
image_url: `data:image/png;base64,${Buffer.from("png-bytes").toString("base64")}`,
detail: "auto",
},
]);
expectNoJsonRequestUrlContaining("/images/edits");
expect(postMultipartRequestMock).not.toHaveBeenCalled();
});
it("satisfies Codex count by issuing one Responses request per image", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw two Codex icons",
cfg: {},
count: 2,
});
expect(postJsonRequestMock).toHaveBeenCalledTimes(2);
const firstBody = postJsonRequestMock.mock.calls[0]?.[0].body as {
tools: Array<Record<string, unknown>>;
};
expect(firstBody.tools[0]).toEqual({
type: "image_generation",
model: "gpt-image-2",
size: "1024x1024",
});
expect(result.images.map((image) => image.fileName)).toEqual(["image-1.png", "image-2.png"]);
});
it("caps Codex image request count at provider maximum", async () => {
mockCodexAuthOnly();
mockCodexImageStream({ imageData: "codex-image" });
const provider = buildOpenAIImageGenerationProvider();
const result = await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw many Codex icons",
cfg: {},
count: 12,
});
expect(postJsonRequestMock).toHaveBeenCalledTimes(4);
expect(result.images.map((image) => image.fileName)).toEqual([
"image-1.png",
"image-2.png",
"image-3.png",
"image-4.png",
]);
});
it("rejects oversized Codex image SSE event streams", async () => {
mockCodexAuthOnly();
const body = Array.from(
{ length: 513 },
(_, index) =>
`data: ${JSON.stringify({ type: "response.output_text.delta", delta: String(index) })}\n\n`,
).join("");
mockCodexRawStream(body);
const provider = buildOpenAIImageGenerationProvider();
await expect(
provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Draw after noisy SSE",
cfg: {},
}),
).rejects.toThrow("OpenAI Codex image generation response exceeded event limit");
});
it("forwards SSRF guard fields to multipart edit requests", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Edit cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "http://127.0.0.1:44080/v1",
models: [],
},
},
},
},
inputImages: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }],
});
expect(multipartRequestCall().url).toBe("http://127.0.0.1:44080/v1/images/edits");
expect(multipartRequestCall().allowPrivateNetwork).toBe(false);
expect(multipartRequestCall().dispatcherPolicy).toBeUndefined();
expect(multipartRequestCall().fetchFn).toBe(fetch);
expectNoJsonRequestUrl("http://127.0.0.1:44080/v1/images/edits");
});
describe("azure openai support", () => {
it("uses api-key header and deployment-scoped URL for Azure .openai.azure.com hosts", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Azure cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.openai.azure.com",
models: [],
},
},
},
},
});
expect(httpConfigCall().defaultHeaders).toEqual({ "api-key": "openai-key" });
expect(jsonRequestCall().url).toBe(
"https://myresource.openai.azure.com/openai/deployments/gpt-image-2/images/generations?api-version=2024-12-01-preview",
);
});
it("omits model from Azure generation body because deployment is URL-scoped", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2-1",
prompt: "Azure cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.openai.azure.com/openai/v1",
models: [],
},
},
},
},
});
expect(jsonRequestCall().url).toBe(
"https://myresource.openai.azure.com/openai/deployments/gpt-image-2-1/images/generations?api-version=2024-12-01-preview",
);
expect(jsonRequestCall().body).toEqual({
prompt: "Azure cat",
n: 1,
size: "1024x1024",
});
expect(jsonRequestCall().timeoutMs).toBe(600_000);
});
it("lets explicit timeoutMs override the Azure image default", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2-1",
prompt: "Azure cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.openai.azure.com/openai/v1",
models: [],
},
},
},
},
timeoutMs: 123_456,
});
expect(jsonRequestCall().timeoutMs).toBe(123_456);
});
it("does not reroute transparent background requests for Azure deployment names", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Transparent Azure sticker",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.openai.azure.com",
models: [],
},
},
},
},
outputFormat: "png",
providerOptions: {
openai: {
background: "transparent",
},
},
});
expect(jsonRequestCall().url).toBe(
"https://myresource.openai.azure.com/openai/deployments/gpt-image-2/images/generations?api-version=2024-12-01-preview",
);
expect(jsonRequestCall().body).toEqual({
prompt: "Transparent Azure sticker",
n: 1,
size: "1024x1024",
output_format: "png",
background: "transparent",
});
});
it("uses api-key header and deployment-scoped URL for .cognitiveservices.azure.com hosts", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Azure cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.cognitiveservices.azure.com",
models: [],
},
},
},
},
});
expect(httpConfigCall().defaultHeaders).toEqual({ "api-key": "openai-key" });
expect(jsonRequestCall().url).toBe(
"https://myresource.cognitiveservices.azure.com/openai/deployments/gpt-image-2/images/generations?api-version=2024-12-01-preview",
);
});
it("uses api-key header and deployment-scoped URL for .services.ai.azure.com hosts", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Azure cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://my-resource.services.ai.azure.com",
models: [],
},
},
},
},
});
expect(httpConfigCall().defaultHeaders).toEqual({ "api-key": "openai-key" });
expect(jsonRequestCall().url).toBe(
"https://my-resource.services.ai.azure.com/openai/deployments/gpt-image-2/images/generations?api-version=2024-12-01-preview",
);
});
it("respects AZURE_OPENAI_API_VERSION env override", async () => {
mockGeneratedPngResponse();
vi.stubEnv("AZURE_OPENAI_API_VERSION", "2025-01-01");
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Azure cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.openai.azure.com",
models: [],
},
},
},
},
});
expect(jsonRequestCall().url).toBe(
"https://myresource.openai.azure.com/openai/deployments/gpt-image-2/images/generations?api-version=2025-01-01",
);
});
it("builds Azure edit URL with deployment and api-version", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Change background",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.openai.azure.com",
models: [],
},
},
},
},
inputImages: [
{
buffer: Buffer.from("png-bytes"),
mimeType: "image/png",
fileName: "reference.png",
},
],
});
expect(multipartRequestCall().url).toBe(
"https://myresource.openai.azure.com/openai/deployments/gpt-image-2/images/edits?api-version=2024-12-01-preview",
);
expect(multipartRequestCall().body).toBeInstanceOf(FormData);
});
it("omits model from Azure edit form because deployment is URL-scoped", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2-1",
prompt: "Change background",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.openai.azure.com/openai/v1",
models: [],
},
},
},
},
inputImages: [
{
buffer: Buffer.from("png-bytes"),
mimeType: "image/png",
fileName: "reference.png",
},
],
});
const editCallArgs = multipartRequestCall() as RequestCall & {
body: FormData;
};
expect(editCallArgs.body.has("model")).toBe(false);
expect(editCallArgs.body.get("prompt")).toBe("Change background");
expect(editCallArgs.body.get("size")).toBe("1024x1024");
});
it("strips trailing /v1 from Azure base URL", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Azure cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.openai.azure.com/v1",
models: [],
},
},
},
},
});
expect(jsonRequestCall().url).toBe(
"https://myresource.openai.azure.com/openai/deployments/gpt-image-2/images/generations?api-version=2024-12-01-preview",
);
});
it("strips trailing /openai/v1 from Azure base URL", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Azure cat",
cfg: {
models: {
providers: {
openai: {
baseUrl: "https://myresource.openai.azure.com/openai/v1",
models: [],
},
},
},
},
});
expect(jsonRequestCall().url).toBe(
"https://myresource.openai.azure.com/openai/deployments/gpt-image-2/images/generations?api-version=2024-12-01-preview",
);
});
it("still uses Bearer auth for public OpenAI hosts", async () => {
mockGeneratedPngResponse();
const provider = buildOpenAIImageGenerationProvider();
await provider.generateImage({
provider: "openai",
model: "gpt-image-2",
prompt: "Public cat",
cfg: {},
});
expect(httpConfigCall().defaultHeaders).toEqual({ Authorization: "Bearer openai-key" });
expect(jsonRequestCall().url).toBe("https://api.openai.com/v1/images/generations");
});
});
});
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */