`$${value < 0.01 ? value.toFixed(3) : value < 1 ? value.toFixed(2) : value.toFixed(1)}`}
+ tickFormatter={value => `${AXES[xAxis].money ? '$' : ''}${Number(value.toPrecision(4))}`}
label={{
- value: `Cost per task (USD) — ${scale.toUpperCase()} SCALE${isZoomed ? ' (ZOOMED)' : ''}`,
+ value: `${AXES[xAxis].label} — ${scale.toUpperCase()} SCALE${isZoomed ? ' (ZOOMED)' : ''}`,
position: 'bottom',
fill: 'rgb(var(--port-text-muted))',
fontSize: 12,
@@ -1217,7 +1247,7 @@ export default function ModelComparison() {
axisLine={{ stroke: 'rgb(var(--port-border))' }}
type="number"
dataKey="y"
- name="Benchmark score"
+ name={AXES[yAxis].label}
domain={
isZoomed
? [
@@ -1229,7 +1259,7 @@ export default function ModelComparison() {
allowDataOverflow={isZoomed}
width={55}
label={{
- value: 'Artificial Analysis Intelligence Index',
+ value: AXES[yAxis].label,
angle: -90,
position: 'insideLeft',
fill: 'rgb(var(--port-text-muted))',
@@ -1263,11 +1293,11 @@ export default function ModelComparison() {
USD / task:{' '}
- ${item.x?.toFixed(4)}
+ {Number.isFinite(item.cost) ? `$${item.cost.toFixed(4)}` : 'Unknown'}
{item.costEstimated && est.}
@@ -1279,6 +1309,9 @@ export default function ModelComparison() {
Speed: {item.tokensPerSecond.value} t/s
)}
+ {item.outputPerMillion && (
+
In:
${item.inputPerMillion.value}/1M
@@ -1326,9 +1359,9 @@ export default function ModelComparison() {
cx={cx}
cy={cy}
r={5}
- fill={payload?.costEstimated ? 'rgb(var(--port-card))' : fill}
- stroke={payload?.costEstimated ? fill : 'rgb(var(--port-card))'}
- strokeWidth={payload?.costEstimated ? 2 : 1.5}
+ fill={payload?.chartCostEstimated ? 'rgb(var(--port-card))' : fill}
+ stroke={payload?.chartCostEstimated ? fill : 'rgb(var(--port-card))'}
+ strokeWidth={payload?.chartCostEstimated ? 2 : 1.5}
/>
)}
@@ -1381,6 +1414,7 @@ export default function ModelComparison() {
)}
+ Missing values stay in the evidence table. Log X excludes zero values, including free pricing; choose linear to include them. Higher speed is better; lower response time is better.
Response-time labels reflect measured source workloads, independent of the intelligence evaluation. Connected lines
link the same model family across reasoning efforts (ordered from low to max). Hollow markers are estimated costs:
Artificial Analysis publishes cost per task for only one effort of most models, so the remaining efforts are scaled
@@ -1429,6 +1463,7 @@ export default function ModelComparison() {
'Provider / model / effort',
'Quality',
'USD / task',
+ 'Input / output USD per 1M',
'Scenario total',
'Response / speed',
'Quota',
@@ -1450,12 +1485,13 @@ export default function ModelComparison() {
{row.billing} · {row.configuration}
-
{row.notes}
+
{row.benchmark} · {row.notes}
-
{row.y ?? 'Unknown'} |
-
{Number.isFinite(row.x) ? `$${row.x.toFixed(4)}` : 'Unknown'} |
+
{row.quality?.value ?? 'Unknown'} |
+
{Number.isFinite(row.cost) ? `$${row.cost.toFixed(4)}` : 'Unknown'}{row.costEstimated ? ' (estimated)' : ''} |
+
{row.inputPerMillion ? `$${row.inputPerMillion.value}` : 'Unknown'} / {row.outputPerMillion ? `$${row.outputPerMillion.value}` : 'Unknown'} |
- {mode === 'scenario' && Number.isFinite(row.x) ? `$${(row.x * scenario.tasks).toFixed(2)}` : '—'}
+ {mode === 'scenario' && Number.isFinite(row.cost) ? `$${(row.cost * scenario.tasks).toFixed(2)}` : '—'}
|
{row.responseSeconds ? `${row.responseSeconds.value}s E2E` : 'E2E unknown'}
diff --git a/client/src/components/models/ModelComparison.test.jsx b/client/src/components/models/ModelComparison.test.jsx
index 48a2d41966..0a3c3583da 100644
--- a/client/src/components/models/ModelComparison.test.jsx
+++ b/client/src/components/models/ModelComparison.test.jsx
@@ -18,7 +18,7 @@ vi.mock('recharts', () => ({
ResponsiveContainer: ({ children }) => {children} ,
ScatterChart: ({ children }) => {children} ,
Scatter: ({ name, data, line }) => (
-
+ [x, y]))} />
),
CartesianGrid: () => null,
LabelList: () => null,
@@ -50,7 +50,7 @@ beforeEach(() => {
it('shows missing evidence, filters models and estimates token costs without inventing reasoning prices or local cost', async () => {
render();
await act(async () => {});
- await screen.findByText('1 plotted · 1 missing quality or cost');
+ await screen.findByText('1 plotted · 1 missing selected metrics or outside log scale');
fireEvent.click(screen.getByText(/Evidence & sources/));
fireEvent.click(screen.getByText('Show or hide providers, models & effort'));
expect(screen.getAllByText(/E2E unknown/)).toHaveLength(2);
@@ -111,7 +111,7 @@ it('connects reasoning effort points with line and allows toggling line style an
expect(screen.getByTestId('scatter-gpt-5.6-sol')).toHaveAttribute('data-has-line', 'false');
// Test scale toggle
- fireEvent.change(screen.getByLabelText('Cost scale'), { target: { value: 'log' } });
+ fireEvent.change(screen.getByLabelText('X-axis scale'), { target: { value: 'log' } });
expect(screen.getByTestId('xaxis')).toHaveAttribute('data-scale', 'log');
// Test quick filter reasoning curves
@@ -204,7 +204,7 @@ it('stretches chart width and adjusts height', async () => {
render();
await act(async () => {});
- await screen.findByText('1 plotted · 1 missing quality or cost');
+ await screen.findByText('1 plotted · 1 missing selected metrics or outside log scale');
// Initially at 1x
expect(screen.getByRole('button', { name: '1×' })).toHaveAttribute('aria-pressed', 'true');
@@ -232,7 +232,7 @@ it('scales axes via zoom buttons and manual bounds inputs', async () => {
render();
await act(async () => {});
- await screen.findByText('3 plotted · 0 missing quality or cost');
+ await screen.findByText('3 plotted · 0 missing selected metrics or outside log scale');
const xaxis = screen.getByTestId('xaxis');
const yaxis = screen.getByTestId('yaxis');
@@ -320,3 +320,33 @@ it('persists settings to localStorage and restores them on a fresh visit', async
expect(screen.getByRole('button', { name: '1.5×' })).toHaveAttribute('aria-pressed', 'true');
});
+
+it('switches metric coordinates, clears stale zoom, and preserves evidence table units', async () => {
+ api.getModelComparison.mockResolvedValue({ observations: [
+ { ...observation, tokensPerSecond: metric(80), responseSeconds: metric(12) },
+ { ...observation, id: 'missing-speed', model: 'other', tokensPerSecond: null },
+ ], inventory: [] });
+ render();
+ await screen.findByLabelText('X axis');
+ fireEvent.change(screen.getByLabelText('X axis'), { target: { value: 'tokensPerSecond' } });
+ fireEvent.change(screen.getByLabelText('Y axis'), { target: { value: 'responseSeconds' } });
+ expect(screen.getByTestId('scatter-example-model')).toHaveAttribute('data-values', '[[80,12]]');
+ expect(screen.queryByTestId('scatter-other')).toBeNull();
+ expect(screen.getByTestId('xaxis')).toHaveAttribute('data-scale', 'linear');
+ expect(screen.getByTestId('xaxis')).toHaveAttribute('data-allow-overflow', 'false');
+ expect(screen.getAllByText('$0.5000')).toHaveLength(2);
+ expect(screen.getAllByText('50')).toHaveLength(2);
+});
+
+it('keeps exact Zen IDs in available coverage and plots free prices without inventing quality', async () => {
+ const free = { ...observation, id: 'free', model: 'example-free', benchmark: 'Unbenchmarked (pricing only)', quality: null, costPerTask: null, inputPerMillion: metric(0), outputPerMillion: metric(0) };
+ api.getModelComparison.mockResolvedValue({ observations: [observation, free], availableModels: ['example'], inventory: [{ id: 'zen', models: [{ model: `opencode/${free.model}`, efforts: [] }] }] });
+ render();
+ expect(await screen.findByTestId('scatter-example-free')).toHaveAttribute('data-values', '[[0,0]]');
+ fireEvent.click(screen.getByRole('button', { name: 'Fit visible' }));
+ const domain = JSON.parse(screen.getByTestId('xaxis').getAttribute('data-domain'));
+ expect(domain[1]).toBeGreaterThan(domain[0]);
+ fireEvent.change(screen.getByLabelText('Y axis'), { target: { value: 'quality' } });
+ expect(screen.queryByTestId('scatter-example-free')).toBeNull();
+ expect(screen.getAllByText('Unknown').length).toBeGreaterThan(0);
+});
diff --git a/data.reference/model-comparison.json b/data.reference/model-comparison.json
index ed3b83267c..f1e820041f 100644
--- a/data.reference/model-comparison.json
+++ b/data.reference/model-comparison.json
@@ -5918,6 +5918,262 @@
},
"quota": null,
"notes": "Sourced from Artificial Analysis (Grok 3 mini Reasoning (high))."
+ },
+ {
+ "id": "zen-free-2026-09-big-pickle",
+ "provider": "OpenCode Zen",
+ "model": "big-pickle",
+ "effort": "unspecified",
+ "configuration": "OpenCode Zen free endpoint; provider default configuration",
+ "billing": "api",
+ "benchmark": "Unbenchmarked (pricing only)",
+ "quality": null,
+ "costPerTask": null,
+ "inputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry big-pickle: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "outputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry big-pickle: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "reasoningPerMillion": null,
+ "responseSeconds": null,
+ "tokensPerSecond": null,
+ "quota": null,
+ "notes": "Big Pickle. Free endpoint listed on 2026-09-06. Pricing is temporary; usage limits and data-use terms apply (https://opencode.ai/docs/zen/). No verified quality or speed measurement for this exact endpoint/effort; do not transfer a similarly named model's scores."
+ },
+ {
+ "id": "zen-free-2026-09-deepseek-v4-flash-free",
+ "provider": "OpenCode Zen",
+ "model": "deepseek-v4-flash-free",
+ "effort": "unspecified",
+ "configuration": "OpenCode Zen free endpoint; provider default configuration",
+ "billing": "api",
+ "benchmark": "Unbenchmarked (pricing only)",
+ "quality": null,
+ "costPerTask": null,
+ "inputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry deepseek-v4-flash-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "outputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry deepseek-v4-flash-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "reasoningPerMillion": null,
+ "responseSeconds": null,
+ "tokensPerSecond": null,
+ "quota": null,
+ "notes": "DeepSeek V4 Flash Free. Free endpoint listed on 2026-09-06. Pricing is temporary; usage limits and data-use terms apply (https://opencode.ai/docs/zen/). No verified quality or speed measurement for this exact endpoint/effort; do not transfer a similarly named model's scores."
+ },
+ {
+ "id": "zen-free-2026-09-muse-spark-1.3-contributor-free",
+ "provider": "OpenCode Zen",
+ "model": "muse-spark-1.3-contributor-free",
+ "effort": "unspecified",
+ "configuration": "OpenCode Zen free endpoint; provider default configuration",
+ "billing": "api",
+ "benchmark": "Unbenchmarked (pricing only)",
+ "quality": null,
+ "costPerTask": null,
+ "inputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry muse-spark-1.3-contributor-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "outputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry muse-spark-1.3-contributor-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "reasoningPerMillion": null,
+ "responseSeconds": null,
+ "tokensPerSecond": null,
+ "quota": null,
+ "notes": "Muse Spark 1.3 Free. Free endpoint listed on 2026-09-06. Pricing is temporary; usage limits and data-use terms apply (https://opencode.ai/docs/zen/). No verified quality or speed measurement for this exact endpoint/effort; do not transfer a similarly named model's scores."
+ },
+ {
+ "id": "zen-free-2026-09-muse-spark-1.2-contributor-free",
+ "provider": "OpenCode Zen",
+ "model": "muse-spark-1.2-contributor-free",
+ "effort": "unspecified",
+ "configuration": "OpenCode Zen free endpoint; provider default configuration",
+ "billing": "api",
+ "benchmark": "Unbenchmarked (pricing only)",
+ "quality": null,
+ "costPerTask": null,
+ "inputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry muse-spark-1.2-contributor-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "outputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry muse-spark-1.2-contributor-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "reasoningPerMillion": null,
+ "responseSeconds": null,
+ "tokensPerSecond": null,
+ "quota": null,
+ "notes": "Muse Spark 1.2 Free. Free endpoint listed on 2026-09-06. Pricing is temporary; usage limits and data-use terms apply (https://opencode.ai/docs/zen/). No verified quality or speed measurement for this exact endpoint/effort; do not transfer a similarly named model's scores."
+ },
+ {
+ "id": "zen-free-2026-09-mimo-v2.5-free",
+ "provider": "OpenCode Zen",
+ "model": "mimo-v2.5-free",
+ "effort": "unspecified",
+ "configuration": "OpenCode Zen free endpoint; provider default configuration",
+ "billing": "api",
+ "benchmark": "Unbenchmarked (pricing only)",
+ "quality": null,
+ "costPerTask": null,
+ "inputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry mimo-v2.5-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "outputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry mimo-v2.5-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "reasoningPerMillion": null,
+ "responseSeconds": null,
+ "tokensPerSecond": null,
+ "quota": null,
+ "notes": "MiMo V2.5 Free. Free endpoint listed on 2026-09-06. Pricing is temporary; usage limits and data-use terms apply (https://opencode.ai/docs/zen/). No verified quality or speed measurement for this exact endpoint/effort; do not transfer a similarly named model's scores."
+ },
+ {
+ "id": "zen-free-2026-09-ling-3.0-flash-fin-free",
+ "provider": "OpenCode Zen",
+ "model": "ling-3.0-flash-fin-free",
+ "effort": "unspecified",
+ "configuration": "OpenCode Zen free endpoint; provider default configuration",
+ "billing": "api",
+ "benchmark": "Unbenchmarked (pricing only)",
+ "quality": null,
+ "costPerTask": null,
+ "inputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry ling-3.0-flash-fin-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "outputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry ling-3.0-flash-fin-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "reasoningPerMillion": null,
+ "responseSeconds": null,
+ "tokensPerSecond": null,
+ "quota": null,
+ "notes": "Ling 3.0 Flash Fin Free. Free endpoint listed on 2026-09-06. Pricing is temporary; usage limits and data-use terms apply (https://opencode.ai/docs/zen/). No verified quality or speed measurement for this exact endpoint/effort; do not transfer a similarly named model's scores."
+ },
+ {
+ "id": "zen-free-2026-09-nemotron-3-ultra-free",
+ "provider": "OpenCode Zen",
+ "model": "nemotron-3-ultra-free",
+ "effort": "unspecified",
+ "configuration": "OpenCode Zen free endpoint; provider default configuration",
+ "billing": "api",
+ "benchmark": "Unbenchmarked (pricing only)",
+ "quality": null,
+ "costPerTask": null,
+ "inputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry nemotron-3-ultra-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "outputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry nemotron-3-ultra-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "reasoningPerMillion": null,
+ "responseSeconds": null,
+ "tokensPerSecond": null,
+ "quota": null,
+ "notes": "Nemotron 3 Ultra Free. Free endpoint listed on 2026-09-06. Pricing is temporary; usage limits and data-use terms apply (https://opencode.ai/docs/zen/). No verified quality or speed measurement for this exact endpoint/effort; do not transfer a similarly named model's scores."
+ },
+ {
+ "id": "zen-free-2026-09-nemotron-3.5-lightning-free",
+ "provider": "OpenCode Zen",
+ "model": "nemotron-3.5-lightning-free",
+ "effort": "unspecified",
+ "configuration": "OpenCode Zen free endpoint; provider default configuration",
+ "billing": "api",
+ "benchmark": "Unbenchmarked (pricing only)",
+ "quality": null,
+ "costPerTask": null,
+ "inputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry nemotron-3.5-lightning-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "outputPerMillion": {
+ "value": 0,
+ "source": {
+ "url": "https://models.dev/api.json",
+ "retrievedAt": "2026-09-06T00:00:00Z",
+ "methodology": "OpenCode provider catalog entry nemotron-3.5-lightning-free: published USD per million input/output tokens. Availability cross-checked at https://opencode.ai/zen/v1/models. Promotional endpoint pricing, not a measured benchmark task or an unlimited quota guarantee."
+ }
+ },
+ "reasoningPerMillion": null,
+ "responseSeconds": null,
+ "tokensPerSecond": null,
+ "quota": null,
+ "notes": "Nemotron 3.5 Lightning Free. Free endpoint listed on 2026-09-06. Pricing is temporary; usage limits and data-use terms apply (https://opencode.ai/docs/zen/). No verified quality or speed measurement for this exact endpoint/effort; do not transfer a similarly named model's scores."
}
]
}
diff --git a/docs/MODEL-COMPARISON.md b/docs/MODEL-COMPARISON.md
index c5be91bb39..9157bfef02 100644
--- a/docs/MODEL-COMPARISON.md
+++ b/docs/MODEL-COMPARISON.md
@@ -61,3 +61,35 @@ node --input-type=module -e 'import { readFile } from "node:fs/promises"; import
Then POST the validated JSON using the normal authenticated API client and GET the catalog to verify. Reusing an ID with changed provider/model/effort/configuration/billing/benchmark is rejected. Create a new ID for a changed identity. Null or older incoming metrics retain the previous metric, and unrelated observations remain. Concurrent imports serialize the read/merge/write operation. A malformed or unsupported-version stored catalog fails visibly and cannot be overwritten by an import.
Partial source refreshes cannot erase old evidence. To retract incorrect data, an operator must deliberately repair the local catalog while preserving a recovery copy; autonomous research does not delete observations. A future schema migration must explicitly preserve installed evidence and source provenance.
+
+## Selectable axes and free Zen coverage
+
+Both axes offer benchmark score, cost per task, speed (output tokens/s), response time (seconds), and input/output token prices. `xAxis` and `yAxis` are bookmarkable and saved with the other chart settings. Changing either clears zoom bounds and selects linear scaling. The X scale can still be logarithmic; zero prices are excluded on log scale and counted explicitly. Missing selected metrics stay in the evidence table, whose quality/cost columns retain their original units regardless of chart axes. Speed and latency remain measurements of their source workloads, not the selected quality benchmark.
+
+The September 6, 2026 snapshot ships eight exact Zen endpoint IDs (the CLI prefixes these with `opencode/`) from the [live Zen model list](https://opencode.ai/zen/v1/models), cross-checked against the OpenCode entries in [Models.dev](https://models.dev/api.json): Big Pickle, DeepSeek V4 Flash Free, MiMo V2.5 Free, Ling 3.0 Flash Fin Free, Nemotron 3 Ultra Free, Nemotron 3.5 Lightning Free, and Muse Spark 1.2/1.3 Contributor Free. Models.dev lists additional historic free entries that the live endpoint no longer advertises; those were excluded.
+
+[Zen's pricing page](https://opencode.ai/docs/zen/) lists six of these; Muse 1.2 and DeepSeek V4 Flash Free are confirmed by the live endpoint plus Models.dev. Each row carries zero published input/output rates, with unknown quota, reasoning-token billing, benchmark task cost, quality and throughput. The free offers are temporary and subject to provider data-use terms. Name similarity does not prove that an AA-tested model matches Zen's revision, effort or serving configuration.
+
+Pricing-only rows appear alongside the selected benchmark as explicitly unbenchmarked evidence. Select input price versus output price to plot them, or use scenario cost with zero reasoning tokens to calculate their published input/output charge. A missing quality measurement cannot become a zero score. Migration 356 appends missing shipped Zen IDs to existing catalogs without overwriting locally researched rows; it validates the entire result before writing and refuses malformed/future-version data.
+
+## Research: skill-specific comparisons
+
+Research checked September 6, 2026. These are candidate adapters and metrics, not newly imported scores. The current benchmark selector already separates named/versioned evaluations; a future skill selector should narrow that list, never mix unrelated score scales.
+
+| Skill | Primary source / metrics | Interpretation and ingestion requirements |
+| --- | --- | --- |
+| Software coding: repository work | [SWE-bench](https://www.swebench.com/): resolved-task percentage, Verified / Pro / Multimodal tracks | Record track, dataset revision, agent scaffold, tools, budget and model effort. A full agent's result is not a model-only score; keep different scaffolds separate. Public leaderboard/repository artifacts are candidates for a reviewed snapshot importer. |
+| Software coding: code generation | [LiveCodeBench](https://livecodebench.github.io/): generation, self-repair, execution and test-output prediction | Its date-windowed contest problems address contamination. Pin release, date window and sampling/pass@k configuration; do not compare different windows. Public code/data and leaderboard are linked by the project. |
+| Creative writing | [EQ-Bench Creative Writing v3](https://eqbench.com/creative_writing.html): rubric score, Elo, repetition and slop frequency | LLM-judged results carry judge/prompt bias. Elo is relative to a participant pool; repetition/slop are diagnostics, not a substitute for human editorial judgment. Pin judge version, rubric and leaderboard snapshot. Inspect linked repository data rather than scraping an empty JavaScript-rendered table. |
+| Image analysis | [MMMU](https://mmmu-benchmark.github.io/): multimodal subject accuracy; MMMU-Pro as a separate benchmark | Measures image-grounded academic understanding, not image generation quality. Pin original/Pro, split, image resolution, prompting and tool access. Report aggregate plus subject coverage; model support for images alone is not an accuracy measurement. |
+| Tool use | [Berkeley Function Calling Leaderboard V4](https://gorilla.cs.berkeley.edu/leaderboard.html): function-calling and agentic task accuracy | Preserve version and category (single/parallel calls, multi-turn, agentic) with tool schemas and execution setup. A syntactically valid call does not prove task success. Use project-published evaluation artifacts and keep different categories distinct. |
+| General / coding / math / runtime | [Artificial Analysis API](https://artificialanalysis.ai/api-reference): intelligence, coding and math indices; individual evaluations; output throughput; time to first token | Existing sync is the easiest extension. The documented response has additional evaluation fields and TTFT. Keep TTFT separate from E2E response time and retain workload/percentile. Key-based server-side fetch, caching and attribution are required; documentation example numbers are not observations. |
+
+### Recommended next implementation
+
+1. Introduce a versioned benchmark registry with stable ID, skill, version, score unit, direction, source adapter and required evaluation configuration. Start with coding and writing; add vision and tool-use only with verified coverage. Offer visible missing-data states for skills without measurements.
+2. Keep observations per benchmark/configuration, not a single mutable `codingScore` on a model. Add explicit source model identity and separately verified endpoint mappings. An alias can aid discovery without authorizing transfer of speed, pricing or scores.
+3. Add a skill selector that filters the benchmark selector. Axis labels and preference direction come from the registry (some diagnostic metrics are lower-is-better); no universal normalized skill score or cross-benchmark averaging.
+4. Extend AA's existing explicit sync with separately versioned coding/math evaluations and TTFT, then build bounded, opt-in snapshot importers for the other sources. Confirm dataset/result redistribution terms before bundling results; public visibility alone does not establish redistribution rights.
+5. Preserve original score and provenance, evaluation date, source retrieval date, sample count/confidence interval where published, judge/scaffold and workload. Render coverage/freshness and measured-versus-reference distinctions. Stable IDs must encode materially different evaluation configurations; schema changes need compatibility/migration tests.
+6. Validate at the import/API and rendered selection boundaries: no mixed versions or units, no cross-endpoint performance transfer, no null-to-zero coercion, and no boot-time research calls. Use the existing Scheduled Task for research rather than introducing a second scheduler.
diff --git a/scripts/migrations/356-zen-comparison-coverage.js b/scripts/migrations/356-zen-comparison-coverage.js
new file mode 100644
index 0000000000..eff3bcd205
--- /dev/null
+++ b/scripts/migrations/356-zen-comparison-coverage.js
@@ -0,0 +1,24 @@
+/** Add shipped Zen evidence without replacing an install's researched observations. */
+import { readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+import { atomicWrite } from '../../server/lib/fileCore.js';
+import { modelComparisonImportSchema } from '../../server/lib/validation.js';
+
+export default {
+ async up({ rootDir }) {
+ const path = join(rootDir, 'data/model-comparison.json');
+ const raw = await readFile(path, 'utf8').catch(error => {
+ if (error.code !== 'ENOENT') throw error;
+ return null;
+ });
+ if (raw === null) return { added: 0 };
+ const current = modelComparisonImportSchema.parse(JSON.parse(raw));
+ const seed = modelComparisonImportSchema.parse(JSON.parse(await readFile(join(rootDir, 'data.reference/model-comparison.json'), 'utf8')));
+ const ids = new Set(current.observations.map(row => row.id));
+ const additions = seed.observations.filter(row => row.id.startsWith('zen-free-2026-09-') && !ids.has(row.id));
+ if (!additions.length) return { added: 0 };
+ const result = modelComparisonImportSchema.parse({ ...current, observations: [...current.observations, ...additions] });
+ await atomicWrite(path, result);
+ return { added: additions.length };
+ },
+};
diff --git a/scripts/migrations/356-zen-comparison-coverage.test.js b/scripts/migrations/356-zen-comparison-coverage.test.js
new file mode 100644
index 0000000000..3aac7a4329
--- /dev/null
+++ b/scripts/migrations/356-zen-comparison-coverage.test.js
@@ -0,0 +1,31 @@
+import { afterEach, expect, it } from 'vitest';
+import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import migration from './356-zen-comparison-coverage.js';
+
+let rootDir;
+afterEach(async () => { if (rootDir) await rm(rootDir, { recursive: true, force: true }); });
+it('upgrades an existing catalog without losing researched evidence and fails closed on future versions', async () => {
+ rootDir = await mkdtemp(join(tmpdir(), 'portos-zen-seed-'));
+ await mkdir(join(rootDir, 'data'));
+ await mkdir(join(rootDir, 'data.reference'));
+ const seed = JSON.parse(await readFile(new URL('../../data.reference/model-comparison.json', import.meta.url), 'utf8'));
+ await writeFile(join(rootDir, 'data.reference/model-comparison.json'), JSON.stringify(seed));
+ expect(await migration.up({ rootDir })).toEqual({ added: 0 });
+ const zen = seed.observations.filter(row => row.provider === 'OpenCode Zen');
+ expect(zen).toHaveLength(8);
+ expect(zen.every(row => row.quality === null && row.tokensPerSecond === null && row.inputPerMillion.value === 0)).toBe(true);
+ const researched = { ...zen[0], notes: 'Example locally researched evidence' };
+ const prior = { schemaVersion: 1, observations: [seed.observations[0], researched] };
+ const path = join(rootDir, 'data/model-comparison.json');
+ await writeFile(path, JSON.stringify(prior));
+ expect(await migration.up({ rootDir })).toEqual({ added: 7 });
+ const result = JSON.parse(await readFile(path, 'utf8'));
+ expect(result.observations.slice(0, 2)).toEqual(prior.observations);
+ expect(await migration.up({ rootDir })).toEqual({ added: 0 });
+ const future = JSON.stringify({ ...prior, schemaVersion: 99 });
+ await writeFile(path, future);
+ await expect(migration.up({ rootDir })).rejects.toThrow();
+ expect(await readFile(path, 'utf8')).toBe(future);
+});
diff --git a/scripts/prune-model-comparison-seed.js b/scripts/prune-model-comparison-seed.js
index 705afa9038..f4595884c0 100644
--- a/scripts/prune-model-comparison-seed.js
+++ b/scripts/prune-model-comparison-seed.js
@@ -45,6 +45,14 @@ export async function inScopeModels() {
models: filterSelectableModels((provider.models || []).map(model => (typeof model === 'string' ? model : model?.id))),
}));
const scope = providerCatalogSlugs(inventory);
+ // Endpoint pricing belongs to the exact serving tier, including stealth IDs
+ // that deliberately cannot resolve to a public benchmark model.
+ for (const { models } of inventory) {
+ for (const model of models) {
+ scope.add(model);
+ if (model.startsWith('opencode/')) scope.add(model.slice('opencode/'.length));
+ }
+ }
for (const model of FRONTIER_ANCHORS) scope.add(model);
return scope;
}
diff --git a/server/services/imessageSync.runSync.test.js b/server/services/imessageSync.runSync.test.js
index 35db1bed55..a8a4e80175 100644
--- a/server/services/imessageSync.runSync.test.js
+++ b/server/services/imessageSync.runSync.test.js
@@ -129,15 +129,17 @@ describe('runSync cursor/persistence contract', () => {
it('shares one in-flight pass across concurrent callers (re-entrancy guard)', async () => {
let release;
+ const { promise: persistenceStarted, resolve: markPersistenceStarted } = Promise.withResolvers();
recordEventsMock.mockImplementation(() => new Promise((resolve) => {
release = () => resolve({ recorded: 2, skipped: 0 });
+ markPersistenceStarted();
}));
autoLogTouchpointsMock.mockResolvedValue({ created: 0, matched: 0 });
const first = runSync();
const second = runSync(); // while the first is still awaiting persistence
- // Give the first call time to reach the pending recordEvents await.
- await new Promise((r) => setTimeout(r, 20));
+ // Wait for persistence itself; fixture I/O can exceed a fixed sleep on CI.
+ await persistenceStarted;
release();
const [a, b] = await Promise.all([first, second]);
expect(a).toBe(b); // same result object — the pass ran once
|