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.
- ai_costs-2.2.3/LICENSE +21 -0
- ai_costs-2.2.3/PKG-INFO +283 -0
- ai_costs-2.2.3/README.md +259 -0
- ai_costs-2.2.3/pyproject.toml +86 -0
- ai_costs-2.2.3/setup.cfg +4 -0
- ai_costs-2.2.3/src/ai_cost/__init__.py +3 -0
- ai_costs-2.2.3/src/ai_cost/__main__.py +16 -0
- ai_costs-2.2.3/src/ai_cost/attribution.py +133 -0
- ai_costs-2.2.3/src/ai_cost/cli.py +614 -0
- ai_costs-2.2.3/src/ai_cost/collectors/__init__.py +28 -0
- ai_costs-2.2.3/src/ai_cost/collectors/claude.py +311 -0
- ai_costs-2.2.3/src/ai_cost/collectors/codex.py +302 -0
- ai_costs-2.2.3/src/ai_cost/collectors/gemini_cli.py +184 -0
- ai_costs-2.2.3/src/ai_cost/collectors/github.py +215 -0
- ai_costs-2.2.3/src/ai_cost/collectors/grok_build.py +154 -0
- ai_costs-2.2.3/src/ai_cost/collectors/usage_log.py +326 -0
- ai_costs-2.2.3/src/ai_cost/config.py +927 -0
- ai_costs-2.2.3/src/ai_cost/daily.py +359 -0
- ai_costs-2.2.3/src/ai_cost/data/config.json +158 -0
- ai_costs-2.2.3/src/ai_cost/data/prices.json +382 -0
- ai_costs-2.2.3/src/ai_cost/errors.py +32 -0
- ai_costs-2.2.3/src/ai_cost/groups.py +439 -0
- ai_costs-2.2.3/src/ai_cost/log.py +343 -0
- ai_costs-2.2.3/src/ai_cost/models.py +310 -0
- ai_costs-2.2.3/src/ai_cost/ops.py +1221 -0
- ai_costs-2.2.3/src/ai_cost/plugins.py +179 -0
- ai_costs-2.2.3/src/ai_cost/prices_check.py +550 -0
- ai_costs-2.2.3/src/ai_cost/pricing.py +242 -0
- ai_costs-2.2.3/src/ai_cost/process.py +15 -0
- ai_costs-2.2.3/src/ai_cost/reconcile.py +211 -0
- ai_costs-2.2.3/src/ai_cost/render.py +287 -0
- ai_costs-2.2.3/src/ai_cost/selftest.py +64 -0
- ai_costs-2.2.3/src/ai_cost/tests/__init__.py +1 -0
- ai_costs-2.2.3/src/ai_cost/tests/fixtures.py +280 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_attribution.py +178 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_collectors.py +633 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_config_and_prices.py +1078 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_daily_and_reconcile.py +500 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_distribution.py +27 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_gemini_cli.py +130 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_grok_build.py +168 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_log.py +269 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_plugins.py +121 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_pricing_and_groups.py +509 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_project_scope.py +260 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_report_and_cli.py +1370 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_usage_log.py +809 -0
- ai_costs-2.2.3/src/ai_cost/tests/test_values.py +75 -0
- ai_costs-2.2.3/src/ai_cost/timeutil.py +62 -0
- ai_costs-2.2.3/src/ai_cost/values.py +88 -0
- ai_costs-2.2.3/src/ai_costs.egg-info/PKG-INFO +283 -0
- ai_costs-2.2.3/src/ai_costs.egg-info/SOURCES.txt +53 -0
- ai_costs-2.2.3/src/ai_costs.egg-info/dependency_links.txt +1 -0
- ai_costs-2.2.3/src/ai_costs.egg-info/entry_points.txt +2 -0
- 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.
|
ai_costs-2.2.3/PKG-INFO
ADDED
|
@@ -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)
|
ai_costs-2.2.3/README.md
ADDED
|
@@ -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)
|