Skip to content

Commit 6df0d5d

Browse files
authored
fix(stats): use capability fallbacks in comparison radar (anomalyco#51161)
1 parent 0f54984 commit 6df0d5d

5 files changed

Lines changed: 164 additions & 39 deletions

File tree

‎.github/actions/setup-bun/action.yml‎

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ runs:
1515
with:
1616
node-version: "24"
1717

18-
- name: Get baseline download URL
18+
- name: Get Bun version and baseline download URL
1919
id: bun-url
2020
shell: bash
2121
run: |
22+
V=$(node -p "require('./package.json').packageManager.split('@')[1]")
23+
# Bun 1.4.2 includes the patched peer-variant fix in oven-sh/bun#33646.
24+
if [ "$RUNNER_OS" = "Windows" ]; then V=1.4.2; fi
25+
echo "version=$V" >> "$GITHUB_OUTPUT"
2226
if [ "$RUNNER_ARCH" = "X64" ]; then
23-
V=$(node -p "require('./package.json').packageManager.split('@')[1]")
2427
case "$RUNNER_OS" in
2528
macOS) OS=darwin ;;
2629
Linux) OS=linux ;;
@@ -30,9 +33,10 @@ runs:
3033
fi
3134
3235
- name: Setup Bun
36+
id: setup-bun
3337
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
3438
with:
35-
bun-version-file: ${{ !steps.bun-url.outputs.url && 'package.json' || '' }}
39+
bun-version: ${{ !steps.bun-url.outputs.url && steps.bun-url.outputs.version || '' }}
3640
bun-download-url: ${{ steps.bun-url.outputs.url }}
3741

