unalloc 0.2.2__tar.gz → 0.3.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. {unalloc-0.2.2 → unalloc-0.3.0}/CHANGELOG.md +25 -0
  2. {unalloc-0.2.2 → unalloc-0.3.0}/PKG-INFO +59 -7
  3. {unalloc-0.2.2 → unalloc-0.3.0}/README.md +58 -6
  4. {unalloc-0.2.2 → unalloc-0.3.0}/pyproject.toml +1 -1
  5. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/__init__.py +1 -1
  6. unalloc-0.3.0/src/unalloc/cli.py +415 -0
  7. unalloc-0.3.0/src/unalloc/core/provenance.py +103 -0
  8. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/sources/anthropic.py +2 -2
  9. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/sources/base.py +10 -2
  10. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/sources/litellm.py +2 -2
  11. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/sources/openai.py +2 -2
  12. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/sources/opencost.py +2 -2
  13. {unalloc-0.2.2 → unalloc-0.3.0}/tests/test_docs_build.py +5 -0
  14. {unalloc-0.2.2 → unalloc-0.3.0}/tests/test_fetch.py +40 -0
  15. {unalloc-0.2.2 → unalloc-0.3.0}/tests/test_paper_numbers.py +50 -0
  16. unalloc-0.3.0/tests/test_provenance.py +157 -0
  17. unalloc-0.2.2/src/unalloc/cli.py +0 -252
  18. {unalloc-0.2.2 → unalloc-0.3.0}/.gitignore +0 -0
  19. {unalloc-0.2.2 → unalloc-0.3.0}/LICENSE +0 -0
  20. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/core/__init__.py +0 -0
  21. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/core/attribute.py +0 -0
  22. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/core/models.py +0 -0
  23. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/core/normalize.py +0 -0
  24. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/core/reconcile.py +0 -0
  25. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/fixtures/anthropic_cost_report.json +0 -0
  26. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/fixtures/litellm_spend.json +0 -0
  27. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/fixtures/openai_costs.json +0 -0
  28. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/fixtures/opencost_allocation.json +0 -0
  29. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/render/__init__.py +0 -0
  30. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/render/table.py +0 -0
  31. {unalloc-0.2.2 → unalloc-0.3.0}/src/unalloc/sources/__init__.py +0 -0
  32. {unalloc-0.2.2 → unalloc-0.3.0}/tests/test_attribute.py +0 -0
  33. {unalloc-0.2.2 → unalloc-0.3.0}/tests/test_cli.py +0 -0
  34. {unalloc-0.2.2 → unalloc-0.3.0}/tests/test_normalize.py +0 -0
  35. {unalloc-0.2.2 → unalloc-0.3.0}/tests/test_paper_lint.py +0 -0
  36. {unalloc-0.2.2 → unalloc-0.3.0}/tests/test_reconcile.py +0 -0
  37. {unalloc-0.2.2 → unalloc-0.3.0}/tests/test_sources.py +0 -0
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 2026-10-04
4
+
5
+ ### Added
6
+ - `report --json` carries a `provenance` block: unalloc version, window (live runs), alias-table digest, one entry per requested source with its status, payload SHA-256, byte count and row count, and a digest of the result body.
7
+ - `report --keep-inputs DIR` writes the exact payloads the run parsed, named like the bundled fixtures, so the run can be replayed without the network.
8
+ - `unalloc verify RESULT --inputs DIR` rebuilds a report from retained inputs. Exit 0 when it reproduces; 3 when an input is missing or its bytes changed at the same path; 4 when the inputs match but the result does not, naming a version or alias-table change; 5 when the recorded result does not conserve its own total.
9
+ - Every command warns on stderr when the ledger is partial, naming each source that failed or had no input.
10
+ - `Source.fetch_payload()` returns the raw provider payload; `fetch()` is now `parse(fetch_payload())` on the base class.
11
+
12
+ ### Fixed
13
+ - A failed live source was printed to stderr and then dropped, so `report --json` gave no sign that its percentage covered only some sources. It is now recorded as `failed` in the provenance block.
14
+ - With `--fixtures` and a missing fixture file, the "skipping" notice went to stdout and made `report --json` output invalid JSON. It goes to stderr.
15
+
16
+ Attribution results are unchanged: every fixture report from 0.2.3 reproduces exactly.
17
+
18
+ ## 0.2.3 — 2026-09-21
19
+
20
+ ### Fixed
21
+ - Figure 9's within-run spread is drawn as its own vertical bar rather than an error bar hung off the whole-run point. The old form had to clip whenever the whole-run share fell outside the window quartiles, which it does at three of the eight plotted points — the whole-run share is weighted by each window's traffic, so it need not lie inside the quartiles of its own windows.
22
+ - §9 no longer presents the window statistics as a bootstrap confidence interval or as a bound on uncertainty. Consecutive windows of one run are not independent observations, so the paragraph now reports median, interquartile range and sign as descriptive statistics of that run, and the cross-reference matches what the figure actually draws. `bootstrap_ci` is renamed `resample_range`, and the metrics key `divergence_mean_ci95` becomes `divergence_mean_resample_range`.
23
+ - arXiv kit refreshed against the built PDF: 14 pages, AI-use disclosure on page 13, and the concept DOI in the Comments field. The standalone abstract gained the synthetic-scenario qualification and the session-initial arrival-rate clarification that the paper's abstract already carried.
24
+
25
+ ### Added
26
+ - `tests/test_paper_numbers.py` also checks the submission kit: the page, figure and table counts against the committed PDF, the concept DOI in both the kit and the paper, and the standalone abstract's length and scope wording.
27
+
3
28
  ## 0.2.2 — 2026-09-21
