llm_cost_tracker 0.3.3 → 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 +15 -0
- data/README.md +32 -15
- 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 +76 -22
- 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_monthly_totals_generator.rb → add_period_totals_generator.rb} +4 -4
- 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 +10 -3
- 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/parsed_usage.rb +16 -7
- data/lib/llm_cost_tracker/parsers/anthropic.rb +7 -6
- 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 -1
- data/lib/llm_cost_tracker/storage/active_record_rollups.rb +122 -0
- data/lib/llm_cost_tracker/storage/active_record_store.rb +33 -80
- data/lib/llm_cost_tracker/stream_collector.rb +4 -2
- data/lib/llm_cost_tracker/tags_column.rb +19 -0
- data/lib/llm_cost_tracker/tracker.rb +54 -32
- 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 +10 -3
- metadata +8 -4
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_monthly_totals_to_llm_cost_tracker.rb.erb +0 -48
- data/lib/llm_cost_tracker/monthly_total.rb +0 -9
|
@@ -16,28 +16,70 @@ module LlmCostTracker
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def record(provider:, model:, input_tokens:, output_tokens:, latency_ms: nil, stream: false,
|
|
19
|
-
usage_source: nil, provider_response_id: nil, metadata: {})
|
|
19
|
+
usage_source: nil, provider_response_id: nil, pricing_mode: nil, metadata: {})
|
|
20
20
|
return unless LlmCostTracker.configuration.enabled
|
|
21
21
|
|
|
22
|
-
usage =
|
|
22
|
+
usage = usage_data(input_tokens, output_tokens, metadata, pricing_mode)
|
|
23
|
+
cost_data = cost_for_usage(provider, model, usage)
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
UnknownPricing.handle!(model) unless cost_data
|
|
26
|
+
|
|
27
|
+
event = build_event(
|
|
28
|
+
provider: provider,
|
|
29
|
+
model: model,
|
|
30
|
+
usage: usage,
|
|
31
|
+
cost_data: cost_data,
|
|
32
|
+
metadata: metadata,
|
|
33
|
+
latency_ms: latency_ms,
|
|
34
|
+
stream: stream,
|
|
35
|
+
usage_source: usage_source,
|
|
36
|
+
provider_response_id: provider_response_id
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
ActiveSupport::Notifications.instrument(EVENT_NAME, event.to_h)
|
|
40
|
+
|
|
41
|
+
stored = store(event)
|
|
42
|
+
Budget.check!(event) unless stored == false
|
|
43
|
+
|
|
44
|
+
event
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def usage_data(input_tokens, output_tokens, metadata, pricing_mode)
|
|
50
|
+
metadata = metadata.merge(pricing_mode: pricing_mode) unless pricing_mode.nil?
|
|
51
|
+
|
|
52
|
+
EventMetadata.usage_data(
|
|
53
|
+
input_tokens,
|
|
54
|
+
output_tokens,
|
|
55
|
+
metadata
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cost_for_usage(provider, model, usage)
|
|
60
|
+
Pricing.cost_for(
|
|
61
|
+
provider: provider,
|
|
25
62
|
model: model,
|
|
26
63
|
input_tokens: usage[:input_tokens],
|
|
27
64
|
output_tokens: usage[:output_tokens],
|
|
28
|
-
cached_input_tokens: usage[:cached_input_tokens],
|
|
29
65
|
cache_read_input_tokens: usage[:cache_read_input_tokens],
|
|
30
|
-
|
|
66
|
+
cache_write_input_tokens: usage[:cache_write_input_tokens],
|
|
67
|
+
pricing_mode: usage[:pricing_mode]
|
|
31
68
|
)
|
|
69
|
+
end
|
|
32
70
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
71
|
+
def build_event(provider:, model:, usage:, cost_data:, metadata:, latency_ms:, stream:, usage_source:,
|
|
72
|
+
provider_response_id:)
|
|
73
|
+
Event.new(
|
|
36
74
|
provider: provider,
|
|
37
75
|
model: model,
|
|
38
76
|
input_tokens: usage[:input_tokens],
|
|
39
77
|
output_tokens: usage[:output_tokens],
|
|
40
78
|
total_tokens: usage[:total_tokens],
|
|
79
|
+
cache_read_input_tokens: usage[:cache_read_input_tokens],
|
|
80
|
+
cache_write_input_tokens: usage[:cache_write_input_tokens],
|
|
81
|
+
hidden_output_tokens: usage[:hidden_output_tokens],
|
|
82
|
+
pricing_mode: usage[:pricing_mode],
|
|
41
83
|
cost: cost_data,
|
|
42
84
|
tags: LlmCostTracker.configuration.default_tags.merge(EventMetadata.tags(metadata)).freeze,
|
|
43
85
|
latency_ms: normalized_latency_ms(latency_ms),
|
|
@@ -46,17 +88,8 @@ module LlmCostTracker
|
|
|
46
88
|
provider_response_id: normalized_provider_response_id(provider_response_id),
|
|
47
89
|
tracked_at: Time.now.utc
|
|
48
90
|
)
|
|
49
|
-
|
|
50
|
-
ActiveSupport::Notifications.instrument(EVENT_NAME, event.to_h)
|
|
51
|
-
|
|
52
|
-
stored = store(event)
|
|
53
|
-
Budget.check!(event) unless stored == false
|
|
54
|
-
|
|
55
|
-
event
|
|
56
91
|
end
|
|
57
92
|
|
|
58
|
-
private
|
|
59
|
-
|
|
60
93
|
def store(event)
|
|
61
94
|
config = LlmCostTracker.configuration
|
|
62
95
|
case config.storage_backend
|
|
@@ -73,7 +106,7 @@ module LlmCostTracker
|
|
|
73
106
|
|
|
74
107
|
def log_event(event, config)
|
|
75
108
|
message = "#{event.provider}/#{event.model} " \
|
|
76
|
-
"tokens=#{event.
|
|
109
|
+
"tokens=#{event.total_tokens} " \
|
|
77
110
|
"cost=#{log_cost_label(event)}"
|
|
78
111
|
message += " latency=#{event.latency_ms}ms" if event.latency_ms
|
|
79
112
|
message += " stream=#{event.stream}" if event.stream
|
|
@@ -84,9 +117,7 @@ module LlmCostTracker
|
|
|
84
117
|
event
|
|
85
118
|
end
|
|
86
119
|
|
|
87
|
-
def log_cost_label(event)
|
|
88
|
-
event.cost ? "$#{format('%.6f', event.cost.total_cost)}" : "unknown"
|
|
89
|
-
end
|
|
120
|
+
def log_cost_label(event) = event.cost ? "$#{format('%.6f', event.cost.total_cost)}" : "unknown"
|
|
90
121
|
|
|
91
122
|
def active_record_save(event)
|
|
92
123
|
require_relative "llm_api_call" unless defined?(LlmCostTracker::LlmApiCall)
|
|
@@ -115,11 +146,7 @@ module LlmCostTracker
|
|
|
115
146
|
end
|
|
116
147
|
end
|
|
117
148
|
|
|
118
|
-
def normalized_latency_ms(latency_ms)
|
|
119
|
-
return nil if latency_ms.nil?
|
|
120
|
-
|
|
121
|
-
[latency_ms.to_i, 0].max
|
|
122
|
-
end
|
|
149
|
+
def normalized_latency_ms(latency_ms) = latency_ms.nil? ? nil : [latency_ms.to_i, 0].max
|
|
123
150
|
|
|
124
151
|
def normalized_usage_source(value)
|
|
125
152
|
return nil if value.nil?
|
|
@@ -128,12 +155,7 @@ module LlmCostTracker
|
|
|
128
155
|
USAGE_SOURCES.include?(symbol) ? symbol.to_s : nil
|
|
129
156
|
end
|
|
130
157
|
|
|
131
|
-
def normalized_provider_response_id(value)
|
|
132
|
-
return nil if value.nil?
|
|
133
|
-
|
|
134
|
-
string = value.to_s
|
|
135
|
-
string.empty? ? nil : string
|
|
136
|
-
end
|
|
158
|
+
def normalized_provider_response_id(value) = value.nil? || value.to_s.empty? ? nil : value.to_s
|
|
137
159
|
end
|
|
138
160
|
end
|
|
139
161
|
end
|
|
@@ -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"
|
|
@@ -69,7 +70,7 @@ module LlmCostTracker
|
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def track(provider:, model:, input_tokens:, output_tokens:, latency_ms: nil, stream: false, usage_source: :manual,
|
|
72
|
-
enforce_budget: false, provider_response_id: nil, **metadata)
|
|
73
|
+
enforce_budget: false, provider_response_id: nil, pricing_mode: nil, **metadata)
|
|
73
74
|
enforce_budget! if enforce_budget
|
|
74
75
|
Tracker.record(
|
|
75
76
|
provider: provider.to_s,
|
|
@@ -80,11 +81,13 @@ module LlmCostTracker
|
|
|
80
81
|
stream: stream,
|
|
81
82
|
usage_source: usage_source,
|
|
82
83
|
provider_response_id: provider_response_id,
|
|
84
|
+
pricing_mode: pricing_mode,
|
|
83
85
|
metadata: metadata
|
|
84
86
|
)
|
|
85
87
|
end
|
|
86
88
|
|
|
87
|
-
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)
|
|
88
91
|
require_relative "llm_cost_tracker/stream_collector"
|
|
89
92
|
enforce_budget! if enforce_budget
|
|
90
93
|
collector = StreamCollector.new(
|
|
@@ -92,6 +95,7 @@ module LlmCostTracker
|
|
|
92
95
|
model: model,
|
|
93
96
|
latency_ms: latency_ms,
|
|
94
97
|
provider_response_id: provider_response_id,
|
|
98
|
+
pricing_mode: pricing_mode,
|
|
95
99
|
metadata: metadata
|
|
96
100
|
)
|
|
97
101
|
yield collector
|
|
@@ -107,7 +111,10 @@ module LlmCostTracker
|
|
|
107
111
|
return unless config.budget_exceeded_behavior == :block_requests
|
|
108
112
|
return if config.active_record?
|
|
109
113
|
|
|
110
|
-
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
|
+
)
|
|
111
118
|
end
|
|
112
119
|
end
|
|
113
120
|
end
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergii Khomenko
|
|
@@ -278,15 +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/
|
|
281
|
+
- lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb
|
|
282
282
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb
|
|
283
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
|
|
284
285
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
|
|
285
286
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
|
|
286
287
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb
|
|
287
|
-
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/
|
|
288
|
+
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb
|
|
288
289
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb
|
|
289
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
|
|
290
292
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
|
|
291
293
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
|
|
292
294
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb
|
|
@@ -297,7 +299,6 @@ files:
|
|
|
297
299
|
- lib/llm_cost_tracker/llm_api_call.rb
|
|
298
300
|
- lib/llm_cost_tracker/logging.rb
|
|
299
301
|
- lib/llm_cost_tracker/middleware/faraday.rb
|
|
300
|
-
- lib/llm_cost_tracker/monthly_total.rb
|
|
301
302
|
- lib/llm_cost_tracker/parameter_hash.rb
|
|
302
303
|
- lib/llm_cost_tracker/parsed_usage.rb
|
|
303
304
|
- lib/llm_cost_tracker/parsers/anthropic.rb
|
|
@@ -309,6 +310,7 @@ files:
|
|
|
309
310
|
- lib/llm_cost_tracker/parsers/registry.rb
|
|
310
311
|
- lib/llm_cost_tracker/parsers/sse.rb
|
|
311
312
|
- lib/llm_cost_tracker/period_grouping.rb
|
|
313
|
+
- lib/llm_cost_tracker/period_total.rb
|
|
312
314
|
- lib/llm_cost_tracker/price_registry.rb
|
|
313
315
|
- lib/llm_cost_tracker/price_sync.rb
|
|
314
316
|
- lib/llm_cost_tracker/price_sync/fetcher.rb
|
|
@@ -330,6 +332,7 @@ files:
|
|
|
330
332
|
- lib/llm_cost_tracker/report_data.rb
|
|
331
333
|
- lib/llm_cost_tracker/report_formatter.rb
|
|
332
334
|
- lib/llm_cost_tracker/retention.rb
|
|
335
|
+
- lib/llm_cost_tracker/storage/active_record_rollups.rb
|
|
333
336
|
- lib/llm_cost_tracker/storage/active_record_store.rb
|
|
334
337
|
- lib/llm_cost_tracker/stream_collector.rb
|
|
335
338
|
- lib/llm_cost_tracker/tag_accessors.rb
|
|
@@ -338,6 +341,7 @@ files:
|
|
|
338
341
|
- lib/llm_cost_tracker/tags_column.rb
|
|
339
342
|
- lib/llm_cost_tracker/tracker.rb
|
|
340
343
|
- lib/llm_cost_tracker/unknown_pricing.rb
|
|
344
|
+
- lib/llm_cost_tracker/usage_breakdown.rb
|
|
341
345
|
- lib/llm_cost_tracker/value_helpers.rb
|
|
342
346
|
- lib/llm_cost_tracker/version.rb
|
|
343
347
|
- lib/tasks/llm_cost_tracker.rake
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
class AddMonthlyTotalsToLlmCostTracker < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
-
def up
|
|
3
|
-
create_table :llm_cost_tracker_monthly_totals do |t|
|
|
4
|
-
t.date :month_start, null: false
|
|
5
|
-
t.decimal :total_cost, precision: 20, scale: 8, null: false, default: 0
|
|
6
|
-
|
|
7
|
-
t.timestamps
|
|
8
|
-
end unless table_exists?(:llm_cost_tracker_monthly_totals)
|
|
9
|
-
|
|
10
|
-
add_index :llm_cost_tracker_monthly_totals, :month_start,
|
|
11
|
-
unique: true unless index_exists?(:llm_cost_tracker_monthly_totals, :month_start)
|
|
12
|
-
|
|
13
|
-
backfill_monthly_totals
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def down
|
|
17
|
-
remove_index :llm_cost_tracker_monthly_totals, :month_start if index_exists?(:llm_cost_tracker_monthly_totals, :month_start)
|
|
18
|
-
drop_table :llm_cost_tracker_monthly_totals if table_exists?(:llm_cost_tracker_monthly_totals)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def backfill_monthly_totals
|
|
24
|
-
return unless table_exists?(:llm_api_calls)
|
|
25
|
-
|
|
26
|
-
execute <<~SQL
|
|
27
|
-
INSERT INTO llm_cost_tracker_monthly_totals (month_start, total_cost, created_at, updated_at)
|
|
28
|
-
SELECT #{month_bucket_sql} AS month_start,
|
|
29
|
-
SUM(total_cost) AS total_cost,
|
|
30
|
-
CURRENT_TIMESTAMP,
|
|
31
|
-
CURRENT_TIMESTAMP
|
|
32
|
-
FROM llm_api_calls
|
|
33
|
-
WHERE total_cost IS NOT NULL
|
|
34
|
-
GROUP BY #{month_bucket_sql}
|
|
35
|
-
SQL
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def month_bucket_sql
|
|
39
|
-
case connection.adapter_name
|
|
40
|
-
when /postgres/i
|
|
41
|
-
"DATE_TRUNC('month', tracked_at)::date"
|
|
42
|
-
when /mysql/i
|
|
43
|
-
"DATE_FORMAT(tracked_at, '%Y-%m-01')"
|
|
44
|
-
else
|
|
45
|
-
"strftime('%Y-%m-01', tracked_at)"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|