ai-functions 2.0.2 → 2.1.3

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 (130) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/CHANGELOG.md +38 -0
  3. package/LICENSE +21 -0
  4. package/README.md +361 -159
  5. package/dist/ai-promise.d.ts +47 -0
  6. package/dist/ai-promise.d.ts.map +1 -1
  7. package/dist/ai-promise.js +291 -3
  8. package/dist/ai-promise.js.map +1 -1
  9. package/dist/ai.d.ts +17 -18
  10. package/dist/ai.d.ts.map +1 -1
  11. package/dist/ai.js +93 -39
  12. package/dist/ai.js.map +1 -1
  13. package/dist/batch-map.d.ts +46 -4
  14. package/dist/batch-map.d.ts.map +1 -1
  15. package/dist/batch-map.js +35 -2
  16. package/dist/batch-map.js.map +1 -1
  17. package/dist/batch-queue.d.ts +116 -12
  18. package/dist/batch-queue.d.ts.map +1 -1
  19. package/dist/batch-queue.js +47 -2
  20. package/dist/batch-queue.js.map +1 -1
  21. package/dist/budget.d.ts +272 -0
  22. package/dist/budget.d.ts.map +1 -0
  23. package/dist/budget.js +500 -0
  24. package/dist/budget.js.map +1 -0
  25. package/dist/cache.d.ts +272 -0
  26. package/dist/cache.d.ts.map +1 -0
  27. package/dist/cache.js +412 -0
  28. package/dist/cache.js.map +1 -0
  29. package/dist/context.d.ts +32 -1
  30. package/dist/context.d.ts.map +1 -1
  31. package/dist/context.js +16 -1
  32. package/dist/context.js.map +1 -1
  33. package/dist/eval/runner.d.ts +2 -1
  34. package/dist/eval/runner.d.ts.map +1 -1
  35. package/dist/eval/runner.js.map +1 -1
  36. package/dist/generate.d.ts.map +1 -1
  37. package/dist/generate.js +6 -10
  38. package/dist/generate.js.map +1 -1
  39. package/dist/index.d.ts +27 -20
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +72 -42
  42. package/dist/index.js.map +1 -1
  43. package/dist/primitives.d.ts +17 -0
  44. package/dist/primitives.d.ts.map +1 -1
  45. package/dist/primitives.js +19 -1
  46. package/dist/primitives.js.map +1 -1
  47. package/dist/retry.d.ts +303 -0
  48. package/dist/retry.d.ts.map +1 -0
  49. package/dist/retry.js +539 -0
  50. package/dist/retry.js.map +1 -0
  51. package/dist/schema.d.ts.map +1 -1
  52. package/dist/schema.js +1 -9
  53. package/dist/schema.js.map +1 -1
  54. package/dist/tool-orchestration.d.ts +391 -0
  55. package/dist/tool-orchestration.d.ts.map +1 -0
  56. package/dist/tool-orchestration.js +663 -0
  57. package/dist/tool-orchestration.js.map +1 -0
  58. package/dist/types.d.ts +50 -33
  59. package/dist/types.d.ts.map +1 -1
  60. package/evalite.config.js +14 -0
  61. package/evals/classification.eval.js +97 -0
  62. package/evals/marketing.eval.js +289 -0
  63. package/evals/math.eval.js +83 -0
  64. package/evals/run-evals.js +151 -0
  65. package/evals/structured-output.eval.js +131 -0
  66. package/evals/writing.eval.js +105 -0
  67. package/examples/batch-blog-posts.js +128 -0
  68. package/package.json +26 -26
  69. package/src/ai-promise.ts +359 -3
  70. package/src/ai.ts +155 -110
  71. package/src/batch/anthropic.js +256 -0
  72. package/src/batch/bedrock.js +584 -0
  73. package/src/batch/cloudflare.js +287 -0
  74. package/src/batch/google.js +359 -0
  75. package/src/batch/index.js +30 -0
  76. package/src/batch/memory.js +187 -0
  77. package/src/batch/openai.js +402 -0
  78. package/src/batch-map.ts +46 -4
  79. package/src/batch-queue.ts +116 -12
  80. package/src/budget.ts +727 -0
  81. package/src/cache.ts +653 -0
  82. package/src/context.ts +33 -1
  83. package/src/eval/index.js +7 -0
  84. package/src/eval/models.js +119 -0
  85. package/src/eval/runner.js +147 -0
  86. package/src/eval/runner.ts +3 -2
  87. package/src/generate.ts +7 -12
  88. package/src/index.ts +231 -53
  89. package/src/primitives.ts +19 -1
  90. package/src/retry.ts +776 -0
  91. package/src/schema.ts +1 -10
  92. package/src/tool-orchestration.ts +1008 -0
  93. package/src/types.ts +59 -41
  94. package/test/ai-proxy.test.js +157 -0
  95. package/test/async-iterators.test.js +261 -0
  96. package/test/backward-compat.test.ts +147 -0
  97. package/test/batch-autosubmit-errors.test.ts +598 -0
  98. package/test/batch-background.test.js +352 -0
  99. package/test/batch-blog-posts.test.js +293 -0
  100. package/test/blog-generation.test.js +390 -0
  101. package/test/browse-read.test.js +480 -0
  102. package/test/budget-tracking.test.ts +800 -0
  103. package/test/cache.test.ts +712 -0
  104. package/test/context-isolation.test.ts +687 -0
  105. package/test/core-functions.test.js +490 -0
  106. package/test/decide.test.js +260 -0
  107. package/test/define.test.js +232 -0
  108. package/test/e2e-bedrock-manual.js +136 -0
  109. package/test/e2e-bedrock.test.js +164 -0
  110. package/test/e2e-flex-gateway.js +131 -0
  111. package/test/e2e-flex-manual.js +156 -0
  112. package/test/e2e-flex.test.js +174 -0
  113. package/test/e2e-google-manual.js +150 -0
  114. package/test/e2e-google.test.js +181 -0
  115. package/test/embeddings.test.js +220 -0
  116. package/test/evals/define-function.eval.test.js +309 -0
  117. package/test/evals/deterministic.eval.test.ts +376 -0
  118. package/test/evals/primitives.eval.test.js +360 -0
  119. package/test/function-types.test.js +407 -0
  120. package/test/generate-core.test.js +213 -0
  121. package/test/generate.test.js +143 -0
  122. package/test/generic-order.test.ts +342 -0
  123. package/test/implicit-batch.test.js +326 -0
  124. package/test/json-parse-error-handling.test.ts +463 -0
  125. package/test/retry.test.ts +1016 -0
  126. package/test/schema.test.js +96 -0
  127. package/test/streaming.test.ts +316 -0
  128. package/test/tagged-templates.test.js +240 -0
  129. package/test/tool-orchestration.test.ts +770 -0
  130. package/vitest.config.js +39 -0
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Blog Post Generation Live Tests
3
+ *
4
+ * These tests run LIVE against real AI providers by default.
5
+ * They verify the complete blog generation workflow:
6
+ *
7
+ * ```ts
8
+ * const titles = await list`10 blog post titles about ${topic}`
9
+ * const posts = titles.map(title => write`a blog post starting with "# ${title}"`)
10
+ * ```
11
+ *
12
+ * Tests cover:
13
+ * - Real API calls to OpenAI, Anthropic, etc.
14
+ * - Action/event storage in the database
15
+ * - Both realtime and batch execution modes
16
+ * - Multiple providers
17
+ *
18
+ * Run with:
19
+ * ```bash
20
+ * pnpm test blog-generation.live
21
+ * ```
22
+ *
23
+ * Skip live tests (CI without API keys):
24
+ * ```bash
25
+ * SKIP_LIVE_TESTS=true pnpm test blog-generation.live
26
+ * ```
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest';
31
+ import { configure, resetContext, withContext, getProvider, getModel, } from '../src/context.js';
32
+ import { createBatch } from '../src/batch-queue.js';
33
+ import { generateObject, generateText } from '../src/generate.js';
34
+ // Database provider for action/event storage
35
+ import { createMemoryProvider } from '../../ai-database/src/memory-provider.js';
36
+ // Batch storage
37
+ import '../src/batch/memory.js';
38
+ import { configureMemoryAdapter, clearBatches, getBatches } from '../src/batch/memory.js';
39
+ // ============================================================================
40
+ // Configuration
41
+ // ============================================================================
42
+ const SKIP_LIVE = process.env.SKIP_LIVE_TESTS === 'true';
43
+ const describeLive = SKIP_LIVE ? describe.skip : describe;
44
+ // Detect available providers
45
+ const hasOpenAI = !!process.env.OPENAI_API_KEY;
46
+ const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
47
+ // Provider configs
48
+ const PROVIDERS = {
49
+ openai: { model: 'gpt-4o-mini', available: hasOpenAI },
50
+ anthropic: { model: 'claude-sonnet-4-20250514', available: hasAnthropic },
51
+ };
52
+ // Get first available provider
53
+ const defaultProvider = Object.entries(PROVIDERS).find(([, cfg]) => cfg.available)?.[0] || 'openai';
54
+ const defaultModel = PROVIDERS[defaultProvider]?.model || 'gpt-4o-mini';
55
+ // ============================================================================
56
+ // Database Setup
57
+ // ============================================================================
58
+ let db;
59
+ let capturedEvents = [];
60
+ // ============================================================================
61
+ // Test Helpers
62
+ // ============================================================================
63
+ async function createAction(data) {
64
+ return db.createAction({
65
+ actor: 'test:live',
66
+ action: data.action,
67
+ object: data.object,
68
+ objectData: data.objectData,
69
+ total: data.total,
70
+ });
71
+ }
72
+ async function generateTitles(topic, count) {
73
+ const action = await createAction({
74
+ action: 'generate',
75
+ object: 'BlogTitles',
76
+ objectData: { topic, count },
77
+ total: 1,
78
+ });
79
+ await db.updateAction(action.id, { status: 'active' });
80
+ const result = await generateObject({
81
+ model: getModel(),
82
+ schema: { titles: [`${count} blog post titles about ${topic}`] },
83
+ prompt: `Generate exactly ${count} creative blog post titles about "${topic}".`,
84
+ });
85
+ const titles = result.object.titles;
86
+ await db.updateAction(action.id, {
87
+ status: 'completed',
88
+ progress: 1,
89
+ result: { titles },
90
+ });
91
+ return { titles, action };
92
+ }
93
+ async function generatePost(title) {
94
+ const result = await generateText({
95
+ model: getModel(),
96
+ prompt: `Write a blog post starting with "# ${title}"\n\nInclude an introduction, 2-3 sections, and conclusion. Be concise.`,
97
+ maxTokens: 800,
98
+ });
99
+ return result.text;
100
+ }
101
+ async function generatePosts(titles, mode = 'realtime') {
102
+ const action = await createAction({
103
+ action: 'generate',
104
+ object: 'BlogPosts',
105
+ objectData: { titles, mode },
106
+ total: titles.length,
107
+ });
108
+ await db.updateAction(action.id, { status: 'active' });
109
+ const posts = [];
110
+ if (mode === 'batch') {
111
+ const batch = createBatch({
112
+ provider: getProvider(),
113
+ model: getModel(),
114
+ metadata: { actionId: action.id },
115
+ });
116
+ titles.forEach((title, i) => {
117
+ batch.add(`Write a blog post starting with "# ${title}"\n\nBe concise.`, { customId: `post-${i}`, metadata: { title } });
118
+ });
119
+ const { completion } = await batch.submit();
120
+ const results = await completion;
121
+ for (const result of results) {
122
+ posts.push(result.status === 'completed' ? result.result : `[Failed]`);
123
+ await db.updateAction(action.id, { progress: posts.length });
124
+ }
125
+ }
126
+ else {
127
+ for (let i = 0; i < titles.length; i++) {
128
+ const post = await generatePost(titles[i]);
129
+ posts.push(post);
130
+ await db.updateAction(action.id, { progress: i + 1 });
131
+ }
132
+ }
133
+ await db.updateAction(action.id, { status: 'completed', result: { count: posts.length } });
134
+ return { posts, action };
135
+ }
136
+ // ============================================================================
137
+ // Tests
138
+ // ============================================================================
139
+ describeLive('Blog Generation (Live)', () => {
140
+ beforeAll(() => {
141
+ console.log('\n🔴 LIVE TEST MODE');
142
+ console.log(` Default provider: ${defaultProvider}`);
143
+ console.log(` OpenAI available: ${hasOpenAI}`);
144
+ console.log(` Anthropic available: ${hasAnthropic}\n`);
145
+ });
146
+ beforeEach(() => {
147
+ resetContext();
148
+ db = createMemoryProvider();
149
+ capturedEvents = [];
150
+ db.on('*', (e) => capturedEvents.push(e));
151
+ configure({ provider: defaultProvider, model: defaultModel, batchMode: 'immediate' });
152
+ clearBatches();
153
+ configureMemoryAdapter({});
154
+ });
155
+ afterEach(() => {
156
+ resetContext();
157
+ clearBatches();
158
+ db.clear();
159
+ });
160
+ // ==========================================================================
161
+ // Core Pattern Tests
162
+ // ==========================================================================
163
+ describe('Core Pattern: list → write', () => {
164
+ it('generates titles from a topic', async () => {
165
+ const { titles, action } = await generateTitles('building AI products', 3);
166
+ expect(titles).toHaveLength(3);
167
+ titles.forEach((t) => expect(typeof t).toBe('string'));
168
+ // Verify action tracking
169
+ expect(action.status).toBe('completed');
170
+ expect(action.result).toEqual({ titles });
171
+ }, 30000);
172
+ it('generates a blog post from a title', async () => {
173
+ const title = 'The Future of AI Development';
174
+ const post = await generatePost(title);
175
+ expect(post).toContain(`# ${title}`);
176
+ expect(post.length).toBeGreaterThan(200);
177
+ }, 30000);
178
+ it('generates multiple posts from titles', async () => {
179
+ const { titles } = await generateTitles('startup growth', 2);
180
+ const { posts, action } = await generatePosts(titles);
181
+ expect(posts).toHaveLength(2);
182
+ posts.forEach((post, i) => {
183
+ expect(post).toContain(`# ${titles[i]}`);
184
+ });
185
+ expect(action.progress).toBe(2);
186
+ expect(action.total).toBe(2);
187
+ }, 60000);
188
+ });
189
+ // ==========================================================================
190
+ // Action & Event Storage
191
+ // ==========================================================================
192
+ describe('Action & Event Storage', () => {
193
+ it('creates actions with verb conjugation', async () => {
194
+ const { action } = await generateTitles('test topic', 2);
195
+ expect(action.action).toBe('generate');
196
+ expect(action.act).toBe('generates');
197
+ expect(action.activity).toBe('generating');
198
+ expect(action.actor).toBe('test:live');
199
+ }, 30000);
200
+ it('tracks action lifecycle timestamps', async () => {
201
+ const { action } = await generateTitles('test', 2);
202
+ const final = await db.getAction(action.id);
203
+ expect(final?.createdAt).toBeInstanceOf(Date);
204
+ expect(final?.startedAt).toBeInstanceOf(Date);
205
+ expect(final?.completedAt).toBeInstanceOf(Date);
206
+ expect(final?.startedAt.getTime()).toBeGreaterThanOrEqual(final?.createdAt.getTime());
207
+ expect(final?.completedAt.getTime()).toBeGreaterThanOrEqual(final?.startedAt.getTime());
208
+ }, 30000);
209
+ it('emits events for state transitions', async () => {
210
+ await generateTitles('test', 2);
211
+ const actionEvents = capturedEvents.filter((e) => e.event.startsWith('Action.'));
212
+ const eventTypes = actionEvents.map((e) => e.event);
213
+ expect(eventTypes).toContain('Action.created');
214
+ expect(eventTypes).toContain('Action.started');
215
+ expect(eventTypes).toContain('Action.completed');
216
+ }, 30000);
217
+ it('stores action result data', async () => {
218
+ const { titles, action } = await generateTitles('AI development', 3);
219
+ const stored = await db.getAction(action.id);
220
+ expect(stored?.result).toEqual({ titles });
221
+ expect(stored?.objectData).toEqual({ topic: 'AI development', count: 3 });
222
+ }, 30000);
223
+ it('queries actions by status', async () => {
224
+ await generateTitles('topic 1', 2);
225
+ await generateTitles('topic 2', 2);
226
+ const completed = await db.listActions({ status: 'completed' });
227
+ expect(completed.length).toBe(2);
228
+ }, 60000);
229
+ it('queries events by type pattern', async () => {
230
+ await generateTitles('test', 2);
231
+ const created = await db.listEvents({ event: 'Action.created' });
232
+ const allAction = await db.listEvents({ event: 'Action.*' });
233
+ expect(created.length).toBeGreaterThanOrEqual(1);
234
+ expect(allAction.length).toBeGreaterThanOrEqual(3); // created, started, completed
235
+ }, 30000);
236
+ });
237
+ // ==========================================================================
238
+ // Realtime Execution
239
+ // ==========================================================================
240
+ describe('Realtime Execution', () => {
241
+ beforeEach(() => {
242
+ configure({ batchMode: 'immediate' });
243
+ });
244
+ it('executes requests immediately', async () => {
245
+ const start = Date.now();
246
+ const { titles } = await generateTitles('quick test', 2);
247
+ const elapsed = Date.now() - start;
248
+ expect(titles).toHaveLength(2);
249
+ // Should complete in reasonable time (not batched/delayed)
250
+ expect(elapsed).toBeLessThan(30000);
251
+ }, 30000);
252
+ it('tracks progress during sequential generation', async () => {
253
+ const titles = ['Post One', 'Post Two'];
254
+ const { posts, action } = await generatePosts(titles, 'realtime');
255
+ expect(posts).toHaveLength(2);
256
+ const final = await db.getAction(action.id);
257
+ expect(final?.progress).toBe(2);
258
+ expect(final?.total).toBe(2);
259
+ }, 60000);
260
+ });
261
+ // ==========================================================================
262
+ // Batch Execution
263
+ // ==========================================================================
264
+ describe('Batch Execution', () => {
265
+ beforeEach(() => {
266
+ configure({ batchMode: 'deferred' });
267
+ });
268
+ it('creates and submits batch jobs', async () => {
269
+ const titles = ['Batch Post 1', 'Batch Post 2'];
270
+ const { posts, action } = await generatePosts(titles, 'batch');
271
+ expect(posts).toHaveLength(2);
272
+ // Verify batch was stored
273
+ const batches = getBatches();
274
+ expect(batches.size).toBe(1);
275
+ const [, batch] = [...batches.entries()][0];
276
+ expect(batch.items).toHaveLength(2);
277
+ expect(batch.status).toBe('completed');
278
+ }, 90000);
279
+ it('stores batch metadata', async () => {
280
+ const batch = createBatch({
281
+ provider: getProvider(),
282
+ model: getModel(),
283
+ metadata: { task: 'test-batch', timestamp: Date.now() },
284
+ });
285
+ batch.add('Write a test post', { customId: 'test-1' });
286
+ const { job } = await batch.submit();
287
+ await batch.wait();
288
+ const stored = getBatches().get(job.id);
289
+ expect(stored?.options.metadata?.task).toBe('test-batch');
290
+ }, 30000);
291
+ });
292
+ // ==========================================================================
293
+ // Multi-Provider Tests
294
+ // ==========================================================================
295
+ describe('Multi-Provider', () => {
296
+ it.skipIf(!hasOpenAI)('generates with OpenAI', async () => {
297
+ configure({ provider: 'openai', model: 'gpt-4o-mini' });
298
+ const { titles } = await generateTitles('OpenAI test', 2);
299
+ expect(titles).toHaveLength(2);
300
+ expect(getProvider()).toBe('openai');
301
+ }, 30000);
302
+ it.skipIf(!hasAnthropic)('generates with Anthropic', async () => {
303
+ configure({ provider: 'anthropic', model: 'claude-sonnet-4-20250514' });
304
+ const { titles } = await generateTitles('Anthropic test', 2);
305
+ expect(titles).toHaveLength(2);
306
+ expect(getProvider()).toBe('anthropic');
307
+ }, 30000);
308
+ it.skipIf(!hasOpenAI || !hasAnthropic)('switches providers mid-workflow', async () => {
309
+ // Generate titles with OpenAI
310
+ configure({ provider: 'openai', model: 'gpt-4o-mini' });
311
+ const { titles } = await generateTitles('cross-provider test', 2);
312
+ // Generate posts with Anthropic
313
+ const posts = await withContext({ provider: 'anthropic', model: 'claude-sonnet-4-20250514' }, async () => {
314
+ expect(getProvider()).toBe('anthropic');
315
+ return Promise.all(titles.slice(0, 1).map(generatePost));
316
+ });
317
+ expect(posts).toHaveLength(1);
318
+ expect(getProvider()).toBe('openai'); // restored
319
+ }, 60000);
320
+ it.skipIf(!hasOpenAI || !hasAnthropic)('runs providers in parallel', async () => {
321
+ const [openaiResult, anthropicResult] = await Promise.all([
322
+ withContext({ provider: 'openai', model: 'gpt-4o-mini' }, () => generateTitles('OpenAI parallel', 2)),
323
+ withContext({ provider: 'anthropic', model: 'claude-sonnet-4-20250514' }, () => generateTitles('Anthropic parallel', 2)),
324
+ ]);
325
+ expect(openaiResult.titles).toHaveLength(2);
326
+ expect(anthropicResult.titles).toHaveLength(2);
327
+ }, 60000);
328
+ });
329
+ // ==========================================================================
330
+ // Error Handling
331
+ // ==========================================================================
332
+ describe('Error Handling', () => {
333
+ it('tracks failed actions', async () => {
334
+ const action = await createAction({
335
+ action: 'generate',
336
+ object: 'FailTest',
337
+ total: 1,
338
+ });
339
+ await db.updateAction(action.id, { status: 'active' });
340
+ // Simulate failure
341
+ await db.updateAction(action.id, {
342
+ status: 'failed',
343
+ error: 'Simulated failure',
344
+ });
345
+ const failed = await db.listActions({ status: 'failed' });
346
+ expect(failed).toHaveLength(1);
347
+ expect(failed[0].error).toBe('Simulated failure');
348
+ // Verify failure event
349
+ const failEvents = capturedEvents.filter((e) => e.event === 'Action.failed');
350
+ expect(failEvents.length).toBe(1);
351
+ });
352
+ it('handles invalid model gracefully', async () => {
353
+ configure({ model: 'invalid-model-xyz' });
354
+ const action = await createAction({
355
+ action: 'generate',
356
+ object: 'InvalidModelTest',
357
+ total: 1,
358
+ });
359
+ await db.updateAction(action.id, { status: 'active' });
360
+ try {
361
+ await generateObject({
362
+ model: 'invalid-model-xyz',
363
+ schema: { test: 'test' },
364
+ prompt: 'test',
365
+ });
366
+ }
367
+ catch (e) {
368
+ await db.updateAction(action.id, {
369
+ status: 'failed',
370
+ error: e instanceof Error ? e.message : 'Unknown error',
371
+ });
372
+ }
373
+ const final = await db.getAction(action.id);
374
+ expect(final?.status).toBe('failed');
375
+ expect(final?.error).toBeDefined();
376
+ }, 30000);
377
+ });
378
+ // ==========================================================================
379
+ // Database Statistics
380
+ // ==========================================================================
381
+ describe('Database Statistics', () => {
382
+ it('tracks aggregate stats', async () => {
383
+ await generateTitles('stats test 1', 2);
384
+ await generateTitles('stats test 2', 2);
385
+ const stats = db.stats();
386
+ expect(stats.actions.completed).toBe(2);
387
+ expect(stats.events).toBeGreaterThan(0);
388
+ }, 60000);
389
+ });
390
+ });