Skip to content

shawnpetros/lithium

Repository files navigation

lithium - cross-provider LLM-spend aggregator

lithium

Mood stabilizer for your AI bill.
Weighed, measured, kept in balance. One number, every provider.

Status: v0.3.x. Anthropic + OpenAI + OpenRouter + ElevenLabs adapters wired. SwiftBar menubar plugin + Claude Code / cship status-line wrapper shipped. Daemon split (lithiumd) parked until polling cadence demands it.

Rust SQLite macOS Linux MIT


See it

lithium spend in the Claude Code status line via cship
Claude Code status line. Live month-to-date variable spend on every prompt.
lithium balance + spend in the macOS menubar
macOS menubar. Always-on glance via the SwiftBar plugin.
lithium dropdown menu showing per-provider breakdown
Drop-down breakdown. Per-provider variable + fixed, day-of-month context, projected EOM, budget readout.

lithium CLI demo
CLI. The original surface. Same numbers, different paint.

What

You use Anthropic. You also use OpenAI. And OpenRouter. Maybe a local model. At the end of the month you have no idea what you spent. Each provider has its own dashboard, none of them talk to each other, and you've been doing the math in a spreadsheet, badly.

lithium is a tiny local daemon that polls every provider you use, normalizes the numbers into one SQLite database, and answers exactly one question:

How much am I actually spending on LLMs this month, across everything, fixed and variable?

That's the whole product. No web dashboard, no SaaS, no telemetry, no analytics. The data lives on your machine. The CLI prints the answer.

Why

Three things go wrong when you run agents across multiple providers:

  1. You don't notice runaway cost until the bill arrives. A misconfigured Whetstone wave or a forgotten cron can burn $200 in a day before you check.
  2. Fixed costs (Max plans, monthly subscriptions) and variable costs (per-token API) live in different mental buckets. Most operators only track one. Both add up.
  3. Cross-provider visibility is nobody's job. Anthropic shows you Anthropic. OpenAI shows you OpenAI. The aggregate is your problem.

lithium makes it the daemon's problem.

Features

Feature Description
lithium today Today's spend, by source, with totals
lithium month Month-to-date + projected end-of-month
lithium status One-line spend output for statusline integrations
lithium adapters List configured providers + last-poll status
lithium config Edit ~/.config/lithium/config.toml in $EDITOR
lithium doctor Verify config + connectivity + DB health
Anthropic Cost Report admin API + Claude Code local-state reader
OpenAI /v1/organization/costs admin API per-day USD by line item
OpenRouter /api/v1/key (regular API key works) for daily/weekly/monthly
ElevenLabs /v1/user/subscription (regular API key): forecasted monthly USD + character usage
Fixed costs Declare flat-rate subscriptions (Max, ChatGPT Pro) for true total
SQLite storage All data local at ~/.local/share/lithium/usage.db
No telemetry Nothing leaves your machine. Period.

Quick Start

# 1. Install
cargo install --git https://github.com/shawnpetros/lithium

# 2. Initialize config + storage
lithium config       # opens ~/.config/lithium/config.toml in $EDITOR
lithium init         # creates the SQLite database

# 3. Add at least one provider's key to the config (uncomment + paste):
#    [providers.anthropic]   admin_api_key = "sk-ant-admin01-..."
#    [providers.openai]      admin_api_key = "sk-admin-..."
#    [providers.openrouter]  api_key       = "sk-or-..."
#    [providers.elevenlabs]  api_key       = "sk_..."

# 4. Pull data
lithium poll

# 5. Look at it
lithium today
lithium month

Each provider is independent. Wire only the ones you use. See the per-provider notes below for where to generate each key.

Getting keys

Anthropic (admin key required)

The Cost Report API requires an admin key (sk-ant-admin01-...), distinct from a regular API key (sk-ant-api03-...). On personal accounts, admin keys are gated behind organization existence:

  1. Go to https://platform.claude.com/settings
  2. If you don't have an org: walk through "Create an organization" first. Personal accounts become 1-person orgs (you're the owner).
  3. https://platform.claude.com/settings/admin-keys → Create Admin Key, name it lithium-local
  4. Paste under [providers.anthropic] admin_api_key

lithium doctor prints the key prefix so you can verify the type at a glance.

OpenAI (admin key required)

/v1/organization/costs also requires an admin key (sk-admin-...):

  1. https://platform.openai.com/settings/organization/admin-keys
  2. Create admin key → paste under [providers.openai] admin_api_key

OpenRouter (regular key works)

/api/v1/key accepts any OpenRouter API key. No admin / management key dance:

  1. https://openrouter.ai/keys → create or copy an existing key
  2. Paste under [providers.openrouter] api_key

Bonus: OpenRouter pre-aggregates usage_daily / usage_weekly / usage_monthly, so polling once gives you all three at once.

ElevenLabs (regular key works)

/v1/user/subscription returns next_invoice.amount_due_cents (forecasted monthly charge) plus character usage vs limit:

  1. https://elevenlabs.io/app/settings/api-keys → create or copy a key
  2. Paste under [providers.elevenlabs] api_key

