A cross-provider multi-model AI orchestration engine. A supervisor model decomposes a goal into a task DAG, routes each sub-task to the best-quality model capable of it — by required strengths and tool support — across providers (Anthropic, any OpenAI-compatible endpoint, Ollama), runs them one-shot or in an agentic tool loop, and synthesizes a final answer. Built without an orchestration framework (no LangChain / CrewAI / LiteLLM).
One conductor, many players — pass the baton from the leader model to the workers and back.
- Supervisor + routing. An LLM plans a validated, acyclic task DAG; a router sends each task
to the strongest model capable of it (by required strengths + tool support).
--prefer cash_protect_quotaright-sizes instead, to protect subscription quota. - Cross-provider.
AnthropicProviderand a genericOpenAICompatProviderspeak to Anthropic, Google AI Studio (Gemini), Groq, OpenRouter, DeepSeek, Moonshot (Kimi), local Ollama, and any other OpenAI-compatible endpoint — no code changes, just env vars. - Hybrid one-shot / agentic. Tasks run as a single call or as a model↔tool loop (
run_pythonin a subprocess sandbox — container-isolated underBATON_SANDBOX=docker— plus host-mediatedfetch_url/read_file). - Shared context. An append-only blackboard carries provenance; each task gets a scoped, budget-capped projection of only the dependency artifacts it needs.
- Streaming everywhere. Live token streaming through the supervisor, workers, and synthesizer, with per-task labels for parallel workers and cooperative early-stop.
- Optional Web UI. A small FastAPI + SSE app streams a run live in the browser (plan → per-task worker output → synthesis → result); runs with real providers or a no-key demo.
- Cost & honesty. A
CostMetertallies per-model usage and cost, and propagates an estimated flag when a provider returns no usage. - Forgery-resistant evaluation. A 3-arm eval (baseline vs. orchestration vs. single-agent) with a scorer that runs untrusted solution code under process + filesystem separation so a model cannot fake a passing score.
- Tested. 560+ tests, zero-network by default (
FakeProvider+ local subprocesses),ruff-clean.
flowchart TD
goal(["goal"]) --> S["Supervisor<br/>plan → validated task DAG<br/>(acyclic · typed · one_shot | agentic)"]
S --> R["Router<br/>strongest capable model per task<br/>(by strengths + tool support)"]
R --> P
subgraph wave["wave execution · asyncio fan-out · fail-fast"]
direction TB
P["Projector<br/>scoped, budget-capped request<br/>(system + task + deps)"]
P --> W["Worker<br/>one-shot"]
P --> AW["AgenticWorker<br/>model ↔ tool loop<br/>(run_python · fetch_url · read_file)"]
end
W --> BB[("Blackboard<br/>append-only · provenance · latest-wins")]
AW --> BB
BB --> SY["Synthesizer<br/>combine artifacts → final answer"]
SY --> result(["result<br/>+ CostMeter totals · usage · duration"])
classDef io stroke:#8b5cf6,stroke-width:2px;
classDef store stroke:#f59e0b,stroke-width:2px;
class goal,result io;
class BB store;
Text version (renders anywhere, e.g. PyPI or a terminal)
┌──────────────┐
goal ──────────► │ Supervisor │ plan → validated task DAG (acyclic, typed, one_shot|agentic)
└──────┬───────┘
▼
┌──────────────┐ per task: pick the strongest model whose strengths +
│ Router │ tool support fit the task (quality-first)
└──────┬───────┘
▼
┌───────────── wave execution (asyncio, fan-out cap, fail-fast) ─────────────┐
│ ┌───────────┐ scoped, budget-capped request (system + task + deps) │
│ │ Projector │──────────────────────────────────────────────────────────► │
│ └───────────┘ │
│ ▼ ▼ │
│ ┌─────────┐ one-shot ┌───────────────┐ model↔tool loop │
│ │ Worker │ │ AgenticWorker │ (run_python sandbox, │
│ └────┬────┘ └───────┬───────┘ fetch_url, read_file) │
│ └──────────────┬──────────────────┘ │
└───────────────────────┼───────────────────────────────────────────────────┘
▼
┌──────────────────────┐ append-only, provenance, latest-wins
│ Blackboard │◄──────────────────────────────────────
└──────────┬───────────┘
▼
┌──────────────┐
│ Synthesizer │ combine artifacts → final answer
└──────┬───────┘
▼
result (+ CostMeter totals, usage, duration)
| Component | File | Responsibility |
|---|---|---|
| Supervisor | src/baton/supervisor.py |
Decompose goal → validated task DAG |
| Router | src/baton/router.py |
Task → strongest capable model (by strengths + tool support) |
| Projector | src/baton/projector.py |
Scoped, budget-capped request from blackboard artifacts |
| Worker | src/baton/worker.py |
One-shot model call |
| AgenticWorker | src/baton/agent.py |
Model↔tool loop with per-turn records |
| Blackboard | src/baton/blackboard.py |
Append-only shared state with provenance |
| Synthesizer | src/baton/synthesizer.py |
Artifacts → final answer |
| Runtime | src/baton/runtime.py |
Orchestrate: plan → waves → synthesize (streaming, fail-fast) |
| Providers | src/baton/providers/ |
Anthropic + OpenAI-compatible adapters (complete/stream/tools) |
| Tools | src/baton/tools/ |
Sandbox / DockerSandbox, run_python, fetch_url, read_file |
| Eval | eval/ |
5 composite goals, 3-arm comparison, forgery-resistant scorer |
Requires Python 3.11+ and uv.
git clone https://github.com/ribato22/baton
cd baton
uv sync --dev # install deps + dev tools
uv run pytest # 580+ tests, no network
uv run ruff check . # lint
# See it orchestrate end-to-end with ZERO API keys (FakeProvider demo):
uv run python examples/fake_provider.pyThen configure at least one real provider (see Providers) and run a demo:
cp .env.example .env # fill in one provider, then `set -a; . .env; set +a`
uv run python demo.py # show detected providers
uv run python demo.py orchestrate # full supervisor → workers → synth, streamed live
uv run python demo.py agentic # one cross-provider agentic coding task (run_python loop)
uv run python demo.py eval # 3-arm eval suitedemo.py orchestrate streams every phase live, then prints the result (illustrative):
Orchestrate demo — planner/synth model=openai/gpt-4o-mini
(planning + workers + synthesis stream live)
[haiku] Threads run as one— / tasks bloom in parallel time, / the join gathers all.
STATUS: success
FINAL:
Threads run as one—
tasks bloom in parallel time,
the join gathers all.
cost: $0.001834
demo.py eval prints the 3-arm table (format_report); read the VERDICT with the warnings
(illustrative numbers):
GOAL WINNER BASE ORCH AGEN
-------------------------------------------------
slugify orchestration 0.70 1.00 0.85
roman baseline 1.00 0.85 0.55
calc orchestration 0.55 0.85 0.70
csv_stats agentic 0.40 0.55 0.85
json_flatten orchestration 0.70 1.00 0.85
-------------------------------------------------
wins: baseline=1 orchestration=3 agentic=1 ties=0
totals: baseline $0.0210 orchestration $0.0480 agentic $0.0350
VERDICT: ORCHESTRATION
Baton ships three surfaces: a one-command CLI (the primary entrypoint), an optional Web UI, and an importable library. All three need at least one configured provider — see Providers — or (Web UI only) fall back to a no-key demo.
uv run baton "write a haiku about concurrency, then explain the metaphor"baton streams the plan, each parallel worker's output (labelled per task), and the synthesis live,
then prints a summary. Flags (baton --help):
| Flag | Description |
|---|---|
--prefer {quality,cash_protect_quota,local,cheap} |
routing objective (default quality — the strongest model capable of each task). cash_protect_quota right-sizes to protect subscription quota; local/cheap are accepted but currently behave as quality |
--provider NAME / -P NAME |
restrict the planner/synth baseline to this provider |
--model ID |
override the planner/synth model_id |
--json |
print the run summary as one parseable JSON line; disables streaming |
--no-stream |
disable live streaming of plan/worker/synth text |
--version |
print the installed version and exit |
Exit codes: 0 success, 1 run failure, 2 config error (e.g. no provider configured), 130
Ctrl-C (prints whatever partial output had streamed so far — never a raw traceback).
The text-mode summary reports billed_usd (real cash spent) vs. credit_usd (subscription/plan
API-equivalent valuation of subscription calls, not cash) plus subscription_models (the count of
distinct subscription-billed models used) — a subscription run is cash-free but still consumes your
interactive quota (see below).
uv sync --extra ui
uv run python -m webui # then open http://127.0.0.1:8000A small FastAPI + Server-Sent-Events app streams a run live in the browser — the plan, each parallel
worker's output (labelled per task), the synthesis, and the final result with cost. It runs with your
configured providers, or a built-in FakeProvider demo if none are set (no API key needed). This is
a source-checkout feature — webui/ is not shipped in the built wheel/PyPI package.
BATON_UI_HOST / BATON_UI_PORT override the bind address. The page inserts all model output via
textContent only (never raw HTML), so streamed text cannot inject markup.
import asyncio
import baton
async def main() -> None:
registry, providers, model_id = baton.build_providers_from_env()
runtime = baton.make_runtime_factory(registry, providers, model_id)()
result = await runtime.aexecute("your goal")
print(result.status, result.billed_usd, result.credit_usd)
asyncio.run(main())The top-level baton package re-exports the common library API (Runtime, Registry,
Router, build_providers_from_env, make_runtime_factory, RunResult, ModelInfo, Task,
LLMProvider, ProviderError — see baton.__all__) so you don't need to reach into submodules
for everyday use.
See examples/ for runnable scripts — including
examples/fake_provider.py, which needs no API key at all. For a
guided tour with hardcoded goals, see the demo script:
uv run python demo.py orchestrate|agentic|eval (walked through in Quickstart).
Baton can drive the official headless CLIs you're already logged into instead of (or alongside) a card-billed API key:
export CLAUDE_CODE_ENABLED=1 # needs `claude` installed and logged in
# CLAUDE_CODE_SYSTEM_PROMPT_MODE=replace is the default — makes `claude -p` behave as a
# raw completion; `append` breaks strict-JSON planning, so leave it unset unless you know why.
export CODEX_ENABLED=1 CODEX_TIER=3 # needs `codex login`
uv run baton "your goal"
⚠️ Subscription runs are cash-free but consume your interactive Claude Code / Codex quota — the same pool your interactive coding sessions draw from. A heavy orchestration run can trip a rate-limit pause. The defaultqualityobjective favors the strongest capable model per task, which can lean on subscription models. Pass--prefer cash_protect_quotato mitigate this — it sends bulk/easy work to cheaper local/free-tier models and reserves subscription models for hard tasks only. A card-billed, free-tier, or local model as planner is recommended: subscription CLIs ignoretemperature, so Baton retries planning with self-correction and can gateclaude -pas planner behind a live parse-plan check (it only plans if it demonstrably emits valid plan JSON).This drives the official headless CLIs (
claude -p,codex exec) that you are already logged into — never the claude.ai / ChatGPT web apps. Scraping those web apps is not implemented (it would violate their Terms of Service).
Baton is a CLI first, so it already works in any editor's integrated terminal (uv run baton "…"). For VSCode there are two extra conveniences:
1. One-keystroke tasks. The repo ships .vscode/tasks.json. Open
Terminal → Run Task… (or press ⌘/Ctrl+Shift+B) and pick:
| Task | What it does |
|---|---|
| Baton: Run goal | Prompts for a goal and orchestrates it (streams plan → workers → synthesis). |
| Baton: Web UI | Serves the live Web UI at http://127.0.0.1:8000. |
| Baton: MCP server (stdio) | Runs the MCP server for AI-agent integration (below). |
| Baton: Test / Lint | pytest / ruff over the project. |
2. MCP server — let the AI inside your editor call Baton. Baton ships an
MCP server (baton_mcp/) exposing one tool,
baton_run(goal, prefer?), that plans → routes → runs → synthesizes and returns the final answer
plus an honest cash/plan-credit footer. Any MCP-capable assistant (Claude Code, Cursor, VS Code
Copilot agent mode, Windsurf) can then delegate whole goals to Baton.
Install it clone-free (recommended), or from a source checkout:
# clone-free — uv fetches the published package + the `mcp` extra on demand:
uvx --from "baton-orchestrator[mcp]" baton-mcp
# or install it and run the console script:
pip install "baton-orchestrator[mcp]" # then:
baton-mcp
# or from a source checkout:
uv sync --extra mcp && uv run --extra mcp python -m baton_mcpRegister it with your client. Claude Code — one command:
claude mcp add baton -- uvx --from "baton-orchestrator[mcp]" baton-mcpCursor / VS Code / Windsurf — add to the client's MCP config (e.g. .cursor/mcp.json, or
VS Code's .vscode/mcp.json under a "servers" key):
The server reads providers from the environment exactly like the CLI (including
CLAUDE_CODE_ENABLED / CODEX_ENABLED), so configure at least one provider first — it does not
fall back to a demo. A full branded VSCode extension is intentionally not shipped; the CLI,
tasks, and the MCP server cover the same ground.
In the official MCP registry. Baton is published to the
official MCP registry as io.github.ribato22/baton
(a validated server.json manifest plus a publish-mcp.yml GitHub Actions workflow
that re-publishes it via OIDC on each release). Directories such as mcp.so,
PulseMCP, and Glama index from it.
As a Claude Code plugin. This repo also doubles as a plugin marketplace — one command wires the
MCP server and a /baton:run slash command into Claude Code:
/plugin marketplace add ribato22/baton
/plugin install baton@baton
Requires uv on your PATH (the plugin launches the server with
uvx). See plugins/baton/.
Other MCP clients (Codex, Cursor, Windsurf, Gemini CLI, Cline). These don't have a plugin marketplace — they consume MCP servers via config. Point them at the same launch command:
-
OpenAI Codex CLI —
codex mcp add baton -- uvx --from "baton-orchestrator[mcp]" baton-mcp(writes an[mcp_servers.baton]block to~/.codex/config.toml; add providers with repeated--env KEY=VALUE). -
Gemini CLI —
gemini mcp add baton uvx -- --from "baton-orchestrator[mcp]" baton-mcp(the--is required because Baton's first arg is--from). -
Cursor / Windsurf / Cline / Roo — add the standard
mcpServersentry to the client's MCP config (~/.cursor/mcp.json,~/.codeium/windsurf/mcp_config.json, or the Cline settings):{ "mcpServers": { "baton": { "command": "uvx", "args": ["--from", "baton-orchestrator[mcp]", "baton-mcp"], "env": { "CLAUDE_CODE_ENABLED": "1", "ANTHROPIC_API_KEY": "sk-ant-..." } } } }
Set your providers in each client's env block (CLAUDE_CODE_ENABLED, CODEX_ENABLED,
ANTHROPIC_API_KEY, OPENAI_COMPAT_*).
Claude Desktop & Smithery — MCPB bundle. A one-file MCPB bundle is attached to each
release as baton-<version>.mcpb. Open it in Claude
Desktop for a one-click install (it shows a provider-config UI). Or publish it to
Smithery as a local server — because Baton is stdio (not a hosted HTTPS
endpoint), that's the CLI bundle path, not the "publish a URL" web form:
# download baton-<version>.mcpb from the release, then (needs a Smithery API key):
npx -y @smithery/cli mcp publish ./baton-<version>.mcpb -n <your-namespace>/batonThe bundle just wraps uvx --from "baton-orchestrator[mcp]" baton-mcp, so it runs locally and your
subscription CLIs + API keys work as usual (needs uv on PATH).
Set environment variables for any subset; baseline priority is
Anthropic > OpenAI-compat > Kimi > Ollama. See .env.example for the full list.
| Provider | Env | Access |
|---|---|---|
| Anthropic (Claude) | ANTHROPIC_API_KEY |
Paid API (console.anthropic.com) |
| Generic OpenAI-compatible | OPENAI_COMPAT_BASE_URL, OPENAI_COMPAT_MODEL (+_KEY/_NAME/_CONTEXT/…) |
Any OpenAI-compatible endpoint |
| Moonshot / Kimi | MOONSHOT_API_KEY |
Paid API |
| Ollama | OLLAMA_BASE_URL |
Local & free |
Subscription CLI agents are opt-in and consume your interactive quota. Scraping claude.ai / ChatGPT is not built (ToS, fragile, ban risk). Instead Baton can drive the official headless CLIs you're already logged into — Claude Code (
claude -p) and Codex (codex exec) — with no API key. This is off by default (CLAUDE_CODE_ENABLED=1/CODEX_ENABLED=1plus the CLI installed) and is never used by the eval. Honest caveat:claude -pandcodex exectoday draw from the same interactive subscription pool as the chat apps (not a separate/metered bucket), so a full orchestration run — and especially the 3-arm eval — can burn your Claude Code / Codex allowance and trip a mid-run hard-pause. Baton reportscredit_usd(subscription value consumed) separately frombilled_usd(cash), routes only hard/high-tier tasks to subscription (bulk work goes to local/free-tier), and caps subscription calls per run (BATON_MAX_SUBSCRIPTION_CALLS, default 4).Billing surface moves — re-verify before trusting it. Whether
claude -pbills against the subscription pool vs. a metered API-rate credit bucket has flipped several times in months (announced 2026-06-15, then paused; still paused as of 2026-07-22). When Anthropic next announces a billing change, repeat the live gate indocs/claude-code-live-gate.mdand re-check the Help Center banner, then update the "verified" date recorded there.
Free, high-intelligence option — Google AI Studio (Gemini Flash), via the generic slot:
export OPENAI_COMPAT_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai/
export OPENAI_COMPAT_KEY=<ai-studio-key> # aistudio.google.com/apikey
export OPENAI_COMPAT_MODEL=gemini-flash-latest # pick a current model from the endpoint's /models
export OPENAI_COMPAT_NAME=google/gemini-flash
uv run python demo.py orchestrateThe generic slot defaults to industry-standard values (context 128k, output 8k, tool-capable, cost
0 for free tiers) and registers its own ModelInfo, so cost/context accounting is correct.
Several providers at once — add OPENAI_COMPAT_2_*, OPENAI_COMPAT_3_*, … (each with its own
model_id / pricing / context). For example Gemini plus Groq, so the supervisor plans on Gemini
while the cheaper Groq model runs the parallel workers — genuine cross-provider orchestration. See
.env.example.
demo.py eval runs a 3-arm comparison over 5 composite coding goals: baseline (one strong
model, one shot), orchestration (the full engine), and agentic-single (one model + a
run_python loop, no decomposition). Each goal is scored by a hidden reference test.
The scorer runs the model's generated solution.py in a subprocess under process + filesystem
separation: a trusted runner drives the untrusted solution in a separate process that never sees
the expected outputs (nonce-authenticated RPC), so a solution must actually compute correct answers —
it cannot fake a passing score.
Read the verdict together with the warnings the harness emits:
WARNING: some costs are estimated …— a provider returned no usage; cost comparison is soft.WARNING: agentic arm failed N run(s) …— a0.0may be infra/provider failure, not capability.WARNING: goal(s) […] produced NO trusted result …— the reference runner itself is broken; those scores are harness artifacts, not real zeros.
This is a study project; its isolation guarantees are deliberately scoped and documented.
- Agentic sandbox is for self-written goals. The default subprocess
Sandboxprotects against accidents, not adversaries: on macOS the host network and disk remain reachable. For real isolation useBATON_SANDBOX=docker(runs code in a container with--network none, read-only root, cgroup limits) — this is the prerequisite for the network-isolation guarantee. - External tools are host-mediated.
fetch_url(domain allowlist) andread_file(root-confined) run in the trusted orchestrator so sandboxed code stays network-isolated. Prompt-injection containment holds only under the Docker sandbox. - Eval scoring is forgery-resistant, best-effort POSIX. Process + filesystem separation stops a
solution from faking a score; a solution calling
setsid()can still escape thekillpggroup (the wall-clock timeout still bounds the run). It is process isolation, not a security sandbox for arbitrary hostile code. - Never put secrets in model context. Allowlists and the read-file root are the trust boundary.
src/baton/ # engine (importable package: `baton`)
providers/ # Anthropic + OpenAI-compatible adapters, FakeProvider
tools/ # Sandbox, DockerSandbox, run_python, fetch_url, read_file
eval/ # goals, 3-arm harness, forgery-resistant scorer, runner
examples/ # small runnable library-API scripts (incl. a no-key FakeProvider demo)
webui/ # optional FastAPI + SSE web UI (uv run python -m webui)
tests/ # 560+ tests (unit + opt-in integration)
docs/ # internal design/build records — see docs/README.md; not user docs
demo.py # end-to-end demo (orchestrate | agentic | eval)
- Test-driven, zero-network by default.
uv run pytestusesFakeProviderand local subprocesses; integration tests that touch the network/Docker are markedintegrationand skipped by default (uv run pytest -m integrationto opt in). - Lint:
uv run ruff check .(line length 100;E,F,I,UP,B). - Contributions welcome — see CONTRIBUTING.md and our Code of Conduct. Security reports: SECURITY.md. Release notes: CHANGELOG.md.
Baton is a study engine for multi-model orchestration, not a production framework. It deliberately avoids LangChain, LiteLLM, and CrewAI — the point is to see how a supervisor, router, projector, and blackboard actually work under the hood, not to hide them behind an abstraction. Don't adopt it as a drop-in production agent framework; treat it as a reference implementation to read, fork, and learn from.
- Run the real 3-arm eval across providers and interpret whether orchestration beats a single model.
- Per-task labelled streaming to a UI; async-generator / backpressure streaming API.
- Ollama tool-calling / streaming integration coverage.
MIT © 2026 ribato.
{ "mcpServers": { "baton": { "command": "uvx", "args": ["--from", "baton-orchestrator[mcp]", "baton-mcp"] } } }