tokenmeter-sdk 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ Metadata-Version: 2.4
2
+ Name: tokenmeter-sdk
3
+ Version: 0.2.0
4
+ Summary: Token Meter Python SDK: wraps OpenAI/Anthropic/Gemini/Bedrock/Azure clients and meters token usage
5
+ Author: Saqlain Rashid
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SaqlainRashed/token-meter
8
+ Project-URL: Repository, https://github.com/SaqlainRashed/token-meter
9
+ Project-URL: Changelog, https://github.com/SaqlainRashed/token-meter/blob/main/sdks/python/CHANGELOG.md
10
+ Keywords: llm,tokens,usage,metering,observability,openai,anthropic,vercel-ai-gateway
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: openai
24
+ Requires-Dist: openai>=1.0; extra == "openai"
25
+ Provides-Extra: anthropic
26
+ Requires-Dist: anthropic>=0.30; extra == "anthropic"
27
+ Provides-Extra: gemini
28
+ Requires-Dist: google-genai>=0.1; extra == "gemini"
29
+ Provides-Extra: bedrock
30
+ Requires-Dist: boto3>=1.34; extra == "bedrock"
31
+ Provides-Extra: azure
32
+ Requires-Dist: openai>=1.0; extra == "azure"
33
+ Provides-Extra: dev
34
+ Requires-Dist: pytest>=8; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # token-meter (Python SDK)
38
+
39
+ ## Read usage data
40
+
41
+ Create a separate read-only key under **Settings → API keys → Read API keys**. It cannot submit usage events.
42
+
43
+ ```python
44
+ import os
45
+ from tokenmeter import TokenMeterReadClient
46
+
47
+ read_api = TokenMeterReadClient(os.environ["TOKEN_METER_READ_KEY"], os.environ["TOKEN_METER_DASHBOARD_URL"])
48
+ page = read_api.list_daily_usage("2026-10-01", "2026-10-31")
49
+ ```
50
+
51
+ This returns daily **metered** usage/spend summaries, not provider invoice data. See [`docs/openapi.yaml`](../../docs/openapi.yaml) for pagination, fields, and limits.
52
+
53
+ In-process LLM usage capture for Python. `tokenmeter.wrap(client)` wraps a
54
+ provider client you already have, reads token usage out of the response
55
+ object it already returns (no extra network hop), normalizes it to a
56
+ canonical disjoint counter shape, and ships batches to the Token Meter Ingest
57
+ API in the background.
58
+
59
+ ## Install
60
+
61
+ ```sh
62
+ pip install tokenmeter-sdk
63
+ ```
64
+
65
+ **Note:** the PyPI distribution is named `tokenmeter-sdk` (like the npm
66
+ package `@tokenmeter/sdk`), but the import name is `tokenmeter`:
67
+ `import tokenmeter`. Distribution and import names are allowed to differ;
68
+ the unrelated PyPI project `tokenmeter` blocks the shorter names.
69
+
70
+ The SDK is public and installable by anyone — it does nothing without a
71
+ `tl_live_` ingest key; rate limits, tier caps, and soft-lock are all
72
+ enforced server-side at the ingest door.
73
+
74
+ ## Quick start
75
+
76
+ ```python
77
+ import tokenmeter
78
+ from openai import OpenAI
79
+
80
+ tokenmeter.init(ingest_key="tl_live_...")
81
+ client = tokenmeter.wrap(OpenAI())
82
+
83
+ with tokenmeter.action("generate-report", customer_id="acme", feature="reports"):
84
+ client.chat.completions.create(model="gpt-4o-mini", messages=[...])
85
+ # usage for this call is captured automatically, tagged with
86
+ # customer_ref=acme, feature=reports, and a shared trace_id.
87
+ ```
88
+
89
+ `wrap()` detects the client shape structurally (no hard dependency on any
90
+ provider's package) and is idempotent — wrapping an already-wrapped client or
91
+ method is a no-op.
92
+
93
+ ### Optimization measurements for agents
94
+
95
+ Enable local prompt-prefix fingerprinting once with
96
+ `tokenmeter.init(ingest_key=key, optimization={"prompt_caching": True})`.
97
+ Declare delay tolerance only for suitable background tasks with
98
+ `tokenmeter.action("report", optimization={"max_completion_delay_ms": 86400000})`.
99
+ The SDK includes metadata in its normal usage events; users do not add HTTP
100
+ headers. Both settings are optional and default to off/absent. An inner action
101
+ can disable them with `"prompt_caching": False` and
102
+ `"max_completion_delay_ms": None`. See
103
+ [integration and deployment details](../../docs/SDK_OPTIMIZATION_TELEMETRY.md)
104
+ for supported agents, privacy behavior and detector requirements.
105
+
106
+ ### Prompt topics
107
+
108
+ Label each call with what it is about, for Analytics > Use cases, without sending any prompt text:
109
+
110
+ ```python
111
+ tokenmeter.init(ingest_key=key, optimization={"topic_classifier": tokenmeter.keyword_topic_classifier})
112
+ ```
113
+
114
+ The classifier runs in your process on the latest user message and returns a topic key (such as `debugging` or
115
+ `legal_review`); only that key is sent, as `tags["prompt_topic"]`. Anything that is not a key
116
+ (`^[a-z][a-z0-9_]{1,39}$`) is dropped. Supply your own synchronous function for better labels; an async one, or one
117
+ that raises, is ignored and the call is untouched. Turn it off for one action with
118
+ `optimization={"topic_classifier": None}`. Set it in `init()`: a classifier in an `action()` does not survive
119
+ `TraceContext.to_dict()` across a queue.
120
+
121
+ For an OpenAI-compatible client pointed at xAI, Mistral, Fireworks, Together
122
+ AI, DeepInfra, OpenRouter, or Vercel AI Gateway (`"vercel_gateway"`), preserve
123
+ the billable provider explicitly:
124
+
125
+ ```python
126
+ client = tokenmeter.wrap_openai_compatible(
127
+ OpenAI(api_key="...", base_url="https://api.together.xyz/v1"), "together"
128
+ )
129
+ ```
130
+
131
+ ## Provider support matrix
132
+
133
+ | Provider | Client | Paths wrapped | Streaming | Normalization confidence |
134
+ |---|---|---|---|---|
135
+ | OpenAI | `openai.OpenAI` / `AsyncOpenAI` | `chat.completions.create`, `responses.create` | Yes (both) | **Proven live** — `prompt_tokens` held constant at 2107 across cached/uncached calls (`docs/spikes/openai-billing.md`) |
136
+ | Anthropic | `anthropic.Anthropic` / `AsyncAnthropic` | `messages.create` | Yes | Doc-stated; cache reads reported separately, passthrough only |
137
+ | Azure OpenAI | `openai.AzureOpenAI` / `AsyncAzureOpenAI` | `chat.completions.create`, `responses.create` | Yes (both) | **Doc-inferred only, NOT empirically verified.** Same field shape as OpenAI, so the same subtraction rule is assumed to apply — but the 5-call cache probe that proved OpenAI's rule has not been replayed against a real Azure deployment. Treat as provisional until that probe runs (`docs/spikes/provider-shapes.md` §2.1, §7) |
138
+ | Google Gemini | `google.genai.Client` | `models.generate_content`, `models.generate_content_stream` | Yes (separate method, not a `stream=True` kwarg) | Doc-verified (`docs/spikes/provider-shapes.md` §3) — mixed rule: cached tokens ARE a subset of prompt (subtract), thinking tokens are NOT a subset of output (do not subtract). Streaming "last chunk is complete" behavior is community-observed, not doc-confirmed |
139
+ | AWS Bedrock — `Converse`/`ConverseStream` | `boto3` `bedrock-runtime` client | `converse`, `converse_stream` | Yes | Doc-verified disjoint (`docs/spikes/provider-shapes.md` §1.1) — the primary, normalized path across model families |
140
+ | AWS Bedrock — `InvokeModel` (Anthropic body) | same | `invoke_model` | No | Doc-verified — reuses native Anthropic semantics |
141
+ | AWS Bedrock — `InvokeModel` (Amazon Nova body) | same | `invoke_model` | No | Doc-verified shape; cache-counter key **casing** is unconfirmed (AWS's own docs disagree) — both spellings are probed defensively |
142
+ | AWS Bedrock — `InvokeModel` (other families / headers fallback) | same | `invoke_model` | No | Input/output only, via `x-amzn-bedrock-*-token-count` response headers; no cache/reasoning split. **Mistral bodies carry no usage at all** — this is the only source for them |
143
+ | AWS Bedrock — `InvokeModelWithResponseStream` | same | `invoke_model_with_response_stream` | Yes | Reads `amazon-bedrock-invocationMetrics` from the terminal chunk; input/output only, no cache split |
144
+
145
+ Not covered: async `google-genai` (`client.aio.models`), `aioboto3`, and
146
+ `InvokeModel` body parsers for Meta Llama beyond the documented
147
+ `prompt_token_count`/`generation_token_count` shape. Add as needed — the
148
+ header fallback already covers these families with reduced granularity.
149
+
150
+ **A provider integration is not "done" until its CI fixture exists** (PRD
151
+ §7.1) — see `tests/test_normalize_*.py`, one file per provider, each pinning
152
+ the exact per-provider arithmetic against a recorded fixture in
153
+ `tests/fixtures/`.
154
+
155
+ ## The normalization arithmetic, per provider
156
+
157
+ Every provider is converted to the same disjoint canonical shape
158
+ (`input_tokens`, `output_tokens`, `cache_read_tokens`, `cache_write_tokens`,
159
+ `reasoning_tokens`, each independently nullable) so that
160
+ `sum(class_count * class_price)` is correct downstream by construction. The
161
+ arithmetic that gets there is **not** the same across providers — two of them
162
+ invert the OpenAI reflex:
163
+
164
+ - **OpenAI / Azure OpenAI**: `input = prompt_tokens − cached_tokens`,
165
+ `output = completion_tokens − reasoning_tokens`. Both are subsets and must
166
+ be subtracted out.
167
+ - **Anthropic**: cache reads/writes are reported **separately** from
168
+ `input_tokens` already — passthrough, no subtraction. Subtracting would
169
+ double-count them as missing.
170
+ - **AWS Bedrock (`Converse`)**: `input = inputTokens` **as-is** — it already
171
+ excludes cache reads/writes (`total input = inputTokens + cacheReadInputTokens
172
+ + cacheWriteInputTokens`, doc-verified). Subtracting cache reads here would
173
+ **double-discount**. `reasoning_tokens` is always `null` — no such field
174
+ exists; Claude thinking tokens are billed inside `outputTokens`.
175
+ - **Google Gemini**: `input = promptTokenCount − cachedContentTokenCount`
176
+ (cached **is** a subset, subtract — same direction as OpenAI), but
177
+ `output = candidatesTokenCount` **as-is** — `thoughtsTokenCount` is
178
+ **separate**, not a subset. Subtracting it would silently **undercount**
179
+ the most expensive (reasoning-heavy) traffic.
180
+
181
+ See `src/tokenmeter/_normalize.py` for the implementation and
182
+ `docs/spikes/provider-shapes.md` for the full citations.
183
+
184
+ ## Azure: the deployment-name landmine
185
+
186
+ Azure's Responses API returns the **deployment name**, not the canonical
187
+ model id — a deployment named `prod-summarizer` matches nothing in the
188
+ pricing catalog. Pass an optional deployment → model id map to `wrap()`:
189
+
190
+ ```python
191
+ client = tokenmeter.wrap(
192
+ AzureOpenAI(...),
193
+ model_map={"prod-summarizer": "gpt-5-2025-08-07"},
194
+ )
195
+ ```
196
+
197
+ Unmapped deployments pass through unresolved and land in the unpriced
198
+ quarantine rather than silently mispricing. `model_map` is ignored for every
199
+ other provider.
200
+
201
+ ## Streaming and failure contracts (all providers)
202
+
203
+ - **Never throws into host code.** Only tokenmeter's own bookkeeping is
204
+ try/except'd; the real provider call and its real exceptions pass through
205
+ untouched — proven for the transport, the buffer, and every wrapper's
206
+ success/error/stream paths in `tests/test_transport_never_throws.py` and
207
+ the per-provider `test_wrap_*.py` files.
208
+ - **Never adds latency.** Capture is post-hoc from the response object the
209
+ provider already returned — no network I/O on the metering path. The one
210
+ exception worth naming: Bedrock `InvokeModel`'s response body is read
211
+ in-memory to extract usage and then replayed via a fresh in-memory stream
212
+ so the host's own `response["body"].read()` still works — this adds
213
+ in-memory work, never a network hop.
214
+ - **A stream that aborts before its terminal usage frame** (or a provider
215
+ response with no usage at all) is recorded `usage_complete=false` with
216
+ every token field `null` — never estimated from partial deltas.
217
+ - **Context propagation** (`trace_id`/`action`/`customer_ref`/...) is
218
+ snapshotted at call time and threaded through every wrapper, including
219
+ streams handed off to be iterated later — see
220
+ `tests/test_context_propagation.py` and the context-propagation cases in
221
+ each `test_wrap_*.py`.
@@ -0,0 +1,18 @@
1
+ tokenmeter/__init__.py,sha256=3YZ8_n55Rf2taZoaX3ZYH8zNVwWk5mARraV-eK5QmDw,1394
2
+ tokenmeter/_buffer.py,sha256=0w-ln8r1Vnf1vN0LCYd31nVMfwVmGRD44E0StCaaUuQ,8601
3
+ tokenmeter/_client.py,sha256=qxmhjl9n7jLf_ToJcQWXkygS_rO82p0UblALD85nB5I,2556
4
+ tokenmeter/_context.py,sha256=E40ZUisb9ibIQCaiQraIKUQrBD7zupdKsSWYGycwLY4,5272
5
+ tokenmeter/_diagnose.py,sha256=9x6NRfvl02Xkv5Nkw8iosU2RmPIyRrsQcLfLNeBfnZ4,3088
6
+ tokenmeter/_ids.py,sha256=AapWpP9T4rFsddRJ1tSu8W8lc6ekDM2wTptdR6LHA0Q,1653
7
+ tokenmeter/_normalize.py,sha256=pba2ULse1ulY_C2r7BVlntJv-T7Hc8m1wzSoKjU4QmE,10420
8
+ tokenmeter/_optimization.py,sha256=laG-nlAxac6VU4L17RQiugU9hlkLoIQU-bVHMdwx93c,4117
9
+ tokenmeter/_preflight.py,sha256=CR-bjnKBd9Zri6siR2ZZdoxTJ4W8sYsAy8TCd47Cu_k,7540
10
+ tokenmeter/_read_api.py,sha256=zsmxxd5MIPc2QVQl7QhJFb6XA1PAaMNAsgFSIPsJOrk,2132
11
+ tokenmeter/_topic.py,sha256=2-hDvQ9uj9vPgMPp6h6ipskU88THVjhMe03Rvp14nYk,4591
12
+ tokenmeter/_transport.py,sha256=pqDSgggQ_kZdYW0VndfWc1Hj68s16pzGuj8FNJlQW_8,3425
13
+ tokenmeter/_wrap.py,sha256=Iu3H2_dkJkXdXvAToGxZMbCbOfCrOuWQreSNooTI_xo,37231
14
+ tokenmeter_sdk-0.2.0.dist-info/licenses/LICENSE,sha256=4Bo5yCSg2wD8laNyw-MejzE-MS-zpxE8OU4cz5ZtKJA,1092
15
+ tokenmeter_sdk-0.2.0.dist-info/METADATA,sha256=IbwfC70xnT6ESpDVTe56TebQp1lc0y9nRf2PwkPsMOQ,12050
16
+ tokenmeter_sdk-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
17
+ tokenmeter_sdk-0.2.0.dist-info/top_level.txt,sha256=VBoi9Vy7-LSAISqogxfAY1AX-MT_dwwKjaNLWK-ZHfw,11
18
+ tokenmeter_sdk-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Saqlain Rashid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ tokenmeter