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 +21 -0
- package/README.md +302 -0
- package/config.json +9 -0
- package/index.ts +370 -0
- package/package.json +54 -0
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
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
|
+
}
|