@diegopetrucci/pi-extensions 0.1.62 → 0.1.63

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
@@ -13,8 +13,9 @@ A collection of [pi](https://pi.dev) agent extensions I made. (btw, [I am also b
13
13
 
14
14
  ### Providers
15
15
 
16
- - [`claude-fast`](./extensions/claude-fast): Adds `/claude-fast` to enable Anthropic Claude Fast mode for supported Claude Opus models by injecting `speed: "fast"`.
17
- - [`openai-fast`](./extensions/openai-fast): Adds `/fast` to enable OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4, GPT-5.5, and GPT-5.6 Codex variants by injecting the priority service tier.
16
+ - [`claude-fast`](./extensions/claude-fast): Standalone-only provider-specific alternative that adds `/claude-fast` for supported Anthropic Claude Opus models.
17
+ - [`fast`](./extensions/fast): Adds one `/fast` toggle that enables the provider-specific Fast mode for the currently selected supported OpenAI Codex or Anthropic Claude model.
18
+ - [`openai-fast`](./extensions/openai-fast): Standalone-only provider-specific alternative that adds `/fast` for ChatGPT-auth GPT-5.4, GPT-5.5, and GPT-5.6 Codex variants.
18
19
 
19
20
  ### UI
20
21
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  A pi extension that enables Anthropic Claude Fast mode for supported Claude Opus models.
4
4
 
5
+ This package is standalone-only and is not auto-loaded by the `@diegopetrucci/pi-extensions` collection package. The collection uses the unified [`fast`](../fast) extension instead.
6
+
5
7
  When active, the extension injects this into eligible Anthropic request payloads:
6
8
 
7
9
  ```json
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-claude-fast",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "A pi extension that enables Anthropic Claude Fast mode for supported Claude Opus models by injecting speed=fast.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -230,19 +230,32 @@ never required to exist. Full shape, with defaults:
230
230
 
231
231
  Notes:
232
232
 
233
- - `thresholds.minCharsSaved` is reserved for a future minimum-size filter and
234
- is **not** currently enforced by the pipeline; the net-benefit gate's
235
- break-even math is what actually decides whether a prune applies.
233
+ - `thresholds.minCharsSaved` is a minimum-size floor enforced against fresh
234
+ **automatic** strategy proposals only, before the net-benefit gate runs: a
235
+ text-only candidate whose raw character savings (original text chars minus
236
+ placeholder chars) fall below this value is dropped up front and never
237
+ reaches gate or batch consideration. A tool-result candidate containing one
238
+ or more image blocks bypasses this raw-text floor (images do not receive
239
+ synthetic character counts) and is evaluated by the existing token-based
240
+ net-benefit gate. Manual prunes (`/prune`) and already-persisted/replayed
241
+ decisions always bypass this floor, same as the gate. Set to `0` to disable
242
+ filtering entirely. This is independent of the net-benefit gate's
243
+ break-even math (`gate.*`), which is a separate, later check on whatever
244
+ survives this floor.
236
245
  - `gate.breakEvenThresholdByState` lets `idle` vs `mid_loop` agent states use
237
246
  different thresholds; both default to `gate.breakEvenThreshold`. Real
238
- mid-loop/idle detection isn't wired through yet, so this always evaluates as
239
- `idle` in v1 — the field exists so the seam is ready once that lands. The
240
- representative-corpus benchmark (see [Roadmap](#roadmap)) shows `mid_loop`
241
- candidates carry ~all realized net benefit and `idle` candidates carry ~none
242
- at r=0.1, which argues for a stricter `idle` default — but since every live
243
- evaluation runs as `idle` today, defaulting it stricter would silently
244
- disable most automatic pruning. Both states are intentionally kept at
245
- parity until real agent-state detection lands; that's a follow-up ticket.
247
+ mid-loop/idle detection is wired through the `context` event handler
248
+ (pe-zy4s): the state is classified straight from the message payload —
249
+ `idle` if the most recent relevant message is a user message (this is the
250
+ first LLM call of the turn), `mid_loop` if it's an assistant message or a
251
+ tool result (the agent is iterating mid-turn). The representative-corpus
252
+ benchmark (see [Roadmap](#roadmap)) was re-derived against this same
253
+ runtime-observable definition and found the state-split REVERSED from an
254
+ earlier turn-end-based analysis: at r=0.1, `idle` candidates now carry
255
+ essentially all the realized net benefit and `mid_loop` candidates carry
256
+ essentially none. Since `idle` is not the worthless state under this
257
+ definition, both states are kept at parity (`22`/`22`) rather than forcing
258
+ a stricter default in either direction.
246
259
  - `gate.breakEvenThreshold`'s default of `22` is calibrated from the
247
260
  representative-corpus benchmark at `cachedPriceRatio` r=0.1 (aggressive
248
261
  prompt caching, the common case) — see [Roadmap](#roadmap) for the full
@@ -296,8 +309,46 @@ Options:
296
309
  - `--ratio R`: cached-price ratio(s) to model; repeatable and/or
297
310
  comma-separated (e.g. `--ratio 0.1,0.2`). Default: `0.1`.
298
311
  - `--json`: emit a full machine-readable JSON dump instead of aligned text.
312
+ - `--simulate-compression`: additionally SIMULATE v2-style range compression
313
+ (docs/v2-design.md §1/§4 go/no-go evidence) alongside — never mixed into —
314
+ the deterministic results. See below.
315
+ - `--sim-summary-fraction F`: summary size as a fraction of range tokens;
316
+ repeatable/comma-separated (e.g. `--sim-summary-fraction 0.1,0.15,0.3`).
317
+ Default: `0.15`.
318
+ - `--sim-summary-min-tokens N`: floor on summary tokens regardless of
319
+ fraction. Default: `200`.
320
+ - `--sim-summarizer-cost-mult M`: relative per-token price of the summarizer
321
+ call vs. the main model. Default: `1.0` (same per-token price; a
322
+ deliberate simplification that ignores summarizer latency/availability).
323
+ - `--sim-min-range-tokens N`: minimum contiguous range size to be
324
+ considered. Default: `2000`.
299
325
  - `--help`: print usage.
300
326
 
327
+ ### Compression-simulation mode (`--simulate-compression`)
328
+
329
+ This is a v2 go/no-go evidence generator that does **not** require building
330
+ v2: it identifies plausible v2-style `compress` (range mode) opportunities —
331
+ contiguous spans of complete, non-recent, non-protected messages, never
332
+ splitting a toolCall/toolResult pair — during the same replay pass, and
333
+ models the outcome:
334
+
335
+ ```text
336
+ summaryTokens = max(fraction * rangeTokens, summaryMinTokens)
337
+ oneTimeCost = cacheBustPenalty(earliest range position)
338
+ + summarizerCostMult * (rangeTokens + summaryTokens)
339
+ recurringSaving = r * (rangeTokens - summaryTokens)
340
+ realizedNetBenefit = actualRemainingCalls * recurringSaving - oneTimeCost
341
+ ```
342
+
343
+ Each distinct range is recorded once, at the earliest call boundary where it
344
+ first exists and meets `--sim-min-range-tokens`; ranges never overlap. The
345
+ same auto-expanding break-even threshold sweep as the deterministic
346
+ strategies runs against this separate SIMULATED population, reported
347
+ side-by-side with (but never combined with) the deterministic results — the
348
+ two populations are computed **independently**; a simulated range may fully
349
+ contain deterministic candidates, and no overlap/interaction between the two
350
+ is modeled or netted out.
351
+
301
352
  With no positional paths, it defaults to every `*.jsonl` file found
302
353
  recursively under `~/.pi/agent/sessions`.
303
354
 
@@ -387,6 +438,22 @@ realized benefit across the whole 1,390-session corpus is only ~20.6k
387
438
  token-units — economically marginal, on the order of pennies. Savings only
388
439
  become material at weaker caching, r>=0.25.
389
440
 
441
+ **pe-zy4s update (2026-07-08): this idle/mid_loop split used a turn-END
442
+ definition** ("idle" = this call IS the turn's final assistant message, only
443
+ knowable in hindsight via replay) **that was never runtime-observable.** Once
444
+ real runtime agent-state detection landed (turn-START definition: "idle" =
445
+ this is the FIRST LLM call of a turn; see the config-notes bullet above), the
446
+ benchmark's candidate labeling was aligned to that same definition and
447
+ re-derived on the representative corpus (460 gate-eligible candidates on the
448
+ current `~/.the-last-harness/agent/sessions` corpus). The split **reverses**:
449
+ at r=0.1, `idle` (T=22, ~20.6k realized net benefit) now carries essentially
450
+ all the value, and `mid_loop` (T=1, ~0 realized net benefit) carries
451
+ essentially none. Per the decision rule (stricter idle default only if idle
452
+ is shown ~worthless), this evidence does **not** support a stricter idle
453
+ default — parity (`22`/`22`) is kept. The table above is retained as the
454
+ historical turn-END-definition record; it no longer reflects how the runtime
455
+ classifies agent state.
456
+
390
457
  **This reframes, rather than weakens, the case for v2.** Small deterministic
391
458
  removals (dedupe/error-purge/superseded-file-ops) structurally cannot beat
392
459
  the cache-bust penalty at r=0.1 — there just isn't enough single-message
@@ -165,10 +165,16 @@ export interface PruneProtections {
165
165
 
166
166
  export interface PruneThresholds {
167
167
  /**
168
- * Minimum characters a prune must remove to be worth persisting. Reserved
169
- * for a future minimum-size filter; NOT used by the net-benefit gate
170
- * (see `PruneGateConfig`/`gate`), which uses break-even token math instead.
171
- * Not enforced by this pipeline yet.
168
+ * Minimum raw text characters a fresh AUTOMATIC prune proposal must remove
169
+ * (original text chars minus placeholder chars) to be considered at all
170
+ * (pe-qdzb). Enforced by `runDynamicContextPruningPipeline` BEFORE the
171
+ * net-benefit gate (see `PruneGateConfig`/`gate`, which uses break-even token
172
+ * math and is a separate, later check): text-only candidates below this floor
173
+ * are dropped up front and never reach gate/batch consideration. Image-bearing
174
+ * tool-result candidates bypass this raw-text floor (images receive no
175
+ * synthetic character count) and proceed to the token-based gate. Manual
176
+ * prunes and already-persisted/replayed decisions always bypass this floor,
177
+ * same as the gate. A value of 0 disables the floor (no filtering).
172
178
  */
173
179
  minCharsSaved: number;
174
180
  }
@@ -194,19 +200,42 @@ export type GateMode = "on" | "off" | "always-apply";
194
200
  * making tool calls) vs idle (waiting on the user). Both states default to
195
201
  * the same threshold today.
196
202
  *
197
- * pe-c5n9 evidence update: the representative-corpus benchmark shows
198
- * mid_loop candidates carry ~all realized net benefit while idle candidates
199
- * carry ~none (literally 0 at r=0.1). That would argue for a stricter idle
200
- * default. However, the only live runtime caller (the `context` event
201
- * handler below) does not yet perform real mid-loop/idle detection — it
202
- * always passes agentState:"idle". Defaulting idle to a much stricter
203
- * threshold today would therefore silently disable most automatic pruning
204
- * for everyone (100% of live evaluations run as "idle"), which is a far
205
- * bigger behavior change than a threshold recalibration. So the per-state
206
- * split is intentionally left at parity for now; a follow-up ticket should
207
- * wire real agent-state detection through `ctx` and then apply the
208
- * idle-vs-mid_loop split (idle stricter, mid_loop at DEFAULT_BREAK_EVEN_THRESHOLD)
209
- * once mid_loop is actually reachable.
203
+ * pe-c5n9 evidence (turn-END definition, since superseded -- see below):
204
+ * the representative-corpus benchmark showed mid_loop candidates carrying
205
+ * ~all realized net benefit while idle candidates carried ~none. That would
206
+ * have argued for a stricter idle default. However, the only live runtime
207
+ * caller (the `context` event handler below) did not yet perform real
208
+ * mid-loop/idle detection at the time, so no default change was made.
209
+ *
210
+ * pe-zy4s update (2026-07-08): real runtime mid_loop/idle detection is now
211
+ * wired through the `context` event handler (`classifyAgentStateFromMessages`),
212
+ * using a turn-START definition (idle = this is the first LLM call of a turn;
213
+ * mid_loop = the agent already made a call or received a tool result this
214
+ * turn) -- see that function's doc comment for the exact runtime-observable
215
+ * semantics. The benchmark's candidate labeling was aligned to this SAME
216
+ * definition (retiring the old turn-END definition above, which was never
217
+ * runtime-observable), and the state-split was re-derived on the
218
+ * representative corpus (~/.the-last-harness/agent/sessions, 460 candidates)
219
+ * under it.
220
+ *
221
+ * The result REVERSES the earlier (turn-END) finding: at r=0.1 the
222
+ * turn-START "idle" population (calls made at the start of a turn) carries
223
+ * essentially ALL the realized net benefit (idle-optimal T=22, total realized
224
+ * net benefit ~20,594 token-units), while "mid_loop" carries essentially NONE
225
+ * (mid_loop-optimal T=1, total realized net benefit ~0.0 -- i.e. no mid_loop
226
+ * candidate is ever worth accepting at r=0.1 under this definition). This is
227
+ * the OPPOSITE of what the pe-c5n9 evidence (measured under the old,
228
+ * non-runtime-observable turn-END definition) suggested.
229
+ *
230
+ * Per the pe-zy4s decision rule (stricter idle default only if idle itself is
231
+ * shown to be ~worthless): idle is NOT worthless here -- it is the ONLY state
232
+ * carrying value at r=0.1 -- so no state-conditioned strictness is justified
233
+ * in either direction. The per-state split is therefore kept at PARITY
234
+ * (`breakEvenThresholdByState: { idle: DEFAULT_BREAK_EVEN_THRESHOLD, mid_loop:
235
+ * DEFAULT_BREAK_EVEN_THRESHOLD }`), both config-overridable. A future ticket
236
+ * could consider a stricter mid_loop default given this evidence, but that is
237
+ * a new, unreviewed direction outside this ticket's authorized decision rule
238
+ * and is left for a follow-up.
210
239
  */
211
240
  export type AgentState = "idle" | "mid_loop";
212
241
 
@@ -397,6 +426,41 @@ export function estimateTokensForContent(content: string | (MinimalTextContent |
397
426
 
398
427
  export type TokenEstimator = (text: string) => number;
399
428
 
429
+ /**
430
+ * Classify the agent's current state from the message payload alone (pe-zy4s):
431
+ * this is the runtime-observable definition used both by the live `context`
432
+ * event handler and (via re-export) by the offline benchmark, so the two can
433
+ * never drift apart.
434
+ *
435
+ * Walk backwards from the end of `messages`, skipping any message that is
436
+ * neither `user`, `assistant`, nor `toolResult` (e.g. system-role bookkeeping
437
+ * entries). The first such message found determines the state:
438
+ * - `user` (a plain user turn message) => "idle": this call is the FIRST LLM
439
+ * call of a turn, i.e. the decision is being made from a turn-start state.
440
+ * - `assistant` or `toolResult` (mid-chain) => "mid_loop": the agent is
441
+ * iterating (already made at least one call this turn, or is reacting to a
442
+ * tool result).
443
+ * - No such message found (empty history, or system-only) => "idle" (safe
444
+ * default: nothing suggests an in-progress loop).
445
+ *
446
+ * The benchmark (scripts/benchmark.mjs) reclassifies its candidate labels to
447
+ * this SAME definition (pe-zy4s), reusing this function directly, so the
448
+ * defaults below rest on evidence measured with the exact definition the
449
+ * runtime observes. (An earlier turn-end-based benchmark definition -- "idle"
450
+ * meant "this IS the turn's last assistant message", only knowable in
451
+ * hindsight -- has been retired: the runtime cannot know in advance whether a
452
+ * call is a turn's last one, so that definition was never runtime-observable.)
453
+ */
454
+ export function classifyAgentStateFromMessages(messages: MinimalMessage[]): AgentState {
455
+ for (let i = messages.length - 1; i >= 0; i--) {
456
+ const role = messages[i].role;
457
+ if (role === "user") return "idle";
458
+ if (role === "assistant" || role === "toolResult") return "mid_loop";
459
+ // Any other role (e.g. "system") is skipped: keep walking further back.
460
+ }
461
+ return "idle";
462
+ }
463
+
400
464
  /** Resolve which break-even threshold applies for a given agent state (pe-s2ho state-conditioning hook). */
401
465
  export function resolveBreakEvenThreshold(gateConfig: PruneGateConfig, agentState: AgentState): number {
402
466
  const override = gateConfig.breakEvenThresholdByState[agentState];
@@ -892,6 +956,20 @@ function contentTextLength(content: (MinimalTextContent | MinimalImageContent)[]
892
956
  return content.reduce((sum, block) => (block.type === "text" ? sum + block.text.length : sum), 0);
893
957
  }
894
958
 
959
+ /** Return true when a decision targets a tool result containing one or more images. */
960
+ function isImageBearingToolResultTarget(
961
+ messages: MinimalMessage[],
962
+ decision: PruneDecisionRecord,
963
+ pairIndex?: Map<string, ToolCallPairIndices>,
964
+ ): boolean {
965
+ if (decision.kind !== "tool_result_content" || decision.correlation.type !== "toolCallId") return false;
966
+ const pair = findToolCallPairIndices(messages, decision.correlation.toolCallId, pairIndex);
967
+ if (pair.resultIndex === undefined) return false;
968
+ const target = messages[pair.resultIndex];
969
+ if (target?.role !== "toolResult") return false;
970
+ return (target as MinimalToolResultMessage).content.some((block) => block.type === "image");
971
+ }
972
+
895
973
  export interface ApplyResult {
896
974
  applied: boolean;
897
975
  charsRemoved: number;
@@ -1050,6 +1128,55 @@ export function estimateDecisionSavings(
1050
1128
  return undefined;
1051
1129
  }
1052
1130
 
1131
+ /**
1132
+ * Estimate the character savings of a single decision (pe-qdzb), i.e. the
1133
+ * same before/after delta basis as `estimateDecisionSavings` but measured in
1134
+ * raw characters rather than estimated tokens. Used by the pipeline to
1135
+ * enforce `thresholds.minCharsSaved` against fresh automatic proposals
1136
+ * *before* they reach the net-benefit gate. Returns undefined under the same
1137
+ * conditions as `estimateDecisionSavings` (target absent / would not apply).
1138
+ * Mirrors `estimateDecisionSavings`'s `Math.max(0, ...)` clamp, so a
1139
+ * decision whose placeholder text would be LONGER than the original content
1140
+ * (negative savings) is reported as 0 chars saved, not a negative number.
1141
+ */
1142
+ export function estimateDecisionCharsSaved(
1143
+ messages: MinimalMessage[],
1144
+ decision: PruneDecisionRecord,
1145
+ pairIndex?: Map<string, ToolCallPairIndices>,
1146
+ ): number | undefined {
1147
+ if (decision.correlation.type !== "toolCallId") return undefined;
1148
+ const pair = findToolCallPairIndices(messages, decision.correlation.toolCallId, pairIndex);
1149
+
1150
+ // Same non-mutating preview approach as `estimateDecisionSavings`.
1151
+ const preview = messages.slice();
1152
+ const result = applyPruneDecision(preview, decision, pairIndex);
1153
+ if (!result.applied) return undefined;
1154
+
1155
+ if (decision.kind === "tool_result_content") {
1156
+ if (pair.resultIndex === undefined) return undefined;
1157
+ const before = messages[pair.resultIndex] as MinimalToolResultMessage;
1158
+ const after = preview[pair.resultIndex] as MinimalToolResultMessage;
1159
+ const charsBefore = contentTextLength(before.content);
1160
+ const charsAfter = contentTextLength(after.content);
1161
+ return Math.max(0, charsBefore - charsAfter);
1162
+ }
1163
+
1164
+ if (decision.kind === "tool_call_input") {
1165
+ if (pair.assistantIndex === undefined || pair.toolCallBlockIndex === undefined) return undefined;
1166
+ const beforeBlock = (messages[pair.assistantIndex] as MinimalAssistantMessage).content[
1167
+ pair.toolCallBlockIndex
1168
+ ] as MinimalToolCallContent;
1169
+ const afterBlock = (preview[pair.assistantIndex] as MinimalAssistantMessage).content[
1170
+ pair.toolCallBlockIndex
1171
+ ] as MinimalToolCallContent;
1172
+ const charsBefore = JSON.stringify(beforeBlock.arguments ?? {}).length;
1173
+ const charsAfter = JSON.stringify(afterBlock.arguments ?? {}).length;
1174
+ return Math.max(0, charsBefore - charsAfter);
1175
+ }
1176
+
1177
+ return undefined;
1178
+ }
1179
+
1053
1180
  /**
1054
1181
  * Cache-aware cost model (pe-s2ho ticket NOTES, binding over the looser prose
1055
1182
  * in the ticket body): prompt caches are prefix-based, so pruning at message
@@ -1877,7 +2004,29 @@ export function runDynamicContextPruningPipeline(input: PipelineInput): Pipeline
1877
2004
  // against the pre-mutation `messages` clone, before the apply loop below
1878
2005
  // mutates anything.
1879
2006
  const freshManual = dedupedFresh.filter((decision) => decision.source === "manual");
1880
- const freshAutomatic = dedupedFresh.filter((decision) => decision.source !== "manual");
2007
+ const freshAutomaticUnfiltered = dedupedFresh.filter((decision) => decision.source !== "manual");
2008
+
2009
+ // Minimum-size floor (pe-qdzb): drop fresh automatic proposals whose
2010
+ // character savings (original content chars minus placeholder chars) fall
2011
+ // below `thresholds.minCharsSaved`, BEFORE gate evaluation so dropped
2012
+ // candidates never consume batch/earliest-position gate consideration.
2013
+ // Applies to automatic proposals only; manual (`freshManual` above) and
2014
+ // already-persisted decisions (`input.persistedDecisions`, applied later)
2015
+ // bypass this floor entirely, same as the net-benefit gate. Tool-result
2016
+ // candidates containing images also bypass this raw-text floor because
2017
+ // images have no character count; their token savings still go through the
2018
+ // existing net-benefit gate. A floor of 0 (the config default before
2019
+ // pe-c5n9's DEFAULT_MIN_CHARS_SAVED constant was introduced) is a no-op:
2020
+ // every non-negative delta clears it.
2021
+ const minCharsSaved = input.config.thresholds.minCharsSaved;
2022
+ const freshAutomatic =
2023
+ minCharsSaved <= 0
2024
+ ? freshAutomaticUnfiltered
2025
+ : freshAutomaticUnfiltered.filter((decision) => {
2026
+ if (isImageBearingToolResultTarget(messages, decision, pairIndex)) return true;
2027
+ const charsSaved = estimateDecisionCharsSaved(messages, decision, pairIndex);
2028
+ return charsSaved !== undefined && charsSaved >= minCharsSaved;
2029
+ });
1881
2030
 
1882
2031
  // Collapse fresh AUTOMATIC candidates that target the same (kind,
1883
2032
  // correlation) down to a single decision (pe-j7sb). Two strategies (e.g.
@@ -2360,9 +2509,10 @@ export default function dynamicContextPruningExtension(pi: ExtensionAPI) {
2360
2509
  persistedDecisions,
2361
2510
  knownIdempotencyKeys,
2362
2511
  restoredIdempotencyKeys,
2363
- // Real mid-loop/idle agent-state detection lands in a follow-up ticket;
2364
- // "idle" is the safe default until it's wired through ctx.
2365
- agentState: "idle",
2512
+ // Real mid-loop/idle agent-state detection (pe-zy4s): derived straight from
2513
+ // the message payload via classifyAgentStateFromMessages -- see its doc
2514
+ // comment for the exact runtime-observable definition.
2515
+ agentState: classifyAgentStateFromMessages(event.messages as unknown as MinimalMessage[]),
2366
2516
  cwd: ctx.sessionManager.getCwd(),
2367
2517
  });
2368
2518
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-dynamic-context-pruning",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "A pi extension that dynamically prunes stale context to keep sessions lean.",
6
6
  "keywords": [
@@ -0,0 +1 @@
1
+ 0.83.0
@@ -0,0 +1,119 @@
1
+ # fast
2
+
3
+ A pi extension that provides one `/fast` toggle for supported OpenAI Codex and Anthropic Claude models. It selects the provider-specific Fast-mode request shape from the currently selected model.
4
+
5
+ For eligible OpenAI Codex models it injects:
6
+
7
+ ```json
8
+ {
9
+ "service_tier": "priority"
10
+ }
11
+ ```
12
+
13
+ For eligible Anthropic Claude models it injects:
14
+
15
+ ```json
16
+ {
17
+ "speed": "fast"
18
+ }
19
+ ```
20
+
21
+ It also adds the required Anthropic beta header value:
22
+
23
+ ```text
24
+ anthropic-beta: fast-mode-2026-02-01
25
+ ```
26
+
27
+ ## Eligibility
28
+
29
+ OpenAI Fast mode requires all of the following:
30
+
31
+ - Provider `openai-codex`.
32
+ - API `openai-codex-responses`.
33
+ - Model `gpt-5.4`, `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, or `gpt-5.6-luna`.
34
+ - ChatGPT OAuth/subscription auth, not API-key auth.
35
+ - No existing `service_tier` field in the request payload.
36
+
37
+ Anthropic Fast mode requires all of the following:
38
+
39
+ - Provider `anthropic`.
40
+ - API `anthropic-messages`.
41
+ - Model `claude-opus-4-6`, `claude-opus-4-7`, or `claude-opus-4-8`.
42
+ - No existing `speed` field in the request payload.
43
+
44
+ Anthropic Fast mode supports both API-key and Claude Code OAuth access when the account has access to the research preview.
45
+
46
+ ## Commands
47
+
48
+ ```text
49
+ /fast
50
+ ```
51
+
52
+ Run `/fast` to toggle Fast mode for the current session/runtime. The enabled state follows model changes: switching between eligible OpenAI and Anthropic models automatically changes the request format without resetting the toggle. The footer shows `fast` only while the selected model is eligible and Fast mode is enabled.
53
+
54
+ The extension defaults to off so installing the full collection does not accidentally spend Fast-mode credits.
55
+
56
+ ## Config
57
+
58
+ Optional global config:
59
+
60
+ ```text
61
+ ~/<pi-config-dir>/agent/extensions/fast.json
62
+ ```
63
+
64
+ Optional project config:
65
+
66
+ ```text
67
+ <project>/<pi-config-dir>/fast.json
68
+ ```
69
+
70
+ Here `<pi-config-dir>` is Pi's runtime config directory name (`CONFIG_DIR_NAME`; `.pi` by default). Project config overrides global config after Pi reports that the project is trusted.
71
+
72
+ ```json
73
+ {
74
+ "enabled": false,
75
+ "showStatus": true
76
+ }
77
+ ```
78
+
79
+ - `enabled`: default Fast-mode state when there is no session override.
80
+ - `showStatus`: show a compact `fast` status when Fast mode is active for the selected model.
81
+
82
+ The unified extension intentionally does not read `openai-fast.json` or `claude-fast.json`; those files belong to the provider-specific standalone packages and may contain conflicting defaults.
83
+
84
+ ## Install
85
+
86
+ ### Standalone npm package
87
+
88
+ ```bash
89
+ pi install npm:@diegopetrucci/pi-fast
90
+ ```
91
+
92
+ ### Collection package
93
+
94
+ ```bash
95
+ pi install npm:@diegopetrucci/pi-extensions
96
+ ```
97
+
98
+ ### GitHub package
99
+
100
+ ```bash
101
+ pi install git:github.com/diegopetrucci/pi-extensions
102
+ ```
103
+
104
+ Then reload pi:
105
+
106
+ ```text
107
+ /reload
108
+ ```
109
+
110
+ ## Legacy provider-specific packages
111
+
112
+ The collection package loads this unified extension instead of `openai-fast` and `claude-fast`. Both provider-specific packages remain available as standalone alternatives. Do not load them together with this extension: independent Fast-mode state and duplicate `/fast` registrations can produce conflicting behavior.
113
+
114
+ ## Notes
115
+
116
+ - Anthropic Fast mode has separate rate limits, costs more than standard speed, and does not share prompt-cache prefixes with standard-speed requests.
117
+ - OpenAI Fast mode intentionally does not affect API-key models.
118
+ - Existing `speed` and `service_tier` fields are never overwritten.
119
+ - Cost accounting still depends on the provider reporting the effective tier in its streamed response.
@@ -0,0 +1,4 @@
1
+ {
2
+ "enabled": false,
3
+ "showStatus": true
4
+ }
@@ -0,0 +1,383 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import {
4
+ CONFIG_DIR_NAME,
5
+ getAgentDir,
6
+ type ExtensionAPI,
7
+ type ExtensionContext,
8
+ } from "@earendil-works/pi-coding-agent";
9
+
10
+ const EXTENSION_ID = "fast";
11
+ const ANTHROPIC_PROVIDER_ID = "anthropic";
12
+ const ANTHROPIC_API_ID = "anthropic-messages";
13
+ const ANTHROPIC_FAST_SPEED = "fast";
14
+ const ANTHROPIC_FAST_BETA = "fast-mode-2026-02-01";
15
+ const CLAUDE_CODE_OAUTH_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
16
+ const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
17
+ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
18
+ const ANTHROPIC_SUPPORTED_MODELS = new Set(["claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8"]);
19
+
20
+ const OPENAI_PROVIDER_ID = "openai-codex";
21
+ const OPENAI_API_ID = "openai-codex-responses";
22
+ const OPENAI_FAST_SERVICE_TIER = "priority";
23
+ const OPENAI_SUPPORTED_MODELS = new Set([
24
+ "gpt-5.4",
25
+ "gpt-5.5",
26
+ "gpt-5.6-sol",
27
+ "gpt-5.6-terra",
28
+ "gpt-5.6-luna",
29
+ ]);
30
+
31
+ const DEFAULT_CONFIG: FastConfig = {
32
+ enabled: false,
33
+ showStatus: true,
34
+ };
35
+
36
+ type FastOverride = "auto" | "on" | "off";
37
+ type FastProvider = "anthropic" | "openai";
38
+
39
+ type FastConfig = {
40
+ /** Default Fast-mode state when there is no session override. */
41
+ enabled: boolean;
42
+ /** Show a compact `fast` status when Fast mode is active for the current model. */
43
+ showStatus: boolean;
44
+ };
45
+
46
+ type SessionState = {
47
+ config: FastConfig;
48
+ override: FastOverride;
49
+ lastInjectedAt?: number;
50
+ lastInjectedModel?: string;
51
+ };
52
+
53
+ type ProjectConfigContext = {
54
+ cwd: string;
55
+ isProjectTrusted?: () => boolean;
56
+ };
57
+
58
+ type RecursivePartial<T> = {
59
+ [P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
60
+ };
61
+
62
+ type PayloadRecord = Record<string, unknown>;
63
+
64
+ type AnthropicHeaderCompat = {
65
+ forceAdaptiveThinking?: boolean;
66
+ supportsEagerToolInputStreaming?: boolean;
67
+ };
68
+
69
+ type Eligibility = {
70
+ eligible: boolean;
71
+ modelKey: string;
72
+ provider?: FastProvider;
73
+ reason?: string;
74
+ };
75
+
76
+ function readConfigFile(path: string): RecursivePartial<FastConfig> {
77
+ if (!existsSync(path)) return {};
78
+
79
+ try {
80
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
81
+ return isPayloadRecord(parsed) ? (parsed as RecursivePartial<FastConfig>) : {};
82
+ } catch (error) {
83
+ console.error(`Warning: Could not parse ${path}: ${error}`);
84
+ return {};
85
+ }
86
+ }
87
+
88
+ function normalizeBoolean(value: unknown, fallback: boolean): boolean {
89
+ return typeof value === "boolean" ? value : fallback;
90
+ }
91
+
92
+ function mergeConfig(base: FastConfig, overrides: RecursivePartial<FastConfig>): FastConfig {
93
+ return {
94
+ enabled: normalizeBoolean(overrides.enabled, base.enabled),
95
+ showStatus: normalizeBoolean(overrides.showStatus, base.showStatus),
96
+ };
97
+ }
98
+
99
+ function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
100
+ return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
101
+ }
102
+
103
+ function findProjectConfigPath(cwd: string): string {
104
+ let current = cwd;
105
+ while (true) {
106
+ const candidate = join(current, CONFIG_DIR_NAME, "fast.json");
107
+ if (existsSync(candidate)) return candidate;
108
+
109
+ const parent = dirname(current);
110
+ if (parent === current) return join(cwd, CONFIG_DIR_NAME, "fast.json");
111
+ current = parent;
112
+ }
113
+ }
114
+
115
+ function loadConfig(ctx: ProjectConfigContext): FastConfig {
116
+ const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "fast.json"));
117
+ const projectConfig = canReadProjectConfig(ctx) ? readConfigFile(findProjectConfigPath(ctx.cwd)) : {};
118
+ return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
119
+ }
120
+
121
+ function isPayloadRecord(payload: unknown): payload is PayloadRecord {
122
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload);
123
+ }
124
+
125
+ function modelKey(ctx: ExtensionContext): string {
126
+ const model = ctx.model;
127
+ return model ? `${model.provider}/${model.id}` : "no-model";
128
+ }
129
+
130
+ function isFastEnabled(state: SessionState): boolean {
131
+ if (state.override === "on") return true;
132
+ if (state.override === "off") return false;
133
+ return state.config.enabled;
134
+ }
135
+
136
+ function describeMode(state: SessionState): string {
137
+ if (state.override === "on") return "on (session override)";
138
+ if (state.override === "off") return "off (session override)";
139
+ return state.config.enabled ? "on (config default)" : "off (config default)";
140
+ }
141
+
142
+ function getEligibility(ctx: ExtensionContext): Eligibility {
143
+ const model = ctx.model;
144
+ if (!model) {
145
+ return { eligible: false, modelKey: "no-model", reason: "no model is selected" };
146
+ }
147
+
148
+ const key = `${model.provider}/${model.id}`;
149
+ if (model.provider === ANTHROPIC_PROVIDER_ID) {
150
+ if (model.api !== ANTHROPIC_API_ID) {
151
+ return {
152
+ eligible: false,
153
+ modelKey: key,
154
+ reason: `current API is ${model.api}, not ${ANTHROPIC_API_ID}`,
155
+ };
156
+ }
157
+
158
+ if (!ANTHROPIC_SUPPORTED_MODELS.has(model.id)) {
159
+ return {
160
+ eligible: false,
161
+ modelKey: key,
162
+ reason: "Fast mode is only enabled for Claude Opus 4.6, 4.7, and 4.8",
163
+ };
164
+ }
165
+
166
+ return { eligible: true, modelKey: key, provider: "anthropic" };
167
+ }
168
+
169
+ if (model.provider === OPENAI_PROVIDER_ID) {
170
+ if (model.api !== OPENAI_API_ID) {
171
+ return {
172
+ eligible: false,
173
+ modelKey: key,
174
+ reason: `current API is ${model.api}, not ${OPENAI_API_ID}`,
175
+ };
176
+ }
177
+
178
+ if (!OPENAI_SUPPORTED_MODELS.has(model.id)) {
179
+ return {
180
+ eligible: false,
181
+ modelKey: key,
182
+ reason: "Fast mode is not enabled for this OpenAI Codex model",
183
+ };
184
+ }
185
+
186
+ if (!ctx.modelRegistry.isUsingOAuth(model)) {
187
+ return {
188
+ eligible: false,
189
+ modelKey: key,
190
+ reason: "ChatGPT OAuth auth is required; API-key auth is intentionally not used",
191
+ };
192
+ }
193
+
194
+ return { eligible: true, modelKey: key, provider: "openai" };
195
+ }
196
+
197
+ return {
198
+ eligible: false,
199
+ modelKey: key,
200
+ reason: `current provider ${model.provider} does not support this extension's Fast mode`,
201
+ };
202
+ }
203
+
204
+ function updateStatus(ctx: ExtensionContext, state: SessionState): void {
205
+ if (!ctx.hasUI) return;
206
+ if (!state.config.showStatus) {
207
+ ctx.ui.setStatus(EXTENSION_ID, undefined);
208
+ return;
209
+ }
210
+
211
+ const eligibility = getEligibility(ctx);
212
+ ctx.ui.setStatus(
213
+ EXTENSION_ID,
214
+ isFastEnabled(state) && eligibility.eligible ? "fast" : undefined,
215
+ );
216
+ }
217
+
218
+ function getStatusMessage(ctx: ExtensionContext, state: SessionState): string {
219
+ const enabled = isFastEnabled(state);
220
+ const eligibility = getEligibility(ctx);
221
+ const active = enabled && eligibility.eligible;
222
+ const injected = state.lastInjectedAt
223
+ ? ` Last injected for ${state.lastInjectedModel ?? "unknown model"} ${Math.max(0, Math.round((Date.now() - state.lastInjectedAt) / 1000))}s ago.`
224
+ : "";
225
+
226
+ if (active) {
227
+ const wireSetting =
228
+ eligibility.provider === "anthropic"
229
+ ? `speed=${ANTHROPIC_FAST_SPEED}`
230
+ : `service_tier=${OPENAI_FAST_SERVICE_TIER}`;
231
+ return `Fast mode is ${describeMode(state)} and active for ${eligibility.modelKey}; requests will use ${wireSetting}.${injected}`;
232
+ }
233
+
234
+ if (enabled) {
235
+ return `Fast mode is ${describeMode(state)}, but inactive for ${eligibility.modelKey}: ${eligibility.reason}.${injected}`;
236
+ }
237
+
238
+ return `Fast mode is ${describeMode(state)}. Current model: ${eligibility.modelKey}.${injected}`;
239
+ }
240
+
241
+ function splitBetaHeader(value: string | null | undefined): string[] {
242
+ return (value ?? "")
243
+ .split(",")
244
+ .map((part) => part.trim())
245
+ .filter(Boolean);
246
+ }
247
+
248
+ async function isAnthropicOAuth(ctx: ExtensionContext): Promise<boolean> {
249
+ try {
250
+ const resolution = await ctx.modelRegistry.getProviderAuth(ANTHROPIC_PROVIDER_ID);
251
+ const apiKey = resolution?.auth.apiKey;
252
+ if (typeof apiKey === "string") return apiKey.includes("sk-ant-oat");
253
+ } catch {
254
+ // Fall back to the synchronous registry snapshot below.
255
+ }
256
+
257
+ return ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false;
258
+ }
259
+
260
+ function getAnthropicProviderBetas(ctx: ExtensionContext, hasActiveTools: boolean): string[] {
261
+ const model = ctx.model;
262
+ if (!model) return [];
263
+ const compat = model.compat as AnthropicHeaderCompat | undefined;
264
+
265
+ const betas: string[] = [];
266
+ if (hasActiveTools && compat?.supportsEagerToolInputStreaming === false) {
267
+ betas.push(FINE_GRAINED_TOOL_STREAMING_BETA);
268
+ }
269
+ if (compat?.forceAdaptiveThinking !== true) {
270
+ betas.push(INTERLEAVED_THINKING_BETA);
271
+ }
272
+ return betas;
273
+ }
274
+
275
+ async function injectAnthropicFastHeader(
276
+ headers: Record<string, string | null>,
277
+ ctx: ExtensionContext,
278
+ state: SessionState,
279
+ hasActiveTools: boolean,
280
+ ): Promise<void> {
281
+ if (!isFastEnabled(state)) return;
282
+ const eligibility = getEligibility(ctx);
283
+ if (!eligibility.eligible || eligibility.provider !== "anthropic") return;
284
+
285
+ const matchingKeys = Object.keys(headers).filter((name) => name.toLowerCase() === "anthropic-beta");
286
+ const existing = matchingKeys.flatMap((name) => splitBetaHeader(headers[name]));
287
+ const matchingModelEntries = Object.entries(ctx.model?.headers ?? {}).filter(
288
+ ([name]) => name.toLowerCase() === "anthropic-beta",
289
+ );
290
+ const modelBetas = matchingModelEntries.flatMap(([, value]) => splitBetaHeader(value));
291
+ const providerBetas = getAnthropicProviderBetas(ctx, hasActiveTools);
292
+ const oauthBetas = (await isAnthropicOAuth(ctx)) ? CLAUDE_CODE_OAUTH_BETAS : [];
293
+ const next = Array.from(
294
+ new Set([...modelBetas, ...existing, ...oauthBetas, ...providerBetas, ANTHROPIC_FAST_BETA]),
295
+ );
296
+
297
+ for (const key of matchingKeys) delete headers[key];
298
+ const outputKey = matchingModelEntries.at(-1)?.[0] ?? "anthropic-beta";
299
+ headers[outputKey] = next.join(",");
300
+ }
301
+
302
+ function injectFastPayload(
303
+ payload: unknown,
304
+ ctx: ExtensionContext,
305
+ state: SessionState,
306
+ ): PayloadRecord | undefined {
307
+ if (!isFastEnabled(state)) return undefined;
308
+ const eligibility = getEligibility(ctx);
309
+ if (!eligibility.eligible || !eligibility.provider) return undefined;
310
+ if (!isPayloadRecord(payload)) return undefined;
311
+ if (payload.model !== ctx.model?.id) return undefined;
312
+
313
+ if (eligibility.provider === "anthropic") {
314
+ if ("speed" in payload) return undefined;
315
+ state.lastInjectedAt = Date.now();
316
+ state.lastInjectedModel = eligibility.modelKey;
317
+ return { ...payload, speed: ANTHROPIC_FAST_SPEED };
318
+ }
319
+
320
+ if ("service_tier" in payload) return undefined;
321
+ state.lastInjectedAt = Date.now();
322
+ state.lastInjectedModel = eligibility.modelKey;
323
+ return { ...payload, service_tier: OPENAI_FAST_SERVICE_TIER };
324
+ }
325
+
326
+ export default function fastExtension(pi: ExtensionAPI) {
327
+ const states = new WeakMap<object, SessionState>();
328
+
329
+ function getState(ctx: ExtensionContext): SessionState {
330
+ let state = states.get(ctx.sessionManager);
331
+ if (!state) {
332
+ state = {
333
+ config: loadConfig(ctx),
334
+ override: "auto",
335
+ };
336
+ states.set(ctx.sessionManager, state);
337
+ }
338
+ return state;
339
+ }
340
+
341
+ pi.on("session_start", (_event, ctx) => {
342
+ const state: SessionState = {
343
+ config: loadConfig(ctx),
344
+ override: "auto",
345
+ };
346
+ states.set(ctx.sessionManager, state);
347
+ updateStatus(ctx, state);
348
+ });
349
+
350
+ pi.on("model_select", (_event, ctx) => {
351
+ updateStatus(ctx, getState(ctx));
352
+ });
353
+
354
+ pi.on("before_provider_headers", async (event, ctx) => {
355
+ const hasActiveTools = typeof pi.getActiveTools === "function" && pi.getActiveTools().length > 0;
356
+ await injectAnthropicFastHeader(event.headers, ctx, getState(ctx), hasActiveTools);
357
+ });
358
+
359
+ pi.on("before_provider_request", (event, ctx) => {
360
+ const state = getState(ctx);
361
+ const nextPayload = injectFastPayload(event.payload, ctx, state);
362
+ updateStatus(ctx, state);
363
+ return nextPayload;
364
+ });
365
+
366
+ pi.registerCommand("fast", {
367
+ description: "Toggle Fast mode for supported OpenAI Codex and Anthropic Claude models",
368
+ getArgumentCompletions: () => null,
369
+ handler: async (args, ctx) => {
370
+ const state = getState(ctx);
371
+ const action = args.trim();
372
+
373
+ if (!action) {
374
+ state.override = isFastEnabled(state) ? "off" : "on";
375
+ updateStatus(ctx, state);
376
+ ctx.ui.notify(getStatusMessage(ctx, state), "info");
377
+ return;
378
+ }
379
+
380
+ ctx.ui.notify("Usage: /fast", "warning");
381
+ },
382
+ });
383
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@diegopetrucci/pi-fast",
3
+ "version": "0.1.0",
4
+ "description": "A pi extension that enables Fast mode for supported OpenAI Codex and Anthropic Claude models based on the currently selected model.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi",
8
+ "openai",
9
+ "anthropic",
10
+ "fast"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/diegopetrucci/pi-extensions.git",
16
+ "directory": "extensions/fast"
17
+ },
18
+ "files": [
19
+ "index.ts",
20
+ "fast.example.json",
21
+ "README.md",
22
+ ".pi-fleet-tested-version"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "pi": {
28
+ "extensions": [
29
+ "index.ts"
30
+ ]
31
+ },
32
+ "peerDependencies": {
33
+ "@earendil-works/pi-coding-agent": "*"
34
+ },
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=22.19.0"
38
+ }
39
+ }
@@ -2,6 +2,8 @@
2
2
 
