llm_cost_tracker 0.1.4 → 0.2.0.alpha2

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -91
  3. data/PLAN_0.2.md +488 -0
  4. data/README.md +140 -320
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +42 -0
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +77 -0
  7. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +54 -0
  8. data/app/controllers/llm_cost_tracker/data_quality_controller.rb +10 -0
  9. data/app/controllers/llm_cost_tracker/models_controller.rb +12 -0
  10. data/app/controllers/llm_cost_tracker/tags_controller.rb +21 -0
  11. data/app/helpers/llm_cost_tracker/application_helper.rb +113 -0
  12. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +38 -0
  13. data/app/services/llm_cost_tracker/dashboard/filter.rb +109 -0
  14. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +87 -0
  15. data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +44 -0
  16. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +58 -0
  17. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +125 -0
  18. data/app/services/llm_cost_tracker/dashboard/time_series.rb +44 -0
  19. data/app/services/llm_cost_tracker/dashboard/top_models.rb +89 -0
  20. data/app/services/llm_cost_tracker/pagination.rb +59 -0
  21. data/app/views/layouts/llm_cost_tracker/application.html.erb +342 -0
  22. data/app/views/llm_cost_tracker/calls/index.html.erb +127 -0
  23. data/app/views/llm_cost_tracker/calls/show.html.erb +67 -0
  24. data/app/views/llm_cost_tracker/dashboard/index.html.erb +145 -0
  25. data/app/views/llm_cost_tracker/data_quality/index.html.erb +110 -0
  26. data/app/views/llm_cost_tracker/errors/database.html.erb +8 -0
  27. data/app/views/llm_cost_tracker/errors/invalid_filter.html.erb +4 -0
  28. data/app/views/llm_cost_tracker/errors/not_found.html.erb +5 -0
  29. data/app/views/llm_cost_tracker/models/index.html.erb +95 -0
  30. data/app/views/llm_cost_tracker/shared/_bar.html.erb +5 -0
  31. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +6 -0
  32. data/app/views/llm_cost_tracker/tags/index.html.erb +34 -0
  33. data/app/views/llm_cost_tracker/tags/show.html.erb +69 -0
  34. data/config/routes.rb +10 -0
  35. data/lib/llm_cost_tracker/budget.rb +16 -38
  36. data/lib/llm_cost_tracker/configuration.rb +3 -1
  37. data/lib/llm_cost_tracker/cost.rb +1 -3
  38. data/lib/llm_cost_tracker/engine.rb +13 -0
  39. data/lib/llm_cost_tracker/engine_compatibility.rb +15 -0
  40. data/lib/llm_cost_tracker/errors.rb +2 -0
  41. data/lib/llm_cost_tracker/event.rb +1 -3
  42. data/lib/llm_cost_tracker/event_metadata.rb +9 -18
  43. data/lib/llm_cost_tracker/llm_api_call.rb +4 -17
  44. data/lib/llm_cost_tracker/middleware/faraday.rb +4 -4
  45. data/lib/llm_cost_tracker/parsed_usage.rb +5 -9
  46. data/lib/llm_cost_tracker/parsers/anthropic.rb +4 -5
  47. data/lib/llm_cost_tracker/parsers/base.rb +3 -8
  48. data/lib/llm_cost_tracker/parsers/gemini.rb +3 -3
  49. data/lib/llm_cost_tracker/parsers/openai_usage.rb +3 -3
  50. data/lib/llm_cost_tracker/parsers/registry.rb +5 -12
  51. data/lib/llm_cost_tracker/period_grouping.rb +68 -0
  52. data/lib/llm_cost_tracker/price_registry.rb +22 -30
  53. data/lib/llm_cost_tracker/pricing.rb +10 -19
  54. data/lib/llm_cost_tracker/report.rb +4 -4
  55. data/lib/llm_cost_tracker/report_data.rb +21 -24
  56. data/lib/llm_cost_tracker/report_formatter.rb +4 -2
  57. data/lib/llm_cost_tracker/storage/active_record_store.rb +1 -3
  58. data/lib/llm_cost_tracker/tag_key.rb +16 -0
  59. data/lib/llm_cost_tracker/tracker.rb +35 -1
  60. data/lib/llm_cost_tracker/version.rb +1 -1
  61. data/lib/llm_cost_tracker.rb +3 -6
  62. data/llm_cost_tracker.gemspec +13 -9
  63. metadata +91 -20
  64. data/.rubocop.yml +0 -44
  65. data/lib/llm_cost_tracker/storage/active_record_backend.rb +0 -19
  66. data/lib/llm_cost_tracker/storage/backends.rb +0 -26
  67. data/lib/llm_cost_tracker/storage/custom_backend.rb +0 -16
  68. data/lib/llm_cost_tracker/storage/log_backend.rb +0 -28
  69. data/lib/llm_cost_tracker/value_object.rb +0 -45
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.1.4
4
+ version: 0.2.0.alpha2
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-18 00:00:00.000000000 Z
11
+ date: 2026-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,7 +16,7 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '7.0'
19
+ version: '7.1'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
22
  version: '9.0'
