@tanstack/openai-base 0.2.1 → 0.3.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 (79) hide show
  1. package/README.md +121 -0
  2. package/dist/esm/adapters/chat-completions-text.d.ts +49 -21
  3. package/dist/esm/adapters/chat-completions-text.js +476 -68
  4. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  5. package/dist/esm/adapters/chat-completions-tool-converter.d.ts +8 -4
  6. package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -1
  7. package/dist/esm/adapters/responses-text.d.ts +46 -33
  8. package/dist/esm/adapters/responses-text.js +657 -142
  9. package/dist/esm/adapters/responses-text.js.map +1 -1
  10. package/dist/esm/index.d.ts +2 -9
  11. package/dist/esm/index.js +4 -16
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/tools/apply-patch-tool.d.ts +2 -2
  14. package/dist/esm/tools/apply-patch-tool.js.map +1 -1
  15. package/dist/esm/tools/code-interpreter-tool.d.ts +3 -2
  16. package/dist/esm/tools/code-interpreter-tool.js.map +1 -1
  17. package/dist/esm/tools/computer-use-tool.d.ts +2 -2
  18. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  19. package/dist/esm/tools/custom-tool.d.ts +2 -2
  20. package/dist/esm/tools/custom-tool.js.map +1 -1
  21. package/dist/esm/tools/file-search-tool.d.ts +2 -2
  22. package/dist/esm/tools/file-search-tool.js.map +1 -1
  23. package/dist/esm/tools/function-tool.d.ts +2 -2
  24. package/dist/esm/tools/function-tool.js.map +1 -1
  25. package/dist/esm/tools/image-generation-tool.d.ts +3 -2
  26. package/dist/esm/tools/image-generation-tool.js.map +1 -1
  27. package/dist/esm/tools/local-shell-tool.d.ts +3 -2
  28. package/dist/esm/tools/local-shell-tool.js.map +1 -1
  29. package/dist/esm/tools/mcp-tool.d.ts +3 -2
  30. package/dist/esm/tools/mcp-tool.js.map +1 -1
  31. package/dist/esm/tools/shell-tool.d.ts +2 -2
  32. package/dist/esm/tools/shell-tool.js.map +1 -1
  33. package/dist/esm/tools/web-search-preview-tool.d.ts +2 -2
  34. package/dist/esm/tools/web-search-preview-tool.js.map +1 -1
  35. package/dist/esm/tools/web-search-tool.d.ts +2 -2
  36. package/dist/esm/tools/web-search-tool.js.map +1 -1
  37. package/package.json +6 -6
  38. package/src/adapters/chat-completions-text.ts +601 -117
  39. package/src/adapters/chat-completions-tool-converter.ts +9 -5
  40. package/src/adapters/responses-text.ts +865 -210
  41. package/src/index.ts +2 -12
  42. package/src/tools/apply-patch-tool.ts +2 -2
  43. package/src/tools/code-interpreter-tool.ts +4 -2
  44. package/src/tools/computer-use-tool.ts +2 -2
  45. package/src/tools/custom-tool.ts +2 -2
  46. package/src/tools/file-search-tool.ts +3 -3
  47. package/src/tools/function-tool.ts +2 -2
  48. package/src/tools/image-generation-tool.ts +4 -2
  49. package/src/tools/local-shell-tool.ts +4 -2
  50. package/src/tools/mcp-tool.ts +4 -2
  51. package/src/tools/shell-tool.ts +2 -2
  52. package/src/tools/web-search-preview-tool.ts +2 -2
  53. package/src/tools/web-search-tool.ts +2 -2
  54. package/dist/esm/adapters/image.d.ts +0 -32
  55. package/dist/esm/adapters/image.js +0 -89
  56. package/dist/esm/adapters/image.js.map +0 -1
  57. package/dist/esm/adapters/summarize.d.ts +0 -28
  58. package/dist/esm/adapters/summarize.js +0 -112
  59. package/dist/esm/adapters/summarize.js.map +0 -1
  60. package/dist/esm/adapters/transcription.d.ts +0 -34
  61. package/dist/esm/adapters/transcription.js +0 -131
  62. package/dist/esm/adapters/transcription.js.map +0 -1
  63. package/dist/esm/adapters/tts.d.ts +0 -26
  64. package/dist/esm/adapters/tts.js +0 -78
  65. package/dist/esm/adapters/tts.js.map +0 -1
  66. package/dist/esm/adapters/video.d.ts +0 -72
  67. package/dist/esm/adapters/video.js +0 -238
  68. package/dist/esm/adapters/video.js.map +0 -1
  69. package/dist/esm/types/config.d.ts +0 -4
  70. package/dist/esm/utils/client.d.ts +0 -3
  71. package/dist/esm/utils/client.js +0 -8
  72. package/dist/esm/utils/client.js.map +0 -1
  73. package/src/adapters/image.ts +0 -158
  74. package/src/adapters/summarize.ts +0 -174
  75. package/src/adapters/transcription.ts +0 -194
  76. package/src/adapters/tts.ts +0 -124
  77. package/src/adapters/video.ts +0 -385
  78. package/src/types/config.ts +0 -5
  79. package/src/utils/client.ts +0 -8
@@ -1,385 +0,0 @@
1
- import { BaseVideoAdapter } from '@tanstack/ai/adapters'
2
- import { toRunErrorPayload } from '@tanstack/ai/adapter-internals'
3
- import { arrayBufferToBase64 } from '@tanstack/ai-utils'
4
- import { createOpenAICompatibleClient } from '../utils/client'
5
- import type {
6
- VideoGenerationOptions,
7
- VideoJobResult,
8
- VideoStatusResult,
9
- VideoUrlResult,
10
- } from '@tanstack/ai'
11
- import type OpenAI_SDK from 'openai'
12
- import type { OpenAICompatibleClientConfig } from '../types/config'
13
-
14
- /**
15
- * Threshold for emitting a "this download will probably OOM serverless
16
- * runtimes" warning. Anything larger than this (in bytes) gets surfaced via
17
- * console.warn — workers and small isolates routinely run out of memory once
18
- * a downloaded video is base64-encoded (the encoded form is ~33% larger and
19
- * resides in V8 heap rather than streaming through the runtime's network
20
- * layer).
21
- */
22
- const LARGE_MEDIA_BUFFER_BYTES = 10 * 1024 * 1024
23
-
24
- function warnIfLargeMediaBuffer(
25
- byteLength: number,
26
- source: string,
27
- providerName: string,
28
- ): void {
29
- if (byteLength <= LARGE_MEDIA_BUFFER_BYTES) return
30
- // No InternalLogger plumbed through to these download paths yet; surface
31
- // via console.warn so Workers / Lambda dashboards still capture it.
32
- console.warn(
33
- `[${providerName}.${source}] downloaded ${(byteLength / 1024 / 1024).toFixed(1)} MiB into memory before base64 encoding. ` +
34
- `Workers/serverless runtimes commonly run out of memory above ~10 MiB. ` +
35
- `Consider streaming the video through a CDN or your own storage layer instead.`,
36
- )
37
- }
38
-
39
- /**
40
- * OpenAI-Compatible Video Generation Adapter
41
- *
42
- * A generalized base class for providers that implement OpenAI-compatible video
43
- * generation APIs. Uses a job/polling architecture for async video generation.
44
- *
45
- * Providers can extend this class and only need to:
46
- * - Set `baseURL` in the config
47
- * - Lock the generic type parameters to provider-specific types
48
- * - Override validation or request building methods as needed
49
- *
50
- * All methods that validate inputs, build requests, or map responses are `protected`
51
- * so subclasses can override them.
52
- *
53
- * @experimental Video generation is an experimental feature and may change.
54
- */
55
- export class OpenAICompatibleVideoAdapter<
56
- TModel extends string,
57
- TProviderOptions extends object = Record<string, any>,
58
- TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,
59
- TModelSizeByName extends Record<string, string> = Record<string, string>,
60
- > extends BaseVideoAdapter<
61
- TModel,
62
- TProviderOptions,
63
- TModelProviderOptionsByName,
64
- TModelSizeByName
65
- > {
66
- readonly name: string
67
-
68
- protected client: OpenAI_SDK
69
- protected clientConfig: OpenAICompatibleClientConfig
70
-
71
- constructor(
72
- config: OpenAICompatibleClientConfig,
73
- model: TModel,
74
- name: string = 'openai-compatible',
75
- ) {
76
- super(config, model)
77
- this.name = name
78
- this.clientConfig = config
79
- this.client = createOpenAICompatibleClient(config)
80
- }
81
-
82
- /**
83
- * Create a new video generation job.
84
- *
85
- * @experimental Video generation is an experimental feature and may change.
86
- */
87
- async createVideoJob(
88
- options: VideoGenerationOptions<TProviderOptions>,
89
- ): Promise<VideoJobResult> {
90
- const { model, size, duration, modelOptions } = options
91
-
92
- // Validate inputs
93
- this.validateVideoSize(model, size)
94
- const seconds = duration ?? (modelOptions as any)?.seconds
95
- this.validateVideoSeconds(model, seconds)
96
-
97
- // Build request
98
- const request = this.buildRequest(options)
99
-
100
- try {
101
- options.logger.request(
102
- `activity=video.create provider=${this.name} model=${model} size=${request.size ?? 'default'} seconds=${request.seconds ?? 'default'}`,
103
- { provider: this.name, model },
104
- )
105
- // The video API on the OpenAI SDK is still experimental and shipped on
106
- // some SDK versions but not others; access through `videosClient` lets
107
- // subclasses override the entry point or supply a polyfill without
108
- // forcing every call site through `as any`.
109
- const videosClient = this.getVideosClient()
110
- const response = await videosClient.create(request)
111
-
112
- return {
113
- jobId: response.id,
114
- model,
115
- }
116
- } catch (error: any) {
117
- options.logger.errors(`${this.name}.createVideoJob fatal`, {
118
- error: toRunErrorPayload(error, `${this.name}.createVideoJob failed`),
119
- source: `${this.name}.createVideoJob`,
120
- })
121
- if (error?.message?.includes('videos') || error?.code === 'invalid_api') {
122
- throw new Error(
123
- `Video generation API is not available. The API may require special access. ` +
124
- `Original error: ${error.message}`,
125
- )
126
- }
127
- throw error
128
- }
129
- }
130
-
131
- /**
132
- * Returns the underlying OpenAI Videos resource. Pulled out as a protected
133
- * accessor so subclasses targeting forks of the SDK can swap the access
134
- * path without forcing each call site to cast through `any`.
135
- */
136
- protected getVideosClient(): {
137
- create: (req: Record<string, any>) => Promise<{ id: string }>
138
- retrieve: (id: string) => Promise<{
139
- id: string
140
- status: string
141
- progress?: number
142
- url?: string
143
- expires_at?: number
144
- error?: { message?: string }
145
- }>
146
- downloadContent?: (id: string) => Promise<Response>
147
- content?: (id: string) => Promise<unknown>
148
- getContent?: (id: string) => Promise<unknown>
149
- download?: (id: string) => Promise<unknown>
150
- } {
151
- return (this.client as unknown as { videos: any }).videos
152
- }
153
-
154
- /**
155
- * Get the current status of a video generation job.
156
- *
157
- * @experimental Video generation is an experimental feature and may change.
158
- */
159
- async getVideoStatus(jobId: string): Promise<VideoStatusResult> {
160
- try {
161
- const videosClient = this.getVideosClient()
162
- const response = await videosClient.retrieve(jobId)
163
-
164
- return {
165
- jobId,
166
- status: this.mapStatus(response.status),
167
- progress: response.progress,
168
- error: response.error?.message,
169
- }
170
- } catch (error: any) {
171
- if (error.status === 404) {
172
- return {
173
- jobId,
174
- status: 'failed',
175
- error: 'Job not found',
176
- }
177
- }
178
- throw error
179
- }
180
- }
181
-
182
- /**
183
- * Get the URL to download/view the generated video.
184
- *
185
- * @experimental Video generation is an experimental feature and may change.
186
- */
187
- async getVideoUrl(jobId: string): Promise<VideoUrlResult> {
188
- try {
189
- const videosClient = this.getVideosClient()
190
-
191
- // Prefer retrieve() because many openai-compatible backends (and the
192
- // aimock test harness) return the URL directly on the video resource
193
- // and do not implement a separate /content endpoint. Subclasses can
194
- // override this method if they need to download raw bytes via
195
- // downloadContent()/content().
196
- const videoInfo = await videosClient.retrieve(jobId)
197
- if (videoInfo.url) {
198
- return {
199
- jobId,
200
- url: videoInfo.url,
201
- expiresAt: videoInfo.expires_at
202
- ? new Date(videoInfo.expires_at)
203
- : undefined,
204
- }
205
- }
206
-
207
- // SDK download fall-through: try the various possible method names in
208
- // decreasing order of modernity.
209
- if (typeof videosClient.downloadContent === 'function') {
210
- const contentResponse = await videosClient.downloadContent(jobId)
211
- const videoBlob = await contentResponse.blob()
212
- const buffer = await videoBlob.arrayBuffer()
213
- warnIfLargeMediaBuffer(
214
- buffer.byteLength,
215
- 'video.downloadContent',
216
- this.name,
217
- )
218
- const base64 = arrayBufferToBase64(buffer)
219
- const mimeType =
220
- contentResponse.headers.get('content-type') || 'video/mp4'
221
- return {
222
- jobId,
223
- url: `data:${mimeType};base64,${base64}`,
224
- expiresAt: undefined,
225
- }
226
- }
227
-
228
- // The remaining SDK fall-throughs all return a binary payload
229
- // (Blob/Response/ArrayBuffer-shaped), NOT an `{ url, expires_at }`
230
- // object the way the bottom return assumed. Convert to a data URL
231
- // here so the caller actually receives a usable URL.
232
- let response: any
233
- if (typeof videosClient.content === 'function') {
234
- response = await videosClient.content(jobId)
235
- } else if (typeof videosClient.getContent === 'function') {
236
- response = await videosClient.getContent(jobId)
237
- } else if (typeof videosClient.download === 'function') {
238
- response = await videosClient.download(jobId)
239
- } else {
240
- // Last resort: raw fetch with auth header.
241
- const baseUrl = this.clientConfig.baseURL || 'https://api.openai.com/v1'
242
- const apiKey = this.clientConfig.apiKey
243
-
244
- const contentResponse = await fetch(
245
- `${baseUrl}/videos/${jobId}/content`,
246
- {
247
- method: 'GET',
248
- headers: {
249
- Authorization: `Bearer ${apiKey}`,
250
- },
251
- },
252
- )
253
-
254
- if (!contentResponse.ok) {
255
- const contentType = contentResponse.headers.get('content-type')
256
- if (contentType?.includes('application/json')) {
257
- const errorData = await contentResponse.json().catch(() => ({}))
258
- throw new Error(
259
- errorData.error?.message ||
260
- `Failed to get video content: ${contentResponse.status}`,
261
- )
262
- }
263
- throw new Error(
264
- `Failed to get video content: ${contentResponse.status}`,
265
- )
266
- }
267
-
268
- const videoBlob = await contentResponse.blob()
269
- const buffer = await videoBlob.arrayBuffer()
270
- warnIfLargeMediaBuffer(buffer.byteLength, 'video.fetch', this.name)
271
- const base64 = arrayBufferToBase64(buffer)
272
- const mimeType =
273
- contentResponse.headers.get('content-type') || 'video/mp4'
274
-
275
- return {
276
- jobId,
277
- url: `data:${mimeType};base64,${base64}`,
278
- expiresAt: undefined,
279
- }
280
- }
281
-
282
- // The fall-through SDK methods produce a Blob-ish or fetch-`Response`-ish
283
- // object. Read it as bytes and wrap in a data URL so callers see an
284
- // actual playable URL instead of the API endpoint URL (which is what
285
- // `response.url` would be on a fetch Response).
286
- const fallthroughBlob =
287
- typeof response?.blob === 'function'
288
- ? await response.blob()
289
- : response instanceof Blob
290
- ? response
291
- : null
292
- if (!fallthroughBlob) {
293
- throw new Error(
294
- `Video content download via SDK fall-through returned an unexpected shape (no blob()). ` +
295
- `Override getVideoUrl() in your subclass to handle this provider.`,
296
- )
297
- }
298
- const fallthroughBuffer = await fallthroughBlob.arrayBuffer()
299
- warnIfLargeMediaBuffer(
300
- fallthroughBuffer.byteLength,
301
- 'video.sdkFallthrough',
302
- this.name,
303
- )
304
- const fallthroughBase64 = arrayBufferToBase64(fallthroughBuffer)
305
- const fallthroughMime =
306
- (typeof response?.headers?.get === 'function'
307
- ? response.headers.get('content-type')
308
- : undefined) ||
309
- fallthroughBlob.type ||
310
- 'video/mp4'
311
- return {
312
- jobId,
313
- url: `data:${fallthroughMime};base64,${fallthroughBase64}`,
314
- expiresAt: undefined,
315
- }
316
- } catch (error: any) {
317
- if (error.status === 404) {
318
- throw new Error(`Video job not found: ${jobId}`)
319
- }
320
- if (error.status === 400) {
321
- throw new Error(
322
- `Video is not ready for download. Check status first. Job ID: ${jobId}`,
323
- )
324
- }
325
- throw error
326
- }
327
- }
328
-
329
- protected buildRequest(
330
- options: VideoGenerationOptions<TProviderOptions>,
331
- ): Record<string, any> {
332
- const { model, prompt, size, duration, modelOptions } = options
333
-
334
- const request: Record<string, any> = {
335
- model,
336
- prompt,
337
- }
338
-
339
- if (size) {
340
- request['size'] = size
341
- } else if ((modelOptions as any)?.size) {
342
- request['size'] = (modelOptions as any).size
343
- }
344
-
345
- const seconds = duration ?? (modelOptions as any)?.seconds
346
- if (seconds !== undefined) {
347
- request['seconds'] = String(seconds)
348
- }
349
-
350
- return request
351
- }
352
-
353
- protected validateVideoSize(_model: string, _size?: string): void {
354
- // Default: no size validation — subclasses can override
355
- }
356
-
357
- protected validateVideoSeconds(
358
- _model: string,
359
- _seconds?: number | string,
360
- ): void {
361
- // Default: no duration validation — subclasses can override
362
- }
363
-
364
- protected mapStatus(
365
- apiStatus: string,
366
- ): 'pending' | 'processing' | 'completed' | 'failed' {
367
- switch (apiStatus) {
368
- case 'queued':
369
- case 'pending':
370
- return 'pending'
371
- case 'processing':
372
- case 'in_progress':
373
- return 'processing'
374
- case 'completed':
375
- case 'succeeded':
376
- return 'completed'
377
- case 'failed':
378
- case 'error':
379
- case 'cancelled':
380
- return 'failed'
381
- default:
382
- return 'processing'
383
- }
384
- }
385
- }
@@ -1,5 +0,0 @@
1
- import type { ClientOptions } from 'openai'
2
-
3
- export interface OpenAICompatibleClientConfig extends ClientOptions {
4
- apiKey: string
5
- }
@@ -1,8 +0,0 @@
1
- import OpenAI from 'openai'
2
- import type { OpenAICompatibleClientConfig } from '../types/config'
3
-
4
- export function createOpenAICompatibleClient(
5
- config: OpenAICompatibleClientConfig,
6
- ): OpenAI {
7
- return new OpenAI(config)
8
- }