diff --git a/extensions/openai/image-generation-provider.test.ts b/extensions/openai/image-generation-provider.test.ts new file mode 100644 index 00000000000..4321ef35098 --- /dev/null +++ b/extensions/openai/image-generation-provider.test.ts @@ -0,0 +1,83 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js"; + +const { + resolveApiKeyForProviderMock, + postJsonRequestMock, + postTranscriptionRequestMock, + assertOkOrThrowHttpErrorMock, + resolveProviderHttpRequestConfigMock, +} = vi.hoisted(() => ({ + resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "openai-key" })), + postJsonRequestMock: vi.fn(), + postTranscriptionRequestMock: vi.fn(), + assertOkOrThrowHttpErrorMock: vi.fn(async () => {}), + resolveProviderHttpRequestConfigMock: vi.fn((params) => ({ + baseUrl: params.baseUrl ?? params.defaultBaseUrl, + allowPrivateNetwork: Boolean(params.allowPrivateNetwork ?? params.baseUrl?.trim()), + headers: new Headers(params.defaultHeaders), + dispatcherPolicy: undefined, + })), +})); + +vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({ + resolveApiKeyForProvider: resolveApiKeyForProviderMock, +})); + +vi.mock("openclaw/plugin-sdk/provider-http", () => ({ + assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock, + postJsonRequest: postJsonRequestMock, + postTranscriptionRequest: postTranscriptionRequestMock, + resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock, +})); + +describe("openai image generation provider", () => { + afterEach(() => { + resolveApiKeyForProviderMock.mockClear(); + postJsonRequestMock.mockReset(); + postTranscriptionRequestMock.mockReset(); + assertOkOrThrowHttpErrorMock.mockClear(); + resolveProviderHttpRequestConfigMock.mockClear(); + }); + + it("allows explicit local baseUrl overrides for image requests", async () => { + postJsonRequestMock.mockResolvedValue({ + response: { + json: async () => ({ + data: [{ b64_json: Buffer.from("png-bytes").toString("base64") }], + }), + }, + release: vi.fn(async () => {}), + }); + + const provider = buildOpenAIImageGenerationProvider(); + const result = await provider.generateImage({ + provider: "openai", + model: "gpt-image-1", + 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: true, + }), + ); + expect(result.images).toHaveLength(1); + }); +}); diff --git a/extensions/openai/image-generation-provider.ts b/extensions/openai/image-generation-provider.ts index 2f8a4035fe6..8a1a1a5e429 100644 --- a/extensions/openai/image-generation-provider.ts +++ b/extensions/openai/image-generation-provider.ts @@ -90,7 +90,6 @@ export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider { resolveProviderHttpRequestConfig({ baseUrl: resolveOpenAIBaseUrl(req.cfg), defaultBaseUrl: DEFAULT_OPENAI_IMAGE_BASE_URL, - allowPrivateNetwork: false, defaultHeaders: { Authorization: `Bearer ${auth.apiKey}`, }, diff --git a/extensions/qa-lab/src/mock-openai-server.test.ts b/extensions/qa-lab/src/mock-openai-server.test.ts index f4447d19511..572b22db21e 100644 --- a/extensions/qa-lab/src/mock-openai-server.test.ts +++ b/extensions/qa-lab/src/mock-openai-server.test.ts @@ -251,6 +251,17 @@ describe("qa mock openai server", () => { expect(await image.json()).toMatchObject({ data: [{ b64_json: expect.any(String) }], }); + + const imageRequests = await fetch(`${server.baseUrl}/debug/image-generations`); + expect(imageRequests.status).toBe(200); + expect(await imageRequests.json()).toMatchObject([ + { + model: "gpt-image-1", + prompt: "Draw a QA lighthouse", + n: 1, + size: "1024x1024", + }, + ]); }); it("returns exact markers for visible and hot-installed skills", async () => { diff --git a/extensions/qa-lab/src/mock-openai-server.ts b/extensions/qa-lab/src/mock-openai-server.ts index b7f7557cddc..b555bcbde91 100644 --- a/extensions/qa-lab/src/mock-openai-server.ts +++ b/extensions/qa-lab/src/mock-openai-server.ts @@ -279,6 +279,11 @@ function extractOrbitCode(text: string) { return /\b(?:ORBIT-9|orbit-9)\b/.exec(text)?.[0]?.toUpperCase() ?? null; } +function extractExactReplyDirective(text: string) { + const match = /reply with exactly:\s*([^\n]+)/i.exec(text); + return match?.[1]?.trim() || null; +} + function buildAssistantText(input: ResponsesInputItem[], body: Record) { const prompt = extractLastUserText(input); const toolOutput = extractToolOutput(input); @@ -295,6 +300,7 @@ function buildAssistantText(input: ResponsesInputItem[], body: Record