ai-functions 2.1.1 → 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 (286) hide show
  1. package/.turbo/turbo-build.log +1 -4
  2. package/CHANGELOG.md +68 -1
  3. package/README.md +397 -157
  4. package/dist/ai-promise.d.ts +50 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +410 -51
  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 +54 -837
  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 +272 -0
  56. package/dist/budget.d.ts.map +1 -0
  57. package/dist/budget.js +513 -0
  58. package/dist/budget.js.map +1 -0
  59. package/dist/cache.d.ts +295 -0
  60. package/dist/cache.d.ts.map +1 -0
  61. package/dist/cache.js +433 -0
  62. package/dist/cache.js.map +1 -0
  63. package/dist/context.d.ts +42 -8
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +64 -62
  66. package/dist/context.js.map +1 -1
  67. package/dist/digital-objects-registry.d.ts +229 -0
  68. package/dist/digital-objects-registry.d.ts.map +1 -0
  69. package/dist/digital-objects-registry.js +617 -0
  70. package/dist/digital-objects-registry.js.map +1 -0
  71. package/dist/embeddings.d.ts +2 -2
  72. package/dist/embeddings.d.ts.map +1 -1
  73. package/dist/errors.d.ts +22 -0
  74. package/dist/errors.d.ts.map +1 -0
  75. package/dist/errors.js +35 -0
  76. package/dist/errors.js.map +1 -0
  77. package/dist/eval/runner.d.ts +10 -1
  78. package/dist/eval/runner.d.ts.map +1 -1
  79. package/dist/eval/runner.js +41 -35
  80. package/dist/eval/runner.js.map +1 -1
  81. package/dist/eval-log/in-memory.d.ts +34 -0
  82. package/dist/eval-log/in-memory.d.ts.map +1 -0
  83. package/dist/eval-log/in-memory.js +84 -0
  84. package/dist/eval-log/in-memory.js.map +1 -0
  85. package/dist/eval-log/index.d.ts +29 -0
  86. package/dist/eval-log/index.d.ts.map +1 -0
  87. package/dist/eval-log/index.js +39 -0
  88. package/dist/eval-log/index.js.map +1 -0
  89. package/dist/eval-log/types.d.ts +101 -0
  90. package/dist/eval-log/types.d.ts.map +1 -0
  91. package/dist/eval-log/types.js +16 -0
  92. package/dist/eval-log/types.js.map +1 -0
  93. package/dist/function-registry.d.ts +116 -0
  94. package/dist/function-registry.d.ts.map +1 -0
  95. package/dist/function-registry.js +546 -0
  96. package/dist/function-registry.js.map +1 -0
  97. package/dist/generate.d.ts +9 -3
  98. package/dist/generate.d.ts.map +1 -1
  99. package/dist/generate.js +18 -22
  100. package/dist/generate.js.map +1 -1
  101. package/dist/index.d.ts +35 -20
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +89 -42
  104. package/dist/index.js.map +1 -1
  105. package/dist/logger.d.ts +118 -0
  106. package/dist/logger.d.ts.map +1 -0
  107. package/dist/logger.js +187 -0
  108. package/dist/logger.js.map +1 -0
  109. package/dist/middleware/budget.d.ts +84 -0
  110. package/dist/middleware/budget.d.ts.map +1 -0
  111. package/dist/middleware/budget.js +110 -0
  112. package/dist/middleware/budget.js.map +1 -0
  113. package/dist/middleware/cache.d.ts +103 -0
  114. package/dist/middleware/cache.d.ts.map +1 -0
  115. package/dist/middleware/cache.js +228 -0
  116. package/dist/middleware/cache.js.map +1 -0
  117. package/dist/middleware/embed-cache.d.ts +99 -0
  118. package/dist/middleware/embed-cache.d.ts.map +1 -0
  119. package/dist/middleware/embed-cache.js +128 -0
  120. package/dist/middleware/embed-cache.js.map +1 -0
  121. package/dist/middleware/index.d.ts +11 -0
  122. package/dist/middleware/index.d.ts.map +1 -0
  123. package/dist/middleware/index.js +11 -0
  124. package/dist/middleware/index.js.map +1 -0
  125. package/dist/middleware/trace.d.ts +103 -0
  126. package/dist/middleware/trace.d.ts.map +1 -0
  127. package/dist/middleware/trace.js +176 -0
  128. package/dist/middleware/trace.js.map +1 -0
  129. package/dist/primitives.d.ts +120 -1
  130. package/dist/primitives.d.ts.map +1 -1
  131. package/dist/primitives.js +398 -26
  132. package/dist/primitives.js.map +1 -1
  133. package/dist/retry.d.ts +368 -0
  134. package/dist/retry.d.ts.map +1 -0
  135. package/dist/retry.js +646 -0
  136. package/dist/retry.js.map +1 -0
  137. package/dist/schema.d.ts.map +1 -1
  138. package/dist/schema.js +2 -10
  139. package/dist/schema.js.map +1 -1
  140. package/dist/telemetry.d.ts +128 -0
  141. package/dist/telemetry.d.ts.map +1 -0
  142. package/dist/telemetry.js +285 -0
  143. package/dist/telemetry.js.map +1 -0
  144. package/dist/template.d.ts.map +1 -1
  145. package/dist/template.js +6 -1
  146. package/dist/template.js.map +1 -1
  147. package/dist/tool-orchestration.d.ts +453 -0
  148. package/dist/tool-orchestration.d.ts.map +1 -0
  149. package/dist/tool-orchestration.js +763 -0
  150. package/dist/tool-orchestration.js.map +1 -0
  151. package/dist/type-guards.d.ts +28 -0
  152. package/dist/type-guards.d.ts.map +1 -0
  153. package/dist/type-guards.js +29 -0
  154. package/dist/type-guards.js.map +1 -0
  155. package/dist/types.d.ts +135 -17
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/types.js +36 -1
  158. package/dist/types.js.map +1 -1
  159. package/dist/wrap-for-v3.d.ts +80 -0
  160. package/dist/wrap-for-v3.d.ts.map +1 -0
  161. package/dist/wrap-for-v3.js +89 -0
  162. package/dist/wrap-for-v3.js.map +1 -0
  163. package/examples/00-quickstart.ts +232 -0
  164. package/examples/01-rag-chatbot.ts +212 -0
  165. package/examples/02-multi-agent-research.ts +290 -0
  166. package/examples/03-email-classification.ts +379 -0
  167. package/examples/04-content-moderation.ts +400 -0
  168. package/examples/05-document-extraction.ts +455 -0
  169. package/examples/06-streaming-chat-nextjs.ts +437 -0
  170. package/examples/07-cloudflare-worker.ts +483 -0
  171. package/examples/08-batch-processing.ts +491 -0
  172. package/examples/09-budget-constrained.ts +527 -0
  173. package/examples/10-tool-orchestration.ts +565 -0
  174. package/examples/11-retry-resilience.ts +403 -0
  175. package/examples/12-caching-strategies.ts +422 -0
  176. package/examples/README.md +145 -0
  177. package/package.json +10 -6
  178. package/src/ai-promise.ts +528 -99
  179. package/src/ai-schemas.ts +122 -0
  180. package/src/ai.ts +69 -1153
  181. package/src/batch/anthropic.ts +96 -161
  182. package/src/batch/bedrock.ts +203 -454
  183. package/src/batch/cloudflare.ts +99 -282
  184. package/src/batch/google.ts +91 -297
  185. package/src/batch/index.ts +4 -1
  186. package/src/batch/memory.ts +15 -10
  187. package/src/batch/openai.ts +65 -193
  188. package/src/batch/provider.ts +336 -0
  189. package/src/batch-map.ts +29 -24
  190. package/src/batch-queue.ts +200 -11
  191. package/src/budget.ts +740 -0
  192. package/src/cache.ts +681 -0
  193. package/src/context.ts +122 -76
  194. package/src/digital-objects-registry.ts +750 -0
  195. package/src/errors.ts +37 -0
  196. package/src/eval/runner.ts +63 -38
  197. package/src/eval-log/in-memory.ts +90 -0
  198. package/src/eval-log/index.ts +46 -0
  199. package/src/eval-log/types.ts +110 -0
  200. package/src/function-registry.ts +671 -0
  201. package/src/generate.ts +33 -33
  202. package/src/index.ts +325 -49
  203. package/src/logger.ts +232 -0
  204. package/src/middleware/budget.ts +171 -0
  205. package/src/middleware/cache.ts +299 -0
  206. package/src/middleware/embed-cache.ts +195 -0
  207. package/src/middleware/index.ts +23 -0
  208. package/src/middleware/trace.ts +248 -0
  209. package/src/primitives.ts +589 -62
  210. package/src/retry.ts +902 -0
  211. package/src/schema.ts +8 -17
  212. package/src/telemetry.ts +403 -0
  213. package/src/template.ts +8 -4
  214. package/src/tool-orchestration.ts +1173 -0
  215. package/src/type-guards.ts +31 -0
  216. package/src/types.ts +164 -25
  217. package/src/wrap-for-v3.ts +105 -0
  218. package/test/ai-promise.test.ts +1080 -0
  219. package/test/ai-proxy.test.ts +1 -1
  220. package/test/backward-compat.test.ts +147 -0
  221. package/test/batch-autosubmit-errors.test.ts +610 -0
  222. package/test/batch-blog-posts.test.ts +87 -129
  223. package/test/budget-tracking.test.ts +800 -0
  224. package/test/cache.test.ts +712 -0
  225. package/test/context-isolation.test.ts +687 -0
  226. package/test/core-functions.test.ts +183 -579
  227. package/test/decide.test.ts +154 -322
  228. package/test/define.test.ts +211 -8
  229. package/test/digital-objects-registry.test.ts +760 -0
  230. package/test/embedding-cache-middleware.test.ts +140 -0
  231. package/test/evals/deterministic.eval.test.ts +376 -0
  232. package/test/generate-core.test.ts +140 -229
  233. package/test/implicit-batch.test.ts +22 -65
  234. package/test/json-parse-error-handling.test.ts +463 -0
  235. package/test/retry-policy-integration.test.ts +117 -0
  236. package/test/retry.test.ts +1016 -0
  237. package/test/schema.test.ts +55 -19
  238. package/test/streaming.test.ts +316 -0
  239. package/test/template.test.ts +1164 -0
  240. package/test/tool-orchestration.test.ts +1040 -0
  241. package/test/wrap-for-v3.test.ts +612 -0
  242. package/vitest.config.js +6 -0
  243. package/vitest.config.ts +20 -0
  244. package/dist/rpc/auth.d.ts +0 -69
  245. package/dist/rpc/auth.d.ts.map +0 -1
  246. package/dist/rpc/auth.js +0 -136
  247. package/dist/rpc/auth.js.map +0 -1
  248. package/dist/rpc/client.d.ts +0 -62
  249. package/dist/rpc/client.d.ts.map +0 -1
  250. package/dist/rpc/client.js +0 -103
  251. package/dist/rpc/client.js.map +0 -1
  252. package/dist/rpc/deferred.d.ts +0 -60
  253. package/dist/rpc/deferred.d.ts.map +0 -1
  254. package/dist/rpc/deferred.js +0 -96
  255. package/dist/rpc/deferred.js.map +0 -1
  256. package/dist/rpc/index.d.ts +0 -22
  257. package/dist/rpc/index.d.ts.map +0 -1
  258. package/dist/rpc/index.js +0 -38
  259. package/dist/rpc/index.js.map +0 -1
  260. package/dist/rpc/local.d.ts +0 -42
  261. package/dist/rpc/local.d.ts.map +0 -1
  262. package/dist/rpc/local.js +0 -50
  263. package/dist/rpc/local.js.map +0 -1
  264. package/dist/rpc/server.d.ts +0 -165
  265. package/dist/rpc/server.d.ts.map +0 -1
  266. package/dist/rpc/server.js +0 -405
  267. package/dist/rpc/server.js.map +0 -1
  268. package/dist/rpc/session.d.ts +0 -32
  269. package/dist/rpc/session.d.ts.map +0 -1
  270. package/dist/rpc/session.js +0 -43
  271. package/dist/rpc/session.js.map +0 -1
  272. package/dist/rpc/transport.d.ts +0 -306
  273. package/dist/rpc/transport.d.ts.map +0 -1
  274. package/dist/rpc/transport.js +0 -731
  275. package/dist/rpc/transport.js.map +0 -1
  276. package/src/batch/anthropic.js +0 -256
  277. package/src/batch/bedrock.js +0 -584
  278. package/src/batch/cloudflare.js +0 -287
  279. package/src/batch/google.js +0 -359
  280. package/src/batch/index.js +0 -30
  281. package/src/batch/memory.js +0 -187
  282. package/src/batch/openai.js +0 -402
  283. package/src/eval/index.js +0 -7
  284. package/src/eval/models.js +0 -119
  285. package/src/eval/runner.js +0 -147
  286. package/test/schema.test.js +0 -96
