llm_cost_tracker 0.4.1 → 0.5.1
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 +30 -0
- data/README.md +132 -405
- data/lib/llm_cost_tracker/configuration/instrumentation.rb +37 -0
- data/lib/llm_cost_tracker/configuration.rb +10 -5
- data/lib/llm_cost_tracker/doctor.rb +166 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +33 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +12 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +53 -21
- data/lib/llm_cost_tracker/integrations/anthropic.rb +75 -0
- data/lib/llm_cost_tracker/integrations/base.rb +72 -0
- data/lib/llm_cost_tracker/integrations/object_reader.rb +56 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +95 -0
- data/lib/llm_cost_tracker/integrations/registry.rb +41 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +6 -5
- data/lib/llm_cost_tracker/parsed_usage.rb +8 -1
- data/lib/llm_cost_tracker/parsers/base.rb +1 -1
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +1 -1
- data/lib/llm_cost_tracker/price_freshness.rb +38 -0
- data/lib/llm_cost_tracker/price_registry.rb +14 -0
- data/lib/llm_cost_tracker/price_sync/fetcher.rb +5 -2
- data/lib/llm_cost_tracker/price_sync/registry_diff.rb +51 -0
- data/lib/llm_cost_tracker/price_sync/registry_writer.rb +5 -1
- data/lib/llm_cost_tracker/price_sync.rb +111 -109
- data/lib/llm_cost_tracker/prices.json +391 -42
- data/lib/llm_cost_tracker/pricing.rb +35 -16
- data/lib/llm_cost_tracker/request_url.rb +20 -0
- data/lib/llm_cost_tracker/storage/dispatcher.rb +68 -0
- data/lib/llm_cost_tracker/stream_collector.rb +3 -3
- data/lib/llm_cost_tracker/tag_context.rb +52 -0
- data/lib/llm_cost_tracker/tracker.rb +7 -60
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +14 -4
- data/lib/tasks/llm_cost_tracker.rake +33 -69
- metadata +28 -12
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +0 -51
- data/lib/llm_cost_tracker/price_sync/merger.rb +0 -72
- data/lib/llm_cost_tracker/price_sync/model_catalog.rb +0 -77
- data/lib/llm_cost_tracker/price_sync/raw_price.rb +0 -33
- data/lib/llm_cost_tracker/price_sync/refresh_plan_builder.rb +0 -162
- data/lib/llm_cost_tracker/price_sync/source.rb +0 -29
- data/lib/llm_cost_tracker/price_sync/source_result.rb +0 -7
- data/lib/llm_cost_tracker/price_sync/sources/litellm.rb +0 -90
- data/lib/llm_cost_tracker/price_sync/sources/open_router.rb +0 -93
- data/lib/llm_cost_tracker/price_sync/validator.rb +0 -66
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/isolated_execution_state"
|
|
4
|
+
|
|
5
|
+
require_relative "value_helpers"
|
|
6
|
+
|
|
7
|
+
module LlmCostTracker
|
|
8
|
+
module TagContext
|
|
9
|
+
KEY = :llm_cost_tracker_tags
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def with(tags)
|
|
13
|
+
stack = current_stack
|
|
14
|
+
ActiveSupport::IsolatedExecutionState[KEY] = stack + [normalize(tags)]
|
|
15
|
+
yield
|
|
16
|
+
ensure
|
|
17
|
+
ActiveSupport::IsolatedExecutionState[KEY] = stack
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tags
|
|
21
|
+
config_tags.merge(scoped_tags)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def clear!
|
|
25
|
+
ActiveSupport::IsolatedExecutionState[KEY] = []
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def config_tags
|
|
31
|
+
normalize(resolve_default_tags)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def resolve_default_tags
|
|
35
|
+
tags = LlmCostTracker.configuration.default_tags
|
|
36
|
+
tags.respond_to?(:call) ? tags.call : tags
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def scoped_tags
|
|
40
|
+
current_stack.reduce({}) { |merged, tags| merged.merge(tags) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def current_stack
|
|
44
|
+
ActiveSupport::IsolatedExecutionState[KEY] || []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def normalize(tags)
|
|
48
|
+
ValueHelpers.deep_dup(tags || {}).to_h
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "storage/dispatcher"
|
|
4
4
|
|
|
5
5
|
module LlmCostTracker
|
|
6
6
|
class Tracker
|
|
7
7
|
EVENT_NAME = "llm_request.llm_cost_tracker"
|
|
8
8
|
|
|
9
|
-
USAGE_SOURCES = %i[response stream_final manual unknown].freeze
|
|
9
|
+
USAGE_SOURCES = %i[response stream_final sdk_response manual unknown].freeze
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
12
|
def enforce_budget!
|
|
@@ -19,6 +19,7 @@ module LlmCostTracker
|
|
|
19
19
|
usage_source: nil, provider_response_id: nil, pricing_mode: nil, metadata: {})
|
|
20
20
|
return unless LlmCostTracker.configuration.enabled
|
|
21
21
|
|
|
22
|
+
model = normalize_model(model)
|
|
22
23
|
usage = usage_data(input_tokens, output_tokens, metadata, pricing_mode)
|
|
23
24
|
cost_data = cost_for_usage(provider, model, usage)
|
|
24
25
|
|
|
@@ -38,7 +39,7 @@ module LlmCostTracker
|
|
|
38
39
|
|
|
39
40
|
ActiveSupport::Notifications.instrument(EVENT_NAME, event.to_h)
|
|
40
41
|
|
|
41
|
-
stored =
|
|
42
|
+
stored = Storage::Dispatcher.save(event)
|
|
42
43
|
Budget.check!(event) unless stored == false
|
|
43
44
|
|
|
44
45
|
event
|
|
@@ -68,6 +69,8 @@ module LlmCostTracker
|
|
|
68
69
|
)
|
|
69
70
|
end
|
|
70
71
|
|
|
72
|
+
def normalize_model(value) = value.to_s.strip.then { |model| model.empty? ? ParsedUsage::UNKNOWN_MODEL : model }
|
|
73
|
+
|
|
71
74
|
def build_event(provider:, model:, usage:, cost_data:, metadata:, latency_ms:, stream:, usage_source:,
|
|
72
75
|
provider_response_id:)
|
|
73
76
|
Event.new(
|
|
@@ -81,7 +84,7 @@ module LlmCostTracker
|
|
|
81
84
|
hidden_output_tokens: usage[:hidden_output_tokens],
|
|
82
85
|
pricing_mode: usage[:pricing_mode],
|
|
83
86
|
cost: cost_data,
|
|
84
|
-
tags: LlmCostTracker.
|
|
87
|
+
tags: LlmCostTracker::TagContext.tags.merge(EventMetadata.tags(metadata)).freeze,
|
|
85
88
|
latency_ms: normalized_latency_ms(latency_ms),
|
|
86
89
|
stream: stream ? true : false,
|
|
87
90
|
usage_source: normalized_usage_source(usage_source),
|
|
@@ -90,62 +93,6 @@ module LlmCostTracker
|
|
|
90
93
|
)
|
|
91
94
|
end
|
|
92
95
|
|
|
93
|
-
def store(event)
|
|
94
|
-
config = LlmCostTracker.configuration
|
|
95
|
-
case config.storage_backend
|
|
96
|
-
when :log then log_event(event, config)
|
|
97
|
-
when :active_record then active_record_save(event)
|
|
98
|
-
when :custom then custom_save(event, config)
|
|
99
|
-
end
|
|
100
|
-
rescue BudgetExceededError, UnknownPricingError
|
|
101
|
-
raise
|
|
102
|
-
rescue StandardError => e
|
|
103
|
-
handle_storage_error(e)
|
|
104
|
-
false
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def log_event(event, config)
|
|
108
|
-
message = "#{event.provider}/#{event.model} " \
|
|
109
|
-
"tokens=#{event.total_tokens} " \
|
|
110
|
-
"cost=#{log_cost_label(event)}"
|
|
111
|
-
message += " latency=#{event.latency_ms}ms" if event.latency_ms
|
|
112
|
-
message += " stream=#{event.stream}" if event.stream
|
|
113
|
-
message += " source=#{event.usage_source}" if event.usage_source
|
|
114
|
-
message += " tags=#{event.tags}" unless event.tags.empty?
|
|
115
|
-
|
|
116
|
-
Logging.log(config.log_level, message)
|
|
117
|
-
event
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def log_cost_label(event) = event.cost ? "$#{format('%.6f', event.cost.total_cost)}" : "unknown"
|
|
121
|
-
|
|
122
|
-
def active_record_save(event)
|
|
123
|
-
require_relative "llm_api_call" unless defined?(LlmCostTracker::LlmApiCall)
|
|
124
|
-
require_relative "storage/active_record_store" unless defined?(LlmCostTracker::Storage::ActiveRecordStore)
|
|
125
|
-
|
|
126
|
-
Storage::ActiveRecordStore.save(event)
|
|
127
|
-
event
|
|
128
|
-
rescue LoadError => e
|
|
129
|
-
raise Error, "ActiveRecord storage requires the active_record gem: #{e.message}"
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def custom_save(event, config)
|
|
133
|
-
result = config.custom_storage&.call(event)
|
|
134
|
-
result == false ? false : event
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def handle_storage_error(error)
|
|
138
|
-
case LlmCostTracker.configuration.storage_error_behavior
|
|
139
|
-
when :ignore
|
|
140
|
-
nil
|
|
141
|
-
when :warn
|
|
142
|
-
Logging.warn("Storage failed; tracking event was not persisted: #{error.class}: #{error.message}")
|
|
143
|
-
when :raise
|
|
144
|
-
storage_error = StorageError.new(error)
|
|
145
|
-
raise storage_error
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
96
|
def normalized_latency_ms(latency_ms) = latency_ms.nil? ? nil : [latency_ms.to_i, 0].max
|
|
150
97
|
|
|
151
98
|
def normalized_usage_source(value)
|
data/lib/llm_cost_tracker.rb
CHANGED
|
@@ -25,9 +25,11 @@ require_relative "llm_cost_tracker/parsers/gemini"
|
|
|
25
25
|
require_relative "llm_cost_tracker/parsers/sse"
|
|
26
26
|
require_relative "llm_cost_tracker/parsers/registry"
|
|
27
27
|
require_relative "llm_cost_tracker/middleware/faraday"
|
|
28
|
+
require_relative "llm_cost_tracker/integrations/registry"
|
|
28
29
|
require_relative "llm_cost_tracker/budget"
|
|
29
30
|
require_relative "llm_cost_tracker/unknown_pricing"
|
|
30
31
|
require_relative "llm_cost_tracker/event_metadata"
|
|
32
|
+
require_relative "llm_cost_tracker/tag_context"
|
|
31
33
|
require_relative "llm_cost_tracker/tags_column"
|
|
32
34
|
require_relative "llm_cost_tracker/tag_key"
|
|
33
35
|
require_relative "llm_cost_tracker/tag_query"
|
|
@@ -37,6 +39,7 @@ require_relative "llm_cost_tracker/retention"
|
|
|
37
39
|
require_relative "llm_cost_tracker/report_data"
|
|
38
40
|
require_relative "llm_cost_tracker/report_formatter"
|
|
39
41
|
require_relative "llm_cost_tracker/report"
|
|
42
|
+
require_relative "llm_cost_tracker/doctor"
|
|
40
43
|
|
|
41
44
|
module LlmCostTracker
|
|
42
45
|
CONFIGURATION_MUTEX = Monitor.new
|
|
@@ -52,10 +55,11 @@ module LlmCostTracker
|
|
|
52
55
|
current = current.dup_for_configuration if current.finalized?
|
|
53
56
|
@configuration = current
|
|
54
57
|
yield(current)
|
|
55
|
-
current.
|
|
58
|
+
current.openai_compatible_providers = current.openai_compatible_providers.dup
|
|
56
59
|
current.finalize!
|
|
57
60
|
current
|
|
58
61
|
end
|
|
62
|
+
Integrations.install!
|
|
59
63
|
warn_for_configuration!(config)
|
|
60
64
|
end
|
|
61
65
|
|
|
@@ -63,14 +67,20 @@ module LlmCostTracker
|
|
|
63
67
|
CONFIGURATION_MUTEX.synchronize { @configuration = Configuration.new }
|
|
64
68
|
UnknownPricing.reset! if defined?(UnknownPricing)
|
|
65
69
|
Storage::ActiveRecordStore.reset! if defined?(Storage::ActiveRecordStore)
|
|
70
|
+
TagContext.clear! if defined?(TagContext)
|
|
66
71
|
end
|
|
67
72
|
|
|
68
73
|
def enforce_budget!
|
|
69
74
|
Tracker.enforce_budget!
|
|
70
75
|
end
|
|
71
76
|
|
|
72
|
-
def
|
|
73
|
-
|
|
77
|
+
def with_tags(tags = nil, **kwargs, &)
|
|
78
|
+
merged = (tags || {}).to_h.merge(kwargs)
|
|
79
|
+
TagContext.with(merged, &)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def track(provider:, input_tokens:, output_tokens:, model: nil, latency_ms: nil, stream: false,
|
|
83
|
+
usage_source: :manual, enforce_budget: false, provider_response_id: nil, pricing_mode: nil, **metadata)
|
|
74
84
|
enforce_budget! if enforce_budget
|
|
75
85
|
Tracker.record(
|
|
76
86
|
provider: provider.to_s,
|
|
@@ -86,7 +96,7 @@ module LlmCostTracker
|
|
|
86
96
|
)
|
|
87
97
|
end
|
|
88
98
|
|
|
89
|
-
def track_stream(provider:, model
|
|
99
|
+
def track_stream(provider:, model: nil, latency_ms: nil, enforce_budget: false, provider_response_id: nil,
|
|
90
100
|
pricing_mode: nil, **metadata)
|
|
91
101
|
require_relative "llm_cost_tracker/stream_collector"
|
|
92
102
|
enforce_budget! if enforce_budget
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
3
5
|
# rubocop:disable Metrics/BlockLength
|
|
4
6
|
namespace :llm_cost_tracker do
|
|
7
|
+
desc "Check LLM Cost Tracker setup"
|
|
8
|
+
task :doctor do
|
|
9
|
+
Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
|
|
10
|
+
checks = LlmCostTracker::Doctor.call
|
|
11
|
+
puts LlmCostTracker::Doctor.report(checks)
|
|
12
|
+
abort("llm_cost_tracker: doctor found setup errors") unless LlmCostTracker::Doctor.healthy?(checks)
|
|
13
|
+
end
|
|
14
|
+
|
|
5
15
|
desc "Print an LLM cost report from ActiveRecord storage"
|
|
6
16
|
task report: :environment do
|
|
7
17
|
days = (ENV["DAYS"] || LlmCostTracker::Report::DEFAULT_DAYS).to_i
|
|
@@ -18,56 +28,49 @@ namespace :llm_cost_tracker do
|
|
|
18
28
|
|
|
19
29
|
namespace :prices do
|
|
20
30
|
desc(
|
|
21
|
-
"
|
|
22
|
-
"Use PREVIEW=1 to preview,
|
|
23
|
-
"or OUTPUT=path/to/file.json."
|
|
31
|
+
"Refresh the configured pricing file from the maintained LLM Cost Tracker price snapshot. " \
|
|
32
|
+
"Use PREVIEW=1 to preview, URL=... to override the source, or OUTPUT=path/to/file.json."
|
|
24
33
|
)
|
|
25
|
-
task :
|
|
34
|
+
task :refresh do
|
|
26
35
|
Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
|
|
27
36
|
require_relative "../llm_cost_tracker"
|
|
28
37
|
|
|
29
|
-
output_path =
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
output_path = price_refresh_output_path
|
|
39
|
+
source_url = LlmCostTracker::PriceSync.configured_remote_url
|
|
40
|
+
preview = ENV["PREVIEW"] == "1"
|
|
41
|
+
result = LlmCostTracker::PriceSync.refresh(
|
|
32
42
|
path: output_path,
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
url: source_url,
|
|
44
|
+
preview: preview
|
|
35
45
|
)
|
|
36
46
|
|
|
37
|
-
action = if
|
|
47
|
+
action = if preview
|
|
38
48
|
"previewed"
|
|
39
49
|
elsif result.written
|
|
40
|
-
"
|
|
50
|
+
"refreshed"
|
|
41
51
|
else
|
|
42
52
|
"kept"
|
|
43
53
|
end
|
|
44
54
|
|
|
45
55
|
puts "llm_cost_tracker: #{action} pricing file #{result.path}"
|
|
46
|
-
|
|
56
|
+
puts " source: #{result.source_url}"
|
|
57
|
+
puts " version: #{result.source_version.inspect}" if result.source_version
|
|
47
58
|
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
59
|
end
|
|
54
60
|
|
|
55
|
-
desc "Compare the current pricing
|
|
61
|
+
desc "Compare the current pricing file with the maintained LLM Cost Tracker price snapshot."
|
|
56
62
|
task :check do
|
|
57
63
|
Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
|
|
58
64
|
require_relative "../llm_cost_tracker"
|
|
59
65
|
|
|
60
|
-
output_path =
|
|
61
|
-
|
|
66
|
+
output_path = price_refresh_output_path
|
|
67
|
+
source_url = LlmCostTracker::PriceSync.configured_remote_url
|
|
68
|
+
result = LlmCostTracker::PriceSync.check(path: output_path, url: source_url)
|
|
62
69
|
|
|
63
70
|
puts "llm_cost_tracker: checked pricing file #{result.path}"
|
|
64
|
-
|
|
71
|
+
puts " source: #{result.source_url}"
|
|
72
|
+
puts " version: #{result.source_version.inspect}" if result.source_version
|
|
65
73
|
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
74
|
puts " pricing is up to date" if result.up_to_date
|
|
72
75
|
abort("llm_cost_tracker: pricing check failed") unless result.up_to_date
|
|
73
76
|
end
|
|
@@ -75,16 +78,6 @@ namespace :llm_cost_tracker do
|
|
|
75
78
|
end
|
|
76
79
|
# rubocop:enable Metrics/BlockLength
|
|
77
80
|
|
|
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
81
|
def print_changes(changes)
|
|
89
82
|
puts " changed models: #{changes.size}"
|
|
90
83
|
return if changes.empty?
|
|
@@ -97,37 +90,8 @@ def print_changes(changes)
|
|
|
97
90
|
end
|
|
98
91
|
end
|
|
99
92
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
93
|
+
def price_refresh_output_path
|
|
94
|
+
path = LlmCostTracker::PriceSync.configured_output_path
|
|
95
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
96
|
+
path
|
|
133
97
|
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.5.1
|
|
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-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -84,6 +84,20 @@ dependencies:
|
|
|
84
84
|
- - "<"
|
|
85
85
|
- !ruby/object:Gem::Version
|
|
86
86
|
version: '9.0'
|
|
87
|
+
- !ruby/object:Gem::Dependency
|
|
88
|
+
name: nokogiri
|
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - "~>"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '1.16'
|
|
94
|
+
type: :development
|
|
95
|
+
prerelease: false
|
|
96
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '1.16'
|
|
87
101
|
- !ruby/object:Gem::Dependency
|
|
88
102
|
name: railties
|
|
89
103
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -272,7 +286,9 @@ files:
|
|
|
272
286
|
- lib/llm_cost_tracker/assets.rb
|
|
273
287
|
- lib/llm_cost_tracker/budget.rb
|
|
274
288
|
- lib/llm_cost_tracker/configuration.rb
|
|
289
|
+
- lib/llm_cost_tracker/configuration/instrumentation.rb
|
|
275
290
|
- lib/llm_cost_tracker/cost.rb
|
|
291
|
+
- lib/llm_cost_tracker/doctor.rb
|
|
276
292
|
- lib/llm_cost_tracker/engine.rb
|
|
277
293
|
- lib/llm_cost_tracker/engine_compatibility.rb
|
|
278
294
|
- lib/llm_cost_tracker/errors.rb
|
|
@@ -292,11 +308,15 @@ files:
|
|
|
292
308
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb
|
|
293
309
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
|
|
294
310
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
|
|
295
|
-
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb
|
|
296
311
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb
|
|
297
312
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb
|
|
298
313
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb
|
|
299
314
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb
|
|
315
|
+
- lib/llm_cost_tracker/integrations/anthropic.rb
|
|
316
|
+
- lib/llm_cost_tracker/integrations/base.rb
|
|
317
|
+
- lib/llm_cost_tracker/integrations/object_reader.rb
|
|
318
|
+
- lib/llm_cost_tracker/integrations/openai.rb
|
|
319
|
+
- lib/llm_cost_tracker/integrations/registry.rb
|
|
300
320
|
- lib/llm_cost_tracker/llm_api_call.rb
|
|
301
321
|
- lib/llm_cost_tracker/logging.rb
|
|
302
322
|
- lib/llm_cost_tracker/middleware/faraday.rb
|
|
@@ -312,31 +332,27 @@ files:
|
|
|
312
332
|
- lib/llm_cost_tracker/parsers/sse.rb
|
|
313
333
|
- lib/llm_cost_tracker/period_grouping.rb
|
|
314
334
|
- lib/llm_cost_tracker/period_total.rb
|
|
335
|
+
- lib/llm_cost_tracker/price_freshness.rb
|
|
315
336
|
- lib/llm_cost_tracker/price_registry.rb
|
|
316
337
|
- lib/llm_cost_tracker/price_sync.rb
|
|
317
338
|
- lib/llm_cost_tracker/price_sync/fetcher.rb
|
|
318
|
-
- lib/llm_cost_tracker/price_sync/
|
|
319
|
-
- lib/llm_cost_tracker/price_sync/model_catalog.rb
|
|
320
|
-
- lib/llm_cost_tracker/price_sync/raw_price.rb
|
|
321
|
-
- lib/llm_cost_tracker/price_sync/refresh_plan_builder.rb
|
|
339
|
+
- lib/llm_cost_tracker/price_sync/registry_diff.rb
|
|
322
340
|
- lib/llm_cost_tracker/price_sync/registry_loader.rb
|
|
323
341
|
- lib/llm_cost_tracker/price_sync/registry_writer.rb
|
|
324
|
-
- lib/llm_cost_tracker/price_sync/source.rb
|
|
325
|
-
- lib/llm_cost_tracker/price_sync/source_result.rb
|
|
326
|
-
- lib/llm_cost_tracker/price_sync/sources/litellm.rb
|
|
327
|
-
- lib/llm_cost_tracker/price_sync/sources/open_router.rb
|
|
328
|
-
- lib/llm_cost_tracker/price_sync/validator.rb
|
|
329
342
|
- lib/llm_cost_tracker/prices.json
|
|
330
343
|
- lib/llm_cost_tracker/pricing.rb
|
|
331
344
|
- lib/llm_cost_tracker/railtie.rb
|
|
332
345
|
- lib/llm_cost_tracker/report.rb
|
|
333
346
|
- lib/llm_cost_tracker/report_data.rb
|
|
334
347
|
- lib/llm_cost_tracker/report_formatter.rb
|
|
348
|
+
- lib/llm_cost_tracker/request_url.rb
|
|
335
349
|
- lib/llm_cost_tracker/retention.rb
|
|
336
350
|
- lib/llm_cost_tracker/storage/active_record_rollups.rb
|
|
337
351
|
- lib/llm_cost_tracker/storage/active_record_store.rb
|
|
352
|
+
- lib/llm_cost_tracker/storage/dispatcher.rb
|
|
338
353
|
- lib/llm_cost_tracker/stream_collector.rb
|
|
339
354
|
- lib/llm_cost_tracker/tag_accessors.rb
|
|
355
|
+
- lib/llm_cost_tracker/tag_context.rb
|
|
340
356
|
- lib/llm_cost_tracker/tag_key.rb
|
|
341
357
|
- lib/llm_cost_tracker/tag_query.rb
|
|
342
358
|
- lib/llm_cost_tracker/tags_column.rb
|
data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Local LlmCostTracker price overrides.
|
|
2
|
-
#
|
|
3
|
-
# Add only the models you want to override or add. Built-in prices still come
|
|
4
|
-
# from the gem's prices.json, and Ruby pricing_overrides still take precedence.
|
|
5
|
-
#
|
|
6
|
-
# Units: USD per 1M tokens.
|
|
7
|
-
#
|
|
8
|
-
# Supported price keys:
|
|
9
|
-
# - input
|
|
10
|
-
# - output
|
|
11
|
-
# - cache_read_input
|
|
12
|
-
# - cache_write_input
|
|
13
|
-
# - mode_input / mode_output / mode_cache_read_input / mode_cache_write_input
|
|
14
|
-
#
|
|
15
|
-
# Optional metadata keys, ignored by cost calculation:
|
|
16
|
-
# - _source
|
|
17
|
-
# - _source_version
|
|
18
|
-
# - _fetched_at
|
|
19
|
-
# - _updated
|
|
20
|
-
# - _notes
|
|
21
|
-
# - _validator_override
|
|
22
|
-
#
|
|
23
|
-
# Example: custom fine-tune
|
|
24
|
-
# models:
|
|
25
|
-
# "ft:gpt-4o-mini:my-org":
|
|
26
|
-
# input: 0.30
|
|
27
|
-
# cache_read_input: 0.15
|
|
28
|
-
# output: 1.20
|
|
29
|
-
# _notes: "Internal fine-tune rate"
|
|
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
|
-
#
|
|
39
|
-
# Example: negotiated provider discount
|
|
40
|
-
# models:
|
|
41
|
-
# "gpt-4o":
|
|
42
|
-
# input: 2.00
|
|
43
|
-
# output: 8.00
|
|
44
|
-
# _source: "manual"
|
|
45
|
-
# _updated: "2026-04-18"
|
|
46
|
-
#
|
|
47
|
-
# Use _source: "manual" for custom or orphaned entries you never want sync to touch.
|
|
48
|
-
# Use _validator_override: ["skip_relative_change"] if a negotiated price would
|
|
49
|
-
# otherwise trip the >3x sync warning.
|
|
50
|
-
|
|
51
|
-
models:
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LlmCostTracker
|
|
4
|
-
module PriceSync
|
|
5
|
-
class Merger
|
|
6
|
-
Discrepancy = Data.define(:model, :field, :values)
|
|
7
|
-
|
|
8
|
-
PRIORITY_ORDER = %i[litellm openrouter].freeze
|
|
9
|
-
SUPPLEMENTAL_FIELDS = %i[cache_read_input cache_write_input].freeze
|
|
10
|
-
|
|
11
|
-
def merge(results_by_source)
|
|
12
|
-
prices = collect_prices(results_by_source)
|
|
13
|
-
discrepancies = []
|
|
14
|
-
|
|
15
|
-
merged = prices.group_by(&:model).sort.to_h.transform_values do |candidates|
|
|
16
|
-
sorted = sort_candidates(candidates)
|
|
17
|
-
discrepancies.concat(detect_discrepancies(sorted))
|
|
18
|
-
fill_missing_fields(sorted.first, sorted.drop(1))
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
[merged, discrepancies]
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def collect_prices(results_by_source)
|
|
27
|
-
results_by_source.flat_map do |source_name, result|
|
|
28
|
-
result.prices.map do |price|
|
|
29
|
-
price.with(source: source_name)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def sort_candidates(candidates)
|
|
35
|
-
candidates.sort_by do |price|
|
|
36
|
-
PRIORITY_ORDER.index(price.source.to_sym) || PRIORITY_ORDER.length
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def fill_missing_fields(primary, fallbacks)
|
|
41
|
-
SUPPLEMENTAL_FIELDS.reduce(primary) do |current, field|
|
|
42
|
-
next current if current.public_send(field)
|
|
43
|
-
|
|
44
|
-
fallback = fallbacks.find { |candidate| candidate.public_send(field) }
|
|
45
|
-
fallback ? current.with(field => fallback.public_send(field)) : current
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def detect_discrepancies(candidates)
|
|
50
|
-
return [] if candidates.length < 2
|
|
51
|
-
|
|
52
|
-
RawPrice::PRICE_FIELDS.filter_map do |field|
|
|
53
|
-
values = candidates.each_with_object({}) do |price, collected|
|
|
54
|
-
value = price.public_send(field)
|
|
55
|
-
collected[price.source] = value unless value.nil?
|
|
56
|
-
end
|
|
57
|
-
next if values.size < 2
|
|
58
|
-
next unless discrepant?(values.values)
|
|
59
|
-
|
|
60
|
-
Discrepancy.new(model: candidates.first.model, field: field, values: values)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def discrepant?(values)
|
|
65
|
-
min, max = values.minmax
|
|
66
|
-
return max != min if min.to_f.zero?
|
|
67
|
-
|
|
68
|
-
((max - min).abs / min.to_f) >= 0.05
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|