Single row per poll covers the current calendar month; subsequent polls within the same month UPSERT in place. Tier name lands in the model label, character usage in raw_payload for future surface use.

Output looks like:

lithium - 2026-04-27

Anthropic
  API direct           $4.21    (claude-sonnet-4-6: $3.80, claude-haiku-4-5: $0.41)
  Claude Code session  47% used  (resets in 1h 12m)
  Claude Code weekly   23% used  (resets in 4d 2h)

Total today: $4.21

Status line (lithium status)

lithium status emits one short line, no trailing newline, ready to compose into any statusline tool that runs a shell command and embeds stdout.

$ lithium status
⚖ $15.17

$ lithium status --no-icon
$15.17

$ lithium status --prefix Li
Li $15.17

$ lithium status --silent-zero       # empty when month-to-date is $0
$ lithium status --threshold-color   # paints by budget (cream / brass / oxblood)
$ lithium status --json              # machine-readable

Full flag list: --no-icon, --icon <STR>, --prefix <STR>, --silent-zero, --threshold-color, --json, --decimals <N>. Default icon is U+2696 (); Nerd Font users can swap to U+F24E ( ) via --icon.

cship + lithium status segment in the Claude Code status line

Integration recipes

Starship / cship. cship hands $custom.X tokens through to starship, so the same block works for both. Put this in ~/.config/starship.toml:

[custom.lithium]
command = 'lithium status'
when    = 'command -v lithium >/dev/null 2>&1'
format  = '[$output]($style) '
style   = 'fg:#F08222'
shell   = ['bash', '--noprofile', '--norc']

For cship users, also reference the module from ~/.config/cship.toml:

[cship]
lines = ["$directory$git_branch$cship.model $custom.lithium"]

Then point Claude Code's statusLine.command at cship and you're done. Bare starship users add $custom.lithium to your normal format string.

oh-my-posh. Add a command segment to your theme JSON:

{
  "type": "command",
  "style": "plain",
  "foreground": "#F08222",
  "properties": { "command": "lithium status --silent-zero" }
}

tmux. Edit ~/.tmux.conf:

set -g status-right '#[fg=#F08222]#(lithium status)#[default] %H:%M'

Powerlevel10k. Add a custom segment in ~/.p10k.zsh:

function prompt_lithium() {
  local out
  out=$(lithium status --silent-zero) || return
  [[ -n $out ]] && p10k segment -f 208 -t "$out"
}
typeset -g POWERLEVEL10K_RIGHT_PROMPT_ELEMENTS+=(lithium)

Bare bash / zsh PS1. Direct command substitution:

PS1='$(lithium status --silent-zero) \w \$ '

Advanced: cleaner cship layout via more [custom.X] modules

cship exports CSHIP_* env vars (CSHIP_MODEL, CSHIP_CONTEXT_PCT, CSHIP_COST_USD, etc) to every starship module <name> subprocess it spawns. That means a [custom.X] block can render the same Claude Code data cship's native modules see, with full control over format and styling. Useful when you want to:

  • drop the powerline branch glyph that cship's git_branch emits but won't let you customize via its own config
  • drop the auto-appended (1M context) annotation that cship.model adds for 1M-variant opus models
  • add a dim (N%) context-window indicator that cship.context_bar doesn't expose

Add to ~/.config/starship.toml:

# Strip the powerline branch glyph. cship's $git_branch passes through
# starship, so [git_branch] config applies even though [cship.git_branch]
# config wouldn't.
[git_branch]
symbol = ""

# Plain model name without Claude Code's auto-appended " (1M context)"
# suffix. CSHIP_MODEL holds display_name verbatim including the annotation.
[custom.model]
command = 'echo "${CSHIP_MODEL% (1M context)}"'
when    = '[ -n "$CSHIP_MODEL" ]'
format  = '$output'
shell   = ['bash', '--noprofile', '--norc']

# Dim (N%) context indicator.
[custom.context]
command = 'echo "($CSHIP_CONTEXT_PCT%)"'
when    = '[ -n "$CSHIP_CONTEXT_PCT" ]'
format  = '[$output]($style)'
style   = 'dimmed'
shell   = ['bash', '--noprofile', '--norc']

# Nerd Font apothecary balance glyph (U+F24E) as the lithium label.
# Trailing space in the format gives breathing room before the amount.
# Replace the printf command with `printf "Li"` if you don't have a Nerd Font.
[custom.balance_icon]
command = 'printf "\xef\x89\x8e"'
when    = 'command -v lithium >/dev/null 2>&1'
format  = '$output '
shell   = ['bash', '--noprofile', '--norc']

# Override the canonical [custom.lithium] block from above with a threshold-
# painted variant: lithium emits the bare amount, --threshold-color paints by
# % of [budget] monthly_variable_usd:
#   <50%   cream    neutral
#   50-75% ember    early warning (orange)
#   75-100% brass   approaching limit
#   >100%  oxblood  over budget
[custom.lithium]
command = 'lithium status --no-icon --threshold-color'
when    = 'command -v lithium >/dev/null 2>&1'
format  = '$output '
shell   = ['bash', '--noprofile', '--norc']

