llm_cost_tracker 0.12.0 → 0.13.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 +18 -0
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +1 -0
- data/app/helpers/llm_cost_tracker/application_helper.rb +6 -4
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +1 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +12 -0
- data/app/views/llm_cost_tracker/calls/show.html.erb +2 -2
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +11 -0
- data/app/views/llm_cost_tracker/pricing/index.html.erb +1 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +5 -4
- data/lib/llm_cost_tracker/charges/line_item.rb +3 -2
- data/lib/llm_cost_tracker/event.rb +3 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +1 -1
- data/lib/llm_cost_tracker/ingestion/batch.rb +25 -7
- data/lib/llm_cost_tracker/integrations/anthropic.rb +8 -7
- data/lib/llm_cost_tracker/integrations/base.rb +1 -1
- data/lib/llm_cost_tracker/ledger/store.rb +8 -1
- data/lib/llm_cost_tracker/prices.json +173 -71
- data/lib/llm_cost_tracker/pricing/calculation.rb +40 -21
- data/lib/llm_cost_tracker/pricing/matcher.rb +17 -3
- data/lib/llm_cost_tracker/pricing/mode.rb +7 -7
- data/lib/llm_cost_tracker/tracker.rb +2 -3
- data/lib/llm_cost_tracker/usage/catalog.rb +11 -0
- data/lib/llm_cost_tracker/usage/dimensions.yml +0 -24
- data/lib/llm_cost_tracker/usage/token_usage.rb +20 -75
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +1 -1
- metadata +1 -1
|
@@ -9,22 +9,36 @@ module LlmCostTracker
|
|
|
9
9
|
module Matcher
|
|
10
10
|
Match = Data.define(:source, :key, :prices, :matched_by)
|
|
11
11
|
|
|
12
|
+
CACHE_LIMIT = 2048
|
|
13
|
+
private_constant :CACHE_LIMIT
|
|
14
|
+
|
|
12
15
|
class << self
|
|
13
16
|
def lookup(provider:, model:)
|
|
14
17
|
provider_name = provider.to_s.presence
|
|
15
18
|
model_name = model.to_s
|
|
16
19
|
return nil if model_name.empty?
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
sources = Registry.sources
|
|
22
|
+
reset_cache(sources) unless @cache_sources.equal?(sources)
|
|
23
|
+
key = [provider_name, model_name].freeze
|
|
24
|
+
return @cache[key] if @cache.key?(key)
|
|
25
|
+
|
|
26
|
+
@cache.clear if @cache.size >= CACHE_LIMIT
|
|
27
|
+
@cache[key] = lookup_match(sources, provider_name, model_name)
|
|
19
28
|
end
|
|
20
29
|
|
|
21
30
|
private
|
|
22
31
|
|
|
23
|
-
def
|
|
32
|
+
def reset_cache(sources)
|
|
33
|
+
@cache_sources = sources
|
|
34
|
+
@cache = {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def lookup_match(sources, provider_name, model_name)
|
|
24
38
|
provider_model = provider_name ? "#{provider_name}/#{model_name}" : model_name
|
|
25
39
|
normalized = normalize_model_name(model_name)
|
|
26
40
|
|
|
27
|
-
|
|
41
|
+
sources.each do |source|
|
|
28
42
|
match = match_in_source(source, provider_model, model_name, normalized)
|
|
29
43
|
return match if match
|
|
30
44
|
end
|
|
@@ -4,8 +4,8 @@ module LlmCostTracker
|
|
|
4
4
|
module Pricing
|
|
5
5
|
module Mode
|
|
6
6
|
STANDARD_MODE_VALUES = %w[auto default standard standard_only unspecified].freeze
|
|
7
|
-
COMPOUND_MODIFIERS = %w[data_residency].freeze
|
|
8
7
|
KNOWN_MODIFIERS = %w[batch flex priority scale fast on_demand data_residency].freeze
|
|
8
|
+
HOST_DERIVED_MODIFIERS = %w[data_residency].freeze
|
|
9
9
|
MAX_PERMUTED_MODIFIERS = 6
|
|
10
10
|
|
|
11
11
|
def self.normalize(value)
|
|
@@ -23,7 +23,7 @@ module LlmCostTracker
|
|
|
23
23
|
return normalize(request_mode) if provider_mode.to_s.strip.empty?
|
|
24
24
|
|
|
25
25
|
provider_tokens = tokenize(provider_mode) - STANDARD_MODE_VALUES
|
|
26
|
-
request_host_tokens = tokenize(request_mode || "") &
|
|
26
|
+
request_host_tokens = tokenize(request_mode || "") & HOST_DERIVED_MODIFIERS
|
|
27
27
|
combined = provider_tokens | request_host_tokens
|
|
28
28
|
return nil if combined.empty?
|
|
29
29
|
|
|
@@ -41,12 +41,12 @@ module LlmCostTracker
|
|
|
41
41
|
loop do
|
|
42
42
|
break if remaining.empty?
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
remaining ==
|
|
44
|
+
known = KNOWN_MODIFIERS.find do |modifier|
|
|
45
|
+
remaining == modifier || remaining.start_with?("#{modifier}_")
|
|
46
46
|
end
|
|
47
|
-
if
|
|
48
|
-
tokens <<
|
|
49
|
-
remaining = remaining.delete_prefix(
|
|
47
|
+
if known
|
|
48
|
+
tokens << known
|
|
49
|
+
remaining = remaining.delete_prefix(known).delete_prefix("_")
|
|
50
50
|
else
|
|
51
51
|
first, _, rest = remaining.partition("_")
|
|
52
52
|
tokens << first unless first.empty?
|
|
@@ -13,16 +13,15 @@ module LlmCostTracker
|
|
|
13
13
|
EVENT_NAME = "llm_request.llm_cost_tracker"
|
|
14
14
|
|
|
15
15
|
class << self
|
|
16
|
-
def record(event:, latency_ms: nil,
|
|
16
|
+
def record(event:, latency_ms: nil, metadata: {}, context_tags: nil, enforce_budget: false)
|
|
17
17
|
return unless LlmCostTracker.configuration.enabled
|
|
18
18
|
|
|
19
|
-
pricing_mode ||= event.pricing_mode
|
|
20
19
|
calculation = Pricing::Calculation.for(
|
|
21
20
|
provider: event.provider,
|
|
22
21
|
model: event.model,
|
|
23
22
|
tokens: event.token_usage,
|
|
24
23
|
line_items: event.line_items,
|
|
25
|
-
pricing_mode: pricing_mode,
|
|
24
|
+
pricing_mode: event.pricing_mode,
|
|
26
25
|
usage_source: event.usage_source
|
|
27
26
|
)
|
|
28
27
|
|
|
@@ -31,6 +31,10 @@ module LlmCostTracker
|
|
|
31
31
|
@token_priced ||= all.select(&:token_key).freeze
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def find_by(kind:, direction:, modality:, cache_state:, unit:)
|
|
35
|
+
by_attributes[[kind, direction, modality, cache_state, unit]]
|
|
36
|
+
end
|
|
37
|
+
|
|
34
38
|
def token_priced_for(kind:, direction:, cache_state:)
|
|
35
39
|
token_priced.find do |dimension|
|
|
36
40
|
dimension.kind == kind && dimension.direction == direction && dimension.cache_state == cache_state
|
|
@@ -43,6 +47,13 @@ module LlmCostTracker
|
|
|
43
47
|
@index ||= all.to_h { |dimension| [dimension.key, dimension] }.freeze
|
|
44
48
|
end
|
|
45
49
|
|
|
50
|
+
def by_attributes
|
|
51
|
+
@by_attributes ||= all.to_h do |dimension|
|
|
52
|
+
key = [dimension.kind, dimension.direction, dimension.modality, dimension.cache_state, dimension.unit]
|
|
53
|
+
[key, dimension]
|
|
54
|
+
end.freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
46
57
|
def load_definitions
|
|
47
58
|
Psych.safe_load_file(DEFINITIONS_PATH, permitted_classes: [], symbolize_names: true)
|
|
48
59
|
.map { |attributes| build(attributes) }
|
|
@@ -140,27 +140,3 @@
|
|
|
140
140
|
cache_state: none
|
|
141
141
|
unit: minute
|
|
142
142
|
rate_basis: per_minute
|
|
143
|
-
|
|
144
|
-
- key: image_generation_call
|
|
145
|
-
kind: image_generation_call
|
|
146
|
-
direction: output
|
|
147
|
-
modality: image
|
|
148
|
-
cache_state: none
|
|
149
|
-
unit: image
|
|
150
|
-
rate_basis: per_image
|
|
151
|
-
|
|
152
|
-
- key: computer_call
|
|
153
|
-
kind: computer_call
|
|
154
|
-
direction: neither
|
|
155
|
-
modality: text
|
|
156
|
-
cache_state: none
|
|
157
|
-
unit: request
|
|
158
|
-
rate_basis: per_request
|
|
159
|
-
|
|
160
|
-
- key: mcp_call
|
|
161
|
-
kind: mcp_call
|
|
162
|
-
direction: neither
|
|
163
|
-
modality: text
|
|
164
|
-
cache_state: none
|
|
165
|
-
unit: request
|
|
166
|
-
rate_basis: per_request
|
|
@@ -4,97 +4,42 @@ require_relative "catalog"
|
|
|
4
4
|
|
|
5
5
|
module LlmCostTracker
|
|
6
6
|
module Usage
|
|
7
|
-
|
|
8
|
-
Catalog.token_priced.map { |dimension| dimension.token_key.to_s } + %w[total_tokens hidden_output_tokens]
|
|
9
|
-
).freeze
|
|
10
|
-
|
|
11
|
-
TokenUsage = Data.define(
|
|
12
|
-
:input_tokens,
|
|
13
|
-
:cache_read_input_tokens,
|
|
14
|
-
:cache_write_input_tokens,
|
|
15
|
-
:cache_write_extended_input_tokens,
|
|
16
|
-
:audio_input_tokens,
|
|
17
|
-
:image_input_tokens,
|
|
18
|
-
:output_tokens,
|
|
19
|
-
:audio_output_tokens,
|
|
20
|
-
:image_output_tokens,
|
|
21
|
-
:total_tokens,
|
|
22
|
-
:hidden_output_tokens
|
|
23
|
-
) do
|
|
7
|
+
TokenUsage = Data.define(*Catalog.token_priced.map(&:token_key), :total_tokens, :hidden_output_tokens) do
|
|
24
8
|
def priced_quantities
|
|
25
9
|
Catalog.token_priced.to_h { |dimension| [dimension.key, public_send(dimension.token_key)] }
|
|
26
10
|
end
|
|
27
11
|
|
|
12
|
+
def self.build(**values)
|
|
13
|
+
unknown = values.keys - members
|
|
14
|
+
raise ArgumentError, "unknown token keys: #{unknown.inspect}" if unknown.any?
|
|
15
|
+
|
|
16
|
+
priced = Catalog.token_priced.to_h do |dimension|
|
|
17
|
+
[dimension.token_key, non_negative_int(values[dimension.token_key])]
|
|
18
|
+
end
|
|
19
|
+
subtotal = priced.values.sum
|
|
20
|
+
declared_total = values[:total_tokens]
|
|
21
|
+
total = declared_total ? [non_negative_int(declared_total), subtotal].max : subtotal
|
|
22
|
+
new(**priced, total_tokens: total, hidden_output_tokens: non_negative_int(values[:hidden_output_tokens]))
|
|
23
|
+
end
|
|
24
|
+
|
|
28
25
|
def self.build_from_tokens(tokens)
|
|
29
26
|
return tokens if tokens.is_a?(self)
|
|
30
27
|
raise ArgumentError, "tokens must be a Hash, got #{tokens.class}" unless tokens.respond_to?(:to_h)
|
|
31
28
|
|
|
32
29
|
values = tokens.to_h.transform_keys(&:to_s)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
known = members.map(&:to_s)
|
|
31
|
+
unknown = values.keys - known
|
|
32
|
+
if unknown.any?
|
|
33
|
+
hint = values.keys.intersect?(known) ? "" : ". Did you pass a raw provider response?"
|
|
34
|
+
raise ArgumentError, "unknown token keys: #{unknown.inspect}; expected #{known.inspect}#{hint}"
|
|
36
35
|
end
|
|
37
36
|
|
|
38
|
-
build(
|
|
39
|
-
**token_attributes,
|
|
40
|
-
total_tokens: values["total_tokens"],
|
|
41
|
-
hidden_output_tokens: values.fetch("hidden_output_tokens", 0)
|
|
42
|
-
)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def self.warn_on_unknown_keys(values)
|
|
46
|
-
return if values.empty?
|
|
47
|
-
return if values.keys.intersect?(KNOWN_TOKEN_KEYS)
|
|
48
|
-
|
|
49
|
-
Logging.warn(
|
|
50
|
-
"tokens hash contains no recognized keys (#{values.keys.inspect}); " \
|
|
51
|
-
"expected one of #{KNOWN_TOKEN_KEYS.inspect}. Did you pass a raw provider response?"
|
|
52
|
-
)
|
|
37
|
+
build(**values.transform_keys(&:to_sym))
|
|
53
38
|
end
|
|
54
39
|
|
|
55
40
|
def self.non_negative_int(value)
|
|
56
41
|
[value.to_i, 0].max
|
|
57
42
|
end
|
|
58
|
-
|
|
59
|
-
def self.build(input_tokens:,
|
|
60
|
-
output_tokens:,
|
|
61
|
-
cache_read_input_tokens: 0,
|
|
62
|
-
cache_write_input_tokens: 0,
|
|
63
|
-
cache_write_extended_input_tokens: 0,
|
|
64
|
-
audio_input_tokens: 0,
|
|
65
|
-
audio_output_tokens: 0,
|
|
66
|
-
image_input_tokens: 0,
|
|
67
|
-
image_output_tokens: 0,
|
|
68
|
-
total_tokens: nil,
|
|
69
|
-
hidden_output_tokens: 0)
|
|
70
|
-
input = non_negative_int(input_tokens)
|
|
71
|
-
output = non_negative_int(output_tokens)
|
|
72
|
-
cache_read = non_negative_int(cache_read_input_tokens)
|
|
73
|
-
cache_write = non_negative_int(cache_write_input_tokens)
|
|
74
|
-
cache_write_extended = non_negative_int(cache_write_extended_input_tokens)
|
|
75
|
-
audio_input = non_negative_int(audio_input_tokens)
|
|
76
|
-
audio_output = non_negative_int(audio_output_tokens)
|
|
77
|
-
image_input = non_negative_int(image_input_tokens)
|
|
78
|
-
image_output = non_negative_int(image_output_tokens)
|
|
79
|
-
hidden_output = non_negative_int(hidden_output_tokens)
|
|
80
|
-
calculated_total = input + cache_read + cache_write + cache_write_extended +
|
|
81
|
-
audio_input + image_input + output + audio_output + image_output
|
|
82
|
-
total = total_tokens ? [non_negative_int(total_tokens), calculated_total].max : calculated_total
|
|
83
|
-
|
|
84
|
-
new(
|
|
85
|
-
input_tokens: input,
|
|
86
|
-
cache_read_input_tokens: cache_read,
|
|
87
|
-
cache_write_input_tokens: cache_write,
|
|
88
|
-
cache_write_extended_input_tokens: cache_write_extended,
|
|
89
|
-
audio_input_tokens: audio_input,
|
|
90
|
-
image_input_tokens: image_input,
|
|
91
|
-
output_tokens: output,
|
|
92
|
-
audio_output_tokens: audio_output,
|
|
93
|
-
image_output_tokens: image_output,
|
|
94
|
-
total_tokens: total,
|
|
95
|
-
hidden_output_tokens: hidden_output
|
|
96
|
-
)
|
|
97
|
-
end
|
|
98
43
|
end
|
|
99
44
|
end
|
|
100
45
|
end
|
data/lib/llm_cost_tracker.rb
CHANGED
|
@@ -30,7 +30,7 @@ require_relative "llm_cost_tracker/ingestion"
|
|
|
30
30
|
require_relative "llm_cost_tracker/tracker"
|
|
31
31
|
|
|
32
32
|
module LlmCostTracker
|
|
33
|
-
|
|
33
|
+
require_relative "llm_cost_tracker/engine"
|
|
34
34
|
autoload :Doctor, "llm_cost_tracker/doctor"
|
|
35
35
|
autoload :CaptureVerifier, "llm_cost_tracker/capture_verifier"
|
|
36
36
|
autoload :Report, "llm_cost_tracker/report"
|