Conversation
A scorer's return contract was "a bool or a finite number in 0-1", with True/False coerced to 1.0/0.0. Two problems. Coercion put two score types on the wire. A threshold then meant different things depending on which type a scorer happened to return, even though LaunchDarkly rules on score-vs-threshold identically for both -- so the comparison a caller reasoned about and the one ingest performed could differ. Worse, it hid bugs. Every non-empty value is truthy, so a scorer that returned "high", or a stray object, scored 1.0 and passed. The failure mode was a green run, which is the one failure mode an evaluation harness must not have. The contract is now a finite number in 0-1 and nothing else. A scorer answering a yes/no question returns 1.0 or 0.0 itself. A bool, a non-finite number, or one out of range is an invalid_score result -- per-criterion ERROR, never a raise, since the row's generation has already been paid for. numeric_score already excluded bool, so the fix is deleting the coercion branch rather than adding a check. ScorerFn drops bool from its signature, so a caller annotating their scorer sees this at type-check time rather than at ingest. BREAKING: a scorer returning a bool now produces an invalid_score result instead of 1.0/0.0. Callers return the number directly. Specced in ai-sdks-monorepo TESTING.md §8.8.4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #63, now merged. Brings the scorer return contract in line with
ai-sdks-monorepoTESTING.md §8.8.4, which was tightened after #63 was written.Problem
A scorer's contract was "a bool or a finite number in
0–1", withTrue/Falsecoerced to1.0/0.0. Two problems with the coercion:It put two score types on the wire. A
thresholdthen meant different things depending on which type a scorer happened to return, even though LaunchDarkly rules on score-vs-threshold identically for both — so the comparison a caller reasoned about and the one ingest performed could differ.It hid bugs, and the failure mode was a green run. Every non-empty value is truthy, so a scorer that returned
"high", or a stray object, scored1.0and passed:A false pass is the one failure mode an evaluation harness must not have — the point of the gate is that a broken check fails loudly.
Change
The contract is now a finite number in
0–1, and nothing else. A scorer answering a yes/no question returns1.0or0.0itself:A
bool, a non-finite number, or one out of range is aninvalid_scoreresult — a per-criterionERRORevent, never a raise, since the row's generation has already been paid for.numeric_score()already excludedbool(Python'sboolis anintsubclass, which is exactly the trap), so the fix deletes the coercion branch rather than adding a check — the two error paths collapse into one whose message names the offending value.ScorerFndropsboolfrom its signature, so a caller who annotates their scorer sees this at type-check time rather than at ingest.A scorer returning a bool now produces an
invalid_scoreresult instead of1.0/0.0. Callers return the number directly. This is deliberate per §8.8.4 ("there is no boolean shortcut and nothing is coerced on the caller's behalf") — but it is a behavior change and worth a release note.Every scorer in the examples repo is converted to match in launchdarkly-labs/ai-sdk-evaluations-example#4.
Validation
uv run pytest -q— 1264 passed, 11 skippeduv run mypy .../evaluations— clean;ruff check/ruff format --check— cleanTrue,False,NaN,Infinity,3,-1,"high",Noneall produceinvalid_scorewith the value named and noscorefield, and the run still completes, flushes, and polls0,1,0.0,1.0,0.25stay valid, ints included — onlyboolis excluded🤖 Generated with Claude Code