@@ -2,327 +2,170 @@
2
2
  * Tests for the decide() function - LLM as Judge
3
3
  *
4
4
  * decide`criteria`(optionA, optionB) - Compare options and pick the best
5
+ *
6
+ * Tests require actual AI calls via the Cloudflare AI Gateway for real decisions.
5
7
  */
6
8
 
7
- import { describe, it, expect, vi, beforeEach } from 'vitest'
8
-
9
- // ============================================================================
10
- // Mock for underlying AI calls
11
- // ============================================================================
12
-
13
- const mockDecide = vi.fn()
9
+ import { describe, it, expect } from 'vitest'
10
+ import { generateObject } from '../src/generate.js'
11
+ import { z } from 'zod'
14
12
 
15
- /**
16
- * Mock decide implementation
17
- * Returns a function that compares options against criteria
18
- */
19
- function createMockDecide() {
20
- return function decide(
21
- promptOrStrings: string | TemplateStringsArray,
22
- ...args: unknown[]
23
- ) {
24
- let criteria: string
25
-
26
- if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
27
- criteria = (promptOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
28
- return acc + str + (args[i] ?? '')
29
- }, '')
30
- } else {
31
- criteria = promptOrStrings as string
32
- }
33
-
34
- // Return a function that accepts options to compare
35
- return function compareOptions<T>(...options: T[]): Promise<T> {
36
- return mockDecide(criteria, options)
37
- }
38
- }
39
- }
13
+ // Skip tests if no gateway configured
14
+ const hasGateway = !!process.env.AI_GATEWAY_URL
40
15
 
