@ai-sdk/black-forest-labs 0.0.0-64aae7dd-20260114144918 → 0.0.0-98261322-20260122142521
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 +31 -4
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/12-black-forest-labs.mdx +200 -0
- package/package.json +10 -5
- package/src/black-forest-labs-image-model.test.ts +576 -0
- package/src/black-forest-labs-image-model.ts +473 -0
- package/src/black-forest-labs-image-settings.ts +9 -0
- package/src/black-forest-labs-provider.test.ts +139 -0
- package/src/black-forest-labs-provider.ts +113 -0
- package/src/index.ts +14 -0
- package/src/version.ts +6 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import type { ImageModelV3, SharedV3Warning } from '@ai-sdk/provider';
|
|
2
|
+
import type { InferSchema, Resolvable } from '@ai-sdk/provider-utils';
|
|
3
|
+
import {
|
|
4
|
+
FetchFunction,
|
|
5
|
+
combineHeaders,
|
|
6
|
+
createBinaryResponseHandler,
|
|
7
|
+
createJsonErrorResponseHandler,
|
|
8
|
+
createJsonResponseHandler,
|
|
9
|
+
createStatusCodeErrorResponseHandler,
|
|
10
|
+
delay,
|
|
11
|
+
getFromApi,
|
|
12
|
+
lazySchema,
|
|
13
|
+
parseProviderOptions,
|
|
14
|
+
postJsonToApi,
|
|
15
|
+
resolve,
|
|
16
|
+
zodSchema,
|
|
17
|
+
} from '@ai-sdk/provider-utils';
|
|
18
|
+
import { z } from 'zod/v4';
|
|
19
|
+
import type { BlackForestLabsAspectRatio } from './black-forest-labs-image-settings';
|
|
20
|
+
import { BlackForestLabsImageModelId } from './black-forest-labs-image-settings';
|
|
21
|
+
|
|
22
|
+
const DEFAULT_POLL_INTERVAL_MILLIS = 500;
|
|
23
|
+
const DEFAULT_POLL_TIMEOUT_MILLIS = 60000;
|
|
24
|
+
|
|
25
|
+
interface BlackForestLabsImageModelConfig {
|
|
26
|
+
provider: string;
|
|
27
|
+
baseURL: string;
|
|
28
|
+
headers?: Resolvable<Record<string, string | undefined>>;
|
|
29
|
+
fetch?: FetchFunction;
|
|
30
|
+
/**
|
|
31
|
+
Poll interval in milliseconds between status checks. Defaults to 500ms.
|
|
32
|
+
*/
|
|
33
|
+
pollIntervalMillis?: number;
|
|
34
|
+
/**
|
|
35
|
+
Overall timeout in milliseconds for polling before giving up. Defaults to 60s.
|
|
36
|
+
*/
|
|
37
|
+
pollTimeoutMillis?: number;
|
|
38
|
+
_internal?: {
|
|
39
|
+
currentDate?: () => Date;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class BlackForestLabsImageModel implements ImageModelV3 {
|
|
44
|
+
readonly specificationVersion = 'v3';
|
|
45
|
+
readonly maxImagesPerCall = 1;
|
|
46
|
+
|
|
47
|
+
get provider(): string {
|
|
48
|
+
return this.config.provider;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
readonly modelId: BlackForestLabsImageModelId,
|
|
53
|
+
private readonly config: BlackForestLabsImageModelConfig,
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
private async getArgs({
|
|
57
|
+
prompt,
|
|
58
|
+
files,
|
|
59
|
+
mask,
|
|
60
|
+
size,
|
|
61
|
+
aspectRatio,
|
|
62
|
+
seed,
|
|
63
|
+
providerOptions,
|
|
64
|
+
}: Parameters<ImageModelV3['doGenerate']>[0]) {
|
|
65
|
+
const warnings: Array<SharedV3Warning> = [];
|
|
66
|
+
|
|
67
|
+
const finalAspectRatio =
|
|
68
|
+
aspectRatio ?? (size ? convertSizeToAspectRatio(size) : undefined);
|
|
69
|
+
|
|
70
|
+
if (size && !aspectRatio) {
|
|
71
|
+
warnings.push({
|
|
72
|
+
type: 'unsupported',
|
|
73
|
+
feature: 'size',
|
|
74
|
+
details:
|
|
75
|
+
'Deriving aspect_ratio from size. Use the width and height provider options to specify dimensions for models that support them.',
|
|
76
|
+
});
|
|
77
|
+
} else if (size && aspectRatio) {
|
|
78
|
+
warnings.push({
|
|
79
|
+
type: 'unsupported',
|
|
80
|
+
feature: 'size',
|
|
81
|
+
details:
|
|
82
|
+
'Black Forest Labs ignores size when aspectRatio is provided. Use the width and height provider options to specify dimensions for models that support them',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const bflOptions = await parseProviderOptions({
|
|
87
|
+
provider: 'blackForestLabs',
|
|
88
|
+
providerOptions,
|
|
89
|
+
schema: blackForestLabsImageProviderOptionsSchema,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const [widthStr, heightStr] = size?.split('x') ?? [];
|
|
93
|
+
|
|
94
|
+
const inputImages: string[] =
|
|
95
|
+
files?.map(file => {
|
|
96
|
+
if (file.type === 'url') {
|
|
97
|
+
return file.url;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (typeof file.data === 'string') {
|
|
101
|
+
return file.data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Buffer.from(file.data).toString('base64');
|
|
105
|
+
}) || [];
|
|
106
|
+
|
|
107
|
+
if (inputImages.length > 10) {
|
|
108
|
+
throw new Error('Black Forest Labs supports up to 10 input images.');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const inputImagesObj: Record<string, string> = inputImages.reduce<
|
|
112
|
+
Record<string, string>
|
|
113
|
+
>((acc, img, index) => {
|
|
114
|
+
acc[`input_image${index === 0 ? '' : `_${index + 1}`}`] = img;
|
|
115
|
+
return acc;
|
|
116
|
+
}, {});
|
|
117
|
+
|
|
118
|
+
let maskValue: string | undefined;
|
|
119
|
+
if (mask) {
|
|
120
|
+
if (mask.type === 'url') {
|
|
121
|
+
maskValue = mask.url;
|
|
122
|
+
} else {
|
|
123
|
+
if (typeof mask.data === 'string') {
|
|
124
|
+
maskValue = mask.data;
|
|
125
|
+
} else {
|
|
126
|
+
maskValue = Buffer.from(mask.data).toString('base64');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const body: Record<string, unknown> = {
|
|
132
|
+
prompt,
|
|
133
|
+
seed,
|
|
134
|
+
aspect_ratio: finalAspectRatio,
|
|
135
|
+
width: bflOptions?.width ?? (size ? Number(widthStr) : undefined),
|
|
136
|
+
height: bflOptions?.height ?? (size ? Number(heightStr) : undefined),
|
|
137
|
+
steps: bflOptions?.steps,
|
|
138
|
+
guidance: bflOptions?.guidance,
|
|
139
|
+
image_prompt_strength: bflOptions?.imagePromptStrength,
|
|
140
|
+
image_prompt: bflOptions?.imagePrompt,
|
|
141
|
+
...inputImagesObj,
|
|
142
|
+
mask: maskValue,
|
|
143
|
+
output_format: bflOptions?.outputFormat,
|
|
144
|
+
prompt_upsampling: bflOptions?.promptUpsampling,
|
|
145
|
+
raw: bflOptions?.raw,
|
|
146
|
+
safety_tolerance: bflOptions?.safetyTolerance,
|
|
147
|
+
webhook_secret: bflOptions?.webhookSecret,
|
|
148
|
+
webhook_url: bflOptions?.webhookUrl,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return { body, warnings };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async doGenerate({
|
|
155
|
+
prompt,
|
|
156
|
+
files,
|
|
157
|
+
mask,
|
|
158
|
+
size,
|
|
159
|
+
aspectRatio,
|
|
160
|
+
seed,
|
|
161
|
+
providerOptions,
|
|
162
|
+
headers,
|
|
163
|
+
abortSignal,
|
|
164
|
+
}: Parameters<ImageModelV3['doGenerate']>[0]): Promise<
|
|
165
|
+
Awaited<ReturnType<ImageModelV3['doGenerate']>>
|
|
166
|
+
> {
|
|
167
|
+
const { body, warnings } = await this.getArgs({
|
|
168
|
+
prompt,
|
|
169
|
+
files,
|
|
170
|
+
mask,
|
|
171
|
+
size,
|
|
172
|
+
aspectRatio,
|
|
173
|
+
seed,
|
|
174
|
+
providerOptions,
|
|
175
|
+
n: 1,
|
|
176
|
+
headers,
|
|
177
|
+
abortSignal,
|
|
178
|
+
} as Parameters<ImageModelV3['doGenerate']>[0]);
|
|
179
|
+
|
|
180
|
+
const bflOptions = await parseProviderOptions({
|
|
181
|
+
provider: 'blackForestLabs',
|
|
182
|
+
providerOptions,
|
|
183
|
+
schema: blackForestLabsImageProviderOptionsSchema,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
|
|
187
|
+
const combinedHeaders = combineHeaders(
|
|
188
|
+
await resolve(this.config.headers),
|
|
189
|
+
headers,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const submit = await postJsonToApi({
|
|
193
|
+
url: `${this.config.baseURL}/${this.modelId}`,
|
|
194
|
+
headers: combinedHeaders,
|
|
195
|
+
body,
|
|
196
|
+
failedResponseHandler: bflFailedResponseHandler,
|
|
197
|
+
successfulResponseHandler: createJsonResponseHandler(bflSubmitSchema),
|
|
198
|
+
abortSignal,
|
|
199
|
+
fetch: this.config.fetch,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const pollUrl = submit.value.polling_url;
|
|
203
|
+
const requestId = submit.value.id;
|
|
204
|
+
|
|
205
|
+
const {
|
|
206
|
+
imageUrl,
|
|
207
|
+
seed: resultSeed,
|
|
208
|
+
start_time: resultStartTime,
|
|
209
|
+
end_time: resultEndTime,
|
|
210
|
+
duration: resultDuration,
|
|
211
|
+
} = await this.pollForImageUrl({
|
|
212
|
+
pollUrl,
|
|
213
|
+
requestId,
|
|
214
|
+
headers: combinedHeaders,
|
|
215
|
+
abortSignal,
|
|
216
|
+
pollOverrides: {
|
|
217
|
+
pollIntervalMillis: bflOptions?.pollIntervalMillis,
|
|
218
|
+
pollTimeoutMillis: bflOptions?.pollTimeoutMillis,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const { value: imageBytes, responseHeaders } = await getFromApi({
|
|
223
|
+
url: imageUrl,
|
|
224
|
+
headers: combinedHeaders,
|
|
225
|
+
abortSignal,
|
|
226
|
+
failedResponseHandler: createStatusCodeErrorResponseHandler(),
|
|
227
|
+
successfulResponseHandler: createBinaryResponseHandler(),
|
|
228
|
+
fetch: this.config.fetch,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
images: [imageBytes],
|
|
233
|
+
warnings,
|
|
234
|
+
providerMetadata: {
|
|
235
|
+
blackForestLabs: {
|
|
236
|
+
images: [
|
|
237
|
+
{
|
|
238
|
+
...(resultSeed != null && { seed: resultSeed }),
|
|
239
|
+
...(resultStartTime != null && { start_time: resultStartTime }),
|
|
240
|
+
...(resultEndTime != null && { end_time: resultEndTime }),
|
|
241
|
+
...(resultDuration != null && { duration: resultDuration }),
|
|
242
|
+
...(submit.value.cost != null && { cost: submit.value.cost }),
|
|
243
|
+
...(submit.value.input_mp != null && {
|
|
244
|
+
inputMegapixels: submit.value.input_mp,
|
|
245
|
+
}),
|
|
246
|
+
...(submit.value.output_mp != null && {
|
|
247
|
+
outputMegapixels: submit.value.output_mp,
|
|
248
|
+
}),
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
response: {
|
|
254
|
+
modelId: this.modelId,
|
|
255
|
+
timestamp: currentDate,
|
|
256
|
+
headers: responseHeaders,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async pollForImageUrl({
|
|
262
|
+
pollUrl,
|
|
263
|
+
requestId,
|
|
264
|
+
headers,
|
|
265
|
+
abortSignal,
|
|
266
|
+
pollOverrides,
|
|
267
|
+
}: {
|
|
268
|
+
pollUrl: string;
|
|
269
|
+
requestId: string;
|
|
270
|
+
headers: Record<string, string | undefined>;
|
|
271
|
+
abortSignal: AbortSignal | undefined;
|
|
272
|
+
pollOverrides?: {
|
|
273
|
+
pollIntervalMillis?: number;
|
|
274
|
+
pollTimeoutMillis?: number;
|
|
275
|
+
};
|
|
276
|
+
}): Promise<{
|
|
277
|
+
imageUrl: string;
|
|
278
|
+
seed?: number;
|
|
279
|
+
start_time?: number;
|
|
280
|
+
end_time?: number;
|
|
281
|
+
duration?: number;
|
|
282
|
+
}> {
|
|
283
|
+
const pollIntervalMillis =
|
|
284
|
+
pollOverrides?.pollIntervalMillis ??
|
|
285
|
+
this.config.pollIntervalMillis ??
|
|
286
|
+
DEFAULT_POLL_INTERVAL_MILLIS;
|
|
287
|
+
const pollTimeoutMillis =
|
|
288
|
+
pollOverrides?.pollTimeoutMillis ??
|
|
289
|
+
this.config.pollTimeoutMillis ??
|
|
290
|
+
DEFAULT_POLL_TIMEOUT_MILLIS;
|
|
291
|
+
const maxPollAttempts = Math.ceil(
|
|
292
|
+
pollTimeoutMillis / Math.max(1, pollIntervalMillis),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const url = new URL(pollUrl);
|
|
296
|
+
if (!url.searchParams.has('id')) {
|
|
297
|
+
url.searchParams.set('id', requestId);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (let i = 0; i < maxPollAttempts; i++) {
|
|
301
|
+
const { value } = await getFromApi({
|
|
302
|
+
url: url.toString(),
|
|
303
|
+
headers,
|
|
304
|
+
failedResponseHandler: bflFailedResponseHandler,
|
|
305
|
+
successfulResponseHandler: createJsonResponseHandler(bflPollSchema),
|
|
306
|
+
abortSignal,
|
|
307
|
+
fetch: this.config.fetch,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const status = value.status;
|
|
311
|
+
if (status === 'Ready') {
|
|
312
|
+
if (typeof value.result?.sample === 'string') {
|
|
313
|
+
return {
|
|
314
|
+
imageUrl: value.result.sample,
|
|
315
|
+
seed: value.result.seed ?? undefined,
|
|
316
|
+
start_time: value.result.start_time ?? undefined,
|
|
317
|
+
end_time: value.result.end_time ?? undefined,
|
|
318
|
+
duration: value.result.duration ?? undefined,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
throw new Error(
|
|
322
|
+
'Black Forest Labs poll response is Ready but missing result.sample',
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (status === 'Error' || status === 'Failed') {
|
|
326
|
+
throw new Error('Black Forest Labs generation failed.');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
await delay(pollIntervalMillis);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
throw new Error('Black Forest Labs generation timed out.');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export const blackForestLabsImageProviderOptionsSchema = lazySchema(() =>
|
|
337
|
+
zodSchema(
|
|
338
|
+
z.object({
|
|
339
|
+
imagePrompt: z.string().optional(),
|
|
340
|
+
imagePromptStrength: z.number().min(0).max(1).optional(),
|
|
341
|
+
/** @deprecated use prompt.images instead */
|
|
342
|
+
inputImage: z.string().optional(),
|
|
343
|
+
/** @deprecated use prompt.images instead */
|
|
344
|
+
inputImage2: z.string().optional(),
|
|
345
|
+
/** @deprecated use prompt.images instead */
|
|
346
|
+
inputImage3: z.string().optional(),
|
|
347
|
+
/** @deprecated use prompt.images instead */
|
|
348
|
+
inputImage4: z.string().optional(),
|
|
349
|
+
/** @deprecated use prompt.images instead */
|
|
350
|
+
inputImage5: z.string().optional(),
|
|
351
|
+
/** @deprecated use prompt.images instead */
|
|
352
|
+
inputImage6: z.string().optional(),
|
|
353
|
+
/** @deprecated use prompt.images instead */
|
|
354
|
+
inputImage7: z.string().optional(),
|
|
355
|
+
/** @deprecated use prompt.images instead */
|
|
356
|
+
inputImage8: z.string().optional(),
|
|
357
|
+
/** @deprecated use prompt.images instead */
|
|
358
|
+
inputImage9: z.string().optional(),
|
|
359
|
+
/** @deprecated use prompt.images instead */
|
|
360
|
+
inputImage10: z.string().optional(),
|
|
361
|
+
steps: z.number().int().positive().optional(),
|
|
362
|
+
guidance: z.number().min(0).optional(),
|
|
363
|
+
width: z.number().int().min(256).max(1920).optional(),
|
|
364
|
+
height: z.number().int().min(256).max(1920).optional(),
|
|
365
|
+
outputFormat: z.enum(['jpeg', 'png']).optional(),
|
|
366
|
+
promptUpsampling: z.boolean().optional(),
|
|
367
|
+
raw: z.boolean().optional(),
|
|
368
|
+
safetyTolerance: z.number().int().min(0).max(6).optional(),
|
|
369
|
+
webhookSecret: z.string().optional(),
|
|
370
|
+
webhookUrl: z.url().optional(),
|
|
371
|
+
pollIntervalMillis: z.number().int().positive().optional(),
|
|
372
|
+
pollTimeoutMillis: z.number().int().positive().optional(),
|
|
373
|
+
}),
|
|
374
|
+
),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
export type BlackForestLabsImageProviderOptions = InferSchema<
|
|
378
|
+
typeof blackForestLabsImageProviderOptionsSchema
|
|
379
|
+
>;
|
|
380
|
+
|
|
381
|
+
function convertSizeToAspectRatio(
|
|
382
|
+
size: string,
|
|
383
|
+
): BlackForestLabsAspectRatio | undefined {
|
|
384
|
+
const [wStr, hStr] = size.split('x');
|
|
385
|
+
const width = Number(wStr);
|
|
386
|
+
const height = Number(hStr);
|
|
387
|
+
if (
|
|
388
|
+
!Number.isFinite(width) ||
|
|
389
|
+
!Number.isFinite(height) ||
|
|
390
|
+
width <= 0 ||
|
|
391
|
+
height <= 0
|
|
392
|
+
) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
const g = gcd(width, height);
|
|
396
|
+
return `${Math.round(width / g)}:${Math.round(height / g)}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function gcd(a: number, b: number): number {
|
|
400
|
+
let x = Math.abs(a);
|
|
401
|
+
let y = Math.abs(b);
|
|
402
|
+
while (y !== 0) {
|
|
403
|
+
const t = y;
|
|
404
|
+
y = x % y;
|
|
405
|
+
x = t;
|
|
406
|
+
}
|
|
407
|
+
return x;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const bflSubmitSchema = z.object({
|
|
411
|
+
id: z.string(),
|
|
412
|
+
polling_url: z.url(),
|
|
413
|
+
cost: z.number().nullish(),
|
|
414
|
+
input_mp: z.number().nullish(),
|
|
415
|
+
output_mp: z.number().nullish(),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const bflStatus = z.union([
|
|
419
|
+
z.literal('Pending'),
|
|
420
|
+
z.literal('Ready'),
|
|
421
|
+
z.literal('Error'),
|
|
422
|
+
z.literal('Failed'),
|
|
423
|
+
z.literal('Request Moderated'),
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
const bflPollSchema = z
|
|
427
|
+
.object({
|
|
428
|
+
status: bflStatus.optional(),
|
|
429
|
+
state: bflStatus.optional(),
|
|
430
|
+
details: z.unknown().optional(),
|
|
431
|
+
result: z
|
|
432
|
+
.object({
|
|
433
|
+
sample: z.url(),
|
|
434
|
+
seed: z.number().optional(),
|
|
435
|
+
start_time: z.number().optional(),
|
|
436
|
+
end_time: z.number().optional(),
|
|
437
|
+
duration: z.number().optional(),
|
|
438
|
+
})
|
|
439
|
+
.nullish(),
|
|
440
|
+
})
|
|
441
|
+
.refine(v => v.status != null || v.state != null, {
|
|
442
|
+
message: 'Missing status in Black Forest Labs poll response',
|
|
443
|
+
})
|
|
444
|
+
.transform(v => ({
|
|
445
|
+
status: (v.status ?? v.state)!,
|
|
446
|
+
result: v.result,
|
|
447
|
+
}));
|
|
448
|
+
|
|
449
|
+
const bflErrorSchema = z.object({
|
|
450
|
+
message: z.string().optional(),
|
|
451
|
+
detail: z.any().optional(),
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const bflFailedResponseHandler = createJsonErrorResponseHandler({
|
|
455
|
+
errorSchema: bflErrorSchema,
|
|
456
|
+
errorToMessage: error =>
|
|
457
|
+
bflErrorToMessage(error) ?? 'Unknown Black Forest Labs error',
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
function bflErrorToMessage(error: unknown): string | undefined {
|
|
461
|
+
const parsed = bflErrorSchema.safeParse(error);
|
|
462
|
+
if (!parsed.success) return undefined;
|
|
463
|
+
const { message, detail } = parsed.data;
|
|
464
|
+
if (typeof detail === 'string') return detail;
|
|
465
|
+
if (detail != null) {
|
|
466
|
+
try {
|
|
467
|
+
return JSON.stringify(detail);
|
|
468
|
+
} catch {
|
|
469
|
+
// ignore
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return message;
|
|
473
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createTestServer } from '@ai-sdk/test-server/with-vitest';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { createBlackForestLabs } from './black-forest-labs-provider';
|
|
4
|
+
|
|
5
|
+
const server = createTestServer({
|
|
6
|
+
'https://api.example.com/v1/flux-pro-1.1': {
|
|
7
|
+
response: {
|
|
8
|
+
type: 'json-value',
|
|
9
|
+
body: {
|
|
10
|
+
id: 'req-123',
|
|
11
|
+
polling_url: 'https://api.example.com/poll',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
'https://api.example.com/poll': {
|
|
16
|
+
response: {
|
|
17
|
+
type: 'json-value',
|
|
18
|
+
body: {
|
|
19
|
+
status: 'Ready',
|
|
20
|
+
result: {
|
|
21
|
+
sample: 'https://api.example.com/image.png',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
'https://api.example.com/image.png': {
|
|
27
|
+
response: {
|
|
28
|
+
type: 'binary',
|
|
29
|
+
body: Buffer.from([1, 2, 3]),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('BlackForestLabs provider', () => {
|
|
35
|
+
it('creates image models via .image and .imageModel', () => {
|
|
36
|
+
const provider = createBlackForestLabs();
|
|
37
|
+
|
|
38
|
+
const imageModel = provider.image('flux-pro-1.1');
|
|
39
|
+
const imageModel2 = provider.imageModel('flux-pro-1.1-ultra');
|
|
40
|
+
|
|
41
|
+
expect(imageModel.provider).toBe('black-forest-labs.image');
|
|
42
|
+
expect(imageModel.modelId).toBe('flux-pro-1.1');
|
|
43
|
+
expect(imageModel2.modelId).toBe('flux-pro-1.1-ultra');
|
|
44
|
+
expect(imageModel.specificationVersion).toBe('v3');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('configures baseURL and headers correctly', async () => {
|
|
48
|
+
const provider = createBlackForestLabs({
|
|
49
|
+
apiKey: 'test-api-key',
|
|
50
|
+
baseURL: 'https://api.example.com/v1',
|
|
51
|
+
headers: {
|
|
52
|
+
'x-extra-header': 'extra',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const model = provider.image('flux-pro-1.1');
|
|
57
|
+
|
|
58
|
+
await model.doGenerate({
|
|
59
|
+
prompt: 'A serene mountain landscape at sunset',
|
|
60
|
+
files: undefined,
|
|
61
|
+
mask: undefined,
|
|
62
|
+
n: 1,
|
|
63
|
+
size: undefined,
|
|
64
|
+
seed: undefined,
|
|
65
|
+
aspectRatio: '1:1',
|
|
66
|
+
providerOptions: {},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(server.calls[0].requestUrl).toBe(
|
|
70
|
+
'https://api.example.com/v1/flux-pro-1.1',
|
|
71
|
+
);
|
|
72
|
+
expect(server.calls[0].requestMethod).toBe('POST');
|
|
73
|
+
expect(server.calls[0].requestHeaders['x-key']).toBe('test-api-key');
|
|
74
|
+
expect(server.calls[0].requestHeaders['x-extra-header']).toBe('extra');
|
|
75
|
+
expect(await server.calls[0].requestBodyJson).toMatchObject({
|
|
76
|
+
prompt: 'A serene mountain landscape at sunset',
|
|
77
|
+
aspect_ratio: '1:1',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(server.calls[0].requestUserAgent).toContain(
|
|
81
|
+
'ai-sdk/black-forest-labs/',
|
|
82
|
+
);
|
|
83
|
+
expect(server.calls[1].requestUserAgent).toContain(
|
|
84
|
+
'ai-sdk/black-forest-labs/',
|
|
85
|
+
);
|
|
86
|
+
expect(server.calls[2].requestUserAgent).toContain(
|
|
87
|
+
'ai-sdk/black-forest-labs/',
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('uses provider polling options for timeout behavior', async () => {
|
|
92
|
+
server.urls['https://api.example.com/poll'].response = ({
|
|
93
|
+
callNumber,
|
|
94
|
+
}) => ({
|
|
95
|
+
type: 'json-value',
|
|
96
|
+
body: { status: 'Pending', callNumber },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const provider = createBlackForestLabs({
|
|
100
|
+
apiKey: 'test-api-key',
|
|
101
|
+
baseURL: 'https://api.example.com/v1',
|
|
102
|
+
pollIntervalMillis: 10,
|
|
103
|
+
pollTimeoutMillis: 25,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const model = provider.image('flux-pro-1.1');
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
model.doGenerate({
|
|
110
|
+
prompt: 'Timeout test',
|
|
111
|
+
files: undefined,
|
|
112
|
+
mask: undefined,
|
|
113
|
+
n: 1,
|
|
114
|
+
size: undefined,
|
|
115
|
+
seed: undefined,
|
|
116
|
+
aspectRatio: '1:1',
|
|
117
|
+
providerOptions: {},
|
|
118
|
+
}),
|
|
119
|
+
).rejects.toThrow('Black Forest Labs generation timed out.');
|
|
120
|
+
|
|
121
|
+
const pollCalls = server.calls.filter(
|
|
122
|
+
c =>
|
|
123
|
+
c.requestMethod === 'GET' &&
|
|
124
|
+
c.requestUrl.startsWith('https://api.example.com/poll'),
|
|
125
|
+
);
|
|
126
|
+
expect(pollCalls.length).toBe(3);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('throws NoSuchModelError for unsupported model types', () => {
|
|
130
|
+
const provider = createBlackForestLabs();
|
|
131
|
+
|
|
132
|
+
expect(() => provider.languageModel('some-id')).toThrowError(
|
|
133
|
+
'No such languageModel',
|
|
134
|
+
);
|
|
135
|
+
expect(() => provider.embeddingModel('some-id')).toThrowError(
|
|
136
|
+
'No such embeddingModel',
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
});
|