@aliaksei-raketski/pi-fast-mode 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.
- package/README.md +42 -0
- package/index.ts +256 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Pi Fast Extension
|
|
2
|
+
|
|
3
|
+
A Pi extension that enables fast mode for supported models with one command:
|
|
4
|
+
|
|
5
|
+
- **`/fast`**: toggles fast mode on/off.
|
|
6
|
+
- No arguments. `/fast` always toggles.
|
|
7
|
+
|
|
8
|
+
The current model determines what gets injected:
|
|
9
|
+
|
|
10
|
+
- **Claude Opus 4.6 / 4.7 / 4.8**
|
|
11
|
+
- Adds `speed: "fast"`
|
|
12
|
+
- Adds required header `anthropic-beta: fast-mode-2026-02-01`
|
|
13
|
+
- **OpenAI Codex GPT-5.4 / GPT-5.5**
|
|
14
|
+
- Adds `service_tier: "priority"`
|
|
15
|
+
- Requires ChatGPT/OAuth auth (API-key models are skipped)
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:@aliaksei-raketski/pi-fast-mode
|
|
21
|
+
# or project-local
|
|
22
|
+
pi install -l npm:@aliaksei-raketski/pi-fast-mode
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Behavior
|
|
26
|
+
|
|
27
|
+
- Start Pi with fast mode enabled:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pi --fast
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- Footer status is always visible as one of:
|
|
34
|
+
|
|
35
|
+
- `fast on` (accent color)
|
|
36
|
+
- `fast off` (gray)
|
|
37
|
+
|
|
38
|
+
When fast mode is enabled but the current model is not supported, status stays as:
|
|
39
|
+
|
|
40
|
+
- `fast on` in gray (so you can see it is enabled, but inactive for current model)
|
|
41
|
+
|
|
42
|
+
When you switch to a supported model, fast mode is applied automatically if it is enabled.
|
package/index.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const FAST_COMMAND = "fast";
|
|
4
|
+
const FAST_FLAG = "fast";
|
|
5
|
+
const FAST_STATUS_KEY = "fast";
|
|
6
|
+
|
|
7
|
+
const FAST_ON_TEXT = "fast on";
|
|
8
|
+
const FAST_OFF_TEXT = "fast off";
|
|
9
|
+
|
|
10
|
+
const FAST_SPEED = "fast";
|
|
11
|
+
const FAST_BETA = "fast-mode-2026-02-01";
|
|
12
|
+
const CLAUDE_CODE_OAUTH_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
|
|
13
|
+
const FAST_SERVICE_TIER = "priority";
|
|
14
|
+
|
|
15
|
+
const CLAUDE_PROVIDER = "anthropic";
|
|
16
|
+
const CLAUDE_API = "anthropic-messages";
|
|
17
|
+
const OPENAI_PROVIDER = "openai-codex";
|
|
18
|
+
const OPENAI_API = "openai-codex-responses";
|
|
19
|
+
|
|
20
|
+
type FastFeature = {
|
|
21
|
+
provider: string;
|
|
22
|
+
api: string;
|
|
23
|
+
supportedModels: Set<string>;
|
|
24
|
+
injectionKey: string;
|
|
25
|
+
injectionValue: string;
|
|
26
|
+
unsupportedModelMessage: string;
|
|
27
|
+
isEligible?: (ctx: ExtensionContext) => string | undefined;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type FastModeState = {
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type CurrentModelStatus = {
|
|
35
|
+
feature?: FastFeature;
|
|
36
|
+
isSupported: boolean;
|
|
37
|
+
reason?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type PayloadRecord = Record<string, unknown>;
|
|
41
|
+
|
|
42
|
+
type HeaderModel = {
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const FEATURES: FastFeature[] = [
|
|
47
|
+
{
|
|
48
|
+
provider: CLAUDE_PROVIDER,
|
|
49
|
+
api: CLAUDE_API,
|
|
50
|
+
supportedModels: new Set(["claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8"]),
|
|
51
|
+
injectionKey: "speed",
|
|
52
|
+
injectionValue: FAST_SPEED,
|
|
53
|
+
unsupportedModelMessage:
|
|
54
|
+
"Fast mode is only available for Claude Opus 4.6, 4.7, and 4.8",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
provider: OPENAI_PROVIDER,
|
|
58
|
+
api: OPENAI_API,
|
|
59
|
+
supportedModels: new Set(["gpt-5.4", "gpt-5.5"]),
|
|
60
|
+
injectionKey: "service_tier",
|
|
61
|
+
injectionValue: FAST_SERVICE_TIER,
|
|
62
|
+
unsupportedModelMessage: "Fast mode is only available for GPT-5.4 and GPT-5.5",
|
|
63
|
+
isEligible: (ctx) =>
|
|
64
|
+
ctx.model && ctx.modelRegistry.isUsingOAuth(ctx.model)
|
|
65
|
+
? undefined
|
|
66
|
+
: "ChatGPT OAuth auth is required; API-key auth is intentionally not used",
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const sessionStates = new WeakMap<object, FastModeState>();
|
|
71
|
+
|
|
72
|
+
function getSessionState(ctx: ExtensionContext): FastModeState {
|
|
73
|
+
let state = sessionStates.get(ctx.sessionManager);
|
|
74
|
+
if (!state) {
|
|
75
|
+
state = { enabled: false };
|
|
76
|
+
sessionStates.set(ctx.sessionManager, state);
|
|
77
|
+
}
|
|
78
|
+
return state;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isPayloadRecord(payload: unknown): payload is PayloadRecord {
|
|
82
|
+
return typeof payload === "object" && payload !== null && !Array.isArray(payload);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function splitBetaHeader(value: string | undefined): string[] {
|
|
86
|
+
return (value ?? "")
|
|
87
|
+
.split(",")
|
|
88
|
+
.map((value) => value.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getCurrentModelStatus(ctx: ExtensionContext): CurrentModelStatus {
|
|
93
|
+
const model = ctx.model;
|
|
94
|
+
if (!model) {
|
|
95
|
+
return {
|
|
96
|
+
isSupported: false,
|
|
97
|
+
reason: "No model is selected",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const modelKey = `${model.provider}/${model.id}`;
|
|
102
|
+
const featuresForBackend = FEATURES.filter(
|
|
103
|
+
(feature) => feature.provider === model.provider && feature.api === model.api,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (featuresForBackend.length === 0) {
|
|
107
|
+
return {
|
|
108
|
+
isSupported: false,
|
|
109
|
+
reason: `Current model (${modelKey}) does not support fast mode`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const matchingFeature = featuresForBackend.find((feature) => feature.supportedModels.has(model.id));
|
|
114
|
+
if (!matchingFeature) {
|
|
115
|
+
return {
|
|
116
|
+
isSupported: false,
|
|
117
|
+
reason: featuresForBackend[0]?.unsupportedModelMessage ?? "Current model does not support fast mode",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (matchingFeature.isEligible) {
|
|
122
|
+
const reason = matchingFeature.isEligible(ctx);
|
|
123
|
+
if (reason) {
|
|
124
|
+
return {
|
|
125
|
+
feature: matchingFeature,
|
|
126
|
+
isSupported: false,
|
|
127
|
+
reason,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
feature: matchingFeature,
|
|
134
|
+
isSupported: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function syncClaudeBetaHeader(
|
|
139
|
+
ctx: ExtensionContext,
|
|
140
|
+
state: FastModeState,
|
|
141
|
+
modelStatus: CurrentModelStatus,
|
|
142
|
+
): void {
|
|
143
|
+
const model = ctx.model as (typeof ctx.model & HeaderModel) | undefined;
|
|
144
|
+
if (!model || model.provider !== CLAUDE_PROVIDER || model.api !== CLAUDE_API) return;
|
|
145
|
+
|
|
146
|
+
const shouldEnable = state.enabled && modelStatus.isSupported && modelStatus.feature?.provider === CLAUDE_PROVIDER;
|
|
147
|
+
const headers = { ...(model.headers ?? {}) };
|
|
148
|
+
const existing = splitBetaHeader(headers["anthropic-beta"] ?? headers["Anthropic-Beta"]);
|
|
149
|
+
const requiredBase = ctx.modelRegistry.isUsingOAuth(model) ? CLAUDE_CODE_OAUTH_BETAS : [];
|
|
150
|
+
const next = shouldEnable
|
|
151
|
+
? Array.from(new Set([...existing, ...requiredBase, FAST_BETA]))
|
|
152
|
+
: existing.filter((beta) => beta !== FAST_BETA);
|
|
153
|
+
|
|
154
|
+
delete headers["Anthropic-Beta"];
|
|
155
|
+
if (next.length > 0) {
|
|
156
|
+
headers["anthropic-beta"] = next.join(",");
|
|
157
|
+
} else {
|
|
158
|
+
delete headers["anthropic-beta"];
|
|
159
|
+
}
|
|
160
|
+
model.headers = headers;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function syncFeatureState(ctx: ExtensionContext, state: FastModeState): CurrentModelStatus {
|
|
164
|
+
const modelStatus = getCurrentModelStatus(ctx);
|
|
165
|
+
syncClaudeBetaHeader(ctx, state, modelStatus);
|
|
166
|
+
return modelStatus;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function updateStatus(ctx: ExtensionContext, state: FastModeState, modelStatus: CurrentModelStatus): void {
|
|
170
|
+
if (!ctx.hasUI) return;
|
|
171
|
+
|
|
172
|
+
const statusText = state.enabled ? FAST_ON_TEXT : FAST_OFF_TEXT;
|
|
173
|
+
const isActiveForCurrentModel = state.enabled && modelStatus.isSupported;
|
|
174
|
+
ctx.ui.setStatus(
|
|
175
|
+
FAST_STATUS_KEY,
|
|
176
|
+
ctx.ui.theme.fg(isActiveForCurrentModel ? "accent" : "muted", statusText),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function getFastPayload(
|
|
181
|
+
payload: unknown,
|
|
182
|
+
ctx: ExtensionContext,
|
|
183
|
+
state: FastModeState,
|
|
184
|
+
modelStatus: CurrentModelStatus,
|
|
185
|
+
): PayloadRecord | undefined {
|
|
186
|
+
if (!state.enabled) return undefined;
|
|
187
|
+
if (!modelStatus.isSupported || !modelStatus.feature) return undefined;
|
|
188
|
+
if (!isPayloadRecord(payload)) return undefined;
|
|
189
|
+
if (payload.model !== ctx.model?.id) return undefined;
|
|
190
|
+
if (modelStatus.feature.injectionKey in payload) return undefined;
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
...payload,
|
|
194
|
+
[modelStatus.feature.injectionKey]: modelStatus.feature.injectionValue,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default function fastMode(pi: ExtensionAPI) {
|
|
199
|
+
pi.registerFlag(FAST_FLAG, {
|
|
200
|
+
description: "Start with fast mode enabled",
|
|
201
|
+
type: "boolean",
|
|
202
|
+
default: false,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
pi.on("session_start", (_event, ctx) => {
|
|
206
|
+
const state = getSessionState(ctx);
|
|
207
|
+
state.enabled = pi.getFlag(FAST_FLAG) === true;
|
|
208
|
+
const modelStatus = syncFeatureState(ctx, state);
|
|
209
|
+
updateStatus(ctx, state, modelStatus);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
pi.on("model_select", (_event, ctx) => {
|
|
213
|
+
const state = getSessionState(ctx);
|
|
214
|
+
const modelStatus = syncFeatureState(ctx, state);
|
|
215
|
+
updateStatus(ctx, state, modelStatus);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
219
|
+
const state = getSessionState(ctx);
|
|
220
|
+
const modelStatus = syncFeatureState(ctx, state);
|
|
221
|
+
updateStatus(ctx, state, modelStatus);
|
|
222
|
+
return getFastPayload(event.payload, ctx, state, modelStatus);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
226
|
+
if (!ctx.hasUI) return;
|
|
227
|
+
ctx.ui.setStatus(FAST_STATUS_KEY, undefined);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
pi.registerCommand(FAST_COMMAND, {
|
|
231
|
+
description: "Toggle fast mode",
|
|
232
|
+
getArgumentCompletions: () => null,
|
|
233
|
+
handler: async (args, ctx) => {
|
|
234
|
+
if (args.trim()) {
|
|
235
|
+
ctx.ui.notify("Usage: /fast", "warning");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const state = getSessionState(ctx);
|
|
240
|
+
state.enabled = !state.enabled;
|
|
241
|
+
|
|
242
|
+
const modelStatus = syncFeatureState(ctx, state);
|
|
243
|
+
updateStatus(ctx, state, modelStatus);
|
|
244
|
+
|
|
245
|
+
ctx.ui.notify(`Fast mode is now ${state.enabled ? "on" : "off"}.`, "info");
|
|
246
|
+
|
|
247
|
+
if (state.enabled && !modelStatus.isSupported) {
|
|
248
|
+
const detail = modelStatus.reason ? ` (${modelStatus.reason})` : "";
|
|
249
|
+
ctx.ui.notify(
|
|
250
|
+
`Current model is not supported for fast mode${detail}. Fast mode will apply automatically once you switch to a supported model.`,
|
|
251
|
+
"warning",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aliaksei-raketski/pi-fast-mode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension that enables fast-mode payload tuning for supported Claude/OpenAI models.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"fast-mode"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"homepage": "https://github.com/aliaksei-raketski/pi-packages/tree/main/fast-mode",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/aliaksei-raketski/pi-packages/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/aliaksei-raketski/pi-packages.git",
|
|
19
|
+
"directory": "fast-mode"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org/"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
30
|
+
},
|
|
31
|
+
"pi": {
|
|
32
|
+
"extensions": [
|
|
33
|
+
"./index.ts"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"index.ts",
|
|
38
|
+
"README.md"
|
|
39
|
+
]
|
|
40
|
+
}
|