llm_cost_tracker 0.10.0 → 0.12.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 +82 -0
- data/README.md +11 -5
- data/app/assets/llm_cost_tracker/application.css +784 -802
- data/app/controllers/llm_cost_tracker/application_controller.rb +14 -2
- data/app/controllers/llm_cost_tracker/calls_controller.rb +28 -21
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -4
- 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/tags_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +19 -16
- data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
- data/app/models/llm_cost_tracker/call.rb +28 -63
- data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
- data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
- data/app/models/llm_cost_tracker/call_tag.rb +0 -2
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +2 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +64 -43
- data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
- data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
- data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +81 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +6 -68
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +20 -12
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
- data/app/views/layouts/llm_cost_tracker/application.html.erb +74 -17
- data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
- data/app/views/llm_cost_tracker/calls/show.html.erb +132 -125
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +120 -159
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +140 -194
- 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/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 +85 -104
- data/config/routes.rb +3 -3
- data/lib/llm_cost_tracker/budget.rb +25 -28
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +2 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +30 -52
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +18 -33
- data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
- data/lib/llm_cost_tracker/charges/cost.rb +27 -0
- data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
- data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +40 -44
- data/lib/llm_cost_tracker/check.rb +5 -0
- data/lib/llm_cost_tracker/configuration.rb +13 -61
- data/lib/llm_cost_tracker/currency.rb +5 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
- data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
- data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
- data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
- data/lib/llm_cost_tracker/doctor.rb +66 -64
- data/lib/llm_cost_tracker/engine.rb +4 -4
- data/lib/llm_cost_tracker/event.rb +12 -20
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +4 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +39 -8
- data/lib/llm_cost_tracker/ingestion/inbox.rb +8 -9
- data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
- data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
- data/lib/llm_cost_tracker/ingestion.rb +24 -36
- data/lib/llm_cost_tracker/integrations/anthropic.rb +94 -116
- data/lib/llm_cost_tracker/integrations/base.rb +39 -57
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +72 -332
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +89 -145
- data/lib/llm_cost_tracker/integrations.rb +32 -25
- data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
- data/lib/llm_cost_tracker/ledger/period.rb +5 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
- data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
- data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
- data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
- data/lib/llm_cost_tracker/ledger/store.rb +18 -42
- data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
- data/lib/llm_cost_tracker/ledger.rb +14 -11
- data/lib/llm_cost_tracker/logging.rb +4 -21
- data/lib/llm_cost_tracker/middleware/faraday.rb +63 -51
- data/lib/llm_cost_tracker/parsers.rb +140 -29
- data/lib/llm_cost_tracker/prices.json +1707 -1
- data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
- data/lib/llm_cost_tracker/pricing/calculation.rb +260 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
- data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +84 -0
- data/lib/llm_cost_tracker/pricing/mode.rb +53 -35
- data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
- data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
- data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
- data/lib/llm_cost_tracker/pricing/source.rb +7 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
- data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
- data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
- data/lib/llm_cost_tracker/pricing.rb +10 -295
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
- data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
- data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +181 -0
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
- data/lib/llm_cost_tracker/providers.rb +35 -0
- data/lib/llm_cost_tracker/railtie.rb +0 -7
- data/lib/llm_cost_tracker/report/data.rb +3 -4
- data/lib/llm_cost_tracker/report/formatter.rb +33 -20
- data/lib/llm_cost_tracker/report.rb +1 -1
- data/lib/llm_cost_tracker/retention.rb +6 -19
- data/lib/llm_cost_tracker/tags/context.rb +9 -6
- data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
- data/lib/llm_cost_tracker/timing.rb +2 -4
- data/lib/llm_cost_tracker/tracker.rb +24 -36
- data/lib/llm_cost_tracker/usage/catalog.rb +58 -0
- data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
- data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +24 -46
- data/lib/llm_cost_tracker/usage/source.rb +14 -0
- data/lib/llm_cost_tracker/usage/token_usage.rb +100 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +43 -52
- data/lib/tasks/llm_cost_tracker.rake +14 -73
- metadata +92 -58
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -106
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
- data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -183
- 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/billing/components.rb +0 -95
- data/lib/llm_cost_tracker/capture/stream.rb +0 -9
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
- data/lib/llm_cost_tracker/doctor/check.rb +0 -7
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -32
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -25
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
- data/lib/llm_cost_tracker/masking.rb +0 -39
- data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -193
- data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
- data/lib/llm_cost_tracker/parsers/base.rb +0 -131
- data/lib/llm_cost_tracker/parsers/gemini.rb +0 -232
- data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -51
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -155
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
- data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
- data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
- data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
- data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -134
- data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
- data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -254
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -172
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -142
- data/lib/llm_cost_tracker/reconciliation.rb +0 -118
- data/lib/llm_cost_tracker/token_usage.rb +0 -93
|
@@ -2,31 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
require "bigdecimal"
|
|
4
4
|
|
|
5
|
-
require_relative "logging"
|
|
6
5
|
require_relative "ledger"
|
|
7
6
|
require_relative "pricing/estimator"
|
|
8
7
|
|
|
9
8
|
module LlmCostTracker
|
|
10
|
-
|
|
9
|
+
module Budget
|
|
11
10
|
BUDGET_TYPE_TO_PERIOD = { monthly: :month, daily: :day }.freeze
|
|
12
11
|
|
|
13
12
|
class << self
|
|
14
|
-
def enforce!(provider: nil, model: nil, request: nil)
|
|
13
|
+
def enforce!(provider: nil, model: nil, request: nil, estimate: nil, force: false)
|
|
15
14
|
config = LlmCostTracker.configuration
|
|
16
|
-
return unless config.
|
|
15
|
+
return unless config.enabled
|
|
16
|
+
return unless force || config.budget_exceeded_behavior == :block_requests
|
|
17
17
|
|
|
18
|
-
estimate
|
|
18
|
+
estimate ||= estimate_cost(provider: provider, model: model, request: request)
|
|
19
19
|
raise_per_call_pre_send(estimate, config.per_call_budget) if config.per_call_budget && estimate.positive?
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
totals = totals_for(budgets.keys, time: Time.now.utc)
|
|
25
|
-
|
|
26
|
-
budgets.each do |budget_type, budget|
|
|
27
|
-
total = totals.fetch(budget_type) + estimate
|
|
28
|
-
next unless total >= budget
|
|
29
|
-
|
|
21
|
+
check_windowed({ monthly: config.monthly_budget, daily: config.daily_budget }.compact,
|
|
22
|
+
time: Time.now.utc,
|
|
23
|
+
estimate: estimate) do |budget_type, total, budget|
|
|
30
24
|
raise BudgetExceededError.new(**budget_payload(
|
|
31
25
|
budget_type: budget_type, total: total, budget: budget, last_event: nil, stage: :pre_send
|
|
32
26
|
))
|
|
@@ -38,13 +32,9 @@ module LlmCostTracker
|
|
|
38
32
|
return unless event.total_cost
|
|
39
33
|
|
|
40
34
|
check_per_call_budget(event, config)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
budgets.each do |budget_type, budget|
|
|
45
|
-
total = totals.fetch(budget_type)
|
|
46
|
-
|
|
47
|
-
handle_exceeded(budget_type: budget_type, total: total, budget: budget, last_event: event) if total >= budget
|
|
35
|
+
check_windowed({ daily: config.daily_budget, monthly: config.monthly_budget }.compact,
|
|
36
|
+
time: event.tracked_at) do |budget_type, total, budget|
|
|
37
|
+
handle_exceeded(budget_type: budget_type, total: total, budget: budget, last_event: event)
|
|
48
38
|
end
|
|
49
39
|
end
|
|
50
40
|
|
|
@@ -74,14 +64,22 @@ module LlmCostTracker
|
|
|
74
64
|
handle_exceeded(budget_type: :per_call, total: total, budget: budget, last_event: event)
|
|
75
65
|
end
|
|
76
66
|
|
|
67
|
+
def check_windowed(budgets, time:, estimate: BigDecimal("0"))
|
|
68
|
+
return if budgets.empty?
|
|
69
|
+
|
|
70
|
+
totals = totals_for(budgets.keys, time: time)
|
|
71
|
+
budgets.each do |budget_type, budget|
|
|
72
|
+
total = totals.fetch(budget_type) + estimate
|
|
73
|
+
yield(budget_type, total, budget) if total >= budget
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
77
|
def totals_for(budget_types, time:)
|
|
78
78
|
return {} if budget_types.empty?
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
period_totals = LlmCostTracker::Ledger::Period::Totals.call(
|
|
82
|
-
|
|
83
|
-
totals[budget_type] = period_totals[period] if period_totals.key?(period)
|
|
84
|
-
end
|
|
80
|
+
period_for = budget_types.to_h { |type| [type, BUDGET_TYPE_TO_PERIOD.fetch(type)] }
|
|
81
|
+
period_totals = LlmCostTracker::Ledger::Period::Totals.call(period_for.values, time: time)
|
|
82
|
+
period_for.transform_values { |period| period_totals.fetch(period) }
|
|
85
83
|
end
|
|
86
84
|
|
|
87
85
|
def handle_exceeded(budget_type:, total:, budget:, last_event: nil)
|
|
@@ -112,8 +110,7 @@ module LlmCostTracker
|
|
|
112
110
|
|
|
113
111
|
def notify_exceeded?(config, budget_type:, total:, budget:, last_event:)
|
|
114
112
|
return false unless config.on_budget_exceeded
|
|
115
|
-
return true
|
|
116
|
-
return true if budget_type == :per_call
|
|
113
|
+
return true if !last_event&.total_cost || budget_type == :per_call
|
|
117
114
|
|
|
118
115
|
total - last_event.total_cost < budget
|
|
119
116
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/object/deep_dup"
|
|
4
|
+
require "active_support/core_ext/object/try"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Capture
|
|
8
|
+
module SdkPayload
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def normalize(value)
|
|
12
|
+
case value
|
|
13
|
+
when Hash
|
|
14
|
+
value.each_with_object({}) { |(key, nested), out| out[key.to_s] = normalize(nested) }
|
|
15
|
+
when Array
|
|
16
|
+
value.map { |nested| normalize(nested) }
|
|
17
|
+
when Symbol
|
|
18
|
+
value.to_s
|
|
19
|
+
when NilClass
|
|
20
|
+
nil
|
|
21
|
+
else
|
|
22
|
+
converted = container_for(value)
|
|
23
|
+
converted ? normalize(converted) : value.deep_dup
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def container_for(value)
|
|
28
|
+
value.try(:deep_to_h) || value.try(:to_h)
|
|
29
|
+
rescue StandardError
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -4,8 +4,7 @@ require "active_support/core_ext/object/blank"
|
|
|
4
4
|
require "active_support/core_ext/object/deep_dup"
|
|
5
5
|
require "json"
|
|
6
6
|
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "../pricing/mode"
|
|
7
|
+
require_relative "sse"
|
|
9
8
|
require_relative "../timing"
|
|
10
9
|
|
|
11
10
|
module LlmCostTracker
|
|
@@ -13,9 +12,17 @@ module LlmCostTracker
|
|
|
13
12
|
class StreamCollector
|
|
14
13
|
attr_reader :provider
|
|
15
14
|
|
|
16
|
-
def initialize(provider:,
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
def initialize(provider:,
|
|
16
|
+
model:,
|
|
17
|
+
latency_ms: nil,
|
|
18
|
+
provider_response_id: nil,
|
|
19
|
+
provider_project_id: nil,
|
|
20
|
+
provider_api_key_id: nil,
|
|
21
|
+
provider_workspace_id: nil,
|
|
22
|
+
pricing_mode: nil,
|
|
23
|
+
metadata: {},
|
|
24
|
+
context_tags: nil,
|
|
25
|
+
request: nil)
|
|
19
26
|
@provider = provider.to_s
|
|
20
27
|
@model = model
|
|
21
28
|
@latency_ms = latency_ms
|
|
@@ -23,7 +30,6 @@ module LlmCostTracker
|
|
|
23
30
|
@provider_project_id = provider_project_id
|
|
24
31
|
@provider_api_key_id = provider_api_key_id
|
|
25
32
|
@provider_workspace_id = provider_workspace_id
|
|
26
|
-
@batch = batch
|
|
27
33
|
@pricing_mode = pricing_mode
|
|
28
34
|
@metadata = (metadata || {}).deep_dup
|
|
29
35
|
@context_tags = (context_tags || LlmCostTracker::Tags::Context.tags).deep_dup
|
|
@@ -38,18 +44,6 @@ module LlmCostTracker
|
|
|
38
44
|
@mutex = Mutex.new
|
|
39
45
|
end
|
|
40
46
|
|
|
41
|
-
def model
|
|
42
|
-
@mutex.synchronize { @model }
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def metadata
|
|
46
|
-
@mutex.synchronize { @metadata.deep_dup }
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def provider_response_id
|
|
50
|
-
@mutex.synchronize { @provider_response_id }
|
|
51
|
-
end
|
|
52
|
-
|
|
53
47
|
def model=(value)
|
|
54
48
|
@mutex.synchronize do
|
|
55
49
|
ensure_open!
|
|
@@ -72,16 +66,20 @@ module LlmCostTracker
|
|
|
72
66
|
end
|
|
73
67
|
|
|
74
68
|
def usage(input_tokens:, output_tokens:, **extra)
|
|
69
|
+
if extra.key?(:batch)
|
|
70
|
+
raise ArgumentError,
|
|
71
|
+
"`batch:` is no longer accepted by stream.usage; " \
|
|
72
|
+
"pass `pricing_mode: :batch` to track_stream"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
75
|
@mutex.synchronize do
|
|
76
76
|
ensure_open!
|
|
77
77
|
@provider_response_id = extra.delete(:provider_response_id) || @provider_response_id
|
|
78
78
|
@provider_project_id = extra.delete(:provider_project_id) || @provider_project_id
|
|
79
79
|
@provider_api_key_id = extra.delete(:provider_api_key_id) || @provider_api_key_id
|
|
80
80
|
@provider_workspace_id = extra.delete(:provider_workspace_id) || @provider_workspace_id
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@explicit_usage = TokenUsage.build(
|
|
84
|
-
**extra.slice(*TokenUsage.members),
|
|
81
|
+
@explicit_usage = Usage::TokenUsage.build(
|
|
82
|
+
**extra.slice(*Usage::TokenUsage.members),
|
|
85
83
|
input_tokens: input_tokens,
|
|
86
84
|
output_tokens: output_tokens
|
|
87
85
|
)
|
|
@@ -102,7 +100,7 @@ module LlmCostTracker
|
|
|
102
100
|
return nil if @finished || @recording
|
|
103
101
|
|
|
104
102
|
@recording = true
|
|
105
|
-
pricing_mode = Pricing.
|
|
103
|
+
pricing_mode = Pricing::Mode.normalize(@pricing_mode)
|
|
106
104
|
{
|
|
107
105
|
events: @events.dup,
|
|
108
106
|
overflowed: @overflowed,
|
|
@@ -110,7 +108,7 @@ module LlmCostTracker
|
|
|
110
108
|
model: @model,
|
|
111
109
|
latency_ms: @latency_ms,
|
|
112
110
|
provider_response_id: @provider_response_id,
|
|
113
|
-
capture_dimensions: capture_dimensions
|
|
111
|
+
capture_dimensions: capture_dimensions,
|
|
114
112
|
pricing_mode: pricing_mode,
|
|
115
113
|
metadata: @metadata.deep_dup,
|
|
116
114
|
context_tags: @context_tags.deep_dup,
|
|
@@ -129,7 +127,7 @@ module LlmCostTracker
|
|
|
129
127
|
Tracker.record(
|
|
130
128
|
event: event,
|
|
131
129
|
latency_ms: snapshot[:latency_ms] || LlmCostTracker::Timing.elapsed_ms(@started_at),
|
|
132
|
-
pricing_mode:
|
|
130
|
+
pricing_mode: Pricing::Mode.merge(event.pricing_mode, snapshot[:pricing_mode]),
|
|
133
131
|
metadata: (errored ? { stream_errored: true } : {}).merge(snapshot[:metadata]),
|
|
134
132
|
context_tags: snapshot[:context_tags]
|
|
135
133
|
) { save_succeeded = true }
|
|
@@ -141,27 +139,11 @@ module LlmCostTracker
|
|
|
141
139
|
end
|
|
142
140
|
end
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
private_constant :HOST_DERIVED_MODE_TOKENS
|
|
146
|
-
|
|
147
|
-
def merge_pricing_modes(provider_mode, request_mode)
|
|
148
|
-
return Pricing.normalize_mode(request_mode) if provider_mode.to_s.strip.empty?
|
|
149
|
-
|
|
150
|
-
provider_tokens = Pricing::Mode.tokenize(provider_mode) - Pricing::STANDARD_MODE_VALUES
|
|
151
|
-
request_host_tokens = Pricing::Mode.tokenize(request_mode || "") & HOST_DERIVED_MODE_TOKENS
|
|
152
|
-
combined = provider_tokens | request_host_tokens
|
|
153
|
-
return nil if combined.empty?
|
|
154
|
-
|
|
155
|
-
Pricing.normalize_mode(combined.join("_"))
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def capture_dimensions(pricing_mode)
|
|
159
|
-
batch = @batch.nil? ? Event.batch_from_pricing_mode?(pricing_mode).presence : @batch
|
|
142
|
+
def capture_dimensions
|
|
160
143
|
{
|
|
161
144
|
provider_project_id: @provider_project_id.to_s.strip.presence,
|
|
162
145
|
provider_api_key_id: @provider_api_key_id.to_s.strip.presence,
|
|
163
|
-
provider_workspace_id: @provider_workspace_id.to_s.strip.presence
|
|
164
|
-
batch: batch
|
|
146
|
+
provider_workspace_id: @provider_workspace_id.to_s.strip.presence
|
|
165
147
|
}.compact
|
|
166
148
|
end
|
|
167
149
|
|
|
@@ -197,12 +179,8 @@ module LlmCostTracker
|
|
|
197
179
|
end
|
|
198
180
|
|
|
199
181
|
def present_model(value)
|
|
200
|
-
return nil if value.nil?
|
|
201
|
-
|
|
202
182
|
string = value.to_s.presence
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
string
|
|
183
|
+
string unless string == Event::UNKNOWN_MODEL
|
|
206
184
|
end
|
|
207
185
|
|
|
208
186
|
def build_from_explicit_usage(snapshot)
|
|
@@ -211,7 +189,7 @@ module LlmCostTracker
|
|
|
211
189
|
model: snapshot[:model] || Event::UNKNOWN_MODEL,
|
|
212
190
|
token_usage: snapshot[:explicit_usage],
|
|
213
191
|
stream: true,
|
|
214
|
-
usage_source:
|
|
192
|
+
usage_source: Usage::Source::MANUAL,
|
|
215
193
|
pricing_mode: snapshot[:pricing_mode],
|
|
216
194
|
**snapshot.fetch(:capture_dimensions)
|
|
217
195
|
)
|
|
@@ -221,9 +199,9 @@ module LlmCostTracker
|
|
|
221
199
|
Event.build(
|
|
222
200
|
provider: @provider,
|
|
223
201
|
model: snapshot[:model] || Event::UNKNOWN_MODEL,
|
|
224
|
-
token_usage: TokenUsage.build(input_tokens: 0, output_tokens: 0, total_tokens: 0),
|
|
202
|
+
token_usage: Usage::TokenUsage.build(input_tokens: 0, output_tokens: 0, total_tokens: 0),
|
|
225
203
|
stream: true,
|
|
226
|
-
usage_source:
|
|
204
|
+
usage_source: Usage::Source::UNKNOWN,
|
|
227
205
|
pricing_mode: snapshot[:pricing_mode],
|
|
228
206
|
**snapshot.fetch(:capture_dimensions)
|
|
229
207
|
)
|
|
@@ -238,7 +216,7 @@ module LlmCostTracker
|
|
|
238
216
|
def capture_event(data, type:)
|
|
239
217
|
event = { event: type, data: strip_heavy_payload(data) }
|
|
240
218
|
size = approximate_bytesize(event)
|
|
241
|
-
if @captured_bytes + size <= Capture::
|
|
219
|
+
if @captured_bytes + size <= Capture::SSE::LIMIT_BYTES
|
|
242
220
|
@events << event
|
|
243
221
|
@captured_bytes += size
|
|
244
222
|
else
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "active_support/core_ext/object/deep_dup"
|
|
4
4
|
require "active_support/core_ext/object/try"
|
|
5
5
|
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "sdk_payload"
|
|
7
7
|
|
|
8
8
|
module LlmCostTracker
|
|
9
9
|
module Capture
|
|
@@ -26,13 +26,24 @@ module LlmCostTracker
|
|
|
26
26
|
if @stream.instance_variable_defined?(:@iterator)
|
|
27
27
|
iterator = @stream.instance_variable_get(:@iterator)
|
|
28
28
|
if iterator.respond_to?(:each)
|
|
29
|
-
@stream.instance_variable_set(:@iterator,
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
@stream.instance_variable_set(:@iterator,
|
|
30
|
+
Enumerator.new do |yielder|
|
|
31
|
+
each_from(iterator) { |event| yielder << event }
|
|
32
|
+
end)
|
|
32
33
|
iterator_wrapped = true
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
|
-
|
|
36
|
+
each_wrapped = false
|
|
37
|
+
if !iterator_wrapped && @stream.respond_to?(:each)
|
|
38
|
+
wrap_each
|
|
39
|
+
each_wrapped = true
|
|
40
|
+
end
|
|
41
|
+
unless iterator_wrapped || each_wrapped
|
|
42
|
+
Logging.warn(
|
|
43
|
+
"stream integration found no wrappable iterator on #{@stream.class} " \
|
|
44
|
+
"(missing both `@iterator` ivar and `#each`); usage will not be captured"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
36
47
|
|
|
37
48
|
register_orphan_finalizer
|
|
38
49
|
@stream
|
|
@@ -74,40 +85,14 @@ module LlmCostTracker
|
|
|
74
85
|
end
|
|
75
86
|
|
|
76
87
|
def capture(event)
|
|
77
|
-
raw_payload = event.try(:deep_to_h) || event.try(:to_h)
|
|
78
|
-
|
|
79
|
-
value = event.try(key)
|
|
80
|
-
attributes[key] = value unless value.nil?
|
|
81
|
-
end
|
|
82
|
-
payload = normalize(raw_payload)
|
|
88
|
+
raw_payload = event.try(:deep_to_h) || event.try(:to_h) || {}
|
|
89
|
+
payload = SdkPayload.normalize(raw_payload)
|
|
83
90
|
type = event.try(:type) || payload["type"]
|
|
84
91
|
@collector.event(payload, type: type&.to_s)
|
|
85
92
|
rescue StandardError => e
|
|
86
93
|
warn_capture_failure(e)
|
|
87
94
|
end
|
|
88
95
|
|
|
89
|
-
def normalize(value)
|
|
90
|
-
case value
|
|
91
|
-
when Hash
|
|
92
|
-
value.each_with_object({}) do |(key, nested), normalized|
|
|
93
|
-
normalized[key.to_s] = normalize(nested)
|
|
94
|
-
end
|
|
95
|
-
when Array
|
|
96
|
-
value.map { |nested| normalize(nested) }
|
|
97
|
-
when Symbol
|
|
98
|
-
value.to_s
|
|
99
|
-
when NilClass
|
|
100
|
-
nil
|
|
101
|
-
else
|
|
102
|
-
converted = begin
|
|
103
|
-
value.try(:deep_to_h) || value.try(:to_h)
|
|
104
|
-
rescue StandardError
|
|
105
|
-
nil
|
|
106
|
-
end
|
|
107
|
-
converted ? normalize(converted) : value.deep_dup
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
96
|
def warn_capture_failure(error)
|
|
112
97
|
should_warn = @mutex.synchronize do
|
|
113
98
|
next false if @capture_failed
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "check"
|
|
4
|
+
require_relative "ingestion"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
class CaptureVerifier
|
|
8
|
+
class << self
|
|
9
|
+
def call
|
|
10
|
+
new.checks
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def report(checks = call)
|
|
14
|
+
(["LLM Cost Tracker capture verification"] + checks.map do |check|
|
|
15
|
+
"[#{check.status}] #{check.name}: #{check.message}"
|
|
16
|
+
end).join("\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def healthy?(checks = call)
|
|
20
|
+
checks.none? { |check| check.status == :error }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def checks
|
|
25
|
+
[
|
|
26
|
+
enabled_check,
|
|
27
|
+
*integration_checks,
|
|
28
|
+
*storage_checks
|
|
29
|
+
].compact
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def enabled_check
|
|
35
|
+
return Check.new(:ok, "tracking", "enabled") if LlmCostTracker.configuration.enabled
|
|
36
|
+
|
|
37
|
+
Check.new(:error, "tracking", "disabled; set config.enabled = true before verifying capture")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def integration_checks
|
|
41
|
+
enabled = LlmCostTracker.configuration.instrumented_integrations
|
|
42
|
+
if enabled.empty?
|
|
43
|
+
return [
|
|
44
|
+
Check.new(:ok, "sdk integrations", "none enabled; Faraday middleware and manual capture remain available")
|
|
45
|
+
]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
LlmCostTracker::Integrations.checks.map do |check|
|
|
49
|
+
check.with(name: "sdk integration #{check.name}")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def storage_checks
|
|
54
|
+
LlmCostTracker::Ingestion.verify
|
|
55
|
+
rescue LlmCostTracker::Error => e
|
|
56
|
+
[Check.new(:error, "storage", e.message)]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module LlmCostTracker
|
|
6
|
+
module Charges
|
|
7
|
+
Cost = Data.define(:components, :total, :currency) do
|
|
8
|
+
def self.from_h(attributes)
|
|
9
|
+
components = attributes.key?(:components) ? attributes[:components] : attributes.except(:total_cost, :currency)
|
|
10
|
+
total = attributes.fetch(:total) { attributes[:total_cost] }
|
|
11
|
+
new(
|
|
12
|
+
components: components.transform_values { |value| BigDecimal(value.to_s) }.freeze,
|
|
13
|
+
total: total && BigDecimal(total.to_s),
|
|
14
|
+
currency: attributes[:currency]
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
components: components.transform_values { |value| value.to_s("F") },
|
|
21
|
+
total: total&.to_s("F"),
|
|
22
|
+
currency: currency
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "../usage/source"
|
|
4
4
|
|
|
5
5
|
module LlmCostTracker
|
|
6
|
-
module
|
|
6
|
+
module Charges
|
|
7
7
|
module CostStatus
|
|
8
8
|
COMPLETE = "complete"
|
|
9
9
|
FREE = "free"
|
|
10
10
|
PARTIAL = "partial"
|
|
11
11
|
UNKNOWN = "unknown"
|
|
12
|
+
INCOMPLETE = [UNKNOWN, PARTIAL].freeze
|
|
13
|
+
|
|
14
|
+
def self.unknown_pricing_sql(total_cost: "total_cost", cost_status: "cost_status")
|
|
15
|
+
statuses = INCOMPLETE.map { |status| ActiveRecord::Base.connection.quote(status) }.join(", ")
|
|
16
|
+
"#{total_cost} IS NULL OR #{cost_status} IN (#{statuses})"
|
|
17
|
+
end
|
|
12
18
|
|
|
13
19
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
14
|
-
def self.call(token_usage:,
|
|
20
|
+
def self.call(token_usage:,
|
|
21
|
+
usage_source:,
|
|
22
|
+
token_cost:,
|
|
23
|
+
service_line_items:,
|
|
24
|
+
total_cost:,
|
|
15
25
|
token_pricing_partial: false)
|
|
16
|
-
return UNKNOWN if usage_source ==
|
|
26
|
+
return UNKNOWN if usage_source == Usage::Source::UNKNOWN
|
|
17
27
|
|
|
18
28
|
token_billable = token_usage.priced_quantities.any? { |_key, quantity| quantity.positive? }
|
|
19
29
|
service_billable = false
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "bigdecimal"
|
|
4
4
|
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "../currency"
|
|
6
|
+
require_relative "../usage/catalog"
|
|
6
7
|
require_relative "cost_status"
|
|
7
8
|
|
|
8
9
|
module LlmCostTracker
|
|
9
|
-
module
|
|
10
|
+
module Charges
|
|
10
11
|
LineItem = Data.define(
|
|
11
12
|
:kind,
|
|
12
13
|
:direction,
|
|
@@ -29,26 +30,24 @@ module LlmCostTracker
|
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
class LineItem
|
|
32
|
-
USD = "USD"
|
|
33
|
-
|
|
34
33
|
def self.build(attributes)
|
|
35
34
|
attributes = attributes.to_h
|
|
36
|
-
|
|
35
|
+
dimension = dimension_for(attributes)
|
|
37
36
|
new(
|
|
38
|
-
kind:
|
|
39
|
-
direction:
|
|
40
|
-
modality:
|
|
41
|
-
cache_state:
|
|
42
|
-
quantity:
|
|
43
|
-
unit:
|
|
37
|
+
kind: attributes[:kind]&.to_s || dimension&.kind,
|
|
38
|
+
direction: attributes[:direction]&.to_s || dimension&.direction,
|
|
39
|
+
modality: attributes[:modality]&.to_s || dimension&.modality,
|
|
40
|
+
cache_state: attributes[:cache_state]&.to_s || dimension&.cache_state || "none",
|
|
41
|
+
quantity: decimal_or_nil(attributes[:quantity]) || BigDecimal("0"),
|
|
42
|
+
unit: attributes[:unit]&.to_s || dimension&.unit,
|
|
44
43
|
rate_amount: decimal_or_nil(attributes[:rate_amount]),
|
|
45
44
|
rate_quantity: decimal_or_nil(attributes[:rate_quantity]) || BigDecimal("1"),
|
|
46
45
|
cost: decimal_or_nil(attributes[:cost]),
|
|
47
|
-
currency: attributes[:currency]
|
|
46
|
+
currency: canonical_currency(attributes[:currency]),
|
|
48
47
|
cost_status: cost_status_for(attributes),
|
|
49
|
-
pricing_basis:
|
|
50
|
-
price_key: attributes[:price_key],
|
|
51
|
-
price_source:
|
|
48
|
+
pricing_basis: attributes[:pricing_basis]&.to_s,
|
|
49
|
+
price_key: attributes[:price_key]&.to_s,
|
|
50
|
+
price_source: attributes[:price_source]&.to_s,
|
|
52
51
|
price_source_version: attributes[:price_source_version],
|
|
53
52
|
provider_field: attributes[:provider_field],
|
|
54
53
|
provider_item_id: attributes[:provider_item_id],
|
|
@@ -62,14 +61,14 @@ module LlmCostTracker
|
|
|
62
61
|
token_usage.priced_quantities.filter_map do |key, quantity|
|
|
63
62
|
next unless quantity.positive?
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
dimension = Usage::Catalog.fetch(key)
|
|
66
65
|
build(
|
|
67
|
-
kind:
|
|
68
|
-
direction:
|
|
69
|
-
modality:
|
|
70
|
-
cache_state:
|
|
66
|
+
kind: dimension.kind,
|
|
67
|
+
direction: dimension.direction,
|
|
68
|
+
modality: dimension.modality,
|
|
69
|
+
cache_state: dimension.cache_state,
|
|
71
70
|
quantity: quantity,
|
|
72
|
-
unit:
|
|
71
|
+
unit: dimension.unit
|
|
73
72
|
)
|
|
74
73
|
end
|
|
75
74
|
end
|
|
@@ -84,17 +83,11 @@ module LlmCostTracker
|
|
|
84
83
|
cost.zero? ? CostStatus::FREE : CostStatus::COMPLETE
|
|
85
84
|
end
|
|
86
85
|
|
|
87
|
-
def self.
|
|
88
|
-
|
|
89
|
-
return nil unless
|
|
90
|
-
|
|
91
|
-
Components::BY_KEY[component_key.to_sym]
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def self.symbol_or_nil(value)
|
|
95
|
-
return nil if value.nil?
|
|
86
|
+
def self.dimension_for(attributes)
|
|
87
|
+
dimension_key = attributes[:dimension_key] || attributes[:price_key]
|
|
88
|
+
return nil unless dimension_key
|
|
96
89
|
|
|
97
|
-
|
|
90
|
+
Usage::Catalog[dimension_key.to_s]
|
|
98
91
|
end
|
|
99
92
|
|
|
100
93
|
def self.decimal_or_nil(value)
|
|
@@ -103,11 +96,11 @@ module LlmCostTracker
|
|
|
103
96
|
BigDecimal(value.to_s)
|
|
104
97
|
end
|
|
105
98
|
|
|
106
|
-
def self.
|
|
107
|
-
|
|
99
|
+
def self.canonical_currency(value)
|
|
100
|
+
(value || LlmCostTracker::DEFAULT_CURRENCY).to_s.upcase
|
|
108
101
|
end
|
|
109
102
|
|
|
110
|
-
private_class_method :cost_status_for, :
|
|
103
|
+
private_class_method :cost_status_for, :dimension_for, :decimal_or_nil, :canonical_currency
|
|
111
104
|
|
|
112
105
|
def billable?
|
|
113
106
|
quantity.positive?
|
|
@@ -122,7 +115,12 @@ module LlmCostTracker
|
|
|
122
115
|
end
|
|
123
116
|
|
|
124
117
|
def token?
|
|
125
|
-
unit ==
|
|
118
|
+
unit == "token"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def dimension
|
|
122
|
+
Usage::Catalog[price_key] ||
|
|
123
|
+
Usage::Catalog.token_priced_for(kind: kind, direction: direction, cache_state: cache_state)
|
|
126
124
|
end
|
|
127
125
|
|
|
128
126
|
def cost_value
|
|
@@ -130,18 +128,16 @@ module LlmCostTracker
|
|
|
130
128
|
end
|
|
131
129
|
|
|
132
130
|
def with_rate(rate)
|
|
133
|
-
|
|
134
|
-
rate_quantity = rate.fetch(:quantity)
|
|
135
|
-
applied_cost = (quantity / rate_quantity) * rate_amount
|
|
131
|
+
applied_cost = (quantity / rate.quantity) * rate.amount
|
|
136
132
|
with(
|
|
137
|
-
rate_amount:
|
|
138
|
-
rate_quantity:
|
|
133
|
+
rate_amount: rate.amount,
|
|
134
|
+
rate_quantity: rate.quantity,
|
|
139
135
|
cost: applied_cost,
|
|
140
|
-
currency: rate.
|
|
136
|
+
currency: rate.currency.upcase,
|
|
141
137
|
cost_status: applied_cost.zero? ? CostStatus::FREE : CostStatus::COMPLETE,
|
|
142
|
-
price_key: rate.
|
|
143
|
-
price_source: rate.
|
|
144
|
-
price_source_version: rate.
|
|
138
|
+
price_key: rate.source_key,
|
|
139
|
+
price_source: rate.source,
|
|
140
|
+
price_source_version: rate.source_version
|
|
145
141
|
)
|
|
146
142
|
end
|
|
147
143
|
|