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
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 =
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 — Mar 2026. { 'claude-sonnet' => { input: 3.0, output: 15.0, cached_input: 0.3 }, 'claude-haiku' => { input: 1.0, output: 5.0, cached_input: 0.1 }, 'claude-opus' => { input: 5.0, output: 25.0, cached_input: 0.5 }, 'gpt-5' => { input: 1.25, output: 10.0, cached_input: 0.3125 }, 'gpt-5-mini' => { input: 0.25, output: 2.0, cached_input: 0.0625 }, 'gpt-5.4' => { input: 2.5, output: 20.0, cached_input: 0.625 }, 'gemini-flash' => { input: 0.3, output: 2.5, cached_input: 0.075 }, 'gemini-pro' => { input: 1.25, output: 10.0, cached_input: 0.3125 } }.freeze
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, 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 Models::EventPublishable
Instance Attribute Details
#feature ⇒ Object (readonly)
64 |
# File 'app/models/ai_usage_log.rb', line 64 validates :provider, :model_id, :feature, presence: true |
#model_id ⇒ Object (readonly)
64 |
# File 'app/models/ai_usage_log.rb', line 64 validates :provider, :model_id, :feature, presence: true |
#provider ⇒ Object (readonly)
64 |
# File 'app/models/ai_usage_log.rb', line 64 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
114 |
# File 'app/models/ai_usage_log.rb', line 114 scope :for_feature, ->(f) { where(feature: f) } |
.for_provider ⇒ ActiveRecord::Relation<AiUsageLog>
A relation of AiUsageLogs that are for provider. Active Record Scope
115 |
# File 'app/models/ai_usage_log.rb', line 115 scope :for_provider, ->(p) { where(provider: p) } |
.log!(provider:, model_id:, feature:, input_tokens: nil, output_tokens: nil, cached_tokens: nil, 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.
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'app/models/ai_usage_log.rb', line 73 def self.log!(provider:, model_id:, feature:, input_tokens: nil, output_tokens: nil, cached_tokens: nil, subject: nil, account_id: nil, metadata: {}) cost = calculate_cost(model_id, input_tokens, output_tokens, cached_tokens) 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, cost_microdollars: cost, subject: subject, account_id: account_id, metadata: || {} ) 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
116 |
# File 'app/models/ai_usage_log.rb', line 116 scope :recent, -> { order(created_at: :desc) } |
.total_cost_usd(scope = all) ⇒ Object
Class helpers
122 123 124 125 |
# File 'app/models/ai_usage_log.rb', line 122 def self.total_cost_usd(scope = all) micros = scope.sum(:cost_microdollars) micros.to_f / MICRODOLLARS_PER_DOLLAR end |
Instance Method Details
#account ⇒ Account
62 |
# File 'app/models/ai_usage_log.rb', line 62 belongs_to :account, optional: true |
#cost_usd ⇒ Object
Estimated cost in USD as a Float (nil if not known).
100 101 102 103 104 |
# File 'app/models/ai_usage_log.rb', line 100 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.
129 130 131 132 133 |
# File 'app/models/ai_usage_log.rb', line 129 def recalculate_cost! cost = self.class.send(:calculate_cost, model_id, input_tokens, output_tokens, cached_tokens) update_column(:cost_microdollars, cost) if cost && cost != cost_microdollars cost end |
#subject ⇒ Subject
61 |
# File 'app/models/ai_usage_log.rb', line 61 belongs_to :subject, polymorphic: true, optional: true |
#total_tokens ⇒ Object
106 107 108 |
# File 'app/models/ai_usage_log.rb', line 106 def total_tokens (input_tokens || 0) + (output_tokens || 0) end |