llm_cost_tracker 0.10.0 → 0.11.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +4 -1
- data/app/assets/llm_cost_tracker/application.css +782 -801
- data/app/controllers/llm_cost_tracker/application_controller.rb +14 -2
- data/app/controllers/llm_cost_tracker/calls_controller.rb +23 -16
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +0 -3
- data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +13 -19
- data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +15 -3
- data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +95 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +27 -33
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +19 -5
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
- data/app/views/layouts/llm_cost_tracker/application.html.erb +80 -17
- data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
- data/app/views/llm_cost_tracker/calls/show.html.erb +118 -119
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +119 -158
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +109 -108
- data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
- data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
- data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +49 -58
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
- data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
- data/app/views/llm_cost_tracker/tags/show.html.erb +83 -102
- data/config/routes.rb +1 -0
- data/lib/llm_cost_tracker/budget.rb +1 -2
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +1 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +2 -16
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +1 -5
- data/lib/llm_cost_tracker/configuration.rb +3 -20
- data/lib/llm_cost_tracker/doctor.rb +74 -8
- data/lib/llm_cost_tracker/event.rb +2 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +4 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +4 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +2 -0
- data/lib/llm_cost_tracker/ingestion/inbox.rb +1 -1
- data/lib/llm_cost_tracker/integrations/anthropic.rb +24 -32
- data/lib/llm_cost_tracker/integrations/openai.rb +39 -93
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +19 -63
- data/lib/llm_cost_tracker/ledger.rb +13 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +2 -1
- data/lib/llm_cost_tracker/parsers/anthropic.rb +14 -31
- data/lib/llm_cost_tracker/parsers/gemini.rb +3 -5
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +1 -7
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +3 -3
- data/lib/llm_cost_tracker/parsers.rb +0 -2
- data/lib/llm_cost_tracker/pricing/explainer.rb +1 -1
- data/lib/llm_cost_tracker/pricing/mode.rb +34 -4
- data/lib/llm_cost_tracker/pricing/service_charges.rb +1 -1
- data/lib/llm_cost_tracker/pricing.rb +1 -18
- data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +15 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +157 -0
- data/lib/llm_cost_tracker/railtie.rb +0 -4
- data/lib/llm_cost_tracker/reconcile_tasks.rb +18 -21
- data/lib/llm_cost_tracker/reconciliation/importer.rb +2 -7
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +6 -30
- data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +40 -0
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +7 -31
- data/lib/llm_cost_tracker/report/formatter.rb +32 -19
- data/lib/llm_cost_tracker/tracker.rb +1 -1
- data/lib/llm_cost_tracker/version.rb +1 -1
- metadata +14 -6
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -155
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
4
|
require_relative "../billing/line_item"
|
|
5
|
-
require_relative "../parsers/openai_service_charges"
|
|
6
5
|
require_relative "../providers/azure/hosts"
|
|
7
6
|
require_relative "../providers/openai/model_families"
|
|
7
|
+
require_relative "../providers/openai/service_charges"
|
|
8
8
|
|
|
9
9
|
module LlmCostTracker
|
|
10
10
|
module Integrations
|
|
@@ -245,7 +245,7 @@ module LlmCostTracker
|
|
|
245
245
|
output_items << chat_search if chat_search
|
|
246
246
|
return [] if output_items.empty?
|
|
247
247
|
|
|
248
|
-
LlmCostTracker::
|
|
248
|
+
LlmCostTracker::Providers::Openai::ServiceCharges.line_items_from_output(
|
|
249
249
|
output_items, request: request, model: model
|
|
250
250
|
)
|
|
251
251
|
end
|
|
@@ -255,9 +255,9 @@ module LlmCostTracker
|
|
|
255
255
|
return nil unless choices.respond_to?(:any?)
|
|
256
256
|
|
|
257
257
|
provider_field = if choices.any? { |choice| choice_used_url_citation?(choice) }
|
|
258
|
-
LlmCostTracker::
|
|
258
|
+
LlmCostTracker::Providers::Openai::ServiceCharges::CHAT_COMPLETIONS_ANNOTATION_PROVIDER_FIELD
|
|
259
259
|
elsif LlmCostTracker::Providers::Openai::ModelFamilies.chat_completions_search?(model)
|
|
260
|
-
LlmCostTracker::
|
|
260
|
+
LlmCostTracker::Providers::Openai::ServiceCharges::CHAT_COMPLETIONS_SEARCH_MODEL_PROVIDER_FIELD
|
|
261
261
|
end
|
|
262
262
|
return nil unless provider_field
|
|
263
263
|
|
|
@@ -355,79 +355,22 @@ module LlmCostTracker
|
|
|
355
355
|
end
|
|
356
356
|
end
|
|
357
357
|
|
|
358
|
-
module ResponsesPatch
|
|
359
|
-
def create(*args, **kwargs)
|
|
360
|
-
request = LlmCostTracker::Integrations::Openai.request_params(args, kwargs)
|
|
361
|
-
LlmCostTracker::Integrations::Openai.enforce_budget!(request: request)
|
|
362
|
-
started_at = LlmCostTracker::Timing.now_monotonic
|
|
363
|
-
response = super(*LlmCostTracker::Integrations::Openai.normalize_sdk_args(args, kwargs))
|
|
364
|
-
LlmCostTracker::Integrations::Openai.record_response(
|
|
365
|
-
response,
|
|
366
|
-
request: request,
|
|
367
|
-
latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at),
|
|
368
|
-
host: LlmCostTracker::Integrations::Openai.client_host_for(self)
|
|
369
|
-
)
|
|
370
|
-
response
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
def stream(*args, **kwargs)
|
|
374
|
-
LlmCostTracker::Integrations::Openai.wrap_stream_call(args, kwargs, self) do |normalized, _|
|
|
375
|
-
super(*normalized)
|
|
376
|
-
end
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
def stream_raw(*args, **kwargs)
|
|
380
|
-
LlmCostTracker::Integrations::Openai.wrap_stream_call(args, kwargs, self) do |normalized, _|
|
|
381
|
-
super(*normalized)
|
|
382
|
-
end
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
def retrieve_streaming(response_id, *args, **kwargs)
|
|
386
|
-
LlmCostTracker::Integrations::Openai.wrap_stream_call(args, kwargs, self) do |normalized, collector|
|
|
387
|
-
collector.provider_response_id = response_id
|
|
388
|
-
super(response_id, *normalized)
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
module ChatCompletionsPatch
|
|
394
|
-
def create(*args, **kwargs)
|
|
395
|
-
request = LlmCostTracker::Integrations::Openai.request_params(args, kwargs)
|
|
396
|
-
LlmCostTracker::Integrations::Openai.enforce_budget!(request: request)
|
|
397
|
-
started_at = LlmCostTracker::Timing.now_monotonic
|
|
398
|
-
response = super(*LlmCostTracker::Integrations::Openai.normalize_sdk_args(args, kwargs))
|
|
399
|
-
LlmCostTracker::Integrations::Openai.record_response(
|
|
400
|
-
response,
|
|
401
|
-
request: request,
|
|
402
|
-
latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at),
|
|
403
|
-
host: LlmCostTracker::Integrations::Openai.client_host_for(self)
|
|
404
|
-
)
|
|
405
|
-
response
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
def stream(*args, **kwargs)
|
|
409
|
-
LlmCostTracker::Integrations::Openai.wrap_stream_call(args, kwargs, self) do |normalized, _|
|
|
410
|
-
super(*normalized)
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
def stream_raw(*args, **kwargs)
|
|
415
|
-
LlmCostTracker::Integrations::Openai.wrap_stream_call(args, kwargs, self) do |normalized, _|
|
|
416
|
-
super(*normalized)
|
|
417
|
-
end
|
|
418
|
-
end
|
|
419
|
-
end
|
|
420
|
-
|
|
421
358
|
module PatchBuilder
|
|
422
359
|
module_function
|
|
423
360
|
|
|
424
361
|
def build(record_method:, methods:)
|
|
425
362
|
Module.new.tap do |mod|
|
|
426
|
-
methods.each { |method_name|
|
|
363
|
+
methods.each { |method_name| define_blocking_method(mod, method_name, record_method) }
|
|
427
364
|
end
|
|
428
365
|
end
|
|
429
366
|
|
|
430
|
-
def
|
|
367
|
+
def build_stream(methods:)
|
|
368
|
+
Module.new.tap do |mod|
|
|
369
|
+
methods.each { |method_name| define_stream_method(mod, method_name) }
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def define_blocking_method(mod, method_name, record_method)
|
|
431
374
|
mod.define_method(method_name) do |*args, **kwargs, &block|
|
|
432
375
|
integration = LlmCostTracker::Integrations::Openai
|
|
433
376
|
request = integration.request_params(args, kwargs)
|
|
@@ -443,38 +386,41 @@ module LlmCostTracker
|
|
|
443
386
|
response
|
|
444
387
|
end
|
|
445
388
|
end
|
|
389
|
+
|
|
390
|
+
def define_stream_method(mod, method_name)
|
|
391
|
+
mod.define_method(method_name) do |*args, **kwargs|
|
|
392
|
+
LlmCostTracker::Integrations::Openai.wrap_stream_call(args, kwargs, self) do |normalized, _|
|
|
393
|
+
super(*normalized)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
446
397
|
end
|
|
447
398
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
TranslationsPatch = PatchBuilder.build(record_method: :record_transcription, methods: %i[create])
|
|
452
|
-
SpeechPatch = PatchBuilder.build(record_method: :record_speech, methods: %i[create])
|
|
453
|
-
ModerationsPatch = PatchBuilder.build(record_method: :record_moderation, methods: %i[create])
|
|
399
|
+
module ResponsesPatch
|
|
400
|
+
include PatchBuilder.build(record_method: :record_response, methods: %i[create])
|
|
401
|
+
include PatchBuilder.build_stream(methods: %i[stream stream_raw])
|
|
454
402
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
LlmCostTracker::Integrations::Openai.enforce_budget!(request: request)
|
|
460
|
-
host = LlmCostTracker::Integrations::Openai.client_host_for(self)
|
|
461
|
-
collector = LlmCostTracker::Integrations::Openai.stream_collector(request, host: host)
|
|
462
|
-
stream = super(*LlmCostTracker::Integrations::Openai.normalize_sdk_args(args, kwargs))
|
|
463
|
-
LlmCostTracker::Integrations::Openai.track_stream(stream, collector: collector)
|
|
403
|
+
def retrieve_streaming(response_id, *args, **kwargs)
|
|
404
|
+
LlmCostTracker::Integrations::Openai.wrap_stream_call(args, kwargs, self) do |normalized, collector|
|
|
405
|
+
collector.provider_response_id = response_id
|
|
406
|
+
super(response_id, *normalized)
|
|
464
407
|
end
|
|
465
408
|
end
|
|
466
409
|
end
|
|
467
410
|
|
|
468
|
-
module
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
LlmCostTracker::Integrations::Openai.enforce_budget!(request: request)
|
|
472
|
-
host = LlmCostTracker::Integrations::Openai.client_host_for(self)
|
|
473
|
-
collector = LlmCostTracker::Integrations::Openai.stream_collector(request, host: host)
|
|
474
|
-
stream = super(*LlmCostTracker::Integrations::Openai.normalize_sdk_args(args, kwargs))
|
|
475
|
-
LlmCostTracker::Integrations::Openai.track_stream(stream, collector: collector)
|
|
476
|
-
end
|
|
411
|
+
module ChatCompletionsPatch
|
|
412
|
+
include PatchBuilder.build(record_method: :record_response, methods: %i[create])
|
|
413
|
+
include PatchBuilder.build_stream(methods: %i[stream stream_raw])
|
|
477
414
|
end
|
|
415
|
+
|
|
416
|
+
EmbeddingsPatch = PatchBuilder.build(record_method: :record_response, methods: %i[create])
|
|
417
|
+
ImagesPatch = PatchBuilder.build(record_method: :record_image, methods: %i[generate edit create_variation])
|
|
418
|
+
TranscriptionsPatch = PatchBuilder.build(record_method: :record_transcription, methods: %i[create])
|
|
419
|
+
TranslationsPatch = PatchBuilder.build(record_method: :record_transcription, methods: %i[create])
|
|
420
|
+
SpeechPatch = PatchBuilder.build(record_method: :record_speech, methods: %i[create])
|
|
421
|
+
ModerationsPatch = PatchBuilder.build(record_method: :record_moderation, methods: %i[create])
|
|
422
|
+
StreamingImagesPatch = PatchBuilder.build_stream(methods: %i[generate_stream_raw edit_stream_raw])
|
|
423
|
+
StreamingTranscriptionsPatch = PatchBuilder.build_stream(methods: %i[create_streaming])
|
|
478
424
|
end
|
|
479
425
|
end
|
|
480
426
|
end
|
|
@@ -31,20 +31,16 @@ module LlmCostTracker
|
|
|
31
31
|
]
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def record_completion(provider, response, request:, latency_ms:,
|
|
34
|
+
def record_completion(provider, response, request:, latency_ms:, has_block:)
|
|
35
35
|
record_usage(
|
|
36
36
|
provider: provider_slug(provider),
|
|
37
37
|
model: response_model_id(response) || model_id(request[:model]),
|
|
38
38
|
response: response,
|
|
39
39
|
latency_ms: latency_ms,
|
|
40
|
-
stream: stream
|
|
40
|
+
stream: has_block || request[:stream] == true
|
|
41
41
|
)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def streaming_request?(request, has_block:)
|
|
45
|
-
has_block || request[:stream] == true
|
|
46
|
-
end
|
|
47
|
-
|
|
48
44
|
def record_embedding(provider, response, request:, latency_ms:)
|
|
49
45
|
record_usage(
|
|
50
46
|
provider: provider_slug(provider),
|
|
@@ -198,77 +194,37 @@ module LlmCostTracker
|
|
|
198
194
|
|
|
199
195
|
module ProviderPatch
|
|
200
196
|
def complete(*args, **kwargs, &)
|
|
201
|
-
|
|
202
|
-
request = integration.request_params(args, kwargs)
|
|
203
|
-
integration.enforce_budget!(request: request)
|
|
204
|
-
started_at = LlmCostTracker::Timing.now_monotonic
|
|
205
|
-
response = super
|
|
206
|
-
integration.record_completion(
|
|
207
|
-
self,
|
|
208
|
-
response,
|
|
209
|
-
request: request,
|
|
210
|
-
latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at),
|
|
211
|
-
stream: integration.streaming_request?(request, has_block: block_given?)
|
|
212
|
-
)
|
|
213
|
-
response
|
|
197
|
+
measure(args, kwargs, recorder: :record_completion, has_block: block_given?) { super }
|
|
214
198
|
end
|
|
215
199
|
|
|
216
200
|
def embed(*args, **kwargs)
|
|
217
|
-
|
|
218
|
-
request = integration.request_params(args, kwargs)
|
|
219
|
-
integration.enforce_budget!(request: request)
|
|
220
|
-
started_at = LlmCostTracker::Timing.now_monotonic
|
|
221
|
-
response = super
|
|
222
|
-
integration.record_embedding(
|
|
223
|
-
self,
|
|
224
|
-
response,
|
|
225
|
-
request: request,
|
|
226
|
-
latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at)
|
|
227
|
-
)
|
|
228
|
-
response
|
|
201
|
+
measure(args, kwargs, recorder: :record_embedding) { super }
|
|
229
202
|
end
|
|
230
203
|
|
|
231
204
|
def transcribe(*args, **kwargs)
|
|
232
|
-
|
|
233
|
-
request = integration.request_params(args, kwargs)
|
|
234
|
-
integration.enforce_budget!(request: request)
|
|
235
|
-
started_at = LlmCostTracker::Timing.now_monotonic
|
|
236
|
-
response = super
|
|
237
|
-
integration.record_transcription(
|
|
238
|
-
self,
|
|
239
|
-
response,
|
|
240
|
-
request: request,
|
|
241
|
-
latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at)
|
|
242
|
-
)
|
|
243
|
-
response
|
|
205
|
+
measure(args, kwargs, recorder: :record_transcription) { super }
|
|
244
206
|
end
|
|
245
207
|
|
|
246
208
|
def paint(*args, **kwargs)
|
|
247
|
-
|
|
248
|
-
request = integration.request_params(args, kwargs)
|
|
249
|
-
integration.enforce_budget!(request: request)
|
|
250
|
-
started_at = LlmCostTracker::Timing.now_monotonic
|
|
251
|
-
response = super
|
|
252
|
-
integration.record_image(
|
|
253
|
-
self,
|
|
254
|
-
response,
|
|
255
|
-
request: request,
|
|
256
|
-
latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at)
|
|
257
|
-
)
|
|
258
|
-
response
|
|
209
|
+
measure(args, kwargs, recorder: :record_image) { super }
|
|
259
210
|
end
|
|
260
211
|
|
|
261
212
|
def moderate(*args, **kwargs)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
213
|
+
measure(args, kwargs, recorder: :record_moderation) { super }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def measure(args, kwargs, recorder:, **extras)
|
|
219
|
+
request = RubyLlm.request_params(args, kwargs)
|
|
220
|
+
RubyLlm.enforce_budget!(request: request)
|
|
265
221
|
started_at = LlmCostTracker::Timing.now_monotonic
|
|
266
|
-
response =
|
|
267
|
-
|
|
268
|
-
self,
|
|
269
|
-
response,
|
|
222
|
+
response = yield
|
|
223
|
+
RubyLlm.public_send(
|
|
224
|
+
recorder, self, response,
|
|
270
225
|
request: request,
|
|
271
|
-
latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at)
|
|
226
|
+
latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at),
|
|
227
|
+
**extras
|
|
272
228
|
)
|
|
273
229
|
response
|
|
274
230
|
end
|
|
@@ -14,3 +14,16 @@ require_relative "ledger/rollups/upsert_sql"
|
|
|
14
14
|
require_relative "ledger/rollups"
|
|
15
15
|
require_relative "ledger/store"
|
|
16
16
|
require_relative "ledger/period/totals"
|
|
17
|
+
|
|
18
|
+
module LlmCostTracker
|
|
19
|
+
module Ledger
|
|
20
|
+
module Schema
|
|
21
|
+
CORE_SCHEMAS = [
|
|
22
|
+
[Calls, "llm_cost_tracker_calls"],
|
|
23
|
+
[CallLineItems, "llm_cost_tracker_call_line_items"],
|
|
24
|
+
[CallTags, "llm_cost_tracker_call_tags"]
|
|
25
|
+
].freeze
|
|
26
|
+
CACHE_ROLLUPS_SCHEMA = [CallRollups, "llm_cost_tracker_call_rollups"].freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -6,6 +6,7 @@ require "stringio"
|
|
|
6
6
|
require "uri"
|
|
7
7
|
|
|
8
8
|
require_relative "../logging"
|
|
9
|
+
require_relative "../capture/sse"
|
|
9
10
|
require_relative "../capture/stream"
|
|
10
11
|
require_relative "../timing"
|
|
11
12
|
|
|
@@ -201,7 +202,7 @@ module LlmCostTracker
|
|
|
201
202
|
)
|
|
202
203
|
end
|
|
203
204
|
|
|
204
|
-
events = overflowed ? [] :
|
|
205
|
+
events = overflowed ? [] : Capture::SSE.parse(body)
|
|
205
206
|
parser.parse_stream(
|
|
206
207
|
request_url: request_url,
|
|
207
208
|
request_body: request_body,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "base"
|
|
4
|
+
require_relative "../providers/anthropic/server_tools"
|
|
4
5
|
require_relative "../providers/anthropic/tier_classification"
|
|
5
6
|
|
|
6
7
|
module LlmCostTracker
|
|
@@ -104,36 +105,18 @@ module LlmCostTracker
|
|
|
104
105
|
server_tool_use = usage["server_tool_use"]
|
|
105
106
|
return [] unless server_tool_use.is_a?(Hash)
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
quantity: server_tool_use["web_search_requests"],
|
|
111
|
-
provider_field: "usage.server_tool_use.web_search_requests"
|
|
112
|
-
),
|
|
113
|
-
service_line_item(
|
|
114
|
-
component_key: :web_fetch_request,
|
|
115
|
-
quantity: server_tool_use["web_fetch_requests"],
|
|
116
|
-
provider_field: "usage.server_tool_use.web_fetch_requests"
|
|
117
|
-
),
|
|
118
|
-
service_line_item(
|
|
119
|
-
component_key: :code_execution_request,
|
|
120
|
-
quantity: server_tool_use["code_execution_requests"],
|
|
121
|
-
provider_field: "usage.server_tool_use.code_execution_requests"
|
|
122
|
-
)
|
|
123
|
-
].compact
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def service_line_item(component_key:, quantity:, provider_field:)
|
|
127
|
-
quantity = quantity.to_i
|
|
128
|
-
return if quantity.zero?
|
|
108
|
+
Providers::Anthropic::ServerTools::LINE_ITEMS.filter_map do |component_key, count_key|
|
|
109
|
+
quantity = server_tool_use[count_key.to_s].to_i
|
|
110
|
+
next if quantity.zero?
|
|
129
111
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
112
|
+
Billing::LineItem.build(
|
|
113
|
+
component_key: component_key,
|
|
114
|
+
quantity: quantity,
|
|
115
|
+
cost_status: Billing::CostStatus::UNKNOWN,
|
|
116
|
+
pricing_basis: :provider_usage,
|
|
117
|
+
provider_field: "usage.server_tool_use.#{count_key}"
|
|
118
|
+
)
|
|
119
|
+
end
|
|
137
120
|
end
|
|
138
121
|
|
|
139
122
|
def token_usage(usage:, cache_read:)
|
|
@@ -176,8 +159,8 @@ module LlmCostTracker
|
|
|
176
159
|
service_tier = usage&.fetch("service_tier", nil) || request["service_tier"]
|
|
177
160
|
service_tier = nil if Providers::Anthropic::TierClassification.standard_equivalent_tier?(service_tier)
|
|
178
161
|
|
|
179
|
-
modes << Pricing.
|
|
180
|
-
modes << Pricing.
|
|
162
|
+
modes << Pricing::Mode.normalize(speed)
|
|
163
|
+
modes << Pricing::Mode.normalize(service_tier)
|
|
181
164
|
geo = inference_geo(request: request, usage: usage).downcase
|
|
182
165
|
modes << "data_residency" if Providers::Anthropic::TierClassification.data_residency_geo?(geo)
|
|
183
166
|
|
|
@@ -152,9 +152,7 @@ module LlmCostTracker
|
|
|
152
152
|
|
|
153
153
|
def modality_tokens(details, modality)
|
|
154
154
|
Array(details).sum do |detail|
|
|
155
|
-
next 0 unless detail.is_a?(Hash)
|
|
156
|
-
|
|
157
|
-
next 0 unless detail["modality"] == modality
|
|
155
|
+
next 0 unless detail.is_a?(Hash) && detail["modality"] == modality
|
|
158
156
|
|
|
159
157
|
(detail["tokenCount"] || detail["token_count"]).to_i
|
|
160
158
|
end
|
|
@@ -174,10 +172,10 @@ module LlmCostTracker
|
|
|
174
172
|
|
|
175
173
|
def pricing_mode(request:, response_headers:)
|
|
176
174
|
response_tier = response_header(response_headers, "x-gemini-service-tier")
|
|
177
|
-
response_mode = Pricing.
|
|
175
|
+
response_mode = Pricing::Mode.normalize(response_tier)
|
|
178
176
|
return response_mode if response_mode
|
|
179
177
|
|
|
180
|
-
request_mode = Pricing.
|
|
178
|
+
request_mode = Pricing::Mode.normalize(
|
|
181
179
|
request["service_tier"] ||
|
|
182
180
|
request["serviceTier"] ||
|
|
183
181
|
request.dig("config", "service_tier") ||
|
|
@@ -30,12 +30,6 @@ module LlmCostTracker
|
|
|
30
30
|
@provider_names = names
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def provider_for(request_url)
|
|
34
|
-
provider_for_uri(parsed_uri(request_url)) || "openai_compatible"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
33
|
def provider_for_uri(uri)
|
|
40
34
|
return nil unless uri
|
|
41
35
|
|
|
@@ -44,7 +38,7 @@ module LlmCostTracker
|
|
|
44
38
|
end
|
|
45
39
|
|
|
46
40
|
def provider_for(request_url)
|
|
47
|
-
self.class.
|
|
41
|
+
self.class.provider_for_uri(parsed_uri(request_url)) || "openai_compatible"
|
|
48
42
|
end
|
|
49
43
|
end
|
|
50
44
|
end
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "openai_service_charges"
|
|
4
3
|
require_relative "../providers/openai/hosts"
|
|
5
4
|
require_relative "../providers/openai/model_families"
|
|
5
|
+
require_relative "../providers/openai/service_charges"
|
|
6
6
|
|
|
7
7
|
module LlmCostTracker
|
|
8
8
|
module Parsers
|
|
9
9
|
module OpenaiUsage
|
|
10
|
-
include
|
|
10
|
+
include LlmCostTracker::Providers::Openai::ServiceCharges
|
|
11
11
|
|
|
12
12
|
class << self
|
|
13
13
|
def combined_pricing_mode(host:, model:, service_tier:)
|
|
14
|
-
modes = [Pricing.
|
|
14
|
+
modes = [Pricing::Mode.normalize(service_tier)]
|
|
15
15
|
modes << "data_residency" if regional_processing?(host: host, model: model)
|
|
16
16
|
modes = modes.compact.uniq
|
|
17
17
|
modes.empty? ? nil : modes.join("_")
|
|
@@ -4,8 +4,6 @@ module LlmCostTracker
|
|
|
4
4
|
module Parsers
|
|
5
5
|
autoload :Base, "llm_cost_tracker/parsers/base"
|
|
6
6
|
autoload :OpenaiUsage, "llm_cost_tracker/parsers/openai_usage"
|
|
7
|
-
autoload :OpenaiServiceCharges, "llm_cost_tracker/parsers/openai_service_charges"
|
|
8
|
-
autoload :SSE, "llm_cost_tracker/parsers/sse"
|
|
9
7
|
autoload :Openai, "llm_cost_tracker/parsers/openai"
|
|
10
8
|
autoload :Azure, "llm_cost_tracker/parsers/azure"
|
|
11
9
|
autoload :OpenaiCompatible, "llm_cost_tracker/parsers/openai_compatible"
|
|
@@ -50,7 +50,7 @@ module LlmCostTracker
|
|
|
50
50
|
|
|
51
51
|
def explanation(provider:, model:, pricing_mode:, match:, usage:)
|
|
52
52
|
prices = match&.prices
|
|
53
|
-
pricing_mode = Pricing.
|
|
53
|
+
pricing_mode = Pricing::Mode.normalize(pricing_mode)
|
|
54
54
|
effective = if prices
|
|
55
55
|
EffectivePrices.call(usage: usage, quantities: usage.priced_quantities,
|
|
56
56
|
prices: prices, pricing_mode: pricing_mode)
|
|
@@ -3,10 +3,39 @@
|
|
|
3
3
|
module LlmCostTracker
|
|
4
4
|
module Pricing
|
|
5
5
|
class Mode
|
|
6
|
-
|
|
6
|
+
STANDARD_MODE_VALUES = %i[auto default standard standard_only].freeze
|
|
7
|
+
COMPOUND_MODIFIERS = %i[data_residency].freeze
|
|
7
8
|
|
|
8
9
|
attr_reader :modifiers
|
|
9
10
|
|
|
11
|
+
def self.normalize(value)
|
|
12
|
+
return nil if value.nil?
|
|
13
|
+
|
|
14
|
+
symbol = normalize_string(value.to_s)
|
|
15
|
+
return nil unless symbol
|
|
16
|
+
|
|
17
|
+
STANDARD_MODE_VALUES.include?(symbol) ? nil : symbol
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.merge(provider_mode, request_mode)
|
|
21
|
+
return normalize(request_mode) if provider_mode.to_s.strip.empty?
|
|
22
|
+
|
|
23
|
+
provider_tokens = tokenize(provider_mode) - STANDARD_MODE_VALUES
|
|
24
|
+
request_host_tokens = tokenize(request_mode || "") & COMPOUND_MODIFIERS
|
|
25
|
+
combined = provider_tokens | request_host_tokens
|
|
26
|
+
return nil if combined.empty?
|
|
27
|
+
|
|
28
|
+
normalize(combined.join("_"))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.normalize_string(value)
|
|
32
|
+
normalized = value.strip
|
|
33
|
+
return nil if normalized.empty?
|
|
34
|
+
|
|
35
|
+
normalized.downcase.tr("-", "_").to_sym
|
|
36
|
+
end
|
|
37
|
+
private_class_method :normalize_string
|
|
38
|
+
|
|
10
39
|
def self.parse(value)
|
|
11
40
|
return value if value.is_a?(self)
|
|
12
41
|
return new([]) if value.nil?
|
|
@@ -21,11 +50,12 @@ module LlmCostTracker
|
|
|
21
50
|
break if remaining.empty?
|
|
22
51
|
|
|
23
52
|
compound = COMPOUND_MODIFIERS.find do |token|
|
|
24
|
-
|
|
53
|
+
name = token.name
|
|
54
|
+
remaining == name || remaining.start_with?("#{name}_")
|
|
25
55
|
end
|
|
26
56
|
if compound
|
|
27
|
-
tokens << compound
|
|
28
|
-
remaining = remaining.delete_prefix(compound).delete_prefix("_")
|
|
57
|
+
tokens << compound
|
|
58
|
+
remaining = remaining.delete_prefix(compound.name).delete_prefix("_")
|
|
29
59
|
else
|
|
30
60
|
first, _, rest = remaining.partition("_")
|
|
31
61
|
tokens << first.to_sym unless first.empty?
|
|
@@ -68,7 +68,7 @@ module LlmCostTracker
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def charge_rate(provider:, component:, pricing_mode:)
|
|
71
|
-
pricing_mode = Pricing.
|
|
71
|
+
pricing_mode = Pricing::Mode.normalize(pricing_mode)
|
|
72
72
|
match = charge_rate_match(provider: provider, component: component, pricing_mode: pricing_mode)
|
|
73
73
|
return nil unless match
|
|
74
74
|
|
|
@@ -21,20 +21,10 @@ module LlmCostTracker
|
|
|
21
21
|
module Pricing # rubocop:disable Metrics/ModuleLength
|
|
22
22
|
extend ServiceCharges
|
|
23
23
|
|
|
24
|
-
STANDARD_MODE_VALUES = %i[auto default standard standard_only].freeze
|
|
25
24
|
RATE_DENOMINATOR_TOKENS = 1_000_000
|
|
26
25
|
private_constant :RATE_DENOMINATOR_TOKENS
|
|
27
26
|
|
|
28
27
|
class << self
|
|
29
|
-
def normalize_mode(value)
|
|
30
|
-
return nil if value.nil?
|
|
31
|
-
|
|
32
|
-
mode = normalize_string_mode(value.to_s)
|
|
33
|
-
return nil unless mode
|
|
34
|
-
|
|
35
|
-
STANDARD_MODE_VALUES.include?(mode) ? nil : mode
|
|
36
|
-
end
|
|
37
|
-
|
|
38
28
|
def cost_for(provider:, model:, tokens:, pricing_mode: nil)
|
|
39
29
|
calculation = calculation_for(
|
|
40
30
|
provider: provider,
|
|
@@ -139,13 +129,6 @@ module LlmCostTracker
|
|
|
139
129
|
)
|
|
140
130
|
end
|
|
141
131
|
|
|
142
|
-
def normalize_string_mode(value)
|
|
143
|
-
normalized = value.strip
|
|
144
|
-
return nil if normalized.empty?
|
|
145
|
-
|
|
146
|
-
normalized.downcase.tr("-", "_").to_sym
|
|
147
|
-
end
|
|
148
|
-
|
|
149
132
|
def cost_from(calculation)
|
|
150
133
|
costs = calculation[:costs]
|
|
151
134
|
values = Billing::Components::TOKEN_PRICED.each_with_object({}) do |component, result|
|
|
@@ -184,7 +167,7 @@ module LlmCostTracker
|
|
|
184
167
|
|
|
185
168
|
token_usage = TokenUsage.build_from_tokens(tokens)
|
|
186
169
|
quantities = token_usage.priced_quantities
|
|
187
|
-
mode =
|
|
170
|
+
mode = Mode.normalize(pricing_mode)
|
|
188
171
|
effective = EffectivePrices.call(usage: token_usage, quantities: quantities, prices: match.prices,
|
|
189
172
|
pricing_mode: mode)
|
|
190
173
|
return nil unless any_billable_priced?(quantities, effective)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Providers
|
|
5
|
+
module Anthropic
|
|
6
|
+
module ServerTools
|
|
7
|
+
LINE_ITEMS = {
|
|
8
|
+
web_search_request: :web_search_requests,
|
|
9
|
+
web_fetch_request: :web_fetch_requests,
|
|
10
|
+
code_execution_request: :code_execution_requests
|
|
11
|
+
}.freeze
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|