llm_cost_tracker 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +11 -5
- data/app/assets/llm_cost_tracker/application.css +784 -802
- data/app/controllers/llm_cost_tracker/application_controller.rb +14 -2
- data/app/controllers/llm_cost_tracker/calls_controller.rb +28 -21
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -4
- data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
- data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +19 -16
- data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
- data/app/models/llm_cost_tracker/call.rb +28 -63
- data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
- data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
- data/app/models/llm_cost_tracker/call_tag.rb +0 -2
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +2 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +64 -43
- data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
- data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
- data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +81 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +6 -68
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +20 -12
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
- data/app/views/layouts/llm_cost_tracker/application.html.erb +74 -17
- data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
- data/app/views/llm_cost_tracker/calls/show.html.erb +132 -125
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +120 -159
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +140 -194
- data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
- data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
- data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
- data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
- data/app/views/llm_cost_tracker/tags/show.html.erb +85 -104
- data/config/routes.rb +3 -3
- data/lib/llm_cost_tracker/budget.rb +25 -28
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +2 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +30 -52
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +18 -33
- data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
- data/lib/llm_cost_tracker/charges/cost.rb +27 -0
- data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
- data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +40 -44
- data/lib/llm_cost_tracker/check.rb +5 -0
- data/lib/llm_cost_tracker/configuration.rb +13 -61
- data/lib/llm_cost_tracker/currency.rb +5 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
- data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
- data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
- data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
- data/lib/llm_cost_tracker/doctor.rb +66 -64
- data/lib/llm_cost_tracker/engine.rb +4 -4
- data/lib/llm_cost_tracker/event.rb +12 -20
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +4 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +39 -8
- data/lib/llm_cost_tracker/ingestion/inbox.rb +8 -9
- data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
- data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
- data/lib/llm_cost_tracker/ingestion.rb +24 -36
- data/lib/llm_cost_tracker/integrations/anthropic.rb +94 -116
- data/lib/llm_cost_tracker/integrations/base.rb +39 -57
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +72 -332
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +89 -145
- data/lib/llm_cost_tracker/integrations.rb +32 -25
- data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
- data/lib/llm_cost_tracker/ledger/period.rb +5 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
- data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
- data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
- data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
- data/lib/llm_cost_tracker/ledger/store.rb +18 -42
- data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
- data/lib/llm_cost_tracker/ledger.rb +14 -11
- data/lib/llm_cost_tracker/logging.rb +4 -21
- data/lib/llm_cost_tracker/middleware/faraday.rb +63 -51
- data/lib/llm_cost_tracker/parsers.rb +140 -29
- data/lib/llm_cost_tracker/prices.json +1707 -1
- data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
- data/lib/llm_cost_tracker/pricing/calculation.rb +260 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
- data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +84 -0
- data/lib/llm_cost_tracker/pricing/mode.rb +53 -35
- data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
- data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
- data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
- data/lib/llm_cost_tracker/pricing/source.rb +7 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
- data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
- data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
- data/lib/llm_cost_tracker/pricing.rb +10 -295
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
- data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
- data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +181 -0
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
- data/lib/llm_cost_tracker/providers.rb +35 -0
- data/lib/llm_cost_tracker/railtie.rb +0 -7
- data/lib/llm_cost_tracker/report/data.rb +3 -4
- data/lib/llm_cost_tracker/report/formatter.rb +33 -20
- data/lib/llm_cost_tracker/report.rb +1 -1
- data/lib/llm_cost_tracker/retention.rb +6 -19
- data/lib/llm_cost_tracker/tags/context.rb +9 -6
- data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
- data/lib/llm_cost_tracker/timing.rb +2 -4
- data/lib/llm_cost_tracker/tracker.rb +24 -36
- data/lib/llm_cost_tracker/usage/catalog.rb +58 -0
- data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
- data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +24 -46
- data/lib/llm_cost_tracker/usage/source.rb +14 -0
- data/lib/llm_cost_tracker/usage/token_usage.rb +100 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +43 -52
- data/lib/tasks/llm_cost_tracker.rake +14 -73
- metadata +92 -58
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -106
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
- data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -183
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
- data/lib/llm_cost_tracker/billing/components.rb +0 -95
- data/lib/llm_cost_tracker/capture/stream.rb +0 -9
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
- data/lib/llm_cost_tracker/doctor/check.rb +0 -7
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -32
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -25
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
- data/lib/llm_cost_tracker/masking.rb +0 -39
- data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -193
- data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
- data/lib/llm_cost_tracker/parsers/base.rb +0 -131
- data/lib/llm_cost_tracker/parsers/gemini.rb +0 -232
- data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -51
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -155
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
- data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
- data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
- data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
- data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -134
- data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
- data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -254
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -172
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -142
- data/lib/llm_cost_tracker/reconciliation.rb +0 -118
- data/lib/llm_cost_tracker/token_usage.rb +0 -93
|
@@ -17,7 +17,7 @@ module LlmCostTracker
|
|
|
17
17
|
INGESTION_MODES = %i[inline async].freeze
|
|
18
18
|
SCALAR_ATTRIBUTES = %i[enabled default_tags on_budget_exceeded monthly_budget daily_budget per_call_budget
|
|
19
19
|
log_level prices_file max_tag_count max_tag_value_bytesize
|
|
20
|
-
ingestion_pool_size].freeze
|
|
20
|
+
ingestion_pool_size auto_enable_stream_usage cache_rollups].freeze
|
|
21
21
|
ENUM_ATTRIBUTES = {
|
|
22
22
|
budget_exceeded_behavior: [BUDGET_EXCEEDED_BEHAVIORS, :notify],
|
|
23
23
|
unknown_pricing_behavior: [UNKNOWN_PRICING_BEHAVIORS, :warn],
|
|
@@ -34,11 +34,7 @@ module LlmCostTracker
|
|
|
34
34
|
:report_tag_breakdowns,
|
|
35
35
|
:redacted_tag_keys,
|
|
36
36
|
:unknown_pricing_behavior,
|
|
37
|
-
:openai_compatible_providers
|
|
38
|
-
:reconciliation_importers,
|
|
39
|
-
:reconciliation_enabled,
|
|
40
|
-
:auto_enable_stream_usage,
|
|
41
|
-
:cache_rollups
|
|
37
|
+
:openai_compatible_providers
|
|
42
38
|
)
|
|
43
39
|
|
|
44
40
|
def initialize
|
|
@@ -60,51 +56,12 @@ module LlmCostTracker
|
|
|
60
56
|
@report_tag_breakdowns = []
|
|
61
57
|
@redacted_tag_keys = DEFAULT_REDACTED_TAG_KEYS.dup
|
|
62
58
|
self.openai_compatible_providers = OPENAI_COMPATIBLE_PROVIDERS
|
|
63
|
-
@reconciliation_importers = {}
|
|
64
|
-
@reconciliation_enabled = false
|
|
65
59
|
@auto_enable_stream_usage = true
|
|
66
60
|
self.ingestion = :inline
|
|
67
61
|
@cache_rollups = false
|
|
68
62
|
@finalized = false
|
|
69
63
|
end
|
|
70
64
|
|
|
71
|
-
def cache_rollups=(value)
|
|
72
|
-
ensure_mutable!
|
|
73
|
-
@cache_rollups = value
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def reconciliation_enabled=(value)
|
|
77
|
-
ensure_mutable!
|
|
78
|
-
@reconciliation_enabled = value
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def auto_enable_stream_usage=(value)
|
|
82
|
-
ensure_mutable!
|
|
83
|
-
@auto_enable_stream_usage = value
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def reconciliation_importers=(importers)
|
|
87
|
-
ensure_mutable!
|
|
88
|
-
raise Error, RECONCILIATION_DISABLED_MESSAGE unless @reconciliation_enabled
|
|
89
|
-
|
|
90
|
-
@reconciliation_importers = (importers || {}).to_h do |source, importer|
|
|
91
|
-
raise Error, "reconciliation_importers[#{source}] must respond to call" unless importer.respond_to?(:call)
|
|
92
|
-
|
|
93
|
-
[source.to_sym, importer]
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def register_reconciliation_importer(source, &block)
|
|
98
|
-
ensure_mutable!
|
|
99
|
-
raise Error, RECONCILIATION_DISABLED_MESSAGE unless @reconciliation_enabled
|
|
100
|
-
raise Error, "register_reconciliation_importer requires a block" unless block
|
|
101
|
-
|
|
102
|
-
@reconciliation_importers[source.to_sym] = block
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
RECONCILIATION_DISABLED_MESSAGE = "reconciliation is disabled; set config.reconciliation_enabled = true first"
|
|
106
|
-
private_constant :RECONCILIATION_DISABLED_MESSAGE
|
|
107
|
-
|
|
108
65
|
def openai_compatible_providers=(providers)
|
|
109
66
|
ensure_mutable!
|
|
110
67
|
@openai_compatible_providers = normalize_openai_compatible_providers(providers)
|
|
@@ -112,8 +69,8 @@ module LlmCostTracker
|
|
|
112
69
|
|
|
113
70
|
def pricing_overrides=(value)
|
|
114
71
|
ensure_mutable!
|
|
115
|
-
@pricing_overrides = Pricing::Registry.
|
|
116
|
-
rescue ArgumentError => e
|
|
72
|
+
@pricing_overrides = Pricing::Registry.normalize_price_entries(value || {}, context: "pricing_overrides")
|
|
73
|
+
rescue ArgumentError, TypeError => e
|
|
117
74
|
raise Error, "invalid pricing_overrides: #{e.message}"
|
|
118
75
|
end
|
|
119
76
|
|
|
@@ -129,7 +86,9 @@ module LlmCostTracker
|
|
|
129
86
|
|
|
130
87
|
def instrument(*names)
|
|
131
88
|
ensure_mutable!
|
|
132
|
-
|
|
89
|
+
names = names.flatten
|
|
90
|
+
names = Integrations.names if names == [:all]
|
|
91
|
+
@instrumented_integrations.merge(names)
|
|
133
92
|
end
|
|
134
93
|
|
|
135
94
|
def instrumented?(name)
|
|
@@ -167,6 +126,12 @@ module LlmCostTracker
|
|
|
167
126
|
Array(@redacted_tag_keys).map { |key| Tags::Sanitizer.normalized_key(key) }.freeze
|
|
168
127
|
end
|
|
169
128
|
|
|
129
|
+
def static_sanitized_default_tags
|
|
130
|
+
return nil if @default_tags.respond_to?(:call)
|
|
131
|
+
|
|
132
|
+
@static_sanitized_default_tags ||= Tags::Sanitizer.call((@default_tags || {}).to_h).freeze
|
|
133
|
+
end
|
|
134
|
+
|
|
170
135
|
def finalized?
|
|
171
136
|
@finalized
|
|
172
137
|
end
|
|
@@ -186,19 +151,6 @@ module LlmCostTracker
|
|
|
186
151
|
end
|
|
187
152
|
end
|
|
188
153
|
|
|
189
|
-
def normalize_instrumentation_names(names)
|
|
190
|
-
names = names.flatten
|
|
191
|
-
integrations = Integrations.names
|
|
192
|
-
return integrations if names == [:all]
|
|
193
|
-
|
|
194
|
-
names.each do |name|
|
|
195
|
-
next if integrations.include?(name)
|
|
196
|
-
|
|
197
|
-
raise Error, "Unknown integration: #{name.inspect}. Use one of: #{integrations.join(', ')}"
|
|
198
|
-
end
|
|
199
|
-
names
|
|
200
|
-
end
|
|
201
|
-
|
|
202
154
|
def ensure_mutable!
|
|
203
155
|
return unless finalized?
|
|
204
156
|
|
|
@@ -1,39 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "check"
|
|
3
|
+
require_relative "../check"
|
|
4
4
|
require_relative "probe"
|
|
5
5
|
require_relative "../ingestion"
|
|
6
6
|
|
|
7
7
|
module LlmCostTracker
|
|
8
8
|
class Doctor
|
|
9
9
|
class IngestionCheck
|
|
10
|
-
PENDING_AGE_WARNING_SECONDS = 60
|
|
11
|
-
|
|
12
10
|
def call
|
|
13
11
|
return unless Probe.table_exists?("llm_cost_tracker_calls")
|
|
14
12
|
return inline_check unless LlmCostTracker::Ingestion.async?
|
|
15
13
|
|
|
16
14
|
missing = missing_parts
|
|
17
|
-
if missing.empty?
|
|
18
|
-
inbox = inbox_snapshot
|
|
19
|
-
quarantined = inbox.try(:quarantined_count).to_i
|
|
20
|
-
if quarantined.positive?
|
|
21
|
-
return Check.new(:warn, "async ingestion", "#{quarantined} inbox entries quarantined after retries")
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
pending_count = inbox.try(:pending_count).to_i
|
|
25
|
-
oldest_pending_at = inbox.try(:oldest_pending_at)&.to_time&.utc
|
|
26
|
-
pending_age = oldest_pending_at && (Time.now.utc - oldest_pending_at)
|
|
27
|
-
if pending_count.positive? && pending_age && pending_age >= PENDING_AGE_WARNING_SECONDS
|
|
28
|
-
return Check.new(
|
|
29
|
-
:warn,
|
|
30
|
-
"async ingestion",
|
|
31
|
-
"#{pending_count} inbox entries pending; oldest pending age #{pending_age.round}s"
|
|
32
|
-
)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
return Check.new(:ok, "async ingestion", "inbox and ingestion lease tables available")
|
|
36
|
-
end
|
|
15
|
+
return async_ok if missing.empty?
|
|
37
16
|
|
|
38
17
|
Check.new(
|
|
39
18
|
:error,
|
|
@@ -44,14 +23,16 @@ module LlmCostTracker
|
|
|
44
23
|
|
|
45
24
|
private
|
|
46
25
|
|
|
26
|
+
def async_ok
|
|
27
|
+
Check.new(:ok, "async ingestion", "inbox and ingestion lease tables available")
|
|
28
|
+
end
|
|
29
|
+
|
|
47
30
|
def inline_check
|
|
48
31
|
leftovers = inline_leftover_tables
|
|
49
32
|
if leftovers.empty?
|
|
50
|
-
return Check.new(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"config.ingestion = :inline; events write directly to the ledger"
|
|
54
|
-
)
|
|
33
|
+
return Check.new(:ok,
|
|
34
|
+
"inline ingestion",
|
|
35
|
+
"config.ingestion = :inline; events write directly to the ledger")
|
|
55
36
|
end
|
|
56
37
|
|
|
57
38
|
Check.new(
|
|
@@ -63,33 +44,18 @@ module LlmCostTracker
|
|
|
63
44
|
end
|
|
64
45
|
|
|
65
46
|
def inline_leftover_tables
|
|
66
|
-
|
|
67
|
-
LlmCostTracker::Ingestion::InboxEntry.table_name,
|
|
68
|
-
LlmCostTracker::Ingestion::Lease.table_name
|
|
69
|
-
].select { |table| Probe.table_exists?(table) }
|
|
47
|
+
async_tables.select { |table| Probe.table_exists?(table) }
|
|
70
48
|
end
|
|
71
49
|
|
|
72
50
|
def missing_parts
|
|
51
|
+
async_tables.reject { |table| Probe.table_exists?(table) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def async_tables
|
|
73
55
|
[
|
|
74
56
|
LlmCostTracker::Ingestion::InboxEntry.table_name,
|
|
75
57
|
LlmCostTracker::Ingestion::Lease.table_name
|
|
76
|
-
]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def inbox_snapshot
|
|
80
|
-
max_attempts = LlmCostTracker::Ingestion::InboxEntry::MAX_ATTEMPTS_BEFORE_QUARANTINE
|
|
81
|
-
LlmCostTracker::Ingestion::InboxEntry
|
|
82
|
-
.select(
|
|
83
|
-
"COALESCE(SUM(CASE WHEN attempts >= #{max_attempts} " \
|
|
84
|
-
"THEN 1 ELSE 0 END), 0) AS quarantined_count, " \
|
|
85
|
-
"COALESCE(SUM(CASE WHEN attempts < #{max_attempts} " \
|
|
86
|
-
"THEN 1 ELSE 0 END), 0) AS pending_count, " \
|
|
87
|
-
"MIN(CASE WHEN attempts < #{max_attempts} " \
|
|
88
|
-
"THEN created_at ELSE NULL END) AS oldest_pending_at"
|
|
89
|
-
)
|
|
90
|
-
.take
|
|
91
|
-
rescue StandardError
|
|
92
|
-
nil
|
|
58
|
+
]
|
|
93
59
|
end
|
|
94
60
|
end
|
|
95
61
|
end
|
|
@@ -5,11 +5,10 @@ require_relative "../ledger"
|
|
|
5
5
|
module LlmCostTracker
|
|
6
6
|
class Doctor
|
|
7
7
|
module Probe
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def table_exists?(name)
|
|
8
|
+
def self.table_exists?(name)
|
|
11
9
|
LlmCostTracker::Call.connection.data_source_exists?(name)
|
|
12
|
-
rescue
|
|
10
|
+
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError,
|
|
11
|
+
ActiveRecord::ConnectionFailed, ActiveRecord::StatementInvalid
|
|
13
12
|
false
|
|
14
13
|
end
|
|
15
14
|
end
|
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "check"
|
|
3
|
+
require_relative "../check"
|
|
4
4
|
require_relative "probe"
|
|
5
5
|
require_relative "../ledger"
|
|
6
6
|
|
|
7
7
|
module LlmCostTracker
|
|
8
8
|
class Doctor
|
|
9
9
|
class SchemaCheck
|
|
10
|
-
def initialize(name:, schema:, table
|
|
10
|
+
def initialize(name:, schema:, table:)
|
|
11
11
|
@name = name
|
|
12
12
|
@schema = schema
|
|
13
13
|
@table = table
|
|
14
|
-
@optional = optional
|
|
15
|
-
@install_command = install_command
|
|
16
14
|
end
|
|
17
15
|
|
|
18
16
|
def call
|
|
19
17
|
return unless Probe.table_exists?("llm_cost_tracker_calls")
|
|
20
|
-
return if @optional && !Probe.table_exists?(@table)
|
|
21
18
|
|
|
22
19
|
errors = @schema.current_schema_errors
|
|
23
20
|
return Check.new(:ok, @name, "#{@table} exists") if errors.empty?
|
|
@@ -26,7 +23,7 @@ module LlmCostTracker
|
|
|
26
23
|
:error,
|
|
27
24
|
@name,
|
|
28
25
|
"current schema required; #{errors.join('; ')}; " \
|
|
29
|
-
"run bin/rails generate
|
|
26
|
+
"run bin/rails generate llm_cost_tracker:install && bin/rails db:migrate"
|
|
30
27
|
)
|
|
31
28
|
end
|
|
32
29
|
end
|
|
@@ -1,35 +1,83 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "ledger"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "check"
|
|
5
5
|
require_relative "doctor/probe"
|
|
6
6
|
require_relative "doctor/ingestion_check"
|
|
7
|
-
require_relative "doctor/legacy_audit_check"
|
|
8
|
-
require_relative "doctor/legacy_billing_status_check"
|
|
9
7
|
require_relative "doctor/price_check"
|
|
10
8
|
require_relative "doctor/schema_check"
|
|
11
|
-
require_relative "doctor/cost_drift_check"
|
|
12
|
-
require_relative "doctor/pricing_snapshot_drift_check"
|
|
13
9
|
|
|
14
10
|
module LlmCostTracker
|
|
15
11
|
class Doctor
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
STATUS_GLYPHS = { ok: "✓", warn: "!", error: "x" }.freeze
|
|
13
|
+
STATUS_COLORS = { ok: 32, warn: 33, error: 31 }.freeze
|
|
14
|
+
|
|
15
|
+
SECTIONS = %w[Setup Schema Operations].freeze
|
|
16
|
+
|
|
17
|
+
SECTION_FOR_CHECK = {
|
|
18
|
+
"configuration" => "Setup",
|
|
19
|
+
"capture" => "Setup",
|
|
20
|
+
"active_record" => "Schema",
|
|
21
|
+
"llm_cost_tracker_calls" => "Schema",
|
|
22
|
+
"llm_cost_tracker_calls columns" => "Schema",
|
|
23
|
+
"call line items" => "Schema",
|
|
24
|
+
"call tags" => "Schema",
|
|
25
|
+
"call rollups" => "Operations",
|
|
26
|
+
"inline ingestion" => "Operations",
|
|
27
|
+
"async ingestion" => "Operations",
|
|
28
|
+
"prices" => "Operations",
|
|
29
|
+
"tracked calls" => "Operations"
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
private_constant :STATUS_GLYPHS, :STATUS_COLORS, :SECTIONS, :SECTION_FOR_CHECK
|
|
18
33
|
|
|
19
34
|
class << self
|
|
20
35
|
def call
|
|
21
36
|
new.checks
|
|
22
37
|
end
|
|
23
38
|
|
|
24
|
-
def report(checks = call)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
39
|
+
def report(checks = call, color: $stdout.tty?)
|
|
40
|
+
name_width = checks.map { |c| c.name.length }.max.to_i
|
|
41
|
+
|
|
42
|
+
lines = [bold("LLM Cost Tracker doctor", color), ""]
|
|
43
|
+
each_section(checks) do |section, members|
|
|
44
|
+
lines << bold(section, color)
|
|
45
|
+
members.each do |check|
|
|
46
|
+
status = paint_status("[#{STATUS_GLYPHS.fetch(check.status, check.status)}]", check.status, color)
|
|
47
|
+
lines << " #{status} #{"#{check.name}:".ljust(name_width + 1)} #{check.message}"
|
|
48
|
+
end
|
|
49
|
+
lines << ""
|
|
50
|
+
end
|
|
51
|
+
lines.pop if lines.last == ""
|
|
52
|
+
lines.join("\n")
|
|
28
53
|
end
|
|
29
54
|
|
|
30
55
|
def healthy?(checks = call)
|
|
31
56
|
checks.none? { |check| check.status == :error }
|
|
32
57
|
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def each_section(checks)
|
|
62
|
+
SECTIONS.each do |section|
|
|
63
|
+
members = checks.select { |c| (SECTION_FOR_CHECK[c.name] || "Setup") == section }
|
|
64
|
+
next if members.empty?
|
|
65
|
+
|
|
66
|
+
yield section, members
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def paint_status(text, status, color)
|
|
71
|
+
return text unless color && STATUS_COLORS.key?(status)
|
|
72
|
+
|
|
73
|
+
"\e[#{STATUS_COLORS[status]}m#{text}\e[0m"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def bold(text, color)
|
|
77
|
+
return text unless color
|
|
78
|
+
|
|
79
|
+
"\e[1m#{text}\e[0m"
|
|
80
|
+
end
|
|
33
81
|
end
|
|
34
82
|
|
|
35
83
|
def checks
|
|
@@ -40,16 +88,7 @@ module LlmCostTracker
|
|
|
40
88
|
active_record_check,
|
|
41
89
|
table_check,
|
|
42
90
|
column_check,
|
|
43
|
-
|
|
44
|
-
table: "llm_cost_tracker_call_line_items").call,
|
|
45
|
-
SchemaCheck.new(name: "call tags", schema: Ledger::Schema::CallTags,
|
|
46
|
-
table: "llm_cost_tracker_call_tags").call,
|
|
47
|
-
*reconciliation_schema_checks,
|
|
48
|
-
CostDriftCheck.new.call,
|
|
49
|
-
PricingSnapshotDriftCheck.new.call,
|
|
50
|
-
*reconciliation_invoice_check,
|
|
51
|
-
LegacyBillingStatusCheck.new.call,
|
|
52
|
-
LegacyAuditCheck.new.call,
|
|
91
|
+
*dependent_core_schema_checks,
|
|
53
92
|
call_rollups_check,
|
|
54
93
|
IngestionCheck.new.call,
|
|
55
94
|
PriceCheck.new.call,
|
|
@@ -59,20 +98,12 @@ module LlmCostTracker
|
|
|
59
98
|
|
|
60
99
|
private
|
|
61
100
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
Reconciliation::SCHEMA_TABLES.map do |schema, table|
|
|
101
|
+
def dependent_core_schema_checks
|
|
102
|
+
Ledger::Schema::CORE_SCHEMAS.reject { |schema, _| schema == Ledger::Schema::Calls }.map do |schema, table|
|
|
66
103
|
SchemaCheck.new(name: table.delete_prefix("llm_cost_tracker_").tr("_", " "),
|
|
67
|
-
schema: schema,
|
|
68
|
-
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def reconciliation_invoice_check
|
|
73
|
-
return [] unless LlmCostTracker.reconciliation_enabled?
|
|
74
|
-
|
|
75
|
-
Array(InvoiceReconciliationCheck.new.call)
|
|
104
|
+
schema: schema,
|
|
105
|
+
table: table).call
|
|
106
|
+
end
|
|
76
107
|
end
|
|
77
108
|
|
|
78
109
|
def configuration_check
|
|
@@ -136,7 +167,7 @@ module LlmCostTracker
|
|
|
136
167
|
return live_rollups_check unless LlmCostTracker.configuration.cache_rollups
|
|
137
168
|
|
|
138
169
|
errors = LlmCostTracker::Ledger::Schema::CallRollups.current_schema_errors
|
|
139
|
-
return
|
|
170
|
+
return Check.new(:ok, "call rollups", "llm_cost_tracker_call_rollups exists") if errors.empty?
|
|
140
171
|
|
|
141
172
|
Check.new(
|
|
142
173
|
:error,
|
|
@@ -145,35 +176,6 @@ module LlmCostTracker
|
|
|
145
176
|
)
|
|
146
177
|
end
|
|
147
178
|
|
|
148
|
-
ROLLUPS_DRIFT_TOLERANCE_PERCENT = 1.0
|
|
149
|
-
private_constant :ROLLUPS_DRIFT_TOLERANCE_PERCENT
|
|
150
|
-
|
|
151
|
-
def rollups_drift_check
|
|
152
|
-
drift_window = Time.now.utc.beginning_of_day
|
|
153
|
-
calls_total = LlmCostTracker::Call
|
|
154
|
-
.where(tracked_at: drift_window..)
|
|
155
|
-
.where.not(total_cost: nil)
|
|
156
|
-
.sum(:total_cost)
|
|
157
|
-
rollup_total = LlmCostTracker::CallRollup
|
|
158
|
-
.where(period: "day", period_start: drift_window.to_date)
|
|
159
|
-
.sum(:total_cost)
|
|
160
|
-
return Check.new(:ok, "call rollups", "llm_cost_tracker_call_rollups exists") if calls_total.zero?
|
|
161
|
-
|
|
162
|
-
drift_percent = ((calls_total - rollup_total).abs * 100.0 / calls_total)
|
|
163
|
-
if drift_percent > ROLLUPS_DRIFT_TOLERANCE_PERCENT
|
|
164
|
-
return Check.new(
|
|
165
|
-
:warn, "call rollups",
|
|
166
|
-
"rollups drift detected: today's calls SUM=#{calls_total} vs rollups SUM=#{rollup_total} " \
|
|
167
|
-
"(#{drift_percent.round(2)}% > #{ROLLUPS_DRIFT_TOLERANCE_PERCENT}% threshold). " \
|
|
168
|
-
"Cached budget reads may understate spend until a rebuild."
|
|
169
|
-
)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
Check.new(:ok, "call rollups", "llm_cost_tracker_call_rollups exists")
|
|
173
|
-
rescue StandardError => e
|
|
174
|
-
Check.new(:warn, "call rollups", "rollups drift check failed: #{e.class}: #{e.message}")
|
|
175
|
-
end
|
|
176
|
-
|
|
177
179
|
def live_rollups_check
|
|
178
180
|
if Probe.table_exists?("llm_cost_tracker_call_rollups")
|
|
179
181
|
Check.new(
|
|
@@ -9,12 +9,12 @@ module LlmCostTracker
|
|
|
9
9
|
class Engine < ::Rails::Engine
|
|
10
10
|
isolate_namespace LlmCostTracker
|
|
11
11
|
|
|
12
|
-
initializer "llm_cost_tracker.filter_parameters" do |app|
|
|
13
|
-
app.config.filter_parameters += %i[tag tag_value]
|
|
14
|
-
end
|
|
15
|
-
|
|
16
12
|
initializer "llm_cost_tracker.dashboard_setup_state" do |app|
|
|
17
13
|
app.reloader.to_prepare { LlmCostTracker::Dashboard::SetupState.reset! }
|
|
18
14
|
end
|
|
15
|
+
|
|
16
|
+
initializer "llm_cost_tracker.pricing_cache" do |app|
|
|
17
|
+
app.reloader.to_prepare { LlmCostTracker::Pricing::Registry.reset! }
|
|
18
|
+
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "pricing"
|
|
4
|
-
require_relative "billing/line_item"
|
|
5
|
-
|
|
6
3
|
module LlmCostTracker
|
|
7
4
|
Event = Data.define(
|
|
8
5
|
:event_id,
|
|
@@ -19,38 +16,30 @@ module LlmCostTracker
|
|
|
19
16
|
:provider_project_id,
|
|
20
17
|
:provider_api_key_id,
|
|
21
18
|
:provider_workspace_id,
|
|
22
|
-
:batch,
|
|
23
19
|
:tracked_at,
|
|
24
20
|
:cost_status,
|
|
25
21
|
:pricing_snapshot,
|
|
26
22
|
:line_items
|
|
27
23
|
) do
|
|
28
|
-
def self.batch_from_pricing_mode?(pricing_mode)
|
|
29
|
-
pricing_mode.to_s.split("_").include?("batch")
|
|
30
|
-
end
|
|
31
|
-
|
|
32
24
|
def self.build(**attributes)
|
|
33
|
-
pricing_mode = Pricing.normalize_mode(attributes[:pricing_mode])
|
|
34
25
|
token_usage = attributes.fetch(:token_usage)
|
|
35
|
-
|
|
36
|
-
line_items = attributes[:line_items] || resolve_line_items(attributes[:service_line_items], token_usage)
|
|
26
|
+
line_items = attributes[:line_items] || resolve_line_items(attributes[:service_line_items])
|
|
37
27
|
|
|
38
28
|
new(
|
|
39
29
|
event_id: attributes[:event_id],
|
|
40
30
|
provider: attributes.fetch(:provider).to_s,
|
|
41
31
|
model: attributes.fetch(:model).to_s.strip.presence || Event::UNKNOWN_MODEL,
|
|
42
32
|
token_usage: token_usage,
|
|
43
|
-
pricing_mode: pricing_mode,
|
|
33
|
+
pricing_mode: attributes[:pricing_mode],
|
|
44
34
|
cost: attributes[:cost],
|
|
45
35
|
tags: attributes[:tags],
|
|
46
36
|
latency_ms: attributes[:latency_ms],
|
|
47
37
|
stream: attributes[:stream] || false,
|
|
48
|
-
usage_source: attributes[:usage_source],
|
|
38
|
+
usage_source: attributes[:usage_source]&.to_s,
|
|
49
39
|
provider_response_id: attributes[:provider_response_id].to_s.strip.presence,
|
|
50
40
|
provider_project_id: attributes[:provider_project_id].to_s.strip.presence,
|
|
51
41
|
provider_api_key_id: attributes[:provider_api_key_id].to_s.strip.presence,
|
|
52
42
|
provider_workspace_id: attributes[:provider_workspace_id].to_s.strip.presence,
|
|
53
|
-
batch: batch,
|
|
54
43
|
tracked_at: attributes[:tracked_at],
|
|
55
44
|
cost_status: attributes[:cost_status],
|
|
56
45
|
pricing_snapshot: attributes[:pricing_snapshot],
|
|
@@ -58,21 +47,24 @@ module LlmCostTracker
|
|
|
58
47
|
)
|
|
59
48
|
end
|
|
60
49
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
def batch?
|
|
51
|
+
pricing_mode.to_s.split("_").include?("batch")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.resolve_line_items(service_items)
|
|
55
|
+
Array(service_items).map do |item|
|
|
56
|
+
item.is_a?(Charges::LineItem) ? item : Charges::LineItem.build(item)
|
|
64
57
|
end
|
|
65
|
-
Billing::LineItem.from_token_usage(token_usage) + service_line_items
|
|
66
58
|
end
|
|
67
59
|
|
|
68
60
|
def total_cost
|
|
69
|
-
cost&.
|
|
61
|
+
cost&.total
|
|
70
62
|
end
|
|
71
63
|
|
|
72
64
|
def to_h
|
|
73
65
|
super.merge(
|
|
74
66
|
token_usage: token_usage.to_h,
|
|
75
|
-
cost: cost
|
|
67
|
+
cost: cost&.to_h,
|
|
76
68
|
tags: tags ? tags.to_h : {},
|
|
77
69
|
line_items: (line_items || []).map(&:to_h)
|
|
78
70
|
)
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "rails/generators"
|
|
4
4
|
require "rails/generators/active_record"
|
|
5
|
-
require "llm_cost_tracker/
|
|
6
|
-
require "llm_cost_tracker/billing/cost_status"
|
|
5
|
+
require "llm_cost_tracker/charges/cost_status"
|
|
7
6
|
require "llm_cost_tracker/pricing"
|
|
8
|
-
require "llm_cost_tracker/token_usage"
|
|
7
|
+
require "llm_cost_tracker/usage/token_usage"
|
|
9
8
|
|
|
10
9
|
module LlmCostTracker
|
|
11
10
|
module Generators
|
|
@@ -11,11 +11,14 @@ module LlmCostTracker
|
|
|
11
11
|
class PricesGenerator < Rails::Generators::Base
|
|
12
12
|
desc "Creates a local LLM Cost Tracker price snapshot"
|
|
13
13
|
|
|
14
|
+
PRICES_PATH = "config/llm_cost_tracker_prices.yml"
|
|
15
|
+
|
|
14
16
|
def create_prices_file
|
|
15
|
-
LlmCostTracker::Pricing::Sync::RegistryWriter.new.
|
|
16
|
-
path: File.join(destination_root,
|
|
17
|
+
payload = LlmCostTracker::Pricing::Sync::RegistryWriter.new.render(
|
|
18
|
+
path: File.join(destination_root, PRICES_PATH),
|
|
17
19
|
registry: YAML.safe_load_file(LlmCostTracker::Pricing::Registry::DEFAULT_PRICES_PATH, aliases: false) || {}
|
|
18
20
|
)
|
|
21
|
+
create_file(PRICES_PATH, payload)
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
end
|
data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
require "llm_cost_tracker/
|
|
2
|
-
require "llm_cost_tracker/billing/cost_status"
|
|
1
|
+
require "llm_cost_tracker/charges/cost_status"
|
|
3
2
|
require "llm_cost_tracker/ledger/schema/adapter"
|
|
4
3
|
|
|
5
4
|
class CreateLlmCostTrackerCalls < ActiveRecord::Migration<%= migration_version %>
|
|
@@ -8,7 +7,7 @@ class CreateLlmCostTrackerCalls < ActiveRecord::Migration<%= migration_version %
|
|
|
8
7
|
t.string :event_id, null: false
|
|
9
8
|
t.string :provider, null: false
|
|
10
9
|
t.string :model, null: false
|
|
11
|
-
<% LlmCostTracker::TokenUsage.members.each do |column| -%>
|
|
10
|
+
<% LlmCostTracker::Usage::TokenUsage.members.each do |column| -%>
|
|
12
11
|
t.integer :<%= column %>, null: false, default: 0
|
|
13
12
|
<% end -%>
|
|
14
13
|
t.decimal :total_cost, precision: 20, scale: 8
|
|
@@ -21,7 +20,7 @@ class CreateLlmCostTrackerCalls < ActiveRecord::Migration<%= migration_version %
|
|
|
21
20
|
t.string :provider_workspace_id
|
|
22
21
|
t.boolean :batch, null: false, default: false
|
|
23
22
|
t.string :pricing_mode
|
|
24
|
-
t.string :cost_status, null: false, default: LlmCostTracker::
|
|
23
|
+
t.string :cost_status, null: false, default: LlmCostTracker::Charges::CostStatus::UNKNOWN
|
|
25
24
|
if postgresql?
|
|
26
25
|
t.jsonb :pricing_snapshot
|
|
27
26
|
elsif mysql?
|
|
@@ -50,7 +49,7 @@ class CreateLlmCostTrackerCalls < ActiveRecord::Migration<%= migration_version %
|
|
|
50
49
|
t.decimal :rate_quantity, precision: 30, scale: 10, null: false, default: 1
|
|
51
50
|
t.decimal :cost, precision: 20, scale: 8
|
|
52
51
|
t.string :currency, null: false, default: "USD"
|
|
53
|
-
t.string :cost_status, null: false, default: LlmCostTracker::
|
|
52
|
+
t.string :cost_status, null: false, default: LlmCostTracker::Charges::CostStatus::UNKNOWN
|
|
54
53
|
t.string :pricing_basis
|
|
55
54
|
t.string :price_key
|
|
56
55
|
t.string :price_source
|
|
@@ -57,8 +57,9 @@ LlmCostTracker.configure do |config|
|
|
|
57
57
|
# thread. Set to :async for a write-ahead inbox + background worker that batches
|
|
58
58
|
# inserts and survives caller transaction rollbacks. Requires the optional
|
|
59
59
|
# inbox/leases tables created by `bin/rails generate llm_cost_tracker:async_ingestion`.
|
|
60
|
-
#
|
|
61
|
-
#
|
|
60
|
+
# Synchronous inbox writes use a dedicated ActiveRecord pool (defaults to 2 connections)
|
|
61
|
+
# so they don't compete with request threads for the default pool when a tracked call
|
|
62
|
+
# happens inside an open caller transaction. Bump ingestion_pool_size if your Puma
|
|
62
63
|
# worker count outgrows that.
|
|
63
64
|
# config.ingestion = :async
|
|
64
65
|
# config.ingestion_pool_size = 5
|
data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb
CHANGED
|
@@ -8,12 +8,16 @@ class UpgradeLlmCostTrackerCallRollupsProvider < ActiveRecord::Migration<%= migr
|
|
|
8
8
|
NEW_INDEX = %i[period period_start currency provider].freeze
|
|
9
9
|
|
|
10
10
|
def up
|
|
11
|
+
return unless table_exists?(TABLE)
|
|
12
|
+
|
|
11
13
|
add_column TABLE, :provider, :string, null: false, default: "" unless column_exists?(TABLE, :provider)
|
|
12
14
|
add_unique_index NEW_INDEX
|
|
13
15
|
remove_index TABLE, column: OLD_INDEX, unique: true, if_exists: true
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def down
|
|
19
|
+
return unless table_exists?(TABLE)
|
|
20
|
+
|
|
17
21
|
add_unique_index OLD_INDEX
|
|
18
22
|
remove_index TABLE, column: NEW_INDEX, unique: true, if_exists: true
|
|
19
23
|
remove_column TABLE, :provider if column_exists?(TABLE, :provider)
|