Class: AiUsageLog

Inherits:
ApplicationRecord show all
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

Belongs to collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#featureObject (readonly)



64
# File 'app/models/ai_usage_log.rb', line 64

validates :provider, :model_id, :feature, presence: true

#model_idObject (readonly)



64
# File 'app/models/ai_usage_log.rb', line 64

validates :provider, :model_id, :feature, presence: true

#providerObject (readonly)



64
# File 'app/models/ai_usage_log.rb', line 64

validates :provider, :model_id, :feature, presence: true

Class Method Details

.for_featureActiveRecord::Relation<AiUsageLog>

A relation of AiUsageLogs that are for feature. Active Record Scope

Returns:

See Also:



114
# File 'app/models/ai_usage_log.rb', line 114

scope :for_feature, ->(f) { where(feature: f) }

.for_providerActiveRecord::Relation<AiUsageLog>

A relation of AiUsageLogs that are for provider. Active Record Scope

Returns:

See Also:



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:       ,
    metadata:          || {}
  )
rescue StandardError => e
  Rails.logger.error "[AiUsageLog] Failed to record usage: #{e.message}"
  nil
end

.recentActiveRecord::Relation<AiUsageLog>

A relation of AiUsageLogs that are recent. Active Record Scope

Returns:

See Also:



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

#accountAccount

Returns:

See Also:



62
# File 'app/models/ai_usage_log.rb', line 62

belongs_to :account, optional: true

#cost_usdObject

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

#subjectSubject

Returns:

  • (Subject)

See Also:



61
# File 'app/models/ai_usage_log.rb', line 61

belongs_to :subject, polymorphic: true, optional: true

#total_tokensObject



106
107
108
# File 'app/models/ai_usage_log.rb', line 106

def total_tokens
  (input_tokens || 0) + (output_tokens || 0)
end