pi-plus 0.1.1 → 0.1.3
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 +55 -19
- package/package.json +4 -1
- package/src/index.ts +95 -9
- package/src/logger.ts +56 -0
- package/src/model-config.ts +55 -47
- package/src/percentages.ts +24 -17
- package/src/settings.ts +97 -0
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ at CC's auto-compact threshold.
|
|
|
12
12
|
|
|
13
13
|
| Behavior | CC source | Here |
|
|
14
14
|
|---|---|---|
|
|
15
|
-
| Context window resolution (
|
|
16
|
-
| Max output tokens per model + 8k slot-reservation cap
|
|
15
|
+
| Context window resolution (`[1m]` suffix → capability ≥100k → 200k default) | `utils/context.ts` | `src/model-config.ts` |
|
|
16
|
+
| Max output tokens per model + 8k slot-reservation cap | `utils/context.ts`, `services/api/claude.ts` | `src/model-config.ts` |
|
|
17
17
|
| Effective window = window − min(maxOutput, 20k) | `services/compact/autoCompact.ts` | `src/model-config.ts` |
|
|
18
18
|
| Auto-compact threshold = effective − 13,000 | `services/compact/autoCompact.ts` | `src/model-config.ts` |
|
|
19
19
|
| Context measurement: last API usage (input+cacheWrite+cacheRead+output) + chars/4 tail estimate; images/PDFs flat 2000 | `utils/tokens.ts`, `services/tokenEstimation.ts` | `src/usage.ts` |
|
|
@@ -21,6 +21,12 @@ at CC's auto-compact threshold.
|
|
|
21
21
|
| Warning/error/blocking thresholds (20k/20k/3k buffers) | `services/compact/autoCompact.ts` | `src/percentages.ts` |
|
|
22
22
|
| Indicator text: dim `N% until auto-compact`, `Context low (N% remaining) · Run /compact to compact & continue` | `components/TokenWarning.tsx` | `src/indicator.ts` |
|
|
23
23
|
|
|
24
|
+
This plugin supports **no Claude Code environment variables**
|
|
25
|
+
(`CLAUDE_CODE_*`, `DISABLE_COMPACT`, … are all ignored). All configuration is
|
|
26
|
+
done through the settings system described below; the only env vars honored
|
|
27
|
+
are the plugin's own kill-switches, `PI_PLUS_NO_AUTO_COMPACT` and
|
|
28
|
+
`PI_PLUS_NO_LOG`.
|
|
29
|
+
|
|
24
30
|
On a real session the plugin's token count matches pi's own `ctx.getContextUsage().tokens`
|
|
25
31
|
exactly for the Anthropic-style usage shape (pi's `cacheWrite` already includes the 1h
|
|
26
32
|
cache subset and `reasoning` ⊂ `output`, so the field-for-field sums coincide).
|
|
@@ -55,28 +61,58 @@ auto-compact and CC's autocompact use). The trigger is edge-triggered: it
|
|
|
55
61
|
fires once per crossing and re-arms after post-compact usage drops below the
|
|
56
62
|
threshold. Manual `/compact` keeps working.
|
|
57
63
|
|
|
58
|
-
Set `
|
|
64
|
+
Set `PI_PLUS_NO_AUTO_COMPACT=1` to disable the plugin's trigger (indicator only).
|
|
59
65
|
|
|
60
|
-
##
|
|
66
|
+
## Configuration
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
Settings are read through [pi-extension-settings](https://github.com/juanibiapina/pi-extension-settings)
|
|
69
|
+
(the pi ecosystem's standard settings helpers — `getSetting()`), stored under a
|
|
70
|
+
`pi-plus` section. Global values live in `~/.pi/agent/settings-extensions.json`,
|
|
71
|
+
project values in `.pi/settings-extensions.json` (project wins per key). Values
|
|
72
|
+
are strings; invalid or absent values fall back to the hardcoded CC-derived
|
|
73
|
+
defaults. If `@juanibiapina/pi-extension-settings` is installed, the settings
|
|
74
|
+
also appear in its `/extension-settings` UI.
|
|
63
75
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"pi-plus": {
|
|
79
|
+
"autoCompact": "true",
|
|
80
|
+
"contextWindow": "256000",
|
|
81
|
+
"autoCompactThreshold": "235000",
|
|
82
|
+
"autoCompactPercent": "50",
|
|
83
|
+
"autoCompactWindowCap": "200000",
|
|
84
|
+
"maxOutputTokens": "8192",
|
|
85
|
+
"disableOutputTokenCap": "false",
|
|
86
|
+
"blockingLimit": "190000"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
73
90
|
|
|
74
|
-
|
|
91
|
+
| Setting | Effect | Default |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `autoCompact` | Master switch for the plugin's auto-compact trigger (`true`/`false`) | `true` |
|
|
94
|
+
| `contextWindow` | Override the model-derived context window (tokens) | model default (200k) |
|
|
95
|
+
| `autoCompactThreshold` | Absolute token threshold for auto-compact; wins over everything below | effective window − 13k |
|
|
96
|
+
| `autoCompactPercent` | Threshold as % of effective window (1–100, for testing) | off |
|
|
97
|
+
| `autoCompactWindowCap` | Cap the window before threshold math (tokens) | off |
|
|
98
|
+
| `maxOutputTokens` | Override max output tokens (bounded by the model's limit) | 8k-capped model default |
|
|
99
|
+
| `disableOutputTokenCap` | Use the model's native default max output instead of the 8k slot cap | `false` |
|
|
100
|
+
| `blockingLimit` | Absolute token usage that blocks further work | effective window − 3k |
|
|
101
|
+
|
|
102
|
+
## Usage-detection log
|
|
103
|
+
|
|
104
|
+
Every usage check is appended as one JSON line to `pi-plus.log` inside pi's
|
|
105
|
+
agent directory (`$PI_CODING_AGENT_DIR` or `~/.pi/agent`). Each line records
|
|
106
|
+
the event (`usage_check` from the indicator or the auto-compact trigger,
|
|
107
|
+
plus `auto_compact_fired` / `auto_compact_error`), the model, the measured
|
|
108
|
+
token count, and the threshold/percent state:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{"ts":"2026-09-17T13:06:11.672Z","event":"usage_check","source":"indicator","model":"kimi-k2","tokens":9977,"percentLeft":94,"aboveWarning":false,"aboveError":false,"aboveAutoCompact":false,"atBlockingLimit":false}
|
|
112
|
+
```
|
|
75
113
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
| `PI_CC_CONTEXT_DISABLE_OTK_CAP` | Disable the 8k slot-reservation cap on max output tokens |
|
|
79
|
-
| `PI_CC_CONTEXT_NO_AUTO_COMPACT` | Never call `ctx.compact()` from the plugin |
|
|
114
|
+
Logging is best-effort — a write failure never affects the host agent.
|
|
115
|
+
Set `PI_PLUS_NO_LOG=1` to disable it.
|
|
80
116
|
|
|
81
117
|
## Known limits
|
|
82
118
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-plus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Claude Code-exact context usage detection for pi (extension)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"pi": {
|
|
@@ -44,5 +44,8 @@
|
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@earendil-works/pi-ai": "*",
|
|
46
46
|
"@earendil-works/pi-coding-agent": "*"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@juanibiapina/pi-extension-settings": "^0.10.0"
|
|
47
50
|
}
|
|
48
51
|
}
|
package/src/index.ts
CHANGED
|
@@ -13,10 +13,13 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { SettingDefinition } from "@juanibiapina/pi-extension-settings";
|
|
16
17
|
import { initialTriggerState, shouldFireAutoCompact, type AutoCompactTriggerState } from "./auto-compact.ts";
|
|
17
18
|
import { getAutoCompactThreshold } from "./model-config.ts";
|
|
18
19
|
import { isAutoCompactEnabled, calculateTokenWarningState } from "./percentages.ts";
|
|
19
20
|
import { renderTokenWarning } from "./indicator.ts";
|
|
21
|
+
import { appendUsageLog } from "./logger.ts";
|
|
22
|
+
import { loadPiPlusSettings } from "./settings.ts";
|
|
20
23
|
import { tokenCountWithEstimation, type RoughMessage } from "./usage.ts";
|
|
21
24
|
|
|
22
25
|
const STATUS_KEY = "cc-context";
|
|
@@ -29,20 +32,90 @@ function branchMessages(ctx: ExtensionContext): RoughMessage[] {
|
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
export default function (pi: ExtensionAPI) {
|
|
35
|
+
// Register our settings so they appear in the /extension-settings UI
|
|
36
|
+
// (provided by @juanibiapina/pi-extension-settings when installed).
|
|
37
|
+
pi.events.emit("pi-extension-settings:register", {
|
|
38
|
+
name: "pi-plus",
|
|
39
|
+
settings: [
|
|
40
|
+
{
|
|
41
|
+
id: "autoCompact",
|
|
42
|
+
label: "Auto-compact at threshold",
|
|
43
|
+
description: "Call /compact automatically when usage crosses the auto-compact threshold",
|
|
44
|
+
defaultValue: "true",
|
|
45
|
+
values: ["true", "false"],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "contextWindow",
|
|
49
|
+
label: "Context window override",
|
|
50
|
+
description: "Override the model-derived context window (tokens). Empty = model default.",
|
|
51
|
+
defaultValue: "",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "autoCompactThreshold",
|
|
55
|
+
label: "Auto-compact threshold",
|
|
56
|
+
description: "Absolute token threshold for auto-compact. Empty = effective window − 13k.",
|
|
57
|
+
defaultValue: "",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "autoCompactPercent",
|
|
61
|
+
label: "Auto-compact percent",
|
|
62
|
+
description: "Threshold as % of effective window (1–100, testing). Empty = off.",
|
|
63
|
+
defaultValue: "",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "autoCompactWindowCap",
|
|
67
|
+
label: "Auto-compact window cap",
|
|
68
|
+
description: "Cap the window before threshold math (tokens). Empty = off.",
|
|
69
|
+
defaultValue: "",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "maxOutputTokens",
|
|
73
|
+
label: "Max output tokens",
|
|
74
|
+
description: "Override max output tokens (bounded by the model's limit). Empty = default.",
|
|
75
|
+
defaultValue: "",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "disableOutputTokenCap",
|
|
79
|
+
label: "Disable 8k output cap",
|
|
80
|
+
description: "Use the model's native default max output instead of the 8k slot cap",
|
|
81
|
+
defaultValue: "false",
|
|
82
|
+
values: ["false", "true"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "blockingLimit",
|
|
86
|
+
label: "Blocking limit",
|
|
87
|
+
description: "Absolute token usage that blocks further work. Empty = effective window − 3k.",
|
|
88
|
+
defaultValue: "",
|
|
89
|
+
},
|
|
90
|
+
] satisfies SettingDefinition[],
|
|
91
|
+
});
|
|
92
|
+
|
|
32
93
|
// Edge-trigger state for the CC-threshold auto-compact. Reset per session;
|
|
33
94
|
// re-arms automatically once post-compact usage drops below the threshold.
|
|
34
95
|
let compactTrigger: AutoCompactTriggerState = initialTriggerState();
|
|
35
96
|
|
|
36
97
|
function updateIndicator(ctx: ExtensionContext): void {
|
|
37
|
-
if (!ctx.hasUI) return;
|
|
38
98
|
const model = ctx.model;
|
|
39
99
|
if (!model) {
|
|
40
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
100
|
+
if (ctx.hasUI) ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
41
101
|
return;
|
|
42
102
|
}
|
|
103
|
+
const settings = loadPiPlusSettings();
|
|
43
104
|
const tokenCount = tokenCountWithEstimation(branchMessages(ctx));
|
|
44
|
-
const state = calculateTokenWarningState(tokenCount, model.id, model.contextWindow, model.maxTokens);
|
|
45
|
-
|
|
105
|
+
const state = calculateTokenWarningState(tokenCount, model.id, model.contextWindow, model.maxTokens, settings);
|
|
106
|
+
appendUsageLog({
|
|
107
|
+
event: "usage_check",
|
|
108
|
+
source: "indicator",
|
|
109
|
+
model: model.id,
|
|
110
|
+
tokens: tokenCount,
|
|
111
|
+
percentLeft: state.percentLeft,
|
|
112
|
+
aboveWarning: state.isAboveWarningThreshold,
|
|
113
|
+
aboveError: state.isAboveErrorThreshold,
|
|
114
|
+
aboveAutoCompact: state.isAboveAutoCompactThreshold,
|
|
115
|
+
atBlockingLimit: state.isAtBlockingLimit,
|
|
116
|
+
});
|
|
117
|
+
if (!ctx.hasUI) return;
|
|
118
|
+
ctx.ui.setStatus(STATUS_KEY, renderTokenWarning(state, isAutoCompactEnabled(settings), ctx.ui.theme));
|
|
46
119
|
}
|
|
47
120
|
|
|
48
121
|
/**
|
|
@@ -51,22 +124,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
51
124
|
* (tool_execution_end) — pi performs the compaction between agent steps
|
|
52
125
|
* and continues the turn with the compacted context.
|
|
53
126
|
*
|
|
54
|
-
* Set
|
|
127
|
+
* Set PI_PLUS_NO_AUTO_COMPACT=1 to disable, and disable pi's
|
|
55
128
|
* built-in auto-compaction (settings.json "compaction": {"enabled": false})
|
|
56
129
|
* so this threshold governs instead of pi's window − 16,384.
|
|
57
130
|
*/
|
|
58
131
|
function maybeAutoCompact(ctx: ExtensionContext): void {
|
|
59
132
|
const model = ctx.model;
|
|
60
133
|
if (!model) return;
|
|
61
|
-
|
|
62
|
-
if (
|
|
134
|
+
const settings = loadPiPlusSettings();
|
|
135
|
+
if (!isAutoCompactEnabled(settings)) return;
|
|
136
|
+
if (process.env.PI_PLUS_NO_AUTO_COMPACT !== undefined) return;
|
|
63
137
|
const tokenCount = tokenCountWithEstimation(branchMessages(ctx));
|
|
64
|
-
const threshold = getAutoCompactThreshold(model.id, model.contextWindow, model.maxTokens);
|
|
65
|
-
|
|
138
|
+
const threshold = getAutoCompactThreshold(model.id, model.contextWindow, model.maxTokens, settings);
|
|
139
|
+
const fired = shouldFireAutoCompact(compactTrigger, tokenCount, threshold);
|
|
140
|
+
appendUsageLog({
|
|
141
|
+
event: "usage_check",
|
|
142
|
+
source: "auto_compact",
|
|
143
|
+
model: model.id,
|
|
144
|
+
tokens: tokenCount,
|
|
145
|
+
threshold,
|
|
146
|
+
belowThreshold: tokenCount < threshold,
|
|
147
|
+
fired,
|
|
148
|
+
});
|
|
149
|
+
if (!fired) return;
|
|
66
150
|
try {
|
|
67
151
|
ctx.compact();
|
|
152
|
+
appendUsageLog({ event: "auto_compact_fired", model: model.id, tokens: tokenCount, threshold });
|
|
68
153
|
} catch (error) {
|
|
69
154
|
ctx.ui.notify?.(`pi-plus: auto-compact failed to start: ${String(error)}`, "warning");
|
|
155
|
+
appendUsageLog({ event: "auto_compact_error", model: model.id, error: String(error) });
|
|
70
156
|
}
|
|
71
157
|
}
|
|
72
158
|
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage-detection logging to a log file inside pi's agent directory
|
|
3
|
+
* (`$PI_CODING_AGENT_DIR` or `~/.pi/agent`, same resolution as pi's
|
|
4
|
+
* getAgentDir() in coding-agent config.ts).
|
|
5
|
+
*
|
|
6
|
+
* pi itself has no general runtime agent log (only an on-demand debug log
|
|
7
|
+
* and a TUI crash log), so this plugin appends its own `pi-plus.log` there.
|
|
8
|
+
* Lines are JSON — one object per usage check — so they can be inspected or
|
|
9
|
+
* replayed easily.
|
|
10
|
+
*
|
|
11
|
+
* Logging is strictly best-effort: any filesystem error is swallowed so the
|
|
12
|
+
* plugin can never crash or disturb the host agent.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { appendFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
/** Mirrors pi's ENV_AGENT_DIR (`PI_CODING_AGENT_DIR` for the stock build). */
|
|
20
|
+
const ENV_AGENT_DIR = "PI_CODING_AGENT_DIR";
|
|
21
|
+
|
|
22
|
+
/** Set PI_PLUS_NO_LOG=1 to disable file logging. */
|
|
23
|
+
export function isLogEnabled(): boolean {
|
|
24
|
+
const v = process.env.PI_PLUS_NO_LOG;
|
|
25
|
+
return v === undefined || v === "" || v === "0" || v.toLowerCase() === "false";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function expandTildePath(p: string): string {
|
|
29
|
+
if (p === "~") return homedir();
|
|
30
|
+
if (p.startsWith("~/")) return join(homedir(), p.slice(2));
|
|
31
|
+
return p;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getAgentDir(): string {
|
|
35
|
+
const envDir = process.env[ENV_AGENT_DIR];
|
|
36
|
+
return envDir ? expandTildePath(envDir) : join(homedir(), ".pi", "agent");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getPiPlusLogPath(): string {
|
|
40
|
+
return join(getAgentDir(), "pi-plus.log");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type UsageLogFields = Record<string, string | number | boolean | null | undefined>;
|
|
44
|
+
|
|
45
|
+
export function formatUsageLogLine(fields: UsageLogFields): string {
|
|
46
|
+
return JSON.stringify({ ts: new Date().toISOString(), ...fields }) + "\n";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function appendUsageLog(fields: UsageLogFields): void {
|
|
50
|
+
if (!isLogEnabled()) return;
|
|
51
|
+
try {
|
|
52
|
+
appendFileSync(getPiPlusLogPath(), formatUsageLogLine(fields));
|
|
53
|
+
} catch {
|
|
54
|
+
// Best-effort: never propagate logging failures into the host agent.
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/model-config.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* - src/utils/context.ts — getContextWindowForModel, getModelMaxOutputTokens
|
|
6
6
|
* - src/services/api/claude.ts — getMaxOutputTokensForModel (slot-reservation cap)
|
|
7
7
|
* - src/services/compact/autoCompact.ts — getEffectiveContextWindowSize, getAutoCompactThreshold
|
|
8
|
+
*
|
|
9
|
+
* This plugin intentionally supports NO environment variables (CC's
|
|
10
|
+
* CLAUDE_CODE_* env overrides included). Everything user-configurable lives
|
|
11
|
+
* in the `pi-plus` section of pi's settings.json — see settings.ts.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
export const MODEL_CONTEXT_WINDOW_DEFAULT = 200_000;
|
|
@@ -17,41 +21,37 @@ const MAX_OUTPUT_TOKENS_UPPER_LIMIT = 64_000;
|
|
|
17
21
|
|
|
18
22
|
const ONE_MILLION = 1_000_000;
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
/** The `pi-plus` settings.json section, as it affects model config. */
|
|
25
|
+
export interface ModelConfigSettings {
|
|
26
|
+
/** Override the model-derived context window (tokens). */
|
|
27
|
+
contextWindow?: number;
|
|
28
|
+
/** Cap the window before threshold math (CC: CLAUDE_CODE_AUTO_COMPACT_WINDOW). */
|
|
29
|
+
autoCompactWindowCap?: number;
|
|
30
|
+
/** Override max output tokens, bounded by the model's upper limit. */
|
|
31
|
+
maxOutputTokens?: number;
|
|
32
|
+
/** Disable the 8k slot-reservation cap on default max output tokens. */
|
|
33
|
+
disableOutputTokenCap?: boolean;
|
|
34
|
+
/** Override the auto-compact threshold directly (absolute tokens). */
|
|
35
|
+
autoCompactThreshold?: number;
|
|
36
|
+
/** Threshold as a percent (1–100) of the effective window (testing). */
|
|
37
|
+
autoCompactPercent?: number;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/** CC: has1mContext — explicit `[1m]` model suffix opt-in. */
|
|
35
41
|
export function has1mContext(modelId: string): boolean {
|
|
36
|
-
return
|
|
42
|
+
return /\[1m\]/i.test(modelId);
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
46
|
* CC: getContextWindowForModel.
|
|
41
|
-
* Resolution order:
|
|
42
|
-
*
|
|
43
|
-
*
|
|
47
|
+
* Resolution order: `[1m]` suffix → model capability (pi's registry
|
|
48
|
+
* `contextWindow` plays the role of CC's `max_input_tokens` capability,
|
|
49
|
+
* with the same >= 100k gate) → 200k default.
|
|
44
50
|
*/
|
|
45
51
|
export function resolveContextWindow(modelId: string, modelContextWindow?: number): number {
|
|
46
|
-
const override = parsePositiveInt(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS);
|
|
47
|
-
if (override) return override;
|
|
48
|
-
|
|
49
52
|
if (has1mContext(modelId)) return ONE_MILLION;
|
|
50
53
|
|
|
51
54
|
if (modelContextWindow && modelContextWindow >= 100_000) {
|
|
52
|
-
if (modelContextWindow > MODEL_CONTEXT_WINDOW_DEFAULT && is1mContextDisabled()) {
|
|
53
|
-
return MODEL_CONTEXT_WINDOW_DEFAULT;
|
|
54
|
-
}
|
|
55
55
|
return modelContextWindow;
|
|
56
56
|
}
|
|
57
57
|
return MODEL_CONTEXT_WINDOW_DEFAULT;
|
|
@@ -111,21 +111,23 @@ export function getModelMaxOutputTokens(
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* CC: getMaxOutputTokensForModel — applies the slot-reservation cap
|
|
114
|
-
* (tengu_otk_slot_v1
|
|
115
|
-
*
|
|
116
|
-
* CLAUDE_CODE_MAX_OUTPUT_TOKENS bounded env override.
|
|
114
|
+
* (tengu_otk_slot_v1: default output dropped to 8k), unless disabled in
|
|
115
|
+
* settings, then the `maxOutputTokens` bounded settings override.
|
|
117
116
|
*/
|
|
118
|
-
export function getMaxOutputTokensForModel(
|
|
117
|
+
export function getMaxOutputTokensForModel(
|
|
118
|
+
modelId: string,
|
|
119
|
+
modelMaxTokens?: number,
|
|
120
|
+
settings?: ModelConfigSettings,
|
|
121
|
+
): number {
|
|
119
122
|
const { default: modelDefault, upperLimit } = getModelMaxOutputTokens(modelId, modelMaxTokens);
|
|
120
|
-
const capDisabled = isEnvTruthy(process.env.PI_CC_CONTEXT_DISABLE_OTK_CAP);
|
|
121
123
|
// Math.min keeps models with lower native defaults (e.g. claude-3-opus at
|
|
122
124
|
// 4k) at their native value.
|
|
123
|
-
const defaultTokens =
|
|
125
|
+
const defaultTokens = settings?.disableOutputTokenCap
|
|
126
|
+
? modelDefault
|
|
127
|
+
: Math.min(modelDefault, CAPPED_DEFAULT_MAX_TOKENS);
|
|
124
128
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const parsed = parseInt(raw, 10);
|
|
128
|
-
if (!Number.isNaN(parsed) && parsed > 0) return Math.min(parsed, upperLimit);
|
|
129
|
+
if (settings?.maxOutputTokens !== undefined && settings.maxOutputTokens > 0) {
|
|
130
|
+
return Math.min(settings.maxOutputTokens, upperLimit);
|
|
129
131
|
}
|
|
130
132
|
return defaultTokens;
|
|
131
133
|
}
|
|
@@ -133,44 +135,50 @@ export function getMaxOutputTokensForModel(modelId: string, modelMaxTokens?: num
|
|
|
133
135
|
/**
|
|
134
136
|
* CC: getEffectiveContextWindowSize — context window minus the reserved
|
|
135
137
|
* output tokens (min(maxOutput, 20k)), optionally capped by
|
|
136
|
-
*
|
|
138
|
+
* `autoCompactWindowCap` from settings.
|
|
139
|
+
*
|
|
140
|
+
* `settings.contextWindow` replaces the model-derived window first.
|
|
137
141
|
*/
|
|
138
142
|
export function getEffectiveContextWindowSize(
|
|
139
143
|
modelId: string,
|
|
140
144
|
modelContextWindow?: number,
|
|
141
145
|
modelMaxTokens?: number,
|
|
146
|
+
settings?: ModelConfigSettings,
|
|
142
147
|
): number {
|
|
143
148
|
const reservedTokensForSummary = Math.min(
|
|
144
|
-
getMaxOutputTokensForModel(modelId, modelMaxTokens),
|
|
149
|
+
getMaxOutputTokensForModel(modelId, modelMaxTokens, settings),
|
|
145
150
|
COMPACT_MAX_OUTPUT_TOKENS,
|
|
146
151
|
);
|
|
147
|
-
let contextWindow = resolveContextWindow(modelId, modelContextWindow);
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
150
|
-
contextWindow = Math.min(contextWindow,
|
|
152
|
+
let contextWindow = settings?.contextWindow ?? resolveContextWindow(modelId, modelContextWindow);
|
|
153
|
+
const windowCap = settings?.autoCompactWindowCap;
|
|
154
|
+
if (windowCap !== undefined && windowCap > 0) {
|
|
155
|
+
contextWindow = Math.min(contextWindow, windowCap);
|
|
151
156
|
}
|
|
152
157
|
return contextWindow - reservedTokensForSummary;
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
/**
|
|
156
161
|
* CC: getAutoCompactThreshold — effective window minus the 13k autocompact
|
|
157
|
-
* buffer.
|
|
162
|
+
* buffer. `autoCompactPercent` (1–100) lowers it for testing.
|
|
163
|
+
*
|
|
164
|
+
* `settings.autoCompactThreshold` (absolute tokens) wins outright;
|
|
165
|
+
* otherwise `settings.contextWindow` / `autoCompactWindowCap` feed the
|
|
166
|
+
* CC math via getEffectiveContextWindowSize.
|
|
158
167
|
*/
|
|
159
168
|
export function getAutoCompactThreshold(
|
|
160
169
|
modelId: string,
|
|
161
170
|
modelContextWindow?: number,
|
|
162
171
|
modelMaxTokens?: number,
|
|
172
|
+
settings?: ModelConfigSettings,
|
|
163
173
|
): number {
|
|
164
|
-
|
|
174
|
+
if (settings?.autoCompactThreshold !== undefined) return settings.autoCompactThreshold;
|
|
175
|
+
const effective = getEffectiveContextWindowSize(modelId, modelContextWindow, modelMaxTokens, settings);
|
|
165
176
|
const autocompactThreshold = effective - AUTOCOMPACT_BUFFER_TOKENS;
|
|
166
177
|
|
|
167
|
-
const
|
|
168
|
-
if (
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
const percentageThreshold = Math.floor(effective * (parsed / 100));
|
|
172
|
-
return Math.min(percentageThreshold, autocompactThreshold);
|
|
173
|
-
}
|
|
178
|
+
const percent = settings?.autoCompactPercent;
|
|
179
|
+
if (percent !== undefined && !Number.isNaN(percent) && percent > 0 && percent <= 100) {
|
|
180
|
+
const percentageThreshold = Math.floor(effective * (percent / 100));
|
|
181
|
+
return Math.min(percentageThreshold, autocompactThreshold);
|
|
174
182
|
}
|
|
175
183
|
return autocompactThreshold;
|
|
176
184
|
}
|
package/src/percentages.ts
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
* Sources (claude-code repo):
|
|
5
5
|
* - src/utils/context.ts — calculateContextPercentages
|
|
6
6
|
* - src/services/compact/autoCompact.ts — calculateTokenWarningState, isAutoCompactEnabled
|
|
7
|
+
*
|
|
8
|
+
* No environment variables: the auto-compact on/off switch and blocking-limit
|
|
9
|
+
* override live in the `pi-plus` settings.json section (see settings.ts).
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import {
|
|
10
13
|
getAutoCompactThreshold,
|
|
11
14
|
getEffectiveContextWindowSize,
|
|
15
|
+
type ModelConfigSettings,
|
|
12
16
|
} from "./model-config.ts";
|
|
13
17
|
import type { UsageSnapshot } from "./usage.ts";
|
|
14
18
|
|
|
@@ -16,17 +20,20 @@ export const WARNING_THRESHOLD_BUFFER_TOKENS = 20_000;
|
|
|
16
20
|
export const ERROR_THRESHOLD_BUFFER_TOKENS = 20_000;
|
|
17
21
|
export const MANUAL_COMPACT_BUFFER_TOKENS = 3_000;
|
|
18
22
|
|
|
23
|
+
/** pi-plus settings keys that affect the warning state. */
|
|
24
|
+
export interface PercentagesSettings extends ModelConfigSettings {
|
|
25
|
+
/** Master switch for auto-compaction (default true). */
|
|
26
|
+
autoCompact?: boolean;
|
|
27
|
+
/** Override the blocking limit (absolute tokens). */
|
|
28
|
+
blockingLimit?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/**
|
|
20
|
-
* CC: isAutoCompactEnabled —
|
|
21
|
-
*
|
|
22
|
-
* equivalent, so this port is env-only and defaults to enabled.)
|
|
32
|
+
* CC: isAutoCompactEnabled — CC consults env vars and a user-config flag;
|
|
33
|
+
* this port uses only the `autoCompact` settings.json flag (default true).
|
|
23
34
|
*/
|
|
24
|
-
export function isAutoCompactEnabled(): boolean {
|
|
25
|
-
|
|
26
|
-
const truthy = (v: string | undefined) => v !== undefined && v !== "" && v !== "0" && v.toLowerCase() !== "false";
|
|
27
|
-
if (truthy(raw("DISABLE_COMPACT"))) return false;
|
|
28
|
-
if (truthy(raw("DISABLE_AUTO_COMPACT"))) return false;
|
|
29
|
-
return true;
|
|
35
|
+
export function isAutoCompactEnabled(settings?: Pick<PercentagesSettings, "autoCompact">): boolean {
|
|
36
|
+
return settings?.autoCompact !== false;
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
/**
|
|
@@ -58,17 +65,18 @@ export interface TokenWarningState {
|
|
|
58
65
|
* CC: calculateTokenWarningState — percent left until the (auto-compact or
|
|
59
66
|
* effective) threshold and the warning/error/blocking threshold crossings.
|
|
60
67
|
* Warning and error buffers are both 20k in CC, so the two crossings are
|
|
61
|
-
* identical unless
|
|
68
|
+
* identical unless overrides differ.
|
|
62
69
|
*/
|
|
63
70
|
export function calculateTokenWarningState(
|
|
64
71
|
tokenUsage: number,
|
|
65
72
|
modelId: string,
|
|
66
73
|
modelContextWindow?: number,
|
|
67
74
|
modelMaxTokens?: number,
|
|
75
|
+
settings?: PercentagesSettings,
|
|
68
76
|
): TokenWarningState {
|
|
69
|
-
const autoCompact = isAutoCompactEnabled();
|
|
70
|
-
const autoCompactThreshold = getAutoCompactThreshold(modelId, modelContextWindow, modelMaxTokens);
|
|
71
|
-
const effectiveContextWindow = getEffectiveContextWindowSize(modelId, modelContextWindow, modelMaxTokens);
|
|
77
|
+
const autoCompact = isAutoCompactEnabled(settings);
|
|
78
|
+
const autoCompactThreshold = getAutoCompactThreshold(modelId, modelContextWindow, modelMaxTokens, settings);
|
|
79
|
+
const effectiveContextWindow = getEffectiveContextWindowSize(modelId, modelContextWindow, modelMaxTokens, settings);
|
|
72
80
|
const threshold = autoCompact ? autoCompactThreshold : effectiveContextWindow;
|
|
73
81
|
|
|
74
82
|
const percentLeft = Math.max(0, Math.round(((threshold - tokenUsage) / threshold) * 100));
|
|
@@ -77,10 +85,9 @@ export function calculateTokenWarningState(
|
|
|
77
85
|
const errorThreshold = threshold - ERROR_THRESHOLD_BUFFER_TOKENS;
|
|
78
86
|
|
|
79
87
|
const defaultBlockingLimit = effectiveContextWindow - MANUAL_COMPACT_BUFFER_TOKENS;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
:
|
|
83
|
-
const blockingLimit = !Number.isNaN(parsedOverride) && parsedOverride > 0 ? parsedOverride : defaultBlockingLimit;
|
|
88
|
+
const configuredBlocking = settings?.blockingLimit;
|
|
89
|
+
const blockingLimit =
|
|
90
|
+
configuredBlocking !== undefined && configuredBlocking > 0 ? configuredBlocking : defaultBlockingLimit;
|
|
84
91
|
|
|
85
92
|
return {
|
|
86
93
|
percentLeft,
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-plus settings, read through the pi ecosystem's standard settings
|
|
3
|
+
* functions (`getSetting` from @juanibiapina/pi-extension-settings) rather
|
|
4
|
+
* than by locating settings files ourselves.
|
|
5
|
+
*
|
|
6
|
+
* Storage: global values live in `~/.pi/agent/settings-extensions.json`,
|
|
7
|
+
* project values in `.pi/settings-extensions.json` (project wins) — both
|
|
8
|
+
* under a `"pi-plus"` extension section:
|
|
9
|
+
*
|
|
10
|
+
* ```json
|
|
11
|
+
* {
|
|
12
|
+
* "pi-plus": {
|
|
13
|
+
* "autoCompact": "true",
|
|
14
|
+
* "contextWindow": "256000",
|
|
15
|
+
* "autoCompactWindowCap": "200000",
|
|
16
|
+
* "maxOutputTokens": "8192",
|
|
17
|
+
* "disableOutputTokenCap": "false",
|
|
18
|
+
* "autoCompactThreshold": "235000",
|
|
19
|
+
* "autoCompactPercent": "50",
|
|
20
|
+
* "blockingLimit": "190000"
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Values are strings (the extension-settings convention); each accessor
|
|
26
|
+
* below parses and validates. Unset or invalid values fall back to the
|
|
27
|
+
* hardcoded CC-derived defaults in model-config.ts / percentages.ts.
|
|
28
|
+
*
|
|
29
|
+
* This plugin supports NO Claude Code environment variables — this settings
|
|
30
|
+
* system is the only configuration surface. (The plugin's own
|
|
31
|
+
* PI_PLUS_NO_AUTO_COMPACT and PI_PLUS_NO_LOG env switches live in index.ts /
|
|
32
|
+
* logger.ts.)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { getSetting, type SettingStorageOptions } from "@juanibiapina/pi-extension-settings";
|
|
36
|
+
import type { ModelConfigSettings } from "./model-config.ts";
|
|
37
|
+
|
|
38
|
+
export interface PiPlusSettings extends ModelConfigSettings {
|
|
39
|
+
/** Master switch for auto-compaction (default true). */
|
|
40
|
+
autoCompact?: boolean;
|
|
41
|
+
/** Override the blocking limit (absolute tokens). */
|
|
42
|
+
blockingLimit?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const EXTENSION_NAME = "pi-plus";
|
|
46
|
+
|
|
47
|
+
function raw(settingId: string, options?: SettingStorageOptions): string | undefined {
|
|
48
|
+
const value = getSetting(EXTENSION_NAME, settingId, undefined, options);
|
|
49
|
+
return value === undefined || value === "" ? undefined : value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function positiveInt(settingId: string, options?: SettingStorageOptions): number | undefined {
|
|
53
|
+
const value = raw(settingId, options);
|
|
54
|
+
if (value === undefined) return undefined;
|
|
55
|
+
const parsed = Number(value);
|
|
56
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return undefined;
|
|
57
|
+
return Math.floor(parsed);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function percentValue(settingId: string, options?: SettingStorageOptions): number | undefined {
|
|
61
|
+
const parsed = positiveInt(settingId, options);
|
|
62
|
+
if (parsed === undefined || parsed > 100) return undefined;
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function booleanValue(settingId: string, options?: SettingStorageOptions): boolean | undefined {
|
|
67
|
+
const value = raw(settingId, options);
|
|
68
|
+
if (value === undefined) return undefined;
|
|
69
|
+
const normalized = value.trim().toLowerCase();
|
|
70
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
71
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Load the merged pi-plus settings (local overrides global). */
|
|
76
|
+
export function loadPiPlusSettings(options?: SettingStorageOptions): PiPlusSettings {
|
|
77
|
+
const settings: PiPlusSettings = {};
|
|
78
|
+
|
|
79
|
+
const contextWindow = positiveInt("contextWindow", options);
|
|
80
|
+
const autoCompactWindowCap = positiveInt("autoCompactWindowCap", options);
|
|
81
|
+
const maxOutputTokens = positiveInt("maxOutputTokens", options);
|
|
82
|
+
const autoCompactThreshold = positiveInt("autoCompactThreshold", options);
|
|
83
|
+
const autoCompactPercent = percentValue("autoCompactPercent", options);
|
|
84
|
+
const blockingLimit = positiveInt("blockingLimit", options);
|
|
85
|
+
const autoCompact = booleanValue("autoCompact", options);
|
|
86
|
+
const disableOutputTokenCap = booleanValue("disableOutputTokenCap", options);
|
|
87
|
+
|
|
88
|
+
if (contextWindow !== undefined) settings.contextWindow = contextWindow;
|
|
89
|
+
if (autoCompactWindowCap !== undefined) settings.autoCompactWindowCap = autoCompactWindowCap;
|
|
90
|
+
if (maxOutputTokens !== undefined) settings.maxOutputTokens = maxOutputTokens;
|
|
91
|
+
if (autoCompactThreshold !== undefined) settings.autoCompactThreshold = autoCompactThreshold;
|
|
92
|
+
if (autoCompactPercent !== undefined) settings.autoCompactPercent = autoCompactPercent;
|
|
93
|
+
if (blockingLimit !== undefined) settings.blockingLimit = blockingLimit;
|
|
94
|
+
if (autoCompact !== undefined) settings.autoCompact = autoCompact;
|
|
95
|
+
if (disableOutputTokenCap !== undefined) settings.disableOutputTokenCap = disableOutputTokenCap;
|
|
96
|
+
return settings;
|
|
97
|
+
}
|