ai-functions 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/.turbo/turbo-build.log +1 -4
  2. package/CHANGELOG.md +68 -1
  3. package/README.md +397 -157
  4. package/dist/ai-promise.d.ts +50 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +410 -51
  7. package/dist/ai-promise.js.map +1 -1
  8. package/dist/ai-schemas.d.ts +56 -0
  9. package/dist/ai-schemas.d.ts.map +1 -0
  10. package/dist/ai-schemas.js +53 -0
  11. package/dist/ai-schemas.js.map +1 -0
  12. package/dist/ai.d.ts +16 -242
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +54 -837
  15. package/dist/ai.js.map +1 -1
  16. package/dist/batch/anthropic.d.ts +6 -4
  17. package/dist/batch/anthropic.d.ts.map +1 -1
  18. package/dist/batch/anthropic.js +83 -145
  19. package/dist/batch/anthropic.js.map +1 -1
  20. package/dist/batch/bedrock.d.ts +8 -30
  21. package/dist/batch/bedrock.d.ts.map +1 -1
  22. package/dist/batch/bedrock.js +155 -338
  23. package/dist/batch/bedrock.js.map +1 -1
  24. package/dist/batch/cloudflare.d.ts +8 -20
  25. package/dist/batch/cloudflare.d.ts.map +1 -1
  26. package/dist/batch/cloudflare.js +68 -189
  27. package/dist/batch/cloudflare.js.map +1 -1
  28. package/dist/batch/google.d.ts +6 -20
  29. package/dist/batch/google.d.ts.map +1 -1
  30. package/dist/batch/google.js +70 -238
  31. package/dist/batch/google.js.map +1 -1
  32. package/dist/batch/index.d.ts +4 -1
  33. package/dist/batch/index.d.ts.map +1 -1
  34. package/dist/batch/index.js +4 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/batch/memory.d.ts +1 -1
  37. package/dist/batch/memory.d.ts.map +1 -1
  38. package/dist/batch/memory.js +14 -10
  39. package/dist/batch/memory.js.map +1 -1
  40. package/dist/batch/openai.d.ts +11 -14
  41. package/dist/batch/openai.d.ts.map +1 -1
  42. package/dist/batch/openai.js +52 -156
  43. package/dist/batch/openai.js.map +1 -1
  44. package/dist/batch/provider.d.ts +111 -0
  45. package/dist/batch/provider.d.ts.map +1 -0
  46. package/dist/batch/provider.js +233 -0
  47. package/dist/batch/provider.js.map +1 -0
  48. package/dist/batch-map.d.ts.map +1 -1
  49. package/dist/batch-map.js +23 -17
  50. package/dist/batch-map.js.map +1 -1
  51. package/dist/batch-queue.d.ts +65 -0
  52. package/dist/batch-queue.d.ts.map +1 -1
  53. package/dist/batch-queue.js +169 -14
  54. package/dist/batch-queue.js.map +1 -1
  55. package/dist/budget.d.ts +272 -0
  56. package/dist/budget.d.ts.map +1 -0
  57. package/dist/budget.js +513 -0
  58. package/dist/budget.js.map +1 -0
  59. package/dist/cache.d.ts +295 -0
  60. package/dist/cache.d.ts.map +1 -0
  61. package/dist/cache.js +433 -0
  62. package/dist/cache.js.map +1 -0
  63. package/dist/context.d.ts +42 -8
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +64 -62
  66. package/dist/context.js.map +1 -1
  67. package/dist/digital-objects-registry.d.ts +229 -0
  68. package/dist/digital-objects-registry.d.ts.map +1 -0
  69. package/dist/digital-objects-registry.js +617 -0
  70. package/dist/digital-objects-registry.js.map +1 -0
  71. package/dist/embeddings.d.ts +2 -2
  72. package/dist/embeddings.d.ts.map +1 -1
  73. package/dist/errors.d.ts +22 -0
  74. package/dist/errors.d.ts.map +1 -0
  75. package/dist/errors.js +35 -0
  76. package/dist/errors.js.map +1 -0
  77. package/dist/eval/runner.d.ts +10 -1
  78. package/dist/eval/runner.d.ts.map +1 -1
  79. package/dist/eval/runner.js +41 -35
  80. package/dist/eval/runner.js.map +1 -1
  81. package/dist/eval-log/in-memory.d.ts +34 -0
  82. package/dist/eval-log/in-memory.d.ts.map +1 -0
  83. package/dist/eval-log/in-memory.js +84 -0
  84. package/dist/eval-log/in-memory.js.map +1 -0
  85. package/dist/eval-log/index.d.ts +29 -0
  86. package/dist/eval-log/index.d.ts.map +1 -0
  87. package/dist/eval-log/index.js +39 -0
  88. package/dist/eval-log/index.js.map +1 -0
  89. package/dist/eval-log/types.d.ts +101 -0
  90. package/dist/eval-log/types.d.ts.map +1 -0
  91. package/dist/eval-log/types.js +16 -0
  92. package/dist/eval-log/types.js.map +1 -0
  93. package/dist/function-registry.d.ts +116 -0
  94. package/dist/function-registry.d.ts.map +1 -0
  95. package/dist/function-registry.js +546 -0
  96. package/dist/function-registry.js.map +1 -0
  97. package/dist/generate.d.ts +9 -3
  98. package/dist/generate.d.ts.map +1 -1
  99. package/dist/generate.js +18 -22
  100. package/dist/generate.js.map +1 -1
  101. package/dist/index.d.ts +35 -20
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +89 -42
  104. package/dist/index.js.map +1 -1
  105. package/dist/logger.d.ts +118 -0
  106. package/dist/logger.d.ts.map +1 -0
  107. package/dist/logger.js +187 -0
  108. package/dist/logger.js.map +1 -0
  109. package/dist/middleware/budget.d.ts +84 -0
  110. package/dist/middleware/budget.d.ts.map +1 -0
  111. package/dist/middleware/budget.js +110 -0
  112. package/dist/middleware/budget.js.map +1 -0
  113. package/dist/middleware/cache.d.ts +103 -0
  114. package/dist/middleware/cache.d.ts.map +1 -0
  115. package/dist/middleware/cache.js +228 -0
  116. package/dist/middleware/cache.js.map +1 -0
  117. package/dist/middleware/embed-cache.d.ts +99 -0
  118. package/dist/middleware/embed-cache.d.ts.map +1 -0
  119. package/dist/middleware/embed-cache.js +128 -0
  120. package/dist/middleware/embed-cache.js.map +1 -0
  121. package/dist/middleware/index.d.ts +11 -0
  122. package/dist/middleware/index.d.ts.map +1 -0
  123. package/dist/middleware/index.js +11 -0
  124. package/dist/middleware/index.js.map +1 -0
  125. package/dist/middleware/trace.d.ts +103 -0
  126. package/dist/middleware/trace.d.ts.map +1 -0
  127. package/dist/middleware/trace.js +176 -0
  128. package/dist/middleware/trace.js.map +1 -0
  129. package/dist/primitives.d.ts +120 -1
  130. package/dist/primitives.d.ts.map +1 -1
  131. package/dist/primitives.js +398 -26
  132. package/dist/primitives.js.map +1 -1
  133. package/dist/retry.d.ts +368 -0
  134. package/dist/retry.d.ts.map +1 -0
  135. package/dist/retry.js +646 -0
  136. package/dist/retry.js.map +1 -0
  137. package/dist/schema.d.ts.map +1 -1
  138. package/dist/schema.js +2 -10
  139. package/dist/schema.js.map +1 -1
  140. package/dist/telemetry.d.ts +128 -0
  141. package/dist/telemetry.d.ts.map +1 -0
  142. package/dist/telemetry.js +285 -0
  143. package/dist/telemetry.js.map +1 -0
  144. package/dist/template.d.ts.map +1 -1
  145. package/dist/template.js +6 -1
  146. package/dist/template.js.map +1 -1
  147. package/dist/tool-orchestration.d.ts +453 -0
  148. package/dist/tool-orchestration.d.ts.map +1 -0
  149. package/dist/tool-orchestration.js +763 -0
  150. package/dist/tool-orchestration.js.map +1 -0
  151. package/dist/type-guards.d.ts +28 -0
  152. package/dist/type-guards.d.ts.map +1 -0
  153. package/dist/type-guards.js +29 -0
  154. package/dist/type-guards.js.map +1 -0
  155. package/dist/types.d.ts +135 -17
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/types.js +36 -1
  158. package/dist/types.js.map +1 -1
  159. package/dist/wrap-for-v3.d.ts +80 -0
  160. package/dist/wrap-for-v3.d.ts.map +1 -0
  161. package/dist/wrap-for-v3.js +89 -0
  162. package/dist/wrap-for-v3.js.map +1 -0
  163. package/examples/00-quickstart.ts +232 -0
  164. package/examples/01-rag-chatbot.ts +212 -0
  165. package/examples/02-multi-agent-research.ts +290 -0
  166. package/examples/03-email-classification.ts +379 -0
  167. package/examples/04-content-moderation.ts +400 -0
  168. package/examples/05-document-extraction.ts +455 -0
  169. package/examples/06-streaming-chat-nextjs.ts +437 -0
  170. package/examples/07-cloudflare-worker.ts +483 -0
  171. package/examples/08-batch-processing.ts +491 -0
  172. package/examples/09-budget-constrained.ts +527 -0
  173. package/examples/10-tool-orchestration.ts +565 -0
  174. package/examples/11-retry-resilience.ts +403 -0
  175. package/examples/12-caching-strategies.ts +422 -0
  176. package/examples/README.md +145 -0
  177. package/package.json +10 -6
  178. package/src/ai-promise.ts +528 -99
  179. package/src/ai-schemas.ts +122 -0
  180. package/src/ai.ts +69 -1153
  181. package/src/batch/anthropic.ts +96 -161
  182. package/src/batch/bedrock.ts +203 -454
  183. package/src/batch/cloudflare.ts +99 -282
  184. package/src/batch/google.ts +91 -297
  185. package/src/batch/index.ts +4 -1
  186. package/src/batch/memory.ts +15 -10
  187. package/src/batch/openai.ts +65 -193
  188. package/src/batch/provider.ts +336 -0
  189. package/src/batch-map.ts +29 -24
  190. package/src/batch-queue.ts +200 -11
  191. package/src/budget.ts +740 -0
  192. package/src/cache.ts +681 -0
  193. package/src/context.ts +122 -76
  194. package/src/digital-objects-registry.ts +750 -0
  195. package/src/errors.ts +37 -0
  196. package/src/eval/runner.ts +63 -38
  197. package/src/eval-log/in-memory.ts +90 -0
  198. package/src/eval-log/index.ts +46 -0
  199. package/src/eval-log/types.ts +110 -0
  200. package/src/function-registry.ts +671 -0
  201. package/src/generate.ts +33 -33
  202. package/src/index.ts +325 -49
  203. package/src/logger.ts +232 -0
  204. package/src/middleware/budget.ts +171 -0
  205. package/src/middleware/cache.ts +299 -0
  206. package/src/middleware/embed-cache.ts +195 -0
  207. package/src/middleware/index.ts +23 -0
  208. package/src/middleware/trace.ts +248 -0
  209. package/src/primitives.ts +589 -62
  210. package/src/retry.ts +902 -0
  211. package/src/schema.ts +8 -17
  212. package/src/telemetry.ts +403 -0
  213. package/src/template.ts +8 -4
  214. package/src/tool-orchestration.ts +1173 -0
  215. package/src/type-guards.ts +31 -0
  216. package/src/types.ts +164 -25
  217. package/src/wrap-for-v3.ts +105 -0
  218. package/test/ai-promise.test.ts +1080 -0
  219. package/test/ai-proxy.test.ts +1 -1
  220. package/test/backward-compat.test.ts +147 -0
  221. package/test/batch-autosubmit-errors.test.ts +610 -0
  222. package/test/batch-blog-posts.test.ts +87 -129
  223. package/test/budget-tracking.test.ts +800 -0
  224. package/test/cache.test.ts +712 -0
  225. package/test/context-isolation.test.ts +687 -0
  226. package/test/core-functions.test.ts +183 -579
  227. package/test/decide.test.ts +154 -322
  228. package/test/define.test.ts +211 -8
  229. package/test/digital-objects-registry.test.ts +760 -0
  230. package/test/embedding-cache-middleware.test.ts +140 -0
  231. package/test/evals/deterministic.eval.test.ts +376 -0
  232. package/test/generate-core.test.ts +140 -229
  233. package/test/implicit-batch.test.ts +22 -65
  234. package/test/json-parse-error-handling.test.ts +463 -0
  235. package/test/retry-policy-integration.test.ts +117 -0
  236. package/test/retry.test.ts +1016 -0
  237. package/test/schema.test.ts +55 -19
  238. package/test/streaming.test.ts +316 -0
  239. package/test/template.test.ts +1164 -0
  240. package/test/tool-orchestration.test.ts +1040 -0
  241. package/test/wrap-for-v3.test.ts +612 -0
  242. package/vitest.config.js +6 -0
  243. package/vitest.config.ts +20 -0
  244. package/dist/rpc/auth.d.ts +0 -69
  245. package/dist/rpc/auth.d.ts.map +0 -1
  246. package/dist/rpc/auth.js +0 -136
  247. package/dist/rpc/auth.js.map +0 -1
  248. package/dist/rpc/client.d.ts +0 -62
  249. package/dist/rpc/client.d.ts.map +0 -1
  250. package/dist/rpc/client.js +0 -103
  251. package/dist/rpc/client.js.map +0 -1
  252. package/dist/rpc/deferred.d.ts +0 -60
  253. package/dist/rpc/deferred.d.ts.map +0 -1
  254. package/dist/rpc/deferred.js +0 -96
  255. package/dist/rpc/deferred.js.map +0 -1
  256. package/dist/rpc/index.d.ts +0 -22
  257. package/dist/rpc/index.d.ts.map +0 -1
  258. package/dist/rpc/index.js +0 -38
  259. package/dist/rpc/index.js.map +0 -1
  260. package/dist/rpc/local.d.ts +0 -42
  261. package/dist/rpc/local.d.ts.map +0 -1
  262. package/dist/rpc/local.js +0 -50
  263. package/dist/rpc/local.js.map +0 -1
  264. package/dist/rpc/server.d.ts +0 -165
  265. package/dist/rpc/server.d.ts.map +0 -1
  266. package/dist/rpc/server.js +0 -405
  267. package/dist/rpc/server.js.map +0 -1
  268. package/dist/rpc/session.d.ts +0 -32
  269. package/dist/rpc/session.d.ts.map +0 -1
  270. package/dist/rpc/session.js +0 -43
  271. package/dist/rpc/session.js.map +0 -1
  272. package/dist/rpc/transport.d.ts +0 -306
  273. package/dist/rpc/transport.d.ts.map +0 -1
  274. package/dist/rpc/transport.js +0 -731
  275. package/dist/rpc/transport.js.map +0 -1
  276. package/src/batch/anthropic.js +0 -256
  277. package/src/batch/bedrock.js +0 -584
  278. package/src/batch/cloudflare.js +0 -287
  279. package/src/batch/google.js +0 -359
  280. package/src/batch/index.js +0 -30
  281. package/src/batch/memory.js +0 -187
  282. package/src/batch/openai.js +0 -402
  283. package/src/eval/index.js +0 -7
  284. package/src/eval/models.js +0 -119
  285. package/src/eval/runner.js +0 -147
  286. package/test/schema.test.js +0 -96
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Tests for caching layer for embeddings and generations
3
+ *
4
+ * TDD: RED Phase - These tests should fail initially
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
8
+ import {
9
+ // Cache storage interface and implementations
10
+ CacheStorage,
11
+ MemoryCache,
12
+
13
+ // Specialized caches
14
+ EmbeddingCache,
15
+ GenerationCache,
16
+
17
+ // Cache wrapper
18
+ withCache,
19
+
20
+ // Utilities
21
+ hashKey,
22
+ createCacheKey,
23
+
24
+ // Types
25
+ CacheEntry,
26
+ CacheOptions,
27
+ CacheStats,
28
+ } from '../src/index.js'
29
+
30
+ describe('CacheStorage interface', () => {
31
+ describe('MemoryCache', () => {
32
+ let cache: MemoryCache<string>
33
+
34
+ beforeEach(() => {
35
+ cache = new MemoryCache<string>()
36
+ })
37
+
38
+ it('stores and retrieves values', async () => {
39
+ await cache.set('key1', 'value1')
40
+ const result = await cache.get('key1')
41
+ expect(result).toBe('value1')
42
+ })
43
+
44
+ it('returns undefined for missing keys', async () => {
45
+ const result = await cache.get('nonexistent')
46
+ expect(result).toBeUndefined()
47
+ })
48
+
49
+ it('checks if key exists', async () => {
50
+ await cache.set('key1', 'value1')
51
+ expect(await cache.has('key1')).toBe(true)
52
+ expect(await cache.has('nonexistent')).toBe(false)
53
+ })
54
+
55
+ it('deletes keys', async () => {
56
+ await cache.set('key1', 'value1')
57
+ await cache.delete('key1')
58
+ expect(await cache.has('key1')).toBe(false)
59
+ })
60
+
61
+ it('clears all entries', async () => {
62
+ await cache.set('key1', 'value1')
63
+ await cache.set('key2', 'value2')
64
+ await cache.clear()
65
+ expect(await cache.has('key1')).toBe(false)
66
+ expect(await cache.has('key2')).toBe(false)
67
+ })
68
+
69
+ it('returns cache size', async () => {
70
+ expect(await cache.size()).toBe(0)
71
+ await cache.set('key1', 'value1')
72
+ expect(await cache.size()).toBe(1)
73
+ await cache.set('key2', 'value2')
74
+ expect(await cache.size()).toBe(2)
75
+ })
76
+
77
+ it('lists all keys', async () => {
78
+ await cache.set('key1', 'value1')
79
+ await cache.set('key2', 'value2')
80
+ const keys = await cache.keys()
81
+ expect(keys).toContain('key1')
82
+ expect(keys).toContain('key2')
83
+ })
84
+ })
85
+
86
+ describe('MemoryCache with TTL', () => {
87
+ beforeEach(() => {
88
+ vi.useFakeTimers()
89
+ })
90
+
91
+ afterEach(() => {
92
+ vi.useRealTimers()
93
+ })
94
+
95
+ it('expires entries after TTL', async () => {
96
+ const cache = new MemoryCache<string>({ defaultTTL: 1000 }) // 1 second TTL
97
+
98
+ await cache.set('key1', 'value1')
99
+ expect(await cache.get('key1')).toBe('value1')
100
+
101
+ // Advance time past TTL
102
+ vi.advanceTimersByTime(1500)
103
+
104
+ expect(await cache.get('key1')).toBeUndefined()
105
+ })
106
+
107
+ it('allows per-key TTL override', async () => {
108
+ const cache = new MemoryCache<string>({ defaultTTL: 1000 })
109
+
110
+ await cache.set('key1', 'value1', { ttl: 500 })
111
+ await cache.set('key2', 'value2', { ttl: 2000 })
112
+
113
+ vi.advanceTimersByTime(750)
114
+
115
+ expect(await cache.get('key1')).toBeUndefined() // Expired
116
+ expect(await cache.get('key2')).toBe('value2') // Still valid
117
+ })
118
+
119
+ it('supports sliding window TTL refresh on access', async () => {
120
+ const cache = new MemoryCache<string>({ defaultTTL: 1000, slidingExpiration: true })
121
+
122
+ await cache.set('key1', 'value1')
123
+
124
+ // Access before expiry to refresh TTL
125
+ vi.advanceTimersByTime(800)
126
+ expect(await cache.get('key1')).toBe('value1') // Refreshes TTL
127
+
128
+ // Another 800ms should still be valid because we refreshed
129
+ vi.advanceTimersByTime(800)
130
+ expect(await cache.get('key1')).toBe('value1')
131
+
132
+ // Without access for full TTL, it should expire
133
+ vi.advanceTimersByTime(1100)
134
+ expect(await cache.get('key1')).toBeUndefined()
135
+ })
136
+
137
+ it('cleans up expired entries automatically', async () => {
138
+ const cache = new MemoryCache<string>({
139
+ defaultTTL: 1000,
140
+ cleanupInterval: 500
141
+ })
142
+
143
+ await cache.set('key1', 'value1')
144
+ expect(await cache.size()).toBe(1)
145
+
146
+ vi.advanceTimersByTime(1500)
147
+
148
+ // After cleanup, size should be 0
149
+ expect(await cache.size()).toBe(0)
150
+
151
+ cache.dispose() // Clean up timer
152
+ })
153
+ })
154
+
155
+ describe('MemoryCache with LRU eviction', () => {
156
+ it('evicts least recently used entries when max size reached', async () => {
157
+ const cache = new MemoryCache<string>({ maxSize: 3 })
158
+
159
+ await cache.set('key1', 'value1')
160
+ await cache.set('key2', 'value2')
161
+ await cache.set('key3', 'value3')
162
+
163
+ // Access key1 to make it recently used
164
+ await cache.get('key1')
165
+
166
+ // Add key4, should evict key2 (least recently used)
167
+ await cache.set('key4', 'value4')
168
+
169
+ expect(await cache.size()).toBe(3)
170
+ expect(await cache.has('key1')).toBe(true)
171
+ expect(await cache.has('key2')).toBe(false) // Evicted
172
+ expect(await cache.has('key3')).toBe(true)
173
+ expect(await cache.has('key4')).toBe(true)
174
+ })
175
+ })
176
+ })
177
+
178
+ describe('Cache key generation', () => {
179
+ describe('hashKey', () => {
180
+ it('generates consistent hashes for same input', () => {
181
+ const hash1 = hashKey('hello world')
182
+ const hash2 = hashKey('hello world')
183
+ expect(hash1).toBe(hash2)
184
+ })
185
+
186
+ it('generates different hashes for different inputs', () => {
187
+ const hash1 = hashKey('hello')
188
+ const hash2 = hashKey('world')
189
+ expect(hash1).not.toBe(hash2)
190
+ })
191
+
192
+ it('handles objects by serializing them', () => {
193
+ const hash1 = hashKey({ a: 1, b: 2 })
194
+ const hash2 = hashKey({ a: 1, b: 2 })
195
+ const hash3 = hashKey({ b: 2, a: 1 }) // Different key order
196
+
197
+ expect(hash1).toBe(hash2)
198
+ // Sorted keys should produce same hash
199
+ expect(hash1).toBe(hash3)
200
+ })
201
+
202
+ it('handles arrays', () => {
203
+ const hash1 = hashKey([1, 2, 3])
204
+ const hash2 = hashKey([1, 2, 3])
205
+ const hash3 = hashKey([3, 2, 1])
206
+
207
+ expect(hash1).toBe(hash2)
208
+ expect(hash1).not.toBe(hash3)
209
+ })
210
+ })
211
+
212
+ describe('createCacheKey', () => {
213
+ it('creates content-addressable keys for embeddings', () => {
214
+ const key1 = createCacheKey('embedding', { content: 'hello world', model: 'text-embedding-3-small' })
215
+ const key2 = createCacheKey('embedding', { content: 'hello world', model: 'text-embedding-3-small' })
216
+ const key3 = createCacheKey('embedding', { content: 'different', model: 'text-embedding-3-small' })
217
+
218
+ expect(key1).toBe(key2)
219
+ expect(key1).not.toBe(key3)
220
+ expect(key1).toContain('embedding:')
221
+ })
222
+
223
+ it('creates parameter-aware keys for generations', () => {
224
+ const key1 = createCacheKey('generation', {
225
+ prompt: 'Write a poem',
226
+ model: 'sonnet',
227
+ temperature: 0.7
228
+ })
229
+ const key2 = createCacheKey('generation', {
230
+ prompt: 'Write a poem',
231
+ model: 'sonnet',
232
+ temperature: 0.7
233
+ })
234
+ const key3 = createCacheKey('generation', {
235
+ prompt: 'Write a poem',
236
+ model: 'sonnet',
237
+ temperature: 0.9 // Different temperature
238
+ })
239
+
240
+ expect(key1).toBe(key2)
241
+ expect(key1).not.toBe(key3)
242
+ expect(key1).toContain('generation:')
243
+ })
244
+
245
+ it('includes schema version in structured output keys', () => {
246
+ const key1 = createCacheKey('generation', {
247
+ prompt: 'Extract data',
248
+ model: 'sonnet',
249
+ schemaVersion: 'v1'
250
+ })
251
+ const key2 = createCacheKey('generation', {
252
+ prompt: 'Extract data',
253
+ model: 'sonnet',
254
+ schemaVersion: 'v2'
255
+ })
256
+
257
+ expect(key1).not.toBe(key2)
258
+ })
259
+ })
260
+ })
261
+
262
+ describe('EmbeddingCache', () => {
263
+ let embeddingCache: EmbeddingCache
264
+
265
+ beforeEach(() => {
266
+ embeddingCache = new EmbeddingCache()
267
+ })
268
+
269
+ it('stores and retrieves embeddings by content hash', async () => {
270
+ const embedding = [0.1, 0.2, 0.3, 0.4, 0.5]
271
+
272
+ await embeddingCache.set('hello world', embedding, { model: 'text-embedding-3-small' })
273
+ const result = await embeddingCache.get('hello world', { model: 'text-embedding-3-small' })
274
+
275
+ expect(result).toEqual(embedding)
276
+ })
277
+
278
+ it('returns undefined for cache miss', async () => {
279
+ const result = await embeddingCache.get('unknown text', { model: 'text-embedding-3-small' })
280
+ expect(result).toBeUndefined()
281
+ })
282
+
283
+ it('differentiates by model', async () => {
284
+ const embedding1 = [0.1, 0.2, 0.3]
285
+ const embedding2 = [0.4, 0.5, 0.6]
286
+
287
+ await embeddingCache.set('hello', embedding1, { model: 'model-a' })
288
+ await embeddingCache.set('hello', embedding2, { model: 'model-b' })
289
+
290
+ expect(await embeddingCache.get('hello', { model: 'model-a' })).toEqual(embedding1)
291
+ expect(await embeddingCache.get('hello', { model: 'model-b' })).toEqual(embedding2)
292
+ })
293
+
294
+ it('caches batch embeddings', async () => {
295
+ const texts = ['doc1', 'doc2', 'doc3']
296
+ const embeddings = [
297
+ [0.1, 0.2],
298
+ [0.3, 0.4],
299
+ [0.5, 0.6]
300
+ ]
301
+
302
+ await embeddingCache.setMany(texts, embeddings, { model: 'text-embedding-3-small' })
303
+
304
+ expect(await embeddingCache.get('doc1', { model: 'text-embedding-3-small' })).toEqual([0.1, 0.2])
305
+ expect(await embeddingCache.get('doc2', { model: 'text-embedding-3-small' })).toEqual([0.3, 0.4])
306
+ expect(await embeddingCache.get('doc3', { model: 'text-embedding-3-small' })).toEqual([0.5, 0.6])
307
+ })
308
+
309
+ it('returns partial hits for batch lookups', async () => {
310
+ // Pre-populate some embeddings
311
+ await embeddingCache.set('doc1', [0.1, 0.2], { model: 'model-a' })
312
+ await embeddingCache.set('doc3', [0.5, 0.6], { model: 'model-a' })
313
+
314
+ const result = await embeddingCache.getMany(['doc1', 'doc2', 'doc3'], { model: 'model-a' })
315
+
316
+ expect(result.hits).toEqual({
317
+ 'doc1': [0.1, 0.2],
318
+ 'doc3': [0.5, 0.6]
319
+ })
320
+ expect(result.misses).toEqual(['doc2'])
321
+ })
322
+
323
+ it('provides cache statistics', async () => {
324
+ await embeddingCache.set('doc1', [0.1], { model: 'model-a' })
325
+
326
+ await embeddingCache.get('doc1', { model: 'model-a' }) // Hit
327
+ await embeddingCache.get('doc2', { model: 'model-a' }) // Miss
328
+
329
+ const stats = embeddingCache.getStats()
330
+
331
+ expect(stats.hits).toBe(1)
332
+ expect(stats.misses).toBe(1)
333
+ expect(stats.hitRate).toBeCloseTo(0.5)
334
+ expect(stats.size).toBe(1)
335
+ })
336
+ })
337
+
338
+ describe('GenerationCache', () => {
339
+ let generationCache: GenerationCache
340
+
341
+ beforeEach(() => {
342
+ generationCache = new GenerationCache()
343
+ })
344
+
345
+ it('caches generation results by prompt and parameters', async () => {
346
+ const result = { text: 'Hello, world!' }
347
+
348
+ await generationCache.set({
349
+ prompt: 'Say hello',
350
+ model: 'sonnet',
351
+ temperature: 0.7
352
+ }, result)
353
+
354
+ const cached = await generationCache.get({
355
+ prompt: 'Say hello',
356
+ model: 'sonnet',
357
+ temperature: 0.7
358
+ })
359
+
360
+ expect(cached).toEqual(result)
361
+ })
362
+
363
+ it('returns undefined for cache miss', async () => {
364
+ const cached = await generationCache.get({
365
+ prompt: 'Unknown prompt',
366
+ model: 'sonnet'
367
+ })
368
+
369
+ expect(cached).toBeUndefined()
370
+ })
371
+
372
+ it('differentiates by temperature', async () => {
373
+ const result1 = { text: 'Deterministic response' }
374
+ const result2 = { text: 'Creative response' }
375
+
376
+ await generationCache.set({
377
+ prompt: 'Write something',
378
+ model: 'sonnet',
379
+ temperature: 0
380
+ }, result1)
381
+
382
+ await generationCache.set({
383
+ prompt: 'Write something',
384
+ model: 'sonnet',
385
+ temperature: 1
386
+ }, result2)
387
+
388
+ expect(await generationCache.get({
389
+ prompt: 'Write something',
390
+ model: 'sonnet',
391
+ temperature: 0
392
+ })).toEqual(result1)
393
+
394
+ expect(await generationCache.get({
395
+ prompt: 'Write something',
396
+ model: 'sonnet',
397
+ temperature: 1
398
+ })).toEqual(result2)
399
+ })
400
+
401
+ it('differentiates by model', async () => {
402
+ await generationCache.set({
403
+ prompt: 'Hello',
404
+ model: 'sonnet'
405
+ }, { text: 'Sonnet response' })
406
+
407
+ await generationCache.set({
408
+ prompt: 'Hello',
409
+ model: 'opus'
410
+ }, { text: 'Opus response' })
411
+
412
+ expect(await generationCache.get({
413
+ prompt: 'Hello',
414
+ model: 'sonnet'
415
+ })).toEqual({ text: 'Sonnet response' })
416
+
417
+ expect(await generationCache.get({
418
+ prompt: 'Hello',
419
+ model: 'opus'
420
+ })).toEqual({ text: 'Opus response' })
421
+ })
422
+
423
+ it('includes system prompt in cache key', async () => {
424
+ await generationCache.set({
425
+ prompt: 'Hello',
426
+ model: 'sonnet',
427
+ system: 'You are a pirate'
428
+ }, { text: 'Ahoy!' })
429
+
430
+ await generationCache.set({
431
+ prompt: 'Hello',
432
+ model: 'sonnet',
433
+ system: 'You are a robot'
434
+ }, { text: 'Beep boop' })
435
+
436
+ expect(await generationCache.get({
437
+ prompt: 'Hello',
438
+ model: 'sonnet',
439
+ system: 'You are a pirate'
440
+ })).toEqual({ text: 'Ahoy!' })
441
+ })
442
+
443
+ it('supports schema versioning for structured outputs', async () => {
444
+ await generationCache.set({
445
+ prompt: 'Extract user',
446
+ model: 'sonnet',
447
+ schemaVersion: 'v1'
448
+ }, { object: { name: 'John' } })
449
+
450
+ await generationCache.set({
451
+ prompt: 'Extract user',
452
+ model: 'sonnet',
453
+ schemaVersion: 'v2'
454
+ }, { object: { name: 'John', age: 30 } })
455
+
456
+ expect(await generationCache.get({
457
+ prompt: 'Extract user',
458
+ model: 'sonnet',
459
+ schemaVersion: 'v1'
460
+ })).toEqual({ object: { name: 'John' } })
461
+
462
+ expect(await generationCache.get({
463
+ prompt: 'Extract user',
464
+ model: 'sonnet',
465
+ schemaVersion: 'v2'
466
+ })).toEqual({ object: { name: 'John', age: 30 } })
467
+ })
468
+
469
+ it('supports cache bypass option', async () => {
470
+ await generationCache.set({
471
+ prompt: 'Hello',
472
+ model: 'sonnet'
473
+ }, { text: 'Cached response' })
474
+
475
+ // With bypass, should return undefined
476
+ const bypassResult = await generationCache.get({
477
+ prompt: 'Hello',
478
+ model: 'sonnet'
479
+ }, { bypass: true })
480
+
481
+ expect(bypassResult).toBeUndefined()
482
+
483
+ // Without bypass, should return cached
484
+ const cachedResult = await generationCache.get({
485
+ prompt: 'Hello',
486
+ model: 'sonnet'
487
+ })
488
+
489
+ expect(cachedResult).toEqual({ text: 'Cached response' })
490
+ })
491
+
492
+ it('provides cache statistics', async () => {
493
+ await generationCache.set({ prompt: 'test', model: 'sonnet' }, { text: 'result' })
494
+
495
+ await generationCache.get({ prompt: 'test', model: 'sonnet' }) // Hit
496
+ await generationCache.get({ prompt: 'other', model: 'sonnet' }) // Miss
497
+
498
+ const stats = generationCache.getStats()
499
+
500
+ expect(stats.hits).toBe(1)
501
+ expect(stats.misses).toBe(1)
502
+ expect(stats.hitRate).toBeCloseTo(0.5)
503
+ })
504
+ })
505
+
506
+ describe('withCache wrapper', () => {
507
+ let cache: MemoryCache<string>
508
+
509
+ beforeEach(() => {
510
+ cache = new MemoryCache<string>()
511
+ })
512
+
513
+ it('wraps an async function with caching', async () => {
514
+ let callCount = 0
515
+ const expensiveOperation = async (input: string) => {
516
+ callCount++
517
+ return `Result for ${input}`
518
+ }
519
+
520
+ const cachedOperation = withCache(cache, expensiveOperation, {
521
+ keyFn: (input) => input
522
+ })
523
+
524
+ // First call - should execute function
525
+ const result1 = await cachedOperation('test')
526
+ expect(result1).toBe('Result for test')
527
+ expect(callCount).toBe(1)
528
+
529
+ // Second call with same input - should use cache
530
+ const result2 = await cachedOperation('test')
531
+ expect(result2).toBe('Result for test')
532
+ expect(callCount).toBe(1) // Still 1, didn't call again
533
+
534
+ // Different input - should execute function
535
+ const result3 = await cachedOperation('other')
536
+ expect(result3).toBe('Result for other')
537
+ expect(callCount).toBe(2)
538
+ })
539
+
540
+ it('supports custom key generation', async () => {
541
+ const fn = async (a: number, b: number) => a + b
542
+
543
+ const cachedFn = withCache(cache, fn, {
544
+ keyFn: (a, b) => `sum:${a}:${b}`
545
+ })
546
+
547
+ await cachedFn(1, 2)
548
+ await cachedFn(1, 2)
549
+
550
+ expect(await cache.has('sum:1:2')).toBe(true)
551
+ })
552
+
553
+ it('respects TTL option', async () => {
554
+ vi.useFakeTimers()
555
+
556
+ let callCount = 0
557
+ const fn = async () => {
558
+ callCount++
559
+ return 'result'
560
+ }
561
+
562
+ const ttlCache = new MemoryCache<string>({ defaultTTL: 1000 })
563
+ const cachedFn = withCache(ttlCache, fn, { keyFn: () => 'key' })
564
+
565
+ await cachedFn()
566
+ expect(callCount).toBe(1)
567
+
568
+ await cachedFn()
569
+ expect(callCount).toBe(1) // Cached
570
+
571
+ vi.advanceTimersByTime(1500)
572
+
573
+ await cachedFn()
574
+ expect(callCount).toBe(2) // Cache expired, called again
575
+
576
+ vi.useRealTimers()
577
+ })
578
+
579
+ it('handles errors without caching them', async () => {
580
+ let callCount = 0
581
+ const failingFn = async () => {
582
+ callCount++
583
+ throw new Error('Failed')
584
+ }
585
+
586
+ const cachedFn = withCache(cache, failingFn, { keyFn: () => 'key' })
587
+
588
+ await expect(cachedFn()).rejects.toThrow('Failed')
589
+ expect(callCount).toBe(1)
590
+
591
+ // Should retry since error wasn't cached
592
+ await expect(cachedFn()).rejects.toThrow('Failed')
593
+ expect(callCount).toBe(2)
594
+
595
+ // Nothing should be in cache
596
+ expect(await cache.has('key')).toBe(false)
597
+ })
598
+
599
+ it('supports bypass option', async () => {
600
+ let callCount = 0
601
+ const fn = async () => {
602
+ callCount++
603
+ return `result-${callCount}`
604
+ }
605
+
606
+ const cachedFn = withCache(cache, fn, { keyFn: () => 'key' })
607
+
608
+ const result1 = await cachedFn()
609
+ expect(result1).toBe('result-1')
610
+
611
+ // Force fresh result
612
+ const result2 = await cachedFn.bypass()
613
+ expect(result2).toBe('result-2')
614
+ expect(callCount).toBe(2)
615
+
616
+ // Cache should be updated with new result
617
+ const result3 = await cachedFn()
618
+ expect(result3).toBe('result-2')
619
+ expect(callCount).toBe(2)
620
+ })
621
+ })
622
+
623
+ describe('Cache entry metadata', () => {
624
+ it('tracks creation time', async () => {
625
+ const cache = new MemoryCache<string>()
626
+ const before = Date.now()
627
+
628
+ await cache.set('key', 'value')
629
+
630
+ const entry = await cache.getEntry('key')
631
+ expect(entry).toBeDefined()
632
+ expect(entry!.createdAt).toBeGreaterThanOrEqual(before)
633
+ expect(entry!.createdAt).toBeLessThanOrEqual(Date.now())
634
+ })
635
+
636
+ it('tracks access time', async () => {
637
+ vi.useFakeTimers()
638
+
639
+ const cache = new MemoryCache<string>()
640
+ await cache.set('key', 'value')
641
+
642
+ const entry1 = await cache.getEntry('key')
643
+ const createdAt = entry1!.createdAt
644
+
645
+ vi.advanceTimersByTime(1000)
646
+
647
+ await cache.get('key')
648
+ const entry2 = await cache.getEntry('key')
649
+
650
+ expect(entry2!.lastAccessedAt).toBeGreaterThan(createdAt)
651
+
652
+ vi.useRealTimers()
653
+ })
654
+
655
+ it('tracks access count', async () => {
656
+ const cache = new MemoryCache<string>()
657
+ await cache.set('key', 'value')
658
+
659
+ await cache.get('key')
660
+ await cache.get('key')
661
+ await cache.get('key')
662
+
663
+ const entry = await cache.getEntry('key')
664
+ expect(entry!.accessCount).toBe(3)
665
+ })
666
+ })
667
+
668
+ describe('Distributed cache interface', () => {
669
+ // These tests verify the interface contracts for Redis-like backends
670
+
671
+ it('CacheStorage interface is properly typed', () => {
672
+ // This is a compile-time check
673
+ const mockStorage: CacheStorage<string> = {
674
+ get: async () => undefined,
675
+ set: async () => {},
676
+ has: async () => false,
677
+ delete: async () => {},
678
+ clear: async () => {},
679
+ size: async () => 0,
680
+ keys: async () => []
681
+ }
682
+
683
+ expect(mockStorage).toBeDefined()
684
+ })
685
+
686
+ it('MemoryCache implements CacheStorage', () => {
687
+ const cache = new MemoryCache<string>()
688
+
689
+ // Verify all methods exist
690
+ expect(typeof cache.get).toBe('function')
691
+ expect(typeof cache.set).toBe('function')
692
+ expect(typeof cache.has).toBe('function')
693
+ expect(typeof cache.delete).toBe('function')
694
+ expect(typeof cache.clear).toBe('function')
695
+ expect(typeof cache.size).toBe('function')
696
+ expect(typeof cache.keys).toBe('function')
697
+ })
698
+ })
699
+
700
+ describe('Cache serialization', () => {
701
+ it('serializes cache entries for distributed storage', async () => {
702
+ const cache = new MemoryCache<{ data: number[] }>()
703
+
704
+ await cache.set('vectors', { data: [0.1, 0.2, 0.3] })
705
+
706
+ const entry = await cache.getEntry('vectors')
707
+ const serialized = JSON.stringify(entry)
708
+ const deserialized = JSON.parse(serialized)
709
+
710
+ expect(deserialized.value.data).toEqual([0.1, 0.2, 0.3])
711
+ })
712
+ })