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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vuri Huang
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 ADDED
@@ -0,0 +1,302 @@
1
+ # pi-fast-mode
2
+
3
+ <p align="center">
4
+ <img src="./showcase.png" alt="pi-fast-mode showcase" width="960" />
5
+ </p>
6
+
7
+ `pi-fast-mode` is a pi extension/package that toggles fast mode for selected models by injecting `service_tier` into provider requests.
8
+
9
+ It follows the same packaging approach as `pi-hodor`:
10
+
11
+ - normal pi package structure
12
+ - bundled default config
13
+ - optional global config bootstrap command
14
+ - project/global/bundled config resolution
15
+ - persistent per-session and per-branch on/off state
16
+
17
+ ## What it does
18
+
19
+ When fast mode is enabled and the current `provider/model` matches a configured target, the extension patches the outgoing provider payload to include:
20
+
21
+ ```json
22
+ {
23
+ "service_tier": "priority"
24
+ }
25
+ ```
26
+
27
+ This is useful when you want a lightweight toggle in pi without changing your provider or model definitions.
28
+
29
+ ## Features
30
+
31
+ - `/fast` toggle command
32
+ - `/fast on|off|status|reload`
33
+ - `Ctrl+Shift+F` keyboard shortcut
34
+ - `--fast` CLI flag for starting a session with fast mode enabled
35
+ - supports custom provider names and custom model ids
36
+ - supports custom `serviceTier` values per target
37
+ - remembers the last on/off state when the session is resumed
38
+ - restores the saved state when navigating branches with `/tree`
39
+ - shows status in the footer while fast mode is active
40
+ - supports project-local, global, legacy-global, and bundled config files
41
+
42
+ ## Requirements
43
+
44
+ - pi with extension support
45
+ - Node.js 20+
46
+
47
+ ## Installation
48
+
49
+ ### Install from npm
50
+
51
+ ```bash
52
+ pi install npm:pi-fast-mode
53
+ ```
54
+
55
+ ### Install from git
56
+
57
+ ```bash
58
+ pi install git:github.com/vurihuang/pi-fast-mode
59
+ ```
60
+
61
+ ### Install from a local path
62
+
63
+ ```bash
64
+ pi install /absolute/path/to/pi-fast-mode
65
+ ```
66
+
67
+ Restart pi after installation so the extension is loaded.
68
+
69
+ ## Quick start
70
+
71
+ ### 1. Bootstrap the global config
72
+
73
+ ```text
74
+ /pi-fast-mode:setup
75
+ ```
76
+
77
+ This creates:
78
+
79
+ ```text
80
+ ~/.pi/agent/extensions/pi-fast-mode/config.json
81
+ ```
82
+
83
+ if it does not already exist.
84
+
85
+ ### 2. Edit the config
86
+
87
+ Example:
88
+
89
+ ```json
90
+ {
91
+ "targets": [
92
+ {
93
+ "provider": "openai-codex",
94
+ "model": "gpt-5.4",
95
+ "serviceTier": "priority"
96
+ },
97
+ {
98
+ "provider": "my-proxy",
99
+ "model": "gpt-5-4",
100
+ "serviceTier": "priority"
101
+ }
102
+ ]
103
+ }
104
+ ```
105
+
106
+ ### 3. Toggle fast mode
107
+
108
+ ```text
109
+ /fast
110
+ ```
111
+
112
+ ## Usage
113
+
114
+ ### Slash command
115
+
116
+ ```text
117
+ /fast
118
+ ```
119
+
120
+ ### Explicit control
121
+
122
+ ```text
123
+ /fast on
124
+ /fast off
125
+ /fast status
126
+ /fast reload
127
+ ```
128
+
129
+ ### Keyboard shortcut
130
+
131
+ ```text
132
+ Ctrl+Shift+F
133
+ ```
134
+
135
+ ### CLI flag
136
+
137
+ ```bash
138
+ pi --fast
139
+ ```
140
+
141
+ `--fast` makes the current session start with fast mode enabled, regardless of the previously saved state.
142
+
143
+ ## Configuration
144
+
145
+ Config is resolved in this order:
146
+
147
+ 1. `./.pi-fast-mode.json`
148
+ 2. `./.pi/pi-fast-mode.json`
149
+ 3. `~/.pi/agent/extensions/pi-fast-mode/config.json`
150
+ 4. legacy fallback: `~/.pi/agent/extensions/fast-mode.json`
151
+ 5. bundled `config.json`
152
+
153
+ That means:
154
+
155
+ - project config overrides global config
156
+ - global config overrides the bundled defaults
157
+ - the legacy single-file path still works as a compatibility fallback
158
+
159
+ ### Config schema
160
+
161
+ ```json
162
+ {
163
+ "targets": [
164
+ {
165
+ "provider": "openai-codex",
166
+ "model": "gpt-5.4",
167
+ "serviceTier": "priority"
168
+ }
169
+ ]
170
+ }
171
+ ```
172
+
173
+ ### Fields
174
+
175
+ | Field | Type | Description |
176
+ | --- | --- | --- |
177
+ | `targets` | `FastTarget[]` | Allowlist of provider/model pairs that should receive `service_tier`. |
178
+ | `targets[].provider` | `string` | Exact pi provider name. Official and unofficial provider names are both supported. |
179
+ | `targets[].model` | `string` | Exact pi model id. Official and unofficial model ids are both supported. |
180
+ | `targets[].serviceTier` | `string` | Value written as `service_tier`. Defaults to `priority` when omitted. |
181
+
182
+ ### Matching behavior
183
+
184
+ Matching is done with exact string equality against:
185
+
186
+ - `ctx.model.provider`
187
+ - `ctx.model.id`
188
+
189
+ So this works with:
190
+
191
+ - built-in providers and models
192
+ - providers added via `models.json`
193
+ - providers registered through other extensions
194
+ - unofficial model names
195
+
196
+ ### Example configs
197
+
198
+ #### Default Codex target
199
+
200
+ ```json
201
+ {
202
+ "targets": [
203
+ {
204
+ "provider": "openai-codex",
205
+ "model": "gpt-5.4"
206
+ }
207
+ ]
208
+ }
209
+ ```
210
+
211
+ #### Multiple custom providers
212
+
213
+ ```json
214
+ {
215
+ "targets": [
216
+ {
217
+ "provider": "my-proxy",
218
+ "model": "gpt-5.4",
219
+ "serviceTier": "priority"
220
+ },
221
+ {
222
+ "provider": "openrouter",
223
+ "model": "openai/gpt-5.4",
224
+ "serviceTier": "priority"
225
+ },
226
+ {
227
+ "provider": "local-gateway",
228
+ "model": "gpt-5.4",
229
+ "serviceTier": "priority"
230
+ }
231
+ ]
232
+ }
233
+ ```
234
+
235
+ ## Persistence behavior
236
+
237
+ Fast mode state is stored in the session as custom entries.
238
+
239
+ That means:
240
+
241
+ - if you turn fast mode on, quit pi, and resume the same session, it comes back on
242
+ - if you turn it off and resume the same session, it stays off
243
+ - if you switch branches with `/tree`, the extension restores the saved state for that branch
244
+
245
+ This persistence is session-aware and branch-aware.
246
+
247
+ ## Notes and limitations
248
+
249
+ - The extension only patches request payloads when fast mode is enabled.
250
+ - It only patches requests for configured provider/model pairs.
251
+ - It does not validate whether a provider actually supports `service_tier`.
252
+ - If a provider ignores unknown fields, the request will continue normally.
253
+ - `/fast reload` reloads config from disk without restarting pi.
254
+
255
+ ## Development
256
+
257
+ Install dependencies:
258
+
259
+ ```bash
260
+ npm install
261
+ ```
262
+
263
+ Run type-check:
264
+
265
+ ```bash
266
+ npm run check
267
+ ```
268
+
269
+ Run release verification:
270
+
271
+ ```bash
272
+ npm run release:check
273
+ ```
274
+
275
+ Preview the package contents:
276
+
277
+ ```bash
278
+ npm run pack:check
279
+ ```
280
+
281
+ ## Publishing checklist
282
+
283
+ Before publishing:
284
+
285
+ 1. update `version` in `package.json`
286
+ 2. verify `repository`, `homepage`, and `bugs` URLs
287
+ 3. run `npm run release:check`
288
+ 4. confirm the tarball only contains intended files
289
+ 5. publish with npm if desired
290
+
291
+ ## Package structure
292
+
293
+ ```text
294
+ .
295
+ ├── config.json
296
+ ├── index.ts
297
+ ├── LICENSE
298
+ ├── README.md
299
+ ├── package.json
300
+ ├── package-lock.json
301
+ └── tsconfig.json
302
+ ```
package/config.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "provider": "openai-codex",
5
+ "model": "gpt-5.4",
6
+ "serviceTier": "priority"
7
+ }
8
+ ]
9
+ }
package/index.ts ADDED
@@ -0,0 +1,370 @@
1
+ import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
4
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
5
+ import { Key } from "@mariozechner/pi-tui";
6
+
7
+ type NotifyLevel = "info" | "warning" | "error";
8
+ type ActiveModel = ExtensionContext["model"];
9
+
10
+ type FastModeState = {
11
+ enabled?: boolean;
12
+ };
13
+
14
+ type FastTarget = {
15
+ provider: string;
16
+ model: string;
17
+ serviceTier?: string;
18
+ };
19
+
20
+ type FastModeConfig = {
21
+ targets: FastTarget[];
22
+ };
23
+
24
+ const EXTENSION_NAME = "pi-fast-mode";
25
+ const ENTRY_TYPE = "fast-mode";
26
+ const STATUS_ID = "fast-mode";
27
+ const BUNDLED_CONFIG_PATH = join(__dirname, "config.json");
28
+ const GLOBAL_CONFIG_PATH = join(getAgentDir(), "extensions", EXTENSION_NAME, "config.json");
29
+ const LEGACY_GLOBAL_CONFIG_PATH = join(getAgentDir(), "extensions", "fast-mode.json");
30
+ const PROJECT_CONFIG_CANDIDATES = [
31
+ ".pi-fast-mode.json",
32
+ join(".pi", "pi-fast-mode.json"),
33
+ ] as const;
34
+ const DEFAULT_CONFIG: FastModeConfig = {
35
+ targets: [{ provider: "openai-codex", model: "gpt-5.4", serviceTier: "priority" }],
36
+ };
37
+
38
+ function isRecord(value: unknown): value is Record<string, unknown> {
39
+ return typeof value === "object" && value !== null && !Array.isArray(value);
40
+ }
41
+
42
+ function getModelLabel(model: ActiveModel): string {
43
+ return model ? `${model.provider}/${model.id}` : "no model selected";
44
+ }
45
+
46
+ function normalizeTarget(raw: unknown): FastTarget | undefined {
47
+ if (!isRecord(raw)) return undefined;
48
+
49
+ const provider = typeof raw.provider === "string" ? raw.provider.trim() : "";
50
+ const model = typeof raw.model === "string" ? raw.model.trim() : "";
51
+ const serviceTier =
52
+ typeof raw.serviceTier === "string"
53
+ ? raw.serviceTier.trim()
54
+ : typeof raw.service_tier === "string"
55
+ ? raw.service_tier.trim()
56
+ : "";
57
+
58
+ if (!provider || !model) return undefined;
59
+ return {
60
+ provider,
61
+ model,
62
+ ...(serviceTier ? { serviceTier } : {}),
63
+ };
64
+ }
65
+
66
+ function normalizeConfig(raw: unknown): FastModeConfig {
67
+ if (!isRecord(raw) || !Array.isArray(raw.targets)) {
68
+ return { targets: [...DEFAULT_CONFIG.targets] };
69
+ }
70
+
71
+ const targets = raw.targets
72
+ .map((target) => normalizeTarget(target))
73
+ .filter((target): target is FastTarget => target !== undefined);
74
+
75
+ return { targets };
76
+ }
77
+
78
+ function dedupeTargets(targets: FastTarget[]): FastTarget[] {
79
+ const byKey = new Map<string, FastTarget>();
80
+ for (const target of targets) {
81
+ byKey.set(`${target.provider}/${target.model}`, target);
82
+ }
83
+ return [...byKey.values()];
84
+ }
85
+
86
+ function getMatchingTarget(model: ActiveModel, targets: FastTarget[]): FastTarget | undefined {
87
+ if (!model) return undefined;
88
+ return targets.find((target) => target.provider === model.provider && target.model === model.id);
89
+ }
90
+
91
+ function getSavedStateFromBranch(ctx: ExtensionContext): boolean | undefined {
92
+ const entries = ctx.sessionManager.getBranch();
93
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
94
+ const entry = entries[i];
95
+ if (entry.type !== "custom" || entry.customType !== ENTRY_TYPE || !isRecord(entry.data)) continue;
96
+ const enabled = (entry.data as FastModeState).enabled;
97
+ if (typeof enabled === "boolean") return enabled;
98
+ }
99
+ return undefined;
100
+ }
101
+
102
+ function safeNotify(ctx: ExtensionContext, message: string, level: NotifyLevel): void {
103
+ if (!ctx.hasUI) return;
104
+ ctx.ui.notify(message, level);
105
+ }
106
+
107
+ async function pathExists(path: string): Promise<boolean> {
108
+ try {
109
+ await access(path);
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ async function ensureBundledConfigFile(): Promise<void> {
117
+ try {
118
+ await access(BUNDLED_CONFIG_PATH);
119
+ } catch {
120
+ await writeFile(BUNDLED_CONFIG_PATH, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`, "utf8");
121
+ }
122
+ }
123
+
124
+ async function resolveConfigPath(cwd: string): Promise<string> {
125
+ for (const relativePath of PROJECT_CONFIG_CANDIDATES) {
126
+ const candidatePath = join(cwd, relativePath);
127
+ if (await pathExists(candidatePath)) return candidatePath;
128
+ }
129
+
130
+ if (await pathExists(GLOBAL_CONFIG_PATH)) return GLOBAL_CONFIG_PATH;
131
+ if (await pathExists(LEGACY_GLOBAL_CONFIG_PATH)) return LEGACY_GLOBAL_CONFIG_PATH;
132
+ return BUNDLED_CONFIG_PATH;
133
+ }
134
+
135
+ async function copyBundledConfig(destinationPath: string): Promise<void> {
136
+ await ensureBundledConfigFile();
137
+ await mkdir(dirname(destinationPath), { recursive: true });
138
+ await copyFile(BUNDLED_CONFIG_PATH, destinationPath);
139
+ }
140
+
141
+ export default function fastModeExtension(pi: ExtensionAPI): void {
142
+ let fastModeEnabled = false;
143
+ let currentModel: ActiveModel;
144
+ let configuredTargets: FastTarget[] = [...DEFAULT_CONFIG.targets];
145
+ let resolvedConfigPath = BUNDLED_CONFIG_PATH;
146
+ const lastConfigError: { value?: string } = {};
147
+
148
+ function activeModel(ctx?: ExtensionContext): ActiveModel {
149
+ return currentModel ?? ctx?.model;
150
+ }
151
+
152
+ function configuredTargetsText(): string {
153
+ return configuredTargets.length > 0
154
+ ? configuredTargets.map((target) => `${target.provider}/${target.model}`).join(", ")
155
+ : "none";
156
+ }
157
+
158
+ async function refreshConfig(cwd: string, ctx?: ExtensionContext): Promise<void> {
159
+ await ensureBundledConfigFile();
160
+ const configPath = await resolveConfigPath(cwd);
161
+ resolvedConfigPath = configPath;
162
+
163
+ try {
164
+ const parsed = normalizeConfig(JSON.parse(await readFile(configPath, "utf8")));
165
+ configuredTargets = dedupeTargets(parsed.targets);
166
+ lastConfigError.value = undefined;
167
+ } catch (error) {
168
+ configuredTargets = [...DEFAULT_CONFIG.targets];
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ const errorKey = `${configPath}:${message}`;
171
+ if (lastConfigError.value !== errorKey) {
172
+ lastConfigError.value = errorKey;
173
+ if (ctx) {
174
+ safeNotify(
175
+ ctx,
176
+ `[${EXTENSION_NAME}] Failed to read config from ${configPath}. Falling back to bundled defaults: ${message}`,
177
+ "warning",
178
+ );
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ function statusText(ctx?: ExtensionContext): string {
185
+ const model = activeModel(ctx);
186
+ if (!fastModeEnabled) {
187
+ return `Fast mode is OFF. Config: ${resolvedConfigPath}`;
188
+ }
189
+
190
+ const target = getMatchingTarget(model, configuredTargets);
191
+ if (target) {
192
+ return `Fast mode is ON for ${getModelLabel(model)} (service_tier=${target.serviceTier ?? "priority"}). Config: ${resolvedConfigPath}`;
193
+ }
194
+
195
+ if (configuredTargets.length === 0) {
196
+ return `Fast mode is ON, but no targets are configured in ${resolvedConfigPath}.`;
197
+ }
198
+
199
+ return `Fast mode is ON, but ${getModelLabel(model)} is not enabled in ${resolvedConfigPath}. Enabled targets: ${configuredTargetsText()}.`;
200
+ }
201
+
202
+ function updateStatus(ctx: ExtensionContext): void {
203
+ if (!ctx.hasUI) return;
204
+ if (!fastModeEnabled) {
205
+ ctx.ui.setStatus(STATUS_ID, undefined);
206
+ return;
207
+ }
208
+
209
+ const target = getMatchingTarget(activeModel(ctx), configuredTargets);
210
+ if (target) {
211
+ ctx.ui.setStatus(STATUS_ID, ctx.ui.theme.fg("accent", "⚡ fast"));
212
+ return;
213
+ }
214
+
215
+ ctx.ui.setStatus(STATUS_ID, ctx.ui.theme.fg("warning", "⚡ fast*"));
216
+ }
217
+
218
+ function persistState(): void {
219
+ pi.appendEntry<FastModeState>(ENTRY_TYPE, { enabled: fastModeEnabled });
220
+ }
221
+
222
+ function notifyState(ctx: ExtensionContext): void {
223
+ safeNotify(
224
+ ctx,
225
+ statusText(ctx),
226
+ fastModeEnabled && !getMatchingTarget(activeModel(ctx), configuredTargets) ? "warning" : "info",
227
+ );
228
+ }
229
+
230
+ function applyEnabledState(enabled: boolean, ctx: ExtensionContext, options?: { notify?: boolean; persist?: boolean }): void {
231
+ fastModeEnabled = enabled;
232
+ if (options?.persist !== false) persistState();
233
+ updateStatus(ctx);
234
+ if (options?.notify !== false) notifyState(ctx);
235
+ }
236
+
237
+ function restoreEnabledState(
238
+ ctx: ExtensionContext,
239
+ options?: { fallback?: boolean; preserveCurrentIfMissing?: boolean },
240
+ ): void {
241
+ const savedState = getSavedStateFromBranch(ctx);
242
+ if (typeof savedState === "boolean") {
243
+ applyEnabledState(savedState, ctx, { notify: false, persist: false });
244
+ return;
245
+ }
246
+
247
+ if (!options?.preserveCurrentIfMissing && typeof options?.fallback === "boolean") {
248
+ applyEnabledState(options.fallback, ctx, { notify: false, persist: false });
249
+ return;
250
+ }
251
+
252
+ updateStatus(ctx);
253
+ }
254
+
255
+ function toggleFastMode(ctx: ExtensionContext): void {
256
+ applyEnabledState(!fastModeEnabled, ctx);
257
+ }
258
+
259
+ pi.registerFlag("fast", {
260
+ description: `Start with fast mode enabled. Targets are loaded from project config, ${GLOBAL_CONFIG_PATH}, or the bundled config.`,
261
+ type: "boolean",
262
+ default: false,
263
+ });
264
+
265
+ pi.registerCommand("pi-fast-mode:setup", {
266
+ description: `Copy the default ${EXTENSION_NAME} config to ${GLOBAL_CONFIG_PATH}`,
267
+ handler: async (_args, ctx) => {
268
+ if (await pathExists(GLOBAL_CONFIG_PATH)) {
269
+ safeNotify(ctx, `[${EXTENSION_NAME}] Config already exists at ${GLOBAL_CONFIG_PATH}`, "warning");
270
+ return;
271
+ }
272
+
273
+ await copyBundledConfig(GLOBAL_CONFIG_PATH);
274
+ safeNotify(ctx, `[${EXTENSION_NAME}] Config copied to ${GLOBAL_CONFIG_PATH}`, "info");
275
+ },
276
+ });
277
+
278
+ pi.registerCommand("fast", {
279
+ description: "Toggle fast mode. Usage: /fast [on|off|status|reload]",
280
+ handler: async (args, ctx) => {
281
+ const action = args.trim().toLowerCase();
282
+ switch (action) {
283
+ case "":
284
+ case "toggle":
285
+ toggleFastMode(ctx);
286
+ return;
287
+ case "on":
288
+ case "enable":
289
+ if (fastModeEnabled) {
290
+ notifyState(ctx);
291
+ return;
292
+ }
293
+ applyEnabledState(true, ctx);
294
+ return;
295
+ case "off":
296
+ case "disable":
297
+ if (!fastModeEnabled) {
298
+ notifyState(ctx);
299
+ return;
300
+ }
301
+ applyEnabledState(false, ctx);
302
+ return;
303
+ case "status":
304
+ notifyState(ctx);
305
+ return;
306
+ case "reload":
307
+ await refreshConfig(ctx.cwd, ctx);
308
+ updateStatus(ctx);
309
+ safeNotify(
310
+ ctx,
311
+ `[${EXTENSION_NAME}] Reloaded targets from ${resolvedConfigPath}. Enabled targets: ${configuredTargetsText()}.`,
312
+ "info",
313
+ );
314
+ return;
315
+ default:
316
+ safeNotify(ctx, "Usage: /fast [on|off|status|reload]", "warning");
317
+ }
318
+ },
319
+ });
320
+
321
+ pi.registerShortcut(Key.ctrlShift("f"), {
322
+ description: "Toggle fast mode",
323
+ handler: async (ctx) => {
324
+ toggleFastMode(ctx);
325
+ },
326
+ });
327
+
328
+ pi.on("session_start", async (_event, ctx) => {
329
+ currentModel = ctx.model;
330
+ await refreshConfig(ctx.cwd, ctx);
331
+
332
+ if (pi.getFlag("fast") === true) {
333
+ applyEnabledState(true, ctx, { notify: false, persist: false });
334
+ return;
335
+ }
336
+
337
+ restoreEnabledState(ctx, { fallback: false, preserveCurrentIfMissing: false });
338
+ });
339
+
340
+ pi.on("session_tree", async (_event, ctx) => {
341
+ restoreEnabledState(ctx, { preserveCurrentIfMissing: true });
342
+ });
343
+
344
+ pi.on("session_shutdown", async (_event, ctx) => {
345
+ if (getSavedStateFromBranch(ctx) !== fastModeEnabled) {
346
+ persistState();
347
+ }
348
+ if (ctx.hasUI) ctx.ui.setStatus(STATUS_ID, undefined);
349
+ });
350
+
351
+ pi.on("model_select", async (event, ctx) => {
352
+ currentModel = event.model;
353
+ updateStatus(ctx);
354
+ });
355
+
356
+ pi.on("before_provider_request", (event, ctx) => {
357
+ if (!fastModeEnabled) return;
358
+ const target = getMatchingTarget(activeModel(ctx), configuredTargets);
359
+ if (!target) return;
360
+ if (!isRecord(event.payload)) return;
361
+ if (typeof event.payload.model !== "string") return;
362
+
363
+ const serviceTier = target.serviceTier ?? "priority";
364
+ if (event.payload.service_tier === serviceTier) return;
365
+ return {
366
+ ...event.payload,
367
+ service_tier: serviceTier,
368
+ };
369
+ });
370
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "pi-fast-mode",
3
+ "version": "0.1.0",
4
+ "description": "Persistent fast-mode toggle for pi that injects service_tier for configured provider/model pairs.",
5
+ "type": "module",
6
+ "author": "Vuri Huang",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi-extension",
10
+ "pi",
11
+ "extension",
12
+ "fast-mode",
13
+ "service-tier",
14
+ "provider-payload",
15
+ "codex"
16
+ ],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/vurihuang/pi-fast-mode.git"
21
+ },
22
+ "homepage": "https://github.com/vurihuang/pi-fast-mode#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/vurihuang/pi-fast-mode/issues"
25
+ },
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "files": [
30
+ "index.ts",
31
+ "config.json",
32
+ "README.md",
33
+ "LICENSE"
34
+ ],
35
+ "scripts": {
36
+ "check": "tsc --noEmit",
37
+ "pack:check": "npm pack --dry-run",
38
+ "release:check": "npm run check && npm run pack:check",
39
+ "prepublishOnly": "npm run release:check"
40
+ },
41
+ "pi": {
42
+ "extensions": [
43
+ "./index.ts"
44
+ ]
45
+ },
46
+ "peerDependencies": {
47
+ "@mariozechner/pi-coding-agent": "*"
48
+ },
49
+ "devDependencies": {
50
+ "@mariozechner/pi-coding-agent": "*",
51
+ "@types/node": "^24.5.2",
52
+ "typescript": "^5.9.2"
53
+ }
54
+ }