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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -1
- data/README.md +114 -70
- data/Rakefile +2 -0
- data/app/assets/llm_cost_tracker/application.css +760 -0
- data/app/controllers/llm_cost_tracker/application_controller.rb +1 -7
- data/app/controllers/llm_cost_tracker/assets_controller.rb +12 -0
- data/app/controllers/llm_cost_tracker/calls_controller.rb +29 -12
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +5 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +46 -5
- data/app/helpers/llm_cost_tracker/chart_helper.rb +133 -0
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +47 -0
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +34 -0
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +58 -0
- data/app/helpers/llm_cost_tracker/pagination_helper.rb +18 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +16 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +22 -3
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +16 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +79 -0
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +19 -46
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +17 -8
- data/app/services/llm_cost_tracker/pagination.rb +6 -0
- data/app/views/layouts/llm_cost_tracker/application.html.erb +35 -333
- data/app/views/llm_cost_tracker/calls/index.html.erb +116 -74
- data/app/views/llm_cost_tracker/calls/show.html.erb +58 -1
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +211 -111
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +224 -78
- data/app/views/llm_cost_tracker/errors/database.html.erb +3 -3
- data/app/views/llm_cost_tracker/errors/invalid_filter.html.erb +3 -3
- data/app/views/llm_cost_tracker/errors/not_found.html.erb +3 -3
- data/app/views/llm_cost_tracker/models/index.html.erb +66 -58
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +16 -0
- data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +18 -0
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +15 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +3 -2
- data/app/views/llm_cost_tracker/tags/index.html.erb +55 -12
- data/app/views/llm_cost_tracker/tags/show.html.erb +88 -39
- data/config/routes.rb +3 -0
- data/lib/llm_cost_tracker/assets.rb +19 -0
- data/lib/llm_cost_tracker/configuration.rb +78 -42
- data/lib/llm_cost_tracker/engine.rb +2 -0
- data/lib/llm_cost_tracker/event.rb +2 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +25 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +4 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +8 -1
- data/lib/llm_cost_tracker/llm_api_call.rb +9 -1
- data/lib/llm_cost_tracker/middleware/faraday.rb +57 -9
- data/lib/llm_cost_tracker/parsed_usage.rb +7 -3
- data/lib/llm_cost_tracker/parsers/anthropic.rb +79 -1
- data/lib/llm_cost_tracker/parsers/base.rb +17 -5
- data/lib/llm_cost_tracker/parsers/gemini.rb +59 -6
- data/lib/llm_cost_tracker/parsers/openai.rb +8 -0
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +8 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +55 -1
- data/lib/llm_cost_tracker/parsers/registry.rb +15 -3
- data/lib/llm_cost_tracker/parsers/sse.rb +81 -0
- data/lib/llm_cost_tracker/price_registry.rb +18 -7
- data/lib/llm_cost_tracker/price_sync/fetcher.rb +72 -0
- data/lib/llm_cost_tracker/price_sync/merger.rb +72 -0
- data/lib/llm_cost_tracker/price_sync/model_catalog.rb +77 -0
- data/lib/llm_cost_tracker/price_sync/raw_price.rb +35 -0
- data/lib/llm_cost_tracker/price_sync/source.rb +29 -0
- data/lib/llm_cost_tracker/price_sync/source_result.rb +7 -0
- data/lib/llm_cost_tracker/price_sync/sources/litellm.rb +91 -0
- data/lib/llm_cost_tracker/price_sync/sources/open_router.rb +94 -0
- data/lib/llm_cost_tracker/price_sync/validator.rb +66 -0
- data/lib/llm_cost_tracker/price_sync.rb +310 -0
- data/lib/llm_cost_tracker/pricing.rb +19 -6
- data/lib/llm_cost_tracker/retention.rb +34 -0
- data/lib/llm_cost_tracker/storage/active_record_store.rb +3 -1
- data/lib/llm_cost_tracker/stream_collector.rb +158 -0
- data/lib/llm_cost_tracker/tag_query.rb +7 -2
- data/lib/llm_cost_tracker/tags_column.rb +21 -1
- data/lib/llm_cost_tracker/tracker.rb +15 -12
- data/lib/llm_cost_tracker/value_helpers.rb +40 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +51 -29
- data/lib/tasks/llm_cost_tracker.rake +124 -0
- data/llm_cost_tracker.gemspec +9 -8
- metadata +40 -12
- 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
|
data/lib/llm_cost_tracker.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
90
|
-
return if
|
|
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
|
data/llm_cost_tracker.gemspec
CHANGED
|
@@ -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
|
|
12
|
-
spec.description = "Tracks token usage and estimated costs for OpenAI, Anthropic,
|
|
13
|
-
"OpenRouter, DeepSeek, and OpenAI-compatible
|
|
14
|
-
"Works
|
|
15
|
-
"
|
|
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", "
|
|
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.
|
|
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-
|
|
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,
|
|
184
|
-
Gemini, OpenRouter, DeepSeek, and OpenAI-compatible
|
|
185
|
-
|
|
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
|
|
333
|
+
summary: Self-hosted LLM usage and cost tracking for Ruby and Rails
|
|
306
334
|
test_files: []
|