Class: AiUsageLog
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- AiUsageLog
- Defined in:
- app/models/ai_usage_log.rb
Overview
Audit trail for every AI API call made by the application.
== Intended callers
AiUsageLog.log!(
provider: 'google',
model_id: 'gemini-2.5-flash-image',
feature: 'image_generation',
input_tokens: 420,
output_tokens: 8,
subject: generated_image, # optional polymorphic
account_id: current_account.id, # optional
metadata: { finish_reason: 'STOP' }
)
== Cost calculation
Pricing is looked up from the +LlmModel+ registry table (populated by
rails ruby_llm:load_models). Costs are stored as integer microdollars
(1_000_000 = $1.00) to avoid floating-point rounding errors.
If no pricing data is found for the model, +cost_microdollars+ is nil.
== Schema Information
Table name: ai_usage_logs
Database name: primary
id :bigint not null, primary key
cache_write_tokens :integer default(0), not null
cached_tokens :integer default(0)
cost_microdollars :integer
feature :string not null
input_tokens :integer
metadata :jsonb not null
output_tokens :integer
provider :string not null
subject_type :string
created_at :datetime not null
updated_at :datetime not null
account_id :bigint
model_id :string not null
subject_id :bigint
Indexes
index_ai_usage_logs_on_account_id (account_id)
index_ai_usage_logs_on_created_at (created_at)
index_ai_usage_logs_on_feature (feature)
index_ai_usage_logs_on_provider_and_model_id (provider,model_id)
index_ai_usage_logs_on_subject_type_and_subject_id (subject_type,subject_id)
Foreign Keys
fk_rails_... (account_id => accounts.id)
Constant Summary collapse
- MICRODOLLARS_PER_DOLLAR =
Microdollars per dollar.
1_000_000- FALLBACK_PRICING =
Per-million-token fallback pricing for model aliases that may not be in
the LlmModel registry. Keyed by exact model_id.
Updated from provider pricing pages — Jun 2026 (Gemini 3.5 Flash / 3.1 Pro).
cache_writeis the Anthropic cache-creation rate (1.25× input);
OpenAI/Gemini don't separately bill cache writes (kept at 0). { 'claude-sonnet' => { input: 3.0, output: 15.0, cached_input: 0.3, cache_write: 3.75 }, 'claude-haiku' => { input: 1.0, output: 5.0, cached_input: 0.1, cache_write: 1.25 }, 'claude-opus' => { input: 5.0, output: 25.0, cached_input: 0.5, cache_write: 6.25 }, 'gpt-5' => { input: 1.25, output: 10.0, cached_input: 0.3125, cache_write: 0.0 }, 'gpt-5-mini' => { input: 0.25, output: 2.0, cached_input: 0.0625, cache_write: 0.0 }, 'gpt-5.5' => { input: 5.0, output: 30.0, cached_input: 0.5, cache_write: 0.0 }, 'gemini-flash' => { input: 1.5, output: 9.0, cached_input: 0.15, cache_write: 0.0 }, 'gemini-pro' => { input: 2.0, output: 12.0, cached_input: 0.20, cache_write: 0.0 } }.freeze
Constants included from Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
- #feature ⇒ Object readonly
- #model_id ⇒ Object readonly
- #provider ⇒ Object readonly
Belongs to collapse
Class Method Summary collapse
-
.for_feature ⇒ ActiveRecord::Relation<AiUsageLog>
A relation of AiUsageLogs that are for feature.
-
.for_provider ⇒ ActiveRecord::Relation<AiUsageLog>
A relation of AiUsageLogs that are for provider.
-
.log!(provider:, model_id:, feature:, input_tokens: nil, output_tokens: nil, cached_tokens: nil, cache_write_tokens: nil, service_tier: 'standard', subject: nil, account_id: nil, metadata: {}) ⇒ Object
Creates a usage log record.
-
.recent ⇒ ActiveRecord::Relation<AiUsageLog>
A relation of AiUsageLogs that are recent.
-
.total_cost_usd(scope = all) ⇒ Object
------------------------------------------------------------------ Class helpers ------------------------------------------------------------------.
Instance Method Summary collapse
-
#cost_usd ⇒ Object
Estimated cost in USD as a Float (nil if not known).
-
#recalculate_cost! ⇒ Object
Recalculate cost from current pricing data.
- #total_tokens ⇒ Object
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#feature ⇒ Object (readonly)
66 |
# File 'app/models/ai_usage_log.rb', line 66 validates :provider, :model_id, :feature, presence: true |
#model_id ⇒ Object (readonly)
66 |
# File 'app/models/ai_usage_log.rb', line 66 validates :provider, :model_id, :feature, presence: true |
#provider ⇒ Object (readonly)
66 |
# File 'app/models/ai_usage_log.rb', line 66 validates :provider, :model_id, :feature, presence: true |
Class Method Details
.for_feature ⇒ ActiveRecord::Relation<AiUsageLog>
A relation of AiUsageLogs that are for feature. Active Record Scope
134 |
# File 'app/models/ai_usage_log.rb', line 134 scope :for_feature, ->(f) { where(feature: f) } |
.for_provider ⇒ ActiveRecord::Relation<AiUsageLog>
A relation of AiUsageLogs that are for provider. Active Record Scope
135 |
# File 'app/models/ai_usage_log.rb', line 135 scope :for_provider, ->(p) { where(provider: p) } |
.log!(provider:, model_id:, feature:, input_tokens: nil, output_tokens: nil, cached_tokens: nil, cache_write_tokens: nil, service_tier: 'standard', subject: nil, account_id: nil, metadata: {}) ⇒ Object
Creates a usage log record. Never raises — logs errors instead so
a tracking failure never interrupts the main flow.
input_tokens is the normalized "standard input only" count from
RubyLLM 1.15+. cached_tokens is cache reads, cache_write_tokens
is cache writes (separately billable on Anthropic at ~1.25× input).
service_tier defaults to 'standard'. Pass 'batch' for Anthropic
Message Batches API usage, which is billed at 50% of standard rates: the
discount is applied to the computed cost and the tier is recorded in
metadata so recalculate_cost! / the backfill task stay consistent.
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'app/models/ai_usage_log.rb', line 84 def self.log!(provider:, model_id:, feature:, # rubocop:disable Metrics/ParameterLists input_tokens: nil, output_tokens: nil, cached_tokens: nil, cache_write_tokens: nil, service_tier: 'standard', subject: nil, account_id: nil, metadata: {}) cost = calculate_cost(model_id, input_tokens, output_tokens, cached_tokens, cache_write_tokens) cost = apply_service_tier(cost, service_tier) create!( provider: provider.to_s, model_id: model_id.to_s, feature: feature.to_s, input_tokens: input_tokens, output_tokens: output_tokens, cached_tokens: cached_tokens || 0, cache_write_tokens: cache_write_tokens || 0, cost_microdollars: cost, subject: subject, account_id: account_id, metadata: merge_service_tier(, service_tier) ) rescue ActiveRecord::RecordNotUnique => e # A concurrent insert already recorded this row (a unique index — e.g. the SEO # batch idempotency index — caught the race). The backstop worked; not an error. Rails.logger.debug { "[AiUsageLog] Skipped duplicate usage row: #{e.}" } nil rescue StandardError => e Rails.logger.error "[AiUsageLog] Failed to record usage: #{e.}" nil end |
.recent ⇒ ActiveRecord::Relation<AiUsageLog>
A relation of AiUsageLogs that are recent. Active Record Scope
136 |
# File 'app/models/ai_usage_log.rb', line 136 scope :recent, -> { order(created_at: :desc) } |
.total_cost_usd(scope = all) ⇒ Object
Class helpers
142 143 144 145 |
# File 'app/models/ai_usage_log.rb', line 142 def self.total_cost_usd(scope = all) micros = scope.sum(:cost_microdollars) micros.to_f / MICRODOLLARS_PER_DOLLAR end |
Instance Method Details
#account ⇒ Account
64 |
# File 'app/models/ai_usage_log.rb', line 64 belongs_to :account, optional: true |
#cost_usd ⇒ Object
Estimated cost in USD as a Float (nil if not known).
120 121 122 123 124 |
# File 'app/models/ai_usage_log.rb', line 120 def cost_usd return nil if cost_microdollars.nil? cost_microdollars.to_f / MICRODOLLARS_PER_DOLLAR end |
#recalculate_cost! ⇒ Object
Recalculate cost from current pricing data. Used by the backfill task
to fix records that were saved with nil cost due to missing pricing.
149 150 151 152 153 154 155 |
# File 'app/models/ai_usage_log.rb', line 149 def recalculate_cost! cost = self.class.send(:calculate_cost, model_id, input_tokens, output_tokens, cached_tokens, cache_write_tokens) cost = self.class.send(:apply_service_tier, cost, ['service_tier']) update_column(:cost_microdollars, cost) if cost && cost != cost_microdollars cost end |
#subject ⇒ Subject
63 |
# File 'app/models/ai_usage_log.rb', line 63 belongs_to :subject, polymorphic: true, optional: true |
#total_tokens ⇒ Object
126 127 128 |
# File 'app/models/ai_usage_log.rb', line 126 def total_tokens (input_tokens || 0) + (cached_tokens || 0) + (cache_write_tokens || 0) + (output_tokens || 0) end |