llm_cost_tracker 0.11.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 +55 -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/pricing_controller.rb +1 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +6 -15
- 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 +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 +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 +31 -23
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +8 -8
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +62 -117
- data/app/views/llm_cost_tracker/models/index.html.erb +5 -5
- data/app/views/llm_cost_tracker/pricing/index.html.erb +2 -2
- 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 +28 -36
- 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 +40 -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 -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/ingestion/batch.rb +39 -8
- 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 +92 -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 +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 +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 +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 +40 -52
- 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 +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 +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
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "bigdecimal"
|
|
4
4
|
|
|
5
|
-
require_relative "
|
|
5
|
+
require_relative "../currency"
|
|
6
|
+
require_relative "../usage/catalog"
|
|
6
7
|
require_relative "cost_status"
|
|
7
8
|
|
|
8
9
|
module LlmCostTracker
|
|
9
|
-
module
|
|
10
|
+
module Charges
|
|
10
11
|
LineItem = Data.define(
|
|
11
12
|
:kind,
|
|
12
13
|
:direction,
|
|
@@ -29,26 +30,24 @@ module LlmCostTracker
|
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
class LineItem
|
|
32
|
-
USD = "USD"
|
|
33
|
-
|
|
34
33
|
def self.build(attributes)
|
|
35
34
|
attributes = attributes.to_h
|
|
36
|
-
|
|
35
|
+
dimension = dimension_for(attributes)
|
|
37
36
|
new(
|
|
38
|
-
kind:
|
|
39
|
-
direction:
|
|
40
|
-
modality:
|
|
41
|
-
cache_state:
|
|
42
|
-
quantity:
|
|
43
|
-
unit:
|
|
37
|
+
kind: attributes[:kind]&.to_s || dimension&.kind,
|
|
38
|
+
direction: attributes[:direction]&.to_s || dimension&.direction,
|
|
39
|
+
modality: attributes[:modality]&.to_s || dimension&.modality,
|
|
40
|
+
cache_state: attributes[:cache_state]&.to_s || dimension&.cache_state || "none",
|
|
41
|
+
quantity: decimal_or_nil(attributes[:quantity]) || BigDecimal("0"),
|
|
42
|
+
unit: attributes[:unit]&.to_s || dimension&.unit,
|
|
44
43
|
rate_amount: decimal_or_nil(attributes[:rate_amount]),
|
|
45
44
|
rate_quantity: decimal_or_nil(attributes[:rate_quantity]) || BigDecimal("1"),
|
|
46
45
|
cost: decimal_or_nil(attributes[:cost]),
|
|
47
|
-
currency: attributes[:currency]
|
|
46
|
+
currency: canonical_currency(attributes[:currency]),
|
|
48
47
|
cost_status: cost_status_for(attributes),
|
|
49
|
-
pricing_basis:
|
|
50
|
-
price_key: attributes[:price_key],
|
|
51
|
-
price_source:
|
|
48
|
+
pricing_basis: attributes[:pricing_basis]&.to_s,
|
|
49
|
+
price_key: attributes[:price_key]&.to_s,
|
|
50
|
+
price_source: attributes[:price_source]&.to_s,
|
|
52
51
|
price_source_version: attributes[:price_source_version],
|
|
53
52
|
provider_field: attributes[:provider_field],
|
|
54
53
|
provider_item_id: attributes[:provider_item_id],
|
|
@@ -62,14 +61,14 @@ module LlmCostTracker
|
|
|
62
61
|
token_usage.priced_quantities.filter_map do |key, quantity|
|
|
63
62
|
next unless quantity.positive?
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
dimension = Usage::Catalog.fetch(key)
|
|
66
65
|
build(
|
|
67
|
-
kind:
|
|
68
|
-
direction:
|
|
69
|
-
modality:
|
|
70
|
-
cache_state:
|
|
66
|
+
kind: dimension.kind,
|
|
67
|
+
direction: dimension.direction,
|
|
68
|
+
modality: dimension.modality,
|
|
69
|
+
cache_state: dimension.cache_state,
|
|
71
70
|
quantity: quantity,
|
|
72
|
-
unit:
|
|
71
|
+
unit: dimension.unit
|
|
73
72
|
)
|
|
74
73
|
end
|
|
75
74
|
end
|
|
@@ -84,17 +83,11 @@ module LlmCostTracker
|
|
|
84
83
|
cost.zero? ? CostStatus::FREE : CostStatus::COMPLETE
|
|
85
84
|
end
|
|
86
85
|
|
|
87
|
-
def self.
|
|
88
|
-
|
|
89
|
-
return nil unless
|
|
90
|
-
|
|
91
|
-
Components::BY_KEY[component_key.to_sym]
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def self.symbol_or_nil(value)
|
|
95
|
-
return nil if value.nil?
|
|
86
|
+
def self.dimension_for(attributes)
|
|
87
|
+
dimension_key = attributes[:dimension_key] || attributes[:price_key]
|
|
88
|
+
return nil unless dimension_key
|
|
96
89
|
|
|
97
|
-
|
|
90
|
+
Usage::Catalog[dimension_key.to_s]
|
|
98
91
|
end
|
|
99
92
|
|
|
100
93
|
def self.decimal_or_nil(value)
|
|
@@ -103,11 +96,11 @@ module LlmCostTracker
|
|
|
103
96
|
BigDecimal(value.to_s)
|
|
104
97
|
end
|
|
105
98
|
|
|
106
|
-
def self.
|
|
107
|
-
|
|
99
|
+
def self.canonical_currency(value)
|
|
100
|
+
(value || LlmCostTracker::DEFAULT_CURRENCY).to_s.upcase
|
|
108
101
|
end
|
|
109
102
|
|
|
110
|
-
private_class_method :cost_status_for, :
|
|
103
|
+
private_class_method :cost_status_for, :dimension_for, :decimal_or_nil, :canonical_currency
|
|
111
104
|
|
|
112
105
|
def billable?
|
|
113
106
|
quantity.positive?
|
|
@@ -122,7 +115,12 @@ module LlmCostTracker
|
|
|
122
115
|
end
|
|
123
116
|
|
|
124
117
|
def token?
|
|
125
|
-
unit ==
|
|
118
|
+
unit == "token"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def dimension
|
|
122
|
+
Usage::Catalog[price_key] ||
|
|
123
|
+
Usage::Catalog.token_priced_for(kind: kind, direction: direction, cache_state: cache_state)
|
|
126
124
|
end
|
|
127
125
|
|
|
128
126
|
def cost_value
|
|
@@ -130,18 +128,16 @@ module LlmCostTracker
|
|
|
130
128
|
end
|
|
131
129
|
|
|
132
130
|
def with_rate(rate)
|
|
133
|
-
|
|
134
|
-
rate_quantity = rate.fetch(:quantity)
|
|
135
|
-
applied_cost = (quantity / rate_quantity) * rate_amount
|
|
131
|
+
applied_cost = (quantity / rate.quantity) * rate.amount
|
|
136
132
|
with(
|
|
137
|
-
rate_amount:
|
|
138
|
-
rate_quantity:
|
|
133
|
+
rate_amount: rate.amount,
|
|
134
|
+
rate_quantity: rate.quantity,
|
|
139
135
|
cost: applied_cost,
|
|
140
|
-
currency: rate.
|
|
136
|
+
currency: rate.currency.upcase,
|
|
141
137
|
cost_status: applied_cost.zero? ? CostStatus::FREE : CostStatus::COMPLETE,
|
|
142
|
-
price_key: rate.
|
|
143
|
-
price_source: rate.
|
|
144
|
-
price_source_version: rate.
|
|
138
|
+
price_key: rate.source_key,
|
|
139
|
+
price_source: rate.source,
|
|
140
|
+
price_source_version: rate.source_version
|
|
145
141
|
)
|
|
146
142
|
end
|
|
147
143
|
|
|
@@ -17,8 +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 auto_enable_stream_usage cache_rollups
|
|
21
|
-
reconciliation_enabled].freeze
|
|
20
|
+
ingestion_pool_size auto_enable_stream_usage cache_rollups].freeze
|
|
22
21
|
ENUM_ATTRIBUTES = {
|
|
23
22
|
budget_exceeded_behavior: [BUDGET_EXCEEDED_BEHAVIORS, :notify],
|
|
24
23
|
unknown_pricing_behavior: [UNKNOWN_PRICING_BEHAVIORS, :warn],
|
|
@@ -35,8 +34,7 @@ module LlmCostTracker
|
|
|
35
34
|
:report_tag_breakdowns,
|
|
36
35
|
:redacted_tag_keys,
|
|
37
36
|
:unknown_pricing_behavior,
|
|
38
|
-
:openai_compatible_providers
|
|
39
|
-
:reconciliation_importers
|
|
37
|
+
:openai_compatible_providers
|
|
40
38
|
)
|
|
41
39
|
|
|
42
40
|
def initialize
|
|
@@ -58,36 +56,12 @@ module LlmCostTracker
|
|
|
58
56
|
@report_tag_breakdowns = []
|
|
59
57
|
@redacted_tag_keys = DEFAULT_REDACTED_TAG_KEYS.dup
|
|
60
58
|
self.openai_compatible_providers = OPENAI_COMPATIBLE_PROVIDERS
|
|
61
|
-
@reconciliation_importers = {}
|
|
62
|
-
@reconciliation_enabled = false
|
|
63
59
|
@auto_enable_stream_usage = true
|
|
64
60
|
self.ingestion = :inline
|
|
65
61
|
@cache_rollups = false
|
|
66
62
|
@finalized = false
|
|
67
63
|
end
|
|
68
64
|
|
|
69
|
-
def reconciliation_importers=(importers)
|
|
70
|
-
ensure_mutable!
|
|
71
|
-
raise Error, RECONCILIATION_DISABLED_MESSAGE unless @reconciliation_enabled
|
|
72
|
-
|
|
73
|
-
@reconciliation_importers = (importers || {}).to_h do |source, importer|
|
|
74
|
-
raise Error, "reconciliation_importers[#{source}] must respond to call" unless importer.respond_to?(:call)
|
|
75
|
-
|
|
76
|
-
[source.to_sym, importer]
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def register_reconciliation_importer(source, &block)
|
|
81
|
-
ensure_mutable!
|
|
82
|
-
raise Error, RECONCILIATION_DISABLED_MESSAGE unless @reconciliation_enabled
|
|
83
|
-
raise Error, "register_reconciliation_importer requires a block" unless block
|
|
84
|
-
|
|
85
|
-
@reconciliation_importers[source.to_sym] = block
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
RECONCILIATION_DISABLED_MESSAGE = "reconciliation is disabled; set config.reconciliation_enabled = true first"
|
|
89
|
-
private_constant :RECONCILIATION_DISABLED_MESSAGE
|
|
90
|
-
|
|
91
65
|
def openai_compatible_providers=(providers)
|
|
92
66
|
ensure_mutable!
|
|
93
67
|
@openai_compatible_providers = normalize_openai_compatible_providers(providers)
|
|
@@ -95,8 +69,8 @@ module LlmCostTracker
|
|
|
95
69
|
|
|
96
70
|
def pricing_overrides=(value)
|
|
97
71
|
ensure_mutable!
|
|
98
|
-
@pricing_overrides = Pricing::Registry.
|
|
99
|
-
rescue ArgumentError => e
|
|
72
|
+
@pricing_overrides = Pricing::Registry.normalize_price_entries(value || {}, context: "pricing_overrides")
|
|
73
|
+
rescue ArgumentError, TypeError => e
|
|
100
74
|
raise Error, "invalid pricing_overrides: #{e.message}"
|
|
101
75
|
end
|
|
102
76
|
|
|
@@ -112,7 +86,9 @@ module LlmCostTracker
|
|
|
112
86
|
|
|
113
87
|
def instrument(*names)
|
|
114
88
|
ensure_mutable!
|
|
115
|
-
|
|
89
|
+
names = names.flatten
|
|
90
|
+
names = Integrations.names if names == [:all]
|
|
91
|
+
@instrumented_integrations.merge(names)
|
|
116
92
|
end
|
|
117
93
|
|
|
118
94
|
def instrumented?(name)
|
|
@@ -150,6 +126,12 @@ module LlmCostTracker
|
|
|
150
126
|
Array(@redacted_tag_keys).map { |key| Tags::Sanitizer.normalized_key(key) }.freeze
|
|
151
127
|
end
|
|
152
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
|
+
|
|
153
135
|
def finalized?
|
|
154
136
|
@finalized
|
|
155
137
|
end
|
|
@@ -169,19 +151,6 @@ module LlmCostTracker
|
|
|
169
151
|
end
|
|
170
152
|
end
|
|
171
153
|
|
|
172
|
-
def normalize_instrumentation_names(names)
|
|
173
|
-
names = names.flatten
|
|
174
|
-
integrations = Integrations.names
|
|
175
|
-
return integrations if names == [:all]
|
|
176
|
-
|
|
177
|
-
names.each do |name|
|
|
178
|
-
next if integrations.include?(name)
|
|
179
|
-
|
|
180
|
-
raise Error, "Unknown integration: #{name.inspect}. Use one of: #{integrations.join(', ')}"
|
|
181
|
-
end
|
|
182
|
-
names
|
|
183
|
-
end
|
|
184
|
-
|
|
185
154
|
def ensure_mutable!
|
|
186
155
|
return unless finalized?
|
|
187
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,25 +1,18 @@
|
|
|
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
|
-
autoload :InvoiceReconciliationCheck, "llm_cost_tracker/doctor/invoice_reconciliation_check"
|
|
17
|
-
autoload :CaptureVerifier, "llm_cost_tracker/doctor/capture_verifier"
|
|
18
|
-
|
|
19
12
|
STATUS_GLYPHS = { ok: "✓", warn: "!", error: "x" }.freeze
|
|
20
13
|
STATUS_COLORS = { ok: 32, warn: 33, error: 31 }.freeze
|
|
21
14
|
|
|
22
|
-
SECTIONS = [
|
|
15
|
+
SECTIONS = %w[Setup Schema Operations].freeze
|
|
23
16
|
|
|
24
17
|
SECTION_FOR_CHECK = {
|
|
25
18
|
"configuration" => "Setup",
|
|
@@ -29,13 +22,6 @@ module LlmCostTracker
|
|
|
29
22
|
"llm_cost_tracker_calls columns" => "Schema",
|
|
30
23
|
"call line items" => "Schema",
|
|
31
24
|
"call tags" => "Schema",
|
|
32
|
-
"provider invoices" => "Schema",
|
|
33
|
-
"provider invoice imports" => "Schema",
|
|
34
|
-
"cost drift" => "Data integrity",
|
|
35
|
-
"pricing snapshot drift" => "Data integrity",
|
|
36
|
-
"pricing snapshot audit" => "Data integrity",
|
|
37
|
-
"cost status" => "Data integrity",
|
|
38
|
-
"invoice reconciliation" => "Data integrity",
|
|
39
25
|
"call rollups" => "Operations",
|
|
40
26
|
"inline ingestion" => "Operations",
|
|
41
27
|
"async ingestion" => "Operations",
|
|
@@ -103,12 +89,6 @@ module LlmCostTracker
|
|
|
103
89
|
table_check,
|
|
104
90
|
column_check,
|
|
105
91
|
*dependent_core_schema_checks,
|
|
106
|
-
*reconciliation_schema_checks,
|
|
107
|
-
CostDriftCheck.new.call,
|
|
108
|
-
PricingSnapshotDriftCheck.new.call,
|
|
109
|
-
*reconciliation_invoice_check,
|
|
110
|
-
LegacyBillingStatusCheck.new.call,
|
|
111
|
-
LegacyAuditCheck.new.call,
|
|
112
92
|
call_rollups_check,
|
|
113
93
|
IngestionCheck.new.call,
|
|
114
94
|
PriceCheck.new.call,
|
|
@@ -121,26 +101,11 @@ module LlmCostTracker
|
|
|
121
101
|
def dependent_core_schema_checks
|
|
122
102
|
Ledger::Schema::CORE_SCHEMAS.reject { |schema, _| schema == Ledger::Schema::Calls }.map do |schema, table|
|
|
123
103
|
SchemaCheck.new(name: table.delete_prefix("llm_cost_tracker_").tr("_", " "),
|
|
124
|
-
schema: schema,
|
|
104
|
+
schema: schema,
|
|
105
|
+
table: table).call
|
|
125
106
|
end
|
|
126
107
|
end
|
|
127
108
|
|
|
128
|
-
def reconciliation_schema_checks
|
|
129
|
-
return [] unless LlmCostTracker.reconciliation_enabled?
|
|
130
|
-
|
|
131
|
-
Reconciliation::SCHEMA_TABLES.map do |schema, table|
|
|
132
|
-
SchemaCheck.new(name: table.delete_prefix("llm_cost_tracker_").tr("_", " "),
|
|
133
|
-
schema: schema, table: table,
|
|
134
|
-
optional: false, install_command: "llm_cost_tracker:reconciliation").call
|
|
135
|
-
end.compact
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def reconciliation_invoice_check
|
|
139
|
-
return [] unless LlmCostTracker.reconciliation_enabled?
|
|
140
|
-
|
|
141
|
-
Array(InvoiceReconciliationCheck.new.call)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
109
|
def configuration_check
|
|
145
110
|
config = LlmCostTracker.configuration
|
|
146
111
|
Check.new(:ok, "configuration", "active_record ledger enabled=#{config.enabled}")
|
|
@@ -202,7 +167,7 @@ module LlmCostTracker
|
|
|
202
167
|
return live_rollups_check unless LlmCostTracker.configuration.cache_rollups
|
|
203
168
|
|
|
204
169
|
errors = LlmCostTracker::Ledger::Schema::CallRollups.current_schema_errors
|
|
205
|
-
return
|
|
170
|
+
return Check.new(:ok, "call rollups", "llm_cost_tracker_call_rollups exists") if errors.empty?
|
|
206
171
|
|
|
207
172
|
Check.new(
|
|
208
173
|
:error,
|
|
@@ -211,35 +176,6 @@ module LlmCostTracker
|
|
|
211
176
|
)
|
|
212
177
|
end
|
|
213
178
|
|
|
214
|
-
ROLLUPS_DRIFT_TOLERANCE_PERCENT = 1.0
|
|
215
|
-
private_constant :ROLLUPS_DRIFT_TOLERANCE_PERCENT
|
|
216
|
-
|
|
217
|
-
def rollups_drift_check
|
|
218
|
-
drift_window = Time.now.utc.beginning_of_day
|
|
219
|
-
calls_total = LlmCostTracker::Call
|
|
220
|
-
.where(tracked_at: drift_window..)
|
|
221
|
-
.where.not(total_cost: nil)
|
|
222
|
-
.sum(:total_cost)
|
|
223
|
-
rollup_total = LlmCostTracker::CallRollup
|
|
224
|
-
.where(period: "day", period_start: drift_window.to_date)
|
|
225
|
-
.sum(:total_cost)
|
|
226
|
-
return Check.new(:ok, "call rollups", "llm_cost_tracker_call_rollups exists") if calls_total.zero?
|
|
227
|
-
|
|
228
|
-
drift_percent = ((calls_total - rollup_total).abs * 100.0 / calls_total)
|
|
229
|
-
if drift_percent > ROLLUPS_DRIFT_TOLERANCE_PERCENT
|
|
230
|
-
return Check.new(
|
|
231
|
-
:warn, "call rollups",
|
|
232
|
-
"rollups drift detected: today's calls SUM=#{calls_total} vs rollups SUM=#{rollup_total} " \
|
|
233
|
-
"(#{drift_percent.round(2)}% > #{ROLLUPS_DRIFT_TOLERANCE_PERCENT}% threshold). " \
|
|
234
|
-
"Cached budget reads may understate spend until a rebuild."
|
|
235
|
-
)
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
Check.new(:ok, "call rollups", "llm_cost_tracker_call_rollups exists")
|
|
239
|
-
rescue StandardError => e
|
|
240
|
-
Check.new(:warn, "call rollups", "rollups drift check failed: #{e.class}: #{e.message}")
|
|
241
|
-
end
|
|
242
|
-
|
|
243
179
|
def live_rollups_check
|
|
244
180
|
if Probe.table_exists?("llm_cost_tracker_call_rollups")
|
|
245
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/mode"
|
|
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::Mode.normalize(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
|