pi-banana 2.5.1 → 2.5.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.
@@ -30,7 +30,7 @@ import {
30
30
  Text,
31
31
  } from "@earendil-works/pi-tui";
32
32
  import { GoogleGenAI } from "@google/genai";
33
- import { existsSync } from "node:fs";
33
+ import { existsSync, readFileSync } from "node:fs";
34
34
  import { homedir } from "node:os";
35
35
  import { mkdir, readFile, writeFile } from "node:fs/promises";
36
36
  import { extname, isAbsolute, resolve } from "node:path";
@@ -53,7 +53,7 @@ const ASPECT_RATIOS = [
53
53
  "21:9",
54
54
  ] as const;
55
55
  const IMAGE_SIZES = ["1K", "2K", "4K"] as const;
56
- const IMAGE_QUALITY = ["lite", "fast", "high"] as const;
56
+ const IMAGE_QUALITY = ["lite", "fast", "high", "max"] as const;
57
57
  const VISION_QUALITY = ["fast", "high"] as const;
58
58
  type ImageQuality = (typeof IMAGE_QUALITY)[number];
59
59
 
@@ -67,6 +67,7 @@ const MODEL_FOR_QUALITY: Record<ImageQuality, string> = {
67
67
  lite: "gemini-3.1-flash-lite-image", // Nano Banana 2 Lite (ultra-low latency, cheapest)
68
68
  fast: "gemini-3.1-flash-image", // Nano Banana 2
69
69
  high: "gemini-3-pro-image", // Nano Banana Pro
70
+ max: "gemini-3-pro-image", // Google fallback; configured provider maps max to GPT Image 2
70
71
  };
71
72
 
72
73
  const DEFAULT_OUTPUT_DIR =
@@ -221,6 +222,19 @@ async function loadReferenceImage(
221
222
  return { data: buf.toString("base64"), mimeType };
222
223
  }
223
224
 
