llm_cost_tracker 0.1.2 → 0.1.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +136 -24
  4. data/lib/llm_cost_tracker/budget.rb +7 -19
  5. data/lib/llm_cost_tracker/configuration.rb +52 -10
  6. data/lib/llm_cost_tracker/cost.rb +15 -0
  7. data/lib/llm_cost_tracker/event.rb +24 -0
  8. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +20 -0
  9. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +36 -0
  10. data/lib/llm_cost_tracker/llm_api_call.rb +15 -50
  11. data/lib/llm_cost_tracker/logging.rb +44 -0
  12. data/lib/llm_cost_tracker/middleware/faraday.rb +15 -12
  13. data/lib/llm_cost_tracker/parsed_usage.rb +45 -0
  14. data/lib/llm_cost_tracker/parsers/anthropic.rb +2 -3
  15. data/lib/llm_cost_tracker/parsers/base.rb +2 -0
  16. data/lib/llm_cost_tracker/parsers/gemini.rb +4 -4
  17. data/lib/llm_cost_tracker/parsers/openai.rb +4 -22
  18. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +12 -8
  19. data/lib/llm_cost_tracker/parsers/openai_usage.rb +33 -0
  20. data/lib/llm_cost_tracker/price_registry.rb +36 -6
  21. data/lib/llm_cost_tracker/pricing.rb +36 -10
  22. data/lib/llm_cost_tracker/railtie.rb +5 -0
  23. data/lib/llm_cost_tracker/report.rb +29 -0
  24. data/lib/llm_cost_tracker/report_data.rb +84 -0
  25. data/lib/llm_cost_tracker/report_formatter.rb +59 -0
  26. data/lib/llm_cost_tracker/storage/active_record_backend.rb +19 -0
  27. data/lib/llm_cost_tracker/storage/active_record_store.rb +11 -11
  28. data/lib/llm_cost_tracker/storage/backends.rb +26 -0
  29. data/lib/llm_cost_tracker/storage/custom_backend.rb +16 -0
  30. data/lib/llm_cost_tracker/storage/log_backend.rb +28 -0
  31. data/lib/llm_cost_tracker/tag_accessors.rb +23 -0
  32. data/lib/llm_cost_tracker/tag_query.rb +38 -0
  33. data/lib/llm_cost_tracker/tags_column.rb +16 -0
  34. data/lib/llm_cost_tracker/tracker.rb +18 -67
  35. data/lib/llm_cost_tracker/unknown_pricing.rb +8 -15
  36. data/lib/llm_cost_tracker/value_object.rb +45 -0
  37. data/lib/llm_cost_tracker/version.rb +1 -1
  38. data/lib/llm_cost_tracker.rb +28 -13
  39. data/lib/tasks/llm_cost_tracker.rake +9 -0
  40. metadata +20 -1
@@ -5,21 +5,21 @@ module LlmCostTracker
5
5
  class ActiveRecordStore
6
6
  class << self
7
7
  def save(event)
8
- tags = stringify_tags(event[:tags] || {})
8
+ tags = stringify_tags(event.tags || {})
9
9
 
10
10
  attributes = {
11
- provider: event[:provider],
12
- model: event[:model],
13
- input_tokens: event[:input_tokens],
14
- output_tokens: event[:output_tokens],
15
- total_tokens: event[:total_tokens],
16
- input_cost: event.dig(:cost, :input_cost),
17
- output_cost: event.dig(:cost, :output_cost),
18
- total_cost: event.dig(:cost, :total_cost),
11
+ provider: event.provider,
12
+ model: event.model,
13
+ input_tokens: event.input_tokens,
14
+ output_tokens: event.output_tokens,
15
+ total_tokens: event.total_tokens,
16
+ input_cost: event.cost&.input_cost,
17
+ output_cost: event.cost&.output_cost,
18
+ total_cost: event.cost&.total_cost,
19
19
  tags: tags_for_storage(tags),
20
- tracked_at: event[:tracked_at]
20
+ tracked_at: event.tracked_at
21
21
  }
