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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +34 -14
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +101 -19
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +65 -0
- data/lib/llm_cost_tracker/budget.rb +85 -21
- data/lib/llm_cost_tracker/configuration.rb +4 -0
- data/lib/llm_cost_tracker/cost.rb +1 -2
- data/lib/llm_cost_tracker/errors.rb +22 -3
- data/lib/llm_cost_tracker/event.rb +4 -0
- data/lib/llm_cost_tracker/event_metadata.rb +21 -15
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +66 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +15 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +11 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +1 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +27 -9
- data/lib/llm_cost_tracker/parsed_usage.rb +16 -7
- data/lib/llm_cost_tracker/parsers/anthropic.rb +7 -6
- data/lib/llm_cost_tracker/parsers/base.rb +2 -1
- data/lib/llm_cost_tracker/parsers/gemini.rb +5 -2
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +18 -5
- data/lib/llm_cost_tracker/period_total.rb +9 -0
- data/lib/llm_cost_tracker/price_registry.rb +14 -4
- data/lib/llm_cost_tracker/price_sync/merger.rb +1 -1
- data/lib/llm_cost_tracker/price_sync/raw_price.rb +3 -5
- data/lib/llm_cost_tracker/price_sync/sources/litellm.rb +2 -3
- data/lib/llm_cost_tracker/price_sync/sources/open_router.rb +2 -3
- data/lib/llm_cost_tracker/prices.json +30 -30
- data/lib/llm_cost_tracker/pricing.rb +44 -32
- data/lib/llm_cost_tracker/railtie.rb +2 -0
- data/lib/llm_cost_tracker/storage/active_record_rollups.rb +122 -0
- data/lib/llm_cost_tracker/storage/active_record_store.rb +38 -13
- data/lib/llm_cost_tracker/stream_collector.rb +5 -3
- data/lib/llm_cost_tracker/tags_column.rb +19 -0
- data/lib/llm_cost_tracker/tracker.rb +58 -32
- data/lib/llm_cost_tracker/unknown_pricing.rb +14 -0
- data/lib/llm_cost_tracker/usage_breakdown.rb +30 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +12 -3
- metadata +10 -4
- 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
|
data/lib/llm_cost_tracker.rb
CHANGED
|
@@ -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,
|
|
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(
|
|
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.
|
|
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-
|
|
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.
|
|
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
|
data/llm_cost_tracker.gemspec
DELETED
|
@@ -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
|