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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +128 -0
- data/README.md +16 -5
- data/app/assets/llm_cost_tracker/application.css +16 -0
- data/app/controllers/llm_cost_tracker/application_controller.rb +16 -3
- data/app/controllers/llm_cost_tracker/calls_controller.rb +1 -3
- data/app/controllers/llm_cost_tracker/models_controller.rb +2 -4
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +2 -2
- data/app/controllers/llm_cost_tracker/tags_controller.rb +9 -7
- data/app/helpers/llm_cost_tracker/application_helper.rb +5 -26
- data/app/helpers/llm_cost_tracker/chart_helper.rb +6 -10
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +5 -0
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +8 -28
- data/app/models/llm_cost_tracker/call.rb +10 -14
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +4 -15
- data/app/services/llm_cost_tracker/dashboard/filter.rb +29 -30
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/params.rb +10 -0
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +2 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +2 -3
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -0
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +3 -5
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -2
- data/app/views/llm_cost_tracker/calls/show.html.erb +8 -22
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -4
- data/app/views/llm_cost_tracker/tags/show.html.erb +8 -5
- data/config/routes.rb +6 -1
- data/lib/llm_cost_tracker/budget/per_tag.rb +21 -11
- data/lib/llm_cost_tracker/budget.rb +44 -55
- data/lib/llm_cost_tracker/capture/event_window.rb +100 -0
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +5 -1
- data/lib/llm_cost_tracker/capture/sse.rb +108 -32
- data/lib/llm_cost_tracker/capture/stream_collector.rb +41 -81
- data/lib/llm_cost_tracker/capture/stream_tap.rb +57 -0
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +14 -58
- data/lib/llm_cost_tracker/capture_verifier.rb +1 -7
- data/lib/llm_cost_tracker/charges/line_item.rb +5 -1
- data/lib/llm_cost_tracker/configuration/budgets.rb +1 -1
- data/lib/llm_cost_tracker/configuration/pricing.rb +1 -1
- data/lib/llm_cost_tracker/configuration.rb +5 -9
- data/lib/llm_cost_tracker/doctor/price_check.rb +14 -3
- data/lib/llm_cost_tracker/doctor/schema_check.rb +1 -2
- data/lib/llm_cost_tracker/doctor.rb +19 -2
- data/lib/llm_cost_tracker/engine.rb +0 -1
- data/lib/llm_cost_tracker/errors.rb +13 -0
- data/lib/llm_cost_tracker/event.rb +8 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +5 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -12
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb +3 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb +3 -8
- data/lib/llm_cost_tracker/ingestion/batch.rb +37 -8
- data/lib/llm_cost_tracker/ingestion/inbox.rb +2 -8
- data/lib/llm_cost_tracker/ingestion.rb +3 -1
- data/lib/llm_cost_tracker/integrations/anthropic.rb +70 -11
- data/lib/llm_cost_tracker/integrations/base.rb +56 -19
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +21 -13
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +8 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +42 -59
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +275 -75
- data/lib/llm_cost_tracker/integrations.rb +1 -20
- data/lib/llm_cost_tracker/ledger/isolation.rb +23 -0
- data/lib/llm_cost_tracker/ledger/period/totals.rb +16 -11
- data/lib/llm_cost_tracker/ledger/rollups.rb +25 -17
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +14 -3
- data/lib/llm_cost_tracker/ledger/schema/base.rb +5 -4
- data/lib/llm_cost_tracker/ledger/storable.rb +16 -0
- data/lib/llm_cost_tracker/ledger/store.rb +14 -13
- data/lib/llm_cost_tracker/ledger/tags/breakdown.rb +1 -5
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +3 -12
- data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -2
- data/lib/llm_cost_tracker/ledger.rb +1 -0
- data/lib/llm_cost_tracker/logging.rb +3 -1
- data/lib/llm_cost_tracker/middleware/faraday.rb +67 -61
- data/lib/llm_cost_tracker/parsers.rb +12 -41
- data/lib/llm_cost_tracker/prices.json +3071 -249
- data/lib/llm_cost_tracker/pricing/backfill.rb +46 -13
- data/lib/llm_cost_tracker/pricing/calculation.rb +118 -75
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +11 -10
- data/lib/llm_cost_tracker/pricing/estimator.rb +5 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +36 -22
- data/lib/llm_cost_tracker/pricing/mode.rb +2 -13
- data/lib/llm_cost_tracker/pricing/price_key.rb +5 -3
- data/lib/llm_cost_tracker/pricing/rate.rb +1 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +17 -17
- data/lib/llm_cost_tracker/pricing/service_rates.rb +1 -8
- data/lib/llm_cost_tracker/pricing/sync/change_printer.rb +6 -1
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +13 -9
- data/lib/llm_cost_tracker/pricing/sync/snapshot_guard.rb +47 -0
- data/lib/llm_cost_tracker/pricing/sync.rb +38 -70
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +33 -3
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +11 -5
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +81 -12
- data/lib/llm_cost_tracker/providers/azure/parser.rb +22 -7
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +175 -47
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +13 -22
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -1
- data/lib/llm_cost_tracker/providers/openai/parser.rb +11 -13
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +101 -27
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +57 -40
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +24 -6
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +7 -1
- data/lib/llm_cost_tracker/redaction.rb +32 -0
- data/lib/llm_cost_tracker/report/data.rb +1 -1
- data/lib/llm_cost_tracker/retention.rb +1 -3
- data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -36
- data/lib/llm_cost_tracker/tracker.rb +17 -10
- data/lib/llm_cost_tracker/usage/catalog.rb +13 -4
- data/lib/llm_cost_tracker/usage/dimension.rb +1 -1
- data/lib/llm_cost_tracker/usage/dimensions.yml +61 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +4 -2
- data/lib/tasks/llm_cost_tracker.rake +26 -21
- data/llm_cost_tracker.gemspec +63 -0
- 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
|
|
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
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
241
|
+
tap = Capture::StreamTap.new(notable: parser.method(:retain_stream_event?))
|
|
246
242
|
request.on_data = proc do |chunk, size, env|
|
|
247
|
-
|
|
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
|
-
|
|
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&.
|
|
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
|
|
11
|
+
PARSER_PROVIDERS = %i[Openai Azure Anthropic Gemini OpenaiCompatible].freeze
|
|
12
12
|
|
|
13
13
|
def self.find_for(url)
|
|
14
|
-
instances.
|
|
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
|
-
|
|
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.
|
|
29
|
-
|
|
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 ||=
|
|
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
|
|
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
|
|