llm_cost_tracker 0.5.2 → 0.6.0

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +8 -3
  4. data/app/controllers/llm_cost_tracker/calls_controller.rb +35 -21
  5. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +3 -1
  6. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +4 -5
  7. data/docs/architecture.md +28 -0
  8. data/docs/budgets.md +45 -0
  9. data/docs/configuration.md +65 -0
  10. data/docs/cookbook.md +185 -0
  11. data/docs/dashboard-overview.png +0 -0
  12. data/docs/dashboard.md +38 -0
  13. data/docs/extending.md +32 -0
  14. data/docs/operations.md +44 -0
  15. data/docs/pricing.md +94 -0
  16. data/docs/querying.md +36 -0
  17. data/docs/streaming.md +70 -0
  18. data/docs/technical/README.md +10 -0
  19. data/docs/technical/data-flow.md +70 -0
  20. data/docs/technical/extension-points.md +111 -0
  21. data/docs/technical/module-map.md +197 -0
  22. data/docs/technical/operational-notes.md +97 -0
  23. data/docs/upgrading.md +47 -0
  24. data/lib/llm_cost_tracker/active_record_adapter.rb +49 -0
  25. data/lib/llm_cost_tracker/capture_verifier.rb +71 -0
  26. data/lib/llm_cost_tracker/configuration/instrumentation.rb +1 -1
  27. data/lib/llm_cost_tracker/configuration/storage_backend.rb +26 -0
  28. data/lib/llm_cost_tracker/configuration.rb +2 -1
  29. data/lib/llm_cost_tracker/doctor/capture_check.rb +39 -0
  30. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +117 -0
  31. data/lib/llm_cost_tracker/doctor.rb +8 -1
  32. data/lib/llm_cost_tracker/event.rb +1 -0
  33. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +29 -0
  34. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +33 -0
  35. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +14 -6
  36. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -4
  37. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +30 -3
  38. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +1 -1
  39. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +3 -1
  40. data/lib/llm_cost_tracker/inbox_event.rb +9 -0
  41. data/lib/llm_cost_tracker/ingestor_lease.rb +9 -0
  42. data/lib/llm_cost_tracker/integrations/anthropic.rb +41 -2
  43. data/lib/llm_cost_tracker/integrations/openai.rb +66 -2
  44. data/lib/llm_cost_tracker/integrations/registry.rb +33 -3
  45. data/lib/llm_cost_tracker/integrations/stream_tracker.rb +166 -0
  46. data/lib/llm_cost_tracker/llm_api_call.rb +2 -78
  47. data/lib/llm_cost_tracker/llm_api_call_metrics.rb +63 -0
  48. data/lib/llm_cost_tracker/parsers/openai_usage.rb +1 -1
  49. data/lib/llm_cost_tracker/period_grouping.rb +4 -3
  50. data/lib/llm_cost_tracker/pricing/effective_prices.rb +75 -0
  51. data/lib/llm_cost_tracker/pricing/explainer.rb +77 -0
  52. data/lib/llm_cost_tracker/pricing/lookup.rb +143 -0
  53. data/lib/llm_cost_tracker/pricing.rb +25 -108
  54. data/lib/llm_cost_tracker/railtie.rb +1 -0
  55. data/lib/llm_cost_tracker/retention.rb +3 -9
  56. data/lib/llm_cost_tracker/storage/active_record_backend.rb +166 -0
  57. data/lib/llm_cost_tracker/storage/active_record_connection_cleanup.rb +13 -0
  58. data/lib/llm_cost_tracker/storage/active_record_inbox.rb +165 -0
  59. data/lib/llm_cost_tracker/storage/active_record_inbox_batch.rb +92 -0
  60. data/lib/llm_cost_tracker/storage/active_record_ingestor.rb +174 -0
  61. data/lib/llm_cost_tracker/storage/active_record_ingestor_lease.rb +38 -0
  62. data/lib/llm_cost_tracker/storage/active_record_period_totals.rb +84 -0
  63. data/lib/llm_cost_tracker/storage/active_record_periods.rb +31 -0
  64. data/lib/llm_cost_tracker/storage/active_record_rollup_batch.rb +41 -0
  65. data/lib/llm_cost_tracker/storage/active_record_rollup_upsert_sql.rb +42 -0
  66. data/lib/llm_cost_tracker/storage/active_record_rollups.rb +59 -55
  67. data/lib/llm_cost_tracker/storage/active_record_store.rb +68 -9
  68. data/lib/llm_cost_tracker/storage/custom_backend.rb +32 -0
  69. data/lib/llm_cost_tracker/storage/dispatcher.rb +11 -34
  70. data/lib/llm_cost_tracker/storage/log_backend.rb +38 -0
  71. data/lib/llm_cost_tracker/storage/registry.rb +63 -0
  72. data/lib/llm_cost_tracker/stream_collector.rb +18 -7
  73. data/lib/llm_cost_tracker/tag_sql.rb +34 -0
  74. data/lib/llm_cost_tracker/tags_column.rb +7 -1
  75. data/lib/llm_cost_tracker/tracker.rb +3 -0
  76. data/lib/llm_cost_tracker/version.rb +1 -1
  77. data/lib/llm_cost_tracker.rb +39 -1
  78. data/lib/tasks/llm_cost_tracker.rake +49 -0
  79. metadata +47 -2
