pi-vision-handoff 0.10.5 → 0.10.7
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 +8 -0
- package/package.json +5 -4
- package/src/describer.ts +5 -0
- package/src/usage.ts +22 -26
package/README.md
CHANGED
|
@@ -16,6 +16,14 @@ _Describe images with a vision model you pick, then feed the text to models that
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## Pi 0.99 compatibility (0.10.7)
|
|
20
|
+
|
|
21
|
+
Tested with Pi **0.99.0**. Host-provided Pi packages and TypeBox are peers (`*`), not bundled runtime dependencies; development uses exact Pi 0.99.0 pins and host-compatible TypeBox where needed.
|
|
22
|
+
|
|
23
|
+
Vision calls use the native ModelRegistry streaming runtime (including native providers/auth/hooks). Fetch energy capture remains necessary for raw SSE comments, but cleanup now preserves later wrappers and retained predecessors cannot recurse after reinstall. Native nested read results are covered.
|
|
24
|
+
|
|
25
|
+
Run `bun run test:host` for the offline real-host load, native codemode/nested-call, module-identity and reload checks. Set `PI99_HOST_PACKAGE` to an installed Pi package directory to test that host explicitly; add `PI99_HOST_ENTRY=bundle` to check the bundled CLI runtime's constructors.
|
|
26
|
+
|
|
19
27
|
## The Problem
|
|
20
28
|
|
|
21
29
|
Some of the best coding models are blind. You paste a screenshot, a UI mock, a stack trace, or a diagram into pi — and a text-only model either silently ignores the image or rejects the request outright. Up to now your only options were to describe the image yourself, or switch to a (often weaker-for-coding) vision model just to read it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vision-handoff",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
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,6 +31,7 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
|
+
"test:host": "node scripts/pi99-host.mjs",
|
|
34
35
|
"test": "vitest run",
|
|
35
36
|
"test:watch": "vitest",
|
|
36
37
|
"test:coverage": "vitest run --coverage",
|
|
@@ -38,9 +39,9 @@
|
|
|
38
39
|
"lint:dead": "knip --no-gitignore"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
|
-
"@earendil-works/pi-ai": "0.
|
|
42
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
43
|
-
"@earendil-works/pi-tui": "0.
|
|
42
|
+
"@earendil-works/pi-ai": "0.99.0",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "0.99.0",
|
|
44
|
+
"@earendil-works/pi-tui": "0.99.0",
|
|
44
45
|
"@types/node": "25.9.1",
|
|
45
46
|
"@vitest/coverage-v8": "4.1.7",
|
|
46
47
|
"knip": "6.14.1",
|
package/src/describer.ts
CHANGED
|
@@ -178,6 +178,11 @@ export async function completeVisionModel(
|
|
|
178
178
|
// here, then hand the same transcript to both the registered-provider stream and
|
|
179
179
|
// the compat `completeSimple` fallback (which also accepts a TranscriptContext).
|
|
180
180
|
const transcript: TranscriptContext = normalizeContext(context);
|
|
181
|
+
// Pi 0.99's runtime owns native providers, auth, environment and request hooks.
|
|
182
|
+
// Calling a legacy provider config directly bypasses those contracts.
|
|
183
|
+
if (typeof modelRegistry.streamSimple === "function") {
|
|
184
|
+
return modelRegistry.streamSimple(model, transcript, isolatedOptions).result();
|
|
185
|
+
}
|
|
181
186
|
const provider = modelRegistry.getRegisteredProviderConfig?.(model.provider);
|
|
182
187
|
if (provider?.streamSimple && provider.api === model.api) {
|
|
183
188
|
return provider.streamSimple(model, transcript, isolatedOptions).result();
|
package/src/usage.ts
CHANGED
|
@@ -218,44 +218,40 @@ export interface DescribeContext {
|
|
|
218
218
|
export const describeAls = new AsyncLocalStorage<DescribeContext>();
|
|
219
219
|
|
|
220
220
|
let fetchInterceptRefCount = 0;
|
|
221
|
-
let
|
|
221
|
+
let installation: { previous: typeof globalThis.fetch; wrapper: typeof globalThis.fetch; active: boolean } | undefined;
|
|
222
222
|
|
|
223
|
-
/**
|
|
224
|
-
*
|
|
223
|
+
/** Refcounted, ownership-safe interception. Each wrapper closes over its own
|
|
224
|
+
* predecessor: another extension may retain it after our session shuts down. */
|
|
225
225
|
export function installFetchInterceptor(): void {
|
|
226
226
|
if (fetchInterceptRefCount === 0) {
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
const current = { previous: globalThis.fetch, wrapper: globalThis.fetch, active: true };
|
|
228
|
+
current.wrapper = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
229
|
+
const response = await current.previous(input, init);
|
|
230
|
+
const store = describeAls.getStore();
|
|
231
|
+
if (!current.active || !store || !response.body) return response;
|
|
232
|
+
const [bodyForSdk, bodyForEnergy] = response.body.tee();
|
|
233
|
+
store.energyReader = readEnergyFromTee(bodyForEnergy);
|
|
234
|
+
return new Response(bodyForSdk, {
|
|
235
|
+
headers: response.headers, status: response.status, statusText: response.statusText,
|
|
236
|
+
});
|
|
237
|
+
}) as typeof globalThis.fetch;
|
|
238
|
+
installation = current;
|
|
239
|
+
globalThis.fetch = current.wrapper;
|
|
229
240
|
}
|
|
230
241
|
fetchInterceptRefCount++;
|
|
231
242
|
}
|
|
232
243
|
|
|
233
|
-
/** Remove the interceptor. Refcounted: only the last caller restores fetch. */
|
|
234
244
|
export function uninstallFetchInterceptor(): void {
|
|
235
245
|
if (fetchInterceptRefCount > 0) fetchInterceptRefCount--;
|
|
236
|
-
if (fetchInterceptRefCount === 0 &&
|
|
237
|
-
|
|
238
|
-
|
|
246
|
+
if (fetchInterceptRefCount === 0 && installation) {
|
|
247
|
+
installation.active = false;
|
|
248
|
+
// Never overwrite a wrapper installed by someone else after ours.
|
|
249
|
+
if (globalThis.fetch === installation.wrapper) globalThis.fetch = installation.previous;
|
|
250
|
+
installation = undefined;
|
|
239
251
|
}
|
|
240
252
|
}
|
|
241
253
|
|
|
242
|
-
/** Current refcount — 0 means
|
|
254
|
+
/** Current refcount — 0 means capture is inactive. Test hook. */
|
|
243
255
|
export function fetchInterceptorRefcount(): number {
|
|
244
256
|
return fetchInterceptRefCount;
|
|
245
257
|
}
|
|
246
|
-
|
|
247
|
-
async function interceptedFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
248
|
-
const real = savedRealFetch ?? globalThis.fetch;
|
|
249
|
-
const response = await real(input, init);
|
|
250
|
-
const store = describeAls.getStore();
|
|
251
|
-
// Outside a describer call (no ALS store) or for bodiless responses: pass
|
|
252
|
-
// through untouched.
|
|
253
|
-
if (!store || !response.body) return response;
|
|
254
|
-
const [bodyForSdk, bodyForEnergy] = response.body.tee();
|
|
255
|
-
store.energyReader = readEnergyFromTee(bodyForEnergy);
|
|
256
|
-
return new Response(bodyForSdk, {
|
|
257
|
-
headers: response.headers,
|
|
258
|
-
status: response.status,
|
|
259
|
-
statusText: response.statusText,
|
|
260
|
-
});
|
|
261
|
-
}
|