pi-quiver 5.2.1 → 5.2.3
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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/extensions/fast-mode.ts +99 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,18 @@ Published to npm as `pi-quiver` (`pi install npm:pi-quiver`). Pushing a
|
|
|
8
8
|
via OIDC trusted publishing. The release helper at
|
|
9
9
|
`.agents/skills/release/scripts/release.sh` cuts the tag; CI publishes.
|
|
10
10
|
|
|
11
|
+
## v5.2.3 - 2026-09-04
|
|
12
|
+
|
|
13
|
+
- `fast-mode`: fix `400 fallbacks: Extra inputs are not permitted` on OAuth/subscription auth with fallback-capable models (`claude-opus-5` on pi 0.84.x). The 5.2.2 beta probe imported `@earendil-works/pi-ai/api/anthropic-messages.lazy`, which pi's extension loader does not alias for installed packages, so it always failed in real installs and the static beta list dropped `server-side-fallback-2026-07-01` while pi kept `fallbacks` in the body. Probe now imports the aliased `@earendil-works/pi-ai/compat`; the static fallback also adds the fallback beta whenever `model.compat.allowedFallbackModels` is non-empty.
|
|
14
|
+
|
|
15
|
+
## v5.2.2 - 2026-09-03
|
|
16
|
+
|
|
17
|
+
- fast-mode: discover pi's `anthropic-beta` list at request time by probing
|
|
18
|
+
pi-ai's request assembly (capturing fetch, zero network) instead of
|
|
19
|
+
rebuilding it from a fixed list - fixes 400 "fallbacks: Extra inputs are
|
|
20
|
+
not permitted" on `claude-opus-5` (#11). Probe failure degrades to the
|
|
21
|
+
previous static header.
|
|
22
|
+
|
|
11
23
|
## v5.2.1 - 2026-09-02
|
|
12
24
|
|
|
13
25
|
- sword-header: clip each header line to the terminal width in `render(width)` (ANSI-aware `truncateToWidth`, hard clip). Fixes a hard crash (`Rendered line N exceeds terminal width`) in terminals/panes narrower than 80 columns.
|
package/README.md
CHANGED
|
@@ -184,7 +184,7 @@ survive untouched.
|
|
|
184
184
|
|
|
185
185
|
`herdrTab` (default `true`) mirrors the same curated label to the Herdr tab bar over Herdr's unix socket, independent of `ghosttyTab` - either sink can be toggled off without affecting the other. It's claim-once: the extension adopts a tab only while it still shows its default numeric label (its live 1-based position among the workspace's tabs); a manually renamed tab, or one renamed mid-session by hand, is never touched again for that session - the human's label always wins. On every shutdown (quit, reload, `/new`, resume, fork) a claimed tab's label is restored to its then-current default position, so a successor session in the same pane can claim cleanly. It only ever runs in TUI mode, on an attached TTY, under Herdr (`HERDR_ENV`/`HERDR_TAB_ID`/`HERDR_SOCKET_PATH` all set) - `pi -p`/json/rpc runs and background subagents never touch the tab. Caveats: a hard crash (`kill -9`) skips the restore and leaves the stale label - rename the tab by hand to recover; and the restored label is internally a custom name (Herdr has no clear-to-auto API), so that tab keeps a number-looking label but stops renumbering on later tab closes/reorders.
|
|
186
186
|
|
|
187
|
-
`fastMode` only affects `claude-opus-4-8` and `claude-opus-5` requests on Anthropic's `anthropic-messages` API; enabling it opts into premium fast-mode pricing. `--fast` forces it on for one launch; `/fast on|off` toggles live. Proxy providers (opencode, cloudflare-ai-gateway) are excluded. `fastMode`'s header injection needs the `before_provider_headers` hook (pi bundling `@earendil-works/pi-coding-agent` >= 0.80.5); on older pi the beta header is silently not sent. See [doc/fetch.md](doc/fetch.md) and [doc/doc-to-md.md](doc/doc-to-md.md) for the ingestion tools' full reference; session-name/sword-header behavior above is complete.
|
|
187
|
+
`fastMode` only affects `claude-opus-4-8` and `claude-opus-5` requests on Anthropic's `anthropic-messages` API; enabling it opts into premium fast-mode pricing. `--fast` forces it on for one launch; `/fast on|off` toggles live. Proxy providers (opencode, cloudflare-ai-gateway) are excluded. `fastMode`'s header injection needs the `before_provider_headers` hook (pi bundling `@earendil-works/pi-coding-agent` >= 0.80.5); on older pi the beta header is silently not sent. The `anthropic-beta` header is discovered at request time by probing pi's own request assembly (no network), so conditional betas pi adds - e.g. `server-side-fallback-2026-07-01` for models with server-side fallback - are preserved alongside `fast-mode-2026-02-01`. If the probe fails, the header falls back to the static OAuth-identity + fast-mode list. See [doc/fetch.md](doc/fetch.md) and [doc/doc-to-md.md](doc/doc-to-md.md) for the ingestion tools' full reference; session-name/sword-header behavior above is complete.
|
|
188
188
|
|
|
189
189
|
`pi-ai` prices every fast request at standard rates - it has no `usage.speed` support and no request-level pricing modifier - so `fastMode` corrects the reported cost itself: a `message_end` handler scales all four `usage.cost` components by `FAST_MODE_COST_MULTIPLIER` (2x) and returns the corrected message. Persisted session JSONL and pi's own native cost display are always exact, since they're written from this corrected message. pi-cohort's live `Σ$` reflects the correction only when pi-quiver's `message_end` handler runs before pi-cohort's - best-effort, depending on extension load order - and is reconciled on pi-cohort's next `session_start` regardless. The upstream fix (teaching `pi-ai`'s `Usage`/`calculateCost` about `usage.speed`) is the better long-term path and is tracked separately.
|
|
190
190
|
|
package/extensions/fast-mode.ts
CHANGED
|
@@ -13,16 +13,22 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Header coupling to pi-ai internals: pi assembles `anthropic-beta` AFTER this
|
|
15
15
|
* hook and merges the hook's headers LAST, so setting the header here REPLACES
|
|
16
|
-
* pi's list.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* pi's list. Instead of guessing that list, the hook discovers it: it probes
|
|
17
|
+
* pi-ai's own request assembly (dynamic import of `@earendil-works/pi-ai/compat`,
|
|
18
|
+
* one of the four specifiers pi's extension loader aliases for installed
|
|
19
|
+
* packages - deeper subpaths like `/api/*.lazy` do NOT resolve there) with a
|
|
20
|
+
* capturing fetch - zero network, zero tokens - and unions
|
|
21
|
+
* the captured betas with the fast-mode beta. Probe failure falls back to the
|
|
22
|
+
* static reconstruction (OAUTH_IDENTITY_BETAS stays, demoted to fallback-only
|
|
23
|
+
* seeding). Known limit: the probe context is tool-less, so tool/thinking-
|
|
24
|
+
* conditional betas are captured under fixed defaults - fine while allowlisted
|
|
25
|
+
* models keep forceAdaptiveThinking: true in the catalog. Probe cost: one
|
|
26
|
+
* extra in-process request-assembly pass per qualifying call, no network -
|
|
27
|
+
* per-request by design, no cache.
|
|
22
28
|
*/
|
|
23
29
|
|
|
24
30
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import type { Usage } from "@earendil-works/pi-ai";
|
|
31
|
+
import type { Context, Model, StreamOptions, Usage } from "@earendil-works/pi-ai";
|
|
26
32
|
import { resolveConfig } from "../lib/extension-config.ts";
|
|
27
33
|
|
|
28
34
|
export const FAST_MODE_BETA = "fast-mode-2026-02-01";
|
|
@@ -46,6 +52,15 @@ export function scaleCost(cost: Usage["cost"], multiplier: number): Usage["cost"
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
export const OAUTH_IDENTITY_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
|
|
55
|
+
// Mirrors pi-ai's SERVER_SIDE_FALLBACK_BETA: pi puts `fallbacks` in the body
|
|
56
|
+
// whenever model.compat.allowedFallbackModels is non-empty, and OAuth rejects
|
|
57
|
+
// that body without the beta (400 "fallbacks: Extra inputs are not permitted").
|
|
58
|
+
export const SERVER_SIDE_FALLBACK_BETA = "server-side-fallback-2026-07-01";
|
|
59
|
+
|
|
60
|
+
export function needsFallbackBeta(model: Model<any> | undefined): boolean {
|
|
61
|
+
const compat = model?.compat as { allowedFallbackModels?: unknown[] } | undefined;
|
|
62
|
+
return (compat?.allowedFallbackModels?.length ?? 0) > 0;
|
|
63
|
+
}
|
|
49
64
|
const STATUS_KEY = "fast-mode";
|
|
50
65
|
const BETA_HEADER = "anthropic-beta";
|
|
51
66
|
|
|
@@ -82,7 +97,12 @@ export function injectSpeed(payload: unknown): unknown {
|
|
|
82
97
|
return { ...(payload as Record<string, unknown>), speed: FAST_SPEED };
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
export function buildBetaHeader(
|
|
100
|
+
export function buildBetaHeader(
|
|
101
|
+
existing: string | null | undefined,
|
|
102
|
+
isOAuth: boolean,
|
|
103
|
+
probedBetas: string | null = null,
|
|
104
|
+
fallbackBeta = false,
|
|
105
|
+
): string {
|
|
86
106
|
const seen = new Set<string>();
|
|
87
107
|
const out: string[] = [];
|
|
88
108
|
const add = (b: string): void => {
|
|
@@ -92,12 +112,65 @@ export function buildBetaHeader(existing: string | null | undefined, isOAuth: bo
|
|
|
92
112
|
out.push(t);
|
|
93
113
|
}
|
|
94
114
|
};
|
|
95
|
-
if (
|
|
115
|
+
if (probedBetas !== null) probedBetas.split(",").forEach(add);
|
|
116
|
+
else {
|
|
117
|
+
if (isOAuth) OAUTH_IDENTITY_BETAS.forEach(add);
|
|
118
|
+
if (fallbackBeta) add(SERVER_SIDE_FALLBACK_BETA);
|
|
119
|
+
}
|
|
96
120
|
if (typeof existing === "string") existing.split(",").forEach(add);
|
|
97
121
|
add(FAST_MODE_BETA);
|
|
98
122
|
return out.join(",");
|
|
99
123
|
}
|
|
100
124
|
|
|
125
|
+
export type ProbeAuth = { apiKey?: string; headers?: Record<string, string | null> };
|
|
126
|
+
|
|
127
|
+
// Minimal context for the beta probe: one user message, no tools, no system
|
|
128
|
+
// prompt. Option-dependent betas (fine-grained tool streaming) are therefore
|
|
129
|
+
// probed under fixed defaults - sufficient while the allowlisted models keep
|
|
130
|
+
// forceAdaptiveThinking: true in the catalog; a future allowlisted model with
|
|
131
|
+
// tool- or thinking-conditional betas would have those dropped, same as today.
|
|
132
|
+
const PROBE_CONTEXT: Context = { messages: [{ role: "user", content: "probe", timestamp: 0 }] };
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Discover the anthropic-beta list pi-ai would send for this model + auth by
|
|
136
|
+
* driving pi-ai's own request assembly against a capturing fetch. Zero
|
|
137
|
+
* network, zero tokens: the fetch records the request headers, then throws.
|
|
138
|
+
* Returns the captured header value, or null on any failure - missing
|
|
139
|
+
* optional peer (the guarded dynamic import() below), probe throw before
|
|
140
|
+
* capture, or no header present - and the caller falls back to the static
|
|
141
|
+
* reconstruction. maxRetries: 0 stops pi-ai's retry wrapper from re-driving
|
|
142
|
+
* the intentionally failing probe fetch.
|
|
143
|
+
*/
|
|
144
|
+
export async function probePiBetaHeader(
|
|
145
|
+
model: Model<any>,
|
|
146
|
+
auth: ProbeAuth,
|
|
147
|
+
fetchImpl?: (input: any, init?: any) => Promise<any>,
|
|
148
|
+
): Promise<string | null> {
|
|
149
|
+
let captured: string | null = null;
|
|
150
|
+
const capturingFetch = async (input: any, init?: any): Promise<any> => {
|
|
151
|
+
const headers = init?.headers ?? input?.headers;
|
|
152
|
+
let value: unknown;
|
|
153
|
+
if (headers && typeof headers.get === "function") value = headers.get(BETA_HEADER);
|
|
154
|
+
else if (headers && typeof headers === "object") value = (headers as Record<string, unknown>)[BETA_HEADER];
|
|
155
|
+
if (typeof value === "string") captured = value;
|
|
156
|
+
throw new Error("fast-mode beta probe abort");
|
|
157
|
+
};
|
|
158
|
+
const options: StreamOptions = {
|
|
159
|
+
apiKey: auth.apiKey,
|
|
160
|
+
headers: auth.headers,
|
|
161
|
+
fetch: (fetchImpl ?? capturingFetch) as unknown as StreamOptions["fetch"],
|
|
162
|
+
maxRetries: 0,
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
const { anthropicMessagesApi } = await import("@earendil-works/pi-ai/compat");
|
|
166
|
+
const stream = anthropicMessagesApi().stream(model, PROBE_CONTEXT, options);
|
|
167
|
+
await stream.result().catch(() => {});
|
|
168
|
+
} catch {
|
|
169
|
+
return captured;
|
|
170
|
+
}
|
|
171
|
+
return captured;
|
|
172
|
+
}
|
|
173
|
+
|
|
101
174
|
type State = { config: boolean; flag: boolean; live: boolean | null };
|
|
102
175
|
|
|
103
176
|
export function resolveEnabled(s: State): boolean {
|
|
@@ -129,12 +202,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
129
202
|
ctx.ui.setStatus(STATUS_KEY, shouldInject(enabled, ctx.model) ? "\u26a1 fast" : "\u26a1 n/a");
|
|
130
203
|
};
|
|
131
204
|
|
|
132
|
-
|
|
205
|
+
type ResolvedAuth = ProbeAuth & { isOAuth: boolean };
|
|
206
|
+
|
|
207
|
+
const resolveAuth = async (ctx: ExtensionContext): Promise<ResolvedAuth | null> => {
|
|
133
208
|
if (!ctx.model) return null;
|
|
134
209
|
try {
|
|
135
210
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
136
211
|
if (!auth.ok) return null;
|
|
137
|
-
return
|
|
212
|
+
return {
|
|
213
|
+
isOAuth: typeof auth.apiKey === "string" && auth.apiKey.includes("sk-ant-oat"),
|
|
214
|
+
apiKey: auth.apiKey,
|
|
215
|
+
headers: auth.headers,
|
|
216
|
+
};
|
|
138
217
|
} catch {
|
|
139
218
|
return null;
|
|
140
219
|
}
|
|
@@ -170,12 +249,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
170
249
|
pendingFastHeader = false;
|
|
171
250
|
return;
|
|
172
251
|
}
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
252
|
+
const auth = await resolveAuth(ctx);
|
|
253
|
+
if (auth === null) {
|
|
175
254
|
pendingFastHeader = false;
|
|
176
255
|
return;
|
|
177
256
|
}
|
|
178
|
-
|
|
257
|
+
const probed = await probePiBetaHeader(ctx.model!, auth);
|
|
258
|
+
event.headers[BETA_HEADER] = buildBetaHeader(
|
|
259
|
+
event.headers[BETA_HEADER],
|
|
260
|
+
auth.isOAuth,
|
|
261
|
+
probed,
|
|
262
|
+
needsFallbackBeta(ctx.model),
|
|
263
|
+
);
|
|
179
264
|
pendingFastHeader = true;
|
|
180
265
|
});
|
|
181
266
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-quiver",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.3",
|
|
4
4
|
"description": "Personal pack of Pi coding-agent extensions: context-safe fetch, doc_to_md PDF/DOCX/PPTX-to-Markdown conversion, session naming, a themed ASCII startup header, Opus 4.8 fast mode, and a provider-stall watchdog.",
|
|
5
5
|
"author": "Jacek Juraszek",
|
|
6
6
|
"license": "MIT",
|