llm_cost_tracker 0.2.0.alpha2 → 0.2.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 +28 -1
- data/README.md +4 -3
- 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 +13 -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 +42 -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/filter.rb +0 -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 +106 -74
- data/app/views/llm_cost_tracker/calls/show.html.erb +58 -1
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +201 -111
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +178 -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 +24 -0
- data/lib/llm_cost_tracker/engine.rb +2 -0
- data/lib/llm_cost_tracker/llm_api_call.rb +1 -1
- data/lib/llm_cost_tracker/price_registry.rb +17 -6
- data/lib/llm_cost_tracker/pricing.rb +19 -6
- data/lib/llm_cost_tracker/retention.rb +34 -0
- data/lib/llm_cost_tracker/tag_query.rb +7 -2
- data/lib/llm_cost_tracker/tags_column.rb +13 -1
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +1 -0
- data/lib/tasks/llm_cost_tracker.rake +8 -0
- data/llm_cost_tracker.gemspec +1 -2
- metadata +17 -5
- data/PLAN_0.2.md +0 -488
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
3
5
|
module LlmCostTracker
|
|
4
6
|
# Calculates costs from price entries expressed in USD per 1M tokens.
|
|
5
7
|
module Pricing
|
|
6
8
|
PRICES = PriceRegistry.builtin_prices
|
|
9
|
+
MUTEX = Monitor.new
|
|
7
10
|
|
|
8
11
|
class << self
|
|
9
12
|
# Estimate model cost from token counts.
|
|
@@ -59,9 +62,14 @@ module LlmCostTracker
|
|
|
59
62
|
cached = @prices_cache
|
|
60
63
|
return cached[:value] if cached && cached[:key] == cache_key
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
MUTEX.synchronize do
|
|
66
|
+
cached = @prices_cache
|
|
67
|
+
return cached[:value] if cached && cached[:key] == cache_key
|
|
68
|
+
|
|
69
|
+
value = PRICES.merge(file_prices).merge(overrides).freeze
|
|
70
|
+
@prices_cache = { key: cache_key, value: value }.freeze
|
|
71
|
+
value
|
|
72
|
+
end
|
|
65
73
|
end
|
|
66
74
|
|
|
67
75
|
private
|
|
@@ -116,9 +124,14 @@ module LlmCostTracker
|
|
|
116
124
|
cached = @sorted_price_keys_cache
|
|
117
125
|
return cached[:keys] if cached && cached[:table].equal?(table)
|
|
118
126
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
MUTEX.synchronize do
|
|
128
|
+
cached = @sorted_price_keys_cache
|
|
129
|
+
return cached[:keys] if cached && cached[:table].equal?(table)
|
|
130
|
+
|
|
131
|
+
keys = table.keys.sort_by { |key| -key.length }
|
|
132
|
+
@sorted_price_keys_cache = { table: table, keys: keys }.freeze
|
|
133
|
+
keys
|
|
134
|
+
end
|
|
122
135
|
end
|
|
123
136
|
end
|
|
124
137
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Retention
|
|
5
|
+
DEFAULT_BATCH_SIZE = 5_000
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def prune(older_than:, batch_size: DEFAULT_BATCH_SIZE, now: Time.now.utc)
|
|
9
|
+
cutoff = resolve_cutoff(older_than, now)
|
|
10
|
+
require_relative "llm_api_call" unless defined?(LlmCostTracker::LlmApiCall)
|
|
11
|
+
|
|
12
|
+
deleted = 0
|
|
13
|
+
loop do
|
|
14
|
+
batch = LlmCostTracker::LlmApiCall.where(tracked_at: ...cutoff).limit(batch_size).delete_all
|
|
15
|
+
deleted += batch
|
|
16
|
+
break if batch < batch_size
|
|
17
|
+
end
|
|
18
|
+
deleted
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def resolve_cutoff(older_than, now)
|
|
24
|
+
case older_than
|
|
25
|
+
when Time, DateTime then older_than.utc
|
|
26
|
+
when ActiveSupport::Duration then now - older_than
|
|
27
|
+
when Integer then now - (older_than * 86_400)
|
|
28
|
+
else
|
|
29
|
+
raise ArgumentError, "older_than must be a Duration, Time, or Integer days: #{older_than.inspect}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -9,7 +9,8 @@ module LlmCostTracker
|
|
|
9
9
|
normalized_tags = normalize_tags(tags)
|
|
10
10
|
return model.all if normalized_tags.empty?
|
|
11
11
|
|
|
12
|
-
return
|
|
12
|
+
return postgres_json_query(model, normalized_tags) if model.tags_jsonb_column?
|
|
13
|
+
return mysql_json_query(model, normalized_tags) if model.tags_mysql_json_column?
|
|
13
14
|
|
|
14
15
|
text_query(model, normalized_tags)
|
|
15
16
|
end
|
|
@@ -20,10 +21,14 @@ module LlmCostTracker
|
|
|
20
21
|
|
|
21
22
|
private
|
|
22
23
|
|
|
23
|
-
def
|
|
24
|
+
def postgres_json_query(model, tags)
|
|
24
25
|
model.where("tags @> ?::jsonb", tags.to_json)
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
def mysql_json_query(model, tags)
|
|
29
|
+
model.where("JSON_CONTAINS(tags, ?)", tags.to_json)
|
|
30
|
+
end
|
|
31
|
+
|
|
27
32
|
def text_query(model, tags)
|
|
28
33
|
tags.reduce(model.all) do |relation, (key, value)|
|
|
29
34
|
relation.where("tags LIKE ? ESCAPE '\\'", "%#{model.sanitize_sql_like(json_tag_fragment(key, value))}%")
|
|
@@ -3,10 +3,22 @@
|
|
|
3
3
|
module LlmCostTracker
|
|
4
4
|
module TagsColumn
|
|
5
5
|
def tags_json_column?
|
|
6
|
+
tags_jsonb_column? || tags_mysql_json_column?
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def tags_jsonb_column?
|
|
10
|
+
column = columns_hash["tags"]
|
|
11
|
+
return false unless column
|
|
12
|
+
|
|
13
|
+
column.type == :jsonb || column.sql_type.to_s.downcase == "jsonb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tags_mysql_json_column?
|
|
6
17
|
column = columns_hash["tags"]
|
|
7
18
|
return false unless column
|
|
19
|
+
return false if tags_jsonb_column?
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
column.type == :json && connection.adapter_name.match?(/mysql/i)
|
|
10
22
|
end
|
|
11
23
|
|
|
12
24
|
def latency_column?
|
data/lib/llm_cost_tracker.rb
CHANGED
|
@@ -28,6 +28,7 @@ require_relative "llm_cost_tracker/tag_key"
|
|
|
28
28
|
require_relative "llm_cost_tracker/tag_query"
|
|
29
29
|
require_relative "llm_cost_tracker/tag_accessors"
|
|
30
30
|
require_relative "llm_cost_tracker/tracker"
|
|
31
|
+
require_relative "llm_cost_tracker/retention"
|
|
31
32
|
require_relative "llm_cost_tracker/report_data"
|
|
32
33
|
require_relative "llm_cost_tracker/report_formatter"
|
|
33
34
|
require_relative "llm_cost_tracker/report"
|
|
@@ -6,4 +6,12 @@ namespace :llm_cost_tracker do
|
|
|
6
6
|
days = (ENV["DAYS"] || LlmCostTracker::Report::DEFAULT_DAYS).to_i
|
|
7
7
|
puts LlmCostTracker::Report.generate(days: days)
|
|
8
8
|
end
|
|
9
|
+
|
|
10
|
+
desc "Delete llm_api_calls older than DAYS (default: 90). Use BATCH_SIZE=N to tune."
|
|
11
|
+
task prune: :environment do
|
|
12
|
+
days = (ENV["DAYS"] || 90).to_i
|
|
13
|
+
batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Retention::DEFAULT_BATCH_SIZE).to_i
|
|
14
|
+
deleted = LlmCostTracker::Retention.prune(older_than: days, batch_size: batch_size)
|
|
15
|
+
puts "llm_cost_tracker: pruned #{deleted} calls older than #{days} days"
|
|
16
|
+
end
|
|
9
17
|
end
|
data/llm_cost_tracker.gemspec
CHANGED
|
@@ -19,7 +19,6 @@ Gem::Specification.new do |spec|
|
|
|
19
19
|
spec.required_ruby_version = ">= 3.3.0"
|
|
20
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"
|
|
25
24
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
@@ -34,7 +33,7 @@ Gem::Specification.new do |spec|
|
|
|
34
33
|
spec.require_paths = ["lib"]
|
|
35
34
|
|
|
36
35
|
spec.add_dependency "activesupport", ">= 7.1", "< 9.0"
|
|
37
|
-
spec.add_dependency "csv", "
|
|
36
|
+
spec.add_dependency "csv", "~> 3.0"
|
|
38
37
|
spec.add_dependency "faraday", ">= 2.0", "< 3.0"
|
|
39
38
|
|
|
40
39
|
spec.add_development_dependency "activerecord", ">= 7.1", "< 9.0"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergii Khomenko
|
|
@@ -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
|
|
@@ -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
|
|
@@ -265,6 +277,7 @@ files:
|
|
|
265
277
|
- lib/llm_cost_tracker/report.rb
|
|
266
278
|
- lib/llm_cost_tracker/report_data.rb
|
|
267
279
|
- lib/llm_cost_tracker/report_formatter.rb
|
|
280
|
+
- lib/llm_cost_tracker/retention.rb
|
|
268
281
|
- lib/llm_cost_tracker/storage/active_record_store.rb
|
|
269
282
|
- lib/llm_cost_tracker/tag_accessors.rb
|
|
270
283
|
- lib/llm_cost_tracker/tag_key.rb
|
|
@@ -280,7 +293,6 @@ licenses:
|
|
|
280
293
|
- MIT
|
|
281
294
|
metadata:
|
|
282
295
|
homepage_uri: https://github.com/sergey-homenko/llm_cost_tracker
|
|
283
|
-
source_code_uri: https://github.com/sergey-homenko/llm_cost_tracker
|
|
284
296
|
bug_tracker_uri: https://github.com/sergey-homenko/llm_cost_tracker/issues
|
|
285
297
|
changelog_uri: https://github.com/sergey-homenko/llm_cost_tracker/blob/main/CHANGELOG.md
|
|
286
298
|
rubygems_mfa_required: 'true'
|