llm_cost_tracker 0.14.0 → 0.14.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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/README.md +16 -5
  4. data/app/assets/llm_cost_tracker/application.css +16 -0
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +16 -3
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +1 -3
  7. data/app/controllers/llm_cost_tracker/models_controller.rb +2 -4
  8. data/app/controllers/llm_cost_tracker/pricing_controller.rb +2 -2
  9. data/app/controllers/llm_cost_tracker/tags_controller.rb +9 -7
  10. data/app/helpers/llm_cost_tracker/application_helper.rb +5 -26
  11. data/app/helpers/llm_cost_tracker/chart_helper.rb +6 -10
  12. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +5 -0
  13. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +8 -28
  14. data/app/models/llm_cost_tracker/call.rb +10 -14
  15. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +4 -15
  16. data/app/services/llm_cost_tracker/dashboard/filter.rb +29 -30
  17. data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
  18. data/app/services/llm_cost_tracker/dashboard/params.rb +10 -0
  19. data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +2 -1
  20. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +1 -1
  21. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +2 -3
  22. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -0
  23. data/app/services/llm_cost_tracker/dashboard/time_series.rb +3 -5
  24. data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -2
  25. data/app/views/llm_cost_tracker/calls/show.html.erb +8 -22
  26. data/app/views/llm_cost_tracker/data_quality/index.html.erb +1 -1
  27. data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -3
  28. data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +1 -4
  29. data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -4
  30. data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -4
  31. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -4
  32. data/app/views/llm_cost_tracker/tags/show.html.erb +8 -5
  33. data/config/routes.rb +6 -1
  34. data/lib/llm_cost_tracker/budget/per_tag.rb +21 -11
  35. data/lib/llm_cost_tracker/budget.rb +44 -55
  36. data/lib/llm_cost_tracker/capture/event_window.rb +100 -0
  37. data/lib/llm_cost_tracker/capture/sdk_payload.rb +5 -1
  38. data/lib/llm_cost_tracker/capture/sse.rb +108 -32
  39. data/lib/llm_cost_tracker/capture/stream_collector.rb +41 -81
  40. data/lib/llm_cost_tracker/capture/stream_tap.rb +57 -0
  41. data/lib/llm_cost_tracker/capture/stream_tracker.rb +14 -58
  42. data/lib/llm_cost_tracker/capture_verifier.rb +1 -7
  43. data/lib/llm_cost_tracker/charges/line_item.rb +5 -1
  44. data/lib/llm_cost_tracker/configuration/budgets.rb +1 -1
  45. data/lib/llm_cost_tracker/configuration/pricing.rb +1 -1
  46. data/lib/llm_cost_tracker/configuration.rb +5 -9
  47. data/lib/llm_cost_tracker/doctor/price_check.rb +14 -3
  48. data/lib/llm_cost_tracker/doctor/schema_check.rb +1 -2
  49. data/lib/llm_cost_tracker/doctor.rb +19 -2
  50. data/lib/llm_cost_tracker/engine.rb +0 -1
  51. data/lib/llm_cost_tracker/errors.rb +13 -0
  52. data/lib/llm_cost_tracker/event.rb +8 -0
  53. data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +0 -6
  54. data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +5 -8
  55. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +0 -6
  56. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +1 -1
  57. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +1 -1
  58. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +1 -1
  59. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -12
  60. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +1 -1
  61. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +1 -1
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +1 -1
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +1 -1
  64. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +1 -1
  65. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -6
  66. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb +0 -6
  67. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb +0 -6
  68. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb +3 -8
  69. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb +3 -8
  70. data/lib/llm_cost_tracker/ingestion/batch.rb +37 -8
  71. data/lib/llm_cost_tracker/ingestion/inbox.rb +2 -8
  72. data/lib/llm_cost_tracker/ingestion.rb +3 -1
  73. data/lib/llm_cost_tracker/integrations/anthropic.rb +70 -11
  74. data/lib/llm_cost_tracker/integrations/base.rb +56 -19
  75. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +21 -13
  76. data/lib/llm_cost_tracker/integrations/openai/patches.rb +8 -0
  77. data/lib/llm_cost_tracker/integrations/openai.rb +42 -59
  78. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +275 -75
  79. data/lib/llm_cost_tracker/integrations.rb +1 -20
  80. data/lib/llm_cost_tracker/ledger/isolation.rb +23 -0
  81. data/lib/llm_cost_tracker/ledger/period/totals.rb +16 -11
  82. data/lib/llm_cost_tracker/ledger/rollups.rb +25 -17
  83. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +14 -3
  84. data/lib/llm_cost_tracker/ledger/schema/base.rb +5 -4
  85. data/lib/llm_cost_tracker/ledger/storable.rb +16 -0
  86. data/lib/llm_cost_tracker/ledger/store.rb +14 -13
  87. data/lib/llm_cost_tracker/ledger/tags/breakdown.rb +1 -5
  88. data/lib/llm_cost_tracker/ledger/tags/encoding.rb +3 -12
  89. data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -2
  90. data/lib/llm_cost_tracker/ledger.rb +1 -0
  91. data/lib/llm_cost_tracker/logging.rb +3 -1
  92. data/lib/llm_cost_tracker/middleware/faraday.rb +67 -61
  93. data/lib/llm_cost_tracker/parsers.rb +12 -41
  94. data/lib/llm_cost_tracker/prices.json +3071 -249
  95. data/lib/llm_cost_tracker/pricing/backfill.rb +46 -13
  96. data/lib/llm_cost_tracker/pricing/calculation.rb +118 -75
  97. data/lib/llm_cost_tracker/pricing/effective_prices.rb +11 -10
  98. data/lib/llm_cost_tracker/pricing/estimator.rb +5 -2
  99. data/lib/llm_cost_tracker/pricing/matcher.rb +36 -22
  100. data/lib/llm_cost_tracker/pricing/mode.rb +2 -13
  101. data/lib/llm_cost_tracker/pricing/price_key.rb +5 -3
  102. data/lib/llm_cost_tracker/pricing/rate.rb +1 -0
  103. data/lib/llm_cost_tracker/pricing/registry.rb +17 -17
  104. data/lib/llm_cost_tracker/pricing/service_rates.rb +1 -8
  105. data/lib/llm_cost_tracker/pricing/sync/change_printer.rb +6 -1
  106. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +13 -9
  107. data/lib/llm_cost_tracker/pricing/sync/snapshot_guard.rb +47 -0
  108. data/lib/llm_cost_tracker/pricing/sync.rb +38 -70
  109. data/lib/llm_cost_tracker/providers/anthropic/parser.rb +33 -3
  110. data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +11 -5
  111. data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +81 -12
  112. data/lib/llm_cost_tracker/providers/azure/parser.rb +22 -7
  113. data/lib/llm_cost_tracker/providers/gemini/parser.rb +175 -47
  114. data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +13 -22
  115. data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -1
  116. data/lib/llm_cost_tracker/providers/openai/parser.rb +11 -13
  117. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +101 -27
  118. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +57 -40
  119. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +24 -6
  120. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +7 -1
  121. data/lib/llm_cost_tracker/redaction.rb +32 -0
  122. data/lib/llm_cost_tracker/report/data.rb +1 -1
  123. data/lib/llm_cost_tracker/retention.rb +1 -3
  124. data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -36
  125. data/lib/llm_cost_tracker/tracker.rb +17 -10
  126. data/lib/llm_cost_tracker/usage/catalog.rb +13 -4
  127. data/lib/llm_cost_tracker/usage/dimension.rb +1 -1
  128. data/lib/llm_cost_tracker/usage/dimensions.yml +61 -0
  129. data/lib/llm_cost_tracker/version.rb +1 -1
  130. data/lib/llm_cost_tracker.rb +4 -2
  131. data/lib/tasks/llm_cost_tracker.rake +26 -21
  132. data/llm_cost_tracker.gemspec +63 -0
  133. metadata +23 -10
@@ -2,15 +2,17 @@
2
2
 
3
3
  require "faraday"
4
4
  require "json"
5
- require "stringio"
6
5
  require "uri"
7
6
 
8
7
  require_relative "../capture/sse"
8
+ require_relative "../capture/stream_tap"
9
9
  require_relative "../timing"
10
10
 
11
11
  module LlmCostTracker
12
12
  module Middleware
13
13
  class Faraday < ::Faraday::Middleware
14
+ MULTIPART_MODEL = /name="model"\r\n(?:[^\r\n]+\r\n)*\r\n([^\r\n]+)\r\n/
15
+
14
16
  def initialize(app, **options)
15
17
  super(app)
16
18
  @tags = options.fetch(:tags, {})
@@ -20,23 +22,24 @@ module LlmCostTracker
20
22
  return @app.call(request_env) unless LlmCostTracker.configuration.enabled
21
23
 
22
24
  request_url = request_env.url.to_s
23
- request_body = read_body(request_env.body)
24
25
  parser = Parsers.find_for(request_url)
26
+ request_body = read_body(request_env.body) || multipart_model_body(request_env, parser)
25
27
  request_parsed = parser&.safe_json_parse(request_body)
26
28
  streaming = parser&.streaming_request?(request_url, request_parsed)
27
29
  if streaming
