@ai-sdk/prodia 2.0.0-beta.3 → 2.0.0-beta.31
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 +230 -0
- package/README.md +2 -0
- package/dist/index.d.ts +34 -2
- package/dist/index.js +754 -183
- package/dist/index.js.map +1 -1
- package/package.json +9 -10
- package/src/index.ts +4 -0
- package/src/prodia-api.ts +198 -0
- package/src/prodia-image-model.ts +34 -197
- package/src/prodia-language-model-settings.ts +6 -0
- package/src/prodia-language-model.ts +429 -0
- package/src/prodia-provider.ts +40 -8
- package/src/prodia-video-model-settings.ts +7 -0
- package/src/prodia-video-model.ts +282 -0
- package/dist/index.d.mts +0 -58
- package/dist/index.mjs +0 -423
- package/dist/index.mjs.map +0 -1
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import type { ImageModelV4, SharedV4Warning } from '@ai-sdk/provider';
|
|
2
|
-
import type { InferSchema
|
|
2
|
+
import type { InferSchema } from '@ai-sdk/provider-utils';
|
|
3
3
|
import {
|
|
4
4
|
combineHeaders,
|
|
5
|
-
createJsonErrorResponseHandler,
|
|
6
|
-
type FetchFunction,
|
|
7
5
|
lazySchema,
|
|
6
|
+
parseJSON,
|
|
8
7
|
parseProviderOptions,
|
|
9
8
|
postToApi,
|
|
10
9
|
resolve,
|
|
10
|
+
serializeModelOptions,
|
|
11
|
+
WORKFLOW_SERIALIZE,
|
|
12
|
+
WORKFLOW_DESERIALIZE,
|
|
11
13
|
zodSchema,
|
|
12
14
|
} from '@ai-sdk/provider-utils';
|
|
13
15
|
import { z } from 'zod/v4';
|
|
16
|
+
import type { ProdiaModelConfig } from './prodia-api';
|
|
17
|
+
import {
|
|
18
|
+
buildProdiaProviderMetadata,
|
|
19
|
+
parseMultipart,
|
|
20
|
+
prodiaFailedResponseHandler,
|
|
21
|
+
prodiaJobResultSchema,
|
|
22
|
+
} from './prodia-api';
|
|
23
|
+
import type { ProdiaJobResult } from './prodia-api';
|
|
14
24
|
import type { ProdiaImageModelId } from './prodia-image-settings';
|
|
15
25
|
|
|
16
26
|
export class ProdiaImageModel implements ImageModelV4 {
|
|
@@ -21,9 +31,23 @@ export class ProdiaImageModel implements ImageModelV4 {
|
|
|
21
31
|
return this.config.provider;
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
static [WORKFLOW_SERIALIZE](model: ProdiaImageModel) {
|
|
35
|
+
return serializeModelOptions({
|
|
36
|
+
modelId: model.modelId,
|
|
37
|
+
config: model.config,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static [WORKFLOW_DESERIALIZE](options: {
|
|
42
|
+
modelId: ProdiaImageModelId;
|
|
43
|
+
config: ProdiaModelConfig;
|
|
44
|
+
}) {
|
|
45
|
+
return new ProdiaImageModel(options.modelId, options.config);
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
constructor(
|
|
25
49
|
readonly modelId: ProdiaImageModelId,
|
|
26
|
-
private readonly config:
|
|
50
|
+
private readonly config: ProdiaModelConfig,
|
|
27
51
|
) {}
|
|
28
52
|
|
|
29
53
|
private async getArgs({
|
|
@@ -109,7 +133,7 @@ export class ProdiaImageModel implements ImageModelV4 {
|
|
|
109
133
|
|
|
110
134
|
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
|
|
111
135
|
const combinedHeaders = combineHeaders(
|
|
112
|
-
await resolve(this.config.headers),
|
|
136
|
+
this.config.headers ? await resolve(this.config.headers) : undefined,
|
|
113
137
|
options.headers,
|
|
114
138
|
);
|
|
115
139
|
|
|
@@ -137,29 +161,7 @@ export class ProdiaImageModel implements ImageModelV4 {
|
|
|
137
161
|
warnings,
|
|
138
162
|
providerMetadata: {
|
|
139
163
|
prodia: {
|
|
140
|
-
images: [
|
|
141
|
-
{
|
|
142
|
-
jobId: jobResult.id,
|
|
143
|
-
...(jobResult.config?.seed != null && {
|
|
144
|
-
seed: jobResult.config.seed,
|
|
145
|
-
}),
|
|
146
|
-
...(jobResult.metrics?.elapsed != null && {
|
|
147
|
-
elapsed: jobResult.metrics.elapsed,
|
|
148
|
-
}),
|
|
149
|
-
...(jobResult.metrics?.ips != null && {
|
|
150
|
-
iterationsPerSecond: jobResult.metrics.ips,
|
|
151
|
-
}),
|
|
152
|
-
...(jobResult.created_at != null && {
|
|
153
|
-
createdAt: jobResult.created_at,
|
|
154
|
-
}),
|
|
155
|
-
...(jobResult.updated_at != null && {
|
|
156
|
-
updatedAt: jobResult.updated_at,
|
|
157
|
-
}),
|
|
158
|
-
...(jobResult.price?.dollars != null && {
|
|
159
|
-
dollars: jobResult.price.dollars,
|
|
160
|
-
}),
|
|
161
|
-
},
|
|
162
|
-
],
|
|
164
|
+
images: [buildProdiaProviderMetadata(jobResult)],
|
|
163
165
|
},
|
|
164
166
|
},
|
|
165
167
|
response: {
|
|
@@ -226,48 +228,6 @@ export type ProdiaImageModelOptions = InferSchema<
|
|
|
226
228
|
typeof prodiaImageModelOptionsSchema
|
|
227
229
|
>;
|
|
228
230
|
|
|
229
|
-
interface ProdiaImageModelConfig {
|
|
230
|
-
provider: string;
|
|
231
|
-
baseURL: string;
|
|
232
|
-
headers?: Resolvable<Record<string, string | undefined>>;
|
|
233
|
-
fetch?: FetchFunction;
|
|
234
|
-
_internal?: {
|
|
235
|
-
currentDate?: () => Date;
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const prodiaJobResultSchema = z.object({
|
|
240
|
-
id: z.string(),
|
|
241
|
-
created_at: z.string().optional(),
|
|
242
|
-
updated_at: z.string().optional(),
|
|
243
|
-
expires_at: z.string().optional(),
|
|
244
|
-
state: z
|
|
245
|
-
.object({
|
|
246
|
-
current: z.string(),
|
|
247
|
-
})
|
|
248
|
-
.optional(),
|
|
249
|
-
config: z
|
|
250
|
-
.object({
|
|
251
|
-
seed: z.number().optional(),
|
|
252
|
-
})
|
|
253
|
-
.passthrough()
|
|
254
|
-
.optional(),
|
|
255
|
-
metrics: z
|
|
256
|
-
.object({
|
|
257
|
-
elapsed: z.number().optional(),
|
|
258
|
-
ips: z.number().optional(),
|
|
259
|
-
})
|
|
260
|
-
.optional(),
|
|
261
|
-
price: z
|
|
262
|
-
.object({
|
|
263
|
-
product: z.string(),
|
|
264
|
-
dollars: z.number(),
|
|
265
|
-
})
|
|
266
|
-
.nullish(),
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
type ProdiaJobResult = z.infer<typeof prodiaJobResultSchema>;
|
|
270
|
-
|
|
271
231
|
interface MultipartResult {
|
|
272
232
|
jobResult: ProdiaJobResult;
|
|
273
233
|
imageBytes: Uint8Array;
|
|
@@ -310,7 +270,10 @@ function createMultipartResponseHandler() {
|
|
|
310
270
|
|
|
311
271
|
if (contentDisposition.includes('name="job"')) {
|
|
312
272
|
const jsonStr = new TextDecoder().decode(part.body);
|
|
313
|
-
jobResult =
|
|
273
|
+
jobResult = await parseJSON({
|
|
274
|
+
text: jsonStr,
|
|
275
|
+
schema: zodSchema(prodiaJobResultSchema),
|
|
276
|
+
});
|
|
314
277
|
} else if (contentDisposition.includes('name="output"')) {
|
|
315
278
|
imageBytes = part.body;
|
|
316
279
|
} else if (partContentType.startsWith('image/')) {
|
|
@@ -331,129 +294,3 @@ function createMultipartResponseHandler() {
|
|
|
331
294
|
};
|
|
332
295
|
};
|
|
333
296
|
}
|
|
334
|
-
|
|
335
|
-
interface MultipartPart {
|
|
336
|
-
headers: Record<string, string>;
|
|
337
|
-
body: Uint8Array;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function parseMultipart(data: Uint8Array, boundary: string): MultipartPart[] {
|
|
341
|
-
const parts: MultipartPart[] = [];
|
|
342
|
-
const boundaryBytes = new TextEncoder().encode(`--${boundary}`);
|
|
343
|
-
const endBoundaryBytes = new TextEncoder().encode(`--${boundary}--`);
|
|
344
|
-
|
|
345
|
-
const positions: number[] = [];
|
|
346
|
-
for (let i = 0; i <= data.length - boundaryBytes.length; i++) {
|
|
347
|
-
let match = true;
|
|
348
|
-
for (let j = 0; j < boundaryBytes.length; j++) {
|
|
349
|
-
if (data[i + j] !== boundaryBytes[j]) {
|
|
350
|
-
match = false;
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
if (match) {
|
|
355
|
-
positions.push(i);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
for (let i = 0; i < positions.length - 1; i++) {
|
|
360
|
-
const start = positions[i] + boundaryBytes.length;
|
|
361
|
-
const end = positions[i + 1];
|
|
362
|
-
|
|
363
|
-
let isEndBoundary = true;
|
|
364
|
-
for (let j = 0; j < endBoundaryBytes.length && isEndBoundary; j++) {
|
|
365
|
-
if (data[positions[i] + j] !== endBoundaryBytes[j]) {
|
|
366
|
-
isEndBoundary = false;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (
|
|
370
|
-
isEndBoundary &&
|
|
371
|
-
positions[i] + endBoundaryBytes.length <= data.length
|
|
372
|
-
) {
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
let partStart = start;
|
|
377
|
-
if (data[partStart] === 0x0d && data[partStart + 1] === 0x0a) {
|
|
378
|
-
partStart += 2;
|
|
379
|
-
} else if (data[partStart] === 0x0a) {
|
|
380
|
-
partStart += 1;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
let partEnd = end;
|
|
384
|
-
if (data[partEnd - 2] === 0x0d && data[partEnd - 1] === 0x0a) {
|
|
385
|
-
partEnd -= 2;
|
|
386
|
-
} else if (data[partEnd - 1] === 0x0a) {
|
|
387
|
-
partEnd -= 1;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const partData = data.slice(partStart, partEnd);
|
|
391
|
-
|
|
392
|
-
let headerEnd = -1;
|
|
393
|
-
for (let j = 0; j < partData.length - 3; j++) {
|
|
394
|
-
if (
|
|
395
|
-
partData[j] === 0x0d &&
|
|
396
|
-
partData[j + 1] === 0x0a &&
|
|
397
|
-
partData[j + 2] === 0x0d &&
|
|
398
|
-
partData[j + 3] === 0x0a
|
|
399
|
-
) {
|
|
400
|
-
headerEnd = j;
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
if (partData[j] === 0x0a && partData[j + 1] === 0x0a) {
|
|
404
|
-
headerEnd = j;
|
|
405
|
-
break;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (headerEnd === -1) {
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const headerBytes = partData.slice(0, headerEnd);
|
|
414
|
-
const headerStr = new TextDecoder().decode(headerBytes);
|
|
415
|
-
const headers: Record<string, string> = {};
|
|
416
|
-
for (const line of headerStr.split(/\r?\n/)) {
|
|
417
|
-
const colonIdx = line.indexOf(':');
|
|
418
|
-
if (colonIdx > 0) {
|
|
419
|
-
const key = line.slice(0, colonIdx).trim().toLowerCase();
|
|
420
|
-
const value = line.slice(colonIdx + 1).trim();
|
|
421
|
-
headers[key] = value;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
let bodyStart = headerEnd + 2;
|
|
426
|
-
if (partData[headerEnd] === 0x0d) {
|
|
427
|
-
bodyStart = headerEnd + 4;
|
|
428
|
-
}
|
|
429
|
-
const body = partData.slice(bodyStart);
|
|
430
|
-
|
|
431
|
-
parts.push({ headers, body });
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return parts;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const prodiaErrorSchema = z.object({
|
|
438
|
-
message: z.string().optional(),
|
|
439
|
-
detail: z.unknown().optional(),
|
|
440
|
-
error: z.string().optional(),
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
const prodiaFailedResponseHandler = createJsonErrorResponseHandler({
|
|
444
|
-
errorSchema: prodiaErrorSchema,
|
|
445
|
-
errorToMessage: error => {
|
|
446
|
-
const parsed = prodiaErrorSchema.safeParse(error);
|
|
447
|
-
if (!parsed.success) return 'Unknown Prodia error';
|
|
448
|
-
const { message, detail, error: errorField } = parsed.data;
|
|
449
|
-
if (typeof detail === 'string') return detail;
|
|
450
|
-
if (detail != null) {
|
|
451
|
-
try {
|
|
452
|
-
return JSON.stringify(detail);
|
|
453
|
-
} catch {
|
|
454
|
-
// ignore
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return errorField ?? message ?? 'Unknown Prodia error';
|
|
458
|
-
},
|
|
459
|
-
});
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type LanguageModelV4,
|
|
3
|
+
type LanguageModelV4CallOptions,
|
|
4
|
+
type LanguageModelV4Content,
|
|
5
|
+
type LanguageModelV4StreamPart,
|
|
6
|
+
type SharedV4Warning,
|
|
7
|
+
UnsupportedFunctionalityError,
|
|
8
|
+
} from '@ai-sdk/provider';
|
|
9
|
+
import type { InferSchema } from '@ai-sdk/provider-utils';
|
|
10
|
+
import {
|
|
11
|
+
combineHeaders,
|
|
12
|
+
isCustomReasoning,
|
|
13
|
+
isProviderReference,
|
|
14
|
+
convertBase64ToUint8Array,
|
|
15
|
+
generateId,
|
|
16
|
+
lazySchema,
|
|
17
|
+
parseJSON,
|
|
18
|
+
parseProviderOptions,
|
|
19
|
+
postFormDataToApi,
|
|
20
|
+
resolve,
|
|
21
|
+
serializeModelOptions,
|
|
22
|
+
WORKFLOW_SERIALIZE,
|
|
23
|
+
WORKFLOW_DESERIALIZE,
|
|
24
|
+
zodSchema,
|
|
25
|
+
} from '@ai-sdk/provider-utils';
|
|
26
|
+
import { z } from 'zod/v4';
|
|
27
|
+
import type { ProdiaModelConfig } from './prodia-api';
|
|
28
|
+
import {
|
|
29
|
+
buildProdiaProviderMetadata,
|
|
30
|
+
parseMultipart,
|
|
31
|
+
prodiaFailedResponseHandler,
|
|
32
|
+
prodiaJobResultSchema,
|
|
33
|
+
} from './prodia-api';
|
|
34
|
+
import type { ProdiaJobResult } from './prodia-api';
|
|
35
|
+
import type { ProdiaLanguageModelId } from './prodia-language-model-settings';
|
|
36
|
+
|
|
37
|
+
export class ProdiaLanguageModel implements LanguageModelV4 {
|
|
38
|
+
readonly specificationVersion = 'v4';
|
|
39
|
+
readonly supportedUrls = {};
|
|
40
|
+
|
|
41
|
+
get provider(): string {
|
|
42
|
+
return this.config.provider;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static [WORKFLOW_SERIALIZE](model: ProdiaLanguageModel) {
|
|
46
|
+
return serializeModelOptions({
|
|
47
|
+
modelId: model.modelId,
|
|
48
|
+
config: model.config,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static [WORKFLOW_DESERIALIZE](options: {
|
|
53
|
+
modelId: ProdiaLanguageModelId;
|
|
54
|
+
config: ProdiaModelConfig;
|
|
55
|
+
}) {
|
|
56
|
+
return new ProdiaLanguageModel(options.modelId, options.config);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
readonly modelId: ProdiaLanguageModelId,
|
|
61
|
+
private readonly config: ProdiaModelConfig,
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
async doGenerate(options: LanguageModelV4CallOptions) {
|
|
65
|
+
const warnings: Array<SharedV4Warning> = [];
|
|
66
|
+
|
|
67
|
+
// Warn about unsupported LLM features
|
|
68
|
+
if (options.temperature !== undefined) {
|
|
69
|
+
warnings.push({ type: 'unsupported', feature: 'temperature' });
|
|
70
|
+
}
|
|
71
|
+
if (options.topP !== undefined) {
|
|
72
|
+
warnings.push({ type: 'unsupported', feature: 'topP' });
|
|
73
|
+
}
|
|
74
|
+
if (options.topK !== undefined) {
|
|
75
|
+
warnings.push({ type: 'unsupported', feature: 'topK' });
|
|
76
|
+
}
|
|
77
|
+
if (options.maxOutputTokens !== undefined) {
|
|
78
|
+
warnings.push({ type: 'unsupported', feature: 'maxOutputTokens' });
|
|
79
|
+
}
|
|
80
|
+
if (options.stopSequences !== undefined) {
|
|
81
|
+
warnings.push({ type: 'unsupported', feature: 'stopSequences' });
|
|
82
|
+
}
|
|
83
|
+
if (options.presencePenalty !== undefined) {
|
|
84
|
+
warnings.push({ type: 'unsupported', feature: 'presencePenalty' });
|
|
85
|
+
}
|
|
86
|
+
if (options.frequencyPenalty !== undefined) {
|
|
87
|
+
warnings.push({ type: 'unsupported', feature: 'frequencyPenalty' });
|
|
88
|
+
}
|
|
89
|
+
if (options.tools !== undefined && options.tools.length > 0) {
|
|
90
|
+
warnings.push({ type: 'unsupported', feature: 'tools' });
|
|
91
|
+
}
|
|
92
|
+
if (options.toolChoice !== undefined) {
|
|
93
|
+
warnings.push({ type: 'unsupported', feature: 'toolChoice' });
|
|
94
|
+
}
|
|
95
|
+
if (
|
|
96
|
+
options.responseFormat !== undefined &&
|
|
97
|
+
options.responseFormat.type !== 'text'
|
|
98
|
+
) {
|
|
99
|
+
warnings.push({ type: 'unsupported', feature: 'responseFormat' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isCustomReasoning(options.reasoning)) {
|
|
103
|
+
warnings.push({
|
|
104
|
+
type: 'unsupported',
|
|
105
|
+
feature: 'reasoning',
|
|
106
|
+
details: 'This provider does not support reasoning configuration.',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const prodiaOptions = await parseProviderOptions({
|
|
111
|
+
provider: 'prodia',
|
|
112
|
+
providerOptions: options.providerOptions,
|
|
113
|
+
schema: prodiaLanguageModelOptionsSchema,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Extract text prompt from messages
|
|
117
|
+
let prompt = '';
|
|
118
|
+
let systemMessage = '';
|
|
119
|
+
for (const message of options.prompt) {
|
|
120
|
+
if (message.role === 'system') {
|
|
121
|
+
systemMessage = message.content;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Get text from the last user message
|
|
125
|
+
for (let i = options.prompt.length - 1; i >= 0; i--) {
|
|
126
|
+
const message = options.prompt[i];
|
|
127
|
+
if (message.role === 'user') {
|
|
128
|
+
for (const part of message.content) {
|
|
129
|
+
if (part.type === 'text') {
|
|
130
|
+
prompt += (prompt ? '\n' : '') + part.text;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (systemMessage) {
|
|
137
|
+
prompt = systemMessage + '\n' + prompt;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Extract image from user messages
|
|
141
|
+
let imageBytes: Uint8Array | undefined;
|
|
142
|
+
let imageMediaType = 'image/png';
|
|
143
|
+
for (let i = options.prompt.length - 1; i >= 0; i--) {
|
|
144
|
+
const message = options.prompt[i];
|
|
145
|
+
if (message.role === 'user') {
|
|
146
|
+
for (const part of message.content) {
|
|
147
|
+
if (part.type === 'file' && part.mediaType.startsWith('image/')) {
|
|
148
|
+
if (isProviderReference(part.data)) {
|
|
149
|
+
throw new UnsupportedFunctionalityError({
|
|
150
|
+
functionality: 'file parts with provider references',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (part.data instanceof Uint8Array) {
|
|
155
|
+
imageBytes = part.data;
|
|
156
|
+
} else if (typeof part.data === 'string') {
|
|
157
|
+
// base64 encoded
|
|
158
|
+
imageBytes = convertBase64ToUint8Array(part.data);
|
|
159
|
+
} else if (part.data instanceof URL) {
|
|
160
|
+
const fetchFn = this.config.fetch ?? globalThis.fetch;
|
|
161
|
+
const response = await fetchFn(part.data.toString());
|
|
162
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
163
|
+
imageBytes = new Uint8Array(arrayBuffer);
|
|
164
|
+
}
|
|
165
|
+
imageMediaType = part.mediaType;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const jobConfig: Record<string, unknown> = {
|
|
174
|
+
prompt,
|
|
175
|
+
include_messages: true,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
if (prodiaOptions?.aspectRatio !== undefined) {
|
|
179
|
+
jobConfig.aspect_ratio = prodiaOptions.aspectRatio;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const body = {
|
|
183
|
+
type: this.modelId,
|
|
184
|
+
config: jobConfig,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const currentDate = this.config._internal?.currentDate?.() ?? new Date();
|
|
188
|
+
const combinedHeaders = combineHeaders(
|
|
189
|
+
this.config.headers ? await resolve(this.config.headers) : undefined,
|
|
190
|
+
options.headers,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Always use multipart form-data since img2img requires image input
|
|
194
|
+
const formData = new FormData();
|
|
195
|
+
formData.append(
|
|
196
|
+
'job',
|
|
197
|
+
new Blob([JSON.stringify(body)], { type: 'application/json' }),
|
|
198
|
+
'job.json',
|
|
199
|
+
);
|
|
200
|
+
if (imageBytes) {
|
|
201
|
+
const ext =
|
|
202
|
+
imageMediaType === 'image/png'
|
|
203
|
+
? '.png'
|
|
204
|
+
: imageMediaType === 'image/jpeg'
|
|
205
|
+
? '.jpg'
|
|
206
|
+
: imageMediaType === 'image/webp'
|
|
207
|
+
? '.webp'
|
|
208
|
+
: '';
|
|
209
|
+
formData.append(
|
|
210
|
+
'input',
|
|
211
|
+
new Blob([imageBytes], { type: imageMediaType }),
|
|
212
|
+
'input' + ext,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const { value: multipartResult, responseHeaders } = await postFormDataToApi(
|
|
217
|
+
{
|
|
218
|
+
url: `${this.config.baseURL}/job?price=true`,
|
|
219
|
+
headers: {
|
|
220
|
+
...combinedHeaders,
|
|
221
|
+
Accept: 'multipart/form-data',
|
|
222
|
+
},
|
|
223
|
+
formData,
|
|
224
|
+
failedResponseHandler: prodiaFailedResponseHandler,
|
|
225
|
+
successfulResponseHandler: createLanguageMultipartResponseHandler(),
|
|
226
|
+
abortSignal: options.abortSignal,
|
|
227
|
+
fetch: this.config.fetch,
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const { jobResult, textContent, fileContent } = multipartResult;
|
|
232
|
+
|
|
233
|
+
const content: Array<LanguageModelV4Content> = [];
|
|
234
|
+
if (textContent !== undefined) {
|
|
235
|
+
content.push({ type: 'text', text: textContent });
|
|
236
|
+
}
|
|
237
|
+
for (const file of fileContent) {
|
|
238
|
+
content.push({
|
|
239
|
+
type: 'file',
|
|
240
|
+
mediaType: file.mediaType,
|
|
241
|
+
data: file.data,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
content,
|
|
247
|
+
finishReason: { unified: 'stop' as const, raw: undefined },
|
|
248
|
+
usage: {
|
|
249
|
+
inputTokens: {
|
|
250
|
+
total: undefined,
|
|
251
|
+
noCache: undefined,
|
|
252
|
+
cacheRead: undefined,
|
|
253
|
+
cacheWrite: undefined,
|
|
254
|
+
},
|
|
255
|
+
outputTokens: {
|
|
256
|
+
total: undefined,
|
|
257
|
+
text: undefined,
|
|
258
|
+
reasoning: undefined,
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
warnings,
|
|
262
|
+
providerMetadata: {
|
|
263
|
+
prodia: buildProdiaProviderMetadata(jobResult),
|
|
264
|
+
},
|
|
265
|
+
response: {
|
|
266
|
+
modelId: this.modelId,
|
|
267
|
+
timestamp: currentDate,
|
|
268
|
+
headers: responseHeaders,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async doStream(options: LanguageModelV4CallOptions) {
|
|
274
|
+
const result = await this.doGenerate(options);
|
|
275
|
+
|
|
276
|
+
const stream = new ReadableStream<LanguageModelV4StreamPart>({
|
|
277
|
+
start(controller) {
|
|
278
|
+
controller.enqueue({
|
|
279
|
+
type: 'stream-start',
|
|
280
|
+
warnings: result.warnings,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
controller.enqueue({
|
|
284
|
+
type: 'response-metadata',
|
|
285
|
+
modelId: result.response?.modelId,
|
|
286
|
+
timestamp: result.response?.timestamp,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
for (const part of result.content) {
|
|
290
|
+
if (part.type === 'text') {
|
|
291
|
+
const id = generateId();
|
|
292
|
+
controller.enqueue({ type: 'text-start', id });
|
|
293
|
+
controller.enqueue({
|
|
294
|
+
type: 'text-delta',
|
|
295
|
+
id,
|
|
296
|
+
delta: part.text,
|
|
297
|
+
});
|
|
298
|
+
controller.enqueue({ type: 'text-end', id });
|
|
299
|
+
} else if (part.type === 'file') {
|
|
300
|
+
controller.enqueue({
|
|
301
|
+
type: 'file',
|
|
302
|
+
mediaType: part.mediaType,
|
|
303
|
+
data: part.data,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
controller.enqueue({
|
|
309
|
+
type: 'finish',
|
|
310
|
+
usage: result.usage,
|
|
311
|
+
finishReason: result.finishReason,
|
|
312
|
+
providerMetadata: result.providerMetadata,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
controller.close();
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
stream,
|
|
321
|
+
response: {
|
|
322
|
+
headers: result.response?.headers,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export const prodiaLanguageModelOptionsSchema = lazySchema(() =>
|
|
329
|
+
zodSchema(
|
|
330
|
+
z.object({
|
|
331
|
+
/**
|
|
332
|
+
* Aspect ratio for the output image.
|
|
333
|
+
*/
|
|
334
|
+
aspectRatio: z
|
|
335
|
+
.enum([
|
|
336
|
+
'1:1',
|
|
337
|
+
'2:3',
|
|
338
|
+
'3:2',
|
|
339
|
+
'4:5',
|
|
340
|
+
'5:4',
|
|
341
|
+
'4:7',
|
|
342
|
+
'7:4',
|
|
343
|
+
'9:16',
|
|
344
|
+
'16:9',
|
|
345
|
+
'9:21',
|
|
346
|
+
'21:9',
|
|
347
|
+
])
|
|
348
|
+
.optional(),
|
|
349
|
+
}),
|
|
350
|
+
),
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
export type ProdiaLanguageModelOptions = InferSchema<
|
|
354
|
+
typeof prodiaLanguageModelOptionsSchema
|
|
355
|
+
>;
|
|
356
|
+
|
|
357
|
+
interface LanguageMultipartResult {
|
|
358
|
+
jobResult: ProdiaJobResult;
|
|
359
|
+
textContent: string | undefined;
|
|
360
|
+
fileContent: Array<{ mediaType: string; data: Uint8Array }>;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function createLanguageMultipartResponseHandler() {
|
|
364
|
+
return async ({
|
|
365
|
+
response,
|
|
366
|
+
}: {
|
|
367
|
+
response: Response;
|
|
368
|
+
}): Promise<{
|
|
369
|
+
value: LanguageMultipartResult;
|
|
370
|
+
responseHeaders: Record<string, string>;
|
|
371
|
+
}> => {
|
|
372
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
373
|
+
const responseHeaders: Record<string, string> = {};
|
|
374
|
+
response.headers.forEach((value, key) => {
|
|
375
|
+
responseHeaders[key] = value;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
|
|
379
|
+
if (!boundaryMatch) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`Prodia response missing multipart boundary in content-type: ${contentType}`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const boundary = boundaryMatch[1];
|
|
385
|
+
|
|
386
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
387
|
+
const bytes = new Uint8Array(arrayBuffer);
|
|
388
|
+
|
|
389
|
+
const parts = parseMultipart(bytes, boundary);
|
|
390
|
+
|
|
391
|
+
let jobResult: ProdiaJobResult | undefined;
|
|
392
|
+
let textContent: string | undefined;
|
|
393
|
+
const fileContent: Array<{ mediaType: string; data: Uint8Array }> = [];
|
|
394
|
+
|
|
395
|
+
for (const part of parts) {
|
|
396
|
+
const contentDisposition = part.headers['content-disposition'] ?? '';
|
|
397
|
+
const partContentType = part.headers['content-type'] ?? '';
|
|
398
|
+
|
|
399
|
+
if (contentDisposition.includes('name="job"')) {
|
|
400
|
+
const jsonStr = new TextDecoder().decode(part.body);
|
|
401
|
+
jobResult = await parseJSON({
|
|
402
|
+
text: jsonStr,
|
|
403
|
+
schema: zodSchema(prodiaJobResultSchema),
|
|
404
|
+
});
|
|
405
|
+
} else if (contentDisposition.includes('name="output"')) {
|
|
406
|
+
if (
|
|
407
|
+
partContentType.startsWith('text/') ||
|
|
408
|
+
contentDisposition.includes('.txt')
|
|
409
|
+
) {
|
|
410
|
+
textContent = new TextDecoder().decode(part.body);
|
|
411
|
+
} else if (partContentType.startsWith('image/')) {
|
|
412
|
+
fileContent.push({
|
|
413
|
+
mediaType: partContentType,
|
|
414
|
+
data: part.body,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (!jobResult) {
|
|
421
|
+
throw new Error('Prodia multipart response missing job part');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
value: { jobResult, textContent, fileContent },
|
|
426
|
+
responseHeaders,
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
}
|