llm_cost_tracker 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.md +124 -68
  4. data/Rakefile +2 -0
  5. data/app/assets/llm_cost_tracker/application.css +1 -4
  6. data/app/controllers/llm_cost_tracker/assets_controller.rb +1 -2
  7. data/app/controllers/llm_cost_tracker/calls_controller.rb +9 -13
  8. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +8 -19
  9. data/app/controllers/llm_cost_tracker/data_quality_controller.rb +1 -2
  10. data/app/controllers/llm_cost_tracker/models_controller.rb +5 -2
  11. data/app/controllers/llm_cost_tracker/tags_controller.rb +2 -4
  12. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +6 -1
  13. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -7
  14. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +5 -9
  15. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +16 -1
  16. data/app/services/llm_cost_tracker/dashboard/filter.rb +26 -24
  17. data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +0 -3
  18. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +0 -2
  19. data/app/services/llm_cost_tracker/pagination.rb +1 -9
  20. data/app/views/layouts/llm_cost_tracker/application.html.erb +1 -16
  21. data/app/views/llm_cost_tracker/calls/index.html.erb +23 -13
  22. data/app/views/llm_cost_tracker/calls/show.html.erb +8 -3
  23. data/app/views/llm_cost_tracker/dashboard/index.html.erb +11 -1
  24. data/app/views/llm_cost_tracker/data_quality/index.html.erb +78 -10
  25. data/app/views/llm_cost_tracker/models/index.html.erb +10 -9
  26. data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +0 -1
  27. data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +0 -1
  28. data/app/views/llm_cost_tracker/tags/index.html.erb +1 -1
  29. data/app/views/llm_cost_tracker/tags/show.html.erb +1 -1
  30. data/lib/llm_cost_tracker/assets.rb +6 -11
  31. data/lib/llm_cost_tracker/configuration.rb +78 -43
  32. data/lib/llm_cost_tracker/event.rb +3 -0
  33. data/lib/llm_cost_tracker/event_metadata.rb +1 -0
  34. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb +29 -0
  35. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +29 -0
  36. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +15 -0
  37. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +25 -0
  38. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +6 -0
  39. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +8 -1
  40. data/lib/llm_cost_tracker/llm_api_call.rb +14 -2
  41. data/lib/llm_cost_tracker/middleware/faraday.rb +58 -9
  42. data/lib/llm_cost_tracker/parameter_hash.rb +33 -0
  43. data/lib/llm_cost_tracker/parsed_usage.rb +18 -3
  44. data/lib/llm_cost_tracker/parsers/anthropic.rb +98 -1
  45. data/lib/llm_cost_tracker/parsers/base.rb +17 -5
  46. data/lib/llm_cost_tracker/parsers/gemini.rb +83 -6
  47. data/lib/llm_cost_tracker/parsers/openai.rb +8 -0
  48. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +12 -5
  49. data/lib/llm_cost_tracker/parsers/openai_usage.rb +69 -1
  50. data/lib/llm_cost_tracker/parsers/registry.rb +15 -3
  51. data/lib/llm_cost_tracker/parsers/sse.rb +81 -0
  52. data/lib/llm_cost_tracker/price_registry.rb +23 -8
  53. data/lib/llm_cost_tracker/price_sync/fetcher.rb +72 -0
  54. data/lib/llm_cost_tracker/price_sync/merger.rb +72 -0
  55. data/lib/llm_cost_tracker/price_sync/model_catalog.rb +77 -0
  56. data/lib/llm_cost_tracker/price_sync/raw_price.rb +35 -0
  57. data/lib/llm_cost_tracker/price_sync/refresh_plan_builder.rb +162 -0
  58. data/lib/llm_cost_tracker/price_sync/registry_loader.rb +55 -0
  59. data/lib/llm_cost_tracker/price_sync/registry_writer.rb +25 -0
  60. data/lib/llm_cost_tracker/price_sync/source.rb +29 -0
  61. data/lib/llm_cost_tracker/price_sync/source_result.rb +7 -0
  62. data/lib/llm_cost_tracker/price_sync/sources/litellm.rb +91 -0
  63. data/lib/llm_cost_tracker/price_sync/sources/open_router.rb +94 -0
  64. data/lib/llm_cost_tracker/price_sync/validator.rb +66 -0
  65. data/lib/llm_cost_tracker/price_sync.rb +142 -0
  66. data/lib/llm_cost_tracker/pricing.rb +0 -11
  67. data/lib/llm_cost_tracker/railtie.rb +0 -1
  68. data/lib/llm_cost_tracker/report.rb +0 -5
  69. data/lib/llm_cost_tracker/storage/active_record_store.rb +10 -9
  70. data/lib/llm_cost_tracker/stream_collector.rb +162 -0
  71. data/lib/llm_cost_tracker/tags_column.rb +12 -0
  72. data/lib/llm_cost_tracker/tracker.rb +23 -12
  73. data/lib/llm_cost_tracker/value_helpers.rb +40 -0
  74. data/lib/llm_cost_tracker/version.rb +1 -1
  75. data/lib/llm_cost_tracker.rb +48 -35
  76. data/lib/tasks/llm_cost_tracker.rake +116 -0
  77. data/llm_cost_tracker.gemspec +8 -6
  78. metadata +30 -8
