pi-vision-handoff 0.9.3 → 0.9.5
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/package.json +4 -4
- package/src/dataloader.ts +45 -2
- package/src/index.ts +14 -0
- package/vision-handoff.ts +13 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-vision-handoff",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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.84.1",
|
|
35
|
+
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
36
|
+
"@earendil-works/pi-tui": "0.84.1",
|
|
37
37
|
"@types/node": "25.9.1",
|
|
38
38
|
"@vitest/coverage-v8": "4.1.7",
|
|
39
39
|
"knip": "6.14.1",
|
package/src/dataloader.ts
CHANGED
|
@@ -154,8 +154,23 @@ export class DescriptionLoader implements Disposable {
|
|
|
154
154
|
* prewarm batch be aborted the moment the live signal arrives. */
|
|
155
155
|
bindTurnContext(ctx: { modelRegistry: ModelRegistry; signal?: AbortSignal }): void {
|
|
156
156
|
this.turnModelRegistry = ctx.modelRegistry;
|
|
157
|
-
const
|
|
158
|
-
|
|
157
|
+
const cfg = this.deps.getConfig();
|
|
158
|
+
const resolved = this.deps.resolveVisionModel(ctx.modelRegistry, cfg.visionModel!);
|
|
159
|
+
if (resolved) {
|
|
160
|
+
this.turnVisionModel = resolved;
|
|
161
|
+
} else {
|
|
162
|
+
// Primary unresolvable (typo, removed model): pick the first resolvable
|
|
163
|
+
// fallback so failover still works instead of leaving the loader dead.
|
|
164
|
+
// Call-time failover in dispatchBatch covers the case where the primary
|
|
165
|
+
// RESOLVES but fails at call time.
|
|
166
|
+
for (const ref of cfg.fallbackModels ?? []) {
|
|
167
|
+
const fb = this.deps.resolveVisionModel(ctx.modelRegistry, ref);
|
|
168
|
+
if (fb) {
|
|
169
|
+
this.turnVisionModel = fb;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
159
174
|
const signal = ctx.signal;
|
|
160
175
|
if (!signal) return;
|
|
161
176
|
if (signal.aborted) {
|
|
@@ -314,6 +329,34 @@ export class DescriptionLoader implements Disposable {
|
|
|
314
329
|
);
|
|
315
330
|
}
|
|
316
331
|
}
|
|
332
|
+
// Failover: the same-model retry failed too. Try each configured fallback
|
|
333
|
+
// describer in order (a different provider has independent rate limits /
|
|
334
|
+
// auth / availability). No per-fallback backoff — the fallback doesn't
|
|
335
|
+
// share the primary's rate-limit state. A partial result from any
|
|
336
|
+
// fallback wins; only when every fallback also returns empty do we give
|
|
337
|
+
// up (UNAVAILABLE, evicted from cache, re-attempted next turn). User
|
|
338
|
+
// cancel (ESC) skips failover.
|
|
339
|
+
if (parsed.size === 0 && misses.length > 0 && !this.turnAbortController.signal.aborted) {
|
|
340
|
+
for (const ref of cfg.fallbackModels ?? []) {
|
|
341
|
+
if (this.turnAbortController.signal.aborted) break;
|
|
342
|
+
const fallbackModel = this.deps.resolveVisionModel(this.turnModelRegistry, ref);
|
|
343
|
+
if (!fallbackModel) {
|
|
344
|
+
this.deps.setLastError(`fallback vision model "${ref}" could not be resolved`);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
this.deps.setLastError(null);
|
|
348
|
+
parsed = await runBatch(
|
|
349
|
+
misses,
|
|
350
|
+
this.pendingTurnPrompt,
|
|
351
|
+
fallbackModel,
|
|
352
|
+
this.turnModelRegistry,
|
|
353
|
+
cfg,
|
|
354
|
+
this.deps,
|
|
355
|
+
this.turnAbortController.signal,
|
|
356
|
+
);
|
|
357
|
+
if (parsed.size > 0 || this.turnAbortController.signal.aborted) break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
317
360
|
// Build per-hash results, then fan them out to every callback. This reaches
|
|
318
361
|
// duplicate-hash callbacks that a key-indexed loop would have skipped (and
|
|
319
362
|
// left hanging).
|
package/src/index.ts
CHANGED
|
@@ -133,6 +133,13 @@ export interface VisionHandoffConfig {
|
|
|
133
133
|
enabled: boolean;
|
|
134
134
|
/** The vision-capable model that describes images, as "provider/id". null = not configured. */
|
|
135
135
|
visionModel: string | null;
|
|
136
|
+
/** Optional fallback describers, tried in order when the primary vision
|
|
137
|
+
* model's batched call fails entirely (auth, rate limit, timeout, network,
|
|
138
|
+
* empty/error stopReason) and the same-model retry didn't recover.
|
|
139
|
+
* Each entry is a "provider/id" ref; providers usually differ from the
|
|
140
|
+
* primary so their rate limits / auth / availability are independent.
|
|
141
|
+
* Empty (default) = no failover. */
|
|
142
|
+
fallbackModels: string[];
|
|
136
143
|
/** When true (default), handoff is applied to every model whose input does not include "image". */
|
|
137
144
|
autoHandoff: boolean;
|
|
138
145
|
/** Extra "provider/id" refs that should ALSO receive handoff (e.g. weak vision models). */
|
|
@@ -188,6 +195,7 @@ export interface VisionHandoffConfig {
|
|
|
188
195
|
export const DEFAULT_CONFIG: VisionHandoffConfig = {
|
|
189
196
|
enabled: true,
|
|
190
197
|
visionModel: null,
|
|
198
|
+
fallbackModels: [],
|
|
191
199
|
autoHandoff: true,
|
|
192
200
|
handoffModels: [],
|
|
193
201
|
prewarmPastedImages: false,
|
|
@@ -233,6 +241,12 @@ export function normalizeConfig(raw: unknown): VisionHandoffConfig {
|
|
|
233
241
|
} else if (obj.visionModel === null) {
|
|
234
242
|
base.visionModel = null;
|
|
235
243
|
}
|
|
244
|
+
if (Array.isArray(obj.fallbackModels)) {
|
|
245
|
+
base.fallbackModels = obj.fallbackModels
|
|
246
|
+
.filter((m): m is string => typeof m === "string")
|
|
247
|
+
.map((m) => m.trim())
|
|
248
|
+
.filter((m) => m && parseModelRef(m));
|
|
249
|
+
}
|
|
236
250
|
if (typeof obj.autoHandoff === "boolean") base.autoHandoff = obj.autoHandoff;
|
|
237
251
|
|
|
238
252
|
if (Array.isArray(obj.handoffModels)) {
|
package/vision-handoff.ts
CHANGED
|
@@ -206,6 +206,16 @@ function resolveVisionModel(modelRegistry: ModelRegistry, ref: string): Model<Ap
|
|
|
206
206
|
return model;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
/** True when the primary vision model resolves, or — failing that — at least
|
|
210
|
+
* one configured fallback resolves. A broken primary ref (typo, removed
|
|
211
|
+
* model) must not short-circuit the whole pipeline to "unresolved" warnings
|
|
212
|
+
* when a fallback could still describe images; the loader's failover picks
|
|
213
|
+
* the resolvable fallback up in bindTurnContext. */
|
|
214
|
+
function isAnyVisionModelResolvable(modelRegistry: ModelRegistry, config: VisionHandoffConfig): boolean {
|
|
215
|
+
if (resolveVisionModel(modelRegistry, config.visionModel!)) return true;
|
|
216
|
+
return config.fallbackModels.some((ref) => resolveVisionModel(modelRegistry, ref));
|
|
217
|
+
}
|
|
218
|
+
|
|
209
219
|
const loaderDeps: LoaderDeps = {
|
|
210
220
|
getConfig: () => config,
|
|
211
221
|
resolveVisionModel,
|
|
@@ -415,7 +425,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
415
425
|
loader.setPendingTurnPrompt(event.prompt || "");
|
|
416
426
|
loader.bindTurnContext(ctx);
|
|
417
427
|
|
|
418
|
-
if (!
|
|
428
|
+
if (!isAnyVisionModelResolvable(ctx.modelRegistry, config)) {
|
|
419
429
|
notifyUnresolvedVisionModel(ctx, config.visionModel!);
|
|
420
430
|
return;
|
|
421
431
|
}
|
|
@@ -575,7 +585,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
575
585
|
|
|
576
586
|
if (imgs.length === 0) return;
|
|
577
587
|
loader.bindTurnContext(ctx);
|
|
578
|
-
if (!
|
|
588
|
+
if (!isAnyVisionModelResolvable(ctx.modelRegistry, config)) {
|
|
579
589
|
notifyUnresolvedVisionModel(ctx, config.visionModel!);
|
|
580
590
|
return;
|
|
581
591
|
}
|
|
@@ -667,7 +677,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
667
677
|
if (!anyImage) return;
|
|
668
678
|
if (!isHandoffTarget(ctx.model, config)) return;
|
|
669
679
|
loader.bindTurnContext(ctx);
|
|
670
|
-
if (!
|
|
680
|
+
if (!isAnyVisionModelResolvable(ctx.modelRegistry, config)) {
|
|
671
681
|
notifyUnresolvedVisionModel(ctx, config.visionModel!);
|
|
672
682
|
return;
|
|
673
683
|
}
|