llm-cost-governor
Composable pre-call and post-call hooks for LLM API calls: pricing, budgets, cost caps, rate limits, event log, observability.
Wrap your existing Anthropic / OpenAI / Voyage SDK calls with a single guarded_call(client, ...), register the hooks you need, and get:
- Priced cost per call from a shared pricing table (Sonnet, Opus, Haiku, GPT-4/5, Voyage embeddings, easy to extend).
- Session or scope budgets with pre-flight enforcement.
- Rolling-window cost caps (hourly / daily / weekly, optionally per-identity) with durable state (local disk or GCS).
- Per-IP request rate limiting as a FastAPI dependency factory.
- Structured event log (one JSON line per call) for offline analysis.
- OpenTelemetry span per call, with LangSmith metadata support and per-request content scrubbing.
- Framework-agnostic core — the FastAPI, GCS, and OTel bits are optional extras. Zero coupling to any host application.
The library was extracted from Pitchcraft and is currently consumed there in production; a second consumer (Rulebook) is scheduled to adopt it.
Adopting this in a new app? See docs/integration.md for the DI pattern, FastAPI init-order gotcha, constructor signatures, and reference implementation.
Install
# Core install
pip install "llm-cost-governor @ git+https://github.com/ecoop/llm-cost-governor@v0.3.0"
# With optional integrations
pip install "llm-cost-governor[fastapi,gcs,otel] @ git+https://github.com/ecoop/llm-cost-governor@v0.3.0"
Requires Python 3.11+. The core has just one dependency (pydantic v2); every integration is behind an optional extra so the install stays lean.
Quick example
from anthropic import Anthropic
from llm_cost_governor.wrapper import guarded_call
from llm_cost_governor.budget import ScopeBudget, ScopeBudgetHook
from llm_cost_governor.counters import CostCounter, WindowedCapHook
from llm_cost_governor.events import EventLogHook
from llm_cost_governor.state import LocalFileBackend
client = Anthropic()
# Wire up the counter at startup — one instance, shared across requests.
counter = CostCounter(
object_name="cost_counter.json",
backend=LocalFileBackend(path="./state"),
enabled=True,
hourly_cap_usd=0.50, daily_cap_usd=2.00,
weekly_cap_usd=10.00, per_token_cap_usd=1.00,
)
counter.load()
# Per-request: build a fresh scope budget, compose the hook chain.
budget = ScopeBudget(limit_usd=0.25)
hooks = [
ScopeBudgetHook(budget),
WindowedCapHook(counter),
EventLogHook(enabled=True),
]
# The one line that replaces `client.messages.create(...)`.
response, usage = guarded_call(
client,
provider="anthropic",
hooks=hooks,
tags={"stage": "drafter"},
model="claude-sonnet-5",
messages=[{"role": "user", "content": "Hello"}],
max_tokens=512,
)
print(f"Cost: ${usage.cost_usd:.4f} Tokens: {usage.input_tokens} in / {usage.output_tokens} out")
That's it. Every hook's pre runs before the SDK call (aborts on BudgetExceeded / CostCapExceeded / RateLimitExceeded); every post runs after with the priced UsageRecord and updates the shared state.
Core concepts
The Hook chain
guarded_call(client, ..., hooks=[...]) runs each hook's pre(ctx) method before the SDK call and each post(ctx, usage) after. A hook is any object with those two methods and a name attribute — implement your own by satisfying the Hook Protocol. The shipped hooks:
| Hook | pre | post |
|---|---|---|
ScopeBudgetHook |
raise BudgetExceeded if the pre-flight estimate would push over |
record the actual cost against the budget |
WindowedCapHook |
raise CostCapExceeded if a rolling window is already at cap |
record cost + trigger alerts on cap crossings |
EventLogHook |
no-op | emit one structured JSON line to stdout |
OTelSpanHook (optional) |
open a span with gen_ai.request.* attrs |
close it with gen_ai.usage.* + cost attrs |
LangSmithMetadataHook (optional) |
stamp langsmith.metadata.* from a caller-supplied identity dict |
no-op |
Providers
guarded_call(provider="anthropic", ...) selects the adapter that knows how to invoke the SDK and normalize the response. The Anthropic adapter ships in-box; OpenAI and Voyage adapters slot in as new modules with a couple lines each. See providers/anthropic.py for the shape.
State backends
Counters can persist their rolling-window state through the StateBackend Protocol. Two implementations ship:
LocalFileBackend(path)— atomic JSON writes to a filesystem path. Default for local dev / CI.GcsBackend(bucket)— Google Cloud Storage blob. Lazily importsgoogle-cloud-storageon first use, so the core install stays dep-free.
Add your own by implementing read(name) -> str | None and write(name, text) -> None.
record_usage — for calls you made yourself
Voyage embeddings, batch APIs, vision — anything that doesn't fit the guarded_call shape. record_usage() runs only the post hooks, still gives you priced cost and event log, without wrapping the call:
from llm_cost_governor.wrapper import record_usage
response = voyage_client.embed(texts=[...], model="voyage-3.5")
record_usage(
provider="voyage", model="voyage-3.5",
input_tokens=response.total_tokens, output_tokens=0,
hooks=hooks, tags={"call_type": "embedding"},
)
What's in / what's out
Included:
- Pricing for currently-shipped Claude models — Fable 5, Opus 5, Sonnet 5, Opus 4.6/4.7/4.8, Sonnet 4.6, Haiku 4.5. Easy to extend for new models as they ship.
- Rolling-window counter with configurable caps + durable persistence.
RollingWeekCounter— a generic per-key, rolling-week cumulative counter with cap enforcement (strict or lenient) across one or more named dimensions; the reusable core behind app-specific caps like per-token/per-IP upload limits. Import fromllm_cost_governor.counters.- Per-IP rate limiter (framework-neutral core + FastAPI dependency factory).
- Structured event log (stdout → any log aggregator).
- Provider adapter for Anthropic.
- OTel span hooks + a request-span context manager + LangSmith metadata.
- Content-scrubbing OTel exporter for per-request telemetry control.
- Discord-webhook alert sink (implements the
AlertSinkProtocol).
Not (yet) included:
- Provider adapters for OpenAI, Voyage, Gemini — the shape is fixed and each is ~30 lines, but they're not in the box until someone needs them.
- Streaming responses. The wrapper is synchronous today; adding async is straightforward but not implemented yet.
- Multi-instance atomic counters — the rolling-window counter is correct at
max-instances=1. Distributed correctness (e.g., Redis-backed) is a future extension. - Retry / circuit-breaker logic. The library never retries — that's the caller's responsibility.
Development
git clone https://github.com/ecoop/llm-cost-governor
cd llm-cost-governor
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
ruff check src tests
CI runs on Python 3.11, 3.12, 3.13 via GitHub Actions.
Versioning
Currently v0.3.1. The 0.3.x line renamed the package from llm-guardrails to llm-cost-governor. Semver from v1.0.0 onward; anything before is "shipped but pre-stable API — expect breaking changes."
Contributing
Issues and pull requests welcome. For substantive changes, open an issue first to discuss the shape before writing code. The Hook Protocol and provider-adapter surface are the two most important extension points — happy to talk through how to add a new provider or hook.
License
MIT. See LICENSE.
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 llm_cost_governor-0.3.2.tar.gz.
File metadata
- Download URL: llm_cost_governor-0.3.2.tar.gz
- Upload date:
- Size: 60.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c4f88f5633aa1df740c3766fb9487fa8f32404716c5ead36392349a1d1f804b
|
|
| MD5 |
90e32a66c1169346c943a734438924d6
|
|
| BLAKE2b-256 |
de3b69ebad1481e0a393fb1adcc8fa73e7fea5614fedfe9caed6330b7fc7b61c
|
Provenance
The following attestation bundles were made for llm_cost_governor-0.3.2.tar.gz:
Publisher:
release.yml on ecoop/llm-cost-governor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
llm_cost_governor-0.3.2.tar.gz -
Subject digest:
6c4f88f5633aa1df740c3766fb9487fa8f32404716c5ead36392349a1d1f804b - Sigstore transparency entry: 2342061635
- Sigstore integration time:
-
Permalink:
ecoop/llm-cost-governor@fa7a75a14c9286a998b98724b44eb5d0c0f726b1 -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/ecoop
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fa7a75a14c9286a998b98724b44eb5d0c0f726b1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file llm_cost_governor-0.3.2-py3-none-any.whl.
File metadata
- Download URL: llm_cost_governor-0.3.2-py3-none-any.whl
- Upload date:
- Size: 52.8 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 |
a23248f5f6767d92594402485b35770a8fdc081ca69ed0bc28a3c45a87317dc4
|
|
| MD5 |
178926119a5573c7b6b567bd8f8a2f93
|
|
| BLAKE2b-256 |
e83147cf2171856b0c82da2f0224a48b23113a7cf1d92c2ffa47d031e31a200c
|
Provenance
The following attestation bundles were made for llm_cost_governor-0.3.2-py3-none-any.whl:
Publisher:
release.yml on ecoop/llm-cost-governor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
llm_cost_governor-0.3.2-py3-none-any.whl -
Subject digest:
a23248f5f6767d92594402485b35770a8fdc081ca69ed0bc28a3c45a87317dc4 - Sigstore transparency entry: 2342061641
- Sigstore integration time:
-
Permalink:
ecoop/llm-cost-governor@fa7a75a14c9286a998b98724b44eb5d0c0f726b1 -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/ecoop
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fa7a75a14c9286a998b98724b44eb5d0c0f726b1 -
Trigger Event:
push
-
Statement type: