0.01
The project is in a healthy, maintained state
Records every LLM call your Rails app makes through RubyLLM, the official OpenAI and Anthropic SDKs, Faraday, or any OpenAI-compatible API: tokens and cost, attributed by user, feature, or any tag. Calls go straight to the provider — no proxy. Includes budgets that can block the next call, bundled prices, and a dashboard.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 13.0
~> 3.0
~> 1.0
~> 3.0
~> 0.22
~> 1.19
~> 1.6
~> 1.61
~> 0.80
>= 1.16, < 3

Runtime

>= 2.0, < 3.0
~> 3.0
>= 8.0, < 9.0
>= 8.0, < 9.0
>= 8.0, < 9.0
 Project Readme

LLM Cost Tracker

LLM spend tracking and budgets for Rails: by user, feature, or any tag, in your own database, no proxy.

Gem Version CI codecov

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.

LLM Cost Tracker dashboard

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:setup

Runs 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
end

Your 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")
end

Mount the dashboard in config/routes.rb. The engine ships without authentication, so put it behind yours:

authenticate :admin do
  mount LlmCostTracker::Engine => "/llm-costs"
end

What 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 track or track_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

Development

bundle install
bin/check

License

MIT. See LICENSE.txt.