pi-vision-handoff 0.9.1 → 0.9.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.
- package/README.md +1 -1
- package/package.json +4 -4
- package/src/describer.ts +33 -2
package/README.md
CHANGED
|
@@ -255,7 +255,7 @@ Because pi runs parallel `read` tool calls via `Promise.all` and fires each read
|
|
|
255
255
|
| `context` (all messages) | `{ type: "input_image", image_url: "data:…" }` (OpenAI Responses) | detected by shape → replaced with `{ type: "input_text", text }` |
|
|
256
256
|
| `context` (all messages) | `{ type: "image", source: { type: "base64", media_type, data } }` (Anthropic Messages) | detected by shape → replaced with `{ type: "text", text }` |
|
|
257
257
|
|
|
258
|
-
The describer call itself goes through pi's normal model machinery (`completeSimple()`), **not** the agent event loop — so it never re-triggers `context` (no recursion). The `read` tool result keeps its image block untouched (kitty inline + `/resume`); only the `context`-cloned LLM-bound payload has images swapped for text.
|
|
258
|
+
The describer call itself goes through pi's normal model machinery (`completeSimple()`), **not** the agent event loop — so it never re-triggers `context` (no recursion). Every real describer request receives a dedicated helper session ID. For Neuralwatt, the request also receives a matching isolated `X-NW-Conversation-ID` instead of inheriting the main Pi session ID; the unrelated image prompt therefore cannot replace the main agent's server-side cache lineage. The `read` tool result keeps its image block untouched (kitty inline + `/resume`); only the `context`-cloned LLM-bound payload has images swapped for text.
|
|
259
259
|
|
|
260
260
|
### Usage reporting
|
|
261
261
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vision-handoff",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@earendil-works/pi-ai": "0.
|
|
35
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
36
|
-
"@earendil-works/pi-tui": "0.
|
|
34
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
35
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
36
|
+
"@earendil-works/pi-tui": "0.83.0",
|
|
37
37
|
"@types/node": "25.9.1",
|
|
38
38
|
"@vitest/coverage-v8": "4.1.7",
|
|
39
39
|
"knip": "6.14.1",
|
package/src/describer.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
type VisionHandoffUsageRecord,
|
|
61
61
|
} from "./usage.js";
|
|
62
62
|
import { imageHash } from "./image.js";
|
|
63
|
+
import { randomUUID } from "node:crypto";
|
|
63
64
|
import { abortWireGuard, fetchInterceptorGuard, timeoutGuard, type AbortWire } from "./dispose.js";
|
|
64
65
|
|
|
65
66
|
/** Tokens reserved for the describer's input so the requested output budget
|
|
@@ -133,17 +134,47 @@ function configSnapshot(cfg: VisionHandoffConfig): {
|
|
|
133
134
|
* completeSimple() directly cannot resolve custom API ids such as `makora`.
|
|
134
135
|
* The optional access preserves compatibility with older ModelRegistry versions,
|
|
135
136
|
* which registered custom streams globally and do not expose the config getter. */
|
|
137
|
+
const NEURALWATT_CONVERSATION_HEADER = "x-nw-conversation-id";
|
|
138
|
+
|
|
139
|
+
function isolateVisionRequest(
|
|
140
|
+
model: Model<Api>,
|
|
141
|
+
options: SimpleStreamOptions,
|
|
142
|
+
): SimpleStreamOptions {
|
|
143
|
+
const sessionId = `pi-vision-handoff:${randomUUID()}`;
|
|
144
|
+
const headers = { ...(options.headers ?? {}) };
|
|
145
|
+
const inheritedConversationHeader = Object.keys(headers).find(
|
|
146
|
+
(name) => name.toLowerCase() === NEURALWATT_CONVERSATION_HEADER,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// Provider auth can carry the active Pi conversation id. A describer call is
|
|
150
|
+
// an independent conversation: reusing that id makes an unrelated image
|
|
151
|
+
// prompt the newest cache lineage for the main agent. Give every real vision
|
|
152
|
+
// request its own identity while preserving all non-session auth headers.
|
|
153
|
+
if (inheritedConversationHeader) {
|
|
154
|
+
headers[inheritedConversationHeader] = sessionId;
|
|
155
|
+
} else if (model.provider.toLowerCase() === "neuralwatt") {
|
|
156
|
+
headers["X-NW-Conversation-ID"] = sessionId;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
...options,
|
|
161
|
+
sessionId,
|
|
162
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
136
166
|
export async function completeVisionModel(
|
|
137
167
|
model: Model<Api>,
|
|
138
168
|
modelRegistry: ModelRegistry,
|
|
139
169
|
context: Context,
|
|
140
170
|
options: SimpleStreamOptions,
|
|
141
171
|
): Promise<AssistantMessage> {
|
|
172
|
+
const isolatedOptions = isolateVisionRequest(model, options);
|
|
142
173
|
const provider = modelRegistry.getRegisteredProviderConfig?.(model.provider);
|
|
143
174
|
if (provider?.streamSimple && provider.api === model.api) {
|
|
144
|
-
return provider.streamSimple(model, context,
|
|
175
|
+
return provider.streamSimple(model, context, isolatedOptions).result();
|
|
145
176
|
}
|
|
146
|
-
return completeSimple(model, context,
|
|
177
|
+
return completeSimple(model, context, isolatedOptions);
|
|
147
178
|
}
|
|
148
179
|
|
|
149
180
|
/** Dependencies the describer can't own itself (held by the engine). */
|