@@ -31,16 +31,20 @@ require_relative "llm_cost_tracker/unknown_pricing"
31
31
  require_relative "llm_cost_tracker/event_metadata"
32
32
  require_relative "llm_cost_tracker/tag_context"
33
33
  require_relative "llm_cost_tracker/tag_sanitizer"
34
+ require_relative "llm_cost_tracker/active_record_adapter"
34
35
  require_relative "llm_cost_tracker/tags_column"
35
36
  require_relative "llm_cost_tracker/tag_key"
37
+ require_relative "llm_cost_tracker/tag_sql"
36
38
  require_relative "llm_cost_tracker/tag_query"
37
39
  require_relative "llm_cost_tracker/tag_accessors"
40
+ require_relative "llm_cost_tracker/llm_api_call_metrics"
38
41
  require_relative "llm_cost_tracker/tracker"
39
42
  require_relative "llm_cost_tracker/retention"
40
43
  require_relative "llm_cost_tracker/report_data"
41
44
  require_relative "llm_cost_tracker/report_formatter"
42
45
  require_relative "llm_cost_tracker/report"
43
46
  require_relative "llm_cost_tracker/doctor"
47
+ require_relative "llm_cost_tracker/capture_verifier"
44
48
 
45
49
  module LlmCostTracker
46
50
  CONFIGURATION_MUTEX = Monitor.new
@@ -50,6 +54,10 @@ module LlmCostTracker
50
54
  CONFIGURATION_MUTEX.synchronize { @configuration ||= Configuration.new }
51
55
  end
52
56
 
57
+ def configuration_generation
58
+ CONFIGURATION_MUTEX.synchronize { @configuration_generation ||= 0 }
59
+ end
60
+
53
61
  def configure
54
62
  config = CONFIGURATION_MUTEX.synchronize do
55
63
  current = @configuration || Configuration.new
@@ -58,6 +66,7 @@ module LlmCostTracker
58
66
  yield(current)
59
67
  current.openai_compatible_providers = current.openai_compatible_providers.dup
60
68
  current.finalize!
69
+ @configuration_generation = @configuration_generation.to_i + 1
61
70
  current
62
71
  end
63
72
  Integrations.install!
@@ -65,12 +74,39 @@ module LlmCostTracker
65
74
  end
66
75
 
67
76
  def reset_configuration!
68
- CONFIGURATION_MUTEX.synchronize { @configuration = Configuration.new }
77
+ Storage::ActiveRecordInbox.reset! if defined?(Storage::ActiveRecordInbox)
78
+ Storage::ActiveRecordIngestor.shutdown!(drain: false) if defined?(Storage::ActiveRecordIngestor)
79
+ CONFIGURATION_MUTEX.synchronize do
80
+ @configuration = Configuration.new
81
+ @configuration_generation = @configuration_generation.to_i + 1
82
+ end
69
83
  UnknownPricing.reset! if defined?(UnknownPricing)
70
84
  Storage::ActiveRecordStore.reset! if defined?(Storage::ActiveRecordStore)
85
+ Storage::ActiveRecordInbox.reset! if defined?(Storage::ActiveRecordInbox)
86
+ Storage::ActiveRecordIngestor.reset! if defined?(Storage::ActiveRecordIngestor)
71
87
  TagContext.clear! if defined?(TagContext)
72
88
  end
73
89
 
90
+ def flush!(timeout: nil)
91
+ return true unless defined?(Storage::ActiveRecordIngestor)
92
+
93
+ if timeout
94
+ Storage::ActiveRecordIngestor.flush!(timeout: timeout)
95
+ else
96
+ Storage::ActiveRecordIngestor.flush!
97
+ end
98
+ end
99
+
100
+ def shutdown!(timeout: nil, drain: true)
101
+ return true unless defined?(Storage::ActiveRecordIngestor)
102
+
103
+ if timeout
104
+ Storage::ActiveRecordIngestor.shutdown!(timeout: timeout, drain: drain)
105
+ else
106
+ Storage::ActiveRecordIngestor.shutdown!(drain: drain)
107
+ end
108
+ end
109
+
74
110
  def enforce_budget!
75
111
  Tracker.enforce_budget!
76
112
  end
