Repository navigation
Conversation
The portal grades exact, must_reject and criteria cases in a runner of its own. Eval runs now have evaluators for the same case types, so one runner can grade them (part of #94). - The contract checks (v2) score an exact case's text (exact-text), and count an empty answer as a refusal on a must_reject case, as the portal graded them. - judge:criteria scores criteria cases on expected.criteria, or on the reference answer when a case has none. Its model is picked once per run for the run's tenant, from another family than the models that wrote the answer, and its calls are charged to that tenant. The run's provenance names it as judge:criteria@<version>+<model>. No judge model, or a failed judge call, gives a failed score with the reason. - The family check compares a judge only with the models that wrote the answer. An embedder or reranker that only searched no longer counts, though the provenance still lists every served model. - Each eval run gets its own judge:criteria, so a model picked for one run is never reused by the next.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved evaluation defects prevent judge calls, allow model-selection failures to abort entire runs, and misclassify structured answers as refusals.
3 open issues
What changed in this PR
This PR moves portal case grading toward the shared eval runner for #94, building on #192’s criteria storage changes. It adds a run-local criteria judge whose model is recorded in its identity.
Changes:
- Extends contract checks for exact text and empty-answer refusals.
- Adds tenant-scoped criteria judges with model selection, failure scores and provenance.
- Separates answer-writing models from search-only models for judge-family checks.
| File | Description |
|---|---|
packages/eval/src/strategy.ts |
Adds per-run evaluator creation. |
packages/eval/src/runner.ts |
Uses answer writers for judge-family checks while preserving all served models. |
packages/eval/src/evaluators.ts |
Updates contract scoring for exact and must-reject cases. |
packages/eval/src/eval.test.ts |
Tests case grading, writer filtering, judge identity and run isolation. |
packages/eval/src/cases.ts |
Implements the criteria evaluator and failed-score handling. |
docs/api/work.md |
Documents evaluator choices and judge provenance. |
apps/api/src/eval-judges.ts |
Creates tenant-scoped judges with tenant-charged calls. |
apps/api/src/eval-judges.test.ts |
Tests judge selection and tenant context. |
apps/api/src/composition.ts |
Registers run-local judges with the eval strategy. |
Review effort: Balanced
- An empty text with structured fields is an answer, not a refusal, on a must_reject case. - When picking the criteria judge's model fails, the case gets a failed score with the reason instead of aborting the run. - The API's eval runs get the model catalogue's family resolver, so the criteria judge can be picked from another family than the writers.
…t/case-evaluators # Conflicts: # packages/eval/src/cases.ts
…t/case-evaluators # Conflicts: # packages/eval/src/eval.test.ts
…t/case-evaluators
- Only an application/json part is a structured answer, so an empty RAG answer, which still carries its sources and retrieval parts, is a refusal on a must_reject case. - An exact case without its text scores 0 on exact-text, as the portal graded it, instead of not applying. - A judge that names its version and model in provenance, judge:criteria@1+<model>, is read as its evaluator for the label queue and for calibration in gates (evaluatorOfJudge). - The eval capability takes avoidAsJudge, models the criteria judge must not share a family with, and the no-model reason names the models the judge was picked against. - An embedder is the only kind that never counts as a writer. - Tests now cover the criteria judge end to end through the platform eval capability, with the reference-answer fallback.
Only the sources and retrieval parts that ride beside an answer are left out of its fields, so the builder assistant's and optimiser's structured answers count again. The System Atlas is rebuilt.
|
Ready for review. Copilot and a claim-checking review are done (no blind review) and CI passes. Open: the criteria judge's calls count under the builder origin, so /usage leaves them out; for discovered Azure deployments a model's family is its name, so a judge can share a vendor with the writer (this was so before); the guard for a principal without a tenant stays, though the kernel refuses one first. |
|
Found while testing the stack in the portal:
Fix: fail |
…ng no criterion A service that answers with a JSON part, such as Klarspråk, gave the judge an empty answer and the criteria, and the judge passed it. The judge now reads the fields as JSON when there is no text, and an empty answer scores 0 without a model call.
|
Fixed in 9c6f6cc: |

Stacked on #192.
What and why
The portal grades exact, must_reject and criteria cases in a runner of its own. Eval runs now have evaluators for the same case types, graded as the portal graded them, so one runner can grade them. Part of #94.
contractv2) score an exact case's text (exact-text, 0 when the case has no text) and count an empty answer as a refusal on a must_reject case. Only anapplication/jsonpart is a structured answer, so an empty RAG answer, which still carries its sources and retrieval parts, is a refusal.judge:criteriascores criteria cases onexpected.criteria, or on the reference answer when a case has none. Its model is picked once per run for the run's tenant, from another family than the models that wrote the answer and thanavoidAsJudge, and its calls are charged to that tenant (apps/api/src/eval-judges.ts). A failed pick or judge call gives a failed score with the reason.judge:criteriareads a structured answer's fields as JSON when it has no text, and scores an empty answer 0 without asking its model (answerText). A service that answers with a JSON part, such as Klarspråk, was judged on an empty answer, and the judge passed it on the criteria alone.judge:criteria@<version>+<model>; the label queue and gate calibration read it as the evaluatorjudge:criteria(evaluatorOfJudge).judge:criteria, so a model picked for one run is never reused by the next. The API's eval runs get the model catalogue's family resolver.docs/api/work.md,docs/api/README.md(API changes).How it was tested
CI on this head: static, size and three test shards pass; shard 4 failed on the console test "previews a new version in English and asks for a reason", which this PR does not touch and which passes locally (the same test failed once on Run the portal's test runs as eval runs, with judges, usage and a lease #194 before). The top of the stack (Delete what genki sent to an agency's Langfuse when it purges the case #197) passes
npm run check: 470 files, 5103 tests.New tests: "checks the case types a person writes: exact text, which fails without its text, and a refusal that may be no answer at all, but never a structured answer", "checks a judge against the models that wrote the answer, never an embedder that only searched", "scores a criteria case on its criteria under its own identity, with its model in the version", "is not applicable to exact and must_reject cases, or to a case without criteria", "records a failed score when no judge model is available, picking one fails, or the judge call fails", "puts its identity with version and model in the provenance of the run", "gives each run its own evaluators, so a judge picked for one run is never reused by the next", "picks the criteria judge's model for the run's tenant, from another family than the writers and the service's own models, and charges it there", "gives a principal without a tenant no criteria judge", "reads the calibration of a judge that names its version and model from its evaluator", "queues the verdicts of a judge that names its version and model under its evaluator", "judges criteria cases with a model from another family, named in provenance, and falls back to the reference answer", "judges a fields-only answer on its fields, and scores an empty answer 0 without asking the model".
The last test runs
platform/evalthroughcreatePlatform, and fails when the composition's family resolver is removed. The review broke 33 rules on purpose and 7 stayed green; the ones it named, the composition'sforRunandmodelFamilyand the reference-answer fallback, are covered by that test, and the reranker exclusion it found dead is gone.npm run checkpassesChecklist
packages/providers-*docs/architecture/modulesare updated if a contract, boundary or module changed