llm_cost_tracker 0.12.0 → 0.14.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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -16
  3. data/README.md +16 -28
  4. data/app/controllers/llm_cost_tracker/application_controller.rb +3 -2
  5. data/app/controllers/llm_cost_tracker/data_quality_controller.rb +2 -0
  6. data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
  7. data/app/helpers/llm_cost_tracker/application_helper.rb +6 -4
  8. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +11 -0
  9. data/app/models/llm_cost_tracker/call.rb +9 -3
  10. data/app/models/llm_cost_tracker/call_rollup.rb +19 -3
  11. data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +1 -0
  12. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +25 -0
  13. data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +1 -1
  14. data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -1
  15. data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +3 -3
  16. data/app/services/llm_cost_tracker/dashboard/setup_state.rb +5 -3
  17. data/app/views/llm_cost_tracker/calls/show.html.erb +8 -10
  18. data/app/views/llm_cost_tracker/data_quality/index.html.erb +22 -0
  19. data/app/views/llm_cost_tracker/pricing/index.html.erb +1 -1
  20. data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +1 -3
  21. data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -3
  22. data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -3
  23. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -3
  24. data/lib/llm_cost_tracker/budget/per_tag.rb +159 -0
  25. data/lib/llm_cost_tracker/budget.rb +120 -32
  26. data/lib/llm_cost_tracker/capture/stream_collector.rb +5 -4
  27. data/lib/llm_cost_tracker/charges/cost_status.rb +4 -3
  28. data/lib/llm_cost_tracker/charges/line_item.rb +3 -2
  29. data/lib/llm_cost_tracker/configuration/budgets.rb +93 -0
  30. data/lib/llm_cost_tracker/configuration/capture.rb +42 -0
  31. data/lib/llm_cost_tracker/configuration/ingestion.rb +20 -0
  32. data/lib/llm_cost_tracker/configuration/mutability.rb +33 -0
  33. data/lib/llm_cost_tracker/configuration/pricing.rb +36 -0
  34. data/lib/llm_cost_tracker/configuration/section.rb +59 -0
  35. data/lib/llm_cost_tracker/configuration/tags.rb +52 -0
  36. data/lib/llm_cost_tracker/configuration.rb +69 -125
  37. data/lib/llm_cost_tracker/deprecator.rb +9 -0
  38. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +17 -8
  39. data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
  40. data/lib/llm_cost_tracker/doctor.rb +4 -4
  41. data/lib/llm_cost_tracker/engine.rb +4 -0
  42. data/lib/llm_cost_tracker/errors.rb +12 -3
  43. data/lib/llm_cost_tracker/event.rb +3 -1
  44. data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +2 -2
  45. data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +2 -2
  46. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +0 -1
  47. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +8 -4
  48. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +50 -30
  49. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +40 -0
  50. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +41 -0
  51. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb +30 -0
  52. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb +30 -0
  53. data/lib/llm_cost_tracker/ingestion/batch.rb +27 -7
  54. data/lib/llm_cost_tracker/ingestion/pool.rb +9 -2
  55. data/lib/llm_cost_tracker/ingestion.rb +3 -7
  56. data/lib/llm_cost_tracker/integrations/anthropic.rb +8 -7
  57. data/lib/llm_cost_tracker/integrations/base.rb +1 -1
  58. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +10 -12
  59. data/lib/llm_cost_tracker/ledger/period/totals.rb +1 -1
  60. data/lib/llm_cost_tracker/ledger/rollups.rb +34 -4
  61. data/lib/llm_cost_tracker/ledger/store.rb +16 -2
  62. data/lib/llm_cost_tracker/ledger/tags/encoding.rb +15 -5
  63. data/lib/llm_cost_tracker/logging.rb +3 -5
  64. data/lib/llm_cost_tracker/middleware/faraday.rb +1 -1
  65. data/lib/llm_cost_tracker/prices.json +1384 -304
  66. data/lib/llm_cost_tracker/pricing/backfill.rb +11 -1
  67. data/lib/llm_cost_tracker/pricing/calculation.rb +44 -25
  68. data/lib/llm_cost_tracker/pricing/effective_prices.rb +26 -15
  69. data/lib/llm_cost_tracker/pricing/matcher.rb +24 -3
  70. data/lib/llm_cost_tracker/pricing/mode.rb +7 -7
  71. data/lib/llm_cost_tracker/pricing/rate.rb +1 -2
  72. data/lib/llm_cost_tracker/pricing/registry.rb +5 -5
  73. data/lib/llm_cost_tracker/pricing/sync.rb +1 -1
  74. data/lib/llm_cost_tracker/pricing/unknown.rb +11 -8
  75. data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +3 -2
  76. data/lib/llm_cost_tracker/providers/openai/model_families.rb +0 -7
  77. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +5 -2
  78. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +5 -4
  79. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +2 -2
  80. data/lib/llm_cost_tracker/railtie.rb +3 -7
  81. data/lib/llm_cost_tracker/report/data.rb +2 -2
  82. data/lib/llm_cost_tracker/retention.rb +22 -10
  83. data/lib/llm_cost_tracker/tags/context.rb +3 -3
  84. data/lib/llm_cost_tracker/tags/sanitizer.rb +4 -4
  85. data/lib/llm_cost_tracker/tracker.rb +16 -26
  86. data/lib/llm_cost_tracker/usage/catalog.rb +12 -2
  87. data/lib/llm_cost_tracker/usage/dimensions.yml +0 -24
  88. data/lib/llm_cost_tracker/usage/token_usage.rb +20 -75
  89. data/lib/llm_cost_tracker/version.rb +1 -1
  90. data/lib/llm_cost_tracker.rb +7 -2
  91. data/lib/tasks/llm_cost_tracker.rake +23 -10
  92. metadata +22 -9