@@ -137,3 +173,5 @@ if defined?(Faraday)
137
173
  llm_cost_tracker: LlmCostTracker::Middleware::Faraday
138
174
  )
139
175
  end
176
+
177
+ at_exit { LlmCostTracker.shutdown!(drain: false) if defined?(LlmCostTracker) }
@@ -7,11 +7,21 @@ namespace :llm_cost_tracker do
7
7
  desc "Check LLM Cost Tracker setup"
8
8
  task :doctor do
9
9
  Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
10
+ require_relative "../llm_cost_tracker"
10
11
  checks = LlmCostTracker::Doctor.call
11
12
  puts LlmCostTracker::Doctor.report(checks)
12
13
  abort("llm_cost_tracker: doctor found setup errors") unless LlmCostTracker::Doctor.healthy?(checks)
13
14
  end
14
15
 
16
+ desc "Verify that LLM Cost Tracker can capture and persist a synthetic event"
17
+ task :verify_capture do
18
+ Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
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)
23
+ end
24
+
15
25
  desc "Print an LLM cost report from ActiveRecord storage"
16
26
  task report: :environment do
17
27
  days = (ENV["DAYS"] || LlmCostTracker::Report::DEFAULT_DAYS).to_i
@@ -74,6 +84,17 @@ namespace :llm_cost_tracker do
74
84
  puts " pricing is up to date" if result.up_to_date
75
85
  abort("llm_cost_tracker: pricing check failed") unless result.up_to_date
76
86
  end
87
+
88
+ desc "Explain how a provider/model price is matched. Use PROVIDER=... MODEL=..."
89
+ task :explain do
90
+ Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
91
+ require_relative "../llm_cost_tracker"
92
+
93
+ explanation = price_explanation_from_env
94
+ puts "llm_cost_tracker: #{explanation.message}"
95
+ print_price_explanation(explanation)
96
+ abort("llm_cost_tracker: price is incomplete or unknown") unless explanation.complete?
97
+ end
77
98
  end
78
99
  end
79
100
  # rubocop:enable Metrics/BlockLength
@@ -95,3 +116,31 @@ def price_refresh_output_path
95
116
  FileUtils.mkdir_p(File.dirname(path))
96
117
  path
97
118
  end
119
+
120
+ def price_explanation_from_env
121
+ provider = ENV["PROVIDER"].to_s.strip
122
+ model = ENV["MODEL"].to_s.strip
123
+ abort("llm_cost_tracker: use PROVIDER=... MODEL=...") if provider.empty? || model.empty?
124
+
125
+ LlmCostTracker::Pricing.explain(
126
+ provider: provider,
127
+ model: model,
128
+ 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
133
+ )
134
+ end
135
+
136
+ def print_price_explanation(explanation)
137
+ return unless explanation.matched?
138
+
139
+ puts " source: #{explanation.source}"
140
+ puts " matched_key: #{explanation.matched_key}"
141
+ puts " matched_by: #{explanation.matched_by}"
142
+ puts " pricing_mode: #{explanation.pricing_mode || 'standard'}"
143
+ explanation.effective_prices.each do |key, value|
144
+ puts " #{key}: #{value.nil? ? 'missing' : value}"
145
+ end
146
+ end
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.5.2
4
+ version: 0.6.0
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-27 00:00:00.000000000 Z
11
+ date: 2026-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -283,18 +283,41 @@ files:
283
283
  - app/views/llm_cost_tracker/tags/index.html.erb
284
284
  - app/views/llm_cost_tracker/tags/show.html.erb
285
285
  - config/routes.rb
286
+ - docs/architecture.md
287
+ - docs/budgets.md
288
+ - docs/configuration.md
289
+ - docs/cookbook.md
290
+ - docs/dashboard-overview.png
291
+ - docs/dashboard.md
292
+ - docs/extending.md
293
+ - docs/operations.md
294
+ - docs/pricing.md
295
+ - docs/querying.md
296
+ - docs/streaming.md
297
+ - docs/technical/README.md
298
+ - docs/technical/data-flow.md
299
+ - docs/technical/extension-points.md
300
+ - docs/technical/module-map.md
301
+ - docs/technical/operational-notes.md
302
+ - docs/upgrading.md
286
303
  - lib/llm_cost_tracker.rb
304
+ - lib/llm_cost_tracker/active_record_adapter.rb
287
305
  - lib/llm_cost_tracker/assets.rb
288
306
  - lib/llm_cost_tracker/budget.rb
307
+ - lib/llm_cost_tracker/capture_verifier.rb
289
308
  - lib/llm_cost_tracker/configuration.rb
290
309
  - lib/llm_cost_tracker/configuration/instrumentation.rb
310
+ - lib/llm_cost_tracker/configuration/storage_backend.rb
291
311
  - lib/llm_cost_tracker/cost.rb
292
312
  - lib/llm_cost_tracker/doctor.rb
