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
|
@@ -3,32 +3,31 @@
|
|
|
3
3
|
module LlmCostTracker
|
|
4
4
|
module EventMetadata
|
|
5
5
|
INTERNAL_TAG_KEYS = %w[
|
|
6
|
-
cache_creation_input_tokens
|
|
7
|
-
cache_creation_tokens
|
|
8
6
|
cache_read_input_tokens
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
cache_write_input_tokens
|
|
8
|
+
hidden_output_tokens
|
|
11
9
|
input_tokens
|
|
12
10
|
output_tokens
|
|
11
|
+
pricing_mode
|
|
13
12
|
provider_response_id
|
|
14
|
-
reasoning_tokens
|
|
15
13
|
total_tokens
|
|
16
14
|
].freeze
|
|
17
15
|
|
|
18
16
|
class << self
|
|
19
17
|
def usage_data(input_tokens, output_tokens, metadata)
|
|
20
18
|
metadata = metadata.to_h.symbolize_keys
|
|
21
|
-
cache_read = first_integer(metadata, :cache_read_input_tokens
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
input_tokens: input_tokens
|
|
26
|
-
output_tokens: output_tokens
|
|
27
|
-
cached_input_tokens: metadata[:cached_input_tokens].to_i,
|
|
19
|
+
cache_read = first_integer(metadata, :cache_read_input_tokens)
|
|
20
|
+
cache_write = first_integer(metadata, :cache_write_input_tokens)
|
|
21
|
+
hidden_output = first_integer(metadata, :hidden_output_tokens)
|
|
22
|
+
breakdown = UsageBreakdown.build(
|
|
23
|
+
input_tokens: input_tokens,
|
|
24
|
+
output_tokens: output_tokens,
|
|
28
25
|
cache_read_input_tokens: cache_read,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
cache_write_input_tokens: cache_write,
|
|
27
|
+
hidden_output_tokens: hidden_output
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
breakdown.to_h.merge(pricing_mode: normalized_pricing_mode(metadata[:pricing_mode])).compact
|
|
32
31
|
end
|
|
33
32
|
|
|
34
33
|
def tags(metadata)
|
|
@@ -41,6 +40,13 @@ module LlmCostTracker
|
|
|
41
40
|
keys.each { |key| return metadata[key].to_i unless metadata[key].nil? }
|
|
42
41
|
0
|
|
43
42
|
end
|
|
43
|
+
|
|
44
|
+
def normalized_pricing_mode(value)
|
|
45
|
+
return nil if value.nil?
|
|
46
|
+
|
|
47
|
+
mode = value.to_s.strip
|
|
48
|
+
mode.empty? || mode == "standard" ? nil : mode
|
|
49
|
+
end
|
|
44
50
|
end
|
|
45
51
|
end
|
|
46
52
|
end
|
|
@@ -5,17 +5,17 @@ require "rails/generators/active_record"
|
|
|
5
5
|
|
|
6
6
|
module LlmCostTracker
|
|
7
7
|
module Generators
|
|
8
|
-
class
|
|
8
|
+
class AddPeriodTotalsGenerator < Rails::Generators::Base
|
|
9
9
|
include ActiveRecord::Generators::Migration
|
|
10
10
|
|
|
11
11
|
source_root File.expand_path("templates", __dir__)
|
|
12
12
|
|
|
13
|
-
desc "Creates a migration to add
|
|
13
|
+
desc "Creates a migration to add llm_cost_tracker_period_totals"
|
|
14
14
|
|
|
15
15
|
def create_migration_file
|
|
16
16
|
migration_template(
|
|
17
|
-
"
|
|
18
|
-
"db/migrate/
|
|
17
|
+
"add_period_totals_to_llm_cost_tracker.rb.erb",
|
|
18
|
+
"db/migrate/add_period_totals_to_llm_cost_tracker.rb"
|
|
19
19
|
)
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Generators
|
|
8
|
+
class AddUsageBreakdownGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Creates a migration to add usage and cost breakdown columns to llm_api_calls"
|
|
14
|
+
|
|
15
|
+
def create_migration_file
|
|
16
|
+
migration_template(
|
|
17
|
+
"add_usage_breakdown_to_llm_api_calls.rb.erb",
|
|
18
|
+
"db/migrate/add_usage_breakdown_to_llm_api_calls.rb"
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def migration_version
|
|
25
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
class AddPeriodTotalsToLlmCostTracker < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
create_table :llm_cost_tracker_period_totals do |t|
|
|
4
|
+
t.string :period, null: false
|
|
5
|
+
t.date :period_start, null: false
|
|
6
|
+
t.decimal :total_cost, precision: 20, scale: 8, null: false, default: 0
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end unless table_exists?(:llm_cost_tracker_period_totals)
|
|
10
|
+
|
|
11
|
+
add_index :llm_cost_tracker_period_totals, [:period, :period_start],
|
|
12
|
+
unique: true unless index_exists?(:llm_cost_tracker_period_totals, [:period, :period_start])
|
|
13
|
+
|
|
14
|
+
backfill_period_totals
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def down
|
|
18
|
+
remove_index :llm_cost_tracker_period_totals, [:period, :period_start] if index_exists?(:llm_cost_tracker_period_totals, [:period, :period_start])
|
|
19
|
+
drop_table :llm_cost_tracker_period_totals if table_exists?(:llm_cost_tracker_period_totals)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def backfill_period_totals
|
|
25
|
+
return unless table_exists?(:llm_api_calls)
|
|
26
|
+
|
|
27
|
+
backfill_period_total("day", day_bucket_sql)
|
|
28
|
+
backfill_period_total("month", month_bucket_sql)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def backfill_period_total(period, bucket_sql)
|
|
32
|
+
execute <<~SQL
|
|
33
|
+
INSERT INTO llm_cost_tracker_period_totals (period, period_start, total_cost, created_at, updated_at)
|
|
34
|
+
SELECT #{connection.quote(period)} AS period,
|
|
35
|
+
#{bucket_sql} AS period_start,
|
|
36
|
+
SUM(total_cost) AS total_cost,
|
|
37
|
+
CURRENT_TIMESTAMP,
|
|
38
|
+
CURRENT_TIMESTAMP
|
|
39
|
+
FROM llm_api_calls
|
|
40
|
+
WHERE total_cost IS NOT NULL
|
|
41
|
+
GROUP BY #{bucket_sql}
|
|
42
|
+
SQL
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def day_bucket_sql
|
|
46
|
+
case connection.adapter_name
|
|
47
|
+
when /postgres/i
|
|
48
|
+
"DATE_TRUNC('day', tracked_at)::date"
|
|
49
|
+
when /mysql/i
|
|
50
|
+
"DATE(tracked_at)"
|
|
51
|
+
else
|
|
52
|
+
"date(tracked_at)"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def month_bucket_sql
|
|
57
|
+
case connection.adapter_name
|
|
58
|
+
when /postgres/i
|
|
59
|
+
"DATE_TRUNC('month', tracked_at)::date"
|
|
60
|
+
when /mysql/i
|
|
61
|
+
"DATE_FORMAT(tracked_at, '%Y-%m-01')"
|
|
62
|
+
else
|
|
63
|
+
"strftime('%Y-%m-01', tracked_at)"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class AddUsageBreakdownToLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
unless column_exists?(:llm_api_calls, :cache_read_input_tokens)
|
|
4
|
+
add_column :llm_api_calls, :cache_read_input_tokens, :integer, null: false, default: 0
|
|
5
|
+
end
|
|
6
|
+
unless column_exists?(:llm_api_calls, :cache_write_input_tokens)
|
|
7
|
+
add_column :llm_api_calls, :cache_write_input_tokens, :integer, null: false, default: 0
|
|
8
|
+
end
|
|
9
|
+
unless column_exists?(:llm_api_calls, :hidden_output_tokens)
|
|
10
|
+
add_column :llm_api_calls, :hidden_output_tokens, :integer, null: false, default: 0
|
|
11
|
+
end
|
|
12
|
+
unless column_exists?(:llm_api_calls, :cache_read_input_cost)
|
|
13
|
+
add_column :llm_api_calls, :cache_read_input_cost, :decimal, precision: 20, scale: 8
|
|
14
|
+
end
|
|
15
|
+
unless column_exists?(:llm_api_calls, :cache_write_input_cost)
|
|
16
|
+
add_column :llm_api_calls, :cache_write_input_cost, :decimal, precision: 20, scale: 8
|
|
17
|
+
end
|
|
18
|
+
add_column :llm_api_calls, :pricing_mode, :string unless column_exists?(:llm_api_calls, :pricing_mode)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def down
|
|
22
|
+
remove_column :llm_api_calls, :pricing_mode if column_exists?(:llm_api_calls, :pricing_mode)
|
|
23
|
+
remove_column :llm_api_calls, :cache_write_input_cost if column_exists?(:llm_api_calls, :cache_write_input_cost)
|
|
24
|
+
remove_column :llm_api_calls, :cache_read_input_cost if column_exists?(:llm_api_calls, :cache_read_input_cost)
|
|
25
|
+
remove_column :llm_api_calls, :hidden_output_tokens if column_exists?(:llm_api_calls, :hidden_output_tokens)
|
|
26
|
+
remove_column :llm_api_calls, :cache_write_input_tokens if column_exists?(:llm_api_calls, :cache_write_input_tokens)
|
|
27
|
+
remove_column :llm_api_calls, :cache_read_input_tokens if column_exists?(:llm_api_calls, :cache_read_input_tokens)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -6,13 +6,19 @@ class CreateLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
|
|
|
6
6
|
t.integer :input_tokens, null: false, default: 0
|
|
7
7
|
t.integer :output_tokens, null: false, default: 0
|
|
8
8
|
t.integer :total_tokens, null: false, default: 0
|
|
9
|
+
t.integer :cache_read_input_tokens, null: false, default: 0
|
|
10
|
+
t.integer :cache_write_input_tokens, null: false, default: 0
|
|
11
|
+
t.integer :hidden_output_tokens, null: false, default: 0
|
|
9
12
|
t.decimal :input_cost, precision: 20, scale: 8
|
|
13
|
+
t.decimal :cache_read_input_cost, precision: 20, scale: 8
|
|
14
|
+
t.decimal :cache_write_input_cost, precision: 20, scale: 8
|
|
10
15
|
t.decimal :output_cost, precision: 20, scale: 8
|
|
11
16
|
t.decimal :total_cost, precision: 20, scale: 8
|
|
12
17
|
t.integer :latency_ms
|
|
13
18
|
t.boolean :stream, null: false, default: false
|
|
14
19
|
t.string :usage_source
|
|
15
20
|
t.string :provider_response_id
|
|
21
|
+
t.string :pricing_mode
|
|
16
22
|
if postgresql?
|
|
17
23
|
t.jsonb :tags, null: false, default: {}
|
|
18
24
|
else
|
|
@@ -23,8 +29,9 @@ class CreateLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
|
|
|
23
29
|
t.timestamps
|
|
24
30
|
end
|
|
25
31
|
|
|
26
|
-
create_table :
|
|
27
|
-
t.
|
|
32
|
+
create_table :llm_cost_tracker_period_totals do |t|
|
|
33
|
+
t.string :period, null: false
|
|
34
|
+
t.date :period_start, null: false
|
|
28
35
|
t.decimal :total_cost, precision: 20, scale: 8, null: false, default: 0
|
|
29
36
|
|
|
30
37
|
t.timestamps
|
|
@@ -38,7 +45,7 @@ class CreateLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
|
|
|
38
45
|
add_index :llm_api_calls, :usage_source
|
|
39
46
|
add_index :llm_api_calls, :provider_response_id
|
|
40
47
|
add_index :llm_api_calls, :tags, using: :gin if postgresql?
|
|
41
|
-
add_index :
|
|
48
|
+
add_index :llm_cost_tracker_period_totals, [:period, :period_start], unique: true
|
|
42
49
|
end
|
|
43
50
|
|
|
44
51
|
private
|
|
@@ -12,6 +12,8 @@ LlmCostTracker.configure do |config|
|
|
|
12
12
|
|
|
13
13
|
# Monthly budget in USD. Set to nil to disable budget alerts.
|
|
14
14
|
# config.monthly_budget = 100.00
|
|
15
|
+
# config.daily_budget = 10.00
|
|
16
|
+
# config.per_call_budget = 1.00
|
|
15
17
|
# config.budget_exceeded_behavior = :notify # :notify, :raise, or :block_requests
|
|
16
18
|
|
|
17
19
|
# What to do when storage fails.
|
|
@@ -23,7 +25,7 @@ LlmCostTracker.configure do |config|
|
|
|
23
25
|
# Callback when monthly budget is exceeded.
|
|
24
26
|
# config.on_budget_exceeded = ->(data) {
|
|
25
27
|
# Rails.logger.warn "[LlmCostTracker] Budget exceeded! " \
|
|
26
|
-
# "
|
|
28
|
+
# "#{data[:budget_type]} total: $#{data[:total]}, Budget: $#{data[:budget]}"
|
|
27
29
|
# # Or send a Slack notification, email, etc.
|
|
28
30
|
# }
|
|
29
31
|
|
data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
# Supported price keys:
|
|
9
9
|
# - input
|
|
10
10
|
# - output
|
|
11
|
-
# - cached_input
|
|
12
11
|
# - cache_read_input
|
|
13
|
-
# -
|
|
12
|
+
# - cache_write_input
|
|
13
|
+
# - mode_input / mode_output / mode_cache_read_input / mode_cache_write_input
|
|
14
14
|
#
|
|
15
15
|
# Optional metadata keys, ignored by cost calculation:
|
|
16
16
|
# - _source
|
|
@@ -24,10 +24,18 @@
|
|
|
24
24
|
# models:
|
|
25
25
|
# "ft:gpt-4o-mini:my-org":
|
|
26
26
|
# input: 0.30
|
|
27
|
-
#
|
|
27
|
+
# cache_read_input: 0.15
|
|
28
28
|
# output: 1.20
|
|
29
29
|
# _notes: "Internal fine-tune rate"
|
|
30
30
|
#
|
|
31
|
+
# Example: alternate pricing mode
|
|
32
|
+
# models:
|
|
33
|
+
# "batchable-model":
|
|
34
|
+
# input: 1.00
|
|
35
|
+
# output: 2.00
|
|
36
|
+
# batch_input: 0.50
|
|
37
|
+
# batch_output: 1.00
|
|
38
|
+
#
|
|
31
39
|
# Example: negotiated provider discount
|
|
32
40
|
# models:
|
|
33
41
|
# "gpt-4o":
|
|
@@ -7,10 +7,9 @@ module LlmCostTracker
|
|
|
7
7
|
:input_tokens,
|
|
8
8
|
:output_tokens,
|
|
9
9
|
:total_tokens,
|
|
10
|
-
:cached_input_tokens,
|
|
11
10
|
:cache_read_input_tokens,
|
|
12
|
-
:
|
|
13
|
-
:
|
|
11
|
+
:cache_write_input_tokens,
|
|
12
|
+
:hidden_output_tokens,
|
|
14
13
|
:stream,
|
|
15
14
|
:usage_source,
|
|
16
15
|
:provider_response_id
|
|
@@ -34,11 +33,10 @@ module LlmCostTracker
|
|
|
34
33
|
model: attributes.fetch(:model),
|
|
35
34
|
input_tokens: attributes.fetch(:input_tokens).to_i,
|
|
36
35
|
output_tokens: attributes.fetch(:output_tokens).to_i,
|
|
37
|
-
total_tokens: attributes.fetch(:total_tokens,
|
|
38
|
-
cached_input_tokens: attributes[:cached_input_tokens],
|
|
36
|
+
total_tokens: attributes.fetch(:total_tokens, usage_breakdown(attributes).total_tokens).to_i,
|
|
39
37
|
cache_read_input_tokens: attributes[:cache_read_input_tokens],
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
cache_write_input_tokens: attributes[:cache_write_input_tokens],
|
|
39
|
+
hidden_output_tokens: attributes[:hidden_output_tokens],
|
|
42
40
|
stream: attributes[:stream] || false,
|
|
43
41
|
usage_source: attributes[:usage_source],
|
|
44
42
|
provider_response_id: attributes[:provider_response_id]
|
|
@@ -52,5 +50,16 @@ module LlmCostTracker
|
|
|
52
50
|
def to_h
|
|
53
51
|
super.compact
|
|
54
52
|
end
|
|
53
|
+
|
|
54
|
+
def self.usage_breakdown(attributes)
|
|
55
|
+
UsageBreakdown.build(
|
|
56
|
+
input_tokens: attributes.fetch(:input_tokens),
|
|
57
|
+
output_tokens: attributes.fetch(:output_tokens),
|
|
58
|
+
cache_read_input_tokens: attributes[:cache_read_input_tokens],
|
|
59
|
+
cache_write_input_tokens: attributes[:cache_write_input_tokens],
|
|
60
|
+
hidden_output_tokens: attributes[:hidden_output_tokens]
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
private_class_method :usage_breakdown
|
|
55
64
|
end
|
|
56
65
|
end
|
|
@@ -28,6 +28,8 @@ module LlmCostTracker
|
|
|
28
28
|
return nil unless usage
|
|
29
29
|
|
|
30
30
|
request = safe_json_parse(request_body)
|
|
31
|
+
cache_read = usage["cache_read_input_tokens"].to_i
|
|
32
|
+
cache_write = usage["cache_creation_input_tokens"].to_i
|
|
31
33
|
|
|
32
34
|
ParsedUsage.build(
|
|
33
35
|
provider: "anthropic",
|
|
@@ -35,10 +37,9 @@ module LlmCostTracker
|
|
|
35
37
|
model: response["model"] || request["model"],
|
|
36
38
|
input_tokens: usage["input_tokens"].to_i,
|
|
37
39
|
output_tokens: usage["output_tokens"].to_i,
|
|
38
|
-
total_tokens: usage["input_tokens"].to_i + usage["output_tokens"].to_i +
|
|
39
|
-
usage["cache_read_input_tokens"].to_i + usage["cache_creation_input_tokens"].to_i,
|
|
40
|
+
total_tokens: usage["input_tokens"].to_i + usage["output_tokens"].to_i + cache_read + cache_write,
|
|
40
41
|
cache_read_input_tokens: usage["cache_read_input_tokens"],
|
|
41
|
-
|
|
42
|
+
cache_write_input_tokens: usage["cache_creation_input_tokens"],
|
|
42
43
|
usage_source: :response
|
|
43
44
|
)
|
|
44
45
|
end
|
|
@@ -105,7 +106,7 @@ module LlmCostTracker
|
|
|
105
106
|
input = usage["input_tokens"].to_i
|
|
106
107
|
output = usage["output_tokens"].to_i
|
|
107
108
|
cache_read = usage["cache_read_input_tokens"].to_i
|
|
108
|
-
|
|
109
|
+
cache_write = usage["cache_creation_input_tokens"].to_i
|
|
109
110
|
|
|
110
111
|
ParsedUsage.build(
|
|
111
112
|
provider: "anthropic",
|
|
@@ -113,9 +114,9 @@ module LlmCostTracker
|
|
|
113
114
|
model: model,
|
|
114
115
|
input_tokens: input,
|
|
115
116
|
output_tokens: output,
|
|
116
|
-
total_tokens: input + output + cache_read +
|
|
117
|
+
total_tokens: input + output + cache_read + cache_write,
|
|
117
118
|
cache_read_input_tokens: usage["cache_read_input_tokens"],
|
|
118
|
-
|
|
119
|
+
cache_write_input_tokens: usage["cache_creation_input_tokens"],
|
|
119
120
|
stream: true,
|
|
120
121
|
usage_source: :stream_final
|
|
121
122
|
)
|
|
@@ -74,13 +74,16 @@ module LlmCostTracker
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
76
|
def build_parsed_usage(request_url, usage, usage_source:, stream: false, provider_response_id: nil)
|
|
77
|
+
cache_read = usage["cachedContentTokenCount"].to_i
|
|
78
|
+
|
|
77
79
|
ParsedUsage.build(
|
|
78
80
|
provider: "gemini",
|
|
79
81
|
model: extract_model_from_url(request_url),
|
|
80
|
-
input_tokens: usage["promptTokenCount"].to_i,
|
|
82
|
+
input_tokens: [usage["promptTokenCount"].to_i - cache_read, 0].max,
|
|
81
83
|
output_tokens: output_tokens(usage),
|
|
82
84
|
total_tokens: usage["totalTokenCount"].to_i,
|
|
83
|
-
|
|
85
|
+
cache_read_input_tokens: usage["cachedContentTokenCount"],
|
|
86
|
+
hidden_output_tokens: usage["thoughtsTokenCount"],
|
|
84
87
|
stream: stream,
|
|
85
88
|
usage_source: usage_source,
|
|
86
89
|
provider_response_id: provider_response_id
|
|
@@ -13,15 +13,17 @@ module LlmCostTracker
|
|
|
13
13
|
return nil unless usage
|
|
14
14
|
|
|
15
15
|
request = safe_json_parse(request_body)
|
|
16
|
+
cache_read = cache_read_input_tokens(usage)
|
|
16
17
|
|
|
17
18
|
ParsedUsage.build(
|
|
18
19
|
provider: provider_for(request_url),
|
|
19
20
|
provider_response_id: response["id"],
|
|
20
21
|
model: response["model"] || request["model"],
|
|
21
|
-
input_tokens: (usage
|
|
22
|
+
input_tokens: regular_input_tokens(usage, cache_read),
|
|
22
23
|
output_tokens: (usage["completion_tokens"] || usage["output_tokens"]).to_i,
|
|
23
24
|
total_tokens: usage["total_tokens"].to_i,
|
|
24
|
-
|
|
25
|
+
cache_read_input_tokens: cache_read,
|
|
26
|
+
hidden_output_tokens: hidden_output_tokens(usage),
|
|
25
27
|
usage_source: :response
|
|
26
28
|
)
|
|
27
29
|
end
|
|
@@ -34,14 +36,16 @@ module LlmCostTracker
|
|
|
34
36
|
usage = detect_stream_usage(events)
|
|
35
37
|
|
|
36
38
|
if usage
|
|
39
|
+
cache_read = cache_read_input_tokens(usage)
|
|
37
40
|
ParsedUsage.build(
|
|
38
41
|
provider: provider_for(request_url),
|
|
39
42
|
provider_response_id: detect_stream_response_id(events),
|
|
40
43
|
model: model,
|
|
41
|
-
input_tokens: (usage
|
|
44
|
+
input_tokens: regular_input_tokens(usage, cache_read),
|
|
42
45
|
output_tokens: (usage["completion_tokens"] || usage["output_tokens"]).to_i,
|
|
43
46
|
total_tokens: usage["total_tokens"].to_i,
|
|
44
|
-
|
|
47
|
+
cache_read_input_tokens: cache_read,
|
|
48
|
+
hidden_output_tokens: hidden_output_tokens(usage),
|
|
45
49
|
stream: true,
|
|
46
50
|
usage_source: :stream_final
|
|
47
51
|
)
|
|
@@ -92,10 +96,19 @@ module LlmCostTracker
|
|
|
92
96
|
nil
|
|
93
97
|
end
|
|
94
98
|
|
|
95
|
-
def
|
|
99
|
+
def regular_input_tokens(usage, cache_read)
|
|
100
|
+
[(usage["prompt_tokens"] || usage["input_tokens"]).to_i - cache_read.to_i, 0].max
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def cache_read_input_tokens(usage)
|
|
96
104
|
details = usage["prompt_tokens_details"] || usage["input_tokens_details"] || {}
|
|
97
105
|
details["cached_tokens"]
|
|
98
106
|
end
|
|
107
|
+
|
|
108
|
+
def hidden_output_tokens(usage)
|
|
109
|
+
details = usage["completion_tokens_details"] || usage["output_tokens_details"] || {}
|
|
110
|
+
details["reasoning_tokens"]
|
|
111
|
+
end
|
|
99
112
|
end
|
|
100
113
|
end
|
|
101
114
|
end
|
|
@@ -10,7 +10,7 @@ module LlmCostTracker
|
|
|
10
10
|
module PriceRegistry
|
|
11
11
|
DEFAULT_PRICES_PATH = File.expand_path("prices.json", __dir__)
|
|
12
12
|
EMPTY_PRICES = {}.freeze
|
|
13
|
-
PRICE_KEYS = %w[input
|
|
13
|
+
PRICE_KEYS = %w[input output cache_read_input cache_write_input].freeze
|
|
14
14
|
METADATA_KEYS = %w[_source _source_version _fetched_at _updated _notes _validator_override].freeze
|
|
15
15
|
MUTEX = Monitor.new
|
|
16
16
|
|
|
@@ -60,7 +60,7 @@ module LlmCostTracker
|
|
|
60
60
|
def normalize_price_entry(price)
|
|
61
61
|
price.each_with_object({}) do |(key, value), normalized|
|
|
62
62
|
key = key.to_s
|
|
63
|
-
normalized[key.to_sym] = Float(value) if
|
|
63
|
+
normalized[key.to_sym] = Float(value) if price_key?(key)
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -80,15 +80,25 @@ module LlmCostTracker
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def warn_unknown_keys(model, price, path)
|
|
83
|
-
unknown_keys = price.keys.map(&:to_s)
|
|
83
|
+
unknown_keys = price.keys.map(&:to_s).reject do |key|
|
|
84
|
+
price_key?(key) || METADATA_KEYS.include?(key)
|
|
85
|
+
end
|
|
84
86
|
return if unknown_keys.empty?
|
|
85
87
|
|
|
86
88
|
Logging.warn(
|
|
87
89
|
"Unknown price keys #{unknown_keys.inspect} for #{model.inspect} in #{path}; " \
|
|
88
|
-
"ignored. Known keys: #{(PRICE_KEYS + METADATA_KEYS).inspect}"
|
|
90
|
+
"ignored. Known keys: #{(PRICE_KEYS + METADATA_KEYS).inspect}; mode-specific keys use mode_input"
|
|
89
91
|
)
|
|
90
92
|
end
|
|
91
93
|
|
|
94
|
+
def price_key?(key)
|
|
95
|
+
return true if PRICE_KEYS.include?(key)
|
|
96
|
+
|
|
97
|
+
PRICE_KEYS.any? do |base_key|
|
|
98
|
+
key.end_with?("_#{base_key}") && key.delete_suffix("_#{base_key}") != ""
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
92
102
|
def load_price_file(path)
|
|
93
103
|
contents = File.read(path)
|
|
94
104
|
return YAML.safe_load(contents, aliases: false) || {} if yaml_file?(path)
|
|
@@ -6,7 +6,7 @@ module LlmCostTracker
|
|
|
6
6
|
Discrepancy = Data.define(:model, :field, :values)
|
|
7
7
|
|
|
8
8
|
PRIORITY_ORDER = %i[litellm openrouter].freeze
|
|
9
|
-
SUPPLEMENTAL_FIELDS = %i[
|
|
9
|
+
SUPPLEMENTAL_FIELDS = %i[cache_read_input cache_write_input].freeze
|
|
10
10
|
|
|
11
11
|
def merge(results_by_source)
|
|
12
12
|
prices = collect_prices(results_by_source)
|
|
@@ -7,24 +7,22 @@ module LlmCostTracker
|
|
|
7
7
|
:provider,
|
|
8
8
|
:input,
|
|
9
9
|
:output,
|
|
10
|
-
:cached_input,
|
|
11
10
|
:cache_read_input,
|
|
12
|
-
:
|
|
11
|
+
:cache_write_input,
|
|
13
12
|
:source,
|
|
14
13
|
:source_version,
|
|
15
14
|
:fetched_at
|
|
16
15
|
)
|
|
17
16
|
|
|
18
17
|
class RawPrice
|
|
19
|
-
PRICE_FIELDS = %w[input output
|
|
18
|
+
PRICE_FIELDS = %w[input output cache_read_input cache_write_input].freeze
|
|
20
19
|
|
|
21
20
|
def to_registry_entry(today:)
|
|
22
21
|
{
|
|
23
22
|
"input" => input,
|
|
24
23
|
"output" => output,
|
|
25
|
-
"cached_input" => cached_input,
|
|
26
24
|
"cache_read_input" => cache_read_input,
|
|
27
|
-
"
|
|
25
|
+
"cache_write_input" => cache_write_input,
|
|
28
26
|
"_source" => source.to_s,
|
|
29
27
|
"_source_version" => source_version,
|
|
30
28
|
"_fetched_at" => fetched_at || today.iso8601
|
|
@@ -65,9 +65,8 @@ module LlmCostTracker
|
|
|
65
65
|
provider: provider,
|
|
66
66
|
input: price_per_million(entry["input_cost_per_token"]),
|
|
67
67
|
output: price_per_million(entry["output_cost_per_token"]),
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
cache_creation_input: provider == "anthropic" ? cache_write : nil,
|
|
68
|
+
cache_read_input: cache_read,
|
|
69
|
+
cache_write_input: cache_write,
|
|
71
70
|
source: name,
|
|
72
71
|
source_version: response_version(response),
|
|
73
72
|
fetched_at: response.fetched_at
|
|
@@ -68,9 +68,8 @@ module LlmCostTracker
|
|
|
68
68
|
provider: provider,
|
|
69
69
|
input: price_per_million(pricing["prompt"]),
|
|
70
70
|
output: price_per_million(pricing["completion"]),
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
cache_creation_input: provider == "anthropic" ? cache_write : nil,
|
|
71
|
+
cache_read_input: cache_read,
|
|
72
|
+
cache_write_input: cache_write,
|
|
74
73
|
source: name,
|
|
75
74
|
source_version: response_version(response),
|
|
76
75
|
fetched_at: response.fetched_at
|