@benvargas/pi-openai-fast 1.0.0 → 1.0.2

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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # @benvargas/pi-openai-fast
2
2
 
3
- Session-scoped `/fast` toggle for pi that enables OpenAI priority service tier on supported GPT-5.4 models.
3
+ `/fast` toggle for pi that enables OpenAI priority service tier on configured models.
4
4
 
5
- This extension does not change the model, thinking level, tools, or prompts. It only adds `service_tier=priority` to provider requests when fast mode is active and the current model supports it.
5
+ This extension does not change the model, thinking level, tools, or prompts. It only adds `service_tier=priority` to provider requests when fast mode is active and the current model matches the configured supported-model list.
6
6
 
7
7
  Requires pi `0.57.0` or newer.
8
8
 
@@ -25,6 +25,8 @@ pi -e npm:@benvargas/pi-openai-fast
25
25
  - `/fast off` explicitly disables fast mode.
26
26
  - `/fast status` reports the current fast-mode state.
27
27
  - `--fast` starts the session with fast mode enabled.
28
+ - By default, fast mode persists across new pi sessions via a JSON config file.
29
+ - Startup state comes from the selected config file, not from resumed session/thread history.
28
30
 
29
31
  Example:
30
32
 
@@ -32,17 +34,41 @@ Example:
32
34
  pi -e npm:@benvargas/pi-openai-fast --fast
