Module: LlmCostTracker::ReconcileTasks

Defined in:
lib/llm_cost_tracker/reconcile_tasks.rb

Constant Summary collapse

SOURCE_PARSERS =
{
  "openai" => Reconciliation::Sources::OpenaiUsage,
  "anthropic" => Reconciliation::Sources::AnthropicUsage
}.freeze
GENERIC_SOURCES =
%w[csv].freeze

Class Method Summary collapse

Class Method Details

.diff_from_env(env: ENV) ⇒ Object



43
44
45
46
47
48
49
50
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 43

def diff_from_env(env: ENV)
  source = required_env(env, "SOURCE")
  period_start = Date.parse(required_env(env, "PERIOD_START"))
  period_end = Date.parse(required_env(env, "PERIOD_END"))
  Reconciliation.diff(source: source.to_sym, period_start: period_start, period_end: period_end,
                      provider: env["PROVIDER"],
                      drilldown_limit: parse_drilldown_limit(env["DRILLDOWN_LIMIT"]))
end

.format_amount(value) ⇒ Object



123
124
125
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 123

def format_amount(value)
  value.nil? ? "n/a" : value.to_s("F")
end

.format_attribution(attribution) ⇒ Object



127
128
129
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 127

def format_attribution(attribution)
  LlmCostTracker::Masking.format_attribution(attribution, separator: ",")
end

.import_from_env(env: ENV) ⇒ Object

Raises:

  • (ArgumentError)


33
34
35
36
37
38
39
40
41
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 33

def import_from_env(env: ENV)
  source = required_env(env, "SOURCE")
  input_path = required_env(env, "INPUT")
  raise ArgumentError, "INPUT file not found: #{input_path}" unless File.exist?(input_path)

  payload = JSON.parse(File.read(input_path))
  rows = parse_rows(source: source, payload: payload)
  Reconciliation.import(source: source.to_sym, rows: rows, provider: env["PROVIDER"])
end

.parse_drilldown_limit(value) ⇒ Object



52
53
54
55
56
57
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 52

def parse_drilldown_limit(value)
  return Reconciliation::Diff::DEFAULT_DRILLDOWN_LIMIT if value.nil? || value.to_s.empty?
  return nil if value.to_s.downcase == "all"

  Integer(value)
end

.parse_rows(source:, payload:) ⇒ Object

Raises:

  • (ArgumentError)


71
72
73
74
75
76
77
78
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 71

def parse_rows(source:, payload:)
  parser = SOURCE_PARSERS[source.to_s]
  return parser.parse(payload) if parser
  return Array(payload["rows"]) if GENERIC_SOURCES.include?(source.to_s)

  known = (SOURCE_PARSERS.keys + GENERIC_SOURCES).join(", ")
  raise ArgumentError, "unknown SOURCE #{source.inspect}; known sources: #{known}"
end


59
60
61
62
63
64
65
66
67
68
69
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 59

def print_diff(diff, output: $stdout)
  output.puts "llm_cost_tracker: reconciliation diff for #{diff.source} " \
              "#{diff.period_start}..#{diff.period_end}"
  output.puts "  provider_total: #{diff.provider_total.to_s('F')} #{diff.currency}"
  output.puts "  local_total:    #{diff.local_total.to_s('F')} #{diff.currency} " \
              "(from #{diff.local_total_source})"
  output.puts "  delta:          #{diff.delta_amount.to_s('F')} (#{diff.delta_percent || 'n/a'}%)"
  print_unmatched_provider_rows(diff, output)
  print_unmatched_local_calls(diff, output)
  print_non_cost_rows(diff, output)
end


102
103
104
105
106
107
108
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 102

def print_non_cost_rows(diff, output)
  print_section(output, "non-cost evidence",
                diff.non_cost_rows, diff.non_cost_rows_total) do |row|
    "[#{row[:row_type]}/#{row[:meter]}] " \
      "#{format_amount(row[:billed_amount])} #{format_attribution(row[:attribution])}"
  end
end


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

def print_section(output, label, rows, total)
  return if rows.empty?

  output.puts "  #{label}#{truncation_suffix(rows.size, total)}:"
  rows.each { |row| output.puts "    #{yield(row)}" }
end


95
96
97
98
99
100
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 95

def print_unmatched_local_calls(diff, output)
  print_section(output, "unmatched local calls",
                diff.unmatched_local_calls, diff.unmatched_local_calls_total) do |row|
    "#{row[:count]} calls / #{row[:total_cost].to_s('F')} #{format_attribution(row[:attribution])}"
  end
end


87
88
89
90
91
92
93
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 87

def print_unmatched_provider_rows(diff, output)
  print_section(output, "unmatched provider rows",
                diff.unmatched_provider_rows, diff.unmatched_provider_rows_total) do |row|
    "#{row[:external_id]} (#{row[:match_basis]}): " \
      "#{format_amount(row[:billed_amount])} #{format_attribution(row[:attribution])}"
  end
end

.required_env(env, key) ⇒ Object

Raises:

  • (ArgumentError)


80
81
82
83
84
85
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 80

def required_env(env, key)
  value = env[key].to_s.strip
  raise ArgumentError, "missing #{key}" if value.empty?

  value
end

.run_diff(env: ENV, output: $stdout) ⇒ Object



27
28
29
30
31
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 27

def run_diff(env: ENV, output: $stdout)
  diff = diff_from_env(env: env)
  print_diff(diff, output: output)
  diff
end

.run_import(env: ENV, output: $stdout, error_output: $stderr) ⇒ Object



17
18
19
20
21
22
23
24
25
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 17

def run_import(env: ENV, output: $stdout, error_output: $stderr)
  result = import_from_env(env: env)
  output.puts "llm_cost_tracker: imported #{result.total_imported} rows " \
              "(inserted=#{result.inserted}, updated=#{result.updated}, skipped=#{result.skipped})"
  result.errors.each { |error| error_output.puts "  error: #{error}" }
  raise "llm_cost_tracker: reconcile import had errors" unless result.success?

  result
end

.truncation_suffix(shown, total) ⇒ Object



117
118
119
120
121
# File 'lib/llm_cost_tracker/reconcile_tasks.rb', line 117

def truncation_suffix(shown, total)
  return "" if shown >= total

  " (showing #{shown} of #{total} — pass DRILLDOWN_LIMIT=all to see every row)"
end