ai-functions 2.1.3 → 2.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 (284) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +90 -1
  3. package/README.md +38 -0
  4. package/dist/ai-promise.d.ts +3 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +135 -64
  7. package/dist/ai-promise.js.map +1 -1
  8. package/dist/ai-schemas.d.ts +56 -0
  9. package/dist/ai-schemas.d.ts.map +1 -0
  10. package/dist/ai-schemas.js +53 -0
  11. package/dist/ai-schemas.js.map +1 -0
  12. package/dist/ai.d.ts +16 -242
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +51 -858
  15. package/dist/ai.js.map +1 -1
  16. package/dist/batch/anthropic.d.ts +6 -4
  17. package/dist/batch/anthropic.d.ts.map +1 -1
  18. package/dist/batch/anthropic.js +83 -145
  19. package/dist/batch/anthropic.js.map +1 -1
  20. package/dist/batch/bedrock.d.ts +8 -30
  21. package/dist/batch/bedrock.d.ts.map +1 -1
  22. package/dist/batch/bedrock.js +155 -338
  23. package/dist/batch/bedrock.js.map +1 -1
  24. package/dist/batch/cloudflare.d.ts +8 -20
  25. package/dist/batch/cloudflare.d.ts.map +1 -1
  26. package/dist/batch/cloudflare.js +68 -189
  27. package/dist/batch/cloudflare.js.map +1 -1
  28. package/dist/batch/google.d.ts +6 -20
  29. package/dist/batch/google.d.ts.map +1 -1
  30. package/dist/batch/google.js +70 -238
  31. package/dist/batch/google.js.map +1 -1
  32. package/dist/batch/index.d.ts +4 -1
  33. package/dist/batch/index.d.ts.map +1 -1
  34. package/dist/batch/index.js +4 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/batch/memory.d.ts +1 -1
  37. package/dist/batch/memory.d.ts.map +1 -1
  38. package/dist/batch/memory.js +14 -10
  39. package/dist/batch/memory.js.map +1 -1
  40. package/dist/batch/openai.d.ts +11 -14
  41. package/dist/batch/openai.d.ts.map +1 -1
  42. package/dist/batch/openai.js +52 -156
  43. package/dist/batch/openai.js.map +1 -1
  44. package/dist/batch/provider.d.ts +111 -0
  45. package/dist/batch/provider.d.ts.map +1 -0
  46. package/dist/batch/provider.js +233 -0
  47. package/dist/batch/provider.js.map +1 -0
  48. package/dist/batch-map.d.ts.map +1 -1
  49. package/dist/batch-map.js +23 -17
  50. package/dist/batch-map.js.map +1 -1
  51. package/dist/batch-queue.d.ts +65 -0
  52. package/dist/batch-queue.d.ts.map +1 -1
  53. package/dist/batch-queue.js +169 -14
  54. package/dist/batch-queue.js.map +1 -1
  55. package/dist/budget.d.ts.map +1 -1
  56. package/dist/budget.js +27 -14
  57. package/dist/budget.js.map +1 -1
  58. package/dist/cache.d.ts +23 -0
  59. package/dist/cache.d.ts.map +1 -1
  60. package/dist/cache.js +36 -15
  61. package/dist/cache.js.map +1 -1
  62. package/dist/context.d.ts +26 -8
  63. package/dist/context.d.ts.map +1 -1
  64. package/dist/context.js +64 -62
  65. package/dist/context.js.map +1 -1
  66. package/dist/digital-objects-registry.d.ts +229 -0
  67. package/dist/digital-objects-registry.d.ts.map +1 -0
  68. package/dist/digital-objects-registry.js +617 -0
  69. package/dist/digital-objects-registry.js.map +1 -0
  70. package/dist/embeddings.d.ts +2 -2
  71. package/dist/embeddings.d.ts.map +1 -1
  72. package/dist/errors.d.ts +22 -0
  73. package/dist/errors.d.ts.map +1 -0
  74. package/dist/errors.js +35 -0
  75. package/dist/errors.js.map +1 -0
  76. package/dist/eval/runner.d.ts +8 -0
  77. package/dist/eval/runner.d.ts.map +1 -1
  78. package/dist/eval/runner.js +41 -35
  79. package/dist/eval/runner.js.map +1 -1
  80. package/dist/eval-log/in-memory.d.ts +34 -0
  81. package/dist/eval-log/in-memory.d.ts.map +1 -0
  82. package/dist/eval-log/in-memory.js +84 -0
  83. package/dist/eval-log/in-memory.js.map +1 -0
  84. package/dist/eval-log/index.d.ts +29 -0
  85. package/dist/eval-log/index.d.ts.map +1 -0
  86. package/dist/eval-log/index.js +39 -0
  87. package/dist/eval-log/index.js.map +1 -0
  88. package/dist/eval-log/types.d.ts +101 -0
  89. package/dist/eval-log/types.d.ts.map +1 -0
  90. package/dist/eval-log/types.js +16 -0
  91. package/dist/eval-log/types.js.map +1 -0
  92. package/dist/function-registry.d.ts +176 -0
  93. package/dist/function-registry.d.ts.map +1 -0
  94. package/dist/function-registry.js +685 -0
  95. package/dist/function-registry.js.map +1 -0
  96. package/dist/generate.d.ts +9 -3
  97. package/dist/generate.d.ts.map +1 -1
  98. package/dist/generate.js +18 -18
  99. package/dist/generate.js.map +1 -1
  100. package/dist/index.d.ts +18 -11
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +35 -18
  103. package/dist/index.js.map +1 -1
  104. package/dist/logger.d.ts +118 -0
  105. package/dist/logger.d.ts.map +1 -0
  106. package/dist/logger.js +187 -0
  107. package/dist/logger.js.map +1 -0
  108. package/dist/middleware/budget.d.ts +84 -0
  109. package/dist/middleware/budget.d.ts.map +1 -0
  110. package/dist/middleware/budget.js +110 -0
  111. package/dist/middleware/budget.js.map +1 -0
  112. package/dist/middleware/cache.d.ts +103 -0
  113. package/dist/middleware/cache.d.ts.map +1 -0
  114. package/dist/middleware/cache.js +228 -0
  115. package/dist/middleware/cache.js.map +1 -0
  116. package/dist/middleware/embed-cache.d.ts +99 -0
  117. package/dist/middleware/embed-cache.d.ts.map +1 -0
  118. package/dist/middleware/embed-cache.js +128 -0
  119. package/dist/middleware/embed-cache.js.map +1 -0
  120. package/dist/middleware/index.d.ts +11 -0
  121. package/dist/middleware/index.d.ts.map +1 -0
  122. package/dist/middleware/index.js +11 -0
  123. package/dist/middleware/index.js.map +1 -0
  124. package/dist/middleware/trace.d.ts +103 -0
  125. package/dist/middleware/trace.d.ts.map +1 -0
  126. package/dist/middleware/trace.js +176 -0
  127. package/dist/middleware/trace.js.map +1 -0
  128. package/dist/primitives.d.ts +120 -1
  129. package/dist/primitives.d.ts.map +1 -1
  130. package/dist/primitives.js +398 -26
  131. package/dist/primitives.js.map +1 -1
  132. package/dist/retry.d.ts +66 -1
  133. package/dist/retry.d.ts.map +1 -1
  134. package/dist/retry.js +115 -8
  135. package/dist/retry.js.map +1 -1
  136. package/dist/sandbox.d.ts +36 -0
  137. package/dist/sandbox.d.ts.map +1 -0
  138. package/dist/sandbox.js +44 -0
  139. package/dist/sandbox.js.map +1 -0
  140. package/dist/schema.js +2 -2
  141. package/dist/schema.js.map +1 -1
  142. package/dist/telemetry.d.ts +128 -0
  143. package/dist/telemetry.d.ts.map +1 -0
  144. package/dist/telemetry.js +285 -0
  145. package/dist/telemetry.js.map +1 -0
  146. package/dist/template.d.ts.map +1 -1
  147. package/dist/template.js +6 -1
  148. package/dist/template.js.map +1 -1
  149. package/dist/tool-orchestration.d.ts +66 -4
  150. package/dist/tool-orchestration.d.ts.map +1 -1
  151. package/dist/tool-orchestration.js +123 -23
  152. package/dist/tool-orchestration.js.map +1 -1
  153. package/dist/type-guards.d.ts +28 -0
  154. package/dist/type-guards.d.ts.map +1 -0
  155. package/dist/type-guards.js +29 -0
  156. package/dist/type-guards.js.map +1 -0
  157. package/dist/types.d.ts +155 -19
  158. package/dist/types.d.ts.map +1 -1
  159. package/dist/types.js +36 -1
  160. package/dist/types.js.map +1 -1
  161. package/dist/wrap-for-v3.d.ts +80 -0
  162. package/dist/wrap-for-v3.d.ts.map +1 -0
  163. package/dist/wrap-for-v3.js +89 -0
  164. package/dist/wrap-for-v3.js.map +1 -0
  165. package/examples/00-quickstart.ts +232 -0
  166. package/examples/01-rag-chatbot.ts +212 -0
  167. package/examples/02-multi-agent-research.ts +290 -0
  168. package/examples/03-email-classification.ts +379 -0
  169. package/examples/04-content-moderation.ts +400 -0
  170. package/examples/05-document-extraction.ts +455 -0
  171. package/examples/06-streaming-chat-nextjs.ts +437 -0
  172. package/examples/07-cloudflare-worker.ts +483 -0
  173. package/examples/08-batch-processing.ts +491 -0
  174. package/examples/09-budget-constrained.ts +527 -0
  175. package/examples/10-tool-orchestration.ts +565 -0
  176. package/examples/11-retry-resilience.ts +403 -0
  177. package/examples/12-caching-strategies.ts +422 -0
  178. package/examples/README.md +145 -0
  179. package/package.json +29 -25
  180. package/src/ai-promise.ts +226 -140
  181. package/src/ai-schemas.ts +122 -0
  182. package/src/ai.ts +71 -1176
  183. package/src/batch/anthropic.ts +96 -161
  184. package/src/batch/bedrock.ts +203 -454
  185. package/src/batch/cloudflare.ts +99 -282
  186. package/src/batch/google.ts +91 -297
  187. package/src/batch/index.ts +4 -1
  188. package/src/batch/memory.ts +15 -10
  189. package/src/batch/openai.ts +65 -193
  190. package/src/batch/provider.ts +336 -0
  191. package/src/batch-map.ts +29 -24
  192. package/src/batch-queue.ts +200 -11
  193. package/src/budget.ts +31 -18
  194. package/src/cache.ts +45 -17
  195. package/src/context.ts +106 -77
  196. package/src/digital-objects-registry.ts +750 -0
  197. package/src/errors.ts +37 -0
  198. package/src/eval/runner.ts +60 -36
  199. package/src/eval-log/in-memory.ts +90 -0
  200. package/src/eval-log/index.ts +46 -0
  201. package/src/eval-log/types.ts +110 -0
  202. package/src/function-registry.ts +874 -0
  203. package/src/generate.ts +33 -28
  204. package/src/index.ts +122 -21
  205. package/src/logger.ts +232 -0
  206. package/src/middleware/budget.ts +171 -0
  207. package/src/middleware/cache.ts +299 -0
  208. package/src/middleware/embed-cache.ts +195 -0
  209. package/src/middleware/index.ts +23 -0
  210. package/src/middleware/trace.ts +248 -0
  211. package/src/primitives.ts +589 -62
  212. package/src/retry.ts +144 -18
  213. package/src/sandbox.ts +52 -0
  214. package/src/schema.ts +8 -8
  215. package/src/telemetry.ts +403 -0
  216. package/src/template.ts +8 -4
  217. package/src/tool-orchestration.ts +213 -48
  218. package/src/type-guards.ts +31 -0
  219. package/src/types.ts +186 -27
  220. package/src/wrap-for-v3.ts +105 -0
  221. package/test/ai-promise.test.ts +1080 -0
  222. package/test/ai-proxy.test.ts +1 -1
  223. package/test/batch-autosubmit-errors.test.ts +49 -37
  224. package/test/batch-blog-posts.test.ts +87 -129
  225. package/test/core-functions.test.ts +183 -579
  226. package/test/decide.test.ts +154 -322
  227. package/test/define.test.ts +211 -8
  228. package/test/digital-objects-registry.test.ts +760 -0
  229. package/test/embedding-cache-middleware.test.ts +140 -0
  230. package/test/fill-template.test.ts +89 -0
  231. package/test/generate-core.test.ts +140 -229
  232. package/test/implicit-batch.test.ts +22 -65
  233. package/test/retry-policy-integration.test.ts +117 -0
  234. package/test/sandbox-execution.test.ts +155 -0
  235. package/test/schema.test.ts +55 -19
  236. package/test/template.test.ts +1164 -0
  237. package/test/tool-orchestration.test.ts +270 -0
  238. package/test/wrap-for-v3.test.ts +612 -0
  239. package/vitest.config.js +6 -0
  240. package/vitest.config.ts +20 -0
  241. package/LICENSE +0 -21
  242. package/dist/rpc/auth.d.ts +0 -69
  243. package/dist/rpc/auth.d.ts.map +0 -1
  244. package/dist/rpc/auth.js +0 -136
  245. package/dist/rpc/auth.js.map +0 -1
  246. package/dist/rpc/client.d.ts +0 -62
  247. package/dist/rpc/client.d.ts.map +0 -1
  248. package/dist/rpc/client.js +0 -103
  249. package/dist/rpc/client.js.map +0 -1
  250. package/dist/rpc/deferred.d.ts +0 -60
  251. package/dist/rpc/deferred.d.ts.map +0 -1
  252. package/dist/rpc/deferred.js +0 -96
  253. package/dist/rpc/deferred.js.map +0 -1
  254. package/dist/rpc/index.d.ts +0 -22
  255. package/dist/rpc/index.d.ts.map +0 -1
  256. package/dist/rpc/index.js +0 -38
  257. package/dist/rpc/index.js.map +0 -1
  258. package/dist/rpc/local.d.ts +0 -42
  259. package/dist/rpc/local.d.ts.map +0 -1
  260. package/dist/rpc/local.js +0 -50
  261. package/dist/rpc/local.js.map +0 -1
  262. package/dist/rpc/server.d.ts +0 -165
  263. package/dist/rpc/server.d.ts.map +0 -1
  264. package/dist/rpc/server.js +0 -405
  265. package/dist/rpc/server.js.map +0 -1
  266. package/dist/rpc/session.d.ts +0 -32
  267. package/dist/rpc/session.d.ts.map +0 -1
  268. package/dist/rpc/session.js +0 -43
  269. package/dist/rpc/session.js.map +0 -1
  270. package/dist/rpc/transport.d.ts +0 -306
  271. package/dist/rpc/transport.d.ts.map +0 -1
  272. package/dist/rpc/transport.js +0 -731
  273. package/dist/rpc/transport.js.map +0 -1
  274. package/src/batch/anthropic.js +0 -256
  275. package/src/batch/bedrock.js +0 -584
  276. package/src/batch/cloudflare.js +0 -287
  277. package/src/batch/google.js +0 -359
  278. package/src/batch/index.js +0 -30
  279. package/src/batch/memory.js +0 -187
  280. package/src/batch/openai.js +0 -402
  281. package/src/eval/index.js +0 -7
  282. package/src/eval/models.js +0 -119
  283. package/src/eval/runner.js +0 -147
  284. package/test/schema.test.js +0 -96
