pi-vision-handoff 0.4.1 โ 0.4.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/README.md +5 -5
- package/package.json +1 -1
- package/src/describer.ts +18 -10
- package/src/index.ts +1 -1
- package/src/usage.ts +2 -2
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ No `settings.json` touched. No per-provider glue. Pick a describer once and ever
|
|
|
39
39
|
- **๐ฎ Interactive picker** โ `/vision-handoff` opens a TUI listing every model, vision-capable ones first (๐), to choose your describer.
|
|
40
40
|
- **๐ผ๏ธ DataLoader-batched descriptions** โ the `read` tools are `load()` callers: N parallel reads coalesce into ONE batched vision call (dispatched via `setImmediate` after the poll phase, so reads completing together batch instead of splitting), awaited during the tool-result phase (free time) so the agent's next turn never blocks on the describer. Descriptions are ready before `context` fires, so the swap is a non-blocking cache hit.
|
|
41
41
|
- **๐งน Hides pi's "model does not support images" note** โ on read results the extension strips pi's `[Current model does not support imagesโฆ]` note from the text block (it's misleading once the handoff delivers the image's content as text), while keeping the image block for kitty inline rendering and `/resume`.
|
|
42
|
-
- **๐ Provider-agnostic** โ uses pi's own model execution machinery (`@earendil-works/pi-ai`'s `
|
|
42
|
+
- **๐ Provider-agnostic** โ uses pi's own model execution machinery (`@earendil-works/pi-ai`'s `completeSimple()`), so it works with any provider/configured model, including custom provider extensions.
|
|
43
43
|
- **๐ง Automatic targets** โ by default, handoff applies to *every* model that lacks native vision. Opt out with `/vision-handoff auto off`.
|
|
44
44
|
- **๐๏ธ Explicit overrides** โ force handoff for specific models (e.g. a weak vision model) with `/vision-handoff add`.
|
|
45
45
|
- **โก Pre-warmed at paste-enter** โ the moment you press enter, `before_agent_start` scans the prompt for pasted clipboard image temp-file paths (pi writes pasted images to `<tmpdir>/pi-clipboard-<uuid>.<ext>` and inserts the path as text), reads them, and kicks off the ONE batched vision call concurrent with the agent's first response โ so by the time the agent reads the files, they're already cache hits.
|
|
@@ -164,7 +164,7 @@ phase (so reads completing together batch instead of splitting into N calls).
|
|
|
164
164
|
tool_result handler calls `loadDescription(img)` for its image
|
|
165
165
|
blocks and AWAITS the shared batch
|
|
166
166
|
โข DataLoader: all `load()` calls in one event-loop poll iteration
|
|
167
|
-
land in ONE batch โ ONE `
|
|
167
|
+
land in ONE batch โ ONE `completeSimple()` vision call for the whole read
|
|
168
168
|
set. `enqueuePostPromiseJob` schedules dispatch via `setImmediate`
|
|
169
169
|
(the check phase, which runs AFTER the whole poll phase โ not
|
|
170
170
|
`process.nextTick`, which would drain between the reads' I/O
|
|
@@ -210,7 +210,7 @@ sends every uncached image in a single user message with per-image
|
|
|
210
210
|
per-image descriptions (keyed by `sha256(mime + base64)` in the per-image
|
|
211
211
|
cache). If the vision model ignores the delimiter format and the batched
|
|
212
212
|
response can't be split, each unparsed image falls back to its own
|
|
213
|
-
single-image `
|
|
213
|
+
single-image `completeSimple()` call **in parallel** (no delimiters to cooperate
|
|
214
214
|
with) โ descriptions still arrive together. Only when a per-image call
|
|
215
215
|
itself genuinely fails (auth, timeout, empty) does that image degrade to
|
|
216
216
|
`[Image: description unavailable]`; one bad image never voids the rest.
|
|
@@ -268,7 +268,7 @@ off mid-stream); earlier sections had `<<<END>>>` delimiters and are complete.
|
|
|
268
268
|
it before the LLM call), so a slow describer would also make aborting a turn
|
|
269
269
|
slow โ pi has to wait for the transform to return. The hook therefore wires
|
|
270
270
|
the turn's abort signal (`ctx.signal`, the active run's `AbortController`)
|
|
271
|
-
into the describer's `
|
|
271
|
+
into the describer's `completeSimple()` call, so a user cancel kills the in-flight
|
|
272
272
|
vision request immediately. A deliberate abort is not warned about (it's
|
|
273
273
|
not a vision-model failure) and the LLM-bound payload is left untouched since
|
|
274
274
|
the turn is being torn down anyway.
|
|
@@ -282,7 +282,7 @@ the turn is being torn down anyway.
|
|
|
282
282
|
| `context` (all messages) | `{ type: "input_image", image_url: "data:โฆ" }` (OpenAI Responses) | detected by shape โ replaced with `{ type: "input_text", text }` |
|
|
283
283
|
| `context` (all messages) | `{ type: "image", source: { type: "base64", media_type, data } }` (Anthropic Messages) | detected by shape โ replaced with `{ type: "text", text }` |
|
|
284
284
|
|
|
285
|
-
The describer call itself goes through pi's normal model machinery (`
|
|
285
|
+
The describer call itself goes through pi's normal model machinery (`completeSimple()`),
|
|
286
286
|
**not** the agent event loop โ so it never re-triggers `context` (no recursion).
|
|
287
287
|
The `read` tool result keeps its image block untouched (kitty inline + `/resume`);
|
|
288
288
|
only the `context`-cloned LLM-bound payload has images swapped for text.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vision-handoff",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Give text-only pi models vision โ describe images with a vision model you pick via an interactive picker, then hand off the text description to non-vision models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Tom X Nguyen",
|
package/src/describer.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The vision describer: calls a vision-capable model via pi-ai's
|
|
3
|
-
* to produce text descriptions of images.
|
|
2
|
+
* The vision describer: calls a vision-capable model via pi-ai's
|
|
3
|
+
* `completeSimple()` to produce text descriptions of images.
|
|
4
|
+
*
|
|
5
|
+
* `completeSimple` (not `complete`) is the path that translates the `reasoning`
|
|
6
|
+
* ThinkingLevel into each provider's `reasoningEffort`/budget. `complete()` โ
|
|
7
|
+
* `stream()` reads only the pre-translated `reasoningEffort` field and silently
|
|
8
|
+
* drops a bare `reasoning`, so the describer's thinking setting would be a no-op
|
|
9
|
+
* through `complete()` โ the bug this swap fixes. The agent loop, SDK, and
|
|
10
|
+
* compaction all route thinking through `completeSimple`/`streamSimple` for the
|
|
11
|
+
* same reason.
|
|
4
12
|
*
|
|
5
13
|
* Two entry points:
|
|
6
14
|
* - {@link runBatch}: ONE batched call describing N images at once (the
|
|
@@ -13,13 +21,13 @@
|
|
|
13
21
|
* Resource lifetimes (fetch interceptor, timeout timer, turn-abort wire) are
|
|
14
22
|
* managed with the `using` keyword via the {@link Disposable} guards in
|
|
15
23
|
* `dispose.ts`, replacing the manual `try`/`finally` cleanup the old code
|
|
16
|
-
* carried. Disposing is lexical and exception-safe: a thrown `
|
|
24
|
+
* carried. Disposing is lexical and exception-safe: a thrown `completeSimple()`
|
|
17
25
|
* still tears down the timer, uninstalls the interceptor, and detaches the
|
|
18
26
|
* abort listener.
|
|
19
27
|
*/
|
|
20
28
|
|
|
21
29
|
import type { Api, ImageContent, Message, Model, TextContent, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
22
|
-
import {
|
|
30
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
23
31
|
import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
24
32
|
import {
|
|
25
33
|
batchUserPrompt,
|
|
@@ -52,7 +60,7 @@ import { abortWireGuard, fetchInterceptorGuard, timeoutGuard, type AbortWire } f
|
|
|
52
60
|
* model (e.g. 262144 context โ 8192 reserve = 253952 output budget). */
|
|
53
61
|
const INPUT_RESERVE_TOKENS = 8192;
|
|
54
62
|
|
|
55
|
-
/** Resolve the `maxTokens` to pass to `
|
|
63
|
+
/** Resolve the `maxTokens` to pass to `completeSimple()` for a describer call.
|
|
56
64
|
*
|
|
57
65
|
* - A configured `cfg.maxTokens` wins (explicit cost/latency cap), clamped to
|
|
58
66
|
* fit the context window.
|
|
@@ -76,7 +84,7 @@ export function resolveMaxTokens(
|
|
|
76
84
|
return Math.max(1, Math.min(requested, cap));
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
/** Resolve the `reasoning` (thinking) level to pass to `
|
|
87
|
+
/** Resolve the `reasoning` (thinking) level to pass to `completeSimple()`, or
|
|
80
88
|
* `undefined` to leave thinking off.
|
|
81
89
|
*
|
|
82
90
|
* - Returns `undefined` when thinking is disabled in config.
|
|
@@ -108,7 +116,7 @@ export interface DescriberDeps {
|
|
|
108
116
|
/** The result of a batched describer call: per-image raw descriptions keyed by hash. */
|
|
109
117
|
export type BatchResult = Map<string, string>;
|
|
110
118
|
|
|
111
|
-
/** Describe N images with ONE batched `
|
|
119
|
+
/** Describe N images with ONE batched `completeSimple()` call and split the response
|
|
112
120
|
* back into per-image descriptions. Returns a map keyed by image hash; an
|
|
113
121
|
* image whose section failed to parse is omitted (the caller treats omission
|
|
114
122
|
* as "description unavailable"). On a genuine call failure (auth, abort,
|
|
@@ -159,7 +167,7 @@ export async function runBatch(
|
|
|
159
167
|
const describeCtx: DescribeContext = { energyReader: undefined };
|
|
160
168
|
try {
|
|
161
169
|
const response = await describeAls.run(describeCtx, async () =>
|
|
162
|
-
|
|
170
|
+
completeSimple(
|
|
163
171
|
visionModel,
|
|
164
172
|
{ systemPrompt, messages: [userMessage] },
|
|
165
173
|
{ apiKey: auth.apiKey, headers: auth.headers, signal: controller.signal, maxTokens, reasoning },
|
|
@@ -231,7 +239,7 @@ export async function runBatch(
|
|
|
231
239
|
}
|
|
232
240
|
}
|
|
233
241
|
|
|
234
|
-
/** Describe a single image with one `
|
|
242
|
+
/** Describe a single image with one `completeSimple()` call and return the RAW
|
|
235
243
|
* description (no envelope, no truncation). Returns null on any genuine
|
|
236
244
|
* failure (auth, abort/error, empty) so the caller only caches `UNAVAILABLE`
|
|
237
245
|
* when a real describer attempt failed. */
|
|
@@ -273,7 +281,7 @@ export async function describeSingle(
|
|
|
273
281
|
const describeCtx: DescribeContext = { energyReader: undefined };
|
|
274
282
|
try {
|
|
275
283
|
const response = await describeAls.run(describeCtx, async () =>
|
|
276
|
-
|
|
284
|
+
completeSimple(
|
|
277
285
|
visionModel,
|
|
278
286
|
{ systemPrompt, messages: [userMessage] },
|
|
279
287
|
{ apiKey: auth.apiKey, headers: auth.headers, signal: controller.signal, maxTokens, reasoning },
|
package/src/index.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const DEFAULT_USER_PROMPT_PREFIX = "The user's request about this image:
|
|
|
38
38
|
export const IMAGE_PLACEHOLDER_PREFIX = "[Image: ";
|
|
39
39
|
export const IMAGE_PLACEHOLDER_SUFFIX = "]";
|
|
40
40
|
|
|
41
|
-
/** Marker appended to a description whose `
|
|
41
|
+
/** Marker appended to a description whose `completeSimple()` call ended with
|
|
42
42
|
* `stopReason: "length"` โ i.e. the vision model hit a token limit (either the
|
|
43
43
|
* configured `maxTokens` or the provider's hard output cap) before finishing.
|
|
44
44
|
* A truncated description is still useful, but the agent must not mistake it
|
package/src/usage.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One record is produced per REAL describer provider call (cache hits emit
|
|
5
5
|
* nothing):
|
|
6
|
-
* - model + tokens: from
|
|
6
|
+
* - model + tokens: from completeSimple()'s AssistantMessage.usage
|
|
7
7
|
* - energy + cost + raw MCR/energy/cost payloads: from Neuralwatt SSE comment
|
|
8
8
|
* lines parsed out of the teed response body (readEnergyFromTee). Present
|
|
9
9
|
* ONLY when the vision model is a Neuralwatt model โ non-Neuralwatt models
|
|
@@ -197,7 +197,7 @@ export function buildUsageRecord(
|
|
|
197
197
|
|
|
198
198
|
// โโ Concurrency-safe fetch interceptor โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
199
199
|
//
|
|
200
|
-
// The only body-interception point for
|
|
200
|
+
// The only body-interception point for completeSimple() is globalThis.fetch (pi-ai's
|
|
201
201
|
// StreamOptions.onResponse exposes headers only, not the body where the
|
|
202
202
|
// `: energy` SSE comments live). before_agent_start fires several describeImage()
|
|
203
203
|
// calls fire-and-forget, so a naรฏve save/patch/restore of globalThis.fetch would
|