pi-multimodal-proxy 1.5.0-beta.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/CHANGELOG.md +66 -0
- package/PRD-Implementation-Status.md +170 -0
- package/PRD.md +599 -0
- package/README.md +219 -0
- package/SECURITY-REVIEW.md +127 -0
- package/extensions/__tests__/integration.test.ts +386 -0
- package/extensions/__tests__/internal.test.ts +1881 -0
- package/extensions/internal.ts +1731 -0
- package/extensions/vision-proxy.ts +2076 -0
- package/package.json +33 -0
package/PRD.md
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# PRD: pi-vision-proxy 1.4.0
|
|
2
|
+
|
|
3
|
+
**Status:** Final
|
|
4
|
+
**Target version:** pi-vision-proxy 1.4.0
|
|
5
|
+
**Date:** 2026-05-03
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Changes since v1.3.0
|
|
10
|
+
|
|
11
|
+
1. **`analyze_image` tool** — agent-facing tool for targeted re-querying of images with multi-form crop support and optional grounding.
|
|
12
|
+
2. **Multi-image batched comparison** — adaptive joint vision calls when ≥2 images arrive together.
|
|
13
|
+
3. **`/vision-proxy describe` slash command** — user-facing re-query and re-describe with extended crop syntax.
|
|
14
|
+
4. **Optional grounded-coordinate output** — per-model native-format grounding with `grounding_format` metadata.
|
|
15
|
+
5. **Spatial-awareness fix** — image dimensions, filenames, and `crop_origin` in all fence types so agents can reason about coordinates without prior knowledge of image geometry.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Background
|
|
20
|
+
|
|
21
|
+
pi-vision-proxy currently provides automatic, transparent image description for non-vision models in Pi. It runs in `before_agent_start`, sends each attached image to a configured vision model once, persists the description in the session keyed by image hash, and re-injects the description on every subsequent LLM call so descriptions survive across turns.
|
|
22
|
+
|
|
23
|
+
Three structural limitations remain:
|
|
24
|
+
|
|
25
|
+
1. **Generic descriptions, no question context.** Detail-level questions ("what error code?", "what is the y-axis maximum?") often require information the generic pass omitted.
|
|
26
|
+
2. **Per-image isolation.** Comparison questions cannot be answered from independently generated per-image descriptions.
|
|
27
|
+
3. **No user-side override.** No user-facing way to refresh or re-query a wrong description.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Summary of features
|
|
32
|
+
|
|
33
|
+
| # | Feature | Type | Default |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| 1 | `analyze_image` tool with multi-form crop & question support | Agent-facing tool | enabled when proxy enabled |
|
|
36
|
+
| 2 | Multi-image batched comparison with adaptive prompting | Behaviour change | enabled when ≥ 2 images in one turn |
|
|
37
|
+
| 3 | `/vision-proxy describe` slash command | User-facing command | always available |
|
|
38
|
+
| 4 | Optional grounded-coordinate output with format metadata | Capability flag per vision model | curated Tier 1 list pre-populated |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Image metadata and dimensions
|
|
43
|
+
|
|
44
|
+
All three fence tags carry image dimensions and filename. Dimensions are stored in an in-memory map populated on first ingestion — no session-entry persistence needed (images are always re-decoded each session).
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
interface ImageMeta {
|
|
48
|
+
width: number;
|
|
49
|
+
height: number;
|
|
50
|
+
filename?: string; // basename only
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const _imageMeta = new Map<string, ImageMeta>();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When `readImageFileWithReason` returns an image, it also returns the basename. The caller stores it in `_imageMeta` alongside dimensions extracted via `image-size` (header-only, no full decode).
|
|
57
|
+
|
|
58
|
+
**Open question:** Whether `sharp` (for cropping and pHash) should be a hard or optional dependency, and what the degradation behavior is when it's absent. `image-size` is always a hard dependency.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Fence tag reference
|
|
63
|
+
|
|
64
|
+
Three distinct tags — each has a clear semantic role so the agent can distinguish generic descriptions from targeted analyses from joint comparisons.
|
|
65
|
+
|
|
66
|
+
| Tag | Producer | Semantics |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `<vision_proxy_description>` | Auto-proxy (`before_agent_start`) | Generic, per-image, always produced for attached images |
|
|
69
|
+
| `<vision_proxy_analysis>` | `analyze_image` tool | Targeted, question-driven, possibly cropped/grounded |
|
|
70
|
+
| `<vision_proxy_joint_description>` | Auto-proxy or tool (≥2 images) | Multi-image comparison |
|
|
71
|
+
|
|
72
|
+
Closing-tag neutralisation is applied to all three fence types. The `fenceUntrusted` function is updated to handle all three tags.
|
|
73
|
+
|
|
74
|
+
**Precedence rule:** `analyze_image` results (`<vision_proxy_analysis>`) are authoritative for the specific question asked. The cached generic description (`<vision_proxy_description>`) remains the default for everything else. The agent receives both in context and resolves contradictions using question specificity and recency. No correction entry is created.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Feature 1 — Targeted analysis via `analyze_image` tool
|
|
79
|
+
|
|
80
|
+
### Problem
|
|
81
|
+
|
|
82
|
+
The automatic proxy generates one generic description per image. When the agent later needs a specific detail, it has no recourse — and even with a vision-capable active model, images attached many turns ago tend to fall out of effective attention.
|
|
83
|
+
|
|
84
|
+
### Functional requirements
|
|
85
|
+
|
|
86
|
+
**FR-1.1** When the proxy is enabled (`mode != off`), pi-vision-proxy registers a tool named `analyze_image` and exposes it to the active model alongside Pi's other tools, including when the active model supports images natively.
|
|
87
|
+
|
|
88
|
+
**FR-1.2** Tool schema:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
analyze_image({
|
|
92
|
+
images: string[], // 1..maxImagesPerCall; path or "sha256:<hex>"
|
|
93
|
+
question: string, // required, non-empty, max 4000 chars
|
|
94
|
+
model?: string, // optional; provider/model-id
|
|
95
|
+
crop?: CropEntry[], // optional; per-image crop
|
|
96
|
+
reason?: string // optional; logged for analytics only
|
|
97
|
+
}) -> string
|
|
98
|
+
|
|
99
|
+
type CropEntry = {
|
|
100
|
+
image_index: number;
|
|
101
|
+
} & (
|
|
102
|
+
| { region: NamedRegion }
|
|
103
|
+
| { normalized: { x: number; y: number; width: number; height: number } }
|
|
104
|
+
| { pixels: { x: number; y: number; width: number; height: number } }
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
type NamedRegion =
|
|
108
|
+
| "top-left" | "top-right" | "bottom-left" | "bottom-right"
|
|
109
|
+
| "top" | "bottom" | "left" | "right" | "center"
|
|
110
|
+
| "top-half" | "bottom-half" | "left-half" | "right-half";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Each `CropEntry` must include exactly one of `region`, `normalized`, or `pixels`. Specifying more than one is a tool error.
|
|
114
|
+
|
|
115
|
+
**FR-1.2.1** *Crop semantics.*
|
|
116
|
+
|
|
117
|
+
- **`region`** — proxy resolves to a normalized rectangle internally. Quadrants (`top-left` etc.) cover 50% × 50%. `top` / `bottom` / `left` / `right` cover the indicated 50% × 100% strip. `center` is the centre 50% × 50%. `*-half` aliases are explicit names for the 50%-strip forms. Always valid; never errors.
|
|
118
|
+
- **`normalized`** — `x`, `y`, `width`, `height` ∈ [0.0, 1.0]. Resolved to pixels by multiplying by image dimensions. Out-of-bounds values are clamped; if the resulting rectangle has zero area, tool error.
|
|
119
|
+
- **`pixels`** — absolute pixel coordinates. Out-of-bounds values are clamped to image dimensions. Zero-area after clamping → tool error. Use this form only when the agent has authoritative pixel coordinates from a prior fence or from a previous grounded response.
|
|
120
|
+
|
|
121
|
+
The proxy converts all three forms to pixels internally before cropping locally and transmitting the cropped region to the vision model.
|
|
122
|
+
|
|
123
|
+
**FR-1.2.2** *`reason` field.* Optional, logged when supplied. No semantic role — purely for analytics.
|
|
124
|
+
|
|
125
|
+
**FR-1.2.3** *Tool description text.* The tool description registered with the active model must include all three crop forms with concrete examples. Required text:
|
|
126
|
+
|
|
127
|
+
> Use `analyze_image` when (a) the cached description of an image lacks a detail you need, (b) you need to compare or cross-reference multiple images, or (c) you need to focus on a specific region.
|
|
128
|
+
>
|
|
129
|
+
> **Cropping.** Three forms, in order of preference:
|
|
130
|
+
>
|
|
131
|
+
> - **`region`** — coarse cut by name. Use when you don't have exact dimensions: `{ image_index: 0, region: "bottom-right" }`.
|
|
132
|
+
> - **`normalized`** — fractional coordinates 0.0–1.0. Default choice for precise crops without knowing image dimensions: `{ image_index: 0, normalized: { x: 0.5, y: 0.5, width: 0.4, height: 0.4 } }`.
|
|
133
|
+
> - **`pixels`** — absolute pixels. Use only when you have authoritative coordinates from a prior `<vision_proxy_description>` or `<vision_proxy_analysis>` (which carry `width` and `height` attributes) or from a previous grounded response. Example: `{ image_index: 0, pixels: { x: 1840, y: 120, width: 840, height: 360 } }`.
|
|
134
|
+
>
|
|
135
|
+
> Image dimensions and filenames are available in the `width`, `height`, and `filename` attributes of `<vision_proxy_description>`, `<vision_proxy_analysis>`, and `<vision_proxy_joint_description>` blocks in your context.
|
|
136
|
+
>
|
|
137
|
+
> When a crop is applied, the response fence carries a `crop_origin` attribute (e.g. `crop_origin="1840,120"`). Add the origin's x to any returned x-coordinate and the origin's y to any returned y-coordinate to map coordinates back to the original full image.
|
|
138
|
+
>
|
|
139
|
+
> The tool result is authoritative for the specific question asked; the cached generic description remains the default for everything else.
|
|
140
|
+
|
|
141
|
+
**FR-1.3** Path resolution and security. Delegates path policy to Pi's `read` tool. Adds: `..` segments rejected, symlink-escape rejected, `images.length ≤ maxImagesPerCall`.
|
|
142
|
+
|
|
143
|
+
**FR-1.4** Model override. Must be registered, must support image input. Agent-initiated overrides honoured even when `PI_VISION_PROXY_MODEL` is env-locked.
|
|
144
|
+
|
|
145
|
+
**FR-1.5** Per-provider consent. First call to a not-yet-consented provider returns a tool error pointing to `/vision-proxy consent yes`. Consent for the auto-proxy's provider carries over to `analyze_image` calls using the same provider.
|
|
146
|
+
|
|
147
|
+
**FR-1.6** Persistence. Image bytes by sha256. Image dimensions are computed on first ingestion and stored in the in-memory `_imageMeta` map (see "Image metadata and dimensions" above). Tool results are not persisted as canonical descriptions. Result LRU keyed by `(sorted_image_hashes, crop_signature, question_hash, model_id)` — `crop_signature` is a stable hash over the resolved-pixels rectangle, so semantically equivalent crops in different forms hit the same cache entry.
|
|
148
|
+
|
|
149
|
+
**FR-1.7** Configuration:
|
|
150
|
+
- `/vision-proxy tool on|off` — default `on` at GA, `off` during beta.
|
|
151
|
+
- `/vision-proxy max-images-per-call <n>` — default 10, range 1–20.
|
|
152
|
+
- `/vision-proxy cache-size <n>` — default 50, range 0–500.
|
|
153
|
+
|
|
154
|
+
**FR-1.8** *Result fencing with image metadata.* The tool result is wrapped in a `<vision_proxy_analysis>` fence that carries the dimensions, filename, and (when applicable) grounding format of the source image:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
<vision_proxy_analysis
|
|
158
|
+
image="sha256:abc123..."
|
|
159
|
+
width="3840"
|
|
160
|
+
height="2160"
|
|
161
|
+
filename="screenshot.png"
|
|
162
|
+
grounding_format="qwen_pixels">
|
|
163
|
+
The error dialog [1840, 120, 2680, 480] shows "Connection timed out"...
|
|
164
|
+
</vision_proxy_analysis>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Required attributes: `image` (always), `width`, `height` (always, in pixels). Optional attributes: `filename` (basename only — full paths are never exposed in fence attributes, per security note), `grounding_format` (only present when grounding is enabled and the model is in `groundingModels`; see FR-4.2 and FR-4.7).
|
|
168
|
+
|
|
169
|
+
For cropped calls, `width` and `height` are the dimensions of the **cropped region** sent to the vision model, not the original image, and `image` is suffixed with the crop signature: `image="sha256:abc...#crop:1840,120,840,360"`. A `crop_origin` attribute is added:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
<vision_proxy_analysis
|
|
173
|
+
image="sha256:abc123...#crop:1840,120,840,360"
|
|
174
|
+
width="840"
|
|
175
|
+
height="360"
|
|
176
|
+
crop_origin="1840,120"
|
|
177
|
+
filename="screenshot.png"
|
|
178
|
+
grounding_format="qwen_pixels">
|
|
179
|
+
The error dialog [1840, 120, 2680, 480] shows "Connection timed out"...
|
|
180
|
+
</vision_proxy_analysis>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**`crop_origin`** — `"<x>,<y>"` comma-separated pixel offset of the crop's top-left corner within the original image. Present **only** on cropped calls. Absent on uncropped calls. The agent adds `crop_origin.x` to any returned x-coordinate and `crop_origin.y` to any returned y-coordinate to map coordinates back to full-image space.
|
|
184
|
+
|
|
185
|
+
Closing-tag neutralisation is applied to the body.
|
|
186
|
+
|
|
187
|
+
**FR-1.9** Telemetry. `vision_proxy.tool_call` entries with timestamp, image hashes, crop form and resolved pixels, question, supplied `reason`, model id, latency_ms, provider-reported token usage, `cache_hit` flag.
|
|
188
|
+
|
|
189
|
+
**FR-1.10** *Precedence between tool results and cached descriptions.* `analyze_image` results (`<vision_proxy_analysis>`) are authoritative for the specific question asked. The cached generic description (`<vision_proxy_description>`) remains the default for everything else. The agent receives both in context and resolves contradictions using question specificity and recency. No correction entry is created.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Feature 2 — Multi-image batched comparison
|
|
194
|
+
|
|
195
|
+
### Problem
|
|
196
|
+
|
|
197
|
+
VLMs natively struggle to link visual cues across images even when both images are in the same prompt. But forcing a contrastive structure when the user just said "describe both" over-constrains.
|
|
198
|
+
|
|
199
|
+
### Approach
|
|
200
|
+
|
|
201
|
+
The vision model itself routes between contrastive and co-presented output structure — it's already multilingual and is already reading the user's question. The proxy supplies language-independent structural hints (filenames, perceptual similarity) as additional context.
|
|
202
|
+
|
|
203
|
+
### Functional requirements
|
|
204
|
+
|
|
205
|
+
**FR-2.1** When the automatic proxy processes a turn containing `N` ≥ 2 images:
|
|
206
|
+
- N independent vision calls produce per-image canonical descriptions (unchanged).
|
|
207
|
+
- One additional vision call produces a "joint description" cached by `(sorted_hashes, turn_index)`. Injected for *this turn only*.
|
|
208
|
+
|
|
209
|
+
**FR-2.2** When `analyze_image` is invoked with `2 ≤ N ≤ maxImagesPerCall` images, one vision call is made with all N images.
|
|
210
|
+
|
|
211
|
+
**FR-2.3** Joint calls obey the existing consent flow, prompt-injection fence, and `include_context` setting.
|
|
212
|
+
|
|
213
|
+
**FR-2.4** `maxBatch` config (slash: `/vision-proxy max-batch <n>`; range 1–10; default 4).
|
|
214
|
+
|
|
215
|
+
**FR-2.5** *Adaptive joint-call system prompt.*
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
You are analysing N images that the user has provided together.
|
|
219
|
+
Refer to them as Image 1 (filename), Image 2 (filename), ... .
|
|
220
|
+
Each image's dimensions will be visible to your reasoning as
|
|
221
|
+
"Image N: WxH pixels".
|
|
222
|
+
|
|
223
|
+
Read the user's question carefully. If the user is asking about
|
|
224
|
+
comparison, difference, change, or relationship between the images,
|
|
225
|
+
structure your response as:
|
|
226
|
+
(1) similarities across the images,
|
|
227
|
+
(2) specific differences,
|
|
228
|
+
(3) a direct, step-by-step answer to the user's question.
|
|
229
|
+
|
|
230
|
+
Otherwise, describe each image in turn and note any obvious relationships
|
|
231
|
+
between them.
|
|
232
|
+
|
|
233
|
+
[Structural hints, if available: ...]
|
|
234
|
+
[User's prompt: ...]
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
This works in any language the underlying VLM supports — comparison cues in German, Italian, Japanese, Chinese, etc. are handled natively. No per-language code in the proxy.
|
|
238
|
+
|
|
239
|
+
**FR-2.5.1** *Filename hints.* See Appendix D. Basename-only.
|
|
240
|
+
|
|
241
|
+
**FR-2.5.2** *Perceptual similarity hint.* pHash computed after resizing both images to a fixed square with letterboxing (e.g. 64×64 with gray padding) to normalise aspect ratio before hashing. Threshold default 0.80 (lowered to account for residual aspect-ratio distortion in content-heavy crops). Configurable via `PI_VISION_PROXY_PHASH_THRESHOLD`.
|
|
242
|
+
|
|
243
|
+
*Known limitation:* Heavy crops (e.g. a small modal window cropped from a full-screen screenshot) may still fall below threshold. This is acceptable — the hint is advisory (FR-2.5.3) and the VLM can still compare the images without it.
|
|
244
|
+
|
|
245
|
+
**FR-2.5.3** Hints are advisory; the user's question wins.
|
|
246
|
+
|
|
247
|
+
**FR-2.5.4** Hints suppressed for `analyze_image` tool path — the agent's `question` is already explicit.
|
|
248
|
+
|
|
249
|
+
**FR-2.6** *Joint description fencing with per-image metadata.*
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
<vision_proxy_joint_description
|
|
253
|
+
images="2"
|
|
254
|
+
dimensions='[{"image":"sha256:aaa","width":1920,"height":1080,"filename":"before.png"},
|
|
255
|
+
{"image":"sha256:bbb","width":1920,"height":1080,"filename":"after.png"}]'
|
|
256
|
+
grounding_format="none">
|
|
257
|
+
Image 1 and Image 2 differ primarily in the navigation sidebar...
|
|
258
|
+
</vision_proxy_joint_description>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`dimensions` is a JSON-encoded array, one entry per image, in the order presented to the vision model. Closing-tag neutralisation is applied.
|
|
262
|
+
|
|
263
|
+
**FR-2.7** Cost telemetry. Provider-reported tokens only.
|
|
264
|
+
|
|
265
|
+
### Edge cases
|
|
266
|
+
|
|
267
|
+
| Case | Behaviour |
|
|
268
|
+
|---|---|
|
|
269
|
+
| 1 image in a turn | No joint call |
|
|
270
|
+
| `maxBatch` exceeded | Joint call skipped; per-image descriptions still produced |
|
|
271
|
+
| One image fails to decode | Joint call skipped |
|
|
272
|
+
| `maxBatch=1` | Joint calls disabled |
|
|
273
|
+
| Filenames contain no recognised pattern | No filename hint; pHash hint may still apply |
|
|
274
|
+
| pHash computation fails | Skip pHash hint; proceed |
|
|
275
|
+
| Cropped images in joint call | Each cropped image's dimensions in the `dimensions` array reflect the cropped region |
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Feature 3 — `/vision-proxy describe` slash command
|
|
280
|
+
|
|
281
|
+
### Functional requirements
|
|
282
|
+
|
|
283
|
+
**FR-3.1** New slash subcommands. Crop syntax supports all three coordinate forms:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
/vision-proxy describe <path-or-hash> [<path-or-hash> ...]
|
|
287
|
+
[--question "<text>"]
|
|
288
|
+
[--crop <image_index>:<form>]
|
|
289
|
+
[--model <provider/model-id>]
|
|
290
|
+
[--save]
|
|
291
|
+
|
|
292
|
+
/vision-proxy redescribe <path-or-hash> [--model <provider/model-id>]
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`<form>` syntax for `--crop`:
|
|
296
|
+
|
|
297
|
+
| Form | Syntax | Example |
|
|
298
|
+
|---|---|---|
|
|
299
|
+
| Named region | `r=<name>` | `--crop 0:r=top-right` |
|
|
300
|
+
| Normalized | `n=<x>,<y>,<w>,<h>` | `--crop 0:n=0.5,0.5,0.4,0.4` |
|
|
301
|
+
| Pixels | `p=<x>,<y>,<w>,<h>` | `--crop 0:p=1840,120,840,360` |
|
|
302
|
+
|
|
303
|
+
Specifying more than one form per crop entry is rejected with a clear error message.
|
|
304
|
+
|
|
305
|
+
**FR-3.2** `describe` semantics:
|
|
306
|
+
- Resolves inputs identical to FR-1.3.
|
|
307
|
+
- Omitting `--question` triggers default generic system prompt.
|
|
308
|
+
- Multiple inputs trigger a joint call (FR-2.5 adaptive prompt + filename/pHash hints).
|
|
309
|
+
- Result printed to TUI only by default; `--save` overwrites canonical session description (single image, no `--question`, no `--crop`).
|
|
310
|
+
|
|
311
|
+
**FR-3.3** `redescribe` is sugar for `describe <hash> --save` with no question and no crop.
|
|
312
|
+
|
|
313
|
+
**FR-3.4** Inline consent prompts.
|
|
314
|
+
|
|
315
|
+
**FR-3.5** TUI output uses distinct visual style with `[Vision Proxy]` prefix.
|
|
316
|
+
|
|
317
|
+
**FR-3.6** Logged as `vision_proxy.command` entries.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Feature 4 — Optional grounded-coordinate output
|
|
322
|
+
|
|
323
|
+
### Functional requirements
|
|
324
|
+
|
|
325
|
+
**FR-4.1** Per-model capability flag in the model registry: `supportsGrounding: boolean`.
|
|
326
|
+
|
|
327
|
+
**FR-4.1.1** *Curated default list* (1.4.0 ships with these marked `supportsGrounding: true`):
|
|
328
|
+
|
|
329
|
+
**Tier 1 — designed-in grounding:**
|
|
330
|
+
- `Qwen/Qwen2.5-VL-3B-Instruct`, `-7B-Instruct`, `-32B-Instruct`, `-72B-Instruct`
|
|
331
|
+
- `Qwen/Qwen3-VL` family
|
|
332
|
+
- `allenai/Molmo2-8B`, `allenai/Molmo2-72B`
|
|
333
|
+
- `deepseek-ai/deepseek-vl2-tiny`, `-small`, `-base`
|
|
334
|
+
- `OpenGVLab/InternVL3` family
|
|
335
|
+
|
|
336
|
+
**Tier 2 — opt-in with quality caveat:**
|
|
337
|
+
- `google/gemini-2.5-pro`, `google/gemini-3-pro`
|
|
338
|
+
|
|
339
|
+
**Excluded (warned on `add` attempt):**
|
|
340
|
+
- `anthropic/claude-*`, `openai/gpt-4o`, `gpt-5`, `meta/llama-*-vision`
|
|
341
|
+
|
|
342
|
+
**FR-4.1.2** Slash commands:
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
/vision-proxy grounding-models add <provider/model-id> [--format <fmt>]
|
|
346
|
+
/vision-proxy grounding-models remove <provider/model-id>
|
|
347
|
+
/vision-proxy grounding-models list
|
|
348
|
+
/vision-proxy grounding-models reset
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
`add` for an excluded model triggers a confirmation prompt explaining the unreliability.
|
|
352
|
+
|
|
353
|
+
**FR-4.2** *Native-format grounding output.* When `supportsGrounding: true`, the system prompt for both per-image and joint calls is appended with a model-specific instruction matching the model's training format. The proxy does **not** force a uniform output format across models. Instead, the proxy:
|
|
354
|
+
|
|
355
|
+
1. Looks up the model's `grounding_format` in the registry (FR-4.7).
|
|
356
|
+
2. Appends an instruction phrased in the model's native convention.
|
|
357
|
+
3. Records the format in the response fence's `grounding_format` attribute so the agent knows what convention to interpret.
|
|
358
|
+
|
|
359
|
+
Example instruction for `qwen_pixels` models:
|
|
360
|
+
|
|
361
|
+
> When you describe a spatial element, follow the description with bounding-box coordinates as `[x1, y1, x2, y2]` in absolute pixels relative to the image. Use `Image-N:` prefix for multi-image inputs.
|
|
362
|
+
|
|
363
|
+
Example instruction for `molmo_points` models:
|
|
364
|
+
|
|
365
|
+
> When you describe a spatial element, follow the description with point coordinates as `<point x="..." y="..." alt="..."/>` using your standard percentage-based convention.
|
|
366
|
+
|
|
367
|
+
**FR-4.3** When `supportsGrounding: false`, system prompt unchanged; `grounding_format` attribute on the fence is `"none"`.
|
|
368
|
+
|
|
369
|
+
**FR-4.4** The proxy does not parse, validate, or rewrite returned coordinates. Downstream agents and tooling can extract them using the `grounding_format` attribute as a key.
|
|
370
|
+
|
|
371
|
+
**FR-4.5** Purely additive — a model that ignores the grounding instruction simply produces a description without coordinates; `grounding_format` will still be set per the registry but the response body will contain no coordinates.
|
|
372
|
+
|
|
373
|
+
**FR-4.6** *Crop-and-grounding interaction.* When grounding is enabled and the call includes a `crop`, returned coordinates are relative to the **cropped region** sent to the vision model. The fence includes a `crop_origin` attribute (e.g. `crop_origin="1840,120"`) giving the pixel offset of the crop's top-left corner in the original image. Agents can recover full-image coordinates by adding `crop_origin` to any returned coordinate. The fence's `width` and `height` reflect the cropped dimensions, and the `image` attribute is suffixed with the crop signature (FR-1.8). Documented in the tool description (FR-1.2.3).
|
|
374
|
+
|
|
375
|
+
**FR-4.7** *Per-model `grounding_format` registry.* New required field on every entry in `groundingModels`. The registry maps each grounding-capable model to one of the following format identifiers:
|
|
376
|
+
|
|
377
|
+
| Format identifier | Convention | Example output |
|
|
378
|
+
|---|---|---|
|
|
379
|
+
| `qwen_pixels` | absolute pixels, two-corner `[x1, y1, x2, y2]` | `[1840, 120, 2680, 480]` |
|
|
380
|
+
| `molmo_points` | percentage-based points (0–100), Molmo's native `<point>` element | `<point x="42.5" y="62.3" alt="error dialog"/>` |
|
|
381
|
+
| `deepseek_bbox` | DeepSeek's `<\|ref\|>desc<\|/ref\|><\|det\|>[[x1,y1,x2,y2]]<\|/det\|>` format | (as documented by DeepSeek) |
|
|
382
|
+
| `internvl_pixels` | InternVL's native bbox format, absolute pixels | `[100, 200, 300, 400]` |
|
|
383
|
+
| `gemini_normalized_1000` | normalized 0–1000 coordinates per Gemini API convention | `[420, 124, 670, 248]` |
|
|
384
|
+
| `none` | no grounding | — |
|
|
385
|
+
|
|
386
|
+
**Default mappings shipped in 1.4.0:**
|
|
387
|
+
|
|
388
|
+
```json
|
|
389
|
+
{
|
|
390
|
+
"Qwen/Qwen2.5-VL-7B-Instruct": "qwen_pixels",
|
|
391
|
+
"Qwen/Qwen2.5-VL-72B-Instruct": "qwen_pixels",
|
|
392
|
+
"Qwen/Qwen3-VL-7B": "qwen_pixels",
|
|
393
|
+
"allenai/Molmo2-8B": "molmo_points",
|
|
394
|
+
"allenai/Molmo2-72B": "molmo_points",
|
|
395
|
+
"deepseek-ai/deepseek-vl2": "deepseek_bbox",
|
|
396
|
+
"deepseek-ai/deepseek-vl2-small": "deepseek_bbox",
|
|
397
|
+
"OpenGVLab/InternVL3-8B": "internvl_pixels",
|
|
398
|
+
"google/gemini-2.5-pro": "gemini_normalized_1000",
|
|
399
|
+
"google/gemini-3-pro": "gemini_normalized_1000"
|
|
400
|
+
}
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
When users add a model not in the default mapping, they must specify its format:
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
/vision-proxy grounding-models add <model-id> --format <format-identifier>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
If `--format` is omitted, defaults to `qwen_pixels` (the most common convention) with a TUI warning.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Cross-feature interaction
|
|
414
|
+
|
|
415
|
+
| Combination | Behaviour |
|
|
416
|
+
|---|---|
|
|
417
|
+
| `analyze_image` with N ≥ 2 images | Single joint vision call using FR-2.5 adaptive prompt; filename/pHash hints suppressed (FR-2.5.4) |
|
|
418
|
+
| `analyze_image` with `crop` and N ≥ 2 | Crops applied per-image before transmission, then batched. Joint description's `dimensions` array reflects cropped sizes |
|
|
419
|
+
| `analyze_image` with `pixels` crop on an image not previously described | Proxy clamps using actual dimensions (computed at ingestion); proceeds without warning |
|
|
420
|
+
| Automatic proxy + `analyze_image` follow-up in same turn | Both happen; tool result is authoritative for its specific question (FR-1.10) |
|
|
421
|
+
| `/vision-proxy describe` during automatic proxy turn | Serialised |
|
|
422
|
+
| Mode `off` | Tool unregistered; joint calls disabled; slash commands refuse |
|
|
423
|
+
| Grounding enabled + cropped call | Coordinates relative to cropped image; `crop_origin` provided for mapping back (FR-4.6) |
|
|
424
|
+
| User in non-English locale | FR-2.5 routing happens inside the VLM in user's language; hints language-independent |
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
## Configuration summary
|
|
429
|
+
|
|
430
|
+
| Setting | Slash | Env | Default | Range |
|
|
431
|
+
|---|---|---|---|---|
|
|
432
|
+
| Tool exposure | `/vision-proxy tool on\|off` | `PI_VISION_PROXY_TOOL` | `on` (GA) / `off` (beta) | — |
|
|
433
|
+
| Max images per call | `/vision-proxy max-images-per-call <n>` | `PI_VISION_PROXY_MAX_IMAGES_PER_CALL` | `10` | 1–20 |
|
|
434
|
+
| Max batch (auto) | `/vision-proxy max-batch <n>` | `PI_VISION_PROXY_MAX_BATCH` | `4` | 1–10 |
|
|
435
|
+
| Result cache size | `/vision-proxy cache-size <n>` | `PI_VISION_PROXY_CACHE_SIZE` | `50` | 0–500 |
|
|
436
|
+
| pHash similarity threshold | (`vision-proxy.json`) | `PI_VISION_PROXY_PHASH_THRESHOLD` | `0.80` | 0.0–1.0 |
|
|
437
|
+
| Grounding-capable models | `/vision-proxy grounding-models …` | — | Tier 1 list (FR-4.1.1) | — |
|
|
438
|
+
|
|
439
|
+
`vision-proxy.json` schema additions:
|
|
440
|
+
|
|
441
|
+
```json
|
|
442
|
+
{
|
|
443
|
+
"tool": "on",
|
|
444
|
+
"maxImagesPerCall": 10,
|
|
445
|
+
"maxBatch": 4,
|
|
446
|
+
"cacheSize": 50,
|
|
447
|
+
"pHashSimilarityThreshold": 0.80,
|
|
448
|
+
"groundingModels": {
|
|
449
|
+
"Qwen/Qwen2.5-VL-7B-Instruct": { "format": "qwen_pixels" },
|
|
450
|
+
"allenai/Molmo2-8B": { "format": "molmo_points" },
|
|
451
|
+
"deepseek-ai/deepseek-vl2": { "format": "deepseek_bbox" },
|
|
452
|
+
"OpenGVLab/InternVL3-8B": { "format": "internvl_pixels" }
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
(Shipped default is the full FR-4.7 mapping; example abbreviated.)
|
|
458
|
+
|
|
459
|
+
Backwards compatibility: existing 1.3.0 `vision-proxy.json` files load unchanged. `groundingModels`, `tool`, `maxImagesPerCall`, `maxBatch`, `cacheSize` were absent in 1.3.0, so the new defaults take effect on first 1.4.0 launch without losing user state.
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Telemetry / observability
|
|
464
|
+
|
|
465
|
+
Session entry types:
|
|
466
|
+
|
|
467
|
+
- `vision_proxy.tool_call` — includes `crop_form` (`region`/`normalized`/`pixels`/none) and `crop_resolved_pixels` for analytics.
|
|
468
|
+
- `vision_proxy.joint_description` — includes `filename_hint`, `phash_similarity`, and `dimensions` array.
|
|
469
|
+
- `vision_proxy.command`
|
|
470
|
+
- `vision_proxy.skip`
|
|
471
|
+
|
|
472
|
+
Each entry includes timestamp, image hashes, model id, `grounding_format` used, latency_ms, provider-reported token usage, `cache_hit` where applicable.
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Security considerations
|
|
477
|
+
|
|
478
|
+
- **Prompt injection.** Closing-tag neutralisation applied to all three fence types (`<vision_proxy_description>`, `<vision_proxy_analysis>`, `<vision_proxy_joint_description>`). Filename hints are basename-only — full paths never appear in fence attributes or hint strings.
|
|
479
|
+
- **Path policy.** Defers to Pi's `read` tool plus `..`/symlink-escape rejection.
|
|
480
|
+
- **Per-provider consent.** Tracked per provider. Consent for one provider does not carry over to a different provider.
|
|
481
|
+
- **Crop as exfiltration vector.** Cropped region still sent to configured vision provider; documented in consent prompt. The three crop forms do not change the security posture — they just determine how the agent specifies what to send.
|
|
482
|
+
- **Dimension metadata leak.** Image dimensions in fence attributes are derived from the user's own attached images and pose no additional risk beyond what the proxy already sends to the vision model.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
## Rollout
|
|
487
|
+
|
|
488
|
+
- **1.4.0-beta.1**: Feature 1 with `tool=off` default, all three crop forms supported, dimensions in description fence.
|
|
489
|
+
- **1.4.0-beta.2**: Feature 3 with extended crop syntax.
|
|
490
|
+
- **1.4.0-beta.3**: Feature 2 with `maxBatch=1` default; adaptive prompt, filename/pHash hints, joint-fence dimensions.
|
|
491
|
+
- **1.4.0-beta.4**: Feature 4 with curated Tier 1 default list and `grounding_format` registry; opt-in flag itself off by default.
|
|
492
|
+
- **1.4.0**: Flip defaults to `tool=on`, `maxBatch=4`. Grounding flag remains opt-in but list pre-populated.
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Out of scope (later versions)
|
|
497
|
+
|
|
498
|
+
- Cross-session image library
|
|
499
|
+
- Per-image TTL on cached descriptions
|
|
500
|
+
- Vision model fallback chain
|
|
501
|
+
- Streaming descriptions
|
|
502
|
+
- Preprocessing (resize, format normalisation) before vision call
|
|
503
|
+
- Dollar-cost telemetry
|
|
504
|
+
- Proxy-side parsing/normalisation of grounding coordinates into a unified format (currently the agent reads `grounding_format` and decides)
|
|
505
|
+
- Auto-detection of grounding capability and format from provider metadata
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Appendix A — References
|
|
510
|
+
|
|
511
|
+
- Dong et al., *Training Multi-Image Vision Agents via End2End Reinforcement Learning* (IMAgent), arXiv 2512.08980, December 2025.
|
|
512
|
+
- Chen et al., *MiCo: Multi-image Contrast for Reinforcement Visual Reasoning*, NeurIPS 2025, arXiv 2506.22434.
|
|
513
|
+
- *LaViT: Aligning Latent Visual Thoughts for Multi-modal Reasoning*, arXiv 2601.10129, January 2026.
|
|
514
|
+
- Wang et al., *CiQi-Agent: Aligning Vision, Tools and Aesthetics in Multimodal Agent for Cultural Reasoning*, arXiv 2603.28474, March 2026.
|
|
515
|
+
- Bai et al., *Qwen2.5-VL Technical Report*, 2025. Source for FR-4.7 `qwen_pixels` format.
|
|
516
|
+
- Deitke et al., *Molmo2: Open Vision-Language Video Model*, arXiv 2601.10611, January 2026. Source for FR-4.7 `molmo_points` format.
|
|
517
|
+
- DeepSeek AI, *DeepSeek-VL2: Mixture-of-Experts Vision-Language Models*, arXiv 2412.10302, December 2024. Source for FR-4.7 `deepseek_bbox` format.
|
|
518
|
+
|
|
519
|
+
## Appendix B — Tool contract
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
type AnalyzeImageInput = {
|
|
523
|
+
images: string[];
|
|
524
|
+
question: string;
|
|
525
|
+
model?: string;
|
|
526
|
+
crop?: CropEntry[];
|
|
527
|
+
reason?: string;
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
type CropEntry = {
|
|
531
|
+
image_index: number;
|
|
532
|
+
} & (
|
|
533
|
+
| { region: NamedRegion }
|
|
534
|
+
| { normalized: { x: number; y: number; width: number; height: number } }
|
|
535
|
+
| { pixels: { x: number; y: number; width: number; height: number } }
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
type NamedRegion =
|
|
539
|
+
| "top-left" | "top-right" | "bottom-left" | "bottom-right"
|
|
540
|
+
| "top" | "bottom" | "left" | "right" | "center"
|
|
541
|
+
| "top-half" | "bottom-half" | "left-half" | "right-half";
|
|
542
|
+
|
|
543
|
+
type AnalyzeImageOutput = string;
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
## Appendix C — Slash commands
|
|
547
|
+
|
|
548
|
+
```
|
|
549
|
+
/vision-proxy tool on|off
|
|
550
|
+
/vision-proxy max-images-per-call <n> # 1..20
|
|
551
|
+
/vision-proxy max-batch <n> # 1..10
|
|
552
|
+
/vision-proxy cache-size <n> # 0..500
|
|
553
|
+
/vision-proxy grounding-models add|remove|list|reset [<provider/model-id>] [--format <fmt>]
|
|
554
|
+
/vision-proxy describe <path|hash>...
|
|
555
|
+
[--question "<text>"]
|
|
556
|
+
[--crop <i>:r=<region> | n=<x>,<y>,<w>,<h> | p=<x>,<y>,<w>,<h>]
|
|
557
|
+
[--model <provider/model-id>]
|
|
558
|
+
[--save]
|
|
559
|
+
/vision-proxy redescribe <path|hash>
|
|
560
|
+
[--model <provider/model-id>]
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
## Appendix D — Filename hint patterns (FR-2.5.1)
|
|
564
|
+
|
|
565
|
+
Basename-only matching, case-insensitive.
|
|
566
|
+
|
|
567
|
+
```
|
|
568
|
+
before.* ∧ after.* → "before/after pair"
|
|
569
|
+
old.* ∧ new.* → "old/new pair"
|
|
570
|
+
{prefix}{version} (≥2, same prefix) → "versioned sequence"
|
|
571
|
+
*_1.* ∧ *_2.* → "numbered sequence"
|
|
572
|
+
*-1.* ∧ *-2.* → "numbered sequence"
|
|
573
|
+
YYYY-MM-DD_*.* (sortable, ≥2 files) → "time-ordered sequence"
|
|
574
|
+
(no match) → no hint emitted
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Version extraction rule:** From each basename, extract a `(prefix, version_number)` tuple by matching the rightmost occurrence of `[vV]?(\d+(?:\.\d+)?)` immediately before the extension. If ≥2 files share the same prefix but have different version numbers → "versioned sequence".
|
|
578
|
+
|
|
579
|
+
Examples that match:
|
|
580
|
+
- `mockup_v2.png ∧ mockup_v4.png` → prefix=`mockup_v`, versions {2, 4}
|
|
581
|
+
- `draft_v1.1.png ∧ draft_v1.2.png` → prefix=`draft_v`, versions {1.1, 1.2}
|
|
582
|
+
- `app2.png ∧ app3.png` → prefix=`app`, versions {2, 3}
|
|
583
|
+
|
|
584
|
+
The "versioned sequence" rule is checked before the `_\d` and `-\d` numbered-sequence rules.
|
|
585
|
+
|
|
586
|
+
## Appendix E — Description fence reference
|
|
587
|
+
|
|
588
|
+
All three fence types share a common metadata schema. Required attributes are always present; optional attributes appear when applicable.
|
|
589
|
+
|
|
590
|
+
| Attribute | Required | Where applied | Notes |
|
|
591
|
+
|---|---|---|---|
|
|
592
|
+
| `image` | yes | `description`, `analysis` | sha256 hash; suffixed with `#crop:x,y,w,h` for cropped calls |
|
|
593
|
+
| `images` | yes | `joint_description` | image count |
|
|
594
|
+
| `dimensions` | yes | `joint_description` | JSON-encoded array, one entry per image |
|
|
595
|
+
| `width` | yes | `description`, `analysis` | pixels of what the vision model saw (cropped or original) |
|
|
596
|
+
| `height` | yes | `description`, `analysis` | as above |
|
|
597
|
+
| `crop_origin` | optional | `description`, `analysis` | `"x,y"` pixels; present only on cropped calls; top-left corner of crop within original image |
|
|
598
|
+
| `filename` | optional | all three | basename only; never full path |
|
|
599
|
+
| `grounding_format` | optional | all three | one of FR-4.7's identifiers; absent or `"none"` when grounding off |
|