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
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { describe, it, expect, beforeEach } from 'vitest'
8
- import { ai, AI, functions, withTemplate } from '../src/index.js'
8
+ import { aiProxy as ai, AI, functions, withTemplate } from '../src/index.js'
9
9
 
10
10
  // Skip tests if no gateway configured
11
11
  const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
@@ -150,7 +150,7 @@ function createRateLimitAdapter(): BatchAdapter {
150
150
  return createFailingAdapter(
151
151
  Object.assign(new Error('Rate limit exceeded'), {
152
152
  status: 429,
153
- headers: { 'retry-after': '60' }
153
+ headers: { 'retry-after': '60' },
154
154
  })
155
155
  )
156
156
  }
@@ -181,14 +181,16 @@ describe('Batch auto-submit error handling', () => {
181
181
  const batch = createBatch({
182
182
  provider: 'openai',
183
183
  autoSubmit: true,
184
- maxItems: 3
184
+ maxItems: 3,
185
185
  })
186
186
 
187
187
  // Subscribe to error events (this is what we expect to exist)
188
188
  // This will fail because BatchQueue doesn't emit events
189
189
  if ('on' in batch) {
190
- (batch as BatchQueue & { on: (event: string, handler: (e: Error) => void) => void })
191
- .on('error', errorHandler)
190
+ ;(batch as BatchQueue & { on: (event: string, handler: (e: Error) => void) => void }).on(
191
+ 'error',
192
+ errorHandler
193
+ )
192
194
  }
193
195
 
194
196
  // Add items to trigger auto-submit
@@ -197,7 +199,7 @@ describe('Batch auto-submit error handling', () => {
197
199
  batch.add('prompt 3') // This should trigger auto-submit
198
200
 
199
201
  // Wait for async auto-submit to complete
200
- await new Promise(resolve => setTimeout(resolve, 100))
202
+ await new Promise((resolve) => setTimeout(resolve, 100))
201
203
 
202
204
  // FAILING: Currently errors are swallowed, errorHandler never called
203
205
  // The error should be propagated to the error handler
@@ -211,7 +213,7 @@ describe('Batch auto-submit error handling', () => {
211
213
  const batch = createBatch({
212
214
  provider: 'openai',
213
215
  autoSubmit: true,
214
- maxItems: 3
216
+ maxItems: 3,
215
217
  })
216
218
 
217
219
  // Get item references before auto-submit triggers
@@ -220,7 +222,7 @@ describe('Batch auto-submit error handling', () => {
220
222
  const item3 = batch.add('prompt 3') // Triggers auto-submit
221
223
 
222
224
  // Wait for async auto-submit to complete
223
- await new Promise(resolve => setTimeout(resolve, 100))
225
+ await new Promise((resolve) => setTimeout(resolve, 100))
224
226
 
225
227
  // FAILING: Items should have error status after failed auto-submit
226
228
  // Currently they remain in 'pending' status with no indication of failure
@@ -237,7 +239,7 @@ describe('Batch auto-submit error handling', () => {
237
239
  const batch = createBatch({
238
240
  provider: 'openai',
239
241
  autoSubmit: true,
240
- maxItems: 3
242
+ maxItems: 3,
241
243
  })
242
244
 
243
245
  batch.add('prompt 1')
@@ -252,7 +254,8 @@ describe('Batch auto-submit error handling', () => {
252
254
  expect('autoSubmitPromise' in batch).toBe(true)
253
255
 
254
256
  // The promise should be available for awaiting
255
- const autoSubmitPromise = (batch as BatchQueue & { autoSubmitPromise?: Promise<void> }).autoSubmitPromise
257
+ const autoSubmitPromise = (batch as BatchQueue & { autoSubmitPromise?: Promise<void> })
258
+ .autoSubmitPromise
256
259
  expect(autoSubmitPromise).toBeDefined()
257
260
 
258
261
  // Awaiting it should surface the error
@@ -267,13 +270,13 @@ describe('Batch auto-submit error handling', () => {
267
270
  const batch = createBatch({
268
271
  provider: 'openai',
269
272
  autoSubmit: true,
270
- maxItems: 2
273
+ maxItems: 2,
271
274
  })
272
275
 
273
276
  batch.add('prompt 1')
274
277
  batch.add('prompt 2') // Triggers auto-submit
275
278
 
276
- await new Promise(resolve => setTimeout(resolve, 100))
279
+ await new Promise((resolve) => setTimeout(resolve, 100))
277
280
 
278
281
  // FAILING: Rate limit error should be exposed to caller
279
282
  // Currently it's only logged to console.error
@@ -292,13 +295,13 @@ describe('Batch auto-submit error handling', () => {
292
295
  const batch = createBatch({
293
296
  provider: 'openai',
294
297
  autoSubmit: true,
295
- maxItems: 2
298
+ maxItems: 2,
296
299
  })
297
300
 
298
301
  batch.add('prompt 1')
299
302
  batch.add('prompt 2')
300
303
 
301
- await new Promise(resolve => setTimeout(resolve, 100))
304
+ await new Promise((resolve) => setTimeout(resolve, 100))
302
305
 
303
306
  // FAILING: Rate limit metadata should be accessible
304
307
  const job = batch.getJob()
@@ -315,17 +318,18 @@ describe('Batch auto-submit error handling', () => {
315
318
  const batch = createBatch({
316
319
  provider: 'openai',
317
320
  autoSubmit: true,
318
- maxItems: 2
321
+ maxItems: 2,
319
322
  })
320
323
 
321
324
  batch.add('prompt 1')
322
325
  batch.add('prompt 2')
323
326
 
324
327
  // Wait for timeout to occur
325
- await new Promise(resolve => setTimeout(resolve, 200))
328
+ await new Promise((resolve) => setTimeout(resolve, 200))
326
329
 
327
330
  // FAILING: Timeout error should be captured and accessible
328
- expect(consoleErrorSpy).toHaveBeenCalledWith(expect.any(Error))
331
+ // Logger calls with format: 'Batch auto-submit failed:', error
332
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Batch auto-submit failed:', expect.any(Error))
329
333
 
330
334
  // Items should reflect the failure
331
335
  const items = batch.getItems()
@@ -342,13 +346,13 @@ describe('Batch auto-submit error handling', () => {
342
346
  const batch = createBatch({
343
347
  provider: 'openai',
344
348
  autoSubmit: true,
345
- maxItems: 2
349
+ maxItems: 2,
346
350
  })
347
351
 
348
352
  batch.add('prompt 1')
349
353
  batch.add('prompt 2') // Triggers auto-submit (fails)
350
354
 
351
- await new Promise(resolve => setTimeout(resolve, 100))
355
+ await new Promise((resolve) => setTimeout(resolve, 100))
352
356
 
353
357
  // Replace with working adapter
354
358
  registerBatchAdapter('openai', createSuccessAdapter())
@@ -372,13 +376,13 @@ describe('Batch auto-submit error handling', () => {
372
376
  const batch = createBatch({
373
377
  provider: 'openai',
374
378
  autoSubmit: true,
375
- maxItems: 2
379
+ maxItems: 2,
376
380
  })
377
381
 
378
382
  batch.add('prompt 1')
379
383
  batch.add('prompt 2') // Triggers auto-submit (fails)
380
384
 
381
- await new Promise(resolve => setTimeout(resolve, 100))
385
+ await new Promise((resolve) => setTimeout(resolve, 100))
382
386
 
383
387
  // Replace with working adapter
384
388
  registerBatchAdapter('openai', createSuccessAdapter())
@@ -411,8 +415,8 @@ describe('Batch auto-submit error handling', () => {
411
415
  provider: 'openai',
412
416
  status: 'completed',
413
417
  totalItems: items.length,
414
- completedItems: results.filter(r => r.status === 'completed').length,
415
- failedItems: results.filter(r => r.status === 'failed').length,
418
+ completedItems: results.filter((r) => r.status === 'completed').length,
419
+ failedItems: results.filter((r) => r.status === 'failed').length,
416
420
  createdAt: new Date(),
417
421
  },
418
422
  completion: Promise.resolve(results),
@@ -444,14 +448,17 @@ describe('Batch auto-submit error handling', () => {
444
448
  const batch = createBatch({
445
449
  provider: 'openai',
446
450
  autoSubmit: true,
447
- maxItems: 4
451
+ maxItems: 4,
448
452
  })
449
453
 
450
454
  // FAILING: There should be a way to subscribe to partial failure events
451
455
  // This tests that callers can be notified when some items fail
452
456
  if ('on' in batch) {
453
- (batch as BatchQueue & { on: (event: string, handler: (results: BatchResult[]) => void) => void })
454
- .on('partial-failure', partialFailureHandler)
457
+ ;(
458
+ batch as BatchQueue & {
459
+ on: (event: string, handler: (results: BatchResult[]) => void) => void
460
+ }
461
+ ).on('partial-failure', partialFailureHandler)
455
462
  }
456
463
 
457
464
  batch.add('prompt 1')
@@ -460,13 +467,13 @@ describe('Batch auto-submit error handling', () => {
460
467
  batch.add('prompt 4') // Triggers auto-submit
461
468
 
462
469
  // Wait for auto-submit to complete
463
- await new Promise(resolve => setTimeout(resolve, 100))
470
+ await new Promise((resolve) => setTimeout(resolve, 100))
464
471
 
465
472
  // FAILING: Partial failure handler should be called with failed items
466
473
  expect(partialFailureHandler).toHaveBeenCalled()
467
474
  expect(partialFailureHandler).toHaveBeenCalledWith(
468
475
  expect.arrayContaining([
469
- expect.objectContaining({ status: 'failed', error: 'Processing failed' })
476
+ expect.objectContaining({ status: 'failed', error: 'Processing failed' }),
470
477
  ])
471
478
  )
472
479
  })
@@ -487,8 +494,8 @@ describe('Batch auto-submit error handling', () => {
487
494
  provider: 'openai',
488
495
  status: 'completed',
489
496
  totalItems: items.length,
490
- completedItems: results.filter(r => r.status === 'completed').length,
491
- failedItems: results.filter(r => r.status === 'failed').length,
497
+ completedItems: results.filter((r) => r.status === 'completed').length,
498
+ failedItems: results.filter((r) => r.status === 'failed').length,
492
499
  createdAt: new Date(),
493
500
  },
494
501
  completion: Promise.resolve(results),
@@ -519,7 +526,7 @@ describe('Batch auto-submit error handling', () => {
519
526
  const batch = createBatch({
520
527
  provider: 'openai',
521
528
  autoSubmit: true,
522
- maxItems: 4
529
+ maxItems: 4,
523
530
  })
524
531
 
525
532
  batch.add('prompt 1')
@@ -527,10 +534,12 @@ describe('Batch auto-submit error handling', () => {
527
534
  batch.add('prompt 3')
528
535
  batch.add('prompt 4') // Triggers auto-submit
529
536
 
530
- await new Promise(resolve => setTimeout(resolve, 100))
537
+ await new Promise((resolve) => setTimeout(resolve, 100))
531
538
 
532
539
  // FAILING: There should be a way to get failure summary
533
- const failedItems = (batch as BatchQueue & { getFailedItems?: () => BatchItem[] }).getFailedItems?.()
540
+ const failedItems = (
541
+ batch as BatchQueue & { getFailedItems?: () => BatchItem[] }
542
+ ).getFailedItems?.()
534
543
  expect(failedItems).toBeDefined()
535
544
  expect(failedItems?.length).toBe(2)
536
545
  })
@@ -544,16 +553,17 @@ describe('Batch auto-submit error handling', () => {
544
553
  const batch = createBatch({
545
554
  provider: 'openai',
546
555
  autoSubmit: true,
547
- maxItems: 2
556
+ maxItems: 2,
548
557
  })
549
558
 
550
559
  batch.add('prompt 1')
551
560
  batch.add('prompt 2') // Triggers auto-submit
552
561
 
553
- await new Promise(resolve => setTimeout(resolve, 100))
562
+ await new Promise((resolve) => setTimeout(resolve, 100))
554
563
 
555
564
  // This passes - errors ARE logged
556
- expect(consoleErrorSpy).toHaveBeenCalledWith(testError)
565
+ // Logger calls with format: 'Batch auto-submit failed:', error
566
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Batch auto-submit failed:', testError)
557
567
 
558
568
  // But there's no other way to access the error
559
569
  // - No error event emitted
@@ -576,7 +586,7 @@ describe('Suggested API improvements', () => {
576
586
  const batch = createBatch({
577
587
  provider: 'openai',
578
588
  autoSubmit: true,
579
- maxItems: 5
589
+ maxItems: 5,
580
590
  })
581
591
 
582
592
  // 1. Event-based error handling
@@ -585,7 +595,9 @@ describe('Suggested API improvements', () => {
585
595
 
586
596
  // 2. Promise-based error handling
587
597
  expect('awaitAutoSubmit' in batch).toBe(true)
588
- expect(typeof (batch as unknown as { awaitAutoSubmit?: unknown }).awaitAutoSubmit).toBe('function')
598
+ expect(typeof (batch as unknown as { awaitAutoSubmit?: unknown }).awaitAutoSubmit).toBe(
599
+ 'function'
600
+ )
589
601
 
590
602
  // 3. Error state inspection
591
603
  expect('submissionError' in batch).toBe(true)
@@ -15,117 +15,71 @@
15
15
  * ```
16
16
  */
17
17
 
18
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
19
- import {
20
- createBatch,
21
- withBatch,
22
- type BatchQueue,
23
- type BatchResult,
24
- } from '../src/batch-queue.js'
25
-
26
- // Import memory adapter to register it
27
- import '../src/batch/memory.js'
28
- import { configureMemoryAdapter, clearBatches } from '../src/batch/memory.js'
18
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
19
+ import { createBatch, withBatchQueue, generateObject, generateText } from '../src/index.js'
20
+ import { z } from 'zod'
29
21
 
30
- // ============================================================================
31
- // Mock Setup
32
- // ============================================================================
22
+ // Memory adapter for testing - simulates batch processing locally
23
+ // Import from .ts file for proper vite resolution
24
+ import { configureMemoryAdapter, clearBatches } from '../src/batch/memory.ts'
33
25
 
34
- // Mock the generate functions
35
- vi.mock('../src/generate.js', () => ({
36
- generateObject: vi.fn().mockImplementation(async ({ prompt, schema }) => {
37
- // Simulate list generation
38
- if (schema?.items) {
39
- return {
40
- object: {
41
- items: [
42
- 'How AI is Revolutionizing Startup Fundraising in 2026',
43
- 'The Rise of Solo Founders: Building $10M ARR Companies Alone',
44
- 'Why Remote-First is Non-Negotiable for 2026 Startups',
45
- 'Sustainable Growth vs Hypergrowth: The 2026 Paradigm Shift',
46
- 'Building in Public: How Transparency Became a Competitive Advantage',
47
- 'The API-First Startup: Lessons from 2026 Unicorns',
48
- 'From Side Project to Series A: The 2026 Playbook',
49
- 'Climate Tech Startups: The Hottest Sector of 2026',
50
- 'The Death of Traditional MVPs: Ship Faster, Learn Faster',
51
- 'Community-Led Growth: The New GTM Strategy for 2026',
52
- ],
53
- },
54
- }
55
- }
56
- // Simulate blog post generation
57
- if (prompt.includes('blog post about')) {
58
- const titleMatch = prompt.match(/blog post about (.+)/)
59
- const title = titleMatch?.[1] || 'Unknown Topic'
60
- return {
61
- object: {
62
- text: `# ${title}\n\nThis is a comprehensive blog post about ${title}.\n\n## Introduction\n\nIn 2026, the startup landscape continues to evolve...\n\n## Key Takeaways\n\n1. Innovation is key\n2. Focus on customer value\n3. Build sustainable businesses\n\n## Conclusion\n\nThe future of startups is bright for those who adapt.`,
63
- },
64
- }
65
- }
66
- return { object: { result: 'Generated content' } }
67
- }),
68
- generateText: vi.fn().mockImplementation(async ({ prompt }) => {
69
- // Simulate blog post text generation
70
- if (prompt.includes('blog post about')) {
71
- const titleMatch = prompt.match(/blog post about (.+)/)
72
- const title = titleMatch?.[1] || 'Unknown Topic'
73
- return {
74
- text: `# ${title}\n\nThis is a comprehensive blog post about ${title}.\n\n## Introduction\n\nIn 2026, the startup landscape continues to evolve rapidly. Entrepreneurs are finding new ways to build, scale, and succeed.\n\n## The State of Startups in 2026\n\nThe ecosystem has matured significantly. AI tools have become indispensable, funding patterns have shifted, and remote work is now the default.\n\n## Key Strategies for Success\n\n1. **Leverage AI Wisely** - Use AI as a multiplier, not a replacement\n2. **Build Community First** - Your early adopters are your growth engine\n3. **Focus on Unit Economics** - Hypergrowth without sustainability is dead\n4. **Embrace Transparency** - Building in public creates trust and accountability\n\n## Practical Steps\n\n- Start with a problem you deeply understand\n- Validate with paying customers, not surveys\n- Build the smallest thing that delivers value\n- Iterate based on real usage data\n\n## Conclusion\n\nBuilding a startup in 2026 requires a blend of traditional business fundamentals and modern tools. The founders who succeed will be those who can navigate this balance effectively.`,
75
- }
76
- }
77
- return { text: 'Generated text content' }
78
- }),
79
- }))
26
+ // Skip AI-dependent tests if no gateway configured
27
+ const hasGateway = !!process.env.AI_GATEWAY_URL
80
28
 
81
29
  // ============================================================================
82
- // Test Helpers
30
+ // Real AI Tests (require gateway)
83
31
  // ============================================================================
84
32
 
85
- /**
86
- * Simulate the list template function
87
- */
88
- async function mockList(prompt: string): Promise<string[]> {
89
- const { generateObject } = await import('../src/generate.js')
90
- const result = await generateObject({
91
- model: 'sonnet',
92
- schema: { items: ['List items'] },
93
- prompt,
33
+ describe.skipIf(!hasGateway)('Batch Blog Post Generation with Real AI', () => {
34
+ it('generates blog post titles using real AI', async () => {
35
+ const result = await generateObject({
36
+ model: 'haiku',
37
+ schema: z.object({
38
+ titles: z.array(z.string()).describe('List of blog post titles'),
39
+ }),
40
+ prompt: 'Generate exactly 3 blog post titles about building startups.',
41
+ })
42
+
43
+ expect(result.object.titles).toHaveLength(3)
44
+ expect(result.object.titles.every((t: string) => typeof t === 'string')).toBe(true)
45
+ })
46
+
47
+ it('generates a single blog post using real AI', async () => {
48
+ const result = await generateText({
49
+ model: 'haiku',
50
+ prompt: 'Write a very short blog post intro (2-3 sentences) about TypeScript.',
51
+ })
52
+
53
+ expect(result.text).toBeDefined()
54
+ expect(result.text.length).toBeGreaterThan(50)
94
55
  })
95
- return (result.object as { items: string[] }).items
96
- }
56
+ })
97
57
 
98
58
  // ============================================================================
99
- // Tests
59
+ // Batch Queue Mechanics Tests (use memory adapter - no AI needed)
100
60
  // ============================================================================
101
61
 
102
- describe('Batch Blog Post Generation', () => {
62
+ describe('Batch Queue Mechanics', () => {
63
+ // Use a simple handler that doesn't call real AI
64
+ const mockHandler = async (item: { prompt: string }) => {
65
+ return `Generated content for: ${item.prompt.substring(0, 50)}...`
66
+ }
67
+
103
68
  beforeEach(() => {
104
- vi.clearAllMocks()
105
69
  clearBatches()
106
- // Use default handler that calls the mock
107
- configureMemoryAdapter({})
70
+ // Configure with mock handler to avoid real AI calls in batch tests
71
+ configureMemoryAdapter({ handler: mockHandler })
108
72
  })
109
73
 
110
74
  afterEach(() => {
111
75
  clearBatches()
112
76
  })
113
77
 
114
- describe('list` immediate execution', () => {
115
- it('list` executes immediately and returns titles', async () => {
116
- const titles = await mockList('10 blog post titles about building startups in 2026')
117
-
118
- expect(titles).toHaveLength(10)
119
- expect(titles[0]).toBe('How AI is Revolutionizing Startup Fundraising in 2026')
120
- expect(titles[9]).toBe('Community-Led Growth: The New GTM Strategy for 2026')
121
- })
122
- })
123
-
124
78
  describe('batch processing workflow', () => {
125
79
  it('creates batch queue and adds items', async () => {
126
80
  const batch = createBatch({ provider: 'openai', model: 'gpt-4o' })
127
81
 
128
- const titles = await mockList('10 blog post titles about building startups in 2026')
82
+ const titles = ['Title 1', 'Title 2', 'Title 3']
129
83
 
130
84
  // Add each title to the batch
131
85
  const items = titles.map((title) =>
@@ -134,45 +88,41 @@ describe('Batch Blog Post Generation', () => {
134
88
  })
135
89
  )
136
90
 
137
- expect(batch.size).toBe(10)
138
- expect(items).toHaveLength(10)
91
+ expect(batch.size).toBe(3)
92
+ expect(items).toHaveLength(3)
139
93
  expect(items[0].status).toBe('pending')
140
94
  })
141
95
 
142
96
  it('submits batch and returns job info', async () => {
143
97
  const batch = createBatch({ provider: 'openai', model: 'gpt-4o' })
144
98
 
145
- const titles = await mockList('10 blog post titles about building startups in 2026')
99
+ const titles = ['Title 1', 'Title 2', 'Title 3']
146
100
 
147
- titles.forEach((title) =>
148
- batch.add(`Write a comprehensive blog post about: ${title}`)
149
- )
101
+ titles.forEach((title) => batch.add(`Write a comprehensive blog post about: ${title}`))
150
102
 
151
103
  const { job, completion } = await batch.submit()
152
104
 
153
105
  expect(job.id).toMatch(/^batch_memory_/)
154
106
  expect(job.provider).toBe('openai')
155
- expect(job.totalItems).toBe(10)
107
+ expect(job.totalItems).toBe(3)
156
108
  expect(job.status).toBe('pending')
157
109
 
158
110
  // Wait for completion
159
111
  const results = await completion
160
- expect(results).toHaveLength(10)
112
+ expect(results).toHaveLength(3)
161
113
  })
162
114
 
163
115
  it('waits for batch completion and returns results', async () => {
164
116
  const batch = createBatch({ provider: 'openai', model: 'gpt-4o' })
165
117
 
166
- const titles = await mockList('10 blog post titles about building startups in 2026')
118
+ const titles = ['Title 1', 'Title 2', 'Title 3']
167
119
 
168
- titles.forEach((title) =>
169
- batch.add(`Write a comprehensive blog post about: ${title}`)
170
- )
120
+ titles.forEach((title) => batch.add(`Write a comprehensive blog post about: ${title}`))
171
121
 
172
122
  await batch.submit()
173
123
  const results = await batch.wait()
174
124
 
175
- expect(results).toHaveLength(10)
125
+ expect(results).toHaveLength(3)
176
126
  expect(results.every((r) => r.status === 'completed')).toBe(true)
177
127
  expect(results[0].result).toBeDefined()
178
128
  })
@@ -181,9 +131,7 @@ describe('Batch Blog Post Generation', () => {
181
131
  const batch = createBatch({ provider: 'openai' })
182
132
 
183
133
  const titles = ['First', 'Second', 'Third']
184
- const items = titles.map((title, i) =>
185
- batch.add(`Write about: ${title}`, { customId: `item_${i}` })
186
- )
134
+ titles.map((title, i) => batch.add(`Write about: ${title}`, { customId: `item_${i}` }))
187
135
 
188
136
  await batch.submit()
189
137
  const results = await batch.wait()
@@ -196,17 +144,14 @@ describe('Batch Blog Post Generation', () => {
196
144
 
197
145
  describe('withBatchQueue helper', () => {
198
146
  it('provides convenient batch execution', async () => {
199
- const titles = await mockList('10 blog post titles about building startups in 2026')
147
+ const titles = ['Title A', 'Title B', 'Title C']
200
148
 
201
- const results = await withBatch(
202
- (batch) =>
203
- titles.map((title) =>
204
- batch.add(`Write a blog post about: ${title}`)
205
- ),
149
+ const results = await withBatchQueue(
150
+ (batch) => titles.map((title) => batch.add(`Write a blog post about: ${title}`)),
206
151
  { provider: 'openai', model: 'gpt-4o' }
207
152
  )
208
153
 
209
- expect(results).toHaveLength(10)
154
+ expect(results).toHaveLength(3)
210
155
  expect(results.every((r) => r.status === 'completed')).toBe(true)
211
156
  })
212
157
  })
@@ -233,8 +178,11 @@ describe('Batch Blog Post Generation', () => {
233
178
 
234
179
  describe('error handling', () => {
235
180
  it('handles partial failures', async () => {
236
- // Configure adapter to fail 30% of requests
237
- configureMemoryAdapter({ failureRate: 0.3 })
181
+ // Configure adapter to fail 30% of requests with a mock handler
182
+ configureMemoryAdapter({
183
+ failureRate: 0.3,
184
+ handler: async (item: { prompt: string }) => `Result for: ${item.prompt}`,
185
+ })
238
186
 
239
187
  const batch = createBatch({ provider: 'openai' })
240
188
 
@@ -280,9 +228,11 @@ describe('Batch Blog Post Generation', () => {
280
228
 
281
229
  describe('batch with custom handler', () => {
282
230
  it('uses custom handler for processing', async () => {
283
- const customHandler = vi.fn().mockImplementation(async (item) => {
231
+ let callCount = 0
232
+ const customHandler = async (item: { prompt: string }) => {
233
+ callCount++
284
234
  return `Custom result for: ${item.prompt}`
285
- })
235
+ }
286
236
 
287
237
  configureMemoryAdapter({ handler: customHandler })
288
238
 
@@ -293,17 +243,20 @@ describe('Batch Blog Post Generation', () => {
293
243
  await batch.submit()
294
244
  const results = await batch.wait()
295
245
 
296
- expect(customHandler).toHaveBeenCalledTimes(2)
246
+ expect(callCount).toBe(2)
297
247
  expect(results[0].result).toBe('Custom result for: Topic 1')
298
248
  expect(results[1].result).toBe('Custom result for: Topic 2')
299
249
  })
300
250
  })
301
251
 
302
- describe('full workflow: list map batch', () => {
252
+ describe('full workflow: list -> map -> batch', () => {
303
253
  it('executes the complete blog post generation workflow', async () => {
304
- // Step 1: Get titles (executes immediately)
305
- const titles = await mockList('10 blog post titles about building startups in 2026')
306
- expect(titles).toHaveLength(10)
254
+ // Step 1: Simulate getting titles (in real usage, this would be AI-generated)
255
+ const titles = [
256
+ 'How AI is Revolutionizing Startup Fundraising',
257
+ 'The Rise of Solo Founders',
258
+ 'Remote-First is Non-Negotiable',
259
+ ]
307
260
 
308
261
  // Step 2: Create batch for blog posts (deferred)
309
262
  const batch = createBatch({
@@ -320,28 +273,26 @@ describe('Batch Blog Post Generation', () => {
320
273
  })
321
274
  )
322
275
 
323
- expect(batch.size).toBe(10)
276
+ expect(batch.size).toBe(3)
324
277
  expect(blogItems.every((item) => item.status === 'pending')).toBe(true)
325
278
 
326
279
  // Step 4: Submit the batch
327
280
  const { job, completion } = await batch.submit()
328
281
 
329
282
  expect(job.id).toBeDefined()
330
- expect(job.totalItems).toBe(10)
283
+ expect(job.totalItems).toBe(3)
331
284
  expect(batch.isSubmitted).toBe(true)
332
285
 
333
286
  // Step 5: Wait for results
334
287
  const results = await completion
335
288
 
336
- expect(results).toHaveLength(10)
289
+ expect(results).toHaveLength(3)
337
290
  expect(results.every((r) => r.status === 'completed')).toBe(true)
338
291
 
339
- // Verify results have blog post content
292
+ // Verify results have content
340
293
  for (const result of results) {
341
294
  expect(result.result).toBeDefined()
342
295
  expect(typeof result.result).toBe('string')
343
- // Blog posts should have some content
344
- expect((result.result as string).length).toBeGreaterThan(100)
345
296
  }
346
297
 
347
298
  // Verify items are updated after completion
@@ -351,9 +302,12 @@ describe('Batch Blog Post Generation', () => {
351
302
  })
352
303
 
353
304
  describe('Provider-specific batch behavior', () => {
305
+ // Use a simple handler that doesn't call real AI
306
+ const mockHandler = async () => 'Test result'
307
+
354
308
  beforeEach(() => {
355
309
  clearBatches()
356
- configureMemoryAdapter({})
310
+ configureMemoryAdapter({ handler: mockHandler })
357
311
  })
358
312
 
359
313
  it('uses specified provider', async () => {
@@ -372,7 +326,11 @@ describe('Provider-specific batch behavior', () => {
372
326
  })
373
327
 
374
328
  it('respects model configuration', async () => {
375
- const customHandler = vi.fn().mockResolvedValue('Result')
329
+ let handlerCalled = false
330
+ const customHandler = async () => {
331
+ handlerCalled = true
332
+ return 'Result'
333
+ }
376
334
  configureMemoryAdapter({ handler: customHandler })
377
335
 
378
336
  const batch = createBatch({ provider: 'openai', model: 'gpt-4o-mini' })
@@ -382,6 +340,6 @@ describe('Provider-specific batch behavior', () => {
382
340
 
383
341
  // The model should be passed to the handler via batch options
384
342
  // (memory adapter doesn't use it, but real adapters would)
385
- expect(customHandler).toHaveBeenCalled()
343
+ expect(handlerCalled).toBe(true)
386
344
  })
387
345
  })