pr_diff verifier: judge via any OpenAI-compatible endpoint - #135
Conversation
The judge was hardwired to Anthropic's Messages API and ANTHROPIC_API_KEY, so a self-hosted model could never score pr_diff tasks and half the reward weight was unavailable without an Anthropic key. R2E_JUDGE_ENDPOINT (plus optional R2E_JUDGE_API_KEY) now sends the request to <endpoint>/chat/completions with a bearer token, at temperature 0 because small local models are noisy judges otherwise. ANTHROPIC_API_KEY is never forwarded to a custom endpoint; the default Anthropic route is unchanged and the verifier stays stdlib-only. Also corrects the docs that described R2E_JUDGE_MODEL as a LiteLLM name and listed judge_status values the verifier never emits.
KNambiarDJsc
left a comment
There was a problem hiding this comment.
Reviewed and tested 140fb59:
- Unit tests:
tests/test_pr_diff_verifier.py+tests/test_pipeline_pr_diff.py: 78 passed.ruff check/ruff format --check: clean. Coverage is genuinely thorough — including the two failure modes I'd have gone looking for myself: an Anthropic-shaped reply arriving on the OpenAI route (status="parse", not a crash), and a thinking-mode server returningcontent: nullwith the real text elsewhere (also"parse", not aTypeError). - Real end-to-end run, not just the mocks: I stood up a real local HTTP server speaking the OpenAI
/chat/completionsshape and ran the actual verifiermain()against it — same code path a Harbor container runs,R2E_JUDGE_ENDPOINT/R2E_JUDGE_MODELpassed exactly as--vewould:The judge score my fake server returned flowed correctly into the weighted reward.{ "final_reward": 0.915, "components": {..., "llm_judge": 0.83}, "judge_model": "local-test-model", "judge_endpoint": "http://127.0.0.1:8791/v1", "judge_status": "ok" } - The "never forward
ANTHROPIC_API_KEYto a custom endpoint" behavior is correct and matches the pattern #104 established for the main LLM client — good consistency across the codebase. test_verifier_source_is_stdlib_onlyis a nice addition independent of this PR's main change — it locks in an invariant (the verifier is base64-baked into a barepython:3.12-slimimage, so it can never depend on anything beyond stdlib) that nothing was previously testing directly.
LGTM. Two small, non-blocking things:
docs/pipelines/README.mdandpr_diff.mdboth usehttp://host.docker.internal:8000/v1in the self-hosted example. That's correct for Docker Desktop, and the comment does call out the Linux bare-daemon caveat (--add-host=host.docker.internal:host-gateway) — just flagging that a reader skimming the command block rather than the prose above it could copy-paste the Docker Desktop form on a Linux CI runner and get a connection refused. Not asking for a change, just noting it since it's the one part of the docs that's environment-dependent._judge_config_from_env's docstring saysR2E_JUDGE_MODELis "then required" when an endpoint is set, but returns""rather than raising when it's missing —llm_judgecatches that downstream via the newno_judge_modelstatus. Accurate as implemented (fails closed, doesn't crash), the docstring phrasing just reads slightly stronger than what actually happens at that specific function boundary.
The self-hosted judge example used host.docker.internal, which only resolves under Docker Desktop, and pointed Linux users at a docker-run flag they cannot pass through harbor. Say so next to the command and give the host-LAN-IP form that works on a bare daemon instead. Also make the _judge_config_from_env docstring describe what it does at its boundary: an unset R2E_JUDGE_MODEL on the endpoint route yields "" and llm_judge reports no_judge_model; nothing raises.
|
@KNambiarDJsc Thanks for the careful pass, and for running it against a real OpenAI-shaped server, that's the same path the Harbor container takes, so it's good to see the score flow through the weights independently. Both points taken, pushed as a follow-up commit:
|
|
Thanks, tested and merged. Also checked that the custom endpoint doesn't receive the Anthropic key. Existing tasks will need their baked verifier updated to use this. |
Summary
pr_diffverifier'sllm_judgewas hardwired to Anthropic's Messages API andANTHROPIC_API_KEY, so a self-hosted model could never act as judge and 0.50 of the reward weight was unavailable without an Anthropic key. Two new verifier env vars, settable per run viaharbor run --ve, fix that:R2E_JUDGE_ENDPOINT(base URL of a vLLM / Ollama / llama.cpp / gateway server; the request goes to<endpoint>/chat/completionswith a bearer token) andR2E_JUDGE_API_KEY(optional).R2E_JUDGE_MODELis required alongside an endpoint since a self-hosted server has no Haiku.ANTHROPIC_API_KEYis never forwarded to the custom server; a placeholder bearer is sent unlessR2E_JUDGE_API_KEYnames one. The default Anthropic route is unchanged byte-for-byte, and the verifier stays stdlib-only (it is baked into a barepython:3.12-slimimage) — a new test parses the baked source and asserts every import is stdlib.temperature: 0. At the server's default temperature Qwen3.5-4B scored the same oracle-as-prediction input 0.5 / 1.0 / unparseable across three calls; at 0 it is deterministic and correct (below). The Anthropic route keeps its defaults — the published weights were calibrated against them.reward-details.jsongainsjudge_endpoint(null on the Anthropic route) so runs are auditable;judge_statusgainsno_judge_modelfor the endpoint-without-model case.R2E_JUDGE_MODELis a bare model id as the serving API names it, not a LiteLLM-qualified name (ENV.md);judge_modelexample and thejudge_statusvalue list inREWARD_SCHEMA.md; RFC 0001's "LLM judge via LiteLLM"; the "always uses Anthropic" comments in the pipeline docs and the dataset-card text inhub.py.pr_diff.mdgets a self-hosted-judgeharbor runexample.Test plan
uv run --all-extras pytest -q— 1880 passed, 7 skipped (py3.12, all extras as CI installs them) (13 new tests intests/test_pr_diff_verifier.pyfor the two request routes, response parsing, and the env → config mapping; 1 new stdlib-only guard intests/test_pipeline_pr_diff.py)uv run ruff check .anduv run ruff format --check .cleanmkdocs buildclean (zero warnings)mainfirst:llm_judge(model="Qwen/Qwen3.5-4B", api_key="EMPTY")builds a request tohttps://api.anthropic.com/v1/messageswith anx-api-keyheader, has no endpoint parameter, and a real call returns(None, "network")Qwen/Qwen3.5-4Bon an RTX 3090, verifier loaded standalone, a realpallets/clickcommit as oracle + its message as the instruction:ok(3/3 runs, ~1 s each)ok(3/3 runs)network_judge_config_from_env()withANTHROPIC_API_KEYalso set → bearer sentEMPTY, score 1.0,ok; vLLM's access log shows the callsharbor run -a oracle -e docker) on a pr_diff task emitted with this branch's verifier baked into its image (a realpallets/clickcommit as the oracle):--ve R2E_JUDGE_ENDPOINT=http://host.docker.internal:8000/v1 --ve R2E_JUDGE_MODEL=Qwen/Qwen3.5-4B→reward-details.json:llm_judge: 1.0,judge_status: "ok",judge_model: "Qwen/Qwen3.5-4B",judge_endpoint: "http://host.docker.internal:8000/v1",final_reward: 1.0; 47 s job runtimellm_judge: null,judge_status: "no_api_key",judge_endpoint: null, weights renormalized,final_reward: 1.0— the default path is unchanged--ve R2E_JUDGE_ENDPOINT=http://host.docker.internal:9/v1(nothing listening) →judge_status: "network", judge weight renormalizedOut of scope
--add-host=host.docker.internal:host-gateway) is documented in the example but was not exercised; the end-to-end run below was on Docker Desktop (WSL2), wherehost.docker.internalresolves out of the box.REWARD_SCHEMA.mdandpr_diff.mdstill describereward.jsonwithreward/cappedfields, while the verifier writesreward-details.jsonwithfinal_rewardand nocapped(since pr_diff verifier: mirror #75 — reward-details.json sidecar #76). Separate fix.Closes #134