41
16
  // ============================================================================
42
- // Basic decide() tests
17
+ // Real AI Decision Tests
43
18
  // ============================================================================
44
19
 
45
- describe('decide() - LLM as Judge', () => {
46
- beforeEach(() => {
47
- mockDecide.mockReset()
48
- })
49
-
20
+ describe.skipIf(!hasGateway)('decide() - LLM as Judge with Real AI', () => {
50
21
  describe('basic comparison', () => {
51
22
  it('compares two options and returns the better one', async () => {
52
- const decide = createMockDecide()
53
-
54
23
  const optionA = { name: 'Simple Solution', complexity: 'low', time: '1 day' }
55
24
  const optionB = { name: 'Complex Solution', complexity: 'high', time: '1 week' }
56
25
 
57
- mockDecide.mockResolvedValue(optionA)
58
-
59
- const result = await decide`fastest to implement`(optionA, optionB)
60
-
61
- expect(mockDecide).toHaveBeenCalledWith(
62
- 'fastest to implement',
63
- [optionA, optionB]
64
- )
65
- expect(result).toBe(optionA)
66
- })
67
-
68
- it('compares multiple options', async () => {
69
- const decide = createMockDecide()
70
-
71
- const options = [
72
- { framework: 'React', popularity: 'high' },
73
- { framework: 'Vue', popularity: 'medium' },
74
- { framework: 'Angular', popularity: 'medium' },
75
- { framework: 'Svelte', popularity: 'growing' },
76
- ]
77
-
78
- mockDecide.mockResolvedValue(options[0])
79
-
80
- const result = await decide`best for large enterprise applications`(
81
- options[0],
82
- options[1],
83
- options[2],
84
- options[3]
85
- )
86
-
87
- expect(mockDecide).toHaveBeenCalledWith(
88
- 'best for large enterprise applications',
89
- options
90
- )
91
- expect(result).toEqual({ framework: 'React', popularity: 'high' })
26
+ const result = await generateObject({
27
+ model: 'haiku',
28
+ schema: z.object({
29
+ winner: z.enum(['A', 'B']).describe('Which option is faster to implement?'),
30
+ reasoning: z.string().describe('Brief explanation'),
31
+ }),
32
+ prompt: `Compare these options and pick the fastest to implement:
33
+ Option A: ${JSON.stringify(optionA)}
34
+ Option B: ${JSON.stringify(optionB)}`,
35
+ })
36
+
37
+ expect(['A', 'B']).toContain(result.object.winner)
38
+ // Simple solution should be faster
39
+ expect(result.object.winner).toBe('A')
40
+ expect(result.object.reasoning.length).toBeGreaterThan(0)
92
41
  })
93
42
 
94
43
  it('works with string options', async () => {
95
- const decide = createMockDecide()
96
-
97
- mockDecide.mockResolvedValue('Option B: More engaging title')
98
-
99
- const result = await decide`better headline for click-through rate`(
100
- 'Option A: Simple Guide to TypeScript',
101
- 'Option B: More engaging title'
102
- )
103
-
104
- expect(result).toBe('Option B: More engaging title')
44
+ const result = await generateObject({
45
+ model: 'haiku',
46
+ schema: z.object({
47
+ winner: z.enum(['A', 'B']).describe('Which headline is better for developers?'),
48
+ }),
49
+ prompt: `Which headline is better for a developer audience?
50
+ A: "Simple Guide to TypeScript"
51
+ B: "TypeScript: Advanced Patterns for Enterprise"`,
52
+ })
53
+
54
+ expect(['A', 'B']).toContain(result.object.winner)
105
55
  })
106
56
  })
107
57
 
108
- describe('criteria as tagged template', () => {
109
- it('supports variable interpolation in criteria', async () => {
110
- const decide = createMockDecide()
111
- const audience = 'enterprise developers'
112
-
113
- mockDecide.mockResolvedValue({ headline: 'A' })
114
-
115
- await decide`best for ${audience}`(
116
- { headline: 'A' },
117
- { headline: 'B' }
118
- )
119
-
120
- expect(mockDecide).toHaveBeenCalledWith(
121
- `best for ${audience}`,
122
- expect.any(Array)
123
- )
124
- })
125
-
126
- it('supports complex criteria descriptions', async () => {
127
- const decide = createMockDecide()
128
-
129
- const requirements = {
130
- performance: 'high',
131
- maintainability: 'critical',
132
- teamExperience: 'junior',
133
- }
134
-
135
- mockDecide.mockResolvedValue({ approach: 'A' })
136
-
137
- // Criteria would have requirements as YAML
138
- await decide`best architecture considering team and requirements`(
139
- { approach: 'A' },
140
- { approach: 'B' }
141
- )
142
-
143
- expect(mockDecide).toHaveBeenCalled()
58
+ describe('multiple options comparison', () => {
59
+ it('compares multiple framework options', async () => {
60
+ const result = await generateObject({
61
+ model: 'haiku',
62
+ schema: z.object({
63
+ winner: z.enum(['React', 'Vue', 'Angular', 'Svelte']).describe('Best for enterprise'),
64
+ reasoning: z.string().describe('Brief explanation'),
65
+ }),
66
+ prompt: `Which frontend framework is best for large enterprise applications with many developers?
67
+ Options: React, Vue, Angular, Svelte`,
68
+ })
69
+
70
+ expect(['React', 'Vue', 'Angular', 'Svelte']).toContain(result.object.winner)
71
+ expect(result.object.reasoning.length).toBeGreaterThan(0)
144
72
  })
145
73
  })
146
74
 
147
- describe('use cases from README', () => {
148
- it('A/B testing headlines', async () => {
149
- const decide = createMockDecide()
150
-
75
+ describe('A/B testing use case', () => {
76
+ it('evaluates headline click-through potential', async () => {
151
77
  const headlineA = 'Get Started with TypeScript Today'
152
78
  const headlineB = 'TypeScript: The Complete Guide for 2025'
153
79
 
154
- mockDecide.mockResolvedValue(headlineB)
155
-
156
- const winner = await decide`higher click-through rate for developer audience`(
157
- headlineA,
158
- headlineB
159
- )
160
-
161
- expect(winner).toBe(headlineB)
162
- })
163
-
164
- it('choosing best code implementation', async () => {
165
- const decide = createMockDecide()
166
-
167
- const implementations = [
168
- `function isPrime(n) {
169
- if (n <= 1) return false;
170
- for (let i = 2; i <= Math.sqrt(n); i++) {
171
- if (n % i === 0) return false;
172
- }
173
- return true;
174
- }`,
175
- `function isPrime(n) {
176
- if (n <= 1) return false;
177
- if (n <= 3) return true;
178
- if (n % 2 === 0 || n % 3 === 0) return false;
179
- for (let i = 5; i * i <= n; i += 6) {
180
- if (n % i === 0 || n % (i + 2) === 0) return false;
181
- }
182
- return true;
183
- }`,
184
- ]
185
-
186
- mockDecide.mockResolvedValue(implementations[1])
187
-
188
- const best = await decide`most performant and readable`(
189
- implementations[0],
190
- implementations[1]
191
- )
192
-
193
- expect(best).toBe(implementations[1])
80
+ const result = await generateObject({
81
+ model: 'haiku',
82
+ schema: z.object({
83
+ winner: z
84
+ .enum(['A', 'B'])
85
+ .describe('Which headline would have higher click-through rate?'),
86
+ confidence: z.number().min(0).max(1).describe('Confidence score'),
87
+ }),
88
+ prompt: `For a developer audience, which headline would have higher click-through rate?
89
+ Headline A: "${headlineA}"
90
+ Headline B: "${headlineB}"`,
91
+ })
92
+
93
+ expect(['A', 'B']).toContain(result.object.winner)
94
+ expect(result.object.confidence).toBeGreaterThanOrEqual(0)
95
+ expect(result.object.confidence).toBeLessThanOrEqual(1)
194
96
  })
195
-
196
- it('selecting marketing copy', async () => {
197
- const decide = createMockDecide()
198
-
199
- const copies = [
200
- { text: 'Build faster with AI', tone: 'direct' },
201
- { text: 'Empower your workflow', tone: 'inspirational' },
202
- { text: '10x your productivity', tone: 'bold' },
203
- ]
204
-
205
- mockDecide.mockResolvedValue(copies[2])
206
-
207
- const winner = await decide`most compelling for startup founders`(
208
- copies[0],
209
- copies[1],
210
- copies[2]
211
- )
212
-
213
- expect(winner).toEqual({ text: '10x your productivity', tone: 'bold' })
214
- })
215
- })
216
- })
217
-
218
- // ============================================================================
219
- // decide() with options parameter
220
- // ============================================================================
221
-
222
- describe('decide() with options', () => {
223
- beforeEach(() => {
224
- mockDecide.mockReset()
225
- })
226
-
227
- it('accepts model option', async () => {
228
- // This tests the concept - actual implementation would pass options
229
- const mockDecideWithOptions = vi.fn()
230
-
231
- function createMockDecideWithOptions() {
232
- return function decide(
233
- promptOrStrings: string | TemplateStringsArray,
234
- ...args: unknown[]
235
- ) {
236
- let criteria: string
237
-
238
- if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
239
- criteria = (promptOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
240
- return acc + str + (args[i] ?? '')
241
- }, '')
242
- } else {
243
- criteria = promptOrStrings as string
244
- }
245
-
246
- return function compareOptions<T>(...options: T[]) {
247
- // Support options as last parameter if it's an object with known keys
248
- const lastArg = options[options.length - 1] as Record<string, unknown>
249
- if (lastArg && typeof lastArg === 'object' && 'model' in lastArg) {
250
- const realOptions = options.slice(0, -1)
251
- return mockDecideWithOptions(criteria, realOptions, lastArg)
252
- }
253
- return mockDecideWithOptions(criteria, options, undefined)
254
- }
255
- }
256
- }
257
-
258
- const decide = createMockDecideWithOptions()
259
- mockDecideWithOptions.mockResolvedValue('A')
260
-
261
- // Two ways to pass options:
262
- // 1. Separate options object
263
- await decide`better option`('A', 'B')
264
-
265
- expect(mockDecideWithOptions).toHaveBeenCalledWith(
266
- 'better option',
267
- ['A', 'B'],
268
- undefined
269
- )
270
97
  })
271
98
 
272
- it('accepts thinking option for complex decisions', async () => {
273
- // Complex decisions might benefit from extended thinking
274
- const mockDecideWithThinking = vi.fn().mockResolvedValue('Option A')
275
-
276
- // Simulating how decide might use thinking
277
- const decision = mockDecideWithThinking('complex criteria', ['A', 'B'], {
278
- thinking: 'high',
99
+ describe('code comparison use case', () => {
100
+ it('compares code implementations', async () => {
101
+ const impl1 = `function isPrime(n) {
102
+ if (n <= 1) return false;
103
+ for (let i = 2; i <= Math.sqrt(n); i++) {
104
+ if (n % i === 0) return false;
105
+ }
106
+ return true;
107
+ }`
108
+
109
+ const impl2 = `function isPrime(n) {
110
+ if (n <= 1) return false;
111
+ if (n <= 3) return true;
112
+ if (n % 2 === 0 || n % 3 === 0) return false;
113
+ for (let i = 5; i * i <= n; i += 6) {
114
+ if (n % i === 0 || n % (i + 2) === 0) return false;
115
+ }
116
+ return true;
117
+ }`
118
+
119
+ const result = await generateObject({
120
+ model: 'haiku',
121
+ schema: z.object({
122
+ winner: z.enum(['A', 'B']).describe('Most performant implementation'),
123
+ reasoning: z.string().describe('Why this is more performant'),
124
+ }),
125
+ prompt: `Which isPrime implementation is more performant?
126
+ Implementation A:
127
+ ${impl1}
128
+
129
+ Implementation B:
130
+ ${impl2}`,
131
+ })
132
+
133
+ expect(['A', 'B']).toContain(result.object.winner)
134
+ // The second implementation with 6k optimization should be faster
135
+ expect(result.object.winner).toBe('B')
279
136
  })
280
-
281
- expect(mockDecideWithThinking).toHaveBeenCalledWith(
282
- 'complex criteria',
283
- ['A', 'B'],
284
- { thinking: 'high' }
285
- )
286
137
  })
287
138
  })
288
139
 
289
140
  // ============================================================================
290
- // decide() return value
141
+ // Type Inference Pattern Tests (no AI needed)
291
142
  // ============================================================================
292
143
 
293
- describe('decide() return value', () => {
294
- beforeEach(() => {
295
- mockDecide.mockReset()
296
- })
297
-
298
- it('returns the exact option object passed in', async () => {
299
- const decide = createMockDecide()
300
-
301
- const optionA = { id: 1, name: 'Option A', metadata: { score: 10 } }
302
- const optionB = { id: 2, name: 'Option B', metadata: { score: 20 } }
144
+ describe('decide() pattern tests', () => {
145
+ it('documents the decide function signature', () => {
146
+ // decide`criteria`(option1, option2, ...) returns the winning option
147
+ // The return type matches the input option type
303
148
 
304
- // Return the exact reference, not a copy
305
- mockDecide.mockResolvedValue(optionA)
149
+ type DecideSignature<T> = (criteria: string, options: T[]) => Promise<T>
306
150
 
307
- const result = await decide`best option`(optionA, optionB)
308
-
309
- // Should be the exact same reference
310
- expect(result).toBe(optionA)
151
+ // Verify the type signature compiles
152
+ const signature: DecideSignature<{ name: string }> = async (_criteria, options) => options[0]
153
+ expect(typeof signature).toBe('function')
311
154
  })
312
155
 
313
- it('can return with reasoning (extended mode)', async () => {
314
- // Extended mode returns both the decision and reasoning
315
- const mockDecideWithReasoning = vi.fn()
156
+ it('documents extended mode with reasoning', () => {
157
+ // Extended mode returns both the choice and reasoning
158
+ type DecideWithReasoning<T> = {
159
+ choice: T
160
+ reasoning: string
161
+ confidence: number
162
+ }
316
163
 
317
- mockDecideWithReasoning.mockResolvedValue({
164
+ const result: DecideWithReasoning<string> = {
318
165
  choice: 'Option A',
319
- reasoning: 'Option A is better because it has lower complexity',
166
+ reasoning: 'Better for the use case',
320
167
  confidence: 0.85,
321
- })
322
-
323
- const result = await mockDecideWithReasoning('criteria', ['A', 'B'], {
324
- explain: true,
325
- })
168
+ }
326
169
 
327
170
  expect(result).toHaveProperty('choice')
328
171
  expect(result).toHaveProperty('reasoning')
@@ -331,63 +174,52 @@ describe('decide() return value', () => {
331
174
  })
332
175
 
333
176
  // ============================================================================
334
- // Edge cases
177
+ // Edge Cases
335
178
  // ============================================================================
336
179
 
337
- describe('decide() edge cases', () => {
338
- beforeEach(() => {
339
- mockDecide.mockReset()
340
- })
341
-
180
+ describe.skipIf(!hasGateway)('decide() edge cases', () => {
342
181
  it('handles identical options', async () => {
343
- const decide = createMockDecide()
344
-
345
- mockDecide.mockResolvedValue('Same')
346
-
347
- const result = await decide`better one`('Same', 'Same')
348
-
349
- expect(result).toBe('Same')
350
- })
351
-
352
- it('handles single option (validation use case)', async () => {
353
- const decide = createMockDecide()
354
-
355
- const option = { content: 'Some text' }
356
- mockDecide.mockResolvedValue(option)
357
-
358
- // Single option - essentially asking "is this good enough?"
359
- const result = await decide`meets quality standards`(option)
182
+ const result = await generateObject({
183
+ model: 'haiku',
184
+ schema: z.object({
185
+ winner: z.enum(['A', 'B']).describe('Which is better?'),
186
+ areSimilar: z.boolean().describe('Are the options essentially the same?'),
187
+ }),
188
+ prompt: `Which option is better?
189
+ Option A: "Hello World"
190
+ Option B: "Hello World"`,
191
+ })
360
192
 
361
- expect(mockDecide).toHaveBeenCalledWith('meets quality standards', [option])
193
+ expect(['A', 'B']).toContain(result.object.winner)
194
+ expect(result.object.areSimilar).toBe(true)
362
195
  })
363
196
 
364
197
  it('handles complex nested objects', async () => {
365
- const decide = createMockDecide()
366
-
367
198
  const architectureA = {
368
199
  name: 'Microservices',
369
- components: {
370
- gateway: { type: 'API Gateway', tech: 'Kong' },
371
- services: ['auth', 'users', 'products'],
372
- database: { type: 'distributed', engine: 'CockroachDB' },
373
- },
200
+ components: ['gateway', 'auth', 'users', 'products'],
201
+ database: 'distributed',
374
202
  }
375
203
 
376
204
  const architectureB = {
377
205
  name: 'Monolith',
378
- components: {
379
- app: { type: 'Monolithic', tech: 'NestJS' },
380
- database: { type: 'single', engine: 'PostgreSQL' },
381
- },
206
+ components: ['app'],
207
+ database: 'single',
382
208
  }
383
209
 
384
- mockDecide.mockResolvedValue(architectureA)
385
-
386
- const result = await decide`better for a startup with 3 developers`(
387
- architectureA,
388
- architectureB
389
- )
210
+ const result = await generateObject({
211
+ model: 'haiku',
212
+ schema: z.object({
213
+ winner: z.enum(['Microservices', 'Monolith']).describe('Better for a 3-developer startup'),
214
+ reasoning: z.string(),
215
+ }),
216
+ prompt: `Which architecture is better for a startup with 3 developers?
217
+ Option 1: ${JSON.stringify(architectureA)}
218
+ Option 2: ${JSON.stringify(architectureB)}`,
219
+ })
390
220
 
391
- expect(result.name).toBe('Microservices')
221
+ expect(['Microservices', 'Monolith']).toContain(result.object.winner)
222
+ // Monolith is typically better for small teams
223
+ expect(result.object.winner).toBe('Monolith')
392
224
  })
393
225
  })