You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(eval-routing): decouple judge/agent_judge backend+model from the agent's route (#137)
* feat(eval-routing): decouple judge/agent_judge backend+model from the 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>
* fix(routing): make _resolve_backend_route's match exhaustive
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>
* fix(eval-routing): restore DEFAULT_JUDGE_MODEL floor + add litellm judge 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>
* feat(litellm-judge): support arbitrary litellm kwargs via params/auth
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>
* refactor(litellm-judge): drop settings coupling, rename auth to env_params
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>
* ci(fix): install litellm extra for pyright, address CodeQL findings
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>
* fix(checker-context): typed model, reject litellm+agent_judge/sim, live 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>
* ci: retrigger checks (previous push did not trigger CI)
* ci: retrigger checks (GH Actions appeared stalled repo-wide)
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
| `checker_context` | dict | Backend/model override for the evaluation side (judge, simulator) — see [Checker Context](TASK_DEFINITION_GUIDE.md#checker-context); **not** currently `-D`-reachable |
133
134
134
135
The `agent` dict is the lever for most A/B tests. Anything on `AgentConfig` is
checker_context: { ... } # Optional: backend/model for the evaluation side (judge, simulator)
66
68
```
67
69
68
70
### `dataset`
@@ -1295,6 +1297,40 @@ Observed label is `"yes"` when either signal is found, else `"no"`. Expected lab
1295
1297
1296
1298
**Typical pattern.** Label each dataset row with its true skill (`expected_skill`, `""` for negatives) and stack one `skill_triggered` criterion per skill against the same dataset — each gets its own confusion matrix from the same agent traces. This is the natural companion to a skill A/B experiment (skill plugin on vs. off); see the [A/B Experiment Guide](AB_EXPERIMENTS.md#recipe-ab-a-skill).
1297
1299
1300
+
## Checker Context
1301
+
1302
+
`checker_context` carries task-authored config for the success-checking side, namespaced by reserved key. Currently the only recognized namespace is **`api_route`**:
1303
+
1304
+
```yaml
1305
+
success_criteria:
1306
+
- type: llm_judge
1307
+
prompt: "Grade the fix for correctness."
1308
+
1309
+
checker_context:
1310
+
api_route:
1311
+
route: bedrock # which backend the whole eval side (llm_judge/agent_judge/simulator) uses
1312
+
model: claude-haiku-4-5 # model override for that route
1313
+
```
1314
+
1315
+
- `route` selects which backend the WHOLE evaluation side calls (`llm_judge`, `agent_judge`, and the simulator all share one resolved eval route per run — this isn't per-criterion), **decoupled from the agent's own route** (a Claude agent can be graded by a differently-backed judge, or vice versa). This is a backend *name* (`direct` / `bedrock` / `litellm`), not a route object. For `direct`/`bedrock` credentials are never read from the task, always from the matching environment variables (`ANTHROPIC_API_KEY` for `direct`, `AWS_BEARER_TOKEN_BEDROCK`/`AWS_REGION` for `bedrock`). An unconfigured or unknown backend name raises at dispatch rather than silently falling back. **`route: litellm` dispatches `llm_judge` through the `litellm` library** (the `coder-eval[litellm]` extra, `litellm.acompletion`) rather than assuming one wire protocol — a gateway-routed judge model (e.g. an Azure AI `/openai/v1` deployment) rarely speaks Anthropic Messages, so this lets `model` carry its own provider hint (e.g. `azure/gpt-5.6-luna`) and get that provider's actual request/response shape handled by the library. Unlike the other two backends, `route: litellm` has NO implicit env-var fallback — see `params`/`env_params` below, which is how it's configured. `model` is required for `route: litellm` (there is no default open-weight/gateway model).
1316
+
- `model` overrides the model that resolved route uses for **`llm_judge` only** — when the criterion itself leaves `model:` unset (precedence: an explicit per-criterion `model:` always wins; below that, `checker_context.api_route.model`; below that, the built-in `DEFAULT_JUDGE_MODEL`). This floor is deliberate and never the agent's own model — an unpinned judge must grade identically regardless of which model the agent under test is using, so `resolve_evaluation_route` never lets the agent's env-configured model (e.g. `BEDROCK_MODEL`) leak into `route.model` on its own; `route.model` is set only when this override was actually given. This works because every `ApiRoute` (`DirectRoute`/`BedrockRoute`/`LiteLLMRoute`) carries its own `model` field; the orchestrator bakes the override into the resolved route's `model` before any criterion runs, so `llm_judge` just reads `context.route.model` — it never reads `checker_context` directly. **`agent_judge` and the simulator do not honor this override** — `agent_judge`'s sub-agent model comes from the criterion's own `agent:` block (defaulted to a fixed judge model), and the simulator's model is pinned by `SimulationConfig.model` (see [Simulation](#simulation) below) — both independent of `checker_context.api_route.model` by design, for the same "measuring instrument stays fixed" reason.
1317
+
- `params`/`env_params` (**`route: litellm` only**) are how the call is actually configured — there is no fallback to the agent's own `LITELLM_BASE_URL`/`LITELLM_AUTH_TOKEN` env vars, since a gateway-routed judge model rarely reuses the agent's own LiteLLM proxy/credential. They also cover any of the dozens of other provider-specific kwargs `litellm.acompletion` accepts (`aws_access_key_id`, `vertex_project`, `api_version`, ...), which have no dedicated field on `LiteLLMRoute`:
1318
+
```yaml
1319
+
checker_context:
1320
+
api_route:
1321
+
route: litellm
1322
+
model: azure/gpt-5.6-luna
1323
+
params: # arbitrary literal passthrough kwargs to litellm.acompletion
1324
+
api_version: "2024-05-01"
1325
+
env_params: # param name -> ENV VAR NAME (never the secret itself)
1326
+
api_base: LITELLM_BASE_URL
1327
+
api_key: LITELLM_AUTH_TOKEN
1328
+
```
1329
+
`params` is merged straight into the `litellm.acompletion(**kwargs)` call — litellm validates the param names itself, so there's no allowlist to keep in sync here. `env_params` maps a kwarg name to the *name* of an environment variable; the value is resolved right before the call, so no secret is ever written into task/experiment YAML — this is how an arbitrary provider's config, including secrets (IAM keys, an Azure AD token, a service-account path, ...), is representable without a dedicated field per provider. `env_params` is resolved after `params`, so it always wins for the same key. Rejected at task-load time if given without `route: litellm`. For an Azure deployment, pin `api_version` via `params` to whatever API version the agent side is actually configured for (e.g. Codex's `CODEX_API_VERSION`) — the judge has no way to inherit it, and a mismatched version can hit a different shape of the same endpoint.
1330
+
**`route: litellm` is `llm_judge`-only** — `agent_judge` and the simulator run as real Claude Code CLI subprocesses that speak the Anthropic Messages protocol, so pointing them at an arbitrary litellm-fronted gateway (which may speak an entirely different wire protocol) isn't representable. The orchestrator rejects the combination at resolution time (a clear error, not a silent misroute) if the task has an enabled `agent_judge` criterion or `simulation.enabled: true` alongside `route: litellm` — use `route: bedrock`/`direct` for those instead.
1331
+
1332
+
`checker_context`merges shallow-per-namespace across `default_experiment.defaults.checker_context` → `experiment.defaults.checker_context` → `task.checker_context` → `variant.checker_context` (same 4-layer precedence as `agent`/`simulation`). So a judge-model A/B, or a judge-backend A/B, is a variant-level config change, not an edit to every task YAML.
1333
+
1298
1334
## Reference Solutions
1299
1335
1300
1336
A reference solution is always a **directory**, given relative to the task YAML's own directory:
@@ -1577,7 +1613,7 @@ simulation:
1577
1613
| `check_criteria` | `end_of_dialog` | `end_of_dialog`, `every_turn`, or `both`. |
1578
1614
| `model` | `anthropic.claude-sonnet-4-6` | Model that plays the simulated user. Auto-translated to the run's backend (Bedrock inference profile / bare Anthropic alias), the same way [`llm_judge`](#llm_judge)'s `model` is. |
1579
1615
1580
-
The simulator runs as a tools-disabled Claude Code agent sharing the coding agent's `ApiRoute`, so temperature and sampling are resolved at the route level (same `-b` flag as the coding agent) and are not configured on this block. The **model is not**: it is pinned by `model` above. Inheriting it from the route meant `BEDROCK_MODEL` decided who the simulated user was, so an A/B varying the subject model silently varied its interlocutor too. Hold `model` fixed across variants for the same reason you hold a judge model fixed — the simulator is part of the measuring instrument, not the thing being measured.
1616
+
The simulator runs as a tools-disabled Claude Code agent on the run's resolved *evaluation* `ApiRoute` (the coding agent's own route unless [`checker_context.api_route.route`](#checker-context) overrides it), so temperature and sampling are resolved at the route level (same `-b` flag as the coding agent by default) and are not configured on this block. The **model is not**: it is pinned by `model` above. Inheriting it from the route meant `BEDROCK_MODEL` decided who the simulated user was, so an A/B varying the subject model silently varied its interlocutor too. Hold `model` fixed across variants for the same reason you hold a judge model fixed — the simulator is part of the measuring instrument, not the thing being measured.
Copy file name to clipboardExpand all lines: plugins/coder-eval/reference/criteria.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -209,7 +209,7 @@ Optional:
209
209
|`include_tool_calls`| When true, include a summary of the latest agent turn's tool calls (via summarize_commands). No-op when turn_records is unavailable. |
210
210
|`include_dialog`| When true, include the full user<->agent conversation across all turns in the judge prompt. In simulation mode the user side is generated by an LLM simulator and may invent premises — the judge should treat any claim made only by the simulated user as possibly fabricated, and not penalize the agent for going along with it unless the task description contradicts it. |
211
211
|`max_dialog_chars`| Aggregate cap on dialog text rendered into the judge prompt. Prevents an N-turn simulation from blowing out the judge's context window. Per-message truncation uses max_file_chars; trailing turns are dropped when this aggregate budget is exceeded (a degraded note is recorded). |
212
-
|`model`| Judge model id (e.g. 'anthropic.claude-sonnet-4-6'). On a BedrockRoute / DirectRoute the value is auto-translated: trailing '-vN[:M]' suffixes and the 'anthropic.' prefix are stripped where the backend doesn't accept them; on Bedrock the cross-region inference-profile prefix is added based on AWS_REGION. |
212
+
|`model`| Judge model id (e.g. 'anthropic.claude-sonnet-4-6'). Leave unset to fall back to checker_context.api_route.model when set, else the built-in default ('anthropic.claude-sonnet-4-6') — the fallback is never the agent's own model, so an unpinned judge grades identically across agent-model A/Bs. On a BedrockRoute / DirectRoute the value is auto-translated: trailing '-vN[:M]' suffixes and the 'anthropic.' prefix are stripped where the backend doesn't accept them; on Bedrock the cross-region inference-profile prefix is added based on AWS_REGION. |
213
213
|`temperature`| Sampling temperature for the judge model. 0.0 keeps grading deterministic. |
214
214
|`max_tokens`| Output token cap. Defaults to 2000 — large enough for the verbose verdict (score + rationale + a handful of findings) without runaway. |
215
215
|`max_file_chars`| Per-file content truncation applied before building the prompt. |
0 commit comments