llmtrack-sdk 0.1.0__tar.gz

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.
Files changed (79) hide show
  1. llmtrack_sdk-0.1.0/.gitignore +8 -0
  2. llmtrack_sdk-0.1.0/LICENSE +21 -0
  3. llmtrack_sdk-0.1.0/PKG-INFO +201 -0
  4. llmtrack_sdk-0.1.0/README.md +179 -0
  5. llmtrack_sdk-0.1.0/integration.py +67 -0
  6. llmtrack_sdk-0.1.0/pyproject.toml +29 -0
  7. llmtrack_sdk-0.1.0/src/llmtrack_sdk/__init__.py +4 -0
  8. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/.github/workflows/python.yml +34 -0
  9. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/.gitignore +66 -0
  10. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/.gitlab-ci.yml +31 -0
  11. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/.openapi-generator/FILES +68 -0
  12. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/.openapi-generator/VERSION +1 -0
  13. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/.openapi-generator-ignore +23 -0
  14. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/.travis.yml +17 -0
  15. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/README.md +147 -0
  16. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/AuthErrorResponse.md +29 -0
  17. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/DefaultApi.md +101 -0
  18. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/DuplicateResponse.md +30 -0
  19. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/FreePlanQuotaResponse.md +31 -0
  20. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/FreePlanQuotaResponseQuota.md +33 -0
  21. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/IngestLlmRequest200Response.md +37 -0
  22. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/IngestRequest.md +44 -0
  23. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/IngestSuccess.md +37 -0
  24. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/InternalErrorResponse.md +29 -0
  25. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/InvalidPayloadResponse.md +31 -0
  26. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/PaidPlanInactiveResponse.md +32 -0
  27. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/PaymentRequiredResponse.md +33 -0
  28. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/SourceTriple.md +31 -0
  29. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/UsageLimitResponse.md +29 -0
  30. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/docs/VisibilityContext.md +31 -0
  31. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/git_push.sh +54 -0
  32. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/__init__.py +76 -0
  33. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/api/__init__.py +5 -0
  34. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/api/default_api.py +341 -0
  35. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/api_client.py +833 -0
  36. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/api_response.py +21 -0
  37. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/configuration.py +683 -0
  38. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/exceptions.py +218 -0
  39. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/__init__.py +30 -0
  40. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/auth_error_response.py +95 -0
  41. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/duplicate_response.py +104 -0
  42. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/free_plan_quota_response.py +123 -0
  43. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/free_plan_quota_response_quota.py +111 -0
  44. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/ingest_llm_request200_response.py +137 -0
  45. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/ingest_request.py +200 -0
  46. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/ingest_success.py +162 -0
  47. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/internal_error_response.py +95 -0
  48. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/invalid_payload_response.py +113 -0
  49. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/paid_plan_inactive_response.py +115 -0
  50. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/payment_required_response.py +151 -0
  51. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/source_triple.py +107 -0
  52. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/usage_limit_response.py +95 -0
  53. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/models/visibility_context.py +107 -0
  54. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/py.typed +0 -0
  55. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/llmtrack_generated/rest.py +334 -0
  56. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/pyproject.toml +94 -0
  57. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/requirements.txt +4 -0
  58. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/setup.cfg +2 -0
  59. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/setup.py +47 -0
  60. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/__init__.py +0 -0
  61. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_auth_error_response.py +52 -0
  62. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_default_api.py +38 -0
  63. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_duplicate_response.py +54 -0
  64. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_free_plan_quota_response.py +66 -0
  65. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_free_plan_quota_response_quota.py +60 -0
  66. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_ingest_llm_request200_response.py +90 -0
  67. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_ingest_request.py +67 -0
  68. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_ingest_success.py +90 -0
  69. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_internal_error_response.py +52 -0
  70. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_invalid_payload_response.py +56 -0
  71. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_paid_plan_inactive_response.py +58 -0
  72. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_payment_required_response.py +70 -0
  73. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_source_triple.py +56 -0
  74. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_usage_limit_response.py +52 -0
  75. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test/test_visibility_context.py +72 -0
  76. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/test-requirements.txt +6 -0
  77. llmtrack_sdk-0.1.0/src/llmtrack_sdk/_generated/tox.ini +9 -0
  78. llmtrack_sdk-0.1.0/src/llmtrack_sdk/client.py +147 -0
  79. llmtrack_sdk-0.1.0/tests/test_client.py +27 -0
