llm_cost_tracker 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +46 -25
  4. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +96 -23
  5. data/app/services/llm_cost_tracker/dashboard/data_quality_aggregate.rb +81 -0
  6. data/app/views/llm_cost_tracker/data_quality/index.html.erb +65 -0
  7. data/lib/llm_cost_tracker/budget.rb +73 -22
  8. data/lib/llm_cost_tracker/configuration.rb +4 -0
  9. data/lib/llm_cost_tracker/cost.rb +1 -2
  10. data/lib/llm_cost_tracker/errors.rb +22 -3
  11. data/lib/llm_cost_tracker/event.rb +4 -0
  12. data/lib/llm_cost_tracker/event_metadata.rb +21 -15
  13. data/lib/llm_cost_tracker/generators/llm_cost_tracker/{add_monthly_totals_generator.rb → add_period_totals_generator.rb} +4 -4
  14. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb +29 -0
  15. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +96 -0
  16. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb +29 -0
  17. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +11 -5
  18. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -1
  19. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +11 -3
  20. data/lib/llm_cost_tracker/parsed_usage.rb +16 -7
  21. data/lib/llm_cost_tracker/parsers/anthropic.rb +24 -55
  22. data/lib/llm_cost_tracker/parsers/base.rb +80 -0
  23. data/lib/llm_cost_tracker/parsers/gemini.rb +17 -37
  24. data/lib/llm_cost_tracker/parsers/openai.rb +1 -6
  25. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +6 -15
  26. data/lib/llm_cost_tracker/parsers/openai_usage.rb +25 -34
  27. data/lib/llm_cost_tracker/parsers/registry.rb +17 -2
  28. data/lib/llm_cost_tracker/period_total.rb +9 -0
  29. data/lib/llm_cost_tracker/price_registry.rb +14 -4
  30. data/lib/llm_cost_tracker/price_sync/merger.rb +1 -1
  31. data/lib/llm_cost_tracker/price_sync/raw_price.rb +3 -5
  32. data/lib/llm_cost_tracker/price_sync/sources/litellm.rb +2 -3
  33. data/lib/llm_cost_tracker/price_sync/sources/open_router.rb +2 -3
  34. data/lib/llm_cost_tracker/prices.json +30 -30
  35. data/lib/llm_cost_tracker/pricing.rb +44 -32
  36. data/lib/llm_cost_tracker/railtie.rb +2 -1
  37. data/lib/llm_cost_tracker/storage/active_record_rollups.rb +142 -0
  38. data/lib/llm_cost_tracker/storage/active_record_store.rb +35 -78
  39. data/lib/llm_cost_tracker/stream_collector.rb +4 -2
  40. data/lib/llm_cost_tracker/tags_column.rb +71 -14
  41. data/lib/llm_cost_tracker/tracker.rb +54 -32
  42. data/lib/llm_cost_tracker/usage_breakdown.rb +30 -0
  43. data/lib/llm_cost_tracker/version.rb +1 -1
  44. data/lib/llm_cost_tracker.rb +10 -3
  45. metadata +9 -4
  46. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_monthly_totals_to_llm_cost_tracker.rb.erb +0 -48
  47. data/lib/llm_cost_tracker/monthly_total.rb +0 -9
@@ -2,39 +2,96 @@
2
2
 
3
3
  module LlmCostTracker
4
4
  module TagsColumn
5
+ USAGE_BREAKDOWN_COLUMNS = %w[
6
+ cache_read_input_tokens
7
+ cache_write_input_tokens
8
+ hidden_output_tokens
9
+ ].freeze
10
+
11
+ USAGE_BREAKDOWN_COST_COLUMNS = %w[
12
+ cache_read_input_cost
13
+ cache_write_input_cost
14
+ ].freeze
15
+
16
+ def reset_column_information
17
+ remove_instance_variable(:@lct_schema_capabilities) if instance_variable_defined?(:@lct_schema_capabilities)
18
+
19
+ super
20
+ end
21
+
5
22
  def tags_json_column?