28
30
  request_body = inject_stream_usage_flag(request_env, parser, request_url, request_parsed) || request_body
29
31
  end
30
- stream_buffer = install_stream_tap(request_env) if streaming
32
+ stream_buffer = install_stream_tap(request_env, parser) if streaming
31
33
 
32
- if parser
34
+ context_tags, metadata = tag_snapshot(request_env) if parser
35
+ if parser && request_env.method == :post
33
36
  Budget.enforce!(
34
37
  provider: parser.provider_for(request_url),
35
38
  model: parser.model_for(request_url, request_parsed),
36
- request: request_parsed
39
+ request: request_parsed,
40
+ tags: Tracker.build_tags(context_tags: context_tags, metadata: metadata)
37
41
  )
38
42
  end
39
- context_tags, metadata = tag_snapshot(request_env) if parser
40
43
  started_at = LlmCostTracker::Timing.now_monotonic
41
44
 
42
45
  invoke_app_with_capture(
@@ -71,7 +74,7 @@ module LlmCostTracker
71
74
  request_url: request_url,
72
75
  request_body: request_body,
73
76
  response_env: response_env,
74
- latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at),
77
+ latency_ms: (LlmCostTracker::Timing.elapsed_ms(started_at) if request_env.method == :post),
75
78
  streaming: streaming,
76
79
  stream_buffer: stream_buffer,
77
80
  context_tags: context_tags,
@@ -95,7 +98,7 @@ module LlmCostTracker
95
98
 
96
99
  def inject_stream_usage_flag(request_env, parser, request_url, request_parsed)
97
100
  return nil unless LlmCostTracker.configuration.capture.request_stream_usage
98
- return nil unless parser&.auto_enable_stream_usage?(request_url)
101
+ return nil unless parser.auto_enable_stream_usage?(request_url, request_parsed)
99
102
 
100
103
  stream_options = request_parsed["stream_options"]
101
104
  return nil if stream_options.is_a?(Hash) && stream_options.key?("include_usage")
@@ -116,21 +119,22 @@ module LlmCostTracker
116
119
  request = parser.safe_json_parse(request_body)
117
120
  event = Event.build(
118
121
  provider: parser.provider_for(request_url),
119
- model: request["model"] || Event::UNKNOWN_MODEL,
122
+ model: parser.model_for(request_url, request) || Event::UNKNOWN_MODEL,
120
123
  token_usage: Usage::TokenUsage.build(input_tokens: 0, output_tokens: 0, total_tokens: 0),
121
124
  stream: true,
122
125
  usage_source: Usage::Source::UNKNOWN
123
126
  )
124
- merged_metadata = (metadata || {}).merge(
125
- stream_interrupted: true,
126
- stream_interrupted_error: "#{error.class}: #{error.message}"
127
- )
127
+ merged_metadata = (metadata || {}).merge(stream_interrupted: true, stream_interrupted_error: error.class.name)
128
+ status = error.try(:response_status)
129
+ merged_metadata[:stream_interrupted_status] = status if status
128
130
  Tracker.record(
129
131
  event: event,
130
132
  latency_ms: latency_ms,
131
133
  metadata: merged_metadata,
132
134
  context_tags: context_tags
133
135
  )
136
+ rescue LlmCostTracker::TransactionAbortedError
137
+ raise
134
138
  rescue StandardError => e
135
139
  Logging.warn("Error recording interrupted stream: #{e.class}: #{e.message}")
136
140
  end
@@ -163,16 +167,12 @@ module LlmCostTracker
163
167
  response_env: response_env
164
168
  )
165
169
  end
166
- return unless parsed
167
170
 
168
- Tracker.record(
169
- event: parsed,
170
- latency_ms: latency_ms,
171
- metadata: metadata,
172
- context_tags: context_tags
173
- )
174
- rescue LlmCostTracker::Error
171
+ Tracker.record(event: parsed, latency_ms: latency_ms, metadata: metadata, context_tags: context_tags) if parsed
172
+ rescue *LlmCostTracker::CALLER_ERRORS
175
173
  raise
174
+ rescue ActiveRecord::RecordNotUnique
175
+ nil
176
176
  rescue StandardError => e
177
177
  Logging.warn("Error processing response: #{e.class}: #{e.message}")
178
178
  end
@@ -198,32 +198,28 @@ module LlmCostTracker
198
198
  end
199
199
 
