llm_cost_tracker 0.2.0.alpha2 → 0.3.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -1
  3. data/README.md +114 -70
  4. data/Rakefile +2 -0
  5. data/app/assets/llm_cost_tracker/application.css +760 -0
  6. data/app/controllers/llm_cost_tracker/application_controller.rb +1 -7
  7. data/app/controllers/llm_cost_tracker/assets_controller.rb +12 -0
  8. data/app/controllers/llm_cost_tracker/calls_controller.rb +29 -12
  9. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +5 -1
  10. data/app/helpers/llm_cost_tracker/application_helper.rb +46 -5
  11. data/app/helpers/llm_cost_tracker/chart_helper.rb +133 -0
  12. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +47 -0
  13. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +34 -0
  14. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +58 -0
  15. data/app/helpers/llm_cost_tracker/pagination_helper.rb +18 -0
  16. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +16 -1
  17. data/app/services/llm_cost_tracker/dashboard/filter.rb +22 -3
  18. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +16 -1
  19. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +79 -0
  20. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +19 -46
  21. data/app/services/llm_cost_tracker/dashboard/top_models.rb +17 -8
  22. data/app/services/llm_cost_tracker/pagination.rb +6 -0
  23. data/app/views/layouts/llm_cost_tracker/application.html.erb +35 -333
  24. data/app/views/llm_cost_tracker/calls/index.html.erb +116 -74
  25. data/app/views/llm_cost_tracker/calls/show.html.erb +58 -1
  26. data/app/views/llm_cost_tracker/dashboard/index.html.erb +211 -111
  27. data/app/views/llm_cost_tracker/data_quality/index.html.erb +224 -78
  28. data/app/views/llm_cost_tracker/errors/database.html.erb +3 -3
  29. data/app/views/llm_cost_tracker/errors/invalid_filter.html.erb +3 -3
  30. data/app/views/llm_cost_tracker/errors/not_found.html.erb +3 -3
  31. data/app/views/llm_cost_tracker/models/index.html.erb +66 -58
  32. data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +16 -0
  33. data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +23 -0
  34. data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +18 -0
  35. data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +15 -0
  36. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +3 -2
  37. data/app/views/llm_cost_tracker/tags/index.html.erb +55 -12
  38. data/app/views/llm_cost_tracker/tags/show.html.erb +88 -39
  39. data/config/routes.rb +3 -0
  40. data/lib/llm_cost_tracker/assets.rb +19 -0
  41. data/lib/llm_cost_tracker/configuration.rb +78 -42
  42. data/lib/llm_cost_tracker/engine.rb +2 -0
  43. data/lib/llm_cost_tracker/event.rb +2 -0
  44. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +29 -0
  45. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +25 -0
  46. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +4 -0
  47. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +8 -1
  48. data/lib/llm_cost_tracker/llm_api_call.rb +9 -1
  49. data/lib/llm_cost_tracker/middleware/faraday.rb +57 -9
  50. data/lib/llm_cost_tracker/parsed_usage.rb +7 -3
  51. data/lib/llm_cost_tracker/parsers/anthropic.rb +79 -1
  52. data/lib/llm_cost_tracker/parsers/base.rb +17 -5
  53. data/lib/llm_cost_tracker/parsers/gemini.rb +59 -6
  54. data/lib/llm_cost_tracker/parsers/openai.rb +8 -0
  55. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +8 -0
  56. data/lib/llm_cost_tracker/parsers/openai_usage.rb +55 -1
  57. data/lib/llm_cost_tracker/parsers/registry.rb +15 -3
  58. data/lib/llm_cost_tracker/parsers/sse.rb +81 -0
  59. data/lib/llm_cost_tracker/price_registry.rb +18 -7
  60. data/lib/llm_cost_tracker/price_sync/fetcher.rb +72 -0
  61. data/lib/llm_cost_tracker/price_sync/merger.rb +72 -0
  62. data/lib/llm_cost_tracker/price_sync/model_catalog.rb +77 -0
  63. data/lib/llm_cost_tracker/price_sync/raw_price.rb +35 -0
  64. data/lib/llm_cost_tracker/price_sync/source.rb +29 -0
  65. data/lib/llm_cost_tracker/price_sync/source_result.rb +7 -0
  66. data/lib/llm_cost_tracker/price_sync/sources/litellm.rb +91 -0
  67. data/lib/llm_cost_tracker/price_sync/sources/open_router.rb +94 -0
  68. data/lib/llm_cost_tracker/price_sync/validator.rb +66 -0
  69. data/lib/llm_cost_tracker/price_sync.rb +310 -0
  70. data/lib/llm_cost_tracker/pricing.rb +19 -6
  71. data/lib/llm_cost_tracker/retention.rb +34 -0
  72. data/lib/llm_cost_tracker/storage/active_record_store.rb +3 -1
  73. data/lib/llm_cost_tracker/stream_collector.rb +158 -0
  74. data/lib/llm_cost_tracker/tag_query.rb +7 -2
  75. data/lib/llm_cost_tracker/tags_column.rb +21 -1
  76. data/lib/llm_cost_tracker/tracker.rb +15 -12
  77. data/lib/llm_cost_tracker/value_helpers.rb +40 -0
  78. data/lib/llm_cost_tracker/version.rb +1 -1
  79. data/lib/llm_cost_tracker.rb +51 -29
  80. data/lib/tasks/llm_cost_tracker.rake +124 -0
  81. data/llm_cost_tracker.gemspec +9 -8
  82. metadata +40 -12
  83. data/PLAN_0.2.md +0 -488
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LlmCostTracker
4
+ module ValueHelpers
5
+ class << self
6
+ def deep_dup(value)
7
+ case value
8
+ when Hash
9
+ value.each_with_object({}) do |(key, nested_value), duplicated|
10
+ duplicated[deep_dup(key)] = deep_dup(nested_value)
11
+ end
12
+ when Array
13
+ value.map { |nested_value| deep_dup(nested_value) }
14
+ when String
15
+ value.dup
16
+ else
17
+ value
18
+ end
19
+ end
20
+
21
+ def deep_freeze(value)
22
+ case value
23
+ when Hash
24
+ value.each do |key, nested_value|
25
+ deep_freeze(key)
26
+ deep_freeze(nested_value)
27
+ end
28
+ value.frozen? ? value : value.freeze
29
+ when Array
30
+ value.each { |nested_value| deep_freeze(nested_value) }
31
+ value.frozen? ? value : value.freeze
32
+ when String
33
+ value.frozen? ? value : value.freeze
34
+ else
35
+ value
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmCostTracker
4
- VERSION = "0.2.0.alpha2"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_support"
4
4
  require "active_support/notifications"
