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.
@@ -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