200
200
  def parse_stream(parser:, request_url:, request_body:, response_env:, stream_buffer:)
201
- overflowed = stream_buffer&.dig(:overflowed) == true
202
- Logging.warn(capture_warning(request_url, stream_buffer)) if overflowed
203
-
204
- body = stream_buffer&.dig(:buffer)&.string
205
- body = read_body(response_env.body) if body.blank?
206
-
207
- if body.blank?
208
- Logging.warn(capture_warning(request_url, stream_buffer)) unless overflowed
209
- return parser.parse_stream(
210
- request_url: request_url,
211
- request_body: request_body,
212
- response_status: response_env.status,
213
- response_headers: response_env.response_headers
214
- )
215
- end
216
-
217
- events = overflowed ? [] : Capture::SSE.parse(body)
218
201
  parser.parse_stream(
219
202
  request_url: request_url,
220
203
  request_body: request_body,
221
204
  response_status: response_env.status,
222
- events: events,
205
+ events: stream_events(request_url, response_env, stream_buffer),
223
206
  response_headers: response_env.response_headers
224
207
  )
225
208
  end
226
209
 
210
+ def stream_events(request_url, response_env, stream_buffer)
211
+ if stream_buffer&.received?
212
+ events = stream_buffer.events
213
+ return events unless stream_buffer.overflowed? || stream_buffer.failed?
214
+ else
215
+ body = read_body(response_env.body)
216
+ return Capture::SSE.parse(body) if body.present?
217
+ end
218
+
219
+ Logging.warn(capture_warning(request_url, stream_buffer))
220
+ []
221
+ end
222
+
227
223
  def forward_on_data_chunk(callable, chunk, size, env)
228
224
  arity = callable.arity
229
225
  return callable.call(chunk, size, env) if arity.negative?
@@ -235,28 +231,19 @@ module LlmCostTracker
235
231
  end
236
232
  end
237
233
 
238
- def install_stream_tap(request_env)
234
+ def install_stream_tap(request_env, parser)
239
235
  request = request_env.request
240
236
  return nil unless request
241
237
 
242
238
  original = request.on_data
243
239
  return nil unless original
244
240
 
245
- state = { buffer: StringIO.new, bytes: 0, overflowed: false }
241
+ tap = Capture::StreamTap.new(notable: parser.method(:retain_stream_event?))
246
242
  request.on_data = proc do |chunk, size, env|
247
- chunk = chunk.to_s
248
- remaining = Capture::SSE::LIMIT_BYTES - state[:bytes]
249
- if chunk.bytesize <= remaining
250
- state[:buffer] << chunk
251
- state[:bytes] += chunk.bytesize
252
- else
253
- state[:buffer] << chunk.byteslice(0, remaining) if remaining.positive?
254
- state[:bytes] += [remaining, 0].max
255
- state[:overflowed] = true
256
- end
243
+ tap << chunk
257
244
  forward_on_data_chunk(original, chunk, size, env)
258
245
  end
259
- state
246
+ tap
260
247
  rescue StandardError => e
261
248
  Logging.warn("Unable to install streaming tap: #{e.class}: #{e.message}")
262
249
  nil
@@ -272,16 +259,35 @@ module LlmCostTracker
272
259
  end
273
260
  end
274
261
 
262
+ def multipart_model_body(request_env, parser)
263
+ body = request_env.body
264
+ multipart = request_env.request_headers["Content-Type"].to_s.start_with?("multipart/form-data")
265
+ return nil unless parser && multipart && body.respond_to?(:read) && body.respond_to?(:rewind)
266
+
267
+ model = multipart_model(body)
268
+ model && { "model" => model }.to_json
269
+ rescue StandardError => e
270
+ Logging.warn("Unable to read the model from a multipart request: #{e.class}: #{e.message}")
271
+ nil
272
+ end
273
+
274
+ def multipart_model(body)
275
+ window = String.new(encoding: Encoding::BINARY)
276
+ while (chunk = body.read(65_536))
277
+ window << chunk.b
278
+ model = window[MULTIPART_MODEL, 1]
279
+ return model.force_encoding(Encoding::UTF_8) if model
280
+
281
+ window = window.byteslice([window.bytesize - 512, 0].max..)
282
+ end
283
+ ensure
284
+ body.rewind
285
+ end
286
+
275
287
  def resolved_tags(request_env)
276
- tags =
277
- if @tags.respond_to?(:call)
278
- @tags.arity.zero? ? @tags.call : @tags.call(request_env)
279
- else
280
- @tags
281
- end
282
- return {} if tags.nil?
288
+ return @tags.to_h unless @tags.respond_to?(:call)
283
289
 
