pi-l1-cache 1.2.2 → 1.4.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/README.md +128 -112
- package/fix-l1-cache.cjs +405 -0
- package/package.json +11 -9
- package/src/index.test.ts +457 -179
- package/src/index.ts +272 -255
package/src/index.test.ts
CHANGED
|
@@ -1,236 +1,514 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// L1 Cache Extension — Test Suite
|
|
2
|
+
//
|
|
3
|
+
// Tests for the working implementation with replay, disk persistence,
|
|
4
|
+
// and proper key semantics.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
import { describe, it, beforeEach, afterEach } from "node:test"
|
|
7
|
+
import * as assert from "node:assert"
|
|
8
|
+
import fs from "node:fs"
|
|
9
|
+
import path from "node:path"
|
|
10
|
+
import os from "node:os"
|
|
11
|
+
import { fileURLToPath } from "node:url"
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
|
|
15
|
+
import l1CacheExtension from "./index.js"
|
|
16
|
+
import { cache, stats, settings, fastHash, estimateSize, coalesceChunks, evictIfNeeded, cleanupExpired, keyForPayload, persistEntry, loadPersisted, resetForTests } from "./index.js"
|
|
17
|
+
|
|
18
|
+
// Test cache directory (isolated from real cache)
|
|
19
|
+
const TEST_CACHE_DIR = path.join(os.tmpdir(), "l1-cache-test-" + Date.now())
|
|
20
|
+
|
|
21
|
+
// Mock ExtensionAPI
|
|
22
|
+
function createMockExtensionAPI(): any {
|
|
23
|
+
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<any>>>()
|
|
24
|
+
const commands = new Map<string, any>()
|
|
25
|
+
let sessionShutdownHandler: ((event: any, ctx?: any) => Promise<any>) | null = null
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
on: (event: string, handler: (event: any, ctx?: any) => Promise<any>) => {
|
|
29
|
+
if (event === "session_shutdown") {
|
|
30
|
+
sessionShutdownHandler = handler
|
|
31
|
+
} else {
|
|
32
|
+
if (!handlers.has(event)) handlers.set(event, [])
|
|
33
|
+
handlers.get(event)!.push(handler)
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
registerCommand: (name: string, config: any) => {
|
|
37
|
+
commands.set(name, config)
|
|
38
|
+
},
|
|
39
|
+
emit: async (event: any) => {
|
|
40
|
+
const eventHandlers = handlers.get(event.type) || []
|
|
41
|
+
for (const handler of eventHandlers) {
|
|
42
|
+
await handler(event, {})
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
_getHandlers: (event: string) => handlers.get(event) || [],
|
|
46
|
+
_getSessionShutdownHandler: () => sessionShutdownHandler,
|
|
47
|
+
_getCommands: () => commands,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("L1 Cache", () => {
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
// Clean test cache directory
|
|
54
|
+
if (fs.existsSync(TEST_CACHE_DIR)) {
|
|
55
|
+
fs.rmSync(TEST_CACHE_DIR, { recursive: true, force: true })
|
|
56
|
+
}
|
|
57
|
+
fs.mkdirSync(TEST_CACHE_DIR, { recursive: true })
|
|
58
|
+
// Isolate: redirect persistence + reset all module state
|
|
59
|
+
process.env.L1_CACHE_DIR = TEST_CACHE_DIR
|
|
60
|
+
resetForTests()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
// Cleanup test cache directory
|
|
65
|
+
if (fs.existsSync(TEST_CACHE_DIR)) {
|
|
66
|
+
fs.rmSync(TEST_CACHE_DIR, { recursive: true, force: true })
|
|
67
|
+
}
|
|
68
|
+
})
|
|
6
69
|
|
|
7
|
-
describe("pi-l1-cache", () => {
|
|
8
70
|
describe("fastHash", () => {
|
|
9
|
-
it("
|
|
10
|
-
const
|
|
11
|
-
|
|
71
|
+
it("produces consistent hashes for identical strings", () => {
|
|
72
|
+
const str = "test string"
|
|
73
|
+
const h1 = fastHash(str)
|
|
74
|
+
const h2 = fastHash(str)
|
|
75
|
+
assert.strictEqual(h1, h2)
|
|
12
76
|
})
|
|
13
77
|
|
|
14
|
-
it("
|
|
15
|
-
|
|
78
|
+
it("produces different hashes for different strings", () => {
|
|
79
|
+
const h1 = fastHash("string1")
|
|
80
|
+
const h2 = fastHash("string2")
|
|
81
|
+
assert.notStrictEqual(h1, h2)
|
|
16
82
|
})
|
|
17
83
|
|
|
18
84
|
it("handles empty string", () => {
|
|
19
|
-
const
|
|
20
|
-
assert.ok(
|
|
85
|
+
const hash = fastHash("")
|
|
86
|
+
assert.ok(hash.length > 0)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("handles unicode characters", () => {
|
|
90
|
+
const hash = fastHash("🚀 test")
|
|
91
|
+
assert.ok(hash.length > 0)
|
|
21
92
|
})
|
|
93
|
+
})
|
|
22
94
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
95
|
+
describe("keyForPayload", () => {
|
|
96
|
+
it("creates key from model and messages", () => {
|
|
97
|
+
const payload = {
|
|
98
|
+
model: "local/deepseek-v4",
|
|
99
|
+
messages: [{ role: "user", content: "test" }],
|
|
100
|
+
}
|
|
101
|
+
const key = keyForPayload(payload)
|
|
102
|
+
assert.ok(key)
|
|
103
|
+
assert.ok(key.length > 0)
|
|
26
104
|
})
|
|
27
105
|
|
|
28
|
-
it("
|
|
29
|
-
const
|
|
30
|
-
|
|
106
|
+
it("excludes volatile fields from key", () => {
|
|
107
|
+
const payload1 = {
|
|
108
|
+
model: "local/deepseek-v4",
|
|
109
|
+
messages: [{ role: "user", content: "test" }],
|
|
110
|
+
prompt_cache_key: "session-123",
|
|
111
|
+
stream: true,
|
|
112
|
+
}
|
|
113
|
+
const payload2 = {
|
|
114
|
+
model: "local/deepseek-v4",
|
|
115
|
+
messages: [{ role: "user", content: "test" }],
|
|
116
|
+
prompt_cache_key: "session-456", // different
|
|
117
|
+
stream: false, // different
|
|
118
|
+
}
|
|
119
|
+
const key1 = keyForPayload(payload1)
|
|
120
|
+
const key2 = keyForPayload(payload2)
|
|
121
|
+
assert.strictEqual(key1, key2) // keys should match despite volatile differences
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("includes tools in key", () => {
|
|
125
|
+
const payload1 = {
|
|
126
|
+
model: "local/deepseek-v4",
|
|
127
|
+
messages: [{ role: "user", content: "test" }],
|
|
128
|
+
tools: [{ name: "tool1" }],
|
|
129
|
+
}
|
|
130
|
+
const payload2 = {
|
|
131
|
+
model: "local/deepseek-v4",
|
|
132
|
+
messages: [{ role: "user", content: "test" }],
|
|
133
|
+
tools: [{ name: "tool2" }], // different
|
|
134
|
+
}
|
|
135
|
+
const key1 = keyForPayload(payload1)
|
|
136
|
+
const key2 = keyForPayload(payload2)
|
|
137
|
+
assert.notStrictEqual(key1, key2)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("returns null for invalid payloads", () => {
|
|
141
|
+
assert.strictEqual(keyForPayload(null), null)
|
|
142
|
+
assert.strictEqual(keyForPayload(undefined), null)
|
|
143
|
+
assert.strictEqual(keyForPayload({}), null)
|
|
144
|
+
assert.strictEqual(keyForPayload({ messages: "not-array" }), null)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe("coalesceChunks", () => {
|
|
149
|
+
it("merges adjacent content deltas", () => {
|
|
150
|
+
const chunks = [
|
|
151
|
+
{ choices: [{ delta: { content: "hello" } }] },
|
|
152
|
+
{ choices: [{ delta: { content: " " } }] },
|
|
153
|
+
{ choices: [{ delta: { content: "world" } }] },
|
|
154
|
+
]
|
|
155
|
+
const coalesced = coalesceChunks(chunks)
|
|
156
|
+
assert.strictEqual(coalesced.length, 1)
|
|
157
|
+
assert.strictEqual(coalesced[0].choices[0].delta.content, "hello world")
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it("merges adjacent reasoning_content deltas", () => {
|
|
161
|
+
const chunks = [
|
|
162
|
+
{ choices: [{ delta: { reasoning_content: "thinking" } }] },
|
|
163
|
+
{ choices: [{ delta: { reasoning_content: " more" } }] },
|
|
164
|
+
]
|
|
165
|
+
const coalesced = coalesceChunks(chunks)
|
|
166
|
+
assert.strictEqual(coalesced.length, 1)
|
|
167
|
+
assert.strictEqual(coalesced[0].choices[0].delta.reasoning_content, "thinking more")
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it("does not merge chunks with finish_reason", () => {
|
|
171
|
+
const chunks = [
|
|
172
|
+
{ choices: [{ delta: { content: "hello" } }] },
|
|
173
|
+
{ choices: [{ delta: { content: "world" }, finish_reason: "stop" }] },
|
|
174
|
+
]
|
|
175
|
+
const coalesced = coalesceChunks(chunks)
|
|
176
|
+
assert.strictEqual(coalesced.length, 2)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it("does not merge chunks with tool_calls", () => {
|
|
180
|
+
const chunks = [
|
|
181
|
+
{ choices: [{ delta: { content: "hello" } }] },
|
|
182
|
+
{ choices: [{ delta: { tool_calls: [{ id: "call1" }] } }] },
|
|
183
|
+
]
|
|
184
|
+
const coalesced = coalesceChunks(chunks)
|
|
185
|
+
assert.strictEqual(coalesced.length, 2)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it("handles empty chunks array", () => {
|
|
189
|
+
const coalesced = coalesceChunks([])
|
|
190
|
+
assert.strictEqual(coalesced.length, 0)
|
|
31
191
|
})
|
|
32
192
|
})
|
|
33
193
|
|
|
34
194
|
describe("estimateSize", () => {
|
|
35
|
-
it("estimates size
|
|
36
|
-
const
|
|
195
|
+
it("estimates size for simple objects", () => {
|
|
196
|
+
const obj = { text: "hello" }
|
|
197
|
+
const size = estimateSize(obj)
|
|
37
198
|
assert.ok(size > 0)
|
|
38
|
-
assert.ok(size < 100)
|
|
39
199
|
})
|
|
40
200
|
|
|
41
|
-
it("
|
|
42
|
-
const obj = {
|
|
43
|
-
const size =
|
|
44
|
-
assert.ok(size >
|
|
201
|
+
it("handles large objects", () => {
|
|
202
|
+
const obj = { text: "x".repeat(10000) }
|
|
203
|
+
const size = estimateSize(obj)
|
|
204
|
+
assert.ok(size > 10000)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("handles undefined", () => {
|
|
208
|
+
const size = estimateSize(undefined)
|
|
209
|
+
assert.strictEqual(size, 4096) // fallback for non-serializable
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe("cache operations", () => {
|
|
214
|
+
it("stores and retrieves entries", async () => {
|
|
215
|
+
const payload = {
|
|
216
|
+
model: "local/deepseek-v4",
|
|
217
|
+
messages: [{ role: "user", content: "test" }],
|
|
218
|
+
}
|
|
219
|
+
const key = keyForPayload(payload)!
|
|
220
|
+
const chunks = [{ choices: [{ delta: { content: "answer" } }] }]
|
|
221
|
+
const size = estimateSize(chunks)
|
|
222
|
+
|
|
223
|
+
cache.set(key, { chunks, timestamp: Date.now(), sizeBytes: size })
|
|
224
|
+
evictIfNeeded()
|
|
225
|
+
|
|
226
|
+
const entry = cache.get(key)
|
|
227
|
+
assert.ok(entry)
|
|
228
|
+
assert.strictEqual(entry!.chunks.length, 1)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it("evicts oldest entries when over memory cap", async () => {
|
|
232
|
+
settings.maxEntries = 2
|
|
233
|
+
settings.maxMemoryBytes = 100
|
|
234
|
+
const chunks = [{ choices: [{ delta: { content: "x".repeat(50) } }] }]
|
|
235
|
+
const size = estimateSize(chunks)
|
|
236
|
+
|
|
237
|
+
// Store 3 entries (should evict to stay within cap)
|
|
238
|
+
for (let i = 0; i < 3; i++) {
|
|
239
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: `test${i}` }] }
|
|
240
|
+
const key = keyForPayload(payload)!
|
|
241
|
+
cache.set(key, { chunks, timestamp: Date.now() + i, sizeBytes: size })
|
|
242
|
+
evictIfNeeded()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
assert.ok(cache.size <= 2)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it("respects TTL expiry", async () => {
|
|
249
|
+
settings.ttlSeconds = 1
|
|
250
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
251
|
+
const key = keyForPayload(payload)!
|
|
252
|
+
const chunks = [{ choices: [{ delta: { content: "answer" } }] }]
|
|
253
|
+
|
|
254
|
+
// Set timestamp in past (expired)
|
|
255
|
+
cache.set(key, { chunks, timestamp: Date.now() - 2000, sizeBytes: estimateSize(chunks) })
|
|
256
|
+
|
|
257
|
+
// Simulate cleanup
|
|
258
|
+
cleanupExpired()
|
|
259
|
+
|
|
260
|
+
assert.strictEqual(cache.has(key), false)
|
|
45
261
|
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe("persistence", () => {
|
|
265
|
+
it("persists entries to disk", async () => {
|
|
266
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
267
|
+
const key = keyForPayload(payload)!
|
|
268
|
+
const chunks = [{ choices: [{ delta: { content: "answer" } }] }]
|
|
46
269
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
270
|
+
persistEntry(key, { chunks, timestamp: Date.now(), sizeBytes: estimateSize(chunks) })
|
|
271
|
+
|
|
272
|
+
// Verify file exists
|
|
273
|
+
const filePath = path.join(TEST_CACHE_DIR, key + ".json")
|
|
274
|
+
assert.ok(fs.existsSync(filePath))
|
|
275
|
+
|
|
276
|
+
// Load and verify
|
|
277
|
+
cache.clear()
|
|
278
|
+
loadPersisted()
|
|
279
|
+
assert.strictEqual(cache.size, 1)
|
|
51
280
|
})
|
|
52
281
|
|
|
53
|
-
it("
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
282
|
+
it("ignores expired entries on load", async () => {
|
|
283
|
+
settings.ttlSeconds = 1
|
|
284
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
285
|
+
const key = keyForPayload(payload)!
|
|
286
|
+
|
|
287
|
+
// Persist with old timestamp
|
|
288
|
+
persistEntry(key, {
|
|
289
|
+
chunks: [{ choices: [{ delta: { content: "answer" } }] }],
|
|
290
|
+
timestamp: Date.now() - 2000,
|
|
291
|
+
sizeBytes: estimateSize([]),
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
cache.clear()
|
|
295
|
+
loadPersisted()
|
|
296
|
+
assert.strictEqual(cache.size, 0)
|
|
58
297
|
})
|
|
59
298
|
|
|
60
|
-
it("handles
|
|
61
|
-
|
|
62
|
-
|
|
299
|
+
it("handles corrupted files gracefully", async () => {
|
|
300
|
+
// Create corrupted file
|
|
301
|
+
fs.writeFileSync(path.join(TEST_CACHE_DIR, "corrupt.json"), "not valid json")
|
|
302
|
+
|
|
303
|
+
cache.clear()
|
|
304
|
+
loadPersisted()
|
|
305
|
+
// Should not crash, corrupted file should be deleted
|
|
306
|
+
assert.strictEqual(fs.existsSync(path.join(TEST_CACHE_DIR, "corrupt.json")), false)
|
|
63
307
|
})
|
|
64
308
|
})
|
|
65
309
|
|
|
66
|
-
describe("
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
310
|
+
describe("extension integration", () => {
|
|
311
|
+
it("initializes and registers commands", async () => {
|
|
312
|
+
const mockApi = createMockExtensionAPI()
|
|
313
|
+
await l1CacheExtension(mockApi)
|
|
314
|
+
|
|
315
|
+
const commands = mockApi._getCommands()
|
|
316
|
+
assert.ok(commands.has("l1-cache"))
|
|
70
317
|
})
|
|
71
318
|
|
|
72
|
-
it("
|
|
73
|
-
|
|
74
|
-
|
|
319
|
+
it("handles before_provider_request (miss)", async () => {
|
|
320
|
+
const mockApi = createMockExtensionAPI()
|
|
321
|
+
await l1CacheExtension(mockApi)
|
|
322
|
+
|
|
323
|
+
const handler = mockApi._getHandlers("before_provider_request")[0]
|
|
324
|
+
const event = {
|
|
325
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
75
326
|
}
|
|
76
|
-
|
|
77
|
-
|
|
327
|
+
|
|
328
|
+
const result = await handler(event, { model: { id: "local/deepseek-v4" } })
|
|
329
|
+
assert.strictEqual(result, undefined) // miss, no replay
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it("handles before_provider_request (hit)", async () => {
|
|
333
|
+
const mockApi = createMockExtensionAPI()
|
|
334
|
+
await l1CacheExtension(mockApi)
|
|
335
|
+
|
|
336
|
+
const handler = mockApi._getHandlers("before_provider_request")[0]
|
|
337
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
338
|
+
const event = { payload }
|
|
339
|
+
|
|
340
|
+
// First call is miss
|
|
341
|
+
await handler(event, { model: { id: "local/deepseek-v4" } })
|
|
342
|
+
|
|
343
|
+
// Simulate a cache entry
|
|
344
|
+
const key = keyForPayload(payload)!
|
|
345
|
+
cache.set(key, {
|
|
346
|
+
chunks: [{ choices: [{ delta: { content: "cached" } }] }],
|
|
347
|
+
timestamp: Date.now(),
|
|
348
|
+
sizeBytes: 100,
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// Second call should hit
|
|
352
|
+
const result = await handler(event, { model: { id: "local/deepseek-v4" } })
|
|
353
|
+
assert.ok(result)
|
|
354
|
+
assert.ok((result as any).__piL1Replay)
|
|
78
355
|
})
|
|
79
356
|
|
|
80
|
-
it("
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
357
|
+
it("handles provider_stream_complete", async () => {
|
|
358
|
+
const mockApi = createMockExtensionAPI()
|
|
359
|
+
await l1CacheExtension(mockApi)
|
|
360
|
+
|
|
361
|
+
const handler = mockApi._getHandlers("provider_stream_complete")[0]
|
|
362
|
+
const event = {
|
|
363
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
364
|
+
chunks: [{ choices: [{ delta: { content: "answer" } }] }],
|
|
85
365
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
366
|
+
|
|
367
|
+
await handler(event)
|
|
368
|
+
|
|
369
|
+
// Verify entry was stored
|
|
370
|
+
const key = keyForPayload(event.payload)!
|
|
371
|
+
const entry = cache.get(key)
|
|
372
|
+
assert.ok(entry)
|
|
373
|
+
assert.strictEqual(entry!.chunks.length, 1)
|
|
89
374
|
})
|
|
90
375
|
|
|
91
|
-
it("
|
|
92
|
-
|
|
93
|
-
|
|
376
|
+
it("handles /l1-cache clear command", async () => {
|
|
377
|
+
const mockApi = createMockExtensionAPI()
|
|
378
|
+
await l1CacheExtension(mockApi)
|
|
379
|
+
|
|
380
|
+
const command = mockApi._getCommands().get("l1-cache")
|
|
381
|
+
const mockCtx = {
|
|
382
|
+
ui: {
|
|
383
|
+
notify: () => {},
|
|
384
|
+
},
|
|
94
385
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
386
|
+
|
|
387
|
+
// Populate first so the assertion is meaningful
|
|
388
|
+
cache.set("k", { chunks: [{ choices: [{ delta: { content: "x" } }] }], timestamp: Date.now(), sizeBytes: 64 })
|
|
389
|
+
|
|
390
|
+
await command!.handler("clear", mockCtx)
|
|
391
|
+
|
|
392
|
+
assert.strictEqual(cache.size, 0)
|
|
98
393
|
})
|
|
99
394
|
|
|
100
|
-
it("
|
|
101
|
-
|
|
102
|
-
|
|
395
|
+
it("handles /l1-cache stats command", async () => {
|
|
396
|
+
const mockApi = createMockExtensionAPI()
|
|
397
|
+
await l1CacheExtension(mockApi)
|
|
398
|
+
|
|
399
|
+
const command = mockApi._getCommands().get("l1-cache")
|
|
400
|
+
const mockCtx = {
|
|
401
|
+
ui: {
|
|
402
|
+
notify: (msg: string) => {
|
|
403
|
+
assert.ok(msg.includes("L1 cache"))
|
|
404
|
+
},
|
|
405
|
+
},
|
|
103
406
|
}
|
|
104
|
-
|
|
105
|
-
|
|
407
|
+
|
|
408
|
+
await command!.handler("stats", mockCtx)
|
|
106
409
|
})
|
|
107
410
|
})
|
|
108
411
|
|
|
109
|
-
describe("
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
412
|
+
describe("edge cases", () => {
|
|
413
|
+
it("handles disabled cache", async () => {
|
|
414
|
+
settings.enabled = false
|
|
415
|
+
|
|
416
|
+
const mockApi = createMockExtensionAPI()
|
|
417
|
+
await l1CacheExtension(mockApi)
|
|
418
|
+
|
|
419
|
+
const handler = mockApi._getHandlers("before_provider_request")[0]
|
|
420
|
+
const event = {
|
|
421
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const result = await handler(event, { model: { id: "local/deepseek-v4" } })
|
|
425
|
+
assert.strictEqual(result, undefined)
|
|
113
426
|
})
|
|
114
427
|
|
|
115
|
-
it("
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
428
|
+
it("handles empty chunks (no storage)", async () => {
|
|
429
|
+
const mockApi = createMockExtensionAPI()
|
|
430
|
+
await l1CacheExtension(mockApi)
|
|
431
|
+
|
|
432
|
+
const handler = mockApi._getHandlers("provider_stream_complete")[0]
|
|
433
|
+
const event = {
|
|
434
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
435
|
+
chunks: [], // empty
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
await handler(event)
|
|
439
|
+
const key = keyForPayload(event.payload)!
|
|
440
|
+
assert.strictEqual(cache.has(key), false)
|
|
120
441
|
})
|
|
121
442
|
|
|
122
|
-
it("
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
443
|
+
it("handles stale ctx gracefully", async () => {
|
|
444
|
+
const mockApi = createMockExtensionAPI()
|
|
445
|
+
await l1CacheExtension(mockApi)
|
|
446
|
+
|
|
447
|
+
const handler = mockApi._getHandlers("before_provider_request")[0]
|
|
448
|
+
const event = {
|
|
449
|
+
payload: { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] },
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Stale ctx (no model.id)
|
|
453
|
+
const result = await handler(event, { model: { id: undefined } })
|
|
454
|
+
assert.strictEqual(result, undefined)
|
|
127
455
|
})
|
|
128
456
|
|
|
129
|
-
it("
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
457
|
+
it("prevents re-storing replays", async () => {
|
|
458
|
+
const mockApi = createMockExtensionAPI()
|
|
459
|
+
await l1CacheExtension(mockApi)
|
|
460
|
+
|
|
461
|
+
const beforeHandler = mockApi._getHandlers("before_provider_request")[0]
|
|
462
|
+
const completeHandler = mockApi._getHandlers("provider_stream_complete")[0]
|
|
463
|
+
|
|
464
|
+
const payload = { model: "local/deepseek-v4", messages: [{ role: "user", content: "test" }] }
|
|
465
|
+
const event = { payload }
|
|
466
|
+
|
|
467
|
+
// First call is miss
|
|
468
|
+
await beforeHandler(event, { model: { id: "local/deepseek-v4" } })
|
|
469
|
+
|
|
470
|
+
// Simulate cache hit
|
|
471
|
+
const key = keyForPayload(payload)!
|
|
472
|
+
cache.set(key, {
|
|
473
|
+
chunks: [{ choices: [{ delta: { content: "cached" } }] }],
|
|
474
|
+
timestamp: Date.now(),
|
|
475
|
+
sizeBytes: 100,
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
// Second call is hit
|
|
479
|
+
const result = await beforeHandler(event, { model: { id: "local/deepseek-v4" } })
|
|
480
|
+
assert.ok(result)
|
|
481
|
+
|
|
482
|
+
// Try to store (should be prevented by lastServed guard)
|
|
483
|
+
const chunksBefore = cache.get(key)!.chunks.length
|
|
484
|
+
await completeHandler({ payload, chunks: [{ choices: [{ delta: { content: "new" } }] }] })
|
|
485
|
+
const chunksAfter = cache.get(key)!.chunks.length
|
|
486
|
+
|
|
487
|
+
assert.strictEqual(chunksBefore, chunksAfter) // should not re-store
|
|
134
488
|
})
|
|
135
489
|
})
|
|
136
490
|
|
|
137
|
-
describe("
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
491
|
+
describe("performance", () => {
|
|
492
|
+
it("hash is fast (< 10µs per call)", () => {
|
|
493
|
+
const iterations = 1000
|
|
494
|
+
const start = Date.now()
|
|
141
495
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
registerCommand: () => {},
|
|
145
|
-
on: (name: string, cb: Handler) => {
|
|
146
|
-
;(events[name] ??= []).push(cb)
|
|
147
|
-
},
|
|
496
|
+
for (let i = 0; i < iterations; i++) {
|
|
497
|
+
fastHash("test string " + i)
|
|
148
498
|
}
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const payload = () => ({ messages: [{ role: "user", content: "hello cache" }], parameters: {} })
|
|
164
|
-
const event = (): any => ({ type: "before_provider_request", payload: payload() })
|
|
165
|
-
const reqCtx = { model: { id: "test-model" } }
|
|
166
|
-
|
|
167
|
-
it("records a miss and stamps _cacheKey on first request", async () => {
|
|
168
|
-
const ev: any = event()
|
|
169
|
-
const ret = await events.before_provider_request[0](ev, reqCtx)
|
|
170
|
-
assert.equal(ret, undefined)
|
|
171
|
-
assert.ok(ev._cacheKey, "should stamp _cacheKey on miss")
|
|
172
|
-
assert.equal(Test.getStats().misses, 1)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it("returns the cached response on a byte-identical repeat when a body is captured", async () => {
|
|
176
|
-
const first: any = event()
|
|
177
|
-
await events.before_provider_request[0](first, reqCtx)
|
|
178
|
-
// Simulate a pi API that exposes the response body
|
|
179
|
-
await events.after_provider_response[0]({
|
|
180
|
-
type: "after_provider_response",
|
|
181
|
-
status: 200,
|
|
182
|
-
headers: {},
|
|
183
|
-
_cacheKey: first._cacheKey,
|
|
184
|
-
response: { content: "cached!" },
|
|
185
|
-
} as any)
|
|
186
|
-
|
|
187
|
-
const second: any = event()
|
|
188
|
-
const out = await events.before_provider_request[0](second, reqCtx)
|
|
189
|
-
assert.ok(out, "identical repeat should short-circuit to cache")
|
|
190
|
-
assert.equal(out.content, "cached!")
|
|
191
|
-
assert.equal(Test.getStats().hits, 1)
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it("never stores an event envelope when no response body is available", async () => {
|
|
195
|
-
const first: any = event()
|
|
196
|
-
await events.before_provider_request[0](first, reqCtx)
|
|
197
|
-
// pi 0.84 + shape: status + headers only, no body
|
|
198
|
-
await events.after_provider_response[0]({
|
|
199
|
-
type: "after_provider_response",
|
|
200
|
-
status: 200,
|
|
201
|
-
headers: {},
|
|
202
|
-
} as any)
|
|
203
|
-
assert.equal(Test._getCacheSize(), 0, "must not cache the event envelope")
|
|
204
|
-
assert.ok(
|
|
205
|
-
Test.getStats().misses >= 0,
|
|
206
|
-
"after_provider_response without a body must not poison the cache",
|
|
207
|
-
)
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it("does not touch the cache when disabled", async () => {
|
|
211
|
-
Test.setSettings({ enabled: false })
|
|
212
|
-
const ev: any = event()
|
|
213
|
-
const ret = await events.before_provider_request[0](ev, reqCtx)
|
|
214
|
-
assert.equal(ret, undefined)
|
|
215
|
-
assert.equal(ev._cacheKey, undefined)
|
|
216
|
-
assert.equal(Test.getStats().misses, 0)
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it("keeps size within cap under interceptor churn", async () => {
|
|
220
|
-
Test.setSettings({ maxEntries: 20 })
|
|
221
|
-
for (let i = 0; i < 50; i++) {
|
|
222
|
-
const ev: any = {
|
|
223
|
-
type: "before_provider_request",
|
|
224
|
-
payload: { messages: [{ role: "user", content: `q-${i}` }], parameters: {} },
|
|
225
|
-
}
|
|
226
|
-
await events.before_provider_request[0](ev, reqCtx)
|
|
227
|
-
await events.after_provider_response[0]({
|
|
228
|
-
_cacheKey: ev._cacheKey,
|
|
229
|
-
response: { content: "a".repeat(50) },
|
|
230
|
-
} as any)
|
|
231
|
-
}
|
|
232
|
-
assert.ok(Test._getCacheSize() <= 20)
|
|
233
|
-
assert.ok(Test.getStats().misses >= 50)
|
|
499
|
+
|
|
500
|
+
const elapsed = Date.now() - start
|
|
501
|
+
const avgPerCall = (elapsed * 1000) / iterations // in µs
|
|
502
|
+
assert.ok(avgPerCall < 10, `Hash average ${avgPerCall}µs, expected < 10µs`)
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it("coalescing reduces chunk count", () => {
|
|
506
|
+
const chunks = Array.from({ length: 100 }, (_, i) => ({
|
|
507
|
+
choices: [{ delta: { content: "x" } }],
|
|
508
|
+
}))
|
|
509
|
+
|
|
510
|
+
const coalesced = coalesceChunks(chunks)
|
|
511
|
+
assert.strictEqual(coalesced.length, 1) // all merged into one
|
|
234
512
|
})
|
|
235
513
|
})
|
|
236
514
|
})
|