ai-costs 2.2.3__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 (55) hide show
  1. ai_costs-2.2.3/LICENSE +21 -0
  2. ai_costs-2.2.3/PKG-INFO +283 -0
  3. ai_costs-2.2.3/README.md +259 -0
  4. ai_costs-2.2.3/pyproject.toml +86 -0
  5. ai_costs-2.2.3/setup.cfg +4 -0
  6. ai_costs-2.2.3/src/ai_cost/__init__.py +3 -0
  7. ai_costs-2.2.3/src/ai_cost/__main__.py +16 -0
  8. ai_costs-2.2.3/src/ai_cost/attribution.py +133 -0
  9. ai_costs-2.2.3/src/ai_cost/cli.py +614 -0
  10. ai_costs-2.2.3/src/ai_cost/collectors/__init__.py +28 -0
  11. ai_costs-2.2.3/src/ai_cost/collectors/claude.py +311 -0
  12. ai_costs-2.2.3/src/ai_cost/collectors/codex.py +302 -0
  13. ai_costs-2.2.3/src/ai_cost/collectors/gemini_cli.py +184 -0
  14. ai_costs-2.2.3/src/ai_cost/collectors/github.py +215 -0
  15. ai_costs-2.2.3/src/ai_cost/collectors/grok_build.py +154 -0
  16. ai_costs-2.2.3/src/ai_cost/collectors/usage_log.py +326 -0
  17. ai_costs-2.2.3/src/ai_cost/config.py +927 -0
  18. ai_costs-2.2.3/src/ai_cost/daily.py +359 -0
  19. ai_costs-2.2.3/src/ai_cost/data/config.json +158 -0
  20. ai_costs-2.2.3/src/ai_cost/data/prices.json +382 -0
  21. ai_costs-2.2.3/src/ai_cost/errors.py +32 -0
  22. ai_costs-2.2.3/src/ai_cost/groups.py +439 -0
  23. ai_costs-2.2.3/src/ai_cost/log.py +343 -0
  24. ai_costs-2.2.3/src/ai_cost/models.py +310 -0
  25. ai_costs-2.2.3/src/ai_cost/ops.py +1221 -0
  26. ai_costs-2.2.3/src/ai_cost/plugins.py +179 -0
  27. ai_costs-2.2.3/src/ai_cost/prices_check.py +550 -0
  28. ai_costs-2.2.3/src/ai_cost/pricing.py +242 -0
  29. ai_costs-2.2.3/src/ai_cost/process.py +15 -0
  30. ai_costs-2.2.3/src/ai_cost/reconcile.py +211 -0
  31. ai_costs-2.2.3/src/ai_cost/render.py +287 -0
  32. ai_costs-2.2.3/src/ai_cost/selftest.py +64 -0
  33. ai_costs-2.2.3/src/ai_cost/tests/__init__.py +1 -0
  34. ai_costs-2.2.3/src/ai_cost/tests/fixtures.py +280 -0
  35. ai_costs-2.2.3/src/ai_cost/tests/test_attribution.py +178 -0
  36. ai_costs-2.2.3/src/ai_cost/tests/test_collectors.py +633 -0
  37. ai_costs-2.2.3/src/ai_cost/tests/test_config_and_prices.py +1078 -0
  38. ai_costs-2.2.3/src/ai_cost/tests/test_daily_and_reconcile.py +500 -0
  39. ai_costs-2.2.3/src/ai_cost/tests/test_distribution.py +27 -0
  40. ai_costs-2.2.3/src/ai_cost/tests/test_gemini_cli.py +130 -0
  41. ai_costs-2.2.3/src/ai_cost/tests/test_grok_build.py +168 -0
  42. ai_costs-2.2.3/src/ai_cost/tests/test_log.py +269 -0
  43. ai_costs-2.2.3/src/ai_cost/tests/test_plugins.py +121 -0
  44. ai_costs-2.2.3/src/ai_cost/tests/test_pricing_and_groups.py +509 -0
  45. ai_costs-2.2.3/src/ai_cost/tests/test_project_scope.py +260 -0
  46. ai_costs-2.2.3/src/ai_cost/tests/test_report_and_cli.py +1370 -0
  47. ai_costs-2.2.3/src/ai_cost/tests/test_usage_log.py +809 -0
  48. ai_costs-2.2.3/src/ai_cost/tests/test_values.py +75 -0
  49. ai_costs-2.2.3/src/ai_cost/timeutil.py +62 -0
  50. ai_costs-2.2.3/src/ai_cost/values.py +88 -0
  51. ai_costs-2.2.3/src/ai_costs.egg-info/PKG-INFO +283 -0
  52. ai_costs-2.2.3/src/ai_costs.egg-info/SOURCES.txt +53 -0
  53. ai_costs-2.2.3/src/ai_costs.egg-info/dependency_links.txt +1 -0
  54. ai_costs-2.2.3/src/ai_costs.egg-info/entry_points.txt +2 -0
  55. ai_costs-2.2.3/src/ai_costs.egg-info/top_level.txt +1 -0
