ai-functions 2.1.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +90 -1
  3. package/README.md +38 -0
  4. package/dist/ai-promise.d.ts +3 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +135 -64
  7. package/dist/ai-promise.js.map +1 -1
  8. package/dist/ai-schemas.d.ts +56 -0
  9. package/dist/ai-schemas.d.ts.map +1 -0
  10. package/dist/ai-schemas.js +53 -0
  11. package/dist/ai-schemas.js.map +1 -0
  12. package/dist/ai.d.ts +16 -242
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +51 -858
  15. package/dist/ai.js.map +1 -1
  16. package/dist/batch/anthropic.d.ts +6 -4
  17. package/dist/batch/anthropic.d.ts.map +1 -1
  18. package/dist/batch/anthropic.js +83 -145
  19. package/dist/batch/anthropic.js.map +1 -1
  20. package/dist/batch/bedrock.d.ts +8 -30
  21. package/dist/batch/bedrock.d.ts.map +1 -1
  22. package/dist/batch/bedrock.js +155 -338
  23. package/dist/batch/bedrock.js.map +1 -1
  24. package/dist/batch/cloudflare.d.ts +8 -20
  25. package/dist/batch/cloudflare.d.ts.map +1 -1
  26. package/dist/batch/cloudflare.js +68 -189
  27. package/dist/batch/cloudflare.js.map +1 -1
  28. package/dist/batch/google.d.ts +6 -20
  29. package/dist/batch/google.d.ts.map +1 -1
  30. package/dist/batch/google.js +70 -238
  31. package/dist/batch/google.js.map +1 -1
  32. package/dist/batch/index.d.ts +4 -1
  33. package/dist/batch/index.d.ts.map +1 -1
  34. package/dist/batch/index.js +4 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/batch/memory.d.ts +1 -1
  37. package/dist/batch/memory.d.ts.map +1 -1
  38. package/dist/batch/memory.js +14 -10
  39. package/dist/batch/memory.js.map +1 -1
  40. package/dist/batch/openai.d.ts +11 -14
  41. package/dist/batch/openai.d.ts.map +1 -1
  42. package/dist/batch/openai.js +52 -156
  43. package/dist/batch/openai.js.map +1 -1
  44. package/dist/batch/provider.d.ts +111 -0
  45. package/dist/batch/provider.d.ts.map +1 -0
  46. package/dist/batch/provider.js +233 -0
  47. package/dist/batch/provider.js.map +1 -0
  48. package/dist/batch-map.d.ts.map +1 -1
  49. package/dist/batch-map.js +23 -17
  50. package/dist/batch-map.js.map +1 -1
  51. package/dist/batch-queue.d.ts +65 -0
  52. package/dist/batch-queue.d.ts.map +1 -1
  53. package/dist/batch-queue.js +169 -14
  54. package/dist/batch-queue.js.map +1 -1
  55. package/dist/budget.d.ts.map +1 -1
  56. package/dist/budget.js +27 -14
  57. package/dist/budget.js.map +1 -1
  58. package/dist/cache.d.ts +23 -0
  59. package/dist/cache.d.ts.map +1 -1
  60. package/dist/cache.js +36 -15
  61. package/dist/cache.js.map +1 -1
  62. package/dist/context.d.ts +26 -8
  63. package/dist/context.d.ts.map +1 -1
  64. package/dist/context.js +64 -62
  65. package/dist/context.js.map +1 -1
  66. package/dist/digital-objects-registry.d.ts +229 -0
  67. package/dist/digital-objects-registry.d.ts.map +1 -0
  68. package/dist/digital-objects-registry.js +617 -0
  69. package/dist/digital-objects-registry.js.map +1 -0
  70. package/dist/embeddings.d.ts +2 -2
  71. package/dist/embeddings.d.ts.map +1 -1
  72. package/dist/errors.d.ts +22 -0
  73. package/dist/errors.d.ts.map +1 -0
  74. package/dist/errors.js +35 -0
  75. package/dist/errors.js.map +1 -0
  76. package/dist/eval/runner.d.ts +8 -0
  77. package/dist/eval/runner.d.ts.map +1 -1
  78. package/dist/eval/runner.js +41 -35
  79. package/dist/eval/runner.js.map +1 -1
  80. package/dist/eval-log/in-memory.d.ts +34 -0
  81. package/dist/eval-log/in-memory.d.ts.map +1 -0
  82. package/dist/eval-log/in-memory.js +84 -0
  83. package/dist/eval-log/in-memory.js.map +1 -0
  84. package/dist/eval-log/index.d.ts +29 -0
  85. package/dist/eval-log/index.d.ts.map +1 -0
  86. package/dist/eval-log/index.js +39 -0
  87. package/dist/eval-log/index.js.map +1 -0
  88. package/dist/eval-log/types.d.ts +101 -0
  89. package/dist/eval-log/types.d.ts.map +1 -0
  90. package/dist/eval-log/types.js +16 -0
  91. package/dist/eval-log/types.js.map +1 -0
  92. package/dist/function-registry.d.ts +176 -0
  93. package/dist/function-registry.d.ts.map +1 -0
  94. package/dist/function-registry.js +685 -0
  95. package/dist/function-registry.js.map +1 -0
  96. package/dist/generate.d.ts +9 -3
  97. package/dist/generate.d.ts.map +1 -1
  98. package/dist/generate.js +18 -18
  99. package/dist/generate.js.map +1 -1
  100. package/dist/index.d.ts +18 -11
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +35 -18
  103. package/dist/index.js.map +1 -1
  104. package/dist/logger.d.ts +118 -0
  105. package/dist/logger.d.ts.map +1 -0
  106. package/dist/logger.js +187 -0
  107. package/dist/logger.js.map +1 -0
  108. package/dist/middleware/budget.d.ts +84 -0
  109. package/dist/middleware/budget.d.ts.map +1 -0
  110. package/dist/middleware/budget.js +110 -0
  111. package/dist/middleware/budget.js.map +1 -0
  112. package/dist/middleware/cache.d.ts +103 -0
  113. package/dist/middleware/cache.d.ts.map +1 -0
  114. package/dist/middleware/cache.js +228 -0
  115. package/dist/middleware/cache.js.map +1 -0
  116. package/dist/middleware/embed-cache.d.ts +99 -0
  117. package/dist/middleware/embed-cache.d.ts.map +1 -0
  118. package/dist/middleware/embed-cache.js +128 -0
  119. package/dist/middleware/embed-cache.js.map +1 -0
  120. package/dist/middleware/index.d.ts +11 -0
  121. package/dist/middleware/index.d.ts.map +1 -0
  122. package/dist/middleware/index.js +11 -0
  123. package/dist/middleware/index.js.map +1 -0
  124. package/dist/middleware/trace.d.ts +103 -0
  125. package/dist/middleware/trace.d.ts.map +1 -0
  126. package/dist/middleware/trace.js +176 -0
  127. package/dist/middleware/trace.js.map +1 -0
  128. package/dist/primitives.d.ts +120 -1
  129. package/dist/primitives.d.ts.map +1 -1
  130. package/dist/primitives.js +398 -26
  131. package/dist/primitives.js.map +1 -1
  132. package/dist/retry.d.ts +66 -1
  133. package/dist/retry.d.ts.map +1 -1
  134. package/dist/retry.js +115 -8
  135. package/dist/retry.js.map +1 -1
  136. package/dist/sandbox.d.ts +36 -0
  137. package/dist/sandbox.d.ts.map +1 -0
  138. package/dist/sandbox.js +44 -0
  139. package/dist/sandbox.js.map +1 -0
  140. package/dist/schema.js +2 -2
  141. package/dist/schema.js.map +1 -1
  142. package/dist/telemetry.d.ts +128 -0
  143. package/dist/telemetry.d.ts.map +1 -0
  144. package/dist/telemetry.js +285 -0
  145. package/dist/telemetry.js.map +1 -0
  146. package/dist/template.d.ts.map +1 -1
  147. package/dist/template.js +6 -1
  148. package/dist/template.js.map +1 -1
  149. package/dist/tool-orchestration.d.ts +66 -4
  150. package/dist/tool-orchestration.d.ts.map +1 -1
  151. package/dist/tool-orchestration.js +123 -23
  152. package/dist/tool-orchestration.js.map +1 -1
  153. package/dist/type-guards.d.ts +28 -0
  154. package/dist/type-guards.d.ts.map +1 -0
  155. package/dist/type-guards.js +29 -0
  156. package/dist/type-guards.js.map +1 -0
  157. package/dist/types.d.ts +155 -19
  158. package/dist/types.d.ts.map +1 -1
  159. package/dist/types.js +36 -1
  160. package/dist/types.js.map +1 -1
  161. package/dist/wrap-for-v3.d.ts +80 -0
  162. package/dist/wrap-for-v3.d.ts.map +1 -0
  163. package/dist/wrap-for-v3.js +89 -0
  164. package/dist/wrap-for-v3.js.map +1 -0
  165. package/examples/00-quickstart.ts +232 -0
  166. package/examples/01-rag-chatbot.ts +212 -0
  167. package/examples/02-multi-agent-research.ts +290 -0
  168. package/examples/03-email-classification.ts +379 -0
  169. package/examples/04-content-moderation.ts +400 -0
  170. package/examples/05-document-extraction.ts +455 -0
  171. package/examples/06-streaming-chat-nextjs.ts +437 -0
  172. package/examples/07-cloudflare-worker.ts +483 -0
  173. package/examples/08-batch-processing.ts +491 -0
  174. package/examples/09-budget-constrained.ts +527 -0
  175. package/examples/10-tool-orchestration.ts +565 -0
  176. package/examples/11-retry-resilience.ts +403 -0
  177. package/examples/12-caching-strategies.ts +422 -0
  178. package/examples/README.md +145 -0
  179. package/package.json +29 -25
  180. package/src/ai-promise.ts +226 -140
  181. package/src/ai-schemas.ts +122 -0
  182. package/src/ai.ts +71 -1176
  183. package/src/batch/anthropic.ts +96 -161
  184. package/src/batch/bedrock.ts +203 -454
  185. package/src/batch/cloudflare.ts +99 -282
  186. package/src/batch/google.ts +91 -297
  187. package/src/batch/index.ts +4 -1
  188. package/src/batch/memory.ts +15 -10
  189. package/src/batch/openai.ts +65 -193
  190. package/src/batch/provider.ts +336 -0
  191. package/src/batch-map.ts +29 -24
  192. package/src/batch-queue.ts +200 -11
  193. package/src/budget.ts +31 -18
  194. package/src/cache.ts +45 -17
  195. package/src/context.ts +106 -77
  196. package/src/digital-objects-registry.ts +750 -0
  197. package/src/errors.ts +37 -0
  198. package/src/eval/runner.ts +60 -36
  199. package/src/eval-log/in-memory.ts +90 -0
  200. package/src/eval-log/index.ts +46 -0
  201. package/src/eval-log/types.ts +110 -0
  202. package/src/function-registry.ts +874 -0
  203. package/src/generate.ts +33 -28
  204. package/src/index.ts +122 -21
  205. package/src/logger.ts +232 -0
  206. package/src/middleware/budget.ts +171 -0
  207. package/src/middleware/cache.ts +299 -0
  208. package/src/middleware/embed-cache.ts +195 -0
  209. package/src/middleware/index.ts +23 -0
  210. package/src/middleware/trace.ts +248 -0
  211. package/src/primitives.ts +589 -62
  212. package/src/retry.ts +144 -18
  213. package/src/sandbox.ts +52 -0
  214. package/src/schema.ts +8 -8
  215. package/src/telemetry.ts +403 -0
  216. package/src/template.ts +8 -4
  217. package/src/tool-orchestration.ts +213 -48
  218. package/src/type-guards.ts +31 -0
  219. package/src/types.ts +186 -27
  220. package/src/wrap-for-v3.ts +105 -0
  221. package/test/ai-promise.test.ts +1080 -0
  222. package/test/ai-proxy.test.ts +1 -1
  223. package/test/batch-autosubmit-errors.test.ts +49 -37
  224. package/test/batch-blog-posts.test.ts +87 -129
  225. package/test/core-functions.test.ts +183 -579
  226. package/test/decide.test.ts +154 -322
  227. package/test/define.test.ts +211 -8
  228. package/test/digital-objects-registry.test.ts +760 -0
  229. package/test/embedding-cache-middleware.test.ts +140 -0
  230. package/test/fill-template.test.ts +89 -0
  231. package/test/generate-core.test.ts +140 -229
  232. package/test/implicit-batch.test.ts +22 -65
  233. package/test/retry-policy-integration.test.ts +117 -0
  234. package/test/sandbox-execution.test.ts +155 -0
  235. package/test/schema.test.ts +55 -19
  236. package/test/template.test.ts +1164 -0
  237. package/test/tool-orchestration.test.ts +270 -0
  238. package/test/wrap-for-v3.test.ts +612 -0
  239. package/vitest.config.js +6 -0
  240. package/vitest.config.ts +20 -0
  241. package/LICENSE +0 -21
  242. package/dist/rpc/auth.d.ts +0 -69
  243. package/dist/rpc/auth.d.ts.map +0 -1
  244. package/dist/rpc/auth.js +0 -136
  245. package/dist/rpc/auth.js.map +0 -1
  246. package/dist/rpc/client.d.ts +0 -62
  247. package/dist/rpc/client.d.ts.map +0 -1
  248. package/dist/rpc/client.js +0 -103
  249. package/dist/rpc/client.js.map +0 -1
  250. package/dist/rpc/deferred.d.ts +0 -60
  251. package/dist/rpc/deferred.d.ts.map +0 -1
  252. package/dist/rpc/deferred.js +0 -96
  253. package/dist/rpc/deferred.js.map +0 -1
  254. package/dist/rpc/index.d.ts +0 -22
  255. package/dist/rpc/index.d.ts.map +0 -1
  256. package/dist/rpc/index.js +0 -38
  257. package/dist/rpc/index.js.map +0 -1
  258. package/dist/rpc/local.d.ts +0 -42
  259. package/dist/rpc/local.d.ts.map +0 -1
  260. package/dist/rpc/local.js +0 -50
  261. package/dist/rpc/local.js.map +0 -1
  262. package/dist/rpc/server.d.ts +0 -165
  263. package/dist/rpc/server.d.ts.map +0 -1
  264. package/dist/rpc/server.js +0 -405
  265. package/dist/rpc/server.js.map +0 -1
  266. package/dist/rpc/session.d.ts +0 -32
  267. package/dist/rpc/session.d.ts.map +0 -1
  268. package/dist/rpc/session.js +0 -43
  269. package/dist/rpc/session.js.map +0 -1
  270. package/dist/rpc/transport.d.ts +0 -306
  271. package/dist/rpc/transport.d.ts.map +0 -1
  272. package/dist/rpc/transport.js +0 -731
  273. package/dist/rpc/transport.js.map +0 -1
  274. package/src/batch/anthropic.js +0 -256
  275. package/src/batch/bedrock.js +0 -584
  276. package/src/batch/cloudflare.js +0 -287
  277. package/src/batch/google.js +0 -359
  278. package/src/batch/index.js +0 -30
  279. package/src/batch/memory.js +0 -187
  280. package/src/batch/openai.js +0 -402
  281. package/src/eval/index.js +0 -7
  282. package/src/eval/models.js +0 -119
  283. package/src/eval/runner.js +0 -147
  284. package/test/schema.test.js +0 -96
