pi-quiver 5.2.0 → 5.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.2 - 2026-09-03
12
+
13
+ - fast-mode: discover pi's `anthropic-beta` list at request time by probing
14
+ pi-ai's request assembly (capturing fetch, zero network) instead of
15
+ rebuilding it from a fixed list - fixes 400 "fallbacks: Extra inputs are
16
+ not permitted" on `claude-opus-5` (#11). Probe failure degrades to the
17
+ previous static header.
18
+
19
+ ## v5.2.1 - 2026-09-02
20
+
21
+ - 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.
22
+
11
23
  ## v5.2.0 - 2026-09-01
12
24
 
13
25
  - session-name: sync the session name to the Herdr tab label (`herdrTab`, default true within the opt-in `quiver.sessionAutoName`). Claim-once - only a tab still on its default numeric label is adopted; manual renames always win. Restores the default label on shutdown; crash leaves the last label (rename by hand to recover).
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
 
@@ -13,16 +13,20 @@
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. For opus-4-8 and opus-5 pi's conditional betas (fine-grained tool
17
- * streaming, interleaved thinking) are never applied (eager tool streaming
18
- * defaults on + forceAdaptiveThinking; verified identical compat in pi-ai
19
- * 0.82.1), so the only betas to preserve are the OAuth identity betas. We
20
- * detect OAuth via the same token marker pi uses and rebuild the exact list.
21
- * If pi later adds betas for these models, revisit buildBetaHeader.
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 the lazy anthropic-messages
18
+ * entrypoint) with a capturing fetch - zero network, zero tokens - and unions
19
+ * the captured betas with the fast-mode beta. Probe failure falls back to the
20
+ * static reconstruction (OAUTH_IDENTITY_BETAS stays, demoted to fallback-only
21
+ * seeding). Known limit: the probe context is tool-less, so tool/thinking-
22
+ * conditional betas are captured under fixed defaults - fine while allowlisted
23
+ * models keep forceAdaptiveThinking: true in the catalog. Probe cost: one
24
+ * extra in-process request-assembly pass per qualifying call, no network -
25
+ * per-request by design, no cache.
22
26
  */
23
27
 
24
28
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
25
- import type { Usage } from "@earendil-works/pi-ai";
29
+ import type { Context, Model, StreamOptions, Usage } from "@earendil-works/pi-ai";
26
30
  import { resolveConfig } from "../lib/extension-config.ts";
27
31
 
28
32
  export const FAST_MODE_BETA = "fast-mode-2026-02-01";
@@ -82,7 +86,11 @@ export function injectSpeed(payload: unknown): unknown {
82
86
  return { ...(payload as Record<string, unknown>), speed: FAST_SPEED };
83
87
  }
84
88
 
85
- export function buildBetaHeader(existing: string | null | undefined, isOAuth: boolean): string {
89
+ export function buildBetaHeader(
90
+ existing: string | null | undefined,
91
+ isOAuth: boolean,
92
+ probedBetas: string | null = null,
93
+ ): string {
86
94
  const seen = new Set<string>();
87
95
  const out: string[] = [];
88
96
  const add = (b: string): void => {
@@ -92,12 +100,62 @@ export function buildBetaHeader(existing: string | null | undefined, isOAuth: bo
92
100
  out.push(t);
93
101
  }
94
102
  };
95
- if (isOAuth) OAUTH_IDENTITY_BETAS.forEach(add);
103
+ if (probedBetas !== null) probedBetas.split(",").forEach(add);
104
+ else if (isOAuth) OAUTH_IDENTITY_BETAS.forEach(add);
96
105
  if (typeof existing === "string") existing.split(",").forEach(add);
97
106
  add(FAST_MODE_BETA);
98
107
  return out.join(",");
99
108
  }
100
109
 
110
+ export type ProbeAuth = { apiKey?: string; headers?: Record<string, string | null> };
111
+
112
+ // Minimal context for the beta probe: one user message, no tools, no system
113
+ // prompt. Option-dependent betas (fine-grained tool streaming) are therefore
114
+ // probed under fixed defaults - sufficient while the allowlisted models keep
115
+ // forceAdaptiveThinking: true in the catalog; a future allowlisted model with
116
+ // tool- or thinking-conditional betas would have those dropped, same as today.
117
+ const PROBE_CONTEXT: Context = { messages: [{ role: "user", content: "probe", timestamp: 0 }] };
118
+
119
+ /**
120
+ * Discover the anthropic-beta list pi-ai would send for this model + auth by
121
+ * driving pi-ai's own request assembly against a capturing fetch. Zero
122
+ * network, zero tokens: the fetch records the request headers, then throws.
123
+ * Returns the captured header value, or null on any failure - missing
124
+ * optional peer (the guarded dynamic import() below), probe throw before
125
+ * capture, or no header present - and the caller falls back to the static
126
+ * reconstruction. maxRetries: 0 stops pi-ai's retry wrapper from re-driving
127
+ * the intentionally failing probe fetch.
128
+ */
129
+ export async function probePiBetaHeader(
130
+ model: Model<any>,
131
+ auth: ProbeAuth,
132
+ fetchImpl?: (input: any, init?: any) => Promise<any>,
133
+ ): Promise<string | null> {
134
+ let captured: string | null = null;
135
+ const capturingFetch = async (input: any, init?: any): Promise<any> => {
136
+ const headers = init?.headers ?? input?.headers;
137
+ let value: unknown;
138
+ if (headers && typeof headers.get === "function") value = headers.get(BETA_HEADER);
139
+ else if (headers && typeof headers === "object") value = (headers as Record<string, unknown>)[BETA_HEADER];
140
+ if (typeof value === "string") captured = value;
141
+ throw new Error("fast-mode beta probe abort");
142
+ };
143
+ const options: StreamOptions = {
144
+ apiKey: auth.apiKey,
145
+ headers: auth.headers,
146
+ fetch: (fetchImpl ?? capturingFetch) as unknown as StreamOptions["fetch"],
147
+ maxRetries: 0,
148
+ };
149
+ try {
150
+ const { anthropicMessagesApi } = await import("@earendil-works/pi-ai/api/anthropic-messages.lazy");
151
+ const stream = anthropicMessagesApi().stream(model, PROBE_CONTEXT, options);
152
+ await stream.result().catch(() => {});
153
+ } catch {
154
+ return captured;
155
+ }
156
+ return captured;
157
+ }
158
+
101
159
  type State = { config: boolean; flag: boolean; live: boolean | null };
102
160
 
103
161
  export function resolveEnabled(s: State): boolean {
@@ -129,12 +187,18 @@ export default function (pi: ExtensionAPI) {
129
187
  ctx.ui.setStatus(STATUS_KEY, shouldInject(enabled, ctx.model) ? "\u26a1 fast" : "\u26a1 n/a");
130
188
  };
131
189
 
132
- const detectOAuth = async (ctx: ExtensionContext): Promise<boolean | null> => {
190
+ type ResolvedAuth = ProbeAuth & { isOAuth: boolean };
191
+
192
+ const resolveAuth = async (ctx: ExtensionContext): Promise<ResolvedAuth | null> => {
133
193
  if (!ctx.model) return null;
134
194
  try {
135
195
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
136
196
  if (!auth.ok) return null;
137
- return typeof auth.apiKey === "string" && auth.apiKey.includes("sk-ant-oat");
197
+ return {
198
+ isOAuth: typeof auth.apiKey === "string" && auth.apiKey.includes("sk-ant-oat"),
199
+ apiKey: auth.apiKey,
200
+ headers: auth.headers,
201
+ };
138
202
  } catch {
139
203
  return null;
140
204
  }
@@ -170,12 +234,13 @@ export default function (pi: ExtensionAPI) {
170
234
  pendingFastHeader = false;
171
235
  return;
172
236
  }
173
- const isOAuth = await detectOAuth(ctx);
174
- if (isOAuth === null) {
237
+ const auth = await resolveAuth(ctx);
238
+ if (auth === null) {
175
239
  pendingFastHeader = false;
176
240
  return;
177
241
  }
178
- event.headers[BETA_HEADER] = buildBetaHeader(event.headers[BETA_HEADER], isOAuth);
242
+ const probed = await probePiBetaHeader(ctx.model!, auth);
243
+ event.headers[BETA_HEADER] = buildBetaHeader(event.headers[BETA_HEADER], auth.isOAuth, probed);
179
244
  pendingFastHeader = true;
180
245
  });
181
246
 
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { ExtensionAPI, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
17
+ import { truncateToWidth } from "@earendil-works/pi-tui";
17
18
  import { resolveConfig } from "../lib/extension-config.ts";
18
19
 
19
20
  type Config = { enabled: boolean };
@@ -69,8 +70,8 @@ export default function (pi: ExtensionAPI) {
69
70
  const cfg = resolveConfig(ctx.cwd, "swordHeader", DEFAULT_CONFIG, coerce, (m) => ctx.ui.notify(m, "warning"));
70
71
  if (!cfg.enabled) return;
71
72
  ctx.ui.setHeader((_tui, theme) => ({
72
- render(_width: number): string[] {
73
- return renderSwordLines(theme);
73
+ render(width: number): string[] {
74
+ return renderSwordLines(theme).map((line) => truncateToWidth(line, width, ""));
74
75
  },
75
76
  invalidate() {},
76
77
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-quiver",
3
- "version": "5.2.0",
3
+ "version": "5.2.2",
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",