@@ -6,27 +6,37 @@
6
6
  * - 24-hour turnaround
7
7
  * - Up to 50,000 requests per batch
8
8
  *
9
+ * Plus a flex adapter that processes items concurrently for faster turnaround
10
+ * at a similar discount.
11
+ *
12
+ * This file is a small adapter on top of the BatchProvider port (`./provider.js`).
13
+ *
9
14
  * @see https://platform.openai.com/docs/guides/batch
10
15
  *
11
16
  * @packageDocumentation
12
17
  */
13
18
 
19
+ import { schema as convertSchema } from '../schema.js'
14
20
  import {
21
+ failedResult,
22
+ pollUntilComplete,
23
+ processConcurrently,
15
24
  registerBatchAdapter,
16
25
  registerFlexAdapter,
26
+ tryParseJson,
27
+ zodToJsonSchema,
17
28
  type BatchAdapter,
18
- type FlexAdapter,
19
29
  type BatchItem,
20
30
  type BatchJob,
21
31
  type BatchQueueOptions,
22
32
  type BatchResult,
23
- type BatchSubmitResult,
24
33
  type BatchStatus,
25
- } from '../batch-queue.js'
26
- import { schema as convertSchema, type SimpleSchema } from '../schema.js'
34
+ type BatchSubmitResult,
35
+ type FlexAdapter,
36
+ } from './provider.js'
27
37
 
