llm_cost_tracker 0.14.0 → 0.14.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -0
  3. data/README.md +1 -1
  4. data/app/controllers/llm_cost_tracker/application_controller.rb +16 -3
  5. data/app/controllers/llm_cost_tracker/pricing_controller.rb +2 -2
  6. data/app/controllers/llm_cost_tracker/tags_controller.rb +5 -4
  7. data/app/helpers/llm_cost_tracker/application_helper.rb +1 -1
  8. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +5 -0
  9. data/app/models/llm_cost_tracker/call.rb +1 -1
  10. data/app/services/llm_cost_tracker/dashboard/filter.rb +24 -21
  11. data/app/services/llm_cost_tracker/dashboard/params.rb +10 -0
  12. data/app/views/llm_cost_tracker/tags/show.html.erb +3 -0
  13. data/config/routes.rb +6 -1
  14. data/lib/llm_cost_tracker/budget/per_tag.rb +8 -4
  15. data/lib/llm_cost_tracker/capture/event_window.rb +99 -0
  16. data/lib/llm_cost_tracker/capture/sse.rb +108 -32
  17. data/lib/llm_cost_tracker/capture/stream_collector.rb +17 -56
  18. data/lib/llm_cost_tracker/capture/stream_tap.rb +57 -0
  19. data/lib/llm_cost_tracker/capture/stream_tracker.rb +5 -35
  20. data/lib/llm_cost_tracker/doctor/price_check.rb +1 -0
  21. data/lib/llm_cost_tracker/errors.rb +13 -0
  22. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +6 -6
  23. data/lib/llm_cost_tracker/ingestion/batch.rb +37 -8
  24. data/lib/llm_cost_tracker/integrations/anthropic.rb +7 -1
  25. data/lib/llm_cost_tracker/integrations/base.rb +17 -1
  26. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +6 -1
  27. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +53 -18
  28. data/lib/llm_cost_tracker/ledger/isolation.rb +30 -0
  29. data/lib/llm_cost_tracker/ledger/period/totals.rb +2 -1
  30. data/lib/llm_cost_tracker/ledger/rollups.rb +12 -4
  31. data/lib/llm_cost_tracker/ledger/storable.rb +16 -0
  32. data/lib/llm_cost_tracker/ledger/store.rb +12 -11
  33. data/lib/llm_cost_tracker/ledger.rb +1 -0
  34. data/lib/llm_cost_tracker/logging.rb +3 -1
  35. data/lib/llm_cost_tracker/middleware/faraday.rb +32 -43
  36. data/lib/llm_cost_tracker/parsers.rb +5 -1
  37. data/lib/llm_cost_tracker/prices.json +878 -139
  38. data/lib/llm_cost_tracker/pricing/registry.rb +12 -1
  39. data/lib/llm_cost_tracker/pricing/sync/change_printer.rb +6 -1
  40. data/lib/llm_cost_tracker/pricing/sync/snapshot_guard.rb +47 -0
  41. data/lib/llm_cost_tracker/pricing/sync.rb +30 -13
  42. data/lib/llm_cost_tracker/providers/azure/parser.rb +19 -0
  43. data/lib/llm_cost_tracker/providers/gemini/parser.rb +4 -0
  44. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +5 -1
  45. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +10 -5
  46. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +4 -0
  47. data/lib/llm_cost_tracker/redaction.rb +32 -0
  48. data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -36
  49. data/lib/llm_cost_tracker/tracker.rb +3 -4
  50. data/lib/llm_cost_tracker/version.rb +1 -1
  51. data/lib/llm_cost_tracker.rb +4 -2
  52. data/lib/tasks/llm_cost_tracker.rake +6 -4
  53. metadata +16 -4
@@ -8,10 +8,14 @@ module LlmCostTracker
8
8
  extend Base
9
9
 
10
10
  minimum_version "1.15.0"
11
+ maximum_version "3.0.0"
11
12
 
12
13
  class << self
13
14
  def patch_targets
14
- [patch_target("RubyLLM::Provider", with: ProviderPatch)]
15
+ [
16
+ patch_target("RubyLLM::Provider", with: ProviderPatch),
17
+ patch_target("RubyLLM::Providers::Gemini::Transcription", with: GeminiTranscriptionPatch, optional: true)
18
+ ]
15
19
  end
16
20
 
17
21
  def record_completion(provider, response, request:, latency_ms:, has_block:)
@@ -36,25 +40,29 @@ module LlmCostTracker
36
40
  end
37
41
 
38
42
  def record_transcription(provider, response, request:, latency_ms:)
43
+ model = response_model_id(response) || model_id_from_request(request[:model])
44
+ match = LlmCostTracker::Pricing::Matcher.lookup(provider: provider.slug.to_s, model: model)
39
45
  record_usage(
40
46
  provider: provider.slug.to_s,
41
- model: response_model_id(response) || model_id_from_request(request[:model]),
47
+ model: model,
42
48
  response: response,
43
49
  latency_ms: latency_ms,
44
- stream: false
50
+ stream: false,
51
+ audio_input: match&.prices&.key?("audio_input")
45
52
  )
46
53
  end
47
54
 
48
55
  def record_image(provider, response, request:, latency_ms:)
49
- usage = response.usage.with_indifferent_access
56
+ image = response.is_a?(Array) ? response.first : response
57
+ usage = image_usage(image)
50
58
  raw_input = usage[:input_tokens].to_i
51
59
  raw_output = usage[:output_tokens].to_i
52
60
  image_input = image_token_detail(usage, :input)
53
61
  image_output = image_token_detail(usage, :output)
54
62
  record_passthrough(
55
63
  provider: provider.slug.to_s,
56
- model: response_model_id(response) || model_id_from_request(request[:model]),
57
- response: response,
64
+ model: response_model_id(image) || model_id_from_request(request[:model]),
65
+ response: image,
58
66
  latency_ms: latency_ms,
59
67
  input_tokens: [raw_input - image_input, 0].max,
60
68
  image_input_tokens: image_input,
@@ -74,6 +82,12 @@ module LlmCostTracker
74
82
  )
75
83
  end
76
84
 
85
+ def image_usage(image)
86
+ usage = image.try(:usage)
87
+ usage = image.send(:raw_usage) if !usage.is_a?(Hash) && image.respond_to?(:raw_usage, true)
88
+ (usage.is_a?(Hash) ? usage : {}).with_indifferent_access
89
+ end
90
+
77
91
  def image_token_detail(usage, direction)
78
92
  container_key = direction == :input ? :input_tokens_details : :output_tokens_details
79
93
  details = usage[container_key]
@@ -111,27 +125,29 @@ module LlmCostTracker
111
125
  end
112
126
  end
113
127
 
114
- def record_usage(provider:, model:, response:, latency_ms:, stream:, output_tokens: nil)
128
+ def record_usage(provider:, model:, response:, latency_ms:, stream:, output_tokens: nil, audio_input: false)
115
129
  return unless active?
116
130
 
117
131
  record_safely do
118
- input_tokens = response.input_tokens
119
- output_tokens = response.output_tokens if output_tokens.nil?
132
+ counts = token_counts(response)
133
+ input_tokens = counts[:input]
134
+ output_tokens = counts[:output] if output_tokens.nil?
120
135
  next if input_tokens.nil? && output_tokens.nil?
121
136
 
122
- cache_write_5m, cache_write_1h = cache_creation_split(provider, response)
137
+ cache_write_5m, cache_write_1h = cache_write_split(provider, response, counts[:cache_write])
123
138
  LlmCostTracker::Tracker.record(
124
139
  event: Event.build(
125
140
  provider: provider,
126
141
  model: model,
127
142
  pricing_mode: pricing_mode_for(provider: provider, response: response),
128
143
  token_usage: Usage::TokenUsage.build(
129
- input_tokens: input_tokens.to_i,
144
+ input_tokens: audio_input ? 0 : input_tokens.to_i,
145
+ audio_input_tokens: audio_input ? input_tokens.to_i : 0,
130
146
  output_tokens: output_tokens.to_i,
131
- cache_read_input_tokens: response.try(:cached_tokens).to_i,
147
+ cache_read_input_tokens: counts[:cache_read].to_i,
132
148
  cache_write_input_tokens: cache_write_5m,
133
149
  cache_write_extended_input_tokens: cache_write_1h,
134
- hidden_output_tokens: response.try(:thinking_tokens).to_i
150
+ hidden_output_tokens: counts[:thinking].to_i
135
151
  ),
136
152
  stream: stream,
137
153
  usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE,
@@ -142,11 +158,22 @@ module LlmCostTracker
142
158
  end
143
159
  end
144
160
 
145
- def cache_creation_split(provider, response)
146
- return [response.try(:cache_creation_tokens).to_i, 0] unless provider == "anthropic"
161
+ def token_counts(response)
162
+ tokens = response.try(:tokens)
163
+ return { input: response.try(:input_tokens), output: response.try(:output_tokens) } unless tokens
164
+
165
+ {
166
+ input: tokens.input,
167
+ output: tokens.output,
168
+ cache_read: tokens.cache_read,
169
+ cache_write: tokens.cache_write,
170
+ thinking: tokens.thinking
171
+ }
172
+ end
147
173
 
148
- cache = raw_body(response).dig("usage", "cache_creation")
149
- return [response.try(:cache_creation_tokens).to_i, 0] unless cache.is_a?(Hash)
174
+ def cache_write_split(provider, response, cache_write)
175
+ cache = raw_body(response).dig("usage", "cache_creation") if provider == "anthropic"
176
+ return [cache_write.to_i, 0] unless cache.is_a?(Hash)
150
177
 
151
178
  [cache["ephemeral_5m_input_tokens"].to_i, cache["ephemeral_1h_input_tokens"].to_i]
152
179
  end
@@ -164,7 +191,8 @@ module LlmCostTracker
164
191
  end
165
192
 
166
193
  def raw_body(response)
167
- body = response.try(:raw)&.body
194
+ raw = response.try(:raw)
195
+ body = raw.respond_to?(:body) ? raw.body : raw
168
196
  body.is_a?(Hash) ? body : {}
169
197
  end
170
198
 
@@ -217,6 +245,13 @@ module LlmCostTracker
217
245
  LlmCostTracker::Integrations::RubyLlm.wrap_blocking(args, kwargs, **seam) { super }
218
246
  end
219
247
  end
248
+
249
+ module GeminiTranscriptionPatch
250
+ def transcribe(*args, **kwargs)
251
+ seam = LlmCostTracker::Integrations::RubyLlm.blocking_seam(self, :record_transcription)
252
+ LlmCostTracker::Integrations::RubyLlm.wrap_blocking(args, kwargs, **seam) { super }
253
+ end
254
+ end
220
255
  end
221
256
  end
222
257
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ module LlmCostTracker
6
+ module Ledger
7
+ module Isolation
8
+ class << self
9
+ def guard(model = LlmCostTracker::Call, &)
10
+ connection = model.connection
11
+ nested = connection.transaction_open?
12
+ return yield unless nested
13
+
14
+ model.transaction(requires_new: true, &)
15
+ rescue ActiveRecord::TransactionRollbackError => e
16
+ raise unless nested && connection.savepoint_errors_invalidate_transactions?
17
+
18
+ raise TransactionAbortedError, e
19
+ end
20
+
21
+ def after_commit(&)
22
+ current = LlmCostTracker::Call.connection.current_transaction
23
+ return yield unless current.open? && current.joinable? && current.respond_to?(:after_commit)
24
+
25
+ current.after_commit(&)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bigdecimal/util"
4
4
 
5
+ require_relative "../isolation"
5
6
  require_relative "../period"
6
7
 
7
8
  module LlmCostTracker
@@ -22,7 +23,7 @@ module LlmCostTracker
22
23
 
23
24
  values = periods.to_h { |period| [period, BigDecimal("0")] }
24
25
  period_by_name = periods.to_h { |period| [period.to_s, period] }
25
- LlmCostTracker::Call.find_by_sql(union_sql).each do |row|
26
+ Isolation.guard { LlmCostTracker::Call.find_by_sql(union_sql) }.each do |row|
26
27
  values[period_by_name.fetch(row.period_key)] = row.total_cost.to_d
27
28
  end
28
29
  values
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bigdecimal/util"
4
4
 
5
+ require_relative "isolation"
5
6
  require_relative "period"
6
7
 
7
8
  module LlmCostTracker
@@ -33,14 +34,21 @@ module LlmCostTracker
33
34
  :REBUILD_INSERT_SLICE
34
35
 
35
36
  def increment_safely!(events)
37
+ return unless LlmCostTracker.configuration.budgets.totals_source == :cache
38
+
39
+ Isolation.after_commit { increment_with_retries(events) }
40
+ end
41
+
42
+ def increment_with_retries(events)
43
+ retryable = !LlmCostTracker::Call.connection.transaction_open?
36
44
  attempt = 0
37
45
  begin
38
46
  attempt += 1
39
- increment!(events)
47
+ Isolation.guard { increment!(events) }
48
+ rescue LlmCostTracker::Error
49
+ raise
40
50
  rescue StandardError => e
41
- raise if LlmCostTracker::Call.connection.open_transactions.positive?
42
-
43
- if attempt < ROLLUP_INCREMENT_ATTEMPTS && rolled_back?(e)
51
+ if retryable && attempt < ROLLUP_INCREMENT_ATTEMPTS && rolled_back?(e)
44
52
  sleep(ROLLUP_INCREMENT_BASE_DELAY_SECONDS * (2**(attempt - 1)))
45
53
  retry
46
54
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LlmCostTracker
4
+ module Ledger
5
+ module Storable
6
+ def self.clean(value)
7
+ case value
8
+ when Hash then value.to_h { |key, nested| [clean(key), clean(nested)] }
9
+ when Array then value.map { |nested| clean(nested) }
10
+ when String then value.dup.force_encoding(Encoding::UTF_8).scrub.delete("\u0000")
11
+ else value
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -6,6 +6,7 @@ require_relative "../pricing"
6
6
  require_relative "rollups"
7
7
  require_relative "../budget/per_tag"
8
8
  require_relative "tags/encoding"
9
+ require_relative "storable"
9
10
 
10
11
  module LlmCostTracker
11
12
  module Ledger
@@ -23,11 +24,13 @@ module LlmCostTracker
23
24
  events = Array(events)
24
25
  return if events.empty?
25
26
 
26
- LlmCostTracker::Call.transaction do
27
- rows = events.map { |event| attributes_for(event) }
28
- call_ids = insert_calls_returning_ids(rows, events)
29
- insert_line_items(events, call_ids)
30
- insert_call_tags(events, call_ids)
27
+ Isolation.guard do
28
+ LlmCostTracker::Call.transaction do
29
+ rows = events.map { |event| attributes_for(event) }
30
+ call_ids = insert_calls_returning_ids(rows, events)
31
+ insert_line_items(events, call_ids)
32
+ insert_call_tags(events, call_ids)
33
+ end
31
34
  end
32
35
  end
33
36
 
@@ -62,9 +65,7 @@ module LlmCostTracker
62
65
  pricing_snapshot: event.pricing_snapshot
63
66
  }
64
67
 
65
- attributes
66
- .merge(event.token_usage.to_h)
67
- .merge(total_cost: event.cost&.total)
68
+ Storable.clean(attributes.merge(event.token_usage.to_h).merge(total_cost: event.cost&.total))
68
69
  end
69
70
 
70
71
  def call_ids_for(events)
@@ -90,7 +91,7 @@ module LlmCostTracker
90
91
  end
91
92
 
92
93
  def line_item_attributes(call_id:, line_item:, position:)
93
- {
94
+ Storable.clean(
94
95
  llm_cost_tracker_call_id: call_id,
95
96
  position: position,
96
97
  kind: line_item.kind,
@@ -112,7 +113,7 @@ module LlmCostTracker
112
113
  provider_item_id: line_item.provider_item_id,
113
114
  details: stored_details(line_item.details),
114
115
  created_at: Time.now.utc
115
- }
116
+ )
116
117
  end
117
118
 
118
119
  def insert_call_tags(events, call_ids)
@@ -121,7 +122,7 @@ module LlmCostTracker
121
122
  {
122
123
  llm_cost_tracker_call_id: call_ids.fetch(event.event_id),
123
124
  key: key.to_s,
124
- value: Tags::Encoding.encode(value)
125
+ value: Tags::Encoding.encode(Storable.clean(value))
125
126
  }.merge(budget_columns_for(event))
126
127
  end
127
128
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "ledger/isolation"
3
4
  require_relative "ledger/schema"
4
5
  require_relative "ledger/period"
5
6
  require_relative "ledger/rollups"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "redaction"
4
+
3
5
  module LlmCostTracker
4
6
  module Logging
5
7
  class << self
@@ -9,7 +11,7 @@ module LlmCostTracker
9
11
 
10
12
  private
11
13
 
12
- def prefixed(message) = "[#{LlmCostTracker.name}] #{message}"
14
+ def prefixed(message) = "[#{LlmCostTracker.name}] #{Redaction.text(message)}"
13
15
  end
14
16
  end
15
17
  end
@@ -2,10 +2,10 @@
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
@@ -27,16 +27,17 @@ module LlmCostTracker
27
27
  if streaming
28
28
  request_body = inject_stream_usage_flag(request_env, parser, request_url, request_parsed) || request_body
29
29
  end
30
- stream_buffer = install_stream_tap(request_env) if streaming
30
+ stream_buffer = install_stream_tap(request_env, parser) if streaming
31
31
 
32
32
  if parser
33
+ context_tags, metadata = tag_snapshot(request_env)
33
34
  Budget.enforce!(
34
35
  provider: parser.provider_for(request_url),
35
36
  model: parser.model_for(request_url, request_parsed),
36
- request: request_parsed
37
+ request: request_parsed,
38
+ tags: Tracker.build_tags(context_tags: context_tags, metadata: metadata)
37
39
  )
38
40
  end
39
- context_tags, metadata = tag_snapshot(request_env) if parser
40
41
  started_at = LlmCostTracker::Timing.now_monotonic
41
42
 
42
43
  invoke_app_with_capture(
@@ -95,7 +96,7 @@ module LlmCostTracker
95
96
 
96
97
  def inject_stream_usage_flag(request_env, parser, request_url, request_parsed)
97
98
  return nil unless LlmCostTracker.configuration.capture.request_stream_usage
98
- return nil unless parser&.auto_enable_stream_usage?(request_url)
99
+ return nil unless parser.auto_enable_stream_usage?(request_url, request_parsed)
99
100
 
100
101
  stream_options = request_parsed["stream_options"]
101
102
  return nil if stream_options.is_a?(Hash) && stream_options.key?("include_usage")
@@ -116,21 +117,22 @@ module LlmCostTracker
116
117
  request = parser.safe_json_parse(request_body)
117
118
  event = Event.build(
118
119
  provider: parser.provider_for(request_url),
119
- model: request["model"] || Event::UNKNOWN_MODEL,
120
+ model: parser.model_for(request_url, request) || Event::UNKNOWN_MODEL,
120
121
  token_usage: Usage::TokenUsage.build(input_tokens: 0, output_tokens: 0, total_tokens: 0),
121
122
  stream: true,
122
123
  usage_source: Usage::Source::UNKNOWN
123
124
  )
124
- merged_metadata = (metadata || {}).merge(
125
- stream_interrupted: true,
126
- stream_interrupted_error: "#{error.class}: #{error.message}"
127
- )
125
+ merged_metadata = (metadata || {}).merge(stream_interrupted: true, stream_interrupted_error: error.class.name)
126
+ status = error.try(:response_status)
127
+ merged_metadata[:stream_interrupted_status] = status if status
128
128
  Tracker.record(
129
129
  event: event,
130
130
  latency_ms: latency_ms,
131
131
  metadata: merged_metadata,
132
132
  context_tags: context_tags
133
133
  )
134
+ rescue LlmCostTracker::TransactionAbortedError
135
+ raise
134
136
  rescue StandardError => e
135
137
  Logging.warn("Error recording interrupted stream: #{e.class}: #{e.message}")
136
138
  end
@@ -171,7 +173,7 @@ module LlmCostTracker
171
173
  metadata: metadata,
172
174
  context_tags: context_tags
173
175
  )
174
- rescue LlmCostTracker::Error
176
+ rescue *LlmCostTracker::CALLER_ERRORS
175
177
  raise
176
178
  rescue StandardError => e
177
179
  Logging.warn("Error processing response: #{e.class}: #{e.message}")
@@ -198,32 +200,28 @@ module LlmCostTracker
198
200
  end
199
201
 
200
202
  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
203
  parser.parse_stream(
219
204
  request_url: request_url,
220
205
  request_body: request_body,
221
206
  response_status: response_env.status,
222
- events: events,
207
+ events: stream_events(request_url, response_env, stream_buffer),
223
208
  response_headers: response_env.response_headers
224
209
  )
225
210
  end
226
211
 
212
+ def stream_events(request_url, response_env, stream_buffer)
213
+ if stream_buffer&.received?
214
+ events = stream_buffer.events
215
+ return events unless stream_buffer.overflowed? || stream_buffer.failed?
216
+ else
217
+ body = read_body(response_env.body)
218
+ return Capture::SSE.parse(body) if body.present?
219
+ end
220
+
221
+ Logging.warn(capture_warning(request_url, stream_buffer))
222
+ []
223
+ end
224
+
227
225
  def forward_on_data_chunk(callable, chunk, size, env)
228
226
  arity = callable.arity
229
227
  return callable.call(chunk, size, env) if arity.negative?
@@ -235,28 +233,19 @@ module LlmCostTracker
235
233
  end
236
234
  end
237
235
 
238
- def install_stream_tap(request_env)
236
+ def install_stream_tap(request_env, parser)
239
237
  request = request_env.request
240
238
  return nil unless request
241
239
 
242
240
  original = request.on_data
243
241
  return nil unless original
244
242
 
245
- state = { buffer: StringIO.new, bytes: 0, overflowed: false }
243
+ tap = Capture::StreamTap.new(notable: parser.method(:retain_stream_event?))
246
244
  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
245
+ tap << chunk
257
246
  forward_on_data_chunk(original, chunk, size, env)
258
247
  end
259
- state
248
+ tap
260
249
  rescue StandardError => e
261
250
  Logging.warn("Unable to install streaming tap: #{e.class}: #{e.message}")
262
251
  nil
@@ -295,7 +284,7 @@ module LlmCostTracker
295
284
  suffix = "recording usage_source=#{Usage::Source::UNKNOWN}. " \
296
285
  "Use LlmCostTracker.track_stream for manual capture."
297
286
  label = request_url_label(request_url)
298
- return "Unable to capture streaming response for #{label}; #{suffix}" unless stream_buffer&.dig(:overflowed)
287
+ return "Unable to capture streaming response for #{label}; #{suffix}" unless stream_buffer&.overflowed?
299
288
 
300
289
  "Streaming response for #{label} exceeded #{Capture::SSE::LIMIT_BYTES} bytes; #{suffix}"
301
290
  end
@@ -104,7 +104,11 @@ module LlmCostTracker
104
104
  nil
105
105
  end
106
106
 
107
- def auto_enable_stream_usage?(_request_url)
107
+ def retain_stream_event?(_data)
108
+ false
109
+ end
110
+
111
+ def auto_enable_stream_usage?(_request_url, _request_parsed)
108
112
  false
109
113
  end
110
114