llm_cost_tracker 0.7.1 → 0.7.2
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 +10 -7
- data/lib/llm_cost_tracker/capture/stream_collector.rb +11 -4
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +1 -1
- data/lib/llm_cost_tracker/configuration.rb +5 -1
- data/lib/llm_cost_tracker/integrations/anthropic.rb +25 -8
- data/lib/llm_cost_tracker/integrations/openai.rb +4 -4
- data/lib/llm_cost_tracker/middleware/faraday.rb +56 -13
- data/lib/llm_cost_tracker/parsers/anthropic.rb +35 -13
- data/lib/llm_cost_tracker/parsers/base.rb +2 -2
- data/lib/llm_cost_tracker/parsers/gemini.rb +38 -12
- data/lib/llm_cost_tracker/parsers/openai.rb +27 -5
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +14 -4
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +41 -13
- data/lib/llm_cost_tracker/prices.json +316 -32
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +23 -17
- data/lib/llm_cost_tracker/pricing/explainer.rb +17 -11
- data/lib/llm_cost_tracker/pricing/lookup.rb +44 -22
- data/lib/llm_cost_tracker/pricing/sync.rb +19 -3
- data/lib/llm_cost_tracker/tracker.rb +6 -4
- data/lib/llm_cost_tracker/version.rb +1 -1
- metadata +2 -2
|
@@ -22,10 +22,27 @@ module LlmCostTracker
|
|
|
22
22
|
current = current_price_tables
|
|
23
23
|
|
|
24
24
|
match =
|
|
25
|
-
explain_table(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
explain_table(
|
|
26
|
+
table: current.fetch(:pricing_overrides),
|
|
27
|
+
source: :pricing_overrides,
|
|
28
|
+
provider_model: provider_model,
|
|
29
|
+
model_name: model_name,
|
|
30
|
+
normalized_model: normalized_model
|
|
31
|
+
) ||
|
|
32
|
+
explain_table(
|
|
33
|
+
table: current.fetch(:file_prices),
|
|
34
|
+
source: :prices_file,
|
|
35
|
+
provider_model: provider_model,
|
|
36
|
+
model_name: model_name,
|
|
37
|
+
normalized_model: normalized_model
|
|
38
|
+
) ||
|
|
39
|
+
explain_table(
|
|
40
|
+
table: Registry.builtin_prices,
|
|
41
|
+
source: :bundled,
|
|
42
|
+
provider_model: provider_model,
|
|
43
|
+
model_name: model_name,
|
|
44
|
+
normalized_model: normalized_model
|
|
45
|
+
)
|
|
29
46
|
cache_lookup(cache_key, match)
|
|
30
47
|
match
|
|
31
48
|
end
|
|
@@ -74,46 +91,51 @@ module LlmCostTracker
|
|
|
74
91
|
end
|
|
75
92
|
end
|
|
76
93
|
|
|
77
|
-
def explain_table(table
|
|
94
|
+
def explain_table(table:, source:, provider_model:, model_name:, normalized_model:)
|
|
78
95
|
return nil if table.empty?
|
|
79
96
|
|
|
80
|
-
direct_match(table, source, provider_model, :provider_model) ||
|
|
81
|
-
direct_match(table, source, model_name, :model) ||
|
|
82
|
-
direct_match(table, source, normalized_model, :normalized_model) ||
|
|
83
|
-
unique_providerless_lookup(normalized_model, table, source) ||
|
|
84
|
-
fuzzy_match(provider_model, normalized_model, table, source) ||
|
|
85
|
-
unique_providerless_fuzzy_match(normalized_model, table, source)
|
|
97
|
+
direct_match(table: table, source: source, key: provider_model, matched_by: :provider_model) ||
|
|
98
|
+
direct_match(table: table, source: source, key: model_name, matched_by: :model) ||
|
|
99
|
+
direct_match(table: table, source: source, key: normalized_model, matched_by: :normalized_model) ||
|
|
100
|
+
unique_providerless_lookup(model: normalized_model, table: table, source: source) ||
|
|
101
|
+
fuzzy_match(model: provider_model, normalized_model: normalized_model, table: table, source: source) ||
|
|
102
|
+
unique_providerless_fuzzy_match(model: normalized_model, table: table, source: source)
|
|
86
103
|
end
|
|
87
104
|
|
|
88
105
|
def normalize_model_name(model)
|
|
89
106
|
model.to_s.split("/").last
|
|
90
107
|
end
|
|
91
108
|
|
|
92
|
-
def unique_providerless_lookup(model
|
|
109
|
+
def unique_providerless_lookup(model:, table:, source:)
|
|
93
110
|
matches = sorted_price_keys(table).select { |key| normalize_model_name(key) == model }
|
|
94
|
-
|
|
111
|
+
return unless matches.one?
|
|
112
|
+
|
|
113
|
+
match(table: table, source: source, key: matches.first, matched_by: :unique_providerless_model)
|
|
95
114
|
end
|
|
96
115
|
|
|
97
|
-
def fuzzy_match(model
|
|
116
|
+
def fuzzy_match(model:, normalized_model:, table:, source:)
|
|
98
117
|
sorted_price_keys(table).each do |key|
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
if snapshot_variant?(model, key) || snapshot_variant?(normalized_model, key)
|
|
119
|
+
return match(table: table, source: source, key: key, matched_by: :dated_snapshot)
|
|
120
|
+
end
|
|
101
121
|
end
|
|
102
122
|
|
|
103
123
|
nil
|
|
104
124
|
end
|
|
105
125
|
|
|
106
|
-
def unique_providerless_fuzzy_match(model
|
|
126
|
+
def unique_providerless_fuzzy_match(model:, table:, source:)
|
|
107
127
|
matches = sorted_price_keys(table).select { |key| snapshot_variant?(model, normalize_model_name(key)) }
|
|
108
|
-
|
|
128
|
+
return unless matches.one?
|
|
129
|
+
|
|
130
|
+
match(table: table, source: source, key: matches.first, matched_by: :unique_providerless_dated_snapshot)
|
|
109
131
|
end
|
|
110
132
|
|
|
111
|
-
def direct_match(table
|
|
112
|
-
match(table, source, key, matched_by) if table.key?(key)
|
|
133
|
+
def direct_match(table:, source:, key:, matched_by:)
|
|
134
|
+
match(table: table, source: source, key: key, matched_by: matched_by) if table.key?(key)
|
|
113
135
|
end
|
|
114
136
|
|
|
115
|
-
def match(table
|
|
116
|
-
Match.new(source.to_s, key, table[key], matched_by.to_s)
|
|
137
|
+
def match(table:, source:, key:, matched_by:)
|
|
138
|
+
Match.new(source: source.to_s, key: key, prices: table[key], matched_by: matched_by.to_s)
|
|
117
139
|
end
|
|
118
140
|
|
|
119
141
|
def snapshot_variant?(model, key)
|
|
@@ -43,12 +43,28 @@ module LlmCostTracker
|
|
|
43
43
|
response = fetcher.get(url, etag: current.dig("metadata", "source_version"))
|
|
44
44
|
|
|
45
45
|
if response.not_modified
|
|
46
|
-
return refresh_result(
|
|
46
|
+
return refresh_result(
|
|
47
|
+
path: path,
|
|
48
|
+
url: url,
|
|
49
|
+
response: response,
|
|
50
|
+
current: current,
|
|
51
|
+
remote: current,
|
|
52
|
+
written: false,
|
|
53
|
+
not_modified: true
|
|
54
|
+
)
|
|
47
55
|
end
|
|
48
56
|
|
|
49
57
|
remote = normalize_remote_registry(response.body, url: url, response: response, today: today)
|
|
50
58
|
RegistryWriter.new.call(path: path, registry: remote) unless preview
|
|
51
|
-
refresh_result(
|
|
59
|
+
refresh_result(
|
|
60
|
+
path: path,
|
|
61
|
+
url: url,
|
|
62
|
+
response: response,
|
|
63
|
+
current: current,
|
|
64
|
+
remote: remote,
|
|
65
|
+
written: !preview,
|
|
66
|
+
not_modified: false
|
|
67
|
+
)
|
|
52
68
|
end
|
|
53
69
|
|
|
54
70
|
def check(path: DEFAULT_OUTPUT_PATH, url: DEFAULT_REMOTE_URL, fetcher: Fetcher.new, today: Date.today)
|
|
@@ -127,7 +143,7 @@ module LlmCostTracker
|
|
|
127
143
|
raise Error, "Unable to parse remote pricing snapshot: #{e.message}"
|
|
128
144
|
end
|
|
129
145
|
|
|
130
|
-
def refresh_result(path
|
|
146
|
+
def refresh_result(path:, url:, response:, current:, remote:, written:, not_modified:)
|
|
131
147
|
RefreshResult.new(
|
|
132
148
|
path: path,
|
|
133
149
|
source_url: url,
|
|
@@ -21,7 +21,7 @@ module LlmCostTracker
|
|
|
21
21
|
Budget.enforce!
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
def record(capture:, latency_ms: nil, pricing_mode: nil, metadata: {})
|
|
24
|
+
def record(capture:, latency_ms: nil, pricing_mode: nil, metadata: {}, context_tags: nil)
|
|
25
25
|
return unless LlmCostTracker.configuration.enabled
|
|
26
26
|
|
|
27
27
|
pricing_mode = Pricing.normalize_mode(pricing_mode) || capture.pricing_mode
|
|
@@ -39,7 +39,8 @@ module LlmCostTracker
|
|
|
39
39
|
pricing_mode: pricing_mode,
|
|
40
40
|
cost_data: cost_data,
|
|
41
41
|
metadata: metadata,
|
|
42
|
-
latency_ms: latency_ms
|
|
42
|
+
latency_ms: latency_ms,
|
|
43
|
+
context_tags: context_tags
|
|
43
44
|
)
|
|
44
45
|
|
|
45
46
|
ActiveSupport::Notifications.instrument(EVENT_NAME, event.to_h)
|
|
@@ -52,7 +53,7 @@ module LlmCostTracker
|
|
|
52
53
|
|
|
53
54
|
private
|
|
54
55
|
|
|
55
|
-
def build_event(capture:, pricing_mode:, cost_data:, metadata:, latency_ms:)
|
|
56
|
+
def build_event(capture:, pricing_mode:, cost_data:, metadata:, latency_ms:, context_tags:)
|
|
56
57
|
usage_source = if capture.usage_source.nil?
|
|
57
58
|
nil
|
|
58
59
|
else
|
|
@@ -60,6 +61,7 @@ module LlmCostTracker
|
|
|
60
61
|
USAGE_SOURCES.include?(symbol) ? symbol.to_s : nil
|
|
61
62
|
end
|
|
62
63
|
tags = metadata.to_h.reject { |key, _value| TRACKING_METADATA_KEYS.include?(key.to_s) }
|
|
64
|
+
context_tags = context_tags.nil? ? LlmCostTracker::Tags::Context.tags : context_tags.to_h
|
|
63
65
|
|
|
64
66
|
Event.new(
|
|
65
67
|
event_id: SecureRandom.uuid,
|
|
@@ -69,7 +71,7 @@ module LlmCostTracker
|
|
|
69
71
|
pricing_mode: pricing_mode,
|
|
70
72
|
cost: cost_data,
|
|
71
73
|
tags: LlmCostTracker::Tags::Sanitizer.call(
|
|
72
|
-
|
|
74
|
+
context_tags.merge(tags)
|
|
73
75
|
).freeze,
|
|
74
76
|
latency_ms: latency_ms.nil? ? nil : [latency_ms.to_i, 0].max,
|
|
75
77
|
stream: capture.stream ? true : false,
|
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.7.
|
|
4
|
+
version: 0.7.2
|
|
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-
|
|
11
|
+
date: 2026-05-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|