Instrumentation for Cost-to-Code attribution.
Project description
Frugal SDK for Python
Instrument AI API calls with one line of code. Get per-call-site metrics — token usage, latency, call counts — exported via OpenTelemetry to any collector.
Install
pip install frugal-sdk-python
# Plus whichever provider SDKs you use:
pip install openai anthropic
Quick start
from frugal_metrics import wrap_openai, wrap_anthropic, wrap_bedrock
from openai import OpenAI
from anthropic import Anthropic
import boto3
client = wrap_openai(OpenAI())
anthropic_client = wrap_anthropic(Anthropic())
bedrock_client = wrap_bedrock(boto3.client("bedrock-runtime", region_name="us-west-2"))
# Every call now emits metrics — no other code changes needed
response = client.chat.completions.create(model="gpt-4o", messages=[...])
Configuration
All configuration is via environment variables. No init() call, no configuration objects.
Required
Set both or the wrapper silently returns the client unmodified:
| Variable | Purpose |
|---|---|
FRUGAL_API_KEY |
Per-customer bearer token issued by Frugal. Builds the export Authorization header. (Or supply the bearer yourself via FRUGAL_OTEL_EXPORTER_OTLP_HEADERS.) |
FRUGAL_PROJECT_ID |
The Frugal project this deployment belongs to. Stats are attributed per project, so the same repo across microservices no longer collides. |
Optional
| Variable | Purpose |
|---|---|
FRUGAL_CUSTOMER_ID |
Customer ID issued by Frugal. When unset, the tenant label (auto-injected server-side by VMAuth from the bearer token) identifies the customer and frugal.customer_id is omitted. |
FRUGAL_REPO_ROOT |
Absolute path to the source root; the strip prefix for caller_file. Defaults to the process working directory. |
FRUGAL_OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP collector URL for Frugal metrics. Defaults to https://metrics.frugal.co/opentelemetry/v1/metrics. Falls back to OTEL_EXPORTER_OTLP_ENDPOINT. |
FRUGAL_OTEL_EXPORTER_OTLP_HEADERS |
Export auth headers (e.g. Authorization=Bearer ...). Overrides the header built from FRUGAL_API_KEY. Falls back to OTEL_EXPORTER_OTLP_HEADERS. |
FRUGAL_COMPONENTS |
Comma-separated logical components (e.g. ai-handler,reco). When set, each metric is ALSO emitted once per component (tagged frugal.component) on top of the base project series, so usage can be viewed for the whole project or split by component. |
FRUGAL_PATH_BLOCK_LIST |
Comma-separated repo-relative paths to skip for caller attribution (see below). |
FRUGAL_METRIC_EXPORT_INTERVAL_MS |
Export cadence in milliseconds. Default 60000. |
FRUGAL_MAX_CALL_SITES |
Cardinality cap — max distinct (caller_file, caller_function) pairs to attribute. Default 500. Set to 0 to disable. |
FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE |
Fraction of calls (0..1) that run the lightweight prompt analyzer. Default 0.1. Set to 0 to disable, 1 for every call. |
FRUGAL_DISABLED |
Set to 1 to disable all instrumentation without uninstalling. |
What gets captured
Every wrapped call emits OTel metrics tagged with the caller's source file and function, the provider, and the model:
| Metric | Kind | Description | Additional attributes |
|---|---|---|---|
gen_ai.client.operation.duration |
histogram | Call latency in seconds | — |
gen_ai.client.token.usage |
histogram | Input and output token counts | gen_ai.token.type |
frugal.gen_ai.calls |
counter | Number of calls per site | — |
frugal.gen_ai.cache_read_tokens |
histogram | Prompt cache hits | — |
frugal.gen_ai.cache_write_tokens |
histogram | Prompt cache writes | — |
frugal.gen_ai.reasoning_tokens |
histogram | OpenAI reasoning tokens (o-series), Anthropic thinking tokens | — |
frugal.gen_ai.errors |
counter | Errors by exception type | frugal.error.type |
frugal.gen_ai.internal_errors |
counter | frugal-metrics internal failures (cardinality cap, bugs) | frugal.internal_error.stage |
frugal.gen_ai.output_cap_utilization |
histogram | output_tokens / max_tokens ratio when max_tokens is set |
frugal.structured_output_mode |
frugal.gen_ai.history_depth |
histogram | messages.length (analyzer-sampled) |
— |
frugal.gen_ai.few_shot_count |
histogram | Few-shot exemplar pairs before final user (analyzer-sampled) | — |
frugal.gen_ai.noise_ratio |
histogram | HTML/base64/whitespace fraction of input (analyzer-sampled) | — |
frugal.gen_ai.cache_prefix_stable |
counter | Per-block prompt-cache stability (analyzer-sampled) | frugal.block, frugal.stable |
frugal.gen_ai.structured_output_hint |
counter | "Return JSON" prompts missing response_format/tools |
— |
The bottom block of analyzer-sampled metrics fires on a fraction of calls
(FRUGAL_PROMPT_ANALYSIS_SAMPLE_RATE, default 10%); the token-side metrics are
100% — never sampled.
Every metric carries this base attribute set:
frugal.project_id— from your env vars (frugal.customer_idtoo when set);frugal.componentis added on the per-component series whenFRUGAL_COMPONENTSis setfrugal.caller_file,frugal.caller_function— auto-detected from the call stackgen_ai.system—openai,anthropic,aws.bedrock, orvertex_aigen_ai.request.model,gen_ai.response.model— the model usedgen_ai.operation.name—chat,text_completion, orembeddingsfrugal.batched—truefor batch-API submissions,falseotherwise
The "Additional attributes" column above lists what each metric carries on top of the base set. See docs/metrics-reference.md for the values each attribute takes, sentinel values for caller fields, and the full internal-error-stage list.
Metrics are aggregated per export interval (not sent per call), so network overhead is minimal regardless of call volume.
Supported providers
| Provider | Usage |
|---|---|
| OpenAI | wrap_openai(OpenAI()) |
| Anthropic | wrap_anthropic(Anthropic()) |
| Claude on AWS Bedrock (via Anthropic SDK) | wrap_anthropic(AnthropicBedrock()) |
| AWS Bedrock — any model, AWS SDKs directly | wrap_bedrock(boto3.client("bedrock-runtime")) |
| Claude on Google Vertex | wrap_anthropic(AnthropicVertex()) |
wrap_bedrock covers all four bedrock-runtime operations (invoke_model,
invoke_model_with_response_stream, converse, converse_stream) across
every model on Bedrock — Anthropic, OpenAI gpt-oss, Amazon Nova, Meta Llama,
Mistral, Cohere, AI21, DeepSeek, Qwen, Kimi, etc. Cross-region inference
profile prefixes (us., eu., jp., apac., au., global.) are
preserved in gen_ai.request.model. Async clients (aiobotocore /
aioboto3) are detected automatically. See docs/quickstart.md for the full Bedrock walkthrough.
Streaming
Streaming works with no extra setup. The wrapper returns a drop-in replacement that passes events through and emits metrics when the stream finishes.
# OpenAI — opt in to include_usage for token counts in streams
stream = client.chat.completions.create(
model="gpt-4o",
messages=[...],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
...
# Anthropic — token counts available natively, no opt-in needed
stream = anthropic_client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[...],
stream=True,
)
for event in stream:
...
Internal library attribution
If your code calls an internal helper library that wraps the AI SDK (e.g. acme_ai_helpers), add it to the block list so attribution lands on your business code:
FRUGAL_PATH_BLOCK_LIST="src/acme_ai_helpers,libs/llm_utils"
Safety guarantees
- If any required env var is unset,
wrap_openai/wrap_anthropicreturn the client completely unmodified with zero overhead. - If any part of our instrumentation fails at runtime (OTel init, stack walking, metric emission), the original SDK call executes normally. We never raise our own exceptions into your code.
- Wrapping is idempotent — calling
wrap_openai(client)twice on the same client is safe.
Verifying the setup
Set the OTLP endpoint to empty to print metrics to the console instead of exporting them:
export FRUGAL_API_KEY=local
export FRUGAL_PROJECT_ID=my-project
export FRUGAL_OTEL_EXPORTER_OTLP_ENDPOINT= # empty → ConsoleMetricExporter
export FRUGAL_METRIC_EXPORT_INTERVAL_MS=5000
python my_script.py
You should see JSON metric records with gen_ai.* and frugal.* attributes every 5 seconds.
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 frugal_sdk_python-0.1.0.tar.gz.
File metadata
- Download URL: frugal_sdk_python-0.1.0.tar.gz
- Upload date:
- Size: 71.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e274bcca93fafaed9493b18dd7e0d0f7748f0fcb24194df18db6c73b28364e5
|
|
| MD5 |
2a28c66db8d75fd82b66040acb481c4c
|
|
| BLAKE2b-256 |
40909641e1fb583ef1d88db24370338f8c0b9715b059ecdce6869af32fc03547
|
Provenance
The following attestation bundles were made for frugal_sdk_python-0.1.0.tar.gz:
Publisher:
cd.yml on frugalco/frugal-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
frugal_sdk_python-0.1.0.tar.gz -
Subject digest:
5e274bcca93fafaed9493b18dd7e0d0f7748f0fcb24194df18db6c73b28364e5 - Sigstore transparency entry: 1859911750
- Sigstore integration time:
-
Permalink:
frugalco/frugal-python-sdk@4d95fc06a432e9073a19ebd7cde5e09d6dbffa5f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/frugalco
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
cd.yml@4d95fc06a432e9073a19ebd7cde5e09d6dbffa5f -
Trigger Event:
release
-
Statement type:
File details
Details for the file frugal_sdk_python-0.1.0-py3-none-any.whl.
File metadata
- Download URL: frugal_sdk_python-0.1.0-py3-none-any.whl
- Upload date:
- Size: 38.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5d5db7e36c033bdd8b036c56bd7dda895a0d634216d83451464831e6c5170a18
|
|
| MD5 |
f6ee6c03df9455ccbc3fe4240b6968ce
|
|
| BLAKE2b-256 |
a36860066b965babd5d3ebc67619a23591e606c26ab79e72e4072610829dc346
|
Provenance
The following attestation bundles were made for frugal_sdk_python-0.1.0-py3-none-any.whl:
Publisher:
cd.yml on frugalco/frugal-python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
frugal_sdk_python-0.1.0-py3-none-any.whl -
Subject digest:
5d5db7e36c033bdd8b036c56bd7dda895a0d634216d83451464831e6c5170a18 - Sigstore transparency entry: 1859911769
- Sigstore integration time:
-
Permalink:
frugalco/frugal-python-sdk@4d95fc06a432e9073a19ebd7cde5e09d6dbffa5f -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/frugalco
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
cd.yml@4d95fc06a432e9073a19ebd7cde5e09d6dbffa5f -
Trigger Event:
release
-
Statement type: