Skip to content

Commit f40d094

Browse files
vercel-ai-sdk[bot]kevinjosethomasgr2m
authored
Backport: feat(fal): deprecate snake_case provider options in favor of camelCase (#10296)
This is an automated backport of #10280 to the release-v5.0 branch. --------- Co-authored-by: Kevin Thomas <[email protected]> Co-authored-by: Gregor Martynus <[email protected]>
1 parent 2145eb4 commit f40d094

File tree

12 files changed

+481
-29
lines changed

12 files changed

+481
-29
lines changed

.changeset/fuzzy-squids-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/fal': patch
3+
---
4+
5+
Create a schema for Fal AI providerOptions, and deprecate snake_case parameters for camelCase options

content/providers/01-ai-sdk-providers/10-fal.mdx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ await generateImage({
152152
prompt: 'Put a donut next to the flour.',
153153
providerOptions: {
154154
fal: {
155-
image_url:
155+
imageUrl:
156156
'https://v3.fal.media/files/rabbit/rmgBxhwGYb2d3pl3x9sKf_output.png',
157157
},
158158
},
@@ -163,11 +163,21 @@ await generateImage({
163163

164164
Fal image models support flexible provider options through the `providerOptions.fal` object. You can pass any parameters supported by the specific Fal model's API. Common options include:
165165

166-
- **image_url** - Reference image URL for image-to-image generation
166+
- **imageUrl** - Reference image URL for image-to-image generation
167167
- **strength** - Controls how much the output differs from the input image
168-
- **guidance_scale** - Controls adherence to the prompt
169-
- **num_inference_steps** - Number of denoising steps
170-
- **safety_checker** - Enable/disable safety filtering
168+
- **guidanceScale** - Controls adherence to the prompt (range: 1-20)
169+
- **numInferenceSteps** - Number of denoising steps (range: 1-50)
170+
- **enableSafetyChecker** - Enable/disable safety filtering
171+
- **outputFormat** - Output format: 'jpeg' or 'png'
172+
- **syncMode** - Wait for completion before returning response
173+
- **acceleration** - Speed of generation: 'none', 'regular', or 'high'
174+
- **safetyTolerance** - Content safety filtering level (1-6, where 1 is strictest)
175+
176+
<Note type="warning">
177+
**Deprecation Notice**: snake_case parameter names (e.g., `image_url`,
178+
`guidance_scale`) are deprecated and will be removed in `@ai-sdk/fal` v2.0.
179+
Please use camelCase names (e.g., `imageUrl`, `guidanceScale`) instead.
180+
</Note>
171181

172182
Refer to the [Fal AI model documentation](https://fal.ai/models) for model-specific parameters.
173183

examples/ai-core/src/generate-image/fal-kontext.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fal } from '@ai-sdk/fal';
1+
import { fal, type FalImageProviderOptions } from '@ai-sdk/fal';
22
import { experimental_generateImage as generateImage } from 'ai';
33
import { presentImages } from '../lib/present-image';
44
import 'dotenv/config';
@@ -9,9 +9,9 @@ async function main() {
99
prompt: 'Put a donut next to the flour.',
1010
providerOptions: {
1111
fal: {
12-
image_url:
12+
imageUrl:
1313
'https://v3.fal.media/files/rabbit/rmgBxhwGYb2d3pl3x9sKf_output.png',
14-
},
14+
} satisfies FalImageProviderOptions,
1515
},
1616
});
1717
await presentImages(images);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { createTransformer } from '../lib/create-transformer';
2+
3+
export default createTransformer((fileInfo, api, options, context) => {
4+
const { j, root } = context;
5+
6+
// Helper function to convert snake_case to camelCase
7+
function snakeToCamel(str: string): string {
8+
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
9+
}
10+
11+
// Helper function to recursively transform snake_case properties in an object
12+
function transformFalProperties(objExpression: any) {
13+
if (objExpression.type !== 'ObjectExpression') return;
14+
15+
objExpression.properties.forEach((prop: any) => {
16+
if (
17+
(prop.type === 'ObjectProperty' || prop.type === 'Property') &&
18+
prop.key.type === 'Identifier'
19+
) {
20+
const originalName = prop.key.name;
21+
const camelCaseName = snakeToCamel(originalName);
22+
23+
// Only transform if the name actually changes (contains snake_case)
24+
if (originalName !== camelCaseName && originalName.includes('_')) {
25+
context.hasChanges = true;
26+
prop.key = j.identifier(camelCaseName);
27+
}
28+
29+
// Recursively transform nested objects
30+
if (prop.value.type === 'ObjectExpression') {
31+
transformFalProperties(prop.value);
32+
}
33+
}
34+
});
35+
}
36+
37+
// Find all ObjectExpression nodes that could contain providerOptions
38+
root.find(j.ObjectExpression).forEach(objectPath => {
39+
// Look for providerOptions property within this object
40+
objectPath.node.properties.forEach((prop: any) => {
41+
if (
42+
(prop.type === 'ObjectProperty' || prop.type === 'Property') &&
43+
prop.key.type === 'Identifier' &&
44+
prop.key.name === 'providerOptions' &&
45+
prop.value.type === 'ObjectExpression'
46+
) {
47+
// Found providerOptions, now look for fal property
48+
prop.value.properties.forEach((providerProp: any) => {
49+
if (
50+
(providerProp.type === 'ObjectProperty' ||
51+
providerProp.type === 'Property') &&
52+
providerProp.key.type === 'Identifier' &&
53+
providerProp.key.name === 'fal' &&
54+
providerProp.value.type === 'ObjectExpression'
55+
) {
56+
// Found fal, transform its properties
57+
transformFalProperties(providerProp.value);
58+
}
59+
});
60+
}
61+
});
62+
});
63+
});

packages/codemod/src/lib/upgrade.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ const bundle = [
5858
'v5/rename-todatastreamresponse-to-touimessagestreamresponse',
5959
'v5/rename-tool-parameters-to-inputschema',
6060
'v5/replace-bedrock-snake-case',
61+
'v5/replace-fal-snake-case',
6162
'v5/replace-content-with-parts',
6263
'v5/replace-experimental-provider-metadata',
6364
'v5/replace-image-type-with-file-type',
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// @ts-nocheck
2+
import { fal } from '@ai-sdk/fal';
3+
import { experimental_generateImage as generateImage } from 'ai';
4+
5+
// Test 1: Basic snake_case conversion
6+
const result1 = await generateImage({
7+
model: fal.image('fal-ai/flux/dev'),
8+
prompt: 'A cute baby sea otter',
9+
providerOptions: {
10+
fal: {
11+
guidance_scale: 7.5,
12+
num_inference_steps: 28,
13+
enable_safety_checker: true,
14+
},
15+
},
16+
});
17+
18+
// Test 2: Image-to-image with image_url
19+
const result2 = await generateImage({
20+
model: fal.image('fal-ai/flux/dev/image-to-image'),
21+
prompt: 'Transform this',
22+
providerOptions: {
23+
fal: {
24+
image_url: 'https://example.com/image.png',
25+
guidance_scale: 7.5,
26+
num_inference_steps: 50,
27+
},
28+
},
29+
});
30+
31+
// Test 3: Mixed snake_case options
32+
const result3 = await generateImage({
33+
model: fal.image('fal-ai/flux/dev'),
34+
prompt: 'Abstract art',
35+
providerOptions: {
36+
fal: {
37+
output_format: 'png',
38+
sync_mode: true,
39+
safety_tolerance: 5,
40+
},
41+
},
42+
});
43+
44+
// Test 4: Already camelCase (should not change)
45+
const result4 = await generateImage({
46+
model: fal.image('fal-ai/flux/dev'),
47+
prompt: 'Landscape',
48+
providerOptions: {
49+
fal: {
50+
guidanceScale: 7.0,
51+
numInferenceSteps: 20,
52+
},
53+
},
54+
});
55+
56+
// Test 5: Nested objects
57+
const config = {
58+
providerOptions: {
59+
fal: {
60+
guidance_scale: 8.0,
61+
enable_safety_checker: false,
62+
},
63+
},
64+
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// @ts-nocheck
2+
import { fal } from '@ai-sdk/fal';
3+
import { experimental_generateImage as generateImage } from 'ai';
4+
5+
// Test 1: Basic snake_case conversion
6+
const result1 = await generateImage({
7+
model: fal.image('fal-ai/flux/dev'),
8+
prompt: 'A cute baby sea otter',
9+
providerOptions: {
10+
fal: {
11+
guidanceScale: 7.5,
12+
numInferenceSteps: 28,
13+
enableSafetyChecker: true,
14+
},
15+
},
16+
});
17+
18+
// Test 2: Image-to-image with image_url
19+
const result2 = await generateImage({
20+
model: fal.image('fal-ai/flux/dev/image-to-image'),
21+
prompt: 'Transform this',
22+
providerOptions: {
23+
fal: {
24+
imageUrl: 'https://example.com/image.png',
25+
guidanceScale: 7.5,
26+
numInferenceSteps: 50,
27+
},
28+
},
29+
});
30+
31+
// Test 3: Mixed snake_case options
32+
const result3 = await generateImage({
33+
model: fal.image('fal-ai/flux/dev'),
34+
prompt: 'Abstract art',
35+
providerOptions: {
36+
fal: {
37+
outputFormat: 'png',
38+
syncMode: true,
39+
safetyTolerance: 5,
40+
},
41+
},
42+
});
43+
44+
// Test 4: Already camelCase (should not change)
45+
const result4 = await generateImage({
46+
model: fal.image('fal-ai/flux/dev'),
47+
prompt: 'Landscape',
48+
providerOptions: {
49+
fal: {
50+
guidanceScale: 7.0,
51+
numInferenceSteps: 20,
52+
},
53+
},
54+
});
55+
56+
// Test 5: Nested objects
57+
const config = {
58+
providerOptions: {
59+
fal: {
60+
guidanceScale: 8.0,
61+
enableSafetyChecker: false,
62+
},
63+
},
64+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { describe, it } from 'vitest';
2+
import transformer from '../codemods/v5/replace-fal-snake-case';
3+
import { testTransform } from './test-utils';
4+
5+
describe('replace-fal-snake-case', () => {
6+
it('transforms correctly', () => {
7+
testTransform(transformer, 'replace-fal-snake-case');
8+
});
9+
});

packages/fal/src/fal-image-model.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,81 @@ describe('FalImageModel', () => {
7373
});
7474
});
7575

76+
it('should convert camelCase provider options to snake_case for API', async () => {
77+
const model = createBasicModel();
78+
79+
const result = await model.doGenerate({
80+
prompt,
81+
n: 1,
82+
size: undefined,
83+
aspectRatio: undefined,
84+
seed: undefined,
85+
providerOptions: {
86+
fal: {
87+
imageUrl: 'https://example.com/image.png',
88+
guidanceScale: 7.5,
89+
numInferenceSteps: 50,
90+
enableSafetyChecker: false,
91+
},
92+
},
93+
});
94+
95+
expect(await server.calls[0].requestBodyJson).toStrictEqual({
96+
prompt,
97+
num_images: 1,
98+
image_url: 'https://example.com/image.png',
99+
guidance_scale: 7.5,
100+
num_inference_steps: 50,
101+
enable_safety_checker: false,
102+
});
103+
104+
expect(result.warnings).toHaveLength(0);
105+
});
106+
107+
it('should accept deprecated snake_case provider options with warning', async () => {
108+
const model = createBasicModel();
109+
110+
const result = await model.doGenerate({
111+
prompt,
112+
n: 1,
113+
size: undefined,
114+
aspectRatio: undefined,
115+
seed: undefined,
116+
providerOptions: {
117+
fal: {
118+
image_url: 'https://example.com/image.png',
119+
guidance_scale: 7.5,
120+
num_inference_steps: 50,
121+
},
122+
},
123+
});
124+
125+
expect(await server.calls[0].requestBodyJson).toStrictEqual({
126+
prompt,
127+
num_images: 1,
128+
image_url: 'https://example.com/image.png',
129+
guidance_scale: 7.5,
130+
num_inference_steps: 50,
131+
});
132+
133+
expect(result.warnings).toHaveLength(1);
134+
expect(result.warnings[0]).toMatchObject({
135+
type: 'other',
136+
message: expect.stringContaining('deprecated snake_case'),
137+
});
138+
139+
const warning = result.warnings[0];
140+
if (warning.type === 'other') {
141+
expect(warning.message).toContain("'image_url' (use 'imageUrl')");
142+
expect(warning.message).toContain(
143+
"'guidance_scale' (use 'guidanceScale')",
144+
);
145+
expect(warning.message).toContain(
146+
"'num_inference_steps' (use 'numInferenceSteps')",
147+
);
148+
}
149+
});
150+
76151
it('should convert aspect ratio to size', async () => {
77152
const model = createBasicModel();
78153

0 commit comments

Comments
 (0)