@ai-sdk/prodia 1.0.6 → 1.0.8
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 +16 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +8 -4
- package/src/prodia-image-model.test.ts +0 -502
- package/src/prodia-provider.test.ts +0 -123
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @ai-sdk/prodia
|
|
2
2
|
|
|
3
|
+
## 1.0.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [462ad00]
|
|
8
|
+
- @ai-sdk/provider-utils@4.0.10
|
|
9
|
+
|
|
10
|
+
## 1.0.7
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 4de5a1d: chore: excluded tests from src folder in npm package
|
|
15
|
+
- Updated dependencies [4de5a1d]
|
|
16
|
+
- @ai-sdk/provider@3.0.5
|
|
17
|
+
- @ai-sdk/provider-utils@4.0.9
|
|
18
|
+
|
|
3
19
|
## 1.0.6
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -376,7 +376,7 @@ var prodiaFailedResponseHandler = (0, import_provider_utils.createJsonErrorRespo
|
|
|
376
376
|
});
|
|
377
377
|
|
|
378
378
|
// src/version.ts
|
|
379
|
-
var VERSION = true ? "1.0.
|
|
379
|
+
var VERSION = true ? "1.0.8" : "0.0.0-test";
|
|
380
380
|
|
|
381
381
|
// src/prodia-provider.ts
|
|
382
382
|
var defaultBaseURL = "https://inference.prodia.com/v2";
|
package/dist/index.mjs
CHANGED
|
@@ -362,7 +362,7 @@ var prodiaFailedResponseHandler = createJsonErrorResponseHandler({
|
|
|
362
362
|
});
|
|
363
363
|
|
|
364
364
|
// src/version.ts
|
|
365
|
-
var VERSION = true ? "1.0.
|
|
365
|
+
var VERSION = true ? "1.0.8" : "0.0.0-test";
|
|
366
366
|
|
|
367
367
|
// src/prodia-provider.ts
|
|
368
368
|
var defaultBaseURL = "https://inference.prodia.com/v2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/prodia",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/**/*",
|
|
11
11
|
"src",
|
|
12
|
+
"!src/**/*.test.ts",
|
|
13
|
+
"!src/**/*.test-d.ts",
|
|
14
|
+
"!src/**/__snapshots__",
|
|
15
|
+
"!src/**/__fixtures__",
|
|
12
16
|
"CHANGELOG.md"
|
|
13
17
|
],
|
|
14
18
|
"exports": {
|
|
@@ -20,15 +24,15 @@
|
|
|
20
24
|
}
|
|
21
25
|
},
|
|
22
26
|
"dependencies": {
|
|
23
|
-
"@ai-sdk/provider": "
|
|
24
|
-
"@ai-sdk/provider
|
|
27
|
+
"@ai-sdk/provider-utils": "4.0.10",
|
|
28
|
+
"@ai-sdk/provider": "3.0.5"
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
31
|
"@types/node": "20.17.24",
|
|
28
32
|
"tsup": "^8",
|
|
29
33
|
"typescript": "5.8.3",
|
|
30
34
|
"zod": "3.25.76",
|
|
31
|
-
"@ai-sdk/test-server": "1.0.
|
|
35
|
+
"@ai-sdk/test-server": "1.0.3",
|
|
32
36
|
"@vercel/ai-tsconfig": "0.0.0"
|
|
33
37
|
},
|
|
34
38
|
"peerDependencies": {
|
|
@@ -1,502 +0,0 @@
|
|
|
1
|
-
import type { 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 { ProdiaImageModel } from './prodia-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
|
-
} = {}) {
|
|
17
|
-
return new ProdiaImageModel('inference.flux-fast.schnell.txt2img.v2', {
|
|
18
|
-
provider: 'prodia.image',
|
|
19
|
-
baseURL: 'https://api.example.com/v2',
|
|
20
|
-
headers: headers ?? (() => ({ Authorization: 'Bearer test-key' })),
|
|
21
|
-
fetch,
|
|
22
|
-
_internal: {
|
|
23
|
-
currentDate,
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function createMultipartResponse(
|
|
29
|
-
jobResult: Record<string, unknown>,
|
|
30
|
-
imageContent: string = 'test-binary-content',
|
|
31
|
-
): { body: Buffer; contentType: string } {
|
|
32
|
-
const boundary = 'test-boundary-12345';
|
|
33
|
-
const jobJson = JSON.stringify(jobResult);
|
|
34
|
-
const imageBuffer = Buffer.from(imageContent);
|
|
35
|
-
|
|
36
|
-
const parts = [
|
|
37
|
-
`--${boundary}\r\n`,
|
|
38
|
-
'Content-Disposition: form-data; name="job"; filename="job.json"\r\n',
|
|
39
|
-
'Content-Type: application/json\r\n',
|
|
40
|
-
'\r\n',
|
|
41
|
-
jobJson,
|
|
42
|
-
'\r\n',
|
|
43
|
-
`--${boundary}\r\n`,
|
|
44
|
-
'Content-Disposition: form-data; name="output"; filename="output.png"\r\n',
|
|
45
|
-
'Content-Type: image/png\r\n',
|
|
46
|
-
'\r\n',
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
const headerPart = Buffer.from(parts.join(''));
|
|
50
|
-
const endPart = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
51
|
-
|
|
52
|
-
const body = Buffer.concat([headerPart, imageBuffer, endPart]);
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
body,
|
|
56
|
-
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const defaultJobResult = {
|
|
61
|
-
id: 'job-123',
|
|
62
|
-
created_at: '2025-01-01T00:00:00Z',
|
|
63
|
-
updated_at: '2025-01-01T00:00:05Z',
|
|
64
|
-
state: { current: 'completed' },
|
|
65
|
-
config: { prompt, seed: 42 },
|
|
66
|
-
metrics: { elapsed: 2.5, ips: 10.5 },
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
describe('ProdiaImageModel', () => {
|
|
70
|
-
const multipartResponse = createMultipartResponse(defaultJobResult);
|
|
71
|
-
|
|
72
|
-
const server = createTestServer({
|
|
73
|
-
'https://api.example.com/v2/job': {
|
|
74
|
-
response: {
|
|
75
|
-
type: 'binary',
|
|
76
|
-
body: multipartResponse.body,
|
|
77
|
-
headers: {
|
|
78
|
-
'content-type': multipartResponse.contentType,
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('doGenerate', () => {
|
|
85
|
-
it('passes the correct parameters including providerOptions', async () => {
|
|
86
|
-
const model = createBasicModel();
|
|
87
|
-
|
|
88
|
-
await model.doGenerate({
|
|
89
|
-
prompt,
|
|
90
|
-
files: undefined,
|
|
91
|
-
mask: undefined,
|
|
92
|
-
n: 1,
|
|
93
|
-
size: undefined,
|
|
94
|
-
aspectRatio: undefined,
|
|
95
|
-
seed: 12345,
|
|
96
|
-
providerOptions: {
|
|
97
|
-
prodia: {
|
|
98
|
-
steps: 4,
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
expect(await server.calls[0].requestBodyJson).toStrictEqual({
|
|
104
|
-
type: 'inference.flux-fast.schnell.txt2img.v2',
|
|
105
|
-
config: {
|
|
106
|
-
prompt,
|
|
107
|
-
seed: 12345,
|
|
108
|
-
steps: 4,
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('includes width and height when size is provided', async () => {
|
|
114
|
-
const model = createBasicModel();
|
|
115
|
-
|
|
116
|
-
await model.doGenerate({
|
|
117
|
-
prompt,
|
|
118
|
-
files: undefined,
|
|
119
|
-
mask: undefined,
|
|
120
|
-
n: 1,
|
|
121
|
-
size: '1024x768',
|
|
122
|
-
aspectRatio: undefined,
|
|
123
|
-
seed: undefined,
|
|
124
|
-
providerOptions: {},
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
expect(await server.calls[0].requestBodyJson).toStrictEqual({
|
|
128
|
-
type: 'inference.flux-fast.schnell.txt2img.v2',
|
|
129
|
-
config: {
|
|
130
|
-
prompt,
|
|
131
|
-
width: 1024,
|
|
132
|
-
height: 768,
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('provider options width/height take precedence over size', async () => {
|
|
138
|
-
const model = createBasicModel();
|
|
139
|
-
|
|
140
|
-
await model.doGenerate({
|
|
141
|
-
prompt,
|
|
142
|
-
files: undefined,
|
|
143
|
-
mask: undefined,
|
|
144
|
-
n: 1,
|
|
145
|
-
size: '1024x768',
|
|
146
|
-
aspectRatio: undefined,
|
|
147
|
-
seed: undefined,
|
|
148
|
-
providerOptions: {
|
|
149
|
-
prodia: {
|
|
150
|
-
width: 512,
|
|
151
|
-
height: 512,
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
expect(await server.calls[0].requestBodyJson).toStrictEqual({
|
|
157
|
-
type: 'inference.flux-fast.schnell.txt2img.v2',
|
|
158
|
-
config: {
|
|
159
|
-
prompt,
|
|
160
|
-
width: 512,
|
|
161
|
-
height: 512,
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('includes style_preset when stylePreset is provided', async () => {
|
|
167
|
-
const model = createBasicModel();
|
|
168
|
-
|
|
169
|
-
await model.doGenerate({
|
|
170
|
-
prompt,
|
|
171
|
-
files: undefined,
|
|
172
|
-
mask: undefined,
|
|
173
|
-
n: 1,
|
|
174
|
-
size: undefined,
|
|
175
|
-
aspectRatio: undefined,
|
|
176
|
-
seed: undefined,
|
|
177
|
-
providerOptions: {
|
|
178
|
-
prodia: {
|
|
179
|
-
stylePreset: 'anime',
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
expect(await server.calls[0].requestBodyJson).toStrictEqual({
|
|
185
|
-
type: 'inference.flux-fast.schnell.txt2img.v2',
|
|
186
|
-
config: {
|
|
187
|
-
prompt,
|
|
188
|
-
style_preset: 'anime',
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('includes loras when provided', async () => {
|
|
194
|
-
const model = createBasicModel();
|
|
195
|
-
|
|
196
|
-
await model.doGenerate({
|
|
197
|
-
prompt,
|
|
198
|
-
files: undefined,
|
|
199
|
-
mask: undefined,
|
|
200
|
-
n: 1,
|
|
201
|
-
size: undefined,
|
|
202
|
-
aspectRatio: undefined,
|
|
203
|
-
seed: undefined,
|
|
204
|
-
providerOptions: {
|
|
205
|
-
prodia: {
|
|
206
|
-
loras: ['prodia/lora/flux/anime@v1', 'prodia/lora/flux/realism@v1'],
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
expect(await server.calls[0].requestBodyJson).toStrictEqual({
|
|
212
|
-
type: 'inference.flux-fast.schnell.txt2img.v2',
|
|
213
|
-
config: {
|
|
214
|
-
prompt,
|
|
215
|
-
loras: ['prodia/lora/flux/anime@v1', 'prodia/lora/flux/realism@v1'],
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('includes progressive when provided', async () => {
|
|
221
|
-
const model = createBasicModel();
|
|
222
|
-
|
|
223
|
-
await model.doGenerate({
|
|
224
|
-
prompt,
|
|
225
|
-
files: undefined,
|
|
226
|
-
mask: undefined,
|
|
227
|
-
n: 1,
|
|
228
|
-
size: undefined,
|
|
229
|
-
aspectRatio: undefined,
|
|
230
|
-
seed: undefined,
|
|
231
|
-
providerOptions: {
|
|
232
|
-
prodia: {
|
|
233
|
-
progressive: true,
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
expect(await server.calls[0].requestBodyJson).toStrictEqual({
|
|
239
|
-
type: 'inference.flux-fast.schnell.txt2img.v2',
|
|
240
|
-
config: {
|
|
241
|
-
prompt,
|
|
242
|
-
progressive: true,
|
|
243
|
-
},
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('calls the correct endpoint', async () => {
|
|
248
|
-
const model = createBasicModel();
|
|
249
|
-
|
|
250
|
-
await model.doGenerate({
|
|
251
|
-
prompt,
|
|
252
|
-
files: undefined,
|
|
253
|
-
mask: undefined,
|
|
254
|
-
n: 1,
|
|
255
|
-
size: undefined,
|
|
256
|
-
aspectRatio: undefined,
|
|
257
|
-
seed: undefined,
|
|
258
|
-
providerOptions: {},
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
expect(server.calls[0].requestMethod).toBe('POST');
|
|
262
|
-
expect(server.calls[0].requestUrl).toBe('https://api.example.com/v2/job');
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('sends Accept: multipart/form-data header', async () => {
|
|
266
|
-
const model = createBasicModel();
|
|
267
|
-
|
|
268
|
-
await model.doGenerate({
|
|
269
|
-
prompt,
|
|
270
|
-
files: undefined,
|
|
271
|
-
mask: undefined,
|
|
272
|
-
n: 1,
|
|
273
|
-
size: undefined,
|
|
274
|
-
aspectRatio: undefined,
|
|
275
|
-
seed: undefined,
|
|
276
|
-
providerOptions: {},
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
expect(server.calls[0].requestHeaders.accept).toBe(
|
|
280
|
-
'multipart/form-data; image/png',
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('merges provider and request headers', async () => {
|
|
285
|
-
const modelWithHeaders = createBasicModel({
|
|
286
|
-
headers: () => ({
|
|
287
|
-
'Custom-Provider-Header': 'provider-header-value',
|
|
288
|
-
Authorization: 'Bearer test-key',
|
|
289
|
-
}),
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
await modelWithHeaders.doGenerate({
|
|
293
|
-
prompt,
|
|
294
|
-
files: undefined,
|
|
295
|
-
mask: undefined,
|
|
296
|
-
n: 1,
|
|
297
|
-
providerOptions: {},
|
|
298
|
-
headers: {
|
|
299
|
-
'Custom-Request-Header': 'request-header-value',
|
|
300
|
-
},
|
|
301
|
-
size: undefined,
|
|
302
|
-
seed: undefined,
|
|
303
|
-
aspectRatio: undefined,
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
expect(server.calls[0].requestHeaders).toMatchObject({
|
|
307
|
-
'content-type': 'application/json',
|
|
308
|
-
'custom-provider-header': 'provider-header-value',
|
|
309
|
-
'custom-request-header': 'request-header-value',
|
|
310
|
-
authorization: 'Bearer test-key',
|
|
311
|
-
accept: 'multipart/form-data; image/png',
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('returns image bytes from multipart response', async () => {
|
|
316
|
-
const model = createBasicModel();
|
|
317
|
-
|
|
318
|
-
const result = await model.doGenerate({
|
|
319
|
-
prompt,
|
|
320
|
-
files: undefined,
|
|
321
|
-
mask: undefined,
|
|
322
|
-
n: 1,
|
|
323
|
-
size: undefined,
|
|
324
|
-
seed: undefined,
|
|
325
|
-
aspectRatio: undefined,
|
|
326
|
-
providerOptions: {},
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
expect(result.images).toHaveLength(1);
|
|
330
|
-
const image = result.images[0];
|
|
331
|
-
expect(image).toBeInstanceOf(Uint8Array);
|
|
332
|
-
expect(Buffer.from(image as Uint8Array<ArrayBufferLike>).toString()).toBe(
|
|
333
|
-
'test-binary-content',
|
|
334
|
-
);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
it('returns provider metadata from job result', async () => {
|
|
338
|
-
const model = createBasicModel();
|
|
339
|
-
|
|
340
|
-
const result = await model.doGenerate({
|
|
341
|
-
prompt,
|
|
342
|
-
files: undefined,
|
|
343
|
-
mask: undefined,
|
|
344
|
-
n: 1,
|
|
345
|
-
size: undefined,
|
|
346
|
-
seed: undefined,
|
|
347
|
-
aspectRatio: undefined,
|
|
348
|
-
providerOptions: {},
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
expect(result.providerMetadata?.prodia).toStrictEqual({
|
|
352
|
-
images: [
|
|
353
|
-
{
|
|
354
|
-
jobId: 'job-123',
|
|
355
|
-
seed: 42,
|
|
356
|
-
elapsed: 2.5,
|
|
357
|
-
iterationsPerSecond: 10.5,
|
|
358
|
-
createdAt: '2025-01-01T00:00:00Z',
|
|
359
|
-
updatedAt: '2025-01-01T00:00:05Z',
|
|
360
|
-
},
|
|
361
|
-
],
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('omits optional metadata fields when not present in job result', async () => {
|
|
366
|
-
const minimalJobResult = {
|
|
367
|
-
id: 'job-456',
|
|
368
|
-
state: { current: 'completed' },
|
|
369
|
-
config: { prompt },
|
|
370
|
-
};
|
|
371
|
-
const response = createMultipartResponse(minimalJobResult);
|
|
372
|
-
|
|
373
|
-
server.urls['https://api.example.com/v2/job'].response = {
|
|
374
|
-
type: 'binary',
|
|
375
|
-
body: response.body,
|
|
376
|
-
headers: {
|
|
377
|
-
'content-type': response.contentType,
|
|
378
|
-
},
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const model = createBasicModel();
|
|
382
|
-
|
|
383
|
-
const result = await model.doGenerate({
|
|
384
|
-
prompt,
|
|
385
|
-
files: undefined,
|
|
386
|
-
mask: undefined,
|
|
387
|
-
n: 1,
|
|
388
|
-
size: undefined,
|
|
389
|
-
seed: undefined,
|
|
390
|
-
aspectRatio: undefined,
|
|
391
|
-
providerOptions: {},
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
expect(result.providerMetadata?.prodia).toStrictEqual({
|
|
395
|
-
images: [
|
|
396
|
-
{
|
|
397
|
-
jobId: 'job-456',
|
|
398
|
-
},
|
|
399
|
-
],
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
it('warns on invalid size format', async () => {
|
|
404
|
-
const model = createBasicModel();
|
|
405
|
-
|
|
406
|
-
const result = await model.doGenerate({
|
|
407
|
-
prompt,
|
|
408
|
-
files: undefined,
|
|
409
|
-
mask: undefined,
|
|
410
|
-
n: 1,
|
|
411
|
-
size: 'invalid' as `${number}x${number}`,
|
|
412
|
-
seed: undefined,
|
|
413
|
-
aspectRatio: undefined,
|
|
414
|
-
providerOptions: {},
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
expect(result.warnings).toMatchInlineSnapshot(`
|
|
418
|
-
[
|
|
419
|
-
{
|
|
420
|
-
"details": "Invalid size format: invalid. Expected format: WIDTHxHEIGHT (e.g., 1024x1024)",
|
|
421
|
-
"feature": "size",
|
|
422
|
-
"type": "unsupported",
|
|
423
|
-
},
|
|
424
|
-
]
|
|
425
|
-
`);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it('handles API errors', async () => {
|
|
429
|
-
server.urls['https://api.example.com/v2/job'].response = {
|
|
430
|
-
type: 'error',
|
|
431
|
-
status: 400,
|
|
432
|
-
body: JSON.stringify({
|
|
433
|
-
message: 'Invalid prompt',
|
|
434
|
-
detail: 'Prompt cannot be empty',
|
|
435
|
-
}),
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const model = createBasicModel();
|
|
439
|
-
|
|
440
|
-
await expect(
|
|
441
|
-
model.doGenerate({
|
|
442
|
-
prompt,
|
|
443
|
-
files: undefined,
|
|
444
|
-
mask: undefined,
|
|
445
|
-
n: 1,
|
|
446
|
-
providerOptions: {},
|
|
447
|
-
size: undefined,
|
|
448
|
-
seed: undefined,
|
|
449
|
-
aspectRatio: undefined,
|
|
450
|
-
}),
|
|
451
|
-
).rejects.toMatchObject({
|
|
452
|
-
message: 'Prompt cannot be empty',
|
|
453
|
-
statusCode: 400,
|
|
454
|
-
url: 'https://api.example.com/v2/job',
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('includes timestamp, headers, and modelId in response metadata', async () => {
|
|
459
|
-
const response = createMultipartResponse(defaultJobResult);
|
|
460
|
-
server.urls['https://api.example.com/v2/job'].response = {
|
|
461
|
-
type: 'binary',
|
|
462
|
-
body: response.body,
|
|
463
|
-
headers: {
|
|
464
|
-
'content-type': response.contentType,
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
const testDate = new Date('2025-01-01T00:00:00Z');
|
|
469
|
-
const model = createBasicModel({
|
|
470
|
-
currentDate: () => testDate,
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
const result = await model.doGenerate({
|
|
474
|
-
prompt,
|
|
475
|
-
files: undefined,
|
|
476
|
-
mask: undefined,
|
|
477
|
-
n: 1,
|
|
478
|
-
providerOptions: {},
|
|
479
|
-
size: undefined,
|
|
480
|
-
seed: undefined,
|
|
481
|
-
aspectRatio: undefined,
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
expect(result.response).toStrictEqual({
|
|
485
|
-
timestamp: testDate,
|
|
486
|
-
modelId: 'inference.flux-fast.schnell.txt2img.v2',
|
|
487
|
-
headers: expect.any(Object),
|
|
488
|
-
});
|
|
489
|
-
});
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
describe('constructor', () => {
|
|
493
|
-
it('exposes correct provider and model information', () => {
|
|
494
|
-
const model = createBasicModel();
|
|
495
|
-
|
|
496
|
-
expect(model.provider).toBe('prodia.image');
|
|
497
|
-
expect(model.modelId).toBe('inference.flux-fast.schnell.txt2img.v2');
|
|
498
|
-
expect(model.specificationVersion).toBe('v3');
|
|
499
|
-
expect(model.maxImagesPerCall).toBe(1);
|
|
500
|
-
});
|
|
501
|
-
});
|
|
502
|
-
});
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { createTestServer } from '@ai-sdk/test-server/with-vitest';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { createProdia } from './prodia-provider';
|
|
4
|
-
|
|
5
|
-
function createMultipartResponse(
|
|
6
|
-
jobResult: Record<string, unknown>,
|
|
7
|
-
imageContent: string = 'test-image',
|
|
8
|
-
): { body: Buffer; contentType: string } {
|
|
9
|
-
const boundary = 'test-boundary-12345';
|
|
10
|
-
const jobJson = JSON.stringify(jobResult);
|
|
11
|
-
const imageBuffer = Buffer.from(imageContent);
|
|
12
|
-
|
|
13
|
-
const parts = [
|
|
14
|
-
`--${boundary}\r\n`,
|
|
15
|
-
'Content-Disposition: form-data; name="job"; filename="job.json"\r\n',
|
|
16
|
-
'Content-Type: application/json\r\n',
|
|
17
|
-
'\r\n',
|
|
18
|
-
jobJson,
|
|
19
|
-
'\r\n',
|
|
20
|
-
`--${boundary}\r\n`,
|
|
21
|
-
'Content-Disposition: form-data; name="output"; filename="output.png"\r\n',
|
|
22
|
-
'Content-Type: image/png\r\n',
|
|
23
|
-
'\r\n',
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const headerPart = Buffer.from(parts.join(''));
|
|
27
|
-
const endPart = Buffer.from(`\r\n--${boundary}--\r\n`);
|
|
28
|
-
|
|
29
|
-
const body = Buffer.concat([headerPart, imageBuffer, endPart]);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
body,
|
|
33
|
-
contentType: `multipart/form-data; boundary=${boundary}`,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const defaultJobResult = {
|
|
38
|
-
id: 'job-123',
|
|
39
|
-
state: { current: 'completed' },
|
|
40
|
-
config: { prompt: 'test' },
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const multipartResponse = createMultipartResponse(defaultJobResult);
|
|
44
|
-
|
|
45
|
-
const server = createTestServer({
|
|
46
|
-
'https://api.example.com/v2/job': {
|
|
47
|
-
response: {
|
|
48
|
-
type: 'binary',
|
|
49
|
-
body: multipartResponse.body,
|
|
50
|
-
headers: {
|
|
51
|
-
'content-type': multipartResponse.contentType,
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('Prodia provider', () => {
|
|
58
|
-
it('creates image models via .image and .imageModel', () => {
|
|
59
|
-
const provider = createProdia();
|
|
60
|
-
|
|
61
|
-
const imageModel = provider.image('inference.flux-fast.schnell.txt2img.v2');
|
|
62
|
-
const imageModel2 = provider.imageModel(
|
|
63
|
-
'inference.flux.schnell.txt2img.v2',
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
expect(imageModel.provider).toBe('prodia.image');
|
|
67
|
-
expect(imageModel.modelId).toBe('inference.flux-fast.schnell.txt2img.v2');
|
|
68
|
-
expect(imageModel2.modelId).toBe('inference.flux.schnell.txt2img.v2');
|
|
69
|
-
expect(imageModel.specificationVersion).toBe('v3');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('configures baseURL and headers correctly', async () => {
|
|
73
|
-
const provider = createProdia({
|
|
74
|
-
apiKey: 'test-api-key',
|
|
75
|
-
baseURL: 'https://api.example.com/v2',
|
|
76
|
-
headers: {
|
|
77
|
-
'x-extra-header': 'extra',
|
|
78
|
-
},
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const model = provider.image('inference.flux-fast.schnell.txt2img.v2');
|
|
82
|
-
|
|
83
|
-
await model.doGenerate({
|
|
84
|
-
prompt: 'A serene mountain landscape at sunset',
|
|
85
|
-
files: undefined,
|
|
86
|
-
mask: undefined,
|
|
87
|
-
n: 1,
|
|
88
|
-
size: undefined,
|
|
89
|
-
seed: undefined,
|
|
90
|
-
aspectRatio: undefined,
|
|
91
|
-
providerOptions: {},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
expect(server.calls[0].requestUrl).toBe('https://api.example.com/v2/job');
|
|
95
|
-
expect(server.calls[0].requestMethod).toBe('POST');
|
|
96
|
-
expect(server.calls[0].requestHeaders.authorization).toBe(
|
|
97
|
-
'Bearer test-api-key',
|
|
98
|
-
);
|
|
99
|
-
expect(server.calls[0].requestHeaders['x-extra-header']).toBe('extra');
|
|
100
|
-
expect(server.calls[0].requestHeaders.accept).toBe(
|
|
101
|
-
'multipart/form-data; image/png',
|
|
102
|
-
);
|
|
103
|
-
expect(await server.calls[0].requestBodyJson).toMatchObject({
|
|
104
|
-
type: 'inference.flux-fast.schnell.txt2img.v2',
|
|
105
|
-
config: {
|
|
106
|
-
prompt: 'A serene mountain landscape at sunset',
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
expect(server.calls[0].requestUserAgent).toContain('ai-sdk/prodia/');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('throws NoSuchModelError for unsupported model types', () => {
|
|
114
|
-
const provider = createProdia();
|
|
115
|
-
|
|
116
|
-
expect(() => provider.languageModel('some-id')).toThrowError(
|
|
117
|
-
'No such languageModel',
|
|
118
|
-
);
|
|
119
|
-
expect(() => provider.embeddingModel('some-id')).toThrowError(
|
|
120
|
-
'No such embeddingModel',
|
|
121
|
-
);
|
|
122
|
-
});
|
|
123
|
-
});
|