3
3
  A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4, GPT-5.5, and GPT-5.6 Codex variants (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`).
4
4
 
5
+ This package is standalone-only and is not auto-loaded by the `@diegopetrucci/pi-extensions` collection package. The collection uses the unified [`fast`](../fast) extension instead.
6
+
5
7
  When active, the extension injects this into eligible OpenAI Codex request payloads:
6
8
 
7
9
  ```json
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-openai-fast",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4, GPT-5.5, and GPT-5.6 Codex variants by injecting the priority service tier.",
5
5
  "keywords": [
6
6
  "pi-package",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "description": "A collection of pi extensions and skills for annotation UIs, context management, workflow audits, contrarian review, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, model/provider helpers, and Xiaohei-style article illustrations.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -45,7 +45,6 @@
45
45
  "./extensions/annotate-git-diff/index.ts",
46
46
  "./extensions/annotate-last-message/index.ts",
47
47
  "./extensions/brrr/index.ts",
48
- "./extensions/claude-fast/index.ts",
49
48
  "./extensions/code-reviewer/index.ts",
50
49
  "./extensions/confirm-destructive/index.ts",
51
50
  "./extensions/context-cap/index.ts",
@@ -53,12 +52,12 @@
53
52
  "./extensions/contrarian/index.ts",
54
53
  "./extensions/dirty-repo-guard/index.ts",
55
54
  "./extensions/dynamic-context-pruning/index.ts",
55
+ "./extensions/fast/index.ts",
56
56
  "./extensions/gnosis/index.ts",
57
57
  "./extensions/inline-bash/index.ts",
58
58
  "./extensions/librarian/index.ts",
59
59
  "./extensions/minimal-footer/index.ts",
60
60
  "./extensions/notify/index.ts",
61
- "./extensions/openai-fast/index.ts",
62
61
  "./extensions/oracle/index.ts",
63
62
  "./extensions/permission-gate/index.ts",
64
63
  "./extensions/quiet-tools/index.ts",