llm_cost_tracker 0.13.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +99 -16
- data/README.md +17 -29
- data/app/controllers/llm_cost_tracker/application_controller.rb +19 -5
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +1 -0
- data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +2 -2
- data/app/controllers/llm_cost_tracker/tags_controller.rb +5 -4
- data/app/helpers/llm_cost_tracker/application_helper.rb +1 -1
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +16 -0
- data/app/models/llm_cost_tracker/call.rb +10 -4
- data/app/models/llm_cost_tracker/call_rollup.rb +19 -3
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +13 -0
- data/app/services/llm_cost_tracker/dashboard/filter.rb +24 -21
- data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -1
- data/app/services/llm_cost_tracker/dashboard/params.rb +10 -0
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +3 -3
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +5 -3
- data/app/views/llm_cost_tracker/calls/show.html.erb +6 -8
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +11 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -3
- data/app/views/llm_cost_tracker/tags/show.html.erb +3 -0
- data/config/routes.rb +6 -1
- data/lib/llm_cost_tracker/budget/per_tag.rb +163 -0
- data/lib/llm_cost_tracker/budget.rb +120 -32
- data/lib/llm_cost_tracker/capture/event_window.rb +99 -0
- data/lib/llm_cost_tracker/capture/sse.rb +108 -32
- data/lib/llm_cost_tracker/capture/stream_collector.rb +17 -56
- data/lib/llm_cost_tracker/capture/stream_tap.rb +57 -0
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +5 -35
- data/lib/llm_cost_tracker/charges/cost_status.rb +4 -3
- data/lib/llm_cost_tracker/configuration/budgets.rb +93 -0
- data/lib/llm_cost_tracker/configuration/capture.rb +42 -0
- data/lib/llm_cost_tracker/configuration/ingestion.rb +20 -0
- data/lib/llm_cost_tracker/configuration/mutability.rb +33 -0
- data/lib/llm_cost_tracker/configuration/pricing.rb +36 -0
- data/lib/llm_cost_tracker/configuration/section.rb +59 -0
- data/lib/llm_cost_tracker/configuration/tags.rb +52 -0
- data/lib/llm_cost_tracker/configuration.rb +69 -125
- data/lib/llm_cost_tracker/deprecator.rb +9 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +17 -8
- data/lib/llm_cost_tracker/doctor/price_check.rb +2 -1
- data/lib/llm_cost_tracker/doctor.rb +4 -4
- data/lib/llm_cost_tracker/engine.rb +4 -0
- data/lib/llm_cost_tracker/errors.rb +25 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +2 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +2 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +0 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +8 -4
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +50 -30
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +40 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +41 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb +30 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb +30 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +38 -7
- data/lib/llm_cost_tracker/ingestion/pool.rb +9 -2
- data/lib/llm_cost_tracker/ingestion.rb +3 -7
- data/lib/llm_cost_tracker/integrations/anthropic.rb +7 -1
- data/lib/llm_cost_tracker/integrations/base.rb +17 -1
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +16 -13
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +53 -18
- data/lib/llm_cost_tracker/ledger/isolation.rb +30 -0
- data/lib/llm_cost_tracker/ledger/period/totals.rb +3 -2
- data/lib/llm_cost_tracker/ledger/rollups.rb +45 -7
- data/lib/llm_cost_tracker/ledger/storable.rb +16 -0
- data/lib/llm_cost_tracker/ledger/store.rb +21 -13
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +15 -5
- data/lib/llm_cost_tracker/ledger.rb +1 -0
- data/lib/llm_cost_tracker/logging.rb +5 -5
- data/lib/llm_cost_tracker/middleware/faraday.rb +33 -44
- data/lib/llm_cost_tracker/parsers.rb +5 -1
- data/lib/llm_cost_tracker/prices.json +2089 -372
- data/lib/llm_cost_tracker/pricing/backfill.rb +11 -1
- data/lib/llm_cost_tracker/pricing/calculation.rb +4 -4
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +26 -15
- data/lib/llm_cost_tracker/pricing/matcher.rb +7 -0
- data/lib/llm_cost_tracker/pricing/rate.rb +1 -2
- data/lib/llm_cost_tracker/pricing/registry.rb +17 -6
- data/lib/llm_cost_tracker/pricing/sync/change_printer.rb +6 -1
- data/lib/llm_cost_tracker/pricing/sync/snapshot_guard.rb +47 -0
- data/lib/llm_cost_tracker/pricing/sync.rb +31 -14
- data/lib/llm_cost_tracker/pricing/unknown.rb +11 -8
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +3 -2
- data/lib/llm_cost_tracker/providers/azure/parser.rb +19 -0
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +4 -0
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +0 -7
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +10 -3
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +14 -8
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +6 -2
- data/lib/llm_cost_tracker/railtie.rb +3 -7
- data/lib/llm_cost_tracker/redaction.rb +32 -0
- data/lib/llm_cost_tracker/report/data.rb +2 -2
- data/lib/llm_cost_tracker/retention.rb +22 -10
- data/lib/llm_cost_tracker/tags/context.rb +3 -3
- data/lib/llm_cost_tracker/tags/sanitizer.rb +14 -40
- data/lib/llm_cost_tracker/tracker.rb +16 -26
- data/lib/llm_cost_tracker/usage/catalog.rb +1 -2
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +10 -3
- data/lib/tasks/llm_cost_tracker.rake +29 -14
- metadata +37 -12
|
@@ -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(
|
|
@@ -94,8 +95,8 @@ module LlmCostTracker
|
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
def inject_stream_usage_flag(request_env, parser, request_url, request_parsed)
|
|
97
|
-
return nil unless LlmCostTracker.configuration.
|
|
98
|
-
return nil unless parser
|
|
98
|
+
return nil unless LlmCostTracker.configuration.capture.request_stream_usage
|
|
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
|
|
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
|
-
|
|
126
|
-
|
|
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::
|
|
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:
|
|
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
|
-
|
|
243
|
+
tap = Capture::StreamTap.new(notable: parser.method(:retain_stream_event?))
|
|
246
244
|
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
|
|
245
|
+
tap << chunk
|
|
257
246
|
forward_on_data_chunk(original, chunk, size, env)
|
|
258
247
|
end
|
|
259
|
-
|
|
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&.
|
|
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
|