llm_cost_tracker 0.11.0 → 0.13.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 +73 -0
- data/README.md +7 -4
- data/app/assets/llm_cost_tracker/application.css +8 -7
- data/app/controllers/llm_cost_tracker/calls_controller.rb +5 -5
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -1
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +1 -0
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +1 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +12 -19
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +4 -4
- 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 +3 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +76 -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 +30 -44
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +4 -60
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +1 -7
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
- data/app/views/layouts/llm_cost_tracker/application.html.erb +0 -6
- data/app/views/llm_cost_tracker/calls/index.html.erb +8 -8
- data/app/views/llm_cost_tracker/calls/show.html.erb +33 -25
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +8 -8
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +73 -117
- data/app/views/llm_cost_tracker/models/index.html.erb +5 -5
- data/app/views/llm_cost_tracker/pricing/index.html.erb +3 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -3
- data/app/views/llm_cost_tracker/tags/show.html.erb +10 -10
- data/config/routes.rb +2 -3
- data/lib/llm_cost_tracker/budget.rb +24 -26
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
- data/lib/llm_cost_tracker/capture/sse.rb +1 -0
- data/lib/llm_cost_tracker/capture/stream_collector.rb +32 -39
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +17 -28
- 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 +41 -44
- data/lib/llm_cost_tracker/check.rb +5 -0
- data/lib/llm_cost_tracker/configuration.rb +13 -44
- 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 +5 -69
- data/lib/llm_cost_tracker/engine.rb +4 -4
- data/lib/llm_cost_tracker/event.rb +12 -18
- 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 +4 -3
- data/lib/llm_cost_tracker/ingestion/batch.rb +61 -12
- data/lib/llm_cost_tracker/ingestion/inbox.rb +7 -8
- 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 +93 -106
- 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 +70 -276
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +87 -99
- 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 +24 -41
- 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 +8 -18
- data/lib/llm_cost_tracker/logging.rb +4 -21
- data/lib/llm_cost_tracker/middleware/faraday.rb +61 -50
- data/lib/llm_cost_tracker/parsers.rb +139 -26
- data/lib/llm_cost_tracker/prices.json +1821 -13
- data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
- data/lib/llm_cost_tracker/pricing/calculation.rb +279 -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 +98 -0
- data/lib/llm_cost_tracker/pricing/mode.rb +44 -56
- 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 -278
- 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 +63 -39
- 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 -3
- data/lib/llm_cost_tracker/report/data.rb +3 -4
- data/lib/llm_cost_tracker/report/formatter.rb +1 -1
- 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 +23 -36
- data/lib/llm_cost_tracker/usage/catalog.rb +69 -0
- data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
- data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +6 -52
- data/lib/llm_cost_tracker/usage/source.rb +14 -0
- data/lib/llm_cost_tracker/usage/token_usage.rb +45 -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 +81 -55
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -100
- 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 -174
- 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 -36
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -27
- 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 -176
- 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 -230
- data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -45
- 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/server_tools.rb +0 -15
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
- data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -131
- 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 -249
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -148
- data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +0 -40
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -118
- data/lib/llm_cost_tracker/reconciliation.rb +0 -118
- data/lib/llm_cost_tracker/token_usage.rb +0 -93
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "llm_cost_tracker/billing/components"
|
|
4
|
-
require "llm_cost_tracker/ledger/schema/adapter"
|
|
5
|
-
|
|
6
3
|
module LlmCostTracker
|
|
7
4
|
module Dashboard
|
|
8
|
-
|
|
5
|
+
module DataQuality
|
|
9
6
|
UnknownPricingRow = ::Data.define(:provider, :model, :calls, :share_percent)
|
|
7
|
+
QuarantinedInbox = ::Data.define(:count, :total_cost)
|
|
10
8
|
StreamingHealthRow = ::Data.define(:provider, :streams, :with_usage, :unknown, :unknown_share)
|
|
11
|
-
Summary = ::Data.define(:total,
|
|
12
|
-
:
|
|
13
|
-
:
|
|
14
|
-
:
|
|
15
|
-
:
|
|
16
|
-
:
|
|
9
|
+
Summary = ::Data.define(:total,
|
|
10
|
+
:unknown_pricing_count,
|
|
11
|
+
:untagged_calls_count,
|
|
12
|
+
:missing_latency_count,
|
|
13
|
+
:streaming_count,
|
|
14
|
+
:streaming_missing_usage,
|
|
15
|
+
:missing_provider_response_id_count,
|
|
16
|
+
:calls_with_pricing,
|
|
17
|
+
:tagged_calls,
|
|
18
|
+
:calls_with_latency,
|
|
19
|
+
:streams_with_usage,
|
|
20
|
+
:calls_with_provider_response_id,
|
|
21
|
+
:unknown_pricing_share,
|
|
22
|
+
:untagged_share,
|
|
23
|
+
:missing_latency_share,
|
|
24
|
+
:streaming_share,
|
|
25
|
+
:streaming_missing_usage_share,
|
|
26
|
+
:cost_coverage,
|
|
27
|
+
:tag_coverage,
|
|
28
|
+
:latency_coverage,
|
|
29
|
+
:stream_coverage,
|
|
17
30
|
:provider_response_id_coverage)
|
|
18
31
|
|
|
19
32
|
class << self
|
|
@@ -21,6 +34,17 @@ module LlmCostTracker
|
|
|
21
34
|
scope.unscope(:order).select(aggregate_selects(scope)).take
|
|
22
35
|
end
|
|
23
36
|
|
|
37
|
+
def quarantined_inbox
|
|
38
|
+
return nil unless Ingestion.async?
|
|
39
|
+
return nil unless Ingestion::InboxEntry.table_exists?
|
|
40
|
+
|
|
41
|
+
row = Ingestion::InboxEntry
|
|
42
|
+
.quarantined
|
|
43
|
+
.select("COUNT(*) AS quarantined_count, COALESCE(SUM(total_cost), 0) AS quarantined_cost")
|
|
44
|
+
.take
|
|
45
|
+
QuarantinedInbox.new(count: row.quarantined_count.to_i, total_cost: row.quarantined_cost.to_d)
|
|
46
|
+
end
|
|
47
|
+
|
|
24
48
|
def summary(stats)
|
|
25
49
|
total = stats.total_calls.to_i
|
|
26
50
|
unknown_pricing_count = stats.unknown_pricing_count.to_i
|
|
@@ -36,14 +60,28 @@ module LlmCostTracker
|
|
|
36
60
|
calls_with_provider_response_id = total - missing_provider_response_id_count
|
|
37
61
|
|
|
38
62
|
Summary.new(
|
|
39
|
-
total,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
total,
|
|
64
|
+
unknown_pricing_count,
|
|
65
|
+
untagged_calls_count,
|
|
66
|
+
missing_latency_count,
|
|
67
|
+
streaming_count,
|
|
68
|
+
streaming_missing_usage,
|
|
69
|
+
missing_provider_response_id_count,
|
|
70
|
+
calls_with_pricing,
|
|
71
|
+
tagged_calls,
|
|
72
|
+
calls_with_latency,
|
|
73
|
+
streams_with_usage,
|
|
74
|
+
calls_with_provider_response_id,
|
|
75
|
+
percentage(unknown_pricing_count, total),
|
|
76
|
+
percentage(untagged_calls_count, total),
|
|
77
|
+
percentage(missing_latency_count, total),
|
|
78
|
+
percentage(streaming_count, total),
|
|
79
|
+
percentage(streaming_missing_usage, streaming_count),
|
|
80
|
+
percentage(calls_with_pricing, total),
|
|
81
|
+
percentage(tagged_calls, total),
|
|
82
|
+
percentage(calls_with_latency, total),
|
|
83
|
+
percentage(streams_with_usage, streaming_count),
|
|
84
|
+
percentage(calls_with_provider_response_id, total)
|
|
47
85
|
)
|
|
48
86
|
end
|
|
49
87
|
|
|
@@ -55,7 +93,9 @@ module LlmCostTracker
|
|
|
55
93
|
.limit(10)
|
|
56
94
|
.map do |row|
|
|
57
95
|
calls = row.calls.to_i
|
|
58
|
-
UnknownPricingRow.new(provider: row.provider,
|
|
96
|
+
UnknownPricingRow.new(provider: row.provider,
|
|
97
|
+
model: row.model,
|
|
98
|
+
calls: calls,
|
|
59
99
|
share_percent: percentage(calls, total_calls))
|
|
60
100
|
end
|
|
61
101
|
end
|
|
@@ -85,7 +125,7 @@ module LlmCostTracker
|
|
|
85
125
|
def usage_rows(stats, component_costs: {})
|
|
86
126
|
billable_tokens = stats.billable_tokens.to_f
|
|
87
127
|
|
|
88
|
-
rows =
|
|
128
|
+
rows = Usage::Catalog.token_priced.map do |component|
|
|
89
129
|
token_value = stats[component.token_key].to_i
|
|
90
130
|
|
|
91
131
|
{
|
|
@@ -118,7 +158,8 @@ module LlmCostTracker
|
|
|
118
158
|
.where(unit: "token")
|
|
119
159
|
.joins(:call)
|
|
120
160
|
.merge(scope.unscope(:select, :order, :group))
|
|
121
|
-
.group("#{line_item_table}.kind",
|
|
161
|
+
.group("#{line_item_table}.kind",
|
|
162
|
+
"#{line_item_table}.direction",
|
|
122
163
|
"#{line_item_table}.cache_state")
|
|
123
164
|
.pluck(Arel.sql("#{line_item_table}.kind"),
|
|
124
165
|
Arel.sql("#{line_item_table}.direction"),
|
|
@@ -130,7 +171,7 @@ module LlmCostTracker
|
|
|
130
171
|
def streaming_health_rows(scope, total_streaming:)
|
|
131
172
|
return [] unless total_streaming.positive?
|
|
132
173
|
|
|
133
|
-
unknown_predicate =
|
|
174
|
+
unknown_predicate = unknown_usage_source_predicate(scope)
|
|
134
175
|
rows = scope.unscope(:select, :order, :group)
|
|
135
176
|
.where(stream: true)
|
|
136
177
|
.group(:provider)
|
|
@@ -169,11 +210,7 @@ module LlmCostTracker
|
|
|
169
210
|
|
|
170
211
|
def index_costs_by_component(rows)
|
|
171
212
|
rows.each_with_object({}) do |(kind, direction, cache_state, cost), accumulator|
|
|
172
|
-
component =
|
|
173
|
-
item.kind.to_s == kind.to_s &&
|
|
174
|
-
item.direction.to_s == direction.to_s &&
|
|
175
|
-
item.cache_state.to_s == cache_state.to_s
|
|
176
|
-
end
|
|
213
|
+
component = Usage::Catalog.token_priced_for(kind: kind, direction: direction, cache_state: cache_state)
|
|
177
214
|
accumulator[component.key] = cost if component
|
|
178
215
|
end
|
|
179
216
|
end
|
|
@@ -185,14 +222,15 @@ module LlmCostTracker
|
|
|
185
222
|
end
|
|
186
223
|
|
|
187
224
|
def aggregate_selects(scope)
|
|
225
|
+
unknown_pricing = Charges::CostStatus.unknown_pricing_sql
|
|
188
226
|
selects = [
|
|
189
227
|
"COUNT(*) AS total_calls",
|
|
190
|
-
"#{conditional_count_sql(
|
|
228
|
+
"#{conditional_count_sql(unknown_pricing)} AS unknown_pricing_count",
|
|
191
229
|
"#{tagged_calls_sql(scope)} AS tagged_calls_count",
|
|
192
230
|
"COUNT(*) - #{tagged_calls_sql(scope)} AS untagged_calls_count",
|
|
193
231
|
"#{conditional_count_sql('latency_ms IS NULL')} AS missing_latency_count",
|
|
194
232
|
"#{conditional_count_sql('stream')} AS streaming_count",
|
|
195
|
-
"#{streaming_missing_usage_select} AS streaming_missing_usage_count",
|
|
233
|
+
"#{streaming_missing_usage_select(scope)} AS streaming_missing_usage_count",
|
|
196
234
|
"#{provider_response_id_select} AS missing_provider_response_id_count"
|
|
197
235
|
]
|
|
198
236
|
|
|
@@ -207,13 +245,13 @@ module LlmCostTracker
|
|
|
207
245
|
end
|
|
208
246
|
|
|
209
247
|
def usage_sum_columns
|
|
210
|
-
|
|
248
|
+
Usage::Catalog.token_priced.map(&:token_key) + [:hidden_output_tokens]
|
|
211
249
|
end
|
|
212
250
|
|
|
213
251
|
def billable_tokens_select(scope)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
252
|
+
Usage::Catalog.token_priced
|
|
253
|
+
.map { |component| column_sum(scope, component.token_key) }
|
|
254
|
+
.join(" + ")
|
|
217
255
|
end
|
|
218
256
|
|
|
219
257
|
def hidden_output_share_select(scope)
|
|
@@ -223,13 +261,9 @@ module LlmCostTracker
|
|
|
223
261
|
"CASE WHEN #{output} > 0 THEN #{hidden_output} * 100.0 / #{output} ELSE 0 END"
|
|
224
262
|
end
|
|
225
263
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
LlmCostTracker::Billing::CostStatus::PARTIAL
|
|
230
|
-
].map { |value| scope.connection.quote(value) }
|
|
231
|
-
|
|
232
|
-
"total_cost IS NULL OR cost_status IN (#{values.join(', ')})"
|
|
264
|
+
def unknown_usage_source_predicate(scope)
|
|
265
|
+
quoted = scope.connection.quote(LlmCostTracker::Usage::Source::UNKNOWN)
|
|
266
|
+
"usage_source = #{quoted} OR usage_source IS NULL"
|
|
233
267
|
end
|
|
234
268
|
|
|
235
269
|
def column_sum(scope, column)
|
|
@@ -240,9 +274,8 @@ module LlmCostTracker
|
|
|
240
274
|
"COALESCE(SUM(CASE WHEN #{predicate} THEN 1 ELSE 0 END), 0)"
|
|
241
275
|
end
|
|
242
276
|
|
|
243
|
-
def streaming_missing_usage_select
|
|
244
|
-
|
|
245
|
-
conditional_count_sql(predicate)
|
|
277
|
+
def streaming_missing_usage_select(scope)
|
|
278
|
+
conditional_count_sql("stream AND (#{unknown_usage_source_predicate(scope)})")
|
|
246
279
|
end
|
|
247
280
|
|
|
248
281
|
def provider_response_id_select
|
|
@@ -5,6 +5,11 @@ require "date"
|
|
|
5
5
|
module LlmCostTracker
|
|
6
6
|
module Dashboard
|
|
7
7
|
class Filter
|
|
8
|
+
STREAM_FILTER_OPTIONS = [
|
|
9
|
+
["Streaming only", "yes"],
|
|
10
|
+
["Non-streaming only", "no"]
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
8
13
|
class << self
|
|
9
14
|
def call(scope: LlmCostTracker::Call.all, params: {})
|
|
10
15
|
new(scope: scope, params: params).relation
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Dashboard
|
|
5
|
+
module Masking
|
|
6
|
+
SENSITIVE_KEYS = %w[provider_api_key_id provider_workspace_id provider_project_id].freeze
|
|
7
|
+
MASK_TAIL_LENGTH = 4
|
|
8
|
+
|
|
9
|
+
def self.mask_value(key, value)
|
|
10
|
+
string = value.to_s
|
|
11
|
+
return string unless SENSITIVE_KEYS.include?(key.to_s)
|
|
12
|
+
return string if string.length <= MASK_TAIL_LENGTH
|
|
13
|
+
|
|
14
|
+
"***#{string[-MASK_TAIL_LENGTH, MASK_TAIL_LENGTH]}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.mask_hash(hash)
|
|
18
|
+
return hash unless hash.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
hash.each_with_object({}) do |(key, value), masked|
|
|
21
|
+
masked[key] = case value
|
|
22
|
+
when Hash then mask_hash(value)
|
|
23
|
+
when Array then value.map { |entry| entry.is_a?(Hash) ? mask_hash(entry) : entry }
|
|
24
|
+
else
|
|
25
|
+
mask_value(key, value)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Dashboard
|
|
5
|
+
module MonthlyBudget
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def status
|
|
9
|
+
budget = LlmCostTracker.configuration.monthly_budget
|
|
10
|
+
return nil unless budget
|
|
11
|
+
|
|
12
|
+
budget = budget.to_f
|
|
13
|
+
now = Time.now.utc
|
|
14
|
+
month_start = now.beginning_of_month
|
|
15
|
+
month_end = now.end_of_month
|
|
16
|
+
spent = LlmCostTracker::Ledger::Period::Totals.call(%i[month], time: now).fetch(:month)
|
|
17
|
+
elapsed_seconds = now - month_start
|
|
18
|
+
total_seconds = month_end - month_start
|
|
19
|
+
projected_spent = if spent.zero? || !elapsed_seconds.positive?
|
|
20
|
+
spent
|
|
21
|
+
else
|
|
22
|
+
spent * (total_seconds / elapsed_seconds)
|
|
23
|
+
end
|
|
24
|
+
percent_used = budget.positive? ? (spent / budget) * 100.0 : 0.0
|
|
25
|
+
projected_percent_used = budget.positive? ? (projected_spent / budget) * 100.0 : 0.0
|
|
26
|
+
projected_delta = projected_spent - budget
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
budget: budget,
|
|
30
|
+
spent: spent,
|
|
31
|
+
percent_used: percent_used,
|
|
32
|
+
projected_spent: projected_spent,
|
|
33
|
+
projected_percent_used: projected_percent_used,
|
|
34
|
+
projected_delta: projected_delta,
|
|
35
|
+
projection_end_label: month_end.strftime("%b %-d"),
|
|
36
|
+
fill_modifier: fill_modifier(percent_used),
|
|
37
|
+
progress_percent: clamped_percent(percent_used),
|
|
38
|
+
projected_marker_percent: clamped_percent(projected_percent_used),
|
|
39
|
+
projected_delta_amount: projected_delta.abs,
|
|
40
|
+
projected_delta_direction: projected_delta.positive? ? "over" : "under",
|
|
41
|
+
projected_delta_status_class: projected_delta_status_class(projected_delta)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def clamped_percent(value)
|
|
46
|
+
value.clamp(0.0, 100.0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fill_modifier(percent)
|
|
50
|
+
return "lct-budget-fill--over" if percent >= 100.0
|
|
51
|
+
return "lct-budget-fill--warn" if percent >= 80.0
|
|
52
|
+
|
|
53
|
+
""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def projected_delta_status_class(delta)
|
|
57
|
+
return "lct-budget-projection-status--over" if delta.positive?
|
|
58
|
+
|
|
59
|
+
"lct-budget-projection-status--under"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "llm_cost_tracker/ledger"
|
|
4
|
-
|
|
5
3
|
module LlmCostTracker
|
|
6
4
|
module Dashboard
|
|
7
|
-
|
|
5
|
+
module OverviewStats
|
|
8
6
|
class << self
|
|
9
7
|
def call(scope: LlmCostTracker::Call.all, previous_scope: nil)
|
|
10
8
|
return scope.select(aggregate_selects).take unless previous_scope
|
|
@@ -16,48 +14,6 @@ module LlmCostTracker
|
|
|
16
14
|
.take
|
|
17
15
|
end
|
|
18
16
|
|
|
19
|
-
def monthly_budget_status
|
|
20
|
-
budget = LlmCostTracker.configuration.monthly_budget
|
|
21
|
-
return nil unless budget
|
|
22
|
-
|
|
23
|
-
budget = budget.to_f
|
|
24
|
-
now = Time.now.utc
|
|
25
|
-
month_start = now.beginning_of_month
|
|
26
|
-
month_end = now.end_of_month
|
|
27
|
-
spent = LlmCostTracker::Ledger::Period::Totals.call(%i[month], time: now).fetch(:month)
|
|
28
|
-
elapsed_seconds = now - month_start
|
|
29
|
-
total_seconds = month_end - month_start
|
|
30
|
-
projected_spent = if spent.zero? || !elapsed_seconds.positive?
|
|
31
|
-
spent
|
|
32
|
-
else
|
|
33
|
-
spent * (total_seconds / elapsed_seconds)
|
|
34
|
-
end
|
|
35
|
-
percent_used = budget.positive? ? (spent / budget) * 100.0 : 0.0
|
|
36
|
-
projected_percent_used = budget.positive? ? (projected_spent / budget) * 100.0 : 0.0
|
|
37
|
-
projected_delta = projected_spent - budget
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
budget: budget,
|
|
41
|
-
spent: spent,
|
|
42
|
-
percent_used: percent_used,
|
|
43
|
-
projected_spent: projected_spent,
|
|
44
|
-
projected_percent_used: projected_percent_used,
|
|
45
|
-
projected_delta: projected_delta,
|
|
46
|
-
projection_end_label: month_end.strftime("%b %-d"),
|
|
47
|
-
fill_modifier: budget_fill_modifier(percent_used),
|
|
48
|
-
progress_percent: clamped_percent(percent_used),
|
|
49
|
-
projected_marker_percent: clamped_percent(projected_percent_used),
|
|
50
|
-
projected_delta_amount: projected_delta.abs,
|
|
51
|
-
projected_delta_direction: projected_delta.positive? ? "over" : "under",
|
|
52
|
-
projected_delta_status_class: projected_delta_status_class(projected_delta)
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
UNKNOWN_PRICING_COST_STATUSES = [
|
|
57
|
-
LlmCostTracker::Billing::CostStatus::UNKNOWN,
|
|
58
|
-
LlmCostTracker::Billing::CostStatus::PARTIAL
|
|
59
|
-
].freeze
|
|
60
|
-
|
|
61
17
|
private
|
|
62
18
|
|
|
63
19
|
def aggregate_selects(table_name: nil, previous: false)
|
|
@@ -69,11 +25,10 @@ module LlmCostTracker
|
|
|
69
25
|
THEN COALESCE(SUM(#{total_cost}), 0) * 1.0 / COUNT(*)
|
|
70
26
|
ELSE 0 END
|
|
71
27
|
SQL
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
SQL
|
|
28
|
+
predicate = LlmCostTracker::Charges::CostStatus.unknown_pricing_sql(
|
|
29
|
+
total_cost: total_cost, cost_status: cost_status
|
|
30
|
+
)
|
|
31
|
+
unknown_pricing_sql = "SUM(CASE WHEN #{predicate} THEN 1 ELSE 0 END)"
|
|
77
32
|
selects = [
|
|
78
33
|
"COUNT(*) AS total_calls",
|
|
79
34
|
"COALESCE(SUM(#{total_cost}), 0) AS total_cost",
|
|
@@ -85,10 +40,6 @@ module LlmCostTracker
|
|
|
85
40
|
selects.join(", ")
|
|
86
41
|
end
|
|
87
42
|
|
|
88
|
-
def connection
|
|
89
|
-
LlmCostTracker::Call.connection
|
|
90
|
-
end
|
|
91
|
-
|
|
92
43
|
def previous_selects(previous)
|
|
93
44
|
unless previous
|
|
94
45
|
return [
|
|
@@ -125,23 +76,6 @@ module LlmCostTracker
|
|
|
125
76
|
.select("COALESCE(SUM(total_cost), 0) AS total_cost", "COUNT(*) AS total_calls")
|
|
126
77
|
.to_sql
|
|
127
78
|
end
|
|
128
|
-
|
|
129
|
-
def clamped_percent(value)
|
|
130
|
-
value.clamp(0.0, 100.0)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def budget_fill_modifier(percent)
|
|
134
|
-
return "lct-budget-fill--over" if percent >= 100.0
|
|
135
|
-
return "lct-budget-fill--warn" if percent >= 80.0
|
|
136
|
-
|
|
137
|
-
""
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def projected_delta_status_class(delta)
|
|
141
|
-
return "lct-budget-projection-status--over" if delta.positive?
|
|
142
|
-
|
|
143
|
-
"lct-budget-projection-status--under"
|
|
144
|
-
end
|
|
145
79
|
end
|
|
146
80
|
end
|
|
147
81
|
end
|
|
@@ -6,6 +6,7 @@ module LlmCostTracker
|
|
|
6
6
|
DEFAULT_PER = 50
|
|
7
7
|
MAX_PER = 200
|
|
8
8
|
MIN_PAGE = 1
|
|
9
|
+
MIN_PER = 1
|
|
9
10
|
|
|
10
11
|
attr_reader :page, :per
|
|
11
12
|
|
|
@@ -13,7 +14,7 @@ module LlmCostTracker
|
|
|
13
14
|
params = Params.to_hash(params).symbolize_keys
|
|
14
15
|
new(
|
|
15
16
|
page: integer_param(params, :page, default: MIN_PAGE, min: MIN_PAGE),
|
|
16
|
-
per: integer_param(params, :per, default: DEFAULT_PER, min:
|
|
17
|
+
per: integer_param(params, :per, default: DEFAULT_PER, min: MIN_PER, max: MAX_PER)
|
|
17
18
|
)
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -33,10 +34,6 @@ module LlmCostTracker
|
|
|
33
34
|
freeze
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
def limit
|
|
37
|
-
per
|
|
38
|
-
end
|
|
39
|
-
|
|
40
37
|
def offset
|
|
41
38
|
(page - 1) * per
|
|
42
39
|
end
|
|
@@ -4,9 +4,13 @@ module LlmCostTracker
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
class PricingOverview
|
|
6
6
|
SOURCES = %i[overrides file bundled].freeze
|
|
7
|
-
RATE_COLUMNS = %
|
|
7
|
+
RATE_COLUMNS = %w[input output cache_read_input cache_write_input batch_input batch_output].freeze
|
|
8
8
|
Row = Data.define(:provider, :model, :rates)
|
|
9
9
|
|
|
10
|
+
SOURCE_NAME = { overrides: "pricing_overrides", file: "prices_file", bundled: "bundled" }.freeze
|
|
11
|
+
LABEL = { overrides: "Overrides", file: "Custom file", bundled: "Bundled" }.freeze
|
|
12
|
+
private_constant :SOURCE_NAME, :LABEL
|
|
13
|
+
|
|
10
14
|
class << self
|
|
11
15
|
def call
|
|
12
16
|
new.call
|
|
@@ -14,9 +18,9 @@ module LlmCostTracker
|
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
def call
|
|
17
|
-
sources = SOURCES.each_with_object({}) do |
|
|
18
|
-
|
|
19
|
-
acc[
|
|
21
|
+
sources = SOURCES.each_with_object({}) do |key, acc|
|
|
22
|
+
source = sources_by_name.fetch(SOURCE_NAME.fetch(key))
|
|
23
|
+
acc[key] = present(key, source) unless source.prices.empty?
|
|
20
24
|
end
|
|
21
25
|
{
|
|
22
26
|
sources: sources,
|
|
@@ -26,54 +30,36 @@ module LlmCostTracker
|
|
|
26
30
|
|
|
27
31
|
private
|
|
28
32
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
when :overrides then build_overrides
|
|
32
|
-
when :file then build_file
|
|
33
|
-
when :bundled then build_bundled
|
|
34
|
-
end
|
|
33
|
+
def sources_by_name
|
|
34
|
+
@sources_by_name ||= Pricing::Registry.sources.to_h { |source| [source.name, source] }
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def
|
|
38
|
-
prices = LlmCostTracker.configuration.pricing_overrides
|
|
39
|
-
return nil if prices.nil? || prices.empty?
|
|
40
|
-
|
|
37
|
+
def present(key, source)
|
|
41
38
|
{
|
|
42
|
-
label:
|
|
43
|
-
subtitle:
|
|
44
|
-
updated_at:
|
|
45
|
-
currency:
|
|
46
|
-
rows: build_rows(prices)
|
|
39
|
+
label: LABEL.fetch(key),
|
|
40
|
+
subtitle: subtitle_for(key),
|
|
41
|
+
updated_at: updated_at_for(key),
|
|
42
|
+
currency: source.currency,
|
|
43
|
+
rows: build_rows(source.prices)
|
|
47
44
|
}
|
|
48
45
|
end
|
|
49
46
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
meta = Pricing::Registry.file_metadata(path)
|
|
58
|
-
{
|
|
59
|
-
label: "Custom file",
|
|
60
|
-
subtitle: path.to_s,
|
|
61
|
-
updated_at: meta["updated_at"] || Pricing::Lookup.prices_file_mtime_iso,
|
|
62
|
-
currency: meta["currency"] || Pricing::Lookup::DEFAULT_CURRENCY,
|
|
63
|
-
rows: build_rows(prices)
|
|
64
|
-
}
|
|
47
|
+
def subtitle_for(key)
|
|
48
|
+
case key
|
|
49
|
+
when :overrides then "config.pricing_overrides"
|
|
50
|
+
when :file then LlmCostTracker.configuration.prices_file.to_s
|
|
51
|
+
when :bundled then "ships with the gem"
|
|
52
|
+
end
|
|
65
53
|
end
|
|
66
54
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
rows: build_rows(prices)
|
|
76
|
-
}
|
|
55
|
+
def updated_at_for(key)
|
|
56
|
+
case key
|
|
57
|
+
when :file
|
|
58
|
+
path = LlmCostTracker.configuration.prices_file
|
|
59
|
+
Pricing::Registry.file_metadata(path)["updated_at"] || Pricing::Registry.prices_file_mtime_iso
|
|
60
|
+
when :bundled
|
|
61
|
+
Pricing::Registry.metadata["updated_at"]
|
|
62
|
+
end
|
|
77
63
|
end
|
|
78
64
|
|
|
79
65
|
def build_rows(prices)
|
|
@@ -1,65 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "llm_cost_tracker/ledger"
|
|
4
|
-
|
|
5
3
|
module LlmCostTracker
|
|
6
4
|
module Dashboard
|
|
7
5
|
module SetupState
|
|
8
6
|
SetupRequired = Data.define(:message, :details)
|
|
9
|
-
DOCS_HINT = "See docs/upgrading.md for the migration path."
|
|
10
|
-
MUTEX = Mutex.new
|
|
11
|
-
|
|
12
|
-
private_constant :MUTEX, :DOCS_HINT
|
|
13
7
|
|
|
14
8
|
class << self
|
|
15
9
|
def current
|
|
16
|
-
|
|
10
|
+
return @current if defined?(@current)
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
if !defined?(@cache_fingerprint) || @cache_fingerprint != fingerprint
|
|
20
|
-
LlmCostTracker::Call.reset_column_information
|
|
21
|
-
@cached = compute
|
|
22
|
-
@cache_fingerprint = fingerprint
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
@cached
|
|
12
|
+
@current = compute
|
|
26
13
|
end
|
|
27
14
|
|
|
28
15
|
def reset!
|
|
29
|
-
|
|
30
|
-
remove_instance_variable(:@cached) if defined?(@cached)
|
|
31
|
-
remove_instance_variable(:@cache_fingerprint) if defined?(@cache_fingerprint)
|
|
32
|
-
end
|
|
16
|
+
remove_instance_variable(:@current) if defined?(@current)
|
|
33
17
|
end
|
|
34
18
|
|
|
35
19
|
private
|
|
36
20
|
|
|
37
|
-
SCHEMA_MIGRATIONS_TABLE = "schema_migrations"
|
|
38
|
-
private_constant :SCHEMA_MIGRATIONS_TABLE
|
|
39
|
-
|
|
40
|
-
def schema_fingerprint
|
|
41
|
-
connection = ActiveRecord::Base.connection
|
|
42
|
-
quoted = connection.quote_table_name(SCHEMA_MIGRATIONS_TABLE)
|
|
43
|
-
connection.query_value("SELECT MAX(version) FROM #{quoted}")
|
|
44
|
-
rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
|
|
45
|
-
nil
|
|
46
|
-
end
|
|
47
|
-
|
|
48
21
|
def compute
|
|
49
22
|
LlmCostTracker::Logging.debug("Dashboard::SetupState recomputing")
|
|
50
23
|
return calls_table_missing unless LlmCostTracker::Call.table_exists?
|
|
51
24
|
|
|
52
|
-
|
|
53
|
-
return core_drift if core_drift
|
|
54
|
-
return nil unless LlmCostTracker.reconciliation_enabled?
|
|
55
|
-
|
|
56
|
-
reconciliation_drift
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def schema_checks_for_current_config
|
|
60
|
-
return LlmCostTracker::Ledger::Schema::CORE_SCHEMAS unless LlmCostTracker.configuration.cache_rollups
|
|
61
|
-
|
|
62
|
-
LlmCostTracker::Ledger::Schema::CORE_SCHEMAS + [LlmCostTracker::Ledger::Schema::CACHE_ROLLUPS_SCHEMA]
|
|
25
|
+
drift_in(LlmCostTracker::Ingestion.guards_for_current_config)
|
|
63
26
|
end
|
|
64
27
|
|
|
65
28
|
def drift_in(checks)
|
|
@@ -73,25 +36,6 @@ module LlmCostTracker
|
|
|
73
36
|
nil
|
|
74
37
|
end
|
|
75
38
|
|
|
76
|
-
def reconciliation_drift
|
|
77
|
-
connection = ActiveRecord::Base.connection
|
|
78
|
-
LlmCostTracker::Reconciliation::SCHEMA_TABLES.each do |schema, table|
|
|
79
|
-
unless connection.data_source_exists?(table)
|
|
80
|
-
return SetupRequired.new(
|
|
81
|
-
message: "The #{table} table is required when reconciliation is enabled.",
|
|
82
|
-
details: ["bin/rails generate llm_cost_tracker:reconciliation && bin/rails db:migrate"]
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
errors = schema.current_schema_errors
|
|
87
|
-
next if errors.empty?
|
|
88
|
-
|
|
89
|
-
message = "The #{table} table does not match the current LLM Cost Tracker schema."
|
|
90
|
-
return SetupRequired.new(message: message, details: errors)
|
|
91
|
-
end
|
|
92
|
-
nil
|
|
93
|
-
end
|
|
94
|
-
|
|
95
39
|
def calls_table_missing
|
|
96
40
|
SetupRequired.new(
|
|
97
41
|
message: "The llm_cost_tracker_calls table is not available yet.",
|