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.
- package/.turbo/turbo-build.log +1 -4
- package/CHANGELOG.md +68 -1
- package/README.md +397 -157
- package/dist/ai-promise.d.ts +50 -3
- package/dist/ai-promise.d.ts.map +1 -1
- package/dist/ai-promise.js +410 -51
- 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 +54 -837
- 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 +272 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +513 -0
- package/dist/budget.js.map +1 -0
- package/dist/cache.d.ts +295 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +433 -0
- package/dist/cache.js.map +1 -0
- package/dist/context.d.ts +42 -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 +10 -1
- 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 -22
- package/dist/generate.js.map +1 -1
- package/dist/index.d.ts +35 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +89 -42
- 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 +368 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +646 -0
- package/dist/retry.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -10
- 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 +453 -0
- package/dist/tool-orchestration.d.ts.map +1 -0
- package/dist/tool-orchestration.js +763 -0
- package/dist/tool-orchestration.js.map +1 -0
- 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 +10 -6
- package/src/ai-promise.ts +528 -99
- package/src/ai-schemas.ts +122 -0
- package/src/ai.ts +69 -1153
- 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 +740 -0
- package/src/cache.ts +681 -0
- package/src/context.ts +122 -76
- package/src/digital-objects-registry.ts +750 -0
- package/src/errors.ts +37 -0
- package/src/eval/runner.ts +63 -38
- 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 -33
- package/src/index.ts +325 -49
- 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 +902 -0
- package/src/schema.ts +8 -17
- package/src/telemetry.ts +403 -0
- package/src/template.ts +8 -4
- package/src/tool-orchestration.ts +1173 -0
- 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/backward-compat.test.ts +147 -0
- package/test/batch-autosubmit-errors.test.ts +610 -0
- package/test/batch-blog-posts.test.ts +87 -129
- package/test/budget-tracking.test.ts +800 -0
- package/test/cache.test.ts +712 -0
- package/test/context-isolation.test.ts +687 -0
- 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/evals/deterministic.eval.test.ts +376 -0
- package/test/generate-core.test.ts +140 -229
- package/test/implicit-batch.test.ts +22 -65
- package/test/json-parse-error-handling.test.ts +463 -0
- package/test/retry-policy-integration.test.ts +117 -0
- package/test/retry.test.ts +1016 -0
- package/test/schema.test.ts +55 -19
- package/test/streaming.test.ts +316 -0
- package/test/template.test.ts +1164 -0
- package/test/tool-orchestration.test.ts +1040 -0
- package/test/wrap-for-v3.test.ts +612 -0
- package/vitest.config.js +6 -0
- package/vitest.config.ts +20 -0
- 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
package/src/cache.ts
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caching layer for embeddings and generations
|
|
3
|
+
*
|
|
4
|
+
* Provides content-addressable caching for embeddings and parameter-aware
|
|
5
|
+
* caching for text/object generations with TTL support and LRU eviction.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Cache entry with metadata for tracking and eviction
|
|
16
|
+
*/
|
|
17
|
+
export interface CacheEntry<T> {
|
|
18
|
+
/** The cached value */
|
|
19
|
+
value: T
|
|
20
|
+
/** When the entry was created */
|
|
21
|
+
createdAt: number
|
|
22
|
+
/** When the entry was last accessed */
|
|
23
|
+
lastAccessedAt: number
|
|
24
|
+
/** Number of times this entry has been accessed */
|
|
25
|
+
accessCount: number
|
|
26
|
+
/** When the entry expires (if TTL is set) */
|
|
27
|
+
expiresAt?: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for cache operations
|
|
32
|
+
*/
|
|
33
|
+
export interface CacheOptions {
|
|
34
|
+
/** Time-to-live in milliseconds */
|
|
35
|
+
ttl?: number
|
|
36
|
+
/** Whether to bypass cache and force fresh result */
|
|
37
|
+
bypass?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Cache statistics for monitoring
|
|
42
|
+
*/
|
|
43
|
+
export interface CacheStats {
|
|
44
|
+
/** Number of cache hits */
|
|
45
|
+
hits: number
|
|
46
|
+
/** Number of cache misses */
|
|
47
|
+
misses: number
|
|
48
|
+
/** Hit rate (0-1) */
|
|
49
|
+
hitRate: number
|
|
50
|
+
/** Current number of entries */
|
|
51
|
+
size: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Configuration options for MemoryCache
|
|
56
|
+
*/
|
|
57
|
+
export interface MemoryCacheOptions {
|
|
58
|
+
/** Default TTL for entries in milliseconds */
|
|
59
|
+
defaultTTL?: number
|
|
60
|
+
/** Maximum number of entries (enables LRU eviction) */
|
|
61
|
+
maxSize?: number
|
|
62
|
+
/** Whether to refresh TTL on access (sliding window) */
|
|
63
|
+
slidingExpiration?: boolean
|
|
64
|
+
/** Interval for cleanup of expired entries in milliseconds */
|
|
65
|
+
cleanupInterval?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Cache Storage Interface
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Abstract cache storage interface for pluggable backends
|
|
74
|
+
*/
|
|
75
|
+
export interface CacheStorage<T> {
|
|
76
|
+
/** Get a value by key */
|
|
77
|
+
get(key: string): Promise<T | undefined>
|
|
78
|
+
/** Set a value by key */
|
|
79
|
+
set(key: string, value: T, options?: CacheOptions): Promise<void>
|
|
80
|
+
/** Check if a key exists */
|
|
81
|
+
has(key: string): Promise<boolean>
|
|
82
|
+
/** Delete a key */
|
|
83
|
+
delete(key: string): Promise<void>
|
|
84
|
+
/** Clear all entries */
|
|
85
|
+
clear(): Promise<void>
|
|
86
|
+
/** Get the number of entries */
|
|
87
|
+
size(): Promise<number>
|
|
88
|
+
/** Get all keys */
|
|
89
|
+
keys(): Promise<string[]>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Memory Cache Implementation
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* In-memory cache implementation with TTL and LRU eviction support
|
|
98
|
+
*
|
|
99
|
+
* @deprecated Phase C Week 1 — `MemoryCache` has zero production callers in
|
|
100
|
+
* primitives.org.ai (audited 2026-05-06; see `bd show aip-ibid`). New code
|
|
101
|
+
* should use `cacheMiddleware` (for `wrapLanguageModel`) or
|
|
102
|
+
* `embeddingCacheMiddleware` (for `wrapEmbeddingModel`) instead — both
|
|
103
|
+
* compose with AI SDK 6's `wrapLanguageModel` / `wrapEmbeddingModel` and
|
|
104
|
+
* carry per-call telemetry (TraceEvent emission) and budget tracking when
|
|
105
|
+
* paired via `wrapForV3`. The `MemoryCache` class will be removed in the
|
|
106
|
+
* Phase C semver bump alongside `EmbeddingCache` and `GenerationCache`.
|
|
107
|
+
*/
|
|
108
|
+
export class MemoryCache<T> implements CacheStorage<T> {
|
|
109
|
+
private cache: Map<string, CacheEntry<T>> = new Map()
|
|
110
|
+
private accessOrder: string[] = []
|
|
111
|
+
private options: MemoryCacheOptions
|
|
112
|
+
private cleanupTimer?: ReturnType<typeof setInterval>
|
|
113
|
+
|
|
114
|
+
constructor(options: MemoryCacheOptions = {}) {
|
|
115
|
+
this.options = options
|
|
116
|
+
|
|
117
|
+
// Start cleanup timer if interval is specified
|
|
118
|
+
if (options.cleanupInterval && options.cleanupInterval > 0) {
|
|
119
|
+
this.cleanupTimer = setInterval(() => {
|
|
120
|
+
this.cleanup()
|
|
121
|
+
}, options.cleanupInterval)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get a value by key
|
|
127
|
+
*/
|
|
128
|
+
async get(key: string): Promise<T | undefined> {
|
|
129
|
+
const entry = this.cache.get(key)
|
|
130
|
+
|
|
131
|
+
if (!entry) {
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if expired
|
|
136
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
137
|
+
this.cache.delete(key)
|
|
138
|
+
this.removeFromAccessOrder(key)
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Update access tracking
|
|
143
|
+
entry.lastAccessedAt = Date.now()
|
|
144
|
+
entry.accessCount++
|
|
145
|
+
|
|
146
|
+
// Update access order for LRU
|
|
147
|
+
this.updateAccessOrder(key)
|
|
148
|
+
|
|
149
|
+
// Sliding expiration - refresh TTL on access
|
|
150
|
+
if (this.options.slidingExpiration && this.options.defaultTTL) {
|
|
151
|
+
entry.expiresAt = Date.now() + this.options.defaultTTL
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return entry.value
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Set a value by key
|
|
159
|
+
*/
|
|
160
|
+
async set(key: string, value: T, options?: CacheOptions): Promise<void> {
|
|
161
|
+
const now = Date.now()
|
|
162
|
+
const ttl = options?.ttl ?? this.options.defaultTTL
|
|
163
|
+
|
|
164
|
+
const entry: CacheEntry<T> = {
|
|
165
|
+
value,
|
|
166
|
+
createdAt: now,
|
|
167
|
+
lastAccessedAt: now,
|
|
168
|
+
accessCount: 0,
|
|
169
|
+
...(ttl ? { expiresAt: now + ttl } : {}),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check if we need to evict (LRU)
|
|
173
|
+
if (this.options.maxSize && this.cache.size >= this.options.maxSize && !this.cache.has(key)) {
|
|
174
|
+
this.evictLRU()
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.cache.set(key, entry)
|
|
178
|
+
this.updateAccessOrder(key)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if a key exists
|
|
183
|
+
*/
|
|
184
|
+
async has(key: string): Promise<boolean> {
|
|
185
|
+
const entry = this.cache.get(key)
|
|
186
|
+
|
|
187
|
+
if (!entry) {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check if expired
|
|
192
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
193
|
+
this.cache.delete(key)
|
|
194
|
+
this.removeFromAccessOrder(key)
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Delete a key
|
|
203
|
+
*/
|
|
204
|
+
async delete(key: string): Promise<void> {
|
|
205
|
+
this.cache.delete(key)
|
|
206
|
+
this.removeFromAccessOrder(key)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Clear all entries
|
|
211
|
+
*/
|
|
212
|
+
async clear(): Promise<void> {
|
|
213
|
+
this.cache.clear()
|
|
214
|
+
this.accessOrder = []
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get the number of entries (excluding expired)
|
|
219
|
+
*/
|
|
220
|
+
async size(): Promise<number> {
|
|
221
|
+
await this.cleanup()
|
|
222
|
+
return this.cache.size
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Get all keys
|
|
227
|
+
*/
|
|
228
|
+
async keys(): Promise<string[]> {
|
|
229
|
+
await this.cleanup()
|
|
230
|
+
return Array.from(this.cache.keys())
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get full entry with metadata
|
|
235
|
+
*/
|
|
236
|
+
async getEntry(key: string): Promise<CacheEntry<T> | undefined> {
|
|
237
|
+
const entry = this.cache.get(key)
|
|
238
|
+
|
|
239
|
+
if (!entry) {
|
|
240
|
+
return undefined
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check if expired
|
|
244
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
245
|
+
this.cache.delete(key)
|
|
246
|
+
this.removeFromAccessOrder(key)
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return entry
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Dispose cleanup timer
|
|
255
|
+
*/
|
|
256
|
+
dispose(): void {
|
|
257
|
+
if (this.cleanupTimer) {
|
|
258
|
+
clearInterval(this.cleanupTimer)
|
|
259
|
+
delete this.cleanupTimer
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Clean up expired entries
|
|
265
|
+
*/
|
|
266
|
+
private async cleanup(): Promise<void> {
|
|
267
|
+
const now = Date.now()
|
|
268
|
+
const keysToDelete: string[] = []
|
|
269
|
+
|
|
270
|
+
for (const [key, entry] of this.cache) {
|
|
271
|
+
if (entry.expiresAt && now > entry.expiresAt) {
|
|
272
|
+
keysToDelete.push(key)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const key of keysToDelete) {
|
|
277
|
+
this.cache.delete(key)
|
|
278
|
+
this.removeFromAccessOrder(key)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Evict the least recently used entry
|
|
284
|
+
*/
|
|
285
|
+
private evictLRU(): void {
|
|
286
|
+
if (this.accessOrder.length === 0) return
|
|
287
|
+
|
|
288
|
+
const lruKey = this.accessOrder[0]
|
|
289
|
+
if (lruKey) {
|
|
290
|
+
this.cache.delete(lruKey)
|
|
291
|
+
this.accessOrder.shift()
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Update access order for LRU tracking
|
|
297
|
+
*/
|
|
298
|
+
private updateAccessOrder(key: string): void {
|
|
299
|
+
this.removeFromAccessOrder(key)
|
|
300
|
+
this.accessOrder.push(key)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Remove a key from access order
|
|
305
|
+
*/
|
|
306
|
+
private removeFromAccessOrder(key: string): void {
|
|
307
|
+
const index = this.accessOrder.indexOf(key)
|
|
308
|
+
if (index > -1) {
|
|
309
|
+
this.accessOrder.splice(index, 1)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ============================================================================
|
|
315
|
+
// Hash / Key Generation
|
|
316
|
+
// ============================================================================
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Generate a hash for cache keys
|
|
320
|
+
* Uses a fast, non-cryptographic hash suitable for cache keys
|
|
321
|
+
*/
|
|
322
|
+
export function hashKey(input: unknown): string {
|
|
323
|
+
const str = typeof input === 'string' ? input : stableStringify(input)
|
|
324
|
+
|
|
325
|
+
// Simple djb2 hash
|
|
326
|
+
let hash = 5381
|
|
327
|
+
for (let i = 0; i < str.length; i++) {
|
|
328
|
+
hash = (hash << 5) + hash + str.charCodeAt(i)
|
|
329
|
+
hash = hash & hash // Convert to 32bit integer
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return (hash >>> 0).toString(36)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Stringify an object with sorted keys for stable hashing
|
|
337
|
+
*/
|
|
338
|
+
function stableStringify(obj: unknown): string {
|
|
339
|
+
if (obj === null || typeof obj !== 'object') {
|
|
340
|
+
return JSON.stringify(obj)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (Array.isArray(obj)) {
|
|
344
|
+
return '[' + obj.map(stableStringify).join(',') + ']'
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const keys = Object.keys(obj).sort()
|
|
348
|
+
const pairs = keys.map((key) => {
|
|
349
|
+
return JSON.stringify(key) + ':' + stableStringify((obj as Record<string, unknown>)[key])
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
return '{' + pairs.join(',') + '}'
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Cache key type
|
|
357
|
+
*/
|
|
358
|
+
export type CacheKeyType = 'embedding' | 'generation'
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Create a cache key for a specific type and parameters
|
|
362
|
+
*/
|
|
363
|
+
export function createCacheKey(type: CacheKeyType, params: Record<string, unknown>): string {
|
|
364
|
+
const hash = hashKey(params)
|
|
365
|
+
return `${type}:${hash}`
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// Embedding Cache
|
|
370
|
+
// ============================================================================
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Options for embedding cache operations
|
|
374
|
+
*/
|
|
375
|
+
export interface EmbeddingCacheOptions {
|
|
376
|
+
/** The embedding model used */
|
|
377
|
+
model: string
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Result from batch embedding cache lookup
|
|
382
|
+
*/
|
|
383
|
+
export interface BatchEmbeddingResult {
|
|
384
|
+
/** Map of text to cached embedding */
|
|
385
|
+
hits: Record<string, number[]>
|
|
386
|
+
/** Texts that were not in cache */
|
|
387
|
+
misses: string[]
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Specialized cache for embedding vectors
|
|
392
|
+
*
|
|
393
|
+
* @deprecated Phase C Week 1 — `EmbeddingCache` has zero production callers
|
|
394
|
+
* in primitives.org.ai (audited 2026-05-06; see `bd show aip-ibid`). New
|
|
395
|
+
* code should use `embeddingCacheMiddleware` (for `wrapEmbeddingModel`) —
|
|
396
|
+
* it composes with AI SDK 6 directly and carries trace + budget telemetry
|
|
397
|
+
* when paired via `wrapForV3`. Note: `embeddingCacheMiddleware` keys on the
|
|
398
|
+
* whole batch, not per-text — callers wanting per-text caching should pass
|
|
399
|
+
* stable per-text batches. Will be removed in the Phase C semver bump.
|
|
400
|
+
*/
|
|
401
|
+
export class EmbeddingCache {
|
|
402
|
+
private storage: MemoryCache<number[]>
|
|
403
|
+
private stats = { hits: 0, misses: 0 }
|
|
404
|
+
|
|
405
|
+
constructor(options?: MemoryCacheOptions) {
|
|
406
|
+
this.storage = new MemoryCache<number[]>(options)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Get a cached embedding
|
|
411
|
+
*/
|
|
412
|
+
async get(content: string, options: EmbeddingCacheOptions): Promise<number[] | undefined> {
|
|
413
|
+
const key = createCacheKey('embedding', { content, model: options.model })
|
|
414
|
+
const result = await this.storage.get(key)
|
|
415
|
+
|
|
416
|
+
if (result) {
|
|
417
|
+
this.stats.hits++
|
|
418
|
+
} else {
|
|
419
|
+
this.stats.misses++
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return result
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Set a cached embedding
|
|
427
|
+
*/
|
|
428
|
+
async set(content: string, embedding: number[], options: EmbeddingCacheOptions): Promise<void> {
|
|
429
|
+
const key = createCacheKey('embedding', { content, model: options.model })
|
|
430
|
+
await this.storage.set(key, embedding)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Set multiple embeddings at once
|
|
435
|
+
*/
|
|
436
|
+
async setMany(
|
|
437
|
+
texts: string[],
|
|
438
|
+
embeddings: number[][],
|
|
439
|
+
options: EmbeddingCacheOptions
|
|
440
|
+
): Promise<void> {
|
|
441
|
+
for (let i = 0; i < texts.length; i++) {
|
|
442
|
+
await this.set(texts[i]!, embeddings[i]!, options)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get multiple embeddings, returning hits and misses
|
|
448
|
+
*/
|
|
449
|
+
async getMany(texts: string[], options: EmbeddingCacheOptions): Promise<BatchEmbeddingResult> {
|
|
450
|
+
const hits: Record<string, number[]> = {}
|
|
451
|
+
const misses: string[] = []
|
|
452
|
+
|
|
453
|
+
for (const text of texts) {
|
|
454
|
+
const embedding = await this.get(text, options)
|
|
455
|
+
if (embedding) {
|
|
456
|
+
hits[text] = embedding
|
|
457
|
+
// Note: hits counter already incremented in get()
|
|
458
|
+
this.stats.hits-- // Avoid double counting
|
|
459
|
+
} else {
|
|
460
|
+
misses.push(text)
|
|
461
|
+
this.stats.misses-- // Avoid double counting
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Add back correct counts
|
|
466
|
+
this.stats.hits += Object.keys(hits).length
|
|
467
|
+
this.stats.misses += misses.length
|
|
468
|
+
|
|
469
|
+
return { hits, misses }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Get cache statistics
|
|
474
|
+
*/
|
|
475
|
+
getStats(): CacheStats {
|
|
476
|
+
const total = this.stats.hits + this.stats.misses
|
|
477
|
+
return {
|
|
478
|
+
hits: this.stats.hits,
|
|
479
|
+
misses: this.stats.misses,
|
|
480
|
+
hitRate: total > 0 ? this.stats.hits / total : 0,
|
|
481
|
+
size: this.storage['cache'].size,
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Clear the cache
|
|
487
|
+
*/
|
|
488
|
+
async clear(): Promise<void> {
|
|
489
|
+
await this.storage.clear()
|
|
490
|
+
this.stats = { hits: 0, misses: 0 }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ============================================================================
|
|
495
|
+
// Generation Cache
|
|
496
|
+
// ============================================================================
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Parameters for generation cache key
|
|
500
|
+
*/
|
|
501
|
+
export interface GenerationParams {
|
|
502
|
+
/** The prompt text */
|
|
503
|
+
prompt: string
|
|
504
|
+
/** The model to use */
|
|
505
|
+
model: string
|
|
506
|
+
/** System prompt */
|
|
507
|
+
system?: string
|
|
508
|
+
/** Temperature setting */
|
|
509
|
+
temperature?: number
|
|
510
|
+
/** Schema version for structured outputs */
|
|
511
|
+
schemaVersion?: string
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Options for generation cache retrieval
|
|
516
|
+
*/
|
|
517
|
+
export interface GenerationCacheGetOptions {
|
|
518
|
+
/** Bypass cache and return undefined */
|
|
519
|
+
bypass?: boolean
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Specialized cache for generation results
|
|
524
|
+
*
|
|
525
|
+
* @deprecated Phase C Week 1 — `GenerationCache` has zero production callers
|
|
526
|
+
* in primitives.org.ai (audited 2026-05-06; see `bd show aip-ibid`). New
|
|
527
|
+
* code should use `cacheMiddleware` (for `wrapLanguageModel`) — it composes
|
|
528
|
+
* with AI SDK 6 directly and carries trace + budget telemetry when paired
|
|
529
|
+
* via `wrapForV3`. Will be removed in the Phase C semver bump.
|
|
530
|
+
*/
|
|
531
|
+
export class GenerationCache {
|
|
532
|
+
private storage: MemoryCache<unknown>
|
|
533
|
+
private stats = { hits: 0, misses: 0 }
|
|
534
|
+
|
|
535
|
+
constructor(options?: MemoryCacheOptions) {
|
|
536
|
+
this.storage = new MemoryCache<unknown>(options)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Get a cached generation result
|
|
541
|
+
*/
|
|
542
|
+
async get<T = unknown>(
|
|
543
|
+
params: GenerationParams,
|
|
544
|
+
options?: GenerationCacheGetOptions
|
|
545
|
+
): Promise<T | undefined> {
|
|
546
|
+
if (options?.bypass) {
|
|
547
|
+
return undefined
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const key = this.createKey(params)
|
|
551
|
+
const result = await this.storage.get(key)
|
|
552
|
+
|
|
553
|
+
if (result) {
|
|
554
|
+
this.stats.hits++
|
|
555
|
+
} else {
|
|
556
|
+
this.stats.misses++
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return result as T | undefined
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Set a cached generation result
|
|
564
|
+
*/
|
|
565
|
+
async set<T = unknown>(params: GenerationParams, result: T): Promise<void> {
|
|
566
|
+
const key = this.createKey(params)
|
|
567
|
+
await this.storage.set(key, result)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Get cache statistics
|
|
572
|
+
*/
|
|
573
|
+
getStats(): CacheStats {
|
|
574
|
+
const total = this.stats.hits + this.stats.misses
|
|
575
|
+
return {
|
|
576
|
+
hits: this.stats.hits,
|
|
577
|
+
misses: this.stats.misses,
|
|
578
|
+
hitRate: total > 0 ? this.stats.hits / total : 0,
|
|
579
|
+
size: this.storage['cache'].size,
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Clear the cache
|
|
585
|
+
*/
|
|
586
|
+
async clear(): Promise<void> {
|
|
587
|
+
await this.storage.clear()
|
|
588
|
+
this.stats = { hits: 0, misses: 0 }
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Create a cache key from generation parameters
|
|
593
|
+
*/
|
|
594
|
+
private createKey(params: GenerationParams): string {
|
|
595
|
+
const keyParams: Record<string, unknown> = {
|
|
596
|
+
prompt: params.prompt,
|
|
597
|
+
model: params.model,
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (params.system !== undefined) {
|
|
601
|
+
keyParams['system'] = params.system
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (params.temperature !== undefined) {
|
|
605
|
+
keyParams['temperature'] = params.temperature
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (params.schemaVersion !== undefined) {
|
|
609
|
+
keyParams['schemaVersion'] = params.schemaVersion
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return createCacheKey('generation', keyParams)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ============================================================================
|
|
617
|
+
// withCache Wrapper
|
|
618
|
+
// ============================================================================
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Options for withCache wrapper
|
|
622
|
+
*/
|
|
623
|
+
export interface WithCacheOptions<TArgs extends unknown[]> {
|
|
624
|
+
/** Function to generate cache key from arguments */
|
|
625
|
+
keyFn: (...args: TArgs) => string
|
|
626
|
+
/** TTL for cached entries */
|
|
627
|
+
ttl?: number
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Cached function type with bypass support
|
|
632
|
+
*/
|
|
633
|
+
export interface CachedFunction<TArgs extends unknown[], TResult> {
|
|
634
|
+
(...args: TArgs): Promise<TResult>
|
|
635
|
+
/** Call with cache bypass (force fresh result) */
|
|
636
|
+
bypass: (...args: TArgs) => Promise<TResult>
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Wrap an async function with caching
|
|
641
|
+
*/
|
|
642
|
+
export function withCache<TArgs extends unknown[], TResult>(
|
|
643
|
+
cache: CacheStorage<TResult>,
|
|
644
|
+
fn: (...args: TArgs) => Promise<TResult>,
|
|
645
|
+
options: WithCacheOptions<TArgs>
|
|
646
|
+
): CachedFunction<TArgs, TResult> {
|
|
647
|
+
const { keyFn, ttl } = options
|
|
648
|
+
|
|
649
|
+
const cachedFn = async (...args: TArgs): Promise<TResult> => {
|
|
650
|
+
const key = keyFn(...args)
|
|
651
|
+
|
|
652
|
+
// Check cache
|
|
653
|
+
const cached = await cache.get(key)
|
|
654
|
+
if (cached !== undefined) {
|
|
655
|
+
return cached
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Execute function
|
|
659
|
+
const result = await fn(...args)
|
|
660
|
+
|
|
661
|
+
// Cache result (don't cache errors - they throw before reaching here)
|
|
662
|
+
await cache.set(key, result, ttl !== undefined ? { ttl } : undefined)
|
|
663
|
+
|
|
664
|
+
return result
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Add bypass method
|
|
668
|
+
cachedFn.bypass = async (...args: TArgs): Promise<TResult> => {
|
|
669
|
+
const key = keyFn(...args)
|
|
670
|
+
|
|
671
|
+
// Execute function without checking cache
|
|
672
|
+
const result = await fn(...args)
|
|
673
|
+
|
|
674
|
+
// Update cache with new result
|
|
675
|
+
await cache.set(key, result, ttl !== undefined ? { ttl } : undefined)
|
|
676
|
+
|
|
677
|
+
return result
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return cachedFn as CachedFunction<TArgs, TResult>
|
|
681
|
+
}
|