llm_cost_tracker 0.7.0 → 0.7.2

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 (174) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +21 -16
  4. data/app/assets/llm_cost_tracker/application.css +3 -0
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +22 -4
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +6 -11
  7. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +2 -1
  8. data/app/controllers/llm_cost_tracker/data_quality_controller.rb +5 -1
  9. data/app/controllers/llm_cost_tracker/models_controller.rb +0 -1
  10. data/app/controllers/llm_cost_tracker/tags_controller.rb +1 -8
  11. data/app/helpers/llm_cost_tracker/application_helper.rb +2 -1
  12. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +1 -2
  13. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -1
  14. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +10 -27
  15. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +58 -0
  16. data/app/models/llm_cost_tracker/ingestion/event.rb +13 -0
  17. data/app/models/llm_cost_tracker/ingestion/lease.rb +11 -0
  18. data/app/models/llm_cost_tracker/ledger/call.rb +45 -0
  19. data/app/models/llm_cost_tracker/ledger/call_metrics.rb +66 -0
  20. data/app/models/llm_cost_tracker/ledger/period/grouping.rb +71 -0
  21. data/app/models/llm_cost_tracker/ledger/period/total.rb +13 -0
  22. data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +19 -0
  23. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +111 -94
  24. data/app/services/llm_cost_tracker/dashboard/date_range.rb +2 -2
  25. data/app/services/llm_cost_tracker/dashboard/filter.rb +7 -18
  26. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +58 -67
  27. data/app/services/llm_cost_tracker/dashboard/pagination.rb +59 -0
  28. data/app/services/llm_cost_tracker/dashboard/params.rb +26 -0
  29. data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +18 -20
  30. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -13
  31. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +28 -61
  32. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +8 -21
  33. data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
  34. data/app/services/llm_cost_tracker/dashboard/top_models.rb +12 -47
  35. data/app/views/llm_cost_tracker/calls/index.html.erb +12 -18
  36. data/app/views/llm_cost_tracker/calls/show.html.erb +30 -32
  37. data/app/views/llm_cost_tracker/dashboard/index.html.erb +17 -19
  38. data/app/views/llm_cost_tracker/data_quality/index.html.erb +108 -135
  39. data/app/views/llm_cost_tracker/models/index.html.erb +8 -9
  40. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +13 -2
  41. data/app/views/llm_cost_tracker/tags/show.html.erb +20 -20
  42. data/lib/llm_cost_tracker/budget.rb +8 -20
  43. data/lib/llm_cost_tracker/capture/stream.rb +9 -0
  44. data/lib/llm_cost_tracker/capture/stream_collector.rb +189 -0
  45. data/lib/llm_cost_tracker/{integrations → capture}/stream_tracker.rb +41 -73
  46. data/lib/llm_cost_tracker/configuration/instrumentation.rb +3 -7
  47. data/lib/llm_cost_tracker/configuration.rb +33 -36
  48. data/lib/llm_cost_tracker/doctor/capture_verifier.rb +61 -0
  49. data/lib/llm_cost_tracker/doctor/check.rb +7 -0
  50. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +22 -59
  51. data/lib/llm_cost_tracker/doctor/price_check.rb +60 -0
  52. data/lib/llm_cost_tracker/doctor.rb +63 -71
  53. data/lib/llm_cost_tracker/errors.rb +4 -15
  54. data/lib/llm_cost_tracker/event.rb +6 -6
  55. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +42 -0
  56. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -0
  57. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +7 -7
  58. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +3 -3
  59. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +22 -0
  60. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +9 -14
  61. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +0 -4
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +12 -1
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +2 -2
  64. data/lib/llm_cost_tracker/{storage/active_record_inbox_batch.rb → ingestion/batch.rb} +21 -20
  65. data/lib/llm_cost_tracker/ingestion/inbox.rb +105 -0
  66. data/lib/llm_cost_tracker/{storage/active_record_ingestor_lease.rb → ingestion/lease_claim.rb} +5 -7
  67. data/lib/llm_cost_tracker/{storage/active_record_ingestor.rb → ingestion/worker.rb} +38 -48
  68. data/lib/llm_cost_tracker/ingestion.rb +129 -0
  69. data/lib/llm_cost_tracker/integrations/anthropic.rb +66 -31
  70. data/lib/llm_cost_tracker/integrations/base.rb +73 -34
  71. data/lib/llm_cost_tracker/integrations/openai.rb +43 -37
  72. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +40 -30
  73. data/lib/llm_cost_tracker/integrations.rb +43 -0
  74. data/lib/llm_cost_tracker/ledger/period/totals.rb +66 -0
  75. data/lib/llm_cost_tracker/{storage/active_record_periods.rb → ledger/period.rb} +2 -2
  76. data/lib/llm_cost_tracker/ledger/rollups/batch.rb +43 -0
  77. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +46 -0
  78. data/lib/llm_cost_tracker/ledger/rollups.rb +87 -0
  79. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +51 -0
  80. data/lib/llm_cost_tracker/ledger/schema/calls.rb +101 -0
  81. data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +32 -0
  82. data/lib/llm_cost_tracker/ledger/store.rb +60 -0
  83. data/lib/llm_cost_tracker/ledger/tags/query.rb +29 -0
  84. data/lib/llm_cost_tracker/ledger/tags/sql.rb +33 -0
  85. data/lib/llm_cost_tracker/ledger.rb +13 -0
  86. data/lib/llm_cost_tracker/logging.rb +3 -6
  87. data/lib/llm_cost_tracker/middleware/faraday.rb +88 -46
  88. data/lib/llm_cost_tracker/parsers/anthropic.rb +62 -29
  89. data/lib/llm_cost_tracker/parsers/base.rb +12 -21
  90. data/lib/llm_cost_tracker/parsers/gemini.rb +50 -25
  91. data/lib/llm_cost_tracker/parsers/openai.rb +27 -5
  92. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +14 -4
  93. data/lib/llm_cost_tracker/parsers/openai_usage.rb +58 -25
  94. data/lib/llm_cost_tracker/parsers/sse.rb +4 -7
  95. data/lib/llm_cost_tracker/parsers.rb +20 -0
  96. data/lib/llm_cost_tracker/prices.json +361 -36
  97. data/lib/llm_cost_tracker/pricing/components.rb +37 -0
  98. data/lib/llm_cost_tracker/pricing/effective_prices.rb +46 -50
  99. data/lib/llm_cost_tracker/pricing/explainer.rb +25 -30
  100. data/lib/llm_cost_tracker/pricing/lookup.rb +67 -46
  101. data/lib/llm_cost_tracker/pricing/registry.rb +156 -0
  102. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +107 -0
  103. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +53 -0
  104. data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +63 -0
  105. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +31 -0
  106. data/lib/llm_cost_tracker/pricing/sync.rb +159 -0
  107. data/lib/llm_cost_tracker/pricing/unknown.rb +46 -0
  108. data/lib/llm_cost_tracker/pricing.rb +33 -32
  109. data/lib/llm_cost_tracker/railtie.rb +7 -8
  110. data/lib/llm_cost_tracker/report/data.rb +72 -0
  111. data/lib/llm_cost_tracker/report/formatter.rb +69 -0
  112. data/lib/llm_cost_tracker/report.rb +8 -8
  113. data/lib/llm_cost_tracker/retention.rb +27 -10
  114. data/lib/llm_cost_tracker/tags/context.rb +35 -0
  115. data/lib/llm_cost_tracker/tags/key.rb +18 -0
  116. data/lib/llm_cost_tracker/tags/sanitizer.rb +68 -0
  117. data/lib/llm_cost_tracker/token_usage.rb +67 -0
  118. data/lib/llm_cost_tracker/tracker.rb +39 -69
  119. data/lib/llm_cost_tracker/usage_capture.rb +37 -0
  120. data/lib/llm_cost_tracker/version.rb +1 -1
  121. data/lib/llm_cost_tracker.rb +56 -78
  122. data/lib/tasks/llm_cost_tracker.rake +18 -13
  123. metadata +54 -58
  124. data/app/services/llm_cost_tracker/dashboard/data_quality_aggregate.rb +0 -81
  125. data/app/services/llm_cost_tracker/pagination.rb +0 -57
  126. data/lib/llm_cost_tracker/active_record_adapter.rb +0 -53
  127. data/lib/llm_cost_tracker/capture_verifier.rb +0 -64
  128. data/lib/llm_cost_tracker/cost.rb +0 -12
  129. data/lib/llm_cost_tracker/doctor/capture_check.rb +0 -39
  130. data/lib/llm_cost_tracker/event_metadata.rb +0 -52
  131. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb +0 -29
  132. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb +0 -29
  133. data/lib/llm_cost_tracker/inbox_event.rb +0 -9
  134. data/lib/llm_cost_tracker/ingestor_lease.rb +0 -9
  135. data/lib/llm_cost_tracker/integrations/object_reader.rb +0 -56
  136. data/lib/llm_cost_tracker/integrations/registry.rb +0 -71
  137. data/lib/llm_cost_tracker/llm_api_call.rb +0 -60
  138. data/lib/llm_cost_tracker/llm_api_call_metrics.rb +0 -63
  139. data/lib/llm_cost_tracker/parameter_hash.rb +0 -33
  140. data/lib/llm_cost_tracker/parsed_usage.rb +0 -72
  141. data/lib/llm_cost_tracker/parsers/registry.rb +0 -58
  142. data/lib/llm_cost_tracker/period_grouping.rb +0 -67
  143. data/lib/llm_cost_tracker/period_total.rb +0 -9
  144. data/lib/llm_cost_tracker/price_freshness.rb +0 -38
  145. data/lib/llm_cost_tracker/price_registry.rb +0 -144
  146. data/lib/llm_cost_tracker/price_sync/fetcher.rb +0 -104
  147. data/lib/llm_cost_tracker/price_sync/registry_diff.rb +0 -51
  148. data/lib/llm_cost_tracker/price_sync/registry_loader.rb +0 -61
  149. data/lib/llm_cost_tracker/price_sync/registry_writer.rb +0 -29
  150. data/lib/llm_cost_tracker/price_sync.rb +0 -144
  151. data/lib/llm_cost_tracker/report_data.rb +0 -94
  152. data/lib/llm_cost_tracker/report_formatter.rb +0 -67
  153. data/lib/llm_cost_tracker/request_url.rb +0 -20
  154. data/lib/llm_cost_tracker/storage/active_record_backend.rb +0 -167
  155. data/lib/llm_cost_tracker/storage/active_record_connection_cleanup.rb +0 -13
  156. data/lib/llm_cost_tracker/storage/active_record_inbox.rb +0 -160
  157. data/lib/llm_cost_tracker/storage/active_record_period_totals.rb +0 -84
  158. data/lib/llm_cost_tracker/storage/active_record_rollup_batch.rb +0 -41
  159. data/lib/llm_cost_tracker/storage/active_record_rollup_upsert_sql.rb +0 -42
  160. data/lib/llm_cost_tracker/storage/active_record_rollups.rb +0 -146
  161. data/lib/llm_cost_tracker/storage/active_record_store.rb +0 -145
  162. data/lib/llm_cost_tracker/storage/writer.rb +0 -35
  163. data/lib/llm_cost_tracker/stream_capture.rb +0 -7
  164. data/lib/llm_cost_tracker/stream_collector.rb +0 -199
  165. data/lib/llm_cost_tracker/tag_accessors.rb +0 -15
  166. data/lib/llm_cost_tracker/tag_context.rb +0 -52
  167. data/lib/llm_cost_tracker/tag_key.rb +0 -16
  168. data/lib/llm_cost_tracker/tag_query.rb +0 -43
  169. data/lib/llm_cost_tracker/tag_sanitizer.rb +0 -81
  170. data/lib/llm_cost_tracker/tag_sql.rb +0 -34
  171. data/lib/llm_cost_tracker/tags_column.rb +0 -105
  172. data/lib/llm_cost_tracker/unknown_pricing.rb +0 -54
  173. data/lib/llm_cost_tracker/usage_breakdown.rb +0 -30
  174. data/lib/llm_cost_tracker/value_helpers.rb +0 -40
