llm_cost_tracker 0.7.1 → 0.7.3

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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +16 -9
  4. data/app/models/llm_cost_tracker/ledger/call.rb +1 -1
  5. data/app/models/llm_cost_tracker/ledger/call_metrics.rb +1 -1
  6. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +9 -9
  7. data/lib/llm_cost_tracker/capture/stream_collector.rb +11 -4
  8. data/lib/llm_cost_tracker/capture/stream_tracker.rb +1 -1
  9. data/lib/llm_cost_tracker/configuration.rb +5 -1
  10. data/lib/llm_cost_tracker/integrations/anthropic.rb +25 -8
  11. data/lib/llm_cost_tracker/integrations/openai.rb +4 -4
  12. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +4 -10
  13. data/lib/llm_cost_tracker/ledger/rollups.rb +7 -7
  14. data/lib/llm_cost_tracker/ledger/store.rb +22 -13
  15. data/lib/llm_cost_tracker/ledger/tags/query.rb +5 -5
  16. data/lib/llm_cost_tracker/ledger/tags/sql.rb +8 -7
  17. data/lib/llm_cost_tracker/middleware/faraday.rb +56 -13
  18. data/lib/llm_cost_tracker/parsers/anthropic.rb +35 -13
  19. data/lib/llm_cost_tracker/parsers/base.rb +2 -2
  20. data/lib/llm_cost_tracker/parsers/gemini.rb +39 -13
  21. data/lib/llm_cost_tracker/parsers/openai.rb +27 -5
  22. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +14 -4
  23. data/lib/llm_cost_tracker/parsers/openai_usage.rb +41 -13
  24. data/lib/llm_cost_tracker/prices.json +316 -32
  25. data/lib/llm_cost_tracker/pricing/effective_prices.rb +23 -17
  26. data/lib/llm_cost_tracker/pricing/explainer.rb +17 -11
  27. data/lib/llm_cost_tracker/pricing/lookup.rb +44 -22
  28. data/lib/llm_cost_tracker/pricing/sync.rb +19 -3
  29. data/lib/llm_cost_tracker/tracker.rb +6 -4
  30. data/lib/llm_cost_tracker/version.rb +1 -1
  31. metadata +2 -2
@@ -22,10 +22,27 @@ module LlmCostTracker
22
22
  current = current_price_tables
23
23
 
24
24
  match =
25
- explain_table(current.fetch(:pricing_overrides), :pricing_overrides, provider_model, model_name,
26
- normalized_model) ||
27
- explain_table(current.fetch(:file_prices), :prices_file, provider_model, model_name, normalized_model) ||
28
- explain_table(Registry.builtin_prices, :bundled, provider_model, model_name, normalized_model)
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, source, provider_model, model_name, normalized_model)
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, table, source)
109
+ def unique_providerless_lookup(model:, table:, source:)
93
110
  matches = sorted_price_keys(table).select { |key| normalize_model_name(key) == model }
94
- match(table, source, matches.first, :unique_providerless_model) if matches.one?
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, normalized_model, table, source)
116
+ def fuzzy_match(model:, normalized_model:, table:, source:)
98
117
  sorted_price_keys(table).each do |key|
99
- return match(table, source, key, :dated_snapshot) if snapshot_variant?(model, key) ||
100
- snapshot_variant?(normalized_model, key)
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, table, source)
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
- match(table, source, matches.first, :unique_providerless_dated_snapshot) if matches.one?
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, source, key, matched_by)
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, source, key, matched_by)
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(path, url, response, current, current, written: false, not_modified: true)
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(path, url, response, current, remote, written: !preview, not_modified: false)
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, url, response, current, remote, written:, not_modified:)
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
- LlmCostTracker::Tags::Context.tags.merge(tags)
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,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmCostTracker
4
- VERSION = "0.7.1"
4
+ VERSION = "0.7.3"
5
5
  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.7.1
4
+ version: 0.7.3
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-30 00:00:00.000000000 Z
11
+ date: 2026-05-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord