tokenwise-sdk 0.1.1__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.
- tokenwise_sdk-0.1.1/.gitignore +221 -0
- tokenwise_sdk-0.1.1/PKG-INFO +149 -0
- tokenwise_sdk-0.1.1/README.md +121 -0
- tokenwise_sdk-0.1.1/pyproject.toml +47 -0
- tokenwise_sdk-0.1.1/tests/__init__.py +0 -0
- tokenwise_sdk-0.1.1/tests/conftest.py +250 -0
- tokenwise_sdk-0.1.1/tests/test_anthropic.py +124 -0
- tokenwise_sdk-0.1.1/tests/test_client.py +166 -0
- tokenwise_sdk-0.1.1/tests/test_config.py +44 -0
- tokenwise_sdk-0.1.1/tests/test_openai.py +112 -0
- tokenwise_sdk-0.1.1/tests/test_privacy.py +89 -0
- tokenwise_sdk-0.1.1/tokenwise/__init__.py +55 -0
- tokenwise_sdk-0.1.1/tokenwise/_capture.py +90 -0
- tokenwise_sdk-0.1.1/tokenwise/_version.py +3 -0
- tokenwise_sdk-0.1.1/tokenwise/anthropic.py +226 -0
- tokenwise_sdk-0.1.1/tokenwise/client.py +167 -0
- tokenwise_sdk-0.1.1/tokenwise/config.py +66 -0
- tokenwise_sdk-0.1.1/tokenwise/event.py +31 -0
- tokenwise_sdk-0.1.1/tokenwise/openai.py +243 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
!frontend/lib/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py.cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
# Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# UV
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# uv.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
# poetry.lock
|
|
110
|
+
# poetry.toml
|
|
111
|
+
|
|
112
|
+
# pdm
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
114
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
115
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
116
|
+
# pdm.lock
|
|
117
|
+
# pdm.toml
|
|
118
|
+
.pdm-python
|
|
119
|
+
.pdm-build/
|
|
120
|
+
|
|
121
|
+
# pixi
|
|
122
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
123
|
+
# pixi.lock
|
|
124
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
125
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
126
|
+
.pixi
|
|
127
|
+
|
|
128
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
129
|
+
__pypackages__/
|
|
130
|
+
|
|
131
|
+
# Celery stuff
|
|
132
|
+
celerybeat-schedule
|
|
133
|
+
celerybeat.pid
|
|
134
|
+
|
|
135
|
+
# Redis
|
|
136
|
+
*.rdb
|
|
137
|
+
*.aof
|
|
138
|
+
*.pid
|
|
139
|
+
|
|
140
|
+
# RabbitMQ
|
|
141
|
+
mnesia/
|
|
142
|
+
rabbitmq/
|
|
143
|
+
rabbitmq-data/
|
|
144
|
+
|
|
145
|
+
# ActiveMQ
|
|
146
|
+
activemq-data/
|
|
147
|
+
|
|
148
|
+
# SageMath parsed files
|
|
149
|
+
*.sage.py
|
|
150
|
+
|
|
151
|
+
# Environments
|
|
152
|
+
.env
|
|
153
|
+
.envrc
|
|
154
|
+
.venv
|
|
155
|
+
env/
|
|
156
|
+
venv/
|
|
157
|
+
ENV/
|
|
158
|
+
env.bak/
|
|
159
|
+
venv.bak/
|
|
160
|
+
|
|
161
|
+
# Spyder project settings
|
|
162
|
+
.spyderproject
|
|
163
|
+
.spyproject
|
|
164
|
+
|
|
165
|
+
# Rope project settings
|
|
166
|
+
.ropeproject
|
|
167
|
+
|
|
168
|
+
# mkdocs documentation
|
|
169
|
+
/site
|
|
170
|
+
|
|
171
|
+
# mypy
|
|
172
|
+
.mypy_cache/
|
|
173
|
+
.dmypy.json
|
|
174
|
+
dmypy.json
|
|
175
|
+
|
|
176
|
+
# Pyre type checker
|
|
177
|
+
.pyre/
|
|
178
|
+
|
|
179
|
+
# pytype static type analyzer
|
|
180
|
+
.pytype/
|
|
181
|
+
|
|
182
|
+
# Cython debug symbols
|
|
183
|
+
cython_debug/
|
|
184
|
+
|
|
185
|
+
# PyCharm
|
|
186
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
187
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
188
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
189
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
190
|
+
# .idea/
|
|
191
|
+
|
|
192
|
+
# Abstra
|
|
193
|
+
# Abstra is an AI-powered process automation framework.
|
|
194
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
195
|
+
# Learn more at https://abstra.io/docs
|
|
196
|
+
.abstra/
|
|
197
|
+
|
|
198
|
+
# Visual Studio Code
|
|
199
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
200
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
201
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
202
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
203
|
+
# .vscode/
|
|
204
|
+
# Temporary file for partial code execution
|
|
205
|
+
tempCodeRunnerFile.py
|
|
206
|
+
|
|
207
|
+
# Ruff stuff:
|
|
208
|
+
.ruff_cache/
|
|
209
|
+
|
|
210
|
+
# PyPI configuration file
|
|
211
|
+
.pypirc
|
|
212
|
+
|
|
213
|
+
# Marimo
|
|
214
|
+
marimo/_static/
|
|
215
|
+
marimo/_lsp/
|
|
216
|
+
__marimo__/
|
|
217
|
+
|
|
218
|
+
# Streamlit
|
|
219
|
+
.streamlit/secrets.toml
|
|
220
|
+
|
|
221
|
+
test_sdk.py
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tokenwise-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Metadata-only usage tracking for Anthropic and OpenAI — swap one import line.
|
|
5
|
+
Project-URL: Homepage, https://tokenwise.io
|
|
6
|
+
Project-URL: Documentation, https://docs.tokenwise.io
|
|
7
|
+
Author: Tokenwise
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: anthropic,cost,llm,observability,openai,tokens,usage
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: httpx>=0.23
|
|
19
|
+
Provides-Extra: anthropic
|
|
20
|
+
Requires-Dist: anthropic>=0.40; extra == 'anthropic'
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: anthropic>=0.40; extra == 'dev'
|
|
23
|
+
Requires-Dist: openai>=1.40; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Provides-Extra: openai
|
|
26
|
+
Requires-Dist: openai>=1.40; extra == 'openai'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# Tokenwise Python SDK
|
|
30
|
+
|
|
31
|
+
Metadata-only usage tracking for Anthropic and OpenAI. Swap **one import line**
|
|
32
|
+
and every API call's token counts and latency flow to your Tokenwise dashboard
|
|
33
|
+
— with **zero access to your prompts or responses**.
|
|
34
|
+
|
|
35
|
+
```diff
|
|
36
|
+
- from anthropic import Anthropic
|
|
37
|
+
+ from tokenwise import Anthropic
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Your code is otherwise unchanged: the wrapper exposes the identical interface,
|
|
41
|
+
forwards every call to the official SDK, and returns its response untouched.
|
|
42
|
+
|
|
43
|
+
## Why it's safe
|
|
44
|
+
|
|
45
|
+
- **Metadata only.** The SDK reads exactly: `model`, `input_tokens`,
|
|
46
|
+
`output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`,
|
|
47
|
+
`latency_ms`, `timestamp`, `endpoint`. It never reads or transmits prompt
|
|
48
|
+
text, response text, system prompts, or tool definitions. (Contrast with
|
|
49
|
+
proxy-based tools, which see all your traffic.)
|
|
50
|
+
- **Non-blocking.** Events are queued and sent on a background daemon thread.
|
|
51
|
+
If Tokenwise is slow or down, your AI calls complete normally.
|
|
52
|
+
- **Fail-silent + bounded.** Up to 1,000 events buffer when offline; the oldest
|
|
53
|
+
drop silently if the buffer fills. Capture never raises, never waits.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install tokenwise-sdk[anthropic] # if you use Anthropic
|
|
59
|
+
pip install tokenwise-sdk[openai] # if you use OpenAI
|
|
60
|
+
pip install tokenwise-sdk[anthropic,openai]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`anthropic` and `openai` are optional extras — install only what you use.
|
|
64
|
+
|
|
65
|
+
## Configure
|
|
66
|
+
|
|
67
|
+
Set your Tokenwise key (from the dashboard, looks like `tw_...`):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export TOKENWISE_API_KEY=tw_your_key
|
|
71
|
+
# optional:
|
|
72
|
+
export TOKENWISE_API_URL=https://tokenwise-production-aa59.up.railway.app # default
|
|
73
|
+
export TOKENWISE_DISABLED=true # emergency kill switch
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Precedence for every setting: constructor argument > environment variable >
|
|
77
|
+
default. If no key is configured the SDK runs disabled and your AI calls behave
|
|
78
|
+
exactly as the official SDK.
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# Pattern 1 — key from environment
|
|
84
|
+
import os
|
|
85
|
+
os.environ["TOKENWISE_API_KEY"] = "tw_abc123"
|
|
86
|
+
from tokenwise import Anthropic
|
|
87
|
+
client = Anthropic(api_key="sk-ant-...")
|
|
88
|
+
msg = client.messages.create(
|
|
89
|
+
model="claude-sonnet-4-6",
|
|
90
|
+
max_tokens=256,
|
|
91
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Pattern 2 — key passed explicitly
|
|
95
|
+
from tokenwise import Anthropic
|
|
96
|
+
client = Anthropic(api_key="sk-ant-...", tokenwise_key="tw_abc123")
|
|
97
|
+
|
|
98
|
+
# Pattern 3 — OpenAI
|
|
99
|
+
from tokenwise import OpenAI
|
|
100
|
+
client = OpenAI(api_key="sk-...", tokenwise_key="tw_abc123")
|
|
101
|
+
client.chat.completions.create(
|
|
102
|
+
model="gpt-5.4",
|
|
103
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Streaming and async work the same way:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Streaming (sync) — usage captured on stream completion
|
|
111
|
+
with client.messages.create(..., stream=True) as stream:
|
|
112
|
+
for event in stream:
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
# Async
|
|
116
|
+
from tokenwise import AsyncAnthropic
|
|
117
|
+
client = AsyncAnthropic(api_key="sk-ant-...", tokenwise_key="tw_abc123")
|
|
118
|
+
msg = await client.messages.create(...)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## What's instrumented (v1)
|
|
122
|
+
|
|
123
|
+
| Provider | Method | Streaming |
|
|
124
|
+
|----------|--------|-----------|
|
|
125
|
+
| Anthropic | `messages.create` | ✅ usage read from the event stream (request unchanged) |
|
|
126
|
+
| OpenAI | `chat.completions.create` | ✅ see note below |
|
|
127
|
+
|
|
128
|
+
Other methods pass through and work, but aren't yet recorded. (OpenAI Responses
|
|
129
|
+
API and legacy completions are planned.)
|
|
130
|
+
|
|
131
|
+
### Note on OpenAI streaming
|
|
132
|
+
|
|
133
|
+
OpenAI only returns token usage on a streamed response when the request includes
|
|
134
|
+
`stream_options={"include_usage": True}`. When you stream **without** supplying
|
|
135
|
+
your own `stream_options`, Tokenwise injects it for you so usage can be captured.
|
|
136
|
+
This adds one final usage-only chunk (with an empty `choices` list) to the
|
|
137
|
+
stream. If you already pass `stream_options`, Tokenwise respects yours and does
|
|
138
|
+
not modify the request (in that case usage is captured only if you enabled it).
|
|
139
|
+
|
|
140
|
+
### Latency semantics
|
|
141
|
+
|
|
142
|
+
For non-streaming calls, `latency_ms` is the wall-clock time of the call. For
|
|
143
|
+
streaming calls it is the **total stream duration** (until the last chunk is
|
|
144
|
+
consumed), which includes time your code spends between chunks — events from
|
|
145
|
+
streaming calls carry `streamed: true` so this is distinguishable.
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Tokenwise Python SDK
|
|
2
|
+
|
|
3
|
+
Metadata-only usage tracking for Anthropic and OpenAI. Swap **one import line**
|
|
4
|
+
and every API call's token counts and latency flow to your Tokenwise dashboard
|
|
5
|
+
— with **zero access to your prompts or responses**.
|
|
6
|
+
|
|
7
|
+
```diff
|
|
8
|
+
- from anthropic import Anthropic
|
|
9
|
+
+ from tokenwise import Anthropic
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Your code is otherwise unchanged: the wrapper exposes the identical interface,
|
|
13
|
+
forwards every call to the official SDK, and returns its response untouched.
|
|
14
|
+
|
|
15
|
+
## Why it's safe
|
|
16
|
+
|
|
17
|
+
- **Metadata only.** The SDK reads exactly: `model`, `input_tokens`,
|
|
18
|
+
`output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`,
|
|
19
|
+
`latency_ms`, `timestamp`, `endpoint`. It never reads or transmits prompt
|
|
20
|
+
text, response text, system prompts, or tool definitions. (Contrast with
|
|
21
|
+
proxy-based tools, which see all your traffic.)
|
|
22
|
+
- **Non-blocking.** Events are queued and sent on a background daemon thread.
|
|
23
|
+
If Tokenwise is slow or down, your AI calls complete normally.
|
|
24
|
+
- **Fail-silent + bounded.** Up to 1,000 events buffer when offline; the oldest
|
|
25
|
+
drop silently if the buffer fills. Capture never raises, never waits.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install tokenwise-sdk[anthropic] # if you use Anthropic
|
|
31
|
+
pip install tokenwise-sdk[openai] # if you use OpenAI
|
|
32
|
+
pip install tokenwise-sdk[anthropic,openai]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`anthropic` and `openai` are optional extras — install only what you use.
|
|
36
|
+
|
|
37
|
+
## Configure
|
|
38
|
+
|
|
39
|
+
Set your Tokenwise key (from the dashboard, looks like `tw_...`):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export TOKENWISE_API_KEY=tw_your_key
|
|
43
|
+
# optional:
|
|
44
|
+
export TOKENWISE_API_URL=https://tokenwise-production-aa59.up.railway.app # default
|
|
45
|
+
export TOKENWISE_DISABLED=true # emergency kill switch
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Precedence for every setting: constructor argument > environment variable >
|
|
49
|
+
default. If no key is configured the SDK runs disabled and your AI calls behave
|
|
50
|
+
exactly as the official SDK.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# Pattern 1 — key from environment
|
|
56
|
+
import os
|
|
57
|
+
os.environ["TOKENWISE_API_KEY"] = "tw_abc123"
|
|
58
|
+
from tokenwise import Anthropic
|
|
59
|
+
client = Anthropic(api_key="sk-ant-...")
|
|
60
|
+
msg = client.messages.create(
|
|
61
|
+
model="claude-sonnet-4-6",
|
|
62
|
+
max_tokens=256,
|
|
63
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Pattern 2 — key passed explicitly
|
|
67
|
+
from tokenwise import Anthropic
|
|
68
|
+
client = Anthropic(api_key="sk-ant-...", tokenwise_key="tw_abc123")
|
|
69
|
+
|
|
70
|
+
# Pattern 3 — OpenAI
|
|
71
|
+
from tokenwise import OpenAI
|
|
72
|
+
client = OpenAI(api_key="sk-...", tokenwise_key="tw_abc123")
|
|
73
|
+
client.chat.completions.create(
|
|
74
|
+
model="gpt-5.4",
|
|
75
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Streaming and async work the same way:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# Streaming (sync) — usage captured on stream completion
|
|
83
|
+
with client.messages.create(..., stream=True) as stream:
|
|
84
|
+
for event in stream:
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
# Async
|
|
88
|
+
from tokenwise import AsyncAnthropic
|
|
89
|
+
client = AsyncAnthropic(api_key="sk-ant-...", tokenwise_key="tw_abc123")
|
|
90
|
+
msg = await client.messages.create(...)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## What's instrumented (v1)
|
|
94
|
+
|
|
95
|
+
| Provider | Method | Streaming |
|
|
96
|
+
|----------|--------|-----------|
|
|
97
|
+
| Anthropic | `messages.create` | ✅ usage read from the event stream (request unchanged) |
|
|
98
|
+
| OpenAI | `chat.completions.create` | ✅ see note below |
|
|
99
|
+
|
|
100
|
+
Other methods pass through and work, but aren't yet recorded. (OpenAI Responses
|
|
101
|
+
API and legacy completions are planned.)
|
|
102
|
+
|
|
103
|
+
### Note on OpenAI streaming
|
|
104
|
+
|
|
105
|
+
OpenAI only returns token usage on a streamed response when the request includes
|
|
106
|
+
`stream_options={"include_usage": True}`. When you stream **without** supplying
|
|
107
|
+
your own `stream_options`, Tokenwise injects it for you so usage can be captured.
|
|
108
|
+
This adds one final usage-only chunk (with an empty `choices` list) to the
|
|
109
|
+
stream. If you already pass `stream_options`, Tokenwise respects yours and does
|
|
110
|
+
not modify the request (in that case usage is captured only if you enabled it).
|
|
111
|
+
|
|
112
|
+
### Latency semantics
|
|
113
|
+
|
|
114
|
+
For non-streaming calls, `latency_ms` is the wall-clock time of the call. For
|
|
115
|
+
streaming calls it is the **total stream duration** (until the last chunk is
|
|
116
|
+
consumed), which includes time your code spends between chunks — events from
|
|
117
|
+
streaming calls carry `streamed: true` so this is distinguishable.
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tokenwise-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Metadata-only usage tracking for Anthropic and OpenAI — swap one import line."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Tokenwise" }]
|
|
13
|
+
keywords = ["anthropic", "openai", "llm", "observability", "cost", "usage", "tokens"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3.9",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
]
|
|
23
|
+
# Core dependency only. The provider SDKs are optional extras — you install
|
|
24
|
+
# whichever you actually use. httpx is already pulled in by both anyway.
|
|
25
|
+
dependencies = ["httpx>=0.23"]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
anthropic = ["anthropic>=0.40"]
|
|
29
|
+
openai = ["openai>=1.40"]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.0",
|
|
32
|
+
"anthropic>=0.40",
|
|
33
|
+
"openai>=1.40",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://tokenwise.io"
|
|
38
|
+
Documentation = "https://docs.tokenwise.io"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.version]
|
|
41
|
+
path = "tokenwise/_version.py"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["tokenwise"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
File without changes
|