5
+ require "monitor"
5
6
 
6
7
  require_relative "llm_cost_tracker/version"
7
8
  require_relative "llm_cost_tracker/configuration"
@@ -11,6 +12,7 @@ require_relative "llm_cost_tracker/cost"
11
12
  require_relative "llm_cost_tracker/event"
12
13
  require_relative "llm_cost_tracker/parsed_usage"
13
14
  require_relative "llm_cost_tracker/price_registry"
15
+ require_relative "llm_cost_tracker/price_sync"
14
16
  require_relative "llm_cost_tracker/pricing"
15
17
  require_relative "llm_cost_tracker/parsers/base"
16
18
  require_relative "llm_cost_tracker/parsers/openai_usage"
@@ -18,6 +20,7 @@ require_relative "llm_cost_tracker/parsers/openai"
18
20
  require_relative "llm_cost_tracker/parsers/openai_compatible"
19
21
  require_relative "llm_cost_tracker/parsers/anthropic"
20
22
  require_relative "llm_cost_tracker/parsers/gemini"
23
+ require_relative "llm_cost_tracker/parsers/sse"
21
24
  require_relative "llm_cost_tracker/parsers/registry"
22
25
  require_relative "llm_cost_tracker/middleware/faraday"
23
26
  require_relative "llm_cost_tracker/budget"
@@ -28,16 +31,17 @@ require_relative "llm_cost_tracker/tag_key"
28
31
  require_relative "llm_cost_tracker/tag_query"
29
32
  require_relative "llm_cost_tracker/tag_accessors"
