Module: LlmCostTracker::Reconciliation

Defined in:
lib/llm_cost_tracker/reconciliation.rb,
lib/llm_cost_tracker/reconciliation/diff.rb,
lib/llm_cost_tracker/reconciliation/importer.rb,
lib/llm_cost_tracker/reconciliation/diff_result.rb,
lib/llm_cost_tracker/reconciliation/import_result.rb,
lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb,
lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb,
lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb

Defined Under Namespace

Modules: Sources Classes: Diff, DiffResult, ImportResult, Importer

Constant Summary collapse

SUPPORTED_SOURCES =
%i[openai anthropic gemini csv].freeze
DEFAULT_THRESHOLD_PERCENT =
5.0
INVOICE_FRESHNESS_DAYS =
14
SOURCE_TO_PROVIDER =
{
  "openai" => "openai",
  "openai_usage" => "openai",
  "anthropic" => "anthropic",
  "anthropic_usage" => "anthropic",
  "gemini" => "gemini"
}.freeze
SCHEMA_TABLES =
{
  Ledger::Schema::ProviderInvoices => "llm_cost_tracker_provider_invoices",
  Ledger::Schema::ProviderInvoiceImports => "llm_cost_tracker_provider_invoice_imports"
}.freeze

Class Method Summary collapse

Class Method Details

.diff(source:, period_start:, period_end:, provider: nil, scope: {}, currency: nil, drilldown_limit: Diff::DEFAULT_DRILLDOWN_LIMIT) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/llm_cost_tracker/reconciliation.rb', line 48

def diff(source:, period_start:, period_end:, provider: nil, scope: {}, currency: nil,
         drilldown_limit: Diff::DEFAULT_DRILLDOWN_LIMIT)
  ensure_enabled!
  ensure_source_present!(source)
  Diff.new(
    source: source,
    provider: resolve_provider(source: source, provider: provider),
    period_start: period_start,
    period_end: period_end,
    scope: scope,
    currency: currency,
    drilldown_limit: drilldown_limit
  ).call
end

.enabled?Boolean

Returns:

  • (Boolean)


105
106
107
# File 'lib/llm_cost_tracker/reconciliation.rb', line 105

def enabled?
  LlmCostTracker.configuration.reconciliation_enabled
end

.ensure_enabled!Object

Raises:



109
110
111
112
113
114
115
# File 'lib/llm_cost_tracker/reconciliation.rb', line 109

def ensure_enabled!
  return if enabled?

  raise Error,
        "reconciliation is disabled; set `config.reconciliation_enabled = true` in your initializer " \
        "(requires admin/org-level provider API keys; see docs/upgrading.md)"
end

.ensure_source_present!(source) ⇒ Object

Raises:

  • (ArgumentError)


63
64
65
66
67
# File 'lib/llm_cost_tracker/reconciliation.rb', line 63

def ensure_source_present!(source)
  return unless source.to_s.empty?

  raise ArgumentError, "source must be present"
end

.import(source:, rows:, provider: nil, imported_at: nil, window: nil, strict_metadata: nil, cursor: nil) ⇒ Object



34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/llm_cost_tracker/reconciliation.rb', line 34

def import(source:, rows:, provider: nil, imported_at: nil, window: nil,
           strict_metadata: nil, cursor: nil)
  ensure_enabled!
  ensure_source_present!(source)
  Importer.new(
    source: source,
    provider: resolve_provider(source: source, provider: provider),
    imported_at: imported_at,
    window: window,
    strict_metadata: ,
    cursor: cursor
  ).call(rows)
end

.metadata_provider_value(metadata) ⇒ Object



96
97
98
99
100
101
102
103
# File 'lib/llm_cost_tracker/reconciliation.rb', line 96

def ()
  case 
  when Hash then ["provider"]
  when String
    parsed = JSON.parse() rescue nil # rubocop:disable Style/RescueModifier
    parsed.is_a?(Hash) ? parsed["provider"] : nil
  end
end

.recorded_provider_for(source) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/llm_cost_tracker/reconciliation.rb', line 84

def recorded_provider_for(source)
  return nil unless LlmCostTracker::ProviderInvoice.table_exists?

   = LlmCostTracker::ProviderInvoice
             .where(source: source.to_s)
             .order(imported_at: :desc)
             .limit(1)
             .pick(:metadata)
  value = ()
  value if value.is_a?(String) && !value.empty?
end

.resolve_provider(source:, provider:) ⇒ Object

Raises:

  • (ArgumentError)


69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/llm_cost_tracker/reconciliation.rb', line 69

def resolve_provider(source:, provider:)
  return provider.to_s if provider

  mapped = SOURCE_TO_PROVIDER[source.to_s]
  return mapped if mapped

  recorded = recorded_provider_for(source)
  return recorded if recorded

  known = SOURCE_TO_PROVIDER.keys.join(", ")
  raise ArgumentError,
        "provider: must be specified for reconciliation source #{source.inspect}; " \
        "sources with a default provider mapping: #{known}"
end