22
- attributes[:latency_ms] = event[:latency_ms] if model_class.latency_column?
22
+ attributes[:latency_ms] = event.latency_ms if model_class.latency_column?
23
23
 
24
24
  model_class.create!(attributes)
25
25
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+ require_relative "log_backend"
5
+ require_relative "active_record_backend"
6
+ require_relative "custom_backend"
7
+
8
+ module LlmCostTracker
9
+ module Storage
10
+ module Backends
11
+ MAP = {
12
+ log: LogBackend,
13
+ active_record: ActiveRecordBackend,
14
+ custom: CustomBackend
15
+ }.freeze
16
+
17
+ class << self
18
+ def fetch(name)
19
+ MAP.fetch(name.to_sym)
20
+ rescue KeyError
21
+ raise Error, "Unknown storage_backend: #{name.inspect}. Use one of: #{MAP.keys.join(', ')}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LlmCostTracker
4
+ module Storage
5
+ module CustomBackend
6
+ class << self
7
+ def save(event, config:)
8
+ result = config.custom_storage&.call(event)
9
+ return false if result == false
10
+
11
+ event
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../logging"
4
+
5
+ module LlmCostTracker
6
+ module Storage
7
+ module LogBackend
8
+ class << self
9
+ def save(event, config:)
10
+ message = "#{event.provider}/#{event.model} " \
11
+ "tokens=#{event.input_tokens}+#{event.output_tokens} " \
12
+ "cost=#{cost_label(event)}"
13
+ message += " latency=#{event.latency_ms}ms" if event.latency_ms
14
+ message += " tags=#{event.tags}" unless event.tags.empty?
15
+
16
+ Logging.log(config.log_level, message)
17
+ event
18
+ end
19
+
20
+ private
21
+
22
+ def cost_label(event)
23
+ event.cost ? "$#{format('%.6f', event.cost.total_cost)}" : "unknown"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module LlmCostTracker
6
+ module TagAccessors
7
+ def parsed_tags
8
+ return tags.transform_keys(&:to_s) if tags.is_a?(Hash)
9
+
10
+ JSON.parse(tags || "{}")
11
+ rescue JSON::ParserError
12
+ {}
13
+ end
14
+
15
+ def feature
16
+ parsed_tags["feature"]
17
+ end
18
+
19
+ def user_id
20
+ parsed_tags["user_id"]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module LlmCostTracker
6
+ module TagQuery
7
+ class << self
8
+ def apply(model, tags)
9
+ normalized_tags = normalize_tags(tags)
10
+ return model.all if normalized_tags.empty?
11
+
12
+ return json_query(model, normalized_tags) if model.tags_json_column?
13
+
14
+ text_query(model, normalized_tags)
15
+ end
16
+
17
+ def normalize_tags(tags)
18
+ (tags || {}).to_h.transform_keys(&:to_s).transform_values(&:to_s)
19
+ end
20
+
21
+ private
22
+
23
+ def json_query(model, tags)
24
+ model.where("tags @> ?::jsonb", tags.to_json)
25
+ end
26
+
27
+ def text_query(model, tags)
28
+ tags.reduce(model.all) do |relation, (key, value)|
29
+ relation.where("tags LIKE ? ESCAPE '\\'", "%#{model.sanitize_sql_like(json_tag_fragment(key, value))}%")
30
+ end
31
+ end
32
+
33
+ def json_tag_fragment(key, value)
34
+ JSON.generate(key => value).delete_prefix("{").delete_suffix("}")
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LlmCostTracker
4
+ module TagsColumn
5
+ def tags_json_column?
6
+ column = columns_hash["tags"]
7
+ return false unless column
8
+
9
+ %i[json jsonb].include?(column.type) || column.sql_type.to_s.downcase == "jsonb"
10
+ end
11
+
12
+ def latency_column?
13
+ columns_hash.key?("latency_ms")
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "logging"
4
+
3
5
  module LlmCostTracker
4
6
  class Tracker
5
7
  EVENT_NAME = "llm_request.llm_cost_tracker"
@@ -9,6 +11,15 @@ module LlmCostTracker
9
11
  Budget.enforce!
10
12
  end
11
13
 
14
+ # Build, notify, persist, and budget-check a single LLM usage event.
15
+ #
16
+ # @param provider [String] Provider name.
17
+ # @param model [String] Model identifier.
18
+ # @param input_tokens [Integer] Input token count.
19
+ # @param output_tokens [Integer] Output token count.
20
+ # @param metadata [Hash] Attribution tags plus provider-specific usage metadata.
21
+ # @param latency_ms [Integer, nil] Optional latency in milliseconds.
22
+ # @return [LlmCostTracker::Event]
12
23
  def record(provider:, model:, input_tokens:, output_tokens:, metadata: {}, latency_ms: nil)
13
24
  usage = EventMetadata.usage_data(input_tokens, output_tokens, metadata)
14
25
 
@@ -23,20 +34,20 @@ module LlmCostTracker
23
34
 
24
35
  UnknownPricing.handle!(model) unless cost_data
25
36
 
26
- event = {
37
+ event = Event.new(
27
38
  provider: provider,
28
39
  model: model,
29
40
  input_tokens: usage[:input_tokens],
30
41
  output_tokens: usage[:output_tokens],
31
42
  total_tokens: usage[:total_tokens],
32
43
  cost: cost_data,
33
- tags: LlmCostTracker.configuration.default_tags.merge(EventMetadata.tags(metadata)),
44
+ tags: LlmCostTracker.configuration.default_tags.merge(EventMetadata.tags(metadata)).freeze,
34
45
  latency_ms: normalized_latency_ms(latency_ms),
35
46
  tracked_at: Time.now.utc
36
- }
47
+ )
37
48
 
38
49
  # Emit ActiveSupport::Notifications event
39
- ActiveSupport::Notifications.instrument(EVENT_NAME, event)
50
+ ActiveSupport::Notifications.instrument(EVENT_NAME, event.to_h)
40
51
 
41
52
  # Store based on backend
42
53
  stored = store(event)
@@ -49,17 +60,7 @@ module LlmCostTracker
49
60
 
50
61
  def store(event)
51
62
  config = LlmCostTracker.configuration
52
-
53
- case config.storage_backend
54
- when :log
55
- log_event(event)
56
- when :active_record
57
- store_active_record(event)
58
- when :custom
59
- config.custom_storage&.call(event)
60
- end
61
-
62
- true
63
+ Storage::Backends.fetch(config.storage_backend).save(event, config: config)
63
64
  rescue BudgetExceededError, UnknownPricingError
64
65
  raise
65
66
  rescue StandardError => e
@@ -67,68 +68,18 @@ module LlmCostTracker
67
68
  false
68
69
  end
69
70
 
70
- def log_event(event)
71
- cost_str = event[:cost] ? "$#{format('%.6f', event[:cost][:total_cost])}" : "unknown"
72
-
73
- message = "[LlmCostTracker] #{event[:provider]}/#{event[:model]} " \
74
- "tokens=#{event[:input_tokens]}+#{event[:output_tokens]} " \
75
- "cost=#{cost_str}"
76
- message += " latency=#{event[:latency_ms]}ms" if event[:latency_ms]
77
- message += " tags=#{event[:tags]}" unless event[:tags].empty?
78
-
79
- case LlmCostTracker.configuration.log_level
80
- when :debug
81
- Rails.logger.debug(message) if defined?(Rails)
82
- when :warn
83
- Rails.logger.warn(message) if defined?(Rails)
84
- else
85
- Rails.logger.info(message) if defined?(Rails)
86
- end
87
-
88
- # Fallback if Rails is not available
89
- warn(message) unless defined?(Rails)
90
- end
91
-
92
- def log_warning(message)
93
- message = "[LlmCostTracker] #{message}"
94
-
95
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
96
- Rails.logger.warn(message)
97
- else
98
- warn message
99
- end
100
- end
101
-
102
- def store_active_record(event)
103
- require_relative "llm_api_call" unless defined?(LlmCostTracker::LlmApiCall)
104
- require_relative "storage/active_record_store" unless defined?(LlmCostTracker::Storage::ActiveRecordStore)
105
-
106
- LlmCostTracker::Storage::ActiveRecordStore.save(event)
107
- rescue LoadError => e
108
- raise Error, "ActiveRecord storage requires the active_record gem: #{e.message}"
109
- end
110
-
111
71
  def handle_storage_error(error)
112
- case storage_error_behavior
72
+ case LlmCostTracker.configuration.storage_error_behavior
113
73
  when :ignore
114
74
  nil
115
75
  when :warn
116
- log_warning("Storage failed; tracking event was not persisted: #{error.class}: #{error.message}")
76
+ Logging.warn("Storage failed; tracking event was not persisted: #{error.class}: #{error.message}")
117
77
  when :raise
118
78
  storage_error = StorageError.new(error)
119
79
  raise storage_error
120
80
  end
121
81
  end
122
82
 
123
- def storage_error_behavior
124
- behavior = (LlmCostTracker.configuration.storage_error_behavior || :warn).to_sym
125
- return behavior if Configuration::STORAGE_ERROR_BEHAVIORS.include?(behavior)
126
-
127
- raise Error,
128
- "Unknown storage_error_behavior: #{behavior.inspect}. " \
129
- "Use one of: #{Configuration::STORAGE_ERROR_BEHAVIORS.join(', ')}"
130
- end
131
-
132
83
  def normalized_latency_ms(latency_ms)
133
84
  return nil if latency_ms.nil?
134
85
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "logging"
4
+
3
5
  module LlmCostTracker
4
6
  class UnknownPricing
5
7
  class << self
@@ -23,24 +25,15 @@ module LlmCostTracker
23
25
  end
24
26
 
25
27
  def warn_missing(model)
26
- message = "[LlmCostTracker] No pricing configured for model #{model.inspect}. " \
27
- "Cost and budget enforcement will be skipped for this event. " \
28
- "Add a pricing_overrides entry or set unknown_pricing_behavior."
29
-
30
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
31
- Rails.logger.warn(message)
32
- else
33
- Kernel.warn(message)
34
- end
28
+ Logging.warn(
29
+ "No pricing configured for model #{model.inspect}. " \
30
+ "Cost and budget enforcement will be skipped for this event. " \
31
+ "Add a pricing_overrides entry or set unknown_pricing_behavior."
32
+ )
35
33
  end
36
34
 
37
35
  def behavior
38
- behavior = (LlmCostTracker.configuration.unknown_pricing_behavior || :warn).to_sym
39
- return behavior if Configuration::UNKNOWN_PRICING_BEHAVIORS.include?(behavior)
40
-
41
- raise Error,
42
- "Unknown unknown_pricing_behavior: #{behavior.inspect}. " \
43
- "Use one of: #{Configuration::UNKNOWN_PRICING_BEHAVIORS.join(', ')}"
36
+ LlmCostTracker.configuration.unknown_pricing_behavior
44
37
  end
45
38
  end
46
39
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LlmCostTracker
4
+ module ValueObject
5
+ class << self
6
+ def define(*members, &block)
7
+ klass = data_class(*members)
8
+ add_hash_like_readers(klass)
9
+ klass.class_eval(&block) if block
10
+ klass
11
+ end
12
+
13
+ private
14
+
15
+ def data_class(*members)
16
+ return Data.define(*members) if defined?(Data)
17
+
18
+ Struct.new(*members, keyword_init: true) do
19
+ def initialize(**kwargs)
20
+ super
21
+ freeze
22
+ end
23
+ end
24
+ end
25
+
26
+ def add_hash_like_readers(klass)
27
+ klass.class_eval do
28
+ def [](key)
29
+ public_send(key.to_sym)
30
+ end
31
+
32
+ def dig(key, *rest)
33
+ value = self[key]
34
+ rest.empty? ? value : value&.dig(*rest)
35
+ end
36
+
37
+ def except(*keys)
38
+ excluded = keys.map(&:to_sym)
39
+ to_h.reject { |key, _value| excluded.include?(key.to_sym) }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmCostTracker
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -6,9 +6,15 @@ require "active_support/notifications"
6
6
  require_relative "llm_cost_tracker/version"