30
33
  require_relative "llm_cost_tracker/tracker"
34
+ require_relative "llm_cost_tracker/retention"
31
35
  require_relative "llm_cost_tracker/report_data"
32
36
  require_relative "llm_cost_tracker/report_formatter"
33
37
  require_relative "llm_cost_tracker/report"
34
38
 
35
39
  module LlmCostTracker
36
- class << self
37
- attr_writer :configuration
40
+ CONFIGURATION_MUTEX = Monitor.new
38
41
 
42
+ class << self
39
43
  def configuration
40
- @configuration ||= Configuration.new
44
+ CONFIGURATION_MUTEX.synchronize { @configuration ||= Configuration.new }
41
45
  end
42
46
 
43
47
  # Configure the gem once during application boot.
@@ -45,49 +49,67 @@ module LlmCostTracker
45
49
  # @yieldparam configuration [LlmCostTracker::Configuration]
46
50
  # @return [void]
47
51
  def configure
48
- yield(configuration)
49
- configuration.normalize_openai_compatible_providers!
50
- warn_for_configuration!
52
+ config = CONFIGURATION_MUTEX.synchronize do
53
+ current = @configuration || Configuration.new
54
+ current = current.dup_for_configuration if current.finalized?
55
+ @configuration = current
56
+ yield(current)
57
+ current.normalize_openai_compatible_providers!
58
+ current.finalize!
59
+ current
60
+ end
61
+ warn_for_configuration!(config)
51
62
  end
52
63
 
53
64
  def reset_configuration!
54
- @configuration = Configuration.new
65
+ CONFIGURATION_MUTEX.synchronize { @configuration = Configuration.new }
55
66
  end
56
67
 
57
- # Track an LLM request manually for non-Faraday clients.
58
- #
59
- # LlmCostTracker.track(
60
- # provider: :openai,
61
- # model: "gpt-4o",
62
- # input_tokens: 150,
63
- # output_tokens: 50,
64
- # feature: "chat",
65
- # user_id: current_user.id
66
- # )
67
- #
68
- # @param provider [String, Symbol] Provider name, such as :openai or :anthropic.
69
- # @param model [String] Provider model identifier.
70
- # @param input_tokens [Integer] Billed input token count.
71
- # @param output_tokens [Integer] Billed output token count.
72
- # @param latency_ms [Integer, nil] Optional request latency in milliseconds.
73
- # @param metadata [Hash] Attribution tags and provider-specific usage metadata.
74
- # @return [LlmCostTracker::Event] The tracked event.
75
- def track(provider:, model:, input_tokens:, output_tokens:, latency_ms: nil, **metadata)
68
+ def enforce_budget!
69
+ Tracker.enforce_budget!
70
+ end
71
+
72
+ def track(provider:, model:, input_tokens:, output_tokens:, **options)
73
+ latency_ms = options.delete(:latency_ms)
74
+ stream = options.key?(:stream) ? options.delete(:stream) : false
75
+ usage_source = options.key?(:usage_source) ? options.delete(:usage_source) : :manual
76
+ enforce_budget = options.key?(:enforce_budget) ? options.delete(:enforce_budget) : false
77
+ metadata = options
78
+
79
+ enforce_budget! if enforce_budget
76
80
  Tracker.record(
77
81
  provider: provider.to_s,
78
82
  model: model,
79
83
  input_tokens: input_tokens,
80
84
  output_tokens: output_tokens,
81
85
  latency_ms: latency_ms,
86
+ stream: stream,
87
+ usage_source: usage_source,
88
+ metadata: metadata
89
+ )
90
+ end
91
+
92
+ def track_stream(provider:, model:, latency_ms: nil, enforce_budget: false, **metadata)
93
+ require_relative "llm_cost_tracker/stream_collector"
94
+ enforce_budget! if enforce_budget
95
+ collector = StreamCollector.new(
96
+ provider: provider.to_s,
97
+ model: model,
98
+ latency_ms: latency_ms,
82
99
  metadata: metadata
83
100
  )
101
+ yield collector
102
+ collector.finish!
103
+ rescue StandardError
104
+ collector&.finish!(errored: true)
105
+ raise
84
106
  end
