fix(fal): derive model-specific image aspect ratios (#117784)

Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
Peter Steinberger
2026-08-01 20:52:50 -07:00
committed by GitHub
parent 0eece5f4c9
commit b8cd96fba7
2 changed files with 135 additions and 10 deletions
@@ -1,5 +1,6 @@
// Fal tests cover image generation provider plugin behavior.
import type { ImageGenerationRequest } from "openclaw/plugin-sdk/image-generation";
import { generateImage } from "openclaw/plugin-sdk/image-generation-runtime";
import * as providerAuth from "openclaw/plugin-sdk/provider-auth-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -114,6 +115,121 @@ describe("fal image-generation provider", () => {
);
});
it.each([
{
model: "krea/v2/medium/text-to-image",
supported: "2.35:1",
unsupported: "21:9",
},
{
model: "krea/v2/large/text-to-image",
supported: "2.35:1",
unsupported: "21:9",
},
{
model: "fal-ai/nano-banana-2",
supported: "21:9",
unsupported: "2.35:1",
},
{
model: "fal-ai/nano-banana-2/edit",
supported: "21:9",
unsupported: "2.35:1",
},
])(
"publishes the native aspect-ratio contract for $model",
({ model, supported, unsupported }) => {
const aspectRatios = provider.capabilities.geometry?.aspectRatiosByModel?.[model];
expect(aspectRatios).toContain(supported);
expect(aspectRatios).not.toContain(unsupported);
},
);
it.each([
{
model: "krea/v2/medium/text-to-image",
requested: "21:9",
applied: "2.35:1",
mode: "generate",
},
{
model: "krea/v2/large/text-to-image",
requested: "21:9",
applied: "2.35:1",
mode: "style",
},
{
model: "fal-ai/nano-banana-2",
requested: "2.35:1",
applied: "21:9",
mode: "generate",
},
{
model: "fal-ai/nano-banana-2/edit",
requested: "2.35:1",
applied: "21:9",
mode: "edit",
},
])("normalizes unsupported $model geometry before provider submission", async (testCase) => {
const image = sourceImage("reference");
const inputImages = testCase.mode === "generate" ? undefined : [image];
try {
fetchWithSsrFGuardMock
.mockResolvedValueOnce(releasedJson({ images: [{ url: "https://v3.fal.media/out.png" }] }))
.mockResolvedValueOnce(releasedImage(Buffer.from("png-data")));
const result = await generateImage(
{
cfg: {
agents: {
defaults: {
mediaModels: { image: { primary: `fal/${testCase.model}` } },
},
},
},
prompt: "preserve the closest native image shape",
aspectRatio: testCase.requested,
...(inputImages ? { inputImages } : {}),
},
{
getProvider: () => provider,
listProviders: () => [provider],
},
);
expect(result.normalization?.aspectRatio).toEqual({
requested: testCase.requested,
applied: testCase.applied,
});
expectFalJsonPost({
url: `https://fal.run/${testCase.model}`,
body: {
prompt: "preserve the closest native image shape",
aspect_ratio: testCase.applied,
...(testCase.mode === "style"
? {
creativity: "medium",
image_style_references: [
{ image_url: `data:image/png;base64,${image.buffer.toString("base64")}` },
],
}
: testCase.mode === "edit"
? {
num_images: 1,
output_format: "png",
image_urls: [`data:image/png;base64,${image.buffer.toString("base64")}`],
}
: testCase.model.startsWith("krea/")
? { creativity: "medium" }
: { num_images: 1, output_format: "png" }),
},
});
} finally {
fetchWithSsrFGuardMock.mockReset();
}
});
it("generates image buffers from the fal sync API", async () => {
const releaseRequest = vi.fn(async () => {});
const releaseDownload = vi.fn(async () => {});
+19 -10
View File
@@ -682,16 +682,25 @@ export function buildFalImageGenerationProvider(): ImageGenerationProvider {
[FAL_KREA_2_LARGE_MODEL]: [],
},
aspectRatios: [...FAL_SUPPORTED_ASPECT_RATIOS],
aspectRatiosByModel: {
[FAL_NANO_BANANA_MODEL]: [...NANO_BANANA_LEGACY_SUPPORTED_ASPECT_RATIOS],
[`${FAL_NANO_BANANA_MODEL}/edit`]: [...NANO_BANANA_LEGACY_SUPPORTED_ASPECT_RATIOS],
[FAL_NANO_BANANA_2_LITE_MODEL]: [...NANO_BANANA_SUPPORTED_ASPECT_RATIOS],
[`${FAL_NANO_BANANA_2_LITE_MODEL}/edit`]: [...NANO_BANANA_SUPPORTED_ASPECT_RATIOS],
[FAL_GROK_IMAGINE_MODEL]: [...GROK_IMAGINE_SUPPORTED_ASPECT_RATIOS],
[`${FAL_GROK_IMAGINE_MODEL}/edit`]: [...GROK_IMAGINE_SUPPORTED_ASPECT_RATIOS],
[`${FAL_GROK_IMAGINE_MODEL}/quality`]: [...GROK_IMAGINE_SUPPORTED_ASPECT_RATIOS],
[`${FAL_GROK_IMAGINE_MODEL}/quality/edit`]: [...GROK_IMAGINE_SUPPORTED_ASPECT_RATIOS],
},
aspectRatiosByModel: Object.fromEntries(
[
FAL_NANO_BANANA_MODEL,
`${FAL_NANO_BANANA_MODEL}/edit`,
FAL_NANO_BANANA_2_LITE_MODEL,
`${FAL_NANO_BANANA_2_LITE_MODEL}/edit`,
FAL_GROK_IMAGINE_MODEL,
`${FAL_GROK_IMAGINE_MODEL}/edit`,
`${FAL_GROK_IMAGINE_MODEL}/quality`,
`${FAL_GROK_IMAGINE_MODEL}/quality/edit`,
FAL_KREA_2_MEDIUM_MODEL,
FAL_KREA_2_LARGE_MODEL,
`${FAL_NANO_BANANA_MODEL}-2`,
`${FAL_NANO_BANANA_MODEL}-2/edit`,
].flatMap((model) => {
const aspectRatios = resolveFalImageModelSchema(model).aspectRatios;
return aspectRatios ? [[model, [...aspectRatios]] as const] : [];
}),
),
resolutions: ["1K", "2K", "4K"],
resolutionsByModel: {
[FAL_KREA_2_MEDIUM_MODEL]: [],