284
- tags.to_h
290
+ (@tags.arity.zero? ? @tags.call : @tags.call(request_env)).to_h
285
291
  end
286
292
 
287
293
  def tag_snapshot(request_env)
@@ -295,7 +301,7 @@ module LlmCostTracker
295
301
  suffix = "recording usage_source=#{Usage::Source::UNKNOWN}. " \
296
302
  "Use LlmCostTracker.track_stream for manual capture."
297
303
  label = request_url_label(request_url)
298
- return "Unable to capture streaming response for #{label}; #{suffix}" unless stream_buffer&.dig(:overflowed)
304
+ return "Unable to capture streaming response for #{label}; #{suffix}" unless stream_buffer&.overflowed?
299
305
 
300
306
  "Streaming response for #{label} exceeded #{Capture::SSE::LIMIT_BYTES} bytes; #{suffix}"
301
307
  end
@@ -8,50 +8,27 @@ require_relative "providers"
8
8
 
9
9
  module LlmCostTracker
10
10
  module Parsers
11
- PARSER_PROVIDERS = %i[Openai Azure OpenaiCompatible Anthropic Gemini].freeze
11
+ PARSER_PROVIDERS = %i[Openai Azure Anthropic Gemini OpenaiCompatible].freeze
12
12
 
13
13
  def self.find_for(url)
14
- instances.each do |klass, instance|
15
- return instance if klass.match?(url)
16
- end
17
- nil
14
+ instances.find { |parser| parser.class.match?(url) }
18
15
  end
19
16
 
20
17
  def self.find_for_provider(provider)
21
- provider_name = provider.to_s.downcase
22
- instances.each do |klass, instance|
23
- return instance if klass.provider_names.include?(provider_name)
24
- end
25
- nil
18
+ all_for_provider(provider).first
26
19
  end
27
20
 
28
- def self.parser_classes
29
- PARSER_PROVIDERS.map { |name| Providers.const_get(name)::Parser }
21
+ def self.all_for_provider(provider)
22
+ provider_name = provider.to_s.downcase
23
+ instances.select { |parser| parser.class.provider_names.include?(provider_name) }
30
24
  end
31
- private_class_method :parser_classes
32
25
 
33
26
  def self.instances
34
- @instances ||= parser_classes.to_h { |klass| [klass, klass.new] }.freeze
27
+ @instances ||= PARSER_PROVIDERS.map { |name| Providers.const_get(name)::Parser.new }.freeze
35
28
  end
36
29
  private_class_method :instances
37
30
 
38
31
  module UrlMatchers
39
- def match_uri?(url, hosts: nil, exact_paths: nil, path_includes: nil, path_suffixes: nil, path_pattern: nil)
40
- uri_matches?(url) do |uri|
41
- host_match = hosts.nil? || hosts.include?(uri.host.to_s.downcase)
42
- path_match = path_matches?(
43
- uri,
44
- exact_paths: exact_paths,
45
- path_includes: path_includes,
46
- path_suffixes: path_suffixes,
47
- path_pattern: path_pattern
48
- )
49
- extra_match = block_given? ? yield(uri) : true
50
-
51
- !!(host_match && path_match && extra_match)
52
- end
53
- end
54
-
55
32
  def uri_matches?(url)
56
33
  uri = parsed_uri(url)
57
34
  uri ? yield(uri) : false
@@ -62,16 +39,6 @@ module LlmCostTracker
62
39
  rescue URI::InvalidURIError
63
40
  nil
64
41
  end
65
-
66
- def path_matches?(uri, exact_paths: nil, path_includes: nil, path_suffixes: nil, path_pattern: nil)
67
- path = uri.path.to_s
68
- matches = true
69
- matches &&= exact_paths.include?(path) if exact_paths
70
- matches &&= Array(path_includes).all? { |fragment| path.include?(fragment) } if path_includes
71
- matches &&= path.match?(path_pattern) if path_pattern
72
- matches &&= path_suffixes.any? { |suffix| path == suffix || path.end_with?(suffix) } if path_suffixes
73
- matches
74
- end
75
42
  end
76
43
 
77
44
  class Base
@@ -104,7 +71,11 @@ module LlmCostTracker
104
71
  nil
105
72
  end
106
73
 
107
- def auto_enable_stream_usage?(_request_url)
74
+ def retain_stream_event?(_data)
75
+ false
76
+ end
77
+
78
+ def auto_enable_stream_usage?(_request_url, _request_parsed)
108
79
  false
109
80
  end
110
81