313
+ - lib/llm_cost_tracker/doctor/capture_check.rb
314
+ - lib/llm_cost_tracker/doctor/ingestion_check.rb
293
315
  - lib/llm_cost_tracker/engine.rb
294
316
  - lib/llm_cost_tracker/engine_compatibility.rb
295
317
  - lib/llm_cost_tracker/errors.rb
296
318
  - lib/llm_cost_tracker/event.rb
297
319
  - lib/llm_cost_tracker/event_metadata.rb
320
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb
298
321
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
299
322
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb
300
323
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb
@@ -302,6 +325,7 @@ files:
302
325
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb
303
326
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
304
327
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
328
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb
305
329
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb
306
330
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb
307
331
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb
@@ -313,13 +337,17 @@ files:
313
337
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb
314
338
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb
315
339
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb
340
+ - lib/llm_cost_tracker/inbox_event.rb
341
+ - lib/llm_cost_tracker/ingestor_lease.rb
316
342
  - lib/llm_cost_tracker/integrations/anthropic.rb
317
343
  - lib/llm_cost_tracker/integrations/base.rb
318
344
  - lib/llm_cost_tracker/integrations/object_reader.rb
319
345
  - lib/llm_cost_tracker/integrations/openai.rb
320
346
  - lib/llm_cost_tracker/integrations/registry.rb
321
347
  - lib/llm_cost_tracker/integrations/ruby_llm.rb
348
+ - lib/llm_cost_tracker/integrations/stream_tracker.rb
322
349
  - lib/llm_cost_tracker/llm_api_call.rb
350
+ - lib/llm_cost_tracker/llm_api_call_metrics.rb
323
351
  - lib/llm_cost_tracker/logging.rb
324
352
  - lib/llm_cost_tracker/middleware/faraday.rb
325
353
  - lib/llm_cost_tracker/parameter_hash.rb
@@ -343,15 +371,31 @@ files:
343
371
  - lib/llm_cost_tracker/price_sync/registry_writer.rb
344
372
  - lib/llm_cost_tracker/prices.json
345
373
  - lib/llm_cost_tracker/pricing.rb
374
+ - lib/llm_cost_tracker/pricing/effective_prices.rb
375
+ - lib/llm_cost_tracker/pricing/explainer.rb
376
+ - lib/llm_cost_tracker/pricing/lookup.rb
346
377
  - lib/llm_cost_tracker/railtie.rb
347
378
  - lib/llm_cost_tracker/report.rb
348
379
  - lib/llm_cost_tracker/report_data.rb
349
380
  - lib/llm_cost_tracker/report_formatter.rb
350
381
  - lib/llm_cost_tracker/request_url.rb
351
382
  - lib/llm_cost_tracker/retention.rb
383
+ - lib/llm_cost_tracker/storage/active_record_backend.rb
384
+ - lib/llm_cost_tracker/storage/active_record_connection_cleanup.rb
385
+ - lib/llm_cost_tracker/storage/active_record_inbox.rb
386
+ - lib/llm_cost_tracker/storage/active_record_inbox_batch.rb
387
+ - lib/llm_cost_tracker/storage/active_record_ingestor.rb
388
+ - lib/llm_cost_tracker/storage/active_record_ingestor_lease.rb
389
+ - lib/llm_cost_tracker/storage/active_record_period_totals.rb
390
+ - lib/llm_cost_tracker/storage/active_record_periods.rb
391
+ - lib/llm_cost_tracker/storage/active_record_rollup_batch.rb
392
+ - lib/llm_cost_tracker/storage/active_record_rollup_upsert_sql.rb
352
393
  - lib/llm_cost_tracker/storage/active_record_rollups.rb
353
394
  - lib/llm_cost_tracker/storage/active_record_store.rb
395
+ - lib/llm_cost_tracker/storage/custom_backend.rb
354
396
  - lib/llm_cost_tracker/storage/dispatcher.rb
397
+ - lib/llm_cost_tracker/storage/log_backend.rb
398
+ - lib/llm_cost_tracker/storage/registry.rb
355
399
  - lib/llm_cost_tracker/stream_capture.rb
356
400
  - lib/llm_cost_tracker/stream_collector.rb
357
401
  - lib/llm_cost_tracker/tag_accessors.rb
@@ -359,6 +403,7 @@ files:
359
403
  - lib/llm_cost_tracker/tag_key.rb
360
404
  - lib/llm_cost_tracker/tag_query.rb
361
405
  - lib/llm_cost_tracker/tag_sanitizer.rb
406
+ - lib/llm_cost_tracker/tag_sql.rb
362
407
  - lib/llm_cost_tracker/tags_column.rb
363
408
  - lib/llm_cost_tracker/tracker.rb
364
409
  - lib/llm_cost_tracker/unknown_pricing.rb