pi-multimodal-proxy 1.21.0 → 1.22.0

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
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.22.0] - 2026-10-10
8
+
9
+ ### Added
10
+
11
+ - **Claude Haiku 5.5 in the implicit-model candidate chain** — `DEFAULT_MODEL_FALLBACKS` now reads `zai/glm-5.3-flash` → `deepseek/deepseek-v4-flash-vision-exp` → `anthropic/claude-haiku-5-5` (Pi ≥ 1.1.0 catalog) → `anthropic/claude-sonnet-5` → `anthropic/claude-sonnet-4-5`. An anthropic-keyed setup whose cheaper candidates are unkeyed lands on Haiku 5.5 ($0.1/M in, $0.5/M out, vision-capable, 1M context; above 100K input tokens $0.5/$2.5 — output still 4× cheaper than Sonnet's flat rate) instead of Sonnet 5 ($2/M in, $10/M out) — 20× cheaper output at typical description-prompt sizes, same API key. Description prompts (a few images + instructions) sit far below the tier threshold. Haiku's catalog `inputLimits.images.resize` (2000 px / q80) feeds the existing model-aware upload downscale. Pre-1.1.0 catalogs simply don't contain Haiku, so the registry check skips it and behavior is unchanged; explicit model choices are never rewritten.
12
+ - **`durationMs` fallback in the `analyze_image` result row (Pi ≥ 1.1.0, feature-detected)** — the pi 1.1.0 tool render context carries `durationMs`, the monotonic-clock `execute()` time recorded with the final result. `formatAnalyzeImageResult` accepts it as a display fallback when `details` carry no `latencyMs`, so rows replayed from foreign sessions, HTML exports, and results recorded before `details` existed still render `✓ … · 1.2s`. `details.latencyMs` (the vision call itself, also reported to codemode scripts via `structuredContent`) always wins — the host duration covers consent checks and crop encoding too, so the two measure different things. Older hosts pass no `durationMs` and rendering is unchanged.
13
+ - **Per-invocation tool toggling documented** — pi 1.1.0's `--tools +name/-name` entries change the default tool selection instead of replacing it. README now shows `pi -t -analyze_image` (one-shot session without the re-query tool) and `pi -t +analyze_image` (add it back to a trimmed selection) alongside the existing persistent toggles (`/multimodal-proxy tool off`, `PI_VISION_PROXY_TOOL=off`). Privacy section updated to spell out the full current default-model chain.
14
+ - Six new tests (546 total, all green): Haiku placement/ordering in the chain, key-aware preference of Haiku over Sonnet, catalog-without-Haiku skip, `durationMs` fallback/preference/finite-guard in the result formatter. Syntax check clean (8 files).
15
+
7
16
  ## [1.21.0] - 2026-10-03
8
17
 
9
18
  ### Added
package/README.md CHANGED
@@ -10,6 +10,14 @@ When **video or audio files** are detected, they are routed to a **multimodal mo
10
10
 
11
11
  **Compact tool rows (Pi ≥ 1.0.1)** — `analyze_image` and `generate_image` calls render as one tidy line each (`analyze_image "What does the legend say?" · 2 images (image="deadbeef…", shot.png) · crop bottom-right@0`) with themed result summaries (`✓ zai/glm-5.3-flash · cached · 1.2s`, `✓ 1 image via google/imagen-4 · $0.0311`), instead of the default JSON-args dump. Generated images still render inline from the result blocks; expand a row to see the full analysis fence.
12
12
 
13
+ ## What's new in 1.22.0
14
+
15
+ Borrowing from Pi 1.1.0:
16
+
17
+ - **Claude Haiku 5.5 joins the implicit-model candidate chain** — `zai/glm-5.3-flash` → `deepseek/deepseek-v4-flash-vision-exp` → `anthropic/claude-haiku-5-5` (new, Pi ≥ 1.1.0) → `anthropic/claude-sonnet-5` → `anthropic/claude-sonnet-4-5`. When your cheaper candidates have no API key, an anthropic-keyed setup now lands on Haiku 5.5 instead of Sonnet 5 for image descriptions — $0.1/M in, $0.5/M out vs $2/M, $10/M (20× cheaper output at typical description-prompt sizes; above 100K input tokens Haiku is $0.5/$2.5, still 4× cheaper output). Same key, still vision-capable, and its catalog `inputLimits.images.resize` feeds the model-aware upload downscale. Catalogs without Haiku (pi < 1.1.0) skip it and behave exactly as before; explicit choices are never rewritten.
18
+ - **Result rows show a duration even without `details`** — pi 1.1.0's render context carries `durationMs` (the recorded `execute()` time of a final tool result). The `analyze_image` result line now falls back to it when its `details` have no `latencyMs`, so rows replayed from foreign/HTML-exported sessions still show `✓ … · 1.2s`. Our own `latencyMs` (the vision call itself, reported to codemode scripts via `structuredContent`) always wins.
19
+ - **Per-invocation tool toggling documented** — pi 1.1.0's `--tools +name/-name` entries modify the default tool selection instead of replacing it, so `pi -t -analyze_image` runs a one-shot session without the re-query tool while keeping everything else (see [Slash commands](#slash-commands)).
20
+
13
21
  ## What's new in 1.21.0
14
22
 
15
23
  Adopting Pi 1.0.1's new `pi.registerToolRenderer()` API:
@@ -249,6 +257,8 @@ Legacy alias: /vision-proxy <args> works identically.
249
257
 
250
258
  When an env var is set, the matching `/multimodal-proxy` subcommand is locked.
251
259
 
260
+ For one-shot sessions you can also toggle tools per invocation (Pi ≥ 1.1.0): `pi -t -analyze_image` removes the re-query tool from the default selection, `pi -t +analyze_image` adds it back to a trimmed one. This complements the persistent `/multimodal-proxy tool off` and `PI_VISION_PROXY_TOOL=off`, which survive across sessions.
261
+
252
262
  ## How it works — Images
253
263
 
254
264
  ```
@@ -375,7 +385,7 @@ When a model is in the grounding registry, a format-specific instruction is appe
375
385
 
376
386
  ## Privacy & security
377
387
 
378
- This extension **sends data to a third-party provider**. By default that is `zai/glm-5.3-flash` for images (`anthropic/claude-sonnet-5` on older Pi versions without GLM 5.3 Flash in the catalog, then `anthropic/claude-sonnet-4-5`) and `xai/grok-4.3` for video/audio. Be aware:
388
+ This extension **sends data to a third-party provider**. By default that is `zai/glm-5.3-flash` for images, falling back through `deepseek/deepseek-v4-flash-vision-exp` → `anthropic/claude-haiku-5-5` (Pi ≥ 1.1.0) → `anthropic/claude-sonnet-5` → `anthropic/claude-sonnet-4-5` to the first candidate in your catalog **and keyed** (`xai/grok-4.3` for video/audio). Be aware:
379
389
 
380
390
  1. **Image and video data is uploaded** to the configured provider on every proxied request. Crop coordinates are applied locally before upload — only the cropped region is sent.
381
391
  2. **Recent conversation context** (last 8 messages, truncated) is uploaded with the image unless you set `/multimodal-proxy context off` or `PI_VISION_PROXY_INCLUDE_CONTEXT=false`. Disable it for sensitive sessions.
@@ -3312,6 +3312,41 @@ describe("1.18.0 key-aware default model resolution", () => {
3312
3312
  assert.equal(DEFAULT_MODEL_FALLBACKS[0]!.modelId, "deepseek-v4-flash-vision-exp");
3313
3313
  });
3314
3314
 
3315
+ it("places Claude Haiku 5.5 between DeepSeek and Sonnet 5 (pi ≥ 1.1.0 catalogs)", () => {
3316
+ const haiku = DEFAULT_MODEL_FALLBACKS.find((m) => m.modelId === "claude-haiku-5-5")!;
3317
+ assert.equal(haiku.provider, "anthropic");
3318
+ const deepseekIdx = DEFAULT_MODEL_FALLBACKS.findIndex((m) => m.provider === "deepseek");
3319
+ const sonnetIdx = DEFAULT_MODEL_FALLBACKS.findIndex((m) => m.modelId === "claude-sonnet-5");
3320
+ assert.ok(deepseekIdx < DEFAULT_MODEL_FALLBACKS.indexOf(haiku));
3321
+ assert.ok(DEFAULT_MODEL_FALLBACKS.indexOf(haiku) < sonnetIdx);
3322
+ });
3323
+
3324
+ it("prefers Haiku 5.5 over Sonnet 5 when both are keyed anthropic candidates", () => {
3325
+ const out = applyDefaultModelFallback(
3326
+ builtin,
3327
+ registryWith(
3328
+ `${builtin.provider}/${builtin.modelId}`,
3329
+ "anthropic/claude-haiku-5-5",
3330
+ "anthropic/claude-sonnet-5",
3331
+ ),
3332
+ false,
3333
+ keyWith("anthropic/claude-haiku-5-5", "anthropic/claude-sonnet-5"),
3334
+ );
3335
+ assert.equal(out.provider, "anthropic");
3336
+ assert.equal(out.modelId, "claude-haiku-5-5");
3337
+ });
3338
+
3339
+ it("skips Haiku 5.5 on pre-1.1.0 catalogs and still lands on Sonnet 5", () => {
3340
+ const out = applyDefaultModelFallback(
3341
+ builtin,
3342
+ registryWith(`${builtin.provider}/${builtin.modelId}`, "anthropic/claude-sonnet-5"),
3343
+ false,
3344
+ keyWith("anthropic/claude-haiku-5-5", "anthropic/claude-sonnet-5"),
3345
+ );
3346
+ assert.equal(out.provider, "anthropic");
3347
+ assert.equal(out.modelId, "claude-sonnet-5");
3348
+ });
3349
+
3315
3350
  it("keeps the default when it is in the catalog and keyed", () => {
3316
3351
  const out = applyDefaultModelFallback(
3317
3352
  builtin,
@@ -196,6 +196,30 @@ describe("formatAnalyzeImageResult", () => {
196
196
  assert.equal(segs[0]!.style, "success");
197
197
  assert.equal(segs[1]!.style, "muted");
198
198
  });
199
+
200
+ describe("durationMs fallback (pi ≥ 1.1.0 render context)", () => {
201
+ it("uses the host durationMs when details carry no latencyMs", () => {
202
+ assert.equal(
203
+ plain(formatAnalyzeImageResult({ ok: true, provider: "zai", model: "m" }, false, 2500)),
204
+ "✓ zai/m · 2.5s",
205
+ );
206
+ // rows with no details at all (replayed foreign sessions, HTML exports)
207
+ assert.equal(plain(formatAnalyzeImageResult(undefined, false, 987)), "✓ · 1.0s");
208
+ });
209
+
210
+ it("prefers details.latencyMs over the host durationMs", () => {
211
+ const segs = formatAnalyzeImageResult(
212
+ { ok: true, provider: "zai", model: "glm-5.3-flash", latencyMs: 1234 },
213
+ false,
214
+ 99999,
215
+ );
216
+ assert.equal(plain(segs), "✓ zai/glm-5.3-flash · 1.2s");
217
+ });
218
+
219
+ it("ignores non-finite durationMs", () => {
220
+ assert.equal(plain(formatAnalyzeImageResult({ ok: true }, false, Number.NaN)), "✓");
221
+ });
222
+ });
199
223
  });
200
224
 
201
225
  describe("formatGenerateImageResult", () => {
@@ -1318,6 +1318,7 @@ export function resolveConfig(
1318
1318
  */
1319
1319
  export const DEFAULT_MODEL_FALLBACKS: ReadonlyArray<{ provider: string; modelId: string }> = [
1320
1320
  { provider: "deepseek", modelId: "deepseek-v4-flash-vision-exp" }, // Pi ≥ 0.84.4, cheap, common key
1321
+ { provider: "anthropic", modelId: "claude-haiku-5-5" }, // Pi ≥ 1.1.0, vision-capable, $0.1/$0.5 per M
1321
1322
  { provider: "anthropic", modelId: "claude-sonnet-5" },
1322
1323
  { provider: "anthropic", modelId: "claude-sonnet-4-5" },
1323
1324
  ];
@@ -3401,8 +3402,19 @@ function recordOf(details: unknown): Record<string, unknown> | undefined {
3401
3402
  : undefined;
3402
3403
  }
3403
3404
 
3404
- /** One-line transcript summary of an analyze_image result from its `details`. */
3405
- export function formatAnalyzeImageResult(details: unknown, isError: boolean): TranscriptSegment[] {
3405
+ /**
3406
+ * One-line transcript summary of an analyze_image result from its `details`.
3407
+ * `durationMs` (pi ≥ 1.1.0 host render context) is a display fallback for rows
3408
+ * whose `details` carry no `latencyMs` — replayed sessions, foreign/HTML
3409
+ * exports, results recorded before `details` existed. Details always win:
3410
+ * `latencyMs` measures the vision call itself, while the host's `durationMs`
3411
+ * covers the whole `execute()` (consent checks, crop encoding included).
3412
+ */
3413
+ export function formatAnalyzeImageResult(
3414
+ details: unknown,
3415
+ isError: boolean,
3416
+ durationMs?: number,
3417
+ ): TranscriptSegment[] {
3406
3418
  const d = recordOf(details);
3407
3419
  if (isError || d?.ok === false) {
3408
3420
  const msg = typeof d?.error === "string" && d.error ? d.error : "analysis failed";
@@ -3414,8 +3426,9 @@ export function formatAnalyzeImageResult(details: unknown, isError: boolean): Tr
3414
3426
  if (provider && modelId) segments.push({ text: ` ${summarizeForTranscript(`${provider}/${modelId}`, 80)}`, style: "muted" });
3415
3427
  else if (modelId) segments.push({ text: ` ${summarizeForTranscript(modelId, 80)}`, style: "muted" });
3416
3428
  if (d?.cached === true) segments.push({ text: " · cached", style: "dim" });
3417
- if (isFiniteNumber(d?.latencyMs)) {
3418
- segments.push({ text: ` · ${(Math.max(0, d.latencyMs) / 1000).toFixed(1)}s`, style: "dim" });
3429
+ const latency = isFiniteNumber(d?.latencyMs) ? d.latencyMs : isFiniteNumber(durationMs) ? durationMs : undefined;
3430
+ if (latency !== undefined) {
3431
+ segments.push({ text: ` · ${(Math.max(0, latency) / 1000).toFixed(1)}s`, style: "dim" });
3419
3432
  }
3420
3433
  return segments;
3421
3434
  }
@@ -2407,6 +2407,8 @@ export default function (pi: ExtensionAPI) {
2407
2407
  isPartial?: boolean;
2408
2408
  expanded?: boolean;
2409
2409
  showImages?: boolean;
2410
+ /** pi ≥ 1.1.0: recorded execute() duration from the final result. */
2411
+ durationMs?: number;
2410
2412
  }
2411
2413
 
2412
2414
  /** AgentToolResult subset the result renderer receives. */
@@ -2580,7 +2582,7 @@ export default function (pi: ExtensionAPI) {
2580
2582
  renderResult: (result, options, theme, context) => {
2581
2583
  if (options.isPartial) return textComponent(theme.fg("warning", "Analyzing…"));
2582
2584
  return resultComponent(
2583
- formatAnalyzeImageResult(result?.details, context?.isError === true),
2585
+ formatAnalyzeImageResult(result?.details, context?.isError === true, context?.durationMs),
2584
2586
  result ?? {},
2585
2587
  options,
2586
2588
  theme,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-multimodal-proxy",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Automatic image, video and audio description for any model in Pi. Routes media to a multimodal model and injects descriptions into context.",
5
5
  "keywords": [
6
6
  "pi-package"