Skip to content

Eval V2: Code-eval scorer helpers + custom-typed metrics (re-landing of #1568) - #1592

Open
chiang-daniel wants to merge 23 commits into
mainfrom
dchiang/eval-helpers-custom-scores
Open

Eval V2: Code-eval scorer helpers + custom-typed metrics (re-landing of #1568)#1592
chiang-daniel wants to merge 23 commits into
mainfrom
dchiang/eval-helpers-custom-scores

Conversation

@chiang-daniel

@chiang-daniel chiang-daniel commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Status, August 19: rebased onto sfierro/KIL-741 (the Evals V2 line) and retargeted there; it rides to main after that branch lands. Off the release critical path: the one user-facing bug this contained (get_tool_results never matching real traces) already shipped to the release line as the minimal #1708, which this PR's fuller version merges over cleanly. Since the original description below was written, review hardening added: custom-typed scores are excluded from run-config ranking on all three comparison pages (they were sorting configs by cost/latency), the agent-callable create_evaluator and draft-test endpoints reject custom-typed scores with a 422 (no UI can complete such an eval yet), comparison headers render "Custom Metric", and the helpers gained overflow/non-finite guards plus readable non-ASCII tool arguments. One product question deferred to review: the compare table still displays custom-score columns while sorting excludes them.

Merge note for the next sync with the base branch: eval_helpers.py conflicts with the minimal fix that shipped as #1708. Resolve to THIS branch's side (role == "tool" only) — it is the deliberate later decision and the merged test file enforces it. Also delete the stray test_get_tool_results_openai_role_tool from #1708 if the merge drops it into this branch's test file; this branch's three get_tool_results tests supersede it.


Extracts the eval scorer-helper work from the multi-turn integration branch so it can ride evals_v2 to main. It has no multi-turn dependencies and patches files that live on this branch, so it lands here rather than waiting on the larger integration.

Content: KilnEvalHelpers for code-eval scorers (count_messages, get_tool_result_content, get_assistant_emitted_text, get_markdown_links, usage/latency/tool-error totals), custom-typed eval output scores gated to code evals with validation, NaN/inf rejection at validation and in the code-eval adapter, a fix for get_tool_results never matching real traces, and linear-time markdown parsing on adversarial input.

Provenance: this is the remediated landing of Mike's #1568 (KIL-764) - his bug reports and feature asks re-landed as a clean 13-commit series, with his co-author credit on 9 commits, plus hardening from a follow-up review pass.

Verification: cherry-picks onto scosman/evals_v2 with zero conflicts; full checks green (7110 python tests passing, web format/lint/check/test/build all pass).

🤖 Generated with Claude Code

chiang-daniel and others added 13 commits July 17, 2026 05:08
TaskRun traces are OpenAI-dialect: tool results are role "tool" messages
(ChatCompletionToolMessageParamWrapper), but the helper only matched
role/type == "tool_result" -- a shape no Kiln code path produces -- so it
returned [] for every real trace. Match role "tool", keeping the
"tool_result" branches as a defensive fallback for non-OpenAI shapes.

Release note: scorers written against the buggy always-empty behavior
will now receive real tool results and may flip verdicts.

Adds a round-trip test (TaskRun -> EvalTaskInput -> helpers) over the
real data path, and updates spec-fidelity row 27-R22.

Co-authored-by: Mike Chatzidakis <mike@getkiln.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…adapter

NaN compares False against every range bound, so a NaN score passed
five_star/pass_fail/pass_fail_critical range checks. The real damage is
at rest: pydantic serializes NaN as null, so the saved EvalRun file fails
Dict[str, float] validation on the next read -- the run file becomes
unloadable, which is worse than a skewed mean.

Two layers: validate_scores_against_output_scores now requires finite
values (protects every save path), and the code-eval adapter fast-fails
non-finite scorer returns so the error surfaces with code-eval context
instead of a generic save failure.

Co-authored-by: Mike Chatzidakis <mike@getkiln.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_eval_configs_score_summary calls normalize_rating on every output
score, and normalize_rating raises on custom-typed scores (they have no
normalization). Once custom scores become creatable, one custom score
whose json_key collides with a human rating (e.g. a metric named
"Overall Rating") turns into an unhandled 500 for the whole endpoint.
Correlation against human ratings is undefined for unbounded metrics,
so skip them in the loop.

Landed ahead of the datamodel change that makes custom scores
creatable, so the endpoint is never exposed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pairs a tool result to its originating call by tool_call_id and returns
the text content, flattening list-of-blocks content. Guards the sandbox
error surface: falsy ids return "" instead of pairing with legacy
entries lacking the key (None == None), malformed blocks (text: None)
coerce instead of raising mid-scorer, and first-match-wins on duplicate
ids is documented.

Co-authored-by: Mike Chatzidakis <mike@getkiln.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Returns (link_text, target) pairs for inline markdown links; the
advertised use is link checks, so the extraction must not mangle
real-world targets. Handles one level of balanced parens in URLs
(wikipedia-style) and strips optional titles; images are excluded and
inline code spans ignored. The unsupported subset (reference-style
links, nested brackets, angle-bracket targets) is documented and the
behavior pinned with tests. Never raises.

Co-authored-by: Mike Chatzidakis <mike@getkiln.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Role counter for trace messages. The docstring is explicit that this
counts messages, not tool calls -- one assistant message may carry
several tool calls or none -- and points tool-call counting at
get_tool_calls/count_tool_calls, so sample scorers don't misuse it.

Co-authored-by: Mike Chatzidakis <mike@getkiln.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The assistant's own output surface for corruption checks: message
content (string or text blocks), refusal text, and reasoning
(include_reasoning=True default). Tool results are never included.
Tool-call ARGUMENTS are opt-in (include_tool_calls=False default):
they're JSON-serialized, so non-ASCII text can appear as \uXXXX escapes
and mislead corruption regexes -- documented. Tool NAMES are never
included; they're schema identifiers, not emitted text.

Co-authored-by: Mike Chatzidakis <mike@getkiln.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_usage_totals, get_total_latency_ms, and get_error_tool_results:
health-metric primitives over per-message trace fields (MessageUsage,
latency_ms, is_error/error_message -- all real persisted fields). The
trace is a sandboxed scorer's only access to usage data.

Caveats live in the docstrings: absent usage sums to 0.0
(indistinguishable from a genuine zero), latency sums across seeded
multi-session traces, and error results only include explicitly flagged
failures. Usage values are duck-typed dict-or-object: JSON transports
deliver dicts, the in-process pickle transport delivers MessageUsage
objects -- covered end-to-end by a real-sandbox test.

Co-authored-by: Mike Chatzidakis <mike@getkiln.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Allow TaskOutputRatingType.custom on EvalOutputScore: unbounded numeric
metrics (tokens, cost, latency, counts) validated as any finite number.
Deletes the validator that rejected custom outright.

Product constraint, enforced two ways: judges structurally can't emit
custom keys (build_score_schema skips them), so one custom score makes
the whole eval code-eval-only. EvalConfig rejects LLM-judge configs on
custom-score evals at creation; BaseEval re-checks at construction as
defense in depth, since load-from-file validation runs before the
parent link exists (hand-edited files).

Polish over the reference implementation: dead validator removed,
EvalOutputScore.type description updated + api_schema.d.ts regenerated,
rating_name() gets a 'custom' label.

Co-authored-by: Mike Chatzidakis <mike@getkiln.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both markdown regexes were quadratic (found in the phase's adversarial
review): a run of backticks made the code-span pattern backtrack one
character per start position (200k backticks = ~90s), and adjacent
whitespace consumers around a possibly-empty target did the same for
"[a](" + spaces. These run in the sandbox on model output, and the
global code-eval lock turns one stalled scorer into a stalled queue.

Code-span pattern now matches backtick runs in one pass; the link
pattern captures the parenthesized interior (one level of nesting) and
splits target from optional title in a second, unambiguous pass. All
previous semantics pinned by the existing tests are unchanged; adds
adjacent-link and unquoted-space cases plus timing regression tests
(pathological 100k-char inputs, generously bounded).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review finding on the custom-score feature: the guard only rejected LLM
judges, but the check-type adapters (exact_match, pattern_match, etc.)
fill every declared score key with 0.0/1.0 via build_binary_scores --
a custom "total cost" metric would silently record meaningless binary
values. The product constraint is that only code evals can produce
custom metrics, so EvalConfig and the BaseEval defense-in-depth check
now reject any non-code-eval config on a custom-score eval
(EvalConfig.is_llm_judge is replaced by is_code_eval).

Same review, same feature: count_human_evals now skips custom scores
like the correlation loop does -- humans can't rate a custom metric, so
counting them pinned every item at "partially rated" and the eval
progress UI could never pass the human-ratings step. The compare-judges
page stops rendering permanently-empty columns (and no-op sort keys)
for custom scores, and the code-eval snippet generator describes custom
scores as unbounded metrics instead of pass/fail.

Adds an end-to-end round-trip test (custom-score EvalRun through a real
parent chain on disk), a check-type rejection test, and rated-count
assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Score validation: an int too large for float (10**400) raised a raw
OverflowError from the float coercion / math.isfinite before the
finiteness guards could fire -- both layers now report the intended
finite-number error instead of throwing.

Helper hardening on malformed trace data: a present-but-null (or
non-dict) "function" in a tool call no longer raises from
get_tool_calls or get_assistant_emitted_text; _field returns None if a
pickled object's attribute access throws. get_tool_result_content's
block flattening now matches get_assistant_emitted_text: textless
blocks are dropped (no stray newlines) and legacy blocks carrying text
under "content" are read.

Docstring corrections: the pickle transport is multiprocessing into the
sandbox subprocess, not in-process (the B5 helper commit message
overstates this); get_tool_result_content pairs all get_tool_results
shapes, not only role "tool". Also replaces a stale model_construct
workaround in the score-summary test -- custom scores construct
normally now.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two implementations of the usage sum exist deliberately -- typed
save-time validation for TaskRun.usage vs never-raise duck-typing in
the sandbox helper. Cross-check them on the same trace and assert the
helper's keys cover every MessageUsage field, so a new usage field or a
semantic drift fails CI instead of quietly disagreeing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Custom output scores now accept finite numeric values and remain limited to code evals. API and UI correlation paths exclude custom metrics. Evaluation trace helpers now support more tool formats, content extraction, usage, latency, error filtering, and Markdown links.

Changes

Custom score support

Layer / File(s) Summary
Custom score contracts and validation
libs/core/kiln_ai/datamodel/eval.py, libs/core/kiln_ai/datamodel/test_eval_model.py
Eval models accept finite custom metrics, require code-eval configurations, and persist custom scores through eval runs.
Code-eval runtime validation
libs/core/kiln_ai/adapters/eval/v2_eval_code_eval.py, libs/core/kiln_ai/adapters/eval/base_eval.py, libs/core/kiln_ai/adapters/eval/test_*.py
Code-eval scoring rejects non-finite results and incompatible runtime configurations. Tests cover these checks and usage-object transport.
API and UI custom-score handling
app/desktop/studio_server/eval_api.py, app/desktop/studio_server/test_eval_api.py, app/web_ui/src/lib/..., app/web_ui/src/routes/...
API validation rejects unsupported custom-score configurations. Human-rating counts, correlations, sorting, and comparison views exclude custom metrics. Labels and generated examples describe custom metrics.

Evaluation trace helper expansion

Layer / File(s) Summary
Trace parsing and inspection APIs
libs/core/kiln_ai/adapters/eval/eval_helpers.py
Trace helpers support OpenAI and legacy tool results, malformed tool calls, assistant text, tool content, message counts, usage, latency, errors, and inline Markdown links.
Trace helper validation and fidelity coverage
libs/core/kiln_ai/adapters/eval/test_eval_helpers.py, specs/projects/evals_v2/spec_fidelity_review/unit_27-code-eval.md
Tests cover parsing, extraction, aggregation, malformed inputs, Markdown parsing, and current tool-result matching evidence.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🔵 Low · up to 562f8

The change adds evaluation helper aggregation and custom metric handling, but extreme latency values can still raise errors or produce invalid totals, making some evaluation results unreliable. This bounded risk is mergeable with explicit owner follow-up to harden latency aggregation and add regression coverage.

Sequence Diagram(s)

sequenceDiagram
  participant EvalDefinition
  participant EvalConfig
  participant CodeEvalAdapter
  participant StudioAPI
  EvalDefinition->>EvalConfig: validate custom output scores
  EvalConfig->>CodeEvalAdapter: allow code-eval configuration
  CodeEvalAdapter->>StudioAPI: return finite custom metric
  StudioAPI->>StudioAPI: exclude custom metric from human counts and correlation
Loading

Possibly related PRs

  • Kiln-AI/Kiln#1579: The PR extends the same custom-score validation, API, UI, and test paths.

Suggested reviewers: scosman, tawnymanticore

Poem

A rabbit checks metrics in rows,
Custom scores take code-eval roads.
Tool calls and links pass through with care,
Usage and latency gather there.
Tests keep every trace clear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the Eval V2 scorer-helper work and custom-typed metrics, which are the primary changes.
Description check ✅ Passed The description thoroughly explains the changes, verification, provenance, and known review point, but omits the template headings and explicit CLA confirmation.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dchiang/eval-helpers-custom-scores

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

📊 Coverage Report

Overall Coverage: 92%

Diff: origin/sfierro/KIL-741...HEAD

  • app/desktop/studio_server/eval_api.py (100%)
  • libs/core/kiln_ai/adapters/eval/base_eval.py (100%)
  • libs/core/kiln_ai/adapters/eval/eval_helpers.py (95.3%): Missing lines 219,222,324-325,434-435
  • libs/core/kiln_ai/adapters/eval/v2_eval_code_eval.py (100%)
  • libs/core/kiln_ai/datamodel/eval.py (90.9%): Missing lines 1025-1026

Summary

  • Total: 184 lines
  • Missing: 8 lines
  • Coverage: 95%

Line-by-line

View line-by-line diff coverage

libs/core/kiln_ai/adapters/eval/eval_helpers.py

Lines 215-226

  215                         try:
  216                             parts.append(
  217                                 json.dumps(args, sort_keys=True, ensure_ascii=False)
  218                             )
! 219                         except (TypeError, ValueError, RecursionError):
  220                             # Unserializable arguments are dropped rather than
  221                             # raised, so a scorer never dies on a bad trace.
! 222                             continue
  223         return "\n".join(p for p in parts if p)
  224 
  225     # -- Tool-call matching -------------------------------------------------

Lines 320-329

  320         if isinstance(obj, dict):
  321             return obj.get(name)
  322         try:
  323             return getattr(obj, name, None)
! 324         except Exception:
! 325             return None
  326 
  327     @staticmethod
  328     def get_usage_totals(trace: list[dict[str, Any]] | None) -> dict[str, float]:
  329         """Sum per-message ``usage`` across assistant messages.

Lines 430-436

  430                     # Unquoted whitespace in the target — not a supported link.
  431                     continue
  432                 links.append((link_text, target.group(1)))
  433             return links
! 434         except Exception:
! 435             return []

libs/core/kiln_ai/datamodel/eval.py

Lines 1021-1030

  1021         if self.is_code_eval():
  1022             return self
  1023         try:
  1024             parent = self.parent_eval()
! 1025         except ValueError:
! 1026             return self
  1027         if parent is not None and any(
  1028             score.type == TaskOutputRatingType.custom for score in parent.output_scores
  1029         ):
  1030             raise ValueError(


@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for custom-typed output scores, which represent unbounded numeric metrics (such as token counts, cost, and latency) that can only be produced by code evaluations. It updates backend validators, database models, and the frontend UI to handle custom metrics appropriately (e.g., skipping them in human rating correlation loops, preventing their use with LLM-judge or check-type configs, and displaying them correctly). Additionally, it expands the KilnEvalHelpers utility class with several new helper methods for parsing markdown links, counting messages, retrieving tool result contents, extracting assistant emitted text, and calculating usage/latency metrics, complete with comprehensive unit tests. I have no feedback to provide as there are no review comments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libs/core/kiln_ai/adapters/eval/base_eval.py`:
- Around line 283-291: Update build_score_schema, specifically its
allow_float_scores handling, to include each custom output-score key as a
numeric property instead of skipping it. Preserve additionalProperties: false
and the existing rejection for non-code evals, then add coverage exercising
run_task_and_eval() with custom scorer results to verify they pass schema
validation.

In `@libs/core/kiln_ai/adapters/eval/eval_helpers.py`:
- Around line 296-310: Guard the numeric aggregation paths near _field,
including the additional locations noted in the review, against malformed metric
values. Safely convert values to floats while handling conversion and overflow
failures, then ignore any non-finite results such as NaN or infinity so
aggregation continues with valid metrics and returns finite totals.
- Around line 200-207: Update the include_tool_calls handling in get_tool_calls
to serialize structured dictionary arguments instead of omitting them. Preserve
string arguments as-is, and append dictionary arguments using the existing JSON
serialization conventions while continuing to ignore unsupported argument types.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3fb51955-37d9-4f2d-aa1e-6a4661d0c5a4

📥 Commits

Reviewing files that changed from the base of the PR and between 3ea1f41 and 4b5bdb9.

📒 Files selected for processing (16)
  • app/desktop/studio_server/eval_api.py
  • app/desktop/studio_server/test_eval_api.py
  • app/web_ui/src/lib/api_schema.d.ts
  • app/web_ui/src/lib/components/eval_types/code_eval_helpers.test.ts
  • app/web_ui/src/lib/components/eval_types/code_eval_helpers.ts
  • app/web_ui/src/lib/utils/formatters.ts
  • app/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/[spec_id]/[eval_id]/eval_configs/+page.svelte
  • libs/core/kiln_ai/adapters/eval/base_eval.py
  • libs/core/kiln_ai/adapters/eval/eval_helpers.py
  • libs/core/kiln_ai/adapters/eval/test_base_eval.py
  • libs/core/kiln_ai/adapters/eval/test_eval_helpers.py
  • libs/core/kiln_ai/adapters/eval/test_v2_eval_code_eval.py
  • libs/core/kiln_ai/adapters/eval/v2_eval_code_eval.py
  • libs/core/kiln_ai/datamodel/eval.py
  • libs/core/kiln_ai/datamodel/test_eval_model.py
  • specs/projects/evals_v2/spec_fidelity_review/unit_27-code-eval.md

Comment thread libs/core/kiln_ai/adapters/eval/base_eval.py
Comment thread libs/core/kiln_ai/adapters/eval/eval_helpers.py
Comment thread libs/core/kiln_ai/adapters/eval/eval_helpers.py
@chiang-daniel chiang-daniel changed the title Code-eval scorer helpers + custom-typed metrics (re-landing of #1568) Eval V2: Code-eval scorer helpers + custom-typed metrics (re-landing of #1568) Jul 17, 2026
Rebases the code-eval scorer helpers + custom-typed metrics work onto the
active Eval V2 integration line so the PR merges cleanly.

Two files conflicted, both resolved by keeping the branch's newer code-eval
architecture and layering this PR's functionality onto it:

- v2_eval_code_eval.py: adopt the bridged-subprocess adapter
  (run_bridged_child / execute_scorer_bridged, NestedToolServer) and drop the
  now-unused asyncio import, while keeping the non-finite score rejection and
  overlarge-int guard in _validate_scores.
- test_v2_eval_code_eval.py: drop TestResolveProjectPath and
  TestExecutionSerialization, which covered helpers the new architecture
  removed, and port TestFiniteScoreValidation and TestUsageObjectTransport to
  the BridgeResult mocking pattern used by the rest of the file.

eval_helpers.py is untouched by the branch, so this PR's get_tool_results fix
(match role "tool" as well as "tool_result", so OpenAI-shaped traces are not
silently dropped) carries through intact, along with its test coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chiang-daniel
chiang-daniel changed the base branch from scosman/evals_v2 to sfierro/KIL-741 August 19, 2026 22:05
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

chiang-daniel and others added 4 commits August 19, 2026 15:15
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
get_assistant_emitted_text dropped tool-call arguments that arrived as
decoded dicts rather than JSON strings; serialize them so they reach the
text surface, with deterministic key order.

get_usage_totals raised OverflowError on ints beyond float range and let
NaN/infinity poison every later sum for a key. Both are now skipped like
absent usage, keeping the helper non-raising inside a user's scorer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spec detail and compare pages ranked run configs by the eval's last
output score. A trailing custom metric like cost or latency made that
rank the most expensive run config first, since higher wins.

Both pages carried a byte-identical copy of the sort, so it moves to a
shared helper that filters custom-typed scores, alongside the score
filter the eval configs page already had. Also gives custom scores a
real label in the comparison table header instead of the raw type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No UI offers a custom score type, and only a code-eval judge can produce
one, so an eval created through this endpoint with a custom score could
never be finished here. Reject it with a 422 pointing callers at the
library, mirroring how the name length limit is enforced.

Also pins the existing 400s for pairing a custom-typed eval with a
non-code-eval config, at create_eval_config, create_llm_judge_config,
test_v2_eval and test_v2_eval_draft. Behavior was already correct but
untested at the HTTP layer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Inline comments:
In `@libs/core/kiln_ai/adapters/eval/eval_helpers.py`:
- Line 11: Update get_usage_totals so each totals[key] aggregate is validated
after addition and cannot become non-finite, handling overflow consistently with
the existing numeric validation. Add a regression test covering two finite
values whose sum overflows, and verify the quality gates using the project’s
available tooling.

Apply the same fix in `@libs/core/kiln_ai/adapters/eval/eval_helpers.py` at line
362.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c8d10ca5-be66-4e6d-b4d6-0f4f8b573f32

📥 Commits

Reviewing files that changed from the base of the PR and between dd84a52 and 5517f73.

📒 Files selected for processing (10)
  • app/desktop/studio_server/eval_api.py
  • app/desktop/studio_server/test_eval_api.py
  • app/web_ui/src/lib/components/output_type_table_preview.svelte
  • app/web_ui/src/lib/utils/eval_types/run_config_sort.test.ts
  • app/web_ui/src/lib/utils/eval_types/run_config_sort.ts
  • app/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/[spec_id]/+page.svelte
  • app/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/[spec_id]/[eval_id]/compare_run_configs/+page.svelte
  • app/web_ui/src/routes/(app)/specs/[project_id]/[task_id]/[spec_id]/[eval_id]/eval_configs/+page.svelte
  • libs/core/kiln_ai/adapters/eval/eval_helpers.py
  • libs/core/kiln_ai/adapters/eval/test_eval_helpers.py

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.

Comment thread libs/core/kiln_ai/adapters/eval/eval_helpers.py
chiang-daniel and others added 2 commits August 19, 2026 16:09
create_evaluator's rejection message no longer points HTTP callers at the
Kiln library, and TestV2EvalDraftRequest gets the same guard so a caller
can't pass a draft test for scores the creation step would refuse.

create_eval_config now appends the underlying validator text to its 400,
so the custom-scores-require-code-eval rule reads as itself instead of a
generic "invalid properties" message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_assistant_emitted_text serializes dict tool-call arguments with
ensure_ascii=False so CJK and emoji stay readable for corruption regexes,
and get_usage_totals now skips values that would push the running total
past float range, not just individually non-finite ones.

The custom branch of the output type preview renders "Custom Metric" to
match rating_name, and its unreachable fallback is gone. Corrects the
run_config_sort docstring: score summaries do include custom means, which
is why they have to be filtered out of ranking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chiang-daniel

Copy link
Copy Markdown
Contributor Author

Post-merge hardening on top of the KIL-741 merge: fixed CI import order; eval helpers now include dict-form tool-call arguments (readable, non-escaped) and harden usage aggregation against overflow and non-finite values; run-config sorting is extracted into a shared helper so custom-typed scores (cost, latency, tokens) no longer drive ranking on the spec detail and compare pages; comparison headers render "Custom Metric"; create_evaluator and the draft-test endpoint both reject custom-typed output scores with a 422 since no UI surface can complete such an eval; create_eval_config now surfaces the underlying rule text instead of a generic message; added tests pinning all of the above.

One known trade-off left for review: the compare table displays custom-score columns while sorting excludes them.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Inline comments:
In `@app/web_ui/src/lib/components/output_type_table_preview.test.ts`:
- Around line 9-18: Make the labels definition exhaustive by changing it to a
Record<TaskOutputRatingType, string>, then derive the test cases from that
record instead of maintaining a tuple array. Preserve all existing rating labels
and ensure adding a new TaskOutputRatingType produces a type error until its
label is defined.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a7aaeb1d-159c-4a29-866f-1d70318ade85

📥 Commits

Reviewing files that changed from the base of the PR and between 5517f73 and 562f84c.

📒 Files selected for processing (7)
  • app/desktop/studio_server/eval_api.py
  • app/desktop/studio_server/test_eval_api.py
  • app/web_ui/src/lib/components/output_type_table_preview.svelte
  • app/web_ui/src/lib/components/output_type_table_preview.test.ts
  • app/web_ui/src/lib/utils/eval_types/run_config_sort.ts
  • libs/core/kiln_ai/adapters/eval/eval_helpers.py
  • libs/core/kiln_ai/adapters/eval/test_eval_helpers.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/web_ui/src/lib/utils/eval_types/run_config_sort.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.

Comment thread app/web_ui/src/lib/components/output_type_table_preview.test.ts Outdated
chiang-daniel and others added 3 commits August 19, 2026 16:38
Nothing produces a role "tool_result" trace entry; tool results are
role "tool" in OpenAI format. The type == "tool_result" defensive
match stays. Test now pins that the role shape does not match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four of them were passing on weaker evidence than they advertised. The run
config sort test never had two correlatable scores that disagreed, so "last"
was indistinguishable from "first"; the code-eval usage test clamped its total
to 5.0, so any total at or above 5 passed. Both now use inputs where a wrong
answer is a different answer, and the table preview's label list is typed as a
Record over the rating type so a new member fails to compile rather than
silently going untested.

The rest is redundancy: the two one-off custom-score rejection tests are cases
of the parametrize next to them, the eval it built is now a fixture, the
non-finite matrix only needs one unbounded and one ranged score type since the
finite check runs before any range logic, and the absent-usage test was already
covered by the empty-trace and object-usage tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cter role match

The base picked up the minimal role-"tool" fix; this branch carries the
same fix plus the deliberate removal of the never-produced "tool_result"
role. Kept this side and dropped the superseded minimal test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from sfierro/KIL-741 to main August 20, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant