@pi-unipi/image 2.12.0 → 2.14.1

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 CHANGED
@@ -7,7 +7,7 @@ Image generation and image recognition tools for the agent.
7
7
  | Tool | Description |
8
8
  |------|-------------|
9
9
  | `image_generate` | Generate an image from a text prompt. Returned inline and saved to disk. |
10
- | `image_recognize` | Analyze an image with a vision model. Accepts a file path, `data:` URL, or base64. |
10
+ | `image_recognize` | Analyze an image with a vision model. Accepts a file path, `data:` URL, or base64. Automatically hidden while the session model itself has vision. |
11
11
 
12
12
  ## Commands
13
13
 
@@ -63,6 +63,13 @@ works. Supported: PNG, JPEG, GIF, WebP. Remote URLs are not fetched.
63
63
  Prefer file paths — inlining base64 into the conversation is far more
64
64
  expensive in tokens.
65
65
 
66
+ **Vision models don't get this tool.** When the session's current model already
67
+ accepts image input, `image_recognize` is dropped from the active tool set —
68
+ the model reads images natively through pi's own tools, so a separate
69
+ recognition round-trip through another model would only duplicate that ability
70
+ and burn context. Switch to a text-only model (via `/model`) and the tool comes
71
+ back automatically.
72
+
66
73
  ## Configuration
67
74
 
68
75
  `~/.unipi/config/image/config.json`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/image",
3
- "version": "2.12.0",
3
+ "version": "2.14.1",
4
4
  "description": "Image generation and image recognition tools for the Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.12.0"
37
+ "@pi-unipi/core": "2.14.1"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-ai": "^0.84.0",
package/src/index.ts CHANGED
@@ -20,7 +20,13 @@ import {
20
20
 
21
21
  import { registerImageCommands } from "./commands.js";
22
22
  import { registerImageTools } from "./tools.js";
23
- import { listImageGenModels, listVisionModels, type ChatModelRegistry } from "./models.js";
23
+ import {
24
+ applyRecognizeGating,
25
+ isVisionModel,
26
+ listImageGenModels,
27
+ listVisionModels,
28
+ type ChatModelRegistry,
29
+ } from "./models.js";
24
30
  import { registerRegistryImageProviders } from "./register-providers.js";
25
31
  import { loadConfig } from "./settings.js";
26
32
 
@@ -37,10 +43,27 @@ function getInfoRegistry() {
37
43
  ).__unipi_info_registry;
38
44
  }
39
45
 
46
+ /**
47
+ * Hide image_recognize when the session model can natively see images, and
48
+ * restore it when a text-only model takes over. Returns whether the tool is
49
+ * provided after gating.
50
+ */
51
+ function applyVisionGating(pi: ExtensionAPI, model: unknown): boolean {
52
+ const active = pi.getActiveTools();
53
+ const next = applyRecognizeGating(active, model, IMAGE_TOOLS.RECOGNIZE);
54
+ if (next !== active) pi.setActiveTools(next);
55
+ return next.includes(IMAGE_TOOLS.RECOGNIZE);
56
+ }
57
+
40
58
  export default function (pi: ExtensionAPI) {
41
59
  registerImageTools(pi);
42
60
  registerImageCommands(pi);
43
61
 
62
+ pi.on("model_select", (event) => {
63
+ if (!loadConfig().recognize.enabled) return;
64
+ applyVisionGating(pi, event.model);
65
+ });
66
+
44
67
  pi.on("session_start", async (_event, ctx) => {
45
68
  const config = loadConfig();
46
69
 
@@ -51,9 +74,16 @@ export default function (pi: ExtensionAPI) {
51
74
  (ctx as unknown as { modelRegistry?: ChatModelRegistry }).modelRegistry,
52
75
  ).catch(() => undefined);
53
76
 
77
+ // A vision-capable session model reads images itself, so image_recognize
78
+ // would only duplicate that ability. Drop it from the active tool set;
79
+ // the model_select handler above restores it when a text-only model is
80
+ // chosen later in the same session.
81
+ const recognizeProvided =
82
+ config.recognize.enabled && applyVisionGating(pi, ctx.model);
83
+
54
84
  const tools: string[] = [];
55
85
  if (config.generate.enabled) tools.push(IMAGE_TOOLS.GENERATE);
56
- if (config.recognize.enabled) tools.push(IMAGE_TOOLS.RECOGNIZE);
86
+ if (recognizeProvided) tools.push(IMAGE_TOOLS.RECOGNIZE);
57
87
 
58
88
  emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
59
89
  name: MODULES.IMAGE,
@@ -92,9 +122,11 @@ export default function (pi: ExtensionAPI) {
92
122
  .modelRegistry;
93
123
  const vision = chatRegistry ? listVisionModels(chatRegistry) : [];
94
124
 
95
- const recognize = current.recognize.enabled
96
- ? current.recognize.model || "Session model"
97
- : "Disabled";
125
+ const recognize = !current.recognize.enabled
126
+ ? "Disabled"
127
+ : isVisionModel(ctx.model)
128
+ ? "Hidden (model has vision)"
129
+ : current.recognize.model || "Session model";
98
130
 
99
131
  return {
100
132
  generate: { value: generate },
package/src/models.ts CHANGED
@@ -383,7 +383,7 @@ export function listVisionModels(registry: ChatModelRegistry): VisionModel[] {
383
383
  return models.filter(isVisionModel);
384
384
  }
385
385
 
386
- function isVisionModel(model: unknown): model is VisionModel {
386
+ export function isVisionModel(model: unknown): model is VisionModel {
387
387
  if (model === null || typeof model !== "object") return false;
388
388
  const candidate = model as Partial<VisionModel>;
389
389
  if (typeof candidate.id !== "string" || typeof candidate.provider !== "string") {
@@ -392,6 +392,29 @@ function isVisionModel(model: unknown): model is VisionModel {
392
392
  return Array.isArray(candidate.input) && candidate.input.includes("image");
393
393
  }
394
394
 
395
+ /**
396
+ * Active tool names after hiding `recognizeTool` for a vision-capable model.
397
+ *
398
+ * A model that accepts image input can read images natively (pi's own read
399
+ * tool hands it the pixels), so a separate image_recognize round-trip through
400
+ * another model only duplicates the ability and burns system-prompt context.
401
+ * Text-only models get the tool back. Models that do not declare their input
402
+ * modalities are treated as non-vision, matching `isVisionModel`.
403
+ */
404
+ export function applyRecognizeGating(
405
+ active: string[],
406
+ model: unknown,
407
+ recognizeTool: string,
408
+ ): string[] {
409
+ const vision = isVisionModel(model);
410
+ const present = active.includes(recognizeTool);
411
+ // Already correct: hidden for a vision model, or provided for a text-only one.
412
+ if (vision !== present) return active;
413
+ return vision
414
+ ? active.filter((name) => name !== recognizeTool)
415
+ : [...active, recognizeTool];
416
+ }
417
+
395
418
  /**
396
419
  * Resolve a vision-model reference, restricted to image-capable models.
397
420
  *