@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 +3 -2
- package/extensions/claude-fast/README.md +2 -0
- package/extensions/claude-fast/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +78 -11
- package/extensions/dynamic-context-pruning/index.ts +171 -21
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/.pi-fleet-tested-version +1 -0
- package/extensions/fast/README.md +119 -0
- package/extensions/fast/fast.example.json +4 -0
- package/extensions/fast/index.ts +383 -0
- package/extensions/fast/package.json +39 -0
- package/extensions/openai-fast/README.md +2 -0
- package/extensions/openai-fast/package.json +1 -1
- package/package.json +2 -3
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):
|
|
17
|
-
- [`
|
|
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
|
|
@@ -230,19 +230,32 @@ never required to exist. Full shape, with defaults:
|
|
|
230
230
|
|
|
231
231
|
Notes:
|
|
232
232
|
|
|
233
|
-
- `thresholds.minCharsSaved` is
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
169
|
-
*
|
|
170
|
-
* (
|
|
171
|
-
*
|
|
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
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* default. However, the only live runtime
|
|
201
|
-
* handler below)
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
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
|
|
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
|
|
2364
|
-
//
|
|
2365
|
-
|
|
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
|
|
|
@@ -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,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.
|
|
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.
|
|
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",
|