llm_cost_tracker 0.3.2 → 0.4.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/README.md +34 -14
  4. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +101 -19
  5. data/app/views/llm_cost_tracker/data_quality/index.html.erb +65 -0
  6. data/lib/llm_cost_tracker/budget.rb +85 -21
  7. data/lib/llm_cost_tracker/configuration.rb +4 -0
  8. data/lib/llm_cost_tracker/cost.rb +1 -2
  9. data/lib/llm_cost_tracker/errors.rb +22 -3
  10. data/lib/llm_cost_tracker/event.rb +4 -0
  11. data/lib/llm_cost_tracker/event_metadata.rb +21 -15
  12. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +29 -0
  13. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb +29 -0
  14. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +66 -0
  15. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb +29 -0
  16. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +15 -0
  17. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -1
  18. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +11 -3
  19. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +1 -0
  20. data/lib/llm_cost_tracker/middleware/faraday.rb +27 -9
  21. data/lib/llm_cost_tracker/parsed_usage.rb +16 -7
  22. data/lib/llm_cost_tracker/parsers/anthropic.rb +7 -6
  23. data/lib/llm_cost_tracker/parsers/base.rb +2 -1
  24. data/lib/llm_cost_tracker/parsers/gemini.rb +5 -2
  25. data/lib/llm_cost_tracker/parsers/openai_usage.rb +18 -5
  26. data/lib/llm_cost_tracker/period_total.rb +9 -0
  27. data/lib/llm_cost_tracker/price_registry.rb +14 -4
  28. data/lib/llm_cost_tracker/price_sync/merger.rb +1 -1
  29. data/lib/llm_cost_tracker/price_sync/raw_price.rb +3 -5
  30. data/lib/llm_cost_tracker/price_sync/sources/litellm.rb +2 -3
  31. data/lib/llm_cost_tracker/price_sync/sources/open_router.rb +2 -3
  32. data/lib/llm_cost_tracker/prices.json +30 -30
  33. data/lib/llm_cost_tracker/pricing.rb +44 -32
  34. data/lib/llm_cost_tracker/railtie.rb +2 -0
  35. data/lib/llm_cost_tracker/storage/active_record_rollups.rb +122 -0
  36. data/lib/llm_cost_tracker/storage/active_record_store.rb +38 -13
  37. data/lib/llm_cost_tracker/stream_collector.rb +5 -3
  38. data/lib/llm_cost_tracker/tags_column.rb +19 -0
  39. data/lib/llm_cost_tracker/tracker.rb +58 -32
  40. data/lib/llm_cost_tracker/unknown_pricing.rb +14 -0
  41. data/lib/llm_cost_tracker/usage_breakdown.rb +30 -0
  42. data/lib/llm_cost_tracker/version.rb +1 -1
  43. data/lib/llm_cost_tracker.rb +12 -3
  44. metadata +10 -4
  45. data/llm_cost_tracker.gemspec +0 -50
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
4
+
3
5
  require_relative "logging"
4
6
 
5
7
  module LlmCostTracker
6
8
  class UnknownPricing
9
+ MUTEX = Monitor.new
10
+
7
11
  class << self
8
12
  def handle!(model)
9
13
  model = normalized_model_name(model)
@@ -18,6 +22,10 @@ module LlmCostTracker
18
22
  end
19
23
  end
20
24
 
25
+ def reset!
26
+ MUTEX.synchronize { @warned_models = Set.new }
27
+ end
28
+
21
29
  private
22
30
 
23
31
  def normalized_model_name(model)
@@ -25,6 +33,12 @@ module LlmCostTracker
25
33
  end
26
34
 
27
35
  def warn_missing(model)
36
+ should_warn = MUTEX.synchronize do
37
+ @warned_models ||= Set.new
38
+ @warned_models.add?(model)
39
+ end
40
+ return unless should_warn
41
+
28
42
  Logging.warn(
29
43
  "No pricing configured for model #{model.inspect}. " \
30
44
  "Cost and budget guardrails will be skipped for this event. " \
@@ -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.2"
4
+ VERSION = "0.4.0"
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"
@@ -60,6 +61,8 @@ module LlmCostTracker
60
61
 
61
62
  def reset_configuration!
62
63
  CONFIGURATION_MUTEX.synchronize { @configuration = Configuration.new }
64
+ UnknownPricing.reset! if defined?(UnknownPricing)
65
+ Storage::ActiveRecordStore.reset! if defined?(Storage::ActiveRecordStore)
63
66
  end
64
67
 
65
68
  def enforce_budget!
@@ -67,7 +70,7 @@ module LlmCostTracker
67
70
  end
68
71
 
69
72
  def track(provider:, model:, input_tokens:, output_tokens:, latency_ms: nil, stream: false, usage_source: :manual,
70
- enforce_budget: false, provider_response_id: nil, **metadata)
73
+ enforce_budget: false, provider_response_id: nil, pricing_mode: nil, **metadata)
71
74
  enforce_budget! if enforce_budget
72
75
  Tracker.record(
73
76
  provider: provider.to_s,
@@ -78,11 +81,13 @@ module LlmCostTracker
78
81
  stream: stream,
79
82
  usage_source: usage_source,
80
83
  provider_response_id: provider_response_id,
84
+ pricing_mode: pricing_mode,
81
85
  metadata: metadata
82
86
  )
83
87
  end
84
88
 
85
- 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)
86
91
  require_relative "llm_cost_tracker/stream_collector"
87
92
  enforce_budget! if enforce_budget
88
93
  collector = StreamCollector.new(
@@ -90,6 +95,7 @@ module LlmCostTracker
90
95
  model: model,
91
96
  latency_ms: latency_ms,
92
97
  provider_response_id: provider_response_id,
98
+ pricing_mode: pricing_mode,
93
99
  metadata: metadata
94
100
  )
