Batch routing¶
Non-urgent tasks (docs, formatting, simple tests) can run through a provider's batch API instead of the interactive one, at roughly half the per-token cost. Batch routing is two steps: a task-shape classifier decides whether a task is a batch candidate, then an adapter-capability gate decides whether it can actually be dispatched to a batch endpoint — a batch-eligible task on an adapter with no batch surface is refused back to the interactive path rather than silently faked onto one.
There is no dedicated CLI for this; it runs automatically at task decomposition and dispatch time. bernstein explain <task> surfaces whether a task was marked batch-eligible.
Step 1 — eligibility classification¶
classify_batch_mode(task) runs at decomposition time and returns a BatchClassification (mode, reason, discount_factor). A task is never batch-eligible when any of these hold:
task.roleismanager,architect,security, ororchestrator.task.priority == 1(critical).task.scope == LARGE.task.complexity == HIGH.task.task_typeisRESEARCHorUPGRADE_PROPOSAL.- The manager explicitly requested
opusviatask.model. task.batch_eligiblewas explicitly set toFalse.
Past those hard gates, a task is batch-eligible when task.batch_eligible was explicitly set to True, or task.complexity == LOW, or its title/description matches a batch-keyword pattern (doc, docstring, readme, changelog, format, lint, comment, type hint, simple test, unit test, release note, bump version, and similar). The discount factor is a flat 0.5 (BATCH_DISCOUNT_FACTOR) whenever eligible, 1.0 otherwise; apply_batch_discount(cost_usd, classification) applies it to a cost estimate.
Step 2 — capability-gated dispatch¶
Eligibility alone does not guarantee a batch dispatch. route_batch(task_id, adapter, batch_eligible) (core/cost/scheduling/batch.py) checks the adapter's declared batch-dispatch capability (adapters/_contract.py::batch_dispatch_capability) before committing to a route:
batch_eligible | Adapter capability | Route | Notes |
|---|---|---|---|
False | any | interactive | Not a batch candidate; no refusal reason recorded. |
True | NATIVE | batch | Dispatched to the provider's batch endpoint. |
True | NONE | interactive | Refused with adapter_no_batch_surface; runs interactively instead of faking a batch path that doesn't exist. |
Only claude, claude_routine, and openai_agents declare NATIVE batch capability today (_BATCH_CAPABLE_ADAPTERS); every other adapter — including unknown or third-party adapters — reports NONE by default, so batch-eligible work is never routed somewhere that cannot honour it.
How it composes¶
route_batch is a pure function of (task eligibility, adapter capability) with no side effects, so it slots into the deterministic dispatch policy without special-casing. The live dispatch path (core/cost/scheduling/live_dispatch.py) calls classify_batch_mode directly to decide whether a task counts as batch for scheduling purposes.
Limitations¶
- Classification is a fixed set of heuristics (role/priority/scope/complexity gates plus a keyword regex) — there is no learned or configurable scoring model, and no per-project keyword override.
- The capability list (
_BATCH_CAPABLE_ADAPTERS) is a hardcoded allowlist in source; adding batch support for a new adapter requires a code change, not a config change.
Source¶
src/bernstein/core/tasks/batch_router.py—classify_batch_mode,BatchClassification,apply_batch_discount.src/bernstein/core/cost/scheduling/batch.py—route_batch,BatchRouteDecision.src/bernstein/adapters/_contract.py—BatchDispatchCapability,batch_dispatch_capability.