llm_cost_tracker 0.8.0 → 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/CHANGELOG.md +108 -0
- data/README.md +12 -5
- data/app/assets/llm_cost_tracker/application.css +65 -5
- data/app/controllers/llm_cost_tracker/application_controller.rb +25 -33
- data/app/controllers/llm_cost_tracker/assets_controller.rb +1 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +5 -7
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +4 -0
- 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 +10 -0
- 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 +5 -1
- data/app/models/llm_cost_tracker/call.rb +0 -3
- data/app/models/llm_cost_tracker/call_line_item.rb +1 -5
- data/app/models/llm_cost_tracker/call_rollup.rb +0 -3
- data/app/models/llm_cost_tracker/call_tag.rb +0 -4
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +0 -4
- data/app/models/llm_cost_tracker/ingestion/lease.rb +0 -3
- data/app/models/llm_cost_tracker/provider_invoice.rb +7 -3
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +24 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +33 -4
- data/app/services/llm_cost_tracker/dashboard/filter.rb +6 -4
- data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -1
- data/app/views/llm_cost_tracker/calls/show.html.erb +25 -40
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -9
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +91 -52
- 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 +3 -0
- data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/show.html.erb +60 -0
- data/config/routes.rb +3 -2
- data/lib/llm_cost_tracker/billing/components.rb +45 -3
- data/lib/llm_cost_tracker/billing/components.yml +71 -0
- data/lib/llm_cost_tracker/billing/line_item.rb +1 -1
- data/lib/llm_cost_tracker/budget.rb +4 -2
- data/lib/llm_cost_tracker/capture/stream_collector.rb +93 -20
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +40 -5
- data/lib/llm_cost_tracker/configuration.rb +53 -1
- data/lib/llm_cost_tracker/dashboard_setup_state.rb +109 -0
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +2 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +26 -0
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +164 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +5 -2
- data/lib/llm_cost_tracker/doctor.rb +72 -3
- data/lib/llm_cost_tracker/engine.rb +9 -0
- data/lib/llm_cost_tracker/event.rb +1 -1
- 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 +13 -3
- 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 +5 -58
- 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/upgrade_call_tags_key_value_index_generator.rb +30 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb +29 -0
- data/lib/llm_cost_tracker/ingestion/inbox.rb +0 -1
- data/lib/llm_cost_tracker/ingestion/inline.rb +22 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +10 -2
- data/lib/llm_cost_tracker/ingestion.rb +48 -10
- data/lib/llm_cost_tracker/integrations/anthropic.rb +24 -5
- data/lib/llm_cost_tracker/integrations/base.rb +22 -5
- data/lib/llm_cost_tracker/integrations/openai.rb +300 -66
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +105 -6
- data/lib/llm_cost_tracker/integrations.rb +19 -1
- data/lib/llm_cost_tracker/ledger/period/totals.rb +21 -5
- data/lib/llm_cost_tracker/ledger/rollups.rb +24 -10
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +30 -1
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -3
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +17 -2
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +2 -0
- 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 +2 -2
- data/lib/llm_cost_tracker/ledger/store.rb +14 -14
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +37 -0
- data/lib/llm_cost_tracker/ledger/tags/query.rb +2 -1
- data/lib/llm_cost_tracker/ledger.rb +2 -1
- data/lib/llm_cost_tracker/masking.rb +39 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +88 -29
- data/lib/llm_cost_tracker/parsers/anthropic.rb +22 -7
- data/lib/llm_cost_tracker/parsers/base.rb +5 -1
- data/lib/llm_cost_tracker/parsers/gemini.rb +4 -0
- data/lib/llm_cost_tracker/parsers/openai.rb +16 -2
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +5 -1
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +49 -10
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +124 -53
- data/lib/llm_cost_tracker/prices.json +110 -19
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +5 -36
- data/lib/llm_cost_tracker/pricing/lookup.rb +36 -3
- data/lib/llm_cost_tracker/pricing/mode.rb +76 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +3 -1
- data/lib/llm_cost_tracker/pricing/service_charges.rb +9 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +50 -1
- data/lib/llm_cost_tracker/pricing/sync.rb +3 -1
- data/lib/llm_cost_tracker/pricing.rb +47 -19
- data/lib/llm_cost_tracker/railtie.rb +6 -0
- 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 +4 -1
- data/lib/llm_cost_tracker/retention.rb +15 -2
- data/lib/llm_cost_tracker/tags/context.rb +3 -4
- data/lib/llm_cost_tracker/tags/sanitizer.rb +60 -4
- data/lib/llm_cost_tracker/token_usage.rb +10 -2
- data/lib/llm_cost_tracker/tracker.rb +45 -18
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +9 -0
- data/lib/tasks/llm_cost_tracker.rake +25 -2
- metadata +36 -1
|
@@ -6,6 +6,7 @@ require "time"
|
|
|
6
6
|
require_relative "version"
|
|
7
7
|
require_relative "token_usage"
|
|
8
8
|
require_relative "billing/components"
|
|
9
|
+
require_relative "pricing/mode"
|
|
9
10
|
require_relative "pricing/registry"
|
|
10
11
|
require_relative "pricing/lookup"
|
|
11
12
|
require_relative "pricing/effective_prices"
|
|
@@ -13,7 +14,7 @@ require_relative "pricing/explainer"
|
|
|
13
14
|
require_relative "pricing/service_charges"
|
|
14
15
|
|
|
15
16
|
module LlmCostTracker
|
|
16
|
-
module Pricing
|
|
17
|
+
module Pricing # rubocop:disable Metrics/ModuleLength
|
|
17
18
|
extend ServiceCharges
|
|
18
19
|
|
|
19
20
|
STANDARD_MODE_VALUES = %i[auto default standard standard_only].freeze
|
|
@@ -24,7 +25,7 @@ module LlmCostTracker
|
|
|
24
25
|
def normalize_mode(value)
|
|
25
26
|
return nil if value.nil?
|
|
26
27
|
|
|
27
|
-
mode =
|
|
28
|
+
mode = normalize_string_mode(value.to_s)
|
|
28
29
|
return nil unless mode
|
|
29
30
|
|
|
30
31
|
STANDARD_MODE_VALUES.include?(mode) ? nil : mode
|
|
@@ -42,29 +43,26 @@ module LlmCostTracker
|
|
|
42
43
|
cost_from(calculation)
|
|
43
44
|
end
|
|
44
45
|
|
|
45
|
-
def
|
|
46
|
+
def calculate(provider:, model:, tokens:, line_items:, pricing_mode: nil)
|
|
46
47
|
calculation = calculation_for(
|
|
47
48
|
provider: provider,
|
|
48
49
|
model: model,
|
|
49
50
|
tokens: tokens,
|
|
50
51
|
pricing_mode: pricing_mode
|
|
51
52
|
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
cost_data = calculation && cost_from(calculation)
|
|
54
|
+
snapshot = calculation && snapshot_from(calculation)
|
|
55
|
+
priced = apply_calculation_to_line_items(line_items, calculation,
|
|
56
|
+
provider: provider, pricing_mode: pricing_mode)
|
|
57
|
+
[cost_data, snapshot, priced]
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
def price_line_items(provider:, model:, line_items:, pricing_mode: nil)
|
|
58
61
|
token_usage = TokenUsage.build_from_tokens(token_attributes_from(line_items))
|
|
59
62
|
calculation = calculation_for(provider: provider, model: model, tokens: token_usage, pricing_mode: pricing_mode)
|
|
60
63
|
snapshot = calculation && snapshot_from(calculation)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
next price_token_line_item(line_item, calculation) if line_item.unit == :token
|
|
64
|
-
|
|
65
|
-
price_service_charge_line_item(line_item, provider: provider, pricing_mode: pricing_mode)
|
|
66
|
-
end
|
|
67
|
-
|
|
64
|
+
priced = apply_calculation_to_line_items(line_items, calculation,
|
|
65
|
+
provider: provider, pricing_mode: pricing_mode)
|
|
68
66
|
[priced, snapshot]
|
|
69
67
|
end
|
|
70
68
|
|
|
@@ -91,7 +89,7 @@ module LlmCostTracker
|
|
|
91
89
|
|
|
92
90
|
def stored_cost_attributes(attributes)
|
|
93
91
|
value = attributes.to_h[:total_cost]
|
|
94
|
-
value
|
|
92
|
+
value ? { total_cost: value } : {}
|
|
95
93
|
end
|
|
96
94
|
|
|
97
95
|
private
|
|
@@ -109,7 +107,7 @@ module LlmCostTracker
|
|
|
109
107
|
cost = costs[component.key]
|
|
110
108
|
result[component.cost_key] = cost.round(8) unless cost.nil?
|
|
111
109
|
end
|
|
112
|
-
values[:total_cost] = costs.values.compact.sum.round(8)
|
|
110
|
+
values[:total_cost] = costs.values.compact.sum(BigDecimal("0")).round(8)
|
|
113
111
|
values
|
|
114
112
|
end
|
|
115
113
|
|
|
@@ -163,6 +161,17 @@ module LlmCostTracker
|
|
|
163
161
|
end
|
|
164
162
|
end
|
|
165
163
|
|
|
164
|
+
def apply_calculation_to_line_items(line_items, calculation, provider:, pricing_mode:)
|
|
165
|
+
line_items.map do |line_item|
|
|
166
|
+
next price_token_line_item(line_item, calculation) if line_item.unit == :token
|
|
167
|
+
|
|
168
|
+
price_service_charge_line_item(line_item,
|
|
169
|
+
provider: provider,
|
|
170
|
+
calculation: calculation,
|
|
171
|
+
pricing_mode: pricing_mode)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
166
175
|
def token_attributes_from(line_items)
|
|
167
176
|
line_items.each_with_object({}) do |line_item, totals|
|
|
168
177
|
next unless line_item.unit == :token
|
|
@@ -195,16 +204,35 @@ module LlmCostTracker
|
|
|
195
204
|
)
|
|
196
205
|
end
|
|
197
206
|
|
|
198
|
-
def price_service_charge_line_item(line_item, provider:, pricing_mode:)
|
|
207
|
+
def price_service_charge_line_item(line_item, provider:, calculation:, pricing_mode:)
|
|
199
208
|
return line_item if line_item.priced?
|
|
200
209
|
return line_item unless line_item.billable?
|
|
201
210
|
|
|
202
|
-
rate =
|
|
211
|
+
rate = model_rate_for(line_item, calculation) ||
|
|
212
|
+
charge_rate(provider: provider, component: line_item.kind, pricing_mode: pricing_mode)
|
|
203
213
|
return line_item unless rate
|
|
204
214
|
|
|
205
215
|
line_item.apply_rate(rate)
|
|
206
216
|
end
|
|
207
217
|
|
|
218
|
+
def model_rate_for(line_item, calculation)
|
|
219
|
+
return nil unless calculation
|
|
220
|
+
|
|
221
|
+
match = calculation[:match]
|
|
222
|
+
amount = match.prices[line_item.kind] || match.prices[line_item.kind.to_s]
|
|
223
|
+
return nil unless amount.is_a?(Numeric)
|
|
224
|
+
|
|
225
|
+
component = Billing::Components::BY_KEY[line_item.kind]
|
|
226
|
+
{
|
|
227
|
+
amount: BigDecimal(amount.to_s),
|
|
228
|
+
quantity: BigDecimal(Billing::RATE_BASIS_QUANTITIES.fetch(component.rate_basis).to_s),
|
|
229
|
+
currency: "USD",
|
|
230
|
+
source: match.source,
|
|
231
|
+
source_key: "#{match.key}.#{line_item.kind}",
|
|
232
|
+
source_version: source_version_for(match.source)
|
|
233
|
+
}
|
|
234
|
+
end
|
|
235
|
+
|
|
208
236
|
def component_for_line_item(line_item)
|
|
209
237
|
Billing::Components::REGISTRY.find do |component|
|
|
210
238
|
component.kind == line_item.kind &&
|
|
@@ -230,10 +258,10 @@ module LlmCostTracker
|
|
|
230
258
|
end
|
|
231
259
|
|
|
232
260
|
def token_cost(tokens, per_million_price)
|
|
233
|
-
return 0
|
|
261
|
+
return BigDecimal("0") if tokens.zero?
|
|
234
262
|
return nil if per_million_price.nil?
|
|
235
263
|
|
|
236
|
-
(tokens * per_million_price) / RATE_DENOMINATOR_TOKENS
|
|
264
|
+
(BigDecimal(tokens.to_s) * BigDecimal(per_million_price.to_s)) / RATE_DENOMINATOR_TOKENS
|
|
237
265
|
end
|
|
238
266
|
end
|
|
239
267
|
end
|
|
@@ -11,6 +11,12 @@ module LlmCostTracker
|
|
|
11
11
|
generators do
|
|
12
12
|
require_relative "generators/llm_cost_tracker/install_generator"
|
|
13
13
|
require_relative "generators/llm_cost_tracker/prices_generator"
|
|
14
|
+
require_relative "generators/llm_cost_tracker/call_rollups_generator"
|
|
15
|
+
require_relative "generators/llm_cost_tracker/durable_ingestion_generator"
|
|
16
|
+
require_relative "generators/llm_cost_tracker/reconciliation_generator"
|
|
17
|
+
require_relative "generators/llm_cost_tracker/upgrade_call_rollups_provider_generator"
|
|
18
|
+
require_relative "generators/llm_cost_tracker/upgrade_image_tokens_generator"
|
|
19
|
+
require_relative "generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator"
|
|
14
20
|
end
|
|
15
21
|
|
|
16
22
|
rake_tasks do
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "reconciliation"
|
|
6
|
+
|
|
7
|
+
module LlmCostTracker
|
|
8
|
+
module ReconcileTasks
|
|
9
|
+
SOURCE_PARSERS = {
|
|
10
|
+
"openai" => Reconciliation::Sources::OpenaiUsage,
|
|
11
|
+
"anthropic" => Reconciliation::Sources::AnthropicUsage
|
|
12
|
+
}.freeze
|
|
13
|
+
GENERIC_SOURCES = %w[csv].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def run_import(env: ENV, output: $stdout, error_output: $stderr)
|
|
18
|
+
result = import_from_env(env: env)
|
|
19
|
+
output.puts "llm_cost_tracker: imported #{result.total_imported} rows " \
|
|
20
|
+
"(inserted=#{result.inserted}, updated=#{result.updated}, skipped=#{result.skipped})"
|
|
21
|
+
result.errors.each { |error| error_output.puts " error: #{error}" }
|
|
22
|
+
raise "llm_cost_tracker: reconcile import had errors" unless result.success?
|
|
23
|
+
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run_diff(env: ENV, output: $stdout)
|
|
28
|
+
diff = diff_from_env(env: env)
|
|
29
|
+
print_diff(diff, output: output)
|
|
30
|
+
diff
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def import_from_env(env: ENV)
|
|
34
|
+
source = required_env(env, "SOURCE")
|
|
35
|
+
input_path = required_env(env, "INPUT")
|
|
36
|
+
raise ArgumentError, "INPUT file not found: #{input_path}" unless File.exist?(input_path)
|
|
37
|
+
|
|
38
|
+
payload = JSON.parse(File.read(input_path))
|
|
39
|
+
rows = parse_rows(source: source, payload: payload)
|
|
40
|
+
Reconciliation.import(source: source.to_sym, rows: rows, provider: env["PROVIDER"])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def diff_from_env(env: ENV)
|
|
44
|
+
source = required_env(env, "SOURCE")
|
|
45
|
+
period_start = Date.parse(required_env(env, "PERIOD_START"))
|
|
46
|
+
period_end = Date.parse(required_env(env, "PERIOD_END"))
|
|
47
|
+
Reconciliation.diff(source: source.to_sym, period_start: period_start, period_end: period_end,
|
|
48
|
+
provider: env["PROVIDER"],
|
|
49
|
+
drilldown_limit: parse_drilldown_limit(env["DRILLDOWN_LIMIT"]))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse_drilldown_limit(value)
|
|
53
|
+
return Reconciliation::Diff::DEFAULT_DRILLDOWN_LIMIT if value.nil? || value.to_s.empty?
|
|
54
|
+
return nil if value.to_s.downcase == "all"
|
|
55
|
+
|
|
56
|
+
Integer(value)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def print_diff(diff, output: $stdout)
|
|
60
|
+
output.puts "llm_cost_tracker: reconciliation diff for #{diff.source} " \
|
|
61
|
+
"#{diff.period_start}..#{diff.period_end}"
|
|
62
|
+
output.puts " provider_total: #{diff.provider_total.to_s('F')} #{diff.currency}"
|
|
63
|
+
output.puts " local_total: #{diff.local_total.to_s('F')} #{diff.currency} " \
|
|
64
|
+
"(from #{diff.local_total_source})"
|
|
65
|
+
output.puts " delta: #{diff.delta_amount.to_s('F')} (#{diff.delta_percent || 'n/a'}%)"
|
|
66
|
+
print_unmatched_provider_rows(diff, output)
|
|
67
|
+
print_unmatched_local_calls(diff, output)
|
|
68
|
+
print_non_cost_rows(diff, output)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_rows(source:, payload:)
|
|
72
|
+
parser = SOURCE_PARSERS[source.to_s]
|
|
73
|
+
return parser.parse(payload) if parser
|
|
74
|
+
return Array(payload["rows"]) if GENERIC_SOURCES.include?(source.to_s)
|
|
75
|
+
|
|
76
|
+
known = (SOURCE_PARSERS.keys + GENERIC_SOURCES).join(", ")
|
|
77
|
+
raise ArgumentError, "unknown SOURCE #{source.inspect}; known sources: #{known}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def required_env(env, key)
|
|
81
|
+
value = env[key].to_s.strip
|
|
82
|
+
raise ArgumentError, "missing #{key}" if value.empty?
|
|
83
|
+
|
|
84
|
+
value
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def print_unmatched_provider_rows(diff, output)
|
|
88
|
+
return if diff.unmatched_provider_rows.empty?
|
|
89
|
+
|
|
90
|
+
output.puts " unmatched provider rows#{truncation_suffix(diff.unmatched_provider_rows.size,
|
|
91
|
+
diff.unmatched_provider_rows_total)}:"
|
|
92
|
+
diff.unmatched_provider_rows.each do |row|
|
|
93
|
+
output.puts " #{row[:external_id]} (#{row[:match_basis]}): " \
|
|
94
|
+
"#{format_amount(row[:billed_amount])} #{format_attribution(row[:attribution])}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def print_unmatched_local_calls(diff, output)
|
|
99
|
+
return if diff.unmatched_local_calls.empty?
|
|
100
|
+
|
|
101
|
+
output.puts " unmatched local calls#{truncation_suffix(diff.unmatched_local_calls.size,
|
|
102
|
+
diff.unmatched_local_calls_total)}:"
|
|
103
|
+
diff.unmatched_local_calls.each do |row|
|
|
104
|
+
output.puts " #{row[:count]} calls / #{row[:total_cost].to_s('F')} " \
|
|
105
|
+
"#{format_attribution(row[:attribution])}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def print_non_cost_rows(diff, output)
|
|
110
|
+
return if diff.non_cost_rows.empty?
|
|
111
|
+
|
|
112
|
+
output.puts " non-cost evidence#{truncation_suffix(diff.non_cost_rows.size,
|
|
113
|
+
diff.non_cost_rows_total)}:"
|
|
114
|
+
diff.non_cost_rows.each do |row|
|
|
115
|
+
output.puts " [#{row[:row_type]}/#{row[:meter]}] #{format_amount(row[:billed_amount])} " \
|
|
116
|
+
"#{format_attribution(row[:attribution])}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def truncation_suffix(shown, total)
|
|
121
|
+
return "" if shown >= total
|
|
122
|
+
|
|
123
|
+
" (showing #{shown} of #{total} — pass DRILLDOWN_LIMIT=all to see every row)"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def format_amount(value)
|
|
127
|
+
value.nil? ? "n/a" : value.to_s("F")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def format_attribution(attribution)
|
|
131
|
+
LlmCostTracker::Masking.format_attribution(attribution, separator: ",")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|