6
- tags_jsonb_column? || tags_mysql_json_column?
23
+ capabilities = lct_schema_capabilities
24
+
25
+ capabilities.fetch(:tags_jsonb) || capabilities.fetch(:tags_mysql_json)
7
26
  end
8
27
 
9
28
  def tags_jsonb_column?
10
- column = columns_hash["tags"]
11
- return false unless column
12
-
13
- column.type == :jsonb || column.sql_type.to_s.downcase == "jsonb"
29
+ lct_schema_capabilities.fetch(:tags_jsonb)
14
30
  end
15
31
 
16
32
  def tags_mysql_json_column?
17
- column = columns_hash["tags"]
18
- return false unless column
19
- return false if tags_jsonb_column?
20
-
21
- column.type == :json && connection.adapter_name.match?(/mysql/i)
33
+ lct_schema_capabilities.fetch(:tags_mysql_json)
22
34
  end
23
35
 
24
36
  def latency_column?
25
- columns_hash.key?("latency_ms")
37
+ lct_schema_capabilities.fetch(:latency)
26
38
  end
27
39
 
28
40
  def stream_column?
29
- columns_hash.key?("stream")
41
+ lct_schema_capabilities.fetch(:stream)
30
42
  end
31
43
 
32
44
  def usage_source_column?
33
- columns_hash.key?("usage_source")
45
+ lct_schema_capabilities.fetch(:usage_source)
34
46
  end
35
47
 
36
48
  def provider_response_id_column?
37
- columns_hash.key?("provider_response_id")
49
+ lct_schema_capabilities.fetch(:provider_response_id)
50
+ end
51
+
52
+ def pricing_mode_column?
53
+ lct_schema_capabilities.fetch(:pricing_mode)
54
+ end
55
+
56
+ def usage_breakdown_columns?
57
+ lct_schema_capabilities.fetch(:usage_breakdown)
58
+ end
59
+
60
+ def usage_breakdown_cost_columns?
61
+ lct_schema_capabilities.fetch(:usage_breakdown_cost)
62
+ end
63
+
64
+ private
65
+
66
+ def lct_schema_capabilities
67
+ columns = columns_hash
68
+ adapter_name = connection.adapter_name
69
+ cache = @lct_schema_capabilities
70
+
71
+ return cache.fetch(:values) if cache && cache.fetch(:columns).equal?(columns) &&
72
+ cache.fetch(:adapter_name) == adapter_name
73
+
74
+ values = build_lct_schema_capabilities(columns, adapter_name)
75
+ @lct_schema_capabilities = { columns: columns, adapter_name: adapter_name, values: values }
76
+ values
77
+ end
78
+
79
+ def build_lct_schema_capabilities(columns, adapter_name)
80
+ tag_column = columns["tags"]
81
+ tags_jsonb = tag_column && (tag_column.type == :jsonb || tag_column.sql_type.to_s.downcase == "jsonb")
82
+ tags_mysql_json = tag_column && !tags_jsonb && tag_column.type == :json && adapter_name.match?(/mysql/i)
83
+
84
+ {
85
+ tags_jsonb: tags_jsonb ? true : false,
86
+ tags_mysql_json: tags_mysql_json ? true : false,
87
+ latency: columns.key?("latency_ms"),
88
+ stream: columns.key?("stream"),
89
+ usage_source: columns.key?("usage_source"),
90
+ provider_response_id: columns.key?("provider_response_id"),
91
+ pricing_mode: columns.key?("pricing_mode"),
92
+ usage_breakdown: USAGE_BREAKDOWN_COLUMNS.all? { |column| columns.key?(column) },
93
+ usage_breakdown_cost: USAGE_BREAKDOWN_COST_COLUMNS.all? { |column| columns.key?(column) }
94
+ }
38
95
  end
39
96
  end
40
97
  end
