ai-functions 2.1.3 → 2.3.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 (277) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +55 -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 +116 -0
  93. package/dist/function-registry.d.ts.map +1 -0
  94. package/dist/function-registry.js +546 -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/schema.js +2 -2
  137. package/dist/schema.js.map +1 -1
  138. package/dist/telemetry.d.ts +128 -0
  139. package/dist/telemetry.d.ts.map +1 -0
  140. package/dist/telemetry.js +285 -0
  141. package/dist/telemetry.js.map +1 -0
  142. package/dist/template.d.ts.map +1 -1
  143. package/dist/template.js +6 -1
  144. package/dist/template.js.map +1 -1
  145. package/dist/tool-orchestration.d.ts +66 -4
  146. package/dist/tool-orchestration.d.ts.map +1 -1
  147. package/dist/tool-orchestration.js +123 -23
  148. package/dist/tool-orchestration.js.map +1 -1
  149. package/dist/type-guards.d.ts +28 -0
  150. package/dist/type-guards.d.ts.map +1 -0
  151. package/dist/type-guards.js +29 -0
  152. package/dist/type-guards.js.map +1 -0
  153. package/dist/types.d.ts +135 -17
  154. package/dist/types.d.ts.map +1 -1
  155. package/dist/types.js +36 -1
  156. package/dist/types.js.map +1 -1
  157. package/dist/wrap-for-v3.d.ts +80 -0
  158. package/dist/wrap-for-v3.d.ts.map +1 -0
  159. package/dist/wrap-for-v3.js +89 -0
  160. package/dist/wrap-for-v3.js.map +1 -0
  161. package/examples/00-quickstart.ts +232 -0
  162. package/examples/01-rag-chatbot.ts +212 -0
  163. package/examples/02-multi-agent-research.ts +290 -0
  164. package/examples/03-email-classification.ts +379 -0
  165. package/examples/04-content-moderation.ts +400 -0
  166. package/examples/05-document-extraction.ts +455 -0
  167. package/examples/06-streaming-chat-nextjs.ts +437 -0
  168. package/examples/07-cloudflare-worker.ts +483 -0
  169. package/examples/08-batch-processing.ts +491 -0
  170. package/examples/09-budget-constrained.ts +527 -0
  171. package/examples/10-tool-orchestration.ts +565 -0
  172. package/examples/11-retry-resilience.ts +403 -0
  173. package/examples/12-caching-strategies.ts +422 -0
  174. package/examples/README.md +145 -0
  175. package/package.json +28 -25
  176. package/src/ai-promise.ts +226 -140
  177. package/src/ai-schemas.ts +122 -0
  178. package/src/ai.ts +69 -1176
  179. package/src/batch/anthropic.ts +96 -161
  180. package/src/batch/bedrock.ts +203 -454
  181. package/src/batch/cloudflare.ts +99 -282
  182. package/src/batch/google.ts +91 -297
  183. package/src/batch/index.ts +4 -1
  184. package/src/batch/memory.ts +15 -10
  185. package/src/batch/openai.ts +65 -193
  186. package/src/batch/provider.ts +336 -0
  187. package/src/batch-map.ts +29 -24
  188. package/src/batch-queue.ts +200 -11
  189. package/src/budget.ts +31 -18
  190. package/src/cache.ts +45 -17
  191. package/src/context.ts +106 -77
  192. package/src/digital-objects-registry.ts +750 -0
  193. package/src/errors.ts +37 -0
  194. package/src/eval/runner.ts +60 -36
  195. package/src/eval-log/in-memory.ts +90 -0
  196. package/src/eval-log/index.ts +46 -0
  197. package/src/eval-log/types.ts +110 -0
  198. package/src/function-registry.ts +671 -0
  199. package/src/generate.ts +33 -28
  200. package/src/index.ts +119 -21
  201. package/src/logger.ts +232 -0
  202. package/src/middleware/budget.ts +171 -0
  203. package/src/middleware/cache.ts +299 -0
  204. package/src/middleware/embed-cache.ts +195 -0
  205. package/src/middleware/index.ts +23 -0
  206. package/src/middleware/trace.ts +248 -0
  207. package/src/primitives.ts +589 -62
  208. package/src/retry.ts +144 -18
  209. package/src/schema.ts +8 -8
  210. package/src/telemetry.ts +403 -0
  211. package/src/template.ts +8 -4
  212. package/src/tool-orchestration.ts +213 -48
  213. package/src/type-guards.ts +31 -0
  214. package/src/types.ts +164 -25
  215. package/src/wrap-for-v3.ts +105 -0
  216. package/test/ai-promise.test.ts +1080 -0
  217. package/test/ai-proxy.test.ts +1 -1
  218. package/test/batch-autosubmit-errors.test.ts +49 -37
  219. package/test/batch-blog-posts.test.ts +87 -129
  220. package/test/core-functions.test.ts +183 -579
  221. package/test/decide.test.ts +154 -322
  222. package/test/define.test.ts +211 -8
  223. package/test/digital-objects-registry.test.ts +760 -0
  224. package/test/embedding-cache-middleware.test.ts +140 -0
  225. package/test/generate-core.test.ts +140 -229
  226. package/test/implicit-batch.test.ts +22 -65
  227. package/test/retry-policy-integration.test.ts +117 -0
  228. package/test/schema.test.ts +55 -19
  229. package/test/template.test.ts +1164 -0
  230. package/test/tool-orchestration.test.ts +270 -0
  231. package/test/wrap-for-v3.test.ts +612 -0
  232. package/vitest.config.js +6 -0
  233. package/vitest.config.ts +20 -0
  234. package/LICENSE +0 -21
  235. package/dist/rpc/auth.d.ts +0 -69
  236. package/dist/rpc/auth.d.ts.map +0 -1
  237. package/dist/rpc/auth.js +0 -136
  238. package/dist/rpc/auth.js.map +0 -1
  239. package/dist/rpc/client.d.ts +0 -62
  240. package/dist/rpc/client.d.ts.map +0 -1
  241. package/dist/rpc/client.js +0 -103
  242. package/dist/rpc/client.js.map +0 -1
  243. package/dist/rpc/deferred.d.ts +0 -60
  244. package/dist/rpc/deferred.d.ts.map +0 -1
  245. package/dist/rpc/deferred.js +0 -96
  246. package/dist/rpc/deferred.js.map +0 -1
  247. package/dist/rpc/index.d.ts +0 -22
  248. package/dist/rpc/index.d.ts.map +0 -1
  249. package/dist/rpc/index.js +0 -38
  250. package/dist/rpc/index.js.map +0 -1
  251. package/dist/rpc/local.d.ts +0 -42
  252. package/dist/rpc/local.d.ts.map +0 -1
  253. package/dist/rpc/local.js +0 -50
  254. package/dist/rpc/local.js.map +0 -1
  255. package/dist/rpc/server.d.ts +0 -165
  256. package/dist/rpc/server.d.ts.map +0 -1
  257. package/dist/rpc/server.js +0 -405
  258. package/dist/rpc/server.js.map +0 -1
  259. package/dist/rpc/session.d.ts +0 -32
  260. package/dist/rpc/session.d.ts.map +0 -1
  261. package/dist/rpc/session.js +0 -43
  262. package/dist/rpc/session.js.map +0 -1
  263. package/dist/rpc/transport.d.ts +0 -306
  264. package/dist/rpc/transport.d.ts.map +0 -1
  265. package/dist/rpc/transport.js +0 -731
  266. package/dist/rpc/transport.js.map +0 -1
  267. package/src/batch/anthropic.js +0 -256
  268. package/src/batch/bedrock.js +0 -584
  269. package/src/batch/cloudflare.js +0 -287
  270. package/src/batch/google.js +0 -359
  271. package/src/batch/index.js +0 -30
  272. package/src/batch/memory.js +0 -187
  273. package/src/batch/openai.js +0 -402
  274. package/src/eval/index.js +0 -7
  275. package/src/eval/models.js +0 -119
  276. package/src/eval/runner.js +0 -147
  277. package/test/schema.test.js +0 -96