85
107
 
86
108
  private
87
109
 
88
- def warn_for_configuration!
89
- return unless configuration.budget_exceeded_behavior == :block_requests
90
- return if configuration.active_record?
110
+ def warn_for_configuration!(config = configuration)
111
+ return unless config.budget_exceeded_behavior == :block_requests
112
+ return if config.active_record?
91
113
 
92
114
  Logging.warn(":block_requests requires storage_backend = :active_record; preflight blocking will be skipped.")
93
115
  end
@@ -1,9 +1,133 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Metrics/BlockLength
3
4
  namespace :llm_cost_tracker do
4
5
  desc "Print an LLM cost report from ActiveRecord storage"
5
6
  task report: :environment do
6
7
  days = (ENV["DAYS"] || LlmCostTracker::Report::DEFAULT_DAYS).to_i
7
8
  puts LlmCostTracker::Report.generate(days: days)
8
9
  end
10
+
11
+ desc "Delete llm_api_calls older than DAYS (default: 90). Use BATCH_SIZE=N to tune."
12
+ task prune: :environment do
13
+ days = (ENV["DAYS"] || 90).to_i
14
+ batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Retention::DEFAULT_BATCH_SIZE).to_i
15
+ deleted = LlmCostTracker::Retention.prune(older_than: days, batch_size: batch_size)
16
+ puts "llm_cost_tracker: pruned #{deleted} calls older than #{days} days"
17
+ end
18
+
19
+ namespace :prices do
20
+ desc(
21
+ "Sync built-in pricing data from LiteLLM/OpenRouter JSON sources. " \
22
+ "Use PREVIEW=1 to preview, STRICT=1 to fail on provider errors, " \
23
+ "or OUTPUT=path/to/file.json."
24
+ )
25
+ task :sync do
26
+ Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
27
+ require_relative "../llm_cost_tracker"
28
+
29
+ output_path = ENV["OUTPUT"] || LlmCostTracker.configuration.prices_file || LlmCostTracker::PriceSync::DEFAULT_OUTPUT_PATH
30
+ strict = ENV["STRICT"] == "1" || ARGV.include?("--strict")
31
+ result = LlmCostTracker::PriceSync.sync(
32
+ path: output_path,
33
+ preview: ENV["PREVIEW"] == "1",
34
+ strict: strict
35
+ )
36
+
37
+ action = if ENV["PREVIEW"] == "1"
38
+ "previewed"
39
+ elsif result.written
40
+ "updated"
41
+ else
42
+ "kept"
43
+ end
44
+
45
+ puts "llm_cost_tracker: #{action} pricing file #{result.path}"
46
+ print_source_usage(result.sources_used)
47
+ print_changes(result.changes)
48
+ print_discrepancies(result.discrepancies)
49
+ print_issues("validator rejected", result.rejected)
50
+ print_issues("validator flagged", result.flagged)
51
+ print_models("orphaned models (no JSON source match)", result.orphaned_models)
52
+ print_failures(result.failed_sources, heading: "source failures (kept existing values)")
53
+ end
54
+
55
+ desc "Compare the current pricing snapshot with LiteLLM/OpenRouter JSON sources and exit non-zero on drift."
56
+ task :check do
57
+ Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
58
+ require_relative "../llm_cost_tracker"
59
+
60
+ output_path = ENV["OUTPUT"] || LlmCostTracker.configuration.prices_file || LlmCostTracker::PriceSync::DEFAULT_OUTPUT_PATH
61
+ result = LlmCostTracker::PriceSync.check(path: output_path)
62
+
63
+ puts "llm_cost_tracker: checked pricing file #{result.path}"
64
+ print_source_usage(result.sources_used)
65
+ print_changes(result.changes)
66
+ print_discrepancies(result.discrepancies)
67
+ print_issues("validator rejected", result.rejected)
68
+ print_issues("validator flagged", result.flagged)
69
+ print_models("orphaned models (no JSON source match)", result.orphaned_models)
70
+ print_failures(result.failed_sources, heading: "source failures")
71
+ puts " pricing is up to date" if result.up_to_date
72
+ abort("llm_cost_tracker: pricing check failed") unless result.up_to_date
73
+ end
74
+ end
75
+ end
76
+ # rubocop:enable Metrics/BlockLength
77
+
78
+ def print_source_usage(sources_used)
79
+ return if sources_used.empty?
80
+
81
+ puts " sources used:"
82
+ sources_used.each do |source, usage|
83
+ version = usage.source_version ? ", version=#{usage.source_version.inspect}" : ""
84
+ puts " - #{source} (#{usage.prices_count} prices#{version})"
85
+ end
86
+ end
87
+
88
+ def print_changes(changes)
89
+ puts " changed models: #{changes.size}"
90
+ return if changes.empty?
91
+
92
+ changes.each do |model, fields|
93
+ puts " - #{model}"
94
+ fields.each do |field, values|
95
+ puts " #{field}: #{values['from'].inspect} -> #{values['to'].inspect}"
96
+ end
97
+ end
98
+ end
99
+
100
+ def print_discrepancies(discrepancies)
101
+ return if discrepancies.empty?
102
+
103
+ puts " source discrepancies: #{discrepancies.size}"
104
+ discrepancies.each do |issue|
105
+ formatted = issue.values.map { |source, value| "#{source}=#{value.inspect}" }.join(", ")
106
+ puts " - #{issue.model} #{issue.field}: #{formatted}"
107
+ end
108
+ end
109
+
110
+ def print_issues(heading, issues)
111
+ return if issues.empty?
112
+
113
+ puts " #{heading}: #{issues.size}"
114
+ issues.each do |issue|
115
+ puts " - #{issue.model}: #{issue.reason}"
116
+ end
117
+ end
118
+
119
+ def print_models(heading, models)
120
+ return if models.empty?
121
+
122
+ puts " #{heading}: #{models.size}"
123
+ models.each { |model| puts " - #{model}" }
124
+ end
125
+
126
+ def print_failures(failed_sources, heading:)
127
+ return if failed_sources.empty?
128
+
129
+ puts " #{heading}: #{failed_sources.size}"
130
+ failed_sources.each do |source, message|
131
+ puts " - #{source}: #{message}"
132
+ end
9
133
  end
