llm_cost_tracker 0.7.3 → 0.9.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/.ruby-version +1 -0
- data/CHANGELOG.md +173 -0
- data/README.md +60 -220
- data/app/assets/llm_cost_tracker/application.css +282 -45
- data/app/controllers/llm_cost_tracker/application_controller.rb +25 -20
- data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +22 -19
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +14 -2
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +106 -0
- data/app/controllers/llm_cost_tracker/tags_controller.rb +15 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +18 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
- data/app/helpers/llm_cost_tracker/inline_style_helper.rb +28 -0
- data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +13 -0
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +24 -7
- data/app/models/llm_cost_tracker/call.rb +166 -0
- data/app/models/llm_cost_tracker/call_line_item.rb +18 -0
- data/app/models/llm_cost_tracker/call_rollup.rb +6 -0
- data/app/models/llm_cost_tracker/call_tag.rb +12 -0
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +9 -0
- data/app/models/llm_cost_tracker/ingestion/lease.rb +0 -3
- data/app/models/llm_cost_tracker/provider_invoice.rb +13 -0
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +24 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +152 -32
- data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +8 -6
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
- data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
- data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
- data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -1
- data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
- data/app/views/llm_cost_tracker/calls/show.html.erb +73 -33
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +16 -57
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +183 -167
- data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
- data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +183 -0
- data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +66 -0
- data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
- data/app/views/llm_cost_tracker/tags/show.html.erb +64 -36
- data/config/routes.rb +3 -2
- data/lib/llm_cost_tracker/billing/components.rb +95 -0
- data/lib/llm_cost_tracker/billing/components.yml +188 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
- data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
- data/lib/llm_cost_tracker/budget.rb +26 -36
- data/lib/llm_cost_tracker/capture/stream_collector.rb +125 -38
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +40 -5
- data/lib/llm_cost_tracker/configuration.rb +86 -17
- data/lib/llm_cost_tracker/dashboard_setup_state.rb +109 -0
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +56 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +48 -30
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +164 -0
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
- data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
- data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +34 -0
- data/lib/llm_cost_tracker/doctor.rb +111 -44
- data/lib/llm_cost_tracker/engine.rb +9 -0
- data/lib/llm_cost_tracker/errors.rb +5 -19
- data/lib/llm_cost_tracker/event.rb +11 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +43 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/durable_ingestion_generator.rb +43 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +17 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +34 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +15 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +104 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_durable_ingestion.rb.erb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +55 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +28 -25
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +20 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +32 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +18 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +38 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/{add_provider_response_id_generator.rb → upgrade_call_tags_key_value_index_generator.rb} +5 -4
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/{add_streaming_generator.rb → upgrade_image_tokens_generator.rb} +4 -4
- data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
- data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -24
- data/lib/llm_cost_tracker/ingestion/inline.rb +22 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +24 -7
- data/lib/llm_cost_tracker/ingestion.rb +66 -22
- data/lib/llm_cost_tracker/integrations/anthropic.rb +68 -42
- data/lib/llm_cost_tracker/integrations/base.rb +56 -32
- data/lib/llm_cost_tracker/integrations/openai.rb +342 -63
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +110 -11
- data/lib/llm_cost_tracker/integrations.rb +21 -3
- data/lib/llm_cost_tracker/ledger/period/totals.rb +30 -11
- data/lib/llm_cost_tracker/ledger/period.rb +5 -5
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +2 -2
- data/lib/llm_cost_tracker/ledger/rollups.rb +90 -25
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +79 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +41 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +36 -23
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +47 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +42 -0
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +46 -0
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
- data/lib/llm_cost_tracker/ledger/store.rb +103 -20
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +37 -0
- data/lib/llm_cost_tracker/ledger/tags/query.rb +6 -11
- data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -15
- data/lib/llm_cost_tracker/ledger.rb +5 -2
- data/lib/llm_cost_tracker/logging.rb +2 -5
- data/lib/llm_cost_tracker/masking.rb +39 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +95 -35
- data/lib/llm_cost_tracker/parsers/anthropic.rb +74 -14
- data/lib/llm_cost_tracker/parsers/base.rb +13 -4
- data/lib/llm_cost_tracker/parsers/gemini.rb +105 -15
- data/lib/llm_cost_tracker/parsers/openai.rb +16 -2
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +15 -3
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +126 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +157 -59
- data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
- data/lib/llm_cost_tracker/parsers.rb +1 -1
- data/lib/llm_cost_tracker/prices.json +198 -22
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +28 -21
- data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
- data/lib/llm_cost_tracker/pricing/lookup.rb +73 -36
- data/lib/llm_cost_tracker/pricing/mode.rb +76 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +67 -45
- data/lib/llm_cost_tracker/pricing/service_charges.rb +210 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +50 -1
- data/lib/llm_cost_tracker/pricing/sync.rb +59 -10
- data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
- data/lib/llm_cost_tracker/pricing.rb +220 -28
- data/lib/llm_cost_tracker/railtie.rb +6 -8
- data/lib/llm_cost_tracker/reconcile_tasks.rb +134 -0
- data/lib/llm_cost_tracker/reconciliation/diff.rb +428 -0
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +48 -0
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +19 -0
- data/lib/llm_cost_tracker/reconciliation/importer.rb +253 -0
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +171 -0
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +20 -0
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +142 -0
- data/lib/llm_cost_tracker/reconciliation.rb +118 -0
- data/lib/llm_cost_tracker/report/data.rb +19 -8
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +22 -9
- data/lib/llm_cost_tracker/tags/context.rb +2 -5
- data/lib/llm_cost_tracker/tags/key.rb +4 -0
- data/lib/llm_cost_tracker/tags/sanitizer.rb +71 -20
- data/lib/llm_cost_tracker/timing.rb +15 -0
- data/lib/llm_cost_tracker/token_usage.rb +64 -42
- data/lib/llm_cost_tracker/tracker.rb +97 -27
- data/lib/llm_cost_tracker/usage_capture.rb +29 -8
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +45 -35
- data/lib/tasks/llm_cost_tracker.rake +45 -17
- metadata +71 -41
- data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
- data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
- data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
- data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
- data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
- data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
- data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
- data/lib/llm_cost_tracker/pricing/components.rb +0 -37
- data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +0 -63
|
@@ -2,23 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "ledger"
|
|
4
4
|
require_relative "doctor/check"
|
|
5
|
+
require_relative "doctor/probe"
|
|
5
6
|
require_relative "doctor/ingestion_check"
|
|
7
|
+
require_relative "doctor/legacy_audit_check"
|
|
8
|
+
require_relative "doctor/legacy_billing_status_check"
|
|
6
9
|
require_relative "doctor/price_check"
|
|
7
|
-
require_relative "
|
|
10
|
+
require_relative "doctor/schema_check"
|
|
11
|
+
require_relative "doctor/cost_drift_check"
|
|
12
|
+
require_relative "doctor/pricing_snapshot_drift_check"
|
|
8
13
|
|
|
9
14
|
module LlmCostTracker
|
|
10
15
|
class Doctor
|
|
11
|
-
|
|
12
|
-
"event_id" => "bin/rails generate llm_cost_tracker:add_ingestion",
|
|
13
|
-
"latency_ms" => "bin/rails generate llm_cost_tracker:add_latency_ms",
|
|
14
|
-
"stream" => "bin/rails generate llm_cost_tracker:add_streaming",
|
|
15
|
-
"usage_source" => "bin/rails generate llm_cost_tracker:add_streaming",
|
|
16
|
-
"provider_response_id" => "bin/rails generate llm_cost_tracker:add_provider_response_id"
|
|
17
|
-
}.merge(
|
|
18
|
-
Generators::AddTokenUsageGenerator::COLUMN_NAMES.to_h do |column|
|
|
19
|
-
[column, "bin/rails generate llm_cost_tracker:add_token_usage"]
|
|
20
|
-
end
|
|
21
|
-
).freeze
|
|
16
|
+
autoload :InvoiceReconciliationCheck, "llm_cost_tracker/doctor/invoice_reconciliation_check"
|
|
22
17
|
|
|
23
18
|
class << self
|
|
24
19
|
def call
|
|
@@ -44,7 +39,17 @@ module LlmCostTracker
|
|
|
44
39
|
active_record_check,
|
|
45
40
|
table_check,
|
|
46
41
|
column_check,
|
|
47
|
-
|
|
42
|
+
SchemaCheck.new(name: "call line items", schema: Ledger::Schema::CallLineItems,
|
|
43
|
+
table: "llm_cost_tracker_call_line_items").call,
|
|
44
|
+
SchemaCheck.new(name: "call tags", schema: Ledger::Schema::CallTags,
|
|
45
|
+
table: "llm_cost_tracker_call_tags").call,
|
|
46
|
+
*reconciliation_schema_checks,
|
|
47
|
+
CostDriftCheck.new.call,
|
|
48
|
+
PricingSnapshotDriftCheck.new.call,
|
|
49
|
+
*reconciliation_invoice_check,
|
|
50
|
+
LegacyBillingStatusCheck.new.call,
|
|
51
|
+
LegacyAuditCheck.new.call,
|
|
52
|
+
call_rollups_check,
|
|
48
53
|
IngestionCheck.new.call,
|
|
49
54
|
PriceCheck.new.call,
|
|
50
55
|
calls_check
|
|
@@ -53,6 +58,26 @@ module LlmCostTracker
|
|
|
53
58
|
|
|
54
59
|
private
|
|
55
60
|
|
|
61
|
+
def reconciliation_schema_checks
|
|
62
|
+
return [] unless LlmCostTracker.reconciliation_enabled?
|
|
63
|
+
|
|
64
|
+
LlmCostTracker.const_get(:Reconciliation) # autoload reconciliation + its ledger schemas
|
|
65
|
+
Reconciliation::SCHEMA_TABLES.map do |schema, table|
|
|
66
|
+
SchemaCheck.new(name: humanize_table(table), schema: schema, table: table,
|
|
67
|
+
optional: false, install_command: "llm_cost_tracker:reconciliation").call
|
|
68
|
+
end.compact
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def humanize_table(table)
|
|
72
|
+
table.delete_prefix("llm_cost_tracker_").tr("_", " ")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def reconciliation_invoice_check
|
|
76
|
+
return [] unless LlmCostTracker.reconciliation_enabled?
|
|
77
|
+
|
|
78
|
+
Array(InvoiceReconciliationCheck.new.call)
|
|
79
|
+
end
|
|
80
|
+
|
|
56
81
|
def configuration_check
|
|
57
82
|
config = LlmCostTracker.configuration
|
|
58
83
|
Check.new(:ok, "configuration", "active_record ledger enabled=#{config.enabled}")
|
|
@@ -68,7 +93,7 @@ module LlmCostTracker
|
|
|
68
93
|
return Check.new(
|
|
69
94
|
:ok,
|
|
70
95
|
"capture",
|
|
71
|
-
"SDK integrations enabled: #{config.instrumented_integrations.join(', ')}"
|
|
96
|
+
"SDK integrations enabled: #{config.instrumented_integrations.to_a.join(', ')}"
|
|
72
97
|
)
|
|
73
98
|
end
|
|
74
99
|
|
|
@@ -93,68 +118,110 @@ module LlmCostTracker
|
|
|
93
118
|
|
|
94
119
|
def table_check
|
|
95
120
|
return unless active_record_available?
|
|
96
|
-
return Check.new(:ok, "
|
|
121
|
+
return Check.new(:ok, "llm_cost_tracker_calls", "table exists") if llm_cost_tracker_calls_table?
|
|
97
122
|
|
|
98
123
|
Check.new(
|
|
99
124
|
:error,
|
|
100
|
-
"
|
|
125
|
+
"llm_cost_tracker_calls",
|
|
101
126
|
"missing; run bin/rails generate llm_cost_tracker:install && bin/rails db:migrate"
|
|
102
127
|
)
|
|
103
128
|
end
|
|
104
129
|
|
|
105
130
|
def column_check
|
|
106
|
-
return unless
|
|
131
|
+
return unless llm_cost_tracker_calls_table?
|
|
107
132
|
|
|
108
133
|
errors = LlmCostTracker::Ledger::Schema::Calls.current_schema_errors
|
|
109
|
-
return Check.new(:ok, "
|
|
110
|
-
|
|
111
|
-
missing = LlmCostTracker::Ledger::Schema::Calls.missing_current_schema_columns
|
|
112
|
-
generators = missing.filter_map { |column| COLUMN_GENERATORS[column] }.uniq
|
|
113
|
-
message = "current schema required; #{errors.join('; ')}"
|
|
114
|
-
message = "#{message}; run #{generators.join(' && ')} && bin/rails db:migrate" if generators.any?
|
|
134
|
+
return Check.new(:ok, "llm_cost_tracker_calls columns", "current") if errors.empty?
|
|
115
135
|
|
|
116
|
-
Check.new(
|
|
136
|
+
Check.new(
|
|
137
|
+
:error,
|
|
138
|
+
"llm_cost_tracker_calls columns",
|
|
139
|
+
"schema mismatch: #{errors.join('; ')}; see docs/upgrading.md"
|
|
140
|
+
)
|
|
117
141
|
end
|
|
118
142
|
|
|
119
|
-
def
|
|
120
|
-
return unless
|
|
143
|
+
def call_rollups_check
|
|
144
|
+
return unless llm_cost_tracker_calls_table?
|
|
145
|
+
return live_rollups_check unless LlmCostTracker.configuration.cache_rollups
|
|
121
146
|
|
|
122
|
-
errors = LlmCostTracker::Ledger::Schema::
|
|
123
|
-
return
|
|
147
|
+
errors = LlmCostTracker::Ledger::Schema::CallRollups.current_schema_errors
|
|
148
|
+
return rollups_drift_check if errors.empty?
|
|
124
149
|
|
|
125
150
|
Check.new(
|
|
126
151
|
:error,
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"run bin/rails generate llm_cost_tracker:add_period_totals && bin/rails db:migrate"
|
|
152
|
+
"call rollups",
|
|
153
|
+
"schema mismatch: #{errors.join('; ')}; see docs/upgrading.md"
|
|
130
154
|
)
|
|
131
155
|
end
|
|
132
156
|
|
|
157
|
+
ROLLUPS_DRIFT_TOLERANCE_PERCENT = 1.0
|
|
158
|
+
private_constant :ROLLUPS_DRIFT_TOLERANCE_PERCENT
|
|
159
|
+
|
|
160
|
+
def rollups_drift_check
|
|
161
|
+
drift_window = Time.now.utc.beginning_of_day
|
|
162
|
+
calls_total = LlmCostTracker::Call
|
|
163
|
+
.where(tracked_at: drift_window..)
|
|
164
|
+
.where.not(total_cost: nil)
|
|
165
|
+
.sum(:total_cost)
|
|
166
|
+
rollup_total = LlmCostTracker::CallRollup
|
|
167
|
+
.where(period: "day", period_start: drift_window.to_date)
|
|
168
|
+
.sum(:total_cost)
|
|
169
|
+
return Check.new(:ok, "call rollups", "llm_cost_tracker_call_rollups exists") if calls_total.zero?
|
|
170
|
+
|
|
171
|
+
drift_percent = ((calls_total - rollup_total).abs * 100.0 / calls_total)
|
|
172
|
+
if drift_percent > ROLLUPS_DRIFT_TOLERANCE_PERCENT
|
|
173
|
+
return Check.new(
|
|
174
|
+
:warn, "call rollups",
|
|
175
|
+
"rollups drift detected: today's calls SUM=#{calls_total} vs rollups SUM=#{rollup_total} " \
|
|
176
|
+
"(#{drift_percent.round(2)}% > #{ROLLUPS_DRIFT_TOLERANCE_PERCENT}% threshold). " \
|
|
177
|
+
"Cached budget reads may understate spend until a rebuild."
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
Check.new(:ok, "call rollups", "llm_cost_tracker_call_rollups exists")
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
Check.new(:warn, "call rollups", "rollups drift check failed: #{e.class}: #{e.message}")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def live_rollups_check
|
|
187
|
+
if Probe.table_exists?("llm_cost_tracker_call_rollups")
|
|
188
|
+
Check.new(
|
|
189
|
+
:warn,
|
|
190
|
+
"call rollups",
|
|
191
|
+
"cache_rollups=false but llm_cost_tracker_call_rollups exists. " \
|
|
192
|
+
"Set config.cache_rollups = true to keep budget reads on the rollups fast path or drop the table."
|
|
193
|
+
)
|
|
194
|
+
else
|
|
195
|
+
Check.new(
|
|
196
|
+
:ok,
|
|
197
|
+
"call rollups",
|
|
198
|
+
"cache_rollups=false; budget reads aggregate from llm_cost_tracker_calls directly"
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
133
203
|
def calls_check
|
|
134
|
-
return unless
|
|
204
|
+
return unless llm_cost_tracker_calls_table?
|
|
135
205
|
|
|
136
|
-
|
|
206
|
+
snapshot = LlmCostTracker::Call
|
|
207
|
+
.select("COUNT(*) AS tracked_call_count, MAX(tracked_at) AS latest_tracked_at")
|
|
208
|
+
.take
|
|
209
|
+
count = snapshot.tracked_call_count.to_i
|
|
137
210
|
return Check.new(:warn, "tracked calls", "none recorded yet") if count.zero?
|
|
138
211
|
|
|
139
|
-
|
|
212
|
+
latest_at = snapshot.latest_tracked_at
|
|
213
|
+
latest_at = latest_at.to_time if latest_at.respond_to?(:to_time)
|
|
214
|
+
latest = latest_at&.utc&.iso8601
|
|
140
215
|
Check.new(:ok, "tracked calls", "#{count} recorded; latest #{latest}")
|
|
141
216
|
end
|
|
142
217
|
|
|
143
218
|
def active_record_available?
|
|
144
|
-
LlmCostTracker::
|
|
219
|
+
LlmCostTracker::Call.connection
|
|
145
220
|
true
|
|
146
221
|
rescue LoadError, StandardError
|
|
147
222
|
false
|
|
148
223
|
end
|
|
149
224
|
|
|
150
|
-
def
|
|
151
|
-
active_record_available? && table_exists?("llm_api_calls")
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def table_exists?(name)
|
|
155
|
-
LlmCostTracker::Ledger::Call.connection.data_source_exists?(name)
|
|
156
|
-
rescue StandardError
|
|
157
|
-
false
|
|
158
|
-
end
|
|
225
|
+
def llm_cost_tracker_calls_table? = active_record_available? && Probe.table_exists?("llm_cost_tracker_calls")
|
|
159
226
|
end
|
|
160
227
|
end
|
|
@@ -3,10 +3,19 @@
|
|
|
3
3
|
require "rails"
|
|
4
4
|
require_relative "../llm_cost_tracker"
|
|
5
5
|
require_relative "assets"
|
|
6
|
+
require_relative "dashboard_setup_state"
|
|
6
7
|
require "rack/files"
|
|
7
8
|
|
|
8
9
|
module LlmCostTracker
|
|
9
10
|
class Engine < ::Rails::Engine
|
|
10
11
|
isolate_namespace LlmCostTracker
|
|
12
|
+
|
|
13
|
+
initializer "llm_cost_tracker.filter_parameters" do |app|
|
|
14
|
+
app.config.filter_parameters += %i[tag tag_value]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer "llm_cost_tracker.dashboard_setup_state" do |app|
|
|
18
|
+
app.reloader.to_prepare { LlmCostTracker::DashboardSetupState.reset! }
|
|
19
|
+
end
|
|
11
20
|
end
|
|
12
21
|
end
|
|
@@ -6,16 +6,12 @@ module LlmCostTracker
|
|
|
6
6
|
class InvalidFilterError < Error; end
|
|
7
7
|
|
|
8
8
|
class BudgetExceededError < Error
|
|
9
|
-
attr_reader :
|
|
10
|
-
|
|
11
|
-
def initialize(budget:,
|
|
12
|
-
|
|
13
|
-
@monthly_total = monthly_total
|
|
14
|
-
@daily_total = daily_total
|
|
15
|
-
@call_cost = call_cost
|
|
16
|
-
@total = total || monthly_total || daily_total || call_cost
|
|
9
|
+
attr_reader :total, :budget, :budget_type, :last_event
|
|
10
|
+
|
|
11
|
+
def initialize(budget:, budget_type:, total:, last_event: nil)
|
|
12
|
+
@total = total
|
|
17
13
|
@budget = budget
|
|
18
|
-
@budget_type = budget_type
|
|
14
|
+
@budget_type = budget_type
|
|
19
15
|
@last_event = last_event
|
|
20
16
|
|
|
21
17
|
super(
|
|
@@ -23,16 +19,6 @@ module LlmCostTracker
|
|
|
23
19
|
"$#{format('%.6f', @total)} / $#{format('%.6f', budget)}"
|
|
24
20
|
)
|
|
25
21
|
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def inferred_budget_type
|
|
30
|
-
return :monthly if monthly_total
|
|
31
|
-
return :daily if daily_total
|
|
32
|
-
return :per_call if call_cost
|
|
33
|
-
|
|
34
|
-
:unknown
|
|
35
|
-
end
|
|
36
22
|
end
|
|
37
23
|
|
|
38
24
|
class UnknownPricingError < Error
|
|
@@ -13,7 +13,14 @@ module LlmCostTracker
|
|
|
13
13
|
:stream,
|
|
14
14
|
:usage_source,
|
|
15
15
|
:provider_response_id,
|
|
16
|
-
:
|
|
16
|
+
:provider_project_id,
|
|
17
|
+
:provider_api_key_id,
|
|
18
|
+
:provider_workspace_id,
|
|
19
|
+
:batch,
|
|
20
|
+
:tracked_at,
|
|
21
|
+
:cost_status,
|
|
22
|
+
:pricing_snapshot,
|
|
23
|
+
:line_items
|
|
17
24
|
) do
|
|
18
25
|
def total_cost
|
|
19
26
|
cost&.fetch(:total_cost, nil)
|
|
@@ -22,8 +29,9 @@ module LlmCostTracker
|
|
|
22
29
|
def to_h
|
|
23
30
|
super.merge(
|
|
24
31
|
token_usage: token_usage.to_h,
|
|
25
|
-
cost: cost
|
|
26
|
-
tags: tags ? tags.to_h : {}
|
|
32
|
+
cost: cost && cost.to_h.transform_values { |v| v.is_a?(BigDecimal) ? v.to_f : v },
|
|
33
|
+
tags: tags ? tags.to_h : {},
|
|
34
|
+
line_items: (line_items || []).map(&:to_h)
|
|
27
35
|
)
|
|
28
36
|
end
|
|
29
37
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Generators
|
|
8
|
+
class CallRollupsGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Creates the optional llm_cost_tracker_call_rollups table for fast budget reads. " \
|
|
14
|
+
"Required when config.cache_rollups = true."
|
|
15
|
+
|
|
16
|
+
def create_migration_file
|
|
17
|
+
migration_template(
|
|
18
|
+
"create_llm_cost_tracker_call_rollups.rb.erb",
|
|
19
|
+
"db/migrate/create_llm_cost_tracker_call_rollups.rb"
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def warn_about_config_flag
|
|
24
|
+
say(<<~MSG, :yellow)
|
|
25
|
+
After migrating, set the following in config/initializers/llm_cost_tracker.rb:
|
|
26
|
+
|
|
27
|
+
LlmCostTracker.configure do |config|
|
|
28
|
+
config.cache_rollups = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Without it Tracker keeps reading budget totals as live SUM aggregates over
|
|
32
|
+
llm_cost_tracker_calls. The doctor check warns about an unused rollups table.
|
|
33
|
+
MSG
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def migration_version
|
|
39
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Generators
|
|
8
|
+
class DurableIngestionGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Creates the durable ingestion tables (llm_cost_tracker_ingestion_inbox_entries + _leases). " \
|
|
14
|
+
"Required when config.durable_ingestion = true."
|
|
15
|
+
|
|
16
|
+
def create_migration_file
|
|
17
|
+
migration_template(
|
|
18
|
+
"create_llm_cost_tracker_durable_ingestion.rb.erb",
|
|
19
|
+
"db/migrate/create_llm_cost_tracker_durable_ingestion.rb"
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def warn_about_config_flag
|
|
24
|
+
say(<<~MSG, :yellow)
|
|
25
|
+
After migrating, set the following in config/initializers/llm_cost_tracker.rb:
|
|
26
|
+
|
|
27
|
+
LlmCostTracker.configure do |config|
|
|
28
|
+
config.durable_ingestion = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Without it the durable inbox tables stay unused and Tracker keeps writing
|
|
32
|
+
inline. The doctor check warns about unused durable tables.
|
|
33
|
+
MSG
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def migration_version
|
|
39
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
4
|
require "rails/generators/active_record"
|
|
5
|
+
require "llm_cost_tracker/billing/components"
|
|
6
|
+
require "llm_cost_tracker/billing/cost_status"
|
|
5
7
|
require "llm_cost_tracker/pricing"
|
|
6
8
|
require "llm_cost_tracker/token_usage"
|
|
7
9
|
|
|
@@ -18,8 +20,8 @@ module LlmCostTracker
|
|
|
18
20
|
|
|
19
21
|
def create_migration_file
|
|
20
22
|
migration_template(
|
|
21
|
-
"
|
|
22
|
-
"db/migrate/
|
|
23
|
+
"create_llm_cost_tracker_calls.rb.erb",
|
|
24
|
+
"db/migrate/create_llm_cost_tracker_calls.rb"
|
|
23
25
|
)
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -33,15 +35,25 @@ module LlmCostTracker
|
|
|
33
35
|
def create_prices_file
|
|
34
36
|
return unless options[:prices]
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
require_relative "prices_generator"
|
|
39
|
+
invoke LlmCostTracker::Generators::PricesGenerator
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
def mount_engine
|
|
40
43
|
return unless options[:dashboard]
|
|
41
44
|
|
|
42
45
|
add_engine_require
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
say(<<~MSG, :yellow)
|
|
47
|
+
The LLM Cost Tracker dashboard ships without authentication.
|
|
48
|
+
Mount it in config/routes.rb behind your app's admin auth, e.g.:
|
|
49
|
+
|
|
50
|
+
authenticate :admin do
|
|
51
|
+
mount LlmCostTracker::Engine => "/llm-costs"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
The generator does NOT add a route automatically — leaving the dashboard
|
|
55
|
+
unauthenticated would expose spend, tags, and provider IDs to anyone.
|
|
56
|
+
MSG
|
|
45
57
|
end
|
|
46
58
|
|
|
47
59
|
private
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
|
+
require "yaml"
|
|
4
5
|
|
|
5
6
|
require_relative "../../pricing/registry"
|
|
6
|
-
require_relative "../../pricing/sync/registry_loader"
|
|
7
7
|
require_relative "../../pricing/sync/registry_writer"
|
|
8
8
|
|
|
9
9
|
module LlmCostTracker
|
|
@@ -12,13 +12,9 @@ module LlmCostTracker
|
|
|
12
12
|
desc "Creates a local LLM Cost Tracker price snapshot"
|
|
13
13
|
|
|
14
14
|
def create_prices_file
|
|
15
|
-
registry = LlmCostTracker::Pricing::Sync::RegistryLoader.new.call(
|
|
16
|
-
path: LlmCostTracker::Pricing::Registry::DEFAULT_PRICES_PATH,
|
|
17
|
-
seed_path: LlmCostTracker::Pricing::Registry::DEFAULT_PRICES_PATH
|
|
18
|
-
)
|
|
19
15
|
LlmCostTracker::Pricing::Sync::RegistryWriter.new.call(
|
|
20
16
|
path: File.join(destination_root, "config/llm_cost_tracker_prices.yml"),
|
|
21
|
-
registry:
|
|
17
|
+
registry: YAML.safe_load_file(LlmCostTracker::Pricing::Registry::DEFAULT_PRICES_PATH, aliases: false) || {}
|
|
22
18
|
)
|
|
23
19
|
end
|
|
24
20
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Generators
|
|
8
|
+
class ReconciliationGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Creates the optional invoice reconciliation tables. Requires provider admin/org-level API keys."
|
|
14
|
+
|
|
15
|
+
def create_migration_file
|
|
16
|
+
migration_template(
|
|
17
|
+
"create_llm_cost_tracker_reconciliation.rb.erb",
|
|
18
|
+
"db/migrate/create_llm_cost_tracker_reconciliation.rb"
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def warn_about_admin_keys
|
|
23
|
+
say "Reconciliation requires admin/org-level API keys (OpenAI sk-admin-..., Anthropic admin keys, " \
|
|
24
|
+
"GCP billing.viewer service accounts). Do NOT use the runtime inference key.", :yellow
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def migration_version
|
|
30
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateLlmCostTrackerCallRollups < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :llm_cost_tracker_call_rollups do |t|
|
|
4
|
+
t.string :period, null: false
|
|
5
|
+
t.date :period_start, null: false
|
|
6
|
+
t.string :currency, null: false, default: "USD"
|
|
7
|
+
t.string :provider, null: false, default: ""
|
|
8
|
+
t.decimal :total_cost, precision: 20, scale: 8, null: false, default: 0
|
|
9
|
+
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :llm_cost_tracker_call_rollups, [:period, :period_start, :currency, :provider], unique: true
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require "llm_cost_tracker/billing/components"
|
|
2
|
+
require "llm_cost_tracker/billing/cost_status"
|
|
3
|
+
require "llm_cost_tracker/ledger/schema/adapter"
|
|
4
|
+
|
|
5
|
+
class CreateLlmCostTrackerCalls < ActiveRecord::Migration<%= migration_version %>
|
|
6
|
+
def change
|
|
7
|
+
create_table :llm_cost_tracker_calls do |t|
|
|
8
|
+
t.string :event_id, null: false
|
|
9
|
+
t.string :provider, null: false
|
|
10
|
+
t.string :model, null: false
|
|
11
|
+
<% LlmCostTracker::TokenUsage.members.each do |column| -%>
|
|
12
|
+
t.integer :<%= column %>, null: false, default: 0
|
|
13
|
+
<% end -%>
|
|
14
|
+
t.decimal :total_cost, precision: 20, scale: 8
|
|
15
|
+
t.integer :latency_ms
|
|
16
|
+
t.boolean :stream, null: false, default: false
|
|
17
|
+
t.string :usage_source
|
|
18
|
+
t.string :provider_response_id
|
|
19
|
+
t.string :provider_project_id
|
|
20
|
+
t.string :provider_api_key_id
|
|
21
|
+
t.string :provider_workspace_id
|
|
22
|
+
t.boolean :batch, null: false, default: false
|
|
23
|
+
t.string :pricing_mode
|
|
24
|
+
t.string :cost_status, null: false, default: LlmCostTracker::Billing::CostStatus::UNKNOWN
|
|
25
|
+
if postgresql?
|
|
26
|
+
t.jsonb :pricing_snapshot
|
|
27
|
+
elsif mysql?
|
|
28
|
+
t.json :pricing_snapshot
|
|
29
|
+
else
|
|
30
|
+
raise "LLM Cost Tracker supports PostgreSQL and MySQL only"
|
|
31
|
+
end
|
|
32
|
+
t.datetime :tracked_at, null: false
|
|
33
|
+
|
|
34
|
+
t.timestamps
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
create_table :llm_cost_tracker_call_line_items do |t|
|
|
38
|
+
t.references :llm_cost_tracker_call,
|
|
39
|
+
null: false,
|
|
40
|
+
index: false,
|
|
41
|
+
foreign_key: { to_table: :llm_cost_tracker_calls, on_delete: :cascade }
|
|
42
|
+
t.integer :position, null: false, default: 0, limit: 2
|
|
43
|
+
t.string :kind, null: false
|
|
44
|
+
t.string :direction, null: false
|
|
45
|
+
t.string :modality, null: false
|
|
46
|
+
t.string :cache_state, null: false, default: "none"
|
|
47
|
+
t.decimal :quantity, precision: 30, scale: 10, null: false
|
|
48
|
+
t.string :unit, null: false
|
|
49
|
+
t.decimal :rate_amount, precision: 20, scale: 8
|
|
50
|
+
t.decimal :rate_quantity, precision: 30, scale: 10, null: false, default: 1
|
|
51
|
+
t.decimal :cost, precision: 20, scale: 8
|
|
52
|
+
t.string :currency, null: false, default: "USD"
|
|
53
|
+
t.string :cost_status, null: false, default: LlmCostTracker::Billing::CostStatus::UNKNOWN
|
|
54
|
+
t.string :pricing_basis
|
|
55
|
+
t.string :price_key
|
|
56
|
+
t.string :price_source
|
|
57
|
+
t.string :price_source_version
|
|
58
|
+
t.string :provider_field
|
|
59
|
+
t.string :provider_item_id
|
|
60
|
+
if postgresql?
|
|
61
|
+
t.jsonb :details, null: false, default: {}
|
|
62
|
+
elsif mysql?
|
|
63
|
+
t.json :details, null: false
|
|
64
|
+
else
|
|
65
|
+
raise "LLM Cost Tracker supports PostgreSQL and MySQL only"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
t.datetime :created_at, null: false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
create_table :llm_cost_tracker_call_tags do |t|
|
|
72
|
+
t.references :llm_cost_tracker_call,
|
|
73
|
+
null: false,
|
|
74
|
+
index: false,
|
|
75
|
+
foreign_key: { to_table: :llm_cost_tracker_calls, on_delete: :cascade }
|
|
76
|
+
t.string :key, null: false
|
|
77
|
+
t.text :value, null: false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
add_index :llm_cost_tracker_calls, :event_id, unique: true
|
|
81
|
+
add_index :llm_cost_tracker_calls, :tracked_at
|
|
82
|
+
add_index :llm_cost_tracker_calls, [:provider, :tracked_at]
|
|
83
|
+
add_index :llm_cost_tracker_calls, [:model, :tracked_at]
|
|
84
|
+
add_index :llm_cost_tracker_calls, :cost_status
|
|
85
|
+
add_index :llm_cost_tracker_calls, :provider_response_id
|
|
86
|
+
add_index :llm_cost_tracker_call_line_items, [:llm_cost_tracker_call_id, :position]
|
|
87
|
+
add_index :llm_cost_tracker_call_tags, :llm_cost_tracker_call_id
|
|
88
|
+
if postgresql?
|
|
89
|
+
add_index :llm_cost_tracker_call_tags, [:key, :value]
|
|
90
|
+
elsif mysql?
|
|
91
|
+
add_index :llm_cost_tracker_call_tags, [:key, :value], length: { value: 191 }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def postgresql?
|
|
98
|
+
LlmCostTracker::Ledger::Schema::Adapter.postgresql?(connection)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def mysql?
|
|
102
|
+
LlmCostTracker::Ledger::Schema::Adapter.mysql?(connection)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class CreateLlmCostTrackerDurableIngestion < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def change
|
|
3
|
+
create_table :llm_cost_tracker_ingestion_inbox_entries do |t|
|
|
4
|
+
t.string :event_id, null: false
|
|
5
|
+
t.decimal :total_cost, precision: 20, scale: 8
|
|
6
|
+
t.datetime :tracked_at, null: false
|
|
7
|
+
t.text :payload, null: false
|
|
8
|
+
t.datetime :locked_at
|
|
9
|
+
t.string :locked_by
|
|
10
|
+
t.integer :attempts, null: false, default: 0
|
|
11
|
+
t.text :last_error
|
|
12
|
+
|
|
13
|
+
t.timestamps
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
create_table :llm_cost_tracker_ingestion_leases do |t|
|
|
17
|
+
t.string :name, null: false
|
|
18
|
+
t.string :locked_by
|
|
19
|
+
t.datetime :locked_until
|
|
20
|
+
|
|
21
|
+
t.timestamps
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
add_index :llm_cost_tracker_ingestion_inbox_entries, :event_id, unique: true
|
|
25
|
+
add_index :llm_cost_tracker_ingestion_inbox_entries, [:tracked_at, :attempts]
|
|
26
|
+
add_index :llm_cost_tracker_ingestion_inbox_entries, [:locked_at, :id]
|
|
27
|
+
add_index :llm_cost_tracker_ingestion_leases, :name, unique: true
|
|
28
|
+
end
|
|
29
|
+
end
|