Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/AB_EXPERIMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ From `ExperimentVariant` (`coder_eval/models/experiment.py`):
| `initial_prompt_file` | str | Prompt replacement loaded from a file |
| `run_limits` | block | Per-key cap overrides (`max_turns`, `task_timeout`, token/USD budgets) |
| `driver` | `tempdir`/`docker` | Sandbox driver — enables tempdir-vs-docker arms |
| `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 |
| `checker_context` | dict | Backend/model override for the judge side (llm_judge, agent_judge) — has no bearing on the simulator; see [Checker Context](TASK_DEFINITION_GUIDE.md#checker-context); **not** currently `-D`-reachable |

The `agent` dict is the lever for most A/B tests. Anything on `AgentConfig` is
fair game: `model`, `permission_mode`, `allowed_tools`, `disallowed_tools`,
Expand Down
8 changes: 5 additions & 3 deletions docs/DIALOG_MODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@ The mechanics:
- The simulator is a **tools-disabled Claude Code agent** with `allowed_tools: []`, an explicit
deny-list, and no plugins or settings sources. It is pure text-in / text-out, and it **cannot see
the sandbox** — no files, no terminal, no agent reasoning. Only what the agent writes in the chat.
- The simulator runs on the run's resolved *evaluation* `ApiRoute` — the coding agent's own route
(`--backend direct` / `--backend bedrock`) unless `checker_context.api_route.route` overrides it
(see [Checker Context](TASK_DEFINITION_GUIDE.md#checker-context)). The **model** is separately
- The simulator resolves its own `ApiRoute` independently of `checker_context.api_route` (that
override is judge-only — see [Checker Context](TASK_DEFINITION_GUIDE.md#checker-context)) — same
resolution as the coding agent's own route (`--backend direct` / `--backend bedrock`), including
the pin to a constant Claude backend when the agent itself runs on an open-weight LiteLLM route.
The **model** is separately
pinned by `simulation.model` (see [Simulation](TASK_DEFINITION_GUIDE.md#simulation)), not inherited
from the route — so an A/B varying the subject model doesn't silently vary the simulated user too.

Expand Down
10 changes: 5 additions & 5 deletions docs/TASK_DEFINITION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ reference: { ... } # Optional reference solution (a directory
pre_run: [ ... ] # Optional pre-run commands (before agent starts)
post_run: [ ... ] # Optional post-run commands
dataset: { ... } # Optional dataset fan-out (one task -> N row-tasks)
checker_context: { ... } # Optional: backend/model for the evaluation side (judge, simulator)
checker_context: { ... } # Optional: backend/model for the judge side (llm_judge/agent_judge)
```

### `dataset`
Expand Down Expand Up @@ -1308,11 +1308,11 @@ success_criteria:

checker_context:
api_route:
route: bedrock # which backend the whole eval side (llm_judge/agent_judge/simulator) uses
route: bedrock # which backend the eval side uses
model: claude-haiku-4-5 # model override for that route
```

- `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).
- `route` selects which backend `llm_judge` and `agent_judge` call — they share one resolved eval route per run for `direct`/`bedrock`/`litellm` (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). The simulator is NOT part of this sharing: it always resolves its own route the same way the agent under test does (subprocess-safe by construction), independent of `checker_context.api_route` entirely — see the `route: litellm` note below. 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` ONLY** 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. `agent_judge` spawns a real Claude Code CLI subprocess that only speaks Anthropic Messages — combining `route: litellm` with an enabled `agent_judge` criterion is rejected at resolution time (see below); the simulator is unaffected since it never reads this override. 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).
- `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.
- `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`:
```yaml
Expand All @@ -1327,7 +1327,7 @@ checker_context:
api_key: LITELLM_AUTH_TOKEN
```
`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.
**`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.
**`route: litellm` has no bearing on the simulator** — the simulator is a real Claude Code CLI subprocess that speaks the Anthropic Messages protocol, so pointing it at an arbitrary litellm-fronted gateway (which may speak an entirely different wire protocol) isn't representable. Rather than rejecting a task that combines `route: litellm` with `simulation.enabled: true`, the simulator always resolves its own route independently of `checker_context.api_route` — the same way the agent under test resolves its route — so `llm_judge` grades through the gateway named here while the simulator behaves exactly as if this override weren't set. A task can freely combine `route: litellm` with `simulation.enabled: true`; there is nothing to disable or remove. **`agent_judge` is NOT decoupled the same way** — it still shares the `llm_judge` eval route, and `agent_judge`'s Claude Code CLI subprocess has no way to honor a `LiteLLMRoute` safely, so the orchestrator rejects `route: litellm` combined with an enabled `agent_judge` criterion at resolution time (a clear error, not a silent misroute) — use `route: bedrock`/`direct` instead if a task needs both.

`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.

Expand Down Expand Up @@ -1613,7 +1613,7 @@ simulation:
| `check_criteria` | `end_of_dialog` | `end_of_dialog`, `every_turn`, or `both`. |
| `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. |

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.
The simulator runs as a tools-disabled Claude Code agent on its own resolved `ApiRoute` (the same resolution as the coding agent's own route [`checker_context.api_route.route`](#checker-context) has no bearing on 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.

**Semantics:**

Expand Down
10 changes: 7 additions & 3 deletions src/coder_eval/models/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ class ApiRouteContext(BaseModel):

route: ApiBackend | None = Field(
default=None,
description="Backend the WHOLE evaluation side (llm_judge/agent_judge/simulator) calls.",
description=(
"Backend the judge side (llm_judge/agent_judge) calls. Has no bearing on the simulator, "
"which always resolves its own route independently."
),
)
model: str | None = Field(default=None, description="Model override for the resolved route.")
params: dict[str, Any] | None = Field(
Expand Down Expand Up @@ -491,8 +494,9 @@ class TaskDefinition(BaseModel): # noqa: CE009 -- soft-launch: see _warn_on_unk
description=(
"Task-authored config for the success-checking side. Currently carries only `api_route` "
"(see ApiRouteContext), e.g. `{api_route: {route: litellm, model: gpt-5}}`: `route` selects "
"the backend the WHOLE evaluation side (llm_judge, agent_judge, the simulator) calls, "
"decoupled from the agent's own route; `model` overrides the model that route uses. Both "
"the backend the judge side (llm_judge, agent_judge) calls, decoupled from the agent's own "
"route; has no bearing on the simulator, which always resolves its own route independently. "
"`model` overrides the model that route uses. Both "
"are consumed by the orchestrator (`resolve_evaluation_route`) BEFORE `CheckContext` is "
"built and baked into the resolved route's own `model` field — no criterion ever reads "
"`checker_context` directly, only `CheckContext.route.model`. Credentials are always "
Expand Down
Loading
Loading