@@ -8,20 +8,21 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Sergii Khomenko"]
9
9
  spec.email = ["sergey@mm.st"]
10
10
 
11
- spec.summary = "Self-hosted LLM API cost guardrails for Ruby and Rails"
12
- spec.description = "Tracks token usage and estimated costs for OpenAI, Anthropic, Google Gemini, " \
13
- "OpenRouter, DeepSeek, and OpenAI-compatible calls. " \
14
- "Works as Faraday middleware for Ruby clients, with ActiveRecord storage, " \
15
- "arbitrary tag-based attribution, and budget guardrails."
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."
16
17
  spec.homepage = "https://github.com/sergey-homenko/llm_cost_tracker"
17
18
  spec.license = "MIT"
18
19
 
19
20
  spec.required_ruby_version = ">= 3.3.0"
20
21
 
21
- spec.metadata["homepage_uri"] = spec.homepage
22
- spec.metadata["source_code_uri"] = spec.homepage
23
22
  spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
24
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"
25
26
  spec.metadata["rubygems_mfa_required"] = "true"
26
27
 
27
28
  spec.files = Dir.chdir(__dir__) do
@@ -34,7 +35,7 @@ Gem::Specification.new do |spec|
34
35
  spec.require_paths = ["lib"]
35
36
 
36
37
  spec.add_dependency "activesupport", ">= 7.1", "< 9.0"
37
- spec.add_dependency "csv", ">= 3.0"
38
+ spec.add_dependency "csv", "~> 3.0"
38
39
  spec.add_dependency "faraday", ">= 2.0", "< 3.0"
39
40
 
40
41
  spec.add_development_dependency "activerecord", ">= 7.1", "< 9.0"
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.2.0.alpha2
4
+ version: 0.3.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-20 00:00:00.000000000 Z
11
+ date: 2026-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -34,14 +34,14 @@ dependencies:
34
34
  name: csv
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - ">="
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '3.0'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
46
  version: '3.0'
47
47
  - !ruby/object:Gem::Dependency
@@ -180,10 +180,10 @@ dependencies:
180
180
  - - "~>"
181
181
  - !ruby/object:Gem::Version
182
182
  version: '3.0'
183
- description: Tracks token usage and estimated costs for OpenAI, Anthropic, Google
184
- Gemini, OpenRouter, DeepSeek, and OpenAI-compatible calls. Works as Faraday middleware
185
- for Ruby clients, with ActiveRecord storage, arbitrary tag-based attribution, and
186
- budget guardrails.
183
+ description: Tracks token usage, latency, and estimated costs for OpenAI, Anthropic,
184
+ Google Gemini, OpenRouter, DeepSeek, and OpenAI-compatible APIs. Works through Faraday
185
+ middleware or explicit track/track_stream helpers, with ActiveRecord storage, tag-based
186
+ attribution, price sync tasks, and budget guardrails.
187
187
  email:
188
188
  - sergey@mm.st
189
189
  executables: []
@@ -193,20 +193,27 @@ files:
193
193
  - ".rspec"
194
194
  - CHANGELOG.md
195
195
  - LICENSE.txt
196
- - PLAN_0.2.md
197
196
  - README.md
198
197
  - Rakefile
198
+ - app/assets/llm_cost_tracker/application.css
199
199
  - app/controllers/llm_cost_tracker/application_controller.rb
200
+ - app/controllers/llm_cost_tracker/assets_controller.rb
200
201
  - app/controllers/llm_cost_tracker/calls_controller.rb
201
202
  - app/controllers/llm_cost_tracker/dashboard_controller.rb
202
203
  - app/controllers/llm_cost_tracker/data_quality_controller.rb
203
204
  - app/controllers/llm_cost_tracker/models_controller.rb
204
205
  - app/controllers/llm_cost_tracker/tags_controller.rb
205
206
  - app/helpers/llm_cost_tracker/application_helper.rb
207
+ - app/helpers/llm_cost_tracker/chart_helper.rb
208
+ - app/helpers/llm_cost_tracker/dashboard_filter_helper.rb
209
+ - app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb
210
+ - app/helpers/llm_cost_tracker/dashboard_query_helper.rb
211
+ - app/helpers/llm_cost_tracker/pagination_helper.rb
206
212
  - app/services/llm_cost_tracker/dashboard/data_quality.rb
207
213
  - app/services/llm_cost_tracker/dashboard/filter.rb
208
214
  - app/services/llm_cost_tracker/dashboard/overview_stats.rb
209
215
  - app/services/llm_cost_tracker/dashboard/provider_breakdown.rb
216
+ - app/services/llm_cost_tracker/dashboard/spend_anomaly.rb
210
217
  - app/services/llm_cost_tracker/dashboard/tag_breakdown.rb
211
218
  - app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb
212
219
  - app/services/llm_cost_tracker/dashboard/time_series.rb
@@ -221,12 +228,17 @@ files:
221
228
  - app/views/llm_cost_tracker/errors/invalid_filter.html.erb
222
229
  - app/views/llm_cost_tracker/errors/not_found.html.erb
223
230
  - app/views/llm_cost_tracker/models/index.html.erb
231
+ - app/views/llm_cost_tracker/shared/_active_filters.html.erb
224
232
  - app/views/llm_cost_tracker/shared/_bar.html.erb
233
+ - app/views/llm_cost_tracker/shared/_metric_stack.html.erb
234
+ - app/views/llm_cost_tracker/shared/_spend_chart.html.erb
235
+ - app/views/llm_cost_tracker/shared/_tag_chips.html.erb
225
236
  - app/views/llm_cost_tracker/shared/setup_required.html.erb
226
237
  - app/views/llm_cost_tracker/tags/index.html.erb
227
238
  - app/views/llm_cost_tracker/tags/show.html.erb
228
239
  - config/routes.rb
229
240
  - lib/llm_cost_tracker.rb
241
+ - lib/llm_cost_tracker/assets.rb
230
242
  - lib/llm_cost_tracker/budget.rb
231
243
  - lib/llm_cost_tracker/configuration.rb
