@tanstack/ai-lovable 0.0.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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -0
  3. package/dist/esm/adapters/embedding.d.ts +16 -0
  4. package/dist/esm/adapters/embedding.js +66 -0
  5. package/dist/esm/adapters/embedding.js.map +1 -0
  6. package/dist/esm/adapters/factory.d.ts +16 -0
  7. package/dist/esm/adapters/factory.js +28 -0
  8. package/dist/esm/adapters/factory.js.map +1 -0
  9. package/dist/esm/adapters/image.d.ts +26 -0
  10. package/dist/esm/adapters/image.js +145 -0
  11. package/dist/esm/adapters/image.js.map +1 -0
  12. package/dist/esm/adapters/responses-text.d.ts +24 -0
  13. package/dist/esm/adapters/responses-text.js +30 -0
  14. package/dist/esm/adapters/responses-text.js.map +1 -0
  15. package/dist/esm/adapters/summarize.d.ts +9 -0
  16. package/dist/esm/adapters/summarize.js +17 -0
  17. package/dist/esm/adapters/summarize.js.map +1 -0
  18. package/dist/esm/adapters/text.d.ts +20 -0
  19. package/dist/esm/adapters/text.js +21 -0
  20. package/dist/esm/adapters/text.js.map +1 -0
  21. package/dist/esm/adapters/transcription.d.ts +19 -0
  22. package/dist/esm/adapters/transcription.js +123 -0
  23. package/dist/esm/adapters/transcription.js.map +1 -0
  24. package/dist/esm/adapters/tts.d.ts +16 -0
  25. package/dist/esm/adapters/tts.js +69 -0
  26. package/dist/esm/adapters/tts.js.map +1 -0
  27. package/dist/esm/adapters/video.d.ts +41 -0
  28. package/dist/esm/adapters/video.js +164 -0
  29. package/dist/esm/adapters/video.js.map +1 -0
  30. package/dist/esm/audio/transcription-provider-options.d.ts +6 -0
  31. package/dist/esm/audio/tts-provider-options.d.ts +8 -0
  32. package/dist/esm/byok.d.ts +1 -0
  33. package/dist/esm/byok.js +11 -0
  34. package/dist/esm/byok.js.map +1 -0
  35. package/dist/esm/embedding/embedding-provider-options.d.ts +9 -0
  36. package/dist/esm/image/image-input-to-file.d.ts +8 -0
  37. package/dist/esm/image/image-input-to-file.js +47 -0
  38. package/dist/esm/image/image-input-to-file.js.map +1 -0
  39. package/dist/esm/image/image-provider-options.d.ts +12 -0
  40. package/dist/esm/index.d.ts +26 -0
  41. package/dist/esm/index.js +12 -0
  42. package/dist/esm/message-types.d.ts +44 -0
  43. package/dist/esm/model-meta.d.ts +71 -0
  44. package/dist/esm/model-meta.js +63 -0
  45. package/dist/esm/model-meta.js.map +1 -0
  46. package/dist/esm/text/responses-provider-options.d.ts +3 -0
  47. package/dist/esm/text/text-provider-options.d.ts +17 -0
  48. package/dist/esm/utils/client.d.ts +14 -0
  49. package/dist/esm/utils/client.js +35 -0
  50. package/dist/esm/utils/client.js.map +1 -0
  51. package/dist/esm/video/video-provider-options.d.ts +19 -0
  52. package/dist/esm/video/video-provider-options.js +47 -0
  53. package/dist/esm/video/video-provider-options.js.map +1 -0
  54. package/package.json +79 -0
  55. package/src/adapters/embedding.ts +102 -0
  56. package/src/adapters/factory.ts +78 -0
  57. package/src/adapters/image.ts +304 -0
  58. package/src/adapters/responses-text.ts +66 -0
  59. package/src/adapters/summarize.ts +35 -0
  60. package/src/adapters/text.ts +49 -0
  61. package/src/adapters/transcription.ts +226 -0
  62. package/src/adapters/tts.ts +97 -0
  63. package/src/adapters/video.ts +303 -0
  64. package/src/audio/transcription-provider-options.ts +7 -0
  65. package/src/audio/tts-provider-options.ts +21 -0
  66. package/src/byok.ts +7 -0
  67. package/src/embedding/embedding-provider-options.ts +9 -0
  68. package/src/image/image-input-to-file.ts +81 -0
  69. package/src/image/image-provider-options.ts +17 -0
  70. package/src/index.ts +125 -0
  71. package/src/message-types.ts +45 -0
  72. package/src/model-meta.ts +161 -0
  73. package/src/text/responses-provider-options.ts +8 -0
  74. package/src/text/text-provider-options.ts +18 -0
  75. package/src/utils/client.ts +50 -0
  76. package/src/video/video-provider-options.ts +128 -0