@@ -17,14 +17,16 @@ namespace :llm_cost_tracker do
17
17
  task :verify_capture do
18
18
  Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
19
19
  require_relative "../llm_cost_tracker"
20
- checks = LlmCostTracker::CaptureVerifier.call
21
- puts LlmCostTracker::CaptureVerifier.report(checks)
22
- abort("llm_cost_tracker: capture verification failed") unless LlmCostTracker::CaptureVerifier.healthy?(checks)
20
+ checks = LlmCostTracker::Doctor::CaptureVerifier.call
21
+ puts LlmCostTracker::Doctor::CaptureVerifier.report(checks)
22
+ unless LlmCostTracker::Doctor::CaptureVerifier.healthy?(checks)
23
+ abort("llm_cost_tracker: capture verification failed")
24
+ end
23
25
  end
24
26
 
25
27
  desc "Print an LLM cost report from ActiveRecord storage"
26
28
  task report: :environment do
27
- days = (ENV["DAYS"] || LlmCostTracker::ReportData::DEFAULT_DAYS).to_i
29
+ days = (ENV["DAYS"] || LlmCostTracker::Report::Data::DEFAULT_DAYS).to_i
28
30
  puts LlmCostTracker::Report.generate(days: days)
29
31
  end
30
32
 
@@ -46,9 +48,9 @@ namespace :llm_cost_tracker do
46
48
  require_relative "../llm_cost_tracker"