28
38
  // ============================================================================
29
- // Types
39
+ // Provider-specific types
30
40
  // ============================================================================
31
41
 
32
42
  interface OpenAIBatchRequest {
@@ -49,11 +59,7 @@ interface OpenAIBatchResponse {
49
59
  status_code: number
50
60
  body: {
51
61
  id: string
52
- choices: Array<{
53
- message: {
54
- content: string
55
- }
56
- }>
62
+ choices: Array<{ message: { content: string } }>
57
63
  usage: {
58
64
  prompt_tokens: number
59
65
  completion_tokens: number
@@ -95,41 +101,34 @@ interface OpenAIBatch {
95
101
  }
96
102
 
97
103
  // ============================================================================
98
- // OpenAI Client
104
+ // OpenAI client
99
105
  // ============================================================================
100
106
 
101
107
  let openaiApiKey: string | undefined
102
108
  let openaiBaseUrl = 'https://api.openai.com/v1'
103
109
 
104
- /**
105
- * Configure the OpenAI client
106
- */
110
+ /** Configure the OpenAI client. */
107
111
  export function configureOpenAI(options: { apiKey?: string; baseUrl?: string }): void {
108
112
  if (options.apiKey) openaiApiKey = options.apiKey
109
113
  if (options.baseUrl) openaiBaseUrl = options.baseUrl
110
114
  }
111
115
 
112
116
  function getApiKey(): string {
113
- const key = openaiApiKey || process.env.OPENAI_API_KEY
117
+ const key = openaiApiKey || process.env['OPENAI_API_KEY']
114
118
  if (!key) {
115
119
  throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY or call configureOpenAI()')
116
120
  }
117
121
  return key
118
122
  }
119
123
 
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, {
124
+ async function openaiRequest<T>(method: 'GET' | 'POST', path: string, body?: unknown): Promise<T> {
125
+ const response = await fetch(`${openaiBaseUrl}${path}`, {
127
126
  method,
128
127
  headers: {
129
128
  Authorization: `Bearer ${getApiKey()}`,
130
129
  'Content-Type': 'application/json',
131
130
  },
132
- body: body ? JSON.stringify(body) : undefined,
131
+ ...(body !== undefined && { body: JSON.stringify(body) }),
133
132
  })
134
133
 
135
134
  if (!response.ok) {
@@ -147,9 +146,7 @@ async function uploadFile(content: string, purpose: string): Promise<{ id: strin
147
146
 
148
147
  const response = await fetch(`${openaiBaseUrl}/files`, {
149
148
  method: 'POST',
150
- headers: {
151
- Authorization: `Bearer ${getApiKey()}`,
152
- },
149
+ headers: { Authorization: `Bearer ${getApiKey()}` },
153
150
  body: formData,
154
151
  })
155
152
 
@@ -163,9 +160,7 @@ async function uploadFile(content: string, purpose: string): Promise<{ id: strin
163
160
 
164
161
  async function downloadFile(fileId: string): Promise<string> {
165
162
  const response = await fetch(`${openaiBaseUrl}/files/${fileId}/content`, {
166
- headers: {
167
- Authorization: `Bearer ${getApiKey()}`,
168
- },
163
+ headers: { Authorization: `Bearer ${getApiKey()}` },
169
164
  })
170
165
 
171
166
  if (!response.ok) {
@@ -176,10 +171,6 @@ async function downloadFile(fileId: string): Promise<string> {
176
171
  return response.text()
177
172
  }
178
173
 
179
- // ============================================================================
180
- // Status Mapping
181
- // ============================================================================
182
-
183
174
  function mapStatus(status: string): BatchStatus {
184
175
  const statusMap: Record<string, BatchStatus> = {
185
176
  validating: 'validating',
@@ -195,14 +186,16 @@ function mapStatus(status: string): BatchStatus {
195
186
  }
196
187
 
197
188
  // ============================================================================
198
- // OpenAI Batch Adapter
189
+ // OpenAI batch adapter (BatchProvider port)
199
190
  // ============================================================================
200
191
 
192
+ const TERMINAL_FOR_OPENAI: ReadonlySet<BatchStatus> = new Set(['completed', 'failed'])
193
+ const THROW_FOR_OPENAI: ReadonlySet<BatchStatus> = new Set(['cancelled', 'expired'])
194
+
201
195
  const openaiAdapter: BatchAdapter = {
202
196
  async submit(items: BatchItem[], options: BatchQueueOptions): Promise<BatchSubmitResult> {
203
197
  const model = options.model || 'gpt-4o'
204
198
 
205
- // Build JSONL content
206
199
  const requests: OpenAIBatchRequest[] = items.map((item) => {
207
200
  const request: OpenAIBatchRequest = {
208
201
  custom_id: item.id,
@@ -214,21 +207,18 @@ const openaiAdapter: BatchAdapter = {
214
207
  ...(item.options?.system ? [{ role: 'system', content: item.options.system }] : []),
215
208
  { role: 'user', content: item.prompt },
216
209
  ],
217
- max_tokens: item.options?.maxTokens,
218
- temperature: item.options?.temperature,
210
+ ...(item.options?.maxTokens !== undefined && { max_tokens: item.options.maxTokens }),
211
+ ...(item.options?.temperature !== undefined && {
212
+ temperature: item.options.temperature,
213
+ }),
219
214
  },
220
215
  }
221
216
 
222
- // Add JSON schema if provided
223
217
  if (item.schema) {
224
218
  const zodSchema = convertSchema(item.schema)
225
- // Convert Zod to JSON Schema (simplified - you'd want a proper converter)
226
219
  request.body.response_format = {
227
220
  type: 'json_schema',
228
- json_schema: {
229
- name: 'response',
230
- schema: zodToJsonSchema(zodSchema),
231
- },
221
+ json_schema: { name: 'response', schema: zodToJsonSchema(zodSchema) },
232
222
  }
233
223
  }
234
224
 
@@ -236,11 +226,8 @@ const openaiAdapter: BatchAdapter = {
236
226
  })
237
227
 
238
228
  const jsonlContent = requests.map((r) => JSON.stringify(r)).join('\n')
239
-
240
- // Upload the input file
241
229
  const inputFile = await uploadFile(jsonlContent, 'batch')
242
230
 
243
- // Create the batch
244
231
  const batch = await openaiRequest<OpenAIBatch>('POST', '/batches', {
245
232
  input_file_id: inputFile.id,
246
233
  endpoint: '/v1/chat/completions',
@@ -256,20 +243,17 @@ const openaiAdapter: BatchAdapter = {
256
243
  completedItems: 0,
257
244
  failedItems: 0,
258
245
  createdAt: new Date(batch.created_at * 1000),
259
- expiresAt: batch.expires_at ? new Date(batch.expires_at * 1000) : undefined,
260
- webhookUrl: options.webhookUrl,
246
+ ...(batch.expires_at && { expiresAt: new Date(batch.expires_at * 1000) }),
247
+ ...(options.webhookUrl !== undefined && { webhookUrl: options.webhookUrl }),
261
248
  inputFileId: batch.input_file_id,
262
249
  }
263
250
 
264
- // Create completion promise
265
251
  const completion = this.waitForCompletion(batch.id)
266
-
267
252
  return { job, completion }
268
253
  },
269
254
 
270
255
  async getStatus(batchId: string): Promise<BatchJob> {
271
256
  const batch = await openaiRequest<OpenAIBatch>('GET', `/batches/${batchId}`)
272
-
273
257
  return {
274
258
  id: batch.id,
275
259
  provider: 'openai',
@@ -278,12 +262,12 @@ const openaiAdapter: BatchAdapter = {
278
262
  completedItems: batch.request_counts.completed,
279
263
  failedItems: batch.request_counts.failed,
280
264
  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,
265
+ ...(batch.in_progress_at && { startedAt: new Date(batch.in_progress_at * 1000) }),
266
+ ...(batch.completed_at && { completedAt: new Date(batch.completed_at * 1000) }),
267
+ ...(batch.expires_at && { expiresAt: new Date(batch.expires_at * 1000) }),
284
268
  inputFileId: batch.input_file_id,
285
- outputFileId: batch.output_file_id || undefined,
286
- errorFileId: batch.error_file_id || undefined,
269
+ ...(batch.output_file_id && { outputFileId: batch.output_file_id }),
270
+ ...(batch.error_file_id && { errorFileId: batch.error_file_id }),
287
271
  }
288
272
  },
289
273
 
@@ -300,11 +284,8 @@ const openaiAdapter: BatchAdapter = {
300
284
 
301
285
  const results: BatchResult[] = []
302
286
 
303
- // Download and parse output file
304
287
  if (status.outputFileId) {
305
- const content = await downloadFile(status.outputFileId)
306
- const lines = content.trim().split('\n')
307
-
288
+ const lines = (await downloadFile(status.outputFileId)).trim().split('\n')
308
289
  for (const line of lines) {
309
290
  const response: OpenAIBatchResponse = JSON.parse(line)
310
291
 
@@ -317,22 +298,11 @@ const openaiAdapter: BatchAdapter = {
317
298
  })
318
299
  } else if (response.response) {
319
300
  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
301
  results.push({
332
302
  id: response.custom_id,
333
303
  customId: response.custom_id,
334
304
  status: 'completed',
335
- result,
305
+ result: tryParseJson(content),
336
306
  usage: {
337
307
  promptTokens: response.response.body.usage.prompt_tokens,
338
308
  completionTokens: response.response.body.usage.completion_tokens,
@@ -343,11 +313,8 @@ const openaiAdapter: BatchAdapter = {
343
313
  }
344
314
  }
345
315
 
346
- // Download and parse error file
347
316
  if (status.errorFileId) {
348
- const content = await downloadFile(status.errorFileId)
349
- const lines = content.trim().split('\n')
350
-
317
+ const lines = (await downloadFile(status.errorFileId)).trim().split('\n')
351
318
  for (const line of lines) {
352
319
  const response: OpenAIBatchResponse = JSON.parse(line)
353
320
  results.push({
@@ -363,122 +330,37 @@ const openaiAdapter: BatchAdapter = {
363
330
  },
364
331
 
365
332
  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
- }
333
+ return pollUntilComplete(this, batchId, {
334
+ pollInterval,
335
+ fetchResultsOn: TERMINAL_FOR_OPENAI,
336
+ throwOn: THROW_FOR_OPENAI,
337
+ })
379
338
  },
380
339
  }
381
340
 
382
341
  // ============================================================================
383
- // Helpers
342
+ // OpenAI flex adapter (FlexAdapter port)
384
343
  // ============================================================================
385
344
 
386
345
  /**
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).
346
+ * Flex processing uses concurrent requests for faster turnaround than batch
347
+ * (minutes vs 24h) at a similar discount. Ideal for 5–500 items.
435
348
  *
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.
349
+ * As of 2026, OpenAI doesn't expose a dedicated "flex" tier API, so this
350
+ * adapter implements concurrent direct chat completions as a middle ground.
442
351
  */
443
352
  const openaiFlexAdapter: FlexAdapter = {
444
353
  async submitFlex(items: BatchItem[], options: { model?: string }): Promise<BatchResult[]> {
445
354
  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
355
+ return processConcurrently(items, (item) => processOpenAIItem(item, model), {
356
+ concurrency: 10,
357
+ })
473
358
  },
474
359
  }
475
360
 
476
- /**
477
- * Process a single item via OpenAI Chat Completions API
478
- */
361
+ /** Process a single item via OpenAI Chat Completions API. */
479
362
  async function processOpenAIItem(item: BatchItem, model: string): Promise<BatchResult> {
480
363
  const messages: Array<{ role: string; content: string }> = []
481
-
482
364
  if (item.options?.system) {
483
365
  messages.push({ role: 'system', content: item.options.system })
484
366
  }
@@ -491,15 +373,11 @@ async function processOpenAIItem(item: BatchItem, model: string): Promise<BatchR
491
373
  temperature: item.options?.temperature,
492
374
  }
493
375
 
494
- // Add JSON schema if provided
495
376
  if (item.schema) {
496
377
  const zodSchema = convertSchema(item.schema)
497
- body.response_format = {
378
+ body['response_format'] = {
498
379
  type: 'json_schema',
499
- json_schema: {
500
- name: 'response',
501
- schema: zodToJsonSchema(zodSchema),
502
- },
380
+ json_schema: { name: 'response', schema: zodToJsonSchema(zodSchema) },
503
381
  }
504
382
  }
505
383
 
@@ -523,22 +401,12 @@ async function processOpenAIItem(item: BatchItem, model: string): Promise<BatchR
523
401
  }
524
402
 
525
403
  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
404
 
537
405
  return {
538
406
  id: item.id,
539
407
  customId: item.id,
540
408
  status: 'completed',
541
- result,
409
+ result: tryParseJson(content, !!item.schema),
542
410
  usage: {
543
411
  promptTokens: data.usage.prompt_tokens,
544
412
  completionTokens: data.usage.completion_tokens,
@@ -547,8 +415,12 @@ async function processOpenAIItem(item: BatchItem, model: string): Promise<BatchR
547
415
  }
548
416
  }
549
417
 
418
+ // `failedResult` re-imported only to keep the import surface stable for tests
419
+ // that may use it. The processConcurrently helper handles failures internally.
420
+ void failedResult
421
+
550
422
  // ============================================================================
551
- // Register Adapters
423
+ // Register adapters
552
424
  // ============================================================================
553
425
 
554
426
  registerBatchAdapter('openai', openaiAdapter)