@@ -2,295 +2,206 @@
2
2
  * Tests for the core generate() primitive
3
3
  *
4
4
  * generate(type, prompt, opts?) is the foundation that all other functions use.
5
+ * Tests require actual AI calls via the Cloudflare AI Gateway.
5
6
  */
6
7
 
7
- import { describe, it, expect, vi, beforeEach } from 'vitest'
8
+ import { describe, it, expect } from 'vitest'
9
+ import { generateObject, generateText } from '../src/generate.js'
10
+ import { z } from 'zod'
8
11
 
9
- // ============================================================================
10
- // Mock for underlying AI calls
11
- // ============================================================================
12
-
13
- const mockAICall = vi.fn()
14
-
15
- // Mock generate implementation
16
- async function generate(
17
- type: string,
18
- prompt: string,
19
- options?: Record<string, unknown>
20
- ): Promise<unknown> {
21
- return mockAICall(type, prompt, options)
22
- }
12
+ // Skip tests if no gateway configured
13
+ const hasGateway = !!process.env.AI_GATEWAY_URL
23
14
 
24
15
  // ============================================================================
25
16
  // generate(type, prompt, opts) signature tests
26
17
  // ============================================================================
27
18
 
28
- describe('generate(type, prompt, opts)', () => {
29
- beforeEach(() => {
30
- mockAICall.mockReset()
31
- })
32
-
19
+ describe.skipIf(!hasGateway)('generate(type, prompt, opts)', () => {
33
20
  describe('type: json', () => {
34
- it('generates JSON without schema (AI infers structure)', async () => {
35
- mockAICall.mockResolvedValue({
36
- competitors: ['Competitor A', 'Competitor B'],
37
- marketSize: 1000000,
38
- trends: ['AI adoption', 'Cloud migration'],
21
+ it('generates JSON without explicit schema (AI infers structure)', async () => {
22
+ const result = await generateObject({
23
+ model: 'haiku',
24
+ schema: z.object({
25
+ competitors: z.array(z.string()).describe('List of competitors'),
26
+ marketSize: z.number().describe('Estimated market size'),
27
+ }),
28
+ prompt:
29
+ 'Provide a simple competitive analysis of the cloud computing market. List 2 competitors and an estimated market size in billions.',
39
30
  })
40
31
 
41
- const result = await generate('json', 'competitive analysis of Acme Corp')
42
-
43
- expect(mockAICall).toHaveBeenCalledWith('json', 'competitive analysis of Acme Corp', undefined)
44
- expect(result).toHaveProperty('competitors')
45
- expect(result).toHaveProperty('marketSize')
32
+ expect(result.object).toHaveProperty('competitors')
33
+ expect(result.object).toHaveProperty('marketSize')
34
+ expect(Array.isArray(result.object.competitors)).toBe(true)
35
+ expect(typeof result.object.marketSize).toBe('number')
46
36
  })
47
37
 
48
38
  it('generates JSON with schema (typed, validated)', async () => {
49
- mockAICall.mockResolvedValue({
50
- name: 'Spaghetti Carbonara',
51
- servings: 4,
52
- ingredients: ['pasta', 'eggs', 'bacon'],
53
- steps: ['Boil pasta', 'Cook bacon', 'Mix eggs'],
39
+ const result = await generateObject({
40
+ model: 'haiku',
41
+ schema: z.object({
42
+ name: z.string().describe('Recipe name'),
43
+ servings: z.number().describe('Number of servings'),
44
+ ingredients: z.array(z.string()).describe('List of ingredients'),
45
+ steps: z.array(z.string()).describe('Cooking steps'),
46
+ }),
47
+ prompt: 'Generate a simple 3-ingredient recipe with 2 steps.',
54
48
  })
55
49
 
56
- const result = await generate('json', 'Italian pasta recipe', {
57
- schema: {
58
- name: 'Recipe name',
59
- servings: 'Number of servings (number)',
60
- ingredients: ['List of ingredients'],
61
- steps: ['Cooking steps'],
62
- },
63
- })
64
-
65
- expect(mockAICall).toHaveBeenCalledWith(
66
- 'json',
67
- 'Italian pasta recipe',
68
- expect.objectContaining({ schema: expect.any(Object) })
69
- )
70
- expect(result).toHaveProperty('name')
71
- expect(result).toHaveProperty('servings')
72
- expect(typeof (result as { servings: number }).servings).toBe('number')
50
+ expect(result.object).toHaveProperty('name')
51
+ expect(result.object).toHaveProperty('servings')
52
+ expect(typeof result.object.name).toBe('string')
53
+ expect(typeof result.object.servings).toBe('number')
54
+ expect(Array.isArray(result.object.ingredients)).toBe(true)
55
+ expect(Array.isArray(result.object.steps)).toBe(true)
73
56
  })
74
57
  })
75
58
 
76
59
  describe('type: text', () => {
77
60
  it('generates plain text', async () => {
78
- mockAICall.mockResolvedValue('This is the generated text content.')
79
-
80
- const result = await generate('text', 'Write a paragraph about AI')
61
+ const result = await generateText({
62
+ model: 'haiku',
63
+ prompt: 'Write one sentence about AI.',
64
+ })
81
65
 
82
- expect(mockAICall).toHaveBeenCalledWith('text', 'Write a paragraph about AI', undefined)
83
- expect(typeof result).toBe('string')
66
+ expect(typeof result.text).toBe('string')
67
+ expect(result.text.length).toBeGreaterThan(10)
84
68
  })
85
69
  })
86
70
 
87
71
  describe('type: code', () => {
88
- it('generates code with language option', async () => {
89
- mockAICall.mockResolvedValue(`
90
- function validateEmail(email: string): boolean {
91
- return /^[^@]+@[^@]+\\.[^@]+$/.test(email);
92
- }
93
- `.trim())
94
-
95
- const result = await generate('code', 'email validation function', {
96
- language: 'typescript',
72
+ it('generates code with language specified in prompt', async () => {
73
+ const result = await generateText({
74
+ model: 'haiku',
75
+ system:
76
+ 'You are a code generator. Output only valid TypeScript code, no explanations or markdown.',
77
+ prompt:
78
+ 'Write a TypeScript function called validateEmail that takes a string and returns boolean.',
97
79
  })
98
80
 
99
- expect(mockAICall).toHaveBeenCalledWith(
100
- 'code',
101
- 'email validation function',
102
- expect.objectContaining({ language: 'typescript' })
103
- )
104
- expect(typeof result).toBe('string')
105
- expect(result).toContain('function')
81
+ expect(typeof result.text).toBe('string')
82
+ expect(result.text).toContain('function')
83
+ expect(result.text).toMatch(/validateEmail|email/i)
106
84
  })
107
85
 
108
86
  it('generates code in different languages', async () => {
109
- mockAICall.mockResolvedValue('def validate_email(email):\n return "@" in email')
110
-
111
- await generate('code', 'email validation', { language: 'python' })
87
+ const result = await generateText({
88
+ model: 'haiku',
89
+ system:
90
+ 'You are a code generator. Output only valid Python code, no explanations or markdown.',
91
+ prompt:
92
+ 'Write a Python function called validate_email that takes a string and returns a boolean.',
93
+ })
112
94
 
113
- expect(mockAICall).toHaveBeenCalledWith(
114
- 'code',
115
- 'email validation',
116
- expect.objectContaining({ language: 'python' })
117
- )
95
+ expect(typeof result.text).toBe('string')
96
+ expect(result.text).toContain('def')
118
97
  })
119
98
  })
120
99
 
121
100
  describe('type: markdown', () => {
122
101
  it('generates markdown content', async () => {
123
- mockAICall.mockResolvedValue('# README\n\n## Features\n\n- Feature 1\n- Feature 2')
124
-
125
- const result = await generate('markdown', 'README for a TypeScript library')
102
+ const result = await generateText({
103
+ model: 'haiku',
104
+ system: 'You write in markdown format.',
105
+ prompt: 'Write a very short README with a heading and 2 bullet points.',
106
+ })
126
107
 
127
- expect(mockAICall).toHaveBeenCalledWith('markdown', 'README for a TypeScript library', undefined)
128
- expect(typeof result).toBe('string')
129
- expect(result).toContain('#')
108
+ expect(typeof result.text).toBe('string')
109
+ expect(result.text).toContain('#')
130
110
  })
131
111
  })
132
112
 
133
113
  describe('type: yaml', () => {
134
114
  it('generates YAML content', async () => {
135
- mockAICall.mockResolvedValue(`
136
- apiVersion: apps/v1
137
- kind: Deployment
138
- metadata:
139
- name: my-app
140
- `.trim())
141
-
142
- const result = await generate('yaml', 'kubernetes deployment for my-app')
143
-
144
- expect(mockAICall).toHaveBeenCalledWith('yaml', 'kubernetes deployment for my-app', undefined)
145
- expect(typeof result).toBe('string')
146
- expect(result).toContain('apiVersion')
115
+ const result = await generateText({
116
+ model: 'haiku',
117
+ system: 'You output only valid YAML, no explanations or markdown fences.',
118
+ prompt: 'Generate a simple YAML config with name: "test-app" and port: 3000.',
119
+ })
120
+
121
+ expect(typeof result.text).toBe('string')
122
+ expect(result.text.toLowerCase()).toContain('name')
147
123
  })
148
124
  })
149
125
 
150
126
  describe('type: list', () => {
151
127
  it('generates a list of items', async () => {
152
- mockAICall.mockResolvedValue(['Item 1', 'Item 2', 'Item 3'])
153
-
154
- const result = await generate('list', '5 startup ideas')
128
+ const result = await generateObject({
129
+ model: 'haiku',
130
+ schema: z.object({
131
+ items: z.array(z.string()).describe('List of startup ideas'),
132
+ }),
133
+ prompt: 'List exactly 3 startup ideas.',
134
+ })
155
135
 
156
- expect(mockAICall).toHaveBeenCalledWith('list', '5 startup ideas', undefined)
157
- expect(Array.isArray(result)).toBe(true)
136
+ expect(Array.isArray(result.object.items)).toBe(true)
137
+ expect(result.object.items.length).toBe(3)
158
138
  })
159
139
  })
160
140
 
161
141
  describe('type: diagram', () => {
162
142
  it('generates diagram code', async () => {
163
- mockAICall.mockResolvedValue('graph TD\n A[Start] --> B[Process]\n B --> C[End]')
164
-
165
- const result = await generate('diagram', 'user flow for authentication', {
166
- format: 'mermaid',
167
- })
168
-
169
- expect(mockAICall).toHaveBeenCalledWith(
170
- 'diagram',
171
- 'user flow for authentication',
172
- expect.objectContaining({ format: 'mermaid' })
173
- )
174
- expect(typeof result).toBe('string')
175
- })
176
- })
177
-
178
- describe('type: slides', () => {
179
- it('generates presentation slides', async () => {
180
- mockAICall.mockResolvedValue('---\ntheme: default\n---\n\n# Title\n\nContent')
181
-
182
- const result = await generate('slides', 'quarterly review presentation', {
183
- format: 'slidev',
184
- slides: 10,
143
+ const result = await generateText({
144
+ model: 'haiku',
145
+ system:
146
+ 'You generate Mermaid diagram code. Output only the diagram code, no explanations or markdown fences.',
147
+ prompt: 'Create a simple flowchart: Start -> Login -> Dashboard.',
185
148
  })
186
149
 
187
- expect(mockAICall).toHaveBeenCalledWith(
188
- 'slides',
189
- 'quarterly review presentation',
190
- expect.objectContaining({ format: 'slidev', slides: 10 })
191
- )
150
+ expect(typeof result.text).toBe('string')
151
+ // Mermaid diagrams typically contain --> or -> for connections
152
+ expect(result.text).toMatch(/(flowchart|graph|-->|->)/i)
192
153
  })
193
154
  })
194
155
  })
195
156
 
196
- // ============================================================================
197
- // Tagged template support on generate
198
- // ============================================================================
199
-
200
- describe('generate as tagged template', () => {
201
- beforeEach(() => {
202
- mockAICall.mockReset()
203
- })
204
-
205
- // Note: This tests the concept - actual implementation would need the template wrapper
206
- it('should support tagged template syntax (conceptual)', async () => {
207
- mockAICall.mockResolvedValue({ analysis: 'Result' })
208
-
209
- // This would be: generate`analysis of ${company}`
210
- const company = 'Acme Corp'
211
- const prompt = `analysis of ${company}`
212
-
213
- const result = await generate('json', prompt)
214
-
215
- expect(result).toHaveProperty('analysis')
216
- })
217
-
218
- it('should convert objects to YAML in templates (conceptual)', async () => {
219
- mockAICall.mockResolvedValue('Generated content')
220
-
221
- const context = {
222
- brand: 'TechCo',
223
- audience: 'developers',
224
- }
225
-
226
- // This would be: generate`content for ${{ context }}`
227
- // The object would be converted to YAML
228
- const prompt = `content for\ncontext:\n brand: TechCo\n audience: developers`
229
-
230
- await generate('text', prompt)
231
-
232
- expect(mockAICall).toHaveBeenCalledWith('text', expect.stringContaining('brand: TechCo'), undefined)
233
- })
234
- })
235
-
236
157
  // ============================================================================
237
158
  // Options parameter tests
238
159
  // ============================================================================
239
160
 
240
- describe('generate options', () => {
241
- beforeEach(() => {
242
- mockAICall.mockReset()
243
- mockAICall.mockResolvedValue('result')
244
- })
245
-
246
- it('passes model option', async () => {
247
- await generate('text', 'test', { model: 'claude-opus-4-5' })
248
-
249
- expect(mockAICall).toHaveBeenCalledWith(
250
- 'text',
251
- 'test',
252
- expect.objectContaining({ model: 'claude-opus-4-5' })
253
- )
254
- })
255
-
256
- it('passes temperature option', async () => {
257
- await generate('text', 'creative writing', { temperature: 0.9 })
258
-
259
- expect(mockAICall).toHaveBeenCalledWith(
260
- 'text',
261
- 'creative writing',
262
- expect.objectContaining({ temperature: 0.9 })
263
- )
264
- })
161
+ describe.skipIf(!hasGateway)('generate options', () => {
162
+ it('respects temperature option (low temperature = more deterministic)', async () => {
163
+ // Low temperature should give consistent results
164
+ const result1 = await generateText({
165
+ model: 'haiku',
166
+ prompt: 'Say exactly "hello" and nothing else.',
167
+ temperature: 0,
168
+ })
265
169
 
266
- it('passes maxTokens option', async () => {
267
- await generate('text', 'long article', { maxTokens: 4000 })
170
+ const result2 = await generateText({
171
+ model: 'haiku',
172
+ prompt: 'Say exactly "hello" and nothing else.',
173
+ temperature: 0,
174
+ })
268
175
 
269
- expect(mockAICall).toHaveBeenCalledWith(
270
- 'text',
271
- 'long article',
272
- expect.objectContaining({ maxTokens: 4000 })
273
- )
176
+ // With temperature 0, responses should be very similar
177
+ expect(result1.text.toLowerCase()).toContain('hello')
178
+ expect(result2.text.toLowerCase()).toContain('hello')
274
179
  })
275
180
 
276
- it('passes thinking option', async () => {
277
- await generate('json', 'complex analysis', { thinking: 'high' })
181
+ it('accepts maxTokens option without error', async () => {
182
+ // This test verifies the maxTokens option is passed through without error
183
+ // The actual truncation behavior is provider-dependent
184
+ const result = await generateText({
185
+ model: 'haiku',
186
+ prompt: 'Say "hello" and nothing else.',
187
+ maxTokens: 50,
188
+ })
278
189
 
279
- expect(mockAICall).toHaveBeenCalledWith(
280
- 'json',
281
- 'complex analysis',
282
- expect.objectContaining({ thinking: 'high' })
283
- )
190
+ // Just verify we got a response - maxTokens behavior varies by provider/gateway
191
+ expect(result.text).toBeDefined()
192
+ expect(typeof result.text).toBe('string')
284
193
  })
285
194
 
286
- it('passes thinking as number (token budget)', async () => {
287
- await generate('json', 'complex analysis', { thinking: 10000 })
195
+ it('passes system prompt correctly', async () => {
196
+ const result = await generateText({
197
+ model: 'haiku',
198
+ system: 'You always respond with exactly one word.',
199
+ prompt: 'What is your favorite color?',
200
+ })
288
201
 
289
- expect(mockAICall).toHaveBeenCalledWith(
290
- 'json',
291
- 'complex analysis',
292
- expect.objectContaining({ thinking: 10000 })
293
- )
202
+ // With the system prompt, response should be short (ideally one word)
203
+ const wordCount = result.text.trim().split(/\s+/).length
204
+ expect(wordCount).toBeLessThanOrEqual(3) // Allow some flexibility
294
205
  })
295
206
  })
296
207
 
@@ -298,22 +209,22 @@ describe('generate options', () => {
298
209
  // All convenience functions use generate
299
210
  // ============================================================================
300
211
 
301
- describe('convenience functions map to generate', () => {
212
+ describe('convenience functions documentation', () => {
302
213
  it('documents the mapping', () => {
303
214
  // This test documents the expected mappings
304
215
  const mappings = {
305
- 'ai(prompt)': "generate('text', prompt)",
306
- 'write(prompt)': "generate('text', prompt)",
307
- 'code(prompt)': "generate('code', prompt)",
308
- 'list(prompt)': "generate('list', prompt)",
309
- 'lists(prompt)': "generate('lists', prompt)",
310
- 'extract(prompt)': "generate('extract', prompt)",
311
- 'summarize(prompt)': "generate('summary', prompt)",
312
- 'diagram(prompt)': "generate('diagram', prompt)",
313
- 'slides(prompt)': "generate('slides', prompt)",
314
- 'is(prompt)': "generate('boolean', prompt)",
216
+ 'ai(prompt)': 'generateText({ model, prompt })',
217
+ 'write(prompt)': 'generateText({ model, prompt })',
218
+ 'code(prompt)': "generateText({ model, system: 'code generator', prompt })",
219
+ 'list(prompt)': 'generateObject({ model, schema: { items: [...] }, prompt })',
220
+ 'lists(prompt)':
221
+ 'generateObject({ model, schema: { listName1: [...], listName2: [...] }, prompt })',
222
+ 'extract(prompt)': 'generateObject({ model, schema: { extracted: [...] }, prompt })',
223
+ 'summarize(prompt)': "generateText({ model, system: 'summarizer', prompt })",
224
+ 'diagram(prompt)': "generateText({ model, system: 'mermaid generator', prompt })",
225
+ 'is(prompt)': 'generateObject({ model, schema: { result: boolean }, prompt })',
315
226
  }
316
227
 
317
- expect(Object.keys(mappings)).toHaveLength(10)
228
+ expect(Object.keys(mappings)).toHaveLength(9)
318
229
  })
319
230
  })
@@ -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