What component(s) are affected?
Opik version
opik 2.2.70, and current main at 5cfff7b013107d44a449bd2b085c1b16f8d92eea. Both carry the same line quoted below.
Describe the problem
evaluate_experiment() re-scores an existing experiment. Its docstring lists task_span as one of the inputs a scoring function may take ("task_span - the data collected during the LLM task execution [optional]"), and evaluate() does support such metrics (covered by tests/e2e/evaluation/test_evaluate_task_span.py). In the re-scoring path, however, every metric that requires task_span is discarded before it can be scored, and nothing reports that it was dropped:
sdks/python/src/opik/evaluation/engine/engine.py:653 (EvaluationEngine.score_test_cases)
regular_metrics, _ = metrics_evaluator.split_into_regular_and_task_span_metrics(
scoring_metrics
)
The second half of the split is thrown away, and only regular_metrics reaches _compute_test_result_for_test_case. So a user who adds a span-based scorer to an existing experiment gets a run that completes normally, a report that lists only the other metrics, no log line, no failed score, and no entry in aggregate_evaluation_scores(). The experiment looks fully evaluated.
score_test_cases has exactly one caller — evaluator.py:930, inside evaluate_experiment — so this is confined to that entry point. Every other evaluation entry point goes through run_and_score, which does handle span metrics.
This is the failure class the SDK already ruled on elsewhere:
tests/unit/evaluation/test_silently_skipped_scores.py — "OPIK-6925: an evaluation the user asked for must never vanish without a trace."
sdks/python/src/opik/evaluation/types.py (ErrorTolerance.METRIC_ERRORS) — "Of the failures that happen before score is entered, a missing required score argument and an item-level evaluator that cannot be built abort the run".
score_test_cases is the remaining place where a configured metric is dropped before argument validation is entered, so neither documented outcome happens. evaluate_experiment builds its engine with ErrorTolerance.METRIC_ERRORS ("This entrypoint does not expose the setting; it runs strict"), i.e. the strictest level, which makes the silence worse rather than better.
Reproduction steps and code snippets
At the public entry point (needs a running Opik instance):
import opik
from opik.evaluation.metrics.score_result import ScoreResult
def answer_is_long_enough(dataset_item, task_outputs):
return ScoreResult(
name="answer_is_long_enough", value=float(len(task_outputs["output"]) > 20)
)
def task_ran_under_the_token_budget(dataset_item, task_outputs, task_span):
# task_span carries the LLM call metadata collected while the task ran
return ScoreResult(
name="task_ran_under_the_token_budget",
value=float((task_span.usage or {}).get("total_tokens", 0) <= 4096),
)
opik.evaluate_experiment(
experiment_name="my-experiment",
scoring_metrics=[],
scoring_functions=[answer_is_long_enough, task_ran_under_the_token_budget],
)
# Completes without error. The experiment receives "answer_is_long_enough" only;
# "task_ran_under_the_token_budget" appears nowhere, and nothing says it could not run.
Without a server, the same thing at engine level (fixtures and import style copied from tests/unit/evaluation/):
from typing import Any
import pytest
from opik import exceptions
from opik.evaluation.metrics import base_metric, score_result
from opik.evaluation.types import ErrorTolerance
class RequiresTaskSpan(base_metric.BaseMetric):
def __init__(self) -> None:
super().__init__(name="requires_task_span", track=False)
def score(self, task_span: Any, **ignored: Any) -> score_result.ScoreResult:
return score_result.ScoreResult(name=self.name, value=1.0)
# `score_test_cases` with a metric that needs the span, both tolerances:
# METRIC_ERRORS -> expected ScoreMethodMissingArguments, nothing raised
# ALL_SCORING_ERRORS -> expected a failed ScoreResult, key absent from results
Error logs or stack trace
Running that expectation against main at 5cfff7b01:
E Failed: DID NOT RAISE <class 'opik.exceptions.ScoreMethodMissingArguments'> # ErrorTolerance.METRIC_ERRORS
E KeyError: 'requires_task_span' # ErrorTolerance.ALL_SCORING_ERRORS
The KeyError is the bug: the score result the user configured is not in the returned results at all, so there is nothing to inspect.
Proposed fix — needs a direction call
Two shapes, and I would rather you pick than have me guess:
- Minimal, no new semantics. Stop discarding the split result: let task-span metrics reach argument validation with nothing bound to
task_span. _compute_metric_scores already handles ScoreMethodMissingArguments exactly as documented — abort below ALL_SCORING_ERRORS, otherwise append _build_failed_score_result(...) with error_info, and keep it out of the persisted feedback scores. That is the same code path evaluate() uses for any other unsatisfiable argument, so the change stays inside score_test_cases (plus how it builds its evaluator) and the existing test file patterns cover it. One detail worth a call: a scoring_functions scorer is wrapped as ScorerWrapperMetricTaskSpan, whose score defaults task_span=None, so it would not trip argument validation and would instead fail inside the wrapped user function. I would make that shape report the same way rather than inherit a TypeError.
- Full support. Reconstruct the task span for each test case from the stored trace and score it properly. This is a better outcome for users but a bigger change: it needs a
TracePublic/SpanPublic → models.SpanModel route, a rule for which span of a stored trace is "the" task span (run_and_score currently uses the first span of the locally recorded tree), and one extra read per item.
A related question for option 1: evaluate_experiment hard-codes ErrorTolerance.METRIC_ERRORS and does not expose the parameter, so a user who wants the run to continue with a reported failure has no way to ask for it. Should this entry point take error_tolerance like evaluate() does? That is a public-API decision, so I am not folding it into a fix unless you want it.
Healthcheck results
Not applicable — reproduced at unit level against source, with no server involved.
Disclosure: this was found by an AI-assisted code read of score_test_cases and the OPIK-6925 tests, and confirmed by running the reproduction above locally against 2.2.70 and main; I have reviewed the cited code and the claimed behavior myself.
What component(s) are affected?
Opik version
opik2.2.70, and currentmainat5cfff7b013107d44a449bd2b085c1b16f8d92eea. Both carry the same line quoted below.Describe the problem
evaluate_experiment()re-scores an existing experiment. Its docstring liststask_spanas one of the inputs a scoring function may take ("task_span- the data collected during the LLM task execution [optional]"), andevaluate()does support such metrics (covered bytests/e2e/evaluation/test_evaluate_task_span.py). In the re-scoring path, however, every metric that requirestask_spanis discarded before it can be scored, and nothing reports that it was dropped:sdks/python/src/opik/evaluation/engine/engine.py:653(EvaluationEngine.score_test_cases)The second half of the split is thrown away, and only
regular_metricsreaches_compute_test_result_for_test_case. So a user who adds a span-based scorer to an existing experiment gets a run that completes normally, a report that lists only the other metrics, no log line, no failed score, and no entry inaggregate_evaluation_scores(). The experiment looks fully evaluated.score_test_caseshas exactly one caller —evaluator.py:930, insideevaluate_experiment— so this is confined to that entry point. Every other evaluation entry point goes throughrun_and_score, which does handle span metrics.This is the failure class the SDK already ruled on elsewhere:
tests/unit/evaluation/test_silently_skipped_scores.py— "OPIK-6925: an evaluation the user asked for must never vanish without a trace."sdks/python/src/opik/evaluation/types.py(ErrorTolerance.METRIC_ERRORS) — "Of the failures that happen beforescoreis entered, a missing required score argument and an item-level evaluator that cannot be built abort the run".score_test_casesis the remaining place where a configured metric is dropped before argument validation is entered, so neither documented outcome happens.evaluate_experimentbuilds its engine withErrorTolerance.METRIC_ERRORS("This entrypoint does not expose the setting; it runs strict"), i.e. the strictest level, which makes the silence worse rather than better.Reproduction steps and code snippets
At the public entry point (needs a running Opik instance):
Without a server, the same thing at engine level (fixtures and import style copied from
tests/unit/evaluation/):Error logs or stack trace
Running that expectation against
mainat5cfff7b01:The
KeyErroris the bug: the score result the user configured is not in the returned results at all, so there is nothing to inspect.Proposed fix — needs a direction call
Two shapes, and I would rather you pick than have me guess:
task_span._compute_metric_scoresalready handlesScoreMethodMissingArgumentsexactly as documented — abort belowALL_SCORING_ERRORS, otherwise append_build_failed_score_result(...)witherror_info, and keep it out of the persisted feedback scores. That is the same code pathevaluate()uses for any other unsatisfiable argument, so the change stays insidescore_test_cases(plus how it builds its evaluator) and the existing test file patterns cover it. One detail worth a call: ascoring_functionsscorer is wrapped asScorerWrapperMetricTaskSpan, whosescoredefaultstask_span=None, so it would not trip argument validation and would instead fail inside the wrapped user function. I would make that shape report the same way rather than inherit aTypeError.TracePublic/SpanPublic→models.SpanModelroute, a rule for which span of a stored trace is "the" task span (run_and_scorecurrently uses the first span of the locally recorded tree), and one extra read per item.A related question for option 1:
evaluate_experimenthard-codesErrorTolerance.METRIC_ERRORSand does not expose the parameter, so a user who wants the run to continue with a reported failure has no way to ask for it. Should this entry point takeerror_tolerancelikeevaluate()does? That is a public-API decision, so I am not folding it into a fix unless you want it.Healthcheck results
Not applicable — reproduced at unit level against source, with no server involved.
Disclosure: this was found by an AI-assisted code read of
score_test_casesand the OPIK-6925 tests, and confirmed by running the reproduction above locally against2.2.70andmain; I have reviewed the cited code and the claimed behavior myself.