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.
- package/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-test.log +105 -0
- package/README.md +232 -37
- package/TODO.md +138 -0
- package/dist/ai-promise.d.ts +219 -0
- package/dist/ai-promise.d.ts.map +1 -0
- package/dist/ai-promise.js +610 -0
- package/dist/ai-promise.js.map +1 -0
- package/dist/ai.d.ts +285 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +842 -0
- package/dist/ai.js.map +1 -0
- package/dist/batch/anthropic.d.ts +23 -0
- package/dist/batch/anthropic.d.ts.map +1 -0
- package/dist/batch/anthropic.js +257 -0
- package/dist/batch/anthropic.js.map +1 -0
- package/dist/batch/bedrock.d.ts +64 -0
- package/dist/batch/bedrock.d.ts.map +1 -0
- package/dist/batch/bedrock.js +586 -0
- package/dist/batch/bedrock.js.map +1 -0
- package/dist/batch/cloudflare.d.ts +37 -0
- package/dist/batch/cloudflare.d.ts.map +1 -0
- package/dist/batch/cloudflare.js +289 -0
- package/dist/batch/cloudflare.js.map +1 -0
- package/dist/batch/google.d.ts +41 -0
- package/dist/batch/google.d.ts.map +1 -0
- package/dist/batch/google.js +360 -0
- package/dist/batch/google.js.map +1 -0
- package/dist/batch/index.d.ts +31 -0
- package/dist/batch/index.d.ts.map +1 -0
- package/dist/batch/index.js +31 -0
- package/dist/batch/index.js.map +1 -0
- package/dist/batch/memory.d.ts +44 -0
- package/dist/batch/memory.d.ts.map +1 -0
- package/dist/batch/memory.js +188 -0
- package/dist/batch/memory.js.map +1 -0
- package/dist/batch/openai.d.ts +37 -0
- package/dist/batch/openai.d.ts.map +1 -0
- package/dist/batch/openai.js +403 -0
- package/dist/batch/openai.js.map +1 -0
- package/dist/batch-map.d.ts +125 -0
- package/dist/batch-map.d.ts.map +1 -0
- package/dist/batch-map.js +406 -0
- package/dist/batch-map.js.map +1 -0
- package/dist/batch-queue.d.ts +273 -0
- package/dist/batch-queue.d.ts.map +1 -0
- package/dist/batch-queue.js +271 -0
- package/dist/batch-queue.js.map +1 -0
- package/dist/context.d.ts +133 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +267 -0
- package/dist/context.js.map +1 -0
- package/dist/embeddings.d.ts +123 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +170 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/eval/index.d.ts +8 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +8 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/models.d.ts +66 -0
- package/dist/eval/models.d.ts.map +1 -0
- package/dist/eval/models.js +120 -0
- package/dist/eval/models.js.map +1 -0
- package/dist/eval/runner.d.ts +64 -0
- package/dist/eval/runner.d.ts.map +1 -0
- package/dist/eval/runner.js +148 -0
- package/dist/eval/runner.js.map +1 -0
- package/dist/generate.d.ts +168 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +174 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives.d.ts +292 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +471 -0
- package/dist/primitives.js.map +1 -0
- package/dist/providers/cloudflare.d.ts +9 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +9 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/index.d.ts +9 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +9 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/schema.d.ts +54 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +109 -0
- package/dist/schema.js.map +1 -0
- package/dist/template.d.ts +73 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +129 -0
- package/dist/template.js.map +1 -0
- package/dist/types.d.ts +481 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/evalite.config.ts +19 -0
- package/evals/README.md +212 -0
- package/evals/classification.eval.ts +108 -0
- package/evals/marketing.eval.ts +370 -0
- package/evals/math.eval.ts +94 -0
- package/evals/run-evals.ts +166 -0
- package/evals/structured-output.eval.ts +143 -0
- package/evals/writing.eval.ts +117 -0
- package/examples/batch-blog-posts.ts +160 -0
- package/package.json +59 -43
- package/src/ai-promise.ts +784 -0
- package/src/ai.ts +1183 -0
- package/src/batch/anthropic.ts +375 -0
- package/src/batch/bedrock.ts +801 -0
- package/src/batch/cloudflare.ts +421 -0
- package/src/batch/google.ts +491 -0
- package/src/batch/index.ts +31 -0
- package/src/batch/memory.ts +253 -0
- package/src/batch/openai.ts +557 -0
- package/src/batch-map.ts +534 -0
- package/src/batch-queue.ts +493 -0
- package/src/context.ts +332 -0
- package/src/embeddings.ts +244 -0
- package/src/eval/index.ts +8 -0
- package/src/eval/models.ts +158 -0
- package/src/eval/runner.ts +217 -0
- package/src/generate.ts +245 -0
- package/src/index.ts +154 -0
- package/src/primitives.ts +612 -0
- package/src/providers/cloudflare.ts +15 -0
- package/src/providers/index.ts +14 -0
- package/src/schema.ts +147 -0
- package/src/template.ts +209 -0
- package/src/types.ts +540 -0
- package/test/README.md +105 -0
- package/test/ai-proxy.test.ts +192 -0
- package/test/async-iterators.test.ts +327 -0
- package/test/batch-background.test.ts +482 -0
- package/test/batch-blog-posts.test.ts +387 -0
- package/test/blog-generation.test.ts +510 -0
- package/test/browse-read.test.ts +611 -0
- package/test/core-functions.test.ts +694 -0
- package/test/decide.test.ts +393 -0
- package/test/define.test.ts +274 -0
- package/test/e2e-bedrock-manual.ts +163 -0
- package/test/e2e-bedrock.test.ts +191 -0
- package/test/e2e-flex-gateway.ts +157 -0
- package/test/e2e-flex-manual.ts +183 -0
- package/test/e2e-flex.test.ts +209 -0
- package/test/e2e-google-manual.ts +178 -0
- package/test/e2e-google.test.ts +216 -0
- package/test/embeddings.test.ts +284 -0
- package/test/evals/define-function.eval.test.ts +379 -0
- package/test/evals/primitives.eval.test.ts +384 -0
- package/test/function-types.test.ts +492 -0
- package/test/generate-core.test.ts +319 -0
- package/test/generate.test.ts +163 -0
- package/test/implicit-batch.test.ts +422 -0
- package/test/schema.test.ts +109 -0
- package/test/tagged-templates.test.ts +302 -0
- package/tsconfig.json +8 -6
- package/vitest.config.ts +42 -0
- package/LICENSE +0 -21
- package/db/cache.ts +0 -6
- package/db/mongo.ts +0 -75
- package/dist/mjs/db/cache.d.ts +0 -1
- package/dist/mjs/db/cache.js +0 -5
- package/dist/mjs/db/mongo.d.ts +0 -31
- package/dist/mjs/db/mongo.js +0 -48
- package/dist/mjs/examples/data.d.ts +0 -1105
- package/dist/mjs/examples/data.js +0 -1105
- package/dist/mjs/functions/ai.d.ts +0 -20
- package/dist/mjs/functions/ai.js +0 -83
- package/dist/mjs/functions/ai.test.d.ts +0 -1
- package/dist/mjs/functions/ai.test.js +0 -29
- package/dist/mjs/functions/gpt.d.ts +0 -4
- package/dist/mjs/functions/gpt.js +0 -10
- package/dist/mjs/functions/list.d.ts +0 -7
- package/dist/mjs/functions/list.js +0 -72
- package/dist/mjs/index.d.ts +0 -3
- package/dist/mjs/index.js +0 -3
- package/dist/mjs/queue/kafka.d.ts +0 -0
- package/dist/mjs/queue/kafka.js +0 -1
- package/dist/mjs/queue/memory.d.ts +0 -0
- package/dist/mjs/queue/memory.js +0 -1
- package/dist/mjs/queue/mongo.d.ts +0 -30
- package/dist/mjs/queue/mongo.js +0 -42
- package/dist/mjs/streams/kafka.d.ts +0 -0
- package/dist/mjs/streams/kafka.js +0 -1
- package/dist/mjs/streams/memory.d.ts +0 -0
- package/dist/mjs/streams/memory.js +0 -1
- package/dist/mjs/streams/mongo.d.ts +0 -0
- package/dist/mjs/streams/mongo.js +0 -1
- package/dist/mjs/streams/types.d.ts +0 -0
- package/dist/mjs/streams/types.js +0 -1
- package/dist/mjs/types.d.ts +0 -11
- package/dist/mjs/types.js +0 -1
- package/dist/mjs/utils/completion.d.ts +0 -9
- package/dist/mjs/utils/completion.js +0 -20
- package/dist/mjs/utils/schema.d.ts +0 -10
- package/dist/mjs/utils/schema.js +0 -72
- package/dist/mjs/utils/schema.test.d.ts +0 -1
- package/dist/mjs/utils/schema.test.js +0 -60
- package/dist/mjs/utils/state.d.ts +0 -1
- package/dist/mjs/utils/state.js +0 -19
- package/examples/data.ts +0 -1105
- package/fixup +0 -11
- package/functions/ai.test.ts +0 -41
- package/functions/ai.ts +0 -115
- package/functions/gpt.ts +0 -12
- package/functions/list.ts +0 -84
- package/index.ts +0 -3
- package/queue/kafka.ts +0 -0
- package/queue/memory.ts +0 -0
- package/queue/mongo.ts +0 -88
- package/streams/kafka.ts +0 -0
- package/streams/memory.ts +0 -0
- package/streams/mongo.ts +0 -0
- package/streams/types.ts +0 -0
- package/tsconfig-backup.json +0 -105
- package/tsconfig-base.json +0 -26
- package/tsconfig-cjs.json +0 -8
- package/types.ts +0 -12
- package/utils/completion.ts +0 -28
- package/utils/schema.test.ts +0 -69
- package/utils/schema.ts +0 -74
- 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
|
+
})
|