7
7
  require_relative "llm_cost_tracker/configuration"
8
8
  require_relative "llm_cost_tracker/errors"
9
+ require_relative "llm_cost_tracker/logging"
10
+ require_relative "llm_cost_tracker/value_object"
11
+ require_relative "llm_cost_tracker/cost"
12
+ require_relative "llm_cost_tracker/event"
13
+ require_relative "llm_cost_tracker/parsed_usage"
9
14
  require_relative "llm_cost_tracker/price_registry"
10
15
  require_relative "llm_cost_tracker/pricing"
11
16
  require_relative "llm_cost_tracker/parsers/base"
17
+ require_relative "llm_cost_tracker/parsers/openai_usage"
12
18
  require_relative "llm_cost_tracker/parsers/openai"
13
19
  require_relative "llm_cost_tracker/parsers/openai_compatible"
14
20
  require_relative "llm_cost_tracker/parsers/anthropic"
@@ -18,7 +24,14 @@ require_relative "llm_cost_tracker/middleware/faraday"
18
24
  require_relative "llm_cost_tracker/budget"
19
25
  require_relative "llm_cost_tracker/unknown_pricing"
20
26
  require_relative "llm_cost_tracker/event_metadata"
27
+ require_relative "llm_cost_tracker/tags_column"
28
+ require_relative "llm_cost_tracker/tag_query"
29
+ require_relative "llm_cost_tracker/tag_accessors"
30
+ require_relative "llm_cost_tracker/storage/backends"
21
31
  require_relative "llm_cost_tracker/tracker"
32
+ require_relative "llm_cost_tracker/report_data"
33
+ require_relative "llm_cost_tracker/report_formatter"
34
+ require_relative "llm_cost_tracker/report"
22
35
 
23
36
  module LlmCostTracker
24
37
  class << self
@@ -30,6 +43,10 @@ module LlmCostTracker
30
43
  @configuration || CONFIGURATION_MUTEX.synchronize { @configuration ||= Configuration.new }
31
44
  end
32
45
 
46
+ # Configure the gem once during application boot.
47
+ #
48
+ # @yieldparam configuration [LlmCostTracker::Configuration]
49
+ # @return [void]
33
50
  def configure
34
51
  yield(configuration)
35
52
  configuration.normalize_openai_compatible_providers!
@@ -40,7 +57,7 @@ module LlmCostTracker
40
57
  CONFIGURATION_MUTEX.synchronize { @configuration = Configuration.new }
41
58
  end
42
59
 
43
- # Manual tracking for non-Faraday clients
60
+ # Track an LLM request manually for non-Faraday clients.
44
61
  #
45
62
  # LlmCostTracker.track(
46
63
  # provider: :openai,
@@ -50,6 +67,14 @@ module LlmCostTracker
50
67
  # feature: "chat",
51
68
  # user_id: current_user.id
52
69
  # )
70
+ #
71
+ # @param provider [String, Symbol] Provider name, such as :openai or :anthropic.
72
+ # @param model [String] Provider model identifier.
73
+ # @param input_tokens [Integer] Billed input token count.
74
+ # @param output_tokens [Integer] Billed output token count.
75
+ # @param latency_ms [Integer, nil] Optional request latency in milliseconds.
76
+ # @param metadata [Hash] Attribution tags and provider-specific usage metadata.
77
+ # @return [LlmCostTracker::Event] The tracked event.
53
78
  def track(provider:, model:, input_tokens:, output_tokens:, latency_ms: nil, **metadata)
