pi-vision-guard 0.2.0 → 0.2.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - Ranking tries each registered classifier in order until one answers: a keyless or failing first pick (e.g. a built-in cloud entry with no API key) no longer blocks a working backend later in the list. Recency fallback now only fires when no classifier answers at all.
6
+
3
7
  ## 0.2.0
4
8
 
5
9
  - Ranking uses the first available registered classifier with no built-in model preference (previously preferred a local clef provider); fully backend-agnostic, pairs with `pi-local-classifier` for offline use.
package/README.md CHANGED
@@ -32,7 +32,7 @@ Three layers, all automatic:
32
32
 
33
33
  ## Classifier ranking
34
34
 
35
- Ranking lights up automatically with the first available registered Pi classifier — no settings. The questions are plain keep/drop judgments, so any Jev-family decision model works. With no classifier present, the guard silently uses newest-first order. Pairs with `pi-local-classifier` for fully offline ranking with no API key.
35
+ Ranking lights up automatically with your registered Pi classifiers — no settings. Each is tried in order until one answers, so a keyless cloud entry never blocks a working local backend. The questions are plain keep/drop judgments, so any Jev-family decision model works. With no classifier present, the guard silently uses newest-first order. Pairs with `pi-local-classifier` for fully offline ranking with no API key.
36
36
 
37
37
  ## ⚠️ Cache-churn caveat
38
38
 
@@ -155,14 +155,19 @@ function lastUserText(messages: any[]): string {
155
155
  return "(unknown)";
156
156
  }
157
157
 
158
- /** One batched classifier call scoring every candidate. Returns scores aligned with slots, or undefined on any failure. */
158
+ /**
159
+ * One batched classifier call scoring every candidate. Tries each registered
160
+ * classifier in order until one returns usable scores — a keyless or failing
161
+ * first pick must not block a working backend later in the list. Returns
162
+ * scores aligned with slots, or undefined when none answer. Abort still throws.
163
+ */
159
164
  async function scoreKeepers(
160
165
  slots: ImageSlot[],
161
166
  taskHint: string,
162
167
  ctx: ExtensionContext,
163
168
  ): Promise<number[] | undefined> {
164
- const clf = ctx.modelRegistry.getModelsOfType("classifier")[0];
165
- if (!clf) return undefined;
169
+ const models = ctx.modelRegistry.getModelsOfType("classifier");
170
+ if (models.length === 0) return undefined;
166
171
  const questions: Record<string, ClassifierQuestion> = {};
167
172
  for (let i = 0; i < slots.length; i++) {
168
173
  questions[`keep_${i}`] = {
@@ -174,44 +179,51 @@ async function scoreKeepers(
174
179
  },
175
180
  };
176
181
  }
177
- try {
178
- const result = await ctx.modelRegistry.classify(
179
- clf,
180
- {
181
- state: {
182
- task: taskHint,
183
- images: slots.map((s, i) => ({
184
- index: i,
185
- role: s.role,
186
- context: s.text.slice(0, 300) || "(no surrounding text)",
187
- })),
182
+ for (const clf of models) {
183
+ if (ctx.signal?.aborted) throw new Error("aborted");
184
+ try {
185
+ const result = await ctx.modelRegistry.classify(
186
+ clf,
187
+ {
188
+ state: {
189
+ task: taskHint,
190
+ images: slots.map((s, i) => ({
191
+ index: i,
192
+ role: s.role,
193
+ context: s.text.slice(0, 300) || "(no surrounding text)",
194
+ })),
195
+ },
196
+ questions,
188
197
  },
189
- questions,
190
- },
191
- { signal: ctx.signal },
192
- );
193
- if (result.stopReason !== "stop") {
198
+ { signal: ctx.signal },
199
+ );
194
200
  if (result.stopReason === "aborted" || ctx.signal?.aborted) throw new Error("aborted");
195
- return undefined;
196
- }
197
- const scores: number[] = [];
198
- for (let i = 0; i < slots.length; i++) {
199
- const a = result.answers[`keep_${i}`];
200
- if (a?.type !== "bool") return undefined;
201
- scores.push(a.probability);
201
+ if (result.stopReason !== "stop") continue;
202
+ const scores: number[] = [];
203
+ let usable = true;
204
+ for (let i = 0; i < slots.length; i++) {
205
+ const a = result.answers[`keep_${i}`];
206
+ if (a?.type !== "bool") {
207
+ usable = false;
208
+ break;
209
+ }
210
+ scores.push(a.probability);
211
+ }
212
+ if (!usable) continue;
213
+ return scores;
214
+ } catch (e) {
215
+ if (ctx.signal?.aborted) throw e;
216
+ continue;
202
217
  }
203
- return scores;
204
- } catch (e) {
205
- if (ctx.signal?.aborted) throw e;
206
- return undefined;
207
218
  }
219
+ return undefined;
208
220
  }
209
221
 
210
222
  /**
211
223
  * Relevance-ranked prune within the same budget the recency guard used.
212
224
  * Newest image is always kept; remaining slots go to the highest classifier
213
225
  * keep-scores, falling back to recency when there is no ranking signal
214
- * (bare images) or the classifier is unavailable.
226
+ * (bare images) or no classifier answers.
215
227
  */
216
228
  async function pruneRanked(
217
229
  messages: any[],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-vision-guard",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Pi extension that caps, shrinks, and relevance-ranks image payloads so long vision sessions stop 413ing",
5
5
  "author": "Jesse Wise",
6
6
  "license": "MIT",