Class: Clacky::Billing::BillingStore
- Inherits:
-
Object
- Object
- Clacky::Billing::BillingStore
- Defined in:
- lib/clacky/billing/billing_store.rb
Overview
Persistent storage for billing records using JSONL files Records are stored in monthly files: ~/.clacky/billing/YYYY-MM.jsonl
Constant Summary collapse
- BILLING_DIR =
File.join(Dir.home, ".clacky", "billing")
Instance Method Summary collapse
-
#append(record) ⇒ String
Append a billing record to the current month’s file.
-
#cleanup(before:) ⇒ Integer
Delete old billing records.
-
#daily_breakdown(days: 30) ⇒ Array<Hash>
Get daily cost breakdown for the last N days.
-
#initialize(billing_dir: nil) ⇒ BillingStore
constructor
A new instance of BillingStore.
-
#query(from: nil, to: nil, model: nil, session_id: nil, limit: nil) ⇒ Array<BillingRecord>
Query billing records with optional filters.
-
#summary(period: :month) ⇒ Hash
Get summary statistics for a time period.
Constructor Details
#initialize(billing_dir: nil) ⇒ BillingStore
Returns a new instance of BillingStore.
15 16 17 18 |
# File 'lib/clacky/billing/billing_store.rb', line 15 def initialize(billing_dir: nil) @billing_dir = billing_dir || BILLING_DIR ensure_billing_dir end |
Instance Method Details
#append(record) ⇒ String
Append a billing record to the current month’s file
23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/clacky/billing/billing_store.rb', line 23 def append(record) record.id ||= SecureRandom.uuid record. ||= Time.now month_file = current_month_file File.open(month_file, "a") do |f| f.puts(JSON.generate(record.to_h)) end FileUtils.chmod(0o600, month_file) record.id end |
#cleanup(before:) ⇒ Integer
Delete old billing records
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/clacky/billing/billing_store.rb', line 145 def cleanup(before:) deleted = 0 billing_files.each do |file| # Parse month from filename (YYYY-MM.jsonl) basename = File.basename(file, ".jsonl") file_month = Time.parse("#{basename}-01") rescue nil next unless file_month # Delete if the entire month is before the cutoff if file_month < before - (31 * 24 * 60 * 60) File.delete(file) deleted += 1 end end deleted end |
#daily_breakdown(days: 30) ⇒ Array<Hash>
Get daily cost breakdown for the last N days
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/clacky/billing/billing_store.rb', line 120 def daily_breakdown(days: 30) from_time = Time.now - (days * 24 * 60 * 60) records = query(from: from_time) by_day = records.group_by { |r| r..strftime("%Y-%m-%d") } (0...days).map do |i| date = (Time.now - (i * 24 * 60 * 60)).strftime("%Y-%m-%d") day_records = by_day[date] || [] { date: date, cost: day_records.sum { |r| r.cost_usd || 0 }.round(6), tokens: day_records.sum { |r| r.total_tokens }, prompt_tokens: day_records.sum { |r| r.prompt_tokens || 0 }, completion_tokens: day_records.sum { |r| r.completion_tokens || 0 }, cache_read_tokens: day_records.sum { |r| r.cache_read_tokens || 0 }, cache_write_tokens: day_records.sum { |r| r.cache_write_tokens || 0 }, requests: day_records.size } end.reverse end |
#query(from: nil, to: nil, model: nil, session_id: nil, limit: nil) ⇒ Array<BillingRecord>
Query billing records with optional filters
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/clacky/billing/billing_store.rb', line 43 def query(from: nil, to: nil, model: nil, session_id: nil, limit: nil) records = [] billing_files.each do |file| File.foreach(file) do |line| next if line.strip.empty? begin hash = JSON.parse(line, symbolize_names: true) record = BillingRecord.from_h(hash) # Apply filters next if from && record. < from next if to && record. > to next if model && record.model != model next if session_id && record.session_id != session_id records << record rescue JSON::ParserError # Skip malformed lines next end end end # Sort by timestamp descending (newest first) records.sort_by! { |r| r. }.reverse! # Apply limit limit ? records.first(limit) : records end |
#summary(period: :month) ⇒ Hash
Get summary statistics for a time period
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/clacky/billing/billing_store.rb', line 78 def summary(period: :month) from_time = period_start(period) records = query(from: from_time) total_cost = records.sum { |r| r.cost_usd || 0 } total_prompt = records.sum { |r| r.prompt_tokens || 0 } total_completion = records.sum { |r| r.completion_tokens || 0 } total_cache_read = records.sum { |r| r.cache_read_tokens || 0 } total_cache_write = records.sum { |r| r.cache_write_tokens || 0 } by_model = records.group_by(&:model).transform_values do |rs| { cost: rs.sum { |r| r.cost_usd || 0 }, prompt_tokens: rs.sum { |r| r.prompt_tokens || 0 }, completion_tokens: rs.sum { |r| r.completion_tokens || 0 }, requests: rs.size } end by_day = records.group_by { |r| r..strftime("%Y-%m-%d") }.transform_values do |rs| rs.sum { |r| r.cost_usd || 0 } end { period: period, from: from_time&.iso8601, to: Time.now.iso8601, total_cost: total_cost.round(6), total_tokens: total_prompt + total_completion, prompt_tokens: total_prompt, completion_tokens: total_completion, cache_read_tokens: total_cache_read, cache_write_tokens: total_cache_write, by_model: by_model, by_day: by_day, record_count: records.size } end |