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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +55 -1
- package/README.md +38 -0
- package/dist/ai-promise.d.ts +3 -3
- package/dist/ai-promise.d.ts.map +1 -1
- package/dist/ai-promise.js +135 -64
- package/dist/ai-promise.js.map +1 -1
- package/dist/ai-schemas.d.ts +56 -0
- package/dist/ai-schemas.d.ts.map +1 -0
- package/dist/ai-schemas.js +53 -0
- package/dist/ai-schemas.js.map +1 -0
- package/dist/ai.d.ts +16 -242
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +51 -858
- package/dist/ai.js.map +1 -1
- package/dist/batch/anthropic.d.ts +6 -4
- package/dist/batch/anthropic.d.ts.map +1 -1
- package/dist/batch/anthropic.js +83 -145
- package/dist/batch/anthropic.js.map +1 -1
- package/dist/batch/bedrock.d.ts +8 -30
- package/dist/batch/bedrock.d.ts.map +1 -1
- package/dist/batch/bedrock.js +155 -338
- package/dist/batch/bedrock.js.map +1 -1
- package/dist/batch/cloudflare.d.ts +8 -20
- package/dist/batch/cloudflare.d.ts.map +1 -1
- package/dist/batch/cloudflare.js +68 -189
- package/dist/batch/cloudflare.js.map +1 -1
- package/dist/batch/google.d.ts +6 -20
- package/dist/batch/google.d.ts.map +1 -1
- package/dist/batch/google.js +70 -238
- package/dist/batch/google.js.map +1 -1
- package/dist/batch/index.d.ts +4 -1
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +4 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/batch/memory.d.ts +1 -1
- package/dist/batch/memory.d.ts.map +1 -1
- package/dist/batch/memory.js +14 -10
- package/dist/batch/memory.js.map +1 -1
- package/dist/batch/openai.d.ts +11 -14
- package/dist/batch/openai.d.ts.map +1 -1
- package/dist/batch/openai.js +52 -156
- package/dist/batch/openai.js.map +1 -1
- package/dist/batch/provider.d.ts +111 -0
- package/dist/batch/provider.d.ts.map +1 -0
- package/dist/batch/provider.js +233 -0
- package/dist/batch/provider.js.map +1 -0
- package/dist/batch-map.d.ts.map +1 -1
- package/dist/batch-map.js +23 -17
- package/dist/batch-map.js.map +1 -1
- package/dist/batch-queue.d.ts +65 -0
- package/dist/batch-queue.d.ts.map +1 -1
- package/dist/batch-queue.js +169 -14
- package/dist/batch-queue.js.map +1 -1
- package/dist/budget.d.ts.map +1 -1
- package/dist/budget.js +27 -14
- package/dist/budget.js.map +1 -1
- package/dist/cache.d.ts +23 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +36 -15
- package/dist/cache.js.map +1 -1
- package/dist/context.d.ts +26 -8
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +64 -62
- package/dist/context.js.map +1 -1
- package/dist/digital-objects-registry.d.ts +229 -0
- package/dist/digital-objects-registry.d.ts.map +1 -0
- package/dist/digital-objects-registry.js +617 -0
- package/dist/digital-objects-registry.js.map +1 -0
- package/dist/embeddings.d.ts +2 -2
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +35 -0
- package/dist/errors.js.map +1 -0
- package/dist/eval/runner.d.ts +8 -0
- package/dist/eval/runner.d.ts.map +1 -1
- package/dist/eval/runner.js +41 -35
- package/dist/eval/runner.js.map +1 -1
- package/dist/eval-log/in-memory.d.ts +34 -0
- package/dist/eval-log/in-memory.d.ts.map +1 -0
- package/dist/eval-log/in-memory.js +84 -0
- package/dist/eval-log/in-memory.js.map +1 -0
- package/dist/eval-log/index.d.ts +29 -0
- package/dist/eval-log/index.d.ts.map +1 -0
- package/dist/eval-log/index.js +39 -0
- package/dist/eval-log/index.js.map +1 -0
- package/dist/eval-log/types.d.ts +101 -0
- package/dist/eval-log/types.d.ts.map +1 -0
- package/dist/eval-log/types.js +16 -0
- package/dist/eval-log/types.js.map +1 -0
- package/dist/function-registry.d.ts +116 -0
- package/dist/function-registry.d.ts.map +1 -0
- package/dist/function-registry.js +546 -0
- package/dist/function-registry.js.map +1 -0
- package/dist/generate.d.ts +9 -3
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +18 -18
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +18 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -18
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +118 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +187 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware/budget.d.ts +84 -0
- package/dist/middleware/budget.d.ts.map +1 -0
- package/dist/middleware/budget.js +110 -0
- package/dist/middleware/budget.js.map +1 -0
- package/dist/middleware/cache.d.ts +103 -0
- package/dist/middleware/cache.d.ts.map +1 -0
- package/dist/middleware/cache.js +228 -0
- package/dist/middleware/cache.js.map +1 -0
- package/dist/middleware/embed-cache.d.ts +99 -0
- package/dist/middleware/embed-cache.d.ts.map +1 -0
- package/dist/middleware/embed-cache.js +128 -0
- package/dist/middleware/embed-cache.js.map +1 -0
- package/dist/middleware/index.d.ts +11 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +11 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/trace.d.ts +103 -0
- package/dist/middleware/trace.d.ts.map +1 -0
- package/dist/middleware/trace.js +176 -0
- package/dist/middleware/trace.js.map +1 -0
- package/dist/primitives.d.ts +120 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +398 -26
- package/dist/primitives.js.map +1 -1
- package/dist/retry.d.ts +66 -1
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +115 -8
- package/dist/retry.js.map +1 -1
- package/dist/schema.js +2 -2
- package/dist/schema.js.map +1 -1
- package/dist/telemetry.d.ts +128 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +285 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/template.d.ts.map +1 -1
- package/dist/template.js +6 -1
- package/dist/template.js.map +1 -1
- package/dist/tool-orchestration.d.ts +66 -4
- package/dist/tool-orchestration.d.ts.map +1 -1
- package/dist/tool-orchestration.js +123 -23
- package/dist/tool-orchestration.js.map +1 -1
- package/dist/type-guards.d.ts +28 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +29 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +135 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +36 -1
- package/dist/types.js.map +1 -1
- package/dist/wrap-for-v3.d.ts +80 -0
- package/dist/wrap-for-v3.d.ts.map +1 -0
- package/dist/wrap-for-v3.js +89 -0
- package/dist/wrap-for-v3.js.map +1 -0
- package/examples/00-quickstart.ts +232 -0
- package/examples/01-rag-chatbot.ts +212 -0
- package/examples/02-multi-agent-research.ts +290 -0
- package/examples/03-email-classification.ts +379 -0
- package/examples/04-content-moderation.ts +400 -0
- package/examples/05-document-extraction.ts +455 -0
- package/examples/06-streaming-chat-nextjs.ts +437 -0
- package/examples/07-cloudflare-worker.ts +483 -0
- package/examples/08-batch-processing.ts +491 -0
- package/examples/09-budget-constrained.ts +527 -0
- package/examples/10-tool-orchestration.ts +565 -0
- package/examples/11-retry-resilience.ts +403 -0
- package/examples/12-caching-strategies.ts +422 -0
- package/examples/README.md +145 -0
- package/package.json +28 -25
- package/src/ai-promise.ts +226 -140
- package/src/ai-schemas.ts +122 -0
- package/src/ai.ts +69 -1176
- package/src/batch/anthropic.ts +96 -161
- package/src/batch/bedrock.ts +203 -454
- package/src/batch/cloudflare.ts +99 -282
- package/src/batch/google.ts +91 -297
- package/src/batch/index.ts +4 -1
- package/src/batch/memory.ts +15 -10
- package/src/batch/openai.ts +65 -193
- package/src/batch/provider.ts +336 -0
- package/src/batch-map.ts +29 -24
- package/src/batch-queue.ts +200 -11
- package/src/budget.ts +31 -18
- package/src/cache.ts +45 -17
- package/src/context.ts +106 -77
- package/src/digital-objects-registry.ts +750 -0
- package/src/errors.ts +37 -0
- package/src/eval/runner.ts +60 -36
- package/src/eval-log/in-memory.ts +90 -0
- package/src/eval-log/index.ts +46 -0
- package/src/eval-log/types.ts +110 -0
- package/src/function-registry.ts +671 -0
- package/src/generate.ts +33 -28
- package/src/index.ts +119 -21
- package/src/logger.ts +232 -0
- package/src/middleware/budget.ts +171 -0
- package/src/middleware/cache.ts +299 -0
- package/src/middleware/embed-cache.ts +195 -0
- package/src/middleware/index.ts +23 -0
- package/src/middleware/trace.ts +248 -0
- package/src/primitives.ts +589 -62
- package/src/retry.ts +144 -18
- package/src/schema.ts +8 -8
- package/src/telemetry.ts +403 -0
- package/src/template.ts +8 -4
- package/src/tool-orchestration.ts +213 -48
- package/src/type-guards.ts +31 -0
- package/src/types.ts +164 -25
- package/src/wrap-for-v3.ts +105 -0
- package/test/ai-promise.test.ts +1080 -0
- package/test/ai-proxy.test.ts +1 -1
- package/test/batch-autosubmit-errors.test.ts +49 -37
- package/test/batch-blog-posts.test.ts +87 -129
- package/test/core-functions.test.ts +183 -579
- package/test/decide.test.ts +154 -322
- package/test/define.test.ts +211 -8
- package/test/digital-objects-registry.test.ts +760 -0
- package/test/embedding-cache-middleware.test.ts +140 -0
- package/test/generate-core.test.ts +140 -229
- package/test/implicit-batch.test.ts +22 -65
- package/test/retry-policy-integration.test.ts +117 -0
- package/test/schema.test.ts +55 -19
- package/test/template.test.ts +1164 -0
- package/test/tool-orchestration.test.ts +270 -0
- package/test/wrap-for-v3.test.ts +612 -0
- package/vitest.config.js +6 -0
- package/vitest.config.ts +20 -0
- package/LICENSE +0 -21
- package/dist/rpc/auth.d.ts +0 -69
- package/dist/rpc/auth.d.ts.map +0 -1
- package/dist/rpc/auth.js +0 -136
- package/dist/rpc/auth.js.map +0 -1
- package/dist/rpc/client.d.ts +0 -62
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/client.js +0 -103
- package/dist/rpc/client.js.map +0 -1
- package/dist/rpc/deferred.d.ts +0 -60
- package/dist/rpc/deferred.d.ts.map +0 -1
- package/dist/rpc/deferred.js +0 -96
- package/dist/rpc/deferred.js.map +0 -1
- package/dist/rpc/index.d.ts +0 -22
- package/dist/rpc/index.d.ts.map +0 -1
- package/dist/rpc/index.js +0 -38
- package/dist/rpc/index.js.map +0 -1
- package/dist/rpc/local.d.ts +0 -42
- package/dist/rpc/local.d.ts.map +0 -1
- package/dist/rpc/local.js +0 -50
- package/dist/rpc/local.js.map +0 -1
- package/dist/rpc/server.d.ts +0 -165
- package/dist/rpc/server.d.ts.map +0 -1
- package/dist/rpc/server.js +0 -405
- package/dist/rpc/server.js.map +0 -1
- package/dist/rpc/session.d.ts +0 -32
- package/dist/rpc/session.d.ts.map +0 -1
- package/dist/rpc/session.js +0 -43
- package/dist/rpc/session.js.map +0 -1
- package/dist/rpc/transport.d.ts +0 -306
- package/dist/rpc/transport.d.ts.map +0 -1
- package/dist/rpc/transport.js +0 -731
- package/dist/rpc/transport.js.map +0 -1
- package/src/batch/anthropic.js +0 -256
- package/src/batch/bedrock.js +0 -584
- package/src/batch/cloudflare.js +0 -287
- package/src/batch/google.js +0 -359
- package/src/batch/index.js +0 -30
- package/src/batch/memory.js +0 -187
- package/src/batch/openai.js +0 -402
- package/src/eval/index.js +0 -7
- package/src/eval/models.js +0 -119
- package/src/eval/runner.js +0 -147
- 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
|
|
8
|
+
import { describe, it, expect } from 'vitest'
|
|
9
|
+
import { generateObject, generateText } from '../src/generate.js'
|
|
10
|
+
import { z } from 'zod'
|
|
8
11
|
|
|
9
|
-
//
|
|
10
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
expect(
|
|
44
|
-
expect(result).
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
61
|
+
const result = await generateText({
|
|
62
|
+
model: 'haiku',
|
|
63
|
+
prompt: 'Write one sentence about AI.',
|
|
64
|
+
})
|
|
81
65
|
|
|
82
|
-
expect(
|
|
83
|
-
expect(
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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(
|
|
114
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
128
|
-
expect(
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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(
|
|
157
|
-
expect(
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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(
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
267
|
-
|
|
170
|
+
const result2 = await generateText({
|
|
171
|
+
model: 'haiku',
|
|
172
|
+
prompt: 'Say exactly "hello" and nothing else.',
|
|
173
|
+
temperature: 0,
|
|
174
|
+
})
|
|
268
175
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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('
|
|
277
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
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)':
|
|
306
|
-
'write(prompt)':
|
|
307
|
-
'code(prompt)': "
|
|
308
|
-
'list(prompt)':
|
|
309
|
-
'lists(prompt)':
|
|
310
|
-
|
|
311
|
-
'
|
|
312
|
-
'
|
|
313
|
-
'
|
|
314
|
-
'is(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(
|
|
228
|
+
expect(Object.keys(mappings)).toHaveLength(9)
|
|
318
229
|
})
|
|
319
230
|
})
|