@@ -26,17 +26,31 @@ dependencies:
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '7.0'
29
+ version: '7.1'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: '9.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: csv
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
33
47
  - !ruby/object:Gem::Dependency
34
48
  name: faraday
35
49
  requirement: !ruby/object:Gem::Requirement
36
50
  requirements:
37
51
  - - ">="
38
52
  - !ruby/object:Gem::Version
39
- version: '1.0'
53
+ version: '2.0'
40
54
  - - "<"
41
55
  - !ruby/object:Gem::Version
42
56
  version: '3.0'
@@ -46,7 +60,7 @@ dependencies:
46
60
  requirements:
47
61
  - - ">="
48
62
  - !ruby/object:Gem::Version
49
- version: '1.0'
63
+ version: '2.0'
50
64
  - - "<"
51
65
  - !ruby/object:Gem::Version
52
66
  version: '3.0'
@@ -56,7 +70,27 @@ dependencies:
56
70
  requirements:
57
71
  - - ">="
58
72
  - !ruby/object:Gem::Version
59
- version: '7.0'
73
+ version: '7.1'
74
+ - - "<"
75
+ - !ruby/object:Gem::Version
76
+ version: '9.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '7.1'
84
+ - - "<"
85
+ - !ruby/object:Gem::Version
86
+ version: '9.0'
87
+ - !ruby/object:Gem::Dependency
88
+ name: railties
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '7.1'
60
94
  - - "<"
61
95
  - !ruby/object:Gem::Version
62
96
  version: '9.0'
@@ -66,7 +100,7 @@ dependencies:
66
100
  requirements:
67
101
  - - ">="
68
102
  - !ruby/object:Gem::Version
69
- version: '7.0'
103
+ version: '7.1'
70
104
  - - "<"
71
105
  - !ruby/object:Gem::Version
72
106
  version: '9.0'
@@ -116,16 +150,22 @@ dependencies:
116
150
  name: sqlite3
117
151
  requirement: !ruby/object:Gem::Requirement
118
152
  requirements:
119
- - - "~>"
153
+ - - ">="
120
154
  - !ruby/object:Gem::Version
121
- version: '2.0'
155
+ version: '1.4'
156
+ - - "<"
157
+ - !ruby/object:Gem::Version
158
+ version: '3.0'
122
159
  type: :development
123
160
  prerelease: false
124
161
  version_requirements: !ruby/object:Gem::Requirement
125
162
  requirements:
126
- - - "~>"
163
+ - - ">="
127
164
  - !ruby/object:Gem::Version
128
- version: '2.0'
165
+ version: '1.4'
166
+ - - "<"
167
+ - !ruby/object:Gem::Version
168
+ version: '3.0'
129
169
  - !ruby/object:Gem::Dependency
130
170
  name: webmock
131
171
  requirement: !ruby/object:Gem::Requirement
@@ -142,7 +182,7 @@ dependencies:
142
182
  version: '3.0'
143
183
  description: Tracks token usage and estimated costs for OpenAI, Anthropic, Google
144
184
  Gemini, OpenRouter, DeepSeek, and OpenAI-compatible calls. Works as Faraday middleware
145
- for Ruby clients, with ActiveRecord storage, per-user/per-feature attribution, and
185
+ for Ruby clients, with ActiveRecord storage, arbitrary tag-based attribution, and
146
186
  budget guardrails.
147
187
  email:
148
188
  - sergey@mm.st
@@ -151,15 +191,47 @@ extensions: []
151
191
  extra_rdoc_files: []
152
192
  files:
153
193
  - ".rspec"
154
- - ".rubocop.yml"
155
194
  - CHANGELOG.md
156
195
  - LICENSE.txt
196
+ - PLAN_0.2.md
157
197
  - README.md
158
198
  - Rakefile
199
+ - app/controllers/llm_cost_tracker/application_controller.rb
200
+ - app/controllers/llm_cost_tracker/calls_controller.rb
201
+ - app/controllers/llm_cost_tracker/dashboard_controller.rb
202
+ - app/controllers/llm_cost_tracker/data_quality_controller.rb
203
+ - app/controllers/llm_cost_tracker/models_controller.rb
204
+ - app/controllers/llm_cost_tracker/tags_controller.rb
205
+ - app/helpers/llm_cost_tracker/application_helper.rb
206
+ - app/services/llm_cost_tracker/dashboard/data_quality.rb
207
+ - app/services/llm_cost_tracker/dashboard/filter.rb
208
+ - app/services/llm_cost_tracker/dashboard/overview_stats.rb
209
+ - app/services/llm_cost_tracker/dashboard/provider_breakdown.rb
210
+ - app/services/llm_cost_tracker/dashboard/tag_breakdown.rb
211
+ - app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb
212
+ - app/services/llm_cost_tracker/dashboard/time_series.rb
213
+ - app/services/llm_cost_tracker/dashboard/top_models.rb
214
+ - app/services/llm_cost_tracker/pagination.rb
215
+ - app/views/layouts/llm_cost_tracker/application.html.erb
216
+ - app/views/llm_cost_tracker/calls/index.html.erb
217
+ - app/views/llm_cost_tracker/calls/show.html.erb
218
+ - app/views/llm_cost_tracker/dashboard/index.html.erb
219
+ - app/views/llm_cost_tracker/data_quality/index.html.erb
220
+ - app/views/llm_cost_tracker/errors/database.html.erb
221
+ - app/views/llm_cost_tracker/errors/invalid_filter.html.erb
222
+ - app/views/llm_cost_tracker/errors/not_found.html.erb
223
+ - app/views/llm_cost_tracker/models/index.html.erb
224
+ - app/views/llm_cost_tracker/shared/_bar.html.erb
225
+ - app/views/llm_cost_tracker/shared/setup_required.html.erb
226
+ - app/views/llm_cost_tracker/tags/index.html.erb
227
+ - app/views/llm_cost_tracker/tags/show.html.erb
228
+ - config/routes.rb
159
229
  - lib/llm_cost_tracker.rb
160
230
  - lib/llm_cost_tracker/budget.rb
161
231
  - lib/llm_cost_tracker/configuration.rb
162
232
  - lib/llm_cost_tracker/cost.rb
233
+ - lib/llm_cost_tracker/engine.rb
234
+ - lib/llm_cost_tracker/engine_compatibility.rb
163
235
  - lib/llm_cost_tracker/errors.rb
164
236
  - lib/llm_cost_tracker/event.rb
165
237
  - lib/llm_cost_tracker/event_metadata.rb
@@ -185,6 +257,7 @@ files:
185
257
  - lib/llm_cost_tracker/parsers/openai_compatible.rb
186
258
  - lib/llm_cost_tracker/parsers/openai_usage.rb
187
259
  - lib/llm_cost_tracker/parsers/registry.rb
260
+ - lib/llm_cost_tracker/period_grouping.rb
188
261
  - lib/llm_cost_tracker/price_registry.rb
189
262
  - lib/llm_cost_tracker/prices.json
190
263
  - lib/llm_cost_tracker/pricing.rb
@@ -192,17 +265,13 @@ files:
192
265
  - lib/llm_cost_tracker/report.rb
193
266
  - lib/llm_cost_tracker/report_data.rb
194
267
  - lib/llm_cost_tracker/report_formatter.rb
195
- - lib/llm_cost_tracker/storage/active_record_backend.rb
196
268
  - lib/llm_cost_tracker/storage/active_record_store.rb
197
- - lib/llm_cost_tracker/storage/backends.rb
198
- - lib/llm_cost_tracker/storage/custom_backend.rb
199
- - lib/llm_cost_tracker/storage/log_backend.rb
200
269
  - lib/llm_cost_tracker/tag_accessors.rb
270
+ - lib/llm_cost_tracker/tag_key.rb
201
271
  - lib/llm_cost_tracker/tag_query.rb
202
272
  - lib/llm_cost_tracker/tags_column.rb
203
273
  - lib/llm_cost_tracker/tracker.rb
204
274
  - lib/llm_cost_tracker/unknown_pricing.rb
205
- - lib/llm_cost_tracker/value_object.rb
206
275
  - lib/llm_cost_tracker/version.rb
207
276
  - lib/tasks/llm_cost_tracker.rake
208
277
  - llm_cost_tracker.gemspec
@@ -211,6 +280,8 @@ licenses:
211
280
  - MIT
212
281
  metadata:
213
282
  homepage_uri: https://github.com/sergey-homenko/llm_cost_tracker
283
+ source_code_uri: https://github.com/sergey-homenko/llm_cost_tracker
284
+ bug_tracker_uri: https://github.com/sergey-homenko/llm_cost_tracker/issues
214
285
  changelog_uri: https://github.com/sergey-homenko/llm_cost_tracker/blob/main/CHANGELOG.md
215
286
  rubygems_mfa_required: 'true'
216
287
  post_install_message:
@@ -221,7 +292,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
221
292
  requirements:
222
293
  - - ">="
223
294
  - !ruby/object:Gem::Version
224
- version: 3.1.0
295
+ version: 3.3.0
225
296
  required_rubygems_version: !ruby/object:Gem::Requirement
226
297
  requirements:
227
298
  - - ">="
data/.rubocop.yml DELETED
@@ -1,44 +0,0 @@
1
- AllCops:
2
- NewCops: enable
3
- TargetRubyVersion: 3.1
4
- SuggestExtensions: false
5
- UseCache: false
6
- Exclude:
7
- - "tmp/**/*"
8
- - "vendor/**/*"
9
- - "pkg/**/*"
10
-
11
- Style/Documentation:
12
- Enabled: false
13
-
14
- Style/StringLiterals:
15
- EnforcedStyle: double_quotes
16
-
17
- Metrics/BlockLength:
18
- Exclude:
19
- - "*.gemspec"
20
- - "spec/**/*.rb"
21
-
22
- Metrics/MethodLength:
23
- Max: 25
24
-
25
- Metrics/AbcSize:
26
- Max: 45
27
-
28
- Metrics/ClassLength:
29
- Max: 130
30
-
31
- Metrics/CyclomaticComplexity:
32
- Max: 10
33
-
34
- Metrics/ParameterLists:
35
- Max: 6
36
-
37
- Metrics/PerceivedComplexity:
38
- Max: 10
39
-
40
- Gemspec/DevelopmentDependencies:
41
- Enabled: false
42
-
43
- Layout/HashAlignment:
44
- Enabled: false
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module Storage
5
- module ActiveRecordBackend
6
- class << self
7
- def save(event, **_options)
8
- require_relative "../llm_api_call" unless defined?(LlmCostTracker::LlmApiCall)
9
- require_relative "active_record_store" unless defined?(LlmCostTracker::Storage::ActiveRecordStore)
10
-
11
- ActiveRecordStore.save(event)
12
- event
13
- rescue LoadError => e
14
- raise Error, "ActiveRecord storage requires the active_record gem: #{e.message}"
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../errors"
4
- require_relative "log_backend"
5
- require_relative "active_record_backend"
6
- require_relative "custom_backend"
7
-
8
- module LlmCostTracker
9
- module Storage
10
- module Backends
11
- MAP = {
12
- log: LogBackend,
13
- active_record: ActiveRecordBackend,
14
- custom: CustomBackend
15
- }.freeze
16
-
17
- class << self
18
- def fetch(name)
19
- MAP.fetch(name.to_sym)
20
- rescue KeyError
21
- raise Error, "Unknown storage_backend: #{name.inspect}. Use one of: #{MAP.keys.join(', ')}"
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module Storage
5
- module CustomBackend
6
- class << self
7
- def save(event, config:)
8
- result = config.custom_storage&.call(event)
9
- return false if result == false
10
-
11
- event
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../logging"
4
-
5
- module LlmCostTracker
6
- module Storage
7
- module LogBackend
8
- class << self
9
- def save(event, config:)
10
- message = "#{event.provider}/#{event.model} " \
11
- "tokens=#{event.input_tokens}+#{event.output_tokens} " \
12
- "cost=#{cost_label(event)}"
13
- message += " latency=#{event.latency_ms}ms" if event.latency_ms
14
- message += " tags=#{event.tags}" unless event.tags.empty?
15
-
16
- Logging.log(config.log_level, message)
17
- event
18
- end
19
-
20
- private
21
-
22
- def cost_label(event)
23
- event.cost ? "$#{format('%.6f', event.cost.total_cost)}" : "unknown"
24
- end
25
- end
26
- end
27
- end
28
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module ValueObject
5
- class << self
6
- def define(*members, &block)
7
- klass = data_class(*members)
8
- add_hash_like_readers(klass)
9
- klass.class_eval(&block) if block
10
- klass
11
- end
12
-
13
- private
14
-
15
- def data_class(*members)
16
- return Data.define(*members) if defined?(Data)
17
-
18
- Struct.new(*members, keyword_init: true) do
19
- def initialize(**kwargs)
20
- super
21
- freeze
22
- end
23
- end
24
- end
25
-
26
- def add_hash_like_readers(klass)
27
- klass.class_eval do
28
- def [](key)
29
- public_send(key.to_sym)
30
- end
31
-
32
- def dig(key, *rest)
33
- value = self[key]
34
- rest.empty? ? value : value&.dig(*rest)
35
- end
36
-
37
- def except(*keys)
38
- excluded = keys.map(&:to_sym)
39
- to_h.reject { |key, _value| excluded.include?(key.to_sym) }
40
- end
41
- end
42
- end
43
- end
44
- end
45
- end