@ai-sdk/fal 2.0.10 → 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 +15 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/10-fal.mdx +320 -0
- package/package.json +13 -5
- package/src/fal-error.test.ts +0 -34
- package/src/fal-image-model.test.ts +0 -930
- package/src/fal-provider.test.ts +0 -57
- package/src/fal-speech-model.test.ts +0 -128
- package/src/fal-transcription-model.test.ts +0 -181
package/src/fal-provider.test.ts
DELETED
|
@@ -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
|
-
});
|