Skip to main content

llm-cost-profiler

PyPI version License

A lightweight, framework-agnostic library for tracking LLM token usage, cost, and latency across OpenAI, LangChain, LangGraph, FastAPI, batch jobs, and any custom Python workflow.

It helps teams bring visibility, observability, and cost analytics into any LLM-powered system with almost zero code changes.

Contents

Overview

llm-cost-profiler is a small, dependency-light Python library for tracking LLM usage, latency, and estimated cost across different model providers. It provides simple instrumentation primitives (manual recording and a decorator), JSONL logging for audit and analysis, optional dynamic pricing lookups, and per-agent-step cost attribution for LangGraph pipelines.

Perfect for developers building RAG systems, agents, batch inference jobs, Streamlit apps, FastAPI services, and more.

Features

  • Token usage (prompt & completion) tracking
  • Model-specific cost estimation
  • Latency & response time tracking
  • Per-agent-step cost attribution for LangGraph — see which node in your pipeline is actually driving cost, not just which model
  • litellm-backed pricing for hundreds of models across providers, with automatic fallback
  • Local Streamlit dashboard — cost/latency by step and model, budget alerts
  • JSONL logging sink for audits & dashboards
  • Simple decorator to profile function calls
  • Dynamic pricing (optional) with safe fallbacks
  • Minimal dependencies, zero required dependencies on AI frameworks
  • Works with OpenAI, LangChain, LangGraph, custom clients, batch jobs, APIs
  • Built for production observability (text logs → data lakes)

Installation

Install from PyPI:

pip install llm-cost-profiler

Optional extras, installable individually or combined:

pip install llm-cost-profiler[langgraph]   # per-step attribution for LangGraph
pip install llm-cost-profiler[litellm]     # pricing for hundreds of models via litellm
pip install llm-cost-profiler[dashboard]   # local Streamlit dashboard
pip install llm-cost-profiler[all]         # everything above

Quick start

from llm_cost_profiler import CostProfiler, profile_llm_call
from openai import OpenAI

client = OpenAI(api_key="YOUR_KEY")
prof = CostProfiler("local_costs.jsonl")

@profile_llm_call(
    prof,
    model_key_getter=lambda a, k: k.get("model", "gpt-4o"),
    token_counts_getter=lambda r: (r["usage"]["prompt_tokens"], r["usage"]["completion_tokens"]),
)
def ask(prompt, model="gpt-4o"):
    return client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
    )

ask("Explain LLM observability like I'm 5")
print(prof.report_summary())

Per-step cost attribution (LangGraph)

Knowing your agent spent $12 today doesn't tell you why. report_summary() groups cost by model — useful, but not enough once you have multiple LLM calls across multiple steps of an agent. report_by_step() groups by which node in your agent actually spent it, so you can answer "which part of my agent is burning the budget?" directly.

from llm_cost_profiler import CostProfiler, profile_llm_call
from llm_cost_profiler.langgraph_integration import profile_node

prof = CostProfiler(sink_path="agent_costs.jsonl")

@profile_llm_call(
    prof,
    model_key_getter=lambda a, k: k.get("model", "gpt-4.1"),
    token_counts_getter=lambda r: (r["usage"]["prompt_tokens"], r["usage"]["completion_tokens"]),
)
def call_llm(prompt, model="gpt-4.1"):
    ...

@profile_node("retrieve_docs", profiler=prof)
def retrieve_docs(state):
    ...  # any LLM calls made in here are auto-tagged step="retrieve_docs"
    return state

@profile_node("generate_answer", profiler=prof)
def generate_answer(state):
    call_llm(state["query"], model="gpt-4.1")
    return state

# ... build your StateGraph with these wrapped node functions as usual ...

print(prof.report_by_step())
# {"steps": {"generate_answer": {"cost_usd": 0.12, ...},
#            "retrieve_docs":   {"cost_usd": 0.0,  ...}}, ...}
# sorted with the most expensive step first

You don't need to change how you call your LLM client, and nodes with no LLM call at all (retrieval, routing, parsing) still show up with their latency, so slow-but-free steps aren't invisible either.

Nodes are wrapped before being added to the graph, rather than instrumented after .compile() — LangGraph's compiled internals are private APIs that change across versions, so wrapping at definition time keeps this stable regardless of which LangGraph version you're on.

See examples/langgraph_step_attribution.py for a full runnable example against a real StateGraph.

litellm-backed pricing (hundreds of models)

The builtin pricing table only knows a handful of models. For broader, better-maintained coverage, use pricing_source="litellm" to price calls using litellm's pricing table, which covers hundreds of models across OpenAI, Anthropic, Google, Cohere, Azure, Bedrock, and more.

from llm_cost_profiler import CostProfiler

prof = CostProfiler("costs.jsonl", pricing_source="litellm")

# OpenAI models work with bare names:
prof.record_call("gpt-4o", 1000, 500, latency=0.02)