225
+ // Inline preview bytes, loaded at render time from the file the tool wrote.
226
+ // Nothing image-sized is kept on the tool result itself: content goes to the
227
+ // model and details go into the saved session, and a base64 PNG in either one
228
+ // is counted against the context by request guards for the rest of the session.
229
+ function readPreview(outputPath?: string): string | undefined {
230
+ if (typeof outputPath !== "string" || !outputPath) return undefined;
231
+ try {
232
+ return readFileSync(outputPath).toString("base64");
233
+ } catch {
234
+ return undefined; // moved, deleted, or unreadable: show the rest of the card
235
+ }
236
+ }
237
+
224
238
  async function resolveOutputPath(
225
239
  cwd: string,
226
240
  prompt: string,
@@ -311,7 +325,7 @@ export default function (pi: ExtensionAPI) {
311
325
  quality: Type.Optional(
312
326
  StringEnum(IMAGE_QUALITY, {
313
327
  description:
314
- "'lite' = Nano Banana 2 Lite (ultra-low latency, cheapest). 'fast' = Nano Banana 2 (default, ~3s, cheap). 'high' = Nano Banana Pro (slower, top quality).",
328
+ "'lite' = Nano Banana 2 Lite (ultra-low latency, cheapest). 'fast' = Nano Banana 2 (default, ~3s, cheap). 'high' = Nano Banana Pro (strong editing). 'max' = premium benchmark tier, GPT Image 2 when the configured provider supports it.",
315
329
  default: DEFAULT_QUALITY,
316
330
  }),
317
331
  ),
@@ -362,7 +376,7 @@ export default function (pi: ExtensionAPI) {
362
376
  const provider = await resolveProvider(ctx);
363
377
  if (provider.kind === "openai-compat") {
364
378
  const { config } = provider;
365
- const oaiModel = config.models[quality] ?? config.models.fast;
379
+ const oaiModel = config.models[quality] ?? config.models.high ?? config.models.fast;
366
380
  if (!oaiModel) {
367
381
  throw new Error(`banana: no model mapped for quality "${quality}". Run /banana-setup.`);
368
382
  }
@@ -388,7 +402,7 @@ export default function (pi: ExtensionAPI) {
388
402
  details: {
389
403
  prompt: params.prompt, model: oaiModel, quality, aspectRatio, imageSize,
390
404
  mimeType: "image/png", outputPath: outPath, editing,
391
- referenceImages: params.referenceImages, imageBase64: buf.toString("base64"),
405
+ referenceImages: params.referenceImages,
392
406
  },
393
407
  };
394
408
  }
@@ -415,7 +429,7 @@ export default function (pi: ExtensionAPI) {
415
429
  content: [
416
430
  {
417
431
  type: "text",
418
- text: `🎨 ${verbLabel} ${aspectRatio} ${imageSize} image with ${quality === "lite" ? "Nano Banana 2 Lite" : quality === "fast" ? "Nano Banana 2" : "Nano Banana Pro"}…`,
432
+ text: `🎨 ${verbLabel} ${aspectRatio} ${imageSize} image with ${quality === "lite" ? "Nano Banana 2 Lite" : quality === "fast" ? "Nano Banana 2" : quality === "high" ? "Nano Banana Pro" : "GPT Image 2"}…`,
419
433
  },
420
434
  ],
421
435
  details: { model, aspectRatio, imageSize, quality, editing },
@@ -509,9 +523,9 @@ export default function (pi: ExtensionAPI) {
509
523
  : `Generated → ${outPath}`;
510
524
 
511
525
  return {
512
- // Only text in content — the base64 image goes into details
513
- // so it does not bloat conversation history sent to the LLM.
514
- // renderResult reads the image from details for inline display.
526
+ // Only text in content, and no image bytes in details either:
527
+ // details are persisted in the session and counted by request
528
+ // guards. renderResult loads the preview from outputPath.
515
529
  content: [{ type: "text", text: summary }],
516
530
  details: {
517
531
  prompt: params.prompt,
@@ -523,14 +537,15 @@ export default function (pi: ExtensionAPI) {
523
537
  outputPath: outPath,
524
538
  editing,
525
539
  referenceImages: params.referenceImages,
526
- imageBase64: base64,
527
540
  },
528
541
  };
529
542
  },
530
543
 
531
544
  renderResult(result, _options, theme) {
532
- // Image is in details (not content) so base64 does not bloat
533
- // conversation history sent to the LLM on subsequent turns.
545
+ // No image bytes travel with the result: content stays text so it
546
+ // does not bloat LLM history, and details stay small so the saved
547
+ // session does not carry a megabyte per picture. The preview is
548
+ // read back from the file that was just written.
534
549
  const { details, content } = result;
535
550
  const container = new Container();
536
551
 
@@ -541,11 +556,12 @@ export default function (pi: ExtensionAPI) {
541
556
  container.addChild(new Text(theme.fg("success", "✔ " + summaryText), 1, 0));
542
557
  }
543
558
 
544
- // 2. Image from details
545
- if (details && (details as any).imageBase64) {
559
+ // 2. Image, read from the saved file
560
+ const preview = readPreview((details as any)?.outputPath);
561
+ if (preview) {
546
562
  container.addChild(new Spacer(1));
547
563
  container.addChild(
548
- new Image((details as any).imageBase64, (details as any).mimeType ?? "image/png", {
564
+ new Image(preview, (details as any).mimeType ?? "image/png", {
549
565
  fallbackColor: (s: string) => theme.fg("muted", s),
550
566
  }, {
551
567
  maxWidthCells: 80,
@@ -18,7 +18,7 @@ import { resolve } from "path";
18
18
  export interface OpenAiCompatConfig {
19
19
  baseUrl: string;
20
20
  apiKey: string;
21
- models: { lite?: string; fast?: string; high?: string };
21
+ models: { lite?: string; fast?: string; high?: string; max?: string };
22
22
  visionModel?: string;
23
23
  /** Optional provider id whose key lives in pi's auth.json (e.g. "mantice"). */
24
24
  providerName?: string;
@@ -69,7 +69,11 @@ export async function resolveProvider(ctx: any): Promise<Provider> {
69
69
 
70
70
  // ─── OpenAI-compat calls ─────────────────────────────────────────────
71
71
 
72
- /** Map aspect ratio + size tier to a generic WxH string. */
72
+ /** Round to the nearest multiple of 16 (never below 16): the OpenAI-compatible
73
+ * image upstream rejects sizes whose width or height is not divisible by 16. */
74
+ const round16 = (n: number): number => Math.max(16, Math.round(n / 16) * 16);
75
+
76
+ /** Map aspect ratio + size tier to a generic WxH string (multiples of 16). */
73
77
  export function sizeFor(aspectRatio: string, imageSize: string): string {
74
78
  const base = imageSize === "4K" ? 4096 : imageSize === "2K" ? 2048 : 1024;
75
79
  const ratio = (() => {
@@ -77,8 +81,8 @@ export function sizeFor(aspectRatio: string, imageSize: string): string {
77
81
  return w && h ? w / h : 1;
78
82
  })();
79
83
  if (Math.abs(ratio - 1) < 0.01) return `${base}x${base}`;
80
- if (ratio > 1) return `${base}x${Math.round(base / ratio)}`;
81
- return `${Math.round(base * ratio)}x${base}`;
84
+ if (ratio > 1) return `${round16(base)}x${round16(Math.round(base / ratio))}`;
85
+ return `${round16(Math.round(base * ratio))}x${round16(base)}`;
82
86
  }
83
87
 
84
88
  async function asBuffer(data: any): Promise<Buffer> {
@@ -121,10 +125,15 @@ export async function openaiEditImage(
121
125
  form.append("model", opts.model);
122
126
  form.append("prompt", opts.prompt);
123
127
  form.append("size", opts.size);
128
+ // A single reference image is the plain "image" file part. Multiple
129
+ // references must be appended as repeated "image[]" parts: the
130
+ // /images/edits upstream rejects duplicate "image" fields with
131
+ // 400 "Duplicate parameter image".
132
+ const field = opts.images.length > 1 ? "image[]" : "image";
124
133
  for (const img of opts.images) {
125
134
  const bytes = Buffer.from(img.data, "base64");
126
135
  const ext = img.mimeType.split("/")[1] ?? "png";
127
- form.append("image", new Blob([bytes], { type: img.mimeType }), `ref.${ext}`);
136
+ form.append(field, new Blob([bytes], { type: img.mimeType }), `ref.${ext}`);
128
137
  }
129
138
  const res = await fetch(`${config.baseUrl}/images/edits`, {
130
139
  method: "POST",
@@ -52,7 +52,8 @@ export async function configureBananaProvider(ctx: any, opts: ConfigureOptions):
52
52
  const models = {
53
53
  lite: pick(/lite|fast|turbo|mini/i),
54
54
  fast: pick(/^(?!.*(lite|high|max|pro|ultra)).*$/i),
55
- high: pick(/max|high|pro|ultra/i),
55
+ high: pick(/high|pro/i),
56
+ max: pick(/max|ultra/i),
56
57
  };
57
58
  const visionModel = opts.visionModel ?? probe.visionModels[0];
58
59
  persistBananaConfig({ baseUrl, apiKey, models, ...(visionModel ? { visionModel } : {}) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-banana",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "Generate, edit, and analyze images in pi using Google Nano Banana (image gen) and Gemini Vision (analysis). Inline terminal preview, reference-image editing, auto-save.",
5
5
  "author": "Francesco Frapporti <effedue@gmail.com>",
6
6
  "license": "MIT",