@ai-sdk/fal 2.0.11 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # @ai-sdk/fal
2
2
 
3
+ ## 2.0.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 4de5a1d: chore: excluded tests from src folder in npm package
8
+ - Updated dependencies [4de5a1d]
9
+ - @ai-sdk/provider@3.0.5
10
+ - @ai-sdk/provider-utils@4.0.9
11
+
3
12
  ## 2.0.11
4
13
 
5
14
  ### Patch Changes
package/dist/index.js CHANGED
@@ -764,7 +764,7 @@ var falSpeechResponseSchema = import_v45.z.object({
764
764
  });
765
765
 
766
766
  // src/version.ts
767
- var VERSION = true ? "2.0.11" : "0.0.0-test";
767
+ var VERSION = true ? "2.0.12" : "0.0.0-test";
768
768
 
769
769
  // src/fal-provider.ts
770
770
  var defaultBaseURL = "https://fal.run";
package/dist/index.mjs CHANGED
@@ -771,7 +771,7 @@ var falSpeechResponseSchema = z5.object({
771
771
  });
772
772
 
773
773
  // src/version.ts
774
- var VERSION = true ? "2.0.11" : "0.0.0-test";
774
+ var VERSION = true ? "2.0.12" : "0.0.0-test";
775
775
 
776
776
  // src/fal-provider.ts
777
777
  var defaultBaseURL = "https://fal.run";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/fal",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -10,6 +10,10 @@
10
10
  "dist/**/*",
11
11
  "docs/**/*",
12
12
  "src",
13
+ "!src/**/*.test.ts",
14
+ "!src/**/*.test-d.ts",
15
+ "!src/**/__snapshots__",
16
+ "!src/**/__fixtures__",
13
17
  "CHANGELOG.md",
14
18
  "README.md"
15
19
  ],
@@ -25,15 +29,15 @@
25
29
  }
26
30
  },
27
31
  "dependencies": {
28
- "@ai-sdk/provider": "3.0.4",
29
- "@ai-sdk/provider-utils": "4.0.8"
32
+ "@ai-sdk/provider": "3.0.5",
33
+ "@ai-sdk/provider-utils": "4.0.9"
30
34
  },
31
35
  "devDependencies": {
32
36
  "@types/node": "20.17.24",
33
37
  "tsup": "^8",
34
38
  "typescript": "5.8.3",
35
39
  "zod": "3.25.76",
36
- "@ai-sdk/test-server": "1.0.2",
40
+ "@ai-sdk/test-server": "1.0.3",
37
41
  "@vercel/ai-tsconfig": "0.0.0"
38
42
  },