# Other providers: use the explicit "provider/model" form - this is
# the reliable pattern. A bare non-OpenAI name sometimes works and
# sometimes doesn't (inconsistent in litellm itself depending on the
# exact model string), so always prefix to be safe:
prof.record_call("anthropic/claude-sonnet-4-5-20250929", 1000, 500, latency=0.02)

If litellm doesn't recognize a model (unmapped, or a very recent release litellm hasn't added yet), CostProfiler automatically falls back to the builtin pricing table for that call - you'll never get a crash or a silent $0 cost from an unrecognized model.

Install with:

pip install llm-cost-profiler[litellm]

See examples/litellm_pricing_example.py.

Local dashboard

View cost, tokens, and latency - overall and broken down by step and model - in a local Streamlit dashboard reading directly from your JSONL log:

pip install llm-cost-profiler[dashboard]
llm-cost-profiler dashboard --sink agent_costs.jsonl

Optional flags:

  • --budget 5.00 — shows a warning banner if total logged cost exceeds this
  • --port 8765 — serve on a specific port
  • --headless — don't try to open a browser (useful on a remote server)

The dashboard shows total cost/calls/tokens, cost by step, cost by model, cumulative cost over time, and average latency by step - the same report_by_step() / report_summary() data as the Python API, in a browsable view you can leave open while developing.

Examples

All runnable examples are inside the examples/ directory. Run any of them with:

python examples/<file>.py

Core

File What it shows
example_basic_profiler.py Minimal fake call + cost
example_multiple_calls.py Tags and multiple calls
example_jsonl_logging.py JSONL logs + reading back
example_model_price_override.py Override the pricing table

Integrations

File What it shows
openai_integration.py Real OpenAI client usage
langchain_integration.py Decorator wrapper over LangChain
langgraph_step_attribution.py Per-node cost attribution over a real LangGraph StateGraph
litellm_pricing_example.py Pricing calls with litellm's maintained pricing table
fastapi_server.py API endpoint that tracks cost per request
batch_job.py Batch summarization worker

How the decorator works

@profile_llm_call(
    prof,
    model_key_getter=lambda args, kwargs: kwargs.get("model"),
    token_counts_getter=lambda resp: (
        resp["usage"]["prompt_tokens"],
        resp["usage"]["completion_tokens"],
    ),
)
def llm_call(...):
    ...

The decorator automatically captures:

  • model name
  • prompt & completion tokens
  • execution time
  • estimated cost
  • any custom tags you pass via tags_getter
  • the currently-executing agent step, if called from inside a profile_node-wrapped function

...and logs it as JSONL.

Configuration

llm-cost-profiler doesn't require any environment variables to work — CostProfiler(sink_path=...) and the built-in pricing table are enough out of the box.

Two optional environment variables affect the dynamic pricing cache (used only if you construct CostProfiler(pricing_source="dynamic")):

Variable Default Purpose
LLM_PRICING_CACHE .llm_pricing_cache.json Path to the local cache file for fetched pricing data
LLM_PRICING_TTL_SECONDS 86400 (24h) How long cached pricing data is considered fresh

Any API keys for the LLM provider you're calling (e.g. OPENAI_API_KEY) are your own client's responsibility — llm-cost-profiler never reads or requires them, since it only wraps your existing calls rather than making any itself.

Development

Clone the repo and use a virtualenv:

python -m venv .venv
source .venv/bin/activate
pip install -e .[dev]
pytest -q

For LangGraph-related development/tests:

pip install -e .[langgraph]

Contributing

Contributions welcome — open issues and PRs. Please follow the project's existing code style and add tests for new behavior.

License

MIT © Boniface Alexander

Metadata

Release files for llm-cost-profiler 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for llm-cost-profiler 0.2.0
File Size Uploaded
llm_cost_profiler-0.2.0.tar.gz 37.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for llm-cost-profiler 0.2.0
File Interpreter ABI Platform
llm_cost_profiler-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 60.5 kB

Release files / llm_cost_profiler-0.2.0.tar.gz

Download URL llm_cost_profiler-0.2.0.tar.gz
Size 37.9 kB
Tags Source
SHA-256 checksum
How to use checksums
7c6d58234f5df50a079b73700d1ab8ed5c27d5836a9f9073ea8e68a5dca8905a
BLAKE2b-256 checksum
How to use checksums
8e85980d2b7b59bc62303ab33b1fc12901644b817245683a44d16aa8de8f7f32
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / llm_cost_profiler-0.2.0-py3-none-any.whl

Download URL llm_cost_profiler-0.2.0-py3-none-any.whl
Size 22.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d48071100b8e013deb40acd1781f966dbdd855f48bc7ddbb5b741cefb9a444c2
BLAKE2b-256 checksum
How to use checksums
443dffae5e324d40257ee1ebc527bfe18797799993e25379b18c03ee613668d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page