@@ -0,0 +1,81 @@
1
+ import { base64ToArrayBuffer } from '@tanstack/ai-utils'
2
+ import type { ImagePart, MediaInputMetadata } from '@tanstack/ai'
3
+
4
+ const DEFAULT_MIME = 'image/png'
5
+ const MIME_TO_EXT: Record<string, string> = {
6
+ 'image/png': 'png',
7
+ 'image/jpeg': 'jpg',
8
+ 'image/jpg': 'jpg',
9
+ 'image/webp': 'webp',
10
+ 'image/gif': 'gif',
11
+ }
12
+
13
+ function extForMime(mimeType: string): string {
14
+ return MIME_TO_EXT[mimeType] ?? mimeType.split('/')[1] ?? 'png'
15
+ }
16
+
17
+ function ensureFileSupport(): void {
18
+ if (typeof File === 'undefined') {
19
+ throw new Error(
20
+ '`File` is not available in this environment. ' +
21
+ 'Image-conditioned generation requires Node 20+ or a browser context.',
22
+ )
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Convert a TanStack `ImagePart` into an OpenAI-compatible `File`.
28
+ *
29
+ * HTTP(S) URLs are fetched only when `allowUrlFetch` is set. The gateway
30
+ * edit and `input_reference` endpoints need uploaded bytes, not a URL.
31
+ */
32
+ export async function imagePartToFile(
33
+ part: ImagePart<MediaInputMetadata>,
34
+ fallbackName: string,
35
+ allowUrlFetch: boolean,
36
+ abortSignal?: AbortSignal,
37
+ ): Promise<File> {
38
+ ensureFileSupport()
39
+
40
+ if (part.source.type === 'data') {
41
+ const mimeType = part.source.mimeType || DEFAULT_MIME
42
+ const bytes = base64ToArrayBuffer(part.source.value)
43
+ return new File([bytes], `${fallbackName}.${extForMime(mimeType)}`, {
44
+ type: mimeType,
45
+ })
46
+ }
47
+
48
+ if (/^https?:\/\//i.test(part.source.value) && !allowUrlFetch) {
49
+ throw new Error(
50
+ `lovable: HTTP(S) URL image inputs are not fetched by default because ` +
51
+ `the edit / input_reference endpoints require uploaded bytes, so ` +
52
+ `the image would be downloaded and buffered in memory (risking OOM on ` +
53
+ `constrained runtimes). Pass a data: URI, or set \`allowUrlFetch: true\` ` +
54
+ `on the adapter config to opt into fetching. URL: ${part.source.value}`,
55
+ )
56
+ }
57
+
58
+ const response = await fetch(
59
+ part.source.value,
60
+ abortSignal ? { signal: abortSignal } : undefined,
61
+ )
62
+ if (!response.ok) {
63
+ throw new Error(
64
+ `Failed to fetch image input (${response.status} ${response.statusText}): ${part.source.value}`,
65
+ )
66
+ }
67
+ const blob = await response.blob()
68
+ const mimeType =
69
+ part.source.mimeType || blob.type || inferMimeFromUrl(part.source.value)
70
+ return new File([blob], `${fallbackName}.${extForMime(mimeType)}`, {
71
+ type: mimeType,
72
+ })
73
+ }
74
+
75
+ function inferMimeFromUrl(url: string): string {
76
+ const match = url.match(/\.(png|jpe?g|webp|gif)(?:\?|#|$)/i)
77
+ if (!match || !match[1]) return DEFAULT_MIME
78
+ const ext = match[1].toLowerCase()
79
+ if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg'
80
+ return `image/${ext}`
81
+ }
@@ -0,0 +1,17 @@
1
+ export type LovableImageSize = '1024x1024' | '1536x1024' | '1024x1536' | 'auto'
2
+
3
+ export type LovableImageQuality = 'high' | 'medium' | 'low' | 'auto'
4
+
5
+ export type LovableImageOutputFormat = 'png' | 'jpeg' | 'webp'
6
+
7
+ export type LovableImageBackground = 'transparent' | 'opaque' | 'auto'
8
+
9
+ export type LovableImageModeration = 'low' | 'auto'
10
+
11
+ export interface LovableImageProviderOptions {
12
+ quality?: LovableImageQuality
13
+ output_format?: LovableImageOutputFormat
14
+ background?: LovableImageBackground
15
+ moderation?: LovableImageModeration
16
+ user?: string
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @module @tanstack/ai-lovable
3
+ *
4
+ * Lovable AI Gateway adapter for TanStack AI.
5
+ */
6
+
7
+ export {
8
+ LovableTextAdapter,
9
+ type LovableTextConfig,
10
+ type LovableTextProviderOptions,
11
+ } from './adapters/text'
12
+
13
+ export {
14
+ createLovableText,
15
+ lovableText,
16
+ type LovableTextApi,
17
+ type LovableResponsesApiConfig,
18
+ type LovableChatApiConfig,
19
+ } from './adapters/factory'
20
+
21
+ export {
22
+ LovableResponsesTextAdapter,
23
+ createLovableResponsesText,
24
+ lovableResponsesText,
25
+ type LovableResponsesTextConfig,
26
+ type LovableResponsesTextProviderOptions,
27
+ } from './adapters/responses-text'
28
+
29
+ export {
30
+ createLovableSummarize,
31
+ lovableSummarize,
32
+ type LovableSummarizeConfig,
33
+ type LovableSummarizeModel,
34
+ } from './adapters/summarize'
35
+
36
+ export {
37
+ LovableEmbeddingAdapter,
38
+ createLovableEmbedding,
39
+ lovableEmbedding,
40
+ type LovableEmbeddingConfig,
41
+ } from './adapters/embedding'
42
+ export type { LovableEmbeddingProviderOptions } from './embedding/embedding-provider-options'
43
+
44
+ export {
45
+ LovableImageAdapter,
46
+ createLovableImage,
47
+ lovableImage,
48
+ type LovableImageConfig,
49
+ } from './adapters/image'
50
+ export type { LovableImageProviderOptions } from './image/image-provider-options'
51
+
52
+ /**
53
+ * @experimental Video generation is an experimental feature and may change.
54
+ */
55
+ export {
56
+ LovableVideoAdapter,
57
+ createLovableVideo,
58
+ lovableVideo,
59
+ type LovableVideoConfig,
60
+ } from './adapters/video'
61
+ export type {
62
+ LovableVideoProviderOptions,
63
+ LovableVideoSize,
64
+ LovableHdVideoSize,
65
+ Lovable4kVideoSize,
66
+ LovableVideoDuration,
67
+ LovableVideoSeconds,
68
+ } from './video/video-provider-options'
69
+
70
+ export {
71
+ LovableTTSAdapter,
72
+ createLovableSpeech,
73
+ lovableSpeech,
74
+ type LovableTTSConfig,
75
+ } from './adapters/tts'
76
+ export type {
77
+ LovableTTSProviderOptions,
78
+ LovableTTSVoice,
79
+ LovableTTSFormat,
80
+ } from './audio/tts-provider-options'
81
+
82
+ export {
83
+ LovableTranscriptionAdapter,
84
+ createLovableTranscription,
85
+ lovableTranscription,
86
+ type LovableTranscriptionConfig,
87
+ } from './adapters/transcription'
88
+ export type { LovableTranscriptionProviderOptions } from './audio/transcription-provider-options'
89
+
90
+ export {
91
+ LOVABLE_CHAT_MODELS,
92
+ LOVABLE_IMAGE_MODELS,
93
+ LOVABLE_VIDEO_MODELS,
94
+ LOVABLE_EMBEDDING_MODELS,
95
+ LOVABLE_TTS_MODELS,
96
+ LOVABLE_TRANSCRIPTION_MODELS,
97
+ } from './model-meta'
98
+ export type {
99
+ LovableChatModel,
100
+ LovableModelId,
101
+ LovableChatModelProviderOptionsByName,
102
+ LovableModelInputModalitiesByName,
103
+ LovableImageModel,
104
+ LovableVideoModel,
105
+ LovableEmbeddingModel,
106
+ LovableTTSModel,
107
+ LovableTranscriptionModel,
108
+ ResolveProviderOptions,
109
+ ResolveInputModalities,
110
+ } from './model-meta'
111
+
112
+ export type {
113
+ LovableTextMetadata,
114
+ LovableImageMetadata,
115
+ LovableAudioMetadata,
116
+ LovableVideoMetadata,
117
+ LovableDocumentMetadata,
118
+ LovableMessageMetadataByModality,
119
+ } from './message-types'
120
+
121
+ export {
122
+ getLovableApiKeyFromEnv,
123
+ lovableGatewayHeaders,
124
+ type LovableClientConfig,
125
+ } from './utils/client'
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Metadata for Lovable AI Gateway document content parts.
3
+ */
4
+ export interface LovableDocumentMetadata {}
5
+
6
+ /**
7
+ * Metadata for Lovable AI Gateway text content parts.
8
+ */
9
+ export interface LovableTextMetadata {}
10
+
11
+ /**
12
+ * Metadata for Lovable AI Gateway image content parts.
13
+ */
14
+ export interface LovableImageMetadata {
15
+ /**
16
+ * Specifies the detail level of the image.
17
+ * - 'auto': Let the model decide based on image size and content
18
+ * - 'low': Use low resolution processing (faster, cheaper, less detail)
19
+ * - 'high': Use high resolution processing (slower, more expensive, more detail)
20
+ *
21
+ * @default 'auto'
22
+ */
23
+ detail?: 'auto' | 'low' | 'high'
24
+ }
25
+
26
+ /**
27
+ * Metadata for Lovable AI Gateway audio content parts.
28
+ */
29
+ export interface LovableAudioMetadata {}
30
+
31
+ /**
32
+ * Metadata for Lovable AI Gateway video content parts.
33
+ */
34
+ export interface LovableVideoMetadata {}
35
+
36
+ /**
37
+ * Map of modality types to their Lovable AI Gateway metadata types.
38
+ */
39
+ export interface LovableMessageMetadataByModality {
40
+ text: LovableTextMetadata
41
+ image: LovableImageMetadata
42
+ audio: LovableAudioMetadata
43
+ video: LovableVideoMetadata
44
+ document: LovableDocumentMetadata
45
+ }
@@ -0,0 +1,161 @@
1
+ import type { LovableEmbeddingProviderOptions } from './embedding/embedding-provider-options'
2
+ import type {
3
+ LovableImageProviderOptions,
4
+ LovableImageSize,
5
+ } from './image/image-provider-options'
6
+ import type { LovableTextProviderOptions } from './text/text-provider-options'
7
+ import type {
8
+ LovableHdVideoSize,
9
+ LovableVideoDuration,
10
+ LovableVideoProviderOptions,
11
+ LovableVideoSize,
12
+ } from './video/video-provider-options'
13
+
14
+ /**
15
+ * Chat models listed on https://docs.lovable.dev/features/ai.
16
+ * Ids use the `google/` and `openai/` form that the gateway accepts.
17
+ */
18
+ export const LOVABLE_CHAT_MODELS = [
19
+ 'google/gemini-3.7-flash',
20
+ 'google/gemini-3.6-flash',
21
+ 'google/gemini-3.5-flash',
22
+ 'google/gemini-3.1-pro-preview',
23
+ 'google/gemini-3.1-flash-lite',
24
+ 'google/gemini-3-flash-preview',
25
+ 'google/gemini-2.5-pro',
26
+ 'google/gemini-2.5-flash',
27
+ 'google/gemini-2.5-flash-lite',
28
+ 'openai/gpt-5.6-sol',
29
+ 'openai/gpt-5.6-terra',
30
+ 'openai/gpt-5.6-luna',
31
+ 'openai/gpt-5.5-pro',
32
+ 'openai/gpt-5.5',
33
+ 'openai/gpt-5.4-pro',
34
+ 'openai/gpt-5.4',
35
+ 'openai/gpt-5.4-mini',
36
+ 'openai/gpt-5.4-nano',
37
+ 'openai/gpt-5.2',
38
+ 'openai/gpt-5',
39
+ 'openai/gpt-5-mini',
40
+ 'openai/gpt-5-nano',
41
+ ] as const
42
+
43
+ export type LovableChatModel = (typeof LOVABLE_CHAT_MODELS)[number]
44
+
45
+ /**
46
+ * A curated chat model id, or any other id the gateway accepts.
47
+ * Uncurated ids fall back to text-only input and the generic provider options.
48
+ */
49
+ export type LovableModelId = LovableChatModel | (string & {})
50
+
51
+ export type LovableChatModelProviderOptionsByName = {
52
+ [K in LovableChatModel]: LovableTextProviderOptions
53
+ }
54
+
55
+ export type LovableModelInputModalitiesByName = {
56
+ [K in LovableChatModel]: readonly ['text', 'image']
57
+ }
58
+
59
+ export type LovableChatModelToolCapabilitiesByName = {
60
+ [K in LovableChatModel]: readonly []
61
+ }
62
+
63
+ export type ResolveProviderOptions<TModel extends string> =
64
+ TModel extends keyof LovableChatModelProviderOptionsByName
65
+ ? LovableChatModelProviderOptionsByName[TModel]
66
+ : LovableTextProviderOptions
67
+
68
+ export type ResolveInputModalities<TModel extends string> =
69
+ TModel extends keyof LovableModelInputModalitiesByName
70
+ ? LovableModelInputModalitiesByName[TModel]
71
+ : readonly ['text']
72
+
73
+ export const LOVABLE_IMAGE_MODELS = [
74
+ 'openai/gpt-image-2',
75
+ 'openai/gpt-image-1-mini',
76
+ 'google/gemini-3.1-flash-image',
77
+ 'google/gemini-3.1-flash-lite-image',
78
+ 'google/gemini-3-pro-image',
79
+ 'google/gemini-2.5-flash-image',
80
+ ] as const
81
+
82
+ export type LovableImageModel = (typeof LOVABLE_IMAGE_MODELS)[number]
83
+
84
+ export type LovableImageModelProviderOptionsByName = {
85
+ [K in LovableImageModel]: LovableImageProviderOptions
86
+ }
87
+
88
+ export type LovableImageModelSizeByName = {
89
+ [K in LovableImageModel]: LovableImageSize
90
+ }
91
+
92
+ export type LovableImageModelInputModalitiesByName = {
93
+ [K in LovableImageModel]: readonly ['image']
94
+ }
95
+
96
+ /**
97
+ * @experimental Video generation is an experimental feature and may change.
98
+ */
99
+ export const LOVABLE_VIDEO_MODELS = [
100
+ 'google/veo-3.1-lite',
101
+ 'google/veo-3.1-fast',
102
+ 'google/veo-3.1',
103
+ ] as const
104
+
105
+ /**
106
+ * @experimental Video generation is an experimental feature and may change.
107
+ */
108
+ export type LovableVideoModel = (typeof LOVABLE_VIDEO_MODELS)[number]
109
+
110
+ export type LovableVideoModelProviderOptionsByName = {
111
+ [K in LovableVideoModel]: LovableVideoProviderOptions
112
+ }
113
+
114
+ export type LovableVideoModelSizeByName = {
115
+ 'google/veo-3.1-lite': LovableHdVideoSize
116
+ 'google/veo-3.1-fast': LovableVideoSize
117
+ 'google/veo-3.1': LovableVideoSize
118
+ }
119
+
120
+ export type LovableVideoModelDurationByName = {
121
+ [K in LovableVideoModel]: LovableVideoDuration
122
+ }
123
+
124
+ export type LovableVideoModelInputModalitiesByName = {
125
+ [K in LovableVideoModel]: readonly ['image']
126
+ }
127
+
128
+ export const LOVABLE_EMBEDDING_MODELS = [
129
+ 'google/gemini-embedding-2',
130
+ 'google/gemini-embedding-001',
131
+ 'openai/text-embedding-3-small',
132
+ 'openai/text-embedding-3-large',
133
+ ] as const
134
+
135
+ export type LovableEmbeddingModel = (typeof LOVABLE_EMBEDDING_MODELS)[number]
136
+
137
+ export type LovableEmbeddingModelProviderOptionsByName = {
138
+ [K in LovableEmbeddingModel]: LovableEmbeddingProviderOptions
139
+ }
140
+
141
+ export type LovableEmbeddingModelInputModalitiesByName = {
142
+ [K in LovableEmbeddingModel]: readonly ['text']
143
+ }
144
+
145
+ export const LOVABLE_TTS_MODELS = [
146
+ 'openai/gpt-4o-mini-tts',
147
+ 'google/gemini-2.5-flash-tts',
148
+ 'google/gemini-2.5-pro-tts',
149
+ 'google/gemini-2.5-flash-lite-preview-tts',
150
+ 'google/gemini-3.1-flash-tts-preview',
151
+ ] as const
152
+
153
+ export type LovableTTSModel = (typeof LOVABLE_TTS_MODELS)[number]
154
+
155
+ export const LOVABLE_TRANSCRIPTION_MODELS = [
156
+ 'openai/gpt-4o-mini-transcribe',
157
+ 'openai/gpt-4o-transcribe',
158
+ ] as const
159
+
160
+ export type LovableTranscriptionModel =
161
+ (typeof LOVABLE_TRANSCRIPTION_MODELS)[number]
@@ -0,0 +1,8 @@
1
+ import type { LovableTextProviderOptions } from './text-provider-options'
2
+
3
+ export type LovableResponsesProviderOptions = Pick<
4
+ LovableTextProviderOptions,
5
+ 'temperature' | 'top_p' | 'max_output_tokens' | 'user'
6
+ >
7
+
8
+ export type ExternalResponsesProviderOptions = LovableResponsesProviderOptions
@@ -0,0 +1,18 @@
1
+ export interface LovableTextProviderOptions {
2
+ temperature?: number | null
3
+ top_p?: number | null
4
+ max_tokens?: number | null
5
+ max_completion_tokens?: number | null
6
+ max_output_tokens?: number | null
7
+ frequency_penalty?: number | null
8
+ presence_penalty?: number | null
9
+ stop?: string | null | Array<string>
10
+ seed?: number | null
11
+ reasoning?: boolean | Record<string, unknown> | null
12
+ include_reasoning?: boolean | null
13
+ response_format?: unknown
14
+ structured_outputs?: boolean | null
15
+ user?: string | null
16
+ }
17
+
18
+ export type ExternalTextProviderOptions = LovableTextProviderOptions
@@ -0,0 +1,50 @@
1
+ import { getApiKeyFromEnv } from '@tanstack/ai-utils'
2
+ import type { ClientOptions } from 'openai'
3
+
4
+ export const LOVABLE_DEFAULT_BASE_URL = 'https://ai.gateway.lovable.dev/v1'
5
+
6
+ export interface LovableClientConfig extends Omit<ClientOptions, 'apiKey'> {
7
+ apiKey: string
8
+ }
9
+
10
+ export function getLovableApiKeyFromEnv(): string {
11
+ try {
12
+ return getApiKeyFromEnv('LOVABLE_API_KEY')
13
+ } catch (cause) {
14
+ throw new Error(
15
+ 'LOVABLE_API_KEY is required. Set it in the environment or pass apiKey to the factory.',
16
+ { cause },
17
+ )
18
+ }
19
+ }
20
+
21
+ export function lovableGatewayHeaders(apiKey: string): {
22
+ 'Lovable-API-Key': string
23
+ 'X-Lovable-AIG-SDK': string
24
+ } {
25
+ return {
26
+ 'Lovable-API-Key': apiKey,
27
+ 'X-Lovable-AIG-SDK': 'tanstack-ai',
28
+ }
29
+ }
30
+
31
+ export function withLovableDefaults(
32
+ config: LovableClientConfig,
33
+ ): ClientOptions {
34
+ const { defaultHeaders, ...rest } = config
35
+ return {
36
+ ...rest,
37
+ baseURL: config.baseURL || LOVABLE_DEFAULT_BASE_URL,
38
+ defaultHeaders: {
39
+ ...lovableGatewayHeaders(config.apiKey),
40
+ ...defaultHeaders,
41
+ },
42
+ }
43
+ }
44
+
45
+ export function openaiRequestOptions(
46
+ abortSignal?: AbortSignal,
47
+ ): { signal: AbortSignal } | undefined {
48
+ if (!abortSignal) return undefined
49
+ return { signal: abortSignal }
50
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @experimental Video generation is an experimental feature and may change.
3
+ */
4
+
5
+ export type LovableHdVideoSize =
6
+ | '1280x720'
7
+ | '720x1280'
8
+ | '1920x1080'
9
+ | '1080x1920'
10
+
11
+ export type Lovable4kVideoSize = '3840x2160' | '2160x3840'
12
+
13
+ export type LovableVideoSize = LovableHdVideoSize | Lovable4kVideoSize
14
+
15
+ export type LovableVideoSeconds = '4' | '6' | '8'
16
+
17
+ export type LovableVideoDuration = 4 | 6 | 8
18
+
19
+ export interface LovableVideoProviderOptions {
20
+ size?: LovableVideoSize
21
+ seconds?: LovableVideoSeconds
22
+ }
23
+
24
+ const HD_SIZES: ReadonlyArray<LovableHdVideoSize> = [
25
+ '1280x720',
26
+ '720x1280',
27
+ '1920x1080',
28
+ '1080x1920',
29
+ ]
30
+
31
+ const FOUR_K_SIZES: ReadonlyArray<Lovable4kVideoSize> = [
32
+ '3840x2160',
33
+ '2160x3840',
34
+ ]
35
+
36
+ const ALL_SIZES: ReadonlyArray<LovableVideoSize> = [
37
+ ...HD_SIZES,
38
+ ...FOUR_K_SIZES,
39
+ ]
40
+
41
+ const HIGH_RES_SIZES: ReadonlySet<string> = new Set([
42
+ '1920x1080',
43
+ '1080x1920',
44
+ '3840x2160',
45
+ '2160x3840',
46
+ ])
47
+
48
+ const FOUR_K_MODELS: ReadonlySet<string> = new Set([
49
+ 'google/veo-3.1-fast',
50
+ 'google/veo-3.1',
51
+ ])
52
+
53
+ export function is4kVideoSize(size: string): size is Lovable4kVideoSize {
54
+ return (FOUR_K_SIZES as ReadonlyArray<string>).includes(size)
55
+ }
56
+
57
+ export function isHighResVideoSize(size: string): boolean {
58
+ return HIGH_RES_SIZES.has(size)
59
+ }
60
+
61
+ export function supports4kVideo(model: string): boolean {
62
+ return FOUR_K_MODELS.has(model)
63
+ }
64
+
65
+ export function validateVideoSize(
66
+ model: string,
67
+ size?: string,
68
+ ): asserts size is LovableVideoSize | undefined {
69
+ if (!size) return
70
+
71
+ if (!(ALL_SIZES as ReadonlyArray<string>).includes(size)) {
72
+ throw new Error(
73
+ `Size "${size}" is not supported by model "${model}". Supported sizes: ${ALL_SIZES.join(', ')}`,
74
+ )
75
+ }
76
+
77
+ if (is4kVideoSize(size) && !supports4kVideo(model)) {
78
+ throw new Error(
79
+ `Size "${size}" is 4K. Only google/veo-3.1-fast and google/veo-3.1 support 4K.`,
80
+ )
81
+ }
82
+ }
83
+
84
+ export function validateVideoSeconds(
85
+ model: string,
86
+ seconds?: number | string,
87
+ ): asserts seconds is LovableVideoSeconds | LovableVideoDuration | undefined {
88
+ if (seconds === undefined) return
89
+
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
+ }
100
+ }
101
+
102
+ export function validateHighResDuration(
103
+ model: string,
104
+ size: string | undefined,
105
+ seconds: number | string | undefined,
106
+ ): void {
107
+ if (!size || !isHighResVideoSize(size)) return
108
+
109
+ const asNumber =
110
+ seconds === undefined
111
+ ? undefined
112
+ : typeof seconds === 'string'
113
+ ? Number(seconds)
114
+ : seconds
115
+
116
+ if (asNumber !== undefined && asNumber !== 8) {
117
+ throw new Error(
118
+ `Model "${model}" requires 8 second clips at 1080p and 4K. Received duration ${seconds}.`,
119
+ )
120
+ }
121
+ }
122
+
123
+ export function toApiSeconds(
124
+ seconds: number | string | undefined,
125
+ ): LovableVideoSeconds | undefined {
126
+ if (seconds === undefined) return undefined
127
+ return String(seconds) as LovableVideoSeconds
128
+ }