pi-vision-handoff 0.10.6 → 0.10.8

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
@@ -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 1.0 compatibility (0.10.8)
20
+
21
+ Tested with Pi **1.0.0**. Host-provided Pi packages and TypeBox are peers (`*`), not bundled runtime dependencies; development uses exact Pi 1.0.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 `PI1_HOST_PACKAGE` to an installed Pi package directory to test that host explicitly; add `PI1_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.6",
3
+ "version": "0.10.8",
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/pi1-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.87.0",
42
- "@earendil-works/pi-coding-agent": "0.87.0",
43
- "@earendil-works/pi-tui": "0.87.0",
42
+ "@earendil-works/pi-ai": "1.0.0",
43
+ "@earendil-works/pi-coding-agent": "1.0.0",
44
+ "@earendil-works/pi-tui": "1.0.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 1.0'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 savedRealFetch: typeof globalThis.fetch | null = null;
221
+ let installation: { previous: typeof globalThis.fetch; wrapper: typeof globalThis.fetch; active: boolean } | undefined;
222
222
 
223
- /** Install the globalThis.fetch interceptor. Refcounted: the first caller
224
- * patches fetch; later callers just bump the count. Idempotent per install. */
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
- savedRealFetch = globalThis.fetch;
228
- globalThis.fetch = interceptedFetch;
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 && savedRealFetch) {
237
- globalThis.fetch = savedRealFetch;
238
- savedRealFetch = null;
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 the interceptor is not installed. Test hook. */
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
- }