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) => ({
baseUrl: params.baseUrl ?? params.defaultBaseUrl,
allowPrivateNetwork:
Boolean (params.allowPrivateNetwork ?? params.request?.allowPri
vateNetwork),
headers: new Headers(params.defaultHeaders),
dispatcherPolicy: undefined,
})),
sanitizeConfiguredModelProviderRequestMock: vi.fn((request) => request),
logInfoMock: vi.fn(),
}));
vi.mock("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,
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-codex" ) {
return { apiKey: "codex-key" , source: "profile:openai-codex:default" , mode: "oauth" };
}
return {};
});
}
function createCodexOAuthAuthStore() {
return {
version: 1 as const ,
profiles: {
"openai-codex:default" : {
type: "oauth" as const ,
provider: "openai-codex" ,
access: "codex-access" ,
refresh: "codex-refresh" ,
expires: Date.now() + 60 _000 ,
},
},
};
}
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" ]);
expect(provider.capabilities.geometry?.sizes).toEqual(
expect.arrayContaining(["2048x2048" , "3840x2160" , "2160x3840" ]),
);
expect(provider.capabilities.output).toEqual({
formats: ["png" , "jpeg" , "webp" ],
qualities: ["low" , "medium" , "high" , "auto" ],
});
});
it("reports configured when either OpenAI API key auth or Codex OAuth auth is available" , () => {
const provider = buildOpenAIImageGenerationProvider();
isProviderApiKeyConfiguredMock.mockImplementation((params?: { provider?: string }) => {
return params?.provider === "openai" ;
});
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(true );
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "openai" ,
agentDir: "/tmp/agent" ,
});
isProviderApiKeyConfiguredMock.mockClear();
isProviderApiKeyConfiguredMock.mockImplementation((params?: { provider?: string }) => {
return params?.provider === "openai-codex" ;
});
expect(provider.isConfigured?.({ agentDir: "/tmp/agent" })).toBe(true );
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "openai" ,
agentDir: "/tmp/agent" ,
});
expect(isProviderApiKeyConfiguredMock).toHaveBeenCalledWith({
provider: "openai-codex" ,
agentDir: "/tmp/agent" ,
});
isProviderApiKeyConfiguredMock.mockReturnValue(false );
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.mockImplementation((params?: { provider?: string }) => {
return params?.provider === "openai-codex" ;
});
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.mockImplementation((params?: { provider?: string }) => {
return params?.provider === "openai-codex" ;
});
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(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
baseUrl: "http://127.0.0.1:44080/v1 ",
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "http://127.0.0.1:44080/v1/images/generations ",
allowPrivateNetwork: 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(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
baseUrl: "http://192.168.1.15:8082/v1 ",
allowPrivateNetwork: true ,
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "http://192.168.1.15:8082/v1/images/generations ",
allowPrivateNetwork: true ,
}),
);
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("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" ,
},
},
});
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.openai.com/v1/images/generations ",
body: {
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 ]).toMatchObject({
mimeType: "image/jpeg" ,
fileName: "image-1.jpg" ,
});
});
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(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
allowPrivateNetwork: true ,
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "http://127.0.0.1:44080/v1/images/generations ",
allowPrivateNetwork: 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(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
allowPrivateNetwork: true ,
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
allowPrivateNetwork: true ,
}),
);
expect(result.images).toHaveLength(1 );
});
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" ,
fileName: "style.jpg" ,
},
],
});
expect(postMultipartRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.openai.com/v1/images/edits ",
body: expect.any(FormData),
allowPrivateNetwork: false ,
dispatcherPolicy: undefined,
fetchFn: fetch,
}),
);
const editCallArgs = postMultipartRequestMock.mock.calls[0 ]?.[0 ] as {
headers: Headers;
body: FormData;
};
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("style.jpg" );
expect(images[1 ]?.type).toBe("image/jpeg" );
expect(postJsonRequestMock).not.toHaveBeenCalledWith(
expect.objectContaining({ url: "https://api.openai.com/v1/images/edits " }),
);
expect(result.images).toHaveLength(1 );
});
it("forwards output and OpenAI-only 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" ,
moderation: "auto" ,
outputCompression: 75 ,
user: "end-user-99" ,
},
},
});
const editCallArgs = postMultipartRequestMock.mock.calls[0 ]?.[0 ] as {
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" )).toBe("auto" );
expect(form.get("output_compression" )).toBe("75" );
expect(form.get("user" )).toBe("end-user-99" );
expect(result.images[0 ]).toMatchObject({
mimeType: "image/webp" ,
fileName: "image-1.webp" ,
});
});
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(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai" ,
store: authStore,
}),
);
expect(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai-codex" ,
store: authStore,
}),
);
expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
defaultBaseUrl: "https://chatgpt.com/backend-api/codex ",
defaultHeaders: expect.objectContaining({
Authorization: "Bearer codex-key" ,
Accept: "text/event-stream" ,
}),
provider: "openai-codex" ,
api: "openai-codex-responses" ,
capability: "image" ,
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://chatgpt.com/backend-api/codex/responses ",
timeoutMs: 180 _000 ,
body: expect.objectContaining({
model: "gpt-5.4" ,
instructions: "You are an image generation assistant." ,
stream: true ,
store: false ,
tools: [
{
type: "image_generation" ,
model: "gpt-image-2" ,
size: "1024x1536" ,
quality: "low" ,
output_format: "jpeg" ,
background: "opaque" ,
output_compression: 55 ,
},
],
tool_choice: { type: "image_generation" },
}),
}),
);
expect(postMultipartRequestMock).not.toHaveBeenCalled();
expect(logInfoMock).toHaveBeenCalledWith(
"image auth selected: provider=openai-codex mode=oauth transport=codex-responses requestedModel=gpt-image-2 responsesModel=gpt-5.4 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("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: "openai-key" , source: "OPENAI_API_KEY" , mode: "api-key" };
}
if (params?.provider === "openai-codex" ) {
return { apiKey: "codex-key" , source: "profile:openai-codex: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(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai-codex" ,
store: authStore,
}),
);
expect(resolveApiKeyForProviderMock).not.toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai" ,
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://chatgpt.com/backend-api/codex/responses ",
}),
);
expect(logInfoMock).toHaveBeenCalledWith(
"image auth selected: provider=openai-codex mode=oauth transport=codex-responses requestedModel=gpt-image-2 responsesModel=gpt-5.4 timeoutMs=180000" ,
);
expect(result.images[0 ]?.buffer).toEqual(Buffer.from("codex-image" ));
});
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(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
expect.objectContaining({ provider: "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(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
expect.objectContaining({ provider: "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" );
}
if (params?.provider === "openai-codex" ) {
return { apiKey: "codex-key" , source: "profile:openai-codex:default" , mode: "oauth" };
}
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(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
expect.objectContaining({ provider: "openai" }),
);
expect(postJsonRequestMock).not.toHaveBeenCalled();
});
it("sanitizes Codex OAuth image auth log values" , async () => {
resolveApiKeyForProviderMock.mockImplementation(async (params?: { provider?: string }) => {
if (params?.provider === "openai-codex" ) {
return {
apiKey: "codex-key" ,
source: "profile:openai-codex: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-codex mode=oauth fakeignored transport=codex-responses requestedModel=gpt-image-2 forged=true next responsesModel=gpt-5.4 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("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-codex" : {
baseUrl: "http://127.0.0.1:44220/backend-api/codex ",
api: "openai-codex-responses" ,
request: { allowPrivateNetwork: true },
models: [],
},
},
},
},
authStore,
});
expect(sanitizeConfiguredModelProviderRequestMock).toHaveBeenCalledWith({
allowPrivateNetwork: true ,
});
expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
baseUrl: "http://127.0.0.1:44220/backend-api/codex ",
request: { allowPrivateNetwork: true },
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "http://127.0.0.1:44220/backend-api/codex/responses ",
allowPrivateNetwork: true ,
}),
);
expect(result.images[0 ]?.buffer).toEqual(Buffer.from("codex-image" ));
});
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-codex" ) {
return { apiKey: "codex-key" , source: "profile:openai-codex: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(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai" ,
store: authStore,
}),
);
expect(resolveApiKeyForProviderMock).not.toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai-codex" ,
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.openai.com/v1/images/generations ",
}),
);
});
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" ,
},
]);
expect(postJsonRequestMock).not.toHaveBeenCalledWith(
expect.objectContaining({ url: expect.stringContaining("/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(postMultipartRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "http://127.0.0.1:44080/v1/images/edits ",
allowPrivateNetwork: false ,
dispatcherPolicy: undefined,
fetchFn: fetch,
}),
);
expect(postJsonRequestMock).not.toHaveBeenCalledWith(
expect.objectContaining({ url: "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(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
defaultHeaders: { "api-key" : "openai-key" },
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://myresource.openai.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 .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(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
defaultHeaders: { "api-key" : "openai-key" },
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "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(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
defaultHeaders: { "api-key" : "openai-key" },
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "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(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "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(postMultipartRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://myresource.openai.azure.com/openai/deployments/gpt-image-2/images/edits?api-version=2024-12-01-preview ",
body: expect.any(FormData),
}),
);
});
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(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "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(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "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(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
expect.objectContaining({
defaultHeaders: { Authorization: "Bearer openai-key" },
}),
);
expect(postJsonRequestMock).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://api.openai.com/v1/images/generations ",
}),
);
});
});
});
Messung V0.5 in Prozent C=97 H=100 G=98
¤ Dauer der Verarbeitung: 0.19 Sekunden
(vorverarbeitet am 2026-06-07)
¤
*© Formatika GbR, Deutschland