LLM Cost Tracker
LLM spend tracking and budgets for Rails: by user, feature, or any tag, in your own database, no proxy.
No prompts, no traces, no replay. Spend attribution only.
Requires Ruby 3.3+, Rails 8.0+, PostgreSQL or MySQL. Built for a Rails monolith, not multiple services.
Quickstart
Shown with RubyLLM; the flow is identical for the official OpenAI and Anthropic SDKs: swap the gem and the instrument name (see the cookbook).
# Gemfile
gem "llm_cost_tracker"
gem "ruby_llm"bin/rails llm_cost_tracker:setupRuns the install generator, writes a local price file, migrates the database, and verifies via llm_cost_tracker:doctor. Then enable the integration in the generated config/initializers/llm_cost_tracker.rb:
LlmCostTracker.configure do |config|
config.tags.default = -> { { environment: Rails.env } }
config.instrument :ruby_llm
endYour RubyLLM calls stay unchanged, and every chat, embedding, transcription, image, and moderation call now lands in the ledger. Tag them to attribute spend:
LlmCostTracker.with_tags(user_id: Current.user&.id, feature: "chat") do
RubyLLM.chat.ask("Hello")
endMount the dashboard in config/routes.rb. The engine ships without authentication, so put it behind yours:
authenticate :admin do
mount LlmCostTracker::Engine => "/llm-costs"
endWhat it records
- Calls. Provider, model, total tokens, total cost, latency, status.
- Line items. Per-component breakdown: text/audio/cached tokens, tool charges (web search, grounding, container sessions).
- Tags. Whatever attribution you pass: user, feature, tenant, env.
-
Provider IDs. Response, plus project, API key and workspace when you pass them to
trackortrack_stream, for downstream audits. - Pricing snapshot. So historical numbers don't drift when prices change.
Budgets
Daily, monthly, and per-call limits, plus per-tag limits such as one monthly budget per tenant_id, or a lifetime cap in dollars or calls per agent run (run_id, which RubyLLM.workflow sets on its calls). A crossed limit calls your on_exceeded hook, raises, or blocks the next call before it is sent. See Budgets.
Supported clients
| Client | Captured through |
|---|---|
| RubyLLM | Instrumentation events (2.x) or the provider layer (1.x) |
| OpenAI, Anthropic | Official SDK or Faraday |
| Azure OpenAI | Official SDK or Faraday, on *.openai.azure.com, and Foundry *.services.ai.azure.com or *.cognitiveservices.azure.com
|
Google Gemini, ruby-openai
|
Faraday |
| OpenRouter, DeepSeek, Groq, xAI, Mistral, Perplexity | Faraday, or the official OpenAI SDK with base_url on that host |
| Other OpenAI-compatible gateways | The same, once the host is added to config.capture.openai_compatible_providers
|
| Anything else | LlmCostTracker.track |
Streams are recorded from the provider's final usage; see Streaming.
Pricing
Captured does not always mean priced:
| Cost comes from | Calls |
|---|---|
The billed amount in the response or final stream chunk: usage.cost, xAI's usage.cost_in_usd_ticks, or Perplexity's usage.cost.total_cost
|
OpenRouter, xAI, Perplexity, and other OpenAI-compatible gateways that return one (through RubyLLM 1.x, OpenRouter, xAI, and Perplexity chats only) |
Bundled prices.json, read from each provider's own pricing page or API |
The OpenAI, Anthropic, Gemini, Groq, OpenRouter, xAI, Mistral, DeepSeek, and Cohere models it lists, DeepSeek at its off-peak rates in its off-peak hours, and the same Claude models on Bedrock through RubyLLM |
| The OpenAI, Anthropic, or Gemini price for the same model name | Azure OpenAI (by the model in the response, not the deployment name), Vertex AI through RubyLLM, gateways that pass a listed model name through |
Nothing: recorded with cost_status: unknown
|
Perplexity without a billed amount (its Router) when no price lists its model, calls through the official OpenAI SDK's Bedrock provider, and through RubyLLM also Ollama, Cohere models without a bundled price (such as Command A+, Embed 4, and Rerank 3.5), other Bedrock models, and Claude on GovCloud (us-gov. profiles) |
Add missing prices to config.pricing.file or config.pricing.overrides (Pricing), then run bin/rails llm_cost_tracker:backfill_unknown_pricing to price the calls already recorded. bin/rails llm_cost_tracker:prices:refresh runs it after writing a new config.pricing.file if the database is reachable.
Manual tracking
For batch jobs, internal gateways, or anything without an SDK or Faraday hook:
LlmCostTracker.track(
provider: :anthropic,
model: "claude-sonnet-4-6",
tokens: { input_tokens: 1500, output_tokens: 320 },
tags: { feature: "summarizer", user_id: current_user.id }
)Docs
- Configuration
- Pricing
- Budgets
- Data model
- Querying
- Dashboard
- Streaming
- Cookbook
- Extending
- Operations
- Architecture
- EU AI Act record-keeping
- Upgrading
- Changelog
Development
bundle install
bin/checkLicense
MIT. See LICENSE.txt.