Metadata-Version: 2.4
Name: tokenmargin
Version: 0.1.0
Summary: Margin observability for AI products. Know which customers cost more than they pay.
Project-URL: Homepage, https://github.com/angiecortez/tokenmargin
Project-URL: Source, https://github.com/angiecortez/tokenmargin
Author: Angie Cortez Tay
License: MIT
License-File: LICENSE
Keywords: anthropic,cost,llm,margin,observability,tokens
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.60.0; extra == 'anthropic'
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# tokenmargin (Python)

```bash
pip install tokenmargin   # not published yet — see ../PUBLISHING.md
```


Margin observability for AI products. Same engine as the TypeScript SDK — the two
are held to a [shared conformance fixture](../conformance/cases.json), so the same
traffic prices identically whichever language sent it.

## Run the demo

No API key needed:

```bash
python3 examples/demo.py
```

## Integration

One line at the client construction site.

```python
from anthropic import Anthropic
from tokenmargin import with_margin, with_customer, JsonlSink

client = with_margin(Anthropic(), sink=JsonlSink("usage.jsonl"))

with with_customer("acme", feature="chat"):
    client.messages.create(...)          # priced and attributed automatically
```

Works with `AsyncAnthropic` too — `with_margin` detects the async client and
returns awaitable wrappers. Streaming is priced on context-manager exit, by which
point `get_final_message()` is already resolved.

Attribution rides a `ContextVar`, so it propagates across `await` boundaries and
into `asyncio` tasks without touching the request body.

## Reading the numbers

```python
from tokenmargin import compute_margins, toxic_customers, cost_by, cache_savings, from_usd

margins = compute_margins(events, [RevenueEntry("acme", from_usd(29))])

toxic_customers(margins)                      # who to act on
cost_by(events, lambda e: e.feature or "-")   # where the money went
cache_savings(events)                         # what caching is worth to you
```

## Tests

Both files run standalone or under pytest:

```bash
python3 tests/test_conformance.py   # must match the TypeScript reference
python3 tests/test_pricing.py       # rate card and margin edge cases
```

## Python-specific notes

**Money is `int` microdollars.** Python ints are unbounded, so the running total
never loses precision no matter how long it accumulates.

**Rounding matches JavaScript.** Python's built-in `round` uses banker's rounding;
`Math.round` rounds half away from zero. `cost.py` uses the JS semantics so the
two SDKs cannot drift apart on a half-microdollar boundary.

**Runs on Python 3.9.** That is what macOS ships as `/usr/bin/python3`, so
requiring 3.10 would mean the package fails to import on a stock Mac. The only
3.10+ feature the package wanted was `slots=True` on its dataclasses; everything
else is annotations, which `from __future__ import annotations` defers to
strings. Dropping slots costs a little per-instance memory and buys every
default interpreter on the platform.

**Sinks never raise.** `record()` swallows its own failures on a background
thread. Dropping a metric is always better than dropping a customer's request.
