ai-functions 2.1.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +55 -1
  3. package/README.md +38 -0
  4. package/dist/ai-promise.d.ts +3 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +135 -64
  7. package/dist/ai-promise.js.map +1 -1
  8. package/dist/ai-schemas.d.ts +56 -0
  9. package/dist/ai-schemas.d.ts.map +1 -0
  10. package/dist/ai-schemas.js +53 -0
  11. package/dist/ai-schemas.js.map +1 -0
  12. package/dist/ai.d.ts +16 -242
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +51 -858
  15. package/dist/ai.js.map +1 -1
  16. package/dist/batch/anthropic.d.ts +6 -4
  17. package/dist/batch/anthropic.d.ts.map +1 -1
  18. package/dist/batch/anthropic.js +83 -145
  19. package/dist/batch/anthropic.js.map +1 -1
  20. package/dist/batch/bedrock.d.ts +8 -30
  21. package/dist/batch/bedrock.d.ts.map +1 -1
  22. package/dist/batch/bedrock.js +155 -338
  23. package/dist/batch/bedrock.js.map +1 -1
  24. package/dist/batch/cloudflare.d.ts +8 -20
  25. package/dist/batch/cloudflare.d.ts.map +1 -1
  26. package/dist/batch/cloudflare.js +68 -189
  27. package/dist/batch/cloudflare.js.map +1 -1
  28. package/dist/batch/google.d.ts +6 -20
  29. package/dist/batch/google.d.ts.map +1 -1
  30. package/dist/batch/google.js +70 -238
  31. package/dist/batch/google.js.map +1 -1
  32. package/dist/batch/index.d.ts +4 -1
  33. package/dist/batch/index.d.ts.map +1 -1
  34. package/dist/batch/index.js +4 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/batch/memory.d.ts +1 -1
  37. package/dist/batch/memory.d.ts.map +1 -1
  38. package/dist/batch/memory.js +14 -10
  39. package/dist/batch/memory.js.map +1 -1
  40. package/dist/batch/openai.d.ts +11 -14
  41. package/dist/batch/openai.d.ts.map +1 -1
  42. package/dist/batch/openai.js +52 -156
  43. package/dist/batch/openai.js.map +1 -1
  44. package/dist/batch/provider.d.ts +111 -0
  45. package/dist/batch/provider.d.ts.map +1 -0
  46. package/dist/batch/provider.js +233 -0
  47. package/dist/batch/provider.js.map +1 -0
  48. package/dist/batch-map.d.ts.map +1 -1
  49. package/dist/batch-map.js +23 -17
  50. package/dist/batch-map.js.map +1 -1
  51. package/dist/batch-queue.d.ts +65 -0
  52. package/dist/batch-queue.d.ts.map +1 -1
  53. package/dist/batch-queue.js +169 -14
  54. package/dist/batch-queue.js.map +1 -1
  55. package/dist/budget.d.ts.map +1 -1
  56. package/dist/budget.js +27 -14
  57. package/dist/budget.js.map +1 -1
  58. package/dist/cache.d.ts +23 -0
  59. package/dist/cache.d.ts.map +1 -1
  60. package/dist/cache.js +36 -15
  61. package/dist/cache.js.map +1 -1
  62. package/dist/context.d.ts +26 -8
  63. package/dist/context.d.ts.map +1 -1
  64. package/dist/context.js +64 -62
  65. package/dist/context.js.map +1 -1
  66. package/dist/digital-objects-registry.d.ts +229 -0
  67. package/dist/digital-objects-registry.d.ts.map +1 -0
  68. package/dist/digital-objects-registry.js +617 -0
  69. package/dist/digital-objects-registry.js.map +1 -0
  70. package/dist/embeddings.d.ts +2 -2
  71. package/dist/embeddings.d.ts.map +1 -1
  72. package/dist/errors.d.ts +22 -0
  73. package/dist/errors.d.ts.map +1 -0
  74. package/dist/errors.js +35 -0
  75. package/dist/errors.js.map +1 -0
  76. package/dist/eval/runner.d.ts +8 -0
  77. package/dist/eval/runner.d.ts.map +1 -1
  78. package/dist/eval/runner.js +41 -35
  79. package/dist/eval/runner.js.map +1 -1
  80. package/dist/eval-log/in-memory.d.ts +34 -0
  81. package/dist/eval-log/in-memory.d.ts.map +1 -0
  82. package/dist/eval-log/in-memory.js +84 -0
  83. package/dist/eval-log/in-memory.js.map +1 -0
  84. package/dist/eval-log/index.d.ts +29 -0
  85. package/dist/eval-log/index.d.ts.map +1 -0
  86. package/dist/eval-log/index.js +39 -0
  87. package/dist/eval-log/index.js.map +1 -0
  88. package/dist/eval-log/types.d.ts +101 -0
  89. package/dist/eval-log/types.d.ts.map +1 -0
  90. package/dist/eval-log/types.js +16 -0
  91. package/dist/eval-log/types.js.map +1 -0
  92. package/dist/function-registry.d.ts +116 -0
  93. package/dist/function-registry.d.ts.map +1 -0
  94. package/dist/function-registry.js +546 -0
  95. package/dist/function-registry.js.map +1 -0
  96. package/dist/generate.d.ts +9 -3
  97. package/dist/generate.d.ts.map +1 -1
  98. package/dist/generate.js +18 -18
  99. package/dist/generate.js.map +1 -1
  100. package/dist/index.d.ts +18 -11
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +35 -18
  103. package/dist/index.js.map +1 -1
  104. package/dist/logger.d.ts +118 -0
  105. package/dist/logger.d.ts.map +1 -0
  106. package/dist/logger.js +187 -0
  107. package/dist/logger.js.map +1 -0
  108. package/dist/middleware/budget.d.ts +84 -0
  109. package/dist/middleware/budget.d.ts.map +1 -0
  110. package/dist/middleware/budget.js +110 -0
  111. package/dist/middleware/budget.js.map +1 -0
  112. package/dist/middleware/cache.d.ts +103 -0
  113. package/dist/middleware/cache.d.ts.map +1 -0
  114. package/dist/middleware/cache.js +228 -0
  115. package/dist/middleware/cache.js.map +1 -0
  116. package/dist/middleware/embed-cache.d.ts +99 -0
  117. package/dist/middleware/embed-cache.d.ts.map +1 -0
  118. package/dist/middleware/embed-cache.js +128 -0
  119. package/dist/middleware/embed-cache.js.map +1 -0
  120. package/dist/middleware/index.d.ts +11 -0
  121. package/dist/middleware/index.d.ts.map +1 -0
  122. package/dist/middleware/index.js +11 -0
  123. package/dist/middleware/index.js.map +1 -0
  124. package/dist/middleware/trace.d.ts +103 -0
  125. package/dist/middleware/trace.d.ts.map +1 -0
  126. package/dist/middleware/trace.js +176 -0
  127. package/dist/middleware/trace.js.map +1 -0
  128. package/dist/primitives.d.ts +120 -1
  129. package/dist/primitives.d.ts.map +1 -1
  130. package/dist/primitives.js +398 -26
  131. package/dist/primitives.js.map +1 -1
  132. package/dist/retry.d.ts +66 -1
  133. package/dist/retry.d.ts.map +1 -1
  134. package/dist/retry.js +115 -8
  135. package/dist/retry.js.map +1 -1
  136. package/dist/schema.js +2 -2
  137. package/dist/schema.js.map +1 -1
  138. package/dist/telemetry.d.ts +128 -0
  139. package/dist/telemetry.d.ts.map +1 -0
  140. package/dist/telemetry.js +285 -0
  141. package/dist/telemetry.js.map +1 -0
  142. package/dist/template.d.ts.map +1 -1
  143. package/dist/template.js +6 -1
  144. package/dist/template.js.map +1 -1
  145. package/dist/tool-orchestration.d.ts +66 -4
  146. package/dist/tool-orchestration.d.ts.map +1 -1
  147. package/dist/tool-orchestration.js +123 -23
  148. package/dist/tool-orchestration.js.map +1 -1
  149. package/dist/type-guards.d.ts +28 -0
  150. package/dist/type-guards.d.ts.map +1 -0
  151. package/dist/type-guards.js +29 -0
  152. package/dist/type-guards.js.map +1 -0
  153. package/dist/types.d.ts +135 -17
  154. package/dist/types.d.ts.map +1 -1
  155. package/dist/types.js +36 -1
  156. package/dist/types.js.map +1 -1
  157. package/dist/wrap-for-v3.d.ts +80 -0
  158. package/dist/wrap-for-v3.d.ts.map +1 -0
  159. package/dist/wrap-for-v3.js +89 -0
  160. package/dist/wrap-for-v3.js.map +1 -0
  161. package/examples/00-quickstart.ts +232 -0
  162. package/examples/01-rag-chatbot.ts +212 -0
  163. package/examples/02-multi-agent-research.ts +290 -0
  164. package/examples/03-email-classification.ts +379 -0
  165. package/examples/04-content-moderation.ts +400 -0
  166. package/examples/05-document-extraction.ts +455 -0
  167. package/examples/06-streaming-chat-nextjs.ts +437 -0
  168. package/examples/07-cloudflare-worker.ts +483 -0
  169. package/examples/08-batch-processing.ts +491 -0
  170. package/examples/09-budget-constrained.ts +527 -0
  171. package/examples/10-tool-orchestration.ts +565 -0
  172. package/examples/11-retry-resilience.ts +403 -0
  173. package/examples/12-caching-strategies.ts +422 -0
  174. package/examples/README.md +145 -0
  175. package/package.json +28 -25
  176. package/src/ai-promise.ts +226 -140
  177. package/src/ai-schemas.ts +122 -0
  178. package/src/ai.ts +69 -1176
  179. package/src/batch/anthropic.ts +96 -161
  180. package/src/batch/bedrock.ts +203 -454
  181. package/src/batch/cloudflare.ts +99 -282
  182. package/src/batch/google.ts +91 -297
  183. package/src/batch/index.ts +4 -1
  184. package/src/batch/memory.ts +15 -10
  185. package/src/batch/openai.ts +65 -193
  186. package/src/batch/provider.ts +336 -0
  187. package/src/batch-map.ts +29 -24
  188. package/src/batch-queue.ts +200 -11
  189. package/src/budget.ts +31 -18
  190. package/src/cache.ts +45 -17
  191. package/src/context.ts +106 -77
  192. package/src/digital-objects-registry.ts +750 -0
  193. package/src/errors.ts +37 -0
  194. package/src/eval/runner.ts +60 -36
  195. package/src/eval-log/in-memory.ts +90 -0
  196. package/src/eval-log/index.ts +46 -0
  197. package/src/eval-log/types.ts +110 -0
  198. package/src/function-registry.ts +671 -0
  199. package/src/generate.ts +33 -28
  200. package/src/index.ts +119 -21
  201. package/src/logger.ts +232 -0
  202. package/src/middleware/budget.ts +171 -0
  203. package/src/middleware/cache.ts +299 -0
  204. package/src/middleware/embed-cache.ts +195 -0
  205. package/src/middleware/index.ts +23 -0
  206. package/src/middleware/trace.ts +248 -0
  207. package/src/primitives.ts +589 -62
  208. package/src/retry.ts +144 -18
  209. package/src/schema.ts +8 -8
  210. package/src/telemetry.ts +403 -0
  211. package/src/template.ts +8 -4
  212. package/src/tool-orchestration.ts +213 -48
  213. package/src/type-guards.ts +31 -0
  214. package/src/types.ts +164 -25
  215. package/src/wrap-for-v3.ts +105 -0
  216. package/test/ai-promise.test.ts +1080 -0
  217. package/test/ai-proxy.test.ts +1 -1
  218. package/test/batch-autosubmit-errors.test.ts +49 -37
  219. package/test/batch-blog-posts.test.ts +87 -129
  220. package/test/core-functions.test.ts +183 -579
  221. package/test/decide.test.ts +154 -322
  222. package/test/define.test.ts +211 -8
  223. package/test/digital-objects-registry.test.ts +760 -0
  224. package/test/embedding-cache-middleware.test.ts +140 -0
  225. package/test/generate-core.test.ts +140 -229
  226. package/test/implicit-batch.test.ts +22 -65
  227. package/test/retry-policy-integration.test.ts +117 -0
  228. package/test/schema.test.ts +55 -19
  229. package/test/template.test.ts +1164 -0
  230. package/test/tool-orchestration.test.ts +270 -0
  231. package/test/wrap-for-v3.test.ts +612 -0
  232. package/vitest.config.js +6 -0
  233. package/vitest.config.ts +20 -0
  234. package/LICENSE +0 -21
  235. package/dist/rpc/auth.d.ts +0 -69
  236. package/dist/rpc/auth.d.ts.map +0 -1
  237. package/dist/rpc/auth.js +0 -136
  238. package/dist/rpc/auth.js.map +0 -1
  239. package/dist/rpc/client.d.ts +0 -62
  240. package/dist/rpc/client.d.ts.map +0 -1
  241. package/dist/rpc/client.js +0 -103
  242. package/dist/rpc/client.js.map +0 -1
  243. package/dist/rpc/deferred.d.ts +0 -60
  244. package/dist/rpc/deferred.d.ts.map +0 -1
  245. package/dist/rpc/deferred.js +0 -96
  246. package/dist/rpc/deferred.js.map +0 -1
  247. package/dist/rpc/index.d.ts +0 -22
  248. package/dist/rpc/index.d.ts.map +0 -1
  249. package/dist/rpc/index.js +0 -38
  250. package/dist/rpc/index.js.map +0 -1
  251. package/dist/rpc/local.d.ts +0 -42
  252. package/dist/rpc/local.d.ts.map +0 -1
  253. package/dist/rpc/local.js +0 -50
  254. package/dist/rpc/local.js.map +0 -1
  255. package/dist/rpc/server.d.ts +0 -165
  256. package/dist/rpc/server.d.ts.map +0 -1
  257. package/dist/rpc/server.js +0 -405
  258. package/dist/rpc/server.js.map +0 -1
  259. package/dist/rpc/session.d.ts +0 -32
  260. package/dist/rpc/session.d.ts.map +0 -1
  261. package/dist/rpc/session.js +0 -43
  262. package/dist/rpc/session.js.map +0 -1
  263. package/dist/rpc/transport.d.ts +0 -306
  264. package/dist/rpc/transport.d.ts.map +0 -1
  265. package/dist/rpc/transport.js +0 -731
  266. package/dist/rpc/transport.js.map +0 -1
  267. package/src/batch/anthropic.js +0 -256
  268. package/src/batch/bedrock.js +0 -584
  269. package/src/batch/cloudflare.js +0 -287
  270. package/src/batch/google.js +0 -359
  271. package/src/batch/index.js +0 -30
  272. package/src/batch/memory.js +0 -187
  273. package/src/batch/openai.js +0 -402
  274. package/src/eval/index.js +0 -7
  275. package/src/eval/models.js +0 -119
  276. package/src/eval/runner.js +0 -147
  277. package/test/schema.test.js +0 -96
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Tests for embeddingCacheMiddleware — embedding-side analogue of
3
+ * cacheMiddleware for `wrapEmbeddingModel`.
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
7
+ import { wrapEmbeddingModel } from 'ai'
8
+ import { MockEmbeddingModelV3 } from 'ai/test'
9
+ import { embeddingCacheMiddleware } from '../src/index.js'
10
+
11
+ describe('embeddingCacheMiddleware', () => {
12
+ const originalGate = process.env['V3_EVAL_CACHE']
13
+
14
+ beforeEach(() => {
15
+ process.env['V3_EVAL_CACHE'] = '1'
16
+ })
17
+
18
+ afterEach(() => {
19
+ if (originalGate === undefined) {
20
+ delete process.env['V3_EVAL_CACHE']
21
+ } else {
22
+ process.env['V3_EVAL_CACHE'] = originalGate
23
+ }
24
+ })
25
+
26
+ it('returns cached embeddings on second call with same values', async () => {
27
+ let callCount = 0
28
+ const upstream = new MockEmbeddingModelV3({
29
+ modelId: 'test-embed',
30
+ doEmbed: async () => {
31
+ callCount++
32
+ return {
33
+ embeddings: [
34
+ [0.1, 0.2, 0.3],
35
+ [0.4, 0.5, 0.6],
36
+ ],
37
+ }
38
+ },
39
+ })
40
+
41
+ const wrapped = wrapEmbeddingModel({
42
+ model: upstream,
43
+ middleware: embeddingCacheMiddleware({ enabled: true }),
44
+ })
45
+
46
+ const r1 = await wrapped.doEmbed({ values: ['a', 'b'] })
47
+ expect(r1.embeddings).toEqual([
48
+ [0.1, 0.2, 0.3],
49
+ [0.4, 0.5, 0.6],
50
+ ])
51
+ expect(callCount).toBe(1)
52
+
53
+ const r2 = await wrapped.doEmbed({ values: ['a', 'b'] })
54
+ expect(r2.embeddings).toEqual([
55
+ [0.1, 0.2, 0.3],
56
+ [0.4, 0.5, 0.6],
57
+ ])
58
+ expect(callCount).toBe(1) // cache hit — no second upstream call
59
+ })
60
+
61
+ it('treats different value batches as separate keys', async () => {
62
+ let callCount = 0
63
+ const upstream = new MockEmbeddingModelV3({
64
+ modelId: 'test-embed',
65
+ doEmbed: async ({ values }) => {
66
+ callCount++
67
+ return {
68
+ embeddings: values.map((_, i) => [i, i + 1]),
69
+ }
70
+ },
71
+ })
72
+ const wrapped = wrapEmbeddingModel({
73
+ model: upstream,
74
+ middleware: embeddingCacheMiddleware({ enabled: true }),
75
+ })
76
+
77
+ await wrapped.doEmbed({ values: ['a'] })
78
+ await wrapped.doEmbed({ values: ['b'] })
79
+ expect(callCount).toBe(2)
80
+ })
81
+
82
+ it('falls through to upstream when env gate is unset', async () => {
83
+ delete process.env['V3_EVAL_CACHE']
84
+ let callCount = 0
85
+ const upstream = new MockEmbeddingModelV3({
86
+ modelId: 'test-embed',
87
+ doEmbed: async () => {
88
+ callCount++
89
+ return { embeddings: [[1, 2, 3]] }
90
+ },
91
+ })
92
+ const wrapped = wrapEmbeddingModel({
93
+ model: upstream,
94
+ middleware: embeddingCacheMiddleware(),
95
+ })
96
+
97
+ await wrapped.doEmbed({ values: ['x'] })
98
+ await wrapped.doEmbed({ values: ['x'] })
99
+ expect(callCount).toBe(2) // no caching when gate is off
100
+ })
101
+
102
+ it('respects explicit enabled: false override', async () => {
103
+ let callCount = 0
104
+ const upstream = new MockEmbeddingModelV3({
105
+ modelId: 'test-embed',
106
+ doEmbed: async () => {
107
+ callCount++
108
+ return { embeddings: [[1, 2, 3]] }
109
+ },
110
+ })
111
+ const wrapped = wrapEmbeddingModel({
112
+ model: upstream,
113
+ middleware: embeddingCacheMiddleware({ enabled: false }),
114
+ })
115
+
116
+ await wrapped.doEmbed({ values: ['x'] })
117
+ await wrapped.doEmbed({ values: ['x'] })
118
+ expect(callCount).toBe(2)
119
+ })
120
+
121
+ it('evicts entries past TTL and re-fetches', async () => {
122
+ let callCount = 0
123
+ const upstream = new MockEmbeddingModelV3({
124
+ modelId: 'test-embed',
125
+ doEmbed: async () => {
126
+ callCount++
127
+ return { embeddings: [[callCount]] }
128
+ },
129
+ })
130
+ const wrapped = wrapEmbeddingModel({
131
+ model: upstream,
132
+ middleware: embeddingCacheMiddleware({ enabled: true, ttlMs: -1 }),
133
+ // TTL = -1 → every entry is "older than -1 ms" → always evicted on access
134
+ })
135
+
136
+ await wrapped.doEmbed({ values: ['x'] })
137
+ await wrapped.doEmbed({ values: ['x'] })
138
+ expect(callCount).toBe(2) // TTL expired, re-fetch
139
+ })
140
+ })
@@ -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
  })