
























@@ -4,11 +4,13 @@ import { buildOpenAIImageGenerationProvider } from "./image-generation-provider.
44const {
55 resolveApiKeyForProviderMock,
66 postJsonRequestMock,
7+ postMultipartRequestMock,
78 assertOkOrThrowHttpErrorMock,
89 resolveProviderHttpRequestConfigMock,
910} = vi.hoisted(() => ({
1011resolveApiKeyForProviderMock: vi.fn(async () => ({ apiKey: "openai-key" })),
1112postJsonRequestMock: vi.fn(),
13+postMultipartRequestMock: vi.fn(),
1214assertOkOrThrowHttpErrorMock: vi.fn(async () => {}),
1315resolveProviderHttpRequestConfigMock: vi.fn((params) => ({
1416baseUrl: params.baseUrl ?? params.defaultBaseUrl,
@@ -25,16 +27,22 @@ vi.mock("openclaw/plugin-sdk/provider-auth-runtime", () => ({
2527vi.mock("openclaw/plugin-sdk/provider-http", () => ({
2628assertOkOrThrowHttpError: assertOkOrThrowHttpErrorMock,
2729postJsonRequest: postJsonRequestMock,
30+postMultipartRequest: postMultipartRequestMock,
2831resolveProviderHttpRequestConfig: resolveProviderHttpRequestConfigMock,
2932}));
30333134function mockGeneratedPngResponse() {
35+const response = {
36+json: async () => ({
37+data: [{ b64_json: Buffer.from("png-bytes").toString("base64") }],
38+}),
39+};
3240postJsonRequestMock.mockResolvedValue({
33-response: {
34- json: async () => ({
35- data: [{ b64_json: Buffer.from("png-bytes").toString("base64") }],
36- }),
37-},
41+ response,
42+release: vi.fn(async () => {}),
43+});
44+postMultipartRequestMock.mockResolvedValue({
45+response,
3846release: vi.fn(async () => {}),
3947});
4048}
@@ -43,6 +51,7 @@ describe("openai image generation provider", () => {
4351afterEach(() => {
4452resolveApiKeyForProviderMock.mockClear();
4553postJsonRequestMock.mockReset();
54+postMultipartRequestMock.mockReset();
4655assertOkOrThrowHttpErrorMock.mockClear();
4756resolveProviderHttpRequestConfigMock.mockClear();
4857vi.unstubAllEnvs();
@@ -212,28 +221,71 @@ describe("openai image generation provider", () => {
212221],
213222});
214223215-expect(postJsonRequestMock).toHaveBeenCalledWith(
224+expect(postMultipartRequestMock).toHaveBeenCalledWith(
216225expect.objectContaining({
217226url: "https://api.openai.com/v1/images/edits",
218-body: expect.objectContaining({
219-model: "gpt-image-2",
220-prompt: "Change only the background to pale blue",
221-n: 2,
222-size: "1024x1536",
223-images: [
224-{
225-image_url: "data:image/png;base64,cG5nLWJ5dGVz",
226-},
227-{
228-image_url: "data:image/jpeg;base64,anBlZy1ieXRlcw==",
229-},
230-],
231-}),
227+body: expect.any(FormData),
228+allowPrivateNetwork: false,
229+dispatcherPolicy: undefined,
230+fetchFn: fetch,
232231}),
233232);
233+const editCallArgs = postMultipartRequestMock.mock.calls[0]?.[0] as {
234+headers: Headers;
235+body: FormData;
236+};
237+expect(editCallArgs.headers.has("Content-Type")).toBe(false);
238+const form = editCallArgs.body;
239+expect(form.get("model")).toBe("gpt-image-2");
240+expect(form.get("prompt")).toBe("Change only the background to pale blue");
241+expect(form.get("n")).toBe("2");
242+expect(form.get("size")).toBe("1024x1536");
243+const images = form.getAll("image[]") as File[];
244+expect(images).toHaveLength(2);
245+expect(images[0]?.name).toBe("reference.png");
246+expect(images[0]?.type).toBe("image/png");
247+expect(images[1]?.name).toBe("style.jpg");
248+expect(images[1]?.type).toBe("image/jpeg");
249+expect(postJsonRequestMock).not.toHaveBeenCalledWith(
250+expect.objectContaining({ url: "https://api.openai.com/v1/images/edits" }),
251+);
234252expect(result.images).toHaveLength(1);
235253});
236254255+it("forwards SSRF guard fields to multipart edit requests", async () => {
256+mockGeneratedPngResponse();
257+258+const provider = buildOpenAIImageGenerationProvider();
259+await provider.generateImage({
260+provider: "openai",
261+model: "gpt-image-2",
262+prompt: "Edit cat",
263+cfg: {
264+models: {
265+providers: {
266+openai: {
267+baseUrl: "http://127.0.0.1:44080/v1",
268+models: [],
269+},
270+},
271+},
272+},
273+inputImages: [{ buffer: Buffer.from("png-bytes"), mimeType: "image/png" }],
274+});
275+276+expect(postMultipartRequestMock).toHaveBeenCalledWith(
277+expect.objectContaining({
278+url: "http://127.0.0.1:44080/v1/images/edits",
279+allowPrivateNetwork: false,
280+dispatcherPolicy: undefined,
281+fetchFn: fetch,
282+}),
283+);
284+expect(postJsonRequestMock).not.toHaveBeenCalledWith(
285+expect.objectContaining({ url: "http://127.0.0.1:44080/v1/images/edits" }),
286+);
287+});
288+237289describe("azure openai support", () => {
238290it("uses api-key header and deployment-scoped URL for Azure .openai.azure.com hosts", async () => {
239291mockGeneratedPngResponse();
@@ -386,9 +438,10 @@ describe("openai image generation provider", () => {
386438],
387439});
388440389-expect(postJsonRequestMock).toHaveBeenCalledWith(
441+expect(postMultipartRequestMock).toHaveBeenCalledWith(
390442expect.objectContaining({
391443url: "https://myresource.openai.azure.com/openai/deployments/gpt-image-2/images/edits?api-version=2024-12-01-preview",
444+body: expect.any(FormData),
392445}),
393446);
394447});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。