@diegopetrucci/pi-context-cap 0.1.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 (3) hide show
  1. package/README.md +60 -0
  2. package/index.ts +179 -0
  3. package/package.json +28 -0
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # context-cap
2
+
3
+ A pi extension that treats large-context models as having an effective 200k-token context window, so pi's built-in auto-compaction starts earlier.
4
+
5
+ By default, pi auto-compacts when:
6
+
7
+ ```text
8
+ contextTokens > model.contextWindow - reserveTokens
9
+ ```
10
+
11
+ This extension changes the active model's in-memory `contextWindow` to:
12
+
13
+ ```text
14
+ min(originalContextWindow, 200000)
15
+ ```
16
+
17
+ With pi's default `reserveTokens` of 16,384, models larger than 200k will proactively compact around 183,616 tokens.
18
+
19
+ ## Commands
20
+
21
+ ```text
22
+ /context-cap status
23
+ /context-cap off
24
+ /context-cap on
25
+ /context-cap toggle
26
+ ```
27
+
28
+ The extension starts enabled by default. Disabling is temporary for the current extension runtime/session; after `/reload`, `/new`, `/resume`, or `/fork`, the extension starts enabled again.
29
+
30
+ ## Install
31
+
32
+ ### Standalone npm package
33
+
34
+ ```bash
35
+ pi install npm:@diegopetrucci/pi-context-cap
36
+ ```
37
+
38
+ ### Collection package
39
+
40
+ ```bash
41
+ pi install npm:@diegopetrucci/pi-extensions
42
+ ```
43
+
44
+ ### GitHub package
45
+
46
+ ```bash
47
+ pi install git:github.com/diegopetrucci/pi-extensions
48
+ ```
49
+
50
+ Then reload pi:
51
+
52
+ ```text
53
+ /reload
54
+ ```
55
+
56
+ ## Notes
57
+
58
+ - This extension mutates pi's in-memory model metadata only. It does not edit `models.json`.
59
+ - The cap affects pi logic that reads `model.contextWindow`, including auto-compaction thresholding and UI context-window display.
60
+ - Because pi also uses `model.contextWindow` for some overflow detection, a request that succeeds above 200k tokens on a larger model may be treated as overflow and retried after compaction. Use `/context-cap off` if you need the full model window temporarily.
package/index.ts ADDED
@@ -0,0 +1,179 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { Model, Api } from "@earendil-works/pi-ai";
3
+
4
+ const DEFAULT_MAX_CONTEXT_WINDOW = 200_000;
5
+ type AnyModel = Model<Api> | Model<any>;
6
+
7
+ const originalContextWindows = new WeakMap<AnyModel, number>();
8
+ const touchedModels = new Set<AnyModel>();
9
+
10
+ type ApplyResult = {
11
+ changed: boolean;
12
+ key: string;
13
+ original: number;
14
+ effective: number;
15
+ };
16
+
17
+ function modelKey(model: AnyModel): string {
18
+ return `${model.provider}/${model.id}`;
19
+ }
20
+
21
+ function getOriginalContextWindow(model: AnyModel): number {
22
+ const existing = originalContextWindows.get(model);
23
+ if (typeof existing === "number") return existing;
24
+
25
+ originalContextWindows.set(model, model.contextWindow);
26
+ touchedModels.add(model);
27
+ return model.contextWindow;
28
+ }
29
+
30
+ function getEffectiveContextWindow(model: AnyModel): number {
31
+ return Math.min(getOriginalContextWindow(model), DEFAULT_MAX_CONTEXT_WINDOW);
32
+ }
33
+
34
+ function applyContextCap(model: AnyModel | undefined): ApplyResult | undefined {
35
+ if (!model) return undefined;
36
+
37
+ const key = modelKey(model);
38
+ const original = getOriginalContextWindow(model);
39
+ const effective = Math.min(original, DEFAULT_MAX_CONTEXT_WINDOW);
40
+ const changed = model.contextWindow !== effective;
41
+ model.contextWindow = effective;
42
+
43
+ return { changed, key, original, effective };
44
+ }
45
+
46
+ function restoreContextWindow(model: AnyModel | undefined): boolean {
47
+ if (!model) return false;
48
+
49
+ const original = originalContextWindows.get(model);
50
+ if (typeof original !== "number" || model.contextWindow === original) return false;
51
+
52
+ model.contextWindow = original;
53
+ return true;
54
+ }
55
+
56
+ function forEachRegistryModel(ctx: ExtensionContext, callback: (model: AnyModel) => void): void {
57
+ try {
58
+ for (const model of ctx.modelRegistry.getAll()) {
59
+ callback(model);
60
+ }
61
+ } catch {
62
+ // Best effort only. The active ctx.model is handled separately by callers.
63
+ }
64
+ }
65
+
66
+ function applyContextCapToSession(ctx: ExtensionContext): number {
67
+ let changed = 0;
68
+
69
+ forEachRegistryModel(ctx, (model) => {
70
+ if (applyContextCap(model)?.changed) changed++;
71
+ });
72
+
73
+ if (applyContextCap(ctx.model)?.changed) changed++;
74
+ return changed;
75
+ }
76
+
77
+ function restoreContextCapForSession(ctx: ExtensionContext): number {
78
+ let changed = 0;
79
+
80
+ forEachRegistryModel(ctx, (model) => {
81
+ if (restoreContextWindow(model)) changed++;
82
+ });
83
+
84
+ if (restoreContextWindow(ctx.model)) changed++;
85
+ return changed;
86
+ }
87
+
88
+ function formatTokens(tokens: number): string {
89
+ return tokens >= 1000 ? `${Math.round(tokens / 1000)}k` : String(tokens);
90
+ }
91
+
92
+ export default function contextCapExtension(pi: ExtensionAPI) {
93
+ let enabled = true;
94
+
95
+ pi.on("session_start", async (_event, ctx) => {
96
+ if (enabled) applyContextCapToSession(ctx);
97
+ });
98
+
99
+ pi.on("model_select", async (event, ctx) => {
100
+ if (!enabled) return;
101
+
102
+ const result = applyContextCap(event.model);
103
+ if (!result || !ctx.hasUI) return;
104
+
105
+ ctx.ui.setStatus(
106
+ "context-cap",
107
+ result.original > DEFAULT_MAX_CONTEXT_WINDOW
108
+ ? `ctx cap ${formatTokens(result.effective)}/${formatTokens(result.original)}`
109
+ : undefined,
110
+ );
111
+ });
112
+
113
+ pi.on("session_shutdown", async (_event, ctx) => {
114
+ restoreContextCapForSession(ctx);
115
+ for (const model of touchedModels) restoreContextWindow(model);
116
+ });
117
+
118
+ pi.registerCommand("context-cap", {
119
+ description: "Toggle the 200k effective context-window cap for auto-compaction",
120
+ getArgumentCompletions: (prefix) => {
121
+ const commands = ["on", "off", "toggle", "status"];
122
+ const matches = commands.filter((command) => command.startsWith(prefix.trim()));
123
+ return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
124
+ },
125
+ handler: async (args, ctx) => {
126
+ const action = args.trim().toLowerCase() || "toggle";
127
+
128
+ if (action === "on" || action === "enable") {
129
+ enabled = true;
130
+ const changed = applyContextCapToSession(ctx);
131
+ ctx.ui.setStatus("context-cap", `ctx cap ${formatTokens(DEFAULT_MAX_CONTEXT_WINDOW)}`);
132
+ ctx.ui.notify(`Context cap enabled (${changed} model window(s) capped/restored).`, "info");
133
+ return;
134
+ }
135
+
136
+ if (action === "off" || action === "disable") {
137
+ enabled = false;
138
+ const changed = restoreContextCapForSession(ctx);
139
+ ctx.ui.setStatus("context-cap", undefined);
140
+ ctx.ui.notify(`Context cap disabled for this extension session (${changed} model window(s) restored).`, "info");
141
+ return;
142
+ }
143
+
144
+ if (action === "toggle") {
145
+ if (enabled) {
146
+ enabled = false;
147
+ const changed = restoreContextCapForSession(ctx);
148
+ ctx.ui.setStatus("context-cap", undefined);
149
+ ctx.ui.notify(`Context cap disabled for this extension session (${changed} model window(s) restored).`, "info");
150
+ } else {
151
+ enabled = true;
152
+ const changed = applyContextCapToSession(ctx);
153
+ ctx.ui.setStatus("context-cap", `ctx cap ${formatTokens(DEFAULT_MAX_CONTEXT_WINDOW)}`);
154
+ ctx.ui.notify(`Context cap enabled (${changed} model window(s) capped/restored).`, "info");
155
+ }
156
+ return;
157
+ }
158
+
159
+ if (action === "status") {
160
+ const model = ctx.model;
161
+ const status = enabled ? "enabled" : "disabled";
162
+ if (!model) {
163
+ ctx.ui.notify(`Context cap is ${status}. No model selected.`, "info");
164
+ return;
165
+ }
166
+
167
+ const original = getOriginalContextWindow(model);
168
+ const effective = enabled ? getEffectiveContextWindow(model) : model.contextWindow;
169
+ ctx.ui.notify(
170
+ `Context cap is ${status}. Current model: ${modelKey(model)} (${formatTokens(effective)}/${formatTokens(original)} effective/original).`,
171
+ "info",
172
+ );
173
+ return;
174
+ }
175
+
176
+ ctx.ui.notify("Usage: /context-cap on | off | toggle | status", "warning");
177
+ },
178
+ });
179
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@diegopetrucci/pi-context-cap",
3
+ "version": "0.1.0",
4
+ "description": "A pi extension that caps effective model context windows at 200k tokens for earlier auto-compaction.",
5
+ "keywords": ["pi-package", "pi", "context", "compaction"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/diegopetrucci/pi-extensions.git",
10
+ "directory": "extensions/context-cap"
11
+ },
12
+ "files": [
13
+ "index.ts",
14
+ "README.md"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "pi": {
20
+ "extensions": [
21
+ "index.ts"
22
+ ]
23
+ },
24
+ "peerDependencies": {
25
+ "@earendil-works/pi-ai": "*",
26
+ "@earendil-works/pi-coding-agent": "*"
27
+ }
28
+ }