Source code for fastrag.benchmark.metrics.operational

"""Operational metrics: cost of running the pipeline, not quality of its output.

``TokenUsage`` and ``Cost`` return ``None`` for every sample against a bare
:class:`~fastrag.retrieval.base.Retriever` or a provider that reports no
usage — "not measurable for this sample"
(:meth:`~fastrag.benchmark.metrics.base.Metric.score`), not a zero, and
it is why a retrieval-only benchmark should not list them: every sample
would come back ``incomplete``.
"""

from __future__ import annotations

from dataclasses import dataclass

from fastrag.benchmark.datasets import EvalSample
from fastrag.benchmark.metrics.base import Metric, QueryOutcome


[docs] class Latency(Metric): """Wall-clock time for one sample, in milliseconds. Never ``None``: :class:`~fastrag.benchmark.benchmark.Benchmark` times every sample itself, so this metric works against a bare :class:`~fastrag.retrieval.base.Retriever` too, with no chat model involved. """ name = "latency_ms" higher_is_better = False
[docs] async def score(self, sample: EvalSample, outcome: QueryOutcome) -> float | None: """Return the sample's wall-clock latency in milliseconds.""" return outcome.latency_ms
[docs] class TokenUsage(Metric): """Input plus output tokens for one sample. ``None`` when either count is missing — a bare :class:`~fastrag.retrieval.base.Retriever` never generates, and :meth:`~fastrag.query.engine.QueryEngine.ask` does not report usage (in the new query layer ``ChatModel.invoke`` returns plain text, not a ``Completion`` carrying token counts). """ name = "total_tokens" higher_is_better = False
[docs] async def score(self, sample: EvalSample, outcome: QueryOutcome) -> float | None: """Total tokens, or ``None`` when the provider reported no usage.""" if outcome.input_tokens is None or outcome.output_tokens is None: return None return float(outcome.input_tokens + outcome.output_tokens)
[docs] @dataclass(frozen=True, kw_only=True) class Cost(Metric): """Dollar cost of one sample's generation call. Args: input_price: Price per 1,000,000 input tokens, in USD. output_price: Price per 1,000,000 output tokens, in USD. """ input_price: float output_price: float name = "cost_usd" higher_is_better = False
[docs] async def score(self, sample: EvalSample, outcome: QueryOutcome) -> float | None: """Dollar cost from token counts, or ``None`` when usage is unreported.""" if outcome.input_tokens is None or outcome.output_tokens is None: return None return ( outcome.input_tokens * self.input_price + outcome.output_tokens * self.output_price ) / 1_000_000