documentation
Using Firstpass
Firstpass is a drop-in, Anthropic-compatible HTTP proxy. Run it, point your agent's base_url at it, and every request is routed to the cheapest model that provably passes your gate — with a signed receipt for each decision. This page covers install, configuration, and operation. For the design rationale, read the SPEC.
Overview
Every request flows through three moves: route to the cheapest rung of a ladder, gate the real output, and escalate one rung only if the gate fails — across providers, with cross-provider failover on a 5xx. The whole decision is written to a tamper-evident, hash-chained trace.
Install
| Method | Command |
|---|---|
| pip / uvx | uvx firstpass · pip install firstpass |
| Homebrew | brew install dshakes/tap/firstpass |
| npm | npx @dshakesnotbot/firstpass |
| curl | sh | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/dshakes/firstpass/releases/latest/download/firstpass-proxy-installer.sh | sh |
| Docker | docker run -p 8080:8080 -e FIRSTPASS_BIND=0.0.0.0:8080 ghcr.io/dshakes/firstpass:latest |
| Cargo | cargo install --git https://github.com/dshakes/firstpass firstpass-proxy |
| crates.io | cargo install firstpass-proxy |
cargo install --git and the Docker image work today. Every other channel — pip · uvx · Homebrew · npm · crates.io · curl | sh · prebuilt binaries — is wired end-to-end and lights up on the first tagged release (each registry push is gated on an operator secret). Upgrade in place with firstpass-proxy-update, or via your channel (brew upgrade, pip install -U firstpass, …).Quickstart
See the whole loop in ~10 seconds — no API keys. The demo stands up a mock upstream and drives one real decision, escalation and all.
# clone & watch the receipt print git clone https://github.com/dshakes/firstpass && cd firstpass cargo run -p firstpass-proxy --example demo
Modes
Firstpass runs in one of two modes, set by FIRSTPASS_MODE:
observe (default)
Transparent pass-through. Requests are forwarded unchanged and a trace is recorded off the hot path — zero behavior change. Use this to shadow traffic and build an audit trail before enforcing anything.
enforce
Full routing: cheapest-first ladder, gate the output, escalate on failure, cross-provider failover. Requires a routing config (below).
FIRSTPASS_MODE=enforce FIRSTPASS_CONFIG=./firstpass.toml firstpass-proxyEnvironment
Configuration is 12-factor. Run firstpass-proxy --help for the reference:
| Variable | Purpose | Default |
|---|---|---|
| FIRSTPASS_MODE | observe | enforce | observe |
| FIRSTPASS_BIND | listen address | 127.0.0.1:8080 |
| FIRSTPASS_CONFIG | path to firstpass.toml | — |
| FIRSTPASS_DB | trace store path | firstpass.db |
| FIRSTPASS_UPSTREAM_ANTHROPIC | upstream base URL | api.anthropic.com |
| FIRSTPASS_UPSTREAM_OPENAI | upstream base URL | api.openai.com |
| FIRSTPASS_TENANT | tenant id for the trace | default |
| FIRSTPASS_PROMPT_SALT | salt for prompt hashing | — |
| RUST_LOG | tracing filter | info |
Routing config
Routing is declarative TOML: ordered routes (first match wins), each binding a slice of traffic to a mode, a cheapest-first model ladder, and the gates that must pass. Start from firstpass.example.toml.
[[route]] match = { agent = "claude-code" } mode = "enforce" ladder = ["anthropic/claude-haiku-4-5", "anthropic/claude-sonnet-5"] gates = ["schema", "cargo-test"] # wildcard fallback — first match wins, top to bottom [[route]] match = {} mode = "observe"
Gates
A gate reads the candidate output and returns a verdict. Two kinds:
- Inline —
non-empty,json-valid, and a minimal JSON-Schema gate (type/required/properties). No process spawn. - Subprocess plugin — your own command (tests, linter, a judge). The candidate is passed on stdin, never argv (injection-resistant); a JSON verdict comes back on stdout. Non-zero exit or timeout → abstain.
Endpoints
| Endpoint | Purpose |
|---|---|
| POST /v1/messages | Drop-in inference — observe or enforce per route. |
| POST /v1/feedback | Report a downstream outcome for a prior trace. |
| GET /v1/capabilities | Machine-readable capability descriptor. |
| GET /healthz | Liveness. |
Receipts & feedback
Every decision is a hash-chained JSON trace an external auditor can re-derive — which model ran, which gates passed, what it cost, and the counterfactual saving. Downstream outcomes (did the tests pass an hour later?) flow back via POST /v1/feedback:
curl localhost:8080/v1/feedback -d '{ "trace_id": "0192f3a1-…", "gate_id": "cargo-test", "verdict": "pass", "reporter": "ci" }'
Offboarding
Leaving is always one environment variable — an invariant of the design:
unset ANTHROPIC_BASE_URL # your agent talks to the provider directly again