A tiny local proxy that hard-caps LLM API spend.
Project description
"Set a budget, walk away, and never come back to a surprise bill."
๐ฌ What it does
agent-circuit-breaker is a local HTTP proxy that sits between your agent and any OpenAI- or Anthropic-compatible API. It enforces hard USD spend caps:
- Per-run budget โ one agent session cannot spend more than you allow.
- Per-day budget โ your total daily spend across all runs is capped.
- Global kill switch โ instantly block all traffic with one command.
When a cap is hit, the proxy returns a clear 429 Budget exhausted response. It does not silently degrade, retry, or fall back to a cheaper model.
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Agent โโโโโโถโ agent-circuit-breaker โโโโโโถโ Real provider โ
โ (SDK/CLI) โ โ http://localhost:8989 โ โ OpenAI/Claude โ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ SQLite state file โ
โ spend + kill flag โ
โโโโโโโโโโโโโโโโโโโโโโโ
๐ง Methodology: why this exists
Most cost-tracking tools (LiteLLM, etc.) are great at reporting spend. They are not designed to hard-stop a runaway agent mid-loop.
A runaway agent can:
- Get stuck in a retry loop.
- Spawn recursive tool calls.
- Call expensive models repeatedly while you are away.
Cost-tracking tools generally sit in one of three places:
- They track spend after the fact.
- They are too heavy to drop in front of a quick script.
- They report, but do not refuse, the request.
agent-circuit-breaker aims at the narrow slot none of those cover: a single-purpose, lightweight guard that says no the moment the cap is reached.
๐ How this compares
The agent-circuit-breaker name on PyPI belongs to a different project, and this one is not it:
- sagarchhatrala/agent-circuit-breaker โ "a deterministic safety gate for AI coding agents", v1.4.8 across 15 releases. It gates what an agent is allowed to do. This tool gates how much it is allowed to spend. Adjacent problems, same name, different tool โ theirs is the more mature project and it got there first.
Because that name is taken, this project installs as localab-circuit-breaker while the repository, module and agent-circuit-breaker CLI command keep their original names. If you want the safety gate, install theirs; if you want a hard USD cap, install this.
Honest scope: hard spend caps are not an unclaimed idea. LiteLLM and friends will happily tell you what you spent. What is genuinely uncommon is a proxy small enough to put in front of a throwaway script that returns 429 and stops, rather than logging the overspend and continuing.
Design principles
| Principle | How we enforce it |
|---|---|
| Hard stop | Returns 429 immediately when a budget is exhausted. |
| Local-first | SQLite state file only. No cloud, no telemetry, no accounts. |
| Minimal surface | No web UI, no plugin system, no dashboards. Just a proxy. |
| User-controlled pricing | Bundled prices.json that you edit. No network price fetching. |
| Transparent | Passes through bytes unchanged; accounting happens on the response path. |
๐๏ธ Architecture
flowchart LR
A[Agent / SDK] -->|HTTP| B(agent-circuit-breaker)
B -->|OpenAI format| C[OpenAI-compatible API]
B -->|Anthropic format| D[Anthropic-compatible API]
B --> E[(SQLite state)]
style B fill:#f9f,stroke:#333,stroke-width:2px
style E fill:#bbf,stroke:#333,stroke-width:2px
Request lifecycle
sequenceDiagram
participant Agent
participant ACB as agent-circuit-breaker
participant SQLite
participant Provider
Agent->>ACB: POST /openai/v1/chat/completions
ACB->>SQLite: Check kill switch
ACB->>SQLite: Check run + daily budget
alt Budget OK
ACB->>Provider: Forward request
Provider-->>ACB: Response + usage
ACB->>SQLite: Record spend
ACB-->>Agent: Return response
else Budget exhausted
ACB-->>Agent: 429 Budget exhausted
end
๐ฆ Installation
Recommended: pipx (global command)
# Windows
pipx install "<path-to-repo>"
# macOS / Linux
pipx install .
This makes agent-circuit-breaker available everywhere without activating a virtual environment.
Development: editable install
# Windows PowerShell
uv venv
uv pip install -e .
.venv\Scripts\agent-circuit-breaker --help
# macOS / Linux
uv venv
uv pip install -e .
.venv/bin/agent-circuit-breaker --help
Why the command was not found? If you installed with
uv pip install -e .but did not activate the virtual environment, Windows does not know about.venv\Scripts. Either activate the venv with.venv\Scripts\Activate.ps1or use the full path.venv\Scripts\agent-circuit-breaker.exe.
๐ Quick start
1. Start the breaker
# Windows PowerShell
agent-circuit-breaker --run-budget 2.00 --daily-budget 20.00
You will see:
agent-circuit-breaker listening on http://127.0.0.1:8989 (run budget $2.00, daily $20.00)
Press Ctrl+C to stop.
2. Point your agent at it
# Windows PowerShell
$env:OPENAI_BASE_URL = "http://localhost:8989/openai"
$env:ANTHROPIC_BASE_URL = "http://localhost:8989/anthropic"
# macOS / Linux
export OPENAI_BASE_URL=http://localhost:8989/openai
export ANTHROPIC_BASE_URL=http://localhost:8989/anthropic
3. Run your agent as normal
The SDK will send requests through the proxy. The proxy forwards them to the real provider and counts the cost.
4. Test it with curl
curl http://localhost:8989/openai/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello"}]
}'
๐ Kill switch
Instantly block all traffic, regardless of budget:
agent-circuit-breaker --kill
# Kill switch ACTIVE
agent-circuit-breaker --unkill
# Kill switch INACTIVE
This is perfect for:
- Stopping a runaway agent without finding its process.
- Emergency shutdown when you notice unexpected spend.
- CI/CD pipelines where you want to guarantee no API calls.
โ๏ธ Configuration
CLI options
agent-circuit-breaker [options]
--run-budget USD Budget for this process (default: 2.00)
--daily-budget USD Budget per calendar day (default: 20.00)
--state PATH SQLite state file
(default: ~/.agent-circuit-breaker/state.db)
--host HOST Bind host (default: 127.0.0.1)
--port PORT Bind port (default: 8989)
--prices PATH Custom prices.json
--openai-base-url URL Upstream OpenAI-compatible API
--anthropic-base-url URL Upstream Anthropic-compatible API
--kill Activate the kill switch and exit
--unkill Deactivate the kill switch and exit
Environment variables
Instead of CLI flags, you can set:
$env:ACB_OPENAI_BASE_URL = "https://api.openai.com"
$env:ACB_ANTHROPIC_BASE_URL = "https://api.anthropic.com"
Pricing file
Pricing is user-maintained. The bundled src/agent_circuit_breaker/prices.json contains a few common models:
{
"gpt-4o": {
"input_price_per_1m": 5.00,
"output_price_per_1m": 15.00
}
}
To override, copy the file, edit it, and run:
agent-circuit-breaker --prices "<path-to-prices.json>"
Get updated prices from:
๐ Accounting methodology
The proxy computes cost from token counts:
cost = input_tokens ร (input_price / 1_000_000)
+ output_tokens ร (output_price / 1_000_000)
Non-streaming requests
Reads usage from the JSON response. If usage is missing, falls back to estimating tokens from message length.
Streaming requests
Accumulates the provider's final usage SSE event:
- OpenAI: relies on
stream_options.include_usage=true. If omitted, the proxy estimates output tokens from streamed content. - Anthropic: uses
message_startandmessage_deltausage events.
When a stream's cap is hit mid-flight, the proxy closes the upstream connection, emits an SSE error chunk, and ends the stream. No silent degradation.
Estimate label
Any cost calculated from on-the-fly token counts is stored in SQLite with estimated = 1 so you can distinguish exact vs. estimated spend.
๐ Performance
The breaker adds one local HTTP hop plus, on the response path, a durable
SQLite spend-record write per request (that write is the point โ it's what
makes the cap survive a crash). To put a number on that fixed overhead, isolated
from real provider latency, benchmarks/overhead.py drives an in-process
OpenAI-compatible fake provider with N=200 non-streaming chat completions,
measured two ways after a 25-request warmup:
| Metric | Direct (us) | Through breaker (us) |
|---|---|---|
| p50 latency | 11,618 | 33,799 |
| p95 latency | 14,183 | 44,810 |
| p99 latency | 15,640 | 49,390 |
| mean latency | 11,893 | 34,336 |
Methodology: the fake provider is an aiohttp server (stdlib http.server
resets ~15% of rapid loopback connections on Windows, which would corrupt the
measurement); the breaker runs as a real subprocess with its real SQLite state
file, so the delta includes the process boundary and the per-request durable
write. Both paths use one pooled httpx client, so the comparison is
steady-state per-request cost, not connection setup. Reproduce with
python benchmarks/overhead.py.
The ~22 ms / ~190% overhead is dominated by the per-request SQLite spend write, not the proxy's byte-forwarding logic. On a real remote provider whose own latency is in the hundreds of milliseconds, that fixed local cost is negligible; it only looks large here because the fake provider answers in microseconds. The breaker does not pool upstream connections, so each proxied request also opens one extra loopback connection to the provider โ visible in the delta but immaterial against a real network upstream.
๐งช Verify the build
# Windows PowerShell
.venv\Scripts\python selfcheck.py
# Or with any Python that has the dependencies
python selfcheck.py
Expected output:
OK responses: 80
Blocked responses: 1
Kill switch ACTIVE for state: ...
Kill switch INACTIVE for state: ...
PASS
The test starts a fake provider with known usage, sends requests through the breaker until the run budget trips, and verifies both the cap and the kill switch.
๐ผ๏ธ Visual summary
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ $ agent-circuit-breaker --run-budget 2.00 โ
โ listening on http://127.0.0.1:8989 โ
โ โ
โ $ my-agent --loop โ
โ ... โ
โ [agent-circuit-breaker] run budget $2.00 exhausted โ
โ [agent] 429 Budget exhausted โ
โ Agent stopped. Total cost: $2.00 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Trust model
What the breaker guarantees, and what it deliberately does not:
- Caps are enforced before forwarding, but spend is recorded after the response. The breaker checks the budget, then forwards; the request that crosses the cap is allowed through and is charged by the upstream provider. The cap stops subsequent requests, not the one that trips it. There is no way to know a response's cost before the provider returns it.
- Cost accuracy depends on your
prices.json. Pricing is user-maintained and offline; if a model's price is missing or stale, the breaker estimates (and marksestimated = 1in SQLite) or treats it as zero. The cap is only as honest as the prices file you keep current. - Localhost-only by default; no auth. It binds
127.0.0.1and has no authentication layer. Anyone who can reach the bound interface can drive spend through it. Don't bind it beyond loopback on a shared host without putting your own auth in front. - No telemetry, no network except the upstream you configure. The only outbound calls are to the OpenAI-/Anthropic-compatible base URLs you pass. State lives in one SQLite file on disk.
โ ๏ธ Limitations
- USD only โ no currency conversion.
- Per-run + per-day budgets only โ no per-model or per-tool caps in v1.
- Caps prevent future requests โ the request that actually crosses the cap may still be charged by the upstream provider.
- Accurate streaming requires the provider to emit usage events. Otherwise the proxy estimates.
๐ค Contributing
This is intentionally small and sharp. Open issues or PRs for bugs and clear improvements. Large feature additions will be evaluated against the "single-purpose guard" philosophy.
๐ License
MIT โ see LICENSE.
Made to save wallets from runaway agents.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file localab_circuit_breaker-0.1.0.tar.gz.
File metadata
- Download URL: localab_circuit_breaker-0.1.0.tar.gz
- Upload date:
- Size: 18.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fed243c91c441562f8579911e570b76c2f73d3811b4a893369a5154844a81b9f
|
|
| MD5 |
d52b8a15698aa65d074b5675c45e6ab9
|
|
| BLAKE2b-256 |
d4c8e777f3bb08b1a7bb71176b10bed603488e3383ee321d656fb7df2860472c
|
File details
Details for the file localab_circuit_breaker-0.1.0-py3-none-any.whl.
File metadata
- Download URL: localab_circuit_breaker-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b7bf9eeef07fdf279e4ac2ff4ab1103248463c1a196c64f78be99369ca59c9f
|
|
| MD5 |
494654254e35a328283f6ce6ccd8941c
|
|
| BLAKE2b-256 |
e155f8ab4fd96b69cbbf575a8c3eb5dd249a869be41c6f576abbfd052dbc5308
|