pi-l1-cache 1.2.1 → 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 +131 -104
- package/fix-l1-cache.cjs +405 -0
- package/package.json +18 -7
- package/src/index.test.ts +462 -84
- package/src/index.ts +269 -214
package/fix-l1-cache.cjs
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// fix-l1-cache.js
|
|
2
|
+
// Hotfix: L1 response-cache support for pi (@earendil-works/pi-coding-agent).
|
|
3
|
+
//
|
|
4
|
+
// Adds two capabilities the stock extension API lacks:
|
|
5
|
+
// 1. REPLAY — an extension may serve a cached response by returning params
|
|
6
|
+
// containing `__piL1Replay: [chunks]` from before_provider_request;
|
|
7
|
+
// pi-ai then feeds the cached chunks through the normal consume
|
|
8
|
+
// path and never contacts the provider.
|
|
9
|
+
// 2. CAPTURE — after a successful completion, pi-ai calls options.onStreamComplete
|
|
10
|
+
// (allChunks, requestParams); the sdk forwards them to extensions as a
|
|
11
|
+
// `provider_stream_complete` event so l1-cache can store the response.
|
|
12
|
+
//
|
|
13
|
+
// Layout-aware (pi >= 0.84 runs from the esbuild bundle at dist/bundle/cli.js —
|
|
14
|
+
// unbundled sources are inert, so this script patches the bundles):
|
|
15
|
+
// BUNDLE (pi >= 0.84):
|
|
16
|
+
// dist/bundle/chunks/openai-completions-*.js (5 sites)
|
|
17
|
+
// dist/bundle/chunks/chunk-*.js (1 site: forward onStreamComplete)
|
|
18
|
+
// dist/bundle/chunks/chunk-*.js (1 site: provider_stream_complete event)
|
|
19
|
+
// PRE-BUNDLE (pi < 0.84):
|
|
20
|
+
// node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js (5 sites)
|
|
21
|
+
// node_modules/@earendil-works/pi-ai/dist/api/simple-options.js (1 site)
|
|
22
|
+
// dist/core/sdk.js (1 site)
|
|
23
|
+
//
|
|
24
|
+
// Usage: node fix-l1-cache.js
|
|
25
|
+
// Idempotent: exits 0 ("already patched") when all sites are patched.
|
|
26
|
+
// Bundle-era site misses print a warning but do NOT abort (L1 cache degrades
|
|
27
|
+
// to pass-through; never block a pi install for an optional cache).
|
|
28
|
+
//
|
|
29
|
+
// NOTE: pre-bundle sites must run AFTER fix-reasoning-content.js in the
|
|
30
|
+
// postinstall chain (site-2 oldText assumes the reasoning_content hotfix
|
|
31
|
+
// try/catch). Bundle site 2's anchor assumes the reasoning bundle patch
|
|
32
|
+
// (the async IIFE) from fix-reasoning-content.js v1.4 (bundle-aware).
|
|
33
|
+
|
|
34
|
+
const fs = require("fs")
|
|
35
|
+
const path = require("path")
|
|
36
|
+
|
|
37
|
+
const PACKAGE_ROOT =
|
|
38
|
+
process.env.PI_PACKAGE_ROOT ||
|
|
39
|
+
path.join(__dirname, "node_modules/@earendil-works/pi-coding-agent")
|
|
40
|
+
|
|
41
|
+
// --------------------------------------------------------------------------
|
|
42
|
+
// File resolution
|
|
43
|
+
// --------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
// Bundle chunks live under the pi-coding-agent package (npm: nested; bun: a
|
|
46
|
+
// flat sibling of pi-ai next to it). Collect candidates primary-first.
|
|
47
|
+
function packageCandidates() {
|
|
48
|
+
const dirs = []
|
|
49
|
+
if (fs.existsSync(path.join(PACKAGE_ROOT, "dist"))) dirs.push(PACKAGE_ROOT)
|
|
50
|
+
const sibling =
|
|
51
|
+
path.basename(PACKAGE_ROOT) === "pi-ai"
|
|
52
|
+
? path.join(path.dirname(PACKAGE_ROOT), "pi-coding-agent")
|
|
53
|
+
: null
|
|
54
|
+
if (sibling && fs.existsSync(path.join(sibling, "dist"))) dirs.push(sibling)
|
|
55
|
+
return dirs
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function bundleChunksDirs(candidates) {
|
|
59
|
+
const out = []
|
|
60
|
+
for (const p of candidates) {
|
|
61
|
+
const d = path.join(p, "dist", "bundle", "chunks")
|
|
62
|
+
if (fs.existsSync(d)) out.push(d)
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isBundleEra(candidates) {
|
|
68
|
+
return candidates.length > 0 && bundleChunksDirs(candidates).length > 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const OPENAI_COMPLETIONS = path.join(
|
|
72
|
+
PACKAGE_ROOT,
|
|
73
|
+
"node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js"
|
|
74
|
+
)
|
|
75
|
+
const SIMPLE_OPTIONS = path.join(
|
|
76
|
+
PACKAGE_ROOT,
|
|
77
|
+
"node_modules/@earendil-works/pi-ai/dist/api/simple-options.js"
|
|
78
|
+
)
|
|
79
|
+
const SDK = path.join(PACKAGE_ROOT, "dist/core/sdk.js")
|
|
80
|
+
|
|
81
|
+
// --------------------------------------------------------------------------
|
|
82
|
+
// Bundle phase (pi >= 0.84)
|
|
83
|
+
// --------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
// Minified exact anchors (verified against the 0.86.0 bundle; chunk-* hashes
|
|
86
|
+
// change per build, so files are located by anchor presence, not filename).
|
|
87
|
+
|
|
88
|
+
// openai-completions provider chunk — 5 sites.
|
|
89
|
+
// Site 1: read the __piL1Replay marker out of the payload (into a var in the
|
|
90
|
+
// enclosing scope) before the provider call path is set up.
|
|
91
|
+
const B1_OLD =
|
|
92
|
+
'nextParams=await options?.onPayload?.(params,model);nextParams!==void 0&&(params=nextParams);let requestOptions={...options?.signal?{signal:options.signal}:{},...options?.timeoutMs!==void 0?{timeout:options.timeoutMs}:{},maxRetries:0}'
|
|
93
|
+
const B1_NEW =
|
|
94
|
+
'nextParams=await options?.onPayload?.(params,model);nextParams!==void 0&&(params=nextParams);let __l1ReplayChunks;if(params&&typeof params==="object"&&Array.isArray(params.__piL1Replay)){__l1ReplayChunks=params.__piL1Replay;delete params.__piL1Replay}let requestOptions={...options?.signal?{signal:options.signal}:{},...options?.timeoutMs!==void 0?{timeout:options.timeoutMs}:{},maxRetries:0}'
|
|
95
|
+
const B1_MARKER = "let __l1ReplayChunks;"
|
|
96
|
+
|
|
97
|
+
// Site 2: skip the provider call on replay. Anchor assumes the fix-reasoning-
|
|
98
|
+
// content.js async IIFE (bundle-aware v1.4) already wrapped the create call.
|
|
99
|
+
const B2_OLD =
|
|
100
|
+
'{data:openaiStream,response}=await(async()=>{try{return await retryProviderRequest(()=>client.chat.completions.create(params,requestOptions).withResponse(),{maxRetries:options?.maxRetries,maxRetryDelayMs:options?.maxRetryDelayMs,signal:options?.signal})}catch(e){'
|
|
101
|
+
const B2_NEW =
|
|
102
|
+
'{data:openaiStream,response}=await(async()=>{if(__l1ReplayChunks!==void 0)return{data:__l1ReplayChunks,response:{status:200,headers:new Headers()}};try{return await retryProviderRequest(()=>client.chat.completions.create(params,requestOptions).withResponse(),{maxRetries:options?.maxRetries,maxRetryDelayMs:options?.maxRetryDelayMs,signal:options?.signal})}catch(e){'
|
|
103
|
+
const B2_MARKER = "if(__l1ReplayChunks!==void 0)return{data:__l1ReplayChunks"
|
|
104
|
+
|
|
105
|
+
// Site 3: collect raw chunks in the consume loop scope.
|
|
106
|
+
const B3_OLD = 'stream2.push({type:"start",partial:output});'
|
|
107
|
+
const B3_NEW = 'stream2.push({type:"start",partial:output});const allChunks=[];'
|
|
108
|
+
const B3_MARKER = "const allChunks=[];"
|
|
109
|
+
|
|
110
|
+
// Site 4: append each streamed chunk.
|
|
111
|
+
const B4_OLD =
|
|
112
|
+
'for await(let chunk of openaiStream){if(!chunk||typeof chunk!="object")continue;'
|
|
113
|
+
const B4_NEW =
|
|
114
|
+
'for await(let chunk of openaiStream){if(!chunk||typeof chunk!="object")continue;allChunks.push(chunk);'
|
|
115
|
+
const B4_MARKER = "allChunks.push(chunk);"
|
|
116
|
+
|
|
117
|
+
// Site 5: fire capture right before the done event.
|
|
118
|
+
const B5_OLD =
|
|
119
|
+
'stream2.push({type:"done",reason:output.stopReason,message:output}),stream2.end()'
|
|
120
|
+
const B5_NEW =
|
|
121
|
+
'if(allChunks.length>0&&options?.onStreamComplete){try{await options.onStreamComplete(allChunks,params)}catch(_l1err){}}stream2.push({type:"done",reason:output.stopReason,message:output}),stream2.end()'
|
|
122
|
+
const B5_MARKER = "options.onStreamComplete(allChunks,params)"
|
|
123
|
+
|
|
124
|
+
// Site 6: forward onStreamComplete through the shared simple-options builder.
|
|
125
|
+
const B6_OLD =
|
|
126
|
+
"onPayload:options?.onPayload,onResponse:options?.onResponse,timeoutMs:options?.timeoutMs"
|
|
127
|
+
const B6_NEW =
|
|
128
|
+
"onPayload:options?.onPayload,onResponse:options?.onResponse,onStreamComplete:options?.onStreamComplete,timeoutMs:options?.timeoutMs"
|
|
129
|
+
const B6_MARKER = "onStreamComplete:options?.onStreamComplete"
|
|
130
|
+
|
|
131
|
+
// Site 7: sdk chunk — emit provider_stream_complete to extensions after a
|
|
132
|
+
// successful completion. Sits inside the options object next to transformHeaders.
|
|
133
|
+
const B7_OLD =
|
|
134
|
+
'transformHeaders:async requestHeaders=>{let headers=mergeProviderAttributionHeaders(requestModel,settingsManager,options2.sessionId,requestHeaders);return headerRunner?.hasHandlers("before_provider_headers")?headerRunner.emitBeforeProviderHeaders(headers??{}):headers??{}}}'
|
|
135
|
+
const B7_NEW =
|
|
136
|
+
'transformHeaders:async requestHeaders=>{let headers=mergeProviderAttributionHeaders(requestModel,settingsManager,options2.sessionId,requestHeaders);return headerRunner?.hasHandlers("before_provider_headers")?headerRunner.emitBeforeProviderHeaders(headers??{}):headers??{}},onStreamComplete:async(chunks,requestParams)=>{let runner=extensionRunnerRef?.current;if(runner?.hasHandlers("provider_stream_complete")){await runner.emit({type:"provider_stream_complete",payload:requestParams,chunks})}}}'
|
|
137
|
+
const B7_MARKER = 'type:"provider_stream_complete"'
|
|
138
|
+
|
|
139
|
+
const BUNDLE_PROVIDER_SITES = [
|
|
140
|
+
{ name: "replay-marker-read", marker: B1_MARKER, old: B1_OLD, next: B1_NEW },
|
|
141
|
+
{ name: "replay-skip-provider-call", marker: B2_MARKER, old: B2_OLD, next: B2_NEW },
|
|
142
|
+
{ name: "allChunks-decl", marker: B3_MARKER, old: B3_OLD, next: B3_NEW },
|
|
143
|
+
{ name: "allChunks-push", marker: B4_MARKER, old: B4_OLD, next: B4_NEW },
|
|
144
|
+
{ name: "onStreamComplete-call", marker: B5_MARKER, old: B5_OLD, next: B5_NEW },
|
|
145
|
+
]
|
|
146
|
+
const BUNDLE_FORWARD_SITES = [
|
|
147
|
+
{ name: "forward-onStreamComplete", marker: B6_MARKER, old: B6_OLD, next: B6_NEW },
|
|
148
|
+
]
|
|
149
|
+
const BUNDLE_SDK_SITES = [
|
|
150
|
+
{ name: "provider_stream_complete-event", marker: B7_MARKER, old: B7_OLD, next: B7_NEW },
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
function patchSitesInFile(file, rel, sites, fatal) {
|
|
154
|
+
let src = fs.readFileSync(file, "utf8")
|
|
155
|
+
let applied = 0
|
|
156
|
+
let already = 0
|
|
157
|
+
const missing = []
|
|
158
|
+
for (const { name, marker, old, next } of sites) {
|
|
159
|
+
if (src.includes(marker)) {
|
|
160
|
+
already++
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
if (!src.includes(old)) {
|
|
164
|
+
missing.push(name)
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
src = src.replace(old, next)
|
|
168
|
+
applied++
|
|
169
|
+
}
|
|
170
|
+
if (missing.length > 0) {
|
|
171
|
+
const msg = `site(s) ${missing.join(", ")} not found (layout changed?)`
|
|
172
|
+
if (fatal) {
|
|
173
|
+
console.error(`fix-l1-cache.js: ${rel}: ${msg}`)
|
|
174
|
+
process.exit(1)
|
|
175
|
+
}
|
|
176
|
+
console.error(`fix-l1-cache.js: ${rel}: ${msg} — skipped (L1 cache stays pass-through)`)
|
|
177
|
+
}
|
|
178
|
+
if (applied > 0 || missing.length > 0) fs.writeFileSync(file, src)
|
|
179
|
+
if (applied > 0 || already > 0)
|
|
180
|
+
console.log(`fix-l1-cache.js: ${rel}: ${applied} applied, ${already} already patched`)
|
|
181
|
+
return missing.length === 0
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function patchBundles(candidates) {
|
|
185
|
+
let anyHandled = false
|
|
186
|
+
for (const chunksDir of bundleChunksDirs(candidates)) {
|
|
187
|
+
const relDir = chunksDir.includes("node_modules")
|
|
188
|
+
? chunksDir.slice(chunksDir.indexOf("node_modules"))
|
|
189
|
+
: chunksDir
|
|
190
|
+
const files = fs.readdirSync(chunksDir).filter((f) => f.endsWith(".js"))
|
|
191
|
+
for (const name of files) {
|
|
192
|
+
const file = path.join(chunksDir, name)
|
|
193
|
+
const rel = name
|
|
194
|
+
// Chunks can be several MB (e.g. the sdk chunk) — scan the full text.
|
|
195
|
+
const srcHead = fs.readFileSync(file, "utf8")
|
|
196
|
+
let did = false
|
|
197
|
+
if (/^openai-completions-.*\.js$/.test(name)) {
|
|
198
|
+
// Provider chunk: the openai-completions stream() consumer. Identify by
|
|
199
|
+
// anchor presence so a re-run or a different build still finds it.
|
|
200
|
+
const isProvider = srcHead.includes(B2_OLD) || srcHead.includes(B2_MARKER)
|
|
201
|
+
if (isProvider) {
|
|
202
|
+
if (patchSitesInFile(file, rel, BUNDLE_PROVIDER_SITES, false)) anyHandled = true
|
|
203
|
+
did = true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (/^chunk-.*\.js$/.test(name)) {
|
|
207
|
+
if (srcHead.includes(B6_OLD) || srcHead.includes(B6_MARKER)) {
|
|
208
|
+
patchSitesInFile(file, rel, BUNDLE_FORWARD_SITES, false)
|
|
209
|
+
anyHandled = true
|
|
210
|
+
did = true
|
|
211
|
+
}
|
|
212
|
+
if (srcHead.includes(B7_OLD) || srcHead.includes(B7_MARKER)) {
|
|
213
|
+
patchSitesInFile(file, rel, BUNDLE_SDK_SITES, false)
|
|
214
|
+
anyHandled = true
|
|
215
|
+
did = true
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
void did
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return anyHandled
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// --------------------------------------------------------------------------
|
|
225
|
+
// Pre-bundle phase (pi < 0.84 — readable sources are the runtime)
|
|
226
|
+
// --------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
function fail(file, why) {
|
|
229
|
+
console.error(`fix-l1-cache.js: ${file}: ${why}`)
|
|
230
|
+
process.exit(1)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function patch(file, sites) {
|
|
234
|
+
if (!fs.existsSync(file)) fail(file, "file not found")
|
|
235
|
+
let src = fs.readFileSync(file, "utf8")
|
|
236
|
+
let applied = 0
|
|
237
|
+
let already = 0
|
|
238
|
+
for (const { name, marker, oldText, newText } of sites) {
|
|
239
|
+
if (src.includes(marker)) {
|
|
240
|
+
already++
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
if (!src.includes(oldText))
|
|
244
|
+
fail(file, `site "${name}" not found (layout changed upstream?)`)
|
|
245
|
+
src = src.replace(oldText, newText)
|
|
246
|
+
applied++
|
|
247
|
+
}
|
|
248
|
+
fs.writeFileSync(file, src)
|
|
249
|
+
console.log(
|
|
250
|
+
`fix-l1-cache.js: ${path.relative(PACKAGE_ROOT, file)}: ${applied} applied, ${already} already patched`
|
|
251
|
+
)
|
|
252
|
+
return applied + already
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function patchReadable() {
|
|
256
|
+
patch(OPENAI_COMPLETIONS, [
|
|
257
|
+
{
|
|
258
|
+
name: "replay-marker-read",
|
|
259
|
+
marker: "__piL1Replay",
|
|
260
|
+
oldText: ` const nextParams = await options?.onPayload?.(params, model);
|
|
261
|
+
if (nextParams !== undefined) {
|
|
262
|
+
params = nextParams;
|
|
263
|
+
}
|
|
264
|
+
const requestOptions = {`,
|
|
265
|
+
newText: ` const nextParams = await options?.onPayload?.(params, model);
|
|
266
|
+
if (nextParams !== undefined) {
|
|
267
|
+
params = nextParams;
|
|
268
|
+
}
|
|
269
|
+
// L1 cache replay: an extension may serve a cached response by
|
|
270
|
+
// returning params with a __piL1Replay array of OpenAI stream chunks.
|
|
271
|
+
// The cached chunks are fed through the normal consume path below,
|
|
272
|
+
// so parsing/usage/stop-reason handling stays identical.
|
|
273
|
+
let __l1ReplayChunks;
|
|
274
|
+
if (params && typeof params === "object" && Array.isArray(params.__piL1Replay)) {
|
|
275
|
+
__l1ReplayChunks = params.__piL1Replay;
|
|
276
|
+
delete params.__piL1Replay;
|
|
277
|
+
}
|
|
278
|
+
const requestOptions = {`,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: "replay-skip-provider-call",
|
|
282
|
+
marker: "else try {",
|
|
283
|
+
oldText: ` let openaiStream, response;
|
|
284
|
+
try {
|
|
285
|
+
({ data: openaiStream, response } = await retryProviderRequest(() => client.chat.completions.create(params, requestOptions).withResponse(), {
|
|
286
|
+
maxRetries: options?.maxRetries,
|
|
287
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
288
|
+
signal: options?.signal,
|
|
289
|
+
}));
|
|
290
|
+
} catch (firstError) {`,
|
|
291
|
+
newText: ` let openaiStream, response;
|
|
292
|
+
if (__l1ReplayChunks !== undefined) {
|
|
293
|
+
response = { status: 200, headers: new Headers() };
|
|
294
|
+
openaiStream = __l1ReplayChunks;
|
|
295
|
+
}
|
|
296
|
+
else try {
|
|
297
|
+
({ data: openaiStream, response } = await retryProviderRequest(() => client.chat.completions.create(params, requestOptions).withResponse(), {
|
|
298
|
+
maxRetries: options?.maxRetries,
|
|
299
|
+
maxRetryDelayMs: options?.maxRetryDelayMs,
|
|
300
|
+
signal: options?.signal,
|
|
301
|
+
}));
|
|
302
|
+
} catch (firstError) {`,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "allChunks-decl",
|
|
306
|
+
marker: "const allChunks = [];",
|
|
307
|
+
oldText: ` await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
|
308
|
+
stream.push({ type: "start", partial: output });`,
|
|
309
|
+
newText: ` await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
|
|
310
|
+
stream.push({ type: "start", partial: output });
|
|
311
|
+
const allChunks = [];`,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "allChunks-push",
|
|
315
|
+
marker: "allChunks.push(chunk);",
|
|
316
|
+
oldText: ` for await (const chunk of openaiStream) {
|
|
317
|
+
if (!chunk || typeof chunk !== "object")
|
|
318
|
+
continue;`,
|
|
319
|
+
newText: ` for await (const chunk of openaiStream) {
|
|
320
|
+
if (!chunk || typeof chunk !== "object")
|
|
321
|
+
continue;
|
|
322
|
+
allChunks.push(chunk);`,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "onStreamComplete-call",
|
|
326
|
+
marker: "options.onStreamComplete(allChunks, params)",
|
|
327
|
+
oldText: ` if ((compat.supportsFinishReason && !hasFinishReason) || output.stopReason === "pending") {
|
|
328
|
+
throw new Error("Stream ended without finish_reason");
|
|
329
|
+
}
|
|
330
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });`,
|
|
331
|
+
newText: ` if ((compat.supportsFinishReason && !hasFinishReason) || output.stopReason === "pending") {
|
|
332
|
+
throw new Error("Stream ended without finish_reason");
|
|
333
|
+
}
|
|
334
|
+
if (allChunks.length > 0 && options?.onStreamComplete) {
|
|
335
|
+
try {
|
|
336
|
+
await options.onStreamComplete(allChunks, params);
|
|
337
|
+
}
|
|
338
|
+
catch (_l1err) { }
|
|
339
|
+
}
|
|
340
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });`,
|
|
341
|
+
},
|
|
342
|
+
])
|
|
343
|
+
|
|
344
|
+
patch(SIMPLE_OPTIONS, [
|
|
345
|
+
{
|
|
346
|
+
name: "forward-onStreamComplete",
|
|
347
|
+
marker: "onStreamComplete: options?.onStreamComplete",
|
|
348
|
+
oldText: ` onPayload: options?.onPayload,
|
|
349
|
+
onResponse: options?.onResponse,`,
|
|
350
|
+
newText: ` onPayload: options?.onPayload,
|
|
351
|
+
onResponse: options?.onResponse,
|
|
352
|
+
onStreamComplete: options?.onStreamComplete,`,
|
|
353
|
+
},
|
|
354
|
+
])
|
|
355
|
+
|
|
356
|
+
patch(SDK, [
|
|
357
|
+
{
|
|
358
|
+
name: "provider_stream_complete-event",
|
|
359
|
+
marker: 'type: "provider_stream_complete"',
|
|
360
|
+
oldText: ` transformHeaders: async (requestHeaders) => {
|
|
361
|
+
const headers = mergeProviderAttributionHeaders(model, settingsManager, options?.sessionId, requestHeaders);
|
|
362
|
+
return headerRunner?.hasHandlers("before_provider_headers")
|
|
363
|
+
? headerRunner.emitBeforeProviderHeaders(headers ?? {})
|
|
364
|
+
: (headers ?? {});
|
|
365
|
+
},
|
|
366
|
+
});`,
|
|
367
|
+
newText: ` transformHeaders: async (requestHeaders) => {
|
|
368
|
+
const headers = mergeProviderAttributionHeaders(model, settingsManager, options?.sessionId, requestHeaders);
|
|
369
|
+
return headerRunner?.hasHandlers("before_provider_headers")
|
|
370
|
+
? headerRunner.emitBeforeProviderHeaders(headers ?? {})
|
|
371
|
+
: (headers ?? {});
|
|
372
|
+
},
|
|
373
|
+
onStreamComplete: async (chunks, requestParams) => {
|
|
374
|
+
const runner = extensionRunnerRef.current;
|
|
375
|
+
if (!runner?.hasHandlers("provider_stream_complete")) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
await runner.emit({
|
|
379
|
+
type: "provider_stream_complete",
|
|
380
|
+
payload: requestParams,
|
|
381
|
+
chunks,
|
|
382
|
+
});
|
|
383
|
+
},
|
|
384
|
+
});`,
|
|
385
|
+
},
|
|
386
|
+
])
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// --------------------------------------------------------------------------
|
|
390
|
+
// Main
|
|
391
|
+
// --------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
const candidates = packageCandidates()
|
|
394
|
+
if (isBundleEra(candidates)) {
|
|
395
|
+
const ok = patchBundles(candidates)
|
|
396
|
+
if (ok)
|
|
397
|
+
console.log("fix-l1-cache.js: bundle sites OK (replay + capture wired into the runtime bundle)")
|
|
398
|
+
else
|
|
399
|
+
console.error(
|
|
400
|
+
"fix-l1-cache.js: bundle era detected but no chunk matched the anchors — L1 cache stays pass-through."
|
|
401
|
+
)
|
|
402
|
+
} else {
|
|
403
|
+
patchReadable()
|
|
404
|
+
console.log("fix-l1-cache.js: all sites OK")
|
|
405
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-l1-cache",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "L1
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "L1 response cache for pi with replay, disk persistence, and working capture",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
7
7
|
"engines": {
|
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
],
|
|
21
21
|
"files": [
|
|
22
22
|
"src",
|
|
23
|
+
"fix-l1-cache.cjs",
|
|
23
24
|
"README.md",
|
|
24
25
|
"LICENSE"
|
|
25
26
|
],
|
|
26
|
-
"author": "Tobias Weiß <
|
|
27
|
+
"author": "Tobias Weiß <info@graphwiz.ai>",
|
|
27
28
|
"license": "MIT",
|
|
28
29
|
"homepage": "https://github.com/tobias-weiss-ai-xr/pi-l1-cache",
|
|
29
30
|
"repository": {
|
|
@@ -34,14 +35,24 @@
|
|
|
34
35
|
"url": "https://github.com/tobias-weiss-ai-xr/pi-l1-cache/issues"
|
|
35
36
|
},
|
|
36
37
|
"pi": {
|
|
37
|
-
"extensions": [
|
|
38
|
+
"extensions": [
|
|
39
|
+
"./src/index.ts"
|
|
40
|
+
],
|
|
38
41
|
"minPiVersion": "0.4.0"
|
|
39
42
|
},
|
|
40
43
|
"scripts": {
|
|
41
|
-
"test": "
|
|
42
|
-
"test:watch": "
|
|
44
|
+
"test": "tsx --test src/*.test.ts",
|
|
45
|
+
"test:watch": "tsx --watch --test src/*.test.ts",
|
|
46
|
+
"typecheck": "tsc --noEmit --skipLibCheck",
|
|
47
|
+
"pack": "npm pack --dry-run"
|
|
43
48
|
},
|
|
44
49
|
"devDependencies": {
|
|
45
|
-
"@
|
|
50
|
+
"@earendil-works/pi-coding-agent": "^0.84.0",
|
|
51
|
+
"@types/node": "^20.0.0",
|
|
52
|
+
"tsx": "^4.23.12",
|
|
53
|
+
"typescript": "^5.8.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
46
57
|
}
|
|
47
58
|
}
|