@xynogen/pix-models 0.1.7 → 0.1.9
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 +3 -2
- package/src/models.test.ts +32 -0
- package/src/models.ts +79 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-models",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Pi extension — enhanced /models picker with BenchLM ranks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@xynogen/pix-data": "*"
|
|
41
|
+
"@xynogen/pix-data": "*",
|
|
42
|
+
"@xynogen/pix-pretty": "*"
|
|
42
43
|
},
|
|
43
44
|
"peerDependencies": {
|
|
44
45
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/models.test.ts
CHANGED
|
@@ -92,4 +92,36 @@ describe("sortModels", () => {
|
|
|
92
92
|
sortModels(models);
|
|
93
93
|
expect(models).toEqual(original);
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
it("puts null score models after all scored models regardless of source order", () => {
|
|
97
|
+
const shuffled = [
|
|
98
|
+
{ provider: "a", id: "m1", name: "Zeta", score: null },
|
|
99
|
+
{ provider: "a", id: "m2", name: "Beta", score: 80 },
|
|
100
|
+
{ provider: "a", id: "m3", name: "Alpha", score: 95 },
|
|
101
|
+
{ provider: "a", id: "m4", name: "Gamma", score: null },
|
|
102
|
+
{ provider: "a", id: "m5", name: "Delta", score: 60 },
|
|
103
|
+
];
|
|
104
|
+
const sorted = sortModels(shuffled);
|
|
105
|
+
const lastTwo = sorted.slice(-2).map((m) => m.name);
|
|
106
|
+
expect(lastTwo).toEqual(["Gamma", "Zeta"]); // nulls last, stable within
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("sinks tier 2 (off-catalog) below tier 1 (benched-but-unscored)", () => {
|
|
110
|
+
// Mirrors the openrouter/owl-alpha bug: a model with no bench entry at
|
|
111
|
+
// all must not interleave with benched models that happen to have a
|
|
112
|
+
// null score.
|
|
113
|
+
const mixed = [
|
|
114
|
+
{ provider: "a", id: "m1", name: "Alpha", score: 95, tier: 0 },
|
|
115
|
+
{ provider: "a", id: "m2", name: "Beta", score: null, tier: 1 },
|
|
116
|
+
{ provider: "a", id: "m3", name: "Gamma", score: undefined, tier: 2 },
|
|
117
|
+
{ provider: "a", id: "m4", name: "Delta", score: 60, tier: 0 },
|
|
118
|
+
];
|
|
119
|
+
const sorted = sortModels(mixed);
|
|
120
|
+
expect(sorted.map((m) => m.name)).toEqual([
|
|
121
|
+
"Alpha",
|
|
122
|
+
"Delta",
|
|
123
|
+
"Beta",
|
|
124
|
+
"Gamma",
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
95
127
|
});
|
package/src/models.ts
CHANGED
|
@@ -12,15 +12,12 @@ import type {
|
|
|
12
12
|
ExtensionAPI,
|
|
13
13
|
ExtensionContext,
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
16
15
|
import {
|
|
17
|
-
Container,
|
|
18
16
|
fuzzyFilter,
|
|
19
17
|
Input,
|
|
20
18
|
matchesKey,
|
|
21
19
|
type SelectItem,
|
|
22
20
|
SelectList,
|
|
23
|
-
Text,
|
|
24
21
|
visibleWidth,
|
|
25
22
|
} from "@earendil-works/pi-tui";
|
|
26
23
|
import {
|
|
@@ -28,6 +25,7 @@ import {
|
|
|
28
25
|
lookupBenchmark,
|
|
29
26
|
lookupModelsDev,
|
|
30
27
|
} from "@xynogen/pix-data";
|
|
28
|
+
import { frameLines, modalWidth } from "@xynogen/pix-pretty/modal-frame";
|
|
31
29
|
import { patchOutBuiltinModelCommand } from "./patch-builtin";
|
|
32
30
|
|
|
33
31
|
// ─── Pure logic (exported for tests) ─────────────────────────────────────────
|
|
@@ -71,13 +69,28 @@ export type SortableModel = {
|
|
|
71
69
|
id: string;
|
|
72
70
|
name?: string;
|
|
73
71
|
score?: number | null;
|
|
72
|
+
/**
|
|
73
|
+
* Sort tier. Lower = earlier. Default 0.
|
|
74
|
+
* 0 = scored (sort by `score` desc)
|
|
75
|
+
* 1 = benched but unscored (sort by name, between scored and off-catalog)
|
|
76
|
+
* 2 = off-catalog (no bench entry at all → always last)
|
|
77
|
+
* The off-catalog tier exists for models like `openrouter/owl-alpha` that
|
|
78
|
+
* are present in the router but missing from every benchmark source —
|
|
79
|
+
* they should never interleave with the benched-but-unscored tail.
|
|
80
|
+
*/
|
|
81
|
+
tier?: number;
|
|
74
82
|
};
|
|
75
83
|
|
|
76
84
|
export function sortModels<T extends SortableModel>(models: T[]): T[] {
|
|
77
85
|
return [...models].sort((a, b) => {
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
86
|
+
const ta = a.tier ?? 0;
|
|
87
|
+
const tb = b.tier ?? 0;
|
|
88
|
+
if (ta !== tb) return ta - tb;
|
|
89
|
+
if (ta === 0) {
|
|
90
|
+
const sa = a.score ?? -1;
|
|
91
|
+
const sb = b.score ?? -1;
|
|
92
|
+
if (sa !== sb) return sb - sa;
|
|
93
|
+
}
|
|
81
94
|
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
82
95
|
});
|
|
83
96
|
}
|
|
@@ -121,19 +134,35 @@ async function showEnhancedPicker(
|
|
|
121
134
|
bench: ReturnType<typeof lookupBenchmark>;
|
|
122
135
|
// Rank among the user's *available* models, not the global catalog.
|
|
123
136
|
localRank: number | null;
|
|
137
|
+
// Sort tier: 0 scored, 1 benched-but-unscored, 2 off-catalog.
|
|
138
|
+
tier: 0 | 1 | 2;
|
|
124
139
|
};
|
|
125
|
-
const rows: Row[] = available.map((m) =>
|
|
126
|
-
m
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
const rows: Row[] = available.map((m) => {
|
|
141
|
+
const bench = lookupBenchmark(m.id);
|
|
142
|
+
const tier = !bench
|
|
143
|
+
? 2 // off-catalog → absolute bottom (no rank)
|
|
144
|
+
: bench.overallScore == null
|
|
145
|
+
? 1 // benched, unscored → middle
|
|
146
|
+
: 0; // scored → top
|
|
147
|
+
return {
|
|
148
|
+
m,
|
|
149
|
+
dev: lookupModelsDev(m.provider, m.id),
|
|
150
|
+
bench,
|
|
151
|
+
localRank: null,
|
|
152
|
+
tier,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Mirror sortModels() — score-desc within tier 0, name-asc otherwise.
|
|
133
157
|
rows.sort((a, b) => {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
158
|
+
const ta = a.tier;
|
|
159
|
+
const tb = b.tier;
|
|
160
|
+
if (ta !== tb) return ta - tb;
|
|
161
|
+
if (ta === 0) {
|
|
162
|
+
const sa = a.bench?.overallScore ?? -1;
|
|
163
|
+
const sb = b.bench?.overallScore ?? -1;
|
|
164
|
+
if (sa !== sb) return sb - sa;
|
|
165
|
+
}
|
|
137
166
|
return (a.m.name ?? a.m.id).localeCompare(b.m.name ?? b.m.id);
|
|
138
167
|
});
|
|
139
168
|
|
|
@@ -148,7 +177,6 @@ async function showEnhancedPicker(
|
|
|
148
177
|
|
|
149
178
|
const result = await ctx.ui.custom<string | null>(
|
|
150
179
|
(_tui, theme, _kb, done) => {
|
|
151
|
-
const container = new Container();
|
|
152
180
|
const accent = "accent";
|
|
153
181
|
|
|
154
182
|
// Find max rank width across all benchmarked rows for # padding
|
|
@@ -174,7 +202,10 @@ async function showEnhancedPicker(
|
|
|
174
202
|
const isCurrent =
|
|
175
203
|
current && m.provider === current.provider && m.id === current.id;
|
|
176
204
|
|
|
177
|
-
// Label: marker +
|
|
205
|
+
// Label: marker + rank cell + accent-colored model name.
|
|
206
|
+
// Ranked models show muted '#' + colored rank. Unranked (no
|
|
207
|
+
// modelgrep entry) show a muted em-dash sized to the rank
|
|
208
|
+
// column, so the model name aligns across rows.
|
|
178
209
|
const marker = isCurrent ? theme.fg(accent, "▶") : " ";
|
|
179
210
|
let rankPrefix: string;
|
|
180
211
|
if (localRank) {
|
|
@@ -184,7 +215,9 @@ async function showEnhancedPicker(
|
|
|
184
215
|
const rankColor = benchScoreColor(bench?.overallScore);
|
|
185
216
|
rankPrefix = mute("#") + theme.fg(rankColor, rankStr);
|
|
186
217
|
} else {
|
|
187
|
-
|
|
218
|
+
// Width = "#" + maxRankWidth chars (e.g. "# " or "#——" for 2-digit ranks).
|
|
219
|
+
const dash = "—".padEnd(maxRankWidth, " ");
|
|
220
|
+
rankPrefix = mute("#") + mute(dash);
|
|
188
221
|
}
|
|
189
222
|
// Display model id only; m.provider is routing provider, not part of id.
|
|
190
223
|
const idColored = theme.fg(accent, m.id);
|
|
@@ -236,19 +269,6 @@ async function showEnhancedPicker(
|
|
|
236
269
|
)
|
|
237
270
|
: 0;
|
|
238
271
|
|
|
239
|
-
container.addChild(new DynamicBorder((s) => theme.fg(accent, s)));
|
|
240
|
-
container.addChild(
|
|
241
|
-
new Text(theme.fg(accent, theme.bold(" Select model"))),
|
|
242
|
-
);
|
|
243
|
-
container.addChild(
|
|
244
|
-
new Text(
|
|
245
|
-
theme.fg(
|
|
246
|
-
"dim",
|
|
247
|
-
"context · pricing · coding rank & score from modelgrep.com",
|
|
248
|
-
),
|
|
249
|
-
),
|
|
250
|
-
);
|
|
251
|
-
|
|
252
272
|
// Widest label (visible width, ANSI-stripped) so the model name
|
|
253
273
|
// column never truncates to "…". Add gap headroom.
|
|
254
274
|
const widestLabel = items.reduce(
|
|
@@ -317,25 +337,34 @@ async function showEnhancedPicker(
|
|
|
317
337
|
internal.invalidate();
|
|
318
338
|
};
|
|
319
339
|
|
|
320
|
-
container.addChild(new Text(theme.fg("muted", "Search:")));
|
|
321
|
-
container.addChild(search);
|
|
322
|
-
container.addChild(list);
|
|
323
|
-
container.addChild(
|
|
324
|
-
new Text(
|
|
325
|
-
theme.fg(
|
|
326
|
-
"dim",
|
|
327
|
-
"fuzzy search · ↑↓ navigate · enter select · esc cancel",
|
|
328
|
-
),
|
|
329
|
-
),
|
|
330
|
-
);
|
|
331
|
-
container.addChild(new DynamicBorder((s) => theme.fg(accent, s)));
|
|
332
|
-
|
|
333
340
|
return {
|
|
334
341
|
render(w: number) {
|
|
335
|
-
|
|
342
|
+
const mw = modalWidth(w);
|
|
343
|
+
const inner = mw - 4; // CHROME = 2 border + 2 padding
|
|
344
|
+
const lines: string[] = [
|
|
345
|
+
theme.fg(accent, theme.bold(" Select model")),
|
|
346
|
+
theme.fg(
|
|
347
|
+
"dim",
|
|
348
|
+
"context · pricing · coding rank & score from modelgrep.com",
|
|
349
|
+
),
|
|
350
|
+
theme.fg("muted", "Search:"),
|
|
351
|
+
...search.render(inner),
|
|
352
|
+
...list.render(inner),
|
|
353
|
+
theme.fg(
|
|
354
|
+
"dim",
|
|
355
|
+
"fuzzy search · ↑↓ navigate · enter select · esc cancel",
|
|
356
|
+
),
|
|
357
|
+
];
|
|
358
|
+
return frameLines({
|
|
359
|
+
width: mw,
|
|
360
|
+
lines,
|
|
361
|
+
color: (s) => theme.fg(accent, s),
|
|
362
|
+
bg: (s) => theme.bg("customMessageBg", s),
|
|
363
|
+
});
|
|
336
364
|
},
|
|
337
365
|
invalidate() {
|
|
338
|
-
|
|
366
|
+
list.invalidate();
|
|
367
|
+
search.invalidate();
|
|
339
368
|
},
|
|
340
369
|
handleInput(data: string) {
|
|
341
370
|
// Detect keys via pi-tui's own parser — the same recognition
|
|
@@ -350,10 +379,11 @@ async function showEnhancedPicker(
|
|
|
350
379
|
search.handleInput?.(data);
|
|
351
380
|
applyFuzzy(search.getValue?.() ?? "");
|
|
352
381
|
}
|
|
353
|
-
|
|
382
|
+
list.invalidate();
|
|
354
383
|
},
|
|
355
384
|
};
|
|
356
385
|
},
|
|
386
|
+
{ overlay: true },
|
|
357
387
|
);
|
|
358
388
|
|
|
359
389
|
if (!result) return;
|