@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,78 @@
1
+ import { getLovableApiKeyFromEnv } from '../utils/client'
2
+ import { LovableTextAdapter } from './text'
3
+ import { LovableResponsesTextAdapter } from './responses-text'
4
+ import type { LovableClientConfig } from '../utils/client'
5
+ import type { LovableTextConfig } from './text'
6
+ import type { LovableResponsesTextConfig } from './responses-text'
7
+ import type { LovableModelId } from '../model-meta'
8
+
9
+ export type LovableTextApi = 'responses' | 'chat' | 'chat-completions'
10
+
11
+ /** Config for the branching factory's Responses mode (default, or api: 'responses'). */
12
+ export type LovableResponsesApiConfig = Omit<
13
+ LovableResponsesTextConfig,
14
+ 'apiKey'
15
+ > & {
16
+ api?: 'responses'
17
+ }
18
+
19
+ /** Config for the branching factory's Chat Completions mode (api required). */
20
+ export type LovableChatApiConfig = Omit<LovableTextConfig, 'apiKey'> & {
21
+ api: 'chat' | 'chat-completions'
22
+ }
23
+
24
+ type AnyLovableTextAdapter<TModel extends LovableModelId> =
25
+ | LovableResponsesTextAdapter<TModel>
26
+ | LovableTextAdapter<TModel>
27
+
28
+ function stripApi<T extends { api?: unknown }>(config: T): Omit<T, 'api'> {
29
+ const { api, ...rest } = config
30
+ void api
31
+ return rest
32
+ }
33
+
34
+ function build<TModel extends LovableModelId>(
35
+ model: TModel,
36
+ config: LovableClientConfig & { api?: LovableTextApi },
37
+ ): AnyLovableTextAdapter<TModel> {
38
+ if (config.api === 'chat' || config.api === 'chat-completions') {
39
+ return new LovableTextAdapter(stripApi(config), model)
40
+ }
41
+ return new LovableResponsesTextAdapter(stripApi(config), model)
42
+ }
43
+
44
+ export function createLovableText<TModel extends LovableModelId>(
45
+ model: TModel,
46
+ apiKey: string,
47
+ config?: LovableResponsesApiConfig,
48
+ ): LovableResponsesTextAdapter<TModel>
49
+ export function createLovableText<TModel extends LovableModelId>(
50
+ model: TModel,
51
+ apiKey: string,
52
+ config: LovableChatApiConfig,
53
+ ): LovableTextAdapter<TModel>
54
+ export function createLovableText<TModel extends LovableModelId>(
55
+ model: TModel,
56
+ apiKey: string,
57
+ config?: LovableResponsesApiConfig | LovableChatApiConfig,
58
+ ): AnyLovableTextAdapter<TModel> {
59
+ return build(model, { ...config, apiKey })
60
+ }
61
+
62
+ export function lovableText<TModel extends LovableModelId>(
63
+ model: TModel,
64
+ config?: LovableResponsesApiConfig,
65
+ ): LovableResponsesTextAdapter<TModel>
66
+ export function lovableText<TModel extends LovableModelId>(
67
+ model: TModel,
68
+ config: LovableChatApiConfig,
69
+ ): LovableTextAdapter<TModel>
70
+ export function lovableText<TModel extends LovableModelId>(
71
+ model: TModel,
72
+ config?: LovableResponsesApiConfig | LovableChatApiConfig,
73
+ ): AnyLovableTextAdapter<TModel> {
74
+ return build(model, {
75
+ ...config,
76
+ apiKey: getLovableApiKeyFromEnv(),
77
+ })
78
+ }
@@ -0,0 +1,304 @@
1
+ import OpenAI from 'openai'
2
+ import { resolveMediaPrompt } from '@tanstack/ai'
3
+ import { BaseImageAdapter } from '@tanstack/ai/adapters'
4
+ import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
5
+ import { buildImagesUsage } from '@tanstack/openai-base'
6
+ import { generateId } from '@tanstack/ai-utils'
7
+ import {
8
+ getLovableApiKeyFromEnv,
9
+ openaiRequestOptions,
10
+ withLovableDefaults,
11
+ } from '../utils/client'
12
+ import { imagePartToFile } from '../image/image-input-to-file'
13
+ import type {
14
+ GeneratedImage,
15
+ ImageGenerationOptions,
16
+ ImageGenerationResult,
17
+ ImagePart,
18
+ MediaInputMetadata,
19
+ } from '@tanstack/ai'
20
+ import type OpenAI_SDK from 'openai'
21
+ import type {
22
+ LovableImageModel,
23
+ LovableImageModelInputModalitiesByName,
24
+ LovableImageModelProviderOptionsByName,
25
+ LovableImageModelSizeByName,
26
+ } from '../model-meta'
27
+ import type { LovableImageProviderOptions } from '../image/image-provider-options'
28
+ import type { LovableClientConfig } from '../utils/client'
29
+
30
+ const MAX_EDIT_IMAGES = 16
31
+
32
+ export interface LovableImageConfig extends LovableClientConfig {
33
+ /**
34
+ * Opt into fetching HTTP(S) image URL inputs for image edits. The gateway
35
+ * `/images/edits` endpoint requires uploaded file bytes, so an HTTP(S) URL
36
+ * has to be downloaded and buffered in memory. When `false` (the default),
37
+ * HTTP(S) URL image inputs throw.
38
+ */
39
+ allowUrlFetch?: boolean
40
+ }
41
+
42
+ function supportsImageMask(model: string): boolean {
43
+ return model.startsWith('openai/')
44
+ }
45
+
46
+ function generatedImagesFromResponse(
47
+ data:
48
+ | Array<{
49
+ b64_json?: string | null
50
+ url?: string | null
51
+ revised_prompt?: string | null
52
+ }>
53
+ | undefined
54
+ | null,
55
+ emptyMessage: string,
56
+ ): Array<GeneratedImage> {
57
+ const images: Array<GeneratedImage> = (data ?? []).flatMap(
58
+ (item): Array<GeneratedImage> => {
59
+ const revisedPromptField =
60
+ item.revised_prompt !== undefined && item.revised_prompt !== null
61
+ ? { revisedPrompt: item.revised_prompt }
62
+ : {}
63
+ if (item.b64_json) {
64
+ return [{ b64Json: item.b64_json, ...revisedPromptField }]
65
+ }
66
+ if (item.url) {
67
+ return [{ url: item.url, ...revisedPromptField }]
68
+ }
69
+ return []
70
+ },
71
+ )
72
+
73
+ if (images.length === 0) {
74
+ throw new Error(emptyMessage)
75
+ }
76
+
77
+ return images
78
+ }
79
+
80
+ export class LovableImageAdapter<
81
+ TModel extends LovableImageModel,
82
+ > extends BaseImageAdapter<
83
+ TModel,
84
+ LovableImageProviderOptions,
85
+ LovableImageModelProviderOptionsByName,
86
+ LovableImageModelSizeByName,
87
+ LovableImageModelInputModalitiesByName
88
+ > {
89
+ override readonly kind = 'image' as const
90
+ readonly name = 'lovable' as const
91
+
92
+ protected client: OpenAI
93
+ private readonly allowUrlFetch: boolean
94
+
95
+ constructor(config: LovableImageConfig, model: TModel) {
96
+ super(model, {})
97
+ const { allowUrlFetch, ...clientOptions } = config
98
+ this.client = new OpenAI(withLovableDefaults(clientOptions))
99
+ this.allowUrlFetch = allowUrlFetch ?? false
100
+ }
101
+
102
+ async generateImages(
103
+ options: ImageGenerationOptions<LovableImageProviderOptions>,
104
+ ): Promise<ImageGenerationResult> {
105
+ const { model, numberOfImages, size, modelOptions } = options
106
+ const resolved = resolveMediaPrompt(options.prompt)
107
+ const prompt = resolved.text
108
+
109
+ if (resolved.videos.length > 0) {
110
+ throw new Error(
111
+ `${this.name}.generateImages does not support video prompt parts (model: ${model}).`,
112
+ )
113
+ }
114
+ if (resolved.audios.length > 0) {
115
+ throw new Error(
116
+ `${this.name}.generateImages does not support audio prompt parts (model: ${model}).`,
117
+ )
118
+ }
119
+
120
+ if (resolved.images.length > 0) {
121
+ return this.editImages({
122
+ model,
123
+ prompt,
124
+ numberOfImages,
125
+ size,
126
+ modelOptions,
127
+ imageInputs: resolved.images,
128
+ logger: options.logger,
129
+ abortSignal: options.abortSignal,
130
+ })
131
+ }
132
+
133
+ const request: OpenAI_SDK.Images.ImageGenerateParams = {
134
+ model,
135
+ prompt,
136
+ n: numberOfImages ?? 1,
137
+ ...(modelOptions ?? {}),
138
+ }
139
+ if (size !== undefined) {
140
+ request.size = size
141
+ }
142
+
143
+ try {
144
+ options.logger.request(
145
+ `activity=image provider=${this.name} model=${model} n=${request.n ?? 1} size=${request.size ?? 'default'}`,
146
+ { provider: this.name, model },
147
+ )
148
+ const response = await this.client.images.generate(
149
+ {
150
+ ...request,
151
+ stream: false,
152
+ },
153
+ openaiRequestOptions(options.abortSignal),
154
+ )
155
+
156
+ const images = generatedImagesFromResponse(
157
+ response.data,
158
+ `${this.name}: image response contained no images`,
159
+ )
160
+ const usage = buildImagesUsage(response.usage)
161
+
162
+ return {
163
+ id: generateId(this.name),
164
+ model,
165
+ images,
166
+ ...(usage ? { usage } : {}),
167
+ }
168
+ } catch (error: unknown) {
169
+ options.logger.errors(`${this.name}.generateImages fatal`, {
170
+ error: toRunErrorPayload(error, `${this.name}.generateImages failed`),
171
+ source: `${this.name}.generateImages`,
172
+ })
173
+ throw error
174
+ }
175
+ }
176
+
177
+ private async editImages(args: {
178
+ model: string
179
+ prompt: string
180
+ numberOfImages?: number
181
+ size?: string
182
+ modelOptions?: LovableImageProviderOptions
183
+ imageInputs: ReadonlyArray<ImagePart<MediaInputMetadata>>
184
+ logger: ImageGenerationOptions<LovableImageProviderOptions>['logger']
185
+ abortSignal?: AbortSignal
186
+ }): Promise<ImageGenerationResult> {
187
+ const {
188
+ model,
189
+ prompt,
190
+ numberOfImages,
191
+ size,
192
+ modelOptions,
193
+ logger,
194
+ abortSignal,
195
+ } = args
196
+
197
+ const maskParts = args.imageInputs.filter(
198
+ (part) => part.metadata?.role === 'mask',
199
+ )
200
+ const sourceParts = args.imageInputs.filter(
201
+ (part) => part.metadata?.role !== 'mask',
202
+ )
203
+
204
+ if (maskParts.length > 0 && !supportsImageMask(model)) {
205
+ throw new Error(
206
+ `${this.name}: model "${model}" does not support mask image parts. Use an openai/ image model for masked edits.`,
207
+ )
208
+ }
209
+ if (maskParts.length > 1) {
210
+ throw new Error(
211
+ `${this.name}: only one input with metadata.role === 'mask' is supported per request.`,
212
+ )
213
+ }
214
+ if (sourceParts.length === 0) {
215
+ throw new Error(
216
+ `${this.name}: the prompt contained only mask image parts; at least one source image is required.`,
217
+ )
218
+ }
219
+ if (sourceParts.length > MAX_EDIT_IMAGES) {
220
+ throw new Error(
221
+ `${this.name}: model "${model}" accepts at most ${MAX_EDIT_IMAGES} source image(s); received ${sourceParts.length}.`,
222
+ )
223
+ }
224
+
225
+ const sourceFiles = await Promise.all(
226
+ sourceParts.map((part, i) =>
227
+ imagePartToFile(part, `source-${i}`, this.allowUrlFetch, abortSignal),
228
+ ),
229
+ )
230
+ const [firstSourceFile] = sourceFiles
231
+ const maskFile = maskParts[0]
232
+ ? await imagePartToFile(
233
+ maskParts[0],
234
+ 'mask',
235
+ this.allowUrlFetch,
236
+ abortSignal,
237
+ )
238
+ : undefined
239
+
240
+ const request: OpenAI_SDK.Images.ImageEditParamsNonStreaming = {
241
+ model,
242
+ prompt,
243
+ image:
244
+ firstSourceFile && sourceFiles.length === 1
245
+ ? firstSourceFile
246
+ : sourceFiles,
247
+ n: numberOfImages ?? 1,
248
+ stream: false,
249
+ ...((modelOptions ??
250
+ {}) as Partial<OpenAI_SDK.Images.ImageEditParamsNonStreaming>),
251
+ }
252
+ if (size !== undefined) {
253
+ request.size = size
254
+ }
255
+ if (maskFile) {
256
+ request.mask = maskFile
257
+ }
258
+
259
+ try {
260
+ logger.request(
261
+ `activity=imageEdit provider=${this.name} model=${model} n=${request.n ?? 1} size=${request.size ?? 'default'} sources=${sourceFiles.length}${maskFile ? ' mask' : ''}`,
262
+ { provider: this.name, model },
263
+ )
264
+ const response = await this.client.images.edit(
265
+ request,
266
+ openaiRequestOptions(abortSignal),
267
+ )
268
+
269
+ const images = generatedImagesFromResponse(
270
+ response.data,
271
+ `${this.name}: image edit response contained no images`,
272
+ )
273
+ const usage = buildImagesUsage(response.usage)
274
+
275
+ return {
276
+ id: generateId(this.name),
277
+ model,
278
+ images,
279
+ ...(usage ? { usage } : {}),
280
+ }
281
+ } catch (error: unknown) {
282
+ logger.errors(`${this.name}.editImages fatal`, {
283
+ error: toRunErrorPayload(error, `${this.name}.editImages failed`),
284
+ source: `${this.name}.editImages`,
285
+ })
286
+ throw error
287
+ }
288
+ }
289
+ }
290
+
291
+ export function createLovableImage<TModel extends LovableImageModel>(
292
+ model: TModel,
293
+ apiKey: string,
294
+ config?: Omit<LovableImageConfig, 'apiKey'>,
295
+ ): LovableImageAdapter<TModel> {
296
+ return new LovableImageAdapter({ apiKey, ...config }, model)
297
+ }
298
+
299
+ export function lovableImage<TModel extends LovableImageModel>(
300
+ model: TModel,
301
+ config?: Omit<LovableImageConfig, 'apiKey'>,
302
+ ): LovableImageAdapter<TModel> {
303
+ return createLovableImage(model, getLovableApiKeyFromEnv(), config)
304
+ }
@@ -0,0 +1,66 @@
1
+ import OpenAI from 'openai'
2
+ import { OpenAIBaseResponsesTextAdapter } from '@tanstack/openai-base'
3
+ import { getLovableApiKeyFromEnv, withLovableDefaults } from '../utils/client'
4
+ import type { Modality } from '@tanstack/ai'
5
+ import type {
6
+ LovableChatModelToolCapabilitiesByName,
7
+ LovableModelId,
8
+ ResolveInputModalities,
9
+ ResolveProviderOptions,
10
+ } from '../model-meta'
11
+ import type { LovableMessageMetadataByModality } from '../message-types'
12
+ import type { ExternalResponsesProviderOptions } from '../text/responses-provider-options'
13
+ import type { LovableClientConfig } from '../utils/client'
14
+
15
+ export interface LovableResponsesTextConfig extends LovableClientConfig {}
16
+
17
+ export type LovableResponsesTextProviderOptions =
18
+ ExternalResponsesProviderOptions
19
+
20
+ type ResolveToolCapabilities<TModel extends string> =
21
+ TModel extends keyof LovableChatModelToolCapabilitiesByName
22
+ ? NonNullable<LovableChatModelToolCapabilitiesByName[TModel]>
23
+ : readonly []
24
+
25
+ /**
26
+ * Lovable AI Gateway Responses text adapter.
27
+ *
28
+ * Talks to the OpenAI-compatible Responses API at
29
+ * `https://ai.gateway.lovable.dev/v1`.
30
+ */
31
+ export class LovableResponsesTextAdapter<
32
+ TModel extends LovableModelId,
33
+ TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,
34
+ TInputModalities extends ReadonlyArray<Modality> =
35
+ ResolveInputModalities<TModel>,
36
+ TToolCapabilities extends ReadonlyArray<string> =
37
+ ResolveToolCapabilities<TModel>,
38
+ > extends OpenAIBaseResponsesTextAdapter<
39
+ TModel,
40
+ TProviderOptions,
41
+ TInputModalities,
42
+ LovableMessageMetadataByModality,
43
+ TToolCapabilities
44
+ > {
45
+ override readonly kind = 'text' as const
46
+ override readonly name = 'lovable' as const
47
+
48
+ constructor(config: LovableResponsesTextConfig, model: TModel) {
49
+ super(model, 'lovable', new OpenAI(withLovableDefaults(config)))
50
+ }
51
+ }
52
+
53
+ export function createLovableResponsesText<TModel extends LovableModelId>(
54
+ model: TModel,
55
+ apiKey: string,
56
+ config?: Omit<LovableResponsesTextConfig, 'apiKey'>,
57
+ ): LovableResponsesTextAdapter<TModel> {
58
+ return new LovableResponsesTextAdapter({ apiKey, ...config }, model)
59
+ }
60
+
61
+ export function lovableResponsesText<TModel extends LovableModelId>(
62
+ model: TModel,
63
+ config?: Omit<LovableResponsesTextConfig, 'apiKey'>,
64
+ ): LovableResponsesTextAdapter<TModel> {
65
+ return createLovableResponsesText(model, getLovableApiKeyFromEnv(), config)
66
+ }
@@ -0,0 +1,35 @@
1
+ import { ChatStreamSummarizeAdapter } from '@tanstack/ai/adapters'
2
+ import { getLovableApiKeyFromEnv } from '../utils/client'
3
+ import { LovableTextAdapter } from './text'
4
+ import type { InferTextProviderOptions } from '@tanstack/ai/adapters'
5
+ import type { LovableModelId } from '../model-meta'
6
+ import type { LovableClientConfig } from '../utils/client'
7
+
8
+ export interface LovableSummarizeConfig extends LovableClientConfig {}
9
+
10
+ export type LovableSummarizeModel = LovableModelId
11
+
12
+ export function createLovableSummarize<TModel extends LovableSummarizeModel>(
13
+ model: TModel,
14
+ apiKey: string,
15
+ config?: Omit<LovableSummarizeConfig, 'apiKey'>,
16
+ ): ChatStreamSummarizeAdapter<
17
+ TModel,
18
+ InferTextProviderOptions<LovableTextAdapter<TModel>>
19
+ > {
20
+ return new ChatStreamSummarizeAdapter(
21
+ new LovableTextAdapter({ apiKey, ...config }, model),
22
+ model,
23
+ 'lovable',
24
+ )
25
+ }
26
+
27
+ export function lovableSummarize<TModel extends LovableSummarizeModel>(
28
+ model: TModel,
29
+ config?: Omit<LovableSummarizeConfig, 'apiKey'>,
30
+ ): ChatStreamSummarizeAdapter<
31
+ TModel,
32
+ InferTextProviderOptions<LovableTextAdapter<TModel>>
33
+ > {
34
+ return createLovableSummarize(model, getLovableApiKeyFromEnv(), config)
35
+ }
@@ -0,0 +1,49 @@
1
+ import OpenAI from 'openai'
2
+ import { OpenAIBaseChatCompletionsTextAdapter } from '@tanstack/openai-base'
3
+ import { withLovableDefaults } from '../utils/client'
4
+ import type { Modality } from '@tanstack/ai'
5
+ import type {
6
+ LovableChatModelToolCapabilitiesByName,
7
+ LovableModelId,
8
+ ResolveInputModalities,
9
+ ResolveProviderOptions,
10
+ } from '../model-meta'
11
+ import type { LovableMessageMetadataByModality } from '../message-types'
12
+ import type { LovableClientConfig } from '../utils/client'
13
+
14
+ type ResolveToolCapabilities<TModel extends string> =
15
+ TModel extends keyof LovableChatModelToolCapabilitiesByName
16
+ ? NonNullable<LovableChatModelToolCapabilitiesByName[TModel]>
17
+ : readonly []
18
+
19
+ export interface LovableTextConfig extends LovableClientConfig {}
20
+
21
+ export type { ExternalTextProviderOptions as LovableTextProviderOptions } from '../text/text-provider-options'
22
+
23
+ /**
24
+ * Lovable AI Gateway text adapter.
25
+ *
26
+ * Talks to the OpenAI-compatible Chat Completions API at
27
+ * `https://ai.gateway.lovable.dev/v1`.
28
+ */
29
+ export class LovableTextAdapter<
30
+ TModel extends LovableModelId,
31
+ TProviderOptions extends Record<string, any> = ResolveProviderOptions<TModel>,
32
+ TInputModalities extends ReadonlyArray<Modality> =
33
+ ResolveInputModalities<TModel>,
34
+ TToolCapabilities extends ReadonlyArray<string> =
35
+ ResolveToolCapabilities<TModel>,
36
+ > extends OpenAIBaseChatCompletionsTextAdapter<
37
+ TModel,
38
+ TProviderOptions,
39
+ TInputModalities,
40
+ LovableMessageMetadataByModality,
41
+ TToolCapabilities
42
+ > {
43
+ override readonly kind = 'text' as const
44
+ override readonly name = 'lovable' as const
45
+
46
+ constructor(config: LovableTextConfig, model: TModel) {
47
+ super(model, 'lovable', new OpenAI(withLovableDefaults(config)))
48
+ }
49
+ }