Skip to content

feat(eval-routing): decouple judge/agent_judge backend+model from the agent's route - #137

Merged
akshaylive merged 9 commits into
mainfrom
akshaya/checker_context
Aug 27, 2026
Merged

feat(eval-routing): decouple judge/agent_judge backend+model from the agent's route#137
akshaylive merged 9 commits into
mainfrom
akshaya/checker_context

Conversation

@akshaylive

Copy link
Copy Markdown
Collaborator

Summary

  • Adds checker_context.api_route: {route, model} on TaskDefinition (4-layer merged like agent/simulation), letting a task/variant pick which backend and model the evaluation side (llm_judge/agent_judge/simulator) uses, independent of the agent under test — enabling cross-vendor grading (checking for self-preference bias) and cheaper judge models without editing every task YAML.
  • The override is baked into the resolved ApiRoute's own model field before any criterion runs, so criteria stay task-blind — llm_judge only ever reads CheckContext.route.model, never checker_context/TaskDefinition directly.
  • checker_context validates its own shape (unknown namespace, unknown key, or unrecognized backend name all raise) both at task-load time and after the experiment-layer merge, so a typo can't silently no-op an override.
  • Separately: BedrockRoute/LiteLLMRoute no longer carry bearer_token/auth_token fields. Those secrets now flow only through the shared coder_eval.config.settings singleton, read directly by each consumer (ClaudeCodeAgent._build_sdk_env, judge_bedrock.invoke_bedrock_judge_async) at the point of use — a route object flowing through CheckContext/environment_info/logging never carries a credential.

Full design discussion in #136 (comment).

Review-driven fixes included in this PR

A multi-axis code review surfaced several real issues, fixed here:

  • Bug: resolve_evaluation_route's reuse-agent-route branch shipped a bare, unqualified model alias straight to the Bedrock API when only checker_context.api_route.model was overridden (no backend override). Fixed via a new shared _bedrock_model_pair helper that also de-duplicates qualification logic previously copy-pasted across three call sites.
  • Bug: judge_bedrock.invoke_bedrock_judge_async's missing-bearer-token check was a bare assert, which handle_criterion_errors(_async) was silently catching and downgrading to a scored 0.0 instead of escalating to FinalStatus.ERROR. Now raises JudgeInfrastructureError.
  • Doc bug: the Checker Context guide's example paired llm_judge with route: litellm, which has no transport implemented yet — copy-pasting it would fail immediately. Fixed to route: bedrock, with the LiteLLM gap called out explicitly.
  • Orchestrator._eval_route_overrides now returns a NamedTuple instead of a bare 2-tuple (closes a silent positional-swap risk); the two duplicated route-resolution call sites in Orchestrator._setup are now one _resolve_routes() helper; environment_info now records eval_model so a judge-model override is visible in run artifacts.