@@ -1,5 +1,6 @@
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
@@ -14,4 +15,119 @@ namespace :llm_cost_tracker do
14
15
  deleted = LlmCostTracker::Retention.prune(older_than: days, batch_size: batch_size)
15
16
  puts "llm_cost_tracker: pruned #{deleted} calls older than #{days} days"
16
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
17
133
  end
@@ -8,19 +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
22
  spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
23
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"
24
26
  spec.metadata["rubygems_mfa_required"] = "true"
25
27
 
26
28
  spec.files = Dir.chdir(__dir__) do
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
4
+ version: 0.3.1
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
@@ -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: []
@@ -248,9 +248,13 @@ files:
248
248
  - lib/llm_cost_tracker/event.rb
249
249
  - lib/llm_cost_tracker/event_metadata.rb
250
250
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
251
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb
252
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb
251
253
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
252
254
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
253
255
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb
256
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb
257
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb
254
258
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
255
259
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
256
260
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb
@@ -261,6 +265,7 @@ files:
261
265
  - lib/llm_cost_tracker/llm_api_call.rb
262
266
  - lib/llm_cost_tracker/logging.rb
263
267
  - lib/llm_cost_tracker/middleware/faraday.rb
268
+ - lib/llm_cost_tracker/parameter_hash.rb
264
269
  - lib/llm_cost_tracker/parsed_usage.rb
265
270
  - lib/llm_cost_tracker/parsers/anthropic.rb
266
271
  - lib/llm_cost_tracker/parsers/base.rb
@@ -269,8 +274,22 @@ files:
269
274
  - lib/llm_cost_tracker/parsers/openai_compatible.rb
270
275
  - lib/llm_cost_tracker/parsers/openai_usage.rb
271
276
  - lib/llm_cost_tracker/parsers/registry.rb
277
+ - lib/llm_cost_tracker/parsers/sse.rb
272
278
  - lib/llm_cost_tracker/period_grouping.rb
273
279
  - lib/llm_cost_tracker/price_registry.rb
280
+ - lib/llm_cost_tracker/price_sync.rb
281
+ - lib/llm_cost_tracker/price_sync/fetcher.rb
282
+ - lib/llm_cost_tracker/price_sync/merger.rb
283
+ - lib/llm_cost_tracker/price_sync/model_catalog.rb
284
+ - lib/llm_cost_tracker/price_sync/raw_price.rb
285
+ - lib/llm_cost_tracker/price_sync/refresh_plan_builder.rb
286
+ - lib/llm_cost_tracker/price_sync/registry_loader.rb
287
+ - lib/llm_cost_tracker/price_sync/registry_writer.rb
288
+ - lib/llm_cost_tracker/price_sync/source.rb
289
+ - lib/llm_cost_tracker/price_sync/source_result.rb
290
+ - lib/llm_cost_tracker/price_sync/sources/litellm.rb
291
+ - lib/llm_cost_tracker/price_sync/sources/open_router.rb
292
+ - lib/llm_cost_tracker/price_sync/validator.rb
274
293
  - lib/llm_cost_tracker/prices.json
275
294
  - lib/llm_cost_tracker/pricing.rb
276
295
  - lib/llm_cost_tracker/railtie.rb
@@ -279,12 +298,14 @@ files:
279
298
  - lib/llm_cost_tracker/report_formatter.rb
280
299
  - lib/llm_cost_tracker/retention.rb
281
300
  - lib/llm_cost_tracker/storage/active_record_store.rb
301
+ - lib/llm_cost_tracker/stream_collector.rb
282
302
  - lib/llm_cost_tracker/tag_accessors.rb
283
303
  - lib/llm_cost_tracker/tag_key.rb
284
304
  - lib/llm_cost_tracker/tag_query.rb
285
305
  - lib/llm_cost_tracker/tags_column.rb
286
306
  - lib/llm_cost_tracker/tracker.rb
287
307
  - lib/llm_cost_tracker/unknown_pricing.rb
308
+ - lib/llm_cost_tracker/value_helpers.rb
288
309
  - lib/llm_cost_tracker/version.rb
289
310
  - lib/tasks/llm_cost_tracker.rake
290
311
  - llm_cost_tracker.gemspec
@@ -292,9 +313,10 @@ homepage: https://github.com/sergey-homenko/llm_cost_tracker
292
313
  licenses:
293
314
  - MIT
294
315
  metadata:
295
- homepage_uri: https://github.com/sergey-homenko/llm_cost_tracker
296
316
  bug_tracker_uri: https://github.com/sergey-homenko/llm_cost_tracker/issues
297
317
  changelog_uri: https://github.com/sergey-homenko/llm_cost_tracker/blob/main/CHANGELOG.md
318
+ source_code_uri: https://github.com/sergey-homenko/llm_cost_tracker
319
+ documentation_uri: https://github.com/sergey-homenko/llm_cost_tracker#readme
298
320
  rubygems_mfa_required: 'true'
299
321
  post_install_message:
300
322
  rdoc_options: []
@@ -314,5 +336,5 @@ requirements: []
314
336
  rubygems_version: 3.5.9
315
337
  signing_key:
316
338
  specification_version: 4
317
- summary: Self-hosted LLM API cost guardrails for Ruby and Rails
339
+ summary: Self-hosted LLM usage and cost tracking for Ruby and Rails
318
340
  test_files: []