39
43
  "peerDependencies": {
@@ -1,34 +0,0 @@
1
- import { safeParseJSON } from '@ai-sdk/provider-utils';
2
- import { falErrorDataSchema } from './fal-error';
3
- import { describe, it, expect } from 'vitest';
4
-
5
- describe('falErrorDataSchema', () => {
6
- it('should parse Fal resource exhausted error', async () => {
7
- const error = `
8
- {"error":{"message":"{\\n \\"error\\": {\\n \\"code\\": 429,\\n \\"message\\": \\"Resource has been exhausted (e.g. check quota).\\",\\n \\"status\\": \\"RESOURCE_EXHAUSTED\\"\\n }\\n}\\n","code":429}}
9
- `;
10
-
11
- const result = await safeParseJSON({
12
- text: error,
13
- schema: falErrorDataSchema,
14
- });
15
-
16
- expect(result).toStrictEqual({
17
- success: true,
18
- value: {
19
- error: {
20
- message:
21
- '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n',
22
- code: 429,
23
- },
24
- },
25
- rawValue: {
26
- error: {
27
- message:
28
- '{\n "error": {\n "code": 429,\n "message": "Resource has been exhausted (e.g. check quota).",\n "status": "RESOURCE_EXHAUSTED"\n }\n}\n',
29
- code: 429,
30
- },
31
- },
32
- });
33
- });
34
- });
@@ -1,930 +0,0 @@
1
- import { FetchFunction } from '@ai-sdk/provider-utils';
2
- import { createTestServer } from '@ai-sdk/test-server/with-vitest';
3
- import { describe, expect, it } from 'vitest';
4
- import { FalImageModel } from './fal-image-model';
5
-
6
- const prompt = 'A cute baby sea otter';
7
-
8
- function createBasicModel({
9
- headers,
10
- fetch,
11
- currentDate,
12
- }: {
13
- headers?: Record<string, string | undefined>;
14
- fetch?: FetchFunction;
15
- currentDate?: () => Date;
16
- settings?: any;
17
- } = {}) {
18
- return new FalImageModel('fal-ai/qwen-image', {
19
- provider: 'fal.image',
20
- baseURL: 'https://api.example.com',
21
- headers: headers ?? { 'api-key': 'test-key' },
22
- fetch,
23
- _internal: {
24
- currentDate,
25
- },
26
- });
27
- }
28
-
29
- describe('FalImageModel', () => {
30
- const server = createTestServer({
31
- 'https://api.example.com/fal-ai/qwen-image': {
32
- response: {
33
- type: 'json-value',
34
- body: {
35
- images: [
36
- {
37
- url: 'https://api.example.com/image.png',
38
- width: 1024,
39
- height: 1024,
40
- content_type: 'image/png',
41
- },
42
- ],
43
- },
44
- },
45
- },
46
- 'https://api.example.com/image.png': {
47
- response: {
48
- type: 'binary',
49
- body: Buffer.from('test-binary-content'),
50
- },
51
- },
52
- });
53
-
54
- describe('doGenerate', () => {
55
- it('should pass the correct parameters including size', async () => {
56
- const model = createBasicModel();
57
-
58
- await model.doGenerate({
59
- prompt,
60
- files: undefined,
61
- mask: undefined,
62
- n: 1,
63
- size: '1024x1024',
64
- aspectRatio: undefined,
65
- seed: 123,
66
- providerOptions: { fal: { additional_param: 'value' } },
67
- });
68
-
69
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
70
- prompt,
71
- seed: 123,
72
- image_size: { width: 1024, height: 1024 },
73
- num_images: 1,
74
- additional_param: 'value',
75
- });
76
- });
77
-
78
- it('should convert camelCase provider options to snake_case for API', async () => {
79
- const model = createBasicModel();
80
-
81
- const result = await model.doGenerate({
82
- prompt,
83
- files: undefined,
84
- mask: undefined,
85
- n: 1,
86
- size: undefined,
87
- aspectRatio: undefined,
88
- seed: undefined,
89
- providerOptions: {
90
- fal: {
91
- imageUrl: 'https://example.com/image.png',
92
- guidanceScale: 7.5,
93
- numInferenceSteps: 50,
94
- enableSafetyChecker: false,
95
- },
96
- },
97
- });
98
-
99
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
100
- prompt,
101
- num_images: 1,
102
- image_url: 'https://example.com/image.png',
103
- guidance_scale: 7.5,
104
- num_inference_steps: 50,
105
- enable_safety_checker: false,
106
- });
107
-
108
- expect(result.warnings).toHaveLength(0);
109
- });
110
-
111
- it('should accept deprecated snake_case provider options with warning', async () => {
112
- const model = createBasicModel();
113
-
114
- const result = await model.doGenerate({
115
- prompt,
116
- files: undefined,
117
- mask: undefined,
118
- n: 1,
119
- size: undefined,
120
- aspectRatio: undefined,
121
- seed: undefined,
122
- providerOptions: {
123
- fal: {
124
- image_url: 'https://example.com/image.png',
125
- guidance_scale: 7.5,
126
- num_inference_steps: 50,
127
- },
128
- },
129
- });
130
-
131
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
132
- prompt,
133
- num_images: 1,
134
- image_url: 'https://example.com/image.png',
135
- guidance_scale: 7.5,
136
- num_inference_steps: 50,
137
- });
138
-
139
- expect(result.warnings).toHaveLength(1);
140
- expect(result.warnings[0]).toMatchObject({
141
- type: 'other',
142
- message: expect.stringContaining('deprecated snake_case'),
143
- });
144
-
145
- const warning = result.warnings[0];
146
- if (warning.type === 'other') {
147
- expect(warning.message).toContain("'image_url' (use 'imageUrl')");
148
- expect(warning.message).toContain(
149
- "'guidance_scale' (use 'guidanceScale')",
150
- );
151
- expect(warning.message).toContain(
152
- "'num_inference_steps' (use 'numInferenceSteps')",
153
- );
154
- }
155
- });
156
-
157
- it('should convert aspect ratio to size', async () => {
158
- const model = createBasicModel();
159
-
160
- await model.doGenerate({
161
- prompt,
162
- files: undefined,
163
- mask: undefined,
164
- n: 1,
165
- size: undefined,
166
- aspectRatio: '16:9',
167
- seed: undefined,
168
- providerOptions: {},
169
- });
170
-
171
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
172
- prompt,
173
- image_size: 'landscape_16_9',
174
- num_images: 1,
175
- });
176
- });
177
-
178
- it('should pass headers', async () => {
179
- const modelWithHeaders = createBasicModel({
180
- headers: {
181
- 'Custom-Provider-Header': 'provider-header-value',
182
- },
183
- });
184
-
185
- await modelWithHeaders.doGenerate({
186
- prompt,
187
- files: undefined,
188
- mask: undefined,
189
- n: 1,
190
- providerOptions: {},
191
- headers: {
192
- 'Custom-Request-Header': 'request-header-value',
193
- },
194
- size: undefined,
195
- seed: undefined,
196
- aspectRatio: undefined,
197
- });
198
-
199
- expect(server.calls[0].requestHeaders).toStrictEqual({
200
- 'content-type': 'application/json',
201
- 'custom-provider-header': 'provider-header-value',
202
- 'custom-request-header': 'request-header-value',
203
- });
204
- });
205
-
206
- it('should handle API errors', async () => {
207
- server.urls['https://api.example.com/fal-ai/qwen-image'].response = {
208
- type: 'error',
209
- status: 400,
210
- body: JSON.stringify({
211
- detail: [
212
- {
213
- loc: ['prompt'],
214
- msg: 'Invalid prompt',
215
- type: 'value_error',
216
- },
217
- ],
218
- }),
219
- };
220
-
221
- const model = createBasicModel();
222
- await expect(
223
- model.doGenerate({
224
- prompt,
225
- files: undefined,
226
- mask: undefined,
227
- n: 1,
228
- providerOptions: {},
229
- size: undefined,
230
- seed: undefined,
231
- aspectRatio: undefined,
232
- }),
233
- ).rejects.toMatchObject({
234
- message: 'prompt: Invalid prompt',
235
- statusCode: 400,
236
- url: 'https://api.example.com/fal-ai/qwen-image',
237
- });
238
- });
239
-
240
- describe('response metadata', () => {
241
- it('should include timestamp, headers and modelId in response', async () => {
242
- const testDate = new Date('2024-01-01T00:00:00Z');
243
- const model = createBasicModel({
244
- currentDate: () => testDate,
245
- });
246
-
247
- const result = await model.doGenerate({
248
- prompt,
249
- files: undefined,
250
- mask: undefined,
251
- n: 1,
252
- providerOptions: {},
253
- size: undefined,
254
- seed: undefined,
255
- aspectRatio: undefined,
256
- });
257
-
258
- expect(result.response).toStrictEqual({
259
- timestamp: testDate,
260
- modelId: 'fal-ai/qwen-image',
261
- headers: expect.any(Object),
262
- });
263
- });
264
- });
265
-
266
- describe('providerMetaData', () => {
267
- // https://fal.ai/models/fal-ai/lora/api#schema-output
268
- it('for lora', async () => {
269
- const responseMetaData = {
270
- prompt: '<prompt>',
271
- seed: 123,
272
- has_nsfw_concepts: [true],
273
- debug_latents: {
274
- url: '<debug_latents url>',
275
- content_type: '<debug_latents content_type>',
276
- file_name: '<debug_latents file_name>',
277
- file_data: '<debug_latents file_data>',
278
- file_size: 123,
279
- },
280
- debug_per_pass_latents: {
281
- url: '<debug_per_pass_latents url>',
282
- content_type: '<debug_per_pass_latents content_type>',
283
- file_name: '<debug_per_pass_latents file_name>',
284
- file_data: '<debug_per_pass_latents file_data>',
285
- file_size: 456,
286
- },
287
- };
288
- server.urls['https://api.example.com/fal-ai/qwen-image'].response = {
289
- type: 'json-value',
290
- body: {
291
- images: [
292
- {
293
- url: 'https://api.example.com/image.png',
294
- width: 1024,
295
- height: 1024,
296
- content_type: 'image/png',
297
- file_data: '<image file_data>',
298
- file_size: 123,
299
- file_name: '<image file_name>',
300
- },
301
- ],
302
- ...responseMetaData,
303
- },
304
- };
305
- const model = createBasicModel();
306
- const result = await model.doGenerate({
307
- prompt,
308
- files: undefined,
309
- mask: undefined,
310
- n: 1,
311
- providerOptions: {},
312
- size: undefined,
313
- seed: undefined,
314
- aspectRatio: undefined,
315
- });
316
- expect(result.providerMetadata).toStrictEqual({
317
- fal: {
318
- images: [
319
- {
320
- width: 1024,
321
- height: 1024,
322
- contentType: 'image/png',
323
- fileName: '<image file_name>',
324
- fileData: '<image file_data>',
325
- fileSize: 123,
326
- nsfw: true,
327
- },
328
- ],
329
- seed: 123,
330
- debug_latents: {
331
- url: '<debug_latents url>',
332
- content_type: '<debug_latents content_type>',
333
- file_name: '<debug_latents file_name>',
334
- file_data: '<debug_latents file_data>',
335
- file_size: 123,
336
- },
337
- debug_per_pass_latents: {
338
- url: '<debug_per_pass_latents url>',
339
- content_type: '<debug_per_pass_latents content_type>',
340
- file_name: '<debug_per_pass_latents file_name>',
341
- file_data: '<debug_per_pass_latents file_data>',
342
- file_size: 456,
343
- },
344
- },
345
- });
346
- });
347
-
348
- it('for lcm', async () => {
349
- const responseMetaData = {
350
- seed: 123,
351
- num_inference_steps: 456,
352
- nsfw_content_detected: [false],
353
- };
354
- server.urls['https://api.example.com/fal-ai/qwen-image'].response = {
355
- type: 'json-value',
356
- body: {
357
- images: [
358
- {
359
- url: 'https://api.example.com/image.png',
360
- width: 1024,
361
- height: 1024,
362
- },
363
- ],
364
- ...responseMetaData,
365
- },
366
- };
367
- const model = createBasicModel();
368
- const result = await model.doGenerate({
369
- prompt,
370
- files: undefined,
371
- mask: undefined,
372
- n: 1,
373
- providerOptions: {},
374
- size: undefined,
375
- seed: undefined,
376
- aspectRatio: undefined,
377
- });
378
- expect(result.providerMetadata).toStrictEqual({
379
- fal: {
380
- images: [
381
- {
382
- width: 1024,
383
- height: 1024,
384
- nsfw: false,
385
- },
386
- ],
387
- seed: 123,
388
- num_inference_steps: 456,
389
- },
390
- });
391
- });
392
- });
393
- });
394
-
395
- describe('constructor', () => {
396
- it('should expose correct provider and model information', () => {
397
- const model = createBasicModel();
398
-
399
- expect(model.provider).toBe('fal.image');
400
- expect(model.modelId).toBe('fal-ai/qwen-image');
401
- expect(model.specificationVersion).toBe('v3');
402
- expect(model.maxImagesPerCall).toBe(1);
403
- });
404
- });
405
-
406
- describe('Image Editing', () => {
407
- it('should send edit request with files as data URI', async () => {
408
- const imageData = new Uint8Array([137, 80, 78, 71]); // PNG magic bytes
409
-
410
- await createBasicModel().doGenerate({
411
- prompt: 'Turn the cat into a dog',
412
- files: [
413
- {
414
- type: 'file',
415
- data: imageData,
416
- mediaType: 'image/png',
417
- },
418
- ],
419
- mask: undefined,
420
- n: 1,
421
- size: undefined,
422
- aspectRatio: undefined,
423
- seed: undefined,
424
- providerOptions: {},
425
- });
426
-
427
- const requestBody = await server.calls[0].requestBodyJson;
428
- expect(requestBody).toMatchInlineSnapshot(`
429
- {
430
- "image_url": "data:image/png;base64,iVBORw==",
431
- "num_images": 1,
432
- "prompt": "Turn the cat into a dog",
433
- }
434
- `);
435
- });
436
-
437
- it('should send edit request with files and mask', async () => {
438
- const imageData = new Uint8Array([137, 80, 78, 71]);
439
- const maskData = new Uint8Array([255, 255, 255, 0]);
440
-
441
- await createBasicModel().doGenerate({
442
- prompt: 'Add a flamingo to the pool',
443
- files: [
444
- {
445
- type: 'file',
446
- data: imageData,
447
- mediaType: 'image/png',
448
- },
449
- ],
450
- mask: {
451
- type: 'file',
452
- data: maskData,
453
- mediaType: 'image/png',
454
- },
455
- n: 1,
456
- size: undefined,
457
- aspectRatio: undefined,
458
- seed: undefined,
459
- providerOptions: {},
460
- });
461
-
462
- const requestBody = await server.calls[0].requestBodyJson;
463
- expect(requestBody).toMatchInlineSnapshot(`
464
- {
465
- "image_url": "data:image/png;base64,iVBORw==",
466
- "mask_url": "data:image/png;base64,////AA==",
467
- "num_images": 1,
468
- "prompt": "Add a flamingo to the pool",
469
- }
470
- `);
471
- });
472
-
473
- it('should send edit request with URL-based file', async () => {
474
- await createBasicModel().doGenerate({
475
- prompt: 'Edit this image',
476
- files: [
477
- {
478
- type: 'url',
479
- url: 'https://example.com/input.png',
480
- },
481
- ],
482
- mask: undefined,
483
- n: 1,
484
- size: undefined,
485
- aspectRatio: undefined,
486
- seed: undefined,
487
- providerOptions: {},
488
- });
489
-
490
- const requestBody = await server.calls[0].requestBodyJson;
491
- expect(requestBody).toMatchInlineSnapshot(`
492
- {
493
- "image_url": "https://example.com/input.png",
494
- "num_images": 1,
495
- "prompt": "Edit this image",
496
- }
497
- `);
498
- });
499
-
500
- it('should send edit request with base64 string data', async () => {
501
- await createBasicModel().doGenerate({
502
- prompt: 'Edit this image',
503
- files: [
504
- {
505
- type: 'file',
506
- data: 'iVBORw0KGgoAAAANSUhEUgAAAAE=',
507
- mediaType: 'image/png',
508
- },
509
- ],
510
- mask: undefined,
511
- n: 1,
512
- size: undefined,
513
- aspectRatio: undefined,
514
- seed: undefined,
515
- providerOptions: {},
516
- });
517
-
518
- const requestBody = await server.calls[0].requestBodyJson;
519
- expect(requestBody).toMatchInlineSnapshot(`
520
- {
521
- "image_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAE=",
522
- "num_images": 1,
523
- "prompt": "Edit this image",
524
- }
525
- `);
526
- });
527
-
528
- it('should warn when multiple files are provided', async () => {
529
- const imageData = new Uint8Array([137, 80, 78, 71]);
530
-
531
- const result = await createBasicModel().doGenerate({
532
- prompt: 'Edit images',
533
- files: [
534
- {
535
- type: 'file',
536
- data: imageData,
537
- mediaType: 'image/png',
538
- },
539
- {
540
- type: 'file',
541
- data: imageData,
542
- mediaType: 'image/png',
543
- },
544
- ],
545
- mask: undefined,
546
- n: 1,
547
- size: undefined,
548
- aspectRatio: undefined,
549
- seed: undefined,
550
- providerOptions: {},
551
- });
552
-
553
- expect(result.warnings).toHaveLength(1);
554
- expect(result.warnings[0]).toMatchObject({
555
- type: 'other',
556
- message: expect.stringContaining('useMultipleImages is not enabled'),
557
- });
558
- });
559
-
560
- it('should send image_urls when useMultipleImages is true', async () => {
561
- const imageData = new Uint8Array([137, 80, 78, 71]);
562
-
563
- await createBasicModel().doGenerate({
564
- prompt: 'Edit these images',
565
- files: [
566
- {
567
- type: 'file',
568
- data: imageData,
569
- mediaType: 'image/png',
570
- },
571
- {
572
- type: 'file',
573
- data: imageData,
574
- mediaType: 'image/png',
575
- },
576
- ],
577
- mask: undefined,
578
- n: 1,
579
- size: undefined,
580
- aspectRatio: undefined,
581
- seed: undefined,
582
- providerOptions: {
583
- fal: {
584
- useMultipleImages: true,
585
- },
586
- },
587
- });
588
-
589
- const requestBody = await server.calls[0].requestBodyJson;
590
- expect(requestBody).toMatchObject({
591
- image_urls: [
592
- 'data:image/png;base64,iVBORw==',
593
- 'data:image/png;base64,iVBORw==',
594
- ],
595
- num_images: 1,
596
- prompt: 'Edit these images',
597
- });
598
- expect(requestBody.image_url).toBeUndefined();
599
- });
600
-
601
- it('should not warn when multiple files provided with useMultipleImages', async () => {
602
- const imageData = new Uint8Array([137, 80, 78, 71]);
603
-
604
- const result = await createBasicModel().doGenerate({
605
- prompt: 'Edit images',
606
- files: [
607
- { type: 'file', data: imageData, mediaType: 'image/png' },
608
- { type: 'file', data: imageData, mediaType: 'image/png' },
609
- ],
610
- mask: undefined,
611
- n: 1,
612
- size: undefined,
613
- aspectRatio: undefined,
614
- seed: undefined,
615
- providerOptions: {
616
- fal: {
617
- useMultipleImages: true,
618
- },
619
- },
620
- });
621
-
622
- expect(result.warnings).toHaveLength(0);
623
- });
624
-
625
- it('should send single image as image_urls array when useMultipleImages is true', async () => {
626
- const imageData = new Uint8Array([137, 80, 78, 71]);
627
-
628
- await createBasicModel().doGenerate({
629
- prompt: 'Edit this image',
630
- files: [
631
- {
632
- type: 'file',
633
- data: imageData,
634
- mediaType: 'image/png',
635
- },
636
- ],
637
- mask: undefined,
638
- n: 1,
639
- size: undefined,
640
- aspectRatio: undefined,
641
- seed: undefined,
642
- providerOptions: {
643
- fal: {
644
- useMultipleImages: true,
645
- },
646
- },
647
- });
648
-
649
- const requestBody = await server.calls[0].requestBodyJson;
650
- expect(requestBody).toMatchObject({
651
- image_urls: ['data:image/png;base64,iVBORw=='],
652
- num_images: 1,
653
- prompt: 'Edit this image',
654
- });
655
- expect(requestBody.image_url).toBeUndefined();
656
- });
657
-
658
- it('should allow imageUrl via provider options', async () => {
659
- await createBasicModel().doGenerate({
660
- prompt: 'Edit via provider options',
661
- files: undefined,
662
- mask: undefined,
663
- n: 1,
664
- size: undefined,
665
- aspectRatio: undefined,
666
- seed: undefined,
667
- providerOptions: {
668
- fal: {
669
- imageUrl: 'https://example.com/provider-image.png',
670
- },
671
- },
672
- });
673
-
674
- const requestBody = await server.calls[0].requestBodyJson;
675
- expect(requestBody).toMatchInlineSnapshot(`
676
- {
677
- "image_url": "https://example.com/provider-image.png",
678
- "num_images": 1,
679
- "prompt": "Edit via provider options",
680
- }
681
- `);
682
- });
683
-
684
- it('should allow maskUrl via provider options', async () => {
685
- await createBasicModel().doGenerate({
686
- prompt: 'Inpaint this',
687
- files: undefined,
688
- mask: undefined,
689
- n: 1,
690
- size: undefined,
691
- aspectRatio: undefined,
692
- seed: undefined,
693
- providerOptions: {
694
- fal: {
695
- imageUrl: 'https://example.com/image.png',
696
- maskUrl: 'https://example.com/mask.png',
697
- },
698
- },
699
- });
700
-
701
- const requestBody = await server.calls[0].requestBodyJson;
702
- expect(requestBody).toMatchInlineSnapshot(`
703
- {
704
- "image_url": "https://example.com/image.png",
705
- "mask_url": "https://example.com/mask.png",
706
- "num_images": 1,
707
- "prompt": "Inpaint this",
708
- }
709
- `);
710
- });
711
- });
712
-
713
- describe('response schema validation', () => {
714
- it('should parse single image response', async () => {
715
- server.urls['https://api.example.com/fal-ai/qwen-image'].response = {
716
- type: 'json-value',
717
- body: {
718
- image: {
719
- url: 'https://api.example.com/image.png',
720
- width: 1024,
721
- height: 1024,
722
- content_type: 'image/png',
723
- },
724
- },
725
- };
726
-
727
- const model = createBasicModel();
728
- const result = await model.doGenerate({
729
- prompt,
730
- files: undefined,
731
- mask: undefined,
732
- n: 1,
733
- providerOptions: {},
734
- size: undefined,
735
- seed: undefined,
736
- aspectRatio: undefined,
737
- });
738
-
739
- expect(result.images).toHaveLength(1);
740
- expect(result.images[0]).toBeInstanceOf(Uint8Array);
741
- });
742
-
743
- it('should parse multiple images response', async () => {
744
- server.urls['https://api.example.com/fal-ai/qwen-image'].response = {
745
- type: 'json-value',
746
- body: {
747
- images: [
748
- {
749
- url: 'https://api.example.com/image.png',
750
- width: 1024,
751
- height: 1024,
752
- content_type: 'image/png',
753
- },
754
- {
755
- url: 'https://api.example.com/image.png',
756
- width: 1024,
757
- height: 1024,
758
- content_type: 'image/png',
759
- },
760
- ],
761
- },
762
- };
763
-
764
- const model = createBasicModel();
765
- const result = await model.doGenerate({
766
- prompt,
767
- files: undefined,
768
- mask: undefined,
769
- n: 2,
770
- providerOptions: {},
771
- size: undefined,
772
- seed: undefined,
773
- aspectRatio: undefined,
774
- });
775
-
776
- expect(result.images).toHaveLength(2);
777
- expect(result.images[0]).toBeInstanceOf(Uint8Array);
778
- expect(result.images[1]).toBeInstanceOf(Uint8Array);
779
- });
780
-
781
- it('should handle null file_name and file_size values', async () => {
782
- server.urls['https://api.example.com/fal-ai/qwen-image'].response = {
783
- type: 'json-value',
784
- body: {
785
- images: [
786
- {
787
- url: 'https://api.example.com/image.png',
788
- content_type: 'image/png',
789
- file_name: null,
790
- file_size: null,
791
- width: 944,
792
- height: 1104,
793
- },
794
- ],
795
- timings: { inference: 5.875932216644287 },
796
- seed: 328395684,
797
- has_nsfw_concepts: [false],
798
- prompt:
799
- 'A female model holding this book, keeping the book unchanged.',
800
- },
801
- };
802
-
803
- const model = createBasicModel();
804
- const result = await model.doGenerate({
805
- prompt,
806
- files: undefined,
807
- mask: undefined,
808
- n: 1,
809
- providerOptions: {},
810
- size: undefined,
811
- seed: undefined,
812
- aspectRatio: undefined,
813
- });
814
-
815
- expect(result.images).toHaveLength(1);
816
- expect(result.images[0]).toBeInstanceOf(Uint8Array);
817
- expect(result.providerMetadata?.fal).toMatchObject({
818
- images: [
819
- {
820
- width: 944,
821
- height: 1104,
822
- contentType: 'image/png',
823
- fileName: null,
824
- fileSize: null,
825
- nsfw: false,
826
- },
827
- ],
828
- timings: { inference: 5.875932216644287 },
829
- seed: 328395684,
830
- });
831
- });
832
-
833
- it('should handle empty timings object', async () => {
834
- server.urls['https://api.example.com/fal-ai/qwen-image'].response = {
835
- type: 'json-value',
836
- body: {
837
- images: [
838
- {
839
- url: 'https://api.example.com/image.png',
840
- content_type: 'image/png',
841
- file_name: null,
842
- file_size: null,
843
- width: 880,
844
- height: 1184,
845
- },
846
- ],
847
- timings: {},
848
- seed: 235205040,
849
- has_nsfw_concepts: [false],
850
- prompt: 'Change the plates to colorful ones',
851
- },
852
- };
853
-
854
- const model = createBasicModel();
855
- const result = await model.doGenerate({
856
- prompt,
857
- files: undefined,
858
- mask: undefined,
859
- n: 1,
860
- providerOptions: {},
861
- size: undefined,
862
- seed: undefined,
863
- aspectRatio: undefined,
864
- });
865
-
866
- expect(result.images).toHaveLength(1);
867
- expect(result.images[0]).toBeInstanceOf(Uint8Array);
868
- expect(result.providerMetadata?.fal).toMatchObject({
869
- images: [
870
- {
871
- width: 880,
872
- height: 1184,
873
- contentType: 'image/png',
874
- fileName: null,
875
- fileSize: null,
876
- nsfw: false,
877
- },
878
- ],
879
- timings: {},
880
- seed: 235205040,
881
- });
882
- });
883
-
884
- it('should handle null width and height values with images array only', async () => {
885
- server.urls['https://api.example.com/fal-ai/qwen-image'].response = {
886
- type: 'json-value',
887
- body: {
888
- images: [
889
- {
890
- url: 'https://api.example.com/image.png',
891
- content_type: 'image/png',
892
- file_name: 'output.png',
893
- file_size: 663399,
894
- width: null,
895
- height: null,
896
- },
897
- ],
898
- description: 'here is an image with null width and height',
899
- },
900
- };
901
-
902
- const model = createBasicModel();
903
- const result = await model.doGenerate({
904
- prompt,
905
- files: undefined,
906
- mask: undefined,
907
- n: 1,
908
- providerOptions: {},
909
- size: undefined,
910
- seed: undefined,
911
- aspectRatio: undefined,
912
- });
913
-
914
- expect(result.images).toHaveLength(1);
915
- expect(result.images[0]).toBeInstanceOf(Uint8Array);
916
- expect(result.providerMetadata?.fal).toMatchObject({
917
- images: [
918
- {
919
- width: null,
920
- height: null,
921
- contentType: 'image/png',
922
- fileName: 'output.png',
923
- fileSize: 663399,
924
- },
925
- ],
926
- description: 'here is an image with null width and height',
927
- });
928
- });
929
- });
930
- });
@@ -1,57 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { createFal } from './fal-provider';
3
- import { FalImageModel } from './fal-image-model';
4
-
5
- vi.mock('./fal-image-model', () => ({
6
- FalImageModel: vi.fn(),
7
- }));
8
-
9
- describe('createFal', () => {
10
- beforeEach(() => {
11
- vi.clearAllMocks();
12
- });
13
-
14
- describe('image', () => {
15
- it('should construct an image model with default configuration', () => {
16
- const provider = createFal();
17
- const modelId = 'fal-ai/flux/dev';
18
-
19
- const model = provider.image(modelId);
20
-
21
- expect(model).toBeInstanceOf(FalImageModel);
22
- expect(FalImageModel).toHaveBeenCalledWith(
23
- modelId,
24
- expect.objectContaining({
25
- provider: 'fal.image',
26
- baseURL: 'https://fal.run',
27
- }),
28
- );
29
- });
30
-
31
- it('should respect custom configuration options', () => {
32
- const customBaseURL = 'https://custom.fal.run';
33
- const customHeaders = { 'X-Custom-Header': 'value' };
34
- const mockFetch = vi.fn();
35
-
36
- const provider = createFal({
37
- apiKey: 'custom-api-key',
38
- baseURL: customBaseURL,
39
- headers: customHeaders,
40
- fetch: mockFetch,
41
- });
42
- const modelId = 'fal-ai/flux/dev';
43
-
44
- provider.image(modelId);
45
-
46
- expect(FalImageModel).toHaveBeenCalledWith(
47
- modelId,
48
- expect.objectContaining({
49
- baseURL: customBaseURL,
50
- headers: expect.any(Function),
51
- fetch: mockFetch,
52
- provider: 'fal.image',
53
- }),
54
- );
55
- });
56
- });
57
- });
@@ -1,128 +0,0 @@
1
- import { createTestServer } from '@ai-sdk/test-server/with-vitest';
2
- import { createFal } from './fal-provider';
3
- import { FalSpeechModel } from './fal-speech-model';
4
- import { describe, it, expect } from 'vitest';
5
-
6
- const provider = createFal({ apiKey: 'test-api-key' });
7
- const model = provider.speech('fal-ai/minimax/speech-02-hd');
8
-
9
- const server = createTestServer({
10
- 'https://fal.run/fal-ai/minimax/speech-02-hd': {},
11
- 'https://fal.media/files/test.mp3': {},
12
- });
13
-
14
- describe('FalSpeechModel.doGenerate', () => {
15
- function prepareResponses({
16
- jsonHeaders,
17
- audioHeaders,
18
- }: {
19
- jsonHeaders?: Record<string, string>;
20
- audioHeaders?: Record<string, string>;
21
- } = {}) {
22
- const audioBuffer = new Uint8Array(100);
23
- server.urls['https://fal.run/fal-ai/minimax/speech-02-hd'].response = {
24
- type: 'json-value',
25
- headers: {
26
- 'content-type': 'application/json',
27
- ...jsonHeaders,
28
- },
29
- body: {
30
- audio: { url: 'https://fal.media/files/test.mp3' },
31
- duration_ms: 1234,
32
- },
33
- };
34
- server.urls['https://fal.media/files/test.mp3'].response = {
35
- type: 'binary',
36
- headers: {
37
- 'content-type': 'audio/mp3',
38
- ...audioHeaders,
39
- },
40
- body: Buffer.from(audioBuffer),
41
- };
42
- return audioBuffer;
43
- }
44
-
45
- it('should pass text and default output_format', async () => {
46
- prepareResponses();
47
-
48
- await model.doGenerate({
49
- text: 'Hello from the AI SDK!',
50
- });
51
-
52
- expect(await server.calls[0].requestBodyJson).toMatchObject({
53
- text: 'Hello from the AI SDK!',
54
- output_format: 'url',
55
- });
56
- });
57
-
58
- it('should pass headers', async () => {
59
- prepareResponses();
60
-
61
- const provider = createFal({
62
- apiKey: 'test-api-key',
63
- headers: {
64
- 'Custom-Provider-Header': 'provider-header-value',
65
- },
66
- });
67
-
68
- await provider.speech('fal-ai/minimax/speech-02-hd').doGenerate({
69
- text: 'Hello from the AI SDK!',
70
- headers: {
71
- 'Custom-Request-Header': 'request-header-value',
72
- },
73
- });
74
-
75
- expect(server.calls[0].requestHeaders).toMatchObject({
76
- authorization: 'Key test-api-key',
77
- 'content-type': 'application/json',
78
- 'custom-provider-header': 'provider-header-value',
79
- 'custom-request-header': 'request-header-value',
80
- });
81
- });
82
-
83
- it('should return audio data', async () => {
84
- const audio = prepareResponses();
85
-
86
- const result = await model.doGenerate({
87
- text: 'Hello from the AI SDK!',
88
- });
89
-
90
- expect(result.audio).toStrictEqual(audio);
91
- });
92
-
93
- it('should include response data with timestamp, modelId and headers', async () => {
94
- prepareResponses({ jsonHeaders: { 'x-request-id': 'test-request-id' } });
95
-
96
- const testDate = new Date(0);
97
- const customModel = new FalSpeechModel('fal-ai/minimax/speech-02-hd', {
98
- provider: 'fal.speech',
99
- url: ({ path }) => path,
100
- headers: () => ({}),
101
- _internal: { currentDate: () => testDate },
102
- });
103
-
104
- const result = await customModel.doGenerate({
105
- text: 'Hello from the AI SDK!',
106
- });
107
-
108
- expect(result.response).toMatchObject({
109
- timestamp: testDate,
110
- modelId: 'fal-ai/minimax/speech-02-hd',
111
- headers: expect.objectContaining({ 'x-request-id': 'test-request-id' }),
112
- });
113
- });
114
-
115
- it('should include warnings for unsupported settings', async () => {
116
- prepareResponses();
117
-
118
- const result = await model.doGenerate({
119
- text: 'Hello from the AI SDK!',
120
- language: 'en',
121
- // invalid outputFormat triggers a warning and defaults to url
122
- // (we still return audio via URL)
123
- outputFormat: 'wav',
124
- });
125
-
126
- expect(result.warnings.length).toBeGreaterThan(0);
127
- });
128
- });
@@ -1,181 +0,0 @@
1
- import { createTestServer } from '@ai-sdk/test-server/with-vitest';
2
- import { createFal } from './fal-provider';
3
- import { FalTranscriptionModel } from './fal-transcription-model';
4
- import { readFile } from 'node:fs/promises';
5
- import path from 'node:path';
6
- import { describe, it, expect } from 'vitest';
7
-
8
- const audioData = await readFile(path.join(__dirname, 'transcript-test.mp3'));
9
- const provider = createFal({ apiKey: 'test-api-key' });
10
- const model = provider.transcription('wizper');
11
-
12
- const server = createTestServer({
13
- 'https://queue.fal.run/fal-ai/wizper': {},
14
- 'https://queue.fal.run/fal-ai/wizper/requests/test-id': {},
15
- });
16
-
17
- describe('doGenerate', () => {
18
- function prepareJsonResponse({
19
- headers,
20
- }: {
21
- headers?: Record<string, string>;
22
- } = {}) {
23
- server.urls['https://queue.fal.run/fal-ai/wizper'].response = {
24
- type: 'json-value',
25
- headers,
26
- body: {
27
- status: 'COMPLETED',
28
- request_id: 'test-id',
29
- response_url:
30
- 'https://queue.fal.run/fal-ai/wizper/requests/test-id/result',
31
- status_url: 'https://queue.fal.run/fal-ai/wizper/requests/test-id',
32
- cancel_url:
33
- 'https://queue.fal.run/fal-ai/wizper/requests/test-id/cancel',
34
- logs: null,
35
- metrics: {},
36
- queue_position: 0,
37
- },
38
- };
39
- server.urls[
40
- 'https://queue.fal.run/fal-ai/wizper/requests/test-id'
41
- ].response = {
42
- type: 'json-value',
43
- headers,
44
- body: {
45
- text: 'Hello world!',
46
- chunks: [
47
- {
48
- text: 'Hello',
49
- timestamp: [0, 1],
50
- speaker: 'speaker_1',
51
- },
52
- {
53
- text: ' ',
54
- timestamp: [1, 2],
55
- speaker: 'speaker_1',
56
- },
57
- {
58
- text: 'world!',
59
- timestamp: [2, 3],
60
- speaker: 'speaker_1',
61
- },
62
- ],
63
- diarization_segments: [
64
- {
65
- speaker: 'speaker_1',
66
- timestamp: [0, 3],
67
- },
68
- ],
69
- },
70
- };
71
- }
72
-
73
- it('should pass the model', async () => {
74
- prepareJsonResponse();
75
-
76
- await model.doGenerate({
77
- audio: audioData,
78
- mediaType: 'audio/wav',
79
- });
80
-
81
- expect(await server.calls[0].requestBodyJson).toMatchObject({
82
- audio_url: expect.stringMatching(/^data:audio\//),
83
- task: 'transcribe',
84
- diarize: true,
85
- chunk_level: 'word',
86
- });
87
- });
88
-
89
- it('should pass headers', async () => {
90
- prepareJsonResponse();
91
-
92
- const provider = createFal({
93
- apiKey: 'test-api-key',
94
- headers: {
95
- 'Custom-Provider-Header': 'provider-header-value',
96
- },
97
- });
98
-
99
- await provider.transcription('wizper').doGenerate({
100
- audio: audioData,
101
- mediaType: 'audio/wav',
102
- headers: {
103
- 'Custom-Request-Header': 'request-header-value',
104
- },
105
- });
106
-
107
- expect(server.calls[0].requestHeaders).toMatchObject({
108
- authorization: 'Key test-api-key',
109
- 'content-type': 'application/json',
110
- 'custom-provider-header': 'provider-header-value',
111
- 'custom-request-header': 'request-header-value',
112
- });
113
- });
114
-
115
- it('should extract the transcription text', async () => {
116
- prepareJsonResponse();
117
-
118
- const result = await model.doGenerate({
119
- audio: audioData,
120
- mediaType: 'audio/wav',
121
- });
122
-
123
- expect(result.text).toBe('Hello world!');
124
- });
125
-
126
- it('should include response data with timestamp, modelId and headers', async () => {
127
- prepareJsonResponse({
128
- headers: {
129
- 'x-request-id': 'test-request-id',
130
- 'x-ratelimit-remaining': '123',
131
- },
132
- });
133
-
134
- const testDate = new Date(0);
135
- const customModel = new FalTranscriptionModel('wizper', {
136
- provider: 'test-provider',
137
- url: ({ path }) => path,
138
- headers: () => ({}),
139
- _internal: {
140
- currentDate: () => testDate,
141
- },
142
- });
143
-
144
- const result = await customModel.doGenerate({
145
- audio: audioData,
146
- mediaType: 'audio/wav',
147
- });
148
-
149
- expect(result.response).toMatchObject({
150
- timestamp: testDate,
151
- modelId: 'wizper',
152
- headers: {
153
- 'content-type': 'application/json',
154
- 'x-request-id': 'test-request-id',
155
- 'x-ratelimit-remaining': '123',
156
- },
157
- });
158
- });
159
-
160
- it('should use real date when no custom date provider is specified', async () => {
161
- prepareJsonResponse();
162
-
163
- const testDate = new Date(0);
164
- const customModel = new FalTranscriptionModel('wizper', {
165
- provider: 'test-provider',
166
- url: ({ path }) => path,
167
- headers: () => ({}),
168
- _internal: {
169
- currentDate: () => testDate,
170
- },
171
- });
172
-
173
- const result = await customModel.doGenerate({
174
- audio: audioData,
175
- mediaType: 'audio/wav',
176
- });
177
-
178
- expect(result.response.timestamp.getTime()).toEqual(testDate.getTime());
179
- expect(result.response.modelId).toBe('wizper');
180
- });
181
- });