feat(benchmarks): custom_eval — declarative user evals as run-config data - #2
feat(benchmarks): custom_eval — declarative user evals as run-config data#2LukasParke wants to merge 1 commit into
Conversation
…data
Ports openrouter-web#31078 (all review rounds) onto the standalone layout:
- EvalSpec v1: dataset (inline cases or HF repo id, shape-validated
owner/name) + prompt template + deterministic scorer (exact/contains/
regex/numeric), fully declarative — user evals are rows, not code.
- Prompt templates substitute EVERY {input} occurrence (replaceAll,
function form so literal $ in inputs survives).
- HF revision pinning: an optional pinned commit SHA is verified against
the dataset's current default-branch revision before streaming; a
mismatch fails closed with the observed SHA (the /rows endpoint only
serves the default branch). Verification is memoized per layer.
- Config-bound dataset sizing: custom_eval's config-free makeDatasetLayer
FAILS loudly instead of answering with a placeholder (an earlier
1-case stand-in truncated orchestrated runs to their first item);
Benchmark.makeDatasetLayerForConfig + datasetSizeById(benchmarkId,
config) size the real dataset.
There was a problem hiding this comment.
"## Perry's Review\n\nVerdict: 💬 Comments / questions\n\n> Note: The maintainer app is not installed on OpenRouterTeam, so I can't post an APPROVE. The review is clean — please approve manually once CI is green.\n\nRisk: 🟢 Low\n\n
PR #2 — custom_eval: declarative user evals as run-config data
\n\nClean implementation of the declarative custom-eval system. The EvalSpec schema is well-structured with proper Zod validation, the scorer dispatch is comprehensive (exact, contains, regex, choice, numeric), and the HF dataset revision pinning is a thoughtful comparability guarantee.\n\nKey observations:\n\n- TherenderPrompt function's use of replaceAll with a function form correctly preserves literal $ sequences in user input — a subtle but important correctness point.\n- The extractLastNumber function handles commas, signs, and decimals correctly.\n- The regex scorer validates patterns at parse time (not scoring time), preventing runtime crashes from malformed patterns.\n- The makeDatasetLayerForConfig / makeDatasetLayer split is the right design: config-bound datasets fail closed when probed without a config, preventing the silent single-case truncation the comment describes.\n- The HF revision verification (fail-closed on mismatch) is well-tested with both matching and divergent SHAs.\n\nOne inline question below.\n| ...state, | ||
| messages: state.messages.map((message, index) => | ||
| index === state.messages.length - 1 && | ||
| typeof message.content === "string" |
There was a problem hiding this comment.
The template application guards on typeof message.content === "string" — if message.content is an array of content parts (multimodal input), the template is silently skipped for that message. Is this intentional for the rung-1 surface, or should non-string content raise an error so the user knows their prompt template isn't being applied?
▶ Prompt for agents: If intentional, consider adding a brief comment noting that multimodal content is not template-rendered in rung-1, so future readers don't treat the silent skip as a bug. If not intentional, throw on non-string content when a promptTemplate is configured.
Stacked on #1. Ports openrouter-web#31078 (all review rounds).
What
src/benchmarks/custom-eval/spec.ts): a fully declarative eval — dataset (inline cases or an HF repo id, shape-validated toowner/nameso path traversal can't reach the HF API), prompt template, system prompt, and a deterministic scorer (exact/contains/regex/numeric, with a schema-time refine rejecting pathological regex constructs).custom_evalbenchmark: resolves entirely from the run config — user evals are rows, not registry code. Prompt templates substitute every{input}occurrence (replaceAll, function form so literal$in inputs survives).src/datasets/huggingface.ts): optional pinned commit SHA verified against the dataset's current default-branch revision before streaming; mismatch fails closed with the observed SHA (the Dataset Viewer/rowsendpoint only serves the default branch, so pinning is enforced by verification). Memoized — one dataset-info request per run.custom_eval's config-freemakeDatasetLayerFAILS loudly instead of answering with a placeholder (an earlier 1-case stand-in silently truncated orchestrated runs to their first item — the worst finding of the review). NewBenchmark.makeDatasetLayerForConfig+datasetSizeById(benchmarkId, config?)size the real dataset; sizing a config-bound benchmark without a config is a loudLeft.Testing
14 custom-eval tests (spec validation, renderPrompt, scorers, dataset layers), revision-pinning tests on the HF layer, registry coverage; full suite green.