Metadata-Version: 2.4
Name: ai-accountant
Version: 0.0.3
Summary: Provider pricing, token cost calculation, and local AI session accounting
Author: Joakim Henriksson
License-Expression: Apache-2.0
Keywords: ai,pricing,tokens,accounting
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Dynamic: license-file

# AI Accountant

A Python library for reading AI session usage and calculating token costs, with
a bundled local accounting CLI. Licensed under
[Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).

The distribution name is `ai-accountant`; the primary import is `ai_accountant`.
Release versions are derived from Git tags. Builds from a source distribution
retain that version without needing Git.
Install this checkout for development:

```sh
uv pip install .
```

```python
from ai_accountant import SessionReader, ModelPrice, calculate_cost

reader = SessionReader()
# sources = reader.discover("codex", home="/path/to/.codex")
# session = reader.parse(sources[0], client="codex")

price = ModelPrice("example", "model", {"input": "2", "output": "10"}, "example")
cost = calculate_cost({"input_tokens": 1000, "output_tokens": 100}, price)
print(cost.amount_usd)  # Decimal('0.003')
```

## Library

### Session reading

`SessionReader` discovers local Codex, Claude Code and Machina accounting sources.
The client is explicit; discovery and parsing only read files when called.

```python
from ai_accountant import SessionReader

reader = SessionReader()
for path in reader.discover("codex"):  # or "claude" / "machina"
    session = reader.parse(path, client="codex")
    print(session.session_id, session.models, session.warnings)
    for event in session.events:
        print(event["provider"], event["model"], event["tokens"])
```

Pass `home="/path/to/store"` to `discover` to override the client's default
location. A parsed session contains metadata, per-model totals, normalized usage
observations, warnings and Codex tool statistics. It does not contain a full
conversation replay. SQLite indexing and reports belong to the CLI.

Normalized `input_tokens` includes fresh input, cache reads and cache writes;
`cached_input_tokens` and `cache_write_input_tokens` are subsets. Five-minute
and one-hour write counts are subsets of cache writes. Reasoning is included
in output. Use `usage_complete` and `reasoning_known` to interpret missing
evidence; an omitted category represented as zero is not proof of measured zero.
Source-specific parsing deduplicates usage snapshots and excludes inherited
Codex fork counters. Provider and model identities are preserved.

### Provider pricing

`default_resolver()` supplies OpenAI, Anthropic, DeepSeek, Z.ai and OpenRouter
with the full frozen release catalogue already loaded. Search and quote work
offline immediately. Construction reads packaged JSON; import, construction
and lookup perform no networking. Refresh explicitly for newer rates. Most
built-ins use the LiteLLM community catalogue; OpenRouter uses its own public API.
Provider identity, provenance and retrieval times are retained.

```python
import asyncio
from ai_accountant import default_resolver

prices = default_resolver()
results = asyncio.run(prices.refresh("anthropic"))
for result in results:
    print(result.status, result.error)
for price in prices.search("*opus*", provider="anthropic"):
    print(price.qualified_id, price.rates)
```

Prices use Decimal USD per million tokens. Refresh failures retain the last good
snapshot. A host owns refresh scheduling, persistence and its event history.
`calculate_cost` accepts normalized counts and a `ModelPrice`; pass
`usage_complete=event["usage_complete"]` when pricing parsed observations.
Missing prices or consumed rates produce an unknown cost, never an assumed zero.
Context-tier pricing needs the per-request context size. These quotes estimate
API cost, not subscription quota usage or reconstructed historical invoices.

Custom provider and session-parser implementations can be registered explicitly.
Installed plugin discovery is opt-in. For API signatures and docstrings:

```python
from ai_accountant import SessionReader, PricingResolver, calculate_cost

help(SessionReader)
help(PricingResolver)
help(calculate_cost)
```

The source distribution includes detailed references at `docs/session-library.md`,
`docs/pricing-library.md` and `docs/campaign-classification.md`, with maintainer
release instructions in `docs/releasing.md`. They are archive paths, not hosted
documentation links.

## CLI

A Python CLI run with uv. It reads local Codex rollouts, Claude Code transcripts,
and Machina turns, then reports token usage per model and estimated USD cost.
SQLite indexing makes combined history queryable. Codex remains the default
source so existing commands keep their behavior.

The installable `accountant_pricing` compatibility facade provides
provider plugins, live price acquisition, an in-memory resolver and pure Decimal
calculation for other programs. Consumers own their usage histories; SQLite
snapshot persistence belongs to this CLI.

```powershell
uv run accountant.py --list
uv run accountant.py SESSION_UUID
uv run accountant.py SESSION_UUID --parent-only
uv run accountant.py --latest
uv run accountant.py 'C:\path\to\rollout.jsonl' --prices prices.json
uv run accountant.py --all --prices prices.json
uv run accountant.py --resets --prices prices.json
uv run accountant.py --reset latest --prices prices.json
uv run accountant.py --reset 1791580844 --window primary --json
uv run accountant.py --client claude --list
uv run accountant.py --client machina --all --json
uv run accountant.py --client all --all --json
```

Use `--client codex|claude|machina|all` to choose a source. `--all` aggregates
the clients selected by `--client`. Codex history defaults to `$CODEX_HOME` or
`~/.codex`; Claude uses `CLAUDE_CONFIG_DIR` or `~/.claude`; Machina uses
`MACHINA_HOME` or `~/.machina`. Override those locations with `--codex-home`,
`--claude-home`, and `--machina-home`. Claude and Machina session IDs are
prefixed with `claude:` and `machina:` in combined reports and the database;
Codex UUIDs remain unchanged. `--latest` picks the most recently modified file
among selected clients, and `--list` includes each session's client.

`--reset` accepts `latest`, a Unix reset deadline, or an ISO timestamp with a
timezone. `--window secondary` selects the secondary quota. Window lengths come
from the logs; primary does not necessarily mean a five-hour window. `--latest`
selects the most recently modified file, including archived and subagent sessions.
Use a UUID or path when selecting a particular conversation. Unique UUID prefixes
work too. `--codex-home PATH` supports other installations or exported histories.
Session reports include recursively linked subagent rollouts by default, even
when archived. `--parent-only` excludes them. JSON includes a per-session
breakdown. Only explicit parent-thread links are followed; forks are separate
conversations. Agents with no parent link cannot be attributed to a conversation.

Codex session reports also tally tool requests by name and recorded completed tool
actions by type (commands, file changes, web extensions, and MCP calls). These
are two overlapping views: an `exec` request can run several underlying tools,
so do not add the two totals. Outputs and collaboration UI echoes are excluded;
repeated call/action IDs and copied pre-creation fork history are not counted
again. Counts include recorded failed actions and requests without outputs.
Shell commands can perform multiple file reads or searches; individual file
operations and hidden tool reviews cannot be reliably inferred from these counts.
Run `--sync` to populate tool statistics in an older index. An index without
these statistics reports them as unavailable rather than zero. Claude and Machina
report tool statistics as unavailable. Aggregate token and quota reports do not
include tool counts.

## SQLite

```powershell
uv run accountant.py --sync
uv run accountant.py SESSION_UUID --db accountant.sqlite3
uv run accountant.py --all --db accountant.sqlite3 --prices prices.json
uv run accountant.py --resets --db accountant.sqlite3 --prices prices.json
uv run accountant.py --reset latest --db accountant.sqlite3 --prices prices.json
uv run accountant.py --query 'SELECT * FROM model_usage ORDER BY total_tokens DESC'
uv run accountant.py --query 'SELECT * FROM reset_usage ORDER BY resets_at, model'
```

