tokenprof 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 (46) hide show
  1. tokenprof-0.1.0/.github/ISSUE_TEMPLATE/adapter-or-payload.md +23 -0
  2. tokenprof-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  3. tokenprof-0.1.0/.github/pull_request_template.md +15 -0
  4. tokenprof-0.1.0/.github/workflows/ci.yml +59 -0
  5. tokenprof-0.1.0/.github/workflows/release.yml +54 -0
  6. tokenprof-0.1.0/.gitignore +16 -0
  7. tokenprof-0.1.0/CODE_OF_CONDUCT.md +36 -0
  8. tokenprof-0.1.0/CONTRIBUTING.md +60 -0
  9. tokenprof-0.1.0/LICENSE +21 -0
  10. tokenprof-0.1.0/PKG-INFO +202 -0
  11. tokenprof-0.1.0/README.md +165 -0
  12. tokenprof-0.1.0/RELEASING.md +36 -0
  13. tokenprof-0.1.0/SECURITY.md +19 -0
  14. tokenprof-0.1.0/docs/demo.svg +1 -0
  15. tokenprof-0.1.0/pyproject.toml +56 -0
  16. tokenprof-0.1.0/src/tokenprof/__init__.py +16 -0
  17. tokenprof-0.1.0/src/tokenprof/adapters/__init__.py +45 -0
  18. tokenprof-0.1.0/src/tokenprof/adapters/anthropic_messages.py +136 -0
  19. tokenprof-0.1.0/src/tokenprof/adapters/base.py +59 -0
  20. tokenprof-0.1.0/src/tokenprof/adapters/openai_chat.py +107 -0
  21. tokenprof-0.1.0/src/tokenprof/attribute.py +93 -0
  22. tokenprof-0.1.0/src/tokenprof/cache.py +108 -0
  23. tokenprof-0.1.0/src/tokenprof/cli.py +162 -0
  24. tokenprof-0.1.0/src/tokenprof/cost.py +74 -0
  25. tokenprof-0.1.0/src/tokenprof/diff.py +69 -0
  26. tokenprof-0.1.0/src/tokenprof/py.typed +0 -0
  27. tokenprof-0.1.0/src/tokenprof/record.py +135 -0
  28. tokenprof-0.1.0/src/tokenprof/report/__init__.py +11 -0
  29. tokenprof-0.1.0/src/tokenprof/report/json_out.py +82 -0
  30. tokenprof-0.1.0/src/tokenprof/report/table.py +222 -0
  31. tokenprof-0.1.0/src/tokenprof/tokenizer.py +72 -0
  32. tokenprof-0.1.0/src/tokenprof/types.py +134 -0
  33. tokenprof-0.1.0/tests/conftest.py +26 -0
  34. tokenprof-0.1.0/tests/fixtures/anthropic_turn.json +96 -0
  35. tokenprof-0.1.0/tests/fixtures/anthropic_turn.jsonl +1 -0
  36. tokenprof-0.1.0/tests/fixtures/cache_thrash.jsonl +4 -0
  37. tokenprof-0.1.0/tests/fixtures/openai_session.jsonl +3 -0
  38. tokenprof-0.1.0/tests/test_adapters.py +160 -0
  39. tokenprof-0.1.0/tests/test_attribute.py +91 -0
  40. tokenprof-0.1.0/tests/test_cache.py +89 -0
  41. tokenprof-0.1.0/tests/test_cli.py +87 -0
  42. tokenprof-0.1.0/tests/test_cost.py +48 -0
  43. tokenprof-0.1.0/tests/test_diff.py +55 -0
  44. tokenprof-0.1.0/tests/test_record.py +80 -0
  45. tokenprof-0.1.0/tests/test_tokenizer.py +26 -0
  46. tokenprof-0.1.0/tests/test_types.py +61 -0
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: Adapter request or unreadable payload
3
+ about: tokenprof cannot read your logs, or you want support for a framework
4
+ title: "Adapter: <framework or provider>"
5
+ labels: adapter
6
+ ---
7
+
8
+ **Framework or provider**
9
+
10
+ **What happened**
11
+ The error, or the wrong numbers you got.
12
+
13
+ **Redacted sample**
14
+ One request payload, with prompt and tool result text replaced by filler of
15
+ roughly the same length. Structure is what the parser needs and length is what
16
+ the profiler needs, so a redacted sample is just as useful as a real one.
17
+
18
+ ```json
19
+
20
+ ```
21
+
22
+ **Anything unusual about the shape**
23
+ Custom message roles, nested content blocks, non-standard tool spec format.
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Adapter request
4
+ url: https://github.com/muhammadwaqar12/tokenprof/issues/new?template=adapter-or-payload.md
5
+ about: tokenprof cannot read your logs, or you want support for a framework
@@ -0,0 +1,15 @@
1
+ **What this changes**
2
+
3
+ **Why**
4
+
5
+ ---
6
+
7
+ - [ ] `pytest -q` passes
8
+ - [ ] `ruff check .` and `ruff format --check .` pass
9
+ - [ ] Added the test that would fail without this change
10
+
11
+ **If this adds an adapter**
12
+
13
+ - [ ] `matches` is exclusive (the detection test asserts exactly one adapter claims a payload)
14
+ - [ ] Segments carry names, so the per-tool breakdown stays useful
15
+ - [ ] Added a fixture under `tests/fixtures/`
@@ -0,0 +1,59 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - run: python -m pip install --upgrade pip
24
+ - run: pip install -e ".[dev]"
25
+ - run: pytest -q
26
+
27
+ tokenizer:
28
+ # The heuristic path is the default, so the exact-tokenizer path needs
29
+ # its own run or it silently rots.
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: "3.12"
36
+ - run: pip install -e ".[dev,tiktoken]"
37
+ - run: pytest -q
38
+
39
+ lint:
40
+ runs-on: ubuntu-latest
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+ - uses: actions/setup-python@v5
44
+ with:
45
+ python-version: "3.12"
46
+ - run: pip install ruff
47
+ - run: ruff check .
48
+ - run: ruff format --check .
49
+
50
+ build:
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - uses: actions/checkout@v4
54
+ - uses: actions/setup-python@v5
55
+ with:
56
+ python-version: "3.12"
57
+ - run: pip install build
58
+ - run: python -m build
59
+ - run: pip install dist/*.whl && tokenprof --version
@@ -0,0 +1,54 @@
1
+ name: release
2
+
3
+ # Publishing happens on a version tag, never on a push to main, so cutting a
4
+ # release is a deliberate act rather than a side effect of merging.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Refuse to release if the tag and the package disagree
22
+ run: |
23
+ TAG="${GITHUB_REF_NAME#v}"
24
+ PKG=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
25
+ echo "tag=$TAG package=$PKG"
26
+ [ "$TAG" = "$PKG" ] || { echo "::error::tag $TAG does not match version $PKG"; exit 1; }
27
+
28
+ - run: pip install build twine
29
+ - run: python -m build
30
+ - name: Check the metadata PyPI will render
31
+ run: twine check --strict dist/*
32
+ - name: Prove the wheel installs and runs
33
+ run: |
34
+ python -m venv /tmp/v
35
+ /tmp/v/bin/pip install dist/*.whl
36
+ /tmp/v/bin/tokenprof --version
37
+ /tmp/v/bin/tokenprof cache tests/fixtures/cache_thrash.jsonl | grep -q "what broke the prefix"
38
+ - uses: actions/upload-artifact@v4
39
+ with:
40
+ name: dist
41
+ path: dist/
42
+
43
+ publish:
44
+ needs: build
45
+ runs-on: ubuntu-latest
46
+ environment: pypi
47
+ permissions:
48
+ id-token: write # PyPI trusted publishing, no API token anywhere
49
+ steps:
50
+ - uses: actions/download-artifact@v4
51
+ with:
52
+ name: dist
53
+ path: dist/
54
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .DS_Store
13
+ *.jsonl
14
+ !tests/fixtures/*.jsonl
15
+ *.cast
16
+ .probe_*
@@ -0,0 +1,36 @@
1
+ # Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes
17
+ * Focusing on what is best for the overall community
18
+
19
+ Examples of unacceptable behavior:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or advances of any kind
22
+ * Trolling, insulting or derogatory comments, and personal or political attacks
23
+ * Public or private harassment
24
+ * Publishing others' private information, such as a physical or email address, without their explicit permission
25
+ * Other conduct which could reasonably be considered inappropriate in a professional setting
26
+
27
+ ## Enforcement
28
+
29
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the maintainer at m_waqar@live.com. All complaints will be reviewed and investigated promptly and fairly.
30
+
31
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, issues, and other contributions that are not aligned to this Code of Conduct.
32
+
33
+ ## Attribution
34
+
35
+ This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1, available at
36
+ https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
@@ -0,0 +1,60 @@
1
+ # Contributing
2
+
3
+ The two most useful things you can send: an adapter, or a payload that breaks detection.
4
+
5
+ ## Payloads that break detection
6
+
7
+ If `tokenprof` cannot read your logs, that is a bug and the fix is usually small. Open an issue with a **redacted** sample: replace the actual prompt and tool result text with filler of roughly the same length, keep the structure. Structure is all the parser cares about, and length is all the profiler cares about, so a redacted sample is just as useful as a real one and safe to post.
8
+
9
+ ## Writing an adapter
10
+
11
+ An adapter has three methods and no framework dependency in the core:
12
+
13
+ ```python
14
+ class MyAdapter:
15
+ name = "my_provider"
16
+
17
+ def matches(self, request: dict) -> bool: ...
18
+ def model_of(self, request: dict) -> str: ...
19
+ def segments(self, request: dict, tok: Tokenizer) -> list[Segment]: ...
20
+ ```
21
+
22
+ Add the module under `src/tokenprof/adapters/`, register it in `ADAPTERS`, and add a fixture under `tests/fixtures/`.
23
+
24
+ Two rules that matter more than they look:
25
+
26
+ **`matches` must be exclusive.** There is a test asserting exactly one adapter claims any given payload. If two match, registry ordering silently decides correctness, and that bug surfaces months later as wrong numbers rather than an error. Discriminate on a field only your provider has.
27
+
28
+ **Emit segments in prompt order.** Cache prefixes are computed over the order you emit, so it has to match the order the provider actually assembles the prompt. Anthropic is tools, then system, then messages. Getting this wrong puts the cache break point in the wrong place and the numbers look plausible while being wrong.
29
+
30
+ **Name your segments.** A tool schema segment named `search_docs` is actionable. An unnamed one is trivia. The per-tool breakdown is the most useful view in the tool, and it only works if adapters carry names through.
31
+
32
+ **Carry the digest through.** `measure()` returns `(tokens, chars, digest)`. A segment without a digest can never count as a cache hit, which is the safe default, but it also means `tokenprof cache` goes blind on your provider. Pass it into every `Segment` you build.
33
+
34
+ **Mark cached segments if your provider has explicit markers.** Anthropic uses `cache_control`, where the marker caches the whole prefix up to and including that block, so only the last marker matters. OpenAI caches automatically with no markers, so its adapter marks nothing and the prefix analysis does the work instead.
35
+
36
+ ## Categories
37
+
38
+ `Category` is deliberately small. A new one has to pass a simple test: could someone do something different in response to seeing it broken out? If the answer is no, it belongs in an existing bucket.
39
+
40
+ ## Tests
41
+
42
+ `pytest -q`. Everything runs offline with no API keys and no network.
43
+
44
+ Fixtures are hand-built rather than captured, so they can be committed without redaction and can encode the specific situations worth testing. The OpenAI fixture deliberately contains one bloated auto-generated tool and registers a new tool partway through the session, because those are the two cases the reports exist to catch.
45
+
46
+ If you add behavior, add the test that would fail without it. If you fix a bug, add the test that was missing.
47
+
48
+ ## Accuracy claims
49
+
50
+ Be careful with numbers in docs and output. The default tokenizer is an approximation and every report says so. If you add a counting path, make it report its own name, and do not describe approximate counts as exact anywhere in the interface.
51
+
52
+ ## Style
53
+
54
+ - `ruff check .` and `ruff format .` before opening a PR. CI runs both.
55
+ - Plain names. `fixed_overhead_tokens` over `calc_fo`.
56
+ - Comments explain why, not what. The code says what.
57
+
58
+ ## Review
59
+
60
+ Pull requests get looked at within a day or two. Ping the thread if yours goes quiet.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Waqar
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,202 @@
1
+ Metadata-Version: 2.5
2
+ Name: tokenprof
3
+ Version: 0.1.0
4
+ Summary: A profiler for the context window. See what is actually eating your tokens, per turn.
5
+ Project-URL: Homepage, https://github.com/muhammadwaqar12/tokenprof
6
+ Project-URL: Issues, https://github.com/muhammadwaqar12/tokenprof/issues
7
+ Author: Muhammad Waqar
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,context,cost,llm,observability,profiler,tokens
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Debuggers
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Provides-Extra: all
26
+ Requires-Dist: rich>=13.0; extra == 'all'
27
+ Requires-Dist: tiktoken>=0.7; extra == 'all'
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
30
+ Requires-Dist: pytest>=7.4; extra == 'dev'
31
+ Requires-Dist: ruff>=0.6; extra == 'dev'
32
+ Provides-Extra: rich
33
+ Requires-Dist: rich>=13.0; extra == 'rich'
34
+ Provides-Extra: tiktoken
35
+ Requires-Dist: tiktoken>=0.7; extra == 'tiktoken'
36
+ Description-Content-Type: text/markdown
37
+
38
+ <h3 align="center">A profiler for the context window. See what is actually eating your tokens, per turn.</h3>
39
+
40
+ <p align="center">
41
+ <a href="#quickstart">Quickstart</a> ·
42
+ <a href="#where-your-tokens-went">Attribution</a> ·
43
+ <a href="#is-your-prompt-cache-actually-working">Cache</a> ·
44
+ <a href="#recording-a-session">Recording</a> ·
45
+ <a href="#adapters">Adapters</a> ·
46
+ <a href="https://github.com/muhammadwaqar12/tokenprof/blob/main/CONTRIBUTING.md">Contribute</a>
47
+ </p>
48
+
49
+ <p align="center">
50
+ <a href="https://github.com/muhammadwaqar12/tokenprof/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-3a6b4d"></a>
51
+ <img alt="Python" src="https://img.shields.io/badge/python-3.9%2B-3a6b4d">
52
+ <img alt="Dependencies" src="https://img.shields.io/badge/dependencies-0-9a6a15">
53
+ <a href="https://github.com/muhammadwaqar12/tokenprof/blob/main/CONTRIBUTING.md"><img alt="PRs welcome" src="https://img.shields.io/badge/PRs-welcome-3a6b4d"></a>
54
+ </p>
55
+
56
+ ---
57
+
58
+ You know how many tokens your last request used, because the API told you. You almost certainly do not know **what they were**.
59
+
60
+ There are tools that compress your context and tools that retrieve into it. There is nothing that opens it up and shows you the bill line by line. So when a session gets slow and expensive, the usual move is to guess.
61
+
62
+ ## Quickstart
63
+
64
+ ```bash
65
+ pip install tokenprof # once released
66
+ pip install git+https://github.com/muhammadwaqar12/tokenprof # from source
67
+ ```
68
+
69
+ Run your program through `record`. It attaches to the OpenAI and Anthropic clients, writes every outgoing request to a file, and changes nothing about what your program does.
70
+
71
+ ```bash
72
+ tokenprof record -o session.jsonl -- python your_agent.py
73
+ tokenprof analyze session.jsonl
74
+ tokenprof cache session.jsonl
75
+ ```
76
+
77
+ No API keys, no config, no code changes. If you already log request payloads, skip `record` and point `analyze` at what you have.
78
+
79
+ ## Where your tokens went
80
+
81
+ ```
82
+ $ tokenprof analyze session.jsonl --turn 2
83
+
84
+ turn 2 model=gpt-4o provider=openai_chat
85
+ tokenizer: heuristic(chars/4)
86
+
87
+ category tokens share cost
88
+ -----------------------------------------------------------------------------
89
+ tool result 2,869 66.6% ###################......... $0.0072
90
+ tool schema 1,142 26.5% #######..................... $0.0029
91
+ system 205 4.8% #........................... $0.0005
92
+ history assistant 67 1.6% ............................ $0.0002
93
+ history user 14 0.3% ............................ $0.0000
94
+ current user 8 0.2% ............................ $0.0000
95
+ -----------------------------------------------------------------------------
96
+ TOTAL 4,305 100.0% $0.0108
97
+
98
+ fixed overhead (system + tool schemas): 1,347 tokens (31.3% of this turn), re-sent on every request
99
+
100
+ tool schemas (4 registered)
101
+ bloated_connector 912 ####..............
102
+ search_docs 85 ..................
103
+ trace_calls 82 ..................
104
+ read_file 63 ..................
105
+ ```
106
+
107
+ One auto-generated tool is eating 80% of the schema budget and is serialized into every request whether the model calls it or not. Nothing errors. Nothing in your dashboard says so.
108
+
109
+ The per-tool line is the part most tools cannot give you. Not "you have 40 tools registered" but **which ones**, ranked, in tokens.
110
+
111
+ ## Is your prompt cache actually working?
112
+
113
+ Providers cache a **prefix**. If the first N tokens of a request are byte-identical to the last one, you pay a fraction for them. One volatile value near the front moves the break point to zero, and you quietly pay full price on every turn while your config still says caching is on.
114
+
115
+ <p align="center">
116
+ <img src="https://raw.githubusercontent.com/muhammadwaqar12/tokenprof/main/docs/demo.svg" alt="tokenprof cache finding a broken prompt cache prefix" width="100%">
117
+ </p>
118
+
119
+ ```
120
+ $ tokenprof cache session.jsonl
121
+
122
+ turns stable total share what broke the prefix
123
+ --------------------------------------------------------------------------
124
+ 0 -> 1 68 248 27.4% system:system
125
+ 1 -> 2 68 302 22.5% system:system
126
+ 2 -> 3 68 356 19.1% system:system
127
+ --------------------------------------------------------------------------
128
+
129
+ reusable across turns: 204 tokens | re-sent after a break: 702
130
+
131
+ segments that broke the prefix
132
+ system:system 3x
133
+
134
+ WARNING: the prefix breaks early. Turn 3 reuses only 19.1% of its prompt.
135
+ Anything that changes near the front of the prompt costs you the whole
136
+ cache. Timestamps, session ids and reordered tool lists are the usual
137
+ causes. Move volatile content to the end.
138
+ ```
139
+
140
+ That session has a timestamp at the top of the system prompt. Every turn misses, and the tool names the segment responsible rather than just reporting a bad number.
141
+
142
+ The other common cause is subtler: **registering one new tool mid-session invalidates the whole prefix**, because tool schemas are serialized ahead of the messages. `tokenprof cache` catches that too.
143
+
144
+ Cost accounting follows the real pricing. Anthropic bills cache reads at 10% of base and OpenAI at 50%, so a turn with `cache_control` markers reports what it actually bills rather than a blended number that overstates exactly the segments you were smart enough to cache.
145
+
146
+ ## Recording a session
147
+
148
+ `record` writes a `sitecustomize` shim onto the path of the process you launch, wraps the client `create` methods, and calls through untouched. Two deliberate constraints: it never raises into your program, and it records requests only, never responses.
149
+
150
+ ```bash
151
+ tokenprof record --verbose -o session.jsonl -- python your_agent.py
152
+ # tokenprof: patched openai, anthropic
153
+ # tokenprof: captured 14 request(s) to session.jsonl
154
+ ```
155
+
156
+ If you would rather log payloads yourself, any JSONL of request objects works. Bare payloads and anything wrapping one under `request`, `body`, `payload` or `kwargs` are all accepted, and `-` reads stdin.
157
+
158
+ > [!NOTE]
159
+ > A recording contains your prompts and your tool results in full, which usually means credentials and customer data. The default `.gitignore` excludes `*.jsonl`. Redact before attaching one to an issue.
160
+
161
+ ## Adapters
162
+
163
+ An adapter turns one provider's payload shape into a list of segments. Three methods, no framework dependency in the core, which is why adding one is an afternoon rather than a refactor.
164
+
165
+ | Adapter | Status |
166
+ |---|---|
167
+ | `anthropic_messages` | Shipped, with `cache_control` support |
168
+ | `openai_chat` | Shipped |
169
+ | LangGraph / LangChain | Wanted |
170
+ | CrewAI | Wanted |
171
+ | Google ADK | Wanted |
172
+ | Bedrock Converse | Wanted |
173
+
174
+ Most frameworks ultimately emit an OpenAI or Anthropic payload, so the two shipped adapters already cover a lot of ground. Detection is automatic, and a test asserts exactly one adapter claims any payload. Pass `--provider` to override.
175
+
176
+ ## Accuracy
177
+
178
+ Every report states which tokenizer produced it, because the honest answer varies.
179
+
180
+ With `tiktoken` installed and an OpenAI model, counts are exact. Otherwise it falls back to `chars / 4`, which runs low on code and non-Latin scripts. That is fine for ranking segments against each other, which is what you are here for, and it is not a context budget.
181
+
182
+ Serialization is close but not exact. Providers wrap messages and schemas in their own formatting, and that wrapper is small next to the payload. Treat the numbers as accurate to a few percent and the ranking as reliable.
183
+
184
+ ## What this is not
185
+
186
+ Not a tracing tool. Langfuse, Phoenix and LangSmith show latency, spans and token *counts*, and do it well. None show token *composition*. Run both.
187
+
188
+ Not a compressor. It tells you what to cut. Cutting is your call.
189
+
190
+ Not a runtime guard. It reads recordings after the fact, on purpose, so a profiler can never sit in the path of a production request.
191
+
192
+ ## Contributing
193
+
194
+ The most useful contributions are adapters, and payload shapes that break detection. If `tokenprof` cannot read your logs, that is a bug and a redacted sample is the whole fix. See [CONTRIBUTING.md](https://github.com/muhammadwaqar12/tokenprof/blob/main/CONTRIBUTING.md).
195
+
196
+ ## License
197
+
198
+ [MIT](https://github.com/muhammadwaqar12/tokenprof/blob/main/LICENSE).
199
+
200
+ ---
201
+
202
+ <sub>Built by <a href="https://github.com/muhammadwaqar12">Muhammad Waqar</a>. Companion to <a href="https://github.com/muhammadwaqar12/awesome-agent-failures">awesome-agent-failures</a>, where CTX-03 covers the tool schema tax and COST-03 covers prompt cache thrash.</sub>
@@ -0,0 +1,165 @@
1
+ <h3 align="center">A profiler for the context window. See what is actually eating your tokens, per turn.</h3>
2
+
3
+ <p align="center">
4
+ <a href="#quickstart">Quickstart</a> ·
5
+ <a href="#where-your-tokens-went">Attribution</a> ·
6
+ <a href="#is-your-prompt-cache-actually-working">Cache</a> ·
7
+ <a href="#recording-a-session">Recording</a> ·
8
+ <a href="#adapters">Adapters</a> ·
9
+ <a href="https://github.com/muhammadwaqar12/tokenprof/blob/main/CONTRIBUTING.md">Contribute</a>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://github.com/muhammadwaqar12/tokenprof/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-3a6b4d"></a>
14
+ <img alt="Python" src="https://img.shields.io/badge/python-3.9%2B-3a6b4d">
15
+ <img alt="Dependencies" src="https://img.shields.io/badge/dependencies-0-9a6a15">
16
+ <a href="https://github.com/muhammadwaqar12/tokenprof/blob/main/CONTRIBUTING.md"><img alt="PRs welcome" src="https://img.shields.io/badge/PRs-welcome-3a6b4d"></a>
17
+ </p>
18
+
19
+ ---
20
+
21
+ You know how many tokens your last request used, because the API told you. You almost certainly do not know **what they were**.
22
+
23
+ There are tools that compress your context and tools that retrieve into it. There is nothing that opens it up and shows you the bill line by line. So when a session gets slow and expensive, the usual move is to guess.
24
+
25
+ ## Quickstart
26
+
27
+ ```bash
28
+ pip install tokenprof # once released
29
+ pip install git+https://github.com/muhammadwaqar12/tokenprof # from source
30
+ ```
31
+
32
+ Run your program through `record`. It attaches to the OpenAI and Anthropic clients, writes every outgoing request to a file, and changes nothing about what your program does.
33
+
34
+ ```bash
35
+ tokenprof record -o session.jsonl -- python your_agent.py
36
+ tokenprof analyze session.jsonl
37
+ tokenprof cache session.jsonl
38
+ ```
39
+
40
+ No API keys, no config, no code changes. If you already log request payloads, skip `record` and point `analyze` at what you have.
41
+
42
+ ## Where your tokens went
43
+
44
+ ```
45
+ $ tokenprof analyze session.jsonl --turn 2
46
+
47
+ turn 2 model=gpt-4o provider=openai_chat
48
+ tokenizer: heuristic(chars/4)
49
+
50
+ category tokens share cost
51
+ -----------------------------------------------------------------------------
52
+ tool result 2,869 66.6% ###################......... $0.0072
53
+ tool schema 1,142 26.5% #######..................... $0.0029
54
+ system 205 4.8% #........................... $0.0005
55
+ history assistant 67 1.6% ............................ $0.0002
56
+ history user 14 0.3% ............................ $0.0000
57
+ current user 8 0.2% ............................ $0.0000
58
+ -----------------------------------------------------------------------------
59
+ TOTAL 4,305 100.0% $0.0108
60
+
61
+ fixed overhead (system + tool schemas): 1,347 tokens (31.3% of this turn), re-sent on every request
62
+
63
+ tool schemas (4 registered)
64
+ bloated_connector 912 ####..............
65
+ search_docs 85 ..................
66
+ trace_calls 82 ..................
67
+ read_file 63 ..................
68
+ ```
69
+
70
+ One auto-generated tool is eating 80% of the schema budget and is serialized into every request whether the model calls it or not. Nothing errors. Nothing in your dashboard says so.
71
+
72
+ The per-tool line is the part most tools cannot give you. Not "you have 40 tools registered" but **which ones**, ranked, in tokens.
73
+
74
+ ## Is your prompt cache actually working?
75
+
76
+ Providers cache a **prefix**. If the first N tokens of a request are byte-identical to the last one, you pay a fraction for them. One volatile value near the front moves the break point to zero, and you quietly pay full price on every turn while your config still says caching is on.
77
+
78
+ <p align="center">
79
+ <img src="https://raw.githubusercontent.com/muhammadwaqar12/tokenprof/main/docs/demo.svg" alt="tokenprof cache finding a broken prompt cache prefix" width="100%">
80
+ </p>
81
+
82
+ ```
83
+ $ tokenprof cache session.jsonl
84
+
85
+ turns stable total share what broke the prefix
86
+ --------------------------------------------------------------------------
87
+ 0 -> 1 68 248 27.4% system:system
88
+ 1 -> 2 68 302 22.5% system:system
89
+ 2 -> 3 68 356 19.1% system:system
90
+ --------------------------------------------------------------------------
91
+
92
+ reusable across turns: 204 tokens | re-sent after a break: 702
93
+
94
+ segments that broke the prefix
95
+ system:system 3x
96
+
97
+ WARNING: the prefix breaks early. Turn 3 reuses only 19.1% of its prompt.
98
+ Anything that changes near the front of the prompt costs you the whole
99
+ cache. Timestamps, session ids and reordered tool lists are the usual
100
+ causes. Move volatile content to the end.
101
+ ```
102
+
103
+ That session has a timestamp at the top of the system prompt. Every turn misses, and the tool names the segment responsible rather than just reporting a bad number.
104
+
105
+ The other common cause is subtler: **registering one new tool mid-session invalidates the whole prefix**, because tool schemas are serialized ahead of the messages. `tokenprof cache` catches that too.
106
+
107
+ Cost accounting follows the real pricing. Anthropic bills cache reads at 10% of base and OpenAI at 50%, so a turn with `cache_control` markers reports what it actually bills rather than a blended number that overstates exactly the segments you were smart enough to cache.
108
+
109
+ ## Recording a session
110
+
111
+ `record` writes a `sitecustomize` shim onto the path of the process you launch, wraps the client `create` methods, and calls through untouched. Two deliberate constraints: it never raises into your program, and it records requests only, never responses.
112
+
113
+ ```bash
114
+ tokenprof record --verbose -o session.jsonl -- python your_agent.py
115
+ # tokenprof: patched openai, anthropic
116
+ # tokenprof: captured 14 request(s) to session.jsonl
117
+ ```
118
+
119
+ If you would rather log payloads yourself, any JSONL of request objects works. Bare payloads and anything wrapping one under `request`, `body`, `payload` or `kwargs` are all accepted, and `-` reads stdin.
120
+
121
+ > [!NOTE]
122
+ > A recording contains your prompts and your tool results in full, which usually means credentials and customer data. The default `.gitignore` excludes `*.jsonl`. Redact before attaching one to an issue.
123
+
124
+ ## Adapters
125
+
126
+ An adapter turns one provider's payload shape into a list of segments. Three methods, no framework dependency in the core, which is why adding one is an afternoon rather than a refactor.
127
+
128
+ | Adapter | Status |
129
+ |---|---|
130
+ | `anthropic_messages` | Shipped, with `cache_control` support |
131
+ | `openai_chat` | Shipped |
132
+ | LangGraph / LangChain | Wanted |
133
+ | CrewAI | Wanted |
134
+ | Google ADK | Wanted |
135
+ | Bedrock Converse | Wanted |
136
+
137
+ Most frameworks ultimately emit an OpenAI or Anthropic payload, so the two shipped adapters already cover a lot of ground. Detection is automatic, and a test asserts exactly one adapter claims any payload. Pass `--provider` to override.
138
+
139
+ ## Accuracy
140
+
141
+ Every report states which tokenizer produced it, because the honest answer varies.
142
+
143
+ With `tiktoken` installed and an OpenAI model, counts are exact. Otherwise it falls back to `chars / 4`, which runs low on code and non-Latin scripts. That is fine for ranking segments against each other, which is what you are here for, and it is not a context budget.
144
+
145
+ Serialization is close but not exact. Providers wrap messages and schemas in their own formatting, and that wrapper is small next to the payload. Treat the numbers as accurate to a few percent and the ranking as reliable.
146
+
147
+ ## What this is not
148
+
149
+ Not a tracing tool. Langfuse, Phoenix and LangSmith show latency, spans and token *counts*, and do it well. None show token *composition*. Run both.
150
+
151
+ Not a compressor. It tells you what to cut. Cutting is your call.
152
+
153
+ Not a runtime guard. It reads recordings after the fact, on purpose, so a profiler can never sit in the path of a production request.
154
+
155
+ ## Contributing
156
+
157
+ The most useful contributions are adapters, and payload shapes that break detection. If `tokenprof` cannot read your logs, that is a bug and a redacted sample is the whole fix. See [CONTRIBUTING.md](https://github.com/muhammadwaqar12/tokenprof/blob/main/CONTRIBUTING.md).
158
+
159
+ ## License
160
+
161
+ [MIT](https://github.com/muhammadwaqar12/tokenprof/blob/main/LICENSE).
162
+
163
+ ---
164
+
165
+ <sub>Built by <a href="https://github.com/muhammadwaqar12">Muhammad Waqar</a>. Companion to <a href="https://github.com/muhammadwaqar12/awesome-agent-failures">awesome-agent-failures</a>, where CTX-03 covers the tool schema tax and COST-03 covers prompt cache thrash.</sub>