232
244
  - lib/llm_cost_tracker/cost.rb
@@ -236,9 +248,11 @@ files:
236
248
  - lib/llm_cost_tracker/event.rb
237
249
  - lib/llm_cost_tracker/event_metadata.rb
238
250
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
251
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb
239
252
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
240
253
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
241
254
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb
255
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb
242
256
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
243
257
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
244
258
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb
@@ -257,21 +271,35 @@ files:
257
271
  - lib/llm_cost_tracker/parsers/openai_compatible.rb
258
272
  - lib/llm_cost_tracker/parsers/openai_usage.rb
259
273
  - lib/llm_cost_tracker/parsers/registry.rb
274
+ - lib/llm_cost_tracker/parsers/sse.rb
260
275
  - lib/llm_cost_tracker/period_grouping.rb
261
276
  - lib/llm_cost_tracker/price_registry.rb
277
+ - lib/llm_cost_tracker/price_sync.rb
278
+ - lib/llm_cost_tracker/price_sync/fetcher.rb
279
+ - lib/llm_cost_tracker/price_sync/merger.rb
280
+ - lib/llm_cost_tracker/price_sync/model_catalog.rb
281
+ - lib/llm_cost_tracker/price_sync/raw_price.rb
282
+ - lib/llm_cost_tracker/price_sync/source.rb
283
+ - lib/llm_cost_tracker/price_sync/source_result.rb
284
+ - lib/llm_cost_tracker/price_sync/sources/litellm.rb
285
+ - lib/llm_cost_tracker/price_sync/sources/open_router.rb
286
+ - lib/llm_cost_tracker/price_sync/validator.rb
262
287
  - lib/llm_cost_tracker/prices.json
263
288
  - lib/llm_cost_tracker/pricing.rb
264
289
  - lib/llm_cost_tracker/railtie.rb
265
290
  - lib/llm_cost_tracker/report.rb
266
291
  - lib/llm_cost_tracker/report_data.rb
267
292
  - lib/llm_cost_tracker/report_formatter.rb
293
+ - lib/llm_cost_tracker/retention.rb
268
294
  - lib/llm_cost_tracker/storage/active_record_store.rb
295
+ - lib/llm_cost_tracker/stream_collector.rb
269
296
  - lib/llm_cost_tracker/tag_accessors.rb
270
297
  - lib/llm_cost_tracker/tag_key.rb
271
298
  - lib/llm_cost_tracker/tag_query.rb
272
299
  - lib/llm_cost_tracker/tags_column.rb
273
300
  - lib/llm_cost_tracker/tracker.rb
274
301
  - lib/llm_cost_tracker/unknown_pricing.rb
302
+ - lib/llm_cost_tracker/value_helpers.rb
275
303
  - lib/llm_cost_tracker/version.rb
276
304
  - lib/tasks/llm_cost_tracker.rake
277
305
  - llm_cost_tracker.gemspec
@@ -279,10 +307,10 @@ homepage: https://github.com/sergey-homenko/llm_cost_tracker
279
307
  licenses:
280
308
  - MIT
281
309
  metadata:
282
- homepage_uri: https://github.com/sergey-homenko/llm_cost_tracker
283
- source_code_uri: https://github.com/sergey-homenko/llm_cost_tracker
284
310
  bug_tracker_uri: https://github.com/sergey-homenko/llm_cost_tracker/issues
285
311
  changelog_uri: https://github.com/sergey-homenko/llm_cost_tracker/blob/main/CHANGELOG.md
312
+ source_code_uri: https://github.com/sergey-homenko/llm_cost_tracker
313
+ documentation_uri: https://github.com/sergey-homenko/llm_cost_tracker#readme
286
314
  rubygems_mfa_required: 'true'
287
315
  post_install_message:
288
316
  rdoc_options: []
@@ -302,5 +330,5 @@ requirements: []
302
330
  rubygems_version: 3.5.9
303
331
  signing_key:
304
332
  specification_version: 4
305
- summary: Self-hosted LLM API cost guardrails for Ruby and Rails
333
+ summary: Self-hosted LLM usage and cost tracking for Ruby and Rails
306
334
  test_files: []