ai_costs-2.2.3/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Quantum Media Technologies sp. z o.o. (https://www.qmediat.io)
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,283 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-costs
3
+ Version: 2.2.3
4
+ Summary: What a block of AI-assisted work cost: real, API-only equivalent, vendor quote — from the usage already on disk
5
+ Author-email: Quantum Media Technologies <dev@qmediat.io>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://www.qmediat.io/open-source
8
+ Project-URL: Repository, https://github.com/qmediat/ai-cost
9
+ Project-URL: Issues, https://github.com/qmediat/ai-cost/issues
10
+ Project-URL: Changelog, https://github.com/qmediat/ai-cost/blob/main/CHANGELOG.md
11
+ Keywords: llm,cost,tokens,pricing,claude,codex,gemini,openai,anthropic
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Software Development
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Dynamic: license-file
24
+
25
+ <p align="left">
26
+ <a href="https://www.qmediat.io/open-source?utm_source=oss-readme&utm_medium=ai-cost&utm_campaign=open-source">
27
+ <picture>
28
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/qmediat/.github/b35746f6b3c933d9eeb539033ef40ea9876349ae/assets/qmediat-wordmark-light.svg">
29
+ <img src="https://raw.githubusercontent.com/qmediat/.github/b35746f6b3c933d9eeb539033ef40ea9876349ae/assets/qmediat-wordmark-badge.svg" alt="Quantum Media Technologies" height="40">
30
+ </picture>
31
+ </a>
32
+ </p>
33
+
34
+ # ai-cost — what did the AI-assisted work cost?
35
+
36
+ One command, three honest answers for a block of work done with LLMs — through Claude Code, Codex CLI, Gemini CLI,
37
+ Grok Build CLI, your own scripts and applications, or anything a plugin can read:
38
+
39
+ | group | question it answers | how |
40
+ |---|---|---|
41
+ | **real** | What did *you* pay? | Subscriptions prorated to the window + pay-per-token API keys at list price. No promos, no negotiated discounts. |
42
+ | **api** | What would the same tokens cost on pay-per-use APIs alone, as if no subscription existed? | Every token at the provider's list price, cache tiers applied the way the APIs bill. GitHub included: Copilot code reviews at the per-credit overage, Actions minutes at the per-minute price (private repos). |
43
+ | **vendor** | What would an outside firm have quoted for the same scope? | Effort bands per item × three staffing options (junior / mid / senior differ in rate *and* in time, juniors get senior review), packaged with an integration allowance, minimum size and rounding. |
44
+
45
+ It is a Python 3 package (stdlib only) shipped as one executable file: a zipapp that
46
+ `python3 scripts/build.py src dist/ai-cost` builds from `src/ai_cost/`, tests included. It reads what is already on disk — Claude Code
47
+ transcripts, Codex CLI rollouts, Gemini CLI sessions, Grok Build CLI sessions, the usage log any program can write —
48
+ and whatever a plugin adds. Nothing is sent anywhere. The network is used only when asked for: the optional price-drift check reads the
49
+ vendors' public pricing pages, and `--github` queries GitHub through `gh` for live counts.
50
+
51
+ ```
52
+ $ ai-cost report --session latest
53
+ # AI cost report — 2026-09-19T15:01:09Z → 2026-09-20T00:35:24Z (9.6 h)
54
+ ## 1. Real cost (what you paid): 54.71 USD
55
+ ## 2. API-only cost (as if no subscription existed): 175.02 USD
56
+ ## 3. Vendor quote (what an outside firm would charge): profile `consultancy-eu`
57
+ | Staffing | Rate EUR/h | Time factor | Senior review | Hours | Working days | Quote EUR |
58
+ | junior | 70 | 1.8 | 25% | 148–296 | 24.7–49.3 | 12 950–25 900 |
59
+ | mid | 100 | 1.2 | 10% | 96–192 | 16–32 | 12 600–25 200 |
60
+ | senior | 150 | 1.0 | 0% | 76–152 | 12.7–25.3 | 11 250–22 500 |
61
+ ```
62
+
63
+ ## Install
64
+
65
+ New here? [`docs/SETUP.md`](docs/SETUP.md) is the deterministic path — the first 10 minutes with a Verify command per
66
+ step, what is read from where, the config reference, the usage-log hook for apps and MCP servers (Python and Node),
67
+ a plugin skeleton, the daily job, `reconcile`, exit codes and every `doctor` line — written for the person and for the
68
+ AI agent operating the tool on their behalf.
69
+
70
+ ```bash
71
+ pipx install ai-costs # or: pip install ai-costs (the PyPI name is ai-costs; the command is ai-cost)
72
+ # or the single executable file from the GitHub release: put it on PATH and run it
73
+ # or no package at all: standalone/SKILL.md is a one-file Claude Code skill that counts and prices with a stdlib snippet
74
+ ai-cost selftest [-v] # the package's own tests, run from the shipped file, offline
75
+ ai-cost install --init-config # writes ~/.config/ai-cost/config.json — put YOUR plans, seats and budgets there
76
+ ai-cost install --schedule 3 # price drift check every 3 days (launchd on macOS, cron elsewhere)
77
+ ai-cost install --schedule-reports # yesterday's global + per-project reports every morning (06:40 local; --at HH:MM)
78
+ ai-cost doctor # sources found, plugins loaded, config, price freshness, both schedules, last daily run
79
+ ```
80
+
81
+ Requirements: Python ≥ 3.9. `gh` only for `--github`.
82
+
83
+ ## Commands
84
+
85
+ | command | does |
86
+ |---|---|
87
+ | `report` (default) | the three groups for a window. `--session <id\|latest\|all\|path>` `--project DIR` (every source scoped to that directory — "Per project" below) `--all-projects` · `--unpriced fail\|skip` · `--since/--until` `--hours N` (N > 0; default the config's `window_default_hours`, 24) · `--github owner/repo` · `--group real,api,vendor` · `--items scope.md` `--vendor-profile NAME` · `--setting PLUGIN.KEY=VALUE` · `--format md\|json\|table` `--out FILE` `--detail` |
88
+ | `log` | append one usage line for a request your program made: `--provider openai --model gpt-5.5 --input 1200 --output 300 [--cost 0.0123] [--ref job-42] [--tag ci]`, or `--from-response resp.json [--provider …]` for a raw Anthropic / Google / OpenAI response; `--log FILE` picks the file — a model the pricebook does not list needs `--cost` (or an entry in your prices file): the line is refused rather than written as a row every report would fail on |
89
+ | `prices show` | merged registry (shipped defaults → your overrides) |
90
+ | `prices check` | re-read every vendor page, report `confirmed` / `changed?` / `not-found` / `fetch-failed`; exit 4 on drift |
91
+ | `prices update` | check, then write unambiguous changes to **your** `~/.config/ai-cost/prices.json` |
92
+ | `doctor` | diagnostics: sources found (Claude, Codex, Gemini CLI, Grok Build), plugins, config, prices, both schedules, the newest daily index, the last reconciliation; exit 1 on problems |
93
+ | `daily` | write one UTC day's reports: `global.{md,json}` over every project and `<project dir>.{md,json}` per Claude project touched that day, plus `index.json` — `--date YYYY-MM-DD` (default yesterday), `--out DIR` (default `AI_COST_REPORTS_DIR` or `$XDG_DATA_HOME/ai-cost/reports`), `--quiet`; the job `install --schedule-reports` runs |
94
+ | `reconcile` | `--provider xai --usd 156.11 [--tokens 80200000] --hours 24` (or `--since/--until`): the local count of one provider against the figure its console shows, gap vs `reconcile.tolerance_pct` (5); exit 1 above it; history in `~/.local/state/ai-cost/reconcile.jsonl` |
95
+ | `monitor` | rolling-window totals (`--hours N`, default the config's `window_default_hours`) → `~/.local/state/ai-cost/history.jsonl` with `--append`; budget check from config, exit 3 on breach; `--history N` |
96
+ | `install` | `--init-config`, `--schedule DAYS`, `--unschedule`, `--schedule-reports [--at HH:MM]` (the daily job, 06:40 local by default), `--unschedule-reports` |
97
+ | `selftest` | offline, no network, no secrets; also runs the tests of every loaded plugin that ships some |
98
+
99
+ The window comes from `--since/--until`, else from the session's first and last timestamp, else the last `--hours`
100
+ (the config's `window_default_hours`, 24 by default).
101
+ Every source is filtered to that window, so a report is reproducible. Without `--session/--project`, the cwd's
102
+ project transcripts are read (every project's when the cwd has none). Codex usage is summed per turn inside the
103
+ window, so a session resumed from before it or still running after it contributes only the turns in between.
104
+
105
+ **Per project.** Every CLI row carries the working directory its CLI ran in (a Codex rollout's `cwd`, a Gemini CLI
106
+ folder mapped through `~/.gemini/projects.json`, a Grok Build session's directory), and a row that names no scope
107
+ takes the scope of the row sharing its ref (a usage-log line with `session`, a ledger line). `--project DIR` — and
108
+ a plain `report` run from inside a project — then keeps the project's Claude transcripts and every row whose
109
+ directory is `DIR` or below it (symlinks resolved), leaves out rows of other directories and rows of named
110
+ workspaces that are no directory (a review workspace, a folder the map does not know — `--attribute` places those),
111
+ and includes rows that name nothing; the header counts each group per source, so the number is honest about what it
112
+ could not place. A git worktree is its own project. `--all-projects` and `--session <id>` never filter.
113
+
114
+ ## Sources it reads
115
+
116
+ | source | path | used for |
117
+ |---|---|---|
118
+ | Claude Code transcripts | `~/.claude/projects/<project>/<session>.jsonl` (+ `<session>/**/*.jsonl` subagents) | tokens per model incl. cache write 5 m / 1 h, cache read, server tools; message ids de-duplicated |
119
+ | Codex CLI rollouts | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` | one row per model and UTC day of every session with turns inside the window: input / cached / output per turn. A rollout that names a ChatGPT plan (`rate_limits.plan_type`) is a subscription session; the rest follow `providers.openai.billing` and stay `unknown` without it. The header's `cwd` is the row's working directory |
120
+ | Gemini CLI sessions | `~/.gemini/tmp/<project>/chats/session-*.jsonl` (older versions: `.json`) | one row per model message (streamed repeats of one message id counted once); `input` includes `cached`, `thoughts` are billed as output. `~/.gemini/projects.json` maps the folder (a name, or the sha256 of the path in older versions) to the working directory |
121
+ | Grok Build CLI sessions | `~/.grok/sessions/<working directory, URL-encoded>/<session>/usage.json` (`GROK_HOME` overrides the home) | one row per turn and model: `inputTokens` (includes `cachedReadTokens`), `outputTokens` + `reasoningTokens` as output, `modelCalls` as the requests, `costUsdTicks / 1e10` as the CLI's own cost estimate — `real` uses it when `providers.xai.trust_cli_cost` (default), `api` prices at list; billing follows `providers.xai.billing` |
122
+ | usage log | `$XDG_DATA_HOME/ai-cost/usage.jsonl` (`AI_COST_USAGE_LOG` overrides; more files in config `usage_logs`) | one JSON line per request that any program writes — `ai-cost log` or the schema below; provider-native counters, `cost` when the program knows it, `event_id` read once |
123
+ | GitHub (optional) | `gh` | Copilot reviews submitted inside the window, Actions **billable** minutes per runner OS (`/actions/runs/{id}/timing`, elapsed time as fallback), private/public |
124
+ | plugins | whatever they read | see below |
125
+
126
+ A row's billing comes first from what its source saw (a named plan, a log line's `billing` key), then from the
127
+ provider's configured billing, and stays `unknown` otherwise — an unknown row is priced in `api` and left out of
128
+ `real` whatever figure it carries (a figure without a rule is an estimate, not a charge), and the report header says
129
+ how many there were. A reported `cost` is the charge of a row that says it paid per use.
130
+
131
+ ## Let an app or script report its own calls
132
+
133
+ Any program appends one JSON line per request to the usage log (`$XDG_DATA_HOME/ai-cost/usage.jsonl`,
134
+ `~/.local/share/ai-cost/usage.jsonl` by default, or the file named by `AI_COST_USAGE_LOG`); the report reads it
135
+ like every other source. Required: `schema` (1), `at` (ISO 8601), `provider`, `model`, and `tokens` or `cost`.
136
+ Counters use the provider's own names so the pricing stays exact:
137
+
138
+ ```json
139
+ {"schema":1,"at":"2026-09-20T10:00:00Z","provider":"openai","model":"gpt-5.5","source":"my-app",
140
+ "tokens":{"input":1200,"cached_input":800,"output":300},"cost":0.0123,"ref":"job-42","branch":"feat/x","tags":["ci"]}
141
+ ```
142
+
143
+ Counters per provider: anthropic `input, output, cache_read, cache_write_5m, cache_write_1h` (or
144
+ `cache_write_unsplit` when the response does not say which TTL) and `web_search` (searches, billed per thousand); openai and OpenAI-compatible APIs `input,
145
+ cached_input, output`; google `prompt, cached, output, thoughts` (thoughts count as output — for every provider: reasoning tokens are billed as output); deepseek `cache_hit,
146
+ cache_miss, output`; anything else `input, output`. A counter the provider's formula never reads (an `input` on a
147
+ deepseek line) makes the line a counted skip, and `ai-cost log` / `record()` refuse it up front — nothing is priced
148
+ at zero in silence. Optional keys: `cost` (what the request was charged, USD — a `currency` other than `USD` is a
149
+ counted skip; a row with `cost` and no list price is priced by it), `billing` (`api` | `subscription`; a line with tokens or a cost is
150
+ `api` by default), `ref`, `session`, `branch`, `pr`, `tags`, `source`, `event_id` (a repeated id within one file is
151
+ read once). From Python: `ai_cost.log.record("openai", "gpt-5.5", {"input": 12, "output": 3}, ref="job-42")`;
152
+ from a shell: `ai-cost log --from-response resp.json --provider openai` maps a raw API response's usage block
153
+ (Google, DeepSeek and Anthropic name themselves — Google by `usageMetadata`, DeepSeek by `prompt_cache_hit_tokens`, Anthropic by `type: message` or its cache counters; a bare OpenAI-shaped usage block needs `--provider`). A line that fails validation (a boolean, a
154
+ negative or fractional count, a malformed provider id) is a counted skip, never a guess; a well-formed provider or model the pricebook does not list, with no cost, is an unpriced row under `--unpriced`.
155
+
156
+ ## Plugins
157
+
158
+ Any program can add a source (rows for a window), an enricher (a pass over every row, e.g. to settle what a key was
159
+ actually charged), doctor lines and tests. A plugin is a module exporting `PLUGIN = ai_cost.plugins.Plugin(...)`,
160
+ discovered through the `ai_cost.plugins` entry-point group, the config list `plugins: ["my_package"]`, or the
161
+ environment variable `AI_COST_PLUGINS=my_package,other`. Its settings live under `plugin_settings.<name>` in the
162
+ config and can be overridden for one run with `--setting <name>.<key>=<value>`. The protocol is in
163
+ [`src/ai_cost/plugins.py`](src/ai_cost/plugins.py): a `Source` has a `name` and `collect(ctx) -> Collected`, an
164
+ `Enricher` has `enrich(rows, ctx) -> rows`, and `Context` gives them the paths, the config, the window, the request,
165
+ the plugin's settings and the shared `skipped` / `warnings` sinks.
166
+
167
+ ## Attributing cost to labels (pull requests, features, teams)
168
+
169
+ One session serves several tasks in turn, so a window alone cannot say what task X cost. `--attribute LABEL=REGEX`
170
+ (repeatable) labels every priced row whole: by its branch / workspace / PR number when exactly one label
171
+ matches (several → `mixed`), otherwise by the label that matches most of the paths and commands the turn touched
172
+ (tie → `mixed`, none → `unattributed`). The section prints each label's calls, API-equivalent cost, the part of it
173
+ that was cache-read context, cash, subscription share (split by the label's share of the plan's provider — a
174
+ policy, printed as such) and the keys it absorbed, and the lines sum to the window totals.
175
+
176
+ ```
177
+ ai-cost report --since 2026-09-20T11:00Z --until 2026-09-20T14:36Z --all-projects \
178
+ --attribute "billing=feat/billing|src/billing/|pulls/66" \
179
+ --attribute "docs=docs/|README"
180
+ ```
181
+
182
+ ## Per project, per day: the daily job
183
+
184
+ `ai-cost daily` prices the UTC day before today once over every project and once per Claude project directory whose
185
+ transcripts were written that day, and writes `<reports>/<date>/global.md` + `global.json`, `<project dir
186
+ name>.md` + `.json` (the `--project` scope of the project's working directory, read from its transcripts' `cwd`)
187
+ and `index.json` — `{day, generated_at, window, directory, real_usd, api_usd, cash_usd, rows, projects: [{project_dir,
188
+ path, markdown, json_file, real_usd, api_usd, rows}], notes}`. A project no transcript can place, one with no usage
189
+ that day, or one whose report failed is a note, never an abort; unpriced rows are skipped and counted, so a new
190
+ model never fails the job. `<reports>` is `AI_COST_REPORTS_DIR`, else `$XDG_DATA_HOME/ai-cost/reports`
191
+ (`~/.local/share/ai-cost/reports`). `ai-cost install --schedule-reports` registers the job at 06:40 local time
192
+ (`--at HH:MM` to move it; launchd on macOS runs a missed time at wake, cron does not); `--unschedule-reports`
193
+ removes it; `doctor` shows whether it is installed and what the newest index says. `ai-cost daily --date 2026-09-20`
194
+ re-runs a day; `--out DIR` writes elsewhere.
195
+
196
+ ## Reconcile with the provider's console
197
+
198
+ `ai-cost reconcile --provider xai --usd 156.11 --tokens 80200000 --hours 24` prints the local count for the window
199
+ (the cash `real` attributes to the provider — a CLI's own figure where the config trusts it, else the list price —
200
+ and the tokens it billed, every counter once) next to the figures you read off the provider's console or export, the
201
+ gap in percent against `reconcile.tolerance_pct` (5 in the shipped config) and a verdict; above tolerance is exit 1.
202
+ Every run is appended to `~/.local/state/ai-cost/reconcile.jsonl` and `doctor` shows the last one. Nothing is
203
+ fetched: no provider offers one public spend endpoint every user could call, so the figure is yours to paste (or a
204
+ script's, from an export). A gap is something to look at — the window's edges, a source the tool does not read, a
205
+ price it applies differently — never a number to hide.
206
+
207
+ ## JSON contract (`--format json`)
208
+
209
+ Top level: `version`, `generated_at`, `window` (`{start, end}`), `window_iso` (`[start, end]`), `window_hours`,
210
+ `row_count`, `sources`, `warnings`, `skipped` (`[{source, path, reason}]`), `prices_checked_at`, `real`
211
+ (`subscriptions[]`, `usage[]`, `cash_usd`, `subscription_usd`, `total_usd`, `unknown_billing`), `api` (`lines[]`,
212
+ `total_usd`), `vendor` (when items exist), `attribution` (with `--attribute`). A line's `calls` is what it folded in
213
+ (rows, review runs, Copilot reviews — the `Runs` column) and `model_calls` the API requests its sources reported
214
+ (`Model calls`; 0 where none does); `--detail` adds `rows[]`, where
215
+ `cost_reported` is the source's own figure for the row: cash for an `api` row (a charge the source reported), the
216
+ source's list-price estimate for a `subscription` row (never counted as cash; the API-equivalent fallback when the
217
+ model has no list price), a part of the session's figure when a session spans several rows; `source` names the
218
+ source that produced the row and `kind` its record shape (`transcript`, `session`, `chat`, `log`, `ledger`,
219
+ `review`, `copilot`, `actions`). Every dataclass property is serialised, so computed totals are always present.
220
+ Changes since 1.0: `window` is an object (the list moved to `window_iso`), `seats` multiply only per-seat plans, an
221
+ unpriced model exits 5 unless `--unpriced skip`.
222
+
223
+ ## Configuration
224
+
225
+ `~/.config/ai-cost/config.json` (`install --init-config` writes it from the shipped defaults,
226
+ [`src/ai_cost/data/config.json`](src/ai_cost/data/config.json); it ships no subscriptions, add yours) — subscriptions
227
+ (plan, seats, attribution `time` | `full` | `none`), provider billing switches (`providers.<name>.billing`, the rule for that provider's rows that carry no billing evidence of their own — a plugin's xai or deepseek rows follow it as the built-in sources follow `anthropic.billing`,
228
+ `openai.billing`, `google.billing` for Gemini CLI sessions, `github.copilot_plan_exhausted`, `xai.trust_cli_cost` — a
229
+ positive CLI-reported cost is the row's
230
+ cash, 0 or absent means the CLI did not price the run and the list price applies), budgets, item sizing thresholds,
231
+ vendor profiles, `plugins` and `plugin_settings`. A `null` anywhere in it means "no override" (the shipped value
232
+ stays); inside a list a `null` is an error — a list such as `subscriptions` replaces the shipped list whole, so there
233
+ is no shipped item a `null` could stand for. `openai.default_model` names the model of a Codex rollout that does not
234
+ say its own (empty by default: such rows are `unknown`).
235
+ `~/.config/ai-cost/prices.json` — price overrides in the shape of
236
+ [`src/ai_cost/data/prices.json`](src/ai_cost/data/prices.json), the registry the shipped file carries (the single
237
+ source; `prices show --format json --snapshot` prints it). An override may change numbers, never a price's shape;
238
+ a `null` anywhere in your file means "no override" (the shipped value stays), and to clear a shipped block set it to an
239
+ empty value: `"next": {}`, `"long": {}`, `"valid_until": ""`; a plan set to `null` is removed.
240
+ `auto_check_days: 0` turns the price-drift check off. `reconcile.tolerance_pct` (5) is the gap `reconcile` accepts.
241
+ Env: `AI_COST_CONFIG`, `AI_COST_PRICES`, `AI_COST_CONFIG_DIR`, `AI_COST_STATE_DIR`, `AI_COST_USAGE_LOG` (else
242
+ `$XDG_DATA_HOME/ai-cost/usage.jsonl`), `AI_COST_REPORTS_DIR` (else `$XDG_DATA_HOME/ai-cost/reports`), `AI_COST_OFFLINE=1`,
243
+ `AI_COST_PLUGINS`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `GEMINI_CLI_HOME` (`~/.gemini` by default), `GROK_HOME`
244
+ (`~/.grok` by default).
245
+
246
+ Where the numbers come from and what they leave out: [`references/pricing-sources.md`](references/pricing-sources.md)
247
+ and [`references/vendor-pricing.md`](references/vendor-pricing.md).
248
+
249
+ ## Development
250
+
251
+ `src/ai_cost/` is the package: `models.py` (the typed data — `UsageRow`, `Tokens`, `Window`, `WorkItem`, `Report`),
252
+ `pricing.py` (one pricer per price shape, chosen from a dispatch table), `groups.py` (real / api / vendor),
253
+ `collectors/` (one module per built-in source — `claude`, `codex`, `gemini_cli`, `grok_build`, `github`, `usage_log` — each
254
+ returning `Collected` rows plus counted `Skipped` records), `plugins.py` (the source / enricher protocol and
255
+ discovery), `config.py` with `data/{prices,config}.json`, `prices_check.py`, `ops.py` (report assembly and the
256
+ per-project scope, doctor, monitor, the scheduled jobs), `daily.py`, `reconcile.py`, `render.py`, `cli.py`. The design note and
257
+ the architecture decision records are kept with the development source, not in the published tree.
258
+
259
+ ```bash
260
+ PYTHONPATH=src python3 -m ai_cost selftest # or python3 -m pytest — the tests live in src/ai_cost/tests/
261
+ python3 scripts/build.py src dist/ai-cost # the shipped single file, reproducibly (fixed timestamps: a rebuild is byte-identical)
262
+ ```
263
+
264
+ ## Honest limits
265
+
266
+ - `real` attributes subscriptions by time (window hours / 730). A plan you would pay for anyway is a sunk cost; the
267
+ `cash_usd` line is the marginal money that actually left the account.
268
+ - Gemini's implicit-cache discount and DeepSeek's off-peak halving are modelled from the documented rules, not from
269
+ your invoice. Check the provider consoles when it matters.
270
+ - The core has no built-in work items: without `--items` (or a plugin that supplies items) there is no vendor
271
+ quote. A plugin's automatic sizing is a proxy — hand-size the scope for a quote you will show anyone.
272
+ - Price pages change; the checker is a heuristic and never rewrites a price silently.
273
+ - `api` prices a row's aggregated counters: a CLI turn of several model calls never gets a long-context tier even if
274
+ one call crossed the threshold. Where a CLI reports its own cost (Grok Build), `real` trusts it and `reconcile`
275
+ measures the gap.
276
+ - Under `--project`, rows that name no scope at all (a usage-log line without `session`) are included in every
277
+ project's report, and rows of named workspaces that are no directory are left out; both are counted in the header.
278
+
279
+ MIT © 2026 Quantum Media Technologies sp. z o.o. Not affiliated with Anthropic, OpenAI, Google, xAI, DeepSeek or GitHub.
280
+
281
+ ---
282
+
283
+ Made by [Quantum Media Technologies](https://www.qmediat.io/open-source?utm_source=oss-readme&utm_medium=ai-cost&utm_campaign=open-source) · [more open source from qmediat](https://github.com/qmediat)
@@ -0,0 +1,259 @@
1
+ <p align="left">
2
+ <a href="https://www.qmediat.io/open-source?utm_source=oss-readme&utm_medium=ai-cost&utm_campaign=open-source">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/qmediat/.github/b35746f6b3c933d9eeb539033ef40ea9876349ae/assets/qmediat-wordmark-light.svg">
5
+ <img src="https://raw.githubusercontent.com/qmediat/.github/b35746f6b3c933d9eeb539033ef40ea9876349ae/assets/qmediat-wordmark-badge.svg" alt="Quantum Media Technologies" height="40">
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ # ai-cost — what did the AI-assisted work cost?
11
+
12
+ One command, three honest answers for a block of work done with LLMs — through Claude Code, Codex CLI, Gemini CLI,
13
+ Grok Build CLI, your own scripts and applications, or anything a plugin can read:
14
+
15
+ | group | question it answers | how |
16
+ |---|---|---|
17
+ | **real** | What did *you* pay? | Subscriptions prorated to the window + pay-per-token API keys at list price. No promos, no negotiated discounts. |
18
+ | **api** | What would the same tokens cost on pay-per-use APIs alone, as if no subscription existed? | Every token at the provider's list price, cache tiers applied the way the APIs bill. GitHub included: Copilot code reviews at the per-credit overage, Actions minutes at the per-minute price (private repos). |
19
+ | **vendor** | What would an outside firm have quoted for the same scope? | Effort bands per item × three staffing options (junior / mid / senior differ in rate *and* in time, juniors get senior review), packaged with an integration allowance, minimum size and rounding. |
20
+
21
+ It is a Python 3 package (stdlib only) shipped as one executable file: a zipapp that
22
+ `python3 scripts/build.py src dist/ai-cost` builds from `src/ai_cost/`, tests included. It reads what is already on disk — Claude Code
23
+ transcripts, Codex CLI rollouts, Gemini CLI sessions, Grok Build CLI sessions, the usage log any program can write —
24
+ and whatever a plugin adds. Nothing is sent anywhere. The network is used only when asked for: the optional price-drift check reads the
25
+ vendors' public pricing pages, and `--github` queries GitHub through `gh` for live counts.
26
+
27
+ ```
28
+ $ ai-cost report --session latest
29
+ # AI cost report — 2026-09-19T15:01:09Z → 2026-09-20T00:35:24Z (9.6 h)
30
+ ## 1. Real cost (what you paid): 54.71 USD
31
+ ## 2. API-only cost (as if no subscription existed): 175.02 USD
32
+ ## 3. Vendor quote (what an outside firm would charge): profile `consultancy-eu`
33
+ | Staffing | Rate EUR/h | Time factor | Senior review | Hours | Working days | Quote EUR |
34
+ | junior | 70 | 1.8 | 25% | 148–296 | 24.7–49.3 | 12 950–25 900 |
35
+ | mid | 100 | 1.2 | 10% | 96–192 | 16–32 | 12 600–25 200 |
36
+ | senior | 150 | 1.0 | 0% | 76–152 | 12.7–25.3 | 11 250–22 500 |
37
+ ```
38
+
39
+ ## Install
40
+
41
+ New here? [`docs/SETUP.md`](docs/SETUP.md) is the deterministic path — the first 10 minutes with a Verify command per
42
+ step, what is read from where, the config reference, the usage-log hook for apps and MCP servers (Python and Node),
43
+ a plugin skeleton, the daily job, `reconcile`, exit codes and every `doctor` line — written for the person and for the
44
+ AI agent operating the tool on their behalf.
45
+
46
+ ```bash
47
+ pipx install ai-costs # or: pip install ai-costs (the PyPI name is ai-costs; the command is ai-cost)
48
+ # or the single executable file from the GitHub release: put it on PATH and run it
49
+ # or no package at all: standalone/SKILL.md is a one-file Claude Code skill that counts and prices with a stdlib snippet
50
+ ai-cost selftest [-v] # the package's own tests, run from the shipped file, offline
51
+ ai-cost install --init-config # writes ~/.config/ai-cost/config.json — put YOUR plans, seats and budgets there
52
+ ai-cost install --schedule 3 # price drift check every 3 days (launchd on macOS, cron elsewhere)
53
+ ai-cost install --schedule-reports # yesterday's global + per-project reports every morning (06:40 local; --at HH:MM)
54
+ ai-cost doctor # sources found, plugins loaded, config, price freshness, both schedules, last daily run
55
+ ```
56
+
57
+ Requirements: Python ≥ 3.9. `gh` only for `--github`.
58
+
59
+ ## Commands
60
+
61
+ | command | does |
62
+ |---|---|
63
+ | `report` (default) | the three groups for a window. `--session <id\|latest\|all\|path>` `--project DIR` (every source scoped to that directory — "Per project" below) `--all-projects` · `--unpriced fail\|skip` · `--since/--until` `--hours N` (N > 0; default the config's `window_default_hours`, 24) · `--github owner/repo` · `--group real,api,vendor` · `--items scope.md` `--vendor-profile NAME` · `--setting PLUGIN.KEY=VALUE` · `--format md\|json\|table` `--out FILE` `--detail` |
64
+ | `log` | append one usage line for a request your program made: `--provider openai --model gpt-5.5 --input 1200 --output 300 [--cost 0.0123] [--ref job-42] [--tag ci]`, or `--from-response resp.json [--provider …]` for a raw Anthropic / Google / OpenAI response; `--log FILE` picks the file — a model the pricebook does not list needs `--cost` (or an entry in your prices file): the line is refused rather than written as a row every report would fail on |
65
+ | `prices show` | merged registry (shipped defaults → your overrides) |
66
+ | `prices check` | re-read every vendor page, report `confirmed` / `changed?` / `not-found` / `fetch-failed`; exit 4 on drift |
67
+ | `prices update` | check, then write unambiguous changes to **your** `~/.config/ai-cost/prices.json` |
68
+ | `doctor` | diagnostics: sources found (Claude, Codex, Gemini CLI, Grok Build), plugins, config, prices, both schedules, the newest daily index, the last reconciliation; exit 1 on problems |
69
+ | `daily` | write one UTC day's reports: `global.{md,json}` over every project and `<project dir>.{md,json}` per Claude project touched that day, plus `index.json` — `--date YYYY-MM-DD` (default yesterday), `--out DIR` (default `AI_COST_REPORTS_DIR` or `$XDG_DATA_HOME/ai-cost/reports`), `--quiet`; the job `install --schedule-reports` runs |
70
+ | `reconcile` | `--provider xai --usd 156.11 [--tokens 80200000] --hours 24` (or `--since/--until`): the local count of one provider against the figure its console shows, gap vs `reconcile.tolerance_pct` (5); exit 1 above it; history in `~/.local/state/ai-cost/reconcile.jsonl` |
71
+ | `monitor` | rolling-window totals (`--hours N`, default the config's `window_default_hours`) → `~/.local/state/ai-cost/history.jsonl` with `--append`; budget check from config, exit 3 on breach; `--history N` |
72
+ | `install` | `--init-config`, `--schedule DAYS`, `--unschedule`, `--schedule-reports [--at HH:MM]` (the daily job, 06:40 local by default), `--unschedule-reports` |
73
+ | `selftest` | offline, no network, no secrets; also runs the tests of every loaded plugin that ships some |
74
+
75
+ The window comes from `--since/--until`, else from the session's first and last timestamp, else the last `--hours`
76
+ (the config's `window_default_hours`, 24 by default).
77
+ Every source is filtered to that window, so a report is reproducible. Without `--session/--project`, the cwd's
78
+ project transcripts are read (every project's when the cwd has none). Codex usage is summed per turn inside the
79
+ window, so a session resumed from before it or still running after it contributes only the turns in between.
80
+
81
+ **Per project.** Every CLI row carries the working directory its CLI ran in (a Codex rollout's `cwd`, a Gemini CLI
82
+ folder mapped through `~/.gemini/projects.json`, a Grok Build session's directory), and a row that names no scope
83
+ takes the scope of the row sharing its ref (a usage-log line with `session`, a ledger line). `--project DIR` — and
84
+ a plain `report` run from inside a project — then keeps the project's Claude transcripts and every row whose
85
+ directory is `DIR` or below it (symlinks resolved), leaves out rows of other directories and rows of named
86
+ workspaces that are no directory (a review workspace, a folder the map does not know — `--attribute` places those),
87
+ and includes rows that name nothing; the header counts each group per source, so the number is honest about what it
88
+ could not place. A git worktree is its own project. `--all-projects` and `--session <id>` never filter.
89
+
90
+ ## Sources it reads
91
+
92
+ | source | path | used for |
93
+ |---|---|---|
94
+ | Claude Code transcripts | `~/.claude/projects/<project>/<session>.jsonl` (+ `<session>/**/*.jsonl` subagents) | tokens per model incl. cache write 5 m / 1 h, cache read, server tools; message ids de-duplicated |
95
+ | Codex CLI rollouts | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` | one row per model and UTC day of every session with turns inside the window: input / cached / output per turn. A rollout that names a ChatGPT plan (`rate_limits.plan_type`) is a subscription session; the rest follow `providers.openai.billing` and stay `unknown` without it. The header's `cwd` is the row's working directory |
96
+ | Gemini CLI sessions | `~/.gemini/tmp/<project>/chats/session-*.jsonl` (older versions: `.json`) | one row per model message (streamed repeats of one message id counted once); `input` includes `cached`, `thoughts` are billed as output. `~/.gemini/projects.json` maps the folder (a name, or the sha256 of the path in older versions) to the working directory |
97
+ | Grok Build CLI sessions | `~/.grok/sessions/<working directory, URL-encoded>/<session>/usage.json` (`GROK_HOME` overrides the home) | one row per turn and model: `inputTokens` (includes `cachedReadTokens`), `outputTokens` + `reasoningTokens` as output, `modelCalls` as the requests, `costUsdTicks / 1e10` as the CLI's own cost estimate — `real` uses it when `providers.xai.trust_cli_cost` (default), `api` prices at list; billing follows `providers.xai.billing` |
98
+ | usage log | `$XDG_DATA_HOME/ai-cost/usage.jsonl` (`AI_COST_USAGE_LOG` overrides; more files in config `usage_logs`) | one JSON line per request that any program writes — `ai-cost log` or the schema below; provider-native counters, `cost` when the program knows it, `event_id` read once |
99
+ | GitHub (optional) | `gh` | Copilot reviews submitted inside the window, Actions **billable** minutes per runner OS (`/actions/runs/{id}/timing`, elapsed time as fallback), private/public |
100
+ | plugins | whatever they read | see below |
101
+
102
+ A row's billing comes first from what its source saw (a named plan, a log line's `billing` key), then from the
103
+ provider's configured billing, and stays `unknown` otherwise — an unknown row is priced in `api` and left out of
104
+ `real` whatever figure it carries (a figure without a rule is an estimate, not a charge), and the report header says
105
+ how many there were. A reported `cost` is the charge of a row that says it paid per use.
106
+
107
+ ## Let an app or script report its own calls
108
+
109
+ Any program appends one JSON line per request to the usage log (`$XDG_DATA_HOME/ai-cost/usage.jsonl`,
110
+ `~/.local/share/ai-cost/usage.jsonl` by default, or the file named by `AI_COST_USAGE_LOG`); the report reads it
111
+ like every other source. Required: `schema` (1), `at` (ISO 8601), `provider`, `model`, and `tokens` or `cost`.
112
+ Counters use the provider's own names so the pricing stays exact:
113
+
114
+ ```json
115
+ {"schema":1,"at":"2026-09-20T10:00:00Z","provider":"openai","model":"gpt-5.5","source":"my-app",
116
+ "tokens":{"input":1200,"cached_input":800,"output":300},"cost":0.0123,"ref":"job-42","branch":"feat/x","tags":["ci"]}
117
+ ```
118
+
119
+ Counters per provider: anthropic `input, output, cache_read, cache_write_5m, cache_write_1h` (or
120
+ `cache_write_unsplit` when the response does not say which TTL) and `web_search` (searches, billed per thousand); openai and OpenAI-compatible APIs `input,
121
+ cached_input, output`; google `prompt, cached, output, thoughts` (thoughts count as output — for every provider: reasoning tokens are billed as output); deepseek `cache_hit,
122
+ cache_miss, output`; anything else `input, output`. A counter the provider's formula never reads (an `input` on a
123
+ deepseek line) makes the line a counted skip, and `ai-cost log` / `record()` refuse it up front — nothing is priced
124
+ at zero in silence. Optional keys: `cost` (what the request was charged, USD — a `currency` other than `USD` is a
125
+ counted skip; a row with `cost` and no list price is priced by it), `billing` (`api` | `subscription`; a line with tokens or a cost is
126
+ `api` by default), `ref`, `session`, `branch`, `pr`, `tags`, `source`, `event_id` (a repeated id within one file is
127
+ read once). From Python: `ai_cost.log.record("openai", "gpt-5.5", {"input": 12, "output": 3}, ref="job-42")`;
128
+ from a shell: `ai-cost log --from-response resp.json --provider openai` maps a raw API response's usage block
129
+ (Google, DeepSeek and Anthropic name themselves — Google by `usageMetadata`, DeepSeek by `prompt_cache_hit_tokens`, Anthropic by `type: message` or its cache counters; a bare OpenAI-shaped usage block needs `--provider`). A line that fails validation (a boolean, a
130
+ negative or fractional count, a malformed provider id) is a counted skip, never a guess; a well-formed provider or model the pricebook does not list, with no cost, is an unpriced row under `--unpriced`.
131
+
132
+ ## Plugins
133
+
134
+ Any program can add a source (rows for a window), an enricher (a pass over every row, e.g. to settle what a key was
135
+ actually charged), doctor lines and tests. A plugin is a module exporting `PLUGIN = ai_cost.plugins.Plugin(...)`,
136
+ discovered through the `ai_cost.plugins` entry-point group, the config list `plugins: ["my_package"]`, or the
137
+ environment variable `AI_COST_PLUGINS=my_package,other`. Its settings live under `plugin_settings.<name>` in the
138
+ config and can be overridden for one run with `--setting <name>.<key>=<value>`. The protocol is in
139
+ [`src/ai_cost/plugins.py`](src/ai_cost/plugins.py): a `Source` has a `name` and `collect(ctx) -> Collected`, an
140
+ `Enricher` has `enrich(rows, ctx) -> rows`, and `Context` gives them the paths, the config, the window, the request,
141
+ the plugin's settings and the shared `skipped` / `warnings` sinks.
142
+
143
+ ## Attributing cost to labels (pull requests, features, teams)
144
+
145
+ One session serves several tasks in turn, so a window alone cannot say what task X cost. `--attribute LABEL=REGEX`
146
+ (repeatable) labels every priced row whole: by its branch / workspace / PR number when exactly one label
147
+ matches (several → `mixed`), otherwise by the label that matches most of the paths and commands the turn touched
148
+ (tie → `mixed`, none → `unattributed`). The section prints each label's calls, API-equivalent cost, the part of it
149
+ that was cache-read context, cash, subscription share (split by the label's share of the plan's provider — a
150
+ policy, printed as such) and the keys it absorbed, and the lines sum to the window totals.
151
+
152
+ ```
153
+ ai-cost report --since 2026-09-20T11:00Z --until 2026-09-20T14:36Z --all-projects \
154
+ --attribute "billing=feat/billing|src/billing/|pulls/66" \
155
+ --attribute "docs=docs/|README"
156
+ ```
157
+
158
+ ## Per project, per day: the daily job
159
+
160
+ `ai-cost daily` prices the UTC day before today once over every project and once per Claude project directory whose
161
+ transcripts were written that day, and writes `<reports>/<date>/global.md` + `global.json`, `<project dir
162
+ name>.md` + `.json` (the `--project` scope of the project's working directory, read from its transcripts' `cwd`)
163
+ and `index.json` — `{day, generated_at, window, directory, real_usd, api_usd, cash_usd, rows, projects: [{project_dir,
164
+ path, markdown, json_file, real_usd, api_usd, rows}], notes}`. A project no transcript can place, one with no usage
165
+ that day, or one whose report failed is a note, never an abort; unpriced rows are skipped and counted, so a new
166
+ model never fails the job. `<reports>` is `AI_COST_REPORTS_DIR`, else `$XDG_DATA_HOME/ai-cost/reports`
167
+ (`~/.local/share/ai-cost/reports`). `ai-cost install --schedule-reports` registers the job at 06:40 local time
168
+ (`--at HH:MM` to move it; launchd on macOS runs a missed time at wake, cron does not); `--unschedule-reports`
169
+ removes it; `doctor` shows whether it is installed and what the newest index says. `ai-cost daily --date 2026-09-20`
170
+ re-runs a day; `--out DIR` writes elsewhere.
171
+
172
+ ## Reconcile with the provider's console
173
+
174
+ `ai-cost reconcile --provider xai --usd 156.11 --tokens 80200000 --hours 24` prints the local count for the window
175
+ (the cash `real` attributes to the provider — a CLI's own figure where the config trusts it, else the list price —
176
+ and the tokens it billed, every counter once) next to the figures you read off the provider's console or export, the
177
+ gap in percent against `reconcile.tolerance_pct` (5 in the shipped config) and a verdict; above tolerance is exit 1.
178
+ Every run is appended to `~/.local/state/ai-cost/reconcile.jsonl` and `doctor` shows the last one. Nothing is
179
+ fetched: no provider offers one public spend endpoint every user could call, so the figure is yours to paste (or a
180
+ script's, from an export). A gap is something to look at — the window's edges, a source the tool does not read, a
181
+ price it applies differently — never a number to hide.
182
+
183
+ ## JSON contract (`--format json`)
184
+
185
+ Top level: `version`, `generated_at`, `window` (`{start, end}`), `window_iso` (`[start, end]`), `window_hours`,
186
+ `row_count`, `sources`, `warnings`, `skipped` (`[{source, path, reason}]`), `prices_checked_at`, `real`
187
+ (`subscriptions[]`, `usage[]`, `cash_usd`, `subscription_usd`, `total_usd`, `unknown_billing`), `api` (`lines[]`,
188
+ `total_usd`), `vendor` (when items exist), `attribution` (with `--attribute`). A line's `calls` is what it folded in
189
+ (rows, review runs, Copilot reviews — the `Runs` column) and `model_calls` the API requests its sources reported
190
+ (`Model calls`; 0 where none does); `--detail` adds `rows[]`, where
191
+ `cost_reported` is the source's own figure for the row: cash for an `api` row (a charge the source reported), the
192
+ source's list-price estimate for a `subscription` row (never counted as cash; the API-equivalent fallback when the
193
+ model has no list price), a part of the session's figure when a session spans several rows; `source` names the
194
+ source that produced the row and `kind` its record shape (`transcript`, `session`, `chat`, `log`, `ledger`,
195
+ `review`, `copilot`, `actions`). Every dataclass property is serialised, so computed totals are always present.
196
+ Changes since 1.0: `window` is an object (the list moved to `window_iso`), `seats` multiply only per-seat plans, an
197
+ unpriced model exits 5 unless `--unpriced skip`.
198
+
199
+ ## Configuration
200
+
201
+ `~/.config/ai-cost/config.json` (`install --init-config` writes it from the shipped defaults,
202
+ [`src/ai_cost/data/config.json`](src/ai_cost/data/config.json); it ships no subscriptions, add yours) — subscriptions
203
+ (plan, seats, attribution `time` | `full` | `none`), provider billing switches (`providers.<name>.billing`, the rule for that provider's rows that carry no billing evidence of their own — a plugin's xai or deepseek rows follow it as the built-in sources follow `anthropic.billing`,
204
+ `openai.billing`, `google.billing` for Gemini CLI sessions, `github.copilot_plan_exhausted`, `xai.trust_cli_cost` — a
205
+ positive CLI-reported cost is the row's
206
+ cash, 0 or absent means the CLI did not price the run and the list price applies), budgets, item sizing thresholds,
207
+ vendor profiles, `plugins` and `plugin_settings`. A `null` anywhere in it means "no override" (the shipped value
208
+ stays); inside a list a `null` is an error — a list such as `subscriptions` replaces the shipped list whole, so there
209
+ is no shipped item a `null` could stand for. `openai.default_model` names the model of a Codex rollout that does not
210
+ say its own (empty by default: such rows are `unknown`).
211
+ `~/.config/ai-cost/prices.json` — price overrides in the shape of
212
+ [`src/ai_cost/data/prices.json`](src/ai_cost/data/prices.json), the registry the shipped file carries (the single
213
+ source; `prices show --format json --snapshot` prints it). An override may change numbers, never a price's shape;
214
+ a `null` anywhere in your file means "no override" (the shipped value stays), and to clear a shipped block set it to an
215
+ empty value: `"next": {}`, `"long": {}`, `"valid_until": ""`; a plan set to `null` is removed.
216
+ `auto_check_days: 0` turns the price-drift check off. `reconcile.tolerance_pct` (5) is the gap `reconcile` accepts.
217
+ Env: `AI_COST_CONFIG`, `AI_COST_PRICES`, `AI_COST_CONFIG_DIR`, `AI_COST_STATE_DIR`, `AI_COST_USAGE_LOG` (else
218
+ `$XDG_DATA_HOME/ai-cost/usage.jsonl`), `AI_COST_REPORTS_DIR` (else `$XDG_DATA_HOME/ai-cost/reports`), `AI_COST_OFFLINE=1`,
219
+ `AI_COST_PLUGINS`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME`, `GEMINI_CLI_HOME` (`~/.gemini` by default), `GROK_HOME`
220
+ (`~/.grok` by default).
221
+
222
+ Where the numbers come from and what they leave out: [`references/pricing-sources.md`](references/pricing-sources.md)
223
+ and [`references/vendor-pricing.md`](references/vendor-pricing.md).
224
+
225
+ ## Development
226
+
227
+ `src/ai_cost/` is the package: `models.py` (the typed data — `UsageRow`, `Tokens`, `Window`, `WorkItem`, `Report`),
228
+ `pricing.py` (one pricer per price shape, chosen from a dispatch table), `groups.py` (real / api / vendor),
229
+ `collectors/` (one module per built-in source — `claude`, `codex`, `gemini_cli`, `grok_build`, `github`, `usage_log` — each
230
+ returning `Collected` rows plus counted `Skipped` records), `plugins.py` (the source / enricher protocol and
231
+ discovery), `config.py` with `data/{prices,config}.json`, `prices_check.py`, `ops.py` (report assembly and the
232
+ per-project scope, doctor, monitor, the scheduled jobs), `daily.py`, `reconcile.py`, `render.py`, `cli.py`. The design note and
233
+ the architecture decision records are kept with the development source, not in the published tree.
234
+
235
+ ```bash
236
+ PYTHONPATH=src python3 -m ai_cost selftest # or python3 -m pytest — the tests live in src/ai_cost/tests/
237
+ python3 scripts/build.py src dist/ai-cost # the shipped single file, reproducibly (fixed timestamps: a rebuild is byte-identical)
238
+ ```
239
+
240
+ ## Honest limits
241
+
242
+ - `real` attributes subscriptions by time (window hours / 730). A plan you would pay for anyway is a sunk cost; the
243
+ `cash_usd` line is the marginal money that actually left the account.
244
+ - Gemini's implicit-cache discount and DeepSeek's off-peak halving are modelled from the documented rules, not from
245
+ your invoice. Check the provider consoles when it matters.
246
+ - The core has no built-in work items: without `--items` (or a plugin that supplies items) there is no vendor
247
+ quote. A plugin's automatic sizing is a proxy — hand-size the scope for a quote you will show anyone.
248
+ - Price pages change; the checker is a heuristic and never rewrites a price silently.
249
+ - `api` prices a row's aggregated counters: a CLI turn of several model calls never gets a long-context tier even if
250
+ one call crossed the threshold. Where a CLI reports its own cost (Grok Build), `real` trusts it and `reconcile`
251
+ measures the gap.
252
+ - Under `--project`, rows that name no scope at all (a usage-log line without `session`) are included in every
253
+ project's report, and rows of named workspaces that are no directory are left out; both are counted in the header.
254
+
255
+ MIT © 2026 Quantum Media Technologies sp. z o.o. Not affiliated with Anthropic, OpenAI, Google, xAI, DeepSeek or GitHub.
256
+
257
+ ---
258
+
259
+ Made by [Quantum Media Technologies](https://www.qmediat.io/open-source?utm_source=oss-readme&utm_medium=ai-cost&utm_campaign=open-source) · [more open source from qmediat](https://github.com/qmediat)