diff --git a/docs/data-transforms.md b/docs/data-transforms.md
index 1dfaf33cd..b04ae973e 100644
--- a/docs/data-transforms.md
+++ b/docs/data-transforms.md
@@ -90,7 +90,7 @@ This is the most complex and bug-prone part of the pipeline. A bad hardware key
1. Base GPU: `entry.hw.split('-')[0]` strips any `-DP` / `-MN` variant suffix from the hardware field (e.g. `"h100-8"` → `"h100"`).
2. Framework suffix: appends `_${entry.framework}`. The direct key (`h100_trt`) is tested via `isKnownGpu()` (checks whether the base GPU exists in `HW_REGISTRY`). If the direct key's base is unknown and `entry.disagg` is true, a `-disagg` variant is tried.
-3. Spec decoding suffix: if `entry.mtp === 'on'` or `entry.spec_decoding === 'mtp'`, appends `_mtp`. Otherwise, any non-`'none'` `spec_decoding` value is appended as-is (e.g. `_eagle`).
+3. Spec decoding suffix: for fixed-sequence rows, if `entry.mtp === 'on'` or `entry.spec_decoding === 'mtp'`, appends `_mtp`. Otherwise, any non-`'none'` `spec_decoding` value is appended as-is (e.g. `_eagle`). Agentic rows deliberately omit this suffix because one production curve may combine speculative and standard-decoding points; `spec_decoding` remains on each point for filtering, tooltip metadata, and point-level identity.
The resulting key's base GPU must exist in `HW_REGISTRY`. Display fields (label, suffix, gpu tooltip) are derived dynamically by `getHardwareConfig()`. Unrecognised base GPUs fall back to the `unknown` hardware config.
diff --git a/packages/app/cypress/component/chart-legend.cy.tsx b/packages/app/cypress/component/chart-legend.cy.tsx
index 8aa4e358e..15b9ab100 100644
--- a/packages/app/cypress/component/chart-legend.cy.tsx
+++ b/packages/app/cypress/component/chart-legend.cy.tsx
@@ -141,8 +141,8 @@ describe('ChartLegend (sidebar variant)', () => {
cy.get('[data-testid="offload-halo-key"]')
.should('be.visible')
- .and('contain.text', 'Dashed halo:')
.and('contain.text', 'KV offload ON')
+ .and('not.contain.text', 'Dashed halo:')
.and('not.have.class', 'no-export');
cy.get('[data-testid="offload-halo-key"] circle[stroke-dasharray="3 2"]').should('exist');
cy.get('[data-testid="chart-legend"]').then(($legend) => {
diff --git a/packages/app/cypress/component/gpu-graph.cy.tsx b/packages/app/cypress/component/gpu-graph.cy.tsx
index 82d214077..0634ffff6 100644
--- a/packages/app/cypress/component/gpu-graph.cy.tsx
+++ b/packages/app/cypress/component/gpu-graph.cy.tsx
@@ -5,7 +5,7 @@ import {
createMockChartDefinition,
createMockHardwareConfig,
} from '../support/mock-data';
-import { Precision } from '@/lib/data-mappings';
+import { Precision, Sequence } from '@/lib/data-mappings';
const defaultChartDef = createMockChartDefinition();
const hwConfig = createMockHardwareConfig();
@@ -132,6 +132,70 @@ describe('GPUGraph', () => {
cy.get('[data-testid="gpu-graph"] svg .visible-shape').should('have.length.greaterThan', 0);
});
+ it('shows spec decoding only on hover while retaining the offload halo', () => {
+ const data = [
+ createMockInferenceData({
+ hwKey: 'h100',
+ x: 32,
+ y: 180,
+ date: '2025-03-01',
+ precision: Precision.FP4,
+ benchmark_type: 'agentic_traces',
+ spec_decoding: 'mtp',
+ offload_mode: 'on',
+ }),
+ createMockInferenceData({
+ hwKey: 'h100',
+ x: 64,
+ y: 210,
+ date: '2025-03-01',
+ precision: Precision.FP4,
+ benchmark_type: 'agentic_traces',
+ spec_decoding: 'none',
+ offload_mode: 'off',
+ }),
+ ];
+
+ mountWithProviders(
+
+
+
,
+ {
+ inference: {
+ hardwareConfig: hwConfig,
+ selectedGPUs: ['h100'],
+ selectedDates: ['2025-03-01'],
+ selectedDateRange: { startDate: '', endDate: '' },
+ activeDates: new Set(['2025-03-01_h100']),
+ selectedPrecisions: [Precision.FP4],
+ selectedSequence: Sequence.AgenticTraces,
+ },
+ },
+ );
+
+ cy.get('#test-gpu-agentic-decorations svg .spec-decode-marker').should('not.exist');
+ cy.get('#test-gpu-agentic-decorations svg .offload-halo').should('have.length', 1);
+ cy.get('#test-gpu-agentic-decorations [data-testid="spec-decode-marker-key"]').should(
+ 'not.exist',
+ );
+ cy.get('#test-gpu-agentic-decorations [data-testid="offload-halo-key"]').should('exist');
+ cy.get('#test-gpu-agentic-decorations [data-testid="agentic-optimization-note"]')
+ .should('contain.text', 'Inference optimizations enabled')
+ .find('button')
+ .focus();
+ cy.contains('Each configuration may use inference optimizations').should('be.visible');
+ cy.get('#test-gpu-agentic-decorations svg .dot-group').first().trigger('mouseenter');
+ cy.get('[data-chart-tooltip]').should('contain.text', 'Speculative Decoding');
+ cy.get('[data-chart-tooltip]').should('contain.text', 'MTP');
+ });
+
it('renders date line labels along each roofline when showLineLabels is on', () => {
// Two GPUs × two dates with enough points each to form rooflines.
const data = [
diff --git a/packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts b/packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts
index 9fe0f70ce..63453aa3e 100644
--- a/packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts
+++ b/packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts
@@ -174,7 +174,6 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
it('explains the offload halo in the legend and distinguishes it from plain points', () => {
cy.get('#chart-0 [data-testid="offload-halo-key"]')
.should('be.visible')
- .and('contain.text', 'Dashed halo:')
.and('contain.text', 'KV offload ON');
cy.get('#chart-0 .offload-halo').should('have.length.at.least', 1);
cy.get('#chart-0 .dot-group').then(($points) => {
@@ -513,7 +512,6 @@ describe('X-Axis Mode Toggle — overlay path (finding #8 regression guard)', ()
);
cy.get('#chart-0 [data-testid="offload-halo-key"]')
.should('be.visible')
- .and('contain.text', 'Dashed halo:')
.and('contain.text', 'KV offload ON');
});
diff --git a/packages/app/src/app/api/v1/benchmarks/history/route.test.ts b/packages/app/src/app/api/v1/benchmarks/history/route.test.ts
index e16549347..cbb8affe5 100644
--- a/packages/app/src/app/api/v1/benchmarks/history/route.test.ts
+++ b/packages/app/src/app/api/v1/benchmarks/history/route.test.ts
@@ -81,6 +81,35 @@ describe('GET /api/v1/benchmarks/history', () => {
expect(mockGetAllBenchmarksForHistory).toHaveBeenCalledWith('mock-sql', ['dsr1'], 1024, 1024);
});
+ it('returns agentic history without numeric sequence lengths', async () => {
+ const mockRows = [{ date: '2026-03-01', benchmark_type: 'agentic_traces' }];
+ mockGetAllBenchmarksForHistory.mockResolvedValueOnce(mockRows);
+
+ const res = await GET(
+ req('/api/v1/benchmarks/history?model=DeepSeek-R1-0528&benchmarkType=agentic_traces'),
+ );
+ expect(res.status).toBe(200);
+ expect(await res.json()).toEqual(mockRows);
+ expect(mockGetAllBenchmarksForHistory).toHaveBeenCalledWith(
+ 'mock-sql',
+ ['dsr1'],
+ null,
+ null,
+ 'agentic_traces',
+ );
+ });
+
+ it('preserves fixed-sequence behavior when benchmarkType=single_turn is supplied', async () => {
+ mockGetAllBenchmarksForHistory.mockResolvedValueOnce([]);
+ const res = await GET(
+ req(
+ '/api/v1/benchmarks/history?model=DeepSeek-R1-0528&isl=1024&osl=1024&benchmarkType=single_turn',
+ ),
+ );
+ expect(res.status).toBe(200);
+ expect(mockGetAllBenchmarksForHistory).toHaveBeenCalledWith('mock-sql', ['dsr1'], 1024, 1024);
+ });
+
it('returns 500 when query throws', async () => {
mockGetAllBenchmarksForHistory.mockRejectedValueOnce(new Error('DB error'));
diff --git a/packages/app/src/app/api/v1/benchmarks/history/route.ts b/packages/app/src/app/api/v1/benchmarks/history/route.ts
index 29f2442b3..113a6ed65 100644
--- a/packages/app/src/app/api/v1/benchmarks/history/route.ts
+++ b/packages/app/src/app/api/v1/benchmarks/history/route.ts
@@ -7,6 +7,7 @@ import { getAllBenchmarksForHistory } from '@semianalysisai/inferencex-db/querie
import { cachedJson, cachedQuery } from '@/lib/api-cache';
import { loadFixture } from '@/lib/test-fixtures';
+import { agenticWorkflowMetadataOnly } from '@/lib/agentic-workflow-metadata';
export const dynamic = 'force-dynamic';
@@ -16,13 +17,26 @@ const getCachedBenchmarkHistory = cachedQuery(
'benchmark-history',
{ blobOnly: true },
);
+const getCachedAgenticBenchmarkHistory = cachedQuery(
+ (modelKeys: string[]) =>
+ getAllBenchmarksForHistory(getDb(), modelKeys, null, null, 'agentic_traces'),
+ 'benchmark-history-agentic',
+ { blobOnly: true },
+);
export async function GET(request: NextRequest) {
const model = request.nextUrl.searchParams.get('model') ?? '';
- const isl = Number(request.nextUrl.searchParams.get('isl'));
- const osl = Number(request.nextUrl.searchParams.get('osl'));
-
- if (!model || !isl || !osl) {
+ const rawIsl = request.nextUrl.searchParams.get('isl');
+ const rawOsl = request.nextUrl.searchParams.get('osl');
+ const benchmarkType = request.nextUrl.searchParams.get('benchmarkType') ?? undefined;
+ const isl = rawIsl === null ? null : Number(rawIsl);
+ const osl = rawOsl === null ? null : Number(rawOsl);
+ const isAgentic = benchmarkType === 'agentic_traces';
+
+ if (!model) {
+ return NextResponse.json({ error: 'model, isl, and osl are required' }, { status: 400 });
+ }
+ if (!isAgentic && (!isl || !osl)) {
return NextResponse.json({ error: 'model, isl, and osl are required' }, { status: 400 });
}
if (FIXTURES_MODE) return cachedJson(loadFixture('benchmarks-history'));
@@ -32,8 +46,10 @@ export async function GET(request: NextRequest) {
if (!modelKeys || modelKeys.length === 0) {
return NextResponse.json({ error: 'Unknown model' }, { status: 400 });
}
- const rows = await getCachedBenchmarkHistory(modelKeys, isl, osl);
- return cachedJson(rows);
+ const rows = isAgentic
+ ? await getCachedAgenticBenchmarkHistory(modelKeys)
+ : await getCachedBenchmarkHistory(modelKeys, isl!, osl!);
+ return cachedJson(agenticWorkflowMetadataOnly(rows));
} catch (error) {
console.error('Error fetching benchmark history:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
diff --git a/packages/app/src/app/api/v1/benchmarks/route.test.ts b/packages/app/src/app/api/v1/benchmarks/route.test.ts
index a1809efda..629339016 100644
--- a/packages/app/src/app/api/v1/benchmarks/route.test.ts
+++ b/packages/app/src/app/api/v1/benchmarks/route.test.ts
@@ -64,6 +64,41 @@ describe('GET /api/v1/benchmarks', () => {
);
});
+ it('does not expose agentic run-selection metadata on fixed-sequence rows', async () => {
+ mockGetLatestBenchmarks.mockResolvedValueOnce([
+ {
+ id: 1,
+ benchmark_type: 'single_turn',
+ workflow_run_id: 42,
+ run_started_at: '2026-08-12T10:00:00Z',
+ },
+ ]);
+
+ const res = await GET(req('/api/v1/benchmarks?model=DeepSeek-R1-0528'));
+ expect(await res.json()).toEqual([{ id: 1, benchmark_type: 'single_turn' }]);
+ });
+
+ it('keeps run-selection metadata on agentic rows', async () => {
+ mockGetLatestBenchmarks.mockResolvedValueOnce([
+ {
+ id: 1,
+ benchmark_type: 'agentic_traces',
+ workflow_run_id: 42,
+ run_started_at: '2026-08-12T10:00:00Z',
+ },
+ ]);
+
+ const res = await GET(req('/api/v1/benchmarks?model=DeepSeek-R1-0528'));
+ expect(await res.json()).toEqual([
+ {
+ id: 1,
+ benchmark_type: 'agentic_traces',
+ workflow_run_id: 42,
+ run_started_at: '2026-08-12T10:00:00Z',
+ },
+ ]);
+ });
+
it('passes date param to query when provided', async () => {
mockGetLatestBenchmarks.mockResolvedValueOnce([]);
diff --git a/packages/app/src/app/api/v1/benchmarks/route.ts b/packages/app/src/app/api/v1/benchmarks/route.ts
index ce151eccf..84ef529f8 100644
--- a/packages/app/src/app/api/v1/benchmarks/route.ts
+++ b/packages/app/src/app/api/v1/benchmarks/route.ts
@@ -11,6 +11,7 @@ import {
import { cachedJson, cachedQuery } from '@/lib/api-cache';
import { toCalculatorBenchmarkRows } from '@/lib/benchmark-api-view';
+import { agenticWorkflowMetadataOnly } from '@/lib/agentic-workflow-metadata';
import { loadFixture } from '@/lib/test-fixtures';
export const dynamic = 'force-dynamic';
@@ -18,7 +19,7 @@ export const dynamic = 'force-dynamic';
const getCachedBenchmarks = cachedQuery(
(dbModelKeys: string[], date?: string, exact?: boolean, runId?: string) =>
getLatestBenchmarks(getDb(), dbModelKeys, date, exact, runId),
- 'benchmarks',
+ 'benchmarks-agentic-run-metadata',
{ blobOnly: true },
);
@@ -26,14 +27,14 @@ const getCachedBenchmarks = cachedQuery(
// under a distinct key prefix so it never collides with the latest/as-of query.
const getCachedBenchmarksForRun = cachedQuery(
(dbModelKeys: string[], runId: string) => getBenchmarksForRun(getDb(), dbModelKeys, runId),
- 'benchmarks-run',
+ 'benchmarks-run-agentic-run-metadata',
{ blobOnly: true },
);
const getCachedCalculatorBenchmarks = cachedQuery(
async (dbModelKeys: string[], sequence: string, date?: string) =>
toCalculatorBenchmarkRows(await getLatestBenchmarks(getDb(), dbModelKeys, date), sequence),
- 'benchmarks-calculator',
+ 'benchmarks-calculator-agentic-run-metadata',
{ blobOnly: true },
);
@@ -70,7 +71,7 @@ export async function GET(request: NextRequest) {
: exactRun && runId
? await getCachedBenchmarksForRun(dbModelKeys, runId)
: await getCachedBenchmarks(dbModelKeys, date, exact || undefined, runId);
- return cachedJson(rows);
+ return cachedJson(agenticWorkflowMetadataOnly(rows));
} catch (error) {
console.error('Error fetching benchmarks:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
diff --git a/packages/app/src/app/api/v1/workflow-info/route.test.ts b/packages/app/src/app/api/v1/workflow-info/route.test.ts
index 32652b01b..e9e07faf0 100644
--- a/packages/app/src/app/api/v1/workflow-info/route.test.ts
+++ b/packages/app/src/app/api/v1/workflow-info/route.test.ts
@@ -89,6 +89,23 @@ describe('GET /api/v1/workflow-info', () => {
expect(mockGetRunConfigsByDate).toHaveBeenCalledWith('mock-sql', '2026-03-01');
});
+ it('scopes run coverage only when agentic is explicitly requested', async () => {
+ mockGetWorkflowRunsByDate.mockResolvedValueOnce([]);
+ mockGetChangelogByDate.mockResolvedValueOnce([]);
+ mockGetDateConfigs.mockResolvedValueOnce([]);
+ mockGetRunConfigsByDate.mockResolvedValueOnce([]);
+
+ const res = await GET(
+ req('/api/v1/workflow-info?date=2026-03-01&benchmarkType=agentic_traces'),
+ );
+ expect(res.status).toBe(200);
+ expect(mockGetRunConfigsByDate).toHaveBeenCalledWith(
+ 'mock-sql',
+ '2026-03-01',
+ 'agentic_traces',
+ );
+ });
+
it('accepts empty date param (returns all)', async () => {
mockGetWorkflowRunsByDate.mockResolvedValueOnce([]);
mockGetChangelogByDate.mockResolvedValueOnce([]);
diff --git a/packages/app/src/app/api/v1/workflow-info/route.ts b/packages/app/src/app/api/v1/workflow-info/route.ts
index 4c5c12bc2..d8d646e97 100644
--- a/packages/app/src/app/api/v1/workflow-info/route.ts
+++ b/packages/app/src/app/api/v1/workflow-info/route.ts
@@ -14,19 +14,34 @@ import { loadFixture } from '@/lib/test-fixtures';
export const dynamic = 'force-dynamic';
-const getCachedWorkflowInfo = cachedQuery(async (date: string) => {
+async function loadWorkflowInfo(date: string, benchmarkType?: 'agentic_traces') {
const sql = getDb();
const [runs, changelogs, configs, runConfigs] = await Promise.all([
getWorkflowRunsByDate(sql, date),
getChangelogByDate(sql, date),
getDateConfigs(sql, date),
- getRunConfigsByDate(sql, date),
+ benchmarkType ? getRunConfigsByDate(sql, date, benchmarkType) : getRunConfigsByDate(sql, date),
]);
return { runs, changelogs, configs, runConfigs };
-}, 'workflow-info');
+}
+
+// Preserve the established public cache and response for calls without a
+// scenario. Scenario-scoped calls use a separate cache namespace.
+const getCachedWorkflowInfo = cachedQuery(
+ (date: string) => loadWorkflowInfo(date),
+ 'workflow-info',
+);
+const getCachedScenarioWorkflowInfo = cachedQuery(
+ (date: string) => loadWorkflowInfo(date, 'agentic_traces'),
+ 'workflow-info-scenario',
+);
export async function GET(request: NextRequest) {
const date = request.nextUrl.searchParams.get('date') ?? '';
+ const benchmarkType =
+ request.nextUrl.searchParams.get('benchmarkType') === 'agentic_traces'
+ ? 'agentic_traces'
+ : undefined;
if (date && !/^\d{4}-\d{2}-\d{2}$/u.test(date)) {
return NextResponse.json(
{ error: 'Invalid date format (YYYY-MM-DD required)' },
@@ -36,7 +51,9 @@ export async function GET(request: NextRequest) {
if (FIXTURES_MODE) return cachedJson(loadFixture('workflow-info'));
try {
- const data = await getCachedWorkflowInfo(date);
+ const data = benchmarkType
+ ? await getCachedScenarioWorkflowInfo(date)
+ : await getCachedWorkflowInfo(date);
return cachedJson(data);
} catch (error) {
console.error('Error fetching workflow info:', error);
diff --git a/packages/app/src/components/inference/InferenceContext.tsx b/packages/app/src/components/inference/InferenceContext.tsx
index 152bc58b2..991c583dc 100644
--- a/packages/app/src/components/inference/InferenceContext.tsx
+++ b/packages/app/src/components/inference/InferenceContext.tsx
@@ -523,7 +523,13 @@ export function InferenceProvider({
if (rowToSequence(r) !== effectiveSequence) return false;
if (!effectivePrecisions.includes(r.precision)) return false;
if (!r.hardware) return false;
- const hwKey = buildAvailabilityHwKey(r.hardware, r.framework, r.spec_method, r.disagg);
+ const hwKey = buildAvailabilityHwKey(
+ r.hardware,
+ r.framework,
+ r.spec_method,
+ r.disagg,
+ r.benchmark_type,
+ );
return selectedGPUs.includes(hwKey);
});
const dates = [...new Set(rows.map((r) => r.date))].toSorted();
@@ -548,7 +554,13 @@ export function InferenceProvider({
if (rowToSequence(r) !== effectiveSequence) continue;
if (!effectivePrecisions.includes(r.precision)) continue;
if (!r.hardware) continue;
- const hwKey = buildAvailabilityHwKey(r.hardware, r.framework, r.spec_method, r.disagg);
+ const hwKey = buildAvailabilityHwKey(
+ r.hardware,
+ r.framework,
+ r.spec_method,
+ r.disagg,
+ r.benchmark_type,
+ );
if (isKnownGpu(hwKey)) hwKeys.add(hwKey);
}
return [...hwKeys]
diff --git a/packages/app/src/components/inference/hooks/useChartData.test.ts b/packages/app/src/components/inference/hooks/useChartData.test.ts
index b61cfb17b..94c9075d3 100644
--- a/packages/app/src/components/inference/hooks/useChartData.test.ts
+++ b/packages/app/src/components/inference/hooks/useChartData.test.ts
@@ -1,6 +1,7 @@
import { describe, it, expect } from 'vitest';
import chartDefinitions from '@/components/inference/inference-chart-config.json';
+import { dedupeAgenticHistoryRuns } from '@/lib/benchmark-run-selection';
import type { InferenceData } from '@/components/inference/types';
import { EMPTY_QUICK_FILTERS } from '@/components/inference/utils/quickFilters';
@@ -24,7 +25,10 @@ interface DedupeInput {
disagg: boolean;
precision: string;
offload_mode?: string | null;
+ benchmark_type?: string;
date: string;
+ workflow_run_id?: number;
+ run_started_at?: string | null;
}
const drow = (over: Partial = {}): DedupeInput => ({
@@ -86,6 +90,160 @@ describe('dedupeRowsToLatestPerConfig', () => {
];
expect(dedupeRowsToLatestPerConfig(rows).map((r) => r.id)).toEqual([2]);
});
+
+ it('dedupes mixed agentic spec methods as one curve', () => {
+ const rows = [
+ drow({ id: 1, benchmark_type: 'agentic_traces', spec_method: 'none', date: '2026-06-01' }),
+ drow({ id: 2, benchmark_type: 'agentic_traces', spec_method: 'mtp', date: '2026-06-03' }),
+ drow({ id: 3, benchmark_type: 'agentic_traces', spec_method: 'eagle', date: '2026-06-03' }),
+ ];
+
+ expect(dedupeRowsToLatestPerConfig(rows).map((r) => r.id)).toEqual([2, 3]);
+ });
+
+ it('continues deduping fixed-sequence spec methods independently', () => {
+ const rows = [
+ drow({ id: 1, benchmark_type: 'single_turn', spec_method: 'none', date: '2026-06-01' }),
+ drow({ id: 2, benchmark_type: 'single_turn', spec_method: 'mtp', date: '2026-06-03' }),
+ ];
+
+ expect(dedupeRowsToLatestPerConfig(rows).map((r) => r.id)).toEqual([1, 2]);
+ });
+
+ it('keeps mixed agentic points from only the newest same-day workflow run', () => {
+ const rows = [
+ drow({
+ id: 1,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'none',
+ workflow_run_id: 10,
+ run_started_at: '2026-06-03T10:00:00Z',
+ }),
+ drow({
+ id: 2,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'mtp',
+ workflow_run_id: 10,
+ run_started_at: '2026-06-03T10:00:00Z',
+ }),
+ drow({
+ id: 3,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'mtp',
+ workflow_run_id: 11,
+ run_started_at: '2026-06-03T12:00:00Z',
+ }),
+ ];
+
+ expect(dedupeRowsToLatestPerConfig(rows).map((r) => r.id)).toEqual([3]);
+ });
+
+ it('keeps every spec method produced by the winning agentic workflow run', () => {
+ const rows = [
+ drow({
+ id: 1,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'none',
+ workflow_run_id: 12,
+ run_started_at: '2026-06-03T12:00:00Z',
+ }),
+ drow({
+ id: 2,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'mtp',
+ workflow_run_id: 12,
+ run_started_at: '2026-06-03T12:00:00Z',
+ }),
+ drow({
+ id: 3,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'eagle',
+ workflow_run_id: 12,
+ run_started_at: '2026-06-03T12:00:00Z',
+ }),
+ ];
+
+ expect(dedupeRowsToLatestPerConfig(rows).map((r) => r.spec_method)).toEqual([
+ 'none',
+ 'mtp',
+ 'eagle',
+ ]);
+ });
+});
+
+describe('dedupeAgenticHistoryRuns', () => {
+ it('keeps the newest agentic workflow per series on each date', () => {
+ const rows = [
+ drow({
+ id: 1,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'none',
+ workflow_run_id: 20,
+ run_started_at: '2026-06-01T10:00:00Z',
+ }),
+ drow({
+ id: 2,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'mtp',
+ workflow_run_id: 21,
+ run_started_at: '2026-06-01T12:00:00Z',
+ }),
+ drow({
+ id: 3,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'none',
+ date: '2026-06-02',
+ workflow_run_id: 22,
+ run_started_at: '2026-06-02T10:00:00Z',
+ }),
+ ];
+
+ expect(dedupeAgenticHistoryRuns(rows).map((row) => row.id)).toEqual([2, 3]);
+ });
+
+ it('keeps mixed spec points together in the winning workflow on every date', () => {
+ const rows = [
+ drow({
+ id: 1,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'none',
+ workflow_run_id: 30,
+ run_started_at: '2026-06-01T10:00:00Z',
+ }),
+ drow({
+ id: 2,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'mtp',
+ workflow_run_id: 30,
+ run_started_at: '2026-06-01T10:00:00Z',
+ }),
+ drow({
+ id: 3,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'eagle',
+ date: '2026-06-02',
+ workflow_run_id: 31,
+ run_started_at: '2026-06-02T10:00:00Z',
+ }),
+ drow({
+ id: 4,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'none',
+ date: '2026-06-02',
+ workflow_run_id: 31,
+ run_started_at: '2026-06-02T10:00:00Z',
+ }),
+ ];
+
+ expect(
+ dedupeAgenticHistoryRuns(rows).map((row) => [row.date, row.workflow_run_id, row.spec_method]),
+ ).toEqual([
+ ['2026-06-01', 30, 'none'],
+ ['2026-06-01', 30, 'mtp'],
+ ['2026-06-02', 31, 'eagle'],
+ ['2026-06-02', 31, 'none'],
+ ]);
+ });
});
describe('buildComparisonDates', () => {
diff --git a/packages/app/src/components/inference/hooks/useChartData.ts b/packages/app/src/components/inference/hooks/useChartData.ts
index 2a9f7904f..1f76ebbb1 100644
--- a/packages/app/src/components/inference/hooks/useChartData.ts
+++ b/packages/app/src/components/inference/hooks/useChartData.ts
@@ -24,6 +24,10 @@ import {
hardwareKeyMatchesAnyBase,
} from '@/lib/constants';
import { mergeRunScopedRows, transformBenchmarkRows } from '@/lib/benchmark-transform';
+import {
+ dedupeAgenticHistoryRuns,
+ dedupeRowsToLatestPerConfig as dedupeLatestBenchmarkSeries,
+} from '@/lib/benchmark-run-selection';
import { Sequence, type Model } from '@/lib/data-mappings';
import { calculateCostsForGpus, calculatePowerForGpus } from '@/lib/utils';
import { overviewServingSeriesKey, type OverviewServingSeriesRow } from '@/lib/overview-data';
@@ -159,32 +163,20 @@ interface DedupeRow {
disagg: boolean;
precision: string;
offload_mode?: string | null;
+ benchmark_type?: string;
date: string;
+ workflow_run_id?: number;
+ run_started_at?: string | null;
}
// offload_mode normalized `?? 'off'` to match the SQL layer's getBenchmarksForRun
// lineKey — agentic offload=on and offload=off are distinct series.
-const dedupeSeriesKey = (r: DedupeRow): string =>
- `${r.hardware}|${r.framework}|${r.spec_method}|${r.disagg}|${r.precision}|${r.offload_mode ?? 'off'}`;
-
/**
- * For each series — (hardware, framework, spec_method, disagg, precision,
- * offload_mode) — keep only the rows from that series' most recent date. When
- * parallelism settings change between runs, old config_ids create stale points
- * under the same legend line; dropping all-but-latest removes them.
- *
- * Without `offload_mode` in the key, an offload=on sweep ingested on a LATER date
- * than the offload=off sweep would win the shared group and silently drop the
- * (earlier-dated) offload=off variant — a data-loss regression.
+ * Keep only the newest workflow run for each chart series. Agentic series omit
+ * point-level spec decoding from their curve identity; fixed-sequence series do not.
*/
export function dedupeRowsToLatestPerConfig(rows: T[]): T[] {
- const maxDatePerGroup = new Map();
- for (const r of rows) {
- const k = dedupeSeriesKey(r);
- const cur = maxDatePerGroup.get(k);
- if (!cur || r.date > cur) maxDatePerGroup.set(k, r.date);
- }
- return rows.filter((r) => r.date === maxDatePerGroup.get(dedupeSeriesKey(r)));
+ return dedupeLatestBenchmarkSeries(rows);
}
/**
@@ -353,12 +345,15 @@ export function useChartData(
selectedRunDate ? { ...r, date: selectedRunDate, actualDate: r.date } : r,
);
if (comparisonDates.length === 0) return mainRows;
- const extraRows = comparisonQueries.flatMap((q, i) =>
- filterOverviewHistoryRows(
+ const extraRows = comparisonQueries.flatMap((q, i) => {
+ const filtered = filterOverviewHistoryRows(
(q.data ?? []).filter(seqFilter),
overviewHistoryPair?.baselineConfigKey,
- ).map((r) => ({ ...r, date: comparisonDates[i], actualDate: r.date })),
- );
+ );
+ const selected =
+ selectedSequence === Sequence.AgenticTraces ? dedupeAgenticHistoryRuns(filtered) : filtered;
+ return selected.map((r) => ({ ...r, date: comparisonDates[i], actualDate: r.date }));
+ });
return [...mainRows, ...extraRows];
}, [
allRows,
diff --git a/packages/app/src/components/inference/hooks/useInterpolatedTrendData.ts b/packages/app/src/components/inference/hooks/useInterpolatedTrendData.ts
index 90e7c5af3..bfef739d3 100644
--- a/packages/app/src/components/inference/hooks/useInterpolatedTrendData.ts
+++ b/packages/app/src/components/inference/hooks/useInterpolatedTrendData.ts
@@ -15,7 +15,8 @@ import { getHardwareKey } from '@/lib/chart-utils';
import { getGpuSpecs, isKnownGpu } from '@/lib/constants';
import { rowToAggDataEntry } from '@/lib/benchmark-transform';
import type { BenchmarkRow } from '@/lib/api';
-import type { Model, Sequence } from '@/lib/data-mappings';
+import { dedupeAgenticHistoryRuns } from '@/lib/benchmark-run-selection';
+import { Sequence, type Model } from '@/lib/data-mappings';
// Trend points never sit on a roofline — they're synthetic per-(date, config)
// aggregates, not the per-load Pareto-frontier points the chart marks. Hardcode
@@ -246,6 +247,7 @@ export function useInterpolatedTrendData({
enabled ? selectedModel : '',
seqIslOsl?.isl ?? 0,
seqIslOsl?.osl ?? 0,
+ selectedSequence === Sequence.AgenticTraces ? 'agentic_traces' : undefined,
);
// Build lightweight InferenceData points grouped by date and hwKey.
@@ -255,7 +257,7 @@ export function useInterpolatedTrendData({
const result = new Map>();
- for (const row of allRows) {
+ for (const row of dedupeAgenticHistoryRuns(allRows)) {
if (!selectedPrecisions.includes(row.precision)) continue;
const point = rowToLightweightPoint(row);
diff --git a/packages/app/src/components/inference/replay/ReplayPanel.tsx b/packages/app/src/components/inference/replay/ReplayPanel.tsx
index 7273ed24c..3d36bca0f 100644
--- a/packages/app/src/components/inference/replay/ReplayPanel.tsx
+++ b/packages/app/src/components/inference/replay/ReplayPanel.tsx
@@ -21,6 +21,7 @@ import {
} from '@/components/ui/select';
import { useBenchmarkHistory } from '@/hooks/api/use-benchmark-history';
import { track } from '@/lib/analytics';
+import { Sequence } from '@/lib/data-mappings';
import { cn } from '@/lib/utils';
import { buildReplayTimeline, computeFullRunDomain } from './buildReplayTimeline';
@@ -68,7 +69,12 @@ export default function ReplayPanel({
const { selectedModel, selectedSequence, activeHwTypes } = inference;
const { isl = 0, osl = 0 } = sequenceToIslOsl(selectedSequence) ?? {};
- const history = useBenchmarkHistory(selectedModel, isl, osl);
+ const history = useBenchmarkHistory(
+ selectedModel,
+ isl,
+ osl,
+ selectedSequence === Sequence.AgenticTraces ? 'agentic_traces' : undefined,
+ );
const effectiveX =
chartDefinition.chartType === 'e2e'
diff --git a/packages/app/src/components/inference/replay/__tests__/buildReplayTimeline.test.ts b/packages/app/src/components/inference/replay/__tests__/buildReplayTimeline.test.ts
index 6e8025e66..6dd9aac94 100644
--- a/packages/app/src/components/inference/replay/__tests__/buildReplayTimeline.test.ts
+++ b/packages/app/src/components/inference/replay/__tests__/buildReplayTimeline.test.ts
@@ -216,6 +216,45 @@ describe('buildReplayTimeline', () => {
expect(t.configs.length).toBeGreaterThanOrEqual(2);
});
+ it('keeps overlapping agentic MTP and standard-decoding replay points distinct', () => {
+ const rows = [
+ baseRow({
+ benchmark_type: 'agentic_traces',
+ spec_method: 'none',
+ isl: null,
+ osl: null,
+ metrics: { tput_per_gpu: 1000, median_itl: 0.02 },
+ }),
+ baseRow({
+ benchmark_type: 'agentic_traces',
+ spec_method: 'mtp',
+ isl: null,
+ osl: null,
+ metrics: { tput_per_gpu: 1200, median_itl: 1 / 55 },
+ }),
+ ];
+
+ const t = buildReplayTimeline(rows, interactivityChartDef, 'y_tpPerGpu', null, ['fp4']);
+ expect(t.configs).toHaveLength(2);
+ expect(new Set(t.configs.map((config) => config.hwKey))).toEqual(new Set(['h100_trt']));
+ expect(t.configs.map((config) => config.configId).toSorted()).toEqual([
+ 'h100_trt|fp4|0|32|0|0|0|spec-mtp',
+ 'h100_trt|fp4|0|32|0|0|0|spec-none',
+ ]);
+ });
+
+ it('preserves fixed-sequence replay identity when offload metadata differs', () => {
+ const rows = [
+ baseRow({ benchmark_type: 'single_turn', offload_mode: 'off' }),
+ baseRow({ benchmark_type: 'single_turn', offload_mode: 'on' }),
+ ];
+
+ const t = buildReplayTimeline(rows, interactivityChartDef, 'y_tpPerGpu', null, ['fp4']);
+ expect(t.configs).toHaveLength(1);
+ expect(t.configs[0].configId).not.toContain('offload');
+ expect(t.configs[0].configId).not.toContain('spec-');
+ });
+
it('computes a global x/y domain spanning all observations', () => {
const rows = [
baseRow({ date: '2025-01-01', metrics: { tput_per_gpu: 100, median_intvty: 10 } }),
diff --git a/packages/app/src/components/inference/replay/buildReplayTimeline.ts b/packages/app/src/components/inference/replay/buildReplayTimeline.ts
index 917616046..717b12840 100644
--- a/packages/app/src/components/inference/replay/buildReplayTimeline.ts
+++ b/packages/app/src/components/inference/replay/buildReplayTimeline.ts
@@ -8,6 +8,8 @@ import type {
InferenceData,
YAxisMetricKey,
} from '@/components/inference/types';
+import { agenticSpecDecodingKeySuffix } from '@/components/inference/utils/point-identity';
+import { dedupeAgenticHistoryRuns } from '@/lib/benchmark-run-selection';
import type { PerStepValue } from './interpolateAtTime';
@@ -81,10 +83,12 @@ export function computeFullRunDomain(
return { x: safeDomain(xMin, xMax), y: safeDomain(yMin, yMax) };
}
-const buildPointConfigId = (point: InferenceData): string => {
+const buildReplayPointConfigId = (point: InferenceData): string => {
let key = `${point.hwKey}|${point.precision}|${point.tp}|${point.conc}|${point.decode_ep ?? 0}|${point.prefill_tp ?? 0}|${point.prefill_ep ?? 0}`;
if (point.disagg) key += `|disagg|${point.num_prefill_gpu ?? 0}|${point.num_decode_gpu ?? 0}`;
- return key;
+ // Preserve the pre-existing replay identity for fixed-sequence points. Agentic
+ // curves need only the decode-method suffix because their hwKey now merges it.
+ return key + agenticSpecDecodingKeySuffix(point);
};
const safeDomain = (lo: number, hi: number): [number, number] => {
@@ -155,7 +159,7 @@ export function buildReplayTimeline(
let yMax = -Infinity;
const dateSet = new Set();
- for (const row of rows) {
+ for (const row of dedupeAgenticHistoryRuns(rows)) {
if (!selectedPrecisions.includes(row.precision)) continue;
const entry = rowToAggDataEntry(row);
@@ -181,7 +185,7 @@ export function buildReplayTimeline(
if (xVal <= 0 || yMetric <= 0) continue;
const finalPoint: InferenceData = { ...point, x: xVal, y: yMetric };
- const configId = buildPointConfigId(finalPoint);
+ const configId = buildReplayPointConfigId(finalPoint);
const dateMs = Date.parse(`${row.date}T00:00:00Z`);
if (Number.isNaN(dateMs)) continue;
diff --git a/packages/app/src/components/inference/ui/AgenticOptimizationNote.tsx b/packages/app/src/components/inference/ui/AgenticOptimizationNote.tsx
new file mode 100644
index 000000000..020d0c38b
--- /dev/null
+++ b/packages/app/src/components/inference/ui/AgenticOptimizationNote.tsx
@@ -0,0 +1,53 @@
+import { Info } from 'lucide-react';
+
+import {
+ TooltipContent,
+ TooltipProvider,
+ TooltipRoot,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import { useLocale } from '@/lib/use-locale';
+
+const STRINGS = {
+ en: {
+ label: 'Inference optimizations enabled',
+ aria: 'About inference optimizations',
+ details:
+ 'Each configuration may use inference optimizations such as speculative decoding. Hover over a point to see its exact settings.',
+ },
+ zh: {
+ label: '已启用推理优化',
+ aria: '关于推理优化',
+ details: '每项配置可能使用推测解码等推理优化。将鼠标悬停在数据点上可查看其具体设置。',
+ },
+} as const;
+
+/** Agentic-only legend note; point tooltips carry the exact optimization method. */
+export function AgenticOptimizationNote() {
+ const t = STRINGS[useLocale()];
+
+ return (
+
+ *{t.label}
+
+
+
+
+
+
+ {t.details}
+
+
+
+
+ );
+}
diff --git a/packages/app/src/components/inference/ui/ChartDisplay.tsx b/packages/app/src/components/inference/ui/ChartDisplay.tsx
index f80f823bb..85160daf2 100644
--- a/packages/app/src/components/inference/ui/ChartDisplay.tsx
+++ b/packages/app/src/components/inference/ui/ChartDisplay.tsx
@@ -43,7 +43,7 @@ import { useUnofficialRun } from '@/components/unofficial-run-provider';
import {
type Model,
type Precision,
- type Sequence,
+ Sequence,
getModelLabel,
getPrecisionLabel,
getSequenceLabel,
@@ -218,12 +218,21 @@ export default function ChartDisplay() {
setSelectedXAxisMode,
quickFilters,
} = useInference();
+ const selectedBenchmarkType: 'single_turn' | 'agentic_traces' =
+ selectedSequence === Sequence.AgenticTraces ? 'agentic_traces' : 'single_turn';
+ const workflowInfoBenchmarkType =
+ selectedSequence === Sequence.AgenticTraces ? 'agentic_traces' : undefined;
const {
changelogs,
loading: changelogsLoading,
totalDatesQueried,
- } = useComparisonChangelogs(selectedGPUs, selectedDateRange, dateRangeAvailableDates);
+ } = useComparisonChangelogs(
+ selectedGPUs,
+ selectedDateRange,
+ dateRangeAvailableDates,
+ workflowInfoBenchmarkType,
+ );
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
@@ -232,7 +241,6 @@ export default function ChartDisplay() {
() => DISPLAY_MODEL_TO_DB[selectedModel] ?? [selectedModel],
[selectedModel],
);
-
// Stable run numbering shared by the changelog and the chart legend: each of a
// date's runs gets a fixed 1-based number (by start time) regardless of which
// are on the chart, so the two surfaces always show the same #N for a run and a
@@ -241,14 +249,17 @@ export default function ChartDisplay() {
const runNumbering = useMemo(() => {
const map = new Map();
for (const c of changelogs) {
- dataRunsForDate(c.runConfigs, { modelDbKeys, selectedGPUs, selectedPrecisions }).forEach(
- (run, idx) => {
- map.set(makeRunComparisonEntry(c.date, run.runId), idx + 1);
- },
- );
+ dataRunsForDate(c.runConfigs, {
+ modelDbKeys,
+ selectedGPUs,
+ selectedPrecisions,
+ benchmarkType: selectedBenchmarkType,
+ }).forEach((run, idx) => {
+ map.set(makeRunComparisonEntry(c.date, run.runId), idx + 1);
+ });
}
return map;
- }, [changelogs, modelDbKeys, selectedGPUs, selectedPrecisions]);
+ }, [changelogs, modelDbKeys, selectedGPUs, selectedPrecisions, selectedBenchmarkType]);
// Expand a plain-date selection into one entry per run once that date's runs are
// known. Picking a date that has multiple runs shows each run as its own series
@@ -257,7 +268,12 @@ export default function ChartDisplay() {
// in sync. Idempotent: after expansion no expandable plain date remains.
useEffect(() => {
const runConfigsByDate = new Map(changelogs.map((c) => [c.date, c.runConfigs]));
- const scope = { modelDbKeys, selectedGPUs, selectedPrecisions };
+ const scope = {
+ modelDbKeys,
+ selectedGPUs,
+ selectedPrecisions,
+ benchmarkType: selectedBenchmarkType,
+ };
setSelectedDatesFromRunExpansion((prev) => {
let changed = false;
const out: string[] = [];
@@ -283,6 +299,7 @@ export default function ChartDisplay() {
modelDbKeys,
selectedGPUs,
selectedPrecisions,
+ selectedBenchmarkType,
selectedDates,
setSelectedDatesFromRunExpansion,
]);
@@ -998,6 +1015,7 @@ export default function ChartDisplay() {
selectedGPUs={selectedGPUs}
selectedPrecisions={selectedPrecisions}
modelDbKeys={modelDbKeys}
+ selectedSequence={selectedSequence}
loading={changelogsLoading}
totalDatesQueried={totalDatesQueried}
selectedDates={selectedDates}
diff --git a/packages/app/src/components/inference/ui/ComparisonChangelog.tsx b/packages/app/src/components/inference/ui/ComparisonChangelog.tsx
index ae906b750..f87a8084c 100644
--- a/packages/app/src/components/inference/ui/ComparisonChangelog.tsx
+++ b/packages/app/src/components/inference/ui/ComparisonChangelog.tsx
@@ -16,6 +16,7 @@ import {
import { makeRunComparisonEntry } from '@/components/inference/utils/comparisonEntry';
import { dataRunsForDate } from '@/components/inference/utils/runEnumeration';
import { getHardwareConfig } from '@/lib/constants';
+import { Sequence, type Sequence as SequenceType } from '@/lib/data-mappings';
import { getDisplayLabel, updateRepoUrl } from '@/lib/utils';
/** Git Commit and Workflow Run external links for a run, each shown when known. */
@@ -72,6 +73,7 @@ interface ComparisonChangelogProps {
* model-scoped).
*/
modelDbKeys: string[];
+ selectedSequence: SequenceType;
loading?: boolean;
totalDatesQueried: number;
selectedDates: string[];
@@ -88,6 +90,7 @@ export default function ComparisonChangelog({
selectedGPUs,
selectedPrecisions,
modelDbKeys,
+ selectedSequence,
loading,
totalDatesQueried,
selectedDates,
@@ -98,6 +101,10 @@ export default function ComparisonChangelog({
firstAvailableDate,
}: ComparisonChangelogProps) {
const [isExpanded, setIsExpanded] = useState(true);
+ const benchmarkType =
+ selectedSequence === Sequence.AgenticTraces ? 'agentic_traces' : 'single_turn';
+ const changelogBenchmarkType =
+ selectedSequence === Sequence.AgenticTraces ? 'agentic_traces' : undefined;
// Filter changelog entries to only show those matching selected GPUs and precisions.
// Always keep range endpoints and first appearance date visible.
@@ -120,7 +127,7 @@ export default function ComparisonChangelog({
return (
modelDbKeys.some((m) => key.startsWith(`${m}-`)) &&
precSet.has(precision) &&
- selectedGPUs.some((gpu) => configKeyMatchesHwKey(key, gpu))
+ selectedGPUs.some((gpu) => configKeyMatchesHwKey(key, gpu, changelogBenchmarkType))
);
}),
),
@@ -136,7 +143,14 @@ export default function ComparisonChangelog({
return mapped
.filter((item) => item.entries.length > 0 || pinnedDates.has(item.date))
.toSorted((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
- }, [changelogs, modelDbKeys, selectedGPUs, selectedPrecisions, pinnedDates]);
+ }, [
+ changelogs,
+ modelDbKeys,
+ selectedGPUs,
+ selectedPrecisions,
+ pinnedDates,
+ changelogBenchmarkType,
+ ]);
const datesOnChart = useMemo(() => {
const set = new Set(selectedDates);
@@ -156,10 +170,10 @@ export default function ComparisonChangelog({
return (
modelDbKeys.some((m) => key.startsWith(`${m}-`)) &&
precSet.has(precision) &&
- selectedGPUs.some((gpu) => configKeyMatchesHwKey(key, gpu))
+ selectedGPUs.some((gpu) => configKeyMatchesHwKey(key, gpu, changelogBenchmarkType))
);
});
- }, [modelDbKeys, selectedPrecisions, selectedGPUs]);
+ }, [modelDbKeys, selectedPrecisions, selectedGPUs, changelogBenchmarkType]);
/**
* Every run that produced data for the selected config on a date, earliest
@@ -173,6 +187,7 @@ export default function ComparisonChangelog({
modelDbKeys,
selectedGPUs,
selectedPrecisions,
+ benchmarkType,
}).map((run) => {
const cl = clByRun.get(run.runId);
return {
@@ -183,7 +198,7 @@ export default function ComparisonChangelog({
};
});
},
- [modelDbKeys, selectedGPUs, selectedPrecisions, entryMatchesSelection],
+ [modelDbKeys, selectedGPUs, selectedPrecisions, benchmarkType, entryMatchesSelection],
);
// Entries the "Add all to chart" button would add: every run not yet on the
@@ -219,7 +234,9 @@ export default function ComparisonChangelog({
if (selectedGPUs.length <= 1) return '';
return selectedGPUs
.filter((gpu) =>
- entries.some((e) => e.config_keys.some((k) => configKeyMatchesHwKey(k, gpu))),
+ entries.some((e) =>
+ e.config_keys.some((k) => configKeyMatchesHwKey(k, gpu, changelogBenchmarkType)),
+ ),
)
.map((gpu) => getDisplayLabel(getHardwareConfig(gpu, displayModel)))
.join(', ');
diff --git a/packages/app/src/components/inference/ui/GPUGraph.tsx b/packages/app/src/components/inference/ui/GPUGraph.tsx
index ea820653f..1171ab66b 100644
--- a/packages/app/src/components/inference/ui/GPUGraph.tsx
+++ b/packages/app/src/components/inference/ui/GPUGraph.tsx
@@ -8,7 +8,7 @@ import { useTheme } from 'next-themes';
import { useInference } from '@/components/inference/InferenceContext';
import ChartLegend from '@/components/ui/chart-legend';
import { getHardwareConfig, getModelSortIndex } from '@/lib/constants';
-import { getChartWatermark } from '@/lib/data-mappings';
+import { getChartWatermark, Sequence } from '@/lib/data-mappings';
import { generateGpuDateColors } from '@/lib/dynamic-colors';
import { useLocale } from '@/lib/use-locale';
import { formatNumber, getDisplayLabel, updateRepoUrl } from '@/lib/utils';
@@ -62,6 +62,7 @@ import {
OFFLOAD_HALO_STROKE_WIDTH,
OffloadHaloLegendKey,
} from '@/components/inference/ui/OffloadHaloLegendKey';
+import { AgenticOptimizationNote } from '@/components/inference/ui/AgenticOptimizationNote';
const CHART_MARGIN = { top: 24, right: 10, bottom: 60, left: 60 };
@@ -115,6 +116,7 @@ const GPUGraph = React.memo(
selectedGPUs,
selectedDateRange,
selectedDates,
+ selectedSequence,
setSelectedDates,
toggleActiveDate,
removeActiveDate,
@@ -1030,7 +1032,14 @@ const GPUGraph = React.memo(
},
]}
precisionIndicators={selectedPrecisions}
- keyIndicators={hasOffloadHalo ? : undefined}
+ keyIndicators={
+ hasOffloadHalo || selectedSequence === Sequence.AgenticTraces ? (
+ <>
+ {hasOffloadHalo && }
+ {selectedSequence === Sequence.AgenticTraces && }
+ >
+ ) : undefined
+ }
/>
}
/>
diff --git a/packages/app/src/components/inference/ui/OffloadHaloLegendKey.tsx b/packages/app/src/components/inference/ui/OffloadHaloLegendKey.tsx
index 82c9f20fb..c7e38e10b 100644
--- a/packages/app/src/components/inference/ui/OffloadHaloLegendKey.tsx
+++ b/packages/app/src/components/inference/ui/OffloadHaloLegendKey.tsx
@@ -34,10 +34,7 @@ export function OffloadHaloLegendKey() {
strokeDasharray={OFFLOAD_HALO_DASHARRAY}
/>
-
- Dashed halo:
- KV offload ON
-
+ KV offload ON
);
}
diff --git a/packages/app/src/components/inference/ui/ScatterGraph.decoration.test.tsx b/packages/app/src/components/inference/ui/ScatterGraph.decoration.test.tsx
index 7b7d57da9..57320c91c 100644
--- a/packages/app/src/components/inference/ui/ScatterGraph.decoration.test.tsx
+++ b/packages/app/src/components/inference/ui/ScatterGraph.decoration.test.tsx
@@ -22,7 +22,9 @@ vi.mock('@/lib/d3-chart/chart-setup', { spy: true });
vi.mock('@/lib/analytics', () => ({ track: vi.fn() }));
vi.mock('next-themes', () => ({ useTheme: () => ({ resolvedTheme: 'dark' }) }));
// The legend is React-rendered (covered elsewhere) — keep the tree light.
-vi.mock('@/components/ui/chart-legend', () => ({ default: () => null }));
+vi.mock('@/components/ui/chart-legend', () => ({
+ default: ({ keyIndicators }: { keyIndicators?: React.ReactNode }) => keyIndicators ?? null,
+}));
const inferenceState = vi.hoisted(() => ({ current: {} as Record }));
vi.mock('@/components/inference/InferenceContext', () => ({
@@ -41,7 +43,7 @@ vi.mock('@/hooks/api/use-trace-availability', () => ({
useTraceAvailability: () => ({ data: undefined }),
}));
-import ScatterGraph from './ScatterGraph';
+import ScatterGraph, { pointLabelText } from './ScatterGraph';
// ── Environment stubs ────────────────────────────────────────────────────────
class MockResizeObserver {
@@ -73,6 +75,28 @@ const CHART_DEFINITION = { chartType: 'interactivity' } as unknown as ChartDefin
const noop = () => {};
+describe('pointLabelText', () => {
+ it('keeps decode mode out of mixed agentic point labels', () => {
+ const standard = point('h100', 'fp8', 1, 1, 8);
+ standard.benchmark_type = 'agentic_traces';
+ standard.spec_decoding = 'none';
+ const mtp = { ...standard, spec_decoding: 'mtp' };
+ const eagle = { ...standard, spec_decoding: 'eagle' };
+
+ expect(pointLabelText(standard, false)).toBe('8\nC=16');
+ expect(pointLabelText(mtp, false)).toBe('8\nC=16');
+ expect(pointLabelText(eagle, false)).toBe('8\nC=16');
+ });
+
+ it('keeps fixed-sequence labels unchanged', () => {
+ const fixed = point('h100', 'fp8', 1, 1, 8);
+ fixed.benchmark_type = 'single_turn';
+ fixed.spec_decoding = 'mtp';
+
+ expect(pointLabelText(fixed, false)).toBe('8\nC=16');
+ });
+});
+
function baseInferenceState() {
return {
activeHwTypes: new Set(['h100', 'b200']),
@@ -212,6 +236,50 @@ describe('ScatterGraph toggle decoration', () => {
unmount();
});
+ it('keeps speculative decoding out of point decorations and shows only KV offload', () => {
+ const standard = {
+ ...point('h100', 'fp8', 1, 1, 1),
+ benchmark_type: 'agentic_traces',
+ spec_decoding: 'none',
+ offload_mode: 'off',
+ } as InferenceData;
+ const mtp = {
+ ...point('h100', 'fp8', 20, 200, 2),
+ benchmark_type: 'agentic_traces',
+ spec_decoding: 'mtp',
+ offload_mode: 'off',
+ } as InferenceData;
+ const mtpWithOffload = {
+ ...point('h100', 'fp8', 40, 400, 4),
+ benchmark_type: 'agentic_traces',
+ spec_decoding: 'mtp',
+ offload_mode: 'on',
+ } as InferenceData;
+ const fixedMtp = {
+ ...point('h100', 'fp8', 100, 1000, 8),
+ benchmark_type: 'single_turn',
+ spec_decoding: 'mtp',
+ offload_mode: 'off',
+ } as InferenceData;
+
+ inferenceState.current = {
+ ...baseInferenceState(),
+ selectedSequence: 'agentic-traces',
+ };
+ const { container, unmount } = mountChart({
+ data: [standard, mtp, mtpWithOffload, fixedMtp],
+ });
+ const groups = dotGroups(container);
+
+ expect(container.querySelector('.spec-decode-marker')).toBeNull();
+ expect(groups[1].querySelector('.offload-halo')).toBeNull();
+ expect(groups[2].querySelector('.offload-halo')).not.toBeNull();
+ expect(container.querySelector('[data-testid="spec-decode-marker-key"]')).toBeNull();
+ expect(container.querySelector('[data-testid="offload-halo-key"]')).not.toBeNull();
+ expect(container.querySelector('[data-testid="agentic-optimization-note"]')).not.toBeNull();
+ unmount();
+ });
+
it('hides a toggled-off hw via opacity without rebuilding the chart', () => {
const { container, rerender, unmount } = mountChart();
const buildsAfterMount = rebuildCount();
@@ -374,6 +442,48 @@ describe('ScatterGraph toggle decoration', () => {
unmount();
});
+ it('keeps speculative decoding out of unofficial-run point decorations', () => {
+ const runUrl = 'https://github.com/o/r/actions/runs/123';
+ const overlayPoints = [
+ {
+ ...point('h100', 'fp8', 30, 300, 2),
+ benchmark_type: 'agentic_traces',
+ spec_decoding: 'mtp',
+ offload_mode: 'on',
+ run_url: runUrl,
+ } as InferenceData,
+ {
+ ...point('h100', 'fp8', 35, 350, 4),
+ benchmark_type: 'agentic_traces',
+ spec_decoding: 'none',
+ offload_mode: 'off',
+ run_url: runUrl,
+ } as InferenceData,
+ ];
+ overlayState.current = {
+ ...baseOverlayState(),
+ isUnofficialRun: true,
+ activeOverlayHwTypes: new Set(['h100']),
+ allOverlayHwTypes: new Set(['h100']),
+ runIndexByUrl: { [runUrl]: 0 },
+ unofficialRunInfos: [{ id: '123', branch: 'test-branch', url: runUrl }],
+ };
+
+ const { container, unmount } = mountChart({
+ overlayData: {
+ data: overlayPoints,
+ hardwareConfig: HARDWARE_CONFIG,
+ } as unknown as Parameters[0]['overlayData'],
+ });
+ const groups = [...container.querySelectorAll('.unofficial-overlay-pt')];
+
+ expect(groups[0].querySelector('.spec-decode-marker')).toBeNull();
+ expect(groups[0].querySelector('.offload-halo')).not.toBeNull();
+ expect(groups[1].querySelector('.spec-decode-marker')).toBeNull();
+ expect(groups[1].querySelector('.offload-halo')).toBeNull();
+ unmount();
+ });
+
it('applies quick filters to unofficial-run overlay markers', () => {
const overlayPoints = [point('h100', 'fp8', 30, 300, 2), point('h100', 'fp8', 35, 350, 4)].map(
(p) => ({ ...p, run_url: 'https://github.com/o/r/actions/runs/123' }),
diff --git a/packages/app/src/components/inference/ui/ScatterGraph.tsx b/packages/app/src/components/inference/ui/ScatterGraph.tsx
index 921bda54b..063d007c2 100644
--- a/packages/app/src/components/inference/ui/ScatterGraph.tsx
+++ b/packages/app/src/components/inference/ui/ScatterGraph.tsx
@@ -69,6 +69,7 @@ import {
generateTooltipContent,
getPointLabel,
} from '@/components/inference/utils/tooltipUtils';
+import { scatterPointConfigId } from '@/components/inference/utils/point-identity';
import LegendPointsDialog from '@/components/inference/ui/LegendPointsDialog';
import {
OFFLOAD_HALO_DASHARRAY,
@@ -76,6 +77,7 @@ import {
OFFLOAD_HALO_STROKE_WIDTH,
OffloadHaloLegendKey,
} from '@/components/inference/ui/OffloadHaloLegendKey';
+import { AgenticOptimizationNote } from '@/components/inference/ui/AgenticOptimizationNote';
import { buildLegendPointsRows } from '@/components/inference/utils/legend-points-table';
import {
type ParetoPointLabel,
@@ -199,6 +201,26 @@ const getXPath = (size: number) => {
return `M ${-s} ${-s} L ${s} ${s} M ${s} ${-s} L ${-s} ${s}`;
};
+/** Render the KV-offload halo around a point when applicable. */
+function renderOffloadHalo(
+ group: d3.Selection,
+ point: InferenceData,
+ stroke: string,
+): void {
+ group
+ .selectAll('.offload-halo')
+ .data(point.offload_mode === 'on' ? [true] : [])
+ .join('circle')
+ .attr('class', 'offload-halo')
+ .attr('r', OFFLOAD_HALO_RADIUS)
+ .attr('fill', 'none')
+ .attr('stroke', stroke)
+ .attr('stroke-width', OFFLOAD_HALO_STROKE_WIDTH)
+ .attr('stroke-dasharray', OFFLOAD_HALO_DASHARRAY)
+ .attr('opacity', 0.9)
+ .attr('pointer-events', 'none');
+}
+
const formatChangelogDescription = (desc: string | string[]): React.JSX.Element => {
if (typeof desc === 'string') {
return (
@@ -245,8 +267,8 @@ function groupPointsByDate(points: InferenceData[]): Map
`${d.hwKey}_${d.precision}_${d.date}-${d.x}-${d.y}`;
-/** Point label lines: TP (or full parallelism label) plus the C= concurrency. */
-const pointLabelText = (d: InferenceData, advanced: boolean): string =>
+/** Point label lines. Decode mode is shown by a point marker instead of text. */
+export const pointLabelText = (d: InferenceData, advanced: boolean): string =>
advanced ? `${getPointLabel(d)}\nC=${d.conc}` : `${d.tp}\nC=${d.conc}`;
// Referentially stable "no overlay data" result (see processedOverlayData).
@@ -745,14 +767,19 @@ const ScatterGraph = React.memo(
const hwKeys = cl.entries.flatMap((entry: any) =>
(entry.config_keys ?? entry['config-keys'] ?? [])
.filter((key: string) => selectedPrecisions.includes(key.split('-')[1]))
- .map(changelogConfigToHwKey)
+ .map((key: string) =>
+ changelogConfigToHwKey(
+ key,
+ selectedSequence === Sequence.AgenticTraces ? 'agentic_traces' : undefined,
+ ),
+ )
.filter((key: string | null): key is string => key !== null),
);
return new Set(hwKeys);
}
}
return new Set();
- }, [availableRuns, selectedRunId, selectedPrecisions]);
+ }, [availableRuns, selectedRunId, selectedPrecisions, selectedSequence]);
// --- Data Processing ---
const groupedData = useMemo(
@@ -814,15 +841,7 @@ const ScatterGraph = React.memo(
return effectiveOfficialHwTypes;
}, [showAllHardwareTypes, groupedData, effectiveOfficialHwTypes]);
- const buildPointConfigId = useCallback((point: InferenceData): string => {
- let key = `${point.hwKey}|${point.precision}|${point.tp}|${point.conc}|${point.decode_ep ?? 0}|${point.prefill_tp ?? 0}|${point.prefill_ep ?? 0}`;
- if (point.disagg) key += `|disagg|${point.num_prefill_gpu ?? 0}|${point.num_decode_gpu ?? 0}`;
- // Agentic runs emit two rows per (config, conc) — one offload=on, one off.
- // Without this suffix, d3's data join treats them as the same point and
- // drops one variant (along with its halo).
- if (point.offload_mode) key += `|offload-${point.offload_mode}`;
- return key;
- }, []);
+ const buildPointConfigId = useCallback(scatterPointConfigId, []);
// filteredData: visible points only (for scale domain calculation)
const filteredData = useMemo(
@@ -1056,7 +1075,6 @@ const ScatterGraph = React.memo(
processedOverlayData.some((point) => point.offload_mode === 'on'),
[pointsData, processedOverlayData],
);
-
// Bulk presence lookup for agentic points: which ids have a stored
// trace_replay blob → controls the "View charts" button in the pinned
// tooltip. We deliberately don't fetch the histograms themselves here;
@@ -2387,6 +2405,16 @@ const ScatterGraph = React.memo(
overlayRunColor(overlayRunIndex(d.run_url ?? null, runIndexByUrl)),
);
+ // Match official points: KV offload is the only persistent
+ // point decoration. Decode method remains in the tooltip.
+ overlayPoints.each(function (d) {
+ renderOffloadHalo(
+ d3.select(this),
+ d,
+ overlayRunColor(overlayRunIndex(d.run_url ?? null, runIndexByUrl)),
+ );
+ });
+
// Labels
const showLabels = showPointLabels && !showGradientLabels;
overlayPoints.each(function (d) {
@@ -2874,19 +2902,7 @@ const ScatterGraph = React.memo(
// Offload halo: dashed ring on every point that used KV offload (Pareto or not)
zoomGroup.selectAll('.dot-group').each(function (d) {
- const showHalo = d.offload_mode === 'on';
- d3.select(this)
- .selectAll('.offload-halo')
- .data(showHalo ? [true] : [])
- .join('circle')
- .attr('class', 'offload-halo')
- .attr('r', OFFLOAD_HALO_RADIUS)
- .attr('fill', 'none')
- .attr('stroke', 'var(--foreground)')
- .attr('stroke-width', OFFLOAD_HALO_STROKE_WIDTH)
- .attr('stroke-dasharray', OFFLOAD_HALO_DASHARRAY)
- .attr('opacity', 0.9)
- .attr('pointer-events', 'none');
+ renderOffloadHalo(d3.select(this), d, 'var(--foreground)');
});
avoidLabelCollisions(zoomGroup);
@@ -2953,6 +2969,9 @@ const ScatterGraph = React.memo(
getShapeKeyForPrecision(d.precision, ir.selectedPrecisions),
color,
);
+ // A precision toggle may replace and append the visible SVG shape.
+ // Keep the offload halo above that shape after the swap.
+ sel.selectAll('.offload-halo').raise();
});
// Overlay X markers: Optimal Only visibility (mirrors the official dot
@@ -3427,7 +3446,14 @@ const ScatterGraph = React.memo(
: []
}
precisionIndicators={selectedPrecisions}
- keyIndicators={hasOffloadHalo ? : undefined}
+ keyIndicators={
+ hasOffloadHalo || selectedSequence === Sequence.AgenticTraces ? (
+ <>
+ {hasOffloadHalo && }
+ {selectedSequence === Sequence.AgenticTraces && }
+ >
+ ) : undefined
+ }
enableTooltips={true}
/>
}
diff --git a/packages/app/src/components/inference/utils/changelogFormatters.test.ts b/packages/app/src/components/inference/utils/changelogFormatters.test.ts
index b657e97e2..a1e80866c 100644
--- a/packages/app/src/components/inference/utils/changelogFormatters.test.ts
+++ b/packages/app/src/components/inference/utils/changelogFormatters.test.ts
@@ -62,10 +62,10 @@ describe('changelogConfigToHwKey', () => {
);
});
- it('keeps a trailing MTP spec method while dropping agentic metadata', () => {
- expect(changelogConfigToHwKey('dsv4-fp4-mi355x-sglang-agentic-hicache-mtp')).toBe(
- 'mi355x_sglang_mtp',
- );
+ it('drops a trailing MTP spec method from agentic legend identity', () => {
+ expect(
+ changelogConfigToHwKey('dsv4-fp4-mi355x-sglang-agentic-hicache-mtp', 'agentic_traces'),
+ ).toBe('mi355x_sglang');
});
});
@@ -98,6 +98,22 @@ describe('configKeyMatchesHwKey', () => {
).toBe(true);
});
+ it('matches an agentic MTP changelog key to the mixed-spec legend key', () => {
+ expect(
+ configKeyMatchesHwKey(
+ 'dsv4-fp4-mi355x-mori-sglang-agentic-hicache-mtp',
+ 'mi355x_mori-sglang',
+ 'agentic_traces',
+ ),
+ ).toBe(true);
+ });
+
+ it('keeps fixed-sequence changelogs out of agentic views', () => {
+ expect(
+ configKeyMatchesHwKey('dsv4-fp4-mi355x-mori-sglang', 'mi355x_mori-sglang', 'agentic_traces'),
+ ).toBe(false);
+ });
+
it('matches sglang framework', () => {
expect(configKeyMatchesHwKey('gptoss-fp8-mi300x-sglang', 'mi300x_sglang')).toBe(true);
});
diff --git a/packages/app/src/components/inference/utils/changelogFormatters.tsx b/packages/app/src/components/inference/utils/changelogFormatters.tsx
index 476ed2211..17a0f2038 100644
--- a/packages/app/src/components/inference/utils/changelogFormatters.tsx
+++ b/packages/app/src/components/inference/utils/changelogFormatters.tsx
@@ -20,7 +20,10 @@ const CHANGELOG_FRAMEWORK_KEYS = [
* `agentic`, `hicache`, and `pcp` after the serving framework; those are not
* framework labels and must not become part of the legend identity.
*/
-export function changelogConfigToHwKey(configKey: string): string | null {
+export function changelogConfigToHwKey(
+ configKey: string,
+ benchmarkType?: 'single_turn' | 'agentic_traces',
+): string | null {
const parts = configKey.toLowerCase().split('-');
const gpu = parts[2];
const remainder = parts.slice(3).join('-');
@@ -32,7 +35,14 @@ export function changelogConfigToHwKey(configKey: string): string | null {
if (!framework) return null;
const trailingParts = remainder.slice(framework.length).split('-').filter(Boolean);
- const specSuffix = trailingParts.includes('mtp') ? '_mtp' : '';
+ const isAgentic = trailingParts.includes('agentic');
+ if (benchmarkType === 'agentic_traces' && !isAgentic) return null;
+ const specSuffix =
+ benchmarkType === 'agentic_traces' && isAgentic
+ ? ''
+ : trailingParts.includes('mtp')
+ ? '_mtp'
+ : '';
return `${gpu}_${resolveFrameworkAlias(framework)}${specSuffix}`;
}
@@ -62,8 +72,12 @@ export function formatChangelogDescription(desc: string | string[]) {
* Check if a changelog config key matches a hwKey.
* Normalizes both to hyphen-separated form for comparison.
*/
-export function configKeyMatchesHwKey(configKey: string, hwKey: string): boolean {
- return changelogConfigToHwKey(configKey) === hwKey;
+export function configKeyMatchesHwKey(
+ configKey: string,
+ hwKey: string,
+ benchmarkType?: 'single_turn' | 'agentic_traces',
+): boolean {
+ return changelogConfigToHwKey(configKey, benchmarkType) === hwKey;
}
export function formatConfigKeys(key: string) {
diff --git a/packages/app/src/components/inference/utils/point-identity.test.ts b/packages/app/src/components/inference/utils/point-identity.test.ts
new file mode 100644
index 000000000..5f3ee14ae
--- /dev/null
+++ b/packages/app/src/components/inference/utils/point-identity.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from 'vitest';
+
+import type { InferenceData } from '@/components/inference/types';
+
+import { scatterPointConfigId } from './point-identity';
+
+const point = (overrides: Partial): InferenceData =>
+ ({
+ hwKey: 'h200_vllm',
+ precision: 'fp8',
+ tp: 8,
+ conc: 32,
+ ...overrides,
+ }) as InferenceData;
+
+describe('scatterPointConfigId', () => {
+ it('keeps overlapping agentic MTP and standard-decoding points distinct', () => {
+ const standard = scatterPointConfigId(
+ point({ benchmark_type: 'agentic_traces', spec_decoding: 'none' }),
+ );
+ const mtp = scatterPointConfigId(
+ point({ benchmark_type: 'agentic_traces', spec_decoding: 'mtp' }),
+ );
+
+ expect(standard).not.toBe(mtp);
+ expect(standard).toContain('|spec-none');
+ expect(mtp).toContain('|spec-mtp');
+ });
+
+ it('keeps agentic offload variants distinct alongside spec methods', () => {
+ const off = scatterPointConfigId(
+ point({
+ benchmark_type: 'agentic_traces',
+ spec_decoding: 'mtp',
+ offload_mode: 'off',
+ }),
+ );
+ const on = scatterPointConfigId(
+ point({
+ benchmark_type: 'agentic_traces',
+ spec_decoding: 'mtp',
+ offload_mode: 'on',
+ }),
+ );
+
+ expect(off).not.toBe(on);
+ });
+});
diff --git a/packages/app/src/components/inference/utils/point-identity.ts b/packages/app/src/components/inference/utils/point-identity.ts
new file mode 100644
index 000000000..f578c8e69
--- /dev/null
+++ b/packages/app/src/components/inference/utils/point-identity.ts
@@ -0,0 +1,24 @@
+import type { InferenceData } from '@/components/inference/types';
+
+interface AgenticSpecIdentity {
+ benchmark_type?: string;
+ spec_decoding?: string;
+}
+
+/** Point-level identity suffix for decode methods merged into one agentic curve. */
+export function agenticSpecDecodingKeySuffix(point: AgenticSpecIdentity): string {
+ return point.benchmark_type === 'agentic_traces' ? `|spec-${point.spec_decoding || 'none'}` : '';
+}
+
+/** Stable D3 join key for one scatter point within a chart series. */
+export function scatterPointConfigId(point: InferenceData): string {
+ let key = `${point.hwKey}|${point.precision}|${point.tp}|${point.conc}|${point.decode_ep ?? 0}|${point.prefill_tp ?? 0}|${point.prefill_ep ?? 0}`;
+ if (point.disagg) {
+ key += `|disagg|${point.num_prefill_gpu ?? 0}|${point.num_decode_gpu ?? 0}`;
+ }
+ if (point.offload_mode) key += `|offload-${point.offload_mode}`;
+ // Agentic series omit spec decoding from hwKey so one curve can mix methods.
+ // It remains point identity to avoid collapsing overlapping MTP/STP results.
+ key += agenticSpecDecodingKeySuffix(point);
+ return key;
+}
diff --git a/packages/app/src/components/inference/utils/runEnumeration.test.ts b/packages/app/src/components/inference/utils/runEnumeration.test.ts
index 155fae66a..3d92f637c 100644
--- a/packages/app/src/components/inference/utils/runEnumeration.test.ts
+++ b/packages/app/src/components/inference/utils/runEnumeration.test.ts
@@ -24,6 +24,7 @@ const SCOPE = {
modelDbKeys: ['minimaxm3'],
selectedGPUs: ['mi300x_vllm'],
selectedPrecisions: ['fp8'],
+ benchmarkType: 'single_turn' as const,
};
describe('dataRunsForDate', () => {
@@ -66,6 +67,15 @@ describe('dataRunsForDate', () => {
expect(runs.map((r) => r.runId)).toEqual(['2']);
});
+ it('maps agentic MTP and non-MTP run coverage to one GPU series', () => {
+ const rows = [
+ rc({ github_run_id: 1, spec_method: 'none' }),
+ rc({ github_run_id: 2, spec_method: 'mtp' }),
+ ];
+ const runs = dataRunsForDate(rows, { ...SCOPE, benchmarkType: 'agentic_traces' });
+ expect(runs.map((r) => r.runId)).toEqual(['1', '2']);
+ });
+
it('excludes runs for other models, precisions, and GPUs', () => {
const rows = [
rc({ github_run_id: 1 }), // matches
diff --git a/packages/app/src/components/inference/utils/runEnumeration.ts b/packages/app/src/components/inference/utils/runEnumeration.ts
index fda1ae30f..1c84b6f25 100644
--- a/packages/app/src/components/inference/utils/runEnumeration.ts
+++ b/packages/app/src/components/inference/utils/runEnumeration.ts
@@ -10,9 +10,9 @@
* changelog entry, and that newest run is exactly the one the plain-date "latest"
* view shows — so enumerating from changelog entries alone would silently drop it.
*
- * Runs are scoped to the selected GPUs using the canonical {@link getHardwareKey}
- * so MTP and disagg variants (separate hw keys) are kept distinct, exactly as the
- * chart keys them.
+ * Runs are scoped to the selected GPUs using the canonical {@link getHardwareKey}.
+ * Fixed-sequence MTP variants stay distinct; agentic MTP and non-MTP points map
+ * to the same series, exactly as the chart keys them.
*/
import type { AggDataEntry } from '@/components/inference/types';
@@ -37,15 +37,18 @@ export interface RunScope {
selectedGPUs: string[];
/** Selected DB precisions, e.g. ['fp8']. */
selectedPrecisions: string[];
+ /** Scenario currently rendered by the chart. */
+ benchmarkType: 'single_turn' | 'agentic_traces';
}
/** The hw key a runConfig maps to, built the same way the chart builds series keys. */
-function runConfigHwKey(rc: RunConfigRow): string {
+function runConfigHwKey(rc: RunConfigRow, benchmarkType: 'single_turn' | 'agentic_traces'): string {
return getHardwareKey({
hw: rc.hardware,
framework: rc.framework,
disagg: rc.disagg,
spec_decoding: rc.spec_method,
+ benchmark_type: benchmarkType,
} as unknown as AggDataEntry);
}
@@ -55,7 +58,7 @@ function runConfigHwKey(rc: RunConfigRow): string {
* assigns read in the order the runs actually happened.
*/
export function dataRunsForDate(runConfigs: RunConfigRow[], scope: RunScope): DataRun[] {
- const { modelDbKeys, selectedGPUs, selectedPrecisions } = scope;
+ const { modelDbKeys, selectedGPUs, selectedPrecisions, benchmarkType } = scope;
const precSet = new Set(selectedPrecisions);
const gpuSet = new Set(selectedGPUs);
const byRun = new Map();
@@ -63,7 +66,7 @@ export function dataRunsForDate(runConfigs: RunConfigRow[], scope: RunScope): Da
for (const rc of runConfigs) {
if (!modelDbKeys.includes(rc.model)) continue;
if (!precSet.has(rc.precision)) continue;
- if (!gpuSet.has(runConfigHwKey(rc))) continue;
+ if (!gpuSet.has(runConfigHwKey(rc, benchmarkType))) continue;
const id = String(rc.github_run_id);
if (!byRun.has(id)) {
diff --git a/packages/app/src/components/inference/utils/tooltip-utils.test.ts b/packages/app/src/components/inference/utils/tooltip-utils.test.ts
index bc254e02b..7edc1268f 100644
--- a/packages/app/src/components/inference/utils/tooltip-utils.test.ts
+++ b/packages/app/src/components/inference/utils/tooltip-utils.test.ts
@@ -473,6 +473,23 @@ describe('generateOverlayTooltipContent', () => {
expect(html).toContain('CPU Cache Hit Rate: 42.0%');
});
+ it('shows point-level speculative decoding for mixed agentic overlays', () => {
+ const mtp = generateOverlayTooltipContent(
+ overlayConfig({
+ data: pt({ benchmark_type: 'agentic_traces', spec_decoding: 'mtp' }),
+ }),
+ );
+ const standardZh = generateOverlayTooltipContent(
+ overlayConfig({
+ data: pt({ benchmark_type: 'agentic_traces', spec_decoding: 'none' }),
+ locale: 'zh',
+ }),
+ );
+
+ expect(mtp).toContain('Speculative Decoding: MTP');
+ expect(standardZh).toContain('投机解码: 关闭');
+ });
+
it('hides stale CPU cache hits for unofficial overlays without offload', () => {
const html = generateOverlayTooltipContent(
overlayConfig({
diff --git a/packages/app/src/components/inference/utils/tooltipUtils.ts b/packages/app/src/components/inference/utils/tooltipUtils.ts
index ae5d89267..eff4f0e29 100644
--- a/packages/app/src/components/inference/utils/tooltipUtils.ts
+++ b/packages/app/src/components/inference/utils/tooltipUtils.ts
@@ -1,4 +1,5 @@
import { formatNumber, getDisplayLabel } from '@/lib/utils';
+import { specMethodDisplayLabel } from '@/lib/compare-variant-slug';
import { isPersistedBenchmarkId } from '@/lib/benchmark-id';
import type { Locale } from '@/lib/i18n';
import { isKvOffloadEnabled } from '@/lib/kv-offload';
@@ -170,10 +171,25 @@ const generateCacheMetadataHTML = (d: InferenceData, locale: Locale): string =>
* Agentic-only request success and token totals. Cache metadata is rendered
* separately because fixed-sequence rows can carry it too.
*/
-const generateAgenticHTML = (d: InferenceData): string => {
+const AGENTIC_STRINGS = {
+ en: { speculativeDecoding: 'Speculative Decoding', off: 'Off' },
+ zh: { speculativeDecoding: '投机解码', off: '关闭' },
+} as const;
+
+const generateAgenticHTML = (d: InferenceData, locale: Locale): string => {
if (d.benchmark_type !== 'agentic_traces') return '';
+ const t = AGENTIC_STRINGS[locale];
const parts: string[] = [];
+ const specMethod = d.spec_decoding ?? 'none';
+ parts.push(
+ tooltipLine(
+ t.speculativeDecoding,
+ specMethod === 'none' || specMethod === ''
+ ? t.off
+ : specMethodDisplayLabel(d.model, specMethod),
+ ),
+ );
if (d.num_requests_total !== undefined && d.num_requests_successful !== undefined) {
const successPct =
@@ -368,7 +384,7 @@ export const generateTooltipContent = (config: TooltipConfig): string => {
Precision: ${d.precision.toUpperCase()}
${generateCacheMetadataHTML(d, locale)}
- ${generateAgenticHTML(d)}
+ ${generateAgenticHTML(d, locale)}
${runLinkHTML(runUrl)}
${viewChartsButtonHTML(isPinned, Boolean(hasTrace), d.id)}
@@ -419,7 +435,7 @@ export const generateOverlayTooltipContent = (config: OverlayTooltipConfig): str
Precision: ${d.precision.toUpperCase()}
${generateCacheMetadataHTML(d, locale)}
- ${generateAgenticHTML(d)}
+ ${generateAgenticHTML(d, locale)}
`;
};
@@ -490,7 +506,7 @@ export const generateGPUGraphTooltipContent = (config: TooltipConfig): string =>
Precision: ${d.precision.toUpperCase()}
${generateCacheMetadataHTML(d, locale)}
- ${generateAgenticHTML(d)}
+ ${generateAgenticHTML(d, locale)}
${runLinkHTML(runUrl)}
${viewChartsButtonHTML(isPinned, Boolean(hasTrace), d.id)}
diff --git a/packages/app/src/hooks/api/use-ai-chart.ts b/packages/app/src/hooks/api/use-ai-chart.ts
index 55d2597ce..92783463a 100644
--- a/packages/app/src/hooks/api/use-ai-chart.ts
+++ b/packages/app/src/hooks/api/use-ai-chart.ts
@@ -22,6 +22,10 @@ import {
type ReliabilityRow,
} from '@/lib/api';
import { transformBenchmarkRows } from '@/lib/benchmark-transform';
+import {
+ dedupeAgenticHistoryRuns,
+ dedupeRowsToLatestPerConfig,
+} from '@/lib/benchmark-run-selection';
import {
getNestedYValue,
normalizeEvalHardwareKey,
@@ -391,11 +395,24 @@ async function resolveSpec(spec: AiChartSpec): Promise {
}
// Benchmarks or History
- const { isl, osl } = sequenceToIslOsl(spec.sequence);
- const rows =
+ const isAgentic = spec.sequence === 'agentic-traces';
+ const { isl, osl } = isAgentic ? { isl: 0, osl: 0 } : sequenceToIslOsl(spec.sequence);
+ const fetchedRows =
spec.dataSource === 'history'
- ? await fetchBenchmarkHistory(spec.model, isl, osl)
+ ? await fetchBenchmarkHistory(
+ spec.model,
+ isl,
+ osl,
+ undefined,
+ isAgentic ? 'agentic_traces' : undefined,
+ )
: await fetchBenchmarks(spec.model);
+ const rows =
+ spec.dataSource === 'history' && isAgentic
+ ? dedupeAgenticHistoryRuns(fetchedRows)
+ : isAgentic
+ ? dedupeRowsToLatestPerConfig(fetchedRows)
+ : fetchedRows;
const { chartData } = transformBenchmarkRows(rows);
let points = chartData[0] ?? [];
diff --git a/packages/app/src/hooks/api/use-benchmark-history.ts b/packages/app/src/hooks/api/use-benchmark-history.ts
index 4d4378172..971919d19 100644
--- a/packages/app/src/hooks/api/use-benchmark-history.ts
+++ b/packages/app/src/hooks/api/use-benchmark-history.ts
@@ -2,10 +2,17 @@ import { useQuery } from '@tanstack/react-query';
import { fetchBenchmarkHistory } from '@/lib/api';
-export function useBenchmarkHistory(model: string, isl: number, osl: number) {
+export function useBenchmarkHistory(
+ model: string,
+ isl: number,
+ osl: number,
+ benchmarkType?: 'agentic_traces',
+) {
return useQuery({
- queryKey: ['benchmark-history', model, isl, osl],
- queryFn: ({ signal }) => fetchBenchmarkHistory(model, isl, osl, signal),
- enabled: Boolean(model && isl && osl),
+ queryKey: benchmarkType
+ ? ['benchmark-history', model, isl, osl, benchmarkType]
+ : ['benchmark-history', model, isl, osl],
+ queryFn: ({ signal }) => fetchBenchmarkHistory(model, isl, osl, signal, benchmarkType),
+ enabled: Boolean(model && (benchmarkType === 'agentic_traces' || (isl && osl))),
});
}
diff --git a/packages/app/src/hooks/api/use-comparison-changelogs.ts b/packages/app/src/hooks/api/use-comparison-changelogs.ts
index 01d60ad3e..b56976140 100644
--- a/packages/app/src/hooks/api/use-comparison-changelogs.ts
+++ b/packages/app/src/hooks/api/use-comparison-changelogs.ts
@@ -43,6 +43,7 @@ export function useComparisonChangelogs(
selectedGPUs: string[],
selectedDateRange: { startDate: string; endDate: string },
availableDates: string[],
+ benchmarkType?: 'agentic_traces',
) {
const hasGPUs = selectedGPUs.length > 0;
const hasDateRange = Boolean(selectedDateRange.startDate) && Boolean(selectedDateRange.endDate);
@@ -64,8 +65,9 @@ export function useComparisonChangelogs(
const queries = useQueries({
queries: datesToQuery.map((date) => ({
- queryKey: ['workflow-info', date],
- queryFn: ({ signal }: { signal: AbortSignal }) => fetchWorkflowInfo(date, signal),
+ queryKey: benchmarkType ? ['workflow-info', date, benchmarkType] : ['workflow-info', date],
+ queryFn: ({ signal }: { signal: AbortSignal }) =>
+ fetchWorkflowInfo(date, signal, benchmarkType),
enabled: hasGPUs,
})),
});
diff --git a/packages/app/src/lib/agentic-workflow-metadata.test.ts b/packages/app/src/lib/agentic-workflow-metadata.test.ts
new file mode 100644
index 000000000..d0d677872
--- /dev/null
+++ b/packages/app/src/lib/agentic-workflow-metadata.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from 'vitest';
+
+import { agenticWorkflowMetadataOnly } from './agentic-workflow-metadata';
+
+describe('agenticWorkflowMetadataOnly', () => {
+ it('preserves workflow metadata for agentic rows', () => {
+ const [row] = agenticWorkflowMetadataOnly([
+ {
+ benchmark_type: 'agentic_traces',
+ workflow_run_id: 42,
+ run_started_at: '2026-08-12T10:00:00Z',
+ },
+ ]);
+ expect(row.workflow_run_id).toBe(42);
+ expect(row.run_started_at).toBe('2026-08-12T10:00:00Z');
+ });
+
+ it('preserves the prior fixed-sequence response shape', () => {
+ const [row] = agenticWorkflowMetadataOnly([
+ {
+ benchmark_type: 'single_turn',
+ workflow_run_id: 42,
+ run_started_at: '2026-08-12T10:00:00Z',
+ },
+ ]);
+ expect(row).toEqual({ benchmark_type: 'single_turn' });
+ expect('workflow_run_id' in row).toBe(false);
+ expect('run_started_at' in row).toBe(false);
+ });
+});
diff --git a/packages/app/src/lib/agentic-workflow-metadata.ts b/packages/app/src/lib/agentic-workflow-metadata.ts
new file mode 100644
index 000000000..9c605fcbd
--- /dev/null
+++ b/packages/app/src/lib/agentic-workflow-metadata.ts
@@ -0,0 +1,14 @@
+interface WorkflowMetadataRow {
+ benchmark_type: string;
+ workflow_run_id?: number;
+ run_started_at?: string | null;
+}
+
+/** Keep workflow identity internal to agentic rows; fixed API rows retain their prior shape. */
+export function agenticWorkflowMetadataOnly(rows: T[]): T[] {
+ return rows.map((row) => {
+ if (row.benchmark_type === 'agentic_traces') return row;
+ const { workflow_run_id: _workflowRunId, run_started_at: _runStartedAt, ...fixedRow } = row;
+ return fixedRow as T;
+ });
+}
diff --git a/packages/app/src/lib/api-documentation.ts b/packages/app/src/lib/api-documentation.ts
index 793a42d2d..5167a85da 100644
--- a/packages/app/src/lib/api-documentation.ts
+++ b/packages/app/src/lib/api-documentation.ts
@@ -234,9 +234,11 @@ const benchmarkRowSchema = objectSchemaWithOptional(
metrics: metricMapSchema,
workers: arraySchema(workerPowerSchema),
date: { type: 'string', format: 'date' },
+ workflow_run_id: integerSchema,
+ run_started_at: { type: ['string', 'null'], format: 'date-time' },
run_url: nullableStringSchema,
},
- ['workers'],
+ ['workers', 'workflow_run_id', 'run_started_at'],
);
const benchmarkRowsSchema = arraySchema(benchmarkRowSchema);
const benchmarkExample = [
@@ -590,6 +592,16 @@ export const apiOperations: readonly ApiOperation[] = [
{ type: 'string', format: 'date' },
'2026-08-08',
),
+ parameter(
+ 'benchmarkType',
+ 'query',
+ false,
+ 'string',
+ 'Set to agentic_traces to scope per-run configuration coverage to Agentic Traces.',
+ '设为 agentic_traces 可将每次运行的配置覆盖限定为 Agentic Traces。',
+ { type: 'string', enum: ['agentic_traces'] },
+ 'agentic_traces',
+ ),
parameter(
'exact',
'query',
@@ -651,8 +663,8 @@ export const apiOperations: readonly ApiOperation[] = [
path: '/api/v1/benchmarks/history',
summary: text('Read benchmark history', '读取基准历史'),
description: text(
- 'Returns every dated benchmark row for one model and fixed input/output token pair.',
- '返回某个模型和固定输入/输出 token 组合的全部历史基准行。',
+ 'Returns every dated benchmark row for one model and either a fixed input/output token pair or Agentic Traces.',
+ '返回某个模型以及固定输入/输出 token 组合或 Agentic Traces 的全部历史基准行。',
),
audience: 'public',
stability: 'stable',
@@ -670,23 +682,33 @@ export const apiOperations: readonly ApiOperation[] = [
parameter(
'isl',
'query',
- true,
+ false,
'integer',
- 'Positive input sequence length in tokens.',
- '正整数输入序列长度,单位为 token。',
+ 'Positive input sequence length in tokens. Required unless benchmarkType=agentic_traces.',
+ '正整数输入序列长度,单位为 token;除非 benchmarkType=agentic_traces,否则必填。',
positiveIdSchema,
1024,
),
parameter(
'osl',
'query',
- true,
+ false,
'integer',
- 'Positive output sequence length in tokens.',
- '正整数输出序列长度,单位为 token。',
+ 'Positive output sequence length in tokens. Required unless benchmarkType=agentic_traces.',
+ '正整数输出序列长度,单位为 token;除非 benchmarkType=agentic_traces,否则必填。',
positiveIdSchema,
1024,
),
+ parameter(
+ 'benchmarkType',
+ 'query',
+ false,
+ 'string',
+ 'Set to agentic_traces to read Agentic Traces history without ISL/OSL.',
+ '设为 agentic_traces 可在不提供 ISL/OSL 的情况下读取 Agentic Traces 历史。',
+ { type: 'string', enum: ['agentic_traces'] },
+ 'agentic_traces',
+ ),
],
responses: [
success('Historical benchmark rows.', '历史基准行。', benchmarkRowsSchema, benchmarkExample),
diff --git a/packages/app/src/lib/api-route-catalog.ts b/packages/app/src/lib/api-route-catalog.ts
index a557dd7f7..07dadbf19 100644
--- a/packages/app/src/lib/api-route-catalog.ts
+++ b/packages/app/src/lib/api-route-catalog.ts
@@ -108,7 +108,7 @@ export const apiRouteCatalog = [
method: 'GET',
classification: 'published-read',
operationId: 'list-benchmarks',
- sourceSha256: 'daf24b2a08ab021782084fc6e5e145013fca518e58d18837355a088e040f257b',
+ sourceSha256: '37b5a31613a9c5a2e1de35758551dfdbbb8b920fcd6ae6baeedf973c8802bc2c',
},
{
source: 'src/app/api/v1/benchmarks/history/route.ts',
@@ -116,7 +116,7 @@ export const apiRouteCatalog = [
method: 'GET',
classification: 'published-read',
operationId: 'list-benchmark-history',
- sourceSha256: 'f99940aabe91d315e33e0e645342d83ace227fc014de029d16868284e158be40',
+ sourceSha256: 'd4b3d2ad8ed6e35df70c6b651f71c9d86b6e1dad9c3eaf6eadc2a8591318d1fb',
},
{
source: 'src/app/api/v1/collectivex/latest/route.ts',
@@ -353,7 +353,7 @@ export const apiRouteCatalog = [
method: 'GET',
classification: 'published-read',
operationId: 'get-workflow-info',
- sourceSha256: 'ec497811abc666e4bf970b6e722fde3a7fefc25a87d6b4e5897a605e22c84ee8',
+ sourceSha256: 'b7b0f215e9bf2c766ce2d4c9a13090504c1704a67f697af3a2fd1f318de1e6d6',
},
] as const satisfies readonly ApiRouteCatalogEntry[];
@@ -380,7 +380,7 @@ export const apiContractSourceDigests = [
},
{
source: 'src/lib/api.ts',
- sourceSha256: '7824444f73bd1331ad06cc62060de08446f9d20661fa77030a254d6125b3636a',
+ sourceSha256: '03809377af6c2ee938169a065e06dccb25d983d7171a54b998ffa08dd970d306',
reviewArea: {
en: 'Public API client parameter serialization and TypeScript response contracts.',
zh: '公开 API 客户端的参数序列化和 TypeScript 响应契约。',
@@ -436,7 +436,7 @@ export const apiContractSourceDigests = [
},
{
source: '../db/src/queries/benchmarks.ts',
- sourceSha256: '05e97742b831d1afdcb8dec1d764fd9b3d4a54564e93da4ceea778f8533c5c49',
+ sourceSha256: 'bb2f2cd28d8e4cea7b556561235da43d6a33363dc2c6d1f0b13408193d04298e',
reviewArea: {
en: 'Benchmark row fields and latest, exact-run, history, and TCO query semantics.',
zh: '基准行字段以及最新、精确运行、历史和 TCO 查询语义。',
@@ -532,7 +532,7 @@ export const apiContractSourceDigests = [
},
{
source: '../db/src/queries/workflow-info.ts',
- sourceSha256: 'a588811952d152ecb5ca3725e565f3f829c7bbdfd14a795b261a11bfa82fe00a',
+ sourceSha256: 'b6611604d41ab69c00cb804ad255d4cbc9f70e41e84ad43330538558956f9eb6',
reviewArea: {
en: 'Availability rows plus workflow runs, changelogs, configurations, and run coverage responses.',
zh: '可用配置行以及工作流运行、变更记录、配置和运行覆盖响应。',
diff --git a/packages/app/src/lib/api.ts b/packages/app/src/lib/api.ts
index 00fea7219..0b2a03f71 100644
--- a/packages/app/src/lib/api.ts
+++ b/packages/app/src/lib/api.ts
@@ -52,6 +52,9 @@ export interface BenchmarkRow {
*/
workers?: WorkerPower[];
date: string;
+ /** Internal workflow identity used to keep merged agentic curves within one run. */
+ workflow_run_id?: number;
+ run_started_at?: string | null;
run_url: string | null;
}
@@ -179,16 +182,21 @@ export function fetchBenchmarkHistory(
isl: number,
osl: number,
signal?: AbortSignal,
+ benchmarkType?: 'agentic_traces',
) {
const params = new URLSearchParams({ model, isl: String(isl), osl: String(osl) });
+ if (benchmarkType) params.set('benchmarkType', benchmarkType);
return fetchJson(`/api/v1/benchmarks/history?${params}`, signal);
}
-export function fetchWorkflowInfo(date: string, signal?: AbortSignal) {
- return fetchJson(
- `/api/v1/workflow-info?date=${encodeURIComponent(date)}`,
- signal,
- );
+export function fetchWorkflowInfo(
+ date: string,
+ signal?: AbortSignal,
+ benchmarkType?: 'agentic_traces',
+) {
+ const params = new URLSearchParams({ date });
+ if (benchmarkType) params.set('benchmarkType', benchmarkType);
+ return fetchJson(`/api/v1/workflow-info?${params}`, signal);
}
export interface AvailabilityRow {
diff --git a/packages/app/src/lib/benchmark-data.server.ts b/packages/app/src/lib/benchmark-data.server.ts
index e288596c2..c3dc23ca5 100644
--- a/packages/app/src/lib/benchmark-data.server.ts
+++ b/packages/app/src/lib/benchmark-data.server.ts
@@ -5,6 +5,7 @@ import {
} from '@semianalysisai/inferencex-db/queries/benchmarks';
import { cachedQuery } from '@/lib/api-cache';
+import { agenticWorkflowMetadataOnly } from '@/lib/agentic-workflow-metadata';
import { loadFixture } from '@/lib/test-fixtures';
/** Cache slot is keyed on the dbKeys array. Both `/compare/` and
@@ -14,9 +15,9 @@ export const getCachedBenchmarks = cachedQuery(
(dbModelKeys: string[]) => {
if (FIXTURES_MODE) return Promise.resolve(loadFixture('benchmarks'));
- return getLatestBenchmarks(getDb(), dbModelKeys);
+ return getLatestBenchmarks(getDb(), dbModelKeys).then(agenticWorkflowMetadataOnly);
},
- 'benchmarks',
+ 'benchmarks-agentic-run-metadata',
{ blobOnly: true },
);
@@ -26,8 +27,8 @@ export const getCachedBenchmarksAsOf = cachedQuery(
(dbModelKeys: string[], date: string) => {
if (FIXTURES_MODE) return Promise.resolve(loadFixture('benchmarks'));
- return getLatestBenchmarks(getDb(), dbModelKeys, date);
+ return getLatestBenchmarks(getDb(), dbModelKeys, date).then(agenticWorkflowMetadataOnly);
},
- 'benchmarks-as-of',
+ 'benchmarks-as-of-agentic-run-metadata',
{ blobOnly: true },
);
diff --git a/packages/app/src/lib/benchmark-run-selection.ts b/packages/app/src/lib/benchmark-run-selection.ts
new file mode 100644
index 000000000..f7332b96c
--- /dev/null
+++ b/packages/app/src/lib/benchmark-run-selection.ts
@@ -0,0 +1,76 @@
+/** Fields needed to select one workflow run for a rendered benchmark series. */
+export interface BenchmarkSeriesRow {
+ hardware: string;
+ framework: string;
+ spec_method: string;
+ disagg: boolean;
+ precision: string;
+ offload_mode?: string | null;
+ benchmark_type?: string;
+ date: string;
+ workflow_run_id?: number;
+ run_started_at?: string | null;
+}
+
+const seriesKey = (row: BenchmarkSeriesRow): string => {
+ const specMethod = row.benchmark_type === 'agentic_traces' ? '' : row.spec_method;
+ return `${row.hardware}|${row.framework}|${specMethod}|${row.disagg}|${row.precision}|${row.offload_mode ?? 'off'}`;
+};
+
+function isLaterRun(candidate: BenchmarkSeriesRow, current: BenchmarkSeriesRow): boolean {
+ const startedAt = candidate.run_started_at ?? '';
+ const currentStartedAt = current.run_started_at ?? '';
+ return (
+ startedAt > currentStartedAt ||
+ (startedAt === currentStartedAt &&
+ (candidate.workflow_run_id ?? Number.NEGATIVE_INFINITY) >
+ (current.workflow_run_id ?? Number.NEGATIVE_INFINITY))
+ );
+}
+
+function isWinningRun(row: BenchmarkSeriesRow, winner: BenchmarkSeriesRow): boolean {
+ return (
+ row.run_started_at === winner.run_started_at && row.workflow_run_id === winner.workflow_run_id
+ );
+}
+
+/** Keep only the newest date for each chart series and, for agentic, one workflow run. */
+export function dedupeRowsToLatestPerConfig(rows: T[]): T[] {
+ const winnerPerGroup = new Map();
+ for (const row of rows) {
+ const key = seriesKey(row);
+ const current = winnerPerGroup.get(key);
+ if (!current || row.date > current.date) {
+ winnerPerGroup.set(key, row);
+ continue;
+ }
+ if (
+ row.date === current.date &&
+ row.benchmark_type === 'agentic_traces' &&
+ isLaterRun(row, current)
+ ) {
+ winnerPerGroup.set(key, row);
+ }
+ }
+ return rows.filter((row) => {
+ const winner = winnerPerGroup.get(seriesKey(row));
+ if (!winner || row.date !== winner.date) return false;
+ return row.benchmark_type !== 'agentic_traces' || isWinningRun(row, winner);
+ });
+}
+
+/** For historical views, keep one agentic workflow run per series on each calendar date. */
+export function dedupeAgenticHistoryRuns(rows: T[]): T[] {
+ const winnerPerDateAndSeries = new Map();
+ for (const row of rows) {
+ if (row.benchmark_type !== 'agentic_traces') continue;
+ const key = `${row.date}|${seriesKey(row)}`;
+ const current = winnerPerDateAndSeries.get(key);
+ if (!current || isLaterRun(row, current)) winnerPerDateAndSeries.set(key, row);
+ }
+ return rows.filter((row) => {
+ if (row.benchmark_type !== 'agentic_traces') return true;
+ const winner = winnerPerDateAndSeries.get(`${row.date}|${seriesKey(row)}`);
+ return winner !== undefined && isWinningRun(row, winner);
+ });
+}
diff --git a/packages/app/src/lib/benchmark-transform.test.ts b/packages/app/src/lib/benchmark-transform.test.ts
index 95ff1958e..35f1eb7ef 100644
--- a/packages/app/src/lib/benchmark-transform.test.ts
+++ b/packages/app/src/lib/benchmark-transform.test.ts
@@ -2,6 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
import { getPointLabel } from '@/components/inference/utils/tooltipUtils';
import type { BenchmarkRow } from '@/lib/api';
+import { dedupeAgenticHistoryRuns } from '@/lib/benchmark-run-selection';
import {
mergeRunScopedRows,
@@ -739,6 +740,89 @@ describe('transformBenchmarkRows — hardware key resolution', () => {
expect(hardwareConfig).toHaveProperty('h200_trt_mtp');
});
+ it('groups none, MTP, and EAGLE agentic points into one hardware series', () => {
+ const rows = [
+ makeRow({
+ benchmark_type: 'agentic_traces',
+ hardware: 'h200',
+ framework: 'trt',
+ spec_method: 'none',
+ }),
+ makeRow({
+ benchmark_type: 'agentic_traces',
+ hardware: 'h200',
+ framework: 'trt',
+ spec_method: 'mtp',
+ }),
+ makeRow({
+ benchmark_type: 'agentic_traces',
+ hardware: 'h200',
+ framework: 'trt',
+ spec_method: 'eagle',
+ }),
+ ];
+
+ const { chartData, hardwareConfig } = transformBenchmarkRows(rows);
+ expect(Object.keys(hardwareConfig)).toEqual(['h200_trt']);
+ expect(chartData[0].map((point) => point.hwKey)).toEqual(['h200_trt', 'h200_trt', 'h200_trt']);
+ expect(chartData[0].map((point) => point.spec_decoding)).toEqual(['none', 'mtp', 'eagle']);
+ });
+
+ it('keeps one mixed-spec agentic series identity across comparison dates', () => {
+ const rows = dedupeAgenticHistoryRuns([
+ makeRow({
+ id: 1,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'none',
+ conc: 8,
+ date: '2026-06-01',
+ workflow_run_id: 30,
+ run_started_at: '2026-06-01T10:00:00Z',
+ }),
+ makeRow({
+ id: 2,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'mtp',
+ conc: 16,
+ date: '2026-06-01',
+ workflow_run_id: 30,
+ run_started_at: '2026-06-01T10:00:00Z',
+ }),
+ makeRow({
+ id: 3,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'eagle',
+ conc: 32,
+ date: '2026-06-02',
+ workflow_run_id: 31,
+ run_started_at: '2026-06-02T10:00:00Z',
+ }),
+ makeRow({
+ id: 4,
+ benchmark_type: 'agentic_traces',
+ spec_method: 'none',
+ conc: 64,
+ date: '2026-06-02',
+ workflow_run_id: 31,
+ run_started_at: '2026-06-02T10:00:00Z',
+ }),
+ ]);
+
+ const { chartData, hardwareConfig } = transformBenchmarkRows(rows);
+ const pointsByDate = Object.groupBy(chartData[0], (point) => point.date);
+
+ expect(Object.keys(hardwareConfig)).toEqual(['h200_trt']);
+ expect(pointsByDate['2026-06-01']?.map((point) => point.spec_decoding)).toEqual([
+ 'none',
+ 'mtp',
+ ]);
+ expect(pointsByDate['2026-06-02']?.map((point) => point.spec_decoding)).toEqual([
+ 'eagle',
+ 'none',
+ ]);
+ expect(new Set(chartData[0].map((point) => point.hwKey))).toEqual(new Set(['h200_trt']));
+ });
+
it('handles AMD hardware with vllm framework', () => {
const rows = [makeRow({ hardware: 'mi300x', framework: 'vllm' })];
const { chartData, hardwareConfig } = transformBenchmarkRows(rows);
diff --git a/packages/app/src/lib/chart-utils.test.ts b/packages/app/src/lib/chart-utils.test.ts
index cfde6deda..b5d667cba 100644
--- a/packages/app/src/lib/chart-utils.test.ts
+++ b/packages/app/src/lib/chart-utils.test.ts
@@ -256,6 +256,15 @@ describe('buildAvailabilityHwKey', () => {
expect(buildAvailabilityHwKey('h200', undefined, 'mtp')).toBe('h200_mtp');
});
+ it('omits speculative decoding from agentic availability series keys', () => {
+ expect(buildAvailabilityHwKey('h200', 'sglang', 'mtp', false, 'agentic_traces')).toBe(
+ 'h200_sglang',
+ );
+ expect(buildAvailabilityHwKey('h200', 'sglang', 'eagle', false, 'agentic_traces')).toBe(
+ 'h200_sglang',
+ );
+ });
+
it('handles undefined framework and spec method', () => {
expect(buildAvailabilityHwKey('b200', undefined, undefined)).toBe('b200');
});
@@ -921,6 +930,13 @@ describe('getHardwareKey', () => {
);
});
+ it('omits speculative decoding from agentic series identity', () => {
+ const base = { hw: 'h100-sxm', framework: 'vllm', benchmark_type: 'agentic_traces' };
+ expect(getHardwareKey(entry({ ...base, spec_decoding: 'none' }))).toBe('h100_vllm');
+ expect(getHardwareKey(entry({ ...base, spec_decoding: 'mtp' }))).toBe('h100_vllm');
+ expect(getHardwareKey(entry({ ...base, spec_decoding: 'eagle' }))).toBe('h100_vllm');
+ });
+
it('appends spec_decoding suffix when not "none" and not "mtp"', () => {
expect(getHardwareKey(entry({ hw: 'b200-sxm', framework: '', spec_decoding: 'eagle' }))).toBe(
'b200_eagle',
diff --git a/packages/app/src/lib/chart-utils.ts b/packages/app/src/lib/chart-utils.ts
index e9b792348..087b3a114 100644
--- a/packages/app/src/lib/chart-utils.ts
+++ b/packages/app/src/lib/chart-utils.ts
@@ -196,7 +196,12 @@ export const Y_AXIS_METRICS = [
export type YAxisMetric = (typeof Y_AXIS_METRICS)[number];
/**
- * Determines the correct hardware key based on the hardware name and MTP status.
+ * Determines the chart-series hardware key.
+ *
+ * Fixed-sequence curves keep speculative decoding in their identity. Agentic
+ * curves deliberately do not: one production curve may choose a speculative
+ * method for some load points and standard decoding for others. The point
+ * still carries `spec_decoding` for filters, tooltips, and point-level keys.
*/
export const getHardwareKey = (entry: AggDataEntry): string => {
let normalizedHwName = entry.hw.split('-')[0];
@@ -215,10 +220,12 @@ export const getHardwareKey = (entry: AggDataEntry): string => {
normalizedHwName = candidateDirect;
}
}
- if (entry.mtp === 'on' || entry['spec_decoding'] === 'mtp') {
- normalizedHwName = `${normalizedHwName}_mtp`;
- } else if (entry['spec_decoding'] && entry['spec_decoding'] !== 'none') {
- normalizedHwName = `${normalizedHwName}_${entry['spec_decoding']}`;
+ if (entry.benchmark_type !== 'agentic_traces') {
+ if (entry.mtp === 'on' || entry['spec_decoding'] === 'mtp') {
+ normalizedHwName = `${normalizedHwName}_mtp`;
+ } else if (entry['spec_decoding'] && entry['spec_decoding'] !== 'none') {
+ normalizedHwName = `${normalizedHwName}_${entry['spec_decoding']}`;
+ }
}
return normalizedHwName;
};
@@ -270,6 +277,7 @@ export function buildAvailabilityHwKey(
framework?: string,
specMethod?: string,
disagg?: boolean,
+ benchmarkType?: string,
): string {
let hwKey = hardware.split('-')[0];
const fw = framework ? resolveFrameworkAlias(framework) : undefined;
@@ -285,8 +293,10 @@ export function buildAvailabilityHwKey(
hwKey = candidateDirect;
}
}
- if (specMethod === 'mtp') hwKey = `${hwKey}_mtp`;
- else if (specMethod && specMethod !== 'none') hwKey = `${hwKey}_${specMethod}`;
+ if (benchmarkType !== 'agentic_traces') {
+ if (specMethod === 'mtp') hwKey = `${hwKey}_mtp`;
+ else if (specMethod && specMethod !== 'none') hwKey = `${hwKey}_${specMethod}`;
+ }
return hwKey;
}
diff --git a/packages/app/src/lib/default-precisions.test.ts b/packages/app/src/lib/default-precisions.test.ts
index 9d8a72cac..0f1140a41 100644
--- a/packages/app/src/lib/default-precisions.test.ts
+++ b/packages/app/src/lib/default-precisions.test.ts
@@ -33,6 +33,24 @@ describe('countCurvesByPrecision', () => {
it('returns {} for no rows', () => {
expect(countCurvesByPrecision([])).toEqual({});
});
+
+ it('counts mixed agentic spec methods as one rendered curve', () => {
+ const base = { ...row('fp8', 'b200'), benchmark_type: 'agentic_traces' };
+ const counts = countCurvesByPrecision([
+ base,
+ { ...base, spec_method: 'mtp' },
+ { ...base, spec_method: 'eagle' },
+ ]);
+
+ expect(counts).toEqual({ fp8: 1 });
+ });
+
+ it('continues counting fixed-sequence spec methods as separate curves', () => {
+ const base = { ...row('fp8', 'b200'), benchmark_type: 'single_turn' };
+ const counts = countCurvesByPrecision([base, { ...base, spec_method: 'mtp' }]);
+
+ expect(counts).toEqual({ fp8: 2 });
+ });
});
describe('pickDefaultPrecisions', () => {
diff --git a/packages/app/src/lib/default-precisions.ts b/packages/app/src/lib/default-precisions.ts
index 71c493868..85fd3d2fe 100644
--- a/packages/app/src/lib/default-precisions.ts
+++ b/packages/app/src/lib/default-precisions.ts
@@ -8,8 +8,9 @@ import { Precision, PRECISION_OPTIONS } from './data-mappings';
* showing a near-empty chart on first load. Instead we default to whichever
* precision has the most data, with a guard against committing to a sparse one.
*
- * "Curves" = distinct (hardware, framework, spec_method, disagg) series that
- * would render for a precision — i.e. how many lines the chart draws.
+ * "Curves" = distinct chart series that would render for a precision. Fixed-
+ * sequence series include `spec_method`; agentic series do not because one line
+ * can contain points from multiple speculative-decoding methods.
*/
/**
@@ -26,6 +27,7 @@ interface CurveRow {
framework: string;
spec_method: string;
disagg: boolean;
+ benchmark_type?: string;
}
/** Count distinct curves per precision from already-filtered rows (model + sequence). */
@@ -37,7 +39,8 @@ export function countCurvesByPrecision(rows: CurveRow[]): Record
curves = new Set();
seen.set(r.precision, curves);
}
- curves.add(`${r.hardware}|${r.framework}|${r.spec_method}|${r.disagg}`);
+ const specMethod = r.benchmark_type === 'agentic_traces' ? '' : r.spec_method;
+ curves.add(`${r.hardware}|${r.framework}|${specMethod}|${r.disagg}`);
}
const counts: Record = {};
for (const [precision, curves] of seen) counts[precision] = curves.size;
diff --git a/packages/app/src/lib/overview-data.test.ts b/packages/app/src/lib/overview-data.test.ts
index 43d3ea64b..98fd146db 100644
--- a/packages/app/src/lib/overview-data.test.ts
+++ b/packages/app/src/lib/overview-data.test.ts
@@ -1119,6 +1119,33 @@ describe('overview platform selection', () => {
);
});
+ it('builds one AgentX frontier from mixed standard and MTP points', () => {
+ const summary = buildOverviewModelSummary(Model.GLM_5_2, [
+ agenticRow(40, 30, 12600, 1200, {
+ hardware: 'b200',
+ conc: 8,
+ spec_method: 'none',
+ }),
+ agenticRow(50, 25, 10800, 850, {
+ hardware: 'b200',
+ conc: 12,
+ spec_method: 'mtp',
+ }),
+ agenticRow(60, 20, 9000, 800, {
+ hardware: 'b200',
+ conc: 16,
+ spec_method: 'none',
+ }),
+ ]);
+
+ const config = summary.platforms.find(({ hardware }) => hardware === 'b200')?.read.config;
+ expect(config).toMatchObject({
+ specMethod: 'mixed',
+ specLabel: 'STP + MTP',
+ hwKey: 'b200_sglang',
+ });
+ });
+
it('restricts AgentX points to the E2E frontier on total throughput', () => {
// The slower-E2E point wins on output tokens but loses on total tokens, so
// the total-token frontier drops it and the tier read becomes unreachable.
diff --git a/packages/app/src/lib/overview-data.ts b/packages/app/src/lib/overview-data.ts
index 1bedd5597..51af391d1 100644
--- a/packages/app/src/lib/overview-data.ts
+++ b/packages/app/src/lib/overview-data.ts
@@ -330,15 +330,16 @@ function overviewScenarioRows(
export type OverviewServingSeriesRow = Pick<
BenchmarkRow,
'model' | 'hardware' | 'framework' | 'spec_method' | 'precision' | 'disagg' | 'is_multinode'
-> & { offload_mode?: string | null };
+> & { offload_mode?: string | null; benchmark_type?: string };
/** Stable identity for one Overview serving envelope across topology points. */
export function overviewServingSeriesKey(row: OverviewServingSeriesRow): string {
+ const specMethod = row.benchmark_type === 'agentic_traces' ? '' : row.spec_method;
return JSON.stringify([
row.model,
row.hardware,
row.framework,
- row.spec_method,
+ specMethod,
row.precision,
row.disagg,
row.is_multinode,
@@ -367,7 +368,19 @@ function buildConfigs(
(latest, row) => (row.date > latest ? row.date : latest),
configRows[0].date,
);
- const latestRows = configRows.filter((row) => row.date === latestDate);
+ let latestRows = configRows.filter((row) => row.date === latestDate);
+ if (scenario === 'agentx' && latestRows.some((row) => row.workflow_run_id !== undefined)) {
+ const winningRow = latestRows.reduce((winner, row) => {
+ const startedAt = row.run_started_at ?? '';
+ const winnerStartedAt = winner.run_started_at ?? '';
+ if (startedAt !== winnerStartedAt) return startedAt > winnerStartedAt ? row : winner;
+ return (row.workflow_run_id ?? Number.NEGATIVE_INFINITY) >
+ (winner.workflow_run_id ?? Number.NEGATIVE_INFINITY)
+ ? row
+ : winner;
+ });
+ latestRows = latestRows.filter((row) => row.workflow_run_id === winningRow.workflow_run_id);
+ }
const config = buildConfigResult(model, scenario, latestRows[0].precision, key, latestRows);
if (config) configs.push(config);
}
@@ -638,7 +651,17 @@ function buildConfigResult(
if (feed.length === 0) return null;
const first = rows[0];
- const { hardware, framework, spec_method: specMethod, disagg, is_multinode: isMultinode } = first;
+ const { hardware, framework, disagg, is_multinode: isMultinode } = first;
+ const specMethods = [...new Set(rows.map((row) => row.spec_method))];
+ const specMethod = specMethods.length === 1 ? specMethods[0] : 'mixed';
+ const specLabel =
+ specMethod === 'mixed'
+ ? specMethods
+ .map((method) =>
+ method === 'none' || method === '' ? 'STP' : resolveFrameworkPartLabel(model, method),
+ )
+ .join(' + ')
+ : resolveFrameworkPartLabel(model, specMethod);
const sourceRunUrls = [
...new Set(rows.flatMap((row) => (row.run_url === null ? [] : [row.run_url]))),
].toSorted();
@@ -646,11 +669,17 @@ function buildConfigResult(
key,
dbModel: first.model,
hardware,
- hwKey: buildAvailabilityHwKey(hardware, framework, specMethod, disagg),
+ hwKey: buildAvailabilityHwKey(
+ hardware,
+ framework,
+ specMethod,
+ disagg,
+ scenario === 'agentx' ? 'agentic_traces' : 'single_turn',
+ ),
framework,
frameworkLabel: resolveFrameworkPartLabel(model, framework),
specMethod,
- specLabel: resolveFrameworkPartLabel(model, specMethod),
+ specLabel,
disagg,
isMultinode,
precision,
diff --git a/packages/app/src/lib/overview-links.test.ts b/packages/app/src/lib/overview-links.test.ts
index 63ef4a085..11bf02dc1 100644
--- a/packages/app/src/lib/overview-links.test.ts
+++ b/packages/app/src/lib/overview-links.test.ts
@@ -135,6 +135,17 @@ describe('buildOverviewDashboardHref', () => {
'i_spec=mtp',
);
});
+
+ it('does not filter an AgentX mixed-spec curve to only one decode method', () => {
+ const href = buildOverviewDashboardHref(
+ 'en',
+ summary({ scenario: 'agentx' }),
+ config({ specMethod: 'mixed', hwKey: 'b200_sglang' }),
+ );
+
+ expect(href).toContain('i_seq=agentic-traces');
+ expect(href).not.toContain('i_spec=');
+ });
});
describe('buildOverviewHistoryDashboardHref', () => {
diff --git a/packages/app/src/lib/overview-links.ts b/packages/app/src/lib/overview-links.ts
index d8fa4eae8..223fe468c 100644
--- a/packages/app/src/lib/overview-links.ts
+++ b/packages/app/src/lib/overview-links.ts
@@ -78,7 +78,8 @@ function inferenceRoute(locale: 'en' | 'zh'): string {
* (mirrors `pointSpecMode` in quickFilters.ts, minus its hwKey suffix check —
* overview `specMethod` comes straight from `spec_method`).
*/
-function dashboardSpecMode(specMethod: string): 'mtp' | 'stp' {
+function dashboardSpecMode(specMethod: string): 'mtp' | 'stp' | undefined {
+ if (specMethod === 'mixed') return undefined;
return specMethod === 'none' || specMethod === '' ? 'stp' : 'mtp';
}
diff --git a/packages/db/src/queries/benchmarks.ts b/packages/db/src/queries/benchmarks.ts
index 87c5efbd2..d7989e4cc 100644
--- a/packages/db/src/queries/benchmarks.ts
+++ b/packages/db/src/queries/benchmarks.ts
@@ -45,6 +45,9 @@ export interface BenchmarkRow {
*/
workers?: BenchmarkWorkerRow[];
date: string;
+ /** Internal workflow identity used to keep merged agentic curves within one run. */
+ workflow_run_id?: number;
+ run_started_at?: string | null;
run_url: string | null;
}
@@ -147,6 +150,8 @@ export async function getLatestBenchmarks(
br.metrics,
br.workers,
br.date::text,
+ br.workflow_run_id,
+ wr.run_started_at::text,
CASE WHEN wr.html_url IS NOT NULL THEN wr.html_url || '/attempts/' || wr.run_attempt ELSE NULL END AS run_url
FROM benchmark_results br
JOIN configs c ON c.id = br.config_id
@@ -194,6 +199,8 @@ export async function getLatestBenchmarks(
lb.metrics,
lb.workers,
lb.date::text,
+ lb.workflow_run_id,
+ wr.run_started_at::text,
CASE WHEN wr.html_url IS NOT NULL THEN wr.html_url || '/attempts/' || wr.run_attempt ELSE NULL END AS run_url
FROM latest_benchmarks lb
JOIN configs c ON c.id = lb.config_id
@@ -246,6 +253,8 @@ export async function getBenchmarksForRun(
br.metrics,
br.workers,
br.date::text,
+ br.workflow_run_id,
+ wr.run_started_at::text,
CASE WHEN wr.html_url IS NOT NULL THEN wr.html_url || '/attempts/' || wr.run_attempt ELSE NULL END AS run_url
FROM benchmark_results br
JOIN configs c ON c.id = br.config_id
@@ -266,10 +275,15 @@ export async function getBenchmarksForRun(
export async function getAllBenchmarksForHistory(
sql: DbClient,
modelKey: string | string[],
- isl: number,
- osl: number,
+ isl: number | null,
+ osl: number | null,
+ benchmarkType?: string,
): Promise {
const modelKeys = Array.isArray(modelKey) ? modelKey : [modelKey];
+ const sequenceFilter =
+ benchmarkType === 'agentic_traces'
+ ? sql`br.benchmark_type = 'agentic_traces'`
+ : sql`br.isl = ${isl} AND br.osl = ${osl}`;
const rows = await sql`
SELECT
br.id,
@@ -299,11 +313,12 @@ export async function getAllBenchmarksForHistory(
br.metrics - '{std_ttft,std_tpot,std_e2el,std_intvty,std_itl,mean_ttft,mean_tpot,mean_e2el,mean_intvty,mean_itl}'::text[] as metrics,
br.workers,
br.date::text,
+ br.workflow_run_id,
+ wr.run_started_at::text,
CASE WHEN wr.html_url IS NOT NULL THEN wr.html_url || '/attempts/' || wr.run_attempt ELSE NULL END AS run_url
FROM configs c
JOIN benchmark_results br ON br.config_id = c.id
- AND br.isl = ${isl}
- AND br.osl = ${osl}
+ AND ${sequenceFilter}
AND br.error IS NULL
JOIN latest_workflow_runs wr ON wr.id = br.workflow_run_id
WHERE c.model = ANY(${modelKeys})
diff --git a/packages/db/src/queries/workflow-info.ts b/packages/db/src/queries/workflow-info.ts
index 01e13dd88..e32401395 100644
--- a/packages/db/src/queries/workflow-info.ts
+++ b/packages/db/src/queries/workflow-info.ts
@@ -84,7 +84,12 @@ export interface RunConfigRow {
* shipped data without a changelog entry still surfaces — the comparison UI uses
* this to enumerate every run on a date, not just runs with changelog notes.
*/
-export async function getRunConfigsByDate(sql: DbClient, date: string): Promise {
+export async function getRunConfigsByDate(
+ sql: DbClient,
+ date: string,
+ benchmarkType?: 'agentic_traces',
+): Promise {
+ const benchmarkTypeFilter = benchmarkType ? sql`AND br.benchmark_type = ${benchmarkType}` : sql``;
const rows = await sql`
SELECT DISTINCT
wr.github_run_id,
@@ -102,6 +107,7 @@ export async function getRunConfigsByDate(sql: DbClient, date: string): Promise<
JOIN latest_workflow_runs wr ON wr.id = br.workflow_run_id
WHERE br.date = ${date}::date
AND br.error IS NULL
+ ${benchmarkTypeFilter}
`;
return rows as unknown as RunConfigRow[];
}