@@ -23,9 +23,9 @@ module LlmCostTracker
23
23
  class << self
24
24
  def call(tags, config: LlmCostTracker.configuration)
25
25
  tags = (tags || {}).to_h
26
- redacted = config.normalized_redacted_tag_keys
27
- limit = [config.max_tag_value_bytesize.to_i, 0].max
28
- max_count = [config.max_tag_count.to_i, 0].max
26
+ redacted = config.tags.normalized_redacted_keys
27
+ limit = [config.tags.max_value_bytesize.to_i, 0].max
28
+ max_count = [config.tags.max_count.to_i, 0].max
29
29
  tags.to_a.last(max_count).each_with_object({}) do |(key, value), sanitized|
30
30
  next unless valid_key?(key)
31
31
 
@@ -43,7 +43,7 @@ module LlmCostTracker
43
43
 
44
44
  def cap(tags, config: LlmCostTracker.configuration)
45
45
  tags = (tags || {}).to_h
46
- max_count = [config.max_tag_count.to_i, 0].max
46
+ max_count = [config.tags.max_count.to_i, 0].max
47
47
  return tags if tags.size <= max_count
48
48
 
49
49
  tags.to_a.last(max_count).to_h
@@ -13,38 +13,26 @@ module LlmCostTracker
13
13
  EVENT_NAME = "llm_request.llm_cost_tracker"
14
14
 
15
15
  class << self
16
- def record(event:, latency_ms: nil, pricing_mode: nil, metadata: {}, context_tags: nil, enforce_budget: false)
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
 
29
- if enforce_budget
30
- Budget.enforce!(provider: event.provider,
31
- model: event.model,
32
- estimate: calculation.cost&.total,
33
- force: true)
34
- end
28
+ tags = build_tags(context_tags: context_tags, metadata: metadata)
35
29
 
36
30
  if calculation.token_cost.nil? && event.token_usage.total_tokens.positive? &&
37
31
  calculation.priced_line_items.none?(&:priced?)
38
- Pricing::Unknown.process(event.model)
32
+ Pricing::Unknown.process(event.model, pricing_mode: calculation.mode)
39
33
  end
40
34
 
41
- event = build_event(
42
- event: event,
43
- calculation: calculation,
44
- metadata: metadata,
45
- latency_ms: latency_ms,
46
- context_tags: context_tags
47
- )
35
+ event = build_event(event: event, calculation: calculation, tags: tags, latency_ms: latency_ms)
48
36
 
49
37
  if Ingestion.async?
50
38
  Ingestion::Inbox.save(event)
@@ -55,11 +43,19 @@ module LlmCostTracker
55
43
 
56
44
  yield if block_given?
57
45
  notify_subscribers(event)
58
- Budget.check!(event)
46
+ behavior_override = :raise if enforce_budget
47
+ Budget.check!(event, behavior_override: behavior_override)
48
+ Budget.check_persisted!([event], behavior_override: behavior_override) unless Ingestion.async?
59
49
 
60
50
  event
61
51
  end
62
52
 
53
+ def build_tags(context_tags:, metadata:)
54
+ resolved = (context_tags || LlmCostTracker::Tags::Context.tags).to_h
55
+ sanitized_metadata = LlmCostTracker::Tags::Sanitizer.call(metadata.to_h)
56
+ LlmCostTracker::Tags::Sanitizer.cap(resolved.merge(sanitized_metadata)).freeze
57
+ end
58
+
63
59
  private
64
60
 
65
61
  def notify_subscribers(event)
@@ -70,13 +66,12 @@ module LlmCostTracker
70
66
  Logging.warn("Subscriber raised on #{EVENT_NAME}: #{e.class}: #{e.message}")
71
67
  end
72
68
 
73
- def build_event(event:, calculation:, metadata:, latency_ms:, context_tags:)
74
- context_tags = (context_tags || LlmCostTracker::Tags::Context.tags).to_h
69
+ def build_event(event:, calculation:, tags:, latency_ms:)
75
70
  event.with(
76
71
  event_id: SecureRandom.uuid,
77
72
  pricing_mode: calculation.mode,
78
73
  cost: calculation.cost,
79
- tags: build_tags(context_tags: context_tags, metadata: metadata),
74
+ tags: tags,
80
75
  latency_ms: finite_latency_ms(latency_ms),
81
76
  tracked_at: Time.now.utc,
82
77
  cost_status: calculation.cost_status,
@@ -85,11 +80,6 @@ module LlmCostTracker
85
80
  )
86
81
  end
87
82
 
88
- def build_tags(context_tags:, metadata:)
89
- sanitized_metadata = LlmCostTracker::Tags::Sanitizer.call(metadata.to_h)
90
- LlmCostTracker::Tags::Sanitizer.cap(context_tags.merge(sanitized_metadata)).freeze
91
- end
92
-
93
83
  def finite_latency_ms(latency_ms)
94
84
  return nil if latency_ms.nil?
95
85
 
@@ -16,8 +16,7 @@ module LlmCostTracker
16
16
  "request" => "per_request",
17
17
  "session" => "per_session",
18
18
  "hour" => "per_hour",
19
- "minute" => "per_minute",
20
- "image" => "per_image"
19
+ "minute" => "per_minute"
21
20
  }.freeze
22
21
 
23
22
  class << self
@@ -31,6 +30,10 @@ module LlmCostTracker
31
30
  @token_priced ||= all.select(&:token_key).freeze
32
31
  end
33
32
 
33
+ def find_by(kind:, direction:, modality:, cache_state:, unit:)
34
+ by_attributes[[kind, direction, modality, cache_state, unit]]
35
+ end
36
+
34
37
  def token_priced_for(kind:, direction:, cache_state:)
35
38
  token_priced.find do |dimension|
36
39
  dimension.kind == kind && dimension.direction == direction && dimension.cache_state == cache_state
@@ -43,6 +46,13 @@ module LlmCostTracker
43
46
  @index ||= all.to_h { |dimension| [dimension.key, dimension] }.freeze
44
47
  end
45
48
 
49
+ def by_attributes
50
+ @by_attributes ||= all.to_h do |dimension|
51
+ key = [dimension.kind, dimension.direction, dimension.modality, dimension.cache_state, dimension.unit]
52
+ [key, dimension]
53
+ end.freeze
54
+ end
55
+
46
56
  def load_definitions
47
57
  Psych.safe_load_file(DEFINITIONS_PATH, permitted_classes: [], symbolize_names: true)
48
58
  .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
- KNOWN_TOKEN_KEYS = (
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
- warn_on_unknown_keys(values)
34
- token_attributes = Catalog.token_priced.to_h do |dimension|
35
- [dimension.token_key, values.fetch(dimension.token_key.to_s, 0)]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmCostTracker
4
- VERSION = "0.12.0"
4
+ VERSION = "0.14.0"
5
5
  end
@@ -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
- autoload :Engine, "llm_cost_tracker/engine"
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"
@@ -110,7 +110,12 @@ module LlmCostTracker
110
110
  provider_workspace_id: nil,
111
111
  pricing_mode: nil)
