






















@@ -1,5 +1,8 @@
11import { afterEach, describe, expect, it, vi } from "vitest";
2-import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js";
2+import {
3+buildOpenAICodexImageGenerationProvider,
4+buildOpenAIImageGenerationProvider,
5+} from "./image-generation-provider.js";
3647const {
58 resolveApiKeyForProviderMock,
@@ -47,6 +50,32 @@ function mockGeneratedPngResponse() {
4750});
4851}
495253+function mockCodexImageStream(params: { imageData?: string; revisedPrompt?: string } = {}) {
54+const image = Buffer.from(params.imageData ?? "codex-png-bytes").toString("base64");
55+const events = [
56+{
57+type: "response.output_item.done",
58+item: {
59+type: "image_generation_call",
60+result: image,
61+ ...(params.revisedPrompt ? { revised_prompt: params.revisedPrompt } : {}),
62+},
63+},
64+{
65+type: "response.completed",
66+response: {
67+usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 },
68+tool_usage: { image_gen: { total_tokens: 30 } },
69+},
70+},
71+];
72+const body = events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("");
73+postJsonRequestMock.mockImplementation(async () => ({
74+response: new Response(body),
75+release: vi.fn(async () => {}),
76+}));
77+}
78+5079describe("openai image generation provider", () => {
5180afterEach(() => {
5281resolveApiKeyForProviderMock.mockClear();
@@ -252,6 +281,132 @@ describe("openai image generation provider", () => {
252281expect(result.images).toHaveLength(1);
253282});
254283284+it("registers Codex OAuth image generation through Responses streaming", async () => {
285+mockCodexImageStream({ imageData: "codex-image", revisedPrompt: "revised codex prompt" });
286+287+const provider = buildOpenAICodexImageGenerationProvider();
288+const authStore = { version: 1, profiles: {} };
289+const result = await provider.generateImage({
290+provider: "openai-codex",
291+model: "gpt-image-2",
292+prompt: "Draw a Codex lighthouse",
293+cfg: {},
294+ authStore,
295+count: 1,
296+size: "1024x1536",
297+});
298+299+expect(resolveApiKeyForProviderMock).toHaveBeenCalledWith(
300+expect.objectContaining({
301+provider: "openai-codex",
302+store: authStore,
303+}),
304+);
305+expect(resolveProviderHttpRequestConfigMock).toHaveBeenCalledWith(
306+expect.objectContaining({
307+defaultBaseUrl: "https://chatgpt.com/backend-api/codex",
308+defaultHeaders: expect.objectContaining({
309+Authorization: "Bearer openai-key",
310+Accept: "text/event-stream",
311+}),
312+provider: "openai-codex",
313+api: "openai-codex-responses",
314+capability: "image",
315+}),
316+);
317+expect(postJsonRequestMock).toHaveBeenCalledWith(
318+expect.objectContaining({
319+url: "https://chatgpt.com/backend-api/codex/responses",
320+body: expect.objectContaining({
321+model: "gpt-5.4",
322+instructions: "You are an image generation assistant.",
323+stream: true,
324+store: false,
325+tools: [
326+{
327+type: "image_generation",
328+model: "gpt-image-2",
329+size: "1024x1536",
330+},
331+],
332+tool_choice: { type: "image_generation" },
333+}),
334+}),
335+);
336+expect(postMultipartRequestMock).not.toHaveBeenCalled();
337+expect(result.images).toEqual([
338+{
339+buffer: Buffer.from("codex-image"),
340+mimeType: "image/png",
341+fileName: "image-1.png",
342+revisedPrompt: "revised codex prompt",
343+},
344+]);
345+expect(result.metadata).toEqual({
346+responses: [
347+{
348+usage: { input_tokens: 10, output_tokens: 20, total_tokens: 30 },
349+toolUsage: { image_gen: { total_tokens: 30 } },
350+},
351+],
352+});
353+});
354+355+it("sends Codex reference images as Responses input images", async () => {
356+mockCodexImageStream();
357+358+const provider = buildOpenAICodexImageGenerationProvider();
359+await provider.generateImage({
360+provider: "openai-codex",
361+model: "gpt-image-2",
362+prompt: "Use the reference image",
363+cfg: {},
364+inputImages: [
365+{ buffer: Buffer.from("png-bytes"), mimeType: "image/png", fileName: "ref.png" },
366+],
367+});
368+369+const body = postJsonRequestMock.mock.calls[0]?.[0].body as {
370+input: Array<{ content: Array<Record<string, string>> }>;
371+};
372+expect(body.input[0]?.content).toEqual([
373+{ type: "input_text", text: "Use the reference image" },
374+{
375+type: "input_image",
376+image_url: `data:image/png;base64,${Buffer.from("png-bytes").toString("base64")}`,
377+detail: "auto",
378+},
379+]);
380+expect(postJsonRequestMock).not.toHaveBeenCalledWith(
381+expect.objectContaining({ url: expect.stringContaining("/images/edits") }),
382+);
383+expect(postMultipartRequestMock).not.toHaveBeenCalled();
384+});
385+386+it("satisfies Codex count by issuing one Responses request per image", async () => {
387+mockCodexImageStream({ imageData: "codex-image" });
388+389+const provider = buildOpenAICodexImageGenerationProvider();
390+const result = await provider.generateImage({
391+provider: "openai-codex",
392+model: "gpt-image-2",
393+prompt: "Draw two Codex icons",
394+cfg: {},
395+count: 2,
396+});
397+398+expect(postJsonRequestMock).toHaveBeenCalledTimes(2);
399+const firstBody = postJsonRequestMock.mock.calls[0]?.[0].body as {
400+tools: Array<Record<string, unknown>>;
401+};
402+expect(firstBody.tools[0]).toEqual({
403+type: "image_generation",
404+model: "gpt-image-2",
405+size: "1024x1024",
406+});
407+expect(result.images.map((image) => image.fileName)).toEqual(["image-1.png", "image-2.png"]);
408+});
409+255410it("forwards SSRF guard fields to multipart edit requests", async () => {
256411mockGeneratedPngResponse();
257412此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。