Class: AiUsage::CostReconciler

Inherits:
Object
  • Object
show all
Defined in:
app/services/ai_usage/cost_reconciler.rb

Overview

Compares Anthropic's real organization-level cost (Cost API) against what the
app recorded in +ai_usage_logs+, surfacing the dashboard-vs-invoice gap per
model. This is the automated form of the manual reconciliation that found
Sunny's Opus usage was only ~43% captured.

Note the asymmetry: the Cost API is org-wide (every API key, including
non-app keys like Claude Code), while +ai_usage_logs+ is app-only. So
100% coverage is only expected when non-app usage is ~zero.

Examples:

report = AiUsage::CostReconciler.new.call(since_days: 30)
report.coverage_pct # => 43.0

Defined Under Namespace

Classes: Report, Row

Instance Method Summary collapse

Constructor Details

#initialize(client: AnthropicAdminClient.new) ⇒ CostReconciler

Returns a new instance of CostReconciler.

Parameters:

  • client (#cost_by_model) (defaults to: AnthropicAdminClient.new)

    admin-API client (injectable for tests)



34
35
36
# File 'app/services/ai_usage/cost_reconciler.rb', line 34

def initialize(client: AnthropicAdminClient.new)
  @client = client
end

Instance Method Details

#call(since_days: 30, ending_at: Time.current) ⇒ Report

Parameters:

  • since_days (Integer) (defaults to: 30)

    window size in days

  • ending_at (Time) (defaults to: Time.current)

    window end (default now)

Returns:



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'app/services/ai_usage/cost_reconciler.rb', line 41

def call(since_days: 30, ending_at: Time.current)
  starting_at = ending_at - since_days.days
  real    = @client.cost_by_model(starting_at: starting_at.utc.iso8601, ending_at: ending_at.utc.iso8601)
  tracked = tracked_cost_by_model(starting_at, ending_at)

  rows = (real[:by_model].keys | tracked.keys).map do |model|
    Row.new(model: model, real_usd: real[:by_model][model] || 0.0, tracked_usd: tracked[model] || 0.0)
  end.sort_by { |row| -row.real_usd }

  Report.new(
    starting_at:       starting_at,
    ending_at:         ending_at,
    real_total_usd:    real[:total_usd],
    tracked_total_usd: tracked.values.sum.round(2),
    rows:              rows
  )
end