






















@@ -171,6 +171,275 @@ describe("fal image-generation provider", () => {
171171});
172172});
173173174+it("routes GPT Image 2 edits through /edit with image_urls", async () => {
175+vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
176+apiKey: "fal-test-key",
177+source: "env",
178+mode: "api-key",
179+});
180+_setFalFetchGuardForTesting(fetchWithSsrFGuardMock);
181+fetchWithSsrFGuardMock
182+.mockResolvedValueOnce({
183+response: new Response(
184+JSON.stringify({
185+images: [{ url: "https://v3.fal.media/files/example/gpt-edited.png" }],
186+}),
187+{
188+status: 200,
189+headers: { "Content-Type": "application/json" },
190+},
191+),
192+release: vi.fn(async () => {}),
193+})
194+.mockResolvedValueOnce({
195+response: new Response(Buffer.from("gpt-edited-data"), {
196+status: 200,
197+headers: { "content-type": "image/png" },
198+}),
199+release: vi.fn(async () => {}),
200+});
201+202+const provider = buildFalImageGenerationProvider();
203+await provider.generateImage({
204+provider: "fal",
205+model: "openai/gpt-image-2",
206+prompt: "combine these references",
207+cfg: {},
208+aspectRatio: "16:9",
209+inputImages: [
210+{ buffer: Buffer.from("first"), mimeType: "image/png" },
211+{ buffer: Buffer.from("second"), mimeType: "image/jpeg" },
212+],
213+});
214+215+expectFalJsonPost({
216+call: 1,
217+url: "https://fal.run/openai/gpt-image-2/edit",
218+body: {
219+prompt: "combine these references",
220+image_size: "landscape_16_9",
221+num_images: 1,
222+output_format: "png",
223+image_urls: [
224+`data:image/png;base64,${Buffer.from("first").toString("base64")}`,
225+`data:image/jpeg;base64,${Buffer.from("second").toString("base64")}`,
226+],
227+},
228+});
229+});
230+231+it("allows GPT Image 2 edits up to 10 reference images", async () => {
232+vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
233+apiKey: "fal-test-key",
234+source: "env",
235+mode: "api-key",
236+});
237+_setFalFetchGuardForTesting(fetchWithSsrFGuardMock);
238+fetchWithSsrFGuardMock
239+.mockResolvedValueOnce({
240+response: new Response(
241+JSON.stringify({
242+images: [{ url: "https://v3.fal.media/files/example/gpt-edited.png" }],
243+}),
244+{
245+status: 200,
246+headers: { "Content-Type": "application/json" },
247+},
248+),
249+release: vi.fn(async () => {}),
250+})
251+.mockResolvedValueOnce({
252+response: new Response(Buffer.from("gpt-edited-data"), {
253+status: 200,
254+headers: { "content-type": "image/png" },
255+}),
256+release: vi.fn(async () => {}),
257+});
258+259+const inputImages = Array.from({ length: 10 }, (_, index) => ({
260+buffer: Buffer.from(`ref-${index + 1}`),
261+mimeType: "image/png",
262+}));
263+264+const provider = buildFalImageGenerationProvider();
265+await provider.generateImage({
266+provider: "fal",
267+model: "openai/gpt-image-2",
268+prompt: "combine all references",
269+cfg: {},
270+ inputImages,
271+});
272+273+expectFalJsonPost({
274+call: 1,
275+url: "https://fal.run/openai/gpt-image-2/edit",
276+body: {
277+prompt: "combine all references",
278+num_images: 1,
279+output_format: "png",
280+image_urls: inputImages.map(
281+(image) => `data:image/png;base64,${image.buffer.toString("base64")}`,
282+),
283+},
284+});
285+});
286+287+it("rejects GPT Image 2 edits above 10 reference images", async () => {
288+vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
289+apiKey: "fal-test-key",
290+source: "env",
291+mode: "api-key",
292+});
293+_setFalFetchGuardForTesting(fetchWithSsrFGuardMock);
294+295+const provider = buildFalImageGenerationProvider();
296+await expect(
297+provider.generateImage({
298+provider: "fal",
299+model: "openai/gpt-image-2",
300+prompt: "too many references",
301+cfg: {},
302+inputImages: Array.from({ length: 11 }, () => ({
303+buffer: Buffer.from("ref"),
304+mimeType: "image/png",
305+})),
306+}),
307+).rejects.toThrow("fal GPT Image edit supports at most 10 reference images");
308+expect(fetchWithSsrFGuardMock).not.toHaveBeenCalled();
309+});
310+311+it("routes Nano Banana 2 edits through /edit with NB2 geometry", async () => {
312+vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
313+apiKey: "fal-test-key",
314+source: "env",
315+mode: "api-key",
316+});
317+_setFalFetchGuardForTesting(fetchWithSsrFGuardMock);
318+fetchWithSsrFGuardMock
319+.mockResolvedValueOnce({
320+response: new Response(
321+JSON.stringify({
322+images: [{ url: "https://v3.fal.media/files/example/nb2-edited.png" }],
323+}),
324+{
325+status: 200,
326+headers: { "Content-Type": "application/json" },
327+},
328+),
329+release: vi.fn(async () => {}),
330+})
331+.mockResolvedValueOnce({
332+response: new Response(Buffer.from("nb2-edited-data"), {
333+status: 200,
334+headers: { "content-type": "image/png" },
335+}),
336+release: vi.fn(async () => {}),
337+});
338+339+const provider = buildFalImageGenerationProvider();
340+await provider.generateImage({
341+provider: "fal",
342+model: "fal-ai/nano-banana-2",
343+prompt: "blend these references",
344+cfg: {},
345+aspectRatio: "9:16",
346+resolution: "2K",
347+inputImages: [
348+{ buffer: Buffer.from("first"), mimeType: "image/png" },
349+{ buffer: Buffer.from("second"), mimeType: "image/png" },
350+],
351+});
352+353+expectFalJsonPost({
354+call: 1,
355+url: "https://fal.run/fal-ai/nano-banana-2/edit",
356+body: {
357+prompt: "blend these references",
358+aspect_ratio: "9:16",
359+resolution: "2K",
360+num_images: 1,
361+output_format: "png",
362+image_urls: [
363+`data:image/png;base64,${Buffer.from("first").toString("base64")}`,
364+`data:image/png;base64,${Buffer.from("second").toString("base64")}`,
365+],
366+},
367+});
368+});
369+370+it("rejects Nano Banana 2 edits above 14 reference images", async () => {
371+vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
372+apiKey: "fal-test-key",
373+source: "env",
374+mode: "api-key",
375+});
376+_setFalFetchGuardForTesting(fetchWithSsrFGuardMock);
377+378+const provider = buildFalImageGenerationProvider();
379+await expect(
380+provider.generateImage({
381+provider: "fal",
382+model: "fal-ai/nano-banana-2",
383+prompt: "too many references",
384+cfg: {},
385+inputImages: Array.from({ length: 15 }, () => ({
386+buffer: Buffer.from("ref"),
387+mimeType: "image/png",
388+})),
389+}),
390+).rejects.toThrow("fal Nano Banana edit supports at most 14 reference images");
391+expect(fetchWithSsrFGuardMock).not.toHaveBeenCalled();
392+});
393+394+it("preserves exact custom Fal edit endpoints", async () => {
395+vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
396+apiKey: "fal-test-key",
397+source: "env",
398+mode: "api-key",
399+});
400+_setFalFetchGuardForTesting(fetchWithSsrFGuardMock);
401+fetchWithSsrFGuardMock
402+.mockResolvedValueOnce({
403+response: new Response(
404+JSON.stringify({
405+images: [{ url: "https://v3.fal.media/files/example/custom-edit.png" }],
406+}),
407+{
408+status: 200,
409+headers: { "Content-Type": "application/json" },
410+},
411+),
412+release: vi.fn(async () => {}),
413+})
414+.mockResolvedValueOnce({
415+response: new Response(Buffer.from("custom-edit-data"), {
416+status: 200,
417+headers: { "content-type": "image/png" },
418+}),
419+release: vi.fn(async () => {}),
420+});
421+422+const provider = buildFalImageGenerationProvider();
423+await provider.generateImage({
424+provider: "fal",
425+model: "fal-ai/custom/edit",
426+prompt: "edit through custom endpoint",
427+cfg: {},
428+inputImages: [{ buffer: Buffer.from("source-image"), mimeType: "image/png" }],
429+});
430+431+expectFalJsonPost({
432+call: 1,
433+url: "https://fal.run/fal-ai/custom/edit",
434+body: {
435+prompt: "edit through custom endpoint",
436+num_images: 1,
437+output_format: "png",
438+image_url: `data:image/png;base64,${Buffer.from("source-image").toString("base64")}`,
439+},
440+});
441+});
442+174443it("maps aspect ratio for text generation without forcing a square default", async () => {
175444vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
176445apiKey: "fal-test-key",
@@ -270,7 +539,7 @@ describe("fal image-generation provider", () => {
270539});
271540});
272541273-it("rejects multi-image edit requests for now", async () => {
542+it("rejects multi-image for Flux edit", async () => {
274543vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
275544apiKey: "fal-test-key",
276545source: "env",
@@ -292,7 +561,7 @@ describe("fal image-generation provider", () => {
292561).rejects.toThrow("at most one reference image");
293562});
294563295-it("rejects aspect ratio overrides for the current edit endpoint", async () => {
564+it("rejects aspect ratio for Flux edit", async () => {
296565vi.spyOn(providerAuth, "resolveApiKeyForProvider").mockResolvedValue({
297566apiKey: "fal-test-key",
298567source: "env",
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。