@xynogen/pix-models 0.1.8 → 0.1.10
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 +1 -1
- package/package.json +1 -1
- package/src/models.test.ts +32 -0
- package/src/models.ts +52 -16
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Pi extension — enhanced `/models` picker with coding score/rank.
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
Registers a `/models` slash command that replaces Pi's built-in `/model` selector with a richer TUI picker. Each row shows model
|
|
7
|
+
Registers a `/models` slash command that replaces Pi's built-in `/model` selector with a richer TUI picker. Each row shows the model id, context window, per-million-token cost, and a coding-focused score/rank (with star bar) when available. The list is sorted by coding score (best first), then alphabetically for unscored models. Fuzzy search filters the list as you type. Selecting a model switches the active model for the session. Model metadata is sourced from `~/.cache/pi/` via `pix-data`; the coding score/rank is computed locally from the modelgrep catalog (best = #1). Requires `@xynogen/pix-data` as a dependency.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
package/package.json
CHANGED
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
|
@@ -69,13 +69,28 @@ export type SortableModel = {
|
|
|
69
69
|
id: string;
|
|
70
70
|
name?: string;
|
|
71
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;
|
|
72
82
|
};
|
|
73
83
|
|
|
74
84
|
export function sortModels<T extends SortableModel>(models: T[]): T[] {
|
|
75
85
|
return [...models].sort((a, b) => {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
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
|
+
}
|
|
79
94
|
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
80
95
|
});
|
|
81
96
|
}
|
|
@@ -119,19 +134,35 @@ async function showEnhancedPicker(
|
|
|
119
134
|
bench: ReturnType<typeof lookupBenchmark>;
|
|
120
135
|
// Rank among the user's *available* models, not the global catalog.
|
|
121
136
|
localRank: number | null;
|
|
137
|
+
// Sort tier: 0 scored, 1 benched-but-unscored, 2 off-catalog.
|
|
138
|
+
tier: 0 | 1 | 2;
|
|
122
139
|
};
|
|
123
|
-
const rows: Row[] = available.map((m) =>
|
|
124
|
-
m
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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.
|
|
131
157
|
rows.sort((a, b) => {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
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
|
+
}
|
|
135
166
|
return (a.m.name ?? a.m.id).localeCompare(b.m.name ?? b.m.id);
|
|
136
167
|
});
|
|
137
168
|
|
|
@@ -171,7 +202,10 @@ async function showEnhancedPicker(
|
|
|
171
202
|
const isCurrent =
|
|
172
203
|
current && m.provider === current.provider && m.id === current.id;
|
|
173
204
|
|
|
174
|
-
// 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.
|
|
175
209
|
const marker = isCurrent ? theme.fg(accent, "▶") : " ";
|
|
176
210
|
let rankPrefix: string;
|
|
177
211
|
if (localRank) {
|
|
@@ -181,7 +215,9 @@ async function showEnhancedPicker(
|
|
|
181
215
|
const rankColor = benchScoreColor(bench?.overallScore);
|
|
182
216
|
rankPrefix = mute("#") + theme.fg(rankColor, rankStr);
|
|
183
217
|
} else {
|
|
184
|
-
|
|
218
|
+
// Width = "#" + maxRankWidth chars (e.g. "# " or "#——" for 2-digit ranks).
|
|
219
|
+
const dash = "—".padEnd(maxRankWidth, " ");
|
|
220
|
+
rankPrefix = mute("#") + mute(dash);
|
|
185
221
|
}
|
|
186
222
|
// Display model id only; m.provider is routing provider, not part of id.
|
|
187
223
|
const idColored = theme.fg(accent, m.id);
|