112
112
  require_relative "llm_cost_tracker/capture/stream_collector"
113
- Budget.enforce!(provider: provider, model: model, force: true) if enforce_budget
113
+ if enforce_budget
114
+ Budget.enforce!(provider: provider,
115
+ model: model,
116
+ force: true,
117
+ tags: Tracker.build_tags(context_tags: nil, metadata: tags))
118
+ end
114
119
  collector = Capture::StreamCollector.new(
115
120
  provider: provider.to_s,
116
121
  model: model,
@@ -6,7 +6,15 @@ require "rails/generators"
6
6
  require_relative "../llm_cost_tracker/generators/llm_cost_tracker/install_generator"
7
7
  require_relative "../llm_cost_tracker/pricing/sync/change_printer"
8
8
 
9
- # rubocop:disable Metrics/BlockLength
9
+ module LlmCostTrackerTasks
10
+ def self.price_refresh_output_path
11
+ path = LlmCostTracker::Pricing::Sync.configured_output_path
12
+ FileUtils.mkdir_p(File.dirname(path))
13
+ path
14
+ end
15
+ end
16
+
17
+ # rubocop:disable-next Metrics/BlockLength
10
18
  namespace :llm_cost_tracker do
11
19
  desc "Install LLM Cost Tracker with dashboard and prices, migrate, and run doctor"
12
20
  task :setup do
@@ -75,6 +83,18 @@ namespace :llm_cost_tracker do
75
83
  puts "llm_cost_tracker: rebuilt #{rows} rollup rows from the calls ledger"
76
84
  end
77
85
 
86
+ desc "Copy call cost and time onto existing llm_cost_tracker_call_tags rows so per-tag " \
87
+ "budgets count history recorded before the upgrade. Use BATCH_SIZE=N to tune."
88
+ task backfill_tag_costs: :environment do
89
+ unless LlmCostTracker::Budget::PerTag.columns?
90
+ abort("llm_cost_tracker: llm_cost_tracker_call_tags is missing the cost columns; " \
91
+ "run the upgrade_per_tag_budgets generator and migrate first")
92
+ end
93
+ batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Budget::PerTag::DEFAULT_BACKFILL_BATCH).to_i
94
+ filled = LlmCostTracker::Budget::PerTag.backfill(batch_size: batch_size)
95
+ puts "llm_cost_tracker: filled cost and time on #{filled} tag rows"
96
+ end
97
+
78
98
  namespace :prices do
