@tanstack/openai-base 0.1.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.
- package/dist/esm/adapters/chat-completions-text.d.ts +76 -0
- package/dist/esm/adapters/chat-completions-text.js +411 -0
- package/dist/esm/adapters/chat-completions-text.js.map +1 -0
- package/dist/esm/adapters/chat-completions-tool-converter.d.ts +24 -0
- package/dist/esm/adapters/chat-completions-tool-converter.js +29 -0
- package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -0
- package/dist/esm/adapters/image.d.ts +32 -0
- package/dist/esm/adapters/image.js +69 -0
- package/dist/esm/adapters/image.js.map +1 -0
- package/dist/esm/adapters/responses-text.d.ts +115 -0
- package/dist/esm/adapters/responses-text.js +635 -0
- package/dist/esm/adapters/responses-text.js.map +1 -0
- package/dist/esm/adapters/responses-tool-converter.d.ts +35 -0
- package/dist/esm/adapters/responses-tool-converter.js +27 -0
- package/dist/esm/adapters/responses-tool-converter.js.map +1 -0
- package/dist/esm/adapters/summarize.d.ts +28 -0
- package/dist/esm/adapters/summarize.js +74 -0
- package/dist/esm/adapters/summarize.js.map +1 -0
- package/dist/esm/adapters/transcription.d.ts +39 -0
- package/dist/esm/adapters/transcription.js +139 -0
- package/dist/esm/adapters/transcription.js.map +1 -0
- package/dist/esm/adapters/tts.d.ts +26 -0
- package/dist/esm/adapters/tts.js +65 -0
- package/dist/esm/adapters/tts.js.map +1 -0
- package/dist/esm/adapters/video.d.ts +48 -0
- package/dist/esm/adapters/video.js +192 -0
- package/dist/esm/adapters/video.js.map +1 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +65 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/tools/apply-patch-tool.d.ts +11 -0
- package/dist/esm/tools/apply-patch-tool.js +17 -0
- package/dist/esm/tools/apply-patch-tool.js.map +1 -0
- package/dist/esm/tools/code-interpreter-tool.d.ts +11 -0
- package/dist/esm/tools/code-interpreter-tool.js +22 -0
- package/dist/esm/tools/code-interpreter-tool.js.map +1 -0
- package/dist/esm/tools/computer-use-tool.d.ts +11 -0
- package/dist/esm/tools/computer-use-tool.js +23 -0
- package/dist/esm/tools/computer-use-tool.js.map +1 -0
- package/dist/esm/tools/custom-tool.d.ts +11 -0
- package/dist/esm/tools/custom-tool.js +23 -0
- package/dist/esm/tools/custom-tool.js.map +1 -0
- package/dist/esm/tools/file-search-tool.d.ts +11 -0
- package/dist/esm/tools/file-search-tool.js +30 -0
- package/dist/esm/tools/file-search-tool.js.map +1 -0
- package/dist/esm/tools/function-tool.d.ts +15 -0
- package/dist/esm/tools/function-tool.js +24 -0
- package/dist/esm/tools/function-tool.js.map +1 -0
- package/dist/esm/tools/image-generation-tool.d.ts +11 -0
- package/dist/esm/tools/image-generation-tool.js +27 -0
- package/dist/esm/tools/image-generation-tool.js.map +1 -0
- package/dist/esm/tools/index.d.ts +27 -0
- package/dist/esm/tools/local-shell-tool.d.ts +11 -0
- package/dist/esm/tools/local-shell-tool.js +17 -0
- package/dist/esm/tools/local-shell-tool.js.map +1 -0
- package/dist/esm/tools/mcp-tool.d.ts +12 -0
- package/dist/esm/tools/mcp-tool.js +31 -0
- package/dist/esm/tools/mcp-tool.js.map +1 -0
- package/dist/esm/tools/shell-tool.d.ts +11 -0
- package/dist/esm/tools/shell-tool.js +17 -0
- package/dist/esm/tools/shell-tool.js.map +1 -0
- package/dist/esm/tools/tool-choice.d.ts +17 -0
- package/dist/esm/tools/tool-converter.d.ts +6 -0
- package/dist/esm/tools/tool-converter.js +61 -0
- package/dist/esm/tools/tool-converter.js.map +1 -0
- package/dist/esm/tools/web-search-preview-tool.d.ts +11 -0
- package/dist/esm/tools/web-search-preview-tool.js +20 -0
- package/dist/esm/tools/web-search-preview-tool.js.map +1 -0
- package/dist/esm/tools/web-search-tool.d.ts +11 -0
- package/dist/esm/tools/web-search-tool.js +16 -0
- package/dist/esm/tools/web-search-tool.js.map +1 -0
- package/dist/esm/types/config.d.ts +4 -0
- package/dist/esm/types/message-metadata.d.ts +19 -0
- package/dist/esm/types/provider-options.d.ts +40 -0
- package/dist/esm/utils/client.d.ts +3 -0
- package/dist/esm/utils/client.js +8 -0
- package/dist/esm/utils/client.js.map +1 -0
- package/dist/esm/utils/schema-converter.d.ts +12 -0
- package/dist/esm/utils/schema-converter.js +65 -0
- package/dist/esm/utils/schema-converter.js.map +1 -0
- package/package.json +57 -0
- package/src/adapters/chat-completions-text.ts +817 -0
- package/src/adapters/chat-completions-tool-converter.ts +70 -0
- package/src/adapters/image.ts +158 -0
- package/src/adapters/responses-text.ts +1147 -0
- package/src/adapters/responses-tool-converter.ts +77 -0
- package/src/adapters/summarize.ts +174 -0
- package/src/adapters/transcription.ts +194 -0
- package/src/adapters/tts.ts +124 -0
- package/src/adapters/video.ts +385 -0
- package/src/index.ts +24 -0
- package/src/tools/apply-patch-tool.ts +32 -0
- package/src/tools/code-interpreter-tool.ts +39 -0
- package/src/tools/computer-use-tool.ts +38 -0
- package/src/tools/custom-tool.ts +33 -0
- package/src/tools/file-search-tool.ts +51 -0
- package/src/tools/function-tool.ts +44 -0
- package/src/tools/image-generation-tool.ts +51 -0
- package/src/tools/index.ts +41 -0
- package/src/tools/local-shell-tool.ts +32 -0
- package/src/tools/mcp-tool.ts +47 -0
- package/src/tools/shell-tool.ts +30 -0
- package/src/tools/tool-choice.ts +31 -0
- package/src/tools/tool-converter.ts +68 -0
- package/src/tools/web-search-preview-tool.ts +39 -0
- package/src/tools/web-search-tool.ts +38 -0
- package/src/types/config.ts +5 -0
- package/src/utils/client.ts +8 -0
- package/src/utils/request-options.ts +16 -0
- package/src/utils/schema-converter.ts +89 -0
|
@@ -0,0 +1,385 @@
|
|
|
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
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { makeStructuredOutputCompatible } from './utils/schema-converter'
|
|
2
|
+
export { createOpenAICompatibleClient } from './utils/client'
|
|
3
|
+
export type { OpenAICompatibleClientConfig } from './types/config'
|
|
4
|
+
export * from './tools/index'
|
|
5
|
+
export { OpenAICompatibleChatCompletionsTextAdapter } from './adapters/chat-completions-text'
|
|
6
|
+
export {
|
|
7
|
+
convertFunctionToolToChatCompletionsFormat,
|
|
8
|
+
convertToolsToChatCompletionsFormat,
|
|
9
|
+
type ChatCompletionFunctionTool,
|
|
10
|
+
} from './adapters/chat-completions-tool-converter'
|
|
11
|
+
export { OpenAICompatibleResponsesTextAdapter } from './adapters/responses-text'
|
|
12
|
+
export {
|
|
13
|
+
convertFunctionToolToResponsesFormat,
|
|
14
|
+
convertToolsToResponsesFormat,
|
|
15
|
+
type ResponsesFunctionTool,
|
|
16
|
+
} from './adapters/responses-tool-converter'
|
|
17
|
+
export { OpenAICompatibleImageAdapter } from './adapters/image'
|
|
18
|
+
export {
|
|
19
|
+
OpenAICompatibleSummarizeAdapter,
|
|
20
|
+
type ChatStreamCapable,
|
|
21
|
+
} from './adapters/summarize'
|
|
22
|
+
export { OpenAICompatibleTranscriptionAdapter } from './adapters/transcription'
|
|
23
|
+
export { OpenAICompatibleTTSAdapter } from './adapters/tts'
|
|
24
|
+
export { OpenAICompatibleVideoAdapter } from './adapters/video'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type OpenAI from 'openai'
|
|
2
|
+
import type { Tool } from '@tanstack/ai'
|
|
3
|
+
|
|
4
|
+
export type ApplyPatchToolConfig = OpenAI.Responses.ApplyPatchTool
|
|
5
|
+
|
|
6
|
+
/** @deprecated Renamed to `ApplyPatchToolConfig`. Will be removed in a future release. */
|
|
7
|
+
export type ApplyPatchTool = ApplyPatchToolConfig
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts a standard Tool to OpenAI ApplyPatchTool format
|
|
11
|
+
*/
|
|
12
|
+
export function convertApplyPatchToolToAdapterFormat(
|
|
13
|
+
_tool: Tool,
|
|
14
|
+
): ApplyPatchToolConfig {
|
|
15
|
+
return {
|
|
16
|
+
type: 'apply_patch',
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a standard Tool from ApplyPatchTool parameters.
|
|
22
|
+
*
|
|
23
|
+
* Base (non-branded) factory. Providers that need branded return types should
|
|
24
|
+
* re-wrap this in their own package.
|
|
25
|
+
*/
|
|
26
|
+
export function applyPatchTool(): Tool {
|
|
27
|
+
return {
|
|
28
|
+
name: 'apply_patch',
|
|
29
|
+
description: 'Apply a patch to modify files',
|
|
30
|
+
metadata: {},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Tool } from '@tanstack/ai'
|
|
2
|
+
import type OpenAI from 'openai'
|
|
3
|
+
|
|
4
|
+
export type CodeInterpreterToolConfig = OpenAI.Responses.Tool.CodeInterpreter
|
|
5
|
+
|
|
6
|
+
/** @deprecated Renamed to `CodeInterpreterToolConfig`. Will be removed in a future release. */
|
|
7
|
+
export type CodeInterpreterTool = CodeInterpreterToolConfig
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts a standard Tool to OpenAI CodeInterpreterTool format
|
|
11
|
+
*/
|
|
12
|
+
export function convertCodeInterpreterToolToAdapterFormat(
|
|
13
|
+
tool: Tool,
|
|
14
|
+
): CodeInterpreterToolConfig {
|
|
15
|
+
const metadata = tool.metadata as CodeInterpreterToolConfig
|
|
16
|
+
return {
|
|
17
|
+
type: 'code_interpreter',
|
|
18
|
+
container: metadata.container,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a standard Tool from CodeInterpreterTool parameters.
|
|
24
|
+
*
|
|
25
|
+
* Base (non-branded) factory. Providers that need branded return types should
|
|
26
|
+
* re-wrap this in their own package.
|
|
27
|
+
*/
|
|
28
|
+
export function codeInterpreterTool(
|
|
29
|
+
container: CodeInterpreterToolConfig,
|
|
30
|
+
): Tool {
|
|
31
|
+
return {
|
|
32
|
+
name: 'code_interpreter',
|
|
33
|
+
description: 'Execute code in a sandboxed environment',
|
|
34
|
+
metadata: {
|
|
35
|
+
type: 'code_interpreter',
|
|
36
|
+
container,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type OpenAI from 'openai'
|
|
2
|
+
import type { Tool } from '@tanstack/ai'
|
|
3
|
+
|
|
4
|
+
export type ComputerUseToolConfig = OpenAI.Responses.ComputerTool
|
|
5
|
+
|
|
6
|
+
/** @deprecated Renamed to `ComputerUseToolConfig`. Will be removed in a future release. */
|
|
7
|
+
export type ComputerUseTool = ComputerUseToolConfig
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts a standard Tool to OpenAI ComputerUseTool format
|
|
11
|
+
*/
|
|
12
|
+
export function convertComputerUseToolToAdapterFormat(
|
|
13
|
+
tool: Tool,
|
|
14
|
+
): ComputerUseToolConfig {
|
|
15
|
+
const metadata = tool.metadata as ComputerUseToolConfig
|
|
16
|
+
return {
|
|
17
|
+
type: 'computer_use_preview',
|
|
18
|
+
display_height: metadata.display_height,
|
|
19
|
+
display_width: metadata.display_width,
|
|
20
|
+
environment: metadata.environment,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a standard Tool from ComputerUseTool parameters.
|
|
26
|
+
*
|
|
27
|
+
* Base (non-branded) factory. Providers that need branded return types should
|
|
28
|
+
* re-wrap this in their own package.
|
|
29
|
+
*/
|
|
30
|
+
export function computerUseTool(toolData: ComputerUseToolConfig): Tool {
|
|
31
|
+
return {
|
|
32
|
+
name: 'computer_use_preview',
|
|
33
|
+
description: 'Control a virtual computer',
|
|
34
|
+
metadata: {
|
|
35
|
+
...toolData,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type OpenAI from 'openai'
|
|
2
|
+
import type { Tool } from '@tanstack/ai'
|
|
3
|
+
|
|
4
|
+
export type CustomToolConfig = OpenAI.Responses.CustomTool
|
|
5
|
+
|
|
6
|
+
/** @deprecated Renamed to `CustomToolConfig`. Will be removed in a future release. */
|
|
7
|
+
export type CustomTool = CustomToolConfig
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts a standard Tool to OpenAI CustomTool format
|
|
11
|
+
*/
|
|
12
|
+
export function convertCustomToolToAdapterFormat(tool: Tool): CustomToolConfig {
|
|
13
|
+
const metadata = tool.metadata as CustomToolConfig
|
|
14
|
+
return {
|
|
15
|
+
type: 'custom',
|
|
16
|
+
name: metadata.name,
|
|
17
|
+
description: metadata.description,
|
|
18
|
+
format: metadata.format,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a standard Tool from CustomTool parameters.
|
|
24
|
+
*/
|
|
25
|
+
export function customTool(toolData: CustomToolConfig): Tool {
|
|
26
|
+
return {
|
|
27
|
+
name: 'custom',
|
|
28
|
+
description: toolData.description || 'A custom tool',
|
|
29
|
+
metadata: {
|
|
30
|
+
...toolData,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type OpenAI from 'openai'
|
|
2
|
+
import type { Tool } from '@tanstack/ai'
|
|
3
|
+
|
|
4
|
+
const validateMaxNumResults = (maxNumResults: number | undefined) => {
|
|
5
|
+
if (
|
|
6
|
+
maxNumResults !== undefined &&
|
|
7
|
+
(maxNumResults < 1 || maxNumResults > 50)
|
|
8
|
+
) {
|
|
9
|
+
throw new Error('max_num_results must be between 1 and 50.')
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FileSearchToolConfig = OpenAI.Responses.FileSearchTool
|
|
14
|
+
|
|
15
|
+
/** @deprecated Renamed to `FileSearchToolConfig`. Will be removed in a future release. */
|
|
16
|
+
export type FileSearchTool = FileSearchToolConfig
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Converts a standard Tool to OpenAI FileSearchTool format
|
|
20
|
+
*/
|
|
21
|
+
export function convertFileSearchToolToAdapterFormat(
|
|
22
|
+
tool: Tool,
|
|
23
|
+
): FileSearchToolConfig {
|
|
24
|
+
const metadata = tool.metadata as FileSearchToolConfig
|
|
25
|
+
return {
|
|
26
|
+
type: 'file_search',
|
|
27
|
+
vector_store_ids: metadata.vector_store_ids,
|
|
28
|
+
max_num_results: metadata.max_num_results,
|
|
29
|
+
ranking_options: metadata.ranking_options,
|
|
30
|
+
filters: metadata.filters,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a standard Tool from FileSearchTool parameters.
|
|
36
|
+
*
|
|
37
|
+
* Validates max_num_results. Base (non-branded) factory; providers that need
|
|
38
|
+
* branded return types should re-wrap in their own package.
|
|
39
|
+
*/
|
|
40
|
+
export function fileSearchTool(toolData: FileSearchToolConfig): Tool {
|
|
41
|
+
validateMaxNumResults(toolData.max_num_results)
|
|
42
|
+
return {
|
|
43
|
+
name: 'file_search',
|
|
44
|
+
description: 'Search files in vector stores',
|
|
45
|
+
metadata: {
|
|
46
|
+
...toolData,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { validateMaxNumResults }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { makeStructuredOutputCompatible } from '../utils/schema-converter'
|
|
2
|
+
import type { JSONSchema, Tool } from '@tanstack/ai'
|
|
3
|
+
import type OpenAI from 'openai'
|
|
4
|
+
|
|
5
|
+
export type FunctionToolConfig = OpenAI.Responses.FunctionTool
|
|
6
|
+
|
|
7
|
+
/** @deprecated Renamed to `FunctionToolConfig`. Will be removed in a future release. */
|
|
8
|
+
export type FunctionTool = FunctionToolConfig
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Converts a standard Tool to OpenAI FunctionTool format.
|
|
12
|
+
*
|
|
13
|
+
* Tool schemas are already converted to JSON Schema in the ai layer.
|
|
14
|
+
* We apply OpenAI-specific transformations for strict mode:
|
|
15
|
+
* - All properties in required array
|
|
16
|
+
* - Optional fields made nullable
|
|
17
|
+
* - additionalProperties: false
|
|
18
|
+
*
|
|
19
|
+
* This enables strict mode for all tools automatically.
|
|
20
|
+
*/
|
|
21
|
+
export function convertFunctionToolToAdapterFormat(
|
|
22
|
+
tool: Tool,
|
|
23
|
+
): FunctionToolConfig {
|
|
24
|
+
const inputSchema = (tool.inputSchema ?? {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {},
|
|
27
|
+
required: [],
|
|
28
|
+
}) as JSONSchema
|
|
29
|
+
|
|
30
|
+
const jsonSchema = makeStructuredOutputCompatible(
|
|
31
|
+
inputSchema,
|
|
32
|
+
inputSchema.required || [],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
jsonSchema.additionalProperties = false
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
type: 'function',
|
|
39
|
+
name: tool.name,
|
|
40
|
+
description: tool.description,
|
|
41
|
+
parameters: jsonSchema,
|
|
42
|
+
strict: true,
|
|
43
|
+
} satisfies FunctionToolConfig
|
|
44
|
+
}
|