4
29
 
5
30
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unalloc
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Find the AI spend nobody owns. Joins OpenCost allocation data with LLM provider bills and reports what's unattributed.
5
5
  Project-URL: Homepage, https://github.com/timurista/unalloc
6
6
  Project-URL: Issues, https://github.com/timurista/unalloc/issues
@@ -47,11 +47,11 @@ Description-Content-Type: text/markdown
47
47
  [![ci](https://github.com/timurista/unalloc/actions/workflows/ci.yml/badge.svg)](https://github.com/timurista/unalloc/actions/workflows/ci.yml)
48
48
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/timurista/unalloc/blob/main/LICENSE)
49
49
  [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://github.com/timurista/unalloc/blob/main/pyproject.toml)
50
- [![Paper](https://img.shields.io/badge/paper-PDF-1f5b45.svg)](https://github.com/timurista/unalloc/blob/main/paper/unalloc-case-studies.pdf)
50
+ [![arXiv](https://img.shields.io/badge/arXiv-2609.24991-b31b1b.svg)](https://arxiv.org/abs/2609.24991)
51
51
  [![Explorer](https://img.shields.io/badge/explorer-live-1f5b45.svg)](https://timurista.github.io/unalloc/explorer.html)
52
52
  [![DOI](https://zenodo.org/badge/1368528624.svg)](https://doi.org/10.5281/zenodo.22761012)
53
53
 
54
- **Project site: [timurista.github.io/unalloc](https://timurista.github.io/unalloc/)**, with the findings, case studies, [paper (PDF)](https://timurista.github.io/unalloc/paper/unalloc-case-studies.pdf), [blog post](https://timurista.github.io/unalloc/blog/who-pays-for-the-kv-cache.html), [GPU addendum](https://timurista.github.io/unalloc/blog/gpu-validation.html) and [ledger explorer](https://timurista.github.io/unalloc/explorer.html).
54
+ **Project site: [timurista.github.io/unalloc](https://timurista.github.io/unalloc/)**, with the findings, case studies, [paper (arXiv:2609.24991)](https://arxiv.org/abs/2609.24991), [blog post](https://timurista.github.io/unalloc/blog/who-pays-for-the-kv-cache.html), [GPU addendum](https://timurista.github.io/unalloc/blog/gpu-validation.html) and [ledger explorer](https://timurista.github.io/unalloc/explorer.html).
55
55
 
56
56
  OpenCost tells you what your Kubernetes workloads cost. Your provider dashboard tells you what your OpenAI and Anthropic calls cost. Getting both into one view is possible today — OpenCost has an OpenAI plugin — but the answer still depends on a join nobody checks: the same dollar can arrive through both your gateway and your provider bill, and the labels that say who owns it are set per workload, not per pod template. `unalloc` pulls all four sources into one normalized ledger, joins them on a label dimension you choose, and reports the number your finance team keeps asking for: how much of this month's AI spend can't be attributed to any team — and how much of the rest was only rescued by a fallback key.
57
57
 
@@ -167,6 +167,42 @@ Configure sources by environment variable:
167
167
 
168
168
  Restrict the run with `--source` to avoid double counting: if your traffic goes through LiteLLM, prefer `--source opencost --source litellm` and skip the direct provider adapters, since the same tokens appear in both. The [end-to-end case study](#case-studies-and-paper) measures exactly how much that costs you if you forget.
169
169
 
170
+ ## Reproducing a result
171
+
172
+ A percentage that ends up in a finance deck will be questioned later, after the
173
+ source data has moved on. A path is not enough to answer that: a fixture or an
174
+ export can be rewritten under the same name, and a live billing API answers a
175
+ re-run with today's data. So `report --json` carries a `provenance` block, and
176
+ `--keep-inputs` retains the exact payloads it parsed:
177
+
178
+ ```bash
179
+ unalloc report -D team --json --keep-inputs runs/2026-09/ > runs/2026-09.json
180
+ unalloc verify runs/2026-09.json --inputs runs/2026-09/
181
+ # reproduced: 73.5% unallocated of $66,630.38, result sha256:…
182
+ ```
183
+
184
+ The provenance block records the unalloc version, the window (live runs only;
185
+ fixtures carry their own dates), a digest of the label alias table, and one
186
+ entry per requested source: its status, the SHA-256 of the payload bytes, and
187
+ the rows it produced. A source that failed or had no input is recorded as
188
+ `failed` or `missing` rather than dropped, and every command warns on stderr
189
+ when the ledger is partial, because the percentage then covers the loaded
190
+ sources only.
191
+
192
+ `verify` exits with a code that says which boundary moved:
193
+
194
+ | Exit | Meaning |
195
+ | --- | --- |
196
+ | 0 | Reproduced: same inputs, same result digest |
197
+ | 3 | Cannot reconstruct: a retained input is missing, or its bytes changed |
198
+ | 4 | Inputs match but the result differs: a rule or the tool changed in between |
199
+ | 5 | The recorded result does not conserve its own total (a dollar counted twice or dropped) |
200
+
201
+ A digest proves byte identity, not correctness. It cannot tell you a meter was
202
+ accurate or a rule was fair, and it can only rebuild a result when the bytes
203
+ it names were kept. Retained payloads contain your billing data; store them
204
+ like the invoices they came from.
205
+
170
206
  ## How it works
171
207
 
172
208
  ```
@@ -193,7 +229,7 @@ Two design choices worth calling out:
193
229
 
194
230
  ## Case studies and paper
195
231
 
196
- Six studies push real (or realistically simulated) inference workloads through unalloc's own adapters, including a run with vLLM on an NVIDIA H100. They exposed nine defects in the tool along the way, all fixed with regression tests, and they're written up as a paper: **[paper/unalloc-case-studies.pdf](https://github.com/timurista/unalloc/blob/main/paper/unalloc-case-studies.pdf)**. For the readable version, see the [blog post](https://timurista.github.io/unalloc/blog/who-pays-for-the-kv-cache.html) and the [GPU addendum](https://timurista.github.io/unalloc/blog/gpu-validation.html) on the [project site](https://timurista.github.io/unalloc/).
232
+ Six studies push real (or realistically simulated) inference workloads through unalloc's own adapters, including a run with vLLM on an NVIDIA H100. They exposed nine defects in the tool along the way, all fixed with regression tests, and they're written up as a paper: **[Who Pays for the KV Cache? (arXiv:2609.24991)](https://arxiv.org/abs/2609.24991)**, a preprint that has not been peer reviewed. The PDF built from this repository is at [paper/unalloc-case-studies.pdf](https://github.com/timurista/unalloc/blob/main/paper/unalloc-case-studies.pdf). For the readable version, see the [blog post](https://timurista.github.io/unalloc/blog/who-pays-for-the-kv-cache.html) and the [GPU addendum](https://timurista.github.io/unalloc/blog/gpu-validation.html) on the [project site](https://timurista.github.io/unalloc/).
197
233
 
198
234
  | Study | What runs | Headline |
199
235
  | --- | --- | --- |
@@ -245,6 +281,7 @@ What `unalloc` does is narrower: read all four sources yourself — OpenCost, a
245
281
  - [ ] Per-feature unit economics using the `quantity`/`unit` fields already carried on `CostRow`
246
282
  - [ ] Azure OpenAI and Bedrock adapters
247
283
  - [x] `--budget` mode: exit non-zero when unallocated share crosses a threshold, for CI
284
+ - [x] Result provenance and `unalloc verify`: rebuild an earlier report from retained inputs, or say why it can't be
248
285
  - [ ] `--exclude label=value` on a source, so gateway traffic can be dropped from provider billing instead of whole sources
249
286
  - [ ] Warn when distinct raw label keys collide on one canonical key
250
287
 
@@ -259,16 +296,31 @@ ruff check . && pytest
259
296
 
260
297
  ## Citing
261
298
 
262
- If you use unalloc or its case studies, please cite the archived release. GitHub's "Cite this repository" button produces the same entry from [`CITATION.cff`](https://github.com/timurista/unalloc/blob/main/CITATION.cff).
299
+ If you use the findings, please cite the paper, a preprint on arXiv ([arXiv:2609.24991](https://arxiv.org/abs/2609.24991), cs.DC):
300
+
301
+ ```bibtex
302
+ @misc{urista_kvcache_2026,
303
+ author = {Urista, Timothy},
304
+ title = {Who Pays for the {KV} Cache? Attributing Shared {AI} Inference Spend Across {Kubernetes} and {LLM} Provider Bills},
305
+ year = {2026},
306
+ eprint = {2609.24991},
307
+ archivePrefix = {arXiv},
308
+ primaryClass = {cs.DC},
309
+ doi = {10.48550/arXiv.2609.24991},
310
+ url = {https://arxiv.org/abs/2609.24991}
311
+ }
312
+ ```
313
+
314
+ If you use the software itself, cite the archived release. GitHub's "Cite this repository" button produces the same entry from [`CITATION.cff`](https://github.com/timurista/unalloc/blob/main/CITATION.cff).
263
315
 
264
- - **All versions:** [doi:10.5281/zenodo.22761012](https://doi.org/10.5281/zenodo.22761012), which always resolves to the latest release — currently 0.2.2, the code the paper describes
316
+ - **All versions:** [doi:10.5281/zenodo.22761012](https://doi.org/10.5281/zenodo.22761012), which always resolves to the latest release — currently 0.3.0. The paper describes 0.2.3; 0.3.0 adds result provenance and `unalloc verify` and changes no attribution result
265
317
  - **Version 0.2.1:** [doi:10.5281/zenodo.22761013](https://doi.org/10.5281/zenodo.22761013)
266
318
 
267
319
  ```bibtex
268
320
  @software{urista_unalloc_2026,
269
321
  author = {Urista, Timothy},
270
322
  title = {unalloc: find the AI spend nobody owns},
271
- version = {0.2.2},
323
+ version = {0.3.0},
272
324
  year = {2026},
273
325
  doi = {10.5281/zenodo.22761012},
274
326
  url = {https://github.com/timurista/unalloc}
@@ -5,11 +5,11 @@
5
5
  [![ci](https://github.com/timurista/unalloc/actions/workflows/ci.yml/badge.svg)](https://github.com/timurista/unalloc/actions/workflows/ci.yml)
6
6
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/timurista/unalloc/blob/main/LICENSE)
7
7
  [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://github.com/timurista/unalloc/blob/main/pyproject.toml)
8
- [![Paper](https://img.shields.io/badge/paper-PDF-1f5b45.svg)](https://github.com/timurista/unalloc/blob/main/paper/unalloc-case-studies.pdf)
8
+ [![arXiv](https://img.shields.io/badge/arXiv-2609.24991-b31b1b.svg)](https://arxiv.org/abs/2609.24991)
9
9
  [![Explorer](https://img.shields.io/badge/explorer-live-1f5b45.svg)](https://timurista.github.io/unalloc/explorer.html)
10
10
  [![DOI](https://zenodo.org/badge/1368528624.svg)](https://doi.org/10.5281/zenodo.22761012)
11
11
 
12
- **Project site: [timurista.github.io/unalloc](https://timurista.github.io/unalloc/)**, with the findings, case studies, [paper (PDF)](https://timurista.github.io/unalloc/paper/unalloc-case-studies.pdf), [blog post](https://timurista.github.io/unalloc/blog/who-pays-for-the-kv-cache.html), [GPU addendum](https://timurista.github.io/unalloc/blog/gpu-validation.html) and [ledger explorer](https://timurista.github.io/unalloc/explorer.html).
12
+ **Project site: [timurista.github.io/unalloc](https://timurista.github.io/unalloc/)**, with the findings, case studies, [paper (arXiv:2609.24991)](https://arxiv.org/abs/2609.24991), [blog post](https://timurista.github.io/unalloc/blog/who-pays-for-the-kv-cache.html), [GPU addendum](https://timurista.github.io/unalloc/blog/gpu-validation.html) and [ledger explorer](https://timurista.github.io/unalloc/explorer.html).
13
13
 
14
14
  OpenCost tells you what your Kubernetes workloads cost. Your provider dashboard tells you what your OpenAI and Anthropic calls cost. Getting both into one view is possible today — OpenCost has an OpenAI plugin — but the answer still depends on a join nobody checks: the same dollar can arrive through both your gateway and your provider bill, and the labels that say who owns it are set per workload, not per pod template. `unalloc` pulls all four sources into one normalized ledger, joins them on a label dimension you choose, and reports the number your finance team keeps asking for: how much of this month's AI spend can't be attributed to any team — and how much of the rest was only rescued by a fallback key.
15
15
 
@@ -125,6 +125,42 @@ Configure sources by environment variable:
125
125
 
126
126
  Restrict the run with `--source` to avoid double counting: if your traffic goes through LiteLLM, prefer `--source opencost --source litellm` and skip the direct provider adapters, since the same tokens appear in both. The [end-to-end case study](#case-studies-and-paper) measures exactly how much that costs you if you forget.
127
127
 
128
+ ## Reproducing a result
129
+
130
+ A percentage that ends up in a finance deck will be questioned later, after the
131
+ source data has moved on. A path is not enough to answer that: a fixture or an
132
+ export can be rewritten under the same name, and a live billing API answers a
133
+ re-run with today's data. So `report --json` carries a `provenance` block, and
134
+ `--keep-inputs` retains the exact payloads it parsed:
135
+
136
+ ```bash
137
+ unalloc report -D team --json --keep-inputs runs/2026-09/ > runs/2026-09.json
138
+ unalloc verify runs/2026-09.json --inputs runs/2026-09/
139
+ # reproduced: 73.5% unallocated of $66,630.38, result sha256:…
140
+ ```
141
+
142
+ The provenance block records the unalloc version, the window (live runs only;
143
+ fixtures carry their own dates), a digest of the label alias table, and one
144
+ entry per requested source: its status, the SHA-256 of the payload bytes, and
145
+ the rows it produced. A source that failed or had no input is recorded as
146
+ `failed` or `missing` rather than dropped, and every command warns on stderr
147
+ when the ledger is partial, because the percentage then covers the loaded
148
+ sources only.
149
+
150
+ `verify` exits with a code that says which boundary moved:
151
+
152
+ | Exit | Meaning |
153
+ | --- | --- |
154
+ | 0 | Reproduced: same inputs, same result digest |
155
+ | 3 | Cannot reconstruct: a retained input is missing, or its bytes changed |
156
+ | 4 | Inputs match but the result differs: a rule or the tool changed in between |
157
+ | 5 | The recorded result does not conserve its own total (a dollar counted twice or dropped) |
158
+
159
+ A digest proves byte identity, not correctness. It cannot tell you a meter was
160
+ accurate or a rule was fair, and it can only rebuild a result when the bytes
161
+ it names were kept. Retained payloads contain your billing data; store them
162
+ like the invoices they came from.
163
+
128
164
  ## How it works
129
165
 
130
166
  ```
@@ -151,7 +187,7 @@ Two design choices worth calling out:
151
187
 
152
188
  ## Case studies and paper
153
189
 
154
- Six studies push real (or realistically simulated) inference workloads through unalloc's own adapters, including a run with vLLM on an NVIDIA H100. They exposed nine defects in the tool along the way, all fixed with regression tests, and they're written up as a paper: **[paper/unalloc-case-studies.pdf](https://github.com/timurista/unalloc/blob/main/paper/unalloc-case-studies.pdf)**. For the readable version, see the [blog post](https://timurista.github.io/unalloc/blog/who-pays-for-the-kv-cache.html) and the [GPU addendum](https://timurista.github.io/unalloc/blog/gpu-validation.html) on the [project site](https://timurista.github.io/unalloc/).
190
+ Six studies push real (or realistically simulated) inference workloads through unalloc's own adapters, including a run with vLLM on an NVIDIA H100. They exposed nine defects in the tool along the way, all fixed with regression tests, and they're written up as a paper: **[Who Pays for the KV Cache? (arXiv:2609.24991)](https://arxiv.org/abs/2609.24991)**, a preprint that has not been peer reviewed. The PDF built from this repository is at [paper/unalloc-case-studies.pdf](https://github.com/timurista/unalloc/blob/main/paper/unalloc-case-studies.pdf). For the readable version, see the [blog post](https://timurista.github.io/unalloc/blog/who-pays-for-the-kv-cache.html) and the [GPU addendum](https://timurista.github.io/unalloc/blog/gpu-validation.html) on the [project site](https://timurista.github.io/unalloc/).
155
191
 
156
192
  | Study | What runs | Headline |
157
193
  | --- | --- | --- |
@@ -203,6 +239,7 @@ What `unalloc` does is narrower: read all four sources yourself — OpenCost, a
203
239
  - [ ] Per-feature unit economics using the `quantity`/`unit` fields already carried on `CostRow`
204
240
  - [ ] Azure OpenAI and Bedrock adapters
205
241
  - [x] `--budget` mode: exit non-zero when unallocated share crosses a threshold, for CI
242
+ - [x] Result provenance and `unalloc verify`: rebuild an earlier report from retained inputs, or say why it can't be
206
243
  - [ ] `--exclude label=value` on a source, so gateway traffic can be dropped from provider billing instead of whole sources
207
244
  - [ ] Warn when distinct raw label keys collide on one canonical key
208
245
 
@@ -217,16 +254,31 @@ ruff check . && pytest
217
254
 
218
255
  ## Citing
219
256
 
220
- If you use unalloc or its case studies, please cite the archived release. GitHub's "Cite this repository" button produces the same entry from [`CITATION.cff`](https://github.com/timurista/unalloc/blob/main/CITATION.cff).
257
+ If you use the findings, please cite the paper, a preprint on arXiv ([arXiv:2609.24991](https://arxiv.org/abs/2609.24991), cs.DC):
258
+
259
+ ```bibtex
260
+ @misc{urista_kvcache_2026,
261
+ author = {Urista, Timothy},
262
+ title = {Who Pays for the {KV} Cache? Attributing Shared {AI} Inference Spend Across {Kubernetes} and {LLM} Provider Bills},
263
+ year = {2026},
264
+ eprint = {2609.24991},
265
+ archivePrefix = {arXiv},
266
+ primaryClass = {cs.DC},
267
+ doi = {10.48550/arXiv.2609.24991},
268
+ url = {https://arxiv.org/abs/2609.24991}
269
+ }
270
+ ```
271
+
272
+ If you use the software itself, cite the archived release. GitHub's "Cite this repository" button produces the same entry from [`CITATION.cff`](https://github.com/timurista/unalloc/blob/main/CITATION.cff).
221
273
 
222
- - **All versions:** [doi:10.5281/zenodo.22761012](https://doi.org/10.5281/zenodo.22761012), which always resolves to the latest release — currently 0.2.2, the code the paper describes
274
+ - **All versions:** [doi:10.5281/zenodo.22761012](https://doi.org/10.5281/zenodo.22761012), which always resolves to the latest release — currently 0.3.0. The paper describes 0.2.3; 0.3.0 adds result provenance and `unalloc verify` and changes no attribution result
223
275
  - **Version 0.2.1:** [doi:10.5281/zenodo.22761013](https://doi.org/10.5281/zenodo.22761013)
224
276
 
225
277
  ```bibtex
226
278
  @software{urista_unalloc_2026,
227
279
  author = {Urista, Timothy},
228
280
  title = {unalloc: find the AI spend nobody owns},
229
- version = {0.2.2},
281
+ version = {0.3.0},
230
282
  year = {2026},
231
283
  doi = {10.5281/zenodo.22761012},
232
284
  url = {https://github.com/timurista/unalloc}
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "unalloc"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  description = "Find the AI spend nobody owns. Joins OpenCost allocation data with LLM provider bills and reports what's unattributed."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -2,5 +2,5 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.2.2"
5
+ __version__ = "0.3.0"
6
6
  __all__ = ["__version__"]