Run `--sync` to migrate older indexes before using session or aggregate reports;
read-only SQL remains available on old schemas. Sync reparses available sources
when the parser version changes, and backfills missing Codex tool statistics.
Sources that are no longer available retain their indexed data; migrated legacy
events remain conservatively marked incomplete until reimported.

Select clients with `--client` when syncing. Sync again to import new or changed
logs. Unchanged files are skipped using size and modification time; changed files
are replaced transactionally. Archiving
a session updates its path without duplicating usage. Sessions whose logs are later
removed remain in the database. The database contains usage metadata, not prompts
or responses. `--db PATH` overrides the database location. Aggregate commands with
`--db` read the indexed snapshot; commands without it read the logs directly.
`--sync --rebuild` refreshes only the selected client index; use
`--client all --sync --rebuild` to rebuild every client.

Tables: `sessions` (including the source client), `usage_events` (usage increments,
provider, source-event identity, completeness flags, cache TTL counts, and raw
rate-limit JSON), and
`rate_limits` (primary/secondary observations), plus `session_links` (parent,
fork, and source metadata), and `tool_statistics` (Codex tool counts).
Views: `model_usage`, `reset_usage`, and
`session_roots`, which maps each session to itself and its recursively linked
descendants. The recursive view guards against cycles. `--query` opens SQLite
read-only and prints JSON. For example, aggregate a root session and all its
descendants by model:

```sql
SELECT u.model, SUM(u.input_tokens + u.output_tokens) AS total_tokens
FROM session_roots r
JOIN usage_events u ON u.session_id = r.session_id
WHERE r.root_session_id = 'SESSION_UUID'
GROUP BY u.model
ORDER BY total_tokens DESC;
```

Other queries:

```sql
SELECT u.model, SUM(u.input_tokens + u.output_tokens) AS total_tokens
FROM usage_events u
WHERE julianday(u.timestamp) >= julianday('2026-10-01T00:00:00Z')
  AND julianday(u.timestamp) < julianday('2026-10-08T00:00:00Z')
GROUP BY u.model;
```

## Prices

Reports discover reference prices through the library's registered provider
fetchers. Missing/stale catalogues are refreshed with a 24-hour TTL; successful
snapshots are retained in `--db` (default `accountant.sqlite3`). This works from
an installed wheel, which includes a complete generated `prices.json` catalogue.
Fresh installations work offline before any database or successful refresh exists.
Listing sessions, syncing usage and SQL queries do not fetch prices.

An exact model ID with one matching provider catalogue is priced automatically.
Multiple matching providers stay unpriced: use `--price-provider PROVIDER` to
choose a reference provider. Reports disclose the chosen provider/source per
model. This is an API-equivalent comparison; it does not infer which provider
actually served the request. Aggregate reports use base-context rates.

`--offline` uses bundled prices and newer saved snapshots without fetching or
creating a database. Older caches never replace newer bundled prices. Failed
refreshes retain last-good prices and emit warnings. The library itself still
performs networking only when its host explicitly calls refresh.

The generated root `prices.json` maps qualified provider/model IDs to rates,
context tiers, provenance and retrieval times. Its `_fallbacks` section retains
supplied internal/reference prices for IDs absent from the catalogues. A checked,
byte-identical copy in `accountant_pricing/prices.json` is bundled in both release
formats. Fallbacks never override catalogue matches or resolve ambiguity.
Use `--prices FILE` for explicit exact-model overrides (which may accompany a
provider selection); rates are **USD per million tokens**. For example:

```json
{
  "gpt-6.1-sol": {"input": 2.0, "cached_input": 0.1, "output": 10.0},
  "gpt-6-luna": {"input": 0.1, "cached_input": 0.01, "output": 0.5}
}
```

Explicit refresh/history commands remain available:

```powershell
uv run accountant.py --refresh-prices --db accountant.sqlite3
uv run accountant.py --refresh-prices --price-provider openai --force-prices
uv run accountant.py --price-history --price-provider openai
uv run accountant.py --all --db accountant.sqlite3 --price-provider openai --json
uv run accountant.py --all --db accountant.sqlite3 --offline --json
```

