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,422 @@
1
+ /**
2
+ * Implicit Batch Processing Tests
3
+ *
4
+ * Tests the automatic batch detection when using .map() without explicit await.
5
+ * Provider and model come from execution context, not code.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // Configure globally or via environment
10
+ * configure({ provider: 'openai', model: 'gpt-4o', batchMode: 'auto' })
11
+ *
12
+ * // Use naturally - batch is automatic
13
+ * const titles = await list`10 blog post titles`
14
+ * const posts = titles.map(title => write`blog post: # ${title}`)
15
+ * console.log(await posts) // Batched automatically!
16
+ * ```
17
+ */
18
+
19
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
20
+ import {
21
+ configure,
22
+ resetContext,
23
+ withContext,
24
+ getProvider,
25
+ getModel,
26
+ getBatchMode,
27
+ shouldUseBatchAPI,
28
+ getExecutionTier,
29
+ getFlexThreshold,
30
+ getBatchThreshold,
31
+ isFlexAvailable,
32
+ } from '../src/context.js'
33
+ import { list, write, ai, is } from '../src/primitives.js'
34
+ import {
35
+ createBatchMap,
36
+ BatchMapPromise,
37
+ captureOperation,
38
+ isInRecordingMode,
39
+ } from '../src/batch-map.js'
40
+
41
+ // Import memory adapter to register it
42
+ import '../src/batch/memory.js'
43
+ import { configureMemoryAdapter, clearBatches } from '../src/batch/memory.js'
44
+
45
+ // ============================================================================
46
+ // Mock Setup
47
+ // ============================================================================
48
+
49
+ vi.mock('../src/generate.js', () => ({
50
+ generateObject: vi.fn().mockImplementation(async ({ prompt, schema }) => {
51
+ // Simulate list generation
52
+ if (schema?.items) {
53
+ return {
54
+ object: {
55
+ items: [
56
+ 'Building AI-First Startups in 2026',
57
+ 'The Future of Remote Work',
58
+ 'Sustainable Tech Growth',
59
+ 'From Idea to MVP in 30 Days',
60
+ 'Community-Led Product Development',
61
+ ],
62
+ },
63
+ }
64
+ }
65
+ // Simulate boolean
66
+ if (schema?.answer) {
67
+ return {
68
+ object: { answer: 'true' },
69
+ }
70
+ }
71
+ // Default object
72
+ return { object: { result: 'Generated content' } }
73
+ }),
74
+ generateText: vi.fn().mockImplementation(async ({ prompt }) => {
75
+ return {
76
+ text: `Generated blog post for: ${prompt.slice(0, 50)}...`,
77
+ }
78
+ }),
79
+ }))
80
+
81
+ // ============================================================================
82
+ // Tests
83
+ // ============================================================================
84
+
85
+ describe('Implicit Batch Processing', () => {
86
+ beforeEach(() => {
87
+ vi.clearAllMocks()
88
+ resetContext()
89
+ clearBatches()
90
+ configureMemoryAdapter({})
91
+ })
92
+
93
+ afterEach(() => {
94
+ resetContext()
95
+ clearBatches()
96
+ })
97
+
98
+ describe('Execution Context', () => {
99
+ it('uses global configuration', () => {
100
+ configure({
101
+ provider: 'anthropic',
102
+ model: 'claude-sonnet-4-20250514',
103
+ batchMode: 'auto',
104
+ })
105
+
106
+ expect(getProvider()).toBe('anthropic')
107
+ expect(getModel()).toBe('claude-sonnet-4-20250514')
108
+ expect(getBatchMode()).toBe('auto')
109
+ })
110
+
111
+ it('supports withContext for scoped configuration', async () => {
112
+ configure({ provider: 'openai', model: 'gpt-4o' })
113
+
114
+ await withContext({ provider: 'anthropic', model: 'claude-opus-4-20250514' }, async () => {
115
+ expect(getProvider()).toBe('anthropic')
116
+ expect(getModel()).toBe('claude-opus-4-20250514')
117
+ })
118
+
119
+ // Back to global after context exits
120
+ expect(getProvider()).toBe('openai')
121
+ })
122
+ })
123
+
124
+ describe('Batch Detection', () => {
125
+ it('shouldUseBatchAPI returns true for large batches', () => {
126
+ configure({ batchMode: 'auto', batchThreshold: 5 })
127
+
128
+ expect(shouldUseBatchAPI(3)).toBe(false)
129
+ expect(shouldUseBatchAPI(5)).toBe(true)
130
+ expect(shouldUseBatchAPI(10)).toBe(true)
131
+ })
132
+
133
+ it('batchMode: deferred always uses batch API', () => {
134
+ configure({ batchMode: 'deferred' })
135
+
136
+ expect(shouldUseBatchAPI(1)).toBe(true)
137
+ expect(shouldUseBatchAPI(100)).toBe(true)
138
+ })
139
+
140
+ it('batchMode: immediate never uses batch API', () => {
141
+ configure({ batchMode: 'immediate' })
142
+
143
+ expect(shouldUseBatchAPI(1)).toBe(false)
144
+ expect(shouldUseBatchAPI(100)).toBe(false)
145
+ })
146
+ })
147
+
148
+ describe('Three-Tier Execution (immediate → flex → batch)', () => {
149
+ it('getExecutionTier returns immediate for < flexThreshold items', () => {
150
+ configure({ batchMode: 'auto', flexThreshold: 5, batchThreshold: 500 })
151
+
152
+ expect(getExecutionTier(1)).toBe('immediate')
153
+ expect(getExecutionTier(3)).toBe('immediate')
154
+ expect(getExecutionTier(4)).toBe('immediate')
155
+ })
156
+
157
+ it('getExecutionTier returns flex for flexThreshold to < batchThreshold items', () => {
158
+ configure({ batchMode: 'auto', flexThreshold: 5, batchThreshold: 500 })
159
+
160
+ expect(getExecutionTier(5)).toBe('flex')
161
+ expect(getExecutionTier(10)).toBe('flex')
162
+ expect(getExecutionTier(100)).toBe('flex')
163
+ expect(getExecutionTier(499)).toBe('flex')
164
+ })
165
+
166
+ it('getExecutionTier returns batch for >= batchThreshold items', () => {
167
+ configure({ batchMode: 'auto', flexThreshold: 5, batchThreshold: 500 })
168
+
169
+ expect(getExecutionTier(500)).toBe('batch')
170
+ expect(getExecutionTier(1000)).toBe('batch')
171
+ expect(getExecutionTier(50000)).toBe('batch')
172
+ })
173
+
174
+ it('respects custom thresholds', () => {
175
+ configure({ batchMode: 'auto', flexThreshold: 10, batchThreshold: 100 })
176
+
177
+ // immediate: < 10
178
+ expect(getExecutionTier(5)).toBe('immediate')
179
+ expect(getExecutionTier(9)).toBe('immediate')
180
+
181
+ // flex: 10-99
182
+ expect(getExecutionTier(10)).toBe('flex')
183
+ expect(getExecutionTier(50)).toBe('flex')
184
+ expect(getExecutionTier(99)).toBe('flex')
185
+
186
+ // batch: 100+
187
+ expect(getExecutionTier(100)).toBe('batch')
188
+ expect(getExecutionTier(200)).toBe('batch')
189
+ })
190
+
191
+ it('batchMode: flex always returns flex tier', () => {
192
+ configure({ batchMode: 'flex' })
193
+
194
+ expect(getExecutionTier(1)).toBe('flex')
195
+ expect(getExecutionTier(10)).toBe('flex')
196
+ expect(getExecutionTier(1000)).toBe('flex')
197
+ })
198
+
199
+ it('batchMode: immediate always returns immediate tier', () => {
200
+ configure({ batchMode: 'immediate' })
201
+
202
+ expect(getExecutionTier(1)).toBe('immediate')
203
+ expect(getExecutionTier(100)).toBe('immediate')
204
+ expect(getExecutionTier(1000)).toBe('immediate')
205
+ })
206
+
207
+ it('batchMode: deferred always returns batch tier', () => {
208
+ configure({ batchMode: 'deferred' })
209
+
210
+ expect(getExecutionTier(1)).toBe('batch')
211
+ expect(getExecutionTier(100)).toBe('batch')
212
+ expect(getExecutionTier(1000)).toBe('batch')
213
+ })
214
+
215
+ it('getFlexThreshold returns configured value or default', () => {
216
+ // Default
217
+ resetContext()
218
+ expect(getFlexThreshold()).toBe(5)
219
+
220
+ // Custom
221
+ configure({ flexThreshold: 10 })
222
+ expect(getFlexThreshold()).toBe(10)
223
+ })
224
+
225
+ it('getBatchThreshold returns configured value or default', () => {
226
+ // Default
227
+ resetContext()
228
+ expect(getBatchThreshold()).toBe(500)
229
+
230
+ // Custom
231
+ configure({ batchThreshold: 1000 })
232
+ expect(getBatchThreshold()).toBe(1000)
233
+ })
234
+ })
235
+
236
+ describe('Flex Availability', () => {
237
+ it('isFlexAvailable returns true for openai', () => {
238
+ configure({ provider: 'openai' })
239
+ expect(isFlexAvailable()).toBe(true)
240
+ })
241
+
242
+ it('isFlexAvailable returns true for bedrock', () => {
243
+ configure({ provider: 'bedrock' })
244
+ expect(isFlexAvailable()).toBe(true)
245
+ })
246
+
247
+ it('isFlexAvailable returns false for anthropic (no native flex)', () => {
248
+ configure({ provider: 'anthropic' })
249
+ expect(isFlexAvailable()).toBe(false)
250
+ })
251
+
252
+ it('isFlexAvailable returns true for google', () => {
253
+ configure({ provider: 'google' })
254
+ expect(isFlexAvailable()).toBe(true)
255
+ })
256
+
257
+ it('isFlexAvailable returns false for cloudflare', () => {
258
+ configure({ provider: 'cloudflare' })
259
+ expect(isFlexAvailable()).toBe(false)
260
+ })
261
+ })
262
+
263
+ describe('Operation Recording', () => {
264
+ it('captures operations during createBatchMap', () => {
265
+ const items = ['Topic A', 'Topic B', 'Topic C']
266
+ let recordedCount = 0
267
+
268
+ // Create batch map - this enters recording mode for each item
269
+ const batchMap = createBatchMap(items, (item) => {
270
+ // When we call write` here, it should capture the operation
271
+ // Since we mocked generateText, we need to manually capture
272
+ captureOperation(`Write about: ${item}`, 'text', undefined, undefined)
273
+ recordedCount++
274
+ return `result_${item}`
275
+ })
276
+
277
+ expect(batchMap.size).toBe(3)
278
+ expect(recordedCount).toBe(3)
279
+ })
280
+ })
281
+
282
+ describe('BatchMapPromise', () => {
283
+ it('resolves with immediate execution for small batches', async () => {
284
+ configure({ batchMode: 'immediate' })
285
+
286
+ const items = ['A', 'B', 'C']
287
+ const batchMap = new BatchMapPromise<string>(
288
+ items,
289
+ items.map((item) => [
290
+ {
291
+ id: `op_${item}`,
292
+ prompt: `Write about: ${item}`,
293
+ itemPlaceholder: item,
294
+ type: 'text' as const,
295
+ },
296
+ ]),
297
+ { immediate: true }
298
+ )
299
+
300
+ const results = await batchMap
301
+
302
+ expect(results).toHaveLength(3)
303
+ // Results should contain generated text
304
+ results.forEach((result) => {
305
+ expect(typeof result).toBe('string')
306
+ })
307
+ })
308
+
309
+ it('supports async iteration', async () => {
310
+ configure({ batchMode: 'immediate' })
311
+
312
+ const items = ['X', 'Y']
313
+ const batchMap = new BatchMapPromise<string>(
314
+ items,
315
+ items.map((item) => [
316
+ {
317
+ id: `op_${item}`,
318
+ prompt: `Generate: ${item}`,
319
+ itemPlaceholder: item,
320
+ type: 'text' as const,
321
+ },
322
+ ]),
323
+ { immediate: true }
324
+ )
325
+
326
+ const collected: string[] = []
327
+ const results = await batchMap
328
+ for (const result of results) {
329
+ collected.push(result)
330
+ }
331
+
332
+ expect(collected).toHaveLength(2)
333
+ })
334
+ })
335
+
336
+ describe('Full Workflow', () => {
337
+ it('list → map → batch flow works end-to-end', async () => {
338
+ // Configure for immediate execution (for testing)
339
+ configure({ batchMode: 'immediate', provider: 'openai', model: 'gpt-4o' })
340
+
341
+ // Step 1: Get titles (this executes immediately)
342
+ // Note: The mock returns { object: { items: [...] } }
343
+ // so we access .items from the result
344
+ const result = await list`5 blog post titles about startups`
345
+ const titles = (result as any).items || result
346
+ expect(titles).toHaveLength(5)
347
+
348
+ // Step 2: Map to blog posts
349
+ // In the real implementation, this would capture operations
350
+ // For this test, we simulate the batch map behavior
351
+ const batchMap = createBatchMap(titles, (title: string) => {
352
+ // Capture the write operation
353
+ captureOperation(`Write a blog post about: ${title}`, 'text')
354
+ return title // Return value not used, operations are captured
355
+ })
356
+
357
+ // Step 3: Await resolves the batch
358
+ expect(batchMap.size).toBe(5)
359
+ })
360
+
361
+ it('supports complex map with multiple operations per item', async () => {
362
+ configure({ batchMode: 'immediate' })
363
+
364
+ const ideas = ['AI Assistant', 'Remote Tools', 'Dev Platform']
365
+
366
+ const batchMap = createBatchMap(ideas, (idea) => {
367
+ // Multiple operations per item
368
+ captureOperation(`Analyze: ${idea}`, 'object')
369
+ captureOperation(`Is ${idea} viable?`, 'boolean')
370
+ captureOperation(`Market for: ${idea}`, 'text')
371
+ return { idea }
372
+ })
373
+
374
+ expect(batchMap.size).toBe(3)
375
+ // Each item should have 3 operations
376
+ })
377
+ })
378
+
379
+ describe('Provider Integration', () => {
380
+ it('falls back to immediate when adapter not available', async () => {
381
+ // Configure for a provider without adapter registered
382
+ configure({ batchMode: 'deferred', provider: 'google' })
383
+
384
+ const items = ['Test']
385
+ const batchMap = new BatchMapPromise<string>(
386
+ items,
387
+ [[{
388
+ id: 'op_1',
389
+ prompt: 'Test prompt',
390
+ itemPlaceholder: 'Test',
391
+ type: 'text' as const,
392
+ }]],
393
+ { deferred: true }
394
+ )
395
+
396
+ // Should not throw, falls back to immediate
397
+ const results = await batchMap
398
+ expect(results).toHaveLength(1)
399
+ })
400
+ })
401
+ })
402
+
403
+ describe('API Design', () => {
404
+ it('demonstrates the clean API', async () => {
405
+ // This is how users will write code:
406
+ //
407
+ // const titles = await list`10 blog post titles about building startups in 2026`
408
+ // const posts = titles.map(title => write`blog post targeting founders starting with "# ${title}"`)
409
+ // console.log(await posts) // Batched automatically based on context!
410
+ //
411
+ // No need to specify provider, model, or batch configuration in the code.
412
+ // Everything comes from environment variables or configure():
413
+ //
414
+ // AI_PROVIDER=anthropic
415
+ // AI_MODEL=claude-sonnet-4-20250514
416
+ // AI_BATCH_MODE=auto
417
+ // AI_BATCH_THRESHOLD=5
418
+
419
+ // For this test, we just verify the types work
420
+ expect(true).toBe(true)
421
+ })
422
+ })
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Tests for schema conversion
3
+ *
4
+ * These are pure unit tests - no AI calls needed.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest'
8
+ import { schema } from '../src/index.js'
9
+ import { z } from 'zod'
10
+
11
+ describe('schema', () => {
12
+ describe('string types', () => {
13
+ it('converts simple string description to z.string()', () => {
14
+ const result = schema('User name')
15
+ expect(result._def.typeName).toBe('ZodString')
16
+ expect(result._def.description).toBe('User name')
17
+ })
18
+
19
+ it('converts (number) hint to z.number()', () => {
20
+ const result = schema('User age (number)')
21
+ expect(result._def.typeName).toBe('ZodNumber')
22
+ expect(result._def.description).toBe('User age')
23
+ })
24
+
25
+ it('converts (boolean) hint to z.boolean()', () => {
26
+ const result = schema('Is active (boolean)')
27
+ expect(result._def.typeName).toBe('ZodBoolean')
28
+ expect(result._def.description).toBe('Is active')
29
+ })
30
+
31
+ it('converts (integer) hint to z.number().int()', () => {
32
+ const result = schema('Item count (integer)')
33
+ expect(result._def.typeName).toBe('ZodNumber')
34
+ expect(result._def.checks?.some((c: { kind: string }) => c.kind === 'int')).toBe(true)
35
+ })
36
+
37
+ it('converts (date) hint to z.string().datetime()', () => {
38
+ const result = schema('Created at (date)')
39
+ expect(result._def.typeName).toBe('ZodString')
40
+ expect(result._def.checks?.some((c: { kind: string }) => c.kind === 'datetime')).toBe(true)
41
+ })
42
+ })
43
+
44
+ describe('enum types', () => {
45
+ it('converts pipe-separated values to z.enum()', () => {
46
+ const result = schema('pending | done | cancelled')
47
+ expect(result._def.typeName).toBe('ZodEnum')
48
+ expect(result._def.values).toEqual(['pending', 'done', 'cancelled'])
49
+ })
50
+
51
+ it('handles spaces around pipe', () => {
52
+ const result = schema('yes | no | maybe')
53
+ expect(result._def.values).toEqual(['yes', 'no', 'maybe'])
54
+ })
55
+ })
56
+
57
+ describe('array types', () => {
58
+ it('converts [string] to z.array(z.string())', () => {
59
+ const result = schema(['List of items'])
60
+ expect(result._def.typeName).toBe('ZodArray')
61
+ expect(result._def.type._def.typeName).toBe('ZodString')
62
+ expect(result._def.description).toBe('List of items')
63
+ })
64
+ })
65
+
66
+ describe('object types', () => {
67
+ it('converts object to z.object()', () => {
68
+ const result = schema({
69
+ name: 'User name',
70
+ age: 'Age (number)',
71
+ })
72
+ expect(result._def.typeName).toBe('ZodObject')
73
+ })
74
+
75
+ it('handles nested objects', () => {
76
+ const result = schema({
77
+ user: {
78
+ name: 'Name',
79
+ profile: {
80
+ bio: 'Bio',
81
+ },
82
+ },
83
+ })
84
+ expect(result._def.typeName).toBe('ZodObject')
85
+ })
86
+
87
+ it('handles mixed types in object', () => {
88
+ const result = schema({
89
+ name: 'Name',
90
+ count: 'Count (number)',
91
+ active: 'Active (boolean)',
92
+ status: 'pending | done',
93
+ tags: ['Tags'],
94
+ })
95
+ expect(result._def.typeName).toBe('ZodObject')
96
+ })
97
+ })
98
+
99
+ describe('zod passthrough', () => {
100
+ it('passes through existing zod schemas', () => {
101
+ const zodSchema = z.object({
102
+ name: z.string(),
103
+ age: z.number(),
104
+ })
105
+ const result = schema(zodSchema)
106
+ expect(result).toBe(zodSchema)
107
+ })
108
+ })
109
+ })