@@ -16,7 +16,7 @@
16
16
  * ```
17
17
  */
18
18
 
19
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
19
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
20
20
  import {
21
21
  configure,
22
22
  resetContext,
@@ -29,54 +29,14 @@ import {
29
29
  getFlexThreshold,
30
30
  getBatchThreshold,
31
31
  isFlexAvailable,
32
- } from '../src/context.js'
33
- import { list, write, ai, is } from '../src/primitives.js'
34
- import {
35
32
  createBatchMap,
36
33
  BatchMapPromise,
37
- captureOperation,
38
- isInRecordingMode,
39
- } from '../src/batch-map.js'
34
+ } from '../src/index.js'
35
+ import { captureOperation } from '../src/batch-map.js'
40
36
 
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
- }))
37
+ // Memory adapter for testing - simulates batch processing locally
38
+ // Import from .ts file for proper vite resolution
39
+ import { configureMemoryAdapter, clearBatches } from '../src/batch/memory.ts'
80
40
 
81
41
  // ============================================================================
82
42
  // Tests
@@ -84,7 +44,6 @@ vi.mock('../src/generate.js', () => ({
84
44
 
85
45
  describe('Implicit Batch Processing', () => {
86
46
  beforeEach(() => {
87
- vi.clearAllMocks()
88
47
  resetContext()
89
48
  clearBatches()
90
49
  configureMemoryAdapter({})
@@ -145,7 +104,7 @@ describe('Implicit Batch Processing', () => {
145
104
  })
146
105
  })
147
106
 
148
- describe('Three-Tier Execution (immediate flex batch)', () => {
107
+ describe('Three-Tier Execution (immediate -> flex -> batch)', () => {
149
108
  it('getExecutionTier returns immediate for < flexThreshold items', () => {
150
109
  configure({ batchMode: 'auto', flexThreshold: 5, batchThreshold: 500 })
151
110
 
@@ -267,8 +226,7 @@ describe('Implicit Batch Processing', () => {
267
226
 
268
227
  // Create batch map - this enters recording mode for each item
269
228
  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
229
+ // Capture operation for each item
272
230
  captureOperation(`Write about: ${item}`, 'text', undefined, undefined)
273
231
  recordedCount++
274
232
  return `result_${item}`
@@ -306,7 +264,7 @@ describe('Implicit Batch Processing', () => {
306
264
  })
307
265
  })
308
266
 
309
- it('supports async iteration', async () => {
267
+ it('supports iteration over results', async () => {
310
268
  configure({ batchMode: 'immediate' })
311
269
 
312
270
  const items = ['X', 'Y']
@@ -334,20 +292,15 @@ describe('Implicit Batch Processing', () => {
334
292
  })
335
293
 
336
294
  describe('Full Workflow', () => {
337
- it('list map batch flow works end-to-end', async () => {
295
+ it('list -> map -> batch flow works end-to-end', async () => {
338
296
  // Configure for immediate execution (for testing)
339
297
  configure({ batchMode: 'immediate', provider: 'openai', model: 'gpt-4o' })
340
298
 
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)
299
+ // Step 1: Simulate getting titles (in production this would be AI-generated)
300
+ const titles = ['Title 1', 'Title 2', 'Title 3', 'Title 4', 'Title 5']
347
301
 
348
302
  // Step 2: Map to blog posts
349
303
  // In the real implementation, this would capture operations
350
- // For this test, we simulate the batch map behavior
351
304
  const batchMap = createBatchMap(titles, (title: string) => {
352
305
  // Capture the write operation
353
306
  captureOperation(`Write a blog post about: ${title}`, 'text')
@@ -384,12 +337,16 @@ describe('Implicit Batch Processing', () => {
384
337
  const items = ['Test']
385
338
  const batchMap = new BatchMapPromise<string>(
386
339
  items,
387
- [[{
388
- id: 'op_1',
389
- prompt: 'Test prompt',
390
- itemPlaceholder: 'Test',
391
- type: 'text' as const,
392
- }]],
340
+ [
341
+ [
342
+ {
343
+ id: 'op_1',
344
+ prompt: 'Test prompt',
345
+ itemPlaceholder: 'Test',
346
+ type: 'text' as const,
347
+ },
348
+ ],
349
+ ],
393
350
  { deferred: true }
394
351
  )
395
352
 
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Integration tests: retry/CB/fallback machinery reads policy data from
3
+ * `language-models`. Validates that the per-model `ModelPolicy` flows through
4
+ * the resilience classes correctly.
5
+ */
6
+
7
+ import { describe, it, expect, vi } from 'vitest'
8
+ import { RetryPolicy, CircuitBreaker, FallbackChain } from '../src/retry.js'
9
+ import { tiersForModel, modelSupportsTier, modelPolicyFor } from '../src/index.js'
10
+
11
+ describe('RetryPolicy.forModel', () => {
12
+ it('uses frontier-provider settings for sonnet', async () => {
13
+ const policy = RetryPolicy.forModel('sonnet')
14
+ let attempts = 0
15
+ const op = vi.fn(async () => {
16
+ attempts++
17
+ if (attempts < 2) {
18
+ const err = new Error('rate limit')
19
+ ;(err as Error & { status?: number }).status = 429
20
+ throw err
21
+ }
22
+ return 'ok'
23
+ })
24
+ const result = await policy.execute(() => op())
25
+ expect(result).toBe('ok')
26
+ expect(attempts).toBe(2)
27
+ })
28
+
29
+ it('forModel respects per-call overrides', async () => {
30
+ const policy = RetryPolicy.forModel('sonnet', { maxRetries: 0 })
31
+ let attempts = 0
32
+ const op = async () => {
33
+ attempts++
34
+ const err = new Error('rate limit')
35
+ ;(err as Error & { status?: number }).status = 429
36
+ throw err
37
+ }
38
+ await expect(policy.execute(() => op())).rejects.toThrow()
39
+ expect(attempts).toBe(1) // No retries
40
+ })
41
+ })
42
+
43
+ describe('CircuitBreaker.forModel', () => {
44
+ it('uses frontier-provider failure threshold for sonnet', async () => {
45
+ const breaker = CircuitBreaker.forModel('sonnet')
46
+ // Frontier threshold is 8 — eight failures should still report state
47
+ // before opening on the 8th.
48
+ for (let i = 0; i < 7; i++) {
49
+ try {
50
+ await breaker.execute(async () => {
51
+ throw new Error('fail')
52
+ })
53
+ } catch {
54
+ // expected
55
+ }
56
+ }
57
+ // Still closed (threshold is 8)
58
+ expect(breaker.state).toBe('closed')
59
+ })
60
+
61
+ it('forModel respects per-call overrides', async () => {
62
+ const breaker = CircuitBreaker.forModel('sonnet', { failureThreshold: 1 })
63
+ try {
64
+ await breaker.execute(async () => {
65
+ throw new Error('fail')
66
+ })
67
+ } catch {
68
+ // expected
69
+ }
70
+ expect(breaker.state).toBe('open')
71
+ })
72
+ })
73
+
74
+ describe('FallbackChain.forModel', () => {
75
+ it('builds a chain from policy.fallbackChain plus the primary model id', async () => {
76
+ const seen: string[] = []
77
+ const chain = FallbackChain.forModel<string, void>('sonnet', async (modelId) => {
78
+ seen.push(modelId)
79
+ throw new Error('fail')
80
+ })
81
+ await expect(chain.execute()).rejects.toThrow('All fallback models failed')
82
+ // First call is the primary alias, rest are fallbacks
83
+ expect(seen[0]).toBe('anthropic/claude-sonnet-4.5')
84
+ expect(seen.length).toBeGreaterThan(1)
85
+ })
86
+
87
+ it('returns first successful model result', async () => {
88
+ let calls = 0
89
+ const chain = FallbackChain.forModel<string, void>('sonnet', async (modelId) => {
90
+ calls++
91
+ if (calls === 1) throw new Error('first fails')
92
+ return modelId
93
+ })
94
+ const result = await chain.execute()
95
+ expect(calls).toBe(2)
96
+ expect(result).toBeTruthy()
97
+ })
98
+ })
99
+
100
+ describe('tier helpers', () => {
101
+ it('tiersForModel returns expected tiers', () => {
102
+ expect(tiersForModel('sonnet')).toContain('immediate')
103
+ expect(tiersForModel('sonnet')).toContain('batch')
104
+ expect(tiersForModel('gpt-4o')).toContain('flex')
105
+ })
106
+
107
+ it('modelSupportsTier checks eligibility', () => {
108
+ expect(modelSupportsTier('gpt-4o', 'flex')).toBe(true)
109
+ expect(modelSupportsTier('sonnet', 'flex')).toBe(false)
110
+ expect(modelSupportsTier('sonnet', 'immediate')).toBe(true)
111
+ })
112
+
113
+ it('modelPolicyFor is re-exported', () => {
114
+ const p = modelPolicyFor('sonnet')
115
+ expect(p.$type).toBe('ModelPolicy')
116
+ })
117
+ })
@@ -2,6 +2,7 @@
2
2
  * Tests for schema conversion
3
3
  *
4
4
  * These are pure unit tests - no AI calls needed.
5
+ * Uses type-safe instanceof checks instead of accessing internal _def property.
5
6
  */
6
7
 
7
8
  import { describe, it, expect } from 'vitest'
@@ -12,54 +13,67 @@ describe('schema', () => {
12
13
  describe('string types', () => {
13
14
  it('converts simple string description to z.string()', () => {
14
15
  const result = schema('User name')
15
- expect(result._def.typeName).toBe('ZodString')
16
- expect(result._def.description).toBe('User name')
16
+ expect(result instanceof z.ZodString).toBe(true)
17
+ expect(result.description).toBe('User name')
17
18
  })
18
19
 
19
20
  it('converts (number) hint to z.number()', () => {
20
21
  const result = schema('User age (number)')
21
- expect(result._def.typeName).toBe('ZodNumber')
22
- expect(result._def.description).toBe('User age')
22
+ expect(result instanceof z.ZodNumber).toBe(true)
23
+ expect(result.description).toBe('User age')
23
24
  })
24
25
 
25
26
  it('converts (boolean) hint to z.boolean()', () => {
26
27
  const result = schema('Is active (boolean)')
27
- expect(result._def.typeName).toBe('ZodBoolean')
28
- expect(result._def.description).toBe('Is active')
28
+ expect(result instanceof z.ZodBoolean).toBe(true)
29
+ expect(result.description).toBe('Is active')
29
30
  })
30
31
 
31
32
  it('converts (integer) hint to z.number().int()', () => {
32
33
  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)
34
+ expect(result instanceof z.ZodNumber).toBe(true)
35
+ // Verify it's an integer by testing validation behavior
36
+ expect(result.safeParse(5).success).toBe(true)
37
+ expect(result.safeParse(5.5).success).toBe(false)
35
38
  })
36
39
 
37
40
  it('converts (date) hint to z.string().datetime()', () => {
38
41
  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)
42
+ expect(result instanceof z.ZodString).toBe(true)
43
+ // Verify it validates datetime format
44
+ expect(result.safeParse('2024-01-15T10:30:00Z').success).toBe(true)
45
+ expect(result.safeParse('not-a-date').success).toBe(false)
41
46
  })
42
47
  })
43
48
 
44
49
  describe('enum types', () => {
45
50
  it('converts pipe-separated values to z.enum()', () => {
46
51
  const result = schema('pending | done | cancelled')
47
- expect(result._def.typeName).toBe('ZodEnum')
48
- expect(result._def.values).toEqual(['pending', 'done', 'cancelled'])
52
+ expect(result instanceof z.ZodEnum).toBe(true)
53
+ // Verify enum values through validation
54
+ expect(result.safeParse('pending').success).toBe(true)
55
+ expect(result.safeParse('done').success).toBe(true)
56
+ expect(result.safeParse('cancelled').success).toBe(true)
57
+ expect(result.safeParse('invalid').success).toBe(false)
49
58
  })
50
59
 
51
60
  it('handles spaces around pipe', () => {
52
61
  const result = schema('yes | no | maybe')
53
- expect(result._def.values).toEqual(['yes', 'no', 'maybe'])
62
+ expect(result instanceof z.ZodEnum).toBe(true)
63
+ expect(result.safeParse('yes').success).toBe(true)
64
+ expect(result.safeParse('no').success).toBe(true)
65
+ expect(result.safeParse('maybe').success).toBe(true)
54
66
  })
55
67
  })
56
68
 
57
69
  describe('array types', () => {
58
70
  it('converts [string] to z.array(z.string())', () => {
59
71
  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')
72
+ expect(result instanceof z.ZodArray).toBe(true)
73
+ expect(result.description).toBe('List of items')
74
+ // Verify array of strings validation
75
+ expect(result.safeParse(['a', 'b', 'c']).success).toBe(true)
76
+ expect(result.safeParse([1, 2, 3]).success).toBe(false)
63
77
  })
64
78
  })
65
79
 
@@ -69,7 +83,10 @@ describe('schema', () => {
69
83
  name: 'User name',
70
84
  age: 'Age (number)',
71
85
  })
72
- expect(result._def.typeName).toBe('ZodObject')
86
+ expect(result instanceof z.ZodObject).toBe(true)
87
+ // Verify object validation
88
+ expect(result.safeParse({ name: 'John', age: 30 }).success).toBe(true)
89
+ expect(result.safeParse({ name: 'John', age: 'thirty' }).success).toBe(false)
73
90
  })
74
91
 
75
92
  it('handles nested objects', () => {
@@ -81,7 +98,16 @@ describe('schema', () => {
81
98
  },
82
99
  },
83
100
  })
84
- expect(result._def.typeName).toBe('ZodObject')
101
+ expect(result instanceof z.ZodObject).toBe(true)
102
+ // Verify nested object validation
103
+ expect(
104
+ result.safeParse({
105
+ user: {
106
+ name: 'John',
107
+ profile: { bio: 'A developer' },
108
+ },
109
+ }).success
110
+ ).toBe(true)
85
111
  })
86
112
 
87
113
  it('handles mixed types in object', () => {
@@ -92,7 +118,17 @@ describe('schema', () => {
92
118
  status: 'pending | done',
93
119
  tags: ['Tags'],
94
120
  })
95
- expect(result._def.typeName).toBe('ZodObject')
121
+ expect(result instanceof z.ZodObject).toBe(true)
122
+ // Verify mixed type validation
123
+ expect(
124
+ result.safeParse({
125
+ name: 'Test',
126
+ count: 5,
127
+ active: true,
128
+ status: 'pending',
129
+ tags: ['tag1', 'tag2'],
130
+ }).success
131
+ ).toBe(true)
96
132
  })
97
133
  })
98
134