@aliaksei-raketski/pi-fast-mode 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aliaksei Raketski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -22,6 +22,12 @@ pi install npm:@aliaksei-raketski/pi-fast-mode
22
22
  pi install -l npm:@aliaksei-raketski/pi-fast-mode
23
23
  ```
24
24
 
25
+ Try locally from this repository:
26
+
27
+ ```bash
28
+ pi -e ./packages/fast-mode
29
+ ```
30
+
25
31
  ## Behavior
26
32
 
27
33
  - Start Pi with fast mode enabled:
@@ -39,4 +45,6 @@ When fast mode is enabled but the current model is not supported, status stays a
39
45
 
40
46
  - `fast on` in gray (so you can see it is enabled, but inactive for current model)
41
47
 
42
- When you switch to a supported model, fast mode is applied automatically if it is enabled.
48
+ When you switch to a supported model, fast mode is applied automatically if it is enabled.
49
+
50
+ The fast mode toggle is stored in the current session, so it survives `/reload`, resume, and branch navigation.
package/package.json CHANGED
@@ -1,40 +1,41 @@
1
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
- ]
2
+ "name": "@aliaksei-raketski/pi-fast-mode",
3
+ "version": "0.2.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/packages/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": "packages/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
+ "./src/index.ts"
34
+ ]
35
+ },
36
+ "files": [
37
+ "src",
38
+ "README.md",
39
+ "LICENSE"
40
+ ]
40
41
  }
@@ -1,11 +1,10 @@
1
- import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ export const FAST_COMMAND = "fast";
2
+ export const FAST_FLAG = "fast";
3
+ export const FAST_STATUS_KEY = "fast";
4
+ export const FAST_STATE_CUSTOM_TYPE = "fast";
2
5
 
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";
6
+ export const FAST_ON_TEXT = "fast on";
7
+ export const FAST_OFF_TEXT = "fast off";
9
8
 
10
9
  const FAST_SPEED = "fast";
11
10
  const FAST_BETA = "fast-mode-2026-02-01";
@@ -17,41 +16,65 @@ const CLAUDE_API = "anthropic-messages";
17
16
  const OPENAI_PROVIDER = "openai-codex";
18
17
  const OPENAI_API = "openai-codex-responses";
19
18
 
20
- type FastFeature = {
19
+ export type FastModel = {
20
+ provider: string;
21
+ api?: string;
22
+ id: string;
23
+ headers?: Record<string, string>;
24
+ };
25
+
26
+ export type FastContext = {
27
+ model?: FastModel;
28
+ modelRegistry: {
29
+ isUsingOAuth(model: FastModel): boolean;
30
+ };
31
+ };
32
+
33
+ export type FastFeature = {
21
34
  provider: string;
22
35
  api: string;
23
36
  supportedModels: Set<string>;
24
37
  injectionKey: string;
25
38
  injectionValue: string;
26
39
  unsupportedModelMessage: string;
27
- isEligible?: (ctx: ExtensionContext) => string | undefined;
40
+ isEligible?: (ctx: FastContext) => string | undefined;
41
+ };
42
+
43
+ export type FastModeState = {
44
+ enabled: boolean;
28
45
  };
29
46
 
30
- type FastModeState = {
47
+ export type FastStateEntryData = {
31
48
  enabled: boolean;
32
49
  };
33
50
 
34
- type CurrentModelStatus = {
51
+ export type FastSessionEntry = {
52
+ type: string;
53
+ customType?: string;
54
+ data?: unknown;
55
+ };
56
+
57
+ export type CurrentModelStatus = {
35
58
  feature?: FastFeature;
36
59
  isSupported: boolean;
37
60
  reason?: string;
38
61
  };
39
62
 
40
- type PayloadRecord = Record<string, unknown>;
41
-
42
- type HeaderModel = {
43
- headers?: Record<string, string>;
63
+ export type FastStatusView = {
64
+ text: string;
65
+ color: "accent" | "muted";
44
66
  };
45
67
 
46
- const FEATURES: FastFeature[] = [
68
+ type PayloadRecord = Record<string, unknown>;
69
+
70
+ export const FEATURES: FastFeature[] = [
47
71
  {
48
72
  provider: CLAUDE_PROVIDER,
49
73
  api: CLAUDE_API,
50
74
  supportedModels: new Set(["claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8"]),
51
75
  injectionKey: "speed",
52
76
  injectionValue: FAST_SPEED,
53
- unsupportedModelMessage:
54
- "Fast mode is only available for Claude Opus 4.6, 4.7, and 4.8",
77
+ unsupportedModelMessage: "Fast mode is only available for Claude Opus 4.6, 4.7, and 4.8",
55
78
  },
56
79
  {
57
80
  provider: OPENAI_PROVIDER,
@@ -67,29 +90,30 @@ const FEATURES: FastFeature[] = [
67
90
  },
68
91
  ];
69
92
 
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;
93
+ export function createFastModeState(enabled = false): FastModeState {
94
+ return { enabled };
79
95
  }
80
96
 
81
- function isPayloadRecord(payload: unknown): payload is PayloadRecord {
82
- return typeof payload === "object" && payload !== null && !Array.isArray(payload);
97
+ export function createFastStateEntryData(state: FastModeState): FastStateEntryData {
98
+ return { enabled: state.enabled };
83
99
  }
84
100
 
85
- function splitBetaHeader(value: string | undefined): string[] {
86
- return (value ?? "")
87
- .split(",")
88
- .map((value) => value.trim())
89
- .filter(Boolean);
101
+ export function restoreFastModeState(
102
+ entries: Iterable<FastSessionEntry>,
103
+ defaultEnabled = false,
104
+ ): FastModeState {
105
+ let enabled = defaultEnabled;
106
+
107
+ for (const entry of entries) {
108
+ if (entry.type !== "custom" || entry.customType !== FAST_STATE_CUSTOM_TYPE) continue;
109
+ if (!isPayloadRecord(entry.data) || typeof entry.data.enabled !== "boolean") continue;
110
+ enabled = entry.data.enabled;
111
+ }
112
+
113
+ return createFastModeState(enabled);
90
114
  }
91
115
 
92
- function getCurrentModelStatus(ctx: ExtensionContext): CurrentModelStatus {
116
+ export function getCurrentModelStatus(ctx: FastContext): CurrentModelStatus {
93
117
  const model = ctx.model;
94
118
  if (!model) {
95
119
  return {
@@ -110,11 +134,15 @@ function getCurrentModelStatus(ctx: ExtensionContext): CurrentModelStatus {
110
134
  };
111
135
  }
112
136
 
113
- const matchingFeature = featuresForBackend.find((feature) => feature.supportedModels.has(model.id));
137
+ const matchingFeature = featuresForBackend.find((feature) =>
138
+ feature.supportedModels.has(model.id),
139
+ );
114
140
  if (!matchingFeature) {
115
141
  return {
116
142
  isSupported: false,
117
- reason: featuresForBackend[0]?.unsupportedModelMessage ?? "Current model does not support fast mode",
143
+ reason:
144
+ featuresForBackend[0]?.unsupportedModelMessage ??
145
+ "Current model does not support fast mode",
118
146
  };
119
147
  }
120
148
 
@@ -135,15 +163,50 @@ function getCurrentModelStatus(ctx: ExtensionContext): CurrentModelStatus {
135
163
  };
136
164
  }
137
165
 
166
+ export function syncFeatureState(ctx: FastContext, state: FastModeState): CurrentModelStatus {
167
+ const modelStatus = getCurrentModelStatus(ctx);
168
+ syncClaudeBetaHeader(ctx, state, modelStatus);
169
+ return modelStatus;
170
+ }
171
+
172
+ export function getStatusView(
173
+ state: FastModeState,
174
+ modelStatus: CurrentModelStatus,
175
+ ): FastStatusView {
176
+ return {
177
+ text: state.enabled ? FAST_ON_TEXT : FAST_OFF_TEXT,
178
+ color: state.enabled && modelStatus.isSupported ? "accent" : "muted",
179
+ };
180
+ }
181
+
182
+ export function getFastPayload(
183
+ payload: unknown,
184
+ ctx: FastContext,
185
+ state: FastModeState,
186
+ modelStatus: CurrentModelStatus,
187
+ ): PayloadRecord | undefined {
188
+ if (!state.enabled) return undefined;
189
+ if (!modelStatus.isSupported || !modelStatus.feature) return undefined;
190
+ if (!isPayloadRecord(payload)) return undefined;
191
+ if (payload.model !== ctx.model?.id) return undefined;
192
+ if (modelStatus.feature.injectionKey in payload) return undefined;
193
+
194
+ return {
195
+ ...payload,
196
+ [modelStatus.feature.injectionKey]: modelStatus.feature.injectionValue,
197
+ };
198
+ }
199
+
138
200
  function syncClaudeBetaHeader(
139
- ctx: ExtensionContext,
201
+ ctx: FastContext,
140
202
  state: FastModeState,
141
203
  modelStatus: CurrentModelStatus,
142
204
  ): void {
143
- const model = ctx.model as (typeof ctx.model & HeaderModel) | undefined;
205
+ const model = ctx.model;
144
206
  if (!model || model.provider !== CLAUDE_PROVIDER || model.api !== CLAUDE_API) return;
145
207
 
146
- const shouldEnable = state.enabled && modelStatus.isSupported && modelStatus.feature?.provider === CLAUDE_PROVIDER;
208
+ const shouldEnable =
209
+ state.enabled && modelStatus.isSupported && modelStatus.feature?.provider === CLAUDE_PROVIDER;
147
210
  const headers = { ...(model.headers ?? {}) };
148
211
  const existing = splitBetaHeader(headers["anthropic-beta"] ?? headers["Anthropic-Beta"]);
149
212
  const requiredBase = ctx.modelRegistry.isUsingOAuth(model) ? CLAUDE_CODE_OAUTH_BETAS : [];
@@ -160,97 +223,13 @@ function syncClaudeBetaHeader(
160
223
  model.headers = headers;
161
224
  }
162
225
 
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
- };
226
+ function splitBetaHeader(value: string | undefined): string[] {
227
+ return (value ?? "")
228
+ .split(",")
229
+ .map((value) => value.trim())
230
+ .filter(Boolean);
196
231
  }
197
232
 
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
- });
233
+ function isPayloadRecord(payload: unknown): payload is PayloadRecord {
234
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload);
256
235
  }
package/src/index.ts ADDED
@@ -0,0 +1,122 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ FAST_COMMAND,
4
+ FAST_FLAG,
5
+ FAST_STATE_CUSTOM_TYPE,
6
+ FAST_STATUS_KEY,
7
+ createFastModeState,
8
+ createFastStateEntryData,
9
+ getFastPayload,
10
+ getStatusView,
11
+ restoreFastModeState,
12
+ syncFeatureState,
13
+ type FastContext,
14
+ type FastModeState,
15
+ type FastModel,
16
+ type CurrentModelStatus,
17
+ } from "./core.ts";
18
+
19
+ const sessionStates = new WeakMap<object, FastModeState>();
20
+
21
+ function getSessionState(ctx: ExtensionContext): FastModeState {
22
+ let state = sessionStates.get(ctx.sessionManager);
23
+ if (!state) {
24
+ state = createFastModeState();
25
+ sessionStates.set(ctx.sessionManager, state);
26
+ }
27
+ return state;
28
+ }
29
+
30
+ function restoreSessionState(ctx: ExtensionContext, defaultEnabled: boolean): FastModeState {
31
+ const state = restoreFastModeState(ctx.sessionManager.getBranch(), defaultEnabled);
32
+ sessionStates.set(ctx.sessionManager, state);
33
+ return state;
34
+ }
35
+
36
+ function toFastContext(ctx: ExtensionContext): FastContext {
37
+ return {
38
+ model: ctx.model as FastModel | undefined,
39
+ modelRegistry: {
40
+ isUsingOAuth: (model) =>
41
+ ctx.modelRegistry.isUsingOAuth(model as NonNullable<typeof ctx.model>),
42
+ },
43
+ };
44
+ }
45
+
46
+ function updateStatus(
47
+ ctx: ExtensionContext,
48
+ state: FastModeState,
49
+ modelStatus: CurrentModelStatus,
50
+ ): void {
51
+ if (!ctx.hasUI) return;
52
+
53
+ const status = getStatusView(state, modelStatus);
54
+ ctx.ui.setStatus(FAST_STATUS_KEY, ctx.ui.theme.fg(status.color, status.text));
55
+ }
56
+
57
+ export default function fastMode(pi: ExtensionAPI) {
58
+ pi.registerFlag(FAST_FLAG, {
59
+ description: "Start with fast mode enabled",
60
+ type: "boolean",
61
+ default: false,
62
+ });
63
+
64
+ pi.on("session_start", (_event, ctx) => {
65
+ const state = restoreSessionState(ctx, pi.getFlag(FAST_FLAG) === true);
66
+ const modelStatus = syncFeatureState(toFastContext(ctx), state);
67
+ updateStatus(ctx, state, modelStatus);
68
+ });
69
+
70
+ pi.on("model_select", (_event, ctx) => {
71
+ const state = getSessionState(ctx);
72
+ const modelStatus = syncFeatureState(toFastContext(ctx), state);
73
+ updateStatus(ctx, state, modelStatus);
74
+ });
75
+
76
+ pi.on("session_tree", (_event, ctx) => {
77
+ const state = restoreSessionState(ctx, pi.getFlag(FAST_FLAG) === true);
78
+ const modelStatus = syncFeatureState(toFastContext(ctx), state);
79
+ updateStatus(ctx, state, modelStatus);
80
+ });
81
+
82
+ pi.on("before_provider_request", (event, ctx) => {
83
+ const state = getSessionState(ctx);
84
+ const fastContext = toFastContext(ctx);
85
+ const modelStatus = syncFeatureState(fastContext, state);
86
+ updateStatus(ctx, state, modelStatus);
87
+ return getFastPayload(event.payload, fastContext, state, modelStatus);
88
+ });
89
+
90
+ pi.on("session_shutdown", (_event, ctx) => {
91
+ if (!ctx.hasUI) return;
92
+ ctx.ui.setStatus(FAST_STATUS_KEY, undefined);
93
+ });
94
+
95
+ pi.registerCommand(FAST_COMMAND, {
96
+ description: "Toggle fast mode",
97
+ getArgumentCompletions: () => null,
98
+ handler: async (args, ctx) => {
99
+ if (args.trim()) {
100
+ ctx.ui.notify("Usage: /fast", "warning");
101
+ return;
102
+ }
103
+
104
+ const state = getSessionState(ctx);
105
+ state.enabled = !state.enabled;
106
+ pi.appendEntry(FAST_STATE_CUSTOM_TYPE, createFastStateEntryData(state));
107
+
108
+ const modelStatus = syncFeatureState(toFastContext(ctx), state);
109
+ updateStatus(ctx, state, modelStatus);
110
+
111
+ ctx.ui.notify(`Fast mode is now ${state.enabled ? "on" : "off"}.`, "info");
112
+
113
+ if (state.enabled && !modelStatus.isSupported) {
114
+ const detail = modelStatus.reason ? ` (${modelStatus.reason})` : "";
115
+ ctx.ui.notify(
116
+ `Current model is not supported for fast mode${detail}. Fast mode will apply automatically once you switch to a supported model.`,
117
+ "warning",
118
+ );
119
+ }
120
+ },
121
+ });
122
+ }