Instrument LLM clients and emit usage events to Lago.
Project description
lago-agent-sdk
Instrument LLM clients and emit usage events to Lago for billing.
┌──────────────┐
your code ──────► │ wrapped client│ ──► provider (Bedrock / Mistral / …)
└──────┬───────┘
│ (extract usage)
▼
┌──────────────┐
│ Lago events │ ──► api.getlago.com
└──────────────┘
What it does
- Wraps your existing LLM client in place — no API surface change for your application code.
- Extracts usage from each response into a normalized shape (
CanonicalUsage). - Buffers events in memory, flushes them in batches to Lago's
/events/batchendpoint. - Survives provider/Lago outages with exponential backoff and a bounded buffer.
- p99 wrap-overhead under 5 ms — your call is never blocked on Lago.
Install
pip install lago-agent-sdk
For Bedrock support: pip install 'lago-agent-sdk[bedrock]' (adds boto3).
For Mistral support: pip install 'lago-agent-sdk[mistral]' (adds mistralai).
For Anthropic native support: pip install 'lago-agent-sdk[anthropic]' (adds anthropic).
For OpenAI native support: pip install 'lago-agent-sdk[openai]' (adds openai).
For Gemini native support: pip install 'lago-agent-sdk[gemini]' (adds google-genai).
Quickstart — Bedrock
import boto3
from lago_agent_sdk import LagoSDK
sdk = LagoSDK(
api_key="<YOUR_LAGO_API_KEY>",
api_url="https://api.getlago.com/api/v1/",
default_subscription_id="sub_acme",
)
client = sdk.wrap(boto3.client("bedrock-runtime", region_name="eu-west-1"))
resp = client.converse(
modelId="eu.amazon.nova-lite-v1:0",
messages=[{"role": "user", "content": [{"text": "Hello"}]}],
)
sdk.flush()
The wrapped client behaves identically to the original — same arguments, same return shape, same exceptions. The SDK adds an in-memory queue that batches events to Lago in the background.
Quickstart — Anthropic
from anthropic import Anthropic
from lago_agent_sdk import LagoSDK
sdk = LagoSDK(api_key="...", default_subscription_id="sub_acme")
client = sdk.wrap(Anthropic(api_key="..."))
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=200,
messages=[{"role": "user", "content": "Hello"}],
)
sdk.flush()
Works with Anthropic and AsyncAnthropic. Both messages.create(..., stream=True) and the messages.stream(...) context manager are instrumented — usage is captured from the final message_delta event in either case.
Quickstart — Mistral
from mistralai.client import Mistral
from lago_agent_sdk import LagoSDK
sdk = LagoSDK(api_key="...", default_subscription_id="sub_acme")
client = sdk.wrap(Mistral(api_key="..."))
resp = client.chat.complete(
model="mistral-small-latest",
messages=[{"role": "user", "content": "Hello"}],
)
sdk.flush()
Quickstart — OpenAI
from openai import OpenAI
from lago_agent_sdk import LagoSDK
sdk = LagoSDK(api_key="...", default_subscription_id="sub_acme")
client = sdk.wrap(OpenAI(api_key="..."))
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Hello"}],
max_completion_tokens=200,
)
sdk.flush()
Works with OpenAI and AsyncOpenAI. Covers both Chat Completions (client.chat.completions.create) and the newer Responses API (client.responses.create), sync + streaming. For streaming, the wrapper auto-injects stream_options={"include_usage": True} so the final chunk carries usage data — without it OpenAI emits no usage on streamed responses.
Reasoning tokens (llm_reasoning_tokens) populate automatically when you call an o-series model (o4-mini, o1, etc.) — OpenAI is the first provider to expose this metric separately.
Quickstart — Gemini
from google import genai
from lago_agent_sdk import LagoSDK
sdk = LagoSDK(api_key="...", default_subscription_id="sub_acme")
client = sdk.wrap(genai.Client(api_key="..."))
resp = client.models.generate_content(
model="gemini-2.5-flash",
contents="Hello",
)
sdk.flush()
Wraps the modern google-genai SDK (from google import genai). Covers client.models.generate_content + generate_content_stream, sync + async (via client.aio.models).
Reasoning tokens populate automatically on Gemini 2.5 — the model reasons internally by default and surfaces thoughts_token_count. Note the semantic difference vs OpenAI:
- OpenAI:
reasoning_tokensis a subset ofcompletion_tokens(already counted in output) - Gemini:
thoughts_token_countis additive tocandidates_token_count(total Google bill = output + reasoning)
Multi-tenant — pick a subscription per call
Three ways to set the external_subscription_id, in priority order:
# 1. Per-call override (highest precedence)
client.converse(..., extra_lago={"subscription": "sub_acme", "dimensions": {"feature": "summarize"}})
# 2. Context-bound (use in middleware to set once per request)
sdk.set_subscription("sub_acme")
# all calls in this thread/asyncio task → sub_acme
# 3. Default at init (fallback)
sdk = LagoSDK(api_key="...", default_subscription_id="sub_default")
Backed by contextvars for safe propagation across asyncio tasks.
Supported providers
| Provider | Access | Status |
|---|---|---|
| AWS Bedrock | Converse (sync + stream) |
✓ |
| AWS Bedrock | InvokeModel (sync + stream), 7 model families |
✓ |
| Anthropic | native SDK (messages.create + messages.stream, sync + async) |
✓ |
| Mistral | native SDK (chat.complete + chat.stream) |
✓ |
| OpenAI | native SDK (chat.completions.create + responses.create, sync + async + stream) |
✓ |
| Google Gemini | native SDK (google-genai: models.generate_content + generate_content_stream, sync + async) |
✓ |
| LiteLLM | callback bridge | Phase 4 |
Token dimensions captured
CanonicalUsage carries 11 numeric fields. Which ones populate depends on the provider:
| Field | Lago metric code | Bedrock | Anthropic | Mistral | OpenAI | Gemini |
|---|---|---|---|---|---|---|
| input | llm_input_tokens |
✓ | ✓ | ✓ | ✓ | ✓ |
| output | llm_output_tokens |
✓ | ✓ | ✓ | ✓ | ✓ |
| cache_read | llm_cached_input_tokens |
✓ (Anthropic) | ✓ | ✓ (when cache hits) | ✓ (auto-cache) | ✓ (CachedContent API) |
| cache_write | llm_cache_creation_tokens |
✓ (Anthropic) | ✓ | ✗ | ✗ | ✗ |
| cache_write_5m / 1h | llm_cache_write_5m/1h_tokens |
✓ (Anthropic InvokeModel) | ✓ | ✗ | ✗ | ✗ |
| reasoning | llm_reasoning_tokens |
✗ (folded into output) | ✗ (folded into output, even with extended thinking) | ✗ (folded into output) | ✓ (o-series, subset) | ✓ (Gemini 2.5, additive) |
| tool_calls | llm_tool_calls |
✓ | ✓ | ✓ | ✓ | ✓ |
| audio_input | llm_audio_input_tokens |
✗ | ✗ | ✗ | ✓ (GPT-4o-audio) | ✓ (multimodal AUDIO) |
| audio_output | llm_audio_output_tokens |
✗ | ✗ | ✗ | ✓ (GPT-4o-audio) | ✓ (multimodal AUDIO) |
| image_input | llm_image_input_tokens |
✗ | ✗ | ✗ | ✗ (Phase 3) | ✓ (multimodal IMAGE) |
Semantic note on reasoning:
- OpenAI's
reasoning_tokensis a SUBSET ofoutput— already counted incompletion_tokens. - Gemini's
thoughts_token_countis ADDITIVE tooutput—candidates + thoughts = total billable output.
Semantic note on input breakdowns (avoid double-counting):
For both OpenAI and Gemini, cache_read, audio_input, and image_input are subsets of input, not additive to it — they are a breakdown of tokens already counted in llm_input_tokens. For example, OpenAI reports cached_tokens under prompt_tokens_details within prompt_tokens, and Gemini's docs state prompt_token_count "includes the number of tokens in the cached content". A billable metric that sums llm_input_tokens + llm_cached_input_tokens (or + llm_audio_input_tokens, + llm_image_input_tokens) will double-count. Bill on llm_input_tokens as the total; use the breakdown fields only for cost attribution or discounted-rate tiers (e.g. cached input billed at a lower rate), subtracting them from input rather than adding.
OpenAI's Predicted Outputs tokens (accepted_prediction_tokens, rejected_prediction_tokens) are not surfaced — see the OpenAI adapter docstring for details on this intentional gap.
Pricing mode — send dollar cost instead of tokens
By default the SDK emits token counts (pricing_mode="tokens"). You can instead have it
compute and emit the dollar cost of each call: Σ(unit_price_per_token × tokens) × markup.
from lago_agent_sdk import LagoSDK, LagoConfig
sdk = LagoSDK(api_key="...", config=LagoConfig(
api_key="...",
default_subscription_id="sub_123",
pricing_mode="price", # "tokens" (default) | "price"
markup=1.2, # optional cost multiplier (1.2 = +20%)
))
client = sdk.wrap(anthropic_client)
# ... use the client normally ...
In price mode the SDK emits one event per call with code llm_cost. The event carries a
top-level precise_total_amount_cents (the total cost in cents, after markup) for Lago's
dynamic charge model, plus a breakdown in properties: unit (total tokens), value (USD
total), base_cost (pre-markup), markup, price_source, and per-field *_tokens /
*_unit_price / *_cost. Set up in Lago a sum-aggregation billable metric llm_cost on
field_name: "unit" and a dynamic charge on it — Lago sums each event's
precise_total_amount_cents into a single fee (unit is the displayed usage quantity). See
testing/lago_setup_pricing_plan.py for a script that creates this.
Per-call override via extra_lago (mode and markup, in addition to subscription/dimensions):
client.messages.create(model="claude-...", messages=[...],
extra_lago={"mode": "price", "markup": 1.5})
Live, public pricing sources (no API keys):
- OpenRouter (
/api/v1/models) for nativeanthropic/openai/mistral/geminiclients — USD per token. - AWS Bedrock Price List Bulk API (public) for Bedrock — parsed per region.
Prices are fetched and cached in the background (TTL pricing_ttl_seconds, default 1h); the
refresh runs on the SDK's background thread, so your LLM call is never blocked on pricing.
Fallback (never under-bill): if a price is unavailable (table not warm on the first call,
or the model isn't found in the source), the SDK falls back to emitting token-count events
and calls on_error so it's visible — it never silently drops the usage.
Bedrock note: AWS's public bulk data lists many models (Titan, Llama, Mistral, Cohere, and older Claude) but, at time of writing, not the current Claude 3.5/3.7/4 models. Bedrock calls for models absent from AWS's data fall back to token events. Native Anthropic clients are priced via OpenRouter and unaffected.
Error policy
The SDK never breaks your LLM call. If anything in instrumentation fails (adapter bug, Lago down, network error), the SDK swallows it, logs a warning, and your call returns normally.
Subscription resolution returns nothing → drop with ERROR log
Configurable via LagoConfig.on_error callback to integrate with Sentry, Datadog, etc.:
from lago_agent_sdk import LagoConfig, LagoSDK
def on_error(exc: Exception, where: str) -> None:
sentry.capture_exception(exc, tags={"sdk_phase": where})
sdk = LagoSDK(
api_key="...",
config=LagoConfig(api_key="...", on_error=on_error),
)
Setting up Lago
The SDK ships with default metric codes (llm_input_tokens, llm_output_tokens, etc.). You need to register matching billable metrics in your Lago tenant before events count toward charges. See Lago docs — Billable Metrics.
Development
git clone https://github.com/getlago/lago-agent-sdk-python
cd lago-agent-sdk-python
python -m venv venv && source venv/bin/activate
pip install -e '.[dev]'
pytest
Run live integration tests (requires real credentials):
AWS_BEARER_TOKEN_BEDROCK="..." \
MISTRAL_API_KEY="..." \
LAGO_API_URL="https://api.getlago.com/api/v1/" \
LAGO_API_KEY="..." \
LAGO_EXTERNAL_SUBSCRIPTION_ID="sub_..." \
pytest tests/integration
Security
Found a vulnerability? See SECURITY.md.
License
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 lago_agent_sdk-0.2.0.tar.gz.
File metadata
- Download URL: lago_agent_sdk-0.2.0.tar.gz
- Upload date:
- Size: 40.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e9441e0fa316dd398b0c5b78d3013c0c5f3ca1b1be01d0bade05e93afbd1739e
|
|
| MD5 |
5173a9c0723ae8ef5205d54273a3cabe
|
|
| BLAKE2b-256 |
d18b1551f9c1dfdffd29a3e01899efb14f61d551f2d02dcdbbde3478c2a0d0b7
|
Provenance
The following attestation bundles were made for lago_agent_sdk-0.2.0.tar.gz:
Publisher:
publish.yml on getlago/lago-agent-sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lago_agent_sdk-0.2.0.tar.gz -
Subject digest:
e9441e0fa316dd398b0c5b78d3013c0c5f3ca1b1be01d0bade05e93afbd1739e - Sigstore transparency entry: 1826532558
- Sigstore integration time:
-
Permalink:
getlago/lago-agent-sdk-python@4c0420a68707245bc8d540756ae5bce459c37a3c -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/getlago
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4c0420a68707245bc8d540756ae5bce459c37a3c -
Trigger Event:
push
-
Statement type:
File details
Details for the file lago_agent_sdk-0.2.0-py3-none-any.whl.
File metadata
- Download URL: lago_agent_sdk-0.2.0-py3-none-any.whl
- Upload date:
- Size: 47.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
568fffb7b075ba4d3e251d3fdebd578df8a29e9d202f7d8cea42941c03ce0c25
|
|
| MD5 |
9b7ed21a2320ed5ebfb7284e3284dae6
|
|
| BLAKE2b-256 |
6f60825b7626265b6b30e5741af5f55760bfcdbd20e66c26766a426b09546829
|
Provenance
The following attestation bundles were made for lago_agent_sdk-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on getlago/lago-agent-sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
lago_agent_sdk-0.2.0-py3-none-any.whl -
Subject digest:
568fffb7b075ba4d3e251d3fdebd578df8a29e9d202f7d8cea42941c03ce0c25 - Sigstore transparency entry: 1826532959
- Sigstore integration time:
-
Permalink:
getlago/lago-agent-sdk-python@4c0420a68707245bc8d540756ae5bce459c37a3c -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/getlago
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@4c0420a68707245bc8d540756ae5bce459c37a3c -
Trigger Event:
push
-
Statement type: