ai-functions 0.2.19 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +105 -0
  3. package/README.md +232 -37
  4. package/TODO.md +138 -0
  5. package/dist/ai-promise.d.ts +219 -0
  6. package/dist/ai-promise.d.ts.map +1 -0
  7. package/dist/ai-promise.js +610 -0
  8. package/dist/ai-promise.js.map +1 -0
  9. package/dist/ai.d.ts +285 -0
  10. package/dist/ai.d.ts.map +1 -0
  11. package/dist/ai.js +842 -0
  12. package/dist/ai.js.map +1 -0
  13. package/dist/batch/anthropic.d.ts +23 -0
  14. package/dist/batch/anthropic.d.ts.map +1 -0
  15. package/dist/batch/anthropic.js +257 -0
  16. package/dist/batch/anthropic.js.map +1 -0
  17. package/dist/batch/bedrock.d.ts +64 -0
  18. package/dist/batch/bedrock.d.ts.map +1 -0
  19. package/dist/batch/bedrock.js +586 -0
  20. package/dist/batch/bedrock.js.map +1 -0
  21. package/dist/batch/cloudflare.d.ts +37 -0
  22. package/dist/batch/cloudflare.d.ts.map +1 -0
  23. package/dist/batch/cloudflare.js +289 -0
  24. package/dist/batch/cloudflare.js.map +1 -0
  25. package/dist/batch/google.d.ts +41 -0
  26. package/dist/batch/google.d.ts.map +1 -0
  27. package/dist/batch/google.js +360 -0
  28. package/dist/batch/google.js.map +1 -0
  29. package/dist/batch/index.d.ts +31 -0
  30. package/dist/batch/index.d.ts.map +1 -0
  31. package/dist/batch/index.js +31 -0
  32. package/dist/batch/index.js.map +1 -0
  33. package/dist/batch/memory.d.ts +44 -0
  34. package/dist/batch/memory.d.ts.map +1 -0
  35. package/dist/batch/memory.js +188 -0
  36. package/dist/batch/memory.js.map +1 -0
  37. package/dist/batch/openai.d.ts +37 -0
  38. package/dist/batch/openai.d.ts.map +1 -0
  39. package/dist/batch/openai.js +403 -0
  40. package/dist/batch/openai.js.map +1 -0
  41. package/dist/batch-map.d.ts +125 -0
  42. package/dist/batch-map.d.ts.map +1 -0
  43. package/dist/batch-map.js +406 -0
  44. package/dist/batch-map.js.map +1 -0
  45. package/dist/batch-queue.d.ts +273 -0
  46. package/dist/batch-queue.d.ts.map +1 -0
  47. package/dist/batch-queue.js +271 -0
  48. package/dist/batch-queue.js.map +1 -0
  49. package/dist/context.d.ts +133 -0
  50. package/dist/context.d.ts.map +1 -0
  51. package/dist/context.js +267 -0
  52. package/dist/context.js.map +1 -0
  53. package/dist/embeddings.d.ts +123 -0
  54. package/dist/embeddings.d.ts.map +1 -0
  55. package/dist/embeddings.js +170 -0
  56. package/dist/embeddings.js.map +1 -0
  57. package/dist/eval/index.d.ts +8 -0
  58. package/dist/eval/index.d.ts.map +1 -0
  59. package/dist/eval/index.js +8 -0
  60. package/dist/eval/index.js.map +1 -0
  61. package/dist/eval/models.d.ts +66 -0
  62. package/dist/eval/models.d.ts.map +1 -0
  63. package/dist/eval/models.js +120 -0
  64. package/dist/eval/models.js.map +1 -0
  65. package/dist/eval/runner.d.ts +64 -0
  66. package/dist/eval/runner.d.ts.map +1 -0
  67. package/dist/eval/runner.js +148 -0
  68. package/dist/eval/runner.js.map +1 -0
  69. package/dist/generate.d.ts +168 -0
  70. package/dist/generate.d.ts.map +1 -0
  71. package/dist/generate.js +174 -0
  72. package/dist/generate.js.map +1 -0
  73. package/dist/index.d.ts +30 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +54 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/primitives.d.ts +292 -0
  78. package/dist/primitives.d.ts.map +1 -0
  79. package/dist/primitives.js +471 -0
  80. package/dist/primitives.js.map +1 -0
  81. package/dist/providers/cloudflare.d.ts +9 -0
  82. package/dist/providers/cloudflare.d.ts.map +1 -0
  83. package/dist/providers/cloudflare.js +9 -0
  84. package/dist/providers/cloudflare.js.map +1 -0
  85. package/dist/providers/index.d.ts +9 -0
  86. package/dist/providers/index.d.ts.map +1 -0
  87. package/dist/providers/index.js +9 -0
  88. package/dist/providers/index.js.map +1 -0
  89. package/dist/schema.d.ts +54 -0
  90. package/dist/schema.d.ts.map +1 -0
  91. package/dist/schema.js +109 -0
  92. package/dist/schema.js.map +1 -0
  93. package/dist/template.d.ts +73 -0
  94. package/dist/template.d.ts.map +1 -0
  95. package/dist/template.js +129 -0
  96. package/dist/template.js.map +1 -0
  97. package/dist/types.d.ts +481 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/types.js +5 -0
  100. package/dist/types.js.map +1 -0
  101. package/evalite.config.ts +19 -0
  102. package/evals/README.md +212 -0
  103. package/evals/classification.eval.ts +108 -0
  104. package/evals/marketing.eval.ts +370 -0
  105. package/evals/math.eval.ts +94 -0
  106. package/evals/run-evals.ts +166 -0
  107. package/evals/structured-output.eval.ts +143 -0
  108. package/evals/writing.eval.ts +117 -0
  109. package/examples/batch-blog-posts.ts +160 -0
  110. package/package.json +59 -43
  111. package/src/ai-promise.ts +784 -0
  112. package/src/ai.ts +1183 -0
  113. package/src/batch/anthropic.ts +375 -0
  114. package/src/batch/bedrock.ts +801 -0
  115. package/src/batch/cloudflare.ts +421 -0
  116. package/src/batch/google.ts +491 -0
  117. package/src/batch/index.ts +31 -0
  118. package/src/batch/memory.ts +253 -0
  119. package/src/batch/openai.ts +557 -0
  120. package/src/batch-map.ts +534 -0
  121. package/src/batch-queue.ts +493 -0
  122. package/src/context.ts +332 -0
  123. package/src/embeddings.ts +244 -0
  124. package/src/eval/index.ts +8 -0
  125. package/src/eval/models.ts +158 -0
  126. package/src/eval/runner.ts +217 -0
  127. package/src/generate.ts +245 -0
  128. package/src/index.ts +154 -0
  129. package/src/primitives.ts +612 -0
  130. package/src/providers/cloudflare.ts +15 -0
  131. package/src/providers/index.ts +14 -0
  132. package/src/schema.ts +147 -0
  133. package/src/template.ts +209 -0
  134. package/src/types.ts +540 -0
  135. package/test/README.md +105 -0
  136. package/test/ai-proxy.test.ts +192 -0
  137. package/test/async-iterators.test.ts +327 -0
  138. package/test/batch-background.test.ts +482 -0
  139. package/test/batch-blog-posts.test.ts +387 -0
  140. package/test/blog-generation.test.ts +510 -0
  141. package/test/browse-read.test.ts +611 -0
  142. package/test/core-functions.test.ts +694 -0
  143. package/test/decide.test.ts +393 -0
  144. package/test/define.test.ts +274 -0
  145. package/test/e2e-bedrock-manual.ts +163 -0
  146. package/test/e2e-bedrock.test.ts +191 -0
  147. package/test/e2e-flex-gateway.ts +157 -0
  148. package/test/e2e-flex-manual.ts +183 -0
  149. package/test/e2e-flex.test.ts +209 -0
  150. package/test/e2e-google-manual.ts +178 -0
  151. package/test/e2e-google.test.ts +216 -0
  152. package/test/embeddings.test.ts +284 -0
  153. package/test/evals/define-function.eval.test.ts +379 -0
  154. package/test/evals/primitives.eval.test.ts +384 -0
  155. package/test/function-types.test.ts +492 -0
  156. package/test/generate-core.test.ts +319 -0
  157. package/test/generate.test.ts +163 -0
  158. package/test/implicit-batch.test.ts +422 -0
  159. package/test/schema.test.ts +109 -0
  160. package/test/tagged-templates.test.ts +302 -0
  161. package/tsconfig.json +8 -6
  162. package/vitest.config.ts +42 -0
  163. package/LICENSE +0 -21
  164. package/db/cache.ts +0 -6
  165. package/db/mongo.ts +0 -75
  166. package/dist/mjs/db/cache.d.ts +0 -1
  167. package/dist/mjs/db/cache.js +0 -5
  168. package/dist/mjs/db/mongo.d.ts +0 -31
  169. package/dist/mjs/db/mongo.js +0 -48
  170. package/dist/mjs/examples/data.d.ts +0 -1105
  171. package/dist/mjs/examples/data.js +0 -1105
  172. package/dist/mjs/functions/ai.d.ts +0 -20
  173. package/dist/mjs/functions/ai.js +0 -83
  174. package/dist/mjs/functions/ai.test.d.ts +0 -1
  175. package/dist/mjs/functions/ai.test.js +0 -29
  176. package/dist/mjs/functions/gpt.d.ts +0 -4
  177. package/dist/mjs/functions/gpt.js +0 -10
  178. package/dist/mjs/functions/list.d.ts +0 -7
  179. package/dist/mjs/functions/list.js +0 -72
  180. package/dist/mjs/index.d.ts +0 -3
  181. package/dist/mjs/index.js +0 -3
  182. package/dist/mjs/queue/kafka.d.ts +0 -0
  183. package/dist/mjs/queue/kafka.js +0 -1
  184. package/dist/mjs/queue/memory.d.ts +0 -0
  185. package/dist/mjs/queue/memory.js +0 -1
  186. package/dist/mjs/queue/mongo.d.ts +0 -30
  187. package/dist/mjs/queue/mongo.js +0 -42
  188. package/dist/mjs/streams/kafka.d.ts +0 -0
  189. package/dist/mjs/streams/kafka.js +0 -1
  190. package/dist/mjs/streams/memory.d.ts +0 -0
  191. package/dist/mjs/streams/memory.js +0 -1
  192. package/dist/mjs/streams/mongo.d.ts +0 -0
  193. package/dist/mjs/streams/mongo.js +0 -1
  194. package/dist/mjs/streams/types.d.ts +0 -0
  195. package/dist/mjs/streams/types.js +0 -1
  196. package/dist/mjs/types.d.ts +0 -11
  197. package/dist/mjs/types.js +0 -1
  198. package/dist/mjs/utils/completion.d.ts +0 -9
  199. package/dist/mjs/utils/completion.js +0 -20
  200. package/dist/mjs/utils/schema.d.ts +0 -10
  201. package/dist/mjs/utils/schema.js +0 -72
  202. package/dist/mjs/utils/schema.test.d.ts +0 -1
  203. package/dist/mjs/utils/schema.test.js +0 -60
  204. package/dist/mjs/utils/state.d.ts +0 -1
  205. package/dist/mjs/utils/state.js +0 -19
  206. package/examples/data.ts +0 -1105
  207. package/fixup +0 -11
  208. package/functions/ai.test.ts +0 -41
  209. package/functions/ai.ts +0 -115
  210. package/functions/gpt.ts +0 -12
  211. package/functions/list.ts +0 -84
  212. package/index.ts +0 -3
  213. package/queue/kafka.ts +0 -0
  214. package/queue/memory.ts +0 -0
  215. package/queue/mongo.ts +0 -88
  216. package/streams/kafka.ts +0 -0
  217. package/streams/memory.ts +0 -0
  218. package/streams/mongo.ts +0 -0
  219. package/streams/types.ts +0 -0
  220. package/tsconfig-backup.json +0 -105
  221. package/tsconfig-base.json +0 -26
  222. package/tsconfig-cjs.json +0 -8
  223. package/types.ts +0 -12
  224. package/utils/completion.ts +0 -28
  225. package/utils/schema.test.ts +0 -69
  226. package/utils/schema.ts +0 -74
  227. package/utils/state.ts +0 -23
@@ -0,0 +1,557 @@
1
+ /**
2
+ * OpenAI Batch API Adapter
3
+ *
4
+ * Implements batch processing using OpenAI's Batch API:
5
+ * - 50% cost discount
6
+ * - 24-hour turnaround
7
+ * - Up to 50,000 requests per batch
8
+ *
9
+ * @see https://platform.openai.com/docs/guides/batch
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+
14
+ import {
15
+ registerBatchAdapter,
16
+ registerFlexAdapter,
17
+ type BatchAdapter,
18
+ type FlexAdapter,
19
+ type BatchItem,
20
+ type BatchJob,
21
+ type BatchQueueOptions,
22
+ type BatchResult,
23
+ type BatchSubmitResult,
24
+ type BatchStatus,
25
+ } from '../batch-queue.js'
26
+ import { schema as convertSchema, type SimpleSchema } from '../schema.js'
27
+
28
+ // ============================================================================
29
+ // Types
30
+ // ============================================================================
31
+
32
+ interface OpenAIBatchRequest {
33
+ custom_id: string
34
+ method: 'POST'
35
+ url: '/v1/chat/completions'
36
+ body: {
37
+ model: string
38
+ messages: Array<{ role: string; content: string }>
39
+ response_format?: { type: 'json_schema'; json_schema: { name: string; schema: unknown } }
40
+ max_tokens?: number
41
+ temperature?: number
42
+ }
43
+ }
44
+
45
+ interface OpenAIBatchResponse {
46
+ id: string
47
+ custom_id: string
48
+ response: {
49
+ status_code: number
50
+ body: {
51
+ id: string
52
+ choices: Array<{
53
+ message: {
54
+ content: string
55
+ }
56
+ }>
57
+ usage: {
58
+ prompt_tokens: number
59
+ completion_tokens: number
60
+ total_tokens: number
61
+ }
62
+ }
63
+ } | null
64
+ error: {
65
+ code: string
66
+ message: string
67
+ } | null
68
+ }
69
+
70
+ interface OpenAIBatch {
71
+ id: string
72
+ object: 'batch'
73
+ endpoint: string
74
+ errors: null | { object: string; data: Array<{ code: string; message: string; line: number }> }
75
+ input_file_id: string
76
+ completion_window: string
77
+ status: string
78
+ output_file_id: string | null
79
+ error_file_id: string | null
80
+ created_at: number
81
+ in_progress_at: number | null
82
+ expires_at: number | null
83
+ finalizing_at: number | null
84
+ completed_at: number | null
85
+ failed_at: number | null
86
+ expired_at: number | null
87
+ cancelling_at: number | null
88
+ cancelled_at: number | null
89
+ request_counts: {
90
+ total: number
91
+ completed: number
92
+ failed: number
93
+ }
94
+ metadata: Record<string, string> | null
95
+ }
96
+
97
+ // ============================================================================
98
+ // OpenAI Client
99
+ // ============================================================================
100
+
101
+ let openaiApiKey: string | undefined
102
+ let openaiBaseUrl = 'https://api.openai.com/v1'
103
+
104
+ /**
105
+ * Configure the OpenAI client
106
+ */
107
+ export function configureOpenAI(options: { apiKey?: string; baseUrl?: string }): void {
108
+ if (options.apiKey) openaiApiKey = options.apiKey
109
+ if (options.baseUrl) openaiBaseUrl = options.baseUrl
110
+ }
111
+
112
+ function getApiKey(): string {
113
+ const key = openaiApiKey || process.env.OPENAI_API_KEY
114
+ if (!key) {
115
+ throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY or call configureOpenAI()')
116
+ }
117
+ return key
118
+ }
119
+
120
+ async function openaiRequest<T>(
121
+ method: 'GET' | 'POST',
122
+ path: string,
123
+ body?: unknown
124
+ ): Promise<T> {
125
+ const url = `${openaiBaseUrl}${path}`
126
+ const response = await fetch(url, {
127
+ method,
128
+ headers: {
129
+ Authorization: `Bearer ${getApiKey()}`,
130
+ 'Content-Type': 'application/json',
131
+ },
132
+ body: body ? JSON.stringify(body) : undefined,
133
+ })
134
+
135
+ if (!response.ok) {
136
+ const error = await response.text()
137
+ throw new Error(`OpenAI API error: ${response.status} ${error}`)
138
+ }
139
+
140
+ return response.json()
141
+ }
142
+
143
+ async function uploadFile(content: string, purpose: string): Promise<{ id: string }> {
144
+ const formData = new FormData()
145
+ formData.append('purpose', purpose)
146
+ formData.append('file', new Blob([content], { type: 'application/jsonl' }), 'batch.jsonl')
147
+
148
+ const response = await fetch(`${openaiBaseUrl}/files`, {
149
+ method: 'POST',
150
+ headers: {
151
+ Authorization: `Bearer ${getApiKey()}`,
152
+ },
153
+ body: formData,
154
+ })
155
+
156
+ if (!response.ok) {
157
+ const error = await response.text()
158
+ throw new Error(`OpenAI file upload error: ${response.status} ${error}`)
159
+ }
160
+
161
+ return response.json()
162
+ }
163
+
164
+ async function downloadFile(fileId: string): Promise<string> {
165
+ const response = await fetch(`${openaiBaseUrl}/files/${fileId}/content`, {
166
+ headers: {
167
+ Authorization: `Bearer ${getApiKey()}`,
168
+ },
169
+ })
170
+
171
+ if (!response.ok) {
172
+ const error = await response.text()
173
+ throw new Error(`OpenAI file download error: ${response.status} ${error}`)
174
+ }
175
+
176
+ return response.text()
177
+ }
178
+
179
+ // ============================================================================
180
+ // Status Mapping
181
+ // ============================================================================
182
+
183
+ function mapStatus(status: string): BatchStatus {
184
+ const statusMap: Record<string, BatchStatus> = {
185
+ validating: 'validating',
186
+ in_progress: 'in_progress',
187
+ finalizing: 'finalizing',
188
+ completed: 'completed',
189
+ failed: 'failed',
190
+ expired: 'expired',
191
+ cancelling: 'cancelling',
192
+ cancelled: 'cancelled',
193
+ }
194
+ return statusMap[status] || 'pending'
195
+ }
196
+
197
+ // ============================================================================
198
+ // OpenAI Batch Adapter
199
+ // ============================================================================
200
+
201
+ const openaiAdapter: BatchAdapter = {
202
+ async submit(items: BatchItem[], options: BatchQueueOptions): Promise<BatchSubmitResult> {
203
+ const model = options.model || 'gpt-4o'
204
+
205
+ // Build JSONL content
206
+ const requests: OpenAIBatchRequest[] = items.map((item) => {
207
+ const request: OpenAIBatchRequest = {
208
+ custom_id: item.id,
209
+ method: 'POST',
210
+ url: '/v1/chat/completions',
211
+ body: {
212
+ model,
213
+ messages: [
214
+ ...(item.options?.system ? [{ role: 'system', content: item.options.system }] : []),
215
+ { role: 'user', content: item.prompt },
216
+ ],
217
+ max_tokens: item.options?.maxTokens,
218
+ temperature: item.options?.temperature,
219
+ },
220
+ }
221
+
222
+ // Add JSON schema if provided
223
+ if (item.schema) {
224
+ const zodSchema = convertSchema(item.schema)
225
+ // Convert Zod to JSON Schema (simplified - you'd want a proper converter)
226
+ request.body.response_format = {
227
+ type: 'json_schema',
228
+ json_schema: {
229
+ name: 'response',
230
+ schema: zodToJsonSchema(zodSchema),
231
+ },
232
+ }
233
+ }
234
+
235
+ return request
236
+ })
237
+
238
+ const jsonlContent = requests.map((r) => JSON.stringify(r)).join('\n')
239
+
240
+ // Upload the input file
241
+ const inputFile = await uploadFile(jsonlContent, 'batch')
242
+
243
+ // Create the batch
244
+ const batch = await openaiRequest<OpenAIBatch>('POST', '/batches', {
245
+ input_file_id: inputFile.id,
246
+ endpoint: '/v1/chat/completions',
247
+ completion_window: '24h',
248
+ metadata: options.metadata,
249
+ })
250
+
251
+ const job: BatchJob = {
252
+ id: batch.id,
253
+ provider: 'openai',
254
+ status: mapStatus(batch.status),
255
+ totalItems: items.length,
256
+ completedItems: 0,
257
+ failedItems: 0,
258
+ createdAt: new Date(batch.created_at * 1000),
259
+ expiresAt: batch.expires_at ? new Date(batch.expires_at * 1000) : undefined,
260
+ webhookUrl: options.webhookUrl,
261
+ inputFileId: batch.input_file_id,
262
+ }
263
+
264
+ // Create completion promise
265
+ const completion = this.waitForCompletion(batch.id)
266
+
267
+ return { job, completion }
268
+ },
269
+
270
+ async getStatus(batchId: string): Promise<BatchJob> {
271
+ const batch = await openaiRequest<OpenAIBatch>('GET', `/batches/${batchId}`)
272
+
273
+ return {
274
+ id: batch.id,
275
+ provider: 'openai',
276
+ status: mapStatus(batch.status),
277
+ totalItems: batch.request_counts.total,
278
+ completedItems: batch.request_counts.completed,
279
+ failedItems: batch.request_counts.failed,
280
+ createdAt: new Date(batch.created_at * 1000),
281
+ startedAt: batch.in_progress_at ? new Date(batch.in_progress_at * 1000) : undefined,
282
+ completedAt: batch.completed_at ? new Date(batch.completed_at * 1000) : undefined,
283
+ expiresAt: batch.expires_at ? new Date(batch.expires_at * 1000) : undefined,
284
+ inputFileId: batch.input_file_id,
285
+ outputFileId: batch.output_file_id || undefined,
286
+ errorFileId: batch.error_file_id || undefined,
287
+ }
288
+ },
289
+
290
+ async cancel(batchId: string): Promise<void> {
291
+ await openaiRequest('POST', `/batches/${batchId}/cancel`)
292
+ },
293
+
294
+ async getResults(batchId: string): Promise<BatchResult[]> {
295
+ const status = await this.getStatus(batchId)
296
+
297
+ if (status.status !== 'completed' && status.status !== 'failed') {
298
+ throw new Error(`Batch not complete. Status: ${status.status}`)
299
+ }
300
+
301
+ const results: BatchResult[] = []
302
+
303
+ // Download and parse output file
304
+ if (status.outputFileId) {
305
+ const content = await downloadFile(status.outputFileId)
306
+ const lines = content.trim().split('\n')
307
+
308
+ for (const line of lines) {
309
+ const response: OpenAIBatchResponse = JSON.parse(line)
310
+
311
+ if (response.error) {
312
+ results.push({
313
+ id: response.custom_id,
314
+ customId: response.custom_id,
315
+ status: 'failed',
316
+ error: response.error.message,
317
+ })
318
+ } else if (response.response) {
319
+ const content = response.response.body.choices[0]?.message?.content
320
+ let result: unknown = content
321
+
322
+ // Try to parse JSON if it looks like JSON
323
+ if (content?.startsWith('{') || content?.startsWith('[')) {
324
+ try {
325
+ result = JSON.parse(content)
326
+ } catch {
327
+ // Keep as string
328
+ }
329
+ }
330
+
331
+ results.push({
332
+ id: response.custom_id,
333
+ customId: response.custom_id,
334
+ status: 'completed',
335
+ result,
336
+ usage: {
337
+ promptTokens: response.response.body.usage.prompt_tokens,
338
+ completionTokens: response.response.body.usage.completion_tokens,
339
+ totalTokens: response.response.body.usage.total_tokens,
340
+ },
341
+ })
342
+ }
343
+ }
344
+ }
345
+
346
+ // Download and parse error file
347
+ if (status.errorFileId) {
348
+ const content = await downloadFile(status.errorFileId)
349
+ const lines = content.trim().split('\n')
350
+
351
+ for (const line of lines) {
352
+ const response: OpenAIBatchResponse = JSON.parse(line)
353
+ results.push({
354
+ id: response.custom_id,
355
+ customId: response.custom_id,
356
+ status: 'failed',
357
+ error: response.error?.message || 'Unknown error',
358
+ })
359
+ }
360
+ }
361
+
362
+ return results
363
+ },
364
+
365
+ async waitForCompletion(batchId: string, pollInterval = 5000): Promise<BatchResult[]> {
366
+ while (true) {
367
+ const status = await this.getStatus(batchId)
368
+
369
+ if (status.status === 'completed' || status.status === 'failed') {
370
+ return this.getResults(batchId)
371
+ }
372
+
373
+ if (status.status === 'cancelled' || status.status === 'expired') {
374
+ throw new Error(`Batch ${status.status}`)
375
+ }
376
+
377
+ await new Promise((resolve) => setTimeout(resolve, pollInterval))
378
+ }
379
+ },
380
+ }
381
+
382
+ // ============================================================================
383
+ // Helpers
384
+ // ============================================================================
385
+
386
+ /**
387
+ * Simple Zod to JSON Schema converter
388
+ * In production, use a proper library like zod-to-json-schema
389
+ */
390
+ function zodToJsonSchema(zodSchema: unknown): Record<string, unknown> {
391
+ // This is a simplified converter - in production use zod-to-json-schema
392
+ const schema = zodSchema as { _def?: { typeName?: string; shape?: unknown } }
393
+
394
+ if (!schema._def) {
395
+ return { type: 'object' }
396
+ }
397
+
398
+ const typeName = schema._def.typeName
399
+
400
+ switch (typeName) {
401
+ case 'ZodString':
402
+ return { type: 'string' }
403
+ case 'ZodNumber':
404
+ return { type: 'number' }
405
+ case 'ZodBoolean':
406
+ return { type: 'boolean' }
407
+ case 'ZodArray':
408
+ return { type: 'array', items: zodToJsonSchema((schema._def as any).type) }
409
+ case 'ZodObject': {
410
+ const shape = (schema._def as any).shape()
411
+ const properties: Record<string, unknown> = {}
412
+ for (const [key, value] of Object.entries(shape)) {
413
+ properties[key] = zodToJsonSchema(value)
414
+ }
415
+ return { type: 'object', properties, required: Object.keys(properties) }
416
+ }
417
+ default:
418
+ return { type: 'object' }
419
+ }
420
+ }
421
+
422
+ // ============================================================================
423
+ // Register Adapter
424
+ // ============================================================================
425
+
426
+ // ============================================================================
427
+ // OpenAI Flex Adapter
428
+ // ============================================================================
429
+
430
+ /**
431
+ * OpenAI Flex Adapter
432
+ *
433
+ * Flex processing uses concurrent requests with a service tier that provides
434
+ * ~50% discount similar to batch, but with much faster turnaround (minutes vs 24hr).
435
+ *
436
+ * This is ideal for 5-500 items where you need results quickly but still want
437
+ * cost savings.
438
+ *
439
+ * Note: As of 2024, OpenAI doesn't have an official "flex" tier API.
440
+ * This adapter implements concurrent processing as a middle ground.
441
+ * When OpenAI adds official flex support, this can be updated.
442
+ */
443
+ const openaiFlexAdapter: FlexAdapter = {
444
+ async submitFlex(items: BatchItem[], options: { model?: string }): Promise<BatchResult[]> {
445
+ const model = options.model || 'gpt-4o'
446
+ const CONCURRENCY = 10 // Higher concurrency for flex tier
447
+
448
+ const results: BatchResult[] = []
449
+
450
+ // Process items concurrently in batches
451
+ for (let i = 0; i < items.length; i += CONCURRENCY) {
452
+ const batch = items.slice(i, i + CONCURRENCY)
453
+
454
+ const batchResults = await Promise.all(
455
+ batch.map(async (item) => {
456
+ try {
457
+ return await processOpenAIItem(item, model)
458
+ } catch (error) {
459
+ return {
460
+ id: item.id,
461
+ customId: item.id,
462
+ status: 'failed' as const,
463
+ error: error instanceof Error ? error.message : 'Unknown error',
464
+ }
465
+ }
466
+ })
467
+ )
468
+
469
+ results.push(...batchResults)
470
+ }
471
+
472
+ return results
473
+ },
474
+ }
475
+
476
+ /**
477
+ * Process a single item via OpenAI Chat Completions API
478
+ */
479
+ async function processOpenAIItem(item: BatchItem, model: string): Promise<BatchResult> {
480
+ const messages: Array<{ role: string; content: string }> = []
481
+
482
+ if (item.options?.system) {
483
+ messages.push({ role: 'system', content: item.options.system })
484
+ }
485
+ messages.push({ role: 'user', content: item.prompt })
486
+
487
+ const body: Record<string, unknown> = {
488
+ model,
489
+ messages,
490
+ max_tokens: item.options?.maxTokens,
491
+ temperature: item.options?.temperature,
492
+ }
493
+
494
+ // Add JSON schema if provided
495
+ if (item.schema) {
496
+ const zodSchema = convertSchema(item.schema)
497
+ body.response_format = {
498
+ type: 'json_schema',
499
+ json_schema: {
500
+ name: 'response',
501
+ schema: zodToJsonSchema(zodSchema),
502
+ },
503
+ }
504
+ }
505
+
506
+ const response = await fetch(`${openaiBaseUrl}/chat/completions`, {
507
+ method: 'POST',
508
+ headers: {
509
+ Authorization: `Bearer ${getApiKey()}`,
510
+ 'Content-Type': 'application/json',
511
+ },
512
+ body: JSON.stringify(body),
513
+ })
514
+
515
+ if (!response.ok) {
516
+ const error = await response.text()
517
+ throw new Error(`OpenAI API error: ${response.status} ${error}`)
518
+ }
519
+
520
+ const data = (await response.json()) as {
521
+ choices: Array<{ message: { content: string } }>
522
+ usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number }
523
+ }
524
+
525
+ const content = data.choices[0]?.message?.content
526
+ let result: unknown = content
527
+
528
+ // Try to parse JSON if schema was provided or content looks like JSON
529
+ if (content && (item.schema || content.startsWith('{') || content.startsWith('['))) {
530
+ try {
531
+ result = JSON.parse(content)
532
+ } catch {
533
+ // Keep as string
534
+ }
535
+ }
536
+
537
+ return {
538
+ id: item.id,
539
+ customId: item.id,
540
+ status: 'completed',
541
+ result,
542
+ usage: {
543
+ promptTokens: data.usage.prompt_tokens,
544
+ completionTokens: data.usage.completion_tokens,
545
+ totalTokens: data.usage.total_tokens,
546
+ },
547
+ }
548
+ }
549
+
550
+ // ============================================================================
551
+ // Register Adapters
552
+ // ============================================================================
553
+
554
+ registerBatchAdapter('openai', openaiAdapter)
555
+ registerFlexAdapter('openai', openaiFlexAdapter)
556
+
557
+ export { openaiAdapter, openaiFlexAdapter }