Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions openapi/ga/individual/platform.openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions openapi/ga/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions openapi/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions sdk/python/nemo-platform/.nmpcontext/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions services/intake/src/nmp/intake/api/v2/experiments/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,18 @@ class EvaluationResponse(BaseModel):
description="Average total tokens (input + output) per test case, aggregated across the evaluation.",
)

@computed_field( # type: ignore[prop-decorator]
Comment thread
shanaiabuggy marked this conversation as resolved.
Outdated
json_schema_extra={"nullable": True},
description=(
"End-to-end latency in milliseconds assuming tasks run serially: the sum of per-test-case "
"latency, where a test case run more than once contributes the average of its attempts. "
"Equal to latency_ms.sum; null when no session carries latency."
),
)
@property
def end_to_end_latency_ms(self) -> float | None:
return self.latency_ms.sum if self.latency_ms is not None else None

@computed_field( # type: ignore[prop-decorator]
deprecated=True,
description="Deprecated single-experiment alias; the first of experiment_ids. Use experiment_ids.",
Expand Down
36 changes: 36 additions & 0 deletions services/intake/tests/test_end_to_end_latency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""``end_to_end_latency_ms`` computed field on EvaluationResponse.

It names ``latency_ms.sum`` — the test-case-weighted latency sum (per-test-case latency, with a
test case's attempts averaged, summed across test cases), i.e. end-to-end latency assuming tasks
run serially. The rollup already computes ``latency_ms.sum`` with that semantics; the field only
surfaces it so consumers don't have to know the convention.
"""

from nmp.intake.api.v2.experiments.schemas import EvaluationResponse, EvaluatorAggregate


def _response(latency: EvaluatorAggregate | None) -> EvaluationResponse:
return EvaluationResponse(
id="e",
name="e",
workspace="default",
experiment_ids=["grp"],
dataset_name="ds",
latency_ms=latency,
)


def test_end_to_end_latency_equals_latency_sum() -> None:
resp = _response(EvaluatorAggregate(sum=1234.5, mean=411.5, count=3))
assert resp.end_to_end_latency_ms == 1234.5
# and it serializes under the field name
assert resp.model_dump()["end_to_end_latency_ms"] == 1234.5


def test_end_to_end_latency_is_none_without_latency() -> None:
assert _response(None).end_to_end_latency_ms is None
# latency present but no summable value (no session carried latency)
assert _response(EvaluatorAggregate()).end_to_end_latency_ms is None
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const STATIC_SORT_FIELD_MAP: Readonly<Record<string, string>> = {
created_at: 'created_at',
cost_usd: 'cost_usd.mean',
latency_ms: 'latency_ms.mean',
end_to_end_latency_ms: 'latency_ms.sum',
tokens: 'tokens.mean',
test_case_count: 'test_case_count',
};
Expand All @@ -64,6 +65,7 @@ const STATIC_SORT_FIELD_MAP: Readonly<Record<string, string>> = {
const sortFieldToColumnId = (field: string): string | undefined => {
if (field === 'name' || field === 'created_at' || field === 'test_case_count') return field;
if (field.startsWith('cost_usd.')) return 'cost_usd';
if (field === 'latency_ms.sum') return 'end_to_end_latency_ms';
if (field.startsWith('latency_ms.')) return 'latency_ms';
if (field.startsWith('tokens.')) return 'tokens';
const evaluatorMatch = field.match(/^evaluators\.(.+)\.[^.]+$/);
Expand Down Expand Up @@ -95,6 +97,7 @@ const seedSortFromDefault = (
const getEvaluationFilterField = (id: string): string | undefined => {
if (id === 'cost_usd') return 'cost_usd.mean';
if (id === 'latency_ms') return 'latency_ms.mean';
if (id === 'end_to_end_latency_ms') return 'latency_ms.sum';
if (id === 'tokens') return 'tokens.mean';
const evaluatorMatch = id.match(/^evaluator-(.+)$/);
if (evaluatorMatch) return `evaluators.${evaluatorMatch[1]}.mean`;
Expand Down Expand Up @@ -434,6 +437,14 @@ export const ExperimentDataView: FC<ExperimentDataViewProps> = ({ group, paretoV
);
},
}),
accessor((original) => original.end_to_end_latency_ms, {
id: 'end_to_end_latency_ms',
header: 'End-to-end latency',
enableSorting: true,
meta: { title: false, filter: numberRangeFilter('End-to-end latency') },
// Sum of per-task latency (a task's attempts averaged): total time to run the tasks serially.
cell: ({ row }) => <Text>{formatDurationMs(row.original.end_to_end_latency_ms)}</Text>,
}),
accessor((original) => original.tokens?.mean, {
id: 'tokens',
header: 'Avg Tokens',
Expand Down
1 change: 1 addition & 0 deletions web/packages/studio/src/mocks/intake/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const mockEvaluation = (name: string): EvaluationResponse => ({
experiment_ids: ['grp_my-group'],
dataset_name: 'sample-dataset',
experiment_group_id: 'grp_my-group',
end_to_end_latency_ms: null,
});

export const mockEvaluationsPage = (): EvaluationResponsesPage => ({
Expand Down