llm_cost_tracker 0.14.2 → 0.15.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -0
  3. data/LICENSE.txt +28 -0
  4. data/README.md +33 -41
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +7 -1
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +12 -94
  7. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +11 -12
  8. data/app/controllers/llm_cost_tracker/pricing_controller.rb +15 -4
  9. data/app/controllers/llm_cost_tracker/tags_controller.rb +21 -14
  10. data/app/helpers/llm_cost_tracker/application_helper.rb +22 -31
  11. data/app/helpers/llm_cost_tracker/chart_helper.rb +57 -108
  12. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +30 -17
  13. data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +18 -25
  14. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +30 -0
  15. data/app/models/llm_cost_tracker/call.rb +6 -4
  16. data/app/models/llm_cost_tracker/call_rollup.rb +6 -6
  17. data/app/services/llm_cost_tracker/dashboard/call_order.rb +36 -0
  18. data/app/services/llm_cost_tracker/dashboard/calls_export.rb +68 -0
  19. data/app/services/llm_cost_tracker/dashboard/data_quality/stats_query.rb +77 -0
  20. data/app/services/llm_cost_tracker/dashboard/data_quality/summary.rb +47 -0
  21. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +66 -210
  22. data/app/services/llm_cost_tracker/dashboard/filter.rb +2 -1
  23. data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +42 -35
  24. data/app/services/llm_cost_tracker/dashboard/percent.rb +12 -0
  25. data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
  26. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +19 -17
  27. data/app/services/llm_cost_tracker/dashboard/spend_chart.rb +50 -0
  28. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +7 -8
  29. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -4
  30. data/app/services/llm_cost_tracker/dashboard/top_models.rb +3 -4
  31. data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -6
  32. data/app/views/llm_cost_tracker/calls/index.html.erb +2 -1
  33. data/app/views/llm_cost_tracker/calls/show.html.erb +3 -14
  34. data/app/views/llm_cost_tracker/errors/query.html.erb +6 -0
  35. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -1
  36. data/app/views/llm_cost_tracker/tags/show.html.erb +1 -1
  37. data/lib/llm_cost_tracker/budget/estimate.rb +35 -0
  38. data/lib/llm_cost_tracker/budget/global.rb +69 -0
  39. data/lib/llm_cost_tracker/budget/limit.rb +52 -0
  40. data/lib/llm_cost_tracker/budget/per_tag.rb +59 -27
  41. data/lib/llm_cost_tracker/budget/tagged.rb +74 -0
  42. data/lib/llm_cost_tracker/budget.rb +19 -157
  43. data/lib/llm_cost_tracker/capture/event_window.rb +16 -13
  44. data/lib/llm_cost_tracker/capture/sdk_payload.rb +22 -17
  45. data/lib/llm_cost_tracker/capture/sse.rb +7 -5
  46. data/lib/llm_cost_tracker/capture/stream_collector.rb +89 -96
  47. data/lib/llm_cost_tracker/capture/stream_tap.rb +2 -2
  48. data/lib/llm_cost_tracker/capture/stream_tracker.rb +32 -37
  49. data/lib/llm_cost_tracker/capture_verifier.rb +2 -3
  50. data/lib/llm_cost_tracker/charges/cost_status.rb +23 -19
  51. data/lib/llm_cost_tracker/charges/line_item.rb +31 -33
  52. data/lib/llm_cost_tracker/configuration/budgets.rb +49 -18
  53. data/lib/llm_cost_tracker/configuration/capture.rb +7 -1
  54. data/lib/llm_cost_tracker/configuration/mutability.rb +3 -3
  55. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +20 -2
  56. data/lib/llm_cost_tracker/doctor/price_check.rb +24 -35
  57. data/lib/llm_cost_tracker/doctor/probe.rb +1 -1
  58. data/lib/llm_cost_tracker/doctor.rb +23 -20
  59. data/lib/llm_cost_tracker/engine.rb +8 -0
  60. data/lib/llm_cost_tracker/errors.rb +2 -4
  61. data/lib/llm_cost_tracker/event.rb +22 -29
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -2
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +6 -4
  64. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +1 -0
  65. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +1 -0
  66. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +2 -1
  67. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +1 -0
  68. data/lib/llm_cost_tracker/ingestion/batch.rb +33 -19
  69. data/lib/llm_cost_tracker/ingestion/inbox.rb +20 -11
  70. data/lib/llm_cost_tracker/ingestion/worker.rb +58 -37
  71. data/lib/llm_cost_tracker/ingestion.rb +20 -35
  72. data/lib/llm_cost_tracker/integrations/anthropic/batch_results_capture.rb +40 -0
  73. data/lib/llm_cost_tracker/integrations/anthropic/patches.rb +56 -0
  74. data/lib/llm_cost_tracker/integrations/anthropic.rb +59 -120
  75. data/lib/llm_cost_tracker/integrations/base/patch_target.rb +35 -0
  76. data/lib/llm_cost_tracker/integrations/base.rb +65 -81
  77. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +20 -19
  78. data/lib/llm_cost_tracker/integrations/openai/patches.rb +51 -40
  79. data/lib/llm_cost_tracker/integrations/openai/websocket_capture.rb +89 -0
  80. data/lib/llm_cost_tracker/integrations/openai.rb +114 -83
  81. data/lib/llm_cost_tracker/integrations/ruby_llm/v1/patches.rb +67 -0
  82. data/lib/llm_cost_tracker/integrations/ruby_llm/v1/reply.rb +155 -0
  83. data/lib/llm_cost_tracker/integrations/ruby_llm/v1.rb +154 -0
  84. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt/token_counts.rb +159 -0
  85. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt.rb +175 -0
  86. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/frame.rb +75 -0
  87. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/recorder.rb +70 -0
  88. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/seams.rb +86 -0
  89. data/lib/llm_cost_tracker/integrations/ruby_llm/v2.rb +161 -0
  90. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +7 -408
  91. data/lib/llm_cost_tracker/ledger/isolation.rb +15 -2
  92. data/lib/llm_cost_tracker/ledger/period/totals.rb +1 -1
  93. data/lib/llm_cost_tracker/ledger/rollups.rb +24 -8
  94. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +23 -6
  95. data/lib/llm_cost_tracker/ledger/schema/base.rb +9 -5
  96. data/lib/llm_cost_tracker/ledger/store.rb +17 -35
  97. data/lib/llm_cost_tracker/ledger/tags/breakdown.rb +2 -2
  98. data/lib/llm_cost_tracker/middleware/faraday/body.rb +57 -0
  99. data/lib/llm_cost_tracker/middleware/faraday/exchange.rb +153 -0
  100. data/lib/llm_cost_tracker/middleware/faraday/response_reader.rb +75 -0
  101. data/lib/llm_cost_tracker/middleware/faraday/stream_tee.rb +47 -0
  102. data/lib/llm_cost_tracker/middleware/faraday.rb +2 -301
  103. data/lib/llm_cost_tracker/parsers.rb +12 -0
  104. data/lib/llm_cost_tracker/prices.json +1179 -208
  105. data/lib/llm_cost_tracker/pricing/backfill.rb +59 -29
  106. data/lib/llm_cost_tracker/pricing/calculation/iterations.rb +49 -0
  107. data/lib/llm_cost_tracker/pricing/calculation/quantities.rb +40 -0
  108. data/lib/llm_cost_tracker/pricing/calculation/snapshot.rb +51 -0
  109. data/lib/llm_cost_tracker/pricing/calculation/totals.rb +59 -0
  110. data/lib/llm_cost_tracker/pricing/calculation.rb +82 -173
  111. data/lib/llm_cost_tracker/pricing/matcher.rb +31 -14
  112. data/lib/llm_cost_tracker/pricing/mode.rb +6 -19
  113. data/lib/llm_cost_tracker/pricing/off_peak.rb +50 -0
  114. data/lib/llm_cost_tracker/pricing/rate.rb +1 -0
  115. data/lib/llm_cost_tracker/pricing/registry/normalizer.rb +110 -0
  116. data/lib/llm_cost_tracker/pricing/registry.rb +38 -111
  117. data/lib/llm_cost_tracker/pricing/sync/change_printer.rb +40 -20
  118. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +35 -34
  119. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +29 -18
  120. data/lib/llm_cost_tracker/pricing/sync/remote_snapshot.rb +78 -0
  121. data/lib/llm_cost_tracker/pricing/sync/snapshot_guard.rb +2 -0
  122. data/lib/llm_cost_tracker/pricing/sync.rb +7 -53
  123. data/lib/llm_cost_tracker/pricing/unknown.rb +1 -1
  124. data/lib/llm_cost_tracker/providers/anthropic/parser.rb +30 -25
  125. data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +5 -3
  126. data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +122 -101
  127. data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -1
  128. data/lib/llm_cost_tracker/providers/azure/parser.rb +3 -6
  129. data/lib/llm_cost_tracker/providers/gemini/grounding.rb +87 -0
  130. data/lib/llm_cost_tracker/providers/gemini/parser.rb +120 -183
  131. data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +66 -47
  132. data/lib/llm_cost_tracker/providers/openai/hosts.rb +11 -0
  133. data/lib/llm_cost_tracker/providers/openai/model_families.rb +1 -1
  134. data/lib/llm_cost_tracker/providers/openai/output_item.rb +84 -0
  135. data/lib/llm_cost_tracker/providers/openai/parser.rb +2 -1
  136. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +81 -149
  137. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +133 -156
  138. data/lib/llm_cost_tracker/providers/openai/stream_parser.rb +127 -0
  139. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -62
  140. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +15 -4
  141. data/lib/llm_cost_tracker/providers.rb +3 -0
  142. data/lib/llm_cost_tracker/redaction.rb +13 -0
  143. data/lib/llm_cost_tracker/report/data.rb +25 -28
  144. data/lib/llm_cost_tracker/tags/context.rb +22 -7
  145. data/lib/llm_cost_tracker/tracker.rb +32 -26
  146. data/lib/llm_cost_tracker/usage/dimensions.yml +24 -0
  147. data/lib/llm_cost_tracker/usage/token_usage.rb +8 -4
  148. data/lib/llm_cost_tracker/version.rb +1 -1
  149. data/lib/llm_cost_tracker.rb +8 -4
  150. data/lib/tasks/llm_cost_tracker.rake +35 -5
  151. data/llm_cost_tracker.gemspec +6 -5
  152. metadata +58 -5