@@ -16,28 +16,70 @@ module LlmCostTracker
16
16
  end
17
17
 
18
18
  def record(provider:, model:, input_tokens:, output_tokens:, latency_ms: nil, stream: false,
19
- usage_source: nil, provider_response_id: nil, metadata: {})
19
+ usage_source: nil, provider_response_id: nil, pricing_mode: nil, metadata: {})
20
20
  return unless LlmCostTracker.configuration.enabled
21
21
 
22
- usage = EventMetadata.usage_data(input_tokens, output_tokens, metadata)
22
+ usage = usage_data(input_tokens, output_tokens, metadata, pricing_mode)
23
+ cost_data = cost_for_usage(provider, model, usage)
23
24
 
24
- cost_data = Pricing.cost_for(
25
+ UnknownPricing.handle!(model) unless cost_data
26
+
27
+ event = build_event(
28
+ provider: provider,
29
+ model: model,
30
+ usage: usage,
31
+ cost_data: cost_data,
32
+ metadata: metadata,
33
+ latency_ms: latency_ms,
34
+ stream: stream,
35
+ usage_source: usage_source,
36
+ provider_response_id: provider_response_id
37
+ )
38
+
39
+ ActiveSupport::Notifications.instrument(EVENT_NAME, event.to_h)
40
+
41
+ stored = store(event)
42
+ Budget.check!(event) unless stored == false
43
+
44
+ event
45
+ end
46
+
47
+ private
48
+
49
+ def usage_data(input_tokens, output_tokens, metadata, pricing_mode)
50
+ metadata = metadata.merge(pricing_mode: pricing_mode) unless pricing_mode.nil?
51
+
52
+ EventMetadata.usage_data(
53
+ input_tokens,
54
+ output_tokens,
55
+ metadata
56
+ )
57
+ end
58
+
59
+ def cost_for_usage(provider, model, usage)
60
+ Pricing.cost_for(
61
+ provider: provider,
25
62
  model: model,
26
63
  input_tokens: usage[:input_tokens],
27
64
  output_tokens: usage[:output_tokens],
28
- cached_input_tokens: usage[:cached_input_tokens],
29
65
  cache_read_input_tokens: usage[:cache_read_input_tokens],
30
- cache_creation_input_tokens: usage[:cache_creation_input_tokens]
66
+ cache_write_input_tokens: usage[:cache_write_input_tokens],
67
+ pricing_mode: usage[:pricing_mode]
31
68
  )
69
+ end
32
70
 
33
- UnknownPricing.handle!(model) unless cost_data
34
-
35
- event = Event.new(
71
+ def build_event(provider:, model:, usage:, cost_data:, metadata:, latency_ms:, stream:, usage_source:,
72
+ provider_response_id:)
73
+ Event.new(
36
74
  provider: provider,
37
75
  model: model,
38
76
  input_tokens: usage[:input_tokens],
39
77
  output_tokens: usage[:output_tokens],
40
78
  total_tokens: usage[:total_tokens],
79
+ cache_read_input_tokens: usage[:cache_read_input_tokens],
80
+ cache_write_input_tokens: usage[:cache_write_input_tokens],
81
+ hidden_output_tokens: usage[:hidden_output_tokens],
82
+ pricing_mode: usage[:pricing_mode],
41
83
  cost: cost_data,
42
84
  tags: LlmCostTracker.configuration.default_tags.merge(EventMetadata.tags(metadata)).freeze,
43
85
  latency_ms: normalized_latency_ms(latency_ms),
@@ -46,17 +88,8 @@ module LlmCostTracker
46
88
  provider_response_id: normalized_provider_response_id(provider_response_id),
47
89
  tracked_at: Time.now.utc
48
90
  )
49
-
50
- ActiveSupport::Notifications.instrument(EVENT_NAME, event.to_h)
51
-
52
- stored = store(event)
53
- Budget.check!(event) unless stored == false
54
-
55
- event
56
91
  end
57
92
 
58
- private
59
-
60
93
  def store(event)
61
94
  config = LlmCostTracker.configuration
62
95
  case config.storage_backend
@@ -73,7 +106,7 @@ module LlmCostTracker
73
106
 
74
107
  def log_event(event, config)
75
108
  message = "#{event.provider}/#{event.model} " \
76
- "tokens=#{event.input_tokens}+#{event.output_tokens} " \
109
+ "tokens=#{event.total_tokens} " \
77
110
  "cost=#{log_cost_label(event)}"
78
111
  message += " latency=#{event.latency_ms}ms" if event.latency_ms
79
112
  message += " stream=#{event.stream}" if event.stream
@@ -84,9 +117,7 @@ module LlmCostTracker
84
117
  event
85
118
  end
86
119
 
87
- def log_cost_label(event)
88
- event.cost ? "$#{format('%.6f', event.cost.total_cost)}" : "unknown"
89
- end
120
+ def log_cost_label(event) = event.cost ? "$#{format('%.6f', event.cost.total_cost)}" : "unknown"
90
121
 
91
122
  def active_record_save(event)
92
123
  require_relative "llm_api_call" unless defined?(LlmCostTracker::LlmApiCall)
@@ -115,11 +146,7 @@ module LlmCostTracker
115
146
  end
116
147
  end
117
148
 
118
- def normalized_latency_ms(latency_ms)
119
- return nil if latency_ms.nil?
120
-
121
- [latency_ms.to_i, 0].max
122
- end
149
+ def normalized_latency_ms(latency_ms) = latency_ms.nil? ? nil : [latency_ms.to_i, 0].max
123
150
 
124
151
  def normalized_usage_source(value)
125
152
  return nil if value.nil?
@@ -128,12 +155,7 @@ module LlmCostTracker
128
155
  USAGE_SOURCES.include?(symbol) ? symbol.to_s : nil
129
156
  end
130
157
 
131
- def normalized_provider_response_id(value)
132
- return nil if value.nil?
133
-
134
- string = value.to_s
135
- string.empty? ? nil : string
136
- end
158
+ def normalized_provider_response_id(value) = value.nil? || value.to_s.empty? ? nil : value.to_s
137
159
  end
138
160
  end
139
161
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LlmCostTracker
4
+ UsageBreakdown = Data.define(
5
+ :input_tokens,
6
+ :cache_read_input_tokens,
7
+ :cache_write_input_tokens,
8
+ :output_tokens,
9
+ :hidden_output_tokens
10
+ ) do
11
+ def self.build(input_tokens:, output_tokens:, cache_read_input_tokens: 0,
12
+ cache_write_input_tokens: 0, hidden_output_tokens: 0)
13
+ new(
14
+ input_tokens: input_tokens.to_i,
15
+ cache_read_input_tokens: cache_read_input_tokens.to_i,
16
+ cache_write_input_tokens: cache_write_input_tokens.to_i,
17
+ output_tokens: output_tokens.to_i,
18
+ hidden_output_tokens: hidden_output_tokens.to_i
19
+ )
20
+ end
21
+
22
+ def total_tokens
23
+ input_tokens + cache_read_input_tokens + cache_write_input_tokens + output_tokens
24
+ end
25
+
26
+ def to_h
27
+ super.merge(total_tokens: total_tokens).compact
28
+ end
29
+ end
30
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmCostTracker
4
- VERSION = "0.3.3"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -10,6 +10,7 @@ require_relative "llm_cost_tracker/errors"
10
10
  require_relative "llm_cost_tracker/logging"
11
11
  require_relative "llm_cost_tracker/parameter_hash"
12
12
  require_relative "llm_cost_tracker/cost"
13
+ require_relative "llm_cost_tracker/usage_breakdown"
13
14
  require_relative "llm_cost_tracker/event"
14
15
  require_relative "llm_cost_tracker/parsed_usage"
15
16
  require_relative "llm_cost_tracker/price_registry"
@@ -69,7 +70,7 @@ module LlmCostTracker
69
70
  end
70
71
 
71
72
  def track(provider:, model:, input_tokens:, output_tokens:, latency_ms: nil, stream: false, usage_source: :manual,
72
- enforce_budget: false, provider_response_id: nil, **metadata)
73
+ enforce_budget: false, provider_response_id: nil, pricing_mode: nil, **metadata)
73
74
  enforce_budget! if enforce_budget
74
75
  Tracker.record(
75
76
  provider: provider.to_s,
@@ -80,11 +81,13 @@ module LlmCostTracker
80
81
  stream: stream,
81
82
  usage_source: usage_source,
82
83
  provider_response_id: provider_response_id,
84
+ pricing_mode: pricing_mode,
83
85
  metadata: metadata
84
86
  )
85
87
  end
86
88
 
87
- def track_stream(provider:, model:, latency_ms: nil, enforce_budget: false, provider_response_id: nil, **metadata)
89
+ def track_stream(provider:, model:, latency_ms: nil, enforce_budget: false, provider_response_id: nil,
90
+ pricing_mode: nil, **metadata)
88
91
  require_relative "llm_cost_tracker/stream_collector"
89
92
  enforce_budget! if enforce_budget
90
93
  collector = StreamCollector.new(
@@ -92,6 +95,7 @@ module LlmCostTracker
92
95
  model: model,
93
96
  latency_ms: latency_ms,
94
97
  provider_response_id: provider_response_id,
98
+ pricing_mode: pricing_mode,
95
99
  metadata: metadata
96
100
  )
97
101
  yield collector
@@ -107,7 +111,10 @@ module LlmCostTracker
107
111
  return unless config.budget_exceeded_behavior == :block_requests
108
112
  return if config.active_record?
109
113
 
110
- Logging.warn(":block_requests requires storage_backend = :active_record; preflight blocking will be skipped.")
114
+ Logging.warn(
115
+ ":block_requests requires storage_backend = :active_record for monthly and daily preflight; " \
116
+ "preflight blocking will be skipped."
117
+ )
111
118
  end
112
119
  end
113
120
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_cost_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergii Khomenko
@@ -240,6 +240,7 @@ files:
240
240
  - app/helpers/llm_cost_tracker/dashboard_query_helper.rb
241
241
  - app/helpers/llm_cost_tracker/pagination_helper.rb
242
242
  - app/services/llm_cost_tracker/dashboard/data_quality.rb
243
+ - app/services/llm_cost_tracker/dashboard/data_quality_aggregate.rb
243
244
  - app/services/llm_cost_tracker/dashboard/filter.rb
244
245
  - app/services/llm_cost_tracker/dashboard/overview_stats.rb
245
246
  - app/services/llm_cost_tracker/dashboard/provider_breakdown.rb
@@ -278,15 +279,17 @@ files:
278
279
  - lib/llm_cost_tracker/event.rb
279
280
  - lib/llm_cost_tracker/event_metadata.rb
280
281
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
281
- - lib/llm_cost_tracker/generators/llm_cost_tracker/add_monthly_totals_generator.rb
282
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb
282
283
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb
283
284
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb
285
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb
284
286
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
285
287
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
286
288
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb
287
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_monthly_totals_to_llm_cost_tracker.rb.erb
289
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb
288
290
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb
289
291
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb
292
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb
290
293
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
291
294
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
292
295
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb
@@ -297,7 +300,6 @@ files:
297
300
  - lib/llm_cost_tracker/llm_api_call.rb
298
301
  - lib/llm_cost_tracker/logging.rb
299
302
  - lib/llm_cost_tracker/middleware/faraday.rb
300
- - lib/llm_cost_tracker/monthly_total.rb
301
303
  - lib/llm_cost_tracker/parameter_hash.rb
302
304
  - lib/llm_cost_tracker/parsed_usage.rb
303
305
  - lib/llm_cost_tracker/parsers/anthropic.rb
@@ -309,6 +311,7 @@ files:
309
311
  - lib/llm_cost_tracker/parsers/registry.rb
310
312
  - lib/llm_cost_tracker/parsers/sse.rb
311
313
  - lib/llm_cost_tracker/period_grouping.rb
314
+ - lib/llm_cost_tracker/period_total.rb
312
315
  - lib/llm_cost_tracker/price_registry.rb
313
316
  - lib/llm_cost_tracker/price_sync.rb
314
317
  - lib/llm_cost_tracker/price_sync/fetcher.rb
@@ -330,6 +333,7 @@ files:
330
333
  - lib/llm_cost_tracker/report_data.rb
331
334
  - lib/llm_cost_tracker/report_formatter.rb
332
335
  - lib/llm_cost_tracker/retention.rb
336
+ - lib/llm_cost_tracker/storage/active_record_rollups.rb
333
337
  - lib/llm_cost_tracker/storage/active_record_store.rb
334
338
  - lib/llm_cost_tracker/stream_collector.rb
335
339
  - lib/llm_cost_tracker/tag_accessors.rb
@@ -338,6 +342,7 @@ files:
338
342
  - lib/llm_cost_tracker/tags_column.rb
339
343
  - lib/llm_cost_tracker/tracker.rb
340
344
  - lib/llm_cost_tracker/unknown_pricing.rb
345
+ - lib/llm_cost_tracker/usage_breakdown.rb
341
346
  - lib/llm_cost_tracker/value_helpers.rb
342
347
  - lib/llm_cost_tracker/version.rb
343
348
  - lib/tasks/llm_cost_tracker.rake
@@ -1,48 +0,0 @@
1
- class AddMonthlyTotalsToLlmCostTracker < ActiveRecord::Migration<%= migration_version %>
2
- def up
3
- create_table :llm_cost_tracker_monthly_totals do |t|
4
- t.date :month_start, null: false
5
- t.decimal :total_cost, precision: 20, scale: 8, null: false, default: 0
6
-
7
- t.timestamps
8
- end unless table_exists?(:llm_cost_tracker_monthly_totals)
9
-
10
- add_index :llm_cost_tracker_monthly_totals, :month_start,
11
- unique: true unless index_exists?(:llm_cost_tracker_monthly_totals, :month_start)
12
-
13
- backfill_monthly_totals
14
- end
15
-
16
- def down
17
- remove_index :llm_cost_tracker_monthly_totals, :month_start if index_exists?(:llm_cost_tracker_monthly_totals, :month_start)
18
- drop_table :llm_cost_tracker_monthly_totals if table_exists?(:llm_cost_tracker_monthly_totals)
19
- end
20
-
21
- private
22
-
23
- def backfill_monthly_totals
24
- return unless table_exists?(:llm_api_calls)
25
-
26
- execute <<~SQL
27
- INSERT INTO llm_cost_tracker_monthly_totals (month_start, total_cost, created_at, updated_at)
28
- SELECT #{month_bucket_sql} AS month_start,
29
- SUM(total_cost) AS total_cost,
30
- CURRENT_TIMESTAMP,
31
- CURRENT_TIMESTAMP
32
- FROM llm_api_calls
33
- WHERE total_cost IS NOT NULL
34
- GROUP BY #{month_bucket_sql}
35
- SQL
36
- end
37
-
38
- def month_bucket_sql
39
- case connection.adapter_name
40
- when /postgres/i
41
- "DATE_TRUNC('month', tracked_at)::date"
42
- when /mysql/i
43
- "DATE_FORMAT(tracked_at, '%Y-%m-01')"
44
- else
45
- "strftime('%Y-%m-01', tracked_at)"
46
- end
47
- end
48
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
-
5
- module LlmCostTracker
6
- class MonthlyTotal < ActiveRecord::Base
7
- self.table_name = "llm_cost_tracker_monthly_totals"
8
- end
9
- end