Spaces:
Running
Running
Fix embeds to filter by canonical benchmark only (no splits in embed until we decide how to display splits) + suppress popup on embed routes
Browse files
app/embed/eval/distribution/[...id]/page.tsx
CHANGED
|
@@ -4,7 +4,10 @@ import { useEffect, useMemo, useState } from "react"
|
|
| 4 |
import { useParams, useSearchParams } from "next/navigation"
|
| 5 |
import { ScoreDistribution } from "@/components/score-distribution"
|
| 6 |
import { fetchEvalSummary } from "@/lib/dashboard-data-client"
|
| 7 |
-
import {
|
|
|
|
|
|
|
|
|
|
| 8 |
import { routeIdFromSegments } from "@/lib/utils"
|
| 9 |
import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
|
| 10 |
|
|
@@ -53,26 +56,10 @@ export default function EmbedEvalDistribution() {
|
|
| 53 |
|
| 54 |
// Slice axis — present when the eval has multiple subtask-scope metrics
|
| 55 |
// sharing a primary root metric (e.g. Global MMLU's 24 language splits).
|
| 56 |
-
const sliceAxis = useMemo(
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
if (!primary?.column_key) return null
|
| 61 |
-
const seen = new Map<string, string>()
|
| 62 |
-
for (const m of metrics) {
|
| 63 |
-
if (m.scope === "subtask" && m.subtask_key && !seen.has(m.subtask_key)) {
|
| 64 |
-
seen.set(m.subtask_key, m.subtask_name ?? m.subtask_key)
|
| 65 |
-
}
|
| 66 |
-
}
|
| 67 |
-
if (seen.size <= 1) return null
|
| 68 |
-
return {
|
| 69 |
-
primaryColumn: primary.column_key,
|
| 70 |
-
primaryLabel: getMetricChipLabel(primary),
|
| 71 |
-
unit: primary.unit ?? summary.metric_config.unit,
|
| 72 |
-
lowerIsBetter: Boolean(primary.lower_is_better ?? summary.metric_config.lower_is_better),
|
| 73 |
-
slices: Array.from(seen, ([key, label]) => ({ key, label })),
|
| 74 |
-
}
|
| 75 |
-
}, [summary])
|
| 76 |
|
| 77 |
const ALL_SLICE_KEY = "__all__"
|
| 78 |
const [activeSlice, setActiveSlice] = useState<string>(() => {
|
|
@@ -89,78 +76,10 @@ export default function EmbedEvalDistribution() {
|
|
| 89 |
}
|
| 90 |
}, [activeSlice, sliceAxis])
|
| 91 |
|
| 92 |
-
const series = useMemo(
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
// Slice-axis path: render one series for the active slice (Overall or
|
| 97 |
-
// a specific subtask). Drives the SPLIT dropdown UX.
|
| 98 |
-
if (sliceAxis) {
|
| 99 |
-
const columnKey =
|
| 100 |
-
activeSlice === ALL_SLICE_KEY
|
| 101 |
-
? sliceAxis.primaryColumn
|
| 102 |
-
: `${sliceAxis.primaryColumn}::${activeSlice}`
|
| 103 |
-
const points: { score: number; releaseDate: string | null; modelName: string }[] = []
|
| 104 |
-
for (const row of rows) {
|
| 105 |
-
const raw = (row.values as Record<string, unknown> | undefined)?.[columnKey]
|
| 106 |
-
const numeric = typeof raw === "number" ? raw : Number(raw)
|
| 107 |
-
if (!Number.isFinite(numeric)) continue
|
| 108 |
-
const modelInfo = (row as { model_info?: { name?: string; release_date?: string | null } }).model_info
|
| 109 |
-
points.push({
|
| 110 |
-
score: numeric,
|
| 111 |
-
modelName: modelInfo?.name ?? "",
|
| 112 |
-
releaseDate: modelInfo?.release_date ?? null,
|
| 113 |
-
})
|
| 114 |
-
}
|
| 115 |
-
if (points.length < 3) return null
|
| 116 |
-
const sliceLabel =
|
| 117 |
-
activeSlice === ALL_SLICE_KEY
|
| 118 |
-
? "Overall"
|
| 119 |
-
: sliceAxis.slices.find((s) => s.key === activeSlice)?.label ?? activeSlice
|
| 120 |
-
return [
|
| 121 |
-
{
|
| 122 |
-
key: `${sliceAxis.primaryColumn}::${activeSlice}`,
|
| 123 |
-
label: `${sliceAxis.primaryLabel} · ${sliceLabel}`,
|
| 124 |
-
values: points.map((p) => p.score),
|
| 125 |
-
unit: sliceAxis.unit,
|
| 126 |
-
lowerIsBetter: sliceAxis.lowerIsBetter,
|
| 127 |
-
points,
|
| 128 |
-
},
|
| 129 |
-
]
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
// Non-slice path: one series per metric (e.g. agentharm's multi-metric
|
| 133 |
-
// histogram). ScoreDistribution surfaces a metric chip picker.
|
| 134 |
-
const metrics = summary.leaderboard_metrics ?? []
|
| 135 |
-
const built = metrics
|
| 136 |
-
.map((metric) => {
|
| 137 |
-
const columnKey = metric.column_key ?? metric.metric_summary_id
|
| 138 |
-
if (!columnKey) return null
|
| 139 |
-
const points: { score: number; releaseDate: string | null; modelName: string }[] = []
|
| 140 |
-
for (const row of rows) {
|
| 141 |
-
const raw = (row.values as Record<string, unknown> | undefined)?.[columnKey]
|
| 142 |
-
const numeric = typeof raw === "number" ? raw : Number(raw)
|
| 143 |
-
if (!Number.isFinite(numeric)) continue
|
| 144 |
-
const modelInfo = (row as { model_info?: { name?: string; release_date?: string | null } }).model_info
|
| 145 |
-
points.push({
|
| 146 |
-
score: numeric,
|
| 147 |
-
modelName: modelInfo?.name ?? "",
|
| 148 |
-
releaseDate: modelInfo?.release_date ?? null,
|
| 149 |
-
})
|
| 150 |
-
}
|
| 151 |
-
if (points.length < 3) return null
|
| 152 |
-
return {
|
| 153 |
-
key: columnKey,
|
| 154 |
-
label: getMetricChipLabel(metric),
|
| 155 |
-
values: points.map((p) => p.score),
|
| 156 |
-
unit: metric.unit ?? summary.metric_config.unit,
|
| 157 |
-
lowerIsBetter: Boolean(metric.lower_is_better ?? summary.metric_config.lower_is_better),
|
| 158 |
-
points,
|
| 159 |
-
}
|
| 160 |
-
})
|
| 161 |
-
.filter((s): s is NonNullable<typeof s> => s !== null)
|
| 162 |
-
return built.length > 0 ? built : null
|
| 163 |
-
}, [summary, sliceAxis, activeSlice])
|
| 164 |
|
| 165 |
if (error) {
|
| 166 |
return (
|
|
|
|
| 4 |
import { useParams, useSearchParams } from "next/navigation"
|
| 5 |
import { ScoreDistribution } from "@/components/score-distribution"
|
| 6 |
import { fetchEvalSummary } from "@/lib/dashboard-data-client"
|
| 7 |
+
import {
|
| 8 |
+
buildDistributionSeries,
|
| 9 |
+
buildDistributionSliceAxis,
|
| 10 |
+
} from "@/lib/distribution-series"
|
| 11 |
import { routeIdFromSegments } from "@/lib/utils"
|
| 12 |
import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
|
| 13 |
|
|
|
|
| 56 |
|
| 57 |
// Slice axis — present when the eval has multiple subtask-scope metrics
|
| 58 |
// sharing a primary root metric (e.g. Global MMLU's 24 language splits).
|
| 59 |
+
const sliceAxis = useMemo(
|
| 60 |
+
() => (summary ? buildDistributionSliceAxis(summary) : null),
|
| 61 |
+
[summary],
|
| 62 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
const ALL_SLICE_KEY = "__all__"
|
| 65 |
const [activeSlice, setActiveSlice] = useState<string>(() => {
|
|
|
|
| 76 |
}
|
| 77 |
}, [activeSlice, sliceAxis])
|
| 78 |
|
| 79 |
+
const series = useMemo(
|
| 80 |
+
() => (summary ? buildDistributionSeries(summary, sliceAxis, activeSlice, ALL_SLICE_KEY) : null),
|
| 81 |
+
[summary, sliceAxis, activeSlice],
|
| 82 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
if (error) {
|
| 85 |
return (
|
components/quick-start.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
import { createContext, useContext, useEffect, useMemo, useState } from "react"
|
| 4 |
import Link from "next/link"
|
|
|
|
| 5 |
import {
|
| 6 |
ArrowRight,
|
| 7 |
BarChart3,
|
|
@@ -87,11 +88,17 @@ const SLIDES: Slide[] = [
|
|
| 87 |
|
| 88 |
export function QuickStartProvider({ children }: { children: React.ReactNode }) {
|
| 89 |
const [open, setOpen] = useState(false)
|
|
|
|
| 90 |
|
| 91 |
// Auto-open on a visitor's first time, mirroring the audience-mode pattern.
|
| 92 |
useEffect(() => {
|
| 93 |
-
//
|
| 94 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
if (window.self !== window.top) return
|
| 96 |
try {
|
| 97 |
if (!window.localStorage.getItem(STORAGE_KEY)) {
|
|
@@ -100,7 +107,7 @@ export function QuickStartProvider({ children }: { children: React.ReactNode })
|
|
| 100 |
} catch {
|
| 101 |
// localStorage unavailable (e.g. privacy mode) — skip the tour silently.
|
| 102 |
}
|
| 103 |
-
}, [])
|
| 104 |
|
| 105 |
const markSeen = () => {
|
| 106 |
try {
|
|
|
|
| 2 |
|
| 3 |
import { createContext, useContext, useEffect, useMemo, useState } from "react"
|
| 4 |
import Link from "next/link"
|
| 5 |
+
import { usePathname } from "next/navigation"
|
| 6 |
import {
|
| 7 |
ArrowRight,
|
| 8 |
BarChart3,
|
|
|
|
| 88 |
|
| 89 |
export function QuickStartProvider({ children }: { children: React.ReactNode }) {
|
| 90 |
const [open, setOpen] = useState(false)
|
| 91 |
+
const pathname = usePathname()
|
| 92 |
|
| 93 |
// Auto-open on a visitor's first time, mirroring the audience-mode pattern.
|
| 94 |
useEffect(() => {
|
| 95 |
+
// Embed surfaces never get the tour. Two independent guards:
|
| 96 |
+
// 1. Route-based — anything under /embed/* is a self-contained card meant
|
| 97 |
+
// for iframing, so the tour must not appear even if the URL is opened
|
| 98 |
+
// directly as a top-level tab (e.g. a "preview" link).
|
| 99 |
+
// 2. Frame-based — when iframed, storage partitioning means the seen-flag
|
| 100 |
+
// can't persist, so the tour would replay on every load.
|
| 101 |
+
if (pathname?.startsWith("/embed")) return
|
| 102 |
if (window.self !== window.top) return
|
| 103 |
try {
|
| 104 |
if (!window.localStorage.getItem(STORAGE_KEY)) {
|
|
|
|
| 107 |
} catch {
|
| 108 |
// localStorage unavailable (e.g. privacy mode) — skip the tour silently.
|
| 109 |
}
|
| 110 |
+
}, [pathname])
|
| 111 |
|
| 112 |
const markSeen = () => {
|
| 113 |
try {
|
lib/distribution-series.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getMetricChipLabel } from "@/lib/metric-labels"
|
| 2 |
+
import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Series-building logic for the score-distribution embed
|
| 6 |
+
* (`app/embed/eval/distribution/[...id]/page.tsx`), extracted as pure
|
| 7 |
+
* functions so the chip-selection rules can be unit-tested against real
|
| 8 |
+
* eval-summary payloads without spinning up the React page.
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
export type DistributionPoint = {
|
| 12 |
+
score: number
|
| 13 |
+
releaseDate: string | null
|
| 14 |
+
modelName: string
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export type DistributionSeries = {
|
| 18 |
+
key: string
|
| 19 |
+
label: string
|
| 20 |
+
values: number[]
|
| 21 |
+
unit?: string
|
| 22 |
+
lowerIsBetter: boolean
|
| 23 |
+
points: DistributionPoint[]
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export type DistributionSliceAxis = {
|
| 27 |
+
primaryColumn: string
|
| 28 |
+
primaryLabel: string
|
| 29 |
+
unit?: string
|
| 30 |
+
lowerIsBetter: boolean
|
| 31 |
+
slices: Array<{ key: string; label: string }>
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* A slice axis exists only when the eval carries MORE THAN ONE distinct
|
| 36 |
+
* subtask slice sharing a root primary metric (e.g. Global MMLU's per-language
|
| 37 |
+
* splits). A lone self-slice (one subtask key that just echoes the eval) does
|
| 38 |
+
* NOT qualify — it falls through to the non-slice path where it is dropped as a
|
| 39 |
+
* redundant twin of the root metric.
|
| 40 |
+
*/
|
| 41 |
+
export function buildDistributionSliceAxis(
|
| 42 |
+
summary: BenchmarkEvalSummary,
|
| 43 |
+
): DistributionSliceAxis | null {
|
| 44 |
+
const metrics = summary.leaderboard_metrics ?? []
|
| 45 |
+
const primary = metrics.find((m) => m.scope !== "subtask")
|
| 46 |
+
if (!primary?.column_key) return null
|
| 47 |
+
const seen = new Map<string, string>()
|
| 48 |
+
for (const m of metrics) {
|
| 49 |
+
if (m.scope === "subtask" && m.subtask_key && !seen.has(m.subtask_key)) {
|
| 50 |
+
seen.set(m.subtask_key, m.subtask_name ?? m.subtask_key)
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
if (seen.size <= 1) return null
|
| 54 |
+
return {
|
| 55 |
+
primaryColumn: primary.column_key,
|
| 56 |
+
primaryLabel: getMetricChipLabel(primary),
|
| 57 |
+
unit: primary.unit ?? summary.metric_config.unit,
|
| 58 |
+
lowerIsBetter: Boolean(primary.lower_is_better ?? summary.metric_config.lower_is_better),
|
| 59 |
+
slices: Array.from(seen, ([key, label]) => ({ key, label })),
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function pointsForColumn(
|
| 64 |
+
rows: BenchmarkEvalSummary["leaderboard_rows"],
|
| 65 |
+
columnKey: string,
|
| 66 |
+
): DistributionPoint[] {
|
| 67 |
+
const points: DistributionPoint[] = []
|
| 68 |
+
for (const row of rows ?? []) {
|
| 69 |
+
const raw = (row.values as Record<string, unknown> | undefined)?.[columnKey]
|
| 70 |
+
const numeric = typeof raw === "number" ? raw : Number(raw)
|
| 71 |
+
if (!Number.isFinite(numeric)) continue
|
| 72 |
+
const modelInfo = (row as { model_info?: { name?: string; release_date?: string | null } }).model_info
|
| 73 |
+
points.push({
|
| 74 |
+
score: numeric,
|
| 75 |
+
modelName: modelInfo?.name ?? "",
|
| 76 |
+
releaseDate: modelInfo?.release_date ?? null,
|
| 77 |
+
})
|
| 78 |
+
}
|
| 79 |
+
return points
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/**
|
| 83 |
+
* Build the score-distribution series for the embed. Returns null when there
|
| 84 |
+
* is nothing renderable (fewer than 3 data points everywhere).
|
| 85 |
+
*/
|
| 86 |
+
export function buildDistributionSeries(
|
| 87 |
+
summary: BenchmarkEvalSummary,
|
| 88 |
+
sliceAxis: DistributionSliceAxis | null,
|
| 89 |
+
activeSlice: string,
|
| 90 |
+
allSliceKey: string,
|
| 91 |
+
): DistributionSeries[] | null {
|
| 92 |
+
const rows = summary.leaderboard_rows ?? []
|
| 93 |
+
|
| 94 |
+
// Slice-axis path: render one series for the active slice (Overall or a
|
| 95 |
+
// specific subtask). Drives the SPLIT dropdown UX.
|
| 96 |
+
if (sliceAxis) {
|
| 97 |
+
const columnKey =
|
| 98 |
+
activeSlice === allSliceKey
|
| 99 |
+
? sliceAxis.primaryColumn
|
| 100 |
+
: `${sliceAxis.primaryColumn}::${activeSlice}`
|
| 101 |
+
const points = pointsForColumn(rows, columnKey)
|
| 102 |
+
if (points.length < 3) return null
|
| 103 |
+
const sliceLabel =
|
| 104 |
+
activeSlice === allSliceKey
|
| 105 |
+
? "Overall"
|
| 106 |
+
: sliceAxis.slices.find((s) => s.key === activeSlice)?.label ?? activeSlice
|
| 107 |
+
return [
|
| 108 |
+
{
|
| 109 |
+
key: `${sliceAxis.primaryColumn}::${activeSlice}`,
|
| 110 |
+
label: `${sliceAxis.primaryLabel} · ${sliceLabel}`,
|
| 111 |
+
values: points.map((p) => p.score),
|
| 112 |
+
unit: sliceAxis.unit,
|
| 113 |
+
lowerIsBetter: sliceAxis.lowerIsBetter,
|
| 114 |
+
points,
|
| 115 |
+
},
|
| 116 |
+
]
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Non-slice path: one series per ROOT metric (e.g. agentharm's multi-metric
|
| 120 |
+
// histogram). ScoreDistribution surfaces a metric chip picker. We mirror the
|
| 121 |
+
// full eval page (eval-detail.tsx) which builds chips only from root-scope
|
| 122 |
+
// metrics: the producer also emits a redundant self-slice subtask for some
|
| 123 |
+
// evals — a metric whose subtask_key is just the slugified eval (e.g.
|
| 124 |
+
// vals-ai/math-500 carries both root `accuracy` and subtask
|
| 125 |
+
// `accuracy::vals ai math500`, same metric_summary_id and label). Multi-slice
|
| 126 |
+
// evals (distinct subtask keys) are handled by the sliceAxis path above, so
|
| 127 |
+
// the only subtasks reaching here are these redundant twins; rendering them
|
| 128 |
+
// would duplicate the chip (two identical "Accuracy" buttons). Fall back to
|
| 129 |
+
// the full set only if an eval somehow carries no root metric, so a
|
| 130 |
+
// subtask-only eval still renders rather than going blank.
|
| 131 |
+
const allMetrics = summary.leaderboard_metrics ?? []
|
| 132 |
+
const rootMetrics = allMetrics.filter((m) => m.scope !== "subtask")
|
| 133 |
+
const metrics = rootMetrics.length > 0 ? rootMetrics : allMetrics
|
| 134 |
+
const built = metrics
|
| 135 |
+
.map((metric) => {
|
| 136 |
+
const columnKey = metric.column_key ?? metric.metric_summary_id
|
| 137 |
+
if (!columnKey) return null
|
| 138 |
+
const points = pointsForColumn(rows, columnKey)
|
| 139 |
+
if (points.length < 3) return null
|
| 140 |
+
return {
|
| 141 |
+
key: columnKey,
|
| 142 |
+
label: getMetricChipLabel(metric),
|
| 143 |
+
values: points.map((p) => p.score),
|
| 144 |
+
unit: metric.unit ?? summary.metric_config.unit,
|
| 145 |
+
lowerIsBetter: Boolean(metric.lower_is_better ?? summary.metric_config.lower_is_better),
|
| 146 |
+
points,
|
| 147 |
+
}
|
| 148 |
+
})
|
| 149 |
+
.filter((s): s is NonNullable<typeof s> => s !== null)
|
| 150 |
+
return built.length > 0 ? built : null
|
| 151 |
+
}
|
tests/distribution-series.test.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { describe, expect, it } from "vitest"
|
| 2 |
+
|
| 3 |
+
import {
|
| 4 |
+
buildDistributionSeries,
|
| 5 |
+
buildDistributionSliceAxis,
|
| 6 |
+
} from "@/lib/distribution-series"
|
| 7 |
+
import type { BenchmarkEvalSummary } from "@/lib/eval-processing"
|
| 8 |
+
import fixtures from "./fixtures/distribution-eval-summaries.json"
|
| 9 |
+
|
| 10 |
+
// Fixtures are trimmed real payloads captured from the live
|
| 11 |
+
// /api/eval-summary route (evalcards.evalevalai.com), so these assertions
|
| 12 |
+
// exercise the actual deployed data shape, not a hand-built mock.
|
| 13 |
+
const ALL = "__all__"
|
| 14 |
+
const summaryOf = (k: keyof typeof fixtures) =>
|
| 15 |
+
fixtures[k] as unknown as BenchmarkEvalSummary
|
| 16 |
+
|
| 17 |
+
describe("buildDistributionSeries — metric chip selection", () => {
|
| 18 |
+
it("vals-ai/math-500: collapses the redundant self-slice subtask to a single Accuracy chip", () => {
|
| 19 |
+
const summary = summaryOf("math500")
|
| 20 |
+
// The live payload carries two metrics with the same metric_summary_id:
|
| 21 |
+
// root `accuracy` + subtask `accuracy::vals ai math500`. The subtask is a
|
| 22 |
+
// self-slice (its key is just the slugified eval), so it must NOT become a
|
| 23 |
+
// second chip.
|
| 24 |
+
expect((summary.leaderboard_metrics ?? []).map((m) => m.scope).sort()).toEqual([
|
| 25 |
+
"root",
|
| 26 |
+
"subtask",
|
| 27 |
+
])
|
| 28 |
+
const sliceAxis = buildDistributionSliceAxis(summary)
|
| 29 |
+
// A lone self-slice is not a real slice axis.
|
| 30 |
+
expect(sliceAxis).toBeNull()
|
| 31 |
+
const series = buildDistributionSeries(summary, sliceAxis, ALL, ALL)
|
| 32 |
+
expect(series).not.toBeNull()
|
| 33 |
+
expect(series!.length).toBe(1)
|
| 34 |
+
expect(series![0].label).toBe("Accuracy")
|
| 35 |
+
})
|
| 36 |
+
|
| 37 |
+
it("agentharm: keeps every distinct root metric (multi-metric embed unbroken)", () => {
|
| 38 |
+
const summary = summaryOf("agentharm")
|
| 39 |
+
const rootCount = (summary.leaderboard_metrics ?? []).filter(
|
| 40 |
+
(m) => m.scope !== "subtask",
|
| 41 |
+
).length
|
| 42 |
+
expect(rootCount).toBeGreaterThan(1)
|
| 43 |
+
const sliceAxis = buildDistributionSliceAxis(summary)
|
| 44 |
+
const series = buildDistributionSeries(summary, sliceAxis, ALL, ALL)
|
| 45 |
+
expect(series).not.toBeNull()
|
| 46 |
+
expect(series!.length).toBe(rootCount)
|
| 47 |
+
// No two chips share a label (the duplication the fix targets).
|
| 48 |
+
const labels = series!.map((s) => s.label)
|
| 49 |
+
expect(new Set(labels).size).toBe(labels.length)
|
| 50 |
+
})
|
| 51 |
+
})
|
tests/fixtures/distribution-eval-summaries.json
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"math500": {
|
| 3 |
+
"evaluation_name": "MATH-500",
|
| 4 |
+
"metric_config": {
|
| 5 |
+
"evaluation_description": "Accuracy",
|
| 6 |
+
"lower_is_better": false,
|
| 7 |
+
"score_type": null,
|
| 8 |
+
"min_score": 0,
|
| 9 |
+
"max_score": 1,
|
| 10 |
+
"unit": "percent"
|
| 11 |
+
},
|
| 12 |
+
"leaderboard_metrics": [
|
| 13 |
+
{
|
| 14 |
+
"column_key": "accuracy",
|
| 15 |
+
"metric_summary_id": "math-500%3Aaccuracy",
|
| 16 |
+
"metric_id": "accuracy",
|
| 17 |
+
"metric_name": "Accuracy",
|
| 18 |
+
"display_name": "Accuracy",
|
| 19 |
+
"canonical_display_name": "Accuracy",
|
| 20 |
+
"lower_is_better": false,
|
| 21 |
+
"unit": "percent",
|
| 22 |
+
"scope": "root",
|
| 23 |
+
"subtask_key": null,
|
| 24 |
+
"subtask_name": null
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"column_key": "accuracy::vals ai math500",
|
| 28 |
+
"metric_summary_id": "math-500%3Aaccuracy",
|
| 29 |
+
"metric_id": "accuracy",
|
| 30 |
+
"metric_name": "Accuracy",
|
| 31 |
+
"display_name": "Accuracy",
|
| 32 |
+
"canonical_display_name": "Accuracy",
|
| 33 |
+
"lower_is_better": false,
|
| 34 |
+
"unit": "percent",
|
| 35 |
+
"scope": "subtask",
|
| 36 |
+
"subtask_key": "vals ai math500",
|
| 37 |
+
"subtask_name": "vals ai math500"
|
| 38 |
+
}
|
| 39 |
+
],
|
| 40 |
+
"leaderboard_rows": [
|
| 41 |
+
{
|
| 42 |
+
"values": {
|
| 43 |
+
"accuracy": 89,
|
| 44 |
+
"accuracy::vals ai math500": 89
|
| 45 |
+
},
|
| 46 |
+
"model_info": {
|
| 47 |
+
"name": "MiniMax M2.1",
|
| 48 |
+
"release_date": "2025-12-20"
|
| 49 |
+
}
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"values": {
|
| 53 |
+
"accuracy": 54.8,
|
| 54 |
+
"accuracy::vals ai math500": 54.8
|
| 55 |
+
},
|
| 56 |
+
"model_info": {
|
| 57 |
+
"name": "Jamba Large 1.6",
|
| 58 |
+
"release_date": null
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"values": {
|
| 63 |
+
"accuracy": 25.4,
|
| 64 |
+
"accuracy::vals ai math500": 25.4
|
| 65 |
+
},
|
| 66 |
+
"model_info": {
|
| 67 |
+
"name": "Jamba Mini 1.6",
|
| 68 |
+
"release_date": null
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"values": {
|
| 73 |
+
"accuracy": 76.8
|
| 74 |
+
},
|
| 75 |
+
"model_info": {
|
| 76 |
+
"name": "Claude 3.7 Sonnet",
|
| 77 |
+
"release_date": "2025-02-19"
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"values": {
|
| 82 |
+
"accuracy": 64.2
|
| 83 |
+
},
|
| 84 |
+
"model_info": {
|
| 85 |
+
"name": "Claude 3 5 Haiku",
|
| 86 |
+
"release_date": "2024-10-22"
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"values": {
|
| 91 |
+
"accuracy": 91.6,
|
| 92 |
+
"accuracy::vals ai math500": 91.6
|
| 93 |
+
},
|
| 94 |
+
"model_info": {
|
| 95 |
+
"name": "Claude 3 7 Sonnet 20250219 Thinking",
|
| 96 |
+
"release_date": null
|
| 97 |
+
}
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"values": {
|
| 101 |
+
"accuracy": 72.4
|
| 102 |
+
},
|
| 103 |
+
"model_info": {
|
| 104 |
+
"name": "Claude 3.5 Sonnet",
|
| 105 |
+
"release_date": "2024-10-22"
|
| 106 |
+
}
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"values": {
|
| 110 |
+
"accuracy": 90.4
|
| 111 |
+
},
|
| 112 |
+
"model_info": {
|
| 113 |
+
"name": "Claude Opus 4",
|
| 114 |
+
"release_date": "2025-05-22"
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
]
|
| 118 |
+
},
|
| 119 |
+
"agentharm": {
|
| 120 |
+
"evaluation_name": "AgentHarm",
|
| 121 |
+
"metric_config": {
|
| 122 |
+
"evaluation_description": null,
|
| 123 |
+
"lower_is_better": false,
|
| 124 |
+
"score_type": null,
|
| 125 |
+
"min_score": null,
|
| 126 |
+
"max_score": null,
|
| 127 |
+
"unit": "proportion"
|
| 128 |
+
},
|
| 129 |
+
"leaderboard_metrics": [
|
| 130 |
+
{
|
| 131 |
+
"column_key": "inspect_evals/avg_full_score",
|
| 132 |
+
"metric_summary_id": "agentharm%3Ainspect_evals%2Favg_full_score",
|
| 133 |
+
"metric_id": "inspect_evals/avg_full_score",
|
| 134 |
+
"metric_name": null,
|
| 135 |
+
"display_name": null,
|
| 136 |
+
"canonical_display_name": null,
|
| 137 |
+
"lower_is_better": false,
|
| 138 |
+
"unit": "proportion",
|
| 139 |
+
"scope": "root",
|
| 140 |
+
"subtask_key": null,
|
| 141 |
+
"subtask_name": null
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"column_key": "inspect_evals/avg_refusals",
|
| 145 |
+
"metric_summary_id": "agentharm%3Ainspect_evals%2Favg_refusals",
|
| 146 |
+
"metric_id": "inspect_evals/avg_refusals",
|
| 147 |
+
"metric_name": null,
|
| 148 |
+
"display_name": null,
|
| 149 |
+
"canonical_display_name": null,
|
| 150 |
+
"lower_is_better": false,
|
| 151 |
+
"unit": "proportion",
|
| 152 |
+
"scope": "root",
|
| 153 |
+
"subtask_key": null,
|
| 154 |
+
"subtask_name": null
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"column_key": "inspect_evals/avg_score",
|
| 158 |
+
"metric_summary_id": "agentharm%3Ainspect_evals%2Favg_score",
|
| 159 |
+
"metric_id": "inspect_evals/avg_score",
|
| 160 |
+
"metric_name": null,
|
| 161 |
+
"display_name": null,
|
| 162 |
+
"canonical_display_name": null,
|
| 163 |
+
"lower_is_better": false,
|
| 164 |
+
"unit": "proportion",
|
| 165 |
+
"scope": "root",
|
| 166 |
+
"subtask_key": null,
|
| 167 |
+
"subtask_name": null
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"column_key": "inspect_evals/avg_score_non_refusals",
|
| 171 |
+
"metric_summary_id": "agentharm%3Ainspect_evals%2Favg_score_non_refusals",
|
| 172 |
+
"metric_id": "inspect_evals/avg_score_non_refusals",
|
| 173 |
+
"metric_name": null,
|
| 174 |
+
"display_name": null,
|
| 175 |
+
"canonical_display_name": null,
|
| 176 |
+
"lower_is_better": false,
|
| 177 |
+
"unit": "proportion",
|
| 178 |
+
"scope": "root",
|
| 179 |
+
"subtask_key": null,
|
| 180 |
+
"subtask_name": null
|
| 181 |
+
}
|
| 182 |
+
],
|
| 183 |
+
"leaderboard_rows": [
|
| 184 |
+
{
|
| 185 |
+
"values": {
|
| 186 |
+
"inspect_evals/avg_full_score": 0.23863636363636365,
|
| 187 |
+
"inspect_evals/avg_refusals": 0.6534090909090909,
|
| 188 |
+
"inspect_evals/avg_score": 0.339788715072806,
|
| 189 |
+
"inspect_evals/avg_score_non_refusals": 0.8732825917252146
|
| 190 |
+
},
|
| 191 |
+
"model_info": {
|
| 192 |
+
"name": "Claude 3.7 Sonnet",
|
| 193 |
+
"release_date": "2025-02-19"
|
| 194 |
+
}
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"values": {
|
| 198 |
+
"inspect_evals/avg_full_score": 0.375,
|
| 199 |
+
"inspect_evals/avg_refusals": 0.022727272727272728,
|
| 200 |
+
"inspect_evals/avg_score": 0.7212594696969696,
|
| 201 |
+
"inspect_evals/avg_score_non_refusals": 0.7380329457364341
|
| 202 |
+
},
|
| 203 |
+
"model_info": {
|
| 204 |
+
"name": "DeepSeek",
|
| 205 |
+
"release_date": "2025-12-01"
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"values": {
|
| 210 |
+
"inspect_evals/avg_full_score": 0.29545454545454547,
|
| 211 |
+
"inspect_evals/avg_refusals": 0.3352272727272727,
|
| 212 |
+
"inspect_evals/avg_score": 0.47781385281385286,
|
| 213 |
+
"inspect_evals/avg_score_non_refusals": 0.7170533170533171
|
| 214 |
+
},
|
| 215 |
+
"model_info": {
|
| 216 |
+
"name": "Gemini 2.0 Flash",
|
| 217 |
+
"release_date": "2024-12-11"
|
| 218 |
+
}
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
"values": {
|
| 222 |
+
"inspect_evals/avg_full_score": 0.125,
|
| 223 |
+
"inspect_evals/avg_refusals": 0.38636363636363635,
|
| 224 |
+
"inspect_evals/avg_score": 0.3750411993309721,
|
| 225 |
+
"inspect_evals/avg_score_non_refusals": 0.582783189033189
|
| 226 |
+
},
|
| 227 |
+
"model_info": {
|
| 228 |
+
"name": "Llama 3.3",
|
| 229 |
+
"release_date": null
|
| 230 |
+
}
|
| 231 |
+
},
|
| 232 |
+
{
|
| 233 |
+
"values": {
|
| 234 |
+
"inspect_evals/avg_full_score": 0.4034090909090909,
|
| 235 |
+
"inspect_evals/avg_refusals": 0.045454545454545456,
|
| 236 |
+
"inspect_evals/avg_score": 0.6345533254624164,
|
| 237 |
+
"inspect_evals/avg_score_non_refusals": 0.6624317151102865
|
| 238 |
+
},
|
| 239 |
+
"model_info": {
|
| 240 |
+
"name": "Mistral Large",
|
| 241 |
+
"release_date": "2024-11-01"
|
| 242 |
+
}
|
| 243 |
+
},
|
| 244 |
+
{
|
| 245 |
+
"values": {
|
| 246 |
+
"inspect_evals/avg_full_score": 0.3125,
|
| 247 |
+
"inspect_evals/avg_refusals": 0.4772727272727273,
|
| 248 |
+
"inspect_evals/avg_score": 0.4302323150334514,
|
| 249 |
+
"inspect_evals/avg_score_non_refusals": 0.8007963956333523
|
| 250 |
+
},
|
| 251 |
+
"model_info": {
|
| 252 |
+
"name": "Mistral Small",
|
| 253 |
+
"release_date": "2026-03-16"
|
| 254 |
+
}
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"values": {
|
| 258 |
+
"inspect_evals/avg_full_score": 0.35795454545454547,
|
| 259 |
+
"inspect_evals/avg_refusals": 0.42613636363636365,
|
| 260 |
+
"inspect_evals/avg_score": 0.4916531385281386,
|
| 261 |
+
"inspect_evals/avg_score_non_refusals": 0.8254125412541253
|
| 262 |
+
},
|
| 263 |
+
"model_info": {
|
| 264 |
+
"name": "GPT-4o",
|
| 265 |
+
"release_date": "2024-05-13"
|
| 266 |
+
}
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"values": {
|
| 270 |
+
"inspect_evals/avg_full_score": 0.32386363636363635,
|
| 271 |
+
"inspect_evals/avg_refusals": 0.19318181818181818,
|
| 272 |
+
"inspect_evals/avg_score": 0.5942812868949233,
|
| 273 |
+
"inspect_evals/avg_score_non_refusals": 0.7107523931467593
|
| 274 |
+
},
|
| 275 |
+
"model_info": {
|
| 276 |
+
"name": "GPT-4o mini",
|
| 277 |
+
"release_date": "2024-07-18"
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
]
|
| 281 |
+
}
|
| 282 |
+
}
|