spendcap
Real-time spend tracking, cost prediction, and runaway-loop circuit breakers for LLM API calls.
Why · Install · Quickstart · How it works · Cost prediction · API · Contributing
Your agent loop just spent $40 while you got coffee. Provider dashboards tell you after the money is gone. spendcap stops the loop before the next call, and tells you what a loop will cost before you run it at all.
- 🧮 Exact metering. Costs computed from the token counts your provider returns, not tokenizer guesses.
- 🛑 Circuit breaker. Set a hard USD cap; the call that would exceed it raises instead of hitting the API.
- 🔮 Cost prediction. Closed-form estimate of an agent loop's cost. Input grows quadratically with turns; most people underestimate this by 10x to 50x.
- 🏷️ Task scoping. Attribute spend to named tasks, with optional per-task caps.
- 📊 Reports. By model, by task, as text or JSON.
- 🪶 Zero dependencies. Works with the Anthropic and OpenAI SDKs (sync and async) and any duck-typed client whose responses carry
usage.
Why spendcap
Every existing way to watch LLM spend reports it after the fact. Enforcement, when it exists at all, lives in infrastructure you have to deploy. spendcap moves both into the process that is actually spending the money:
| spendcap | Provider dashboard | Observability platform | Gateway proxy | |
|---|---|---|---|---|
| Live spend visibility | Exact, in-process | Hours behind | Near real time | Yes |
| Stops the next call once a cap is spent | Yes, raises before the API is reached | No | No (alerts) | Some, per API key |
| Predicts a loop's cost before you run it | Yes, closed form | No | No | No |
| Per-task attribution with per-task caps | Yes | No | Tags, no caps | Per key |
| Setup | pip install, wrap your client |
None | SDK plus external service | Deploy a proxy, route traffic |
| Runtime dependencies | Zero | n/a | Vendor SDK | Infrastructure |
These tools are complementary, not competing: platforms like Langfuse or Helicone give you dashboards and traces across services, and a LiteLLM proxy gives you org-wide key management. spendcap is the last line of defense inside the process: the library import that guarantees a runaway loop stops itself.
Installation
pip install git+https://github.com/ShreyanshGoyal/spendcap.git
Python 3.9+, no runtime dependencies. A PyPI release is coming soon.
Quickstart
Wrap your client. Nothing else changes.
import anthropic
import spendcap
meter = spendcap.Meter(budget=spendcap.Budget(usd=5.00))
client = meter.wrap(anthropic.Anthropic())
# ... your agent loop, exactly as before ...
resp = client.messages.create(model="claude-haiku-4-5", max_tokens=1024,
messages=[{"role": "user", "content": "hi"}])
print(meter.spent) # 0.0023 (USD, exact)
print(meter.remaining) # 4.9977
When the cap is spent, the next call raises instead of reaching the API:
try:
while True:
resp = client.messages.create(...) # metered every call
...
except spendcap.BudgetExceededError as e:
print(e) # spendcap: meter budget exceeded (spent $5.0031 of $5.00 cap)
The call that crosses the cap still returns its response (you paid for it); the breaker refuses the one after. A warning fires once at 80% of the cap, configurable via Budget(usd=5, warn_at=0.5, on_warn=my_callback). Set Budget(hard=False) for observe-only mode.
How it works
meter.wrap() returns a transparent proxy around your client. Attribute access passes through untouched; only actual calls do metering work:
sequenceDiagram
participant Code as your code
participant Proxy as spendcap proxy
participant API as provider API
Code->>Proxy: client.messages.create(...)
Proxy->>Proxy: budget check (raises if cap spent)
Proxy->>API: call forwarded unchanged
API-->>Proxy: response with usage
Proxy->>Proxy: record exact cost on the meter
Proxy-->>Code: response, untouched
No provider SDK is imported and nothing is monkeypatched: the proxy is duck-typed against the usage shapes of the Anthropic and OpenAI SDKs (both API styles), handles sync and async clients with the same code path, and understands each provider's cache-billing semantics. The original client is always available as client.__wrapped__.
Predict a loop's cost before running it
Agent loops resend the whole conversation every turn, so input tokens grow quadratically:
est = spendcap.estimate_loop("claude-haiku-4-5", turns=200,
new_tokens_per_turn=1200,
output_tokens_per_turn=300,
system_tokens=1500)
print(est.summary())
Loop estimate: claude-haiku-4-5, 200 turns
history growth: 1,200 new + 300 output tokens/turn, 1,500 system tokens
total input: 30,390,000 tok total output: 60,000 tok
estimated cost: $30.69 (turn 1: $0.0042 -> turn 200: $0.3027, 72x growth)
with 90% prompt-cache hits: $6.07
Compare models for the same loop:
spendcap.compare_models(["claude-haiku-4-5", "gpt-5.4-mini", "gemini-3-flash"], turns=100)
# [('gpt-5.4-mini', ...), ('gemini-3-flash', ...), ...] cheapest first
Task scoping and per-task caps
with meter.task("research", cap_usd=1.50):
... # calls here are tagged 'research' and capped at $1.50
with meter.task("summarize"):
...
print(meter.report())
spendcap report: spent $2.4312 of $5.00 cap (48.6%)
calls: 41 input: 1,912,340 tok output: 96,200 tok cached: 210,000 tok
by model:
claude-haiku-4-5 38 calls $2.1201
gpt-5.4-mini 3 calls $0.3111
by task:
research 30 calls $1.4890
summarize 11 calls $0.9422
meter.report().to_json() for machines.
No wrapper? Record manually
Works with any provider and any framework. Just feed it the usage numbers:
meter.record("gpt-5.4-mini", input_tokens=1200, output_tokens=340)
meter.record("claude-haiku-4-5", input_tokens=100, cached_input_tokens=2000)
API at a glance
| API | What it does |
|---|---|
Meter(budget=Budget(usd=5.00)) |
Create a meter with a $5 hard cap |
meter.wrap(client) |
Return a metered proxy of any provider client |
meter.spent / meter.remaining |
Exact USD totals, live |
meter.task(name, cap_usd=...) |
Context manager: tag and cap a block of calls |
meter.record(model, ...) |
Meter a call manually from raw token counts |
meter.report() |
Spend breakdown by model and task (str or JSON) |
estimate_loop(model, turns, ...) |
Predict an agent loop's cost before running it |
compare_models([...], turns=...) |
Rank models by cost for the same loop |
register_model(...) / load_pricing(...) |
Override or extend the price table at runtime |
Pricing data
Built-in prices (USD per 1M tokens) for current Anthropic, OpenAI, and Google models, verified 2026-07-17 (spendcap.PRICING_AS_OF). Model IDs resolve fuzzily: anthropic/claude-haiku-4-5-20251001 resolves to claude-haiku-4-5. Unknown models warn once and book at $0, or raise with Meter(strict_pricing=True).
Prices change. Override anything at runtime, no fork needed:
spendcap.register_model("groq-llama-4-70b", input_per_m=0.59, output_per_m=0.79)
spendcap.load_pricing("my_prices.json") # {"model": {"input": .., "output": .., "cached": ..}}
Note: Claude Sonnet 5 is listed at its introductory $2/$10 rate, which runs through 2026-08-31 ($3/$15 after).
Cache accounting mirrors the providers: Anthropic cache reads bill at the cached rate and cache writes at 1.25x input (input_tokens already excludes both); OpenAI prompt_tokens includes cached tokens, so spendcap splits them out.
Demo
No API key needed. A fake client that bills like the real thing:
python examples/runaway_agent.py
The loop's cost is predicted up front ($30.69 for 200 turns), then a $1.00 hard cap stops the runaway loop at turn 55 and the report shows where the money went. The GIF at the top of this page is this demo.
Limitations (v0.1)
- Streaming responses that don't return usage aren't metered (spendcap warns once). Use
stream_options={"include_usage": True}(OpenAI) ormeter.record()manually. Full capture is #3. - Wrapping is duck-typed;
isinstancechecks against the SDK's client class won't see through the proxy (client.__wrapped__gives the original). - Budgets live in memory, per process. Persistence is #5.
Roadmap
Each item is an open issue with a design sketch; discussion welcome.
- Streaming usage capture (#3)
- A CLI for estimates and the price table (#4)
- Persistent SQLite ledger, so caps survive restarts (#5)
- Google Gemini SDK support (#2)
- Prices for more providers (#1)
- LangChain and agent-framework callbacks
Contributing
Issues and PRs are welcome. The good first issues are genuinely small and self-contained: price-table updates and new provider usage shapes.
pip install -e ".[dev]"
pytest
The only hard rule: spendcap stays zero-dependency.
License
MIT © Shreyansh Goyal
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 spendcap-0.1.0.tar.gz.
File metadata
- Download URL: spendcap-0.1.0.tar.gz
- Upload date:
- Size: 24.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4279ef919e01ae5dbe6fd14a618c1ccbbf5952803163146affa87deb8b879cf8
|
|
| MD5 |
a3fb01da9e06fb6fa5370021d9800fe9
|
|
| BLAKE2b-256 |
64b729ae04dabca90872ffc7ff50b48c9cb0f4c158a9f5357a850cb8956066c9
|
Provenance
The following attestation bundles were made for spendcap-0.1.0.tar.gz:
Publisher:
publish.yml on ShreyanshGoyal/spendcap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spendcap-0.1.0.tar.gz -
Subject digest:
4279ef919e01ae5dbe6fd14a618c1ccbbf5952803163146affa87deb8b879cf8 - Sigstore transparency entry: 2418929705
- Sigstore integration time:
-
Permalink:
ShreyanshGoyal/spendcap@3f16be0fbdb0075c857dbefe37aef4c883827380 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ShreyanshGoyal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3f16be0fbdb0075c857dbefe37aef4c883827380 -
Trigger Event:
push
-
Statement type:
File details
Details for the file spendcap-0.1.0-py3-none-any.whl.
File metadata
- Download URL: spendcap-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f976c99f1dcffd6c445cb6520fe22a739c140a4f456d0d69d0a9b2a889f1fdb4
|
|
| MD5 |
5ba66a45a7f1556f0d42b0b1088c1173
|
|
| BLAKE2b-256 |
86b086333215bff046dccf974c50049bf20cff96909c6de11d3bdfddb41605be
|
Provenance
The following attestation bundles were made for spendcap-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on ShreyanshGoyal/spendcap
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
spendcap-0.1.0-py3-none-any.whl -
Subject digest:
f976c99f1dcffd6c445cb6520fe22a739c140a4f456d0d69d0a9b2a889f1fdb4 - Sigstore transparency entry: 2418929807
- Sigstore integration time:
-
Permalink:
ShreyanshGoyal/spendcap@3f16be0fbdb0075c857dbefe37aef4c883827380 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ShreyanshGoyal
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3f16be0fbdb0075c857dbefe37aef4c883827380 -
Trigger Event:
push
-
Statement type: