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,113 +2,49 @@
2
2
  * Tests for core AI functions
3
3
  *
4
4
  * These tests verify the API contracts for each function.
5
- * Tests marked with .skipIf(!hasGateway) require actual AI calls.
5
+ * Tests require actual AI calls via the Cloudflare AI Gateway.
6
6
  */
7
7
 
8
- import { describe, it, expect, vi, beforeEach } from 'vitest'
9
- import { stringify as yamlStringify } from 'yaml'
8
+ import { describe, it, expect } from 'vitest'
9
+ import { generateText, generateObject } from '../src/generate.js'
10
+ import { z } from 'zod'
10
11
 
11
12
  // Skip tests if no gateway configured
12
- const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
13
-
14
- // ============================================================================
15
- // Mock implementations for unit tests
16
- // ============================================================================
17
-
18
- // Mock generate function that all others should call
19
- const mockGenerate = vi.fn()
13
+ const hasGateway = !!process.env.AI_GATEWAY_URL
20
14
 
21
15
  // ============================================================================
22
16
  // ai() - Direct text generation
23
17
  // ============================================================================
24
18
 
25
- describe('ai()', () => {
26
- beforeEach(() => {
27
- mockGenerate.mockReset()
28
- mockGenerate.mockResolvedValue('Generated text')
29
- })
30
-
31
- it('should accept a string prompt', async () => {
32
- const ai = createMockAi()
33
- const result = await ai('Write a haiku')
34
-
35
- expect(mockGenerate).toHaveBeenCalledWith(
36
- 'text',
37
- 'Write a haiku',
38
- expect.any(Object)
39
- )
40
- expect(result).toBe('Generated text')
41
- })
42
-
43
- it('should accept tagged template literal', async () => {
44
- const ai = createMockAi()
45
- const topic = 'TypeScript'
46
- const result = await ai`Write about ${topic}`
19
+ describe.skipIf(!hasGateway)('ai()', () => {
20
+ it('should generate text from a string prompt', async () => {
21
+ const result = await generateText({
22
+ model: 'haiku',
23
+ prompt: 'Say "hello world" and nothing else.',
24
+ })
47
25
 
48
- expect(mockGenerate).toHaveBeenCalled()
49
- const [, prompt] = mockGenerate.mock.calls[0]
50
- expect(prompt).toContain('TypeScript')
51
- expect(result).toBe('Generated text')
26
+ expect(result.text).toBeDefined()
27
+ expect(typeof result.text).toBe('string')
28
+ expect(result.text.toLowerCase()).toContain('hello')
52
29
  })
53
30
 
54
- it('should accept options parameter', async () => {
55
- const ai = createMockAi()
56
- await ai`test`({ model: 'claude-opus-4-5' })
31
+ it('should respect model parameter', async () => {
32
+ const result = await generateText({
33
+ model: 'haiku',
34
+ prompt: 'Respond with just the word "yes".',
35
+ })
57
36
 
58
- expect(mockGenerate).toHaveBeenCalledWith(
59
- 'text',
60
- 'test',
61
- expect.objectContaining({ model: 'claude-opus-4-5' })
62
- )
37
+ expect(result.text).toBeDefined()
38
+ expect(result.text.toLowerCase()).toContain('yes')
63
39
  })
64
40
 
65
41
  it('should return string type', async () => {
66
- const ai = createMockAi()
67
- const result = await ai('test')
68
- expect(typeof result).toBe('string')
69
- })
70
- })
71
-
72
- // ============================================================================
73
- // summarize() - Condense text
74
- // ============================================================================
75
-
76
- describe('summarize()', () => {
77
- beforeEach(() => {
78
- mockGenerate.mockReset()
79
- mockGenerate.mockResolvedValue('Summary of content')
80
- })
81
-
82
- it('should accept text to summarize', async () => {
83
- const summarize = createMockSummarize()
84
- const result = await summarize`${longArticle}`
85
-
86
- expect(mockGenerate).toHaveBeenCalledWith(
87
- 'summary',
88
- expect.stringContaining('article'),
89
- expect.any(Object)
90
- )
91
- expect(result).toBe('Summary of content')
92
- })
93
-
94
- it('should support length option', async () => {
95
- const summarize = createMockSummarize()
96
- await summarize`${longArticle}`({ length: 'short' })
97
-
98
- expect(mockGenerate).toHaveBeenCalledWith(
99
- 'summary',
100
- expect.any(String),
101
- expect.objectContaining({ length: 'short' })
102
- )
103
- })
104
-
105
- it('should support audience option', async () => {
106
- const summarize = createMockSummarize()
107
- await summarize`${technicalReport}${{ audience: 'executives', focus: 'business impact' }}`
42
+ const result = await generateText({
43
+ model: 'haiku',
44
+ prompt: 'Say "test".',
45
+ })
108
46
 
109
- const [, prompt] = mockGenerate.mock.calls[0]
110
- expect(prompt).toContain('executives')
111
- expect(prompt).toContain('business impact')
47
+ expect(typeof result.text).toBe('string')
112
48
  })
113
49
  })
114
50
 
@@ -116,51 +52,41 @@ describe('summarize()', () => {
116
52
  // is() - Boolean classification
117
53
  // ============================================================================
118
54
 
119
- describe('is()', () => {
120
- beforeEach(() => {
121
- mockGenerate.mockReset()
122
- })
123
-
124
- it('should return boolean true', async () => {
125
- mockGenerate.mockResolvedValue(true)
126
- const is = createMockIs()
127
-
128
- const result = await is`${'hello@example.com'} a valid email?`
129
- expect(result).toBe(true)
130
- })
131
-
132
- it('should return boolean false', async () => {
133
- mockGenerate.mockResolvedValue(false)
134
- const is = createMockIs()
55
+ describe.skipIf(!hasGateway)('is()', () => {
56
+ it('should return boolean true for valid classification', async () => {
57
+ const result = await generateObject({
58
+ model: 'haiku',
59
+ schema: z.object({
60
+ isValid: z.boolean().describe('Is this a valid email address?'),
61
+ }),
62
+ prompt: 'Is "hello@example.com" a valid email address?',
63
+ })
135
64
 
136
- const result = await is`${'not-an-email'} a valid email?`
137
- expect(result).toBe(false)
65
+ expect(result.object.isValid).toBe(true)
138
66
  })
139
67
 
140
- it('should accept natural question format', async () => {
141
- mockGenerate.mockResolvedValue(true)
142
- const is = createMockIs()
143
-
144
- await is`${'The product is amazing!'} positive sentiment?`
68
+ it('should return boolean false for invalid classification', async () => {
69
+ const result = await generateObject({
70
+ model: 'haiku',
71
+ schema: z.object({
72
+ isValid: z.boolean().describe('Is this a valid email address?'),
73
+ }),
74
+ prompt: 'Is "not-an-email" a valid email address?',
75
+ })
145
76
 
146
- expect(mockGenerate).toHaveBeenCalledWith(
147
- 'boolean',
148
- expect.stringContaining('positive sentiment'),
149
- expect.any(Object)
150
- )
77
+ expect(result.object.isValid).toBe(false)
151
78
  })
152
79
 
153
- it('should support model option for complex classifications', async () => {
154
- mockGenerate.mockResolvedValue(true)
155
- const is = createMockIs()
156
-
157
- await is`${claim} factually accurate?`({ model: 'claude-opus-4-5' })
80
+ it('should handle sentiment classification', async () => {
81
+ const result = await generateObject({
82
+ model: 'haiku',
83
+ schema: z.object({
84
+ isPositive: z.boolean().describe('Is this positive sentiment?'),
85
+ }),
86
+ prompt: 'Is "I love this product, it\'s amazing!" positive sentiment?',
87
+ })
158
88
 
159
- expect(mockGenerate).toHaveBeenCalledWith(
160
- 'boolean',
161
- expect.any(String),
162
- expect.objectContaining({ model: 'claude-opus-4-5' })
163
- )
89
+ expect(result.object.isPositive).toBe(true)
164
90
  })
165
91
  })
166
92
 
@@ -168,41 +94,31 @@ describe('is()', () => {
168
94
  // list() - Generate a list
169
95
  // ============================================================================
170
96
 
171
- describe('list()', () => {
172
- beforeEach(() => {
173
- mockGenerate.mockReset()
174
- mockGenerate.mockResolvedValue(['Item 1', 'Item 2', 'Item 3'])
175
- })
176
-
97
+ describe.skipIf(!hasGateway)('list()', () => {
177
98
  it('should return an array of strings', async () => {
178
- const list = createMockList()
179
- const result = await list`startup ideas for ${industry}`
99
+ const result = await generateObject({
100
+ model: 'haiku',
101
+ schema: z.object({
102
+ items: z.array(z.string()).describe('List of 3 colors'),
103
+ }),
104
+ prompt: 'List exactly 3 colors.',
105
+ })
180
106
 
181
- expect(Array.isArray(result)).toBe(true)
182
- expect(result).toHaveLength(3)
183
- expect(result.every((item: unknown) => typeof item === 'string')).toBe(true)
107
+ expect(Array.isArray(result.object.items)).toBe(true)
108
+ expect(result.object.items.length).toBeGreaterThanOrEqual(1)
109
+ expect(result.object.items.every((item: unknown) => typeof item === 'string')).toBe(true)
184
110
  })
185
111
 
186
112
  it('should respect count in prompt', async () => {
187
- const list = createMockList()
188
- await list`10 blog post titles for ${topic}`
189
-
190
- expect(mockGenerate).toHaveBeenCalledWith(
191
- 'list',
192
- expect.stringContaining('10'),
193
- expect.any(Object)
194
- )
195
- })
196
-
197
- it('should support count option', async () => {
198
- const list = createMockList()
199
- await list('startup ideas', { count: 10 })
113
+ const result = await generateObject({
114
+ model: 'haiku',
115
+ schema: z.object({
116
+ items: z.array(z.string()).describe('List of items'),
117
+ }),
118
+ prompt: 'List exactly 5 fruits.',
119
+ })
200
120
 
201
- expect(mockGenerate).toHaveBeenCalledWith(
202
- 'list',
203
- 'startup ideas',
204
- expect.objectContaining({ count: 10 })
205
- )
121
+ expect(result.object.items.length).toBe(5)
206
122
  })
207
123
  })
208
124
 
@@ -210,40 +126,21 @@ describe('list()', () => {
210
126
  // lists() - Generate multiple named lists
211
127
  // ============================================================================
212
128
 
213
- describe('lists()', () => {
214
- beforeEach(() => {
215
- mockGenerate.mockReset()
216
- mockGenerate.mockResolvedValue({
217
- pros: ['Pro 1', 'Pro 2'],
218
- cons: ['Con 1', 'Con 2'],
219
- })
220
- })
221
-
129
+ describe.skipIf(!hasGateway)('lists()', () => {
222
130
  it('should return named lists object', async () => {
223
- const lists = createMockLists()
224
- const result = await lists`pros and cons of ${topic}`
225
-
226
- expect(result).toHaveProperty('pros')
227
- expect(result).toHaveProperty('cons')
228
- expect(Array.isArray(result.pros)).toBe(true)
229
- expect(Array.isArray(result.cons)).toBe(true)
230
- })
231
-
232
- it('should support SWOT analysis format', async () => {
233
- mockGenerate.mockResolvedValue({
234
- strengths: ['S1'],
235
- weaknesses: ['W1'],
236
- opportunities: ['O1'],
237
- threats: ['T1'],
131
+ const result = await generateObject({
132
+ model: 'haiku',
133
+ schema: z.object({
134
+ pros: z.array(z.string()).describe('List of pros/advantages'),
135
+ cons: z.array(z.string()).describe('List of cons/disadvantages'),
136
+ }),
137
+ prompt: 'List 2 pros and 2 cons of working remotely.',
238
138
  })
239
139
 
240
- const lists = createMockLists()
241
- const result = await lists`SWOT analysis for ${{ company, market }}`
242
-
243
- expect(result).toHaveProperty('strengths')
244
- expect(result).toHaveProperty('weaknesses')
245
- expect(result).toHaveProperty('opportunities')
246
- expect(result).toHaveProperty('threats')
140
+ expect(result.object).toHaveProperty('pros')
141
+ expect(result.object).toHaveProperty('cons')
142
+ expect(Array.isArray(result.object.pros)).toBe(true)
143
+ expect(Array.isArray(result.object.cons)).toBe(true)
247
144
  })
248
145
  })
249
146
 
@@ -251,37 +148,39 @@ describe('lists()', () => {
251
148
  // extract() - Extract from text
252
149
  // ============================================================================
253
150
 
254
- describe('extract()', () => {
255
- beforeEach(() => {
256
- mockGenerate.mockReset()
257
- mockGenerate.mockResolvedValue(['John Smith', 'Jane Doe'])
258
- })
259
-
151
+ describe.skipIf(!hasGateway)('extract()', () => {
260
152
  it('should extract items from text', async () => {
261
- const extract = createMockExtract()
262
- const result = await extract`person names from ${article}`
153
+ const result = await generateObject({
154
+ model: 'haiku',
155
+ schema: z.object({
156
+ names: z.array(z.string()).describe('Person names mentioned in the text'),
157
+ }),
158
+ prompt:
159
+ 'Extract all person names from: "John Smith met with Jane Doe yesterday. Bob was also there."',
160
+ })
263
161
 
264
- expect(Array.isArray(result)).toBe(true)
265
- expect(result).toContain('John Smith')
266
- expect(result).toContain('Jane Doe')
162
+ expect(Array.isArray(result.object.names)).toBe(true)
163
+ expect(result.object.names.length).toBeGreaterThanOrEqual(2)
267
164
  })
268
165
 
269
166
  it('should support schema for structured extraction', async () => {
270
- mockGenerate.mockResolvedValue([
271
- { name: 'Acme Corp', role: 'competitor' },
272
- { name: 'Beta Inc', role: 'partner' },
273
- ])
274
-
275
- const extract = createMockExtract()
276
- const result = await extract`companies from ${text}${{
277
- schema: {
278
- name: 'Company name',
279
- role: 'mentioned as: competitor | partner | customer',
280
- },
281
- }}`
282
-
283
- expect(result[0]).toHaveProperty('name')
284
- expect(result[0]).toHaveProperty('role')
167
+ const result = await generateObject({
168
+ model: 'haiku',
169
+ schema: z.object({
170
+ companies: z.array(
171
+ z.object({
172
+ name: z.string().describe('Company name'),
173
+ role: z.enum(['competitor', 'partner', 'customer']).describe('Role mentioned'),
174
+ })
175
+ ),
176
+ }),
177
+ prompt:
178
+ 'Extract companies from: "Our competitor Acme Corp launched a new product. Our partner Beta Inc helped with distribution."',
179
+ })
180
+
181
+ expect(result.object.companies.length).toBeGreaterThanOrEqual(2)
182
+ expect(result.object.companies[0]).toHaveProperty('name')
183
+ expect(result.object.companies[0]).toHaveProperty('role')
285
184
  })
286
185
  })
287
186
 
@@ -289,40 +188,26 @@ describe('extract()', () => {
289
188
  // write() - Generate text content
290
189
  // ============================================================================
291
190
 
292
- describe('write()', () => {
293
- beforeEach(() => {
294
- mockGenerate.mockReset()
295
- mockGenerate.mockResolvedValue('Generated content here...')
296
- })
297
-
191
+ describe.skipIf(!hasGateway)('write()', () => {
298
192
  it('should generate text content', async () => {
299
- const write = createMockWrite()
300
- const result = await write`professional email to ${recipient} about ${subject}`
301
-
302
- expect(typeof result).toBe('string')
303
- expect(result.length).toBeGreaterThan(0)
304
- })
305
-
306
- it('should support tone option', async () => {
307
- const write = createMockWrite()
308
- await write('blog post', { tone: 'casual', topic: 'TypeScript' })
193
+ const result = await generateText({
194
+ model: 'haiku',
195
+ prompt: 'Write a short greeting message (1-2 sentences).',
196
+ })
309
197
 
310
- expect(mockGenerate).toHaveBeenCalledWith(
311
- 'text',
312
- 'blog post',
313
- expect.objectContaining({ tone: 'casual' })
314
- )
198
+ expect(typeof result.text).toBe('string')
199
+ expect(result.text.length).toBeGreaterThan(0)
315
200
  })
316
201
 
317
- it('should support length option', async () => {
318
- const write = createMockWrite()
319
- await write('article', { length: 'long' })
202
+ it('should support system prompt for tone', async () => {
203
+ const result = await generateText({
204
+ model: 'haiku',
205
+ system: 'You write in a casual, friendly tone.',
206
+ prompt: 'Write a one-sentence welcome message.',
207
+ })
320
208
 
321
- expect(mockGenerate).toHaveBeenCalledWith(
322
- 'text',
323
- 'article',
324
- expect.objectContaining({ length: 'long' })
325
- )
209
+ expect(typeof result.text).toBe('string')
210
+ expect(result.text.length).toBeGreaterThan(0)
326
211
  })
327
212
  })
328
213
 
@@ -330,45 +215,28 @@ describe('write()', () => {
330
215
  // code() - Generate code
331
216
  // ============================================================================
332
217
 
333
- describe('code()', () => {
334
- beforeEach(() => {
335
- mockGenerate.mockReset()
336
- mockGenerate.mockResolvedValue('function validate(email) { return email.includes("@"); }')
337
- })
338
-
218
+ describe.skipIf(!hasGateway)('code()', () => {
339
219
  it('should generate code', async () => {
340
- const code = createMockCode()
341
- const result = await code`email validation function`
342
-
343
- expect(typeof result).toBe('string')
344
- expect(result).toContain('function')
345
- })
346
-
347
- it('should support language option', async () => {
348
- const code = createMockCode()
349
- await code('REST API endpoints', { language: 'typescript' })
220
+ const result = await generateText({
221
+ model: 'haiku',
222
+ system: 'You are a code generator. Output only code, no explanations.',
223
+ prompt: 'Write a JavaScript function called isEven that returns true if a number is even.',
224
+ })
350
225
 
351
- expect(mockGenerate).toHaveBeenCalledWith(
352
- 'code',
353
- 'REST API endpoints',
354
- expect.objectContaining({ language: 'typescript' })
355
- )
226
+ expect(typeof result.text).toBe('string')
227
+ expect(result.text).toContain('function')
356
228
  })
357
229
 
358
- it('should handle complex requirements via object interpolation', async () => {
359
- const code = createMockCode()
360
- const requirements = {
361
- pages: ['home', 'about', 'pricing'],
362
- features: ['dark mode', 'animations'],
363
- stack: 'Next.js + Tailwind',
364
- }
365
-
366
- await code`marketing website${{ requirements }}`
230
+ it('should generate TypeScript code', async () => {
231
+ const result = await generateText({
232
+ model: 'haiku',
233
+ system: 'You are a TypeScript code generator. Output only code, no explanations.',
234
+ prompt:
235
+ 'Write a TypeScript function called add that takes two numbers and returns their sum. Include type annotations.',
236
+ })
367
237
 
368
- const [, prompt] = mockGenerate.mock.calls[0]
369
- expect(prompt).toContain('pages:')
370
- expect(prompt).toContain('- home')
371
- expect(prompt).toContain('features:')
238
+ expect(typeof result.text).toBe('string')
239
+ expect(result.text).toContain('number')
372
240
  })
373
241
  })
374
242
 
@@ -376,319 +244,55 @@ describe('code()', () => {
376
244
  // diagram() - Generate diagrams
377
245
  // ============================================================================
378
246
 
379
- describe('diagram()', () => {
380
- beforeEach(() => {
381
- mockGenerate.mockReset()
382
- mockGenerate.mockResolvedValue('graph TD\n A --> B\n B --> C')
383
- })
384
-
247
+ describe.skipIf(!hasGateway)('diagram()', () => {
385
248
  it('should generate mermaid diagrams', async () => {
386
- const diagram = createMockDiagram()
387
- const result = await diagram`user authentication flow`
388
-
389
- expect(typeof result).toBe('string')
390
- expect(result).toContain('graph')
391
- })
392
-
393
- it('should support format option', async () => {
394
- const diagram = createMockDiagram()
395
- await diagram('database schema', { format: 'mermaid', type: 'erd' })
396
-
397
- expect(mockGenerate).toHaveBeenCalledWith(
398
- 'diagram',
399
- 'database schema',
400
- expect.objectContaining({ format: 'mermaid', type: 'erd' })
401
- )
402
- })
403
- })
404
-
405
- // ============================================================================
406
- // slides() - Generate presentations
407
- // ============================================================================
408
-
409
- describe('slides()', () => {
410
- beforeEach(() => {
411
- mockGenerate.mockReset()
412
- mockGenerate.mockResolvedValue('---\ntheme: default\n---\n\n# Slide 1\n\nContent here')
413
- })
414
-
415
- it('should generate slidev-format markdown', async () => {
416
- const slides = createMockSlides()
417
- const result = await slides`${topic}`
418
-
419
- expect(typeof result).toBe('string')
420
- expect(result).toContain('---')
421
- })
422
-
423
- it('should support format option', async () => {
424
- const slides = createMockSlides()
425
- await slides('quarterly review', { format: 'marp', slides: 12 })
426
-
427
- expect(mockGenerate).toHaveBeenCalledWith(
428
- 'slides',
429
- 'quarterly review',
430
- expect.objectContaining({ format: 'marp', slides: 12 })
431
- )
432
- })
433
-
434
- it('should support speaker notes', async () => {
435
- const slides = createMockSlides()
436
- await slides('workshop', { includeNotes: true, duration: '2 hours' })
437
-
438
- expect(mockGenerate).toHaveBeenCalledWith(
439
- 'slides',
440
- 'workshop',
441
- expect.objectContaining({ includeNotes: true })
442
- )
443
- })
444
- })
445
-
446
- // ============================================================================
447
- // image() - Generate images
448
- // ============================================================================
449
-
450
- describe('image()', () => {
451
- beforeEach(() => {
452
- mockGenerate.mockReset()
453
- mockGenerate.mockResolvedValue(Buffer.from('fake-image-data'))
454
- })
455
-
456
- it('should generate image buffer', async () => {
457
- const image = createMockImage()
458
- const result = await image`minimalist logo for ${companyName}`
459
-
460
- expect(Buffer.isBuffer(result)).toBe(true)
461
- })
462
-
463
- it('should support size option', async () => {
464
- const image = createMockImage()
465
- await image('robot reading a book', { size: '1024x1024', style: 'cartoon' })
466
-
467
- expect(mockGenerate).toHaveBeenCalledWith(
468
- 'image',
469
- 'robot reading a book',
470
- expect.objectContaining({ size: '1024x1024', style: 'cartoon' })
471
- )
472
- })
473
- })
474
-
475
- // ============================================================================
476
- // video() - Generate videos
477
- // ============================================================================
478
-
479
- describe('video()', () => {
480
- beforeEach(() => {
481
- mockGenerate.mockReset()
482
- mockGenerate.mockResolvedValue(Buffer.from('fake-video-data'))
483
- })
484
-
485
- it('should generate video buffer', async () => {
486
- const video = createMockVideo()
487
- const result = await video`product demo for ${productName}`
488
-
489
- expect(Buffer.isBuffer(result)).toBe(true)
490
- })
491
-
492
- it('should support duration and aspect options', async () => {
493
- const video = createMockVideo()
494
- await video('promotional video', { duration: 30, aspect: '16:9', style: 'motion graphics' })
495
-
496
- expect(mockGenerate).toHaveBeenCalledWith(
497
- 'video',
498
- 'promotional video',
499
- expect.objectContaining({ duration: 30, aspect: '16:9' })
500
- )
501
- })
502
- })
503
-
504
- // ============================================================================
505
- // research() - Agentic research
506
- // ============================================================================
507
-
508
- describe('research()', () => {
509
- beforeEach(() => {
510
- mockGenerate.mockReset()
511
- mockGenerate.mockResolvedValue({
512
- summary: 'Key findings...',
513
- sources: [{ url: 'https://example.com', title: 'Source 1' }],
514
- findings: ['Finding 1', 'Finding 2'],
515
- confidence: 0.85,
249
+ const result = await generateText({
250
+ model: 'haiku',
251
+ system:
252
+ 'You generate Mermaid diagram code. Output only the mermaid code, no explanations or markdown fences.',
253
+ prompt: 'Create a simple flowchart with Start -> Process -> End.',
516
254
  })
517
- })
518
-
519
- it('should return structured research results', async () => {
520
- const research = createMockResearch()
521
- const result = await research`${topic}`
522
-
523
- expect(result).toHaveProperty('summary')
524
- expect(result).toHaveProperty('sources')
525
- expect(result).toHaveProperty('findings')
526
- expect(result).toHaveProperty('confidence')
527
- })
528
-
529
- it('should support depth option', async () => {
530
- const research = createMockResearch()
531
- await research`market size for AI tools`({ depth: 'thorough' })
532
255
 
533
- expect(mockGenerate).toHaveBeenCalledWith(
534
- 'research',
535
- expect.any(String),
536
- expect.objectContaining({ depth: 'thorough' })
537
- )
256
+ expect(typeof result.text).toBe('string')
257
+ // Mermaid flowcharts use arrows like --> or ->
258
+ expect(result.text).toMatch(/(-->|->)/)
538
259
  })
539
260
  })
540
261
 
541
262
  // ============================================================================
542
- // do() - Single-pass task with tools
263
+ // Structured object generation
543
264
  // ============================================================================
544
265
 
545
- describe('do()', () => {
546
- beforeEach(() => {
547
- mockGenerate.mockReset()
548
- mockGenerate.mockResolvedValue({ summary: 'Done', result: 'Task completed' })
549
- })
550
-
551
- it('should execute a task', async () => {
552
- const doFn = createMockDo()
553
- const result = await doFn`translate ${text} to Spanish`
554
-
555
- expect(result).toBeDefined()
556
- })
557
-
558
- it('should handle complex multi-function tasks', async () => {
559
- mockGenerate.mockResolvedValue({
560
- summary: 'Article summary',
561
- people: ['John', 'Jane'],
562
- actionItems: ['Review', 'Follow up'],
266
+ describe.skipIf(!hasGateway)('generateObject()', () => {
267
+ it('should generate a structured recipe', async () => {
268
+ const result = await generateObject({
269
+ model: 'haiku',
270
+ schema: z.object({
271
+ name: z.string().describe('Recipe name'),
272
+ servings: z.number().describe('Number of servings'),
273
+ ingredients: z.array(z.string()).describe('List of ingredients'),
274
+ }),
275
+ prompt: 'Generate a simple 2-ingredient recipe.',
563
276
  })
564
277
 
565
- const doFn = createMockDo()
566
- const result = await doFn`
567
- analyze this article and give me a summary,
568
- key people mentioned, and action items
569
- ${article}
570
- `
571
-
572
- expect(result).toHaveProperty('summary')
573
- })
574
-
575
- it('is single-pass, not agentic loop', async () => {
576
- const doFn = createMockDo()
577
- await doFn`analyze ${data}`
278
+ expect(result.object).toHaveProperty('name')
279
+ expect(result.object).toHaveProperty('servings')
280
+ expect(result.object).toHaveProperty('ingredients')
281
+ expect(typeof result.object.name).toBe('string')
282
+ expect(typeof result.object.servings).toBe('number')
283
+ expect(Array.isArray(result.object.ingredients)).toBe(true)
284
+ })
285
+
286
+ it('should respect enum constraints', async () => {
287
+ const result = await generateObject({
288
+ model: 'haiku',
289
+ schema: z.object({
290
+ sentiment: z.enum(['positive', 'negative', 'neutral']).describe('Sentiment of the text'),
291
+ }),
292
+ prompt: 'Analyze the sentiment of: "I had a wonderful day today!"',
293
+ })
578
294
 
579
- // Should only call generate once (single pass)
580
- expect(mockGenerate).toHaveBeenCalledTimes(1)
295
+ expect(['positive', 'negative', 'neutral']).toContain(result.object.sentiment)
296
+ expect(result.object.sentiment).toBe('positive')
581
297
  })
582
298
  })
583
-
584
- // ============================================================================
585
- // Helper functions to create mock implementations
586
- // ============================================================================
587
-
588
- function createMockAi() {
589
- return createMockFunction('text')
590
- }
591
-
592
- function createMockSummarize() {
593
- return createMockFunction('summary')
594
- }
595
-
596
- function createMockIs() {
597
- return createMockFunction('boolean')
598
- }
599
-
600
- function createMockList() {
601
- return createMockFunction('list')
602
- }
603
-
604
- function createMockLists() {
605
- return createMockFunction('lists')
606
- }
607
-
608
- function createMockExtract() {
609
- return createMockFunction('extract')
610
- }
611
-
612
- function createMockWrite() {
613
- return createMockFunction('text')
614
- }
615
-
616
- function createMockCode() {
617
- return createMockFunction('code')
618
- }
619
-
620
- function createMockDiagram() {
621
- return createMockFunction('diagram')
622
- }
623
-
624
- function createMockSlides() {
625
- return createMockFunction('slides')
626
- }
627
-
628
- function createMockImage() {
629
- return createMockFunction('image')
630
- }
631
-
632
- function createMockVideo() {
633
- return createMockFunction('video')
634
- }
635
-
636
- function createMockResearch() {
637
- return createMockFunction('research')
638
- }
639
-
640
- function createMockDo() {
641
- return createMockFunction('do')
642
- }
643
-
644
- function createMockFunction(type: string) {
645
- return function (promptOrStrings: string | TemplateStringsArray, ...args: unknown[]) {
646
- let prompt: string
647
-
648
- if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
649
- // Tagged template
650
- prompt = (promptOrStrings as TemplateStringsArray).reduce((acc, str, i) => {
651
- const value = args[i]
652
- if (value === undefined) return acc + str
653
- if (typeof value === 'object' && value !== null) {
654
- // Convert objects to YAML for readability (matches real implementation)
655
- return acc + str + '\n' + yamlStringify(value).trim()
656
- }
657
- return acc + str + String(value)
658
- }, '')
659
-
660
- // Return chainable for options - properly make it thenable
661
- const basePromise = mockGenerate(type, prompt, {})
662
- const chainable = (options?: Record<string, unknown>) => mockGenerate(type, prompt, options || {})
663
-
664
- // Add then/catch to make it awaitable
665
- ;(chainable as unknown as Promise<unknown>).then = basePromise.then.bind(basePromise)
666
- ;(chainable as unknown as Promise<unknown>).catch = basePromise.catch.bind(basePromise)
667
-
668
- return chainable
669
- }
670
-
671
- // Regular call
672
- prompt = promptOrStrings as string
673
- return mockGenerate(type, prompt, args[0] || {})
674
- }
675
- }
676
-
677
- // ============================================================================
678
- // Test fixtures
679
- // ============================================================================
680
-
681
- const longArticle = 'This is a long article about technology and innovation...'
682
- const technicalReport = 'Technical analysis of system performance metrics...'
683
- const industry = 'fintech'
684
- const topic = 'TypeScript'
685
- const claim = 'The Earth is round'
686
- const article = 'Article mentioning John Smith and Jane Doe...'
687
- const text = 'Some text content'
688
- const company = 'Acme Corp'
689
- const market = 'SaaS'
690
- const recipient = 'John'
691
- const subject = 'Project Update'
692
- const companyName = 'TechCorp'
693
- const productName = 'ProductX'
694
- const data = { key: 'value' }