79
99
  desc(
80
100
  "Refresh the configured pricing file from the maintained LLM Cost Tracker price snapshot. " \
@@ -84,7 +104,7 @@ namespace :llm_cost_tracker do
84
104
  Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
85
105
  require_relative "../llm_cost_tracker"
86
106
 
87
- output_path = price_refresh_output_path
107
+ output_path = LlmCostTrackerTasks.price_refresh_output_path
88
108
  source_url = LlmCostTracker::Pricing::Sync.configured_remote_url
89
109
  preview = ENV["PREVIEW"] == "1"
90
110
  result = LlmCostTracker::Pricing::Sync.refresh(
@@ -112,7 +132,7 @@ namespace :llm_cost_tracker do
112
132
  Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
113
133
  require_relative "../llm_cost_tracker"
114
134
 
115
- output_path = price_refresh_output_path
135
+ output_path = LlmCostTrackerTasks.price_refresh_output_path
116
136
  source_url = LlmCostTracker::Pricing::Sync.configured_remote_url
117
137
  result = LlmCostTracker::Pricing::Sync.check(path: output_path, url: source_url)
118
138
 
@@ -125,10 +145,3 @@ namespace :llm_cost_tracker do
125
145
  end
126
146
  end
127
147
  end
128
- # rubocop:enable Metrics/BlockLength
129
-
130
- def price_refresh_output_path
131
- path = LlmCostTracker::Pricing::Sync.configured_output_path
132
- FileUtils.mkdir_p(File.dirname(path))
133
- path
134
- end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_cost_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergii Khomenko
@@ -109,42 +109,42 @@ dependencies:
109
109
  requirements:
110
110
  - - "~>"
111
111
  - !ruby/object:Gem::Version
112
- version: '1.42'
112
+ version: '1.61'
113
113
  type: :development
114
114
  prerelease: false
115
115
  version_requirements: !ruby/object:Gem::Requirement
116
116
  requirements:
117
117
  - - "~>"
118
118
  - !ruby/object:Gem::Version
119
- version: '1.42'
119
+ version: '1.61'
120
120
  - !ruby/object:Gem::Dependency
121
121
  name: nokogiri
122
122
  requirement: !ruby/object:Gem::Requirement
123
123
  requirements:
124
124
  - - "~>"
125
125
  - !ruby/object:Gem::Version
126
- version: '1.16'
126
+ version: '1.19'
127
127
  type: :development
128
128
  prerelease: false
129
129
  version_requirements: !ruby/object:Gem::Requirement
130
130
  requirements:
131
131
  - - "~>"
132
132
  - !ruby/object:Gem::Version
133
- version: '1.16'
133
+ version: '1.19'
134
134
  - !ruby/object:Gem::Dependency
135
135
  name: openai
136
136
  requirement: !ruby/object:Gem::Requirement
137
137
  requirements:
138
138
  - - "~>"
139
139
  - !ruby/object:Gem::Version
140
- version: '0.63'
140
+ version: '0.80'
141
141
  type: :development
142
142
  prerelease: false
143
143
  version_requirements: !ruby/object:Gem::Requirement
144
144
  requirements:
145
145
  - - "~>"
146
146
  - !ruby/object:Gem::Version
147
- version: '0.63'
147
+ version: '0.80'
148
148
  - !ruby/object:Gem::Dependency
149
149
  name: pg
150
150
  requirement: !ruby/object:Gem::Requirement
@@ -207,14 +207,14 @@ dependencies:
207
207
  requirements:
208
208
  - - "~>"
209
209
  - !ruby/object:Gem::Version
210
- version: '1.15'
210
+ version: '1.16'
211
211
  type: :development
212
212
  prerelease: false
213
213
  version_requirements: !ruby/object:Gem::Requirement
214
214
  requirements:
215
215
  - - "~>"
216
216
  - !ruby/object:Gem::Version
217
- version: '1.15'
217
+ version: '1.16'
218
218
  - !ruby/object:Gem::Dependency
219
219
  name: simplecov
220
220
  requirement: !ruby/object:Gem::Requirement
@@ -339,6 +339,7 @@ files:
339
339
  - lib/llm_cost_tracker.rb
340
340
  - lib/llm_cost_tracker/assets.rb
341
341
  - lib/llm_cost_tracker/budget.rb
342
+ - lib/llm_cost_tracker/budget/per_tag.rb
342
343
  - lib/llm_cost_tracker/capture/sdk_payload.rb
343
344
  - lib/llm_cost_tracker/capture/sse.rb
344
345
  - lib/llm_cost_tracker/capture/stream_collector.rb
@@ -349,7 +350,15 @@ files:
349
350
  - lib/llm_cost_tracker/charges/line_item.rb
350
351
  - lib/llm_cost_tracker/check.rb
351
352
  - lib/llm_cost_tracker/configuration.rb
353
+ - lib/llm_cost_tracker/configuration/budgets.rb
354
+ - lib/llm_cost_tracker/configuration/capture.rb
355
+ - lib/llm_cost_tracker/configuration/ingestion.rb
356
+ - lib/llm_cost_tracker/configuration/mutability.rb
357
+ - lib/llm_cost_tracker/configuration/pricing.rb
358
+ - lib/llm_cost_tracker/configuration/section.rb
359
+ - lib/llm_cost_tracker/configuration/tags.rb
352
360
  - lib/llm_cost_tracker/currency.rb
361
+ - lib/llm_cost_tracker/deprecator.rb
353
362
  - lib/llm_cost_tracker/doctor.rb
354
363
  - lib/llm_cost_tracker/doctor/ingestion_check.rb
355
364
  - lib/llm_cost_tracker/doctor/price_check.rb
@@ -369,9 +378,13 @@ files:
369
378
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb
370
379
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb
371
380
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb
381
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb
382
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb
372
383
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb
373
384
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb
374
385
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb
386
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb
387
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb
375
388
  - lib/llm_cost_tracker/ingestion.rb
376
389
  - lib/llm_cost_tracker/ingestion/batch.rb
377
390
  - lib/llm_cost_tracker/ingestion/inbox.rb