3842
- name: Get cache directory
@@ -45,29 +49,21 @@ runs:
4549
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
4650
with:
4751
path: ${{ steps.cache.outputs.dir }}
48-
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
52+
key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-${{ hashFiles('**/bun.lock') }}
4953
restore-keys: |
50-
${{ runner.os }}-bun-
54+
${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-
5155
5256
- name: Install setuptools for distutils compatibility
5357
run: python3 -m pip install setuptools || pip install setuptools || true
5458
shell: bash
5559

5660
- name: Install dependencies
57-
run: |
58-
# Workaround for patched peer variants
59-
# e.g. ./patches/ for standard-openapi
60-
# https://github.com/oven-sh/bun/issues/28147
61-
if [ "$RUNNER_OS" = "Windows" ]; then
62-
bun install --linker hoisted ${{ inputs.install-flags }}
63-
else
64-
bun install ${{ inputs.install-flags }}
65-
fi
61+
run: bun install ${{ inputs.install-flags }}
6662
shell: bash
6763

6864
- name: Save Bun dependencies
6965
if: steps.bun-cache.outputs.cache-hit != 'true' && github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
7066
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
7167
with:
7268
path: ${{ steps.cache.outputs.dir }}
73-
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
69+
key: ${{ runner.os }}-bun-${{ steps.setup-bun.outputs.bun-version }}-${{ hashFiles('**/bun.lock') }}

‎packages/stats/app/src/routes/compare-radar.tsx‎

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ type RadarAxis = {
2222
label: string
2323
description: string
2424
score: (model: ModelCatalogEntry) => number | undefined
25+
capability?: "reasoning" | "toolCall"
26+
}
27+
28+
type RadarScore = {
29+
value: number
30+
fallback?: string
2531
}
2632

2733
type RadarPoint = {
@@ -37,7 +43,7 @@ export function ComparisonRadar(props: ComparisonRadarProps) {
3743
name: model.name,
3844
labName: model.labName,
3945
color: radarColors[index % radarColors.length],
40-
scores: axes().map((axis) => (model.catalog ? axis.score(model.catalog) : undefined)),
46+
scores: axes().map((axis) => resolveRadarScore(axis, model.catalog)),
4147
})),
4248
)
4349
const accessibleDescription = createMemo(() =>
@@ -62,6 +68,9 @@ export function ComparisonRadar(props: ComparisonRadarProps) {
6268
<span>
6369
<strong>{model.name}</strong>
6470
<Show when={model.labName}>{(name) => <small>{name()}</small>}</Show>
71+
<Show when={model.scores.some((score) => score.fallback)}>
72+
<small data-slot="compare-radar-coverage">Hollow points use fallbacks · hover for details</small>
73+
</Show>
6574
</span>
6675
</li>
6776
)}
@@ -89,10 +98,16 @@ export function ComparisonRadar(props: ComparisonRadarProps) {
8998
<polygon data-slot="compare-radar-area" points={radarSeriesPolygon(model.scores)} />
9099
<For each={model.scores}>
91100
{(score, index) => {
92-
const point = () => radarPoint(index(), axes().length, score ?? 0)
101+
const point = () => radarPoint(index(), axes().length, score.value)
93102
return (
94103
<>
95-
<circle data-slot="compare-radar-point" cx={point().x} cy={point().y} r="0.95" />
104+
<circle
105+
data-slot="compare-radar-point"
106+
data-fallback={score.fallback ? "true" : undefined}
107+
cx={point().x}
108+
cy={point().y}
109+
r="0.95"
110+
/>
96111
<circle
97112
data-slot="compare-radar-point-hit"
98113
cx={point().x}
@@ -138,6 +153,13 @@ export function ComparisonRadar(props: ComparisonRadarProps) {
138153
>
139154
<strong>{axes()[activeAxis() ?? 0]?.label}</strong>
140155
<p>{axes()[activeAxis() ?? 0]?.description}</p>
156+
<For each={series()}>
157+
{(model) => (
158+
<p>
159+
{model.name}: {formatRadarScore(model.scores[activeAxis() ?? 0])}
160+
</p>
161+
)}
162+
</For>
141163
</div>
142164
</Show>
143165
</div>
@@ -166,7 +188,7 @@ export function ComparisonRadar(props: ComparisonRadarProps) {
166188
)
167189
}
168190

169-
function buildRadarAxes(catalogModels: readonly ModelCatalogEntry[]): RadarAxis[] {
191+
export function buildRadarAxes(catalogModels: readonly ModelCatalogEntry[]): RadarAxis[] {
170192
const benchmarks = benchmarkScoreGroups(catalogModels)
171193
const toolUseBenchmarks = benchmarkScoreGroups(catalogModels, true)
172194
const costs = catalogModels.flatMap((model) => {
@@ -180,9 +202,10 @@ function buildRadarAxes(catalogModels: readonly ModelCatalogEntry[]): RadarAxis[
180202
return [
181203
{
182204
label: "Reasoning",
183-
description: "Ability to solve complex, multi-step problems. Based on reasoning benchmarks when available.",
184-
score: (model) =>
185-
benchmarkPercentile(model, benchmarks, reasoningBenchmarkPattern) ?? (model.reasoning ? 100 : 0),
205+
capability: "reasoning",
206+
description:
207+
"Ability to solve complex, multi-step problems. Benchmarks take priority; reasoning support defaults to 50/100.",
208+
score: (model) => benchmarkPercentile(model, benchmarks, reasoningBenchmarkPattern),
186209
},
187210
{
188211
label: "Coding",
@@ -218,7 +241,8 @@ function buildRadarAxes(catalogModels: readonly ModelCatalogEntry[]): RadarAxis[
218241
},
219242
{
220243
label: "Tool use",
221-
description: "Performance on agent benchmarks including Terminal-Bench, Tau3, and Claw-Eval.",
244+
capability: "toolCall",
245+
description: "Agent benchmark performance. Benchmarks take priority; tool calling support defaults to 50/100.",
222246
score: (model) =>
223247
benchmarkPercentile(model, toolUseBenchmarks, toolUseBenchmarkPattern, {
224248
aggregate: "average",
@@ -324,9 +348,9 @@ function radarPolygonPoints(count: number, score: number) {
324348
.join(" ")
325349
}
326350

327-
function radarSeriesPolygon(scores: (number | undefined)[]) {
351+
function radarSeriesPolygon(scores: RadarScore[]) {
328352
return scores
329-
.map((score, index) => radarPoint(index, scores.length, score ?? 0))
353+
.map((score, index) => radarPoint(index, scores.length, score.value))
330354
.map((point) => `${point.x},${point.y}`)
331355
.join(" ")
332356
}
@@ -355,6 +379,17 @@ function roundRadarCoordinate(value: number) {
355379
return Math.round(value * 1000) / 1000
356380
}
357381

358-
function formatRadarScore(score: number | undefined) {
359-
return score === undefined ? "No data" : `${Math.round(score)}/100`
382+
function formatRadarScore(score: RadarScore) {
383+
return `${Math.round(score.value)}/100${score.fallback ? ` — ${score.fallback}` : ""}`
384+
}
385+
386+
export function resolveRadarScore(axis: RadarAxis, model: ModelCatalogEntry | null): RadarScore {
387+
const score = model ? axis.score(model) : undefined
388+
if (score !== undefined) return { value: score }
389+
const supported = axis.capability ? model?.[axis.capability] : undefined
390+
if (supported === undefined) return { value: 50, fallback: "No data; neutral placeholder" }
391+
const capability = axis.capability === "toolCall" ? "Tool calling" : "Reasoning"
392+
return supported
393+
? { value: 50, fallback: `${capability} supported; no comparable benchmark` }
394+
: { value: 0, fallback: `${capability} not supported` }
360395
}

‎packages/stats/app/src/routes/index.css‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6291,8 +6291,7 @@ body {
62916291
vector-effect: non-scaling-stroke;
62926292
}
62936293

6294-
[data-page="stats"] [data-slot="compare-radar-area"],
6295-
[data-page="stats"] [data-slot="compare-radar-line"] {
6294+
[data-page="stats"] [data-slot="compare-radar-area"] {
62966295
stroke: currentColor;
62976296
stroke-width: 1.5px;
62986297
stroke-linejoin: round;
@@ -6304,17 +6303,17 @@ body {
63046303
fill-opacity: 0.09;
63056304
}
63066305

6307-
[data-page="stats"] [data-slot="compare-radar-line"] {
6308-
fill: none;
6309-
}
6310-
63116306
[data-page="stats"] [data-slot="compare-radar-point"] {
63126307
fill: currentColor;
63136308
stroke: currentColor;
63146309
stroke-width: 1px;
63156310
vector-effect: non-scaling-stroke;
63166311
}
63176312

6313+
[data-page="stats"] [data-slot="compare-radar-point"][data-fallback="true"] {
6314+
fill: var(--stats-bg);
6315+
}
6316+
63186317
[data-page="stats"] [data-slot="compare-radar-point-hit"] {
63196318
fill: transparent;
63206319
cursor: pointer;

‎packages/stats/app/src/routes/model-catalog.ts‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export type ModelCatalogEntry = {
2525
limit?: { context?: number; output?: number }
2626
modalities: { input: string[]; output: string[] }
2727
openWeights: boolean
28-
reasoning: boolean
29-
toolCall: boolean
28+
reasoning?: boolean
29+
toolCall?: boolean
3030
attachment: boolean
3131
temperature: boolean
3232
cost?: ModelCatalogCost
@@ -54,6 +54,7 @@ export type ModelCatalogLab = {
5454

5555
export type ModelCatalog = {
5656
models: ModelCatalogEntry[]
57+
aliases?: ModelCatalogEntry[]
5758
labs: ModelCatalogLab[]
5859
}
5960

@@ -80,7 +81,8 @@ export function findModelCatalogEntry(catalog: ModelCatalog, model: string, lab?
8081
return (
8182
catalog.models.find((entry) => entry.id.toLowerCase() === normalizedId) ??
8283
catalog.models.find((entry) => (lab ? entry.lab === catalogLabSlug(lab) : true) && entry.slug === leaf) ??
83-
catalog.models.find((entry) => entry.slug === leaf)
84+
catalog.models.find((entry) => entry.slug === leaf) ??
85+
catalog.aliases?.find((entry) => (lab ? entry.lab === catalogLabSlug(lab) : true) && entry.slug === leaf)
8486
)
8587
}
8688

@@ -133,7 +135,7 @@ export function catalogSlug(value: string) {
133135
.replace(/-{2,}/g, "-")
134136
}
135137

136-
function buildModelCatalog(payload: unknown, pricingPayload?: unknown, labPayload?: unknown): ModelCatalog {
138+
export function buildModelCatalog(payload: unknown, pricingPayload?: unknown, labPayload?: unknown): ModelCatalog {
137139
const costs = readCatalogCosts(pricingPayload)
138140
const labDescriptions = readCatalogLabDescriptions(payload, pricingPayload, labPayload)
139141
const models = readCatalogModels(payload)
@@ -149,6 +151,25 @@ function buildModelCatalog(payload: unknown, pricingPayload?: unknown, labPayloa
149151
.toSorted((a, b) => a.lab.localeCompare(b.lab) || displayDateTime(b.releaseDate) - displayDateTime(a.releaseDate))
150152
return {
151153
models,
154+
// Contributor is a serving tier of these Muse models, with its own pricing.
155+
// Keep aliases out of the model population used to normalize benchmark scores.
156+
aliases: ["meta/muse-spark-1.2", "meta/muse-spark-1.3"].flatMap((id) => {
157+
const model = models.find((entry) => entry.id === id)
158+
if (!model) return []
159+
const alias = `${id}-contributor`
160+
return [
161+
{
162+
...model,
163+
id: alias,
164+
slug: `${model.slug}-contributor`,
165+
name: `${model.name} Contributor`,
166+
cost:
167+
costs.get(catalogIdKey(alias)) ??
168+
costs.get(`${model.lab}/${model.slug}-contributor`) ??
169+
costs.get(`${model.slug}-contributor`),
170+
},
171+
]
172+
}),
152173
labs: Object.values(
153174
models.reduce<Record<string, ModelCatalogLab>>((result, model) => {
154175
result[model.lab] = {
@@ -184,8 +205,8 @@ function readModelCatalogEntry(value: unknown): ModelCatalogEntry[] {
184205
limit: readCatalogLimit(value.limit),
185206
modalities: readCatalogModalities(value.modalities),
186207
openWeights: booleanValue(value.open_weights),
187-
reasoning: booleanValue(value.reasoning),
188-
toolCall: booleanValue(value.tool_call),
208+
reasoning: typeof value.reasoning === "boolean" ? value.reasoning : undefined,
209+
toolCall: typeof value.tool_call === "boolean" ? value.tool_call : undefined,
189210
attachment: booleanValue(value.attachment),
190211
temperature: booleanValue(value.temperature),
191212
cost: readCatalogCost(value.cost),
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { buildRadarAxes, resolveRadarScore } from "../src/routes/compare-radar"
3+
import type { ModelCatalogEntry } from "../src/routes/model-catalog"
4+
5+
const model: ModelCatalogEntry = {
6+
id: "meta/muse-spark-1.3",
7+
lab: "meta",
8+
slug: "muse-spark-1-3",
9+
name: "Muse Spark 1.3",
10+
modalities: { input: ["text", "image"], output: ["text"] },
11+
reasoning: true,
12+
toolCall: true,
13+
openWeights: false,
14+
attachment: true,
15+
temperature: true,
16+
weights: [],
17+
benchmarks: [],
18+
}
19+
20+
function scores(entry: ModelCatalogEntry | null, catalog = [model]) {
21+
return Object.fromEntries(buildRadarAxes(catalog).map((axis) => [axis.label, resolveRadarScore(axis, entry)]))
22+
}
23+
24+
describe("radar capability fallbacks", () => {
25+
test("supported capabilities have visible baselines without benchmarks", () => {
26+
const result = scores(model)
27+
expect(result["Tool use"]).toEqual({ value: 50, fallback: "Tool calling supported; no comparable benchmark" })
28+
expect(result.Reasoning).toEqual({ value: 50, fallback: "Reasoning supported; no comparable benchmark" })
29+
expect(result.Coding).toEqual({ value: 50, fallback: "No data; neutral placeholder" })
30+
})
31+
32+
test("explicitly unsupported capabilities remain zero", () => {
33+
const result = scores({ ...model, reasoning: false, toolCall: false })
34+
expect(result["Tool use"]).toEqual({ value: 0, fallback: "Tool calling not supported" })
35+
expect(result.Reasoning).toEqual({ value: 0, fallback: "Reasoning not supported" })
36+
})
37+
38+
test("unknown capabilities and unmatched models use neutral placeholders", () => {
39+
const result = scores({ ...model, reasoning: undefined, toolCall: undefined })
40+
expect(result["Tool use"]).toEqual({ value: 50, fallback: "No data; neutral placeholder" })
41+
expect(result.Reasoning).toEqual({ value: 50, fallback: "No data; neutral placeholder" })
42+
expect(Object.values(scores(null))).toEqual(Array(6).fill({ value: 50, fallback: "No data; neutral placeholder" }))
43+
})
44+
45+
test("measured benchmark percentiles override fallbacks, including zero", () => {
46+
const low = {
47+
...model,
48+
benchmarks: [
49+
{ name: "Tau3", score: 20 },
50+
{ name: "GPQA", score: 40 },
51+
],
52+
}
53+
const high = {
54+
...model,
55+
id: "other/model",
56+
benchmarks: [
57+
{ name: "Tau3", score: 80 },
58+
{ name: "GPQA", score: 90 },
59+
],
60+
}
61+
expect(scores(low, [low, high])["Tool use"]).toEqual({ value: 0 })
62+
expect(scores(low, [low, high]).Reasoning).toEqual({ value: 0 })
63+
expect(scores(high, [low, high])["Tool use"]).toEqual({ value: 100 })
64+
expect(scores(high, [low, high]).Reasoning).toEqual({ value: 100 })
65+
})
66+
67+
test("a benchmark without comparison peers retains the capability baseline", () => {
68+
const entry = { ...model, benchmarks: [{ name: "Tau3", score: 90 }] }
69+
expect(scores(entry, [entry])["Tool use"]).toEqual({
70+
value: 50,
71+
fallback: "Tool calling supported; no comparable benchmark",
72+
})
73+
})
74+
})

0 commit comments

Comments
 (0)