Drop-in spend governor for Python LLM workloads — cost attribution, budget enforcement, and OpenTelemetry signals via a single httpx transport.
Project description
TokenWarden
Drop-in spend governor for Python LLM workloads.
Real-time cost attribution, budget enforcement, and OpenTelemetry signals — via a single httpx transport.
What it does
Every HTTP call your application makes to an LLM provider is intercepted by TokenWarden's httpx transport. For each request it:
- Detects the provider (OpenAI, Anthropic, Gemini, xAI, DeepSeek)
- Resolves attribution context (tenant, user, feature) from Python context variables
- Checks current budget spend — returns a
429response if an Enforce budget is exceeded - Forwards the request
- Parses token usage from the response (including streaming SSE responses)
- Calculates cost using a live price catalog
- Increments budget counters
- Emits OpenTelemetry metrics
Because it works at the httpx transport layer, it is compatible with the official OpenAI, Anthropic, and any other SDK that accepts a custom http_client.
Installation
pip install tokenwarden
With optional extras:
pip install tokenwarden[redis] # Redis-backed distributed budget store
pip install tokenwarden[otel] # OpenTelemetry signals
pip install tokenwarden[all] # Everything
Quick start
import httpx
from openai import OpenAI
from tokenwarden import TokenWardenTransport
from tokenwarden.budget._store import BudgetConfig, BudgetMode
transport = TokenWardenTransport(
budgets=[
BudgetConfig(
key="global-daily",
limit=50.00, # $50/day
window_seconds=86_400,
mode=BudgetMode.ENFORCE,
)
]
)
client = OpenAI(http_client=httpx.Client(transport=transport))
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)
When the daily budget is exhausted, the next request receives a 429 before it ever leaves your process — no API call is made and no cost is incurred.
Provider examples
OpenAI
import httpx
from openai import OpenAI
from tokenwarden import TokenWardenTransport
transport = TokenWardenTransport()
client = OpenAI(http_client=httpx.Client(transport=transport))
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Explain quantum computing simply."}],
)
print(response.choices[0].message.content)
Anthropic
import anthropic
import httpx
from tokenwarden import TokenWardenTransport
transport = TokenWardenTransport()
client = anthropic.Anthropic(http_client=httpx.Client(transport=transport))
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Write a haiku about the ocean."}],
)
print(message.content[0].text)
Google Gemini
import os
import httpx
from tokenwarden import TokenWardenTransport
transport = TokenWardenTransport()
api_key = os.environ["GEMINI_API_KEY"]
with httpx.Client(transport=transport) as client:
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={api_key}"
response = client.post(url, json={
"contents": [{"parts": [{"text": "What is the capital of France?"}]}]
})
data = response.json()
print(data["candidates"][0]["content"]["parts"][0]["text"])
xAI (Grok)
import os
import httpx
from openai import OpenAI
from tokenwarden import TokenWardenTransport
transport = TokenWardenTransport()
client = OpenAI(
api_key=os.environ["XAI_API_KEY"],
base_url="https://api.x.ai/v1",
http_client=httpx.Client(transport=transport),
)
response = client.chat.completions.create(
model="grok-3",
messages=[{"role": "user", "content": "What is the meaning of life?"}],
)
print(response.choices[0].message.content)
DeepSeek
import os
import httpx
from openai import OpenAI
from tokenwarden import TokenWardenTransport
transport = TokenWardenTransport()
client = OpenAI(
api_key=os.environ["DEEPSEEK_API_KEY"],
base_url="https://api.deepseek.com/v1",
http_client=httpx.Client(transport=transport),
)
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "Summarise the theory of relativity."}],
)
print(response.choices[0].message.content)
Attribution
Attribution tells TokenWarden who made a call so budget limits can be applied per tenant, per user, or per feature. Set it using context variables — safe across threads and async tasks.
from tokenwarden import set_attribution, clear_attribution
set_attribution(
tenant_id="acme-corp",
user_id="user-42",
feature="report-generation",
)
# LLM calls made here are attributed automatically
clear_attribution() # optional cleanup
In a FastAPI middleware
from fastapi import Request
from tokenwarden import set_attribution
@app.middleware("http")
async def attach_attribution(request: Request, call_next):
set_attribution(
tenant_id=request.headers.get("X-Tenant-Id"),
user_id=request.headers.get("X-User-Id"),
)
return await call_next(request)
In a Django view
from tokenwarden import set_attribution
def my_view(request):
set_attribution(
tenant_id=request.user.profile.tenant_id,
user_id=str(request.user.id),
)
# ... call LLM
Budget configuration
Budget modes
| Mode | Behaviour |
|---|---|
BudgetMode.ENFORCE |
Returns 429 application/problem+json before the request is sent |
BudgetMode.WARN |
Allows the request but emits an OTel breach counter |
Budget dimensions
| Dimension | Tracks spend per |
|---|---|
"global" (default) |
All requests combined |
"tenant" |
tenant_id set via set_attribution() |
"user" |
user_id set via set_attribution() |
"feature" |
feature set via set_attribution() |
Examples
from tokenwarden.budget._store import BudgetConfig, BudgetMode
# $50/day global — hard block
BudgetConfig(key="global-daily", limit=50.00, window_seconds=86_400)
# $1,000/month per tenant — hard block
BudgetConfig(key="tenant-monthly", limit=1_000.00, window_seconds=2_592_000,
mode=BudgetMode.ENFORCE, dimension="tenant")
# $5/day per user — warn only
BudgetConfig(key="user-daily", limit=5.00, window_seconds=86_400,
mode=BudgetMode.WARN, dimension="user")
Multiple budgets applied at once:
from tokenwarden import TokenWardenTransport, set_attribution
from tokenwarden.budget._store import BudgetConfig, BudgetMode
import httpx
from openai import OpenAI
transport = TokenWardenTransport(
budgets=[
BudgetConfig(key="tenant-monthly", limit=1_000.00,
window_seconds=2_592_000, dimension="tenant"),
BudgetConfig(key="user-daily", limit=5.00,
window_seconds=86_400, dimension="user",
mode=BudgetMode.WARN),
]
)
set_attribution(tenant_id="acme-corp", user_id="user-42")
client = OpenAI(http_client=httpx.Client(transport=transport))
Async usage
Use AsyncTokenWardenTransport with httpx.AsyncClient:
import asyncio
import httpx
from openai import AsyncOpenAI
from tokenwarden import AsyncTokenWardenTransport
from tokenwarden.budget._store import BudgetConfig
transport = AsyncTokenWardenTransport(
budgets=[BudgetConfig(key="global-daily", limit=50.00, window_seconds=86_400)]
)
async def main():
client = AsyncOpenAI(http_client=httpx.AsyncClient(transport=transport))
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)
asyncio.run(main())
Redis budget store
For multi-instance deployments where budget state must be shared across processes:
pip install tokenwarden[redis]
from tokenwarden import TokenWardenTransport, RedisBudgetStore
from tokenwarden.budget._store import BudgetConfig
import httpx
from openai import OpenAI
store = RedisBudgetStore("redis://localhost:6379")
transport = TokenWardenTransport(
store=store,
budgets=[
BudgetConfig(key="tenant-monthly", limit=1_000.00,
window_seconds=2_592_000, dimension="tenant")
]
)
client = OpenAI(http_client=httpx.Client(transport=transport))
Uses an atomic Lua script (INCRBYFLOAT + EXPIRE) — concurrent requests across multiple instances are safe.
Live price catalog
TokenWarden ships with an embedded price catalog. To keep prices current without reinstalling, start a background refresh thread:
from tokenwarden import TokenWardenTransport, CatalogRefreshThread
from tokenwarden.catalog._catalog import PriceCatalog
import httpx
from openai import OpenAI
catalog = PriceCatalog()
refresher = CatalogRefreshThread(catalog, interval_seconds=21_600) # every 6 hours
refresher.start() # daemon thread — exits with the process
transport = TokenWardenTransport()
client = OpenAI(http_client=httpx.Client(transport=transport))
On any fetch failure the last-known-good catalog is retained.
OpenTelemetry
pip install tokenwarden[otel]
Wire up your exporter as normal — TokenWarden emits automatically:
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
metrics.set_meter_provider(MeterProvider(
metric_readers=[PeriodicExportingMetricReader(ConsoleMetricExporter())]
))
from tokenwarden import TokenWardenTransport
transport = TokenWardenTransport() # signals flow automatically
Signals emitted
| Signal | Type | Tags |
|---|---|---|
tokenwarden.tokens_consumed |
Counter | gen_ai.system, gen_ai.request.model |
tokenwarden.cost_usd |
Counter | gen_ai.system, gen_ai.request.model |
tokenwarden.request_latency_ms |
Histogram | gen_ai.system, gen_ai.request.model |
tokenwarden.budget_breaches |
Counter | tokenwarden.budget_key, tokenwarden.budget_mode |
Attribution tags (tokenwarden.tenant_id, tokenwarden.user_id, tokenwarden.feature) are added when set via set_attribution().
Handling budget exceeded responses
When an Enforce budget is breached, TokenWarden returns a 429 with a JSON body:
{
"type": "https://tokenwarden.dev/errors/budget-exceeded",
"title": "Budget Exceeded",
"status": 429,
"detail": "Budget 'tenant-monthly' has been exceeded."
}
The OpenAI and Anthropic SDKs raise RateLimitError on 429, so catch it normally:
import openai
try:
response = client.chat.completions.create(...)
except openai.RateLimitError as e:
print("Budget exhausted — try again later")
Supported providers
| Provider | Host | Notes |
|---|---|---|
| OpenAI | api.openai.com |
Streaming: final chunk usage |
| Anthropic | api.anthropic.com |
Streaming: message_start + message_delta |
| Google Gemini | generativelanguage.googleapis.com |
usageMetadata in response |
| xAI (Grok) | api.x.ai |
OpenAI-compatible |
| DeepSeek | api.deepseek.com |
OpenAI-compatible |
License
MIT
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 tokenwarden-0.1.0.tar.gz.
File metadata
- Download URL: tokenwarden-0.1.0.tar.gz
- Upload date:
- Size: 14.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e0deeb76709c66dcacdc4495f3de861ec2edb0a1809b3643a11484aab6f8b493
|
|
| MD5 |
e073ff2e5334e4f1bb0b154143d9152a
|
|
| BLAKE2b-256 |
0720a76ff55d36cbab9e13f18a7e5082a51f1e5a464644e24cfd0fd68584c38e
|
File details
Details for the file tokenwarden-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tokenwarden-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
41ae6faf6ffe717e89e0ecf5df55fec22aec1f82d0255cd6c356d5689c4d3aad
|
|
| MD5 |
8c3400f6648b94cc38992ddd7a2f6110
|
|
| BLAKE2b-256 |
8c15b39f7aa3d3339ee82b5e15cee5dd87627d084d7af1d03a59f4d589b10e59
|