@@ -1,18 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "faraday"
4
- require "json"
5
- require "uri"
6
4
 
7
- require_relative "../capture/sse"
8
- require_relative "../capture/stream_tap"
9
- require_relative "../timing"
5
+ require_relative "faraday/exchange"
10
6
 
11
7
  module LlmCostTracker
12
8
  module Middleware
13
9
  class Faraday < ::Faraday::Middleware
14
- MULTIPART_MODEL = /name="model"\r\n(?:[^\r\n]+\r\n)*\r\n([^\r\n]+)\r\n/
15
-
16
10
  def initialize(app, **options)
17
11
  super(app)
18
12
  @tags = options.fetch(:tags, {})
@@ -21,300 +15,7 @@ module LlmCostTracker
21
15
  def call(request_env)
22
16
  return @app.call(request_env) unless LlmCostTracker.configuration.enabled
23
17
 
24
- request_url = request_env.url.to_s
25
- parser = Parsers.find_for(request_url)
26
- request_body = read_body(request_env.body) || multipart_model_body(request_env, parser)
27
- request_parsed = parser&.safe_json_parse(request_body)
28
- streaming = parser&.streaming_request?(request_url, request_parsed)
29
- if streaming
30
- request_body = inject_stream_usage_flag(request_env, parser, request_url, request_parsed) || request_body
31
- end
32
- stream_buffer = install_stream_tap(request_env, parser) if streaming
33
-
34
- context_tags, metadata = tag_snapshot(request_env) if parser
35
- if parser && request_env.method == :post
36
- Budget.enforce!(
37
- provider: parser.provider_for(request_url),
38
- model: parser.model_for(request_url, request_parsed),
39
- request: request_parsed,
40
- tags: Tracker.build_tags(context_tags: context_tags, metadata: metadata)
41
- )
42
- end
43
- started_at = LlmCostTracker::Timing.now_monotonic
44
-
45
- invoke_app_with_capture(
46
- request_env: request_env,
47
- parser: parser,
48
- request_url: request_url,
49
- request_body: request_body,
50
- streaming: streaming,
51
- stream_buffer: stream_buffer,
52
- context_tags: context_tags,
53
- metadata: metadata,
54
- started_at: started_at
55
- )
56
- end
57
-
58
- private
59
-
60
- def invoke_app_with_capture(request_env:,
61
- parser:,
62
- request_url:,
63
- request_body:,
64
- streaming:,
65
- stream_buffer:,
66
- context_tags:,
67
- metadata:,
68
- started_at:)
69
- response_received = false
70
- @app.call(request_env).on_complete do |response_env|
71
- response_received = true
72
- process(
73
- parser: parser,
74
- request_url: request_url,
75
- request_body: request_body,
76
- response_env: response_env,
77
- latency_ms: (LlmCostTracker::Timing.elapsed_ms(started_at) if request_env.method == :post),
78
- streaming: streaming,
79
- stream_buffer: stream_buffer,
80
- context_tags: context_tags,
81
- metadata: metadata
82
- )
83
- end
84
- rescue StandardError => e
85
- if streaming && parser && !response_received
86
- process_interrupted_stream(
87
- parser: parser,
88
- request_url: request_url,
89
- request_body: request_body,
90
- latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at),
91
- context_tags: context_tags,
92
- metadata: metadata,
93
- error: e
94
- )
95
- end
96
- raise
97
- end
98
-
99
- def inject_stream_usage_flag(request_env, parser, request_url, request_parsed)
100
- return nil unless LlmCostTracker.configuration.capture.request_stream_usage
101
- return nil unless parser.auto_enable_stream_usage?(request_url, request_parsed)
102
-
103
- stream_options = request_parsed["stream_options"]
104
- return nil if stream_options.is_a?(Hash) && stream_options.key?("include_usage")
105
-
106
- request_parsed["stream_options"] = (stream_options || {}).merge("include_usage" => true)
107
- new_body = request_parsed.to_json
108
- request_env.body = new_body
109
- new_body
110
- end
111
-
112
- def process_interrupted_stream(parser:,
113
- request_url:,
114
- request_body:,
115
- latency_ms:,
116
- context_tags:,
117
- metadata:,
118
- error:)
119
- request = parser.safe_json_parse(request_body)
120
- event = Event.build(
121
- provider: parser.provider_for(request_url),
122
- model: parser.model_for(request_url, request) || Event::UNKNOWN_MODEL,
123
- token_usage: Usage::TokenUsage.build(input_tokens: 0, output_tokens: 0, total_tokens: 0),
124
- stream: true,
125
- usage_source: Usage::Source::UNKNOWN
126
- )
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
130
- Tracker.record(
131
- event: event,
132
- latency_ms: latency_ms,
133
- metadata: merged_metadata,
134
- context_tags: context_tags
135
- )
136
- rescue LlmCostTracker::TransactionAbortedError
137
- raise
138
- rescue StandardError => e
139
- Logging.warn("Error recording interrupted stream: #{e.class}: #{e.message}")
140
- end
141
-
142
- def process(parser:,
143
- request_url:,
144
- request_body:,
145
- response_env:,
146
- latency_ms:,
147
- streaming:,
148
- stream_buffer:,
149
- context_tags:,
150
- metadata:)
151
- return unless parser
152
-
153
- parsed =
154
- if streaming
155
- parse_stream(
156
- parser: parser,
157
- request_url: request_url,
158
- request_body: request_body,
159
- response_env: response_env,
160
- stream_buffer: stream_buffer
161
- )
162
- else
163
- parse_response(
164
- parser: parser,
165
- request_url: request_url,
166
- request_body: request_body,
167
- response_env: response_env
168
- )
169
- end
170
-
171
- Tracker.record(event: parsed, latency_ms: latency_ms, metadata: metadata, context_tags: context_tags) if parsed
172
- rescue *LlmCostTracker::CALLER_ERRORS
173
- raise
174
- rescue ActiveRecord::RecordNotUnique
175
- nil
176
- rescue StandardError => e
177
- Logging.warn("Error processing response: #{e.class}: #{e.message}")
178
- end
179
-
180
- def parse_response(parser:, request_url:, request_body:, response_env:)
181
- response_body = read_body(response_env.body)
182
- unless response_body
183
- Logging.warn(
184
- "Unable to read response body for #{request_url_label(request_url)}; " \
185
- "known streaming responses are captured automatically, or via LlmCostTracker.track_stream " \
186
- "for custom clients."
187
- )
188
- return nil
189
- end
190
-
191
- parser.parse(
192
- request_url: request_url,
193
- request_body: request_body,
194
- response_status: response_env.status,
195
- response_body: response_body,
196
- response_headers: response_env.response_headers
197
- )
198
- end
199
-
200
- def parse_stream(parser:, request_url:, request_body:, response_env:, stream_buffer:)
201
- parser.parse_stream(
202
- request_url: request_url,
203
- request_body: request_body,
204
- response_status: response_env.status,
205
- events: stream_events(request_url, response_env, stream_buffer),
206
- response_headers: response_env.response_headers
207
- )
208
- end
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
-
223
- def forward_on_data_chunk(callable, chunk, size, env)
224
- arity = callable.arity
225
- return callable.call(chunk, size, env) if arity.negative?
226
-
227
- case arity
228
- when 0, 1 then callable.call(chunk)
229
- when 2 then callable.call(chunk, size)
230
- else callable.call(chunk, size, env)
231
- end
232
- end
233
-
234
- def install_stream_tap(request_env, parser)
235
- request = request_env.request
236
- return nil unless request
237
-
238
- original = request.on_data
239
- return nil unless original
240
-
241
- tap = Capture::StreamTap.new(notable: parser.method(:retain_stream_event?))
242
- request.on_data = proc do |chunk, size, env|
243
- tap << chunk
244
- forward_on_data_chunk(original, chunk, size, env)
245
- end
246
- tap
247
- rescue StandardError => e
248
- Logging.warn("Unable to install streaming tap: #{e.class}: #{e.message}")
249
- nil
250
- end
251
-
252
- def read_body(body)
253
- case body
254
- when String then body
255
- when nil then ""
256
- when Hash, Array then body.to_json
257
- else
258
- body.try(:to_str)
259
- end
260
- end
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
-
287
- def resolved_tags(request_env)
288
- return @tags.to_h unless @tags.respond_to?(:call)
289
-
290
- (@tags.arity.zero? ? @tags.call : @tags.call(request_env)).to_h
291
- end
292
-
293
- def tag_snapshot(request_env)
294
- [LlmCostTracker::Tags::Context.tags, resolved_tags(request_env)]
295
- rescue StandardError => e
296
- Logging.warn("Error resolving request tags: #{e.class}: #{e.message}")
297
- [{}, {}]
298
- end
299
-
300
- def capture_warning(request_url, stream_buffer)
301
- suffix = "recording usage_source=#{Usage::Source::UNKNOWN}. " \
302
- "Use LlmCostTracker.track_stream for manual capture."
303
- label = request_url_label(request_url)
304
- return "Unable to capture streaming response for #{label}; #{suffix}" unless stream_buffer&.overflowed?
305
-
306
- "Streaming response for #{label} exceeded #{Capture::SSE::LIMIT_BYTES} bytes; #{suffix}"
307
- end
308
-
309
- def request_url_label(value)
310
- uri = URI.parse(value.to_s)
311
- uri.query = nil
312
- uri.fragment = nil
313
- uri.user = nil
314
- uri.password = nil
315
- uri.to_s
316
- rescue URI::InvalidURIError
317
- value.to_s.split("?", 2).first
18
+ Exchange.new(request_env, tags: @tags).call(@app)
318
19
  end
319
20
  end
320
21
  end
@@ -14,6 +14,13 @@ module LlmCostTracker
14
14
  instances.find { |parser| parser.class.match?(url) }
15
15
  end
16
16
 
17
+ def self.batch_submission?(uri)
18
+ host = uri.host.to_s.downcase
19
+ uri.path.to_s.match?(%r{/(?:v1|openai)/batches\z}) &&
20
+ (Providers::Openai::Hosts::API_HOSTS.include?(host) || Providers::Azure::Hosts.openai?(host) ||
21
+ LlmCostTracker.configuration.capture.openai_compatible_providers.key?(host))
22
+ end
23
+
17
24
  def self.find_for_provider(provider)
18
25
  all_for_provider(provider).first
19
26
  end
@@ -75,11 +82,16 @@ module LlmCostTracker
75
82
  false
76
83
  end
77
84
 
85
+ def trim_stream_event(data)
86
+ data
87
+ end
88
+
78
89
  def auto_enable_stream_usage?(_request_url, _request_parsed)
79
90
  false
80
91
  end
81
92
 
82
93
  def safe_json_parse(body)
94
+ return body if body.is_a?(Hash)
83
95
  return {} if body.blank?
84
96
 
85
97
  parsed = JSON.parse(body)