Skip to content
Open
6 changes: 5 additions & 1 deletion packages/app/cypress/e2e/gpu-compare-agentic-detail.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { unlockAgenticGate } from '../support/e2e';
import { interceptDerivedAgenticMetrics, unlockAgenticGate } from '../support/e2e';

// ---------------------------------------------------------------------------
// Spec-scoped fixture helpers
Expand Down Expand Up @@ -147,6 +147,9 @@ describe('GPU comparison agentic point detail', () => {
);
request.reply({ body: result });
});
// The agentic default x-axis mode (OSL / E2EL) fetches derived metrics on
// mount; without values every point drops out of the (remapped) data set.
interceptDerivedAgenticMetrics();

cy.visit('/inference?g_model=DeepSeek-V4-Pro&i_seq=agentic-traces&i_prec=fp4', {
onBeforeLoad(win) {
Expand Down Expand Up @@ -206,6 +209,7 @@ describe('GPU comparison agentic point detail', () => {
'agenticAvailability',
);
cy.intercept('GET', '/api/v1/benchmarks*', { body: agenticBenchmarks }).as('agenticBenchmarks');
interceptDerivedAgenticMetrics();

cy.visit(
'/inference?g_model=DeepSeek-V4-Pro&i_seq=agentic-traces&i_prec=fp4&i_gpus=b200_sglang,b200_vllm&i_dates=2026-06-12&i_dstart=2026-06-12&i_dend=2026-06-12',
Expand Down
7 changes: 6 additions & 1 deletion packages/app/cypress/e2e/overlay-legend-remove.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* appeared to do nothing. The legend toggle already had the overlay-aware
* split (`unifiedToggle`); the X now shares it (`handleRemoveHwType`).
*/
import { unlockAgenticGate } from '../support/e2e';
import { interceptDerivedAgenticMetrics, unlockAgenticGate } from '../support/e2e';
import {
countVisible,
interceptOverlayRun,
Expand All @@ -20,13 +20,18 @@ import {
describe('Official legend X works while an unofficial overlay is loaded', () => {
before(() => {
interceptOverlayRun();
// The agentic default mode is OSL / E2EL (which suppresses overlays and
// fetches derived metrics) — stub the fetch, then switch to Interactivity
// where the overlay renders.
interceptDerivedAgenticMetrics();
cy.visit(`/inference?unofficialrun=${OVERLAY_RUN_ID}&i_seq=agentic-traces&i_pctl=p90`, {
onBeforeLoad(win) {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
unlockAgenticGate(win);
},
});
cy.wait('@unofficialRun');
cy.get('[data-testid="x-axis-mode-interactivity"]').click();
cy.get('[data-testid="chart-figure"]').should('have.length.at.least', 1);
cy.get('[data-testid="inference-chart-display"] svg .unofficial-overlay-pt').should(
'have.length',
Expand Down
7 changes: 6 additions & 1 deletion packages/app/cypress/e2e/overlay-optimal-only.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* dashed roofline (the monotone spline between C=8 and C=2 passes within
* ~0.5% of it) while the official twin was hidden.
*/
import { unlockAgenticGate } from '../support/e2e';
import { interceptDerivedAgenticMetrics, unlockAgenticGate } from '../support/e2e';
import {
countVisible,
interceptOverlayRun,
Expand All @@ -21,13 +21,18 @@ import {
describe('Overlay points respect Optimal Only (agentic interactivity)', () => {
before(() => {
interceptOverlayRun();
// The agentic default mode is OSL / E2EL (which suppresses overlays and
// fetches derived metrics) — stub the fetch, then switch to the
// Interactivity mode this suite is about.
interceptDerivedAgenticMetrics();
cy.visit(`/inference?unofficialrun=${OVERLAY_RUN_ID}&i_seq=agentic-traces&i_pctl=p90`, {
onBeforeLoad(win) {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
unlockAgenticGate(win);
},
});
cy.wait('@unofficialRun');
cy.get('[data-testid="x-axis-mode-interactivity"]').click();
cy.get('[data-testid="chart-figure"]').should('have.length.at.least', 1);
cy.get('[data-testid="x-axis-mode-interactivity"]').should(
'have.attr',
Expand Down
152 changes: 90 additions & 62 deletions packages/app/cypress/e2e/ttft-x-axis-toggle.cy.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,4 @@
import { unlockAgenticGate } from '../support/e2e';

const interceptDerivedMetrics = () => {
cy.intercept('GET', '/api/v1/derived-agentic-metrics*', (request) => {
const ids = new URL(request.url).searchParams.get('ids')?.split(',').filter(Boolean) ?? [];
request.reply({
body: Object.fromEntries(
ids.map((id, index) => [
id,
{
id: Number(id),
normalized_session_time_s: 60 + index,
p90_prefill_tps_per_user: 100 + index,
p75_normalized_e2e_400_s: 8 + index,
p90_normalized_e2e_400_s: 12 + index,
p75_osl_per_e2el: 40 + index,
p90_osl_per_e2el: 25 + index,
},
]),
),
});
}).as('derivedAgenticMetrics');
};
import { interceptDerivedAgenticMetrics, unlockAgenticGate } from '../support/e2e';

// This spec exercises the agentic x-axis modes, which only exist when the
// selected model resolves to the Agentic Traces scenario. The default e2e
Expand Down Expand Up @@ -148,10 +126,9 @@ const interceptFixedSequenceData = () => {
describe('X-Axis Mode Toggle (inference chart)', () => {
before(() => {
interceptAgenticData();
// Stub derived metrics before the visit: if the agentic DEFAULT x-axis mode
// is (or becomes) a derived mode that fetches /derived-agentic-metrics on
// mount, the chart must not sit on its loading skeleton.
interceptDerivedMetrics();
// The agentic default mode is OSL / E2EL, which fetches derived metrics
// on first render — stub them before the visit.
interceptDerivedAgenticMetrics();
cy.visit('/inference?i_seq=agentic-traces', {
onBeforeLoad(win) {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
Expand All @@ -162,14 +139,32 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
cy.get('[data-testid="chart-figure"]').should('have.length.at.least', 1);
});

it('shows Interactivity by default for the agentic view', () => {
it('shows OSL / E2EL by default for the agentic view, as the leftmost option', () => {
cy.get('[data-testid="scenario-selector"]').should('contain.text', 'Agentic Traces');
cy.get('[data-testid="x-axis-mode-ttft"]').should('be.visible');
cy.get('[data-testid="x-axis-mode-e2e"]').should('be.visible');
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').should('be.visible');
cy.get('[data-testid="x-axis-mode-interactivity"]')
cy.get('[data-testid="x-axis-mode-interactivity"]').should('be.visible');
cy.get('[data-testid="x-axis-mode-osl-e2el"]')
.should('be.visible')
.and('have.attr', 'aria-selected', 'true');
// OSL / E2EL leads the mode list for agentic.
cy.get('[data-testid="x-axis-mode-buttons"] [role="tab"]')
.first()
.should('have.attr', 'data-testid', 'x-axis-mode-osl-e2el');
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'P90 OSL / E2EL');
cy.get('[data-testid="chart-figure"] svg').should(
'contain.text',
'P90 OSL / E2EL (tok/s/user)',
);
});

it('switches to Interactivity and updates the heading', () => {
cy.get('[data-testid="x-axis-mode-interactivity"]').click();
cy.get('[data-testid="x-axis-mode-interactivity"]').should(
'have.attr',
'aria-selected',
'true',
);
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'Interactivity');
});

Expand Down Expand Up @@ -230,6 +225,9 @@ describe('X-Axis Mode Toggle (inference chart)', () => {

it('honors explicit label URL overrides for the agentic view', () => {
interceptAgenticData();
// Fresh page load → fresh React Query cache → the default OSL / E2EL
// mode refetches derived metrics.
interceptDerivedAgenticMetrics();
cy.visit('/inference?i_seq=agentic-traces&i_label=0&i_advlabel=0&i_linelabel=1', {
onBeforeLoad(win) {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
Expand All @@ -255,30 +253,27 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
cy.get('[data-testid="chart-figure"] svg').should('contain.text', 'P90 End-to-end Latency (s)');
});

it('switches to request-level normalized E2E at 400 output tokens', () => {
interceptDerivedMetrics();
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').click();
cy.wait('@derivedAgenticMetrics');
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').should(
'have.attr',
'aria-selected',
'true',
);
cy.get('[data-testid="chart-figure"] h2').should(
'contain.text',
'P90 Normalized E2E @ 400 output tokens',
);
it('switches back to request-level OSL / E2EL', () => {
// No cy.wait here: the derived metrics were fetched (and stubbed) on the
// initial default-mode load and are still fresh in the React Query cache
// (staleTime 5 min), so re-entering the mode fires no new request.
cy.get('[data-testid="x-axis-mode-osl-e2el"]').click();
cy.get('[data-testid="x-axis-mode-osl-e2el"]').should('have.attr', 'aria-selected', 'true');
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'P90 OSL / E2EL');
cy.get('[data-testid="chart-figure"] svg').should(
'contain.text',
'P90 Normalized E2E @ 400 output tokens (s)',
'P90 OSL / E2EL (tok/s/user)',
);

cy.get('[data-testid="percentile-selector"]').click();
cy.contains('[role="option"]', 'p75').click();
cy.get('[data-testid="chart-figure"] h2').should(
'contain.text',
'P75 Normalized E2E @ 400 output tokens',
);
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'P75 OSL / E2EL');

// The percentile selector is shared page state for the whole suite —
// restore the p90 default so later tests assert against a known value.
cy.get('[data-testid="percentile-selector"]').click();
cy.contains('[role="option"]', 'p90').click();
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'P90 OSL / E2EL');
});

it('switches back to Interactivity', () => {
Expand All @@ -289,14 +284,52 @@ describe('X-Axis Mode Toggle (inference chart)', () => {
'true',
);
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'Interactivity');
// Percentile was switched to p75 in the previous test — the axis label follows.
cy.get('[data-testid="chart-figure"] svg').should(
'contain.text',
'P90 Interactivity (tok/s/user)',
);
});

it('follows the percentile selector in the Interactivity axis label', () => {
// Select p75 here rather than inheriting it from another test — the axis
// label must track the selector on its own.
cy.get('[data-testid="x-axis-mode-interactivity"]').click();
cy.get('[data-testid="percentile-selector"]').click();
cy.contains('[role="option"]', 'p75').click();
cy.get('[data-testid="chart-figure"] svg').should(
'contain.text',
'P75 Interactivity (tok/s/user)',
);
});
});

describe('X-axis mode URL param', () => {
// Regression: the reconcile effect used to run before availability resolved
// the sequence. It recorded the fixed-seq placeholder kind, then treated the
// switch to agentic as a user-driven kind change and clobbered the
// URL-restored mode with the agentic default (OSL / E2EL).
it('keeps a URL-restored mode through the agentic sequence resolving', () => {
interceptAgenticData();
interceptDerivedAgenticMetrics();
cy.visit('/inference?i_seq=agentic-traces&i_xmode=interactivity', {
onBeforeLoad(win) {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
unlockAgenticGate(win);
},
});
cy.get('[data-testid="scenario-selector"]').should('contain.text', 'Agentic Traces');
cy.get('[data-testid="x-axis-mode-interactivity"]').should(
'have.attr',
'aria-selected',
'true',
);
// Assert on the rendered chart too: the clobber happened one tick after
// the buttons first painted, so a button-only check could pass too early.
cy.get('[data-testid="chart-figure"] h2').should('contain.text', 'Interactivity');
cy.get('[data-testid="x-axis-mode-osl-e2el"]').should('have.attr', 'aria-selected', 'false');
});
});

describe('Default scenario', () => {
it('bare /inference defaults to 8K / 1K even when the model has agentic data', () => {
// Availability contains BOTH agentic and fixed-seq rows for the default
Expand Down Expand Up @@ -411,9 +444,8 @@ const interceptAgenticDataWithOverlay = () => {
describe('X-Axis Mode Toggle — overlay path (finding #8 regression guard)', () => {
before(() => {
interceptAgenticDataWithOverlay();
// Same as the main suite: keep the visit independent of the agentic default
// x-axis mode (a derived default fetches /derived-agentic-metrics on mount).
interceptDerivedMetrics();
// Default agentic mode is OSL / E2EL → derived metrics fetch on mount.
interceptDerivedAgenticMetrics();
cy.visit(`/inference?unofficialrun=${OVERLAY_RUN_ID}&i_seq=agentic-traces`, {
onBeforeLoad(win) {
win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now()));
Expand Down Expand Up @@ -463,18 +495,14 @@ describe('X-Axis Mode Toggle — overlay path (finding #8 regression guard)', ()
});
});

it('normalized-e2e mode shows suppression banner for unofficial-run overlays', () => {
interceptDerivedMetrics();
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').click();
cy.get('[data-testid="x-axis-mode-normalized-e2e"]').should(
'have.attr',
'aria-selected',
'true',
);
it('osl-e2el mode shows suppression banner for unofficial-run overlays', () => {
// Derived metrics are cached from the initial default-mode load.
cy.get('[data-testid="x-axis-mode-osl-e2el"]').click();
cy.get('[data-testid="x-axis-mode-osl-e2el"]').should('have.attr', 'aria-selected', 'true');
// The suppression message appears because isUnofficialRun is true and the
// mode is 'normalized-e2e' (documented in ChartDisplay.tsx ~line 640).
// mode is 'osl-e2el' (documented in ChartDisplay.tsx).
cy.contains(
'Normalized E2E requires persisted per-request traces, so unofficial-run overlays are unavailable for this experimental view.',
'OSL / E2EL requires persisted per-request traces, so unofficial-run overlays are unavailable for this experimental view.',
).should('be.visible');
});
});
27 changes: 27 additions & 0 deletions packages/app/cypress/support/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,30 @@ export function unlockAgenticGate(win: Window): void {
// agentic surfaces are public regardless.
}
}

/**
* Stub `/api/v1/derived-agentic-metrics` with deterministic per-id values.
*
* OSL / E2EL is the DEFAULT x-axis mode for agentic scenarios, so any spec
* that loads the agentic view fires this fetch on mount — without a stub the
* fixture server has no DB and the chart sits on its loading skeleton until
* the query errors out. Values are index-stable so axis positions are
* deterministic. Register BEFORE `cy.visit`.
*/
export function interceptDerivedAgenticMetrics(): void {
cy.intercept('GET', '/api/v1/derived-agentic-metrics*', (request) => {
const ids = new URL(request.url).searchParams.get('ids')?.split(',').filter(Boolean) ?? [];
request.reply({
body: Object.fromEntries(
ids.map((id, index) => [
id,
{
id: Number(id),
p75_osl_per_e2el: 40 + index,
p90_osl_per_e2el: 25 + index,
},
]),
),
});
}).as('derivedAgenticMetrics');
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,9 @@ const getCachedDerivedAgenticMetrics = cachedQuery(
*
* Returns per-id derived metrics computed live from the stored aiperf
* profile_export.jsonl blobs:
* - normalized_session_time_s: mean across sessions of session e2e time
* (Σ per-turn request_latency) rescaled by mean_load / session_load.
* - p90_prefill_tps_per_user: P90 of per-turn prefill TPS/user (ISL / TTFT)
* across every turn in every session.
* - p75/p90_normalized_e2e_400_s: percentile of per-request
* TTFT + 399 × observed ITL.
* - p75/p90_osl_per_e2el: slow-tail OSL / E2E-latency in tok/s/user,
* computed as 1 / pXX(per-request E2EL/OSL) across every turn in every
* session — plain interactivity charged for the prefill wait.
*
* Ids without a trace_replay blob or with unparseable records are omitted.
*/
Expand Down
12 changes: 9 additions & 3 deletions packages/app/src/components/inference/InferenceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -611,14 +611,20 @@ export function InferenceProvider({

// Reconcile the x-axis mode with the scenario kind:
// - On mount with no `i_xmode` URL param: snap to the kind's natural default
// (interactivity for both agentic and fixed-sequence scenarios). The state was initialized
// (OSL / E2EL for agentic — the "north star" e2e-interactivity view —
// and interactivity for fixed-sequence scenarios). The state was initialized
// to a SSR-stable constant so server and client render the same DOM; this
// effect fixes it up after hydration.
// - When the user later switches sequence kinds: snap to the new kind's
// natural default (the prior selection was for a different kind, so it
// doesn't carry over).
const lastSeqKindRef = useRef<ReturnType<typeof sequenceKind> | null>(null);
useEffect(() => {
// Wait for availability to resolve the sequence. Before it does,
// `effectiveSequence` is a fixed-seq placeholder; recording that kind here
// would make the later switch to agentic look like a user-driven kind
// change and clobber a URL-restored `i_xmode` with the kind's default.
if (!sequenceResolved) return;
const kind = sequenceKind(effectiveSequence);
const isInitialMount = lastSeqKindRef.current === null;
const isAgenticOnlyMode = isAgenticOnlyXAxisMode(selectedXAxisMode);
Expand All @@ -642,8 +648,8 @@ export function InferenceProvider({
// — fall through to the default snap below.
return;
}
handleSetXAxisMode('interactivity');
}, [effectiveSequence, selectedXAxisMode, handleSetXAxisMode]);
handleSetXAxisMode(kind === 'agentic' ? 'osl-e2el' : 'interactivity');
}, [sequenceResolved, effectiveSequence, selectedXAxisMode, handleSetXAxisMode]);

// Reconcile selectedE2eXAxisMetric whenever the mode, sequence kind, or
// agentic percentile changes. For fixed-seq the JSONB only carries
Expand Down
Loading