And reference them from ~/.config/cship.toml:

[cship]
lines = ["$directory$git_branch$custom.model $custom.context $custom.balance_icon $custom.lithium"]

Renders: lithium on main Opus 4.7 (12%) $15.17 where the amount auto-paints by budget. Set [budget] monthly_variable_usd in your lithium config to enable threshold paint; without it, the amount stays cream regardless of spend. No wrapper script anywhere; pure declarative config.

SwiftBar menubar (Phase 3)

The first user-visible UI surface lands as a SwiftBar plugin: glance at month-to-date variable spend in your menubar, drop the menu down for the per-provider breakdown and the projected end-of-month total. Cream + brass thresholds match the alchemy/leather aesthetic.

lithium balance + spend in the macOS menubar

lithium dropdown menu showing per-provider breakdown

Install

# Prerequisites: SwiftBar (brew install --cask swiftbar) + jq (brew install jq)
# Then symlink the plugin into the SwiftBar plugins folder.
mkdir -p "$HOME/Library/Application Support/SwiftBar/Plugins"
ln -sf "$HOME/projects/lithium/plugins/swiftbar/lithium-spend.5m.sh" \
       "$HOME/Library/Application Support/SwiftBar/Plugins/lithium-spend.5m.sh"
# Refresh: SwiftBar -> Refresh All (cmd+R from any plugin) or quit + relaunch.

The filename suffix .5m.sh controls refresh cadence (every 5 minutes). Edit to .15m.sh for slower polls or .1h.sh for hourly.

What it shows

  • Menubar: ⚖ $XX.XX where the dollar amount is month-to-date variable spend across all configured providers. Color shifts based on [budget] monthly_variable_usd if set: cream by default, brass at 75-100%, oxblood over budget.
  • Submenu: day-of-month context, projected EOM total, per-provider subtotals (sorted by spend), declared fixed monthly subscriptions, budget readout, plus action items (refresh, run poll, edit config, open repo).

Optional: declare a budget

In ~/.config/lithium/config.toml:

[budget]
monthly_variable_usd = 100.0

The plugin then warns at 75% (brass) and alarms over 100% (oxblood). Without this, the menubar stays cream regardless of spend.

How It Works

┌─ Provider adapters (Rust) ─────────────────────────┐
│  anthropic.rs   - Admin API + Claude Code session  │
│  openai.rs      - Admin API           [phase 2]    │
│  openrouter.rs  - /api/v1/key         [phase 2]    │
│  elevenlabs.rs  - /v1/user/subscr.    [phase 2]    │
└────────────────┬───────────────────────────────────┘
                 │
                 ▼
        SQLite at ~/.local/share/lithium/usage.db
                 │
                 ▼
   ┌─────────────┼─────────────┬──────────┬─────────┐
   ▼             ▼             ▼          ▼         ▼
  CLI         cship         SwiftBar   OpenClaw    Web
 today/      status         menubar    MCP tool   dashboard
 month       line                      + hooks    [phase 4]
            [✓ v0.3]       [✓ v0.3]    [phase 4]

Phase 1 ships only the CLI. Each subsequent phase adds one surface, polished to the same standard before the next one starts.

Roadmap

Phase Scope Status
P1 Anthropic adapter + CLI surface ✅ v0.1.0
P2 OpenAI + OpenRouter + ElevenLabs adapters ✅ v0.2.x
P3 SwiftBar menubar plugin ✅ v0.3.0
P3.x cship status-line segment ✅ v0.3.4
P3.y lithiumd daemon (sub-5min polling) Not started
P4 OpenClaw MCP hooks (cost gates) + optional web dashboard Not started

The discipline: each phase ships at finished quality before the next one starts. No half-built surface in main. See docs/ADAPTER-CONTRACT.md if you want to contribute another provider.

Privacy

lithium runs entirely on your machine. No analytics, no telemetry, no phoning home. The only network calls go directly to provider APIs (Anthropic, OpenAI, OpenRouter) using the admin keys you provide. Source is auditable; if you find a single egress that isn't to a provider you configured, open an issue and call it out.

Tech Stack

  • Rust for the daemon and CLI
  • SQLite for storage (via rusqlite)
  • Reqwest for provider API calls
  • Tracing for structured logs
  • Clap for the CLI
  • Tokio runtime

Contributing

lithium is built in the open as a santifer-discipline project: each phase ships at finished public-portfolio quality before the next one is started. Issues, PRs, and adapter contributions for additional providers welcome. Adapter contract is documented in docs/ADAPTER-CONTRACT.md (added in Phase 2).

License

MIT. See LICENSE.

Author

Built by Shawn Petros (petrosindustries.com).


Named after the periodic-table element and the mood stabilizer. Both stop runaway.
An apothecary's balance, kept in a workshop drawer.

About

Mood stabilizer for your AI bill. Cross-provider LLM-spend aggregator (Anthropic / OpenAI / OpenRouter). Local Rust daemon + SQLite, served via CLI, statusline, menubar, harness hooks.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors