costtrace 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 (37) hide show
  1. costtrace-0.1.0/LICENSE +21 -0
  2. costtrace-0.1.0/PKG-INFO +422 -0
  3. costtrace-0.1.0/README.md +393 -0
  4. costtrace-0.1.0/costtrace.egg-info/PKG-INFO +422 -0
  5. costtrace-0.1.0/costtrace.egg-info/SOURCES.txt +35 -0
  6. costtrace-0.1.0/costtrace.egg-info/dependency_links.txt +1 -0
  7. costtrace-0.1.0/costtrace.egg-info/entry_points.txt +2 -0
  8. costtrace-0.1.0/costtrace.egg-info/requires.txt +9 -0
  9. costtrace-0.1.0/costtrace.egg-info/top_level.txt +1 -0
  10. costtrace-0.1.0/pyproject.toml +48 -0
  11. costtrace-0.1.0/setup.cfg +4 -0
  12. costtrace-0.1.0/src/__init__.py +60 -0
  13. costtrace-0.1.0/src/analysis/__init__.py +42 -0
  14. costtrace-0.1.0/src/analysis/aggregate.py +149 -0
  15. costtrace-0.1.0/src/analysis/detect.py +245 -0
  16. costtrace-0.1.0/src/analysis/judge.py +141 -0
  17. costtrace-0.1.0/src/analysis/metrics.py +169 -0
  18. costtrace-0.1.0/src/analysis/store.py +89 -0
  19. costtrace-0.1.0/src/capture.py +119 -0
  20. costtrace-0.1.0/src/capture_schema.py +244 -0
  21. costtrace-0.1.0/src/cli.py +315 -0
  22. costtrace-0.1.0/src/fetch.py +69 -0
  23. costtrace-0.1.0/src/ingest.py +165 -0
  24. costtrace-0.1.0/src/paths.py +49 -0
  25. costtrace-0.1.0/src/pricing.py +208 -0
  26. costtrace-0.1.0/src/pricing.yaml +49 -0
  27. costtrace-0.1.0/src/providers/__init__.py +39 -0
  28. costtrace-0.1.0/src/providers/base.py +68 -0
  29. costtrace-0.1.0/src/providers/openai_agents.py +221 -0
  30. costtrace-0.1.0/src/schema.py +31 -0
  31. costtrace-0.1.0/src/trace.py +87 -0
  32. costtrace-0.1.0/tests/test_analysis.py +176 -0
  33. costtrace-0.1.0/tests/test_capture.py +125 -0
  34. costtrace-0.1.0/tests/test_cli.py +77 -0
  35. costtrace-0.1.0/tests/test_fetch.py +179 -0
  36. costtrace-0.1.0/tests/test_ingest.py +75 -0
  37. costtrace-0.1.0/tests/test_pricing.py +77 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sanat Bhalla
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,422 @@
1
+ Metadata-Version: 2.4
2
+ Name: costtrace
3
+ Version: 0.1.0
4
+ Summary: A deterministic cost-attribution profiler for LLM traces — an X-ray for where your token spend goes.
5
+ Author: Sanat Bhalla
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/sanat77/CostTrace
8
+ Project-URL: Repository, https://github.com/sanat77/CostTrace
9
+ Project-URL: Issues, https://github.com/sanat77/CostTrace/issues
10
+ Keywords: llm,cost,finops,tokens,profiler,observability
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Monitoring
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: PyYAML>=6.0
22
+ Requires-Dist: tiktoken>=0.7
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Requires-Dist: pyflakes>=3.0; extra == "dev"
26
+ Provides-Extra: examples
27
+ Requires-Dist: openai-agents>=0.18; extra == "examples"
28
+ Dynamic: license-file
29
+
30
+ # CostTrace
31
+
32
+ **An X-ray for your LLM token spend.**
33
+
34
+ CostTrace is a deterministic cost-attribution profiler for LLM applications. You
35
+ add **one line** after each agent run to capture the durable identity of your
36
+ billed calls. Later, CostTrace re-fetches the exact usage from the provider,
37
+ prices it, and tells you three things most dashboards can't:
38
+
39
+ 1. **Where** your money goes — split across system prompt, conversation history,
40
+ retrieved context, the user's turn, and the model's output.
41
+ 2. **What changed** over time — cost-per-turn jumps, model switches, history
42
+ bloat, longer outputs, cache regressions, or system-prompt edits.
43
+ 3. **Why** — an LLM-as-judge explains the most likely driver in plain English,
44
+ grounded strictly in your real numbers.
45
+
46
+ > **Integrations:** today CostTrace supports the **OpenAI Agents SDK (Python)**
47
+ > only. The design is provider-tagged, and **support for more SDKs and providers
48
+ > is planned** (see [Roadmap](#roadmap)).
49
+
50
+ ---
51
+
52
+ ## Table of contents
53
+
54
+ - [Why CostTrace](#why-costtrace)
55
+ - [How it's different](#how-its-different)
56
+ - [Install](#install)
57
+ - [Quickstart](#quickstart)
58
+ - [Core concepts](#core-concepts)
59
+ - [Providers](#providers)
60
+ - [CLI reference](#cli-reference)
61
+ - [Programmatic API](#programmatic-api)
62
+ - [How it works](#how-it-works)
63
+ - [The cost model](#the-cost-model)
64
+ - [Data, privacy & retention](#data-privacy--retention)
65
+ - [Configuration](#configuration)
66
+ - [Development](#development)
67
+ - [Roadmap](#roadmap)
68
+ - [FAQ](#faq)
69
+ - [License](#license)
70
+
71
+ ---
72
+
73
+ ## Why CostTrace
74
+
75
+ LLM bills are opaque. A provider hands you a token count per call, not dollars,
76
+ and never tells you *which part* of the prompt cost the money. In a multi-turn
77
+ agent, the conversation history is re-sent on every turn, a verbose system
78
+ prompt is billed on every call, and a single model swap can 100× your cost — but
79
+ the invoice just says "tokens."
80
+
81
+ CostTrace turns that opaque number into an **itemized, timestamped, queryable**
82
+ picture of your spend, so you can answer questions like:
83
+
84
+ - "What fraction of my bill is just re-sending history?"
85
+ - "Our costs doubled last week — what changed?"
86
+ - "How much cheaper is `nano` than `pro` for the *same* workload?"
87
+ - "Did that system-prompt edit break prompt caching?"
88
+
89
+ ## How it's different
90
+
91
+ - **Capture is tiny and cheap.** We store only `{provider, conversation_id,
92
+ response_ids}` — no usage numbers, no message content. Everything else is
93
+ re-fetched on demand (provider GET reads are **not** billed).
94
+ - **Accounting is deterministic.** Costs are computed by us as `rate × tokens`
95
+ using live [LiteLLM](https://github.com/BerriAI/litellm) pricing (with a
96
+ bundled offline fallback) — reproducible, auditable, no guessing.
97
+ - **Detection is general, not single-cause.** Rather than blaming one thing,
98
+ CostTrace compares a recent window against a prior one across *every*
99
+ dimension and surfaces each signal that moved.
100
+ - **The LLM only explains, never computes.** The judge receives a compact,
101
+ number-grounded digest and is instructed to reason over it — it never invents
102
+ figures.
103
+ - **It's a profiler, not a proxy.** No gateway to route traffic through, no
104
+ latency added to your requests. You capture ids; analysis happens offline,
105
+ on demand.
106
+
107
+ ## Install
108
+
109
+ ```bash
110
+ pip install costtrace
111
+ ```
112
+
113
+ This installs the importable **`CostTrace`** package and the **`costtrace`** CLI.
114
+
115
+ Set your OpenAI key via the environment or a `.env` in your project root:
116
+
117
+ ```bash
118
+ export OPENAI_API_KEY=sk-...
119
+ # or, in ./.env
120
+ # OPENAI_API_KEY=sk-...
121
+ ```
122
+
123
+ Requires Python 3.9+.
124
+
125
+ ## Quickstart
126
+
127
+ ### 1. Capture — one line after each agent run
128
+
129
+ ```python
130
+ from agents import Agent, OpenAIConversationsSession, Runner
131
+ from CostTrace import capture
132
+
133
+ session = OpenAIConversationsSession()
134
+ result = await Runner.run(agent, user_input, session=session)
135
+
136
+ capture(result, conversation_id, provider="openai_agents") # -> ./.costtrace/captures.json
137
+ ```
138
+
139
+ `capture()` extracts the response ids from the run, validates them, de-dupes,
140
+ timestamps them, and appends them under the conversation id. It's idempotent and
141
+ prunes anything older than the retention window on every write.
142
+
143
+ `provider` is **required** — it records which backend the ids came from so they
144
+ can be re-fetched later. Passing an unknown/missing provider raises an error that
145
+ lists the [available providers](#providers).
146
+
147
+ You can also pass a plain list of ids instead of a `RunResult`:
148
+
149
+ ```python
150
+ capture(["resp_abc", "resp_def"], conversation_id, provider="openai_agents")
151
+ ```
152
+
153
+ ### 2. Analyze — from the CLI
154
+
155
+ ```bash
156
+ costtrace analyze # everything captured
157
+ costtrace analyze --limit 5 # 5 most recent conversations
158
+ costtrace analyze --date 2026/07/20 # a single day
159
+ costtrace analyze --starting-date 2026/07/01 # from that day until today
160
+ costtrace analyze --conversationId conv_abc # one conversation
161
+ costtrace analyze --no-judge # skip the LLM explanation
162
+ ```
163
+
164
+ Example output:
165
+
166
+ ```
167
+ ======================================================================
168
+ WHERE THE MONEY GOES
169
+ ======================================================================
170
+ sessions: 3 total: $6.143908 avg cache-hit: 53%
171
+ output 85.9% $5.275134
172
+ history 14.0% $0.857593
173
+ system 0.1% $0.009336
174
+ user 0.0% $0.001846
175
+ retrieved 0.0% $0.000000
176
+
177
+ ======================================================================
178
+ PER-MODEL COMPARISON
179
+ ======================================================================
180
+ model cost/turn cache out tok/turn turns
181
+ gpt-5.4-pro $ 1.118778 0% 5376 5
182
+ gpt-5.4 $ 0.043458 80% 2305 12
183
+ gpt-5.4-nano $ 0.002377 78% 1472 12
184
+
185
+ gpt-5.4-pro costs 470.6x more per turn than gpt-5.4-nano
186
+
187
+ ======================================================================
188
+ WHAT CHANGED (deterministic signals)
189
+ ======================================================================
190
+ [1] (medium) dominant_bucket: 'output' is 86% of total spend ($5.275134)
191
+
192
+ ======================================================================
193
+ JUDGE (LLM explanation)
194
+ ======================================================================
195
+ Output dominates spend; the cost gap tracks the switch to gpt-5.4-pro
196
+ (higher rate + 3.6x longer outputs + 0% cache vs ~80%). Consider routing
197
+ routine turns to gpt-5.4-nano and capping output length.
198
+ ```
199
+
200
+ ## Core concepts
201
+
202
+ | Term | Meaning |
203
+ |------|---------|
204
+ | **capture** | The dev-side one-liner. Persists `{provider, conversation_id, response_ids}` — the durable *identity* of billed calls, nothing else. |
205
+ | **conversation** | One session, grouped by the provider's conversation id. |
206
+ | **call** | One billed model request == one `response_id`. |
207
+ | **message kind** | Each item in a call is classified as `system`, `history`, `retrieved`, `user`, or `output`. |
208
+ | **bucket** | Cost aggregated by message kind — the "where the money goes" split. |
209
+ | **session metric** | One durable, timestamped row per conversation (cost, buckets, cache rate, cost/turn, model, system-prompt fingerprint). |
210
+ | **provider tag** | Routes fetching to the right backend. Currently only `openai_agents`. |
211
+
212
+ ## Providers
213
+
214
+ `provider` is a **required** argument to `capture()` — it records which backend
215
+ the response ids belong to, so CostTrace knows where to re-fetch usage from.
216
+
217
+ | Provider tag | Backend | Status |
218
+ |--------------|---------|--------|
219
+ | `openai_agents` | OpenAI Agents SDK (Python) | ✅ Available |
220
+ | _(more)_ | OpenAI Python SDK, Anthropic, Langfuse/Helicone exports, … | 🚧 Planned ([roadmap](#roadmap)) |
221
+
222
+ Passing an unknown or missing provider fails fast with an error that lists the
223
+ currently supported tags:
224
+
225
+ ```python
226
+ >>> capture(result, conversation_id) # missing
227
+ CaptureSchemaError: provider is required — pass one of the supported providers:
228
+ ['openai_agents'] (e.g. capture(result, conversation_id, provider='openai_agents'))
229
+
230
+ >>> capture(result, conversation_id, provider="anthropic") # unknown
231
+ CaptureSchemaError: unknown provider 'anthropic'; supported providers: ['openai_agents']
232
+ ```
233
+
234
+ The available tags are also exposed programmatically:
235
+
236
+ ```python
237
+ from CostTrace import Provider
238
+ Provider.values() # ['openai_agents']
239
+ ```
240
+
241
+ ## CLI reference
242
+
243
+ ```
244
+ costtrace analyze [options]
245
+ ```
246
+
247
+ | Option | Description |
248
+ |--------|-------------|
249
+ | `--limit N` | Analyze the N most recent conversations. |
250
+ | `--conversationId conv_xxx` | Analyze a single conversation (alias: `--conversation-id`). |
251
+ | `--date yyyy/mm/dd` | Only conversations from that day. |
252
+ | `--starting-date yyyy/mm/dd` | Conversations from that day until today. |
253
+ | `--no-judge` | Skip the LLM explanation (fully offline). |
254
+
255
+ `--date` and `--starting-date` are mutually exclusive. Run `costtrace --help` or
256
+ `costtrace analyze --help` for the full, self-documenting listing.
257
+
258
+ ## Programmatic API
259
+
260
+ ```python
261
+ from CostTrace import load_captures, fetch_captures, Pricer
262
+
263
+ captures = load_captures(".costtrace/captures.json") # validated ids
264
+ report = fetch_captures(captures) # validated traces
265
+
266
+ report.conversations # list[ConversationTrace] — ready to analyze
267
+ report.rejections # ids dropped (expired/invalid), each with a reason
268
+ report.fetched_calls # count of successfully fetched calls
269
+ ```
270
+
271
+ Build your own analysis with the `analysis` module:
272
+
273
+ ```python
274
+ from CostTrace.analysis import (
275
+ compute_session_metric, upsert_metrics, load_metrics,
276
+ summarize_overall, summarize_by_model, detect_all, judge,
277
+ )
278
+
279
+ pricer = Pricer.default() # live LiteLLM pricing, cached, yaml fallback
280
+ metrics = [compute_session_metric(c, pricer) for c in report.conversations]
281
+ upsert_metrics(metrics) # persist to .costtrace/metrics.jsonl
282
+
283
+ rows = load_metrics()
284
+ overall = summarize_overall(rows)
285
+ findings = detect_all(overall, rows)
286
+ verdict = judge(overall, findings, by_model=summarize_by_model(rows))
287
+ print(verdict.verdict)
288
+ ```
289
+
290
+ ## How it works
291
+
292
+ ```
293
+ your app
294
+ │ capture(result, conversation_id)
295
+
296
+ .costtrace/captures.json ── just ids, 15-day retention, auto-pruned
297
+
298
+ costtrace analyze:
299
+
300
+ ingest ──► fetch ──► metrics ──► store ──► aggregate ──► detect ──► judge
301
+ (validate (re-fetch (bucket (durable (overall + (what (LLM
302
+ ids) usage & split + JSONL) per-model + changed) explains
303
+ content) cost) per-prompt) why)
304
+ ```
305
+
306
+ 1. **Ingest** — strict schema + id-format validation; clear errors; duplicate
307
+ conversations merged.
308
+ 2. **Fetch** — for each id, pull the response (model + usage) and its input
309
+ items from the provider, then classify each message (system/history/
310
+ retrieved/user/output). **Resilient:** expired or invalid ids are recorded as
311
+ rejections and skipped; only auth failures are fatal.
312
+ 3. **Metrics** — per conversation, compute total cost and split the input cost
313
+ across buckets by token share (estimated with a tokenizer, then scaled so the
314
+ parts sum to the provider's real `input_tokens`). Output cost is exact.
315
+ 4. **Store** — append/upsert one row per conversation to
316
+ `.costtrace/metrics.jsonl`; refresh rows that gained turns; prune expired
317
+ rows. This history survives the id-retention window.
318
+ 5. **Aggregate** — overall spend split, per-model comparison, per-prompt-version
319
+ grouping.
320
+ 6. **Detect** — compare a recent window vs a prior one and flag every dimension
321
+ that moved: `cost_increase`, `model_shift`, `bucket_shift`, `cache_drop`,
322
+ `output_growth`, `prompt_change`, plus a static `dominant_bucket`.
323
+ 7. **Judge** — assemble a compact, grounded digest and ask an LLM to name the
324
+ likely cause and a fix. It reasons over the signals; it does not compute
325
+ numbers.
326
+
327
+ ## The cost model
328
+
329
+ Providers return **tokens, not dollars** — and never a per-call price. CostTrace
330
+ computes cost itself:
331
+
332
+ ```
333
+ cost = uncached_input_tokens × input_rate
334
+ + cached_input_tokens × cache_read_rate
335
+ + output_tokens × output_rate
336
+ ```
337
+
338
+ Rates come from LiteLLM's community-maintained pricing table (≈3,000 models),
339
+ fetched and cached locally, with a small bundled `pricing.yaml` as an offline
340
+ fallback. Because accounting is pure arithmetic on real token counts, results
341
+ are reproducible and auditable.
342
+
343
+ The **bucket split** for the input side is an approximation: the provider bills
344
+ one `input_tokens` number per call, so CostTrace estimates each message's share
345
+ with a tokenizer and scales those shares to sum exactly to the real input cost.
346
+ The **output** bucket is exact.
347
+
348
+ ## Data, privacy & retention
349
+
350
+ - **What's stored on disk:** only response/conversation **ids** in
351
+ `.costtrace/captures.json`, plus computed **metrics** (numbers, model names,
352
+ and a hash + copy of the system prompt) in `.costtrace/metrics.jsonl`, plus a
353
+ cached pricing table. No user/assistant message content is persisted.
354
+ - **Where:** a project-local `.costtrace/` directory, discovered by walking up
355
+ to the nearest `.git` / `pyproject.toml` (like git finds its root), so
356
+ `capture()` and the CLI always agree regardless of your working directory.
357
+ Override with `COSTTRACE_HOME`.
358
+ - **Retention:** ids are only re-fetchable from the provider for a limited
359
+ window, so captures older than **15 days** are pruned automatically on write —
360
+ you lose nothing that could still be fetched.
361
+ - **The judge digest** contains aggregate numbers and (only when a prompt change
362
+ is detected) the system-prompt text — never raw conversation content.
363
+ - **Secrets:** your API key is read from the environment or a `.env`; it is
364
+ never written to the data directory.
365
+
366
+ ## Configuration
367
+
368
+ | Variable | Purpose |
369
+ |----------|---------|
370
+ | `OPENAI_API_KEY` | Required for fetching usage and for the LLM judge. |
371
+ | `COSTTRACE_HOME` | Override the `.costtrace/` data directory location. |
372
+
373
+ ## Development
374
+
375
+ ```bash
376
+ git clone https://github.com/sanat77/CostTrace.git
377
+ cd CostTrace
378
+ python -m venv .venv && source .venv/bin/activate
379
+ pip install -e .[dev]
380
+
381
+ python -m pytest -q # run the test suite
382
+ python -m pyflakes src tests conftest.py # lint
383
+ ```
384
+
385
+ The package source lives in `src/` and is mapped to the import name `CostTrace`
386
+ via `package-dir` in `pyproject.toml`, so the built wheel ships `CostTrace/` and
387
+ `pip install` / `import CostTrace` are unaffected by the on-disk layout. A
388
+ `conftest.py` replicates that mapping so tests import from the source tree
389
+ without installing.
390
+
391
+ ## Roadmap
392
+
393
+ CostTrace v1 targets the **OpenAI Agents SDK (Python)** (`provider =
394
+ "openai_agents"`). Planned next:
395
+
396
+ - **More Python LLM SDKs / providers** (e.g. the OpenAI Python SDK directly,
397
+ Anthropic, and others) behind the same provider-tagged architecture.
398
+ - **Ingesting exports** from existing observability tools (e.g. Langfuse /
399
+ Helicone) as additional provider tags.
400
+ - **Longer retention & trend history** as an opt-in tier.
401
+ - **Richer detectors** and per-tenant / per-feature attribution.
402
+
403
+ Contributions and provider requests are welcome — open an issue.
404
+
405
+ ## FAQ
406
+
407
+ **Does this add latency to my app?** No. You only save ids at runtime; all
408
+ fetching and analysis happens later, offline.
409
+
410
+ **Do the re-fetches cost money?** No — provider GET reads of past responses are
411
+ not billed. Only your original generations were.
412
+
413
+ **Why can't I analyze conversations from last month?** Ids expire at the
414
+ provider (~retention window), so they can't be re-fetched. The computed metrics
415
+ you already stored remain, but new deep analysis needs live ids.
416
+
417
+ **Distribution name vs import name?** You `pip install costtrace` (lowercase)
418
+ and `import CostTrace` (capitalized) — standard PyPI convention.
419
+
420
+ ## License
421
+
422
+ MIT — see [LICENSE](LICENSE).