model-compare-opencode-plugin 0.5.0 → 0.6.0
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 +8 -8
- package/dist/tui.js +18 -6
- package/package.json +1 -1
- package/src/core/catalog.ts +98 -16
- package/src/core/reasoning.ts +96 -0
- package/src/core/steering.ts +36 -0
- package/src/core/types.ts +34 -1
- package/src/index.ts +41 -14
- package/src/opencode/aliases.ts +19 -5
- package/src/opencode/commands.ts +41 -3
- package/src/opencode/guard.ts +4 -2
- package/src/opencode/usebest.ts +13 -3
- package/src/rpc.ts +30 -2
- package/src/tui.tsx +24 -4
- package/vendor/UPSTREAM.json +3 -3
- package/vendor/model_compare.py +2140 -430
package/src/opencode/aliases.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// is the applier's job (it reads them through editor.get), so this module never
|
|
6
6
|
// touches model internals.
|
|
7
7
|
|
|
8
|
+
import { formatDepthAlternative } from "../core/reasoning"
|
|
8
9
|
import type { ModelEntry, Priority } from "../core/types"
|
|
9
10
|
import { sanitizeCatalogText } from "./commands"
|
|
10
11
|
|
|
@@ -24,10 +25,17 @@ export type AliasPlan = {
|
|
|
24
25
|
// §8 rule: a missing pick model DISABLES the alias with a warning — it is never
|
|
25
26
|
// silently skipped. `model` is undefined when the pick's model was removed by
|
|
26
27
|
// another plugin, or when there is no catalog to look it up in.
|
|
28
|
+
//
|
|
29
|
+
// `displayModel` is the CATALOG entry for the same pick: the applier's
|
|
30
|
+
// presence proof is the editor's concrete model (which carries no ranking
|
|
31
|
+
// metadata), so the effort/alternative label reads the catalog entry instead.
|
|
32
|
+
// Absent (a fallback pick with no catalog, or a caller that has only the
|
|
33
|
+
// editor model) falls back to `model`.
|
|
27
34
|
export function planAlias(
|
|
28
35
|
p: Priority,
|
|
29
36
|
target: AliasTarget,
|
|
30
37
|
model: ModelEntry | undefined,
|
|
38
|
+
displayModel?: ModelEntry,
|
|
31
39
|
): AliasPlan {
|
|
32
40
|
if (target.id === null) {
|
|
33
41
|
return {
|
|
@@ -45,13 +53,19 @@ export function planAlias(
|
|
|
45
53
|
warnings: [`pick model ${target.id} missing from catalog for ${p}`],
|
|
46
54
|
}
|
|
47
55
|
}
|
|
56
|
+
// The effort the pick's score represents, followed by the best measured
|
|
57
|
+
// selectable alternative (catalog v5). Both are catalog-derived and
|
|
58
|
+
// flattened (F-20) for the model picker, the TUI status line and dialogs;
|
|
59
|
+
// `modelID` stays RAW — switching needs the real id.
|
|
60
|
+
const shown = displayModel ?? model
|
|
61
|
+
const effort =
|
|
62
|
+
shown.qualityEffort == null ? "" : ` · ${sanitizeCatalogText(shown.qualityEffort)}`
|
|
63
|
+
const alternative = formatDepthAlternative(shown)
|
|
48
64
|
return {
|
|
49
65
|
modelID: target.id,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// real id.
|
|
54
|
-
name: `Model Compare: ${p} → ${sanitizeCatalogText(target.id)}${target.fromFallback ? " (fallback)" : ""}`,
|
|
66
|
+
name: `Model Compare: ${p} → ${sanitizeCatalogText(target.id)}${effort}${
|
|
67
|
+
target.fromFallback ? " (fallback)" : ""
|
|
68
|
+
}${alternative === "" ? "" : ` [alt ${sanitizeCatalogText(alternative)}]`}`,
|
|
55
69
|
enabled: true,
|
|
56
70
|
warnings: [],
|
|
57
71
|
}
|
package/src/opencode/commands.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// not upstream's `generated_at`.
|
|
9
9
|
|
|
10
10
|
import { effectiveWeights, overallScore } from "../core/rank"
|
|
11
|
+
import { formatDepthAlternative } from "../core/reasoning"
|
|
11
12
|
import type { Catalog, EndpointCapture, ModelEntry, Priority, Sourced, Weights } from "../core/types"
|
|
12
13
|
import type { AliasTarget } from "./aliases"
|
|
13
14
|
|
|
@@ -141,6 +142,30 @@ function overrideInEffect(
|
|
|
141
142
|
return eff.applied && !eff.zeroSum ? eff.weights : null
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
// Catalog v5 display suffixes: the effort the quality score was measured at
|
|
146
|
+
// (` · high`) and the best measured selectable alternative (` · alt max 56.0`).
|
|
147
|
+
// Both are catalog-derived, so they are flattened before joining the report
|
|
148
|
+
// text / dialog (F-20). An absent effort/alternative adds nothing.
|
|
149
|
+
function effortLabel(model: ModelEntry | undefined): string {
|
|
150
|
+
const effort = model?.qualityEffort
|
|
151
|
+
return effort == null ? "" : sanitizeCatalogText(effort)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function alternativeLabel(model: ModelEntry | undefined): string {
|
|
155
|
+
const alternative = formatDepthAlternative(model)
|
|
156
|
+
return alternative === "" ? "" : sanitizeCatalogText(alternative)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function reasoningSuffix(model: ModelEntry | undefined): string {
|
|
160
|
+
const label = effortLabel(model)
|
|
161
|
+
return label === "" ? "" : ` · ${label}`
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function alternativeSuffix(model: ModelEntry | undefined): string {
|
|
165
|
+
const label = alternativeLabel(model)
|
|
166
|
+
return label === "" ? "" : ` · alt ${label}`
|
|
167
|
+
}
|
|
168
|
+
|
|
144
169
|
// §10: /model-compare's report. `ranks` arrives already ranked, top-first
|
|
145
170
|
// (under the same overrides, see ReportContext), and only the top 5 per
|
|
146
171
|
// priority are rendered; a ranked id absent from the catalog still takes its
|
|
@@ -175,18 +200,23 @@ export function formatReport(
|
|
|
175
200
|
model === undefined ? null : weights ? overallScore(model, weights) : model.scores.overall[p]
|
|
176
201
|
const score = value === null ? "-" : value.toFixed(3)
|
|
177
202
|
const marker = pick?.id === id ? " ← current pick" : ""
|
|
178
|
-
lines.push(
|
|
203
|
+
lines.push(
|
|
204
|
+
`${index + 1}. ${sanitizeCatalogText(id)} (${score})${reasoningSuffix(model)}${alternativeSuffix(model)}${marker}`,
|
|
205
|
+
)
|
|
179
206
|
}
|
|
180
207
|
// A pick that is not visible in the list above needs its own line.
|
|
181
208
|
if (pick !== undefined && (pick.id === null || !top.includes(pick.id))) {
|
|
209
|
+
const pickModel = pick.id === null ? undefined : byId.get(pick.id)
|
|
182
210
|
if (pick.id === null) {
|
|
183
211
|
lines.push("Current pick: none (alias disabled)")
|
|
184
212
|
} else if (pick.fromFallback) {
|
|
185
|
-
lines.push(
|
|
213
|
+
lines.push(
|
|
214
|
+
`Current pick: ${sanitizeCatalogText(pick.id)} (fallback)${reasoningSuffix(pickModel)}${alternativeSuffix(pickModel)}`,
|
|
215
|
+
)
|
|
186
216
|
} else {
|
|
187
217
|
const rank = ranks[p].indexOf(pick.id)
|
|
188
218
|
lines.push(
|
|
189
|
-
`Current pick: ${sanitizeCatalogText(pick.id)} (${rank === -1 ? "not in the ranking" : `rank ${rank + 1}`})`,
|
|
219
|
+
`Current pick: ${sanitizeCatalogText(pick.id)} (${rank === -1 ? "not in the ranking" : `rank ${rank + 1}`})${reasoningSuffix(pickModel)}${alternativeSuffix(pickModel)}`,
|
|
190
220
|
)
|
|
191
221
|
}
|
|
192
222
|
}
|
|
@@ -225,6 +255,10 @@ export type ReportEntry = {
|
|
|
225
255
|
value: string
|
|
226
256
|
score: string
|
|
227
257
|
quality: string
|
|
258
|
+
// The effort behind `quality` ("" when unknown) and the best selectable
|
|
259
|
+
// measured alternative as "<effort> <index>" ("" when there is none).
|
|
260
|
+
effort: string
|
|
261
|
+
alternative: string
|
|
228
262
|
price: string
|
|
229
263
|
current: boolean
|
|
230
264
|
fromFallback: boolean
|
|
@@ -279,6 +313,8 @@ export function reportEntries(
|
|
|
279
313
|
value: id,
|
|
280
314
|
score: scoreValue === null ? "-" : scoreValue.toFixed(3),
|
|
281
315
|
quality: fmtQuality(model),
|
|
316
|
+
effort: effortLabel(model),
|
|
317
|
+
alternative: alternativeLabel(model),
|
|
282
318
|
price: fmtPrice(model),
|
|
283
319
|
current,
|
|
284
320
|
// A top-5 row that IS the current pick carries the pick's fallback
|
|
@@ -302,6 +338,8 @@ export function reportEntries(
|
|
|
302
338
|
value: pick.id,
|
|
303
339
|
score: "-",
|
|
304
340
|
quality: fmtQuality(model),
|
|
341
|
+
effort: effortLabel(model),
|
|
342
|
+
alternative: alternativeLabel(model),
|
|
305
343
|
price: fmtPrice(model),
|
|
306
344
|
current: true,
|
|
307
345
|
fromFallback: pick.fromFallback,
|
package/src/opencode/guard.ts
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import type { Priority } from "../core/types"
|
|
12
12
|
|
|
13
|
-
// A model reference
|
|
14
|
-
|
|
13
|
+
// A model reference (agent config, session selection or request event). The
|
|
14
|
+
// optional `variant` is the `#variant` selection; alias identification stays
|
|
15
|
+
// variant-blind (it is not stable across binding styles).
|
|
16
|
+
export type ModelRef = { providerID: string; id: string; variant?: string }
|
|
15
17
|
|
|
16
18
|
const ALIAS_PREFIX = "model-compare/"
|
|
17
19
|
|
package/src/opencode/usebest.ts
CHANGED
|
@@ -80,12 +80,22 @@ export function readAliasViews(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
// The pick shown in an alias's display name:
|
|
83
|
-
// "Model Compare: <p> → <pick>[(
|
|
84
|
-
// for the "(unavailable)" placeholder names.
|
|
83
|
+
// "Model Compare: <p> → <pick>[ (fallback)]" → "<pick>[ (fallback)]", or null
|
|
84
|
+
// for the "(unavailable)" placeholder names. A v5 name appends the scored
|
|
85
|
+
// effort and the best alternative (" · high [alt max 56.0]") AFTER the pick
|
|
86
|
+
// (the fallback marker stays between them); those annotations are stripped so
|
|
87
|
+
// the status line and toasts still name the served model, not its depth.
|
|
85
88
|
export function pickFromName(name: string): string | null {
|
|
86
89
|
const arrow = name.indexOf("→")
|
|
87
90
|
if (arrow === -1) return null
|
|
88
|
-
|
|
91
|
+
const rest = name.slice(arrow + 1).replace(/ \[alt .*\]$/, "")
|
|
92
|
+
// Our name builder separates the pick from its v5 annotations with " · ";
|
|
93
|
+
// OpenRouter ids contain no spaces, so the first segment is the pick. The
|
|
94
|
+
// fallback marker lives in the annotation segment (or already after a pick
|
|
95
|
+
// with no annotations), so carry it over exactly once.
|
|
96
|
+
const [pick, ...annotations] = rest.split(" · ")
|
|
97
|
+
const fallback = annotations.join(" ").includes("(fallback)") ? " (fallback)" : ""
|
|
98
|
+
return `${pick.trim()}${fallback}`
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
// The pick an ENABLED alias view serves, or null when nothing observable is
|
package/src/rpc.ts
CHANGED
|
@@ -54,11 +54,25 @@ export const ModelCompare = Rpc.define({
|
|
|
54
54
|
value: { type: "string" },
|
|
55
55
|
score: { type: "string" },
|
|
56
56
|
quality: { type: "string" },
|
|
57
|
+
effort: { type: "string" },
|
|
58
|
+
alternative: { type: "string" },
|
|
57
59
|
price: { type: "string" },
|
|
58
60
|
current: { type: "boolean" },
|
|
59
61
|
fromFallback: { type: "boolean" },
|
|
60
62
|
},
|
|
61
|
-
required: [
|
|
63
|
+
required: [
|
|
64
|
+
"priority",
|
|
65
|
+
"rank",
|
|
66
|
+
"id",
|
|
67
|
+
"value",
|
|
68
|
+
"score",
|
|
69
|
+
"quality",
|
|
70
|
+
"effort",
|
|
71
|
+
"alternative",
|
|
72
|
+
"price",
|
|
73
|
+
"current",
|
|
74
|
+
"fromFallback",
|
|
75
|
+
],
|
|
62
76
|
additionalProperties: false,
|
|
63
77
|
},
|
|
64
78
|
},
|
|
@@ -88,11 +102,25 @@ export const ModelCompare = Rpc.define({
|
|
|
88
102
|
value: { type: "string" },
|
|
89
103
|
score: { type: "string" },
|
|
90
104
|
quality: { type: "string" },
|
|
105
|
+
effort: { type: "string" },
|
|
106
|
+
alternative: { type: "string" },
|
|
91
107
|
price: { type: "string" },
|
|
92
108
|
current: { type: "boolean" },
|
|
93
109
|
fromFallback: { type: "boolean" },
|
|
94
110
|
},
|
|
95
|
-
required: [
|
|
111
|
+
required: [
|
|
112
|
+
"priority",
|
|
113
|
+
"rank",
|
|
114
|
+
"id",
|
|
115
|
+
"value",
|
|
116
|
+
"score",
|
|
117
|
+
"quality",
|
|
118
|
+
"effort",
|
|
119
|
+
"alternative",
|
|
120
|
+
"price",
|
|
121
|
+
"current",
|
|
122
|
+
"fromFallback",
|
|
123
|
+
],
|
|
96
124
|
additionalProperties: false,
|
|
97
125
|
},
|
|
98
126
|
},
|
package/src/tui.tsx
CHANGED
|
@@ -186,6 +186,8 @@ function toReportEntries(raw: unknown): ReportEntry[] {
|
|
|
186
186
|
value: entry.value,
|
|
187
187
|
score: typeof entry.score === "string" ? entry.score : "-",
|
|
188
188
|
quality: typeof entry.quality === "string" ? entry.quality : "-",
|
|
189
|
+
effort: typeof entry.effort === "string" ? entry.effort : "",
|
|
190
|
+
alternative: typeof entry.alternative === "string" ? entry.alternative : "",
|
|
189
191
|
price: typeof entry.price === "string" ? entry.price : "-",
|
|
190
192
|
current: entry.current === true,
|
|
191
193
|
fromFallback: entry.fromFallback === true,
|
|
@@ -726,8 +728,15 @@ export default Plugin.define({
|
|
|
726
728
|
const labelOf = new Map(data.entries.map((entry, index) => [entry, labels[index]]))
|
|
727
729
|
const colW = (values: string[], floor: number, cap: number): number =>
|
|
728
730
|
Math.min(cap, Math.max(floor, ...values.map((value) => value.length)))
|
|
729
|
-
|
|
730
|
-
|
|
731
|
+
// Quality is a one-decimal AA index (max "100.0"); the caps leave the
|
|
732
|
+
// 52-char row budget intact (see `fixed` below) while fitting real
|
|
733
|
+
// values: "minimal" (effort), "minimal 100.0" (alt), "10.00/100.00".
|
|
734
|
+
const qualW = colW(data.entries.map((entry) => entry.quality), "qual".length, 5)
|
|
735
|
+
// Catalog v5: the effort the quality was measured at and the best
|
|
736
|
+
// selectable measured alternative ("max 56.0"). A missing value is "-".
|
|
737
|
+
const effortW = colW(data.entries.map((entry) => entry.effort || "-"), "effort".length, 7)
|
|
738
|
+
const altW = colW(data.entries.map((entry) => entry.alternative || "-"), "alt".length, 13)
|
|
739
|
+
const priceW = colW(data.entries.map((entry) => entry.price), "$ in/out".length, 13)
|
|
731
740
|
// A cell wider than its clamped column is truncated to fit.
|
|
732
741
|
const cell = (value: string, width: number): string =>
|
|
733
742
|
value.length > width ? value.slice(0, width - 1) + "…" : value
|
|
@@ -736,8 +745,11 @@ export default Plugin.define({
|
|
|
736
745
|
// stays exactly 2 (padStart pushed each value right by its slack).
|
|
737
746
|
// The default dialog is 60 wide: 2 list insets + 6 row insets leave 52
|
|
738
747
|
// for row content; the header shares that budget (plus its 3-indent).
|
|
739
|
-
|
|
740
|
-
|
|
748
|
+
// Column caps keep the budget even for hostile strings: 5+7+13+13 plus
|
|
749
|
+
// the four 2-char paddings and the 2-char row indent = 46, leaving the
|
|
750
|
+
// name floor of 6 (the name column shrinks first for real content).
|
|
751
|
+
const fixed = 2 + qualW + 2 + effortW + 2 + altW + 2 + priceW
|
|
752
|
+
const nameW = Math.max(6, Math.min(52 - fixed, Math.max(...labels.map((label) => label.length))))
|
|
741
753
|
const trim = (label: string): string =>
|
|
742
754
|
label.length > nameW ? label.slice(0, nameW - 1) + "…" : label
|
|
743
755
|
const headerLine =
|
|
@@ -746,6 +758,10 @@ export default Plugin.define({
|
|
|
746
758
|
" " +
|
|
747
759
|
"qual".padStart(qualW) +
|
|
748
760
|
" " +
|
|
761
|
+
"effort".padStart(effortW) +
|
|
762
|
+
" " +
|
|
763
|
+
"alt".padEnd(altW) +
|
|
764
|
+
" " +
|
|
749
765
|
"$ in/out".padEnd(priceW)
|
|
750
766
|
const rows: ListRow[] = [{ label: "", headerLine }]
|
|
751
767
|
for (const priority of PRIORITIES) {
|
|
@@ -759,6 +775,10 @@ export default Plugin.define({
|
|
|
759
775
|
" " +
|
|
760
776
|
cell(entry.quality, qualW).padStart(qualW) +
|
|
761
777
|
" " +
|
|
778
|
+
cell(entry.effort || "-", effortW).padStart(effortW) +
|
|
779
|
+
" " +
|
|
780
|
+
cell(entry.alternative || "-", altW).padEnd(altW) +
|
|
781
|
+
" " +
|
|
762
782
|
cell(entry.price, priceW).padEnd(priceW),
|
|
763
783
|
// F-20 id split: the row DISPLAYS the sanitized id (in `label`)
|
|
764
784
|
// but picks `value`, the raw catalog id switchToModel needs.
|
package/vendor/UPSTREAM.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repo": "canonical/model-compare",
|
|
3
|
-
"tag": "v0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"sha256": "
|
|
3
|
+
"tag": "v0.6.0",
|
|
4
|
+
"commit": "2f39ed320ca87703b1a91a6a89b3ac796c5212ae",
|
|
5
|
+
"sha256": "94627727c2c5ef0e5d2f7b1753952aaa02d375a4fb2bd35c82124891a5b29830"
|
|
6
6
|
}
|