47
49
 
48
50
  output_path = price_refresh_output_path
49
- source_url = LlmCostTracker::PriceSync.configured_remote_url
51
+ source_url = LlmCostTracker::Pricing::Sync.configured_remote_url
50
52
  preview = ENV["PREVIEW"] == "1"
51
- result = LlmCostTracker::PriceSync.refresh(
53
+ result = LlmCostTracker::Pricing::Sync.refresh(
52
54
  path: output_path,
53
55
  url: source_url,
54
56
  preview: preview
@@ -74,8 +76,8 @@ namespace :llm_cost_tracker do
74
76
  require_relative "../llm_cost_tracker"
75
77
 
76
78
  output_path = price_refresh_output_path
77
- source_url = LlmCostTracker::PriceSync.configured_remote_url
78
- result = LlmCostTracker::PriceSync.check(path: output_path, url: source_url)
79
+ source_url = LlmCostTracker::Pricing::Sync.configured_remote_url
80
+ result = LlmCostTracker::Pricing::Sync.check(path: output_path, url: source_url)
79
81
 
80
82
  puts "llm_cost_tracker: checked pricing file #{result.path}"
81
83
  puts " source: #{result.source_url}"
@@ -112,7 +114,7 @@ def print_changes(changes)
112
114
  end
113
115
 
114
116
  def price_refresh_output_path
115
- path = LlmCostTracker::PriceSync.configured_output_path
117
+ path = LlmCostTracker::Pricing::Sync.configured_output_path
116
118
  FileUtils.mkdir_p(File.dirname(path))
117
119
  path
118
120
  end
@@ -126,10 +128,13 @@ def price_explanation_from_env
126
128
  provider: provider,
127
129
  model: model,
128
130
  pricing_mode: ENV.fetch("PRICING_MODE", nil),
129
- input_tokens: ENV.fetch("INPUT_TOKENS", 1).to_i,
130
- output_tokens: ENV.fetch("OUTPUT_TOKENS", 1).to_i,
131
- cache_read_input_tokens: ENV.fetch("CACHE_READ_INPUT_TOKENS", 0).to_i,
132
- cache_write_input_tokens: ENV.fetch("CACHE_WRITE_INPUT_TOKENS", 0).to_i
131
+ token_usage: LlmCostTracker::TokenUsage.build(
132
+ input_tokens: ENV.fetch("INPUT_TOKENS", 1).to_i,
133
+ output_tokens: ENV.fetch("OUTPUT_TOKENS", 1).to_i,
134
+ cache_read_input_tokens: ENV.fetch("CACHE_READ_INPUT_TOKENS", 0).to_i,
135
+ cache_write_input_tokens: ENV.fetch("CACHE_WRITE_INPUT_TOKENS", 0).to_i,
136
+ cache_write_1h_input_tokens: ENV.fetch("CACHE_WRITE_1H_INPUT_TOKENS", 0).to_i
137
+ )
133
138
  )
134
139
  end
135
140
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_cost_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergii Khomenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-29 00:00:00.000000000 Z
11
+ date: 2026-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -248,18 +248,26 @@ files:
248
248
  - app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb
249
249
  - app/helpers/llm_cost_tracker/dashboard_query_helper.rb
250
250
  - app/helpers/llm_cost_tracker/pagination_helper.rb
251
+ - app/helpers/llm_cost_tracker/token_usage_helper.rb
252
+ - app/models/llm_cost_tracker/ingestion/event.rb
253
+ - app/models/llm_cost_tracker/ingestion/lease.rb
254
+ - app/models/llm_cost_tracker/ledger/call.rb
255
+ - app/models/llm_cost_tracker/ledger/call_metrics.rb
256
+ - app/models/llm_cost_tracker/ledger/period/grouping.rb
257
+ - app/models/llm_cost_tracker/ledger/period/total.rb
258
+ - app/models/llm_cost_tracker/ledger/tags/accessors.rb
251
259
  - app/services/llm_cost_tracker/dashboard/data_quality.rb
252
- - app/services/llm_cost_tracker/dashboard/data_quality_aggregate.rb
253
260
  - app/services/llm_cost_tracker/dashboard/date_range.rb
254
261
  - app/services/llm_cost_tracker/dashboard/filter.rb
255
262
  - app/services/llm_cost_tracker/dashboard/overview_stats.rb
263
+ - app/services/llm_cost_tracker/dashboard/pagination.rb
264
+ - app/services/llm_cost_tracker/dashboard/params.rb
256
265
  - app/services/llm_cost_tracker/dashboard/provider_breakdown.rb
257
266
  - app/services/llm_cost_tracker/dashboard/spend_anomaly.rb
258
267
  - app/services/llm_cost_tracker/dashboard/tag_breakdown.rb
259
268
  - app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb
260
269
  - app/services/llm_cost_tracker/dashboard/time_series.rb
261
270
  - app/services/llm_cost_tracker/dashboard/top_models.rb
262
- - app/services/llm_cost_tracker/pagination.rb
263
271
  - app/views/layouts/llm_cost_tracker/application.html.erb
264
272
  - app/views/llm_cost_tracker/calls/index.html.erb
265
273
  - app/views/llm_cost_tracker/calls/show.html.erb
@@ -279,26 +287,27 @@ files:
279
287
  - app/views/llm_cost_tracker/tags/show.html.erb
280
288
  - config/routes.rb
281
289
  - lib/llm_cost_tracker.rb
282
- - lib/llm_cost_tracker/active_record_adapter.rb
283
290
  - lib/llm_cost_tracker/assets.rb
284
291
  - lib/llm_cost_tracker/budget.rb
285
- - lib/llm_cost_tracker/capture_verifier.rb
292
+ - lib/llm_cost_tracker/capture/stream.rb
293
+ - lib/llm_cost_tracker/capture/stream_collector.rb
294
+ - lib/llm_cost_tracker/capture/stream_tracker.rb
286
295
  - lib/llm_cost_tracker/configuration.rb
287
296
  - lib/llm_cost_tracker/configuration/instrumentation.rb
288
- - lib/llm_cost_tracker/cost.rb
289
297
  - lib/llm_cost_tracker/doctor.rb
290
- - lib/llm_cost_tracker/doctor/capture_check.rb
298
+ - lib/llm_cost_tracker/doctor/capture_verifier.rb
299
+ - lib/llm_cost_tracker/doctor/check.rb
291
300
  - lib/llm_cost_tracker/doctor/ingestion_check.rb
301
+ - lib/llm_cost_tracker/doctor/price_check.rb
292
302
  - lib/llm_cost_tracker/engine.rb
293
303
  - lib/llm_cost_tracker/errors.rb
294
304
  - lib/llm_cost_tracker/event.rb
295
- - lib/llm_cost_tracker/event_metadata.rb
296
305
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb
297
306
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
298
307
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb
299
308
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb
300
309
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb
301
- - lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb
310
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb
302
311
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
303
312
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
304
313
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb
@@ -306,82 +315,69 @@ files:
306
315
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb
307
316
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb
308
317
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb
309
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb
318
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb
310
319
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
311
320
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
312
321
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb
313
322
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb
314
323
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb
315
324
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb
316
- - lib/llm_cost_tracker/inbox_event.rb
317
- - lib/llm_cost_tracker/ingestor_lease.rb
325
+ - lib/llm_cost_tracker/ingestion.rb
326
+ - lib/llm_cost_tracker/ingestion/batch.rb
327
+ - lib/llm_cost_tracker/ingestion/inbox.rb
328
+ - lib/llm_cost_tracker/ingestion/lease_claim.rb
329
+ - lib/llm_cost_tracker/ingestion/worker.rb
330
+ - lib/llm_cost_tracker/integrations.rb
318
331
  - lib/llm_cost_tracker/integrations/anthropic.rb
319
332
  - lib/llm_cost_tracker/integrations/base.rb
320
- - lib/llm_cost_tracker/integrations/object_reader.rb
321
333
  - lib/llm_cost_tracker/integrations/openai.rb
322
- - lib/llm_cost_tracker/integrations/registry.rb
323
334
  - lib/llm_cost_tracker/integrations/ruby_llm.rb
324
- - lib/llm_cost_tracker/integrations/stream_tracker.rb
325
- - lib/llm_cost_tracker/llm_api_call.rb
326
- - lib/llm_cost_tracker/llm_api_call_metrics.rb
335
+ - lib/llm_cost_tracker/ledger.rb
336
+ - lib/llm_cost_tracker/ledger/period.rb
337
+ - lib/llm_cost_tracker/ledger/period/totals.rb
338
+ - lib/llm_cost_tracker/ledger/rollups.rb
339
+ - lib/llm_cost_tracker/ledger/rollups/batch.rb
340
+ - lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb
341
+ - lib/llm_cost_tracker/ledger/schema/adapter.rb
342
+ - lib/llm_cost_tracker/ledger/schema/calls.rb
343
+ - lib/llm_cost_tracker/ledger/schema/period_totals.rb
344
+ - lib/llm_cost_tracker/ledger/store.rb
345
+ - lib/llm_cost_tracker/ledger/tags/query.rb
346
+ - lib/llm_cost_tracker/ledger/tags/sql.rb
327
347
  - lib/llm_cost_tracker/logging.rb
328
348
  - lib/llm_cost_tracker/middleware/faraday.rb
329
- - lib/llm_cost_tracker/parameter_hash.rb
330
- - lib/llm_cost_tracker/parsed_usage.rb
349
+ - lib/llm_cost_tracker/parsers.rb
331
350
  - lib/llm_cost_tracker/parsers/anthropic.rb
332
351
  - lib/llm_cost_tracker/parsers/base.rb
333
352
  - lib/llm_cost_tracker/parsers/gemini.rb
334
353
  - lib/llm_cost_tracker/parsers/openai.rb
335
354
  - lib/llm_cost_tracker/parsers/openai_compatible.rb
336
355
  - lib/llm_cost_tracker/parsers/openai_usage.rb
337
- - lib/llm_cost_tracker/parsers/registry.rb
338
356
  - lib/llm_cost_tracker/parsers/sse.rb
339
- - lib/llm_cost_tracker/period_grouping.rb
340
- - lib/llm_cost_tracker/period_total.rb
341
- - lib/llm_cost_tracker/price_freshness.rb
342
- - lib/llm_cost_tracker/price_registry.rb
343
- - lib/llm_cost_tracker/price_sync.rb
344
- - lib/llm_cost_tracker/price_sync/fetcher.rb
345
- - lib/llm_cost_tracker/price_sync/registry_diff.rb
346
- - lib/llm_cost_tracker/price_sync/registry_loader.rb
347
- - lib/llm_cost_tracker/price_sync/registry_writer.rb
348
357
  - lib/llm_cost_tracker/prices.json
349
358
  - lib/llm_cost_tracker/pricing.rb
359
+ - lib/llm_cost_tracker/pricing/components.rb
350
360
  - lib/llm_cost_tracker/pricing/effective_prices.rb
351
361
  - lib/llm_cost_tracker/pricing/explainer.rb
352
362
  - lib/llm_cost_tracker/pricing/lookup.rb
363
+ - lib/llm_cost_tracker/pricing/registry.rb
364
+ - lib/llm_cost_tracker/pricing/sync.rb
365
+ - lib/llm_cost_tracker/pricing/sync/fetcher.rb
366
+ - lib/llm_cost_tracker/pricing/sync/registry_diff.rb
367
+ - lib/llm_cost_tracker/pricing/sync/registry_loader.rb
368
+ - lib/llm_cost_tracker/pricing/sync/registry_writer.rb
369
+ - lib/llm_cost_tracker/pricing/unknown.rb
353
370
  - lib/llm_cost_tracker/railtie.rb
354
371
  - lib/llm_cost_tracker/report.rb
355
- - lib/llm_cost_tracker/report_data.rb
356
- - lib/llm_cost_tracker/report_formatter.rb
357
- - lib/llm_cost_tracker/request_url.rb
372
+ - lib/llm_cost_tracker/report/data.rb
373
+ - lib/llm_cost_tracker/report/formatter.rb
358
374
  - lib/llm_cost_tracker/retention.rb
359
- - lib/llm_cost_tracker/storage/active_record_backend.rb
360
- - lib/llm_cost_tracker/storage/active_record_connection_cleanup.rb
361
- - lib/llm_cost_tracker/storage/active_record_inbox.rb
362
- - lib/llm_cost_tracker/storage/active_record_inbox_batch.rb
363
- - lib/llm_cost_tracker/storage/active_record_ingestor.rb
364
- - lib/llm_cost_tracker/storage/active_record_ingestor_lease.rb
365
- - lib/llm_cost_tracker/storage/active_record_period_totals.rb
366
- - lib/llm_cost_tracker/storage/active_record_periods.rb
367
- - lib/llm_cost_tracker/storage/active_record_rollup_batch.rb
368
- - lib/llm_cost_tracker/storage/active_record_rollup_upsert_sql.rb
369
- - lib/llm_cost_tracker/storage/active_record_rollups.rb
370
- - lib/llm_cost_tracker/storage/active_record_store.rb
371
- - lib/llm_cost_tracker/storage/writer.rb
372
- - lib/llm_cost_tracker/stream_capture.rb
373
- - lib/llm_cost_tracker/stream_collector.rb
374
- - lib/llm_cost_tracker/tag_accessors.rb
375
- - lib/llm_cost_tracker/tag_context.rb
376
- - lib/llm_cost_tracker/tag_key.rb
377
- - lib/llm_cost_tracker/tag_query.rb
378
- - lib/llm_cost_tracker/tag_sanitizer.rb
379
- - lib/llm_cost_tracker/tag_sql.rb
380
- - lib/llm_cost_tracker/tags_column.rb
375
+ - lib/llm_cost_tracker/tags/context.rb
376
+ - lib/llm_cost_tracker/tags/key.rb
377
+ - lib/llm_cost_tracker/tags/sanitizer.rb
378
+ - lib/llm_cost_tracker/token_usage.rb
381
379
  - lib/llm_cost_tracker/tracker.rb
382
- - lib/llm_cost_tracker/unknown_pricing.rb
383
- - lib/llm_cost_tracker/usage_breakdown.rb
384
- - lib/llm_cost_tracker/value_helpers.rb
380
+ - lib/llm_cost_tracker/usage_capture.rb
385
381
  - lib/llm_cost_tracker/version.rb
386
382
  - lib/tasks/llm_cost_tracker.rake
387
383
  homepage: https://github.com/sergey-homenko/llm_cost_tracker
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module Dashboard
5
- class DataQualityAggregate
6
- class << self
7
- def call(scope:)
8
- model = scope.klass
9
- expressions = aggregate_expressions(scope, model:)
10
- values = Array(scope.unscope(:order).pick(*expressions.values))
11
-
12
- expressions.keys.zip(values).to_h
13
- end
14
-
15
- private
16
-
17
- def aggregate_expressions(scope, model:)
18
- usage_breakdown_present = model.usage_breakdown_columns?
19
- usage_breakdown_cost_present = model.usage_breakdown_cost_columns?
20
-
21
- expressions = {
22
- total_calls: Arel.sql("COUNT(*)"),
23
- unknown_pricing_count: conditional_count_expression("total_cost IS NULL"),
24
- tagged_calls_count: tagged_calls_expression(model)
25
- }
26
-
27
- if model.latency_column?
28
- expressions[:missing_latency_count] = conditional_count_expression("latency_ms IS NULL")
29
- end
30
- expressions[:streaming_count] = conditional_count_expression("stream") if model.stream_column?
31
- if model.stream_column? && model.usage_source_column?
32
- expressions[:streaming_missing_usage_count] =
33
- conditional_count_expression("stream AND (usage_source = 'unknown' OR usage_source IS NULL)")
34
- end
35
- if model.provider_response_id_column?
36
- expressions[:missing_provider_response_id_count] =
37
- conditional_count_expression("provider_response_id IS NULL OR provider_response_id = ''")
38
- end
39
-
40
- usage_sum_columns(usage_breakdown_present, usage_breakdown_cost_present).each do |column|
41
- expressions[column] = sum_expression(scope, column)
42
- end
43
-
44
- expressions
45
- end
46
-
47
- def usage_sum_columns(usage_breakdown_present, usage_breakdown_cost_present)
48
- columns = %i[input_tokens output_tokens input_cost output_cost]
49
- if usage_breakdown_present
50
- columns += %i[cache_read_input_tokens cache_write_input_tokens hidden_output_tokens]
51
- end
52
- columns += %i[cache_read_input_cost cache_write_input_cost] if usage_breakdown_cost_present
53
- columns
54
- end
55
-
56
- def conditional_count_expression(predicate)
57
- Arel.sql("COALESCE(SUM(CASE WHEN #{predicate} THEN 1 ELSE 0 END), 0)")
58
- end
59
-
60
- def tagged_calls_expression(model)
61
- table = model.quoted_table_name
62
- column = "#{table}.#{model.connection.quote_column_name('tags')}"
63
-
64
- Arel.sql(case
65
- when model.tags_jsonb_column?
66
- "COALESCE(SUM(CASE WHEN #{column} <> '{}'::jsonb THEN 1 ELSE 0 END), 0)"
67
- when model.tags_mysql_json_column?
68
- "COALESCE(SUM(CASE WHEN JSON_LENGTH(#{column}) > 0 THEN 1 ELSE 0 END), 0)"
69
- else
70
- "COALESCE(SUM(CASE WHEN #{column} IS NOT NULL AND #{column} <> '' " \
71
- "AND #{column} <> '{}' THEN 1 ELSE 0 END), 0)"
72
- end)
73
- end
74
-
75
- def sum_expression(scope, column)
76
- Arel.sql("COALESCE(SUM(#{scope.connection.quote_column_name(column)}), 0)")
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- class Pagination
5
- DEFAULT_PER = 50
6
- MAX_PER = 200
7
- MIN_PAGE = 1
8
-
9
- attr_reader :page, :per
10
-
11
- def self.call(params)
12
- params = LlmCostTracker::ParameterHash.with_indifferent_access(params)
13
- new(
14
- page: integer_param(params, :page, default: MIN_PAGE, min: MIN_PAGE),
15
- per: integer_param(params, :per, default: DEFAULT_PER, min: 1, max: MAX_PER)
16
- )
17
- end
18
-
19
- def self.integer_param(params, key, default:, min:, max: nil)
20
- value = Integer(params[key], 10)
21
- value = [value, min].max
22
- value = [value, max].min if max
23
- value
24
- rescue ArgumentError, TypeError
25
- default
26
- end
27
- private_class_method :integer_param
28
-
29
- def initialize(page:, per:)
30
- @page = page
31
- @per = per
32
- freeze
33
- end
34
-
35
- def limit
36
- per
37
- end
38
-
39
- def offset
40
- (page - 1) * per
41
- end
42
-
43
- def prev_page?
44
- page > MIN_PAGE
45
- end
46
-
47
- def next_page?(total_count)
48
- offset + per < total_count.to_i
49
- end
50
-
51
- def total_pages(total_count)
52
- return MIN_PAGE if total_count.to_i <= 0
53
-
54
- [(total_count.to_f / per).ceil, MIN_PAGE].max
55
- end
56
- end
57
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "errors"
4
-
5
- module LlmCostTracker
6
- module ActiveRecordAdapter
7
- MYSQL_ADAPTERS = %w[
8
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
9
- ActiveRecord::ConnectionAdapters::Mysql2Adapter
10
- ActiveRecord::ConnectionAdapters::TrilogyAdapter
11
- ].freeze
12
- POSTGRESQL_ADAPTERS = %w[
13
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
14
- ].freeze
15
- MYSQL_PATTERN = /mysql|trilogy|mariadb/i
16
- POSTGRESQL_PATTERN = /postgres/i
17
-
18
- class << self
19
- def mysql?(value) = adapter_instance?(value, MYSQL_ADAPTERS) || adapter_name(value).match?(MYSQL_PATTERN)
20
-
21
- def postgresql?(value)
22
- adapter_instance?(value, POSTGRESQL_ADAPTERS) || adapter_name(value).match?(POSTGRESQL_PATTERN)
23
- end
24
-
25
- def supported?(value) = mysql?(value) || postgresql?(value)
26
-
27
- def ensure_supported!(value)
28
- return if supported?(value)
29
-
30
- raise Error, "Unsupported database adapter: #{adapter_name(value)}. Use PostgreSQL or MySQL."
31
- end
32
-
33
- private
34
-
35
- def adapter_instance?(value, class_names)
36
- class_names.any? do |class_name|
37
- adapter_class = constantize(class_name)
38
- adapter_class && value.is_a?(adapter_class)
39
- end
40
- end
41
-
42
- def constantize(name)
43
- name.split("::").reduce(Object) { |namespace, part| namespace.const_get(part, false) }
44
- rescue NameError
45
- nil
46
- end
47
-
48
- def adapter_name(value)
49
- value.respond_to?(:adapter_name) ? value.adapter_name.to_s : value.to_s
50
- end
51
- end
52
- end
53
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "storage/active_record_backend"
4
-
5
- module LlmCostTracker
6
- class CaptureVerifier
7
- Check = Data.define(:status, :name, :message)
8
-
9
- class << self
10
- def call = new.checks
11
-
12
- def report(checks = call)
13
- (["LLM Cost Tracker capture verification"] + checks.map { |check| format_check(check) }).join("\n")
14
- end
15
-
16
- def healthy?(checks = call)
17
- checks.none? { |check| check.status == :error }
18
- end
19
-
20
- private
21
-
22
- def format_check(check)
23
- "[#{check.status}] #{check.name}: #{check.message}"
24
- end
25
- end
26
-
27
- def checks
28
- [
29
- enabled_check,
30
- *integration_checks,
31
- *storage_checks
32
- ].compact
33
- end
34
-
35
- private
36
-
37
- def enabled_check
38
- return Check.new(:ok, "tracking", "enabled") if LlmCostTracker.configuration.enabled
39
-
40
- Check.new(:error, "tracking", "disabled; set config.enabled = true before verifying capture")
41
- end
42
-
43
- def integration_checks
44
- enabled = LlmCostTracker.configuration.instrumented_integrations
45
- if enabled.empty?
46
- return [
47
- Check.new(:ok, "sdk integrations", "none enabled; Faraday middleware and manual capture remain available")
48
- ]
49
- end
50
-
51
- LlmCostTracker::Integrations::Registry.checks.map do |check|
52
- Check.new(check.status, "sdk integration #{check.name}", check.message)
53
- end
54
- end
55
-
56
- def storage_checks
57
- LlmCostTracker::Storage::ActiveRecordBackend.verify.map do |check|
58
- Check.new(check.status, check.name, check.message)
59
- end
60
- rescue LlmCostTracker::Error => e
61
- [Check.new(:error, "storage", e.message)]
62
- end
63
- end
64
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- Cost = Data.define(
5
- :input_cost,
6
- :cache_read_input_cost,
7
- :cache_write_input_cost,
8
- :output_cost,
9
- :total_cost,
10
- :currency
11
- )
12
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- class Doctor
5
- class CaptureCheck
6
- def self.call(check_class)
7
- new(check_class).call
8
- end
9
-
10
- def initialize(check_class)
11
- @check_class = check_class
12
- end
13
-
14
- def call
15
- config = LlmCostTracker.configuration
16
- return disabled_check unless config.enabled
17
- return integrations_check(config.instrumented_integrations) if config.instrumented_integrations.any?
18
-
19
- check(:ok, "no SDK integrations enabled; Faraday middleware and manual capture remain available")
20
- end
21
-
22
- private
23
-
24
- attr_reader :check_class
25
-
26
- def disabled_check
27
- check(:warn, "tracking is disabled; set config.enabled = true to record calls")
28
- end
29
-
30
- def integrations_check(integrations)
31
- check(:ok, "SDK integrations enabled: #{integrations.join(', ')}")
32
- end
33
-
34
- def check(status, message)
35
- check_class.new(status, "capture", message)
36
- end
37
- end
38
- end
39
- end
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module EventMetadata
5
- INTERNAL_TAG_KEYS = %w[
6
- cache_read_input_tokens
7
- cache_write_input_tokens
8
- hidden_output_tokens
9
- input_tokens
10
- output_tokens
11
- pricing_mode
12
- provider_response_id
13
- total_tokens
14
- ].freeze
15
-
16
- class << self
17
- def usage_data(input_tokens, output_tokens, metadata)
18
- metadata = metadata.to_h.symbolize_keys
19
- cache_read = first_integer(metadata, :cache_read_input_tokens)
20
- cache_write = first_integer(metadata, :cache_write_input_tokens)
21
- hidden_output = first_integer(metadata, :hidden_output_tokens)
22
- breakdown = UsageBreakdown.build(
23
- input_tokens: input_tokens,
24
- output_tokens: output_tokens,
25
- cache_read_input_tokens: cache_read,
26
- cache_write_input_tokens: cache_write,
27
- hidden_output_tokens: hidden_output
28
- )
29
-
30
- breakdown.to_h.merge(pricing_mode: normalized_pricing_mode(metadata[:pricing_mode])).compact
31
- end
32
-
33
- def tags(metadata)
34
- metadata.reject { |key, _value| INTERNAL_TAG_KEYS.include?(key.to_s) }
35
- end
36
-
37
- private
38
-
39
- def first_integer(metadata, *keys)
40
- keys.each { |key| return metadata[key].to_i unless metadata[key].nil? }
41
- 0
42
- end
43
-
44
- def normalized_pricing_mode(value)
45
- return nil if value.nil?
46
-
47
- mode = value.to_s.strip
48
- mode.empty? || mode == "standard" ? nil : mode
49
- end
50
- end
51
- end
52
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module LlmCostTracker
7
- module Generators
8
- class AddUsageBreakdownGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add usage and cost breakdown columns to llm_api_calls"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_usage_breakdown_to_llm_api_calls.rb.erb",
18
- "db/migrate/add_usage_breakdown_to_llm_api_calls.rb"
19
- )
20
- end
21
-
22
- private
23
-
24
- def migration_version
25
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
26
- end
27
- end
28
- end
29
- end