Known follow-up (not fixed in this PR — flagging transparently)

  • The new checker_context/route-override mechanism has no dedicated test coverage yet (the diff's test-file changes are mechanical adaptations to the bearer_token/auth_token field removal, not new tests). Recommend adding coverage for resolve_evaluation_route's override paths and llm_judge's route.model fallback before relying on this in CI.
  • agent_judge does not currently honor checker_context.api_route.model (its sub-agent's model comes from a hardcoded default in _default_judge_agent_config) — documented in the guide as a known gap rather than fixed, since the fix touches files outside this diff's scope.
  • The LiteLLM judge transport itself is still unimplemented (tracked in llm_judge: support non-Anthropic judge routes (LiteLLM / OpenAI-compatible) #136) — route: litellm resolves cleanly but fails at grading time with a clear "not implemented yet" error.

Test plan

  • make format / make check (ruff) — clean
  • uv run pyright — 0 errors
  • make lint (custom architectural rules incl. CE030 doc-schema-parity) — clean
  • Full test suite: 4698 passed, 8 skipped (pre-existing, environment-gated, unrelated)
  • Manually verified the Bedrock-qualification fix by direct execution (eu.anthropic.claude-opus-4-6-v1 now correctly qualified, was previously bare)

🤖 Generated with Claude Code

… agent's own route

Adds `checker_context.api_route: {route, model}` on TaskDefinition (4-layer
merged like `agent`/`simulation`), letting a task/variant pick which backend
and model the evaluation side (llm_judge/agent_judge/simulator) uses,
independent of the agent under test — enabling cross-vendor grading and
cheaper judge models without editing every task YAML. The override is baked
into the resolved ApiRoute's own `model` field before any criterion runs, so
criteria stay task-blind (only ever reading CheckContext.route.model, never
checker_context/TaskDefinition directly). checker_context validates its shape
(unknown namespace/key/backend name raises) both at task-load time and after
the experiment-layer merge.

Separately, BedrockRoute/LiteLLMRoute no longer carry bearer_token/auth_token
fields — those secrets now flow only through the coder_eval.config.settings
singleton, read directly by each consumer (ClaudeCodeAgent._build_sdk_env,
judge_bedrock.invoke_bedrock_judge_async) at the point of use, so a route
object flowing through CheckContext/environment_info/logging never carries a
credential.

Includes fixes from a code review pass: Bedrock model_override is now
region-qualified when reusing the agent's own route (previously shipped a
bare alias to the Bedrock API); a missing bearer token now raises
JudgeInfrastructureError instead of an assert that handle_criterion_errors
was silently downgrading to a scored 0.0; eval_model is now recorded in
environment_info; the two orchestrator route-resolution call sites are
deduplicated into one helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread src/coder_eval/models/routing.py Fixed
@UiPath UiPath deleted a comment from github-actions Bot Aug 26, 2026
uipreliga

This comment was marked as outdated.

@bai-uipath bai-uipath left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Doesn't get us to luna — this is the selector, not the capability, and that gap is the whole reason #136 exists.

The judge can only be pointed at backends that reach Claude. The endpoint codex actually uses for luna isn't one of them, and the judge has no transport that could talk to it if it were — so nothing here moves the sonnet-4.6 → luna swap. Getting there needs a new backend on top of the transport work in #136. Fine as a prerequisite; worth saying so up front rather than merging this looking like it delivers the thing.

The simulator swap isn't covered either — its model comes from the simulation config, not the eval route.

Fix what you agree with on the rest:

  • The default judge model silently changes. With no checker_context anywhere, the judge stops using the criterion default and picks up BEDROCK_MODEL — which the nightly sets to sonnet-5, which 400s on temperature (verified against live Bedrock). That's all 361 llm_judge tasks in the skills suite once the pin moves. Fix: only populate the eval route's model when an override was actually supplied.
  • The unset-detection doesn't survive the docker driver — the task round-trip marks every field as set, so the model override is a silent no-op there while working on tempdir. Same fix retires it.
  • No tests on any of the new paths. Every existing judge test uses a model-less route, which is why the suite stayed green through the above.
  • The model half may be the wrong shape. The route half earns its keep; the model override is doing a job a judge: block on ExperimentDefaults would do more directly — set the criterion's model explicitly and the two bugs above stop existing. Worth considering landing the selector alone and bringing the model override back alongside the transport.

akshaylive and others added 2 commits August 26, 2026 10:59
Addresses a CodeQL finding on PR #137: mixing explicit returns per
case with an implicit fall-through return (None) reads as a possible
bug. Add a raising wildcard arm so every path returns explicitly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dge transport

Addresses the PR #137 review blockers and adds the litellm-library-backed
LiteLLMRoute judge transport:

- resolve_evaluation_route no longer lets the agent's own env-sourced model
  (e.g. BEDROCK_MODEL) leak into the eval route's `model` on the no-override
  reuse/pin paths — route.model now means "an explicit checker_context
  override was given", restoring DEFAULT_JUDGE_MODEL as the judge's floor.
- LLMJudgeCriterion.model is now `str | None = None` (was a materialized
  DEFAULT_JUDGE_MODEL default gated by model_fields_set, which doesn't
  survive the docker driver's model_dump/reload round trip). Precedence is
  now `criterion.model or route.model or DEFAULT_JUDGE_MODEL`, computed at
  check time.
- _build_sdk_env's DirectRoute arm now neutralizes inherited Bedrock creds
  (AWS_BEARER_TOKEN_BEDROCK/CLAUDE_CODE_USE_BEDROCK), matching the LiteLLM
  arm, so an explicit `route: direct` can't silently spend the operator's
  Bedrock token.
- Implement the `checker_context.api_route.route: litellm` judge transport
  via the `litellm` library (new `coder-eval[litellm]` extra), with a
  LiteLLMRoute.include_temperature flag to avoid a live round-trip on
  gateways that reject `temperature`.
- Added ~15 tests covering backend_override/model_override resolution,
  the judge-model floor regression, docker-serialization round-tripping,
  and DirectRoute env neutralization — routing.py coverage 69% -> 96.5%.
- Doc fixes: corrected stale claims about the judge-model fallback chain,
  the simulator sharing the agent's ApiRoute, and checker_context's
  placement/example in the guide and AB_EXPERIMENTS.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread tests/test_llm_judge_criterion.py Fixed
Comment thread tests/test_llm_judge_criterion.py Fixed
litellm.acompletion() takes dozens of provider-specific kwargs
(aws_access_key_id, vertex_project, api_version, ...) that LiteLLMRoute had
no way to express, and secrets couldn't be put in task YAML anyway. Extend
checker_context.api_route with two new keys, litellm-route only:

- `params`: arbitrary passthrough dict merged straight into the
  litellm.acompletion() call — no allowlist to maintain, litellm validates
  param names itself.
- `auth`: maps a kwarg name to the ENV VAR NAME (never the secret value) to
  resolve it from right before the call — so an arbitrary provider's auth
  shape (IAM keys, an Azure AD token, ...) is representable without a
  dedicated field per provider and without a secret ever landing in YAML.

The plain LITELLM_AUTH_TOKEN requirement is relaxed to "LITELLM_AUTH_TOKEN
OR a non-empty `auth` override", since some providers (e.g. Bedrock via
IAM) have no `api_key` concept at all. `validate_checker_context_shape`
rejects `params`/`auth` on any route other than `litellm` at task-load time,
and type-checks `auth`'s values are env-var-name strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread src/coder_eval/models/routing.py Dismissed
…arams

Per review: the litellm judge transport was silently reading from
coder_eval.config.settings (litellm_base_url/litellm_auth_token) even
though checker_context.api_route.route: litellm is meant to be fully
task-author-owned and independent of the agent's own LiteLLM backend.

- LiteLLMRoute no longer carries base_url at all -- the agent's own
  LiteLLM backend (_build_sdk_env, environment_info recording) now reads
  settings.litellm_base_url directly instead of storing it on the route,
  mirroring how the bearer token is already handled.
- The checker's litellm route is built ENTIRELY from
  checker_context.api_route.{params,env_params} -- no implicit fallback
  to the agent's LITELLM_BASE_URL/LITELLM_AUTH_TOKEN. `model` is now
  required when route: litellm (no default open-weight/gateway model).
- Renamed `auth` -> `env_params` for clarity (it's not auth-specific --
  api_base, aws_region_name, etc. can all be env-sourced too).
- Removed LiteLLMRoute.include_temperature and the BadRequestError
  retry-without-temperature logic: invoke_litellm_judge_async no longer
  takes a `temperature` kwarg at all -- a gateway-routed model may reject
  it outright (observed live against an Azure AI deployment) with no
  reliable way to detect that in advance, so the task author opts in via
  `params: {temperature: ...}` if their model accepts it.

Verified end-to-end against a real Azure AI gateway (checker_context:
{api_route: {route: litellm, model: azure/gpt-5.6-luna, env_params:
{api_base: LITELLM_BASE_URL, api_key: LITELLM_AUTH_TOKEN}}}) -- SUCCESS,
score 1.000.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread tests/test_orchestrator.py Fixed
@akshaylive
akshaylive force-pushed the akshaya/checker_context branch from c07a041 to f7583c0 Compare August 26, 2026 20:34
Two CI gates (Quality Gate, Windows Smoke Test) were failing pyright:
without `--extra litellm` in `uv sync`, `import litellm` in
judge_litellm.py resolved to the repo's own top-level `litellm/`
directory (the LiteLLM PROXY scripts, a namespace package with no
`acompletion`/`exceptions`/`types.utils`) instead of the real PyPI
package, since the real litellm distribution was never installed.

Also fixes 4 CodeQL findings from the latest analysis:
- routing.py: resolve_route()'s match (unlike its sibling
  _resolve_backend_route) had no `case _:`, so a 4th ApiBackend member
  would silently fall through and return None (mixed explicit/implicit
  returns) — added the same exhaustive-match guard.
- test_llm_judge_criterion.py: two redundant local `import json`
  (already imported at module top).
- test_orchestrator.py: `coder_eval.orchestrator` was imported both via
  `import ... as orch_mod` and `from ... import Orchestrator, ...` —
  patch `coder_eval.config.settings` directly instead (same singleton
  object orchestrator.py already imports).

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

@bai-uipath bai-uipath left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM overall. The transport landed, so this now does what the title says: a judge can be pointed at luna. The two grading-correctness bugs from the last pass are fixed and tested, and the judge-model floor holds. Fix what you agree with below.

  • route: litellm breaks the simulator and agent_judge. Both run as Claude Code sub-agents on the resolved eval route, but only the judge reads params/env_params. The SDK env builder still falls back to the agent's own LiteLLM settings, so a task with a simulation block or an agent_judge criterion either gets an empty base URL, or silently runs on the agent's proxy: the exact misrouting LiteLLMRoute's own docstring says must never happen. Either reject that combination at load, or thread the params into the sub-agent env. This is the blocking one for me, since the simulator swap is half the reason we wanted this.
  • checker_context is still an untyped bag. Two authored values, plus a hand-written allowlist validator called from two sites, plus a hand-rolled merge that records no config lineage. model's value type is never checked, so model: 5 loads clean and gets stringified into a model id. That was raised as blocking on the last review and the two new keys grew the validator instead of replacing it. Either land the typed model or say you are declining it, but it should not go quiet.
  • Nothing in the repo exercises the feature. No task, no experiment variant. The luna config that proves this works lives in a Slack message. One committed variant would make it real and give CI something to catch regressions with.
  • Pin api_version for the Azure path. The codex agent sets it explicitly for that deployment; the judge does not inherit it and litellm falls back to its own default, so judge and agent can hit different API versions of the same endpoint. Also: the docs say azure_ai/, the tested config says azure/. Different providers, different request shapes. Make the doc match what actually ran.

Minor: the LiteLLM secret assertion in the orchestrator tests is still vacuous, the token string it checks for is never set on the settings object it now patches.

akshaylive and others added 3 commits August 26, 2026 15:37
…ve test

Addresses bai-uipath's PR #137 follow-up review:

- Blocking: checker_context.api_route.route: litellm dispatches llm_judge
  through the litellm library in-process, but agent_judge and the simulator
  run as real Claude Code CLI subprocesses speaking Anthropic Messages only.
  Orchestrator._reject_litellm_eval_route_if_unsupported() now raises a clear
  error at route-resolution time when route: litellm is combined with an
  enabled agent_judge criterion or simulation.enabled, instead of silently
  misrouting onto the agent's own unrelated LiteLLM settings.
- checker_context is now typed (CheckerContext/ApiRouteContext pydantic
  models, extra="forbid") instead of a hand-validated open dict — deletes
  validate_checker_context_shape and its two call sites. A YAML `model: 5`
  is now rejected at load time instead of being str()-ified into a model id.
  _resolve_checker_context merges through the shared merge_layers engine
  (mirroring _resolve_simulation) and records config lineage.
- Added tests/test_litellm_judge_live.py: a live regression test hitting a
  real gateway via litellm.acompletion, reusing the existing CODEX_API_KEY/
  CODEX_BASE_URL/CODEX_MODEL CI secrets, wired into the codex-live-tests CI
  job — closes "nothing in the repo exercises the feature".
- Docs: azure_ai/ -> azure/ (matches what actually ran), api_version pinning
  guidance, documents the new agent_judge/simulator restriction.
- Fixed a vacuous test assertion: the orchestrator secret-leak test now
  actually sets litellm_auth_token before asserting it's absent from
  environment_info.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@akshaylive
akshaylive merged commit 727bb7b into main Aug 27, 2026
15 checks passed
@akshaylive
akshaylive deleted the akshaya/checker_context branch August 27, 2026 00:16
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.

4 participants