@ai-sdk/topaz 2.0.0 → 3.0.1

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.
@@ -2,9 +2,10 @@ import {
2
2
  AISDKError,
3
3
  InvalidArgumentError,
4
4
  InvalidResponseDataError,
5
- type ImageModelV3,
6
- type ImageModelV3CallOptions,
7
- type SharedV3Warning,
5
+ type ImageModelV4,
6
+ type ImageModelV4CallOptions,
7
+ type ImageModelV4Result,
8
+ type SharedV4Warning,
8
9
  } from '@ai-sdk/provider';
9
10
  import {
10
11
  combineHeaders,
@@ -12,16 +13,19 @@ import {
12
13
  createBinaryResponseHandler,
13
14
  createJsonResponseHandler,
14
15
  delay,
16
+ getFromApi,
15
17
  mediaTypeToExtension,
16
18
  parseProviderOptions,
17
19
  postFormDataToApi,
18
20
  removeUndefinedEntries,
19
21
  resolve,
22
+ serializeModelOptions,
23
+ WORKFLOW_DESERIALIZE,
24
+ WORKFLOW_SERIALIZE,
20
25
  } from '@ai-sdk/provider-utils';
21
26
  import { z } from 'zod/v4';
22
27
  import type { TopazConfig } from './topaz-config';
23
28
  import { topazFailedResponseHandler } from './topaz-error';
24
- import { topazGetFromApi } from './topaz-get-from-api';
25
29
  import {
26
30
  topazImageModelOptionsSchema,
27
31
  type TopazImageModelOptions,
@@ -38,8 +42,8 @@ const DEFAULT_POLL_TIMEOUT_MILLIS = 600_000;
38
42
  * Topaz enhances an image that the caller supplies, so `files` is required and
39
43
  * exactly one image is processed per call.
40
44
  */
41
- export class TopazImageModel implements ImageModelV3 {
42
- readonly specificationVersion = 'v3';
45
+ export class TopazImageModel implements ImageModelV4 {
46
+ readonly specificationVersion = 'v4';
43
47
  readonly maxImagesPerCall = 1;
44
48
  readonly supportsFileInputs = true;
45
49
  readonly supportsMaskInputs = false;
@@ -48,16 +52,30 @@ export class TopazImageModel implements ImageModelV3 {
48
52
  return this.config.provider;
49
53
  }
50
54
 
55
+ static [WORKFLOW_SERIALIZE](model: TopazImageModel) {
56
+ return serializeModelOptions({
57
+ modelId: model.modelId,
58
+ config: model.config,
59
+ });
60
+ }
61
+
62
+ static [WORKFLOW_DESERIALIZE](options: {
63
+ modelId: TopazImageModelId;
64
+ config: TopazConfig;
65
+ }) {
66
+ return new TopazImageModel(options.modelId, options.config);
67
+ }
68
+
51
69
  constructor(
52
70
  readonly modelId: TopazImageModelId,
53
71
  private readonly config: TopazConfig,
54
72
  ) {}
55
73
 
56
74
  async doGenerate(
57
- options: ImageModelV3CallOptions,
58
- ): Promise<Awaited<ReturnType<ImageModelV3['doGenerate']>>> {
75
+ options: ImageModelV4CallOptions,
76
+ ): Promise<ImageModelV4Result> {
59
77
  const currentDate = this.config._internal?.currentDate?.() ?? new Date();
60
- const warnings: SharedV3Warning[] = [];
78
+ const warnings: SharedV4Warning[] = [];
61
79
 
62
80
  const topazOptions = await parseProviderOptions({
63
81
  provider: 'topaz',
@@ -151,8 +169,8 @@ export class TopazImageModel implements ImageModelV3 {
151
169
  }
152
170
 
153
171
  private addUnsupportedWarnings(
154
- options: ImageModelV3CallOptions,
155
- warnings: SharedV3Warning[],
172
+ options: ImageModelV4CallOptions,
173
+ warnings: SharedV4Warning[],
156
174
  ): void {
157
175
  if (options.prompt != null) {
158
176
  warnings.push({
@@ -202,9 +220,9 @@ export class TopazImageModel implements ImageModelV3 {
202
220
  }
203
221
 
204
222
  private async buildFormData(
205
- options: ImageModelV3CallOptions,
223
+ options: ImageModelV4CallOptions,
206
224
  topazOptions: TopazImageModelOptions | undefined,
207
- warnings: SharedV3Warning[],
225
+ warnings: SharedV4Warning[],
208
226
  ): Promise<FormData> {
209
227
  const file = options.files?.[0];
210
228
 
@@ -298,7 +316,7 @@ export class TopazImageModel implements ImageModelV3 {
298
316
  const deadline = Date.now() + pollTimeoutMillis;
299
317
 
300
318
  while (true) {
301
- const { value: status } = await topazGetFromApi({
319
+ const { value: status } = await getFromApi({
302
320
  url: `${this.config.baseURL}/image/v1/status/${processId}`,
303
321
  // Built from the configured baseURL, not from response data.
304
322
  validateUrl: false,
@@ -367,7 +385,7 @@ export class TopazImageModel implements ImageModelV3 {
367
385
  image: Uint8Array;
368
386
  responseHeaders: Record<string, string> | undefined;
369
387
  }> {
370
- const { value: downloadResponse } = await topazGetFromApi({
388
+ const { value: downloadResponse } = await getFromApi({
371
389
  url: `${this.config.baseURL}/image/v1/download/${processId}`,
372
390
  // Built from the configured baseURL, not from response data.
373
391
  validateUrl: false,
@@ -388,7 +406,7 @@ export class TopazImageModel implements ImageModelV3 {
388
406
  });
389
407
  }
390
408
 
391
- const { value: image, responseHeaders } = await topazGetFromApi({
409
+ const { value: image, responseHeaders } = await getFromApi({
392
410
  url: downloadUrl,
393
411
  // Comes from the Topaz response body and normally points at object
394
412
  // storage, so it is validated and the API key is withheld off-origin.
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  NoSuchModelError,
3
- type Experimental_VideoModelV3 as VideoModelV3,
4
- type ImageModelV3,
5
- type ProviderV3,
3
+ type Experimental_VideoModelV4 as VideoModelV4,
4
+ type ImageModelV4,
5
+ type ProviderV4,
6
6
  } from '@ai-sdk/provider';
7
7
  import {
8
8
  loadApiKey,
@@ -42,26 +42,26 @@ export interface TopazProviderSettings {
42
42
  fetch?: FetchFunction;
43
43
  }
44
44
 
45
- export interface TopazProvider extends ProviderV3 {
45
+ export interface TopazProvider extends ProviderV4 {
46
46
  /**
47
47
  * Creates a model for image enhancement.
48
48
  */
49
- image(modelId: TopazImageModelId): ImageModelV3;
49
+ image(modelId: TopazImageModelId): ImageModelV4;
50
50
 
51
51
  /**
52
52
  * Creates a model for image enhancement.
53
53
  */
54
- imageModel(modelId: TopazImageModelId): ImageModelV3;
54
+ imageModel(modelId: TopazImageModelId): ImageModelV4;
55
55
 
56
56
  /**
57
57
  * Creates a model for video enhancement.
58
58
  */
59
- video(modelId: TopazVideoModelId): VideoModelV3;
59
+ video(modelId: TopazVideoModelId): VideoModelV4;
60
60
 
61
61
  /**
62
62
  * Creates a model for video enhancement.
63
63
  */
64
- videoModel(modelId: TopazVideoModelId): VideoModelV3;
64
+ videoModel(modelId: TopazVideoModelId): VideoModelV4;
65
65
  }
66
66
 
67
67
  const defaultBaseURL = 'https://api.topazlabs.com';
@@ -89,7 +89,7 @@ export function createTopaz(
89
89
  `ai-sdk-topaz/${VERSION}`,
90
90
  );
91
91
 
92
- const createImageModel = (modelId: TopazImageModelId): ImageModelV3 =>
92
+ const createImageModel = (modelId: TopazImageModelId): ImageModelV4 =>
93
93
  new TopazImageModel(modelId, {
94
94
  provider: 'topaz.image',
95
95
  baseURL,
@@ -97,7 +97,7 @@ export function createTopaz(
97
97
  fetch: options.fetch,
98
98
  });
99
99
 
100
- const createVideoModel = (modelId: TopazVideoModelId): VideoModelV3 =>
100
+ const createVideoModel = (modelId: TopazVideoModelId): VideoModelV4 =>
101
101
  new TopazVideoModel(modelId, {
102
102
  provider: 'topaz.video',
103
103
  baseURL,
@@ -118,7 +118,7 @@ export function createTopaz(
118
118
  };
119
119
 
120
120
  const provider: TopazProvider = {
121
- specificationVersion: 'v3' as const,
121
+ specificationVersion: 'v4' as const,
122
122
  image: createImageModel,
123
123
  imageModel: createImageModel,
124
124
  video: createVideoModel,
@@ -175,10 +175,6 @@ export type TopazVideoOutput = z.infer<typeof topazVideoOutputSchema>;
175
175
  export const topazVideoModelOptionsSchema = lazySchema(() =>
176
176
  zodSchema(
177
177
  z.object({
178
- /** Interval between status polls, in milliseconds. Defaults to 2000. */
179
- pollIntervalMillis: z.number().positive().optional(),
180
- /** Maximum time to poll, in milliseconds. Defaults to 3600000 (1 hour). */
181
- pollTimeoutMillis: z.number().positive().optional(),
182
178
  source: topazVideoSourceSchema.optional(),
183
179
  output: topazVideoOutputSchema.optional(),
184
180
 
@@ -280,8 +276,6 @@ export type TopazVideoModelOptions = InferSchema<
280
276
  * forwarded into the `filters[]` entry.
281
277
  */
282
278
  export const TOPAZ_NON_FILTER_OPTION_KEYS = [
283
- 'pollIntervalMillis',
284
- 'pollTimeoutMillis',
285
279
  'source',
286
280
  'output',
287
281
  'additionalFilters',
@@ -1,29 +1,32 @@
1
1
  import {
2
- AISDKError,
3
2
  APICallError,
4
3
  InvalidArgumentError,
5
4
  InvalidResponseDataError,
6
- type Experimental_VideoModelV3 as VideoModelV3,
7
- type Experimental_VideoModelV3File as VideoModelV3File,
8
- type SharedV3Warning,
5
+ type Experimental_VideoModelV4 as VideoModelV4,
6
+ type Experimental_VideoModelV4File as VideoModelV4File,
7
+ type Experimental_VideoModelV4OperationStartResult as VideoModelV4OperationStartResult,
8
+ type Experimental_VideoModelV4OperationStatusResult as VideoModelV4OperationStatusResult,
9
+ type SharedV4Warning,
9
10
  } from '@ai-sdk/provider';
10
11
  import {
11
12
  combineHeaders,
12
13
  convertBase64ToUint8Array,
13
14
  createJsonResponseHandler,
14
- delay,
15
+ getFromApi,
15
16
  isSameOrigin,
16
17
  parseProviderOptions,
17
18
  postJsonToApi,
18
19
  removeUndefinedEntries,
19
20
  resolve,
21
+ serializeModelOptions,
20
22
  validateDownloadUrl,
21
23
  withUserAgentSuffix,
24
+ WORKFLOW_DESERIALIZE,
25
+ WORKFLOW_SERIALIZE,
22
26
  } from '@ai-sdk/provider-utils';
23
27
  import { z } from 'zod/v4';
24
28
  import type { TopazConfig } from './topaz-config';
25
29
  import { topazFailedResponseHandler } from './topaz-error';
26
- import { topazGetFromApi } from './topaz-get-from-api';
27
30
  import {
28
31
  TOPAZ_NON_FILTER_OPTION_KEYS,
29
32
  type topazOutputContainers,
@@ -37,12 +40,6 @@ import {
37
40
  } from './topaz-video-settings';
38
41
  import { VERSION } from './version';
39
42
 
40
- type VideoModelResult = Awaited<ReturnType<VideoModelV3['doGenerate']>>;
41
- type TopazVideoOperation = { requestId: string; outputContainer: string };
42
-
43
- const DEFAULT_POLL_INTERVAL_MILLIS = 2000;
44
- const DEFAULT_POLL_TIMEOUT_MILLIS = 3_600_000;
45
-
46
43
  type TopazSourceContainer = (typeof topazSourceContainers)[number];
47
44
  type TopazOutputContainer = (typeof topazOutputContainers)[number];
48
45
 
@@ -87,91 +84,42 @@ const containerMediaTypes: Record<string, string> = {
87
84
 
88
85
  /**
89
86
  * Topaz video models enhance a video the caller supplies, passed through
90
- * `inputReferences`. `doGenerate` creates an express request: Topaz fetches URL
87
+ * `inputReferences`. `doStart` creates an express request: Topaz fetches URL
91
88
  * inputs itself, and file inputs are uploaded to the returned URL. Processing
92
- * starts once Topaz has the video, and the provider polls until it completes.
89
+ * starts once Topaz has the video, and `doStatus` polls.
93
90
  */
94
- export class TopazVideoModel implements VideoModelV3 {
95
- readonly specificationVersion = 'v3';
91
+ export class TopazVideoModel implements VideoModelV4 {
92
+ readonly specificationVersion = 'v4';
96
93
  readonly maxVideosPerCall = 1;
97
94
 
98
95
  get provider(): string {
99
96
  return this.config.provider;
100
97
  }
101
98
 
99
+ static [WORKFLOW_SERIALIZE](model: TopazVideoModel) {
100
+ return serializeModelOptions({
101
+ modelId: model.modelId,
102
+ config: model.config,
103
+ });
104
+ }
105
+
106
+ static [WORKFLOW_DESERIALIZE](options: {
107
+ modelId: TopazVideoModelId;
108
+ config: TopazConfig;
109
+ }) {
110
+ return new TopazVideoModel(options.modelId, options.config);
111
+ }
112
+
102
113
  constructor(
103
114
  readonly modelId: TopazVideoModelId,
104
115
  private readonly config: TopazConfig,
105
116
  ) {}
106
117
 
107
- async doGenerate(
108
- options: Parameters<VideoModelV3['doGenerate']>[0],
109
- ): Promise<VideoModelResult> {
110
- const topazOptions = await parseProviderOptions({
111
- provider: 'topaz',
112
- providerOptions: options.providerOptions,
113
- schema: topazVideoModelOptionsSchema,
114
- });
115
- const started = await this.start(options);
116
- const pollIntervalMillis =
117
- topazOptions?.pollIntervalMillis ?? DEFAULT_POLL_INTERVAL_MILLIS;
118
- const pollTimeoutMillis =
119
- topazOptions?.pollTimeoutMillis ?? DEFAULT_POLL_TIMEOUT_MILLIS;
120
- const deadline = Date.now() + pollTimeoutMillis;
121
-
122
- try {
123
- while (true) {
124
- const result = await this.getStatus({
125
- operation: started.operation,
126
- headers: options.headers,
127
- abortSignal: options.abortSignal,
128
- });
129
- if (result != null) {
130
- return {
131
- ...result,
132
- warnings: [...started.warnings, ...result.warnings],
133
- response: {
134
- ...result.response,
135
- timestamp: started.response.timestamp,
136
- },
137
- };
138
- }
139
-
140
- if (Date.now() + pollIntervalMillis > deadline) {
141
- const headers = combineHeaders(
142
- await resolve(this.config.headers),
143
- options.headers,
144
- );
145
- await this.cancelQuietly(started.operation.requestId, headers);
146
- throw new AISDKError({
147
- name: 'TOPAZ_VIDEO_ENHANCEMENT_TIMEOUT',
148
- message:
149
- `Topaz video enhancement did not finish within ${pollTimeoutMillis}ms ` +
150
- `(request ${started.operation.requestId}). ` +
151
- 'Increase the `pollTimeoutMillis` provider option if the job needs longer.',
152
- });
153
- }
154
- await delay(pollIntervalMillis, { abortSignal: options.abortSignal });
155
- }
156
- } catch (error) {
157
- if (options.abortSignal?.aborted) {
158
- const headers = combineHeaders(
159
- await resolve(this.config.headers),
160
- options.headers,
161
- );
162
- await this.cancelQuietly(started.operation.requestId, headers);
163
- }
164
- throw error;
165
- }
166
- }
167
-
168
- private async start(
169
- options: Parameters<VideoModelV3['doGenerate']>[0],
170
- ): Promise<
171
- Omit<VideoModelResult, 'videos'> & { operation: TopazVideoOperation }
172
- > {
118
+ async doStart(
119
+ options: Parameters<NonNullable<VideoModelV4['doStart']>>[0],
120
+ ): Promise<VideoModelV4OperationStartResult> {
173
121
  const currentDate = this.config._internal?.currentDate?.() ?? new Date();
174
- const warnings: SharedV3Warning[] = [];
122
+ const warnings: SharedV4Warning[] = [];
175
123
 
176
124
  const topazOptions = await parseProviderOptions({
177
125
  provider: 'topaz',
@@ -248,7 +196,7 @@ export class TopazVideoModel implements VideoModelV3 {
248
196
  }
249
197
 
250
198
  return {
251
- // The output container travels with the operation so the polling code can
199
+ // The output container travels with the operation so `doStatus` can
252
200
  // report the right media type without re-deriving it.
253
201
  operation: { requestId, outputContainer: output.container },
254
202
  warnings,
@@ -277,18 +225,16 @@ export class TopazVideoModel implements VideoModelV3 {
277
225
  }
278
226
  }
279
227
 
280
- private async getStatus(options: {
281
- operation: TopazVideoOperation;
282
- headers?: Record<string, string | undefined>;
283
- abortSignal?: AbortSignal;
284
- }): Promise<VideoModelResult | undefined> {
228
+ async doStatus(
229
+ options: Parameters<NonNullable<VideoModelV4['doStatus']>>[0],
230
+ ): Promise<VideoModelV4OperationStatusResult> {
285
231
  const currentDate = this.config._internal?.currentDate?.() ?? new Date();
286
232
  const { requestId, outputContainer } = options.operation as {
287
233
  requestId: string;
288
234
  outputContainer?: string;
289
235
  };
290
236
 
291
- const { value: status, responseHeaders } = await topazGetFromApi({
237
+ const { value: status, responseHeaders } = await getFromApi({
292
238
  url: `${this.config.baseURL}/video/${requestId}/status`,
293
239
  // Built from the configured baseURL, not from response data.
294
240
  validateUrl: false,
@@ -324,6 +270,7 @@ export class TopazVideoModel implements VideoModelV3 {
324
270
  }
325
271
 
326
272
  return {
273
+ status: 'completed',
327
274
  videos: [
328
275
  {
329
276
  type: 'url',
@@ -355,24 +302,33 @@ export class TopazVideoModel implements VideoModelV3 {
355
302
  }
356
303
 
357
304
  if (status.status === 'failed' || status.status === 'canceled') {
358
- throw new AISDKError({
359
- name: 'TOPAZ_VIDEO_ENHANCEMENT_FAILED',
360
- message:
305
+ return {
306
+ status: 'error',
307
+ error:
361
308
  `Topaz video request ${requestId} ${status.status}` +
362
309
  (status.errorCode != null ? ` (${status.errorCode})` : '') +
363
310
  (status.message != null ? `: ${status.message}` : '.'),
364
- });
311
+ providerMetadata: {
312
+ topaz: {
313
+ requestId,
314
+ ...(status.errorCode != null
315
+ ? { errorCode: status.errorCode }
316
+ : {}),
317
+ },
318
+ },
319
+ response,
320
+ };
365
321
  }
366
322
 
367
323
  // Documented in-progress values are requested, accepted, initializing,
368
324
  // preprocessing, processing, postprocessing and canceling. Anything else
369
325
  // keeps polling too, so a newly added state cannot break running jobs.
370
- return undefined;
326
+ return { status: 'pending', response };
371
327
  }
372
328
 
373
329
  private addUnsupportedWarnings(
374
- options: Parameters<VideoModelV3['doGenerate']>[0],
375
- warnings: SharedV3Warning[],
330
+ options: Parameters<NonNullable<VideoModelV4['doStart']>>[0],
331
+ warnings: SharedV4Warning[],
376
332
  ): void {
377
333
  // `generateVideo` requires a prompt, so an empty one is the expected way
378
334
  // to call an enhancement model and does not warrant a warning.
@@ -445,9 +401,9 @@ export class TopazVideoModel implements VideoModelV3 {
445
401
  }
446
402
 
447
403
  private selectInputVideo(
448
- options: Parameters<VideoModelV3['doGenerate']>[0],
449
- warnings: SharedV3Warning[],
450
- ): VideoModelV3File {
404
+ options: Parameters<NonNullable<VideoModelV4['doStart']>>[0],
405
+ warnings: SharedV4Warning[],
406
+ ): VideoModelV4File {
451
407
  const references = options.inputReferences ?? [];
452
408
  const videos = references.filter(isVideoReference);
453
409
 
@@ -560,7 +516,7 @@ function lowerBoundCredits(
560
516
  }
561
517
 
562
518
  function fileBytes(
563
- input: Extract<VideoModelV3File, { type: 'file' }>,
519
+ input: Extract<VideoModelV4File, { type: 'file' }>,
564
520
  ): Uint8Array {
565
521
  return typeof input.data === 'string'
566
522
  ? convertBase64ToUint8Array(input.data)
@@ -568,7 +524,7 @@ function fileBytes(
568
524
  }
569
525
 
570
526
  function resolveContainer(
571
- input: VideoModelV3File,
527
+ input: VideoModelV4File,
572
528
  declaredContainer: TopazSourceContainer | undefined,
573
529
  ): TopazSourceContainer {
574
530
  if (declaredContainer != null) {
@@ -607,7 +563,7 @@ function resolveContainer(
607
563
  return container;
608
564
  }
609
565
 
610
- function isVideoReference(reference: VideoModelV3File): boolean {
566
+ function isVideoReference(reference: VideoModelV4File): boolean {
611
567
  if (reference.type === 'file') {
612
568
  return reference.mediaType.toLowerCase().startsWith('video/');
613
569
  }
@@ -709,7 +665,7 @@ function buildOutput({
709
665
  topazOptions,
710
666
  source,
711
667
  }: {
712
- options: Parameters<VideoModelV3['doGenerate']>[0];
668
+ options: Parameters<NonNullable<VideoModelV4['doStart']>>[0];
713
669
  topazOptions: TopazVideoModelOptions | undefined;
714
670
  source: ResolvedSource;
715
671
  }): Record<string, unknown> & { container: TopazOutputContainer } {
@@ -762,7 +718,7 @@ function buildOutput({
762
718
  }
763
719
 
764
720
  /**
765
- * Mirrors Topaz's container rules so the polling code can report the media type of
721
+ * Mirrors Topaz's container rules so `doStatus` can report the media type of
766
722
  * the file Topaz will actually produce.
767
723
  */
768
724
  function resolveOutputContainer(
@@ -1,53 +0,0 @@
1
- import {
2
- fetchWithValidatedEndpoint,
3
- fetchWithValidatedRedirects,
4
- getFromApi,
5
- isSameOrigin,
6
- type FetchFunction,
7
- } from '@ai-sdk/provider-utils';
8
-
9
- /**
10
- * Adapts URL validation to v6's getFromApi, which does not yet expose the
11
- * validateUrl and credentialedOrigin options.
12
- */
13
- export async function topazGetFromApi<T>({
14
- validateUrl,
15
- credentialedOrigin,
16
- trustedOrigin,
17
- ...options
18
- }: Parameters<typeof getFromApi<T>>[0] & {
19
- validateUrl: boolean;
20
- credentialedOrigin?: string;
21
- trustedOrigin?: string;
22
- }) {
23
- const validatedFetch: FetchFunction = (url, init) =>
24
- fetchWithValidatedRedirects({
25
- url: url.toString(),
26
- headers: init?.headers,
27
- abortSignal: init?.signal ?? undefined,
28
- trustedOrigin,
29
- fetch: (hopUrl, hopInit) =>
30
- fetchWithValidatedEndpoint({
31
- url: hopUrl.toString(),
32
- init: {
33
- ...hopInit,
34
- headers:
35
- credentialedOrigin != null &&
36
- isSameOrigin(hopUrl.toString(), credentialedOrigin)
37
- ? hopInit?.headers
38
- : undefined,
39
- },
40
- trustedOrigin,
41
- redirect: 'manual',
42
- fetch: options.fetch,
43
- }),
44
- });
45
-
46
- // v6 ignores this flag; validation is handled by the injected fetch above.
47
- const requestOptions = {
48
- ...options,
49
- validateUrl: false,
50
- fetch: validateUrl ? validatedFetch : options.fetch,
51
- };
52
- return getFromApi(requestOptions);
53
- }