The source archive's `docs/pricing-library.md` details source coverage,
plugin extension, TTL, failure retention and snapshot history. Snapshot reports
use standard base-context API reference rates and include provenance/assumptions.
These are current estimates rather than reconstructed invoices.

The following describes the retained `_fallbacks` section, rather than the fetched
catalogue. OpenAI fallback rates were supplied on 2026-10-04 and were not
independently fetched during onboarding. GPT-5.5 Pro's "no discount" is represented
by a cached-input rate equal to its input rate. The user supplied `gpt-5.6-luna`
rates matching `gpt-6-luna`, and `gpt-5.6-terra` input rates matching `gpt-6.1-sol`
with output at $12 per million tokens. The user supplied free pricing for
`codex-auto-review`; all its token rates are zero. No other aliases are inferred.
DeepSeek V4 Flash and Pro have explicit bare and `deepseek/`-prefixed entries.
Their current [official prices](https://api-docs.deepseek.com/quick_start/pricing/)
were retrieved via the [DeepSeek homepage](https://www.deepseek.com/) on
2026-10-12. The flat configuration uses **peak** rates: input/cache/output
$0.30/$0.006/$1.20 for Flash and $1.32/$0.044/$3.96 for Pro, per million tokens.
Off-peak rates are half and retained as metadata; the loader does not apply them.
Peak hours are weekdays 01:00–04:00 and 06:00–10:00 UTC excluding Chinese public
holidays. The official page explicitly bills the retired V4 Flash alias at
current V4.1 Flash rates; Pro currently identifies V4-Pro-0813. These are current
direct-provider estimates applied to history, not reconstructed invoices. Codex
DeepSeek records report provider `cog`; its actual charges are not established.

The observed `claude-opus-4-8` model has [official Anthropic rates](https://platform.claude.com/docs/en/models/opus-4-8/overview)
retrieved on 2026-10-12: input/cache-read/output $5/$0.50/$25 per million,
with separate five-minute and one-hour cache-write rates $6.25/$10.
Unclassified cache writes have no assumed rate. The user reports using alternative
endpoints rather than Anthropic; for these logs the Opus rate is a hypothetical
comparison. The CLI still prices exact model labels and does not validate the
serving endpoint or actual underlying model.

The observed `glm-5.2` model has [official Z.ai rates](https://docs.z.ai/guides/overview/pricing)
retrieved on 2026-10-12: input/cache-read/output $1.40/$0.26/$4.40 per million.
Cache storage is temporarily free on that page; it is not mapped to a token
cache-write rate. These are standard API estimates, not Coding Plan subscription
charges. Incomplete GLM usage remains unpriced even with known model rates.

Optional `cache_write` supplies a generic cache-write rate. `cache_write_5m` and
`cache_write_1h` can price the two TTL buckets separately; each overrides the
generic rate for its bucket. Unclassified writes still need `cache_write`.
Unknown models or missing applicable rates yield an unknown total, with a priced
subtotal. Exact names avoid guessing prices for aliases. Estimates apply the supplied
reference rates to all history; they do not infer service-tier multipliers, historical
price changes, long-context rates, subscription charges, or tool fees.

Cost = `(uncached input × input rate + cached input × cached rate + cache-write
input × cache-write rate + output × output rate) / 1,000,000`.
Cached and cache-write tokens are treated as subsets of input. Cache-write TTL
counts are subsets of total cache writes. Reports show `cached_percent` as cached
input divided by inclusive input, and `reasoning_output_tokens: null` when a
client does not report reasoning counts. Incomplete usage is retained and warned
about; its cost is not treated as zero. A model with any incomplete usage is left
out of the priced subtotal as a whole. Provider names are stored only when a log
reports them. Rates come from exact catalogue matches or the disclosed
override/fallback. Claude Code client identity does not imply an
Anthropic model or endpoint. Older
Machina turn totals with ambiguous input/cache semantics stay visible as observed
values, carry an incomplete flag, and do not produce a cached percentage or cost.
Current canonical Machina request rounds preserve direct input, cache reads,
cache writes, and output separately.
Machina `session.yaml` metadata is read with PyYAML's safe loader when PyYAML is
installed; without it, the reader uses transcript-directory identity and still
accounts for current canonical request rounds. To enable YAML metadata for one
run, use `uv run --with pyyaml accountant.py --client machina --latest`.

CLI aggregates and prices group by exact model name across selected clients and
providers. SQL `model_usage` separates client/provider/model; `reset_usage` also
separates the quota identity. Supply rates only when the model-wide pricing
assumption is appropriate. Codex quota resets are recorded; other clients
currently have no quota observations and appear as unassigned usage.

## Interpretation

- Codex metadata `source: "vscode"` does not establish VS Code use. Local
  rollouts with that marker also record `originator: "codex-tui"` or
  `"JetBrains.IntelliJ IDEA"`. Report source and originator as recorded metadata;
  neither proves deliberate editor use or agent activity.

- Cumulative counter differences avoid counting duplicate notifications. Model
  attribution follows the latest `turn_context`. Decreasing counters start a new
  segment and generate a warning. Missing models stay `unknown`.
- Reset reports assign each increment to the reset deadline attached to its event.
  Deadlines within 60 seconds are grouped for the same limit ID and window length
  to absorb observed clock jitter. SQL `reset_usage` preserves exact deadlines.
- Percentages are account quota observations, not per-model token percentages.
  Reports show first, last, and maximum observed percentages; percentages are never
  added. Times are recorded event timestamps and reset deadlines in UTC, not elapsed
  work time. Usage without a recorded reset is reported separately.
- Results cover available logs only. Missing history, usage imported by a fork,
  inherited cumulative baselines, and consumption from other devices may affect
  completeness. A large cumulative increment is attributed to its current event;
  absent intermediate records cannot reconstruct its earlier timing or model.
  For forks with creation metadata, copied events before creation establish the
  counter baseline but do not contribute usage to the fork. A counter emitted at
  creation before local turn/request activity also establishes the inherited
  baseline. Real usage and tools at creation after activity starts remain counted.
  Parser version changes trigger reimport on the next sync, including this fix.
  The first session
  metadata record identifies the rollout; copied parent metadata does not replace it.
- Active rollouts can have a partially written final line. Invalid lines are skipped
  with warnings and can be recovered on the next sync when the file changes.

## Verification

```powershell
uv run python -m unittest discover -s tests -v
```

If your environment restricts the default uv cache directory, add
`--cache-dir .uv-cache` immediately after `uv`.

The CLI needs no third-party dependencies for token accounting and can also run
directly with Python 3.11 or later:

```sh
python3 accountant.py --help
python3 -m unittest discover -s tests -v
```

GitHub Actions runs the tests on Python 3.11 and 3.13. Commit `uv.lock` with
changes to project dependencies. Local environments, SQLite indexes, session
logs, and generated historical usage reports are excluded from version control;
`prices.json` is generated through provider acquisition and bundled for offline
use, with manual rates isolated in `_fallbacks`. Online reports can write
price snapshots to the selected database but never rewrite usage events.
Use `--offline` for reports without fetching or snapshot writes.

## Development and local artifacts

The source distribution includes `docs/README.md` (architecture) and
`docs/source-map.md` for ownership, adapter contracts and index lifecycle.

The onboarding copied the seed index, its backup, historical-models.json,
claude-cache-report.json, claude-cache-by-session.csv and diagnose-network.ps1
into this workspace as ignored local files. They are snapshots and diagnostics,
not shared project inputs. The seed originals were preserved. Environments and
caches are regenerated locally rather than copied.
