@nijaru/pi-provider-compaction 0.0.11 → 0.0.13
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 +5 -6
- package/cached-summary.ts +1 -2
- package/dist/index.js +3 -7
- package/index.ts +2 -4
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-provider-compaction
|
|
2
2
|
|
|
3
|
-
Experimental cache-aware compaction for [Pi](https://github.com/earendil-works/pi), with portable summaries and optional provider-native checkpoints. Minimum runtime: Pi 0.87.0. Development checks target Pi 1.
|
|
3
|
+
Experimental cache-aware compaction for [Pi](https://github.com/earendil-works/pi), with portable summaries and optional provider-native checkpoints. Minimum runtime: Pi 0.87.0. Development checks target Pi 1.5.0.
|
|
4
4
|
|
|
5
5
|
**Experimental; qualified backend probes are not production validation.** Real runner/adapter fixtures and live DeepSeek, MiMo and Codex probes pass, with the preservation and cache-hit qualifications below. Broader backend and deployment validation remains incomplete.
|
|
6
6
|
|
|
@@ -39,15 +39,14 @@ The summary request extends the captured logical prompt, rather than serializing
|
|
|
39
39
|
```json
|
|
40
40
|
{
|
|
41
41
|
"providerCompaction": {
|
|
42
|
-
"cachedSummaries": true
|
|
43
|
-
"outcomeNotifications": false
|
|
42
|
+
"cachedSummaries": true
|
|
44
43
|
}
|
|
45
44
|
}
|
|
46
45
|
```
|
|
47
46
|
|
|
48
47
|
These are the defaults on hosts exposing Pi's `getSettings` API; older hosts retain native compaction with cached summaries disabled. Optional `cacheRetention` accepts `"short"`, `"long"`, or `"none"`; omission inherits Pi/provider behavior. Retention is applied per request, not through process-wide environment changes. A longer tier can increase cache-write charges and only works where the adapter/backend supports it.
|
|
49
48
|
|
|
50
|
-
A cache hit is best effort, not a promised saving.
|
|
49
|
+
A cache hit is best effort, not a promised saving. Expiry estimates use the model's declared `promptCache` lifetime for the active tier, measured conservatively from request dispatch. Missing lifetimes remain unknown: DeepSeek, for example, [documents best-effort eviction over hours to days](https://api-docs.deepseek.com/guides/kv_cache/), not a predictable expiry. Pi's successful warmer requests refresh the observed cycle and estimated expiry.
|
|
51
50
|
|
|
52
51
|
With [pi-compactor's opt-in idle expiry policy](https://github.com/nijaru/pi-compactor#cache-aware-idle-compaction), compaction can run before an estimated expiry. Both extensions share a live ownership/input guard, checked before paid summary and native-checkpoint stages regardless of extension order. A failed or revoked cached-only attempt cancels instead of starting an ordinary summary. It does not start a continuation.
|
|
53
52
|
|
|
@@ -77,7 +76,7 @@ Legacy v1/v2 whole-history checkpoints remain inspectable but are never reinterp
|
|
|
77
76
|
|
|
78
77
|
## Diagnose portable fallback
|
|
79
78
|
|
|
80
|
-
Committed compaction entries retain the selected path, elapsed time, reported usage, and estimated cost.
|
|
79
|
+
Committed compaction entries retain the selected path, elapsed time, reported usage, and estimated cost. Portable fallback appends a `pi-provider-compaction:diagnostic` session entry with a fixed reason. Cancelled attempts with reported usage retain that usage in the same diagnostic record; without a saved checkpoint, Pi's extension API cannot include it in compaction totals. Diagnostics stay out of model context and never include request content, credentials, or backend error text.
|
|
81
80
|
|
|
82
81
|
These records describe attempts made while the extension was loaded; they cannot explain earlier compactions retroactively. A committed native checkpoint does not by itself prove that a later request replayed it.
|
|
83
82
|
|
|
@@ -102,7 +101,7 @@ pi --extension ./dist/index.js
|
|
|
102
101
|
|
|
103
102
|
The manifest loads the built `dist/index.js`, including a bundled stateless Responses serializer validator; source or host dependency changes require a rebuild. `bun run check` rebuilds first and tests the artifact against Pi's actual host serializer, including function-call history replayed as grammar tools. Keep global installation disabled while testing. Publishing and global activation are separate approval steps.
|
|
104
103
|
|
|
105
|
-
Tests include real Pi 1.
|
|
104
|
+
Tests include real Pi 1.5.0 runner/adapter fixtures for all three protocols, repeated prefix compaction, transformed suffix preservation, unchanged fallback, grammar tools, strict defaults, tool IDs, images, prompt updates, cross-model thinking and mid-conversation effort changes. Set `PI_COMPACTOR_FIXTURE_PATH` to a `pi-compactor` entrypoint to check SDK flag precedence and real idle-expiry transactions in both extension orders. Cached-summary fixtures also exercise built-in DeepSeek/Xiaomi dispatch, Anthropic Messages, late tool output, truncated-output fallback and measured usage. The optional installed-fast-mode fixture (`PI_FAST_MODE_FIXTURE_PATH`) checks actual `pi-fast-mode` composition across `/reload`; it creates an isolated fast-mode configuration. `PI_PEAK_HOURS_FIXTURE_PATH` adds request-wide pricing checks in both extension orders, including warming, branch summaries, mixed-API dispatch and composition with `/fast`.
|
|
106
105
|
|
|
107
106
|
Authenticated cached-summary smoke tests use synthetic history, including a value beyond the ordinary tool-result excerpt, and check continuation:
|
|
108
107
|
|
package/cached-summary.ts
CHANGED
|
@@ -17,13 +17,12 @@ export interface CacheState {
|
|
|
17
17
|
cachedTokens: number;
|
|
18
18
|
inputTokens: number;
|
|
19
19
|
}
|
|
20
|
-
export interface CacheConfig { cachedSummaries: boolean; cacheRetention?: CacheRetention
|
|
20
|
+
export interface CacheConfig { cachedSummaries: boolean; cacheRetention?: CacheRetention }
|
|
21
21
|
export function cacheConfig(settings: unknown): CacheConfig {
|
|
22
22
|
const raw = isObject(settings) && isObject(settings.providerCompaction) ? settings.providerCompaction : {};
|
|
23
23
|
return {
|
|
24
24
|
cachedSummaries: raw.cachedSummaries !== false,
|
|
25
25
|
cacheRetention: raw.cacheRetention === "long" || raw.cacheRetention === "short" || raw.cacheRetention === "none" ? raw.cacheRetention : undefined,
|
|
26
|
-
outcomeNotifications: raw.outcomeNotifications === true,
|
|
27
26
|
};
|
|
28
27
|
}
|
|
29
28
|
export function supportsCachedSummary(model: Model<any> | undefined): boolean {
|
package/dist/index.js
CHANGED
|
@@ -1591,8 +1591,7 @@ function cacheConfig(settings) {
|
|
|
1591
1591
|
const raw = isObject(settings) && isObject(settings.providerCompaction) ? settings.providerCompaction : {};
|
|
1592
1592
|
return {
|
|
1593
1593
|
cachedSummaries: raw.cachedSummaries !== false,
|
|
1594
|
-
cacheRetention: raw.cacheRetention === "long" || raw.cacheRetention === "short" || raw.cacheRetention === "none" ? raw.cacheRetention : undefined
|
|
1595
|
-
outcomeNotifications: raw.outcomeNotifications === true
|
|
1594
|
+
cacheRetention: raw.cacheRetention === "long" || raw.cacheRetention === "short" || raw.cacheRetention === "none" ? raw.cacheRetention : undefined
|
|
1596
1595
|
};
|
|
1597
1596
|
}
|
|
1598
1597
|
function supportsCachedSummary(model) {
|
|
@@ -2246,6 +2245,7 @@ function combineUsage(first, second) {
|
|
|
2246
2245
|
result[key] = (first[key] ?? 0) + (second[key] ?? 0);
|
|
2247
2246
|
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"])
|
|
2248
2247
|
result.cost[key] += second.cost[key];
|
|
2248
|
+
result.cost.totalFromProvider = first.cost.totalFromProvider === true && second.cost.totalFromProvider === true;
|
|
2249
2249
|
return result;
|
|
2250
2250
|
}
|
|
2251
2251
|
function supportedRouteOptions(model, options) {
|
|
@@ -2257,7 +2257,7 @@ function supportedRouteOptions(model, options) {
|
|
|
2257
2257
|
return !["AZURE_OPENAI_BASE_URL", "AZURE_OPENAI_RESOURCE_NAME", "AZURE_OPENAI_API_VERSION", "AZURE_OPENAI_DEPLOYMENT_NAME_MAP"].some((key) => process.env[key] !== undefined && options.env?.[key] === undefined);
|
|
2258
2258
|
}
|
|
2259
2259
|
function pi_provider_compaction_default(pi) {
|
|
2260
|
-
const configuration = () => typeof pi.getSettings === "function" ? cacheConfig(pi.getSettings()) : { cachedSummaries: false,
|
|
2260
|
+
const configuration = () => typeof pi.getSettings === "function" ? cacheConfig(pi.getSettings()) : { cachedSummaries: false, cacheRetention: undefined };
|
|
2261
2261
|
const secret = randomBytes(32);
|
|
2262
2262
|
const routeDigest = (value) => createHmac("sha256", secret).update(JSON.stringify(value)).digest("hex");
|
|
2263
2263
|
let runtimeId = randomUUID2();
|
|
@@ -2705,10 +2705,6 @@ function pi_provider_compaction_default(pi) {
|
|
|
2705
2705
|
const owned = isObject(recorded) && equal(recorded, lastOutcome);
|
|
2706
2706
|
if (!owned && !fallbackReason && readNativeCompactionDetails(event.compactionEntry.details)?.version !== 3)
|
|
2707
2707
|
fallback("another handler supplied the committed result");
|
|
2708
|
-
if (owned) {
|
|
2709
|
-
if (configuration().outcomeNotifications && lastOutcome)
|
|
2710
|
-
context.ui.notify(`Compacted via ${lastOutcome.path} in ${(lastOutcome.elapsedMs / 1000).toFixed(1)}s${lastOutcome.usage ? `; cache read ${lastOutcome.usage.cacheRead}, write ${lastOutcome.usage.cacheWrite}; estimated cost $${lastOutcome.usage.cost.total.toFixed(6)}` : ""}`, "info");
|
|
2711
|
-
}
|
|
2712
2708
|
if (fallbackReason)
|
|
2713
2709
|
pi.appendEntry("pi-provider-compaction:diagnostic", { status: `portable: ${fallbackReason}` });
|
|
2714
2710
|
active = undefined;
|
package/index.ts
CHANGED
|
@@ -112,6 +112,7 @@ function combineUsage(first: Usage | undefined, second: Usage | undefined): Usag
|
|
|
112
112
|
for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"] as const) result[key] += second[key];
|
|
113
113
|
for (const key of ["reasoning", "cacheWrite1h"] as const) if (first[key] !== undefined || second[key] !== undefined) result[key] = (first[key] ?? 0) + (second[key] ?? 0);
|
|
114
114
|
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) result.cost[key] += second.cost[key];
|
|
115
|
+
result.cost.totalFromProvider = first.cost.totalFromProvider === true && second.cost.totalFromProvider === true;
|
|
115
116
|
return result;
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -157,7 +158,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
157
158
|
// hook/privacy policy, even if the visible summary text happens to match.
|
|
158
159
|
// Older hosts in the native adapter's supported range lack getSettings.
|
|
159
160
|
// Keep their existing native path; cache preferences require that host API.
|
|
160
|
-
const configuration = () => typeof pi.getSettings === "function" ? cacheConfig(pi.getSettings()) : { cachedSummaries: false,
|
|
161
|
+
const configuration = () => typeof pi.getSettings === "function" ? cacheConfig(pi.getSettings()) : { cachedSummaries: false, cacheRetention: undefined };
|
|
161
162
|
const secret = randomBytes(32);
|
|
162
163
|
const routeDigest = (value: unknown) => createHmac("sha256", secret).update(JSON.stringify(value)).digest("hex");
|
|
163
164
|
let runtimeId = randomUUID();
|
|
@@ -493,9 +494,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
493
494
|
const recorded = isObject(event.compactionEntry.details) ? event.compactionEntry.details.providerCompaction : undefined;
|
|
494
495
|
const owned = isObject(recorded) && equal(recorded, lastOutcome);
|
|
495
496
|
if (!owned && !fallbackReason && readNativeCompactionDetails(event.compactionEntry.details)?.version !== 3) fallback("another handler supplied the committed result");
|
|
496
|
-
if (owned) {
|
|
497
|
-
if (configuration().outcomeNotifications && lastOutcome) context.ui.notify(`Compacted via ${lastOutcome.path} in ${(lastOutcome.elapsedMs / 1000).toFixed(1)}s${lastOutcome.usage ? `; cache read ${lastOutcome.usage.cacheRead}, write ${lastOutcome.usage.cacheWrite}; estimated cost $${lastOutcome.usage.cost.total.toFixed(6)}` : ""}`, "info");
|
|
498
|
-
}
|
|
499
497
|
// Only static diagnostics are persisted, never request/auth data or error text.
|
|
500
498
|
if (fallbackReason) pi.appendEntry("pi-provider-compaction:diagnostic", { status: `portable: ${fallbackReason}` });
|
|
501
499
|
active = undefined; snapshot = undefined; cache = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nijaru/pi-provider-compaction",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Provider-native context compaction adapters for Pi",
|
|
6
6
|
"keywords": [
|
|
@@ -46,11 +46,12 @@
|
|
|
46
46
|
"LICENSE"
|
|
47
47
|
],
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@earendil-works/pi-ai": "1.
|
|
50
|
-
"@earendil-works/pi-agent-core": "1.
|
|
51
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
49
|
+
"@earendil-works/pi-ai": "1.5.0",
|
|
50
|
+
"@earendil-works/pi-agent-core": "1.5.0",
|
|
51
|
+
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
52
52
|
"@types/bun": "latest",
|
|
53
53
|
"oxlint": "^1.81.0",
|
|
54
|
+
"typebox": "^1.3.0",
|
|
54
55
|
"typescript": "^6.0.3"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|