33
35
  ```
34
36
 
35
- ## Supported Models
37
+ ## Config
36
38
 
37
- - `openai/gpt-5.4`
38
- - `openai-codex/gpt-5.4`
39
+ Config files follow the same project-over-global pattern as the other packages:
39
40
 
40
- If fast mode is enabled on an unsupported model, the setting stays on but requests are left unchanged until you switch back to a supported model.
41
+ - Project: `<repo>/.pi/extensions/pi-openai-fast.json`
42
+ - Global: `~/.pi/agent/extensions/pi-openai-fast.json`
43
+
44
+ If neither exists, the extension writes a default global config on first run.
45
+
46
+ Default config:
47
+
48
+ ```json
49
+ {
50
+ "persistState": true,
51
+ "active": false,
52
+ "supportedModels": [
53
+ "openai/gpt-5.4",
54
+ "openai-codex/gpt-5.4"
55
+ ]
56
+ }
57
+ ```
58
+
59
+ Settings:
60
+
61
+ - `persistState`: when `true`, `/fast` writes the current on/off state to config so it resumes in new pi sessions. Default: `true`.
62
+ - `active`: persisted fast-mode state used on startup when `persistState` is enabled.
63
+ - `supportedModels`: list of `provider/model-id` strings that should receive `service_tier=priority`.
64
+
65
+ Project config overrides global config. `/fast on` and `/fast off` write to the selected config file, so if a project config exists the remembered state is project-specific. If fast mode is enabled on a model that is not in `supportedModels`, the setting stays on but requests are left unchanged until you switch back to a configured model.
41
66
 
42
67
  ## Notes
43
68
 
44
- - Fast mode is stored as session state, so it persists with the session branch.
45
- - On supported models, fast mode maps to OpenAI `service_tier=priority`.
69
+ - When `persistState` is enabled, the last `/fast` setting also carries across brand-new pi sessions.
70
+ - Resumed sessions do not override the config-backed startup state.
71
+ - On configured models, fast mode maps to OpenAI `service_tier=priority`.
46
72
 
47
73
  ## Uninstall
48
74
 
@@ -1,44 +1,200 @@
1
+ /**
2
+ * OpenAI fast mode for pi.
3
+ *
4
+ * `/fast` and `--fast` toggle `service_tier=priority` for configured models.
5
+ * This extension does not change the selected model, thinking level, tools, or prompts.
6
+ *
7
+ * Startup state comes from `pi-openai-fast.json`, not resumed session history.
8
+ * Config precedence is project `.pi/extensions/pi-openai-fast.json` over
9
+ * global `~/.pi/agent/extensions/pi-openai-fast.json`.
10
+ *
11
+ * `supportedModels` controls which `provider/model-id` pairs receive the flag.
12
+ */
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { dirname, join } from "node:path";
1
16
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
17
 
3
18
  const FAST_COMMAND = "fast";
4
19
  const FAST_FLAG = "fast";
5
- const FAST_STATE_ENTRY = "pi-openai-fast.state";
20
+ const FAST_CONFIG_BASENAME = "pi-openai-fast.json";
6
21
  const FAST_COMMAND_ARGS = ["on", "off", "status"] as const;
7
22
  const FAST_SERVICE_TIER = "priority";
8
- const FAST_SUPPORTED_MODELS = [
9
- { provider: "openai", id: "gpt-5.4" },
10
- { provider: "openai-codex", id: "gpt-5.4" },
11
- ] as const;
23
+ const DEFAULT_SUPPORTED_MODEL_KEYS = ["openai/gpt-5.4", "openai-codex/gpt-5.4"] as const;
12
24
 
13
25
  interface FastModeState {
14
26
  active: boolean;
15
27
  }
16
28
 
29
+ interface FastSupportedModel {
30
+ provider: string;
31
+ id: string;
32
+ }
33
+
34
+ interface FastConfigFile {
35
+ persistState?: boolean;
36
+ active?: boolean;
37
+ supportedModels?: string[];
38
+ }
39
+
40
+ interface ResolvedFastConfig {
41
+ configPath: string;
42
+ persistState: boolean;
43
+ active: boolean | undefined;
44
+ supportedModels: FastSupportedModel[];
45
+ }
46
+
17
47
  type FastPayload = {
18
48
  service_tier?: string;
19
49
  [key: string]: unknown;
20
50
  };
21
51
 
52
+ const DEFAULT_CONFIG_FILE: FastConfigFile = {
53
+ persistState: true,
54
+ active: false,
55
+ supportedModels: [...DEFAULT_SUPPORTED_MODEL_KEYS],
56
+ };
57
+
22
58
  function isRecord(value: unknown): value is Record<string, unknown> {
23
59
  return typeof value === "object" && value !== null && !Array.isArray(value);
24
60
  }
25
61
 
26
- function parseFastModeState(value: unknown): FastModeState | undefined {
27
- if (!isRecord(value) || typeof value.active !== "boolean") {
62
+ function getConfigCwd(ctx: ExtensionContext): string {
63
+ return ctx.cwd || process.cwd();
64
+ }
65
+
66
+ function getConfigPaths(
67
+ cwd: string,
68
+ homeDir: string = homedir(),
69
+ ): {
70
+ projectConfigPath: string;
71
+ globalConfigPath: string;
72
+ } {
73
+ return {
74
+ projectConfigPath: join(cwd, ".pi", "extensions", FAST_CONFIG_BASENAME),
75
+ globalConfigPath: join(homeDir, ".pi", "agent", "extensions", FAST_CONFIG_BASENAME),
76
+ };
77
+ }
78
+
79
+ function parseSupportedModelKey(value: string): FastSupportedModel | undefined {
80
+ const trimmed = value.trim();
81
+ if (!trimmed) {
28
82
  return undefined;
29
83
  }
30
- return { active: value.active };
84
+ const slashIndex = trimmed.indexOf("/");
85
+ if (slashIndex <= 0 || slashIndex >= trimmed.length - 1) {
86
+ return undefined;
87
+ }
88
+ const provider = trimmed.slice(0, slashIndex).trim();
89
+ const id = trimmed.slice(slashIndex + 1).trim();
90
+ if (!provider || !id) {
91
+ return undefined;
92
+ }
93
+ return { provider, id };
94
+ }
95
+
96
+ function normalizeSupportedModelKeys(value: unknown): string[] | undefined {
97
+ if (value === undefined) {
98
+ return undefined;
99
+ }
100
+ if (!Array.isArray(value)) {
101
+ return undefined;
102
+ }
103
+ const normalized: string[] = [];
104
+ for (const entry of value) {
105
+ if (typeof entry !== "string") {
106
+ continue;
107
+ }
108
+ const parsed = parseSupportedModelKey(entry);
109
+ if (!parsed) {
110
+ continue;
111
+ }
112
+ normalized.push(`${parsed.provider}/${parsed.id}`);
113
+ }
114
+ return normalized;
31
115
  }
32
116
 
33
- function getSavedFastModeState(ctx: ExtensionContext): FastModeState | undefined {
34
- const entries = ctx.sessionManager.getBranch();
35
- for (let i = entries.length - 1; i >= 0; i--) {
36
- const entry = entries[i];
37
- if (entry.type === "custom" && entry.customType === FAST_STATE_ENTRY) {
38
- return parseFastModeState(entry.data);
117
+ function parseSupportedModels(value: readonly string[]): FastSupportedModel[];
118
+ function parseSupportedModels(value: unknown): FastSupportedModel[] | undefined;
119
+ function parseSupportedModels(value: unknown): FastSupportedModel[] | undefined {
120
+ const normalized = normalizeSupportedModelKeys(value);
121
+ if (normalized === undefined) {
122
+ return undefined;
123
+ }
124
+ const models: FastSupportedModel[] = [];
125
+ for (const entry of normalized) {
126
+ const parsed = parseSupportedModelKey(entry);
127
+ if (!parsed) {
128
+ continue;
39
129
  }
130
+ models.push(parsed);
40
131
  }
41
- return undefined;
132
+ return models;
133
+ }
134
+
135
+ function readConfigFile(filePath: string): FastConfigFile | null {
136
+ if (!existsSync(filePath)) {
137
+ return null;
138
+ }
139
+ try {
140
+ const raw = readFileSync(filePath, "utf-8");
141
+ const parsed = JSON.parse(raw) as unknown;
142
+ if (!isRecord(parsed)) {
143
+ return {};
144
+ }
145
+ const config: FastConfigFile = {};
146
+ if (typeof parsed.persistState === "boolean") {
147
+ config.persistState = parsed.persistState;
148
+ }
149
+ if (typeof parsed.active === "boolean") {
150
+ config.active = parsed.active;
151
+ }
152
+ const supportedModels = normalizeSupportedModelKeys(parsed.supportedModels);
153
+ if (supportedModels !== undefined) {
154
+ config.supportedModels = supportedModels;
155
+ }
156
+ return config;
157
+ } catch (error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ console.warn(`[pi-openai-fast] Failed to read ${filePath}: ${message}`);
160
+ return null;
161
+ }
162
+ }
163
+
164
+ function writeConfigFile(filePath: string, config: FastConfigFile): void {
165
+ try {
166
+ mkdirSync(dirname(filePath), { recursive: true });
167
+ writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
168
+ } catch (error) {
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ console.warn(`[pi-openai-fast] Failed to write ${filePath}: ${message}`);
171
+ }
172
+ }
173
+
174
+ function ensureDefaultConfigFile(projectConfigPath: string, globalConfigPath: string): void {
175
+ if (existsSync(projectConfigPath) || existsSync(globalConfigPath)) {
176
+ return;
177
+ }
178
+ writeConfigFile(globalConfigPath, DEFAULT_CONFIG_FILE);
179
+ }
180
+
181
+ function resolveFastConfig(cwd: string, homeDir: string = homedir()): ResolvedFastConfig {
182
+ const { projectConfigPath, globalConfigPath } = getConfigPaths(cwd, homeDir);
183
+ ensureDefaultConfigFile(projectConfigPath, globalConfigPath);
184
+
185
+ const globalConfig = readConfigFile(globalConfigPath) ?? {};
186
+ const projectConfig = readConfigFile(projectConfigPath) ?? {};
187
+ const selectedConfigPath = existsSync(projectConfigPath) ? projectConfigPath : globalConfigPath;
188
+ const merged = { ...globalConfig, ...projectConfig };
189
+ const supportedModels =
190
+ parseSupportedModels(merged.supportedModels) ?? parseSupportedModels(DEFAULT_SUPPORTED_MODEL_KEYS);
191
+
192
+ return {
193
+ configPath: selectedConfigPath,
194
+ persistState: merged.persistState ?? DEFAULT_CONFIG_FILE.persistState ?? true,
195
+ active: typeof merged.active === "boolean" ? merged.active : undefined,
196
+ supportedModels,
197
+ };
42
198
  }
43
199
 
44
200
  function getCurrentModelKey(model: ExtensionContext["model"]): string | undefined {
@@ -48,29 +204,32 @@ function getCurrentModelKey(model: ExtensionContext["model"]): string | undefine
48
204
  return `${model.provider}/${model.id}`;
49
205
  }
50
206
 
51
- function isFastSupportedModel(model: ExtensionContext["model"]): boolean {
207
+ function isFastSupportedModel(model: ExtensionContext["model"], supportedModels: FastSupportedModel[]): boolean {
52
208
  if (!model) {
53
209
  return false;
54
210
  }
55
- return FAST_SUPPORTED_MODELS.some((supported) => supported.provider === model.provider && supported.id === model.id);
211
+ return supportedModels.some((supported) => supported.provider === model.provider && supported.id === model.id);
56
212
  }
57
213
 
58
- function describeSupportedModels(): string {
59
- return FAST_SUPPORTED_MODELS.map((supported) => `${supported.provider}/${supported.id}`).join(", ");
214
+ function describeSupportedModels(supportedModels: FastSupportedModel[]): string {
215
+ if (supportedModels.length === 0) {
216
+ return "none configured";
217
+ }
218
+ return supportedModels.map((supported) => `${supported.provider}/${supported.id}`).join(", ");
60
219
  }
61
220
 
62
- function describeCurrentState(ctx: ExtensionContext, active: boolean): string {
221
+ function describeCurrentState(ctx: ExtensionContext, active: boolean, supportedModels: FastSupportedModel[]): string {
63
222
  const model = getCurrentModelKey(ctx.model) ?? "none";
64
223
  if (!active) {
65
224
  return `Fast mode is off. Current model: ${model}.`;
66
225
  }
67
226
  if (!ctx.model) {
68
- return `Fast mode is on. No model is selected. Supported models: ${describeSupportedModels()}.`;
227
+ return `Fast mode is on. No model is selected. Supported models: ${describeSupportedModels(supportedModels)}.`;
69
228
  }
70
- if (isFastSupportedModel(ctx.model)) {
229
+ if (isFastSupportedModel(ctx.model, supportedModels)) {
71
230
  return `Fast mode is on for ${model}.`;
72
231
  }
73
- return `Fast mode is on, but ${model} does not support it. Supported models: ${describeSupportedModels()}.`;
232
+ return `Fast mode is on, but ${model} does not support it. Supported models: ${describeSupportedModels(supportedModels)}.`;
74
233
  }
75
234
 
76
235
  function applyFastServiceTier(payload: unknown): unknown {
@@ -85,12 +244,28 @@ function applyFastServiceTier(payload: unknown): unknown {
85
244
 
86
245
  export default function piOpenAIFast(pi: ExtensionAPI): void {
87
246
  let state: FastModeState = { active: false };
247
+ let cachedConfig: ResolvedFastConfig | undefined;
248
+
249
+ function refreshConfig(ctx: ExtensionContext): ResolvedFastConfig {
250
+ cachedConfig = resolveFastConfig(getConfigCwd(ctx));
251
+ return cachedConfig;
252
+ }
253
+
254
+ function getConfig(ctx: ExtensionContext): ResolvedFastConfig {
255
+ return cachedConfig ?? refreshConfig(ctx);
256
+ }
88
257
 
89
- function persistState(): void {
90
- pi.appendEntry(FAST_STATE_ENTRY, state);
258
+ function persistState(config: ResolvedFastConfig): void {
259
+ cachedConfig = { ...config, active: state.active };
260
+ if (!config.persistState) {
261
+ return;
262
+ }
263
+ const nextConfig = { ...(readConfigFile(config.configPath) ?? {}), active: state.active };
264
+ writeConfigFile(config.configPath, nextConfig);
91
265
  }
92
266
 
93
267
  async function enableFastMode(ctx: ExtensionContext, options?: { notify?: boolean }): Promise<void> {
268
+ const config = refreshConfig(ctx);
94
269
  if (state.active) {
95
270
  if (options?.notify !== false) {
96
271
  ctx.ui.notify("Fast mode is already on.", "info");
@@ -99,14 +274,15 @@ export default function piOpenAIFast(pi: ExtensionAPI): void {
99
274
  }
100
275
 
101
276
  state = { active: true };
102
- persistState();
277
+ persistState(config);
103
278
 
104
279
  if (options?.notify !== false) {
105
- ctx.ui.notify(describeCurrentState(ctx, state.active), "info");
280
+ ctx.ui.notify(describeCurrentState(ctx, state.active, config.supportedModels), "info");
106
281
  }
107
282
  }
108
283
 
109
284
  async function disableFastMode(ctx: ExtensionContext, options?: { notify?: boolean }): Promise<void> {
285
+ const config = refreshConfig(ctx);
110
286
  if (!state.active) {
111
287
  if (options?.notify !== false) {
112
288
  ctx.ui.notify("Fast mode is already off.", "info");
@@ -115,7 +291,7 @@ export default function piOpenAIFast(pi: ExtensionAPI): void {
115
291
  }
116
292
 
117
293
  state = { active: false };
118
- persistState();
294
+ persistState(config);
119
295
 
120
296
  if (options?.notify !== false) {
121
297
  ctx.ui.notify("Fast mode disabled.", "info");
@@ -137,7 +313,7 @@ export default function piOpenAIFast(pi: ExtensionAPI): void {
137
313
  });
138
314
 
139
315
  pi.registerCommand(FAST_COMMAND, {
140
- description: "Toggle fast mode (priority service tier for supported OpenAI GPT-5.4 models)",
316
+ description: "Toggle fast mode (priority service tier for configured models)",
141
317
  getArgumentCompletions: (prefix) => {
142
318
  const items = FAST_COMMAND_ARGS.filter((value) => value.startsWith(prefix)).map((value) => ({
143
319
  value,
@@ -161,7 +337,7 @@ export default function piOpenAIFast(pi: ExtensionAPI): void {
161
337
  await disableFastMode(ctx);
162
338
  return;
163
339
  case "status":
164
- ctx.ui.notify(describeCurrentState(ctx, state.active), "info");
340
+ ctx.ui.notify(describeCurrentState(ctx, state.active, refreshConfig(ctx).supportedModels), "info");
165
341
  return;
166
342
  default:
167
343
  ctx.ui.notify("Usage: /fast [on|off|status]", "error");
@@ -170,17 +346,28 @@ export default function piOpenAIFast(pi: ExtensionAPI): void {
170
346
  });
171
347
 
172
348
  pi.on("before_provider_request", (event, ctx) => {
173
- if (!state.active || !isFastSupportedModel(ctx.model)) {
349
+ const config = getConfig(ctx);
350
+ if (!state.active || !isFastSupportedModel(ctx.model, config.supportedModels)) {
174
351
  return;
175
352
  }
176
353
  return applyFastServiceTier(event.payload);
177
354
  });
178
355
 
179
356
  pi.on("session_start", async (_event, ctx) => {
180
- state = getSavedFastModeState(ctx) ?? { active: false };
357
+ const config = refreshConfig(ctx);
358
+ state = config.persistState && typeof config.active === "boolean" ? { active: config.active } : { active: false };
181
359
 
182
- if (pi.getFlag(FAST_FLAG) === true && !state.active) {
183
- await enableFastMode(ctx, { notify: true });
360
+ if (pi.getFlag(FAST_FLAG) === true) {
361
+ if (!state.active) {
362
+ state = { active: true };
363
+ persistState(config);
364
+ }
365
+ ctx.ui.notify(describeCurrentState(ctx, state.active, config.supportedModels), "info");
366
+ return;
367
+ }
368
+
369
+ if (state.active) {
370
+ ctx.ui.notify(describeCurrentState(ctx, state.active, config.supportedModels), "info");
184
371
  }
185
372
  });
186
373
  }
@@ -188,12 +375,18 @@ export default function piOpenAIFast(pi: ExtensionAPI): void {
188
375
  export const _test = {
189
376
  FAST_COMMAND,
190
377
  FAST_FLAG,
191
- FAST_STATE_ENTRY,
378
+ FAST_CONFIG_BASENAME,
192
379
  FAST_COMMAND_ARGS,
193
380
  FAST_SERVICE_TIER,
194
- FAST_SUPPORTED_MODELS,
195
- parseFastModeState,
381
+ DEFAULT_SUPPORTED_MODEL_KEYS,
382
+ DEFAULT_CONFIG_FILE,
383
+ getConfigPaths,
384
+ parseSupportedModelKey,
385
+ parseSupportedModels,
386
+ readConfigFile,
387
+ resolveFastConfig,
196
388
  isFastSupportedModel,
389
+ describeSupportedModels,
197
390
  describeCurrentState,
198
391
  applyFastServiceTier,
199
392
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benvargas/pi-openai-fast",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenAI fast mode toggle for pi - Enables priority service tier on supported GPT-5.4 models",
5
5
  "keywords": [
6
6
  "pi",