54
79
  Tracker.record(
55
80
  provider: provider.to_s,
@@ -64,20 +89,10 @@ module LlmCostTracker
64
89
  private
65
90
 
66
91
  def warn_for_configuration!
67
- return unless (configuration.budget_exceeded_behavior || :notify).to_sym == :block_requests
92
+ return unless configuration.budget_exceeded_behavior == :block_requests
68
93
  return if configuration.active_record?
69
94
 
70
- log_warning(":block_requests requires storage_backend = :active_record; preflight blocking will be skipped.")
71
- end
72
-
73
- def log_warning(message)
74
- message = "[LlmCostTracker] #{message}"
75
-
76
- if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
77
- Rails.logger.warn(message)
78
- else
79
- warn message
80
- end
95
+ Logging.warn(":block_requests requires storage_backend = :active_record; preflight blocking will be skipped.")
81
96
  end
82
97
  end
83
98
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :llm_cost_tracker do
4
+ desc "Print an LLM cost report from ActiveRecord storage"
5
+ task report: :environment do
6
+ days = (ENV["DAYS"] || LlmCostTracker::Report::DEFAULT_DAYS).to_i
7
+ puts LlmCostTracker::Report.generate(days: days)
8
+ end
9
+ 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.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergii Khomenko
@@ -159,33 +159,52 @@ files:
159
159
  - lib/llm_cost_tracker.rb
160
160
  - lib/llm_cost_tracker/budget.rb
161
161
  - lib/llm_cost_tracker/configuration.rb
162
+ - lib/llm_cost_tracker/cost.rb
162
163
  - lib/llm_cost_tracker/errors.rb
164
+ - lib/llm_cost_tracker/event.rb
163
165
  - lib/llm_cost_tracker/event_metadata.rb
164
166
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
165
167
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
168
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
166
169
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb
167
170
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
168
171
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
172
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb
169
173
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb
170
174
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb
171
175
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb
172
176
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb
173
177
  - lib/llm_cost_tracker/llm_api_call.rb
178
+ - lib/llm_cost_tracker/logging.rb
174
179
  - lib/llm_cost_tracker/middleware/faraday.rb
180
+ - lib/llm_cost_tracker/parsed_usage.rb
175
181
  - lib/llm_cost_tracker/parsers/anthropic.rb
176
182
  - lib/llm_cost_tracker/parsers/base.rb
177
183
  - lib/llm_cost_tracker/parsers/gemini.rb
178
184
  - lib/llm_cost_tracker/parsers/openai.rb
179
185
  - lib/llm_cost_tracker/parsers/openai_compatible.rb
186
+ - lib/llm_cost_tracker/parsers/openai_usage.rb
180
187
  - lib/llm_cost_tracker/parsers/registry.rb
181
188
  - lib/llm_cost_tracker/price_registry.rb
182
189
  - lib/llm_cost_tracker/prices.json
183
190
  - lib/llm_cost_tracker/pricing.rb
184
191
  - lib/llm_cost_tracker/railtie.rb
192
+ - lib/llm_cost_tracker/report.rb
193
+ - lib/llm_cost_tracker/report_data.rb
194
+ - lib/llm_cost_tracker/report_formatter.rb
195
+ - lib/llm_cost_tracker/storage/active_record_backend.rb
185
196
  - lib/llm_cost_tracker/storage/active_record_store.rb
197
+ - lib/llm_cost_tracker/storage/backends.rb
198
+ - lib/llm_cost_tracker/storage/custom_backend.rb
199
+ - lib/llm_cost_tracker/storage/log_backend.rb
200
+ - lib/llm_cost_tracker/tag_accessors.rb
201
+ - lib/llm_cost_tracker/tag_query.rb
202
+ - lib/llm_cost_tracker/tags_column.rb
186
203
  - lib/llm_cost_tracker/tracker.rb
187
204
  - lib/llm_cost_tracker/unknown_pricing.rb
205
+ - lib/llm_cost_tracker/value_object.rb
188
206
  - lib/llm_cost_tracker/version.rb
207
+ - lib/tasks/llm_cost_tracker.rake
189
208
  - llm_cost_tracker.gemspec
190
209
  homepage: https://github.com/sergey-homenko/llm_cost_tracker
191
210
  licenses: