Batch-aware Claude-judge dispatch in the eval path (speed-threshold routing: sync <2k, batch >=2k in 10k splits)
kind: infra
track:Highlight text on any card (body, plan, or an event) to anchor a comment, or leave a whole-task comment here. Mention @claude to summon a reply.
No comments yet.
Motivation
Judge scoring (alignment/EM/refusal judging of raw completions) currently runs as synchronous per-request API calls. Decision (2026-06-12, token-audit fix #22 + threshold research): route by request count, optimizing SPEED — sync judging competes with the org's many live Claude Code sessions for the shared org-level rate limits (Sonnet-pool OTPM is the binding constraint for judges), while the Message Batches API has its own separate queue (500k requests in flight at Tier 4) and a 50% price discount as a side benefit.
Routing rule (implement exactly; constants configurable)
N < 2_000→ synchronous:AsyncAnthropicmax-concurrency; judge rubric/system prompt behind acache_controlbreakpoint; fire 1 request first, await first token, then fire the rest (so they read the cache the first one wrote); rely on SDK 429 backoff.N >= 2_000→ Message Batches API, split into sub-batches of ≤10_000 requests (also keeps each under the 256MB cap; sub-batches end independently → results arrive in waves).--force-syncflag for critical-path runs (batch latency is a distribution: most <1h, hard cap 24h — a tail-stall must never block a same-day decision).- Tier scaling: read
anthropic-ratelimit-output-tokens-limitfrom one probe response; scale the sync ceiling byOTPM/400_000instead of hardcoding Tier 4 (at Tier 2 the sync threshold drops to ~500).
Implementation requirements
- One dispatch helper in the judge module (find the current judge call sites under
src/explore_persona_space/eval//llm/); both paths return identical result shapes keyed by completion id (custom_idmapping for batch). - Batch path:
client.messages.batches.createwith oneRequestper completion; pollretrieve()untilprocessing_status == "ended"; collect viaresults(); retry theerroredsubset (once, then surface);expiredrequests resubmitted in a follow-up batch. - Checkpoint-per-phase (hard requirement): persist submitted
batch_ids + the custom_id→completion mapping to disk the moment a batch is submitted, so a crashed/resumed session polls existing batches instead of resubmitting (and never double-pays). Same pattern as the existing checkpoint rules. - Use the 1-hour cache TTL (
cache_control: {type: "ephemeral", ttl: "1h"}) on the shared judge rubric inside batch requests — batch requests execute out of order and the 5-min TTL often expires before a request runs (observed hit rates 30-98%). - Off-pod execution unchanged: judge scoring already runs on the VM after pod termination per CLAUDE.md (CPU-only phases don't hold GPU pods) — batch latency therefore costs zero GPU time. The
/issueorchestrator's bg-Bash poller watches the batch the same way it watches pods. - Tests: routing logic (threshold, tier scaling, splitting), checkpoint resume, result-merge from multiple sub-batches, errored-subset retry — all with mocked API. No live API calls in tests.
Acceptance criteria
- Helper + call-site migration merged;
uv run pytestgreen on new tests; ruff clean. - A dry-run mode prints the routing decision (N, threshold, tier read, chosen path, sub-batch plan) without calling the API.
- One real smoke validation on a small N (<50, sync path) against a handful of stored completions.
- Docs: short usage note in the judge module docstring; no new always-on CLAUDE.md prose.
Activity