llm_cost_tracker 0.9.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 +55 -0
- data/README.md +6 -2
- data/app/assets/llm_cost_tracker/application.css +782 -801
- data/app/controllers/llm_cost_tracker/application_controller.rb +15 -3
- data/app/controllers/llm_cost_tracker/calls_controller.rb +39 -20
- 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 +16 -4
- 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/models/llm_cost_tracker/provider_invoice_import.rb +9 -4
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +95 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +104 -0
- 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 +119 -120
- 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/billing/cost_status.rb +21 -25
- data/lib/llm_cost_tracker/billing/line_item.rb +15 -49
- data/lib/llm_cost_tracker/budget.rb +29 -8
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +1 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +34 -42
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +2 -6
- data/lib/llm_cost_tracker/configuration.rb +30 -44
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +1 -1
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +8 -8
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -2
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -2
- data/lib/llm_cost_tracker/doctor.rb +80 -25
- data/lib/llm_cost_tracker/engine.rb +1 -2
- data/lib/llm_cost_tracker/errors.rb +3 -2
- data/lib/llm_cost_tracker/event.rb +47 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/{durable_ingestion_generator.rb → async_ingestion_generator.rb} +8 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -23
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/{create_llm_cost_tracker_durable_ingestion.rb.erb → create_llm_cost_tracker_async_ingestion.rb.erb} +3 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +6 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -7
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +27 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +5 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +36 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +27 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +31 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +31 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +5 -2
- data/lib/llm_cost_tracker/ingestion/inbox.rb +4 -25
- data/lib/llm_cost_tracker/ingestion/pool.rb +44 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +22 -36
- data/lib/llm_cost_tracker/ingestion.rb +8 -9
- data/lib/llm_cost_tracker/integrations/anthropic.rb +46 -68
- data/lib/llm_cost_tracker/integrations/base.rb +14 -11
- data/lib/llm_cost_tracker/integrations/openai.rb +104 -131
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +27 -73
- data/lib/llm_cost_tracker/integrations.rb +14 -13
- data/lib/llm_cost_tracker/ledger/period/totals.rb +5 -3
- data/lib/llm_cost_tracker/ledger/rollups.rb +4 -13
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +11 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +11 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +0 -4
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +19 -9
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +26 -11
- data/lib/llm_cost_tracker/ledger/store.rb +21 -18
- data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -1
- data/lib/llm_cost_tracker/ledger.rb +13 -0
- data/lib/llm_cost_tracker/logging.rb +0 -4
- data/lib/llm_cost_tracker/middleware/faraday.rb +46 -17
- data/lib/llm_cost_tracker/parsers/anthropic.rb +35 -59
- data/lib/llm_cost_tracker/parsers/azure.rb +46 -0
- data/lib/llm_cost_tracker/parsers/base.rb +53 -47
- data/lib/llm_cost_tracker/parsers/gemini.rb +23 -27
- data/lib/llm_cost_tracker/parsers/openai.rb +8 -40
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +26 -49
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +19 -23
- data/lib/llm_cost_tracker/parsers.rb +29 -4
- data/lib/llm_cost_tracker/prices.json +567 -579
- data/lib/llm_cost_tracker/pricing/backfill.rb +140 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +2 -4
- data/lib/llm_cost_tracker/pricing/estimator.rb +33 -0
- data/lib/llm_cost_tracker/pricing/explainer.rb +5 -2
- data/lib/llm_cost_tracker/pricing/lookup.rb +37 -2
- data/lib/llm_cost_tracker/pricing/mode.rb +34 -4
- data/lib/llm_cost_tracker/pricing/registry.rb +0 -7
- data/lib/llm_cost_tracker/pricing/service_charges.rb +6 -10
- data/lib/llm_cost_tracker/pricing/{sync_change_printer.rb → sync/change_printer.rb} +3 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +14 -2
- data/lib/llm_cost_tracker/pricing/sync.rb +1 -9
- data/lib/llm_cost_tracker/pricing/unknown.rb +5 -2
- data/lib/llm_cost_tracker/pricing.rb +71 -43
- data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +15 -0
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +22 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +17 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +17 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +35 -0
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +51 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +157 -0
- data/lib/llm_cost_tracker/railtie.rb +3 -5
- data/lib/llm_cost_tracker/reconcile_tasks.rb +18 -21
- data/lib/llm_cost_tracker/reconciliation/diff.rb +26 -45
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -4
- data/lib/llm_cost_tracker/reconciliation/importer.rb +3 -7
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +10 -33
- 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/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +20 -8
- data/lib/llm_cost_tracker/tags/sanitizer.rb +13 -17
- data/lib/llm_cost_tracker/token_usage.rb +4 -0
- data/lib/llm_cost_tracker/tracker.rb +33 -74
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +11 -15
- data/lib/tasks/llm_cost_tracker.rake +16 -2
- metadata +31 -12
- 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/dashboard_setup_state.rb +0 -109
- data/lib/llm_cost_tracker/ingestion/inline.rb +0 -22
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -126
- data/lib/llm_cost_tracker/usage_capture.rb +0 -58
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "active_support/core_ext/object/blank"
|
|
4
|
-
require "bigdecimal"
|
|
5
4
|
require "securerandom"
|
|
6
5
|
|
|
7
6
|
require_relative "ingestion"
|
|
@@ -15,28 +14,30 @@ module LlmCostTracker
|
|
|
15
14
|
EVENT_NAME = "llm_request.llm_cost_tracker"
|
|
16
15
|
|
|
17
16
|
class << self
|
|
18
|
-
def enforce_budget!
|
|
17
|
+
def enforce_budget!(provider: nil, model: nil, request: nil)
|
|
19
18
|
return unless LlmCostTracker.configuration.enabled
|
|
20
19
|
|
|
21
|
-
Budget.enforce!
|
|
20
|
+
Budget.enforce!(provider: provider, model: model, request: request)
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
def record(
|
|
23
|
+
def record(event:, latency_ms: nil, pricing_mode: nil, metadata: {}, context_tags: nil)
|
|
25
24
|
return unless LlmCostTracker.configuration.enabled
|
|
26
25
|
|
|
27
|
-
pricing_mode = Pricing.
|
|
26
|
+
pricing_mode = Pricing::Mode.normalize(pricing_mode) || event.pricing_mode
|
|
28
27
|
cost_data, pricing_snapshot, priced_line_items = Pricing.calculate(
|
|
29
|
-
provider:
|
|
30
|
-
model:
|
|
31
|
-
tokens:
|
|
32
|
-
line_items:
|
|
28
|
+
provider: event.provider,
|
|
29
|
+
model: event.model,
|
|
30
|
+
tokens: event.token_usage,
|
|
31
|
+
line_items: event.line_items,
|
|
33
32
|
pricing_mode: pricing_mode
|
|
34
33
|
)
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
if cost_data.nil? && event.token_usage.total_tokens.positive? && priced_line_items.none?(&:priced?)
|
|
36
|
+
Pricing::Unknown.process(event.model)
|
|
37
|
+
end
|
|
37
38
|
|
|
38
39
|
event = build_event(
|
|
39
|
-
|
|
40
|
+
event: event,
|
|
40
41
|
pricing_mode: pricing_mode,
|
|
41
42
|
cost_data: cost_data,
|
|
42
43
|
pricing_snapshot: pricing_snapshot,
|
|
@@ -46,21 +47,21 @@ module LlmCostTracker
|
|
|
46
47
|
context_tags: context_tags
|
|
47
48
|
)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
if Ingestion.async?
|
|
51
|
+
Ingestion::Inbox.save(event)
|
|
52
|
+
Ingestion::Worker.ensure_started
|
|
53
|
+
else
|
|
54
|
+
Ledger::Store.insert(event, skip_existence_check: true)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
yield if block_given?
|
|
51
58
|
notify_subscribers(event)
|
|
52
59
|
Budget.check!(event)
|
|
53
60
|
|
|
54
61
|
event
|
|
55
62
|
end
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
if LlmCostTracker.configuration.durable_ingestion
|
|
59
|
-
Ingestion::Inbox.save(event)
|
|
60
|
-
else
|
|
61
|
-
Ingestion::Inline.save(event)
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
+
private
|
|
64
65
|
|
|
65
66
|
def notify_subscribers(event)
|
|
66
67
|
return unless ActiveSupport::Notifications.notifier.listening?(EVENT_NAME)
|
|
@@ -70,45 +71,25 @@ module LlmCostTracker
|
|
|
70
71
|
Logging.warn("Subscriber raised on #{EVENT_NAME}: #{e.class}: #{e.message}")
|
|
71
72
|
end
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def token_pricing_partial?(token_usage:, cost_data:)
|
|
76
|
-
return false unless cost_data
|
|
77
|
-
|
|
78
|
-
Billing::Components::TOKEN_PRICED.any? do |component|
|
|
79
|
-
token_usage.public_send(component.token_key).positive? && cost_data[component.cost_key].nil?
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def build_event(capture:, pricing_mode:, cost_data:, pricing_snapshot:, line_items:,
|
|
74
|
+
def build_event(event:, pricing_mode:, cost_data:, pricing_snapshot:, line_items:,
|
|
84
75
|
metadata:, latency_ms:, context_tags:)
|
|
85
76
|
context_tags = (context_tags || LlmCostTracker::Tags::Context.tags).to_h
|
|
86
|
-
cost =
|
|
77
|
+
cost = Pricing.combine_with_service_lines(cost_data, line_items)
|
|
87
78
|
cost_status = Billing::CostStatus.call(
|
|
88
|
-
token_usage:
|
|
89
|
-
usage_source:
|
|
79
|
+
token_usage: event.token_usage,
|
|
80
|
+
usage_source: event.usage_source,
|
|
90
81
|
token_cost: cost_data,
|
|
91
|
-
token_pricing_partial: token_pricing_partial?(
|
|
82
|
+
token_pricing_partial: Pricing.token_pricing_partial?(event.token_usage, cost_data),
|
|
92
83
|
service_line_items: line_items.reject(&:token?),
|
|
93
84
|
total_cost: cost&.fetch(:total_cost, nil)
|
|
94
85
|
)
|
|
95
86
|
|
|
96
|
-
|
|
87
|
+
event.with(
|
|
97
88
|
event_id: SecureRandom.uuid,
|
|
98
|
-
provider: capture.provider,
|
|
99
|
-
model: capture.model,
|
|
100
|
-
token_usage: capture.token_usage,
|
|
101
89
|
pricing_mode: pricing_mode,
|
|
102
90
|
cost: cost,
|
|
103
|
-
tags:
|
|
91
|
+
tags: build_tags(context_tags: context_tags, metadata: metadata),
|
|
104
92
|
latency_ms: finite_latency_ms(latency_ms),
|
|
105
|
-
stream: capture.stream,
|
|
106
|
-
usage_source: capture.usage_source,
|
|
107
|
-
provider_response_id: capture.provider_response_id,
|
|
108
|
-
provider_project_id: capture.provider_project_id,
|
|
109
|
-
provider_api_key_id: capture.provider_api_key_id,
|
|
110
|
-
provider_workspace_id: capture.provider_workspace_id,
|
|
111
|
-
batch: capture.batch,
|
|
112
93
|
tracked_at: Time.now.utc,
|
|
113
94
|
cost_status: cost_status,
|
|
114
95
|
pricing_snapshot: pricing_snapshot,
|
|
@@ -116,6 +97,11 @@ module LlmCostTracker
|
|
|
116
97
|
)
|
|
117
98
|
end
|
|
118
99
|
|
|
100
|
+
def build_tags(context_tags:, metadata:)
|
|
101
|
+
sanitized_metadata = LlmCostTracker::Tags::Sanitizer.call(metadata.to_h)
|
|
102
|
+
LlmCostTracker::Tags::Sanitizer.cap(context_tags.merge(sanitized_metadata)).freeze
|
|
103
|
+
end
|
|
104
|
+
|
|
119
105
|
def finite_latency_ms(latency_ms)
|
|
120
106
|
return nil if latency_ms.nil?
|
|
121
107
|
|
|
@@ -123,33 +109,6 @@ module LlmCostTracker
|
|
|
123
109
|
rescue ArgumentError, TypeError, FloatDomainError
|
|
124
110
|
nil
|
|
125
111
|
end
|
|
126
|
-
|
|
127
|
-
def cost_with_service_lines(cost_data, line_items)
|
|
128
|
-
priced_services = line_items.reject(&:token?).select(&:priced?)
|
|
129
|
-
return cost_data if priced_services.empty?
|
|
130
|
-
|
|
131
|
-
base_currency = (cost_data && cost_data[:currency]) || Billing::LineItem::USD
|
|
132
|
-
matching, mismatched = priced_services.partition { |line| line.currency.to_s == base_currency.to_s }
|
|
133
|
-
warn_currency_mismatch(mismatched, base_currency) if mismatched.any?
|
|
134
|
-
|
|
135
|
-
cost = cost_data ? cost_data.dup : {}
|
|
136
|
-
cost[:currency] ||= base_currency.to_s
|
|
137
|
-
return cost if matching.empty?
|
|
138
|
-
|
|
139
|
-
service_total = matching.sum(BigDecimal("0"), &:cost_value)
|
|
140
|
-
base_total = BigDecimal(cost.fetch(:total_cost, 0).to_s)
|
|
141
|
-
cost[:total_cost] = (base_total + service_total).round(8)
|
|
142
|
-
cost
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def warn_currency_mismatch(lines, base_currency)
|
|
146
|
-
currencies = lines.map { |line| line.currency.to_s }.uniq.sort
|
|
147
|
-
Logging.warn(
|
|
148
|
-
"Service line currency mismatch: header is #{base_currency}, dropping " \
|
|
149
|
-
"#{lines.size} priced line(s) in #{currencies.join(', ')} from header total. " \
|
|
150
|
-
"Per-line costs are still recorded; header total reflects #{base_currency} only."
|
|
151
|
-
)
|
|
152
|
-
end
|
|
153
112
|
end
|
|
154
113
|
end
|
|
155
114
|
end
|
data/lib/llm_cost_tracker.rb
CHANGED
|
@@ -23,15 +23,6 @@ require_relative "llm_cost_tracker/billing/line_item"
|
|
|
23
23
|
require_relative "llm_cost_tracker/billing/cost_status"
|
|
24
24
|
require_relative "llm_cost_tracker/event"
|
|
25
25
|
require_relative "llm_cost_tracker/pricing"
|
|
26
|
-
require_relative "llm_cost_tracker/usage_capture"
|
|
27
|
-
require_relative "llm_cost_tracker/pricing/sync"
|
|
28
|
-
require_relative "llm_cost_tracker/parsers/base"
|
|
29
|
-
require_relative "llm_cost_tracker/parsers/openai_usage"
|
|
30
|
-
require_relative "llm_cost_tracker/parsers/openai"
|
|
31
|
-
require_relative "llm_cost_tracker/parsers/openai_compatible"
|
|
32
|
-
require_relative "llm_cost_tracker/parsers/anthropic"
|
|
33
|
-
require_relative "llm_cost_tracker/parsers/gemini"
|
|
34
|
-
require_relative "llm_cost_tracker/parsers/sse"
|
|
35
26
|
require_relative "llm_cost_tracker/parsers"
|
|
36
27
|
require_relative "llm_cost_tracker/middleware/faraday"
|
|
37
28
|
require_relative "llm_cost_tracker/integrations"
|
|
@@ -40,14 +31,18 @@ require_relative "llm_cost_tracker/pricing/unknown"
|
|
|
40
31
|
require_relative "llm_cost_tracker/ledger"
|
|
41
32
|
require_relative "llm_cost_tracker/ingestion"
|
|
42
33
|
require_relative "llm_cost_tracker/tracker"
|
|
43
|
-
require_relative "llm_cost_tracker/retention"
|
|
44
|
-
require_relative "llm_cost_tracker/report"
|
|
45
|
-
require_relative "llm_cost_tracker/doctor"
|
|
46
|
-
require_relative "llm_cost_tracker/doctor/capture_verifier"
|
|
47
34
|
|
|
48
35
|
module LlmCostTracker
|
|
36
|
+
autoload :Engine, "llm_cost_tracker/engine"
|
|
49
37
|
autoload :Reconciliation, "llm_cost_tracker/reconciliation"
|
|
50
38
|
autoload :ReconcileTasks, "llm_cost_tracker/reconcile_tasks"
|
|
39
|
+
autoload :Doctor, "llm_cost_tracker/doctor"
|
|
40
|
+
autoload :Report, "llm_cost_tracker/report"
|
|
41
|
+
autoload :Retention, "llm_cost_tracker/retention"
|
|
42
|
+
|
|
43
|
+
module Pricing
|
|
44
|
+
autoload :Sync, "llm_cost_tracker/pricing/sync"
|
|
45
|
+
end
|
|
51
46
|
|
|
52
47
|
@configuration = Configuration.new
|
|
53
48
|
|
|
@@ -77,6 +72,7 @@ module LlmCostTracker
|
|
|
77
72
|
|
|
78
73
|
def reset_configuration!
|
|
79
74
|
Ingestion::Worker.shutdown!(drain: false)
|
|
75
|
+
Ingestion::Pool.reset!
|
|
80
76
|
@configuration = Configuration.new
|
|
81
77
|
Pricing::Lookup.reset!
|
|
82
78
|
Pricing::Registry.reset!
|
|
@@ -84,7 +80,7 @@ module LlmCostTracker
|
|
|
84
80
|
Pricing::Unknown.reset!
|
|
85
81
|
Ingestion::Worker.reset!
|
|
86
82
|
Tags::Context.clear!
|
|
87
|
-
|
|
83
|
+
Dashboard::SetupState.reset!
|
|
88
84
|
end
|
|
89
85
|
|
|
90
86
|
def with_tags(tags = nil, **kwargs, &)
|
|
@@ -98,7 +94,7 @@ module LlmCostTracker
|
|
|
98
94
|
Tracker.enforce_budget! if enforce_budget
|
|
99
95
|
|
|
100
96
|
Tracker.record(
|
|
101
|
-
|
|
97
|
+
event: Event.build(
|
|
102
98
|
provider: provider,
|
|
103
99
|
model: model,
|
|
104
100
|
token_usage: TokenUsage.build_from_tokens(tokens),
|
|
@@ -4,7 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "rails/generators"
|
|
5
5
|
|
|
6
6
|
require_relative "../llm_cost_tracker/generators/llm_cost_tracker/install_generator"
|
|
7
|
-
require_relative "../llm_cost_tracker/pricing/
|
|
7
|
+
require_relative "../llm_cost_tracker/pricing/sync/change_printer"
|
|
8
8
|
|
|
9
9
|
# rubocop:disable Metrics/BlockLength
|
|
10
10
|
namespace :llm_cost_tracker do
|
|
@@ -48,12 +48,26 @@ namespace :llm_cost_tracker do
|
|
|
48
48
|
puts LlmCostTracker::Report.generate(days: days)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
desc "Recompute total_cost for calls with unknown pricing using the current price registry. " \
|
|
52
|
+
"Use BATCH_SIZE=N to tune."
|
|
53
|
+
task backfill_unknown_pricing: :environment do
|
|
54
|
+
require_relative "../llm_cost_tracker/pricing/backfill"
|
|
55
|
+
batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Pricing::Backfill::DEFAULT_BATCH_SIZE).to_i
|
|
56
|
+
result = LlmCostTracker::Pricing::Backfill.call(batch_size: batch_size)
|
|
57
|
+
puts "llm_cost_tracker: examined #{result.examined} calls, recomputed #{result.recomputed}, " \
|
|
58
|
+
"still unknown #{result.still_unknown}"
|
|
59
|
+
end
|
|
60
|
+
|
|
51
61
|
desc "Delete llm_cost_tracker_calls older than DAYS (default: 90). Use BATCH_SIZE=N to tune."
|
|
52
62
|
task prune: :environment do
|
|
53
63
|
days = (ENV["DAYS"] || 90).to_i
|
|
54
64
|
batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Retention::DEFAULT_BATCH_SIZE).to_i
|
|
55
65
|
deleted = LlmCostTracker::Retention.prune(older_than: days, batch_size: batch_size)
|
|
56
66
|
puts "llm_cost_tracker: pruned #{deleted} calls older than #{days} days"
|
|
67
|
+
inbox_pruned = LlmCostTracker::Retention.prune_inbox(older_than: days)
|
|
68
|
+
puts "llm_cost_tracker: pruned #{inbox_pruned} inbox entries older than #{days} days"
|
|
69
|
+
invoice_imports_pruned = LlmCostTracker::Retention.prune_invoice_imports(older_than: days)
|
|
70
|
+
puts "llm_cost_tracker: pruned #{invoice_imports_pruned} provider invoice imports older than #{days} days"
|
|
57
71
|
end
|
|
58
72
|
|
|
59
73
|
namespace :prices do
|
|
@@ -136,7 +150,7 @@ def reconcile_run(method)
|
|
|
136
150
|
end
|
|
137
151
|
|
|
138
152
|
def print_changes(changes)
|
|
139
|
-
LlmCostTracker::Pricing::
|
|
153
|
+
LlmCostTracker::Pricing::Sync::ChangePrinter.call(changes)
|
|
140
154
|
end
|
|
141
155
|
|
|
142
156
|
def price_refresh_output_path
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm_cost_tracker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sergii Khomenko
|
|
@@ -239,6 +239,7 @@ files:
|
|
|
239
239
|
- app/controllers/llm_cost_tracker/dashboard_controller.rb
|
|
240
240
|
- app/controllers/llm_cost_tracker/data_quality_controller.rb
|
|
241
241
|
- app/controllers/llm_cost_tracker/models_controller.rb
|
|
242
|
+
- app/controllers/llm_cost_tracker/pricing_controller.rb
|
|
242
243
|
- app/controllers/llm_cost_tracker/reconciliation_controller.rb
|
|
243
244
|
- app/controllers/llm_cost_tracker/tags_controller.rb
|
|
244
245
|
- app/helpers/llm_cost_tracker/application_helper.rb
|
|
@@ -249,6 +250,7 @@ files:
|
|
|
249
250
|
- app/helpers/llm_cost_tracker/inline_style_helper.rb
|
|
250
251
|
- app/helpers/llm_cost_tracker/pagination_helper.rb
|
|
251
252
|
- app/helpers/llm_cost_tracker/reconciliation_helper.rb
|
|
253
|
+
- app/helpers/llm_cost_tracker/sortable_table_helper.rb
|
|
252
254
|
- app/helpers/llm_cost_tracker/token_usage_helper.rb
|
|
253
255
|
- app/models/llm_cost_tracker/call.rb
|
|
254
256
|
- app/models/llm_cost_tracker/call_line_item.rb
|
|
@@ -264,7 +266,10 @@ files:
|
|
|
264
266
|
- app/services/llm_cost_tracker/dashboard/overview_stats.rb
|
|
265
267
|
- app/services/llm_cost_tracker/dashboard/pagination.rb
|
|
266
268
|
- app/services/llm_cost_tracker/dashboard/params.rb
|
|
269
|
+
- app/services/llm_cost_tracker/dashboard/pricing_overview.rb
|
|
267
270
|
- app/services/llm_cost_tracker/dashboard/provider_breakdown.rb
|
|
271
|
+
- app/services/llm_cost_tracker/dashboard/setup_state.rb
|
|
272
|
+
- app/services/llm_cost_tracker/dashboard/sort.rb
|
|
268
273
|
- app/services/llm_cost_tracker/dashboard/spend_anomaly.rb
|
|
269
274
|
- app/services/llm_cost_tracker/dashboard/tag_breakdown.rb
|
|
270
275
|
- app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb
|
|
@@ -279,12 +284,14 @@ files:
|
|
|
279
284
|
- app/views/llm_cost_tracker/errors/invalid_filter.html.erb
|
|
280
285
|
- app/views/llm_cost_tracker/errors/not_found.html.erb
|
|
281
286
|
- app/views/llm_cost_tracker/models/index.html.erb
|
|
287
|
+
- app/views/llm_cost_tracker/pricing/index.html.erb
|
|
282
288
|
- app/views/llm_cost_tracker/reconciliation/index.html.erb
|
|
283
|
-
- app/views/llm_cost_tracker/shared/_active_filters.html.erb
|
|
284
289
|
- app/views/llm_cost_tracker/shared/_bar.html.erb
|
|
285
|
-
- app/views/llm_cost_tracker/shared/
|
|
290
|
+
- app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb
|
|
291
|
+
- app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb
|
|
292
|
+
- app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb
|
|
293
|
+
- app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb
|
|
286
294
|
- app/views/llm_cost_tracker/shared/_metric_stack.html.erb
|
|
287
|
-
- app/views/llm_cost_tracker/shared/_sort.html.erb
|
|
288
295
|
- app/views/llm_cost_tracker/shared/_spend_chart.html.erb
|
|
289
296
|
- app/views/llm_cost_tracker/shared/_tag_chips.html.erb
|
|
290
297
|
- app/views/llm_cost_tracker/shared/setup_required.html.erb
|
|
@@ -298,11 +305,11 @@ files:
|
|
|
298
305
|
- lib/llm_cost_tracker/billing/cost_status.rb
|
|
299
306
|
- lib/llm_cost_tracker/billing/line_item.rb
|
|
300
307
|
- lib/llm_cost_tracker/budget.rb
|
|
308
|
+
- lib/llm_cost_tracker/capture/sse.rb
|
|
301
309
|
- lib/llm_cost_tracker/capture/stream.rb
|
|
302
310
|
- lib/llm_cost_tracker/capture/stream_collector.rb
|
|
303
311
|
- lib/llm_cost_tracker/capture/stream_tracker.rb
|
|
304
312
|
- lib/llm_cost_tracker/configuration.rb
|
|
305
|
-
- lib/llm_cost_tracker/dashboard_setup_state.rb
|
|
306
313
|
- lib/llm_cost_tracker/doctor.rb
|
|
307
314
|
- lib/llm_cost_tracker/doctor/capture_verifier.rb
|
|
308
315
|
- lib/llm_cost_tracker/doctor/check.rb
|
|
@@ -318,27 +325,31 @@ files:
|
|
|
318
325
|
- lib/llm_cost_tracker/engine.rb
|
|
319
326
|
- lib/llm_cost_tracker/errors.rb
|
|
320
327
|
- lib/llm_cost_tracker/event.rb
|
|
328
|
+
- lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb
|
|
321
329
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb
|
|
322
|
-
- lib/llm_cost_tracker/generators/llm_cost_tracker/durable_ingestion_generator.rb
|
|
323
330
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
|
|
324
331
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
|
|
325
332
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb
|
|
333
|
+
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb
|
|
326
334
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb
|
|
327
335
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb
|
|
328
|
-
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_durable_ingestion.rb.erb
|
|
329
336
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb
|
|
330
337
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
|
|
331
338
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb
|
|
332
339
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb
|
|
333
340
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb
|
|
341
|
+
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb
|
|
342
|
+
- lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb
|
|
334
343
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb
|
|
335
344
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb
|
|
336
345
|
- lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb
|
|
346
|
+
- lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb
|
|
347
|
+
- lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb
|
|
337
348
|
- lib/llm_cost_tracker/ingestion.rb
|
|
338
349
|
- lib/llm_cost_tracker/ingestion/batch.rb
|
|
339
350
|
- lib/llm_cost_tracker/ingestion/inbox.rb
|
|
340
|
-
- lib/llm_cost_tracker/ingestion/inline.rb
|
|
341
351
|
- lib/llm_cost_tracker/ingestion/lease_claim.rb
|
|
352
|
+
- lib/llm_cost_tracker/ingestion/pool.rb
|
|
342
353
|
- lib/llm_cost_tracker/ingestion/worker.rb
|
|
343
354
|
- lib/llm_cost_tracker/integrations.rb
|
|
344
355
|
- lib/llm_cost_tracker/integrations/anthropic.rb
|
|
@@ -368,27 +379,35 @@ files:
|
|
|
368
379
|
- lib/llm_cost_tracker/middleware/faraday.rb
|
|
369
380
|
- lib/llm_cost_tracker/parsers.rb
|
|
370
381
|
- lib/llm_cost_tracker/parsers/anthropic.rb
|
|
382
|
+
- lib/llm_cost_tracker/parsers/azure.rb
|
|
371
383
|
- lib/llm_cost_tracker/parsers/base.rb
|
|
372
384
|
- lib/llm_cost_tracker/parsers/gemini.rb
|
|
373
385
|
- lib/llm_cost_tracker/parsers/openai.rb
|
|
374
386
|
- lib/llm_cost_tracker/parsers/openai_compatible.rb
|
|
375
|
-
- lib/llm_cost_tracker/parsers/openai_service_charges.rb
|
|
376
387
|
- lib/llm_cost_tracker/parsers/openai_usage.rb
|
|
377
|
-
- lib/llm_cost_tracker/parsers/sse.rb
|
|
378
388
|
- lib/llm_cost_tracker/prices.json
|
|
379
389
|
- lib/llm_cost_tracker/pricing.rb
|
|
390
|
+
- lib/llm_cost_tracker/pricing/backfill.rb
|
|
380
391
|
- lib/llm_cost_tracker/pricing/effective_prices.rb
|
|
392
|
+
- lib/llm_cost_tracker/pricing/estimator.rb
|
|
381
393
|
- lib/llm_cost_tracker/pricing/explainer.rb
|
|
382
394
|
- lib/llm_cost_tracker/pricing/lookup.rb
|
|
383
395
|
- lib/llm_cost_tracker/pricing/mode.rb
|
|
384
396
|
- lib/llm_cost_tracker/pricing/registry.rb
|
|
385
397
|
- lib/llm_cost_tracker/pricing/service_charges.rb
|
|
386
398
|
- lib/llm_cost_tracker/pricing/sync.rb
|
|
399
|
+
- lib/llm_cost_tracker/pricing/sync/change_printer.rb
|
|
387
400
|
- lib/llm_cost_tracker/pricing/sync/fetcher.rb
|
|
388
401
|
- lib/llm_cost_tracker/pricing/sync/registry_diff.rb
|
|
389
402
|
- lib/llm_cost_tracker/pricing/sync/registry_writer.rb
|
|
390
|
-
- lib/llm_cost_tracker/pricing/sync_change_printer.rb
|
|
391
403
|
- lib/llm_cost_tracker/pricing/unknown.rb
|
|
404
|
+
- lib/llm_cost_tracker/providers/anthropic/server_tools.rb
|
|
405
|
+
- lib/llm_cost_tracker/providers/anthropic/tier_classification.rb
|
|
406
|
+
- lib/llm_cost_tracker/providers/azure/hosts.rb
|
|
407
|
+
- lib/llm_cost_tracker/providers/gemini/model_families.rb
|
|
408
|
+
- lib/llm_cost_tracker/providers/openai/hosts.rb
|
|
409
|
+
- lib/llm_cost_tracker/providers/openai/model_families.rb
|
|
410
|
+
- lib/llm_cost_tracker/providers/openai/service_charges.rb
|
|
392
411
|
- lib/llm_cost_tracker/railtie.rb
|
|
393
412
|
- lib/llm_cost_tracker/reconcile_tasks.rb
|
|
394
413
|
- lib/llm_cost_tracker/reconciliation.rb
|
|
@@ -397,6 +416,7 @@ files:
|
|
|
397
416
|
- lib/llm_cost_tracker/reconciliation/import_result.rb
|
|
398
417
|
- lib/llm_cost_tracker/reconciliation/importer.rb
|
|
399
418
|
- lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb
|
|
419
|
+
- lib/llm_cost_tracker/reconciliation/sources/coercion.rb
|
|
400
420
|
- lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb
|
|
401
421
|
- lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb
|
|
402
422
|
- lib/llm_cost_tracker/report.rb
|
|
@@ -409,7 +429,6 @@ files:
|
|
|
409
429
|
- lib/llm_cost_tracker/timing.rb
|
|
410
430
|
- lib/llm_cost_tracker/token_usage.rb
|
|
411
431
|
- lib/llm_cost_tracker/tracker.rb
|
|
412
|
-
- lib/llm_cost_tracker/usage_capture.rb
|
|
413
432
|
- lib/llm_cost_tracker/version.rb
|
|
414
433
|
- lib/tasks/llm_cost_tracker.rake
|
|
415
434
|
homepage: https://github.com/sergey-homenko/llm_cost_tracker
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<% if chips.any? %>
|
|
2
|
-
<div class="lct-chip-row" aria-label="Active filters">
|
|
3
|
-
<% chips.each do |chip| %>
|
|
4
|
-
<span class="lct-chip">
|
|
5
|
-
<span class="lct-chip-label"><%= chip[:label] %></span>
|
|
6
|
-
<span><%= chip[:value] %></span>
|
|
7
|
-
<% if chip[:path] %>
|
|
8
|
-
<%= link_to "×", chip[:path], class: "lct-chip-remove", aria: { label: "Remove #{chip[:label]} #{chip[:value]}" } %>
|
|
9
|
-
<% end %>
|
|
10
|
-
</span>
|
|
11
|
-
<% end %>
|
|
12
|
-
<% if local_assigns[:clear_path] %>
|
|
13
|
-
<%= link_to "Clear all", clear_path, class: "lct-clear-link" %>
|
|
14
|
-
<% end %>
|
|
15
|
-
</div>
|
|
16
|
-
<% end %>
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
<%
|
|
2
|
-
fields = local_assigns.fetch(:fields, %i[from to provider model stream])
|
|
3
|
-
default_range = LlmCostTracker::Dashboard::DateRange.call(params: params)
|
|
4
|
-
defaults = {
|
|
5
|
-
from: default_range.from.iso8601,
|
|
6
|
-
to: default_range.to.iso8601
|
|
7
|
-
}.merge(local_assigns.fetch(:defaults, {}))
|
|
8
|
-
reset_path = local_assigns.fetch(:reset_path, path)
|
|
9
|
-
filter_scope = local_assigns.fetch(:filter_scope, params)
|
|
10
|
-
%>
|
|
11
|
-
|
|
12
|
-
<form class="lct-filters" action="<%= path %>" method="get">
|
|
13
|
-
<% local_assigns.fetch(:hidden_fields, {}).each do |key, val| %>
|
|
14
|
-
<%= hidden_field_tag(key, val) %>
|
|
15
|
-
<% end %>
|
|
16
|
-
<div class="lct-filter-row">
|
|
17
|
-
<% if fields.include?(:from) %>
|
|
18
|
-
<div class="lct-field">
|
|
19
|
-
<label for="lct-filter-from">From</label>
|
|
20
|
-
<input id="lct-filter-from" type="date" name="from" value="<%= params[:from] || defaults[:from] %>">
|
|
21
|
-
</div>
|
|
22
|
-
<% end %>
|
|
23
|
-
|
|
24
|
-
<% if fields.include?(:to) %>
|
|
25
|
-
<div class="lct-field">
|
|
26
|
-
<label for="lct-filter-to">To</label>
|
|
27
|
-
<input id="lct-filter-to" type="date" name="to" value="<%= params[:to] || defaults[:to] %>">
|
|
28
|
-
</div>
|
|
29
|
-
<% end %>
|
|
30
|
-
|
|
31
|
-
<% if fields.include?(:provider) %>
|
|
32
|
-
<div class="lct-field">
|
|
33
|
-
<label for="lct-filter-provider">Provider</label>
|
|
34
|
-
<%= select_tag :provider,
|
|
35
|
-
options_for_select(provider_filter_options(filter_params: filter_scope), params[:provider]),
|
|
36
|
-
include_blank: "All providers",
|
|
37
|
-
id: "lct-filter-provider" %>
|
|
38
|
-
</div>
|
|
39
|
-
<% end %>
|
|
40
|
-
|
|
41
|
-
<% if fields.include?(:model) %>
|
|
42
|
-
<div class="lct-field">
|
|
43
|
-
<label for="lct-filter-model">Model</label>
|
|
44
|
-
<%= select_tag :model,
|
|
45
|
-
options_for_select(model_filter_options(filter_params: filter_scope), params[:model]),
|
|
46
|
-
include_blank: "All models",
|
|
47
|
-
id: "lct-filter-model" %>
|
|
48
|
-
</div>
|
|
49
|
-
<% end %>
|
|
50
|
-
|
|
51
|
-
<% if fields.include?(:stream) %>
|
|
52
|
-
<div class="lct-field">
|
|
53
|
-
<label for="lct-filter-stream">Stream</label>
|
|
54
|
-
<%= select_tag :stream,
|
|
55
|
-
options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, params[:stream]),
|
|
56
|
-
include_blank: "All calls",
|
|
57
|
-
id: "lct-filter-stream" %>
|
|
58
|
-
</div>
|
|
59
|
-
<% end %>
|
|
60
|
-
|
|
61
|
-
<div class="lct-filter-actions">
|
|
62
|
-
<button class="lct-button" type="submit">Apply</button>
|
|
63
|
-
<%= link_to("Reset", reset_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</form>
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<%
|
|
2
|
-
current = local_assigns.fetch(:current).to_s
|
|
3
|
-
options = local_assigns.fetch(:options)
|
|
4
|
-
%>
|
|
5
|
-
|
|
6
|
-
<nav class="lct-sort" aria-label="Sort by">
|
|
7
|
-
<% options.each do |label, value| %>
|
|
8
|
-
<%= link_to label,
|
|
9
|
-
path_for_sort.call(value),
|
|
10
|
-
class: ["lct-sort-option", ("is-active" if current == value.to_s)].compact.join(" "),
|
|
11
|
-
aria: ({ current: "true" } if current == value.to_s) %>
|
|
12
|
-
<% end %>
|
|
13
|
-
</nav>
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "ledger/schema/calls"
|
|
4
|
-
require_relative "ledger/schema/call_line_items"
|
|
5
|
-
require_relative "ledger/schema/call_tags"
|
|
6
|
-
require_relative "ledger/schema/call_rollups"
|
|
7
|
-
|
|
8
|
-
module LlmCostTracker
|
|
9
|
-
module DashboardSetupState
|
|
10
|
-
SetupRequired = Data.define(:message, :details)
|
|
11
|
-
DOCS_HINT = "See docs/upgrading.md for the migration path."
|
|
12
|
-
MUTEX = Mutex.new
|
|
13
|
-
|
|
14
|
-
CORE_SCHEMA_CHECKS = [
|
|
15
|
-
[
|
|
16
|
-
LlmCostTracker::Ledger::Schema::Calls,
|
|
17
|
-
"The llm_cost_tracker_calls table does not match the current LLM Cost Tracker schema."
|
|
18
|
-
],
|
|
19
|
-
[
|
|
20
|
-
LlmCostTracker::Ledger::Schema::CallLineItems,
|
|
21
|
-
"The llm_cost_tracker_call_line_items table does not match the current LLM Cost Tracker schema."
|
|
22
|
-
],
|
|
23
|
-
[
|
|
24
|
-
LlmCostTracker::Ledger::Schema::CallTags,
|
|
25
|
-
"The llm_cost_tracker_call_tags table does not match the current LLM Cost Tracker schema."
|
|
26
|
-
]
|
|
27
|
-
].freeze
|
|
28
|
-
|
|
29
|
-
OPTIONAL_CALL_ROLLUPS_CHECK = [
|
|
30
|
-
LlmCostTracker::Ledger::Schema::CallRollups,
|
|
31
|
-
"The llm_cost_tracker_call_rollups table does not match the current LLM Cost Tracker schema."
|
|
32
|
-
].freeze
|
|
33
|
-
|
|
34
|
-
private_constant :MUTEX, :CORE_SCHEMA_CHECKS, :OPTIONAL_CALL_ROLLUPS_CHECK, :DOCS_HINT
|
|
35
|
-
|
|
36
|
-
class << self
|
|
37
|
-
def current
|
|
38
|
-
return @cached if defined?(@cached)
|
|
39
|
-
|
|
40
|
-
MUTEX.synchronize do
|
|
41
|
-
@cached = compute unless defined?(@cached)
|
|
42
|
-
end
|
|
43
|
-
@cached
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def reset!
|
|
47
|
-
MUTEX.synchronize do
|
|
48
|
-
remove_instance_variable(:@cached) if defined?(@cached)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
def compute
|
|
55
|
-
LlmCostTracker::Logging.debug("DashboardSetupState recomputing")
|
|
56
|
-
return calls_table_missing unless LlmCostTracker::Call.table_exists?
|
|
57
|
-
|
|
58
|
-
core_drift = drift_in(schema_checks_for_current_config)
|
|
59
|
-
return core_drift if core_drift
|
|
60
|
-
return nil unless LlmCostTracker.reconciliation_enabled?
|
|
61
|
-
|
|
62
|
-
reconciliation_drift
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def schema_checks_for_current_config
|
|
66
|
-
return CORE_SCHEMA_CHECKS unless LlmCostTracker.configuration.cache_rollups
|
|
67
|
-
|
|
68
|
-
CORE_SCHEMA_CHECKS + [OPTIONAL_CALL_ROLLUPS_CHECK]
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def drift_in(checks)
|
|
72
|
-
checks.each do |schema, message|
|
|
73
|
-
errors = schema.current_schema_errors
|
|
74
|
-
next if errors.empty?
|
|
75
|
-
|
|
76
|
-
return SetupRequired.new(message: message, details: errors + [DOCS_HINT])
|
|
77
|
-
end
|
|
78
|
-
nil
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def reconciliation_drift
|
|
82
|
-
LlmCostTracker.const_get(:Reconciliation) # autoload reconciliation + its ledger schemas
|
|
83
|
-
connection = ActiveRecord::Base.connection
|
|
84
|
-
LlmCostTracker::Reconciliation::SCHEMA_TABLES.each do |schema, table|
|
|
85
|
-
unless connection.data_source_exists?(table)
|
|
86
|
-
return SetupRequired.new(
|
|
87
|
-
message: "The #{table} table is required when reconciliation is enabled.",
|
|
88
|
-
details: ["run bin/rails generate llm_cost_tracker:reconciliation && bin/rails db:migrate", DOCS_HINT]
|
|
89
|
-
)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
errors = schema.current_schema_errors
|
|
93
|
-
next if errors.empty?
|
|
94
|
-
|
|
95
|
-
message = "The #{table} table does not match the current LLM Cost Tracker schema."
|
|
96
|
-
return SetupRequired.new(message: message, details: errors + [DOCS_HINT])
|
|
97
|
-
end
|
|
98
|
-
nil
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def calls_table_missing
|
|
102
|
-
SetupRequired.new(
|
|
103
|
-
message: "The llm_cost_tracker_calls table is not available yet.",
|
|
104
|
-
details: nil
|
|
105
|
-
)
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|