95
101
  yield collector
@@ -105,7 +111,10 @@ module LlmCostTracker
105
111
  return unless config.budget_exceeded_behavior == :block_requests
106
112
  return if config.active_record?
107
113
 
108
- 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
+ )
109
118
  end
110
119
  end
111
120
  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.3.2
4
+ version: 0.4.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-22 00:00:00.000000000 Z
11
+ date: 2026-04-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -278,13 +278,17 @@ files:
278
278
  - lib/llm_cost_tracker/event.rb
279
279
  - lib/llm_cost_tracker/event_metadata.rb
280
280
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
281
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb
281
282
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb
282
283
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb
284
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb
283
285
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
284
286
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
285
287
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb
288
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb
286
289
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb
287
290
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb
291
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb
288
292
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
289
293
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
290
294
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb
@@ -306,6 +310,7 @@ files:
306
310
  - lib/llm_cost_tracker/parsers/registry.rb
307
311
  - lib/llm_cost_tracker/parsers/sse.rb
308
312
  - lib/llm_cost_tracker/period_grouping.rb
313
+ - lib/llm_cost_tracker/period_total.rb
309
314
  - lib/llm_cost_tracker/price_registry.rb
310
315
  - lib/llm_cost_tracker/price_sync.rb
311
316
  - lib/llm_cost_tracker/price_sync/fetcher.rb
@@ -327,6 +332,7 @@ files:
327
332
  - lib/llm_cost_tracker/report_data.rb
328
333
  - lib/llm_cost_tracker/report_formatter.rb
329
334
  - lib/llm_cost_tracker/retention.rb
335
+ - lib/llm_cost_tracker/storage/active_record_rollups.rb
330
336
  - lib/llm_cost_tracker/storage/active_record_store.rb
331
337
  - lib/llm_cost_tracker/stream_collector.rb
332
338
  - lib/llm_cost_tracker/tag_accessors.rb
@@ -335,10 +341,10 @@ files:
335
341
  - lib/llm_cost_tracker/tags_column.rb
336
342
  - lib/llm_cost_tracker/tracker.rb
337
343
  - lib/llm_cost_tracker/unknown_pricing.rb
344
+ - lib/llm_cost_tracker/usage_breakdown.rb
338
345
  - lib/llm_cost_tracker/value_helpers.rb
339
346
  - lib/llm_cost_tracker/version.rb
340
347
  - lib/tasks/llm_cost_tracker.rake
341
- - llm_cost_tracker.gemspec
342
348
  homepage: https://github.com/sergey-homenko/llm_cost_tracker
343
349
  licenses:
344
350
  - MIT
@@ -363,7 +369,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
363
369
  - !ruby/object:Gem::Version
364
370
  version: '0'
365
371
  requirements: []
366
- rubygems_version: 3.5.9
372
+ rubygems_version: 3.5.22
367
373
  signing_key:
368
374
  specification_version: 4
369
375
  summary: Self-hosted LLM usage and cost tracking for Ruby and Rails
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/llm_cost_tracker/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "llm_cost_tracker"
7
- spec.version = LlmCostTracker::VERSION
8
- spec.authors = ["Sergii Khomenko"]
9
- spec.email = ["sergey@mm.st"]
10
-
11
- spec.summary = "Self-hosted LLM usage and cost tracking for Ruby and Rails"
12
- spec.description = "Tracks token usage, latency, and estimated costs for OpenAI, Anthropic, " \
13
- "Google Gemini, OpenRouter, DeepSeek, and OpenAI-compatible APIs. " \
14
- "Works through Faraday middleware or explicit track/track_stream helpers, " \
15
- "with ActiveRecord storage, tag-based attribution, price sync tasks, " \
16
- "and budget guardrails."
17
- spec.homepage = "https://github.com/sergey-homenko/llm_cost_tracker"
18
- spec.license = "MIT"
19
-
20
- spec.required_ruby_version = ">= 3.3.0"
21
-
22
- spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
23
- spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
24
- spec.metadata["source_code_uri"] = spec.homepage
25
- spec.metadata["documentation_uri"] = "#{spec.homepage}#readme"
26
- spec.metadata["rubygems_mfa_required"] = "true"
27
-
28
- spec.files = Dir.chdir(__dir__) do
29
- `git ls-files -z`.split("\x0").reject do |f|
30
- (File.expand_path(f) == __FILE__) ||
31
- f.start_with?("bin/", "docs/", "test/", "spec/", ".git", ".github", "gemfiles/", ".rubocop", "Gemfile")
32
- end
33
- end
34
-
35
- spec.require_paths = ["lib"]
36
-
37
- spec.add_dependency "activesupport", ">= 7.1", "< 9.0"
38
- spec.add_dependency "csv", "~> 3.0"
39
- spec.add_dependency "faraday", ">= 2.0", "< 3.0"
40
-
41
- spec.add_development_dependency "activerecord", ">= 7.1", "< 9.0"
42
- spec.add_development_dependency "railties", ">= 7.1", "< 9.0"
43
- spec.add_development_dependency "rake", "~> 13.0"
44
- spec.add_development_dependency "rspec", "~> 3.0"
45
- spec.add_development_dependency "rubocop", "~> 1.0"
46
- spec.add_development_dependency "simplecov", "~> 0.22"
47
- spec.add_development_dependency "simplecov-lcov", "~> 0.8"
48
- spec.add_development_dependency "sqlite3", ">= 1.4", "< 3.0"
49
- spec.add_development_dependency "webmock", "~> 3.0"
50
- end