@tanstack/ai-lovable 0.3.1 → 0.4.0

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.
@@ -28,7 +28,6 @@ export declare class LovableVideoAdapter<TModel extends LovableVideoModel> exten
28
28
  getVideoStatus(jobId: string): Promise<VideoStatusResult>;
29
29
  getVideoUrl(jobId: string): Promise<VideoUrlResult>;
30
30
  availableDurations(): DurationOptions<LovableVideoDuration>;
31
- snapDuration(seconds: number): LovableVideoDuration | undefined;
32
31
  protected mapStatus(apiStatus: string): 'pending' | 'processing' | 'completed' | 'failed';
33
32
  }
34
33
  /**
@@ -3,7 +3,7 @@ import { imagePartToFile } from "../image/image-input-to-file.js";
3
3
  import { toApiSeconds, validateHighResDuration, validateVideoSeconds, validateVideoSize } from "../video/video-provider-options.js";
4
4
  import OpenAI from "openai";
5
5
  import { arrayBufferToBase64 } from "@tanstack/ai-utils";
6
- import { BaseVideoAdapter, snapToDurationOption } from "@tanstack/ai/adapters";
6
+ import { BaseVideoAdapter } from "@tanstack/ai/adapters";
7
7
  import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
8
8
  import { resolveMediaPrompt } from "@tanstack/ai";
9
9
  //#region src/adapters/video.ts
@@ -109,9 +109,6 @@ var LovableVideoAdapter = class extends BaseVideoAdapter {
109
109
  values: VIDEO_DURATIONS
110
110
  };
111
111
  }
112
- snapDuration(seconds) {
113
- return snapToDurationOption(seconds, this.availableDurations());
114
- }
115
112
  mapStatus(apiStatus) {
116
113
  switch (apiStatus) {
117
114
  case "queued":
@@ -1 +1 @@
1
- {"version":3,"file":"video.js","names":[],"sources":["../../../src/adapters/video.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { arrayBufferToBase64 } from '@tanstack/ai-utils'\nimport {\n getLovableApiKeyFromEnv,\n openaiRequestOptions,\n withLovableDefaults,\n} from '../utils/client'\nimport { imagePartToFile } from '../image/image-input-to-file'\nimport {\n toApiSeconds,\n validateHighResDuration,\n validateVideoSeconds,\n validateVideoSize,\n} from '../video/video-provider-options'\nimport type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n VideoGenerationOptions,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type OpenAI_SDK from 'openai'\nimport type {\n LovableVideoModel,\n LovableVideoModelDurationByName,\n LovableVideoModelInputModalitiesByName,\n LovableVideoModelProviderOptionsByName,\n LovableVideoModelSizeByName,\n} from '../model-meta'\nimport type {\n LovableVideoDuration,\n LovableVideoProviderOptions,\n} from '../video/video-provider-options'\nimport type { LovableClientConfig } from '../utils/client'\n\nconst LARGE_MEDIA_BUFFER_BYTES = 10 * 1024 * 1024\nconst VIDEO_DURATIONS = [\n 4, 6, 8,\n] as const satisfies ReadonlyArray<LovableVideoDuration>\n\nfunction warnIfLargeMediaBuffer(byteLength: number, source: string): void {\n if (byteLength <= LARGE_MEDIA_BUFFER_BYTES) return\n console.warn(\n `[lovable.${source}] downloaded ${(byteLength / 1024 / 1024).toFixed(1)} MiB into memory before base64 encoding. ` +\n `Workers/serverless runtimes commonly run out of memory above ~10 MiB. ` +\n `Consider streaming the video through a CDN or your own storage layer instead.`,\n )\n}\n\n/**\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface LovableVideoConfig extends LovableClientConfig {\n /**\n * Opt into fetching HTTP(S) image URL inputs for `input_reference`.\n * The endpoint requires uploaded file bytes, so an HTTP(S) URL has to be\n * downloaded and buffered in memory. When `false` (the default), HTTP(S)\n * URL image inputs throw.\n */\n allowUrlFetch?: boolean\n}\n\n/**\n * @experimental Video generation is an experimental feature and may change.\n */\nexport class LovableVideoAdapter<\n TModel extends LovableVideoModel,\n> extends BaseVideoAdapter<\n TModel,\n LovableVideoProviderOptions,\n LovableVideoModelProviderOptionsByName,\n LovableVideoModelSizeByName,\n LovableVideoModelInputModalitiesByName,\n LovableVideoModelDurationByName\n> {\n readonly name = 'lovable' as const\n\n protected client: OpenAI\n protected clientConfig: LovableVideoConfig\n\n constructor(config: LovableVideoConfig, model: TModel) {\n super({}, model)\n this.clientConfig = config\n const { allowUrlFetch: _allowUrlFetch, ...clientOptions } = config\n this.client = new OpenAI(withLovableDefaults(clientOptions))\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n LovableVideoProviderOptions,\n LovableVideoModelSizeByName[TModel],\n LovableVideoModelDurationByName[TModel]\n >,\n ): Promise<VideoJobResult> {\n const { model, size, duration, modelOptions } = options\n\n const resolvedSize = size ?? modelOptions?.size\n validateVideoSize(model, resolvedSize)\n const seconds = duration ?? modelOptions?.seconds\n validateVideoSeconds(model, seconds)\n validateHighResDuration(model, resolvedSize, seconds)\n\n const resolved = resolveMediaPrompt(options.prompt)\n\n if (resolved.videos.length > 0) {\n throw new Error(\n `${this.name}.createVideoJob does not support video prompt parts (model: ${model}).`,\n )\n }\n if (resolved.audios.length > 0) {\n throw new Error(\n `${this.name}.createVideoJob does not support audio prompt parts (model: ${model}).`,\n )\n }\n if (resolved.images.length > 1) {\n throw new Error(\n `${this.name}: video models accept at most one input_reference image; received ${resolved.images.length}.`,\n )\n }\n\n const request: OpenAI_SDK.Videos.VideoCreateParams = {\n model,\n prompt: resolved.text,\n }\n const [inputReference] = resolved.images\n if (inputReference) {\n request.input_reference = await imagePartToFile(\n inputReference,\n 'input-reference',\n this.clientConfig.allowUrlFetch ?? false,\n options.abortSignal,\n )\n }\n if (resolvedSize) {\n // Gateway Veo sizes include 1080p and 4K, which are not in the OpenAI SDK union.\n request.size = resolvedSize as OpenAI_SDK.Videos.VideoSize\n }\n if (seconds !== undefined) {\n const apiSeconds = toApiSeconds(seconds)\n if (apiSeconds !== undefined) {\n // Gateway Veo clips can be 6 seconds. The OpenAI SDK union is 4/8/12.\n request.seconds = apiSeconds as OpenAI_SDK.Videos.VideoSeconds\n }\n }\n\n try {\n options.logger.request(\n `activity=video.create provider=${this.name} model=${model} size=${request.size ?? 'default'} seconds=${request.seconds ?? 'default'}`,\n { provider: this.name, model },\n )\n const response = await this.client.videos.create(\n request,\n openaiRequestOptions(options.abortSignal),\n )\n return { jobId: response.id, model }\n } catch (error: unknown) {\n options.logger.errors(`${this.name}.createVideoJob fatal`, {\n error: toRunErrorPayload(error, `${this.name}.createVideoJob failed`),\n source: `${this.name}.createVideoJob`,\n })\n throw error\n }\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n try {\n const response = await this.client.videos.retrieve(jobId)\n return {\n jobId,\n status: this.mapStatus(response.status),\n progress: response.progress,\n ...(response.error?.message !== undefined && {\n error: response.error.message,\n }),\n }\n } catch (error: unknown) {\n if (isHttpStatus(error, 404)) {\n return { jobId, status: 'failed', error: 'Job not found' }\n }\n throw error\n }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n try {\n const videoInfo = await this.client.videos.retrieve(jobId)\n const directUrl = videoResourceUrl(videoInfo)\n if (directUrl) {\n return {\n jobId,\n url: directUrl,\n ...(videoInfo.expires_at != null && {\n expiresAt: new Date(videoInfo.expires_at * 1000),\n }),\n }\n }\n\n const contentResponse = await this.client.videos.downloadContent(jobId)\n return dataUrlFromResponse(\n jobId,\n contentResponse,\n 'video.downloadContent',\n )\n } catch (error: unknown) {\n if (isHttpStatus(error, 404)) {\n throw new Error(`Video job not found: ${jobId}`)\n }\n if (isHttpStatus(error, 400)) {\n throw new Error(\n `Video is not ready for download. Check status first. Job ID: ${jobId}`,\n )\n }\n throw error\n }\n }\n\n override availableDurations(): DurationOptions<LovableVideoDuration> {\n return { kind: 'discrete', values: VIDEO_DURATIONS }\n }\n\n override snapDuration(seconds: number): LovableVideoDuration | undefined {\n return snapToDurationOption(seconds, this.availableDurations())\n }\n\n protected mapStatus(\n apiStatus: string,\n ): 'pending' | 'processing' | 'completed' | 'failed' {\n switch (apiStatus) {\n case 'queued':\n case 'pending':\n return 'pending'\n case 'processing':\n case 'in_progress':\n return 'processing'\n case 'completed':\n case 'succeeded':\n return 'completed'\n case 'failed':\n case 'error':\n case 'cancelled':\n return 'failed'\n default:\n return 'processing'\n }\n }\n}\n\nasync function dataUrlFromResponse(\n jobId: string,\n contentResponse: Response,\n source: string,\n): Promise<VideoUrlResult> {\n const videoBlob = await contentResponse.blob()\n const buffer = await videoBlob.arrayBuffer()\n warnIfLargeMediaBuffer(buffer.byteLength, source)\n const base64 = arrayBufferToBase64(buffer)\n const mimeType = contentResponse.headers.get('content-type') || 'video/mp4'\n return {\n jobId,\n url: `data:${mimeType};base64,${base64}`,\n }\n}\n\nfunction videoResourceUrl(video: OpenAI_SDK.Videos.Video): string | undefined {\n const extra = video as OpenAI_SDK.Videos.Video & { url?: string }\n if (extra.url && extra.url.length > 0) {\n return extra.url\n }\n return undefined\n}\n\nfunction isHttpStatus(error: unknown, status: number): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n (error as { status: unknown }).status === status\n )\n}\n\n/**\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function createLovableVideo<TModel extends LovableVideoModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<LovableVideoConfig, 'apiKey'>,\n): LovableVideoAdapter<TModel> {\n return new LovableVideoAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function lovableVideo<TModel extends LovableVideoModel>(\n model: TModel,\n config?: Omit<LovableVideoConfig, 'apiKey'>,\n): LovableVideoAdapter<TModel> {\n return createLovableVideo(model, getLovableApiKeyFromEnv(), config)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"video.js","names":[],"sources":["../../../src/adapters/video.ts"],"sourcesContent":["import OpenAI from 'openai'\nimport { resolveMediaPrompt } from '@tanstack/ai'\nimport { BaseVideoAdapter } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { arrayBufferToBase64 } from '@tanstack/ai-utils'\nimport {\n getLovableApiKeyFromEnv,\n openaiRequestOptions,\n withLovableDefaults,\n} from '../utils/client'\nimport { imagePartToFile } from '../image/image-input-to-file'\nimport {\n toApiSeconds,\n validateHighResDuration,\n validateVideoSeconds,\n validateVideoSize,\n} from '../video/video-provider-options'\nimport type { DurationOptions } from '@tanstack/ai/adapters'\nimport type {\n VideoGenerationOptions,\n VideoJobResult,\n VideoStatusResult,\n VideoUrlResult,\n} from '@tanstack/ai'\nimport type OpenAI_SDK from 'openai'\nimport type {\n LovableVideoModel,\n LovableVideoModelDurationByName,\n LovableVideoModelInputModalitiesByName,\n LovableVideoModelProviderOptionsByName,\n LovableVideoModelSizeByName,\n} from '../model-meta'\nimport type {\n LovableVideoDuration,\n LovableVideoProviderOptions,\n} from '../video/video-provider-options'\nimport type { LovableClientConfig } from '../utils/client'\n\nconst LARGE_MEDIA_BUFFER_BYTES = 10 * 1024 * 1024\nconst VIDEO_DURATIONS = [\n 4, 6, 8,\n] as const satisfies ReadonlyArray<LovableVideoDuration>\n\nfunction warnIfLargeMediaBuffer(byteLength: number, source: string): void {\n if (byteLength <= LARGE_MEDIA_BUFFER_BYTES) return\n console.warn(\n `[lovable.${source}] downloaded ${(byteLength / 1024 / 1024).toFixed(1)} MiB into memory before base64 encoding. ` +\n `Workers/serverless runtimes commonly run out of memory above ~10 MiB. ` +\n `Consider streaming the video through a CDN or your own storage layer instead.`,\n )\n}\n\n/**\n * @experimental Video generation is an experimental feature and may change.\n */\nexport interface LovableVideoConfig extends LovableClientConfig {\n /**\n * Opt into fetching HTTP(S) image URL inputs for `input_reference`.\n * The endpoint requires uploaded file bytes, so an HTTP(S) URL has to be\n * downloaded and buffered in memory. When `false` (the default), HTTP(S)\n * URL image inputs throw.\n */\n allowUrlFetch?: boolean\n}\n\n/**\n * @experimental Video generation is an experimental feature and may change.\n */\nexport class LovableVideoAdapter<\n TModel extends LovableVideoModel,\n> extends BaseVideoAdapter<\n TModel,\n LovableVideoProviderOptions,\n LovableVideoModelProviderOptionsByName,\n LovableVideoModelSizeByName,\n LovableVideoModelInputModalitiesByName,\n LovableVideoModelDurationByName\n> {\n readonly name = 'lovable' as const\n\n protected client: OpenAI\n protected clientConfig: LovableVideoConfig\n\n constructor(config: LovableVideoConfig, model: TModel) {\n super({}, model)\n this.clientConfig = config\n const { allowUrlFetch: _allowUrlFetch, ...clientOptions } = config\n this.client = new OpenAI(withLovableDefaults(clientOptions))\n }\n\n async createVideoJob(\n options: VideoGenerationOptions<\n LovableVideoProviderOptions,\n LovableVideoModelSizeByName[TModel],\n LovableVideoModelDurationByName[TModel]\n >,\n ): Promise<VideoJobResult> {\n const { model, size, duration, modelOptions } = options\n\n const resolvedSize = size ?? modelOptions?.size\n validateVideoSize(model, resolvedSize)\n const seconds = duration ?? modelOptions?.seconds\n validateVideoSeconds(model, seconds)\n validateHighResDuration(model, resolvedSize, seconds)\n\n const resolved = resolveMediaPrompt(options.prompt)\n\n if (resolved.videos.length > 0) {\n throw new Error(\n `${this.name}.createVideoJob does not support video prompt parts (model: ${model}).`,\n )\n }\n if (resolved.audios.length > 0) {\n throw new Error(\n `${this.name}.createVideoJob does not support audio prompt parts (model: ${model}).`,\n )\n }\n if (resolved.images.length > 1) {\n throw new Error(\n `${this.name}: video models accept at most one input_reference image; received ${resolved.images.length}.`,\n )\n }\n\n const request: OpenAI_SDK.Videos.VideoCreateParams = {\n model,\n prompt: resolved.text,\n }\n const [inputReference] = resolved.images\n if (inputReference) {\n request.input_reference = await imagePartToFile(\n inputReference,\n 'input-reference',\n this.clientConfig.allowUrlFetch ?? false,\n options.abortSignal,\n )\n }\n if (resolvedSize) {\n // Gateway Veo sizes include 1080p and 4K, which are not in the OpenAI SDK union.\n request.size = resolvedSize as OpenAI_SDK.Videos.VideoSize\n }\n if (seconds !== undefined) {\n const apiSeconds = toApiSeconds(seconds)\n if (apiSeconds !== undefined) {\n // Gateway Veo clips can be 6 seconds. The OpenAI SDK union is 4/8/12.\n request.seconds = apiSeconds as OpenAI_SDK.Videos.VideoSeconds\n }\n }\n\n try {\n options.logger.request(\n `activity=video.create provider=${this.name} model=${model} size=${request.size ?? 'default'} seconds=${request.seconds ?? 'default'}`,\n { provider: this.name, model },\n )\n const response = await this.client.videos.create(\n request,\n openaiRequestOptions(options.abortSignal),\n )\n return { jobId: response.id, model }\n } catch (error: unknown) {\n options.logger.errors(`${this.name}.createVideoJob fatal`, {\n error: toRunErrorPayload(error, `${this.name}.createVideoJob failed`),\n source: `${this.name}.createVideoJob`,\n })\n throw error\n }\n }\n\n async getVideoStatus(jobId: string): Promise<VideoStatusResult> {\n try {\n const response = await this.client.videos.retrieve(jobId)\n return {\n jobId,\n status: this.mapStatus(response.status),\n progress: response.progress,\n ...(response.error?.message !== undefined && {\n error: response.error.message,\n }),\n }\n } catch (error: unknown) {\n if (isHttpStatus(error, 404)) {\n return { jobId, status: 'failed', error: 'Job not found' }\n }\n throw error\n }\n }\n\n async getVideoUrl(jobId: string): Promise<VideoUrlResult> {\n try {\n const videoInfo = await this.client.videos.retrieve(jobId)\n const directUrl = videoResourceUrl(videoInfo)\n if (directUrl) {\n return {\n jobId,\n url: directUrl,\n ...(videoInfo.expires_at != null && {\n expiresAt: new Date(videoInfo.expires_at * 1000),\n }),\n }\n }\n\n const contentResponse = await this.client.videos.downloadContent(jobId)\n return dataUrlFromResponse(\n jobId,\n contentResponse,\n 'video.downloadContent',\n )\n } catch (error: unknown) {\n if (isHttpStatus(error, 404)) {\n throw new Error(`Video job not found: ${jobId}`)\n }\n if (isHttpStatus(error, 400)) {\n throw new Error(\n `Video is not ready for download. Check status first. Job ID: ${jobId}`,\n )\n }\n throw error\n }\n }\n\n override availableDurations(): DurationOptions<LovableVideoDuration> {\n return { kind: 'discrete', values: VIDEO_DURATIONS }\n }\n\n protected mapStatus(\n apiStatus: string,\n ): 'pending' | 'processing' | 'completed' | 'failed' {\n switch (apiStatus) {\n case 'queued':\n case 'pending':\n return 'pending'\n case 'processing':\n case 'in_progress':\n return 'processing'\n case 'completed':\n case 'succeeded':\n return 'completed'\n case 'failed':\n case 'error':\n case 'cancelled':\n return 'failed'\n default:\n return 'processing'\n }\n }\n}\n\nasync function dataUrlFromResponse(\n jobId: string,\n contentResponse: Response,\n source: string,\n): Promise<VideoUrlResult> {\n const videoBlob = await contentResponse.blob()\n const buffer = await videoBlob.arrayBuffer()\n warnIfLargeMediaBuffer(buffer.byteLength, source)\n const base64 = arrayBufferToBase64(buffer)\n const mimeType = contentResponse.headers.get('content-type') || 'video/mp4'\n return {\n jobId,\n url: `data:${mimeType};base64,${base64}`,\n }\n}\n\nfunction videoResourceUrl(video: OpenAI_SDK.Videos.Video): string | undefined {\n const extra = video as OpenAI_SDK.Videos.Video & { url?: string }\n if (extra.url && extra.url.length > 0) {\n return extra.url\n }\n return undefined\n}\n\nfunction isHttpStatus(error: unknown, status: number): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n (error as { status: unknown }).status === status\n )\n}\n\n/**\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function createLovableVideo<TModel extends LovableVideoModel>(\n model: TModel,\n apiKey: string,\n config?: Omit<LovableVideoConfig, 'apiKey'>,\n): LovableVideoAdapter<TModel> {\n return new LovableVideoAdapter({ apiKey, ...config }, model)\n}\n\n/**\n * @experimental Video generation is an experimental feature and may change.\n */\nexport function lovableVideo<TModel extends LovableVideoModel>(\n model: TModel,\n config?: Omit<LovableVideoConfig, 'apiKey'>,\n): LovableVideoAdapter<TModel> {\n return createLovableVideo(model, getLovableApiKeyFromEnv(), config)\n}\n"],"mappings":"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"}
@@ -1,11 +1,13 @@
1
- /**
2
- * @experimental Video generation is an experimental feature and may change.
3
- */
1
+ import { VideoDurationSpell } from '@tanstack/ai/adapters';
4
2
  export type LovableHdVideoSize = '1280x720' | '720x1280' | '1920x1080' | '1080x1920';
5
3
  export type Lovable4kVideoSize = '3840x2160' | '2160x3840';
6
4
  export type LovableVideoSize = LovableHdVideoSize | Lovable4kVideoSize;
7
5
  export type LovableVideoSeconds = '4' | '6' | '8';
8
- export type LovableVideoDuration = 4 | 6 | 8;
6
+ /**
7
+ * Gateway Veo clips are 4, 6, or 8 seconds. Callers may pass the number,
8
+ * the numeric string, or a `"6s"` template. The adapter sends `"4" | "6" | "8"`.
9
+ */
10
+ export type LovableVideoDuration = VideoDurationSpell<4 | 6 | 8>;
9
11
  export interface LovableVideoProviderOptions {
10
12
  size?: LovableVideoSize;
11
13
  seconds?: LovableVideoSeconds;
@@ -14,6 +16,6 @@ export declare function is4kVideoSize(size: string): size is Lovable4kVideoSize;
14
16
  export declare function isHighResVideoSize(size: string): boolean;
15
17
  export declare function supports4kVideo(model: string): boolean;
16
18
  export declare function validateVideoSize(model: string, size?: string): asserts size is LovableVideoSize | undefined;
17
- export declare function validateVideoSeconds(model: string, seconds?: number | string): asserts seconds is LovableVideoSeconds | LovableVideoDuration | undefined;
19
+ export declare function validateVideoSeconds(model: string, seconds?: number | string): asserts seconds is LovableVideoDuration | undefined;
18
20
  export declare function validateHighResDuration(model: string, size: string | undefined, seconds: number | string | undefined): void;
19
21
  export declare function toApiSeconds(seconds: number | string | undefined): LovableVideoSeconds | undefined;
@@ -1,4 +1,8 @@
1
+ import { durationToSeconds } from "@tanstack/ai/adapters";
1
2
  //#region src/video/video-provider-options.ts
3
+ /**
4
+ * @experimental Video generation is an experimental feature and may change.
5
+ */
2
6
  var HD_SIZES = [
3
7
  "1280x720",
4
8
  "720x1280",
@@ -30,16 +34,20 @@ function validateVideoSize(model, size) {
30
34
  }
31
35
  function validateVideoSeconds(model, seconds) {
32
36
  if (seconds === void 0) return;
33
- if (!(typeof seconds === "string" ? seconds === "4" || seconds === "6" || seconds === "8" : seconds === 4 || seconds === 6 || seconds === 8)) throw new Error(`Duration "${seconds}" is not supported by model "${model}". Supported durations: 4, 6, or 8 seconds`);
37
+ if (toApiSeconds(seconds) !== void 0) return;
38
+ throw new Error(`Duration "${seconds}" is not supported by model "${model}". Supported durations: 4, 6, or 8 seconds ("6", "6s", or 6).`);
34
39
  }
35
40
  function validateHighResDuration(model, size, seconds) {
36
41
  if (!size || !isHighResVideoSize(size)) return;
37
- const asNumber = seconds === void 0 ? void 0 : typeof seconds === "string" ? Number(seconds) : seconds;
42
+ const asNumber = seconds === void 0 ? void 0 : durationToSeconds(seconds);
38
43
  if (asNumber !== void 0 && asNumber !== 8) throw new Error(`Model "${model}" requires 8 second clips at 1080p and 4K. Received duration ${seconds}.`);
39
44
  }
40
45
  function toApiSeconds(seconds) {
41
46
  if (seconds === void 0) return void 0;
42
- return String(seconds);
47
+ const value = durationToSeconds(seconds);
48
+ if (value === 4) return "4";
49
+ if (value === 6) return "6";
50
+ if (value === 8) return "8";
43
51
  }
44
52
  //#endregion
45
53
  export { is4kVideoSize, isHighResVideoSize, supports4kVideo, toApiSeconds, validateHighResDuration, validateVideoSeconds, validateVideoSize };
@@ -1 +1 @@
1
- {"version":3,"file":"video-provider-options.js","names":[],"sources":["../../../src/video/video-provider-options.ts"],"sourcesContent":["/**\n * @experimental Video generation is an experimental feature and may change.\n */\n\nexport type LovableHdVideoSize =\n | '1280x720'\n | '720x1280'\n | '1920x1080'\n | '1080x1920'\n\nexport type Lovable4kVideoSize = '3840x2160' | '2160x3840'\n\nexport type LovableVideoSize = LovableHdVideoSize | Lovable4kVideoSize\n\nexport type LovableVideoSeconds = '4' | '6' | '8'\n\nexport type LovableVideoDuration = 4 | 6 | 8\n\nexport interface LovableVideoProviderOptions {\n size?: LovableVideoSize\n seconds?: LovableVideoSeconds\n}\n\nconst HD_SIZES: ReadonlyArray<LovableHdVideoSize> = [\n '1280x720',\n '720x1280',\n '1920x1080',\n '1080x1920',\n]\n\nconst FOUR_K_SIZES: ReadonlyArray<Lovable4kVideoSize> = [\n '3840x2160',\n '2160x3840',\n]\n\nconst ALL_SIZES: ReadonlyArray<LovableVideoSize> = [\n ...HD_SIZES,\n ...FOUR_K_SIZES,\n]\n\nconst HIGH_RES_SIZES: ReadonlySet<string> = new Set([\n '1920x1080',\n '1080x1920',\n '3840x2160',\n '2160x3840',\n])\n\nconst FOUR_K_MODELS: ReadonlySet<string> = new Set([\n 'google/veo-3.1-fast',\n 'google/veo-3.1',\n])\n\nexport function is4kVideoSize(size: string): size is Lovable4kVideoSize {\n return (FOUR_K_SIZES as ReadonlyArray<string>).includes(size)\n}\n\nexport function isHighResVideoSize(size: string): boolean {\n return HIGH_RES_SIZES.has(size)\n}\n\nexport function supports4kVideo(model: string): boolean {\n return FOUR_K_MODELS.has(model)\n}\n\nexport function validateVideoSize(\n model: string,\n size?: string,\n): asserts size is LovableVideoSize | undefined {\n if (!size) return\n\n if (!(ALL_SIZES as ReadonlyArray<string>).includes(size)) {\n throw new Error(\n `Size \"${size}\" is not supported by model \"${model}\". Supported sizes: ${ALL_SIZES.join(', ')}`,\n )\n }\n\n if (is4kVideoSize(size) && !supports4kVideo(model)) {\n throw new Error(\n `Size \"${size}\" is 4K. Only google/veo-3.1-fast and google/veo-3.1 support 4K.`,\n )\n }\n}\n\nexport function validateVideoSeconds(\n model: string,\n seconds?: number | string,\n): asserts seconds is LovableVideoSeconds | LovableVideoDuration | undefined {\n if (seconds === undefined) return\n\n const isValid =\n typeof seconds === 'string'\n ? seconds === '4' || seconds === '6' || seconds === '8'\n : seconds === 4 || seconds === 6 || seconds === 8\n\n if (!isValid) {\n throw new Error(\n `Duration \"${seconds}\" is not supported by model \"${model}\". Supported durations: 4, 6, or 8 seconds`,\n )\n }\n}\n\nexport function validateHighResDuration(\n model: string,\n size: string | undefined,\n seconds: number | string | undefined,\n): void {\n if (!size || !isHighResVideoSize(size)) return\n\n const asNumber =\n seconds === undefined\n ? undefined\n : typeof seconds === 'string'\n ? Number(seconds)\n : seconds\n\n if (asNumber !== undefined && asNumber !== 8) {\n throw new Error(\n `Model \"${model}\" requires 8 second clips at 1080p and 4K. Received duration ${seconds}.`,\n )\n }\n}\n\nexport function toApiSeconds(\n seconds: number | string | undefined,\n): LovableVideoSeconds | undefined {\n if (seconds === undefined) return undefined\n return String(seconds) as LovableVideoSeconds\n}\n"],"mappings":";AAuBA,IAAM,WAA8C;CAClD;CACA;CACA;CACA;AACF;AAEA,IAAM,eAAkD,CACtD,aACA,WACF;AAEA,IAAM,YAA6C,CACjD,GAAG,UACH,GAAG,YACL;AAEA,IAAM,iCAAsC,IAAI,IAAI;CAClD;CACA;CACA;CACA;AACF,CAAC;AAED,IAAM,gCAAqC,IAAI,IAAI,CACjD,uBACA,gBACF,CAAC;AAED,SAAgB,cAAc,MAA0C;CACtE,OAAQ,aAAuC,SAAS,IAAI;AAC9D;AAEA,SAAgB,mBAAmB,MAAuB;CACxD,OAAO,eAAe,IAAI,IAAI;AAChC;AAEA,SAAgB,gBAAgB,OAAwB;CACtD,OAAO,cAAc,IAAI,KAAK;AAChC;AAEA,SAAgB,kBACd,OACA,MAC8C;CAC9C,IAAI,CAAC,MAAM;CAEX,IAAI,CAAE,UAAoC,SAAS,IAAI,GACrD,MAAM,IAAI,MACR,SAAS,KAAK,+BAA+B,MAAM,sBAAsB,UAAU,KAAK,IAAI,GAC9F;CAGF,IAAI,cAAc,IAAI,KAAK,CAAC,gBAAgB,KAAK,GAC/C,MAAM,IAAI,MACR,SAAS,KAAK,iEAChB;AAEJ;AAEA,SAAgB,qBACd,OACA,SAC2E;CAC3E,IAAI,YAAY,KAAA,GAAW;CAO3B,IAAI,EAJF,OAAO,YAAY,WACf,YAAY,OAAO,YAAY,OAAO,YAAY,MAClD,YAAY,KAAK,YAAY,KAAK,YAAY,IAGlD,MAAM,IAAI,MACR,aAAa,QAAQ,+BAA+B,MAAM,2CAC5D;AAEJ;AAEA,SAAgB,wBACd,OACA,MACA,SACM;CACN,IAAI,CAAC,QAAQ,CAAC,mBAAmB,IAAI,GAAG;CAExC,MAAM,WACJ,YAAY,KAAA,IACR,KAAA,IACA,OAAO,YAAY,WACjB,OAAO,OAAO,IACd;CAER,IAAI,aAAa,KAAA,KAAa,aAAa,GACzC,MAAM,IAAI,MACR,UAAU,MAAM,+DAA+D,QAAQ,EACzF;AAEJ;AAEA,SAAgB,aACd,SACiC;CACjC,IAAI,YAAY,KAAA,GAAW,OAAO,KAAA;CAClC,OAAO,OAAO,OAAO;AACvB"}
1
+ {"version":3,"file":"video-provider-options.js","names":[],"sources":["../../../src/video/video-provider-options.ts"],"sourcesContent":["/**\n * @experimental Video generation is an experimental feature and may change.\n */\n\nimport { durationToSeconds } from '@tanstack/ai/adapters'\nimport type { VideoDurationSpell } from '@tanstack/ai/adapters'\n\nexport type LovableHdVideoSize =\n | '1280x720'\n | '720x1280'\n | '1920x1080'\n | '1080x1920'\n\nexport type Lovable4kVideoSize = '3840x2160' | '2160x3840'\n\nexport type LovableVideoSize = LovableHdVideoSize | Lovable4kVideoSize\n\nexport type LovableVideoSeconds = '4' | '6' | '8'\n\n/**\n * Gateway Veo clips are 4, 6, or 8 seconds. Callers may pass the number,\n * the numeric string, or a `\"6s\"` template. The adapter sends `\"4\" | \"6\" | \"8\"`.\n */\nexport type LovableVideoDuration = VideoDurationSpell<4 | 6 | 8>\n\nexport interface LovableVideoProviderOptions {\n size?: LovableVideoSize\n seconds?: LovableVideoSeconds\n}\n\nconst HD_SIZES: ReadonlyArray<LovableHdVideoSize> = [\n '1280x720',\n '720x1280',\n '1920x1080',\n '1080x1920',\n]\n\nconst FOUR_K_SIZES: ReadonlyArray<Lovable4kVideoSize> = [\n '3840x2160',\n '2160x3840',\n]\n\nconst ALL_SIZES: ReadonlyArray<LovableVideoSize> = [\n ...HD_SIZES,\n ...FOUR_K_SIZES,\n]\n\nconst HIGH_RES_SIZES: ReadonlySet<string> = new Set([\n '1920x1080',\n '1080x1920',\n '3840x2160',\n '2160x3840',\n])\n\nconst FOUR_K_MODELS: ReadonlySet<string> = new Set([\n 'google/veo-3.1-fast',\n 'google/veo-3.1',\n])\n\nexport function is4kVideoSize(size: string): size is Lovable4kVideoSize {\n return (FOUR_K_SIZES as ReadonlyArray<string>).includes(size)\n}\n\nexport function isHighResVideoSize(size: string): boolean {\n return HIGH_RES_SIZES.has(size)\n}\n\nexport function supports4kVideo(model: string): boolean {\n return FOUR_K_MODELS.has(model)\n}\n\nexport function validateVideoSize(\n model: string,\n size?: string,\n): asserts size is LovableVideoSize | undefined {\n if (!size) return\n\n if (!(ALL_SIZES as ReadonlyArray<string>).includes(size)) {\n throw new Error(\n `Size \"${size}\" is not supported by model \"${model}\". Supported sizes: ${ALL_SIZES.join(', ')}`,\n )\n }\n\n if (is4kVideoSize(size) && !supports4kVideo(model)) {\n throw new Error(\n `Size \"${size}\" is 4K. Only google/veo-3.1-fast and google/veo-3.1 support 4K.`,\n )\n }\n}\n\nexport function validateVideoSeconds(\n model: string,\n seconds?: number | string,\n): asserts seconds is LovableVideoDuration | undefined {\n if (seconds === undefined) return\n if (toApiSeconds(seconds) !== undefined) return\n\n throw new Error(\n `Duration \"${seconds}\" is not supported by model \"${model}\". Supported durations: 4, 6, or 8 seconds (\"6\", \"6s\", or 6).`,\n )\n}\n\nexport function validateHighResDuration(\n model: string,\n size: string | undefined,\n seconds: number | string | undefined,\n): void {\n if (!size || !isHighResVideoSize(size)) return\n\n const asNumber =\n seconds === undefined ? undefined : durationToSeconds(seconds)\n\n if (asNumber !== undefined && asNumber !== 8) {\n throw new Error(\n `Model \"${model}\" requires 8 second clips at 1080p and 4K. Received duration ${seconds}.`,\n )\n }\n}\n\nexport function toApiSeconds(\n seconds: number | string | undefined,\n): LovableVideoSeconds | undefined {\n if (seconds === undefined) return undefined\n const value = durationToSeconds(seconds)\n if (value === 4) return '4'\n if (value === 6) return '6'\n if (value === 8) return '8'\n return undefined\n}\n"],"mappings":";;;;;AA8BA,IAAM,WAA8C;CAClD;CACA;CACA;CACA;AACF;AAEA,IAAM,eAAkD,CACtD,aACA,WACF;AAEA,IAAM,YAA6C,CACjD,GAAG,UACH,GAAG,YACL;AAEA,IAAM,iCAAsC,IAAI,IAAI;CAClD;CACA;CACA;CACA;AACF,CAAC;AAED,IAAM,gCAAqC,IAAI,IAAI,CACjD,uBACA,gBACF,CAAC;AAED,SAAgB,cAAc,MAA0C;CACtE,OAAQ,aAAuC,SAAS,IAAI;AAC9D;AAEA,SAAgB,mBAAmB,MAAuB;CACxD,OAAO,eAAe,IAAI,IAAI;AAChC;AAEA,SAAgB,gBAAgB,OAAwB;CACtD,OAAO,cAAc,IAAI,KAAK;AAChC;AAEA,SAAgB,kBACd,OACA,MAC8C;CAC9C,IAAI,CAAC,MAAM;CAEX,IAAI,CAAE,UAAoC,SAAS,IAAI,GACrD,MAAM,IAAI,MACR,SAAS,KAAK,+BAA+B,MAAM,sBAAsB,UAAU,KAAK,IAAI,GAC9F;CAGF,IAAI,cAAc,IAAI,KAAK,CAAC,gBAAgB,KAAK,GAC/C,MAAM,IAAI,MACR,SAAS,KAAK,iEAChB;AAEJ;AAEA,SAAgB,qBACd,OACA,SACqD;CACrD,IAAI,YAAY,KAAA,GAAW;CAC3B,IAAI,aAAa,OAAO,MAAM,KAAA,GAAW;CAEzC,MAAM,IAAI,MACR,aAAa,QAAQ,+BAA+B,MAAM,8DAC5D;AACF;AAEA,SAAgB,wBACd,OACA,MACA,SACM;CACN,IAAI,CAAC,QAAQ,CAAC,mBAAmB,IAAI,GAAG;CAExC,MAAM,WACJ,YAAY,KAAA,IAAY,KAAA,IAAY,kBAAkB,OAAO;CAE/D,IAAI,aAAa,KAAA,KAAa,aAAa,GACzC,MAAM,IAAI,MACR,UAAU,MAAM,+DAA+D,QAAQ,EACzF;AAEJ;AAEA,SAAgB,aACd,SACiC;CACjC,IAAI,YAAY,KAAA,GAAW,OAAO,KAAA;CAClC,MAAM,QAAQ,kBAAkB,OAAO;CACvC,IAAI,UAAU,GAAG,OAAO;CACxB,IAAI,UAAU,GAAG,OAAO;CACxB,IAAI,UAAU,GAAG,OAAO;AAE1B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-lovable",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Lovable AI Gateway adapter for TanStack AI chat, embeddings, image, video, speech, and transcription.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -54,15 +54,15 @@
54
54
  "devDependencies": {
55
55
  "@vitest/coverage-v8": "4.1.10",
56
56
  "vite": "^8.2.1",
57
- "@tanstack/ai": "0.63.0"
57
+ "@tanstack/ai": "0.64.0"
58
58
  },
59
59
  "peerDependencies": {
60
- "@tanstack/ai": "^0.63.0"
60
+ "@tanstack/ai": "^0.64.0"
61
61
  },
62
62
  "dependencies": {
63
63
  "openai": "^6.41.0",
64
64
  "@tanstack/ai-utils": "^0.4.1",
65
- "@tanstack/openai-base": "^0.12.1"
65
+ "@tanstack/openai-base": "^0.12.2"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "vite build",
@@ -1,6 +1,6 @@
1
1
  import OpenAI from 'openai'
2
2
  import { resolveMediaPrompt } from '@tanstack/ai'
3
- import { BaseVideoAdapter, snapToDurationOption } from '@tanstack/ai/adapters'
3
+ import { BaseVideoAdapter } from '@tanstack/ai/adapters'
4
4
  import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
5
5
  import { arrayBufferToBase64 } from '@tanstack/ai-utils'
6
6
  import {
@@ -221,10 +221,6 @@ export class LovableVideoAdapter<
221
221
  return { kind: 'discrete', values: VIDEO_DURATIONS }
222
222
  }
223
223
 
224
- override snapDuration(seconds: number): LovableVideoDuration | undefined {
225
- return snapToDurationOption(seconds, this.availableDurations())
226
- }
227
-
228
224
  protected mapStatus(
229
225
  apiStatus: string,
230
226
  ): 'pending' | 'processing' | 'completed' | 'failed' {
@@ -2,6 +2,9 @@
2
2
  * @experimental Video generation is an experimental feature and may change.
3
3
  */
4
4
 
5
+ import { durationToSeconds } from '@tanstack/ai/adapters'
6
+ import type { VideoDurationSpell } from '@tanstack/ai/adapters'
7
+
5
8
  export type LovableHdVideoSize =
6
9
  | '1280x720'
7
10
  | '720x1280'
@@ -14,7 +17,11 @@ export type LovableVideoSize = LovableHdVideoSize | Lovable4kVideoSize
14
17
 
15
18
  export type LovableVideoSeconds = '4' | '6' | '8'
16
19
 
17
- export type LovableVideoDuration = 4 | 6 | 8
20
+ /**
21
+ * Gateway Veo clips are 4, 6, or 8 seconds. Callers may pass the number,
22
+ * the numeric string, or a `"6s"` template. The adapter sends `"4" | "6" | "8"`.
23
+ */
24
+ export type LovableVideoDuration = VideoDurationSpell<4 | 6 | 8>
18
25
 
19
26
  export interface LovableVideoProviderOptions {
20
27
  size?: LovableVideoSize
@@ -84,19 +91,13 @@ export function validateVideoSize(
84
91
  export function validateVideoSeconds(
85
92
  model: string,
86
93
  seconds?: number | string,
87
- ): asserts seconds is LovableVideoSeconds | LovableVideoDuration | undefined {
94
+ ): asserts seconds is LovableVideoDuration | undefined {
88
95
  if (seconds === undefined) return
96
+ if (toApiSeconds(seconds) !== undefined) return
89
97
 
90
- const isValid =
91
- typeof seconds === 'string'
92
- ? seconds === '4' || seconds === '6' || seconds === '8'
93
- : seconds === 4 || seconds === 6 || seconds === 8
94
-
95
- if (!isValid) {
96
- throw new Error(
97
- `Duration "${seconds}" is not supported by model "${model}". Supported durations: 4, 6, or 8 seconds`,
98
- )
99
- }
98
+ throw new Error(
99
+ `Duration "${seconds}" is not supported by model "${model}". Supported durations: 4, 6, or 8 seconds ("6", "6s", or 6).`,
100
+ )
100
101
  }
101
102
 
102
103
  export function validateHighResDuration(
@@ -107,11 +108,7 @@ export function validateHighResDuration(
107
108
  if (!size || !isHighResVideoSize(size)) return
108
109
 
109
110
  const asNumber =
110
- seconds === undefined
111
- ? undefined
112
- : typeof seconds === 'string'
113
- ? Number(seconds)
114
- : seconds
111
+ seconds === undefined ? undefined : durationToSeconds(seconds)
115
112
 
116
113
  if (asNumber !== undefined && asNumber !== 8) {
117
114
  throw new Error(
@@ -124,5 +121,9 @@ export function toApiSeconds(
124
121
  seconds: number | string | undefined,
125
122
  ): LovableVideoSeconds | undefined {
126
123
  if (seconds === undefined) return undefined
127
- return String(seconds) as LovableVideoSeconds
124
+ const value = durationToSeconds(seconds)
125
+ if (value === 4) return '4'
126
+ if (value === 6) return '6'
127
+ if (value === 8) return '8'
128
+ return undefined
128
129
  }