evals: persist judge model usage on the legacy G-Eval path - #1629
tawnymanticore wants to merge 2 commits into
Conversation
Judging spend is currently unmeasurable: the judge call's usage is assembled by the adapter (aggregated across every LLM call the judgment makes, including the two-call COT heuristic) and then discarded with the un-saved judge TaskRun. - EvalRun gains eval_usage: Usage | None — the judge model's usage, distinct from task_run_usage (the evaluated run's usage). Default None keeps every existing record loading unchanged. - GEval.run_eval and the V2 LlmJudgeEval keep the judge TaskRun's usage; BaseEval.run_eval/run_task_and_eval return it alongside the scores, and V2EvalResult carries it as eval_usage. - eval_runner threads it onto every EvalRun construction that ran an evaluator (legacy path + the V2 EvalInput / task_run_eval / eval_config_eval paths). Non-LLM evals persist None. - api_schema.d.ts regenerated for the new field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review. WalkthroughThe evaluation pipeline now returns optional model usage metadata from evaluators, propagates it through task execution, and stores it on legacy ChangesEvaluation usage tracking
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to The change is localized to evaluation-usage persistence with no supplied correctness or deployment failures; it is mergeable with explicit owner awareness that the requested Python validation, including type checking, should be confirmed. Sequence Diagram(s)sequenceDiagram
participant GEval
participant JudgeAdapter
participant BaseEval
participant EvalRunner
participant EvalRun
GEval->>JudgeAdapter: invoke judge evaluation
JudgeAdapter-->>GEval: return TaskRun with usage
GEval-->>BaseEval: return scores, outputs, and usage
BaseEval-->>EvalRunner: return task result and eval usage
EvalRunner->>EvalRun: persist eval_usage
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📊 Coverage ReportOverall Coverage: 92% Diff: origin/scosman/evals_v2...HEAD
Summary
Line-by-lineView line-by-line diff coveragelibs/core/kiln_ai/adapters/eval/base_eval.pyLines 481-486 481 if result.skipped_reason is not None:
482 raise ValueError(
483 f"V2 eval was skipped ({result.skipped_reason}): {result.skipped_detail}"
484 )
! 485 return result.scores, result.intermediate_outputs, result.usagelibs/core/kiln_ai/adapters/eval/g_eval.pyLines 326-339 326
327 # The judge TaskRun is never persisted (allow_saving=False), but its usage covers
328 # every LLM call the judgment made (the COT heuristic can make two), so we keep it.
329 # invoke_returning_run_output() also runs validations for us over _run().
! 330 judge_run, run_output = await adapter.invoke_returning_run_output(
331 run_description
332 )
333
334 if self.eval_config.config_type == EvalConfigType.llm_as_judge:
! 335 return (
336 self.build_llm_as_judge_score(run_output),
337 run_output.intermediate_outputs,
338 judge_run.usage,
339 )Lines 337-345 337 run_output.intermediate_outputs,
338 judge_run.usage,
339 )
340 else:
! 341 return (
342 self.build_g_eval_score(run_output),
343 run_output.intermediate_outputs,
344 judge_run.usage,
345 )
|
The base now records judge usage for V2 evals on its own: `EvalRun.eval_usage` and `V2EvalResult.usage` are in the datamodel, `LlmJudgeEval.evaluate` captures the judge TaskRun's usage, and `_persist_score` / `_persist_judgment` write it on every V2 record. All of that is dropped here in favour of the base's version — including the duplicate datamodel round-trip test, which the base already has as test_eval_usage_defaults_to_none_and_round_trips. What is left is the one lane the base does not cover: `_run_legacy_job`, which still discards the judge run for g_eval / llm_as_judge configs. That is: - GEval.run_eval keeps the judge TaskRun instead of dropping it on the floor, and returns its usage. The value covers the whole judgment — the adapter sums usage across every call, so the two-call COT heuristic is aggregated. - BaseEval.run_eval / run_task_and_eval carry it through as a third/fourth tuple element; BaseV2EvalBridge forwards the base's V2EvalResult.usage. - _run_legacy_job sets eval_usage on the EvalRun it saves. Non-LLM evals persist None — honestly unset rather than zero. libs/core/kiln_ai/adapters/eval/ + test_eval_model.py + test_eval_api.py: 1090 passed. Full libs/ + app/desktop run: 7507 passed, with 5 failures and 5 errors that all reproduce on the base (vector-store, chunker, model-cache benchmark, document-api, and tkinter-less desktop imports).
|
Closing — legacy evals are being sunset, and the legacy G-Eval path was all this branch had left. For the record of what was verified before closing: everything else this PR originally added has independently landed on The only thing that goes with it is If legacy evals turn out to have a longer tail than expected, the change is ~15 lines of production code: keep the judge TaskRun in Generated by Claude Code |
What the base already does
Everything this PR originally added for V2 evals is on
scosman/evals_v2today, and is dropped here in favour of the base's version:EvalRun.eval_usageandV2EvalResult.usagein the datamodel.LlmJudgeEval.evaluatekeeping the judge TaskRun's usage._persist_score/_persist_judgmentwriting it on every V2 record.test_eval_usage_defaults_to_none_and_round_trips), so this branch's duplicate is gone.What is left
EvalRunner.run_jobdispatches oneval_config.config_type:v2goes to_run_v2_job, andg_eval/llm_as_judgego to_run_legacy_job. The legacy path still throws the judge run away, so nog_eval-configured eval on disk records what its own scoring cost.GEval.run_evalkeeps the judge TaskRun instead of dropping it (_, run_output = ...) and returns its usage. The value covers the full judgment — the adapter sums usage across every model call in the invocation, so the two-call COT heuristic is aggregated, not just the last call.BaseEval.run_eval/run_task_and_evalcarry it through as an extra tuple element;BaseV2EvalBridgeforwards the base'sV2EvalResult.usage._run_legacy_jobsetseval_usageon the EvalRun it saves.Non-LLM evals persist
None— honestly unset rather than zero.Why not just close it
The legacy path is live, not sunset:
legacy_eval_adapter_from_typestill returnsGEvalfor bothg_evalandllm_as_judge, and any eval config already on disk with those types still runs through it. Until that dispatch arm is deleted, this is the last hole in per-lane usage accounting. If legacy evals are being removed outright, closing this is a one-click alternative — nothing else in the branch survives on its own.Back-compat
Unchanged from the original:
eval_usageis an optional field defaulting toNone, already on the base. The only behaviour change is that legacy-judge records written after this carry a value where they previously carriedNone.Testing
libs/core/kiln_ai/adapters/eval/+test_eval_model.py+test_eval_api.py: 1090 passed.test_run_job_persists_judge_eval_usage— a judged legacy EvalRun carries the judge's usage, throughrun_joband a disk round-trip.test_run_job_full_trace_serializes_per_message_usage.libs/+app/desktoprun: 7507 passed. The 5 failures and 5 errors (vector-store, chunker, model-cache benchmark, document-api, and tkinter-less desktop imports) all reproduce onorigin/scosman/evals_v2with this branch's changes absent.ruff format --checkclean on every touched file.