@diegopetrucci/pi-extensions 0.1.20 → 0.1.22
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 +3 -2
- package/extensions/librarian/README.md +15 -4
- package/extensions/librarian/index.ts +130 -22
- package/extensions/librarian/package.json +1 -1
- package/extensions/openai-fast/README.md +96 -0
- package/extensions/openai-fast/index.ts +302 -0
- package/extensions/openai-fast/openai-fast.example.json +4 -0
- package/extensions/openai-fast/package.json +28 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -5,9 +5,10 @@ A collection of [pi](https://github.com/earendil-works/pi-mono) agent extensions
|
|
|
5
5
|
- [`confirm-destructive`](./extensions/confirm-destructive): Confirms before destructive session actions like clear, switch, and fork.
|
|
6
6
|
- [`context-cap`](./extensions/context-cap): Caps effective model context windows at 200k tokens by default so pi avoids the `dumb zone`; toggle temporarily with `/context-cap`.
|
|
7
7
|
- [`context-inspector`](./extensions/context-inspector): Adds `/context`, a local self-contained HTML dashboard that breaks down where the current session context is going, with category overview, top offenders, and drilldown search.
|
|
8
|
-
- [`librarian`](./extensions/librarian): Adds a GitHub research scout
|
|
8
|
+
- [`librarian`](./extensions/librarian): Adds a GitHub research scout with a local repo checkout cache enabled by default under the OS user cache directory, toggleable with `/librarian-cache`, with cached repos expiring after 30 days of non-use.
|
|
9
9
|
- [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal configurable two-line layout: branch/repo on the first line, context/model on the second, optional `DUMB ZONE`, plus OpenAI Codex 5-hour and 7-day usage when available.
|
|
10
10
|
- [`notify`](./extensions/notify): Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input.
|
|
11
|
+
- [`openai-fast`](./extensions/openai-fast): Adds `/fast` to enable OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.
|
|
11
12
|
- [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, covers pi’s built-in providers with hardcoded rankings, sets reasoning to xhigh by default, and shows live status while running.
|
|
12
13
|
- [`permission-gate`](./extensions/permission-gate): Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`.
|
|
13
14
|
- [`quiet-tools`](./extensions/quiet-tools): Renders collapsed built-in tool rows as a one-line invocation plus an expand hint without changing model-visible tool results; toggle temporarily with `/quiet-tools`.
|
|
@@ -25,7 +26,7 @@ pi install npm:@diegopetrucci/pi-extensions
|
|
|
25
26
|
Or pin the GitHub package to this release:
|
|
26
27
|
|
|
27
28
|
```bash
|
|
28
|
-
pi install git:github.com/diegopetrucci/pi-extensions@v0.1.
|
|
29
|
+
pi install git:github.com/diegopetrucci/pi-extensions@v0.1.22
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
Or a specific extension:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# librarian
|
|
2
2
|
|
|
3
|
-
A pi GitHub research scout inspired by `pi-librarian`, with
|
|
3
|
+
A pi GitHub research scout inspired by `pi-librarian`, with a local checkout cache enabled by default.
|
|
4
4
|
|
|
5
|
-
When the `librarian` tool runs, it
|
|
5
|
+
When the `librarian` tool runs, it can cache/reuse repository checkouts locally. Use `/librarian-cache off` to force GitHub API/search and temporary fetched files only, or `/librarian-cache on` to re-enable cached local checkouts.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -35,10 +35,21 @@ Then reload pi:
|
|
|
35
35
|
- Tool name: `librarian`
|
|
36
36
|
- Uses a restricted subagent with `bash` and `read`
|
|
37
37
|
- Uses `gh` for GitHub search/API access
|
|
38
|
-
-
|
|
39
|
-
-
|
|
38
|
+
- Uses cached local checkouts by default
|
|
39
|
+
- Toggle cache behavior for future calls with `/librarian-cache on | off | toggle | status`
|
|
40
40
|
- Cached repos are removed lazily after 30 days without use
|
|
41
41
|
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
/librarian-cache status
|
|
46
|
+
/librarian-cache off
|
|
47
|
+
/librarian-cache on
|
|
48
|
+
/librarian-cache toggle
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The command works in interactive mode, RPC mode, and print/JSON mode. It writes a global preference to `~/.pi/agent/extensions/librarian.json`, so separate non-UI invocations use the same setting. In non-UI modes, command feedback is written to stderr so stdout remains usable for normal output or JSON events.
|
|
52
|
+
|
|
42
53
|
## Cache location
|
|
43
54
|
|
|
44
55
|
macOS:
|
|
@@ -25,11 +25,14 @@ const CACHE_TTL_DAYS = 30;
|
|
|
25
25
|
const CACHE_TTL_MS = CACHE_TTL_DAYS * 24 * 60 * 60 * 1000;
|
|
26
26
|
const CACHE_METADATA_FILE = ".pi-librarian-cache.json";
|
|
27
27
|
const CACHE_MARKER_FILE = ".pi-librarian-cache-used";
|
|
28
|
+
const CACHE_CONFIG_FILE = "librarian.json";
|
|
28
29
|
|
|
29
30
|
type LibrarianStatus = "running" | "done" | "error" | "aborted";
|
|
30
31
|
|
|
31
32
|
type CacheMode = "disabled" | "enabled";
|
|
32
33
|
|
|
34
|
+
const DEFAULT_CACHE_MODE: CacheMode = "enabled";
|
|
35
|
+
|
|
33
36
|
type ToolCall = {
|
|
34
37
|
id: string;
|
|
35
38
|
name: string;
|
|
@@ -252,30 +255,67 @@ async function cleanupExpiredCache(cacheRoot: string): Promise<{ deleted: number
|
|
|
252
255
|
}
|
|
253
256
|
}
|
|
254
257
|
|
|
255
|
-
|
|
256
|
-
|
|
258
|
+
function getCacheConfigPath(): string {
|
|
259
|
+
return path.join(getAgentDir(), "extensions", CACHE_CONFIG_FILE);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseCacheMode(value: unknown): CacheMode | undefined {
|
|
263
|
+
if (value === "enabled" || value === "on" || value === true) return "enabled";
|
|
264
|
+
if (value === "disabled" || value === "off" || value === false) return "disabled";
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
257
267
|
|
|
268
|
+
async function readCachePreference(): Promise<CacheMode> {
|
|
258
269
|
try {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
const raw = await fs.readFile(getCacheConfigPath(), "utf8");
|
|
271
|
+
const parsed = JSON.parse(raw) as {
|
|
272
|
+
cacheMode?: unknown;
|
|
273
|
+
cacheEnabled?: unknown;
|
|
274
|
+
cache?: { mode?: unknown; enabled?: unknown };
|
|
275
|
+
};
|
|
276
|
+
return (
|
|
277
|
+
parseCacheMode(parsed.cacheMode) ??
|
|
278
|
+
parseCacheMode(parsed.cache?.mode) ??
|
|
279
|
+
parseCacheMode(parsed.cacheEnabled) ??
|
|
280
|
+
parseCacheMode(parsed.cache?.enabled) ??
|
|
281
|
+
DEFAULT_CACHE_MODE
|
|
270
282
|
);
|
|
283
|
+
} catch {
|
|
284
|
+
return DEFAULT_CACHE_MODE;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
271
287
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
288
|
+
async function writeCachePreference(preference: CacheMode): Promise<void> {
|
|
289
|
+
const configPath = getCacheConfigPath();
|
|
290
|
+
const config = {
|
|
291
|
+
cacheMode: preference,
|
|
292
|
+
cacheEnabled: preference === "enabled",
|
|
293
|
+
updatedAt: new Date().toISOString(),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
297
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function resolveCacheDecision(preference: CacheMode): { enabled: boolean; reason: string } {
|
|
301
|
+
if (preference === "enabled") {
|
|
302
|
+
return { enabled: true, reason: "cache preference enabled; using cached local checkouts" };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return { enabled: false, reason: "cache preference disabled; using GitHub API/temp files only" };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function formatCachePreference(preference: CacheMode): string {
|
|
309
|
+
return preference === "enabled" ? "on" : "off";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function notifyCommand(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "info"): void {
|
|
313
|
+
if (ctx.hasUI) {
|
|
314
|
+
ctx.ui.notify(message, type);
|
|
315
|
+
return;
|
|
278
316
|
}
|
|
317
|
+
|
|
318
|
+
console.error(message);
|
|
279
319
|
}
|
|
280
320
|
|
|
281
321
|
function resolveToolPath(cwd: string, rawPath: string): string {
|
|
@@ -438,13 +478,81 @@ function isAbortLikeError(error: unknown): boolean {
|
|
|
438
478
|
}
|
|
439
479
|
|
|
440
480
|
export default function librarianExtension(pi: ExtensionAPI) {
|
|
481
|
+
let cachePreference: CacheMode = DEFAULT_CACHE_MODE;
|
|
482
|
+
|
|
483
|
+
pi.on("session_start", async () => {
|
|
484
|
+
cachePreference = await readCachePreference();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
pi.registerCommand("librarian-cache", {
|
|
488
|
+
description: "Toggle Librarian local checkout cache for future librarian calls",
|
|
489
|
+
getArgumentCompletions: (prefix) => {
|
|
490
|
+
const commands = ["on", "off", "toggle", "status"];
|
|
491
|
+
const query = prefix.trim().toLowerCase();
|
|
492
|
+
const matches = commands.filter((command) => command.startsWith(query));
|
|
493
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
494
|
+
},
|
|
495
|
+
handler: async (args, ctx) => {
|
|
496
|
+
const action = args.trim().toLowerCase() || "status";
|
|
497
|
+
const cacheRoot = getUserCacheReposRoot();
|
|
498
|
+
const configPath = getCacheConfigPath();
|
|
499
|
+
|
|
500
|
+
const setPreference = async (mode: CacheMode): Promise<string | undefined> => {
|
|
501
|
+
cachePreference = mode;
|
|
502
|
+
try {
|
|
503
|
+
await writeCachePreference(mode);
|
|
504
|
+
return undefined;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
507
|
+
return `Preference changed for this process, but could not save ${configPath}: ${message}`;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const formatSetMessage = (mode: CacheMode, warning?: string): string => {
|
|
512
|
+
const main = mode === "enabled"
|
|
513
|
+
? `Librarian cache enabled. Future librarian calls will reuse local checkouts under ${cacheRoot}.`
|
|
514
|
+
: "Librarian cache disabled. Future librarian calls will use GitHub API/search and temporary fetched files only.";
|
|
515
|
+
return warning ? `${main} ${warning}` : main;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
if (action === "on" || action === "enable") {
|
|
519
|
+
const warning = await setPreference("enabled");
|
|
520
|
+
notifyCommand(ctx, formatSetMessage("enabled", warning), warning ? "warning" : "info");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (action === "off" || action === "disable") {
|
|
525
|
+
const warning = await setPreference("disabled");
|
|
526
|
+
notifyCommand(ctx, formatSetMessage("disabled", warning), warning ? "warning" : "info");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (action === "toggle") {
|
|
531
|
+
const next = cachePreference === "enabled" ? "disabled" : "enabled";
|
|
532
|
+
const warning = await setPreference(next);
|
|
533
|
+
notifyCommand(ctx, formatSetMessage(next, warning), warning ? "warning" : "info");
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (action === "status") {
|
|
538
|
+
notifyCommand(
|
|
539
|
+
ctx,
|
|
540
|
+
`Librarian cache is ${formatCachePreference(cachePreference)}. Cache directory: ${cacheRoot}. Config: ${configPath}. Repos unused for ${CACHE_TTL_DAYS} days are removed lazily.`,
|
|
541
|
+
);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
notifyCommand(ctx, "Usage: /librarian-cache on | off | toggle | status", "warning");
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
|
|
441
549
|
pi.registerTool({
|
|
442
550
|
name: "librarian",
|
|
443
551
|
label: "Librarian",
|
|
444
552
|
description:
|
|
445
|
-
"GitHub research scout for coding and personal-assistant tasks. Use when the answer likely lives in GitHub repos, exact repo/path locations are unknown, or you'd otherwise do exploratory gh search/tree probes plus local rg/read inspection. Librarian
|
|
553
|
+
"GitHub research scout for coding and personal-assistant tasks. Use when the answer likely lives in GitHub repos, exact repo/path locations are unknown, or you'd otherwise do exploratory gh search/tree probes plus local rg/read inspection. Librarian uses an optional 30-day local checkout cache by default; toggle it with /librarian-cache.",
|
|
446
554
|
promptSnippet:
|
|
447
|
-
"Research GitHub repositories with evidence-first path and line citations;
|
|
555
|
+
"Research GitHub repositories with evidence-first path and line citations; local checkout cache is enabled by default and user-toggleable with /librarian-cache.",
|
|
448
556
|
promptGuidelines: [
|
|
449
557
|
"Use librarian when the answer likely requires exploratory GitHub repository search or line-cited evidence from external repos.",
|
|
450
558
|
"Do not use librarian for files already present in the current workspace unless the user asks for external GitHub research.",
|
|
@@ -472,7 +580,7 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
472
580
|
await fs.mkdir(path.join(workspace, "repos"), { recursive: true });
|
|
473
581
|
|
|
474
582
|
const cacheRoot = getUserCacheReposRoot();
|
|
475
|
-
const cacheDecision =
|
|
583
|
+
const cacheDecision = resolveCacheDecision(cachePreference);
|
|
476
584
|
if (cacheDecision.enabled) await fs.mkdir(cacheRoot, { recursive: true });
|
|
477
585
|
const cleanup = cacheDecision.enabled ? await cleanupExpiredCache(cacheRoot) : { deleted: 0, errors: [] };
|
|
478
586
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-librarian",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "A pi GitHub research scout
|
|
4
|
+
"description": "A pi GitHub research scout with a toggleable local repo checkout cache under the user's OS cache directory.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "github", "research", "subagent", "cache"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# openai-fast
|
|
2
|
+
|
|
3
|
+
A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5.
|
|
4
|
+
|
|
5
|
+
When active, the extension injects this into eligible OpenAI Codex request payloads:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"service_tier": "priority"
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The user-facing feature is OpenAI Codex **Fast mode**. The wire value is `priority` because current Codex clients map Fast mode to the OpenAI priority service tier.
|
|
14
|
+
|
|
15
|
+
## Eligibility
|
|
16
|
+
|
|
17
|
+
Fast mode is only injected when all of these are true:
|
|
18
|
+
|
|
19
|
+
- The current provider is `openai-codex`.
|
|
20
|
+
- The current API is `openai-codex-responses`.
|
|
21
|
+
- The current model is `gpt-5.4` or `gpt-5.5`.
|
|
22
|
+
- The provider is using ChatGPT OAuth/subscription auth, not API-key auth.
|
|
23
|
+
- The request payload does not already include `service_tier`.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
/fast
|
|
29
|
+
/fast status
|
|
30
|
+
/fast on
|
|
31
|
+
/fast off
|
|
32
|
+
/fast auto
|
|
33
|
+
/fast toggle
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run `/fast` without arguments to pick an action from a menu. `/fast on` and `/fast off` are temporary session/runtime overrides. Use `/fast auto` to reload and follow config defaults again.
|
|
37
|
+
|
|
38
|
+
The extension defaults to off so installing the full collection does not accidentally spend Fast-mode credits.
|
|
39
|
+
|
|
40
|
+
## Config
|
|
41
|
+
|
|
42
|
+
Optional global config:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
~/.pi/agent/extensions/openai-fast.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Optional project config:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
.pi/openai-fast.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Project config overrides global config.
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"enabled": false,
|
|
59
|
+
"showStatus": true
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `enabled`: default Fast-mode state when there is no session override.
|
|
64
|
+
- `showStatus`: show a compact `fast` status when Fast mode is active for the current model.
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
### Standalone npm package
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pi install npm:@diegopetrucci/pi-openai-fast
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Collection package
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pi install npm:@diegopetrucci/pi-extensions
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### GitHub package
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pi install git:github.com/diegopetrucci/pi-extensions
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then reload pi:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
/reload
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Notes
|
|
93
|
+
|
|
94
|
+
- This extension intentionally does not affect API-key OpenAI models.
|
|
95
|
+
- Pi may only account Fast-mode cost correctly when the backend reports `service_tier: "priority"` in the streamed response. The extension does not patch usage totals to avoid double-counting.
|
|
96
|
+
- If pi adds first-class service-tier support later, this extension skips payloads that already contain `service_tier`.
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
getAgentDir,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
const EXTENSION_ID = "openai-fast";
|
|
10
|
+
const PROVIDER_ID = "openai-codex";
|
|
11
|
+
const API_ID = "openai-codex-responses";
|
|
12
|
+
const FAST_SERVICE_TIER = "priority";
|
|
13
|
+
const SUPPORTED_MODELS = new Set(["gpt-5.4", "gpt-5.5"]);
|
|
14
|
+
const COMMANDS = ["status", "on", "off", "auto", "toggle"];
|
|
15
|
+
|
|
16
|
+
const DEFAULT_CONFIG: OpenAIFastConfig = {
|
|
17
|
+
enabled: false,
|
|
18
|
+
showStatus: true,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type FastOverride = "auto" | "on" | "off";
|
|
22
|
+
|
|
23
|
+
type OpenAIFastConfig = {
|
|
24
|
+
/** Default Fast-mode state when there is no session override. */
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
/** Show a compact `fast` status when Fast mode is active for the current model. */
|
|
27
|
+
showStatus: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type SessionState = {
|
|
31
|
+
config: OpenAIFastConfig;
|
|
32
|
+
override: FastOverride;
|
|
33
|
+
lastInjectedAt?: number;
|
|
34
|
+
lastInjectedModel?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type RecursivePartial<T> = {
|
|
38
|
+
[P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type PayloadRecord = Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
type Eligibility = {
|
|
44
|
+
eligible: boolean;
|
|
45
|
+
modelKey: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function readConfigFile(path: string): RecursivePartial<OpenAIFastConfig> {
|
|
50
|
+
if (!existsSync(path)) return {};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
return isPayloadRecord(parsed) ? (parsed as RecursivePartial<OpenAIFastConfig>) : {};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(`Warning: Could not parse ${path}: ${error}`);
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeBoolean(value: unknown, fallback: boolean): boolean {
|
|
62
|
+
return typeof value === "boolean" ? value : fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function mergeConfig(
|
|
66
|
+
base: OpenAIFastConfig,
|
|
67
|
+
overrides: RecursivePartial<OpenAIFastConfig>,
|
|
68
|
+
): OpenAIFastConfig {
|
|
69
|
+
return {
|
|
70
|
+
enabled: normalizeBoolean(overrides.enabled, base.enabled),
|
|
71
|
+
showStatus: normalizeBoolean(overrides.showStatus, base.showStatus),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function findProjectConfigPath(cwd: string): string {
|
|
76
|
+
let current = cwd;
|
|
77
|
+
while (true) {
|
|
78
|
+
const candidate = join(current, ".pi", "openai-fast.json");
|
|
79
|
+
if (existsSync(candidate)) return candidate;
|
|
80
|
+
|
|
81
|
+
const parent = dirname(current);
|
|
82
|
+
if (parent === current) return join(cwd, ".pi", "openai-fast.json");
|
|
83
|
+
current = parent;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function loadConfig(cwd: string): OpenAIFastConfig {
|
|
88
|
+
const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "openai-fast.json"));
|
|
89
|
+
const projectConfig = readConfigFile(findProjectConfigPath(cwd));
|
|
90
|
+
return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isPayloadRecord(payload: unknown): payload is PayloadRecord {
|
|
94
|
+
return typeof payload === "object" && payload !== null && !Array.isArray(payload);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function modelKey(ctx: ExtensionContext): string {
|
|
98
|
+
const model = ctx.model;
|
|
99
|
+
return model ? `${model.provider}/${model.id}` : "no-model";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isFastEnabled(state: SessionState): boolean {
|
|
103
|
+
if (state.override === "on") return true;
|
|
104
|
+
if (state.override === "off") return false;
|
|
105
|
+
return state.config.enabled;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function describeMode(state: SessionState): string {
|
|
109
|
+
if (state.override === "on") return "on (session override)";
|
|
110
|
+
if (state.override === "off") return "off (session override)";
|
|
111
|
+
return state.config.enabled ? "on (config default)" : "off (config default)";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getEligibility(ctx: ExtensionContext): Eligibility {
|
|
115
|
+
const model = ctx.model;
|
|
116
|
+
if (!model) {
|
|
117
|
+
return { eligible: false, modelKey: "no-model", reason: "no model is selected" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const key = `${model.provider}/${model.id}`;
|
|
121
|
+
if (model.provider !== PROVIDER_ID) {
|
|
122
|
+
return {
|
|
123
|
+
eligible: false,
|
|
124
|
+
modelKey: key,
|
|
125
|
+
reason: `current provider is ${model.provider}, not ${PROVIDER_ID}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (model.api !== API_ID) {
|
|
130
|
+
return {
|
|
131
|
+
eligible: false,
|
|
132
|
+
modelKey: key,
|
|
133
|
+
reason: `current API is ${model.api}, not ${API_ID}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!SUPPORTED_MODELS.has(model.id)) {
|
|
138
|
+
return {
|
|
139
|
+
eligible: false,
|
|
140
|
+
modelKey: key,
|
|
141
|
+
reason: "Fast mode is only enabled for gpt-5.4 and gpt-5.5",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!ctx.modelRegistry.isUsingOAuth(model)) {
|
|
146
|
+
return {
|
|
147
|
+
eligible: false,
|
|
148
|
+
modelKey: key,
|
|
149
|
+
reason: "ChatGPT OAuth auth is required; API-key auth is intentionally not used",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { eligible: true, modelKey: key };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function updateStatus(ctx: ExtensionContext, state: SessionState): void {
|
|
157
|
+
if (!ctx.hasUI) return;
|
|
158
|
+
if (!state.config.showStatus) {
|
|
159
|
+
ctx.ui.setStatus(EXTENSION_ID, undefined);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const eligibility = getEligibility(ctx);
|
|
164
|
+
ctx.ui.setStatus(
|
|
165
|
+
EXTENSION_ID,
|
|
166
|
+
isFastEnabled(state) && eligibility.eligible ? "fast" : undefined,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getStatusMessage(ctx: ExtensionContext, state: SessionState): string {
|
|
171
|
+
const enabled = isFastEnabled(state);
|
|
172
|
+
const eligibility = getEligibility(ctx);
|
|
173
|
+
const active = enabled && eligibility.eligible;
|
|
174
|
+
const injected = state.lastInjectedAt
|
|
175
|
+
? ` Last injected for ${state.lastInjectedModel ?? "unknown model"} ${Math.max(0, Math.round((Date.now() - state.lastInjectedAt) / 1000))}s ago.`
|
|
176
|
+
: "";
|
|
177
|
+
|
|
178
|
+
if (active) {
|
|
179
|
+
return `OpenAI Fast mode is ${describeMode(state)} and active for ${eligibility.modelKey}; requests will use service_tier=${FAST_SERVICE_TIER}.${injected}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (enabled) {
|
|
183
|
+
return `OpenAI Fast mode is ${describeMode(state)}, but inactive for ${eligibility.modelKey}: ${eligibility.reason}.${injected}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return `OpenAI Fast mode is ${describeMode(state)}. Current model: ${eligibility.modelKey}.${injected}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function injectFastServiceTier(
|
|
190
|
+
payload: unknown,
|
|
191
|
+
ctx: ExtensionContext,
|
|
192
|
+
state: SessionState,
|
|
193
|
+
): PayloadRecord | undefined {
|
|
194
|
+
if (!isFastEnabled(state)) return undefined;
|
|
195
|
+
if (!getEligibility(ctx).eligible) return undefined;
|
|
196
|
+
if (!isPayloadRecord(payload)) return undefined;
|
|
197
|
+
if (payload.model !== ctx.model?.id) return undefined;
|
|
198
|
+
if ("service_tier" in payload) return undefined;
|
|
199
|
+
|
|
200
|
+
state.lastInjectedAt = Date.now();
|
|
201
|
+
state.lastInjectedModel = modelKey(ctx);
|
|
202
|
+
return {
|
|
203
|
+
...payload,
|
|
204
|
+
service_tier: FAST_SERVICE_TIER,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export default function openAIFastExtension(pi: ExtensionAPI) {
|
|
209
|
+
const states = new WeakMap<object, SessionState>();
|
|
210
|
+
|
|
211
|
+
function getState(ctx: ExtensionContext): SessionState {
|
|
212
|
+
let state = states.get(ctx.sessionManager);
|
|
213
|
+
if (!state) {
|
|
214
|
+
state = {
|
|
215
|
+
config: loadConfig(ctx.cwd),
|
|
216
|
+
override: "auto",
|
|
217
|
+
};
|
|
218
|
+
states.set(ctx.sessionManager, state);
|
|
219
|
+
}
|
|
220
|
+
return state;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pi.on("session_start", (_event, ctx) => {
|
|
224
|
+
const state: SessionState = {
|
|
225
|
+
config: loadConfig(ctx.cwd),
|
|
226
|
+
override: "auto",
|
|
227
|
+
};
|
|
228
|
+
states.set(ctx.sessionManager, state);
|
|
229
|
+
updateStatus(ctx, state);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
pi.on("model_select", (_event, ctx) => {
|
|
233
|
+
updateStatus(ctx, getState(ctx));
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
237
|
+
const state = getState(ctx);
|
|
238
|
+
const nextPayload = injectFastServiceTier(event.payload, ctx, state);
|
|
239
|
+
updateStatus(ctx, state);
|
|
240
|
+
return nextPayload;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
pi.registerCommand("fast", {
|
|
244
|
+
description: "Manage OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4/GPT-5.5",
|
|
245
|
+
getArgumentCompletions: (prefix) => {
|
|
246
|
+
const normalized = prefix.trim().toLowerCase();
|
|
247
|
+
const matches = COMMANDS.filter((command) => command.startsWith(normalized));
|
|
248
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
249
|
+
},
|
|
250
|
+
handler: async (args, ctx) => {
|
|
251
|
+
const state = getState(ctx);
|
|
252
|
+
let action = args.trim().toLowerCase();
|
|
253
|
+
|
|
254
|
+
if (!action) {
|
|
255
|
+
if (!ctx.hasUI) {
|
|
256
|
+
action = "status";
|
|
257
|
+
} else {
|
|
258
|
+
const selection = await ctx.ui.select("OpenAI Fast mode", COMMANDS);
|
|
259
|
+
if (!selection) return;
|
|
260
|
+
action = selection;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (action === "on" || action === "enable") {
|
|
265
|
+
state.override = "on";
|
|
266
|
+
updateStatus(ctx, state);
|
|
267
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (action === "off" || action === "disable") {
|
|
272
|
+
state.override = "off";
|
|
273
|
+
updateStatus(ctx, state);
|
|
274
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (action === "auto" || action === "default") {
|
|
279
|
+
state.override = "auto";
|
|
280
|
+
state.config = loadConfig(ctx.cwd);
|
|
281
|
+
updateStatus(ctx, state);
|
|
282
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (action === "toggle") {
|
|
287
|
+
state.override = isFastEnabled(state) ? "off" : "on";
|
|
288
|
+
updateStatus(ctx, state);
|
|
289
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (action === "status") {
|
|
294
|
+
updateStatus(ctx, state);
|
|
295
|
+
ctx.ui.notify(getStatusMessage(ctx, state), "info");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
ctx.ui.notify("Usage: /fast on | off | auto | toggle | status", "warning");
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diegopetrucci/pi-openai-fast",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.",
|
|
5
|
+
"keywords": ["pi-package", "pi", "openai", "codex", "fast"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/diegopetrucci/pi-extensions.git",
|
|
10
|
+
"directory": "extensions/openai-fast"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.ts",
|
|
14
|
+
"openai-fast.example.json",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"pi": {
|
|
21
|
+
"extensions": [
|
|
22
|
+
"index.ts"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
27
|
+
}
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A collection of pi extensions, including a GitHub librarian with
|
|
3
|
+
"version": "0.1.22",
|
|
4
|
+
"description": "A collection of pi extensions, including a GitHub librarian with toggleable local repo checkout caching, a minimal custom footer, an Amp-style oracle, a 200k context cap for auto-compaction, a local HTML context inspector, OpenAI Codex Fast mode controls, quiet one-line collapsed invocation previews, a permission gate for dangerous bash commands, confirm-before-destructive session actions, and terminal notifications when pi is ready for input.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "terminal", "agent"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"./extensions/quiet-tools/index.ts",
|
|
37
37
|
"./extensions/permission-gate/index.ts",
|
|
38
38
|
"./extensions/confirm-destructive/index.ts",
|
|
39
|
-
"./extensions/notify/index.ts"
|
|
39
|
+
"./extensions/notify/index.ts",
|
|
40
|
+
"./extensions/openai-fast/index.ts"
|
|
40
41
|
],
|
|
41
42
|
"image": "https://raw.githubusercontent.com/diegopetrucci/pi-extensions/main/assets/oracle-preview.svg"
|
|
42
43
|
}
|