@@ -0,0 +1,8 @@
1
+ node_modules/
2
+ dist/
3
+ .venv/
4
+ __pycache__/
5
+ .pytest_cache/
6
+ *.egg-info/
7
+ packages/node/src/generated/
8
+ packages/python/src/llmtrack_sdk/_generated/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LLMtrack
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,201 @@
1
+ Metadata-Version: 2.5
2
+ Name: llmtrack-sdk
3
+ Version: 0.1.0
4
+ Summary: Server-side SDK for tracking LLM token usage, costs, and observability data in LLMtrack
5
+ Project-URL: Homepage, https://llm-track.com
6
+ Project-URL: Documentation, https://llm-track.com/docs
7
+ Project-URL: Repository, https://github.com/othmanemer9-cloud/llmtrack-sdks
8
+ Project-URL: Issues, https://github.com/othmanemer9-cloud/llmtrack-sdks/issues
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,anthropic,cost,cost-tracking,llm,observability,openai,tokens
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: httpx<1,>=0.27
14
+ Provides-Extra: dev
15
+ Requires-Dist: build>=1; extra == 'dev'
16
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
17
+ Requires-Dist: pytest>=8; extra == 'dev'
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
20
+ Requires-Dist: pytest>=8; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # llmtrack-sdk (Python)
24
+
25
+ LLMtrack records server-side LLM token usage, cost, and request context so teams can understand AI usage in one dashboard.
26
+
27
+ > [!WARNING]
28
+ > **Server-side only.** Never put an LLMtrack key in browser, mobile, desktop-client, or other public code. Load it from a server-side environment variable only.
29
+
30
+ The PyPI distribution is named `llmtrack-sdk`, while Python imports use `llmtrack_sdk`: distribution names can contain hyphens, but import identifiers cannot, and the distinct name also avoids the unrelated `llmtrack` distribution.
31
+
32
+ ## Install
33
+
34
+ ```sh
35
+ pip install llmtrack-sdk
36
+ ```
37
+
38
+ ## Quickstart
39
+
40
+ Create an ingestion key in LLMtrack, save it at `~/.config/llmtrack/api-key`, and run:
41
+
42
+ ```sh
43
+ export LLMTRACK_API_KEY="$(cat ~/.config/llmtrack/api-key)"
44
+ ```
45
+
46
+ ```python
47
+ import os
48
+ from llmtrack_sdk import LLMtrack
49
+ tracker = LLMtrack(api_key=os.environ["LLMTRACK_API_KEY"])
50
+ tracker.track(provider="openai", model="gpt-5.6-sol", prompt_tokens=241,
51
+ completion_tokens=86, reasoning_tokens=32, feature="support-chat")
52
+ ```
53
+
54
+ ## Provider examples
55
+
56
+ Both examples call `track()` immediately after the provider completion and read counts from the official response objects.
57
+
58
+ ### OpenAI
59
+
60
+ ```python
61
+ import os
62
+ from openai import OpenAI
63
+ from llmtrack_sdk import LLMtrack
64
+ openai, tracker = OpenAI(), LLMtrack(api_key=os.environ["LLMTRACK_API_KEY"])
65
+ response = openai.chat.completions.create(model="gpt-5.6-sol", messages=[{"role": "user", "content": "Explain why the sky is blue in two sentences."}])
66
+ usage = response.usage
67
+ tracker.track(provider="openai", model=response.model, prompt_tokens=usage.prompt_tokens, completion_tokens=usage.completion_tokens,
68
+ reasoning_tokens=usage.completion_tokens_details.reasoning_tokens if usage.completion_tokens_details else 0, feature="science-explainer")
69
+ ```
70
+
71
+ ### Anthropic
72
+
73
+ Anthropic includes thinking tokens in `output_tokens` rather than exposing a separate reasoning-token count, so do not duplicate them in `reasoning_tokens`.
74
+
75
+ ```python
76
+ import os
77
+ from anthropic import Anthropic
78
+ from llmtrack_sdk import LLMtrack
79
+ anthropic, tracker = Anthropic(), LLMtrack(api_key=os.environ["LLMTRACK_API_KEY"])
80
+ response = anthropic.messages.create(model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Summarize the benefits of typed APIs."}])
81
+ tracker.track(provider="anthropic", model=response.model, prompt_tokens=response.usage.input_tokens,
82
+ completion_tokens=response.usage.output_tokens, feature="document-summary")
83
+ ```
84
+
85
+ ## Fire-and-forget and awaited usage
86
+
87
+ | Method | Behavior |
88
+ |---|---|
89
+ | `track(**event)` | Synchronous fire-and-forget entry point: starts a daemon delivery thread, returns `None` immediately, **never blocks and never raises**. Errors go to `on_error`. |
90
+ | `await track_sync(**event)` | Async awaited entry point: returns the API dictionary and **raises `LLMtrackError`** on failure. Despite its compatibility name, it must be awaited. |
91
+
92
+ ```python
93
+ result = await tracker.track_sync(provider="openai", model="gpt-5.6-sol", prompt_tokens=241,
94
+ completion_tokens=86, reasoning_tokens=32, feature="support-chat")
95
+ ```
96
+
97
+ Set `enabled=False` in tests and local development to make both methods no-ops:
98
+
99
+ ```python
100
+ tracker = LLMtrack(api_key=os.environ["LLMTRACK_API_KEY"], enabled=False)
101
+ ```
102
+
103
+ ## Constructor options
104
+
105
+ | Name | Type | Default | Description |
106
+ |---|---|---|---|
107
+ | `api_key` | `str` | required | Server-side LLMtrack ingestion key. |
108
+ | `base_url` | `str` | `https://llm-track.com` | API origin; primarily useful with a test server. |
109
+ | `environment` | `str` | `production` | Environment applied when an event does not override it. |
110
+ | `on_error` | `Callable[[LLMtrackError], None] \| None` | logs one `[llmtrack]` warning | Receives `track()` background errors; callback exceptions are contained. |
111
+ | `on_warning` | `Callable[[LLMtrackWarning], None] \| None` | logs one `[llmtrack]` warning | Receives visibility and pricing warnings, once per distinct warning per process. |
112
+ | `enabled` | `bool` | `True` | When `False`, both tracking methods do nothing. |
113
+ | `timeout_ms` | `int` | `5000` | Timeout in milliseconds for each request attempt. |
114
+ | `max_retries` | `int` | `3` | Maximum attempts, including the first request. |
115
+
116
+ ## Event fields
117
+
118
+ The wrapper intentionally accepts the following snake-case subset of the `IngestRequest` contract. `total_tokens`, `cached_input_tokens`, and `cache_write_tokens` exist in the wire contract but are not arguments in the current Python public API; do not pass them to this SDK.
119
+
120
+ | Name | Type | Requirement | Description |
121
+ |---|---|---|---|
122
+ | `provider` | `str` | required | Provider name, such as `openai` or `anthropic`. |
123
+ | `model` | `str` | required | Provider model name, such as `gpt-5.6-sol`. |
124
+ | `prompt_tokens` | `int` | required | Non-negative integer input-token count. |
125
+ | `completion_tokens` | `int` | required | Non-negative integer output-token count. |
126
+ | `total_tokens` | `int \| None` | not exposed | Optional explicit total in `IngestRequest`; the current wrapper relies on the server-computed total. |
127
+ | `reasoning_tokens` | `int \| None` | optional | Non-negative reasoning-token count when separately reported. |
128
+ | `cached_input_tokens` | `int \| None` | not exposed | Optional cached-input count in `IngestRequest`; not an argument in the current Python public API. |
129
+ | `cache_write_tokens` | `int \| None` | not exposed | Optional cache-write count in `IngestRequest`; not an argument in the current Python public API. |
130
+ | `latency_ms` | `int \| None` | optional | End-to-end latency in milliseconds. |
131
+ | `status` | `str \| None` | optional | One of `success`, `error`, `timeout`, or `cancelled`. |
132
+ | `feature` | `str \| None` | optional | Product feature, such as `support-chat`; defaults server-side to `unknown`. |
133
+ | `customer_id` | `str \| None` | optional | Your stable customer identifier. |
134
+ | `customer_name` | `str \| None` | optional | Your customer display name. |
135
+ | `environment` | `str \| None` | optional | Overrides the constructor environment for this event. |
136
+ | `metadata` | `dict[str, Any] \| None` | optional | JSON object containing request context; maximum serialized size is 8 KiB (8192 bytes). |
137
+ | `idempotency_key` | `str \| None` | optional | SDK-only header option used to deduplicate the call, not an event-body field. |
138
+
139
+ The client validates `prompt_tokens`, `completion_tokens`, and `reasoning_tokens` as non-negative integers and rejects metadata whose compact UTF-8 JSON serialization exceeds 8192 bytes. The server validates the remaining contract constraints.
140
+
141
+ ## Delivery, retries, and idempotency
142
+
143
+ Each call automatically receives a UUID v4 idempotency key. That key stays stable across retries, preventing double-counting when a stored response is lost. Supply your own stable key to deduplicate separate calls:
144
+
145
+ ```python
146
+ tracker.track(provider="openai", model="gpt-5.6-sol", prompt_tokens=241, completion_tokens=86,
147
+ reasoning_tokens=32, feature="support-chat", idempotency_key="9ea0c2ec-7b98-4bc3-9802-20ae6a468a35")
148
+ ```
149
+
150
+ The SDK retries only network failures, timeouts, and HTTP 5xx responses. It never retries HTTP 4xx responses.
151
+
152
+ ## Errors
153
+
154
+ | Code | Meaning | Fix |
155
+ |---|---|---|
156
+ | `INVALID_API_KEY` | The key is missing, unknown, or belongs to a missing workspace. | Check `LLMTRACK_API_KEY` and create or copy a valid ingestion key. |
157
+ | `REVOKED_API_KEY` | The key was revoked. | Replace it with an active key. |
158
+ | `INACTIVE_API_KEY` | The key was deactivated. | Reactivate it or replace it. |
159
+ | `INVALID_PAYLOAD` | A field failed local or server validation. | Correct the named field; check integer token counts and metadata size. |
160
+ | `QUOTA_EXCEEDED` | The event allowance or pay-per-event credits are exhausted. | Wait for reset, add credits, or upgrade. |
161
+ | `PLAN_INACTIVE` | Billing status prevents ingestion. | Restore billing and reactivate the plan. |
162
+ | `NETWORK_ERROR` | Network/timeout retries were exhausted, or an unclassified HTTP/server failure occurred. | Check connectivity, `base_url`, timeout, and service availability. |
163
+
164
+ `track_sync()` raises these errors. `track()` sends them to `on_error` and never raises.
165
+
166
+ ## Warnings and free-plan key binding
167
+
168
+ | Response | Meaning |
169
+ |---|---|
170
+ | `dashboard_visible: false` | The event was accepted and billed/consumed quota, but is hidden because its source does not match the free-key binding. |
171
+ | `pricing_status: unknown_model` | No active pricing matched the provider/model, so cost is `0`; token usage is still recorded. |
172
+
173
+ **The most common reason events do not appear:** free-plan keys bind to one **provider/model/feature triple**. Mismatched events are accepted and billed (or consume quota) but hidden. Match all three values to the binding shown in LLMtrack. `NOT_DASHBOARD_VISIBLE` includes the bound and submitted triples.
174
+
175
+ ## Troubleshooting
176
+
177
+ ### Events are not appearing
178
+
179
+ Check warnings for `NOT_DASHBOARD_VISIBLE` and compare provider, model, and feature with the key binding. A short-lived process can exit before the daemon thread finishes; use `await track_sync()` in scripts and jobs that must confirm delivery.
180
+
181
+ ### Cost shows 0
182
+
183
+ Check for `UNKNOWN_MODEL` or `pricing_status: unknown_model`. Use the exact `response.model` returned by the provider so active pricing can match it.
184
+
185
+ ### The key is rejected
186
+
187
+ Inspect `LLMtrackError.code`: replace invalid/revoked keys, reactivate inactive keys, and confirm the environment variable is available to the server process. Use `await track_sync()` to inspect the complete exception.
188
+
189
+ ## Contributing
190
+
191
+ Async tests use `pytest-asyncio` (included in the `test` and `dev` extras):
192
+
193
+ ```sh
194
+ pip install -e 'packages/python[test]'
195
+ pytest packages/python/tests
196
+ ```
197
+
198
+ ## More information
199
+
200
+ - [LLMtrack documentation](https://llm-track.com/docs)
201
+ - [MIT License](./LICENSE)
@@ -0,0 +1,179 @@
1
+ # llmtrack-sdk (Python)
2
+
3
+ LLMtrack records server-side LLM token usage, cost, and request context so teams can understand AI usage in one dashboard.
4
+
5
+ > [!WARNING]
6
+ > **Server-side only.** Never put an LLMtrack key in browser, mobile, desktop-client, or other public code. Load it from a server-side environment variable only.
7
+
8
+ The PyPI distribution is named `llmtrack-sdk`, while Python imports use `llmtrack_sdk`: distribution names can contain hyphens, but import identifiers cannot, and the distinct name also avoids the unrelated `llmtrack` distribution.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ pip install llmtrack-sdk
14
+ ```
15
+
16
+ ## Quickstart
17
+
18
+ Create an ingestion key in LLMtrack, save it at `~/.config/llmtrack/api-key`, and run:
19
+
20
+ ```sh
21
+ export LLMTRACK_API_KEY="$(cat ~/.config/llmtrack/api-key)"
22
+ ```
23
+
24
+ ```python
25
+ import os
26
+ from llmtrack_sdk import LLMtrack
27
+ tracker = LLMtrack(api_key=os.environ["LLMTRACK_API_KEY"])
28
+ tracker.track(provider="openai", model="gpt-5.6-sol", prompt_tokens=241,
29
+ completion_tokens=86, reasoning_tokens=32, feature="support-chat")
30
+ ```
31
+
32
+ ## Provider examples
33
+
34
+ Both examples call `track()` immediately after the provider completion and read counts from the official response objects.
35
+
36
+ ### OpenAI
37
+
38
+ ```python
39
+ import os
40
+ from openai import OpenAI
41
+ from llmtrack_sdk import LLMtrack
42
+ openai, tracker = OpenAI(), LLMtrack(api_key=os.environ["LLMTRACK_API_KEY"])
43
+ response = openai.chat.completions.create(model="gpt-5.6-sol", messages=[{"role": "user", "content": "Explain why the sky is blue in two sentences."}])
44
+ usage = response.usage
45
+ tracker.track(provider="openai", model=response.model, prompt_tokens=usage.prompt_tokens, completion_tokens=usage.completion_tokens,
46
+ reasoning_tokens=usage.completion_tokens_details.reasoning_tokens if usage.completion_tokens_details else 0, feature="science-explainer")
47
+ ```
48
+
49
+ ### Anthropic
50
+
51
+ Anthropic includes thinking tokens in `output_tokens` rather than exposing a separate reasoning-token count, so do not duplicate them in `reasoning_tokens`.
52
+
53
+ ```python
54
+ import os
55
+ from anthropic import Anthropic
56
+ from llmtrack_sdk import LLMtrack
57
+ anthropic, tracker = Anthropic(), LLMtrack(api_key=os.environ["LLMTRACK_API_KEY"])
58
+ response = anthropic.messages.create(model="claude-opus-5", max_tokens=1024, messages=[{"role": "user", "content": "Summarize the benefits of typed APIs."}])
59
+ tracker.track(provider="anthropic", model=response.model, prompt_tokens=response.usage.input_tokens,
60
+ completion_tokens=response.usage.output_tokens, feature="document-summary")
61
+ ```
62
+
63
+ ## Fire-and-forget and awaited usage
64
+
65
+ | Method | Behavior |
66
+ |---|---|
67
+ | `track(**event)` | Synchronous fire-and-forget entry point: starts a daemon delivery thread, returns `None` immediately, **never blocks and never raises**. Errors go to `on_error`. |
68
+ | `await track_sync(**event)` | Async awaited entry point: returns the API dictionary and **raises `LLMtrackError`** on failure. Despite its compatibility name, it must be awaited. |
69
+
70
+ ```python
71
+ result = await tracker.track_sync(provider="openai", model="gpt-5.6-sol", prompt_tokens=241,
72
+ completion_tokens=86, reasoning_tokens=32, feature="support-chat")
73
+ ```
74
+
75
+ Set `enabled=False` in tests and local development to make both methods no-ops:
76
+
77
+ ```python
78
+ tracker = LLMtrack(api_key=os.environ["LLMTRACK_API_KEY"], enabled=False)
79
+ ```
80
+
81
+ ## Constructor options
82
+
83
+ | Name | Type | Default | Description |
84
+ |---|---|---|---|
85
+ | `api_key` | `str` | required | Server-side LLMtrack ingestion key. |
86
+ | `base_url` | `str` | `https://llm-track.com` | API origin; primarily useful with a test server. |
87
+ | `environment` | `str` | `production` | Environment applied when an event does not override it. |
88
+ | `on_error` | `Callable[[LLMtrackError], None] \| None` | logs one `[llmtrack]` warning | Receives `track()` background errors; callback exceptions are contained. |
89
+ | `on_warning` | `Callable[[LLMtrackWarning], None] \| None` | logs one `[llmtrack]` warning | Receives visibility and pricing warnings, once per distinct warning per process. |
90
+ | `enabled` | `bool` | `True` | When `False`, both tracking methods do nothing. |
91
+ | `timeout_ms` | `int` | `5000` | Timeout in milliseconds for each request attempt. |
92
+ | `max_retries` | `int` | `3` | Maximum attempts, including the first request. |
93
+
94
+ ## Event fields
95
+
96
+ The wrapper intentionally accepts the following snake-case subset of the `IngestRequest` contract. `total_tokens`, `cached_input_tokens`, and `cache_write_tokens` exist in the wire contract but are not arguments in the current Python public API; do not pass them to this SDK.
97
+
98
+ | Name | Type | Requirement | Description |
99
+ |---|---|---|---|
100
+ | `provider` | `str` | required | Provider name, such as `openai` or `anthropic`. |
101
+ | `model` | `str` | required | Provider model name, such as `gpt-5.6-sol`. |
102
+ | `prompt_tokens` | `int` | required | Non-negative integer input-token count. |
103
+ | `completion_tokens` | `int` | required | Non-negative integer output-token count. |
104
+ | `total_tokens` | `int \| None` | not exposed | Optional explicit total in `IngestRequest`; the current wrapper relies on the server-computed total. |
105
+ | `reasoning_tokens` | `int \| None` | optional | Non-negative reasoning-token count when separately reported. |
106
+ | `cached_input_tokens` | `int \| None` | not exposed | Optional cached-input count in `IngestRequest`; not an argument in the current Python public API. |
107
+ | `cache_write_tokens` | `int \| None` | not exposed | Optional cache-write count in `IngestRequest`; not an argument in the current Python public API. |
108
+ | `latency_ms` | `int \| None` | optional | End-to-end latency in milliseconds. |
109
+ | `status` | `str \| None` | optional | One of `success`, `error`, `timeout`, or `cancelled`. |
110
+ | `feature` | `str \| None` | optional | Product feature, such as `support-chat`; defaults server-side to `unknown`. |
111
+ | `customer_id` | `str \| None` | optional | Your stable customer identifier. |
112
+ | `customer_name` | `str \| None` | optional | Your customer display name. |
113
+ | `environment` | `str \| None` | optional | Overrides the constructor environment for this event. |
114
+ | `metadata` | `dict[str, Any] \| None` | optional | JSON object containing request context; maximum serialized size is 8 KiB (8192 bytes). |
115
+ | `idempotency_key` | `str \| None` | optional | SDK-only header option used to deduplicate the call, not an event-body field. |
116
+
117
+ The client validates `prompt_tokens`, `completion_tokens`, and `reasoning_tokens` as non-negative integers and rejects metadata whose compact UTF-8 JSON serialization exceeds 8192 bytes. The server validates the remaining contract constraints.
118
+
119
+ ## Delivery, retries, and idempotency
120
+
121
+ Each call automatically receives a UUID v4 idempotency key. That key stays stable across retries, preventing double-counting when a stored response is lost. Supply your own stable key to deduplicate separate calls:
122
+
123
+ ```python
124
+ tracker.track(provider="openai", model="gpt-5.6-sol", prompt_tokens=241, completion_tokens=86,
125
+ reasoning_tokens=32, feature="support-chat", idempotency_key="9ea0c2ec-7b98-4bc3-9802-20ae6a468a35")
126
+ ```
127
+
128
+ The SDK retries only network failures, timeouts, and HTTP 5xx responses. It never retries HTTP 4xx responses.
129
+
130
+ ## Errors
131
+
132
+ | Code | Meaning | Fix |
133
+ |---|---|---|
134
+ | `INVALID_API_KEY` | The key is missing, unknown, or belongs to a missing workspace. | Check `LLMTRACK_API_KEY` and create or copy a valid ingestion key. |
135
+ | `REVOKED_API_KEY` | The key was revoked. | Replace it with an active key. |
136
+ | `INACTIVE_API_KEY` | The key was deactivated. | Reactivate it or replace it. |
137
+ | `INVALID_PAYLOAD` | A field failed local or server validation. | Correct the named field; check integer token counts and metadata size. |
138
+ | `QUOTA_EXCEEDED` | The event allowance or pay-per-event credits are exhausted. | Wait for reset, add credits, or upgrade. |
139
+ | `PLAN_INACTIVE` | Billing status prevents ingestion. | Restore billing and reactivate the plan. |
140
+ | `NETWORK_ERROR` | Network/timeout retries were exhausted, or an unclassified HTTP/server failure occurred. | Check connectivity, `base_url`, timeout, and service availability. |
141
+
142
+ `track_sync()` raises these errors. `track()` sends them to `on_error` and never raises.
143
+
144
+ ## Warnings and free-plan key binding
145
+
146
+ | Response | Meaning |
147
+ |---|---|
148
+ | `dashboard_visible: false` | The event was accepted and billed/consumed quota, but is hidden because its source does not match the free-key binding. |
149
+ | `pricing_status: unknown_model` | No active pricing matched the provider/model, so cost is `0`; token usage is still recorded. |
150
+
151
+ **The most common reason events do not appear:** free-plan keys bind to one **provider/model/feature triple**. Mismatched events are accepted and billed (or consume quota) but hidden. Match all three values to the binding shown in LLMtrack. `NOT_DASHBOARD_VISIBLE` includes the bound and submitted triples.
152
+
153
+ ## Troubleshooting
154
+
155
+ ### Events are not appearing
156
+
157
+ Check warnings for `NOT_DASHBOARD_VISIBLE` and compare provider, model, and feature with the key binding. A short-lived process can exit before the daemon thread finishes; use `await track_sync()` in scripts and jobs that must confirm delivery.
158
+
159
+ ### Cost shows 0
160
+
161
+ Check for `UNKNOWN_MODEL` or `pricing_status: unknown_model`. Use the exact `response.model` returned by the provider so active pricing can match it.
162
+
163
+ ### The key is rejected
164
+
165
+ Inspect `LLMtrackError.code`: replace invalid/revoked keys, reactivate inactive keys, and confirm the environment variable is available to the server process. Use `await track_sync()` to inspect the complete exception.
166
+
167
+ ## Contributing
168
+
169
+ Async tests use `pytest-asyncio` (included in the `test` and `dev` extras):
170
+
171
+ ```sh
172
+ pip install -e 'packages/python[test]'
173
+ pytest packages/python/tests
174
+ ```
175
+
176
+ ## More information
177
+
178
+ - [LLMtrack documentation](https://llm-track.com/docs)
179
+ - [MIT License](./LICENSE)
@@ -0,0 +1,67 @@
1
+ """Opt-in live checks. Requires a dedicated LLMTRACK_API_KEY."""
2
+ import asyncio
3
+ import os
4
+ import uuid
5
+
6
+ from llmtrack_sdk import LLMtrack, LLMtrackError
7
+
8
+ API_KEY = os.environ.get("LLMTRACK_API_KEY")
9
+ if not API_KEY:
10
+ raise SystemExit("FAIL setup: LLMTRACK_API_KEY is required")
11
+ EVENT = dict(provider="openai", model="gpt-4o-mini", prompt_tokens=2, completion_tokens=1,
12
+ reasoning_tokens=1, feature="sdk-integration")
13
+ failures = 0
14
+
15
+ async def check(name, operation):
16
+ global failures
17
+ try:
18
+ await operation()
19
+ print(f"PASS {name}")
20
+ except Exception as exc:
21
+ failures += 1
22
+ print(f"FAIL {name}: {exc}")
23
+
24
+ async def callback_error(client):
25
+ loop = asyncio.get_running_loop()
26
+ done = loop.create_future()
27
+ def receive(error):
28
+ loop.call_soon_threadsafe(lambda: None if done.done() else done.set_result(error))
29
+ client.on_error = receive
30
+ client.track(**EVENT)
31
+ return await asyncio.wait_for(done, 7)
32
+
33
+ async def main():
34
+ client = LLMtrack(api_key=API_KEY)
35
+ await check("happy path with reasoning tokens", lambda: client.track_sync(**EVENT))
36
+ async def invalid():
37
+ error = await callback_error(LLMtrack(api_key="invalid-integration-key", max_retries=1))
38
+ assert error.code == "INVALID_API_KEY", error.code
39
+ await check("invalid key is reported and track never raises", invalid)
40
+ async def visibility():
41
+ warnings=[]; c=LLMtrack(api_key=API_KEY, environment=f"sdk-mismatch-{uuid.uuid4()}", on_warning=warnings.append)
42
+ await c.track_sync(**EVENT)
43
+ assert any(w.code == "NOT_DASHBOARD_VISIBLE" for w in warnings), "use a free-plan test key bound to another environment"
44
+ await check("free-plan visibility warning", visibility)
45
+ async def pricing():
46
+ warnings=[]; c=LLMtrack(api_key=API_KEY, on_warning=warnings.append)
47
+ await c.track_sync(**{**EVENT,"model":f"unknown-integration-{uuid.uuid4()}"})
48
+ assert any(w.code == "UNKNOWN_MODEL" for w in warnings), "missing UNKNOWN_MODEL warning"
49
+ await check("unknown-model pricing warning", pricing)
50
+ async def invalid_payload(**changes):
51
+ try: await client.track_sync(**{**EVENT,**changes})
52
+ except LLMtrackError as exc:
53
+ assert exc.code == "INVALID_PAYLOAD"; return
54
+ raise AssertionError("expected INVALID_PAYLOAD")
55
+ await check("oversized metadata is rejected client-side", lambda: invalid_payload(metadata={"value":"x"*8193}))
56
+ await check("negative tokens are rejected client-side", lambda: invalid_payload(prompt_tokens=-1))
57
+ async def duplicate():
58
+ key=str(uuid.uuid4()); await client.track_sync(**EVENT,idempotency_key=key)
59
+ second=await client.track_sync(**EVENT,idempotency_key=key); assert second["duplicate"] is True
60
+ await check("same idempotency key is a successful duplicate", duplicate)
61
+ async def network():
62
+ error=await callback_error(LLMtrack(api_key=API_KEY,base_url="http://127.0.0.1:1",timeout_ms=250,max_retries=1))
63
+ assert error.code == "NETWORK_ERROR", error.code
64
+ await check("unreachable network failure is reported without raising", network)
65
+
66
+ asyncio.run(main())
67
+ raise SystemExit(1 if failures else 0)
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "llmtrack-sdk"
7
+ version = "0.1.0"
8
+ description = "Server-side SDK for tracking LLM token usage, costs, and observability data in LLMtrack"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ dependencies = ["httpx>=0.27,<1"]
13
+ keywords = ["llm", "openai", "anthropic", "cost", "cost-tracking", "observability", "tokens", "ai"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://llm-track.com"
17
+ Documentation = "https://llm-track.com/docs"
18
+ Repository = "https://github.com/othmanemer9-cloud/llmtrack-sdks"
19
+ Issues = "https://github.com/othmanemer9-cloud/llmtrack-sdks/issues"
20
+
21
+ [project.optional-dependencies]
22
+ test = ["pytest>=8", "pytest-asyncio>=0.24"]
23
+ dev = ["pytest>=8", "pytest-asyncio>=0.24", "build>=1"]
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/llmtrack_sdk"]
27
+
28
+ [tool.pytest.ini_options]
29
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ """Public LLMtrack SDK API."""
2
+ from .client import LLMtrack, LLMtrackError, LLMtrackWarning
3
+
4
+ __all__ = ["LLMtrack", "LLMtrackError", "LLMtrackWarning"]
@@ -0,0 +1,34 @@
1
+ # NOTE: This file is auto generated by OpenAPI Generator.
2
+ # URL: https://openapi-generator.tech
3
+ #
4
+ # ref: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
5
+
6
+ name: llmtrack_generated Python package
7
+
8
+ on: [push, pull_request]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ build:
15
+
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ matrix:
19
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v4
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+ - name: Install dependencies
28
+ run: |
29
+ python -m pip install --upgrade pip
30
+ pip install -r requirements.txt
31
+ pip install -r test-requirements.txt
32
+ - name: Test with pytest
33
+ run: |
34
+ pytest --cov=llmtrack_generated
@@ -0,0 +1,66 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ env/
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ *.egg-info/
24
+ .installed.cfg
25
+ *.egg
26
+
27
+ # PyInstaller
28
+ # Usually these files are written by a python script from a template
29
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
30
+ *.manifest
31
+ *.spec
32
+
33
+ # Installer logs
34
+ pip-log.txt
35
+ pip-delete-this-directory.txt
36
+
37
+ # Unit test / coverage reports
38
+ htmlcov/
39
+ .tox/
40
+ .coverage
41
+ .coverage.*
42
+ .cache
43
+ nosetests.xml
44
+ coverage.xml
45
+ *,cover
46
+ .hypothesis/
47
+ venv/
48
+ .venv/
49
+ .python-version
50
+ .pytest_cache
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+
59
+ # Sphinx documentation
60
+ docs/_build/
61
+
62
+ # PyBuilder
63
+ target/
64
+
65
+ # Ipython Notebook
66
+ .ipynb_checkpoints
@@ -0,0 +1,31 @@
1
+ # NOTE: This file is auto generated by OpenAPI Generator.
2
+ # URL: https://openapi-generator.tech
3
+ #
4
+ # ref: https://docs.gitlab.com/ee/ci/README.html
5
+ # ref: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml
6
+
7
+ stages:
8
+ - test
9
+
10
+ .pytest:
11
+ stage: test
12
+ script:
13
+ - pip install -r requirements.txt
14
+ - pip install -r test-requirements.txt
15
+ - pytest --cov=llmtrack_generated
16
+
17
+ pytest-3.10:
18
+ extends: .pytest
19
+ image: python:3.10-alpine
20
+ pytest-3.11:
21
+ extends: .pytest
22
+ image: python:3.11-alpine
23
+ pytest-3.12:
24
+ extends: .pytest
25
+ image: python:3.12-alpine
26
+ pytest-3.13:
27
+ extends: .pytest
28
+ image: python:3.13-alpine
29
+ pytest-3.14:
30
+ extends: .pytest
31
+ image: python:3.14-alpine