llm_cost_tracker 0.1.0 → 0.1.2

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +44 -0
  3. data/CHANGELOG.md +62 -0
  4. data/README.md +243 -26
  5. data/Rakefile +3 -1
  6. data/lib/llm_cost_tracker/budget.rb +97 -0
  7. data/lib/llm_cost_tracker/configuration.rb +37 -0
  8. data/lib/llm_cost_tracker/errors.rb +37 -0
  9. data/lib/llm_cost_tracker/event_metadata.rb +54 -0
  10. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +29 -0
  11. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +9 -0
  12. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +16 -4
  13. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -1
  14. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +15 -0
  15. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +41 -0
  16. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +29 -0
  17. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +29 -0
  18. data/lib/llm_cost_tracker/llm_api_call.rb +69 -1
  19. data/lib/llm_cost_tracker/middleware/faraday.rb +51 -14
  20. data/lib/llm_cost_tracker/parsers/anthropic.rb +10 -5
  21. data/lib/llm_cost_tracker/parsers/gemini.rb +13 -5
  22. data/lib/llm_cost_tracker/parsers/openai.rb +22 -7
  23. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +44 -0
  24. data/lib/llm_cost_tracker/parsers/registry.rb +16 -7
  25. data/lib/llm_cost_tracker/price_registry.rb +69 -0
  26. data/lib/llm_cost_tracker/prices.json +51 -0
  27. data/lib/llm_cost_tracker/pricing.rb +76 -41
  28. data/lib/llm_cost_tracker/railtie.rb +3 -0
  29. data/lib/llm_cost_tracker/storage/active_record_store.rb +24 -3
  30. data/lib/llm_cost_tracker/tracker.rb +65 -33
  31. data/lib/llm_cost_tracker/unknown_pricing.rb +47 -0
  32. data/lib/llm_cost_tracker/version.rb +1 -1
  33. data/lib/llm_cost_tracker.rb +33 -5
  34. data/llm_cost_tracker.gemspec +9 -7
  35. metadata +38 -23
@@ -5,31 +5,39 @@ require "active_support/notifications"
5
5
 
6
6
  require_relative "llm_cost_tracker/version"
7
7
  require_relative "llm_cost_tracker/configuration"
8
+ require_relative "llm_cost_tracker/errors"
9
+ require_relative "llm_cost_tracker/price_registry"
8
10
  require_relative "llm_cost_tracker/pricing"
9
11
  require_relative "llm_cost_tracker/parsers/base"
10
12
  require_relative "llm_cost_tracker/parsers/openai"
13
+ require_relative "llm_cost_tracker/parsers/openai_compatible"
11
14
  require_relative "llm_cost_tracker/parsers/anthropic"
12
15
  require_relative "llm_cost_tracker/parsers/gemini"
13
16
  require_relative "llm_cost_tracker/parsers/registry"
14
17
  require_relative "llm_cost_tracker/middleware/faraday"
18
+ require_relative "llm_cost_tracker/budget"
19
+ require_relative "llm_cost_tracker/unknown_pricing"
20
+ require_relative "llm_cost_tracker/event_metadata"
15
21
  require_relative "llm_cost_tracker/tracker"
16
22
 
17
23
  module LlmCostTracker
18
- class Error < StandardError; end
19
-
20
24
  class << self
25
+ CONFIGURATION_MUTEX = Mutex.new
26
+
21
27
  attr_writer :configuration
22
28
 
23
29
  def configuration
24
- @configuration ||= Configuration.new
30
+ @configuration || CONFIGURATION_MUTEX.synchronize { @configuration ||= Configuration.new }
25
31
  end
26
32
 
27
33
  def configure
28
34
  yield(configuration)
35
+ configuration.normalize_openai_compatible_providers!
36
+ warn_for_configuration!
29
37
  end
30
38
 
31
39
  def reset_configuration!
32
- @configuration = Configuration.new
40
+ CONFIGURATION_MUTEX.synchronize { @configuration = Configuration.new }
33
41
  end
34
42
 
35
43
  # Manual tracking for non-Faraday clients
@@ -42,15 +50,35 @@ module LlmCostTracker
42
50
  # feature: "chat",
43
51
  # user_id: current_user.id
44
52
  # )
45
- def track(provider:, model:, input_tokens:, output_tokens:, **metadata)
53
+ def track(provider:, model:, input_tokens:, output_tokens:, latency_ms: nil, **metadata)
46
54
  Tracker.record(
47
55
  provider: provider.to_s,
48
56
  model: model,
49
57
  input_tokens: input_tokens,
50
58
  output_tokens: output_tokens,
59
+ latency_ms: latency_ms,
51
60
  metadata: metadata
52
61
  )
53
62
  end
63
+
64
+ private
65
+
66
+ def warn_for_configuration!
67
+ return unless (configuration.budget_exceeded_behavior || :notify).to_sym == :block_requests
68
+ return if configuration.active_record?
69
+
70
+ log_warning(":block_requests requires storage_backend = :active_record; preflight blocking will be skipped.")
71
+ end
72
+
73
+ def log_warning(message)
74
+ message = "[LlmCostTracker] #{message}"
75
+
76
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
77
+ Rails.logger.warn(message)
78
+ else
79
+ warn message
80
+ end
81
+ end
54
82
  end
55
83
  end
56
84
 
@@ -8,10 +8,11 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Sergii Khomenko"]
9
9
  spec.email = ["sergey@mm.st"]
10
10
 
11
- spec.summary = "Provider-agnostic LLM API cost tracking for Ruby"
12
- spec.description = "Automatically tracks token usage and costs for LLM API calls (OpenAI, Anthropic, Google Gemini, and more). " \
13
- "Works as Faraday middleware — plugs into any Ruby HTTP client. " \
14
- "Provides ActiveRecord storage, per-user/per-feature attribution, and budget alerts."
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
+ "per-user/per-feature attribution, budget alerts, and budget enforcement."
15
16
  spec.homepage = "https://github.com/sergey-homenko/llm_cost_tracker"
16
17
  spec.license = "MIT"
17
18
 
@@ -19,6 +20,7 @@ Gem::Specification.new do |spec|
19
20
 
20
21
  spec.metadata["homepage_uri"] = spec.homepage
21
22
  spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
23
+ spec.metadata["rubygems_mfa_required"] = "true"
22
24
 
23
25
  spec.files = Dir.chdir(__dir__) do
24
26
  `git ls-files -z`.split("\x0").reject do |f|
@@ -29,13 +31,13 @@ Gem::Specification.new do |spec|
29
31
 
30
32
  spec.require_paths = ["lib"]
31
33
 
32
- spec.add_dependency "faraday", ">= 1.0", "< 3.0"
33
34
  spec.add_dependency "activesupport", ">= 7.0", "< 9.0"
35
+ spec.add_dependency "faraday", ">= 1.0", "< 3.0"
34
36
 
35
37
  spec.add_development_dependency "activerecord", ">= 7.0", "< 9.0"
36
38
  spec.add_development_dependency "rake", "~> 13.0"
37
39
  spec.add_development_dependency "rspec", "~> 3.0"
38
- spec.add_development_dependency "webmock", "~> 3.0"
39
- spec.add_development_dependency "sqlite3", "~> 2.0"
40
40
  spec.add_development_dependency "rubocop", "~> 1.0"
41
+ spec.add_development_dependency "sqlite3", "~> 2.0"
42
+ spec.add_development_dependency "webmock", "~> 3.0"
41
43
  end
metadata CHANGED
@@ -1,55 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_cost_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
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-16 00:00:00.000000000 Z
11
+ date: 2026-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: faraday
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.0'
19
+ version: '7.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '3.0'
22
+ version: '9.0'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '1.0'
29
+ version: '7.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '3.0'
32
+ version: '9.0'
33
33
  - !ruby/object:Gem::Dependency
34
- name: activesupport
34
+ name: faraday
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '7.0'
39
+ version: '1.0'
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: '9.0'
42
+ version: '3.0'
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: '7.0'
49
+ version: '1.0'
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: '9.0'
52
+ version: '3.0'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: activerecord
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -99,19 +99,19 @@ dependencies:
99
99
  - !ruby/object:Gem::Version
100
100
  version: '3.0'
101
101
  - !ruby/object:Gem::Dependency
102
- name: webmock
102
+ name: rubocop
103
103
  requirement: !ruby/object:Gem::Requirement
104
104
  requirements:
105
105
  - - "~>"
106
106
  - !ruby/object:Gem::Version
107
- version: '3.0'
107
+ version: '1.0'
108
108
  type: :development
109
109
  prerelease: false
110
110
  version_requirements: !ruby/object:Gem::Requirement
111
111
  requirements:
112
112
  - - "~>"
113
113
  - !ruby/object:Gem::Version
114
- version: '3.0'
114
+ version: '1.0'
115
115
  - !ruby/object:Gem::Dependency
116
116
  name: sqlite3
117
117
  requirement: !ruby/object:Gem::Requirement
@@ -127,23 +127,23 @@ dependencies:
127
127
  - !ruby/object:Gem::Version
128
128
  version: '2.0'
129
129
  - !ruby/object:Gem::Dependency
130
- name: rubocop
130
+ name: webmock
131
131
  requirement: !ruby/object:Gem::Requirement
132
132
  requirements:
133
133
  - - "~>"
134
134
  - !ruby/object:Gem::Version
135
- version: '1.0'
135
+ version: '3.0'
136
136
  type: :development
137
137
  prerelease: false
138
138
  version_requirements: !ruby/object:Gem::Requirement
139
139
  requirements:
140
140
  - - "~>"
141
141
  - !ruby/object:Gem::Version
142
- version: '1.0'
143
- description: Automatically tracks token usage and costs for LLM API calls (OpenAI,
144
- Anthropic, Google Gemini, and more). Works as Faraday middleware — plugs into any
145
- Ruby HTTP client. Provides ActiveRecord storage, per-user/per-feature attribution,
146
- and budget alerts.
142
+ version: '3.0'
143
+ description: Tracks token usage and estimated costs for OpenAI, Anthropic, Google
144
+ Gemini, OpenRouter, DeepSeek, and OpenAI-compatible calls. Works as Faraday middleware
145
+ for Ruby clients, with ActiveRecord storage, per-user/per-feature attribution, budget
146
+ alerts, and budget enforcement.
147
147
  email:
148
148
  - sergey@mm.st
149
149
  executables: []
@@ -151,26 +151,40 @@ extensions: []
151
151
  extra_rdoc_files: []
152
152
  files:
153
153
  - ".rspec"
154
+ - ".rubocop.yml"
154
155
  - CHANGELOG.md
155
156
  - LICENSE.txt
156
157
  - README.md
157
158
  - Rakefile
158
159
  - lib/llm_cost_tracker.rb
160
+ - lib/llm_cost_tracker/budget.rb
159
161
  - lib/llm_cost_tracker/configuration.rb
162
+ - lib/llm_cost_tracker/errors.rb
163
+ - lib/llm_cost_tracker/event_metadata.rb
164
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
160
165
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
166
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb
161
167
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
162
168
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
169
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb
170
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb
171
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb
172
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb
163
173
  - lib/llm_cost_tracker/llm_api_call.rb
164
174
  - lib/llm_cost_tracker/middleware/faraday.rb
165
175
  - lib/llm_cost_tracker/parsers/anthropic.rb
166
176
  - lib/llm_cost_tracker/parsers/base.rb
167
177
  - lib/llm_cost_tracker/parsers/gemini.rb
168
178
  - lib/llm_cost_tracker/parsers/openai.rb
179
+ - lib/llm_cost_tracker/parsers/openai_compatible.rb
169
180
  - lib/llm_cost_tracker/parsers/registry.rb
181
+ - lib/llm_cost_tracker/price_registry.rb
182
+ - lib/llm_cost_tracker/prices.json
170
183
  - lib/llm_cost_tracker/pricing.rb
171
184
  - lib/llm_cost_tracker/railtie.rb
172
185
  - lib/llm_cost_tracker/storage/active_record_store.rb
173
186
  - lib/llm_cost_tracker/tracker.rb
187
+ - lib/llm_cost_tracker/unknown_pricing.rb
174
188
  - lib/llm_cost_tracker/version.rb
175
189
  - llm_cost_tracker.gemspec
176
190
  homepage: https://github.com/sergey-homenko/llm_cost_tracker
@@ -179,6 +193,7 @@ licenses:
179
193
  metadata:
180
194
  homepage_uri: https://github.com/sergey-homenko/llm_cost_tracker
181
195
  changelog_uri: https://github.com/sergey-homenko/llm_cost_tracker/blob/main/CHANGELOG.md
196
+ rubygems_mfa_required: 'true'
182
197
  post_install_message:
183
198
  rdoc_options: []
184
199
  require_paths:
@@ -197,5 +212,5 @@ requirements: []
197
212
  rubygems_version: 3.5.9
198
213
  signing_key:
199
214
  specification_version: 4
200
- summary: Provider-agnostic LLM API cost tracking for Ruby
215
+ summary: Self-hosted LLM API cost guardrails for Ruby and Rails
201
216
  test_files: []