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.
Files changed (209) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +11 -5
  4. data/app/assets/llm_cost_tracker/application.css +784 -802
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +14 -2
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +28 -21
  7. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -4
  8. data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
  9. data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
  10. data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
  11. data/app/helpers/llm_cost_tracker/application_helper.rb +19 -16
  12. data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
  13. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
  14. data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
  15. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
  16. data/app/models/llm_cost_tracker/call.rb +28 -63
  17. data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
  18. data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
  19. data/app/models/llm_cost_tracker/call_tag.rb +0 -2
  20. data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +2 -0
  21. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +64 -43
  22. data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
  23. data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
  24. data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
  25. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
  26. data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
  27. data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +81 -0
  28. data/app/services/llm_cost_tracker/dashboard/setup_state.rb +6 -68
  29. data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
  30. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +20 -12
  31. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
  32. data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
  33. data/app/views/layouts/llm_cost_tracker/application.html.erb +74 -17
  34. data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
  35. data/app/views/llm_cost_tracker/calls/show.html.erb +132 -125
  36. data/app/views/llm_cost_tracker/dashboard/index.html.erb +120 -159
  37. data/app/views/llm_cost_tracker/data_quality/index.html.erb +140 -194
  38. data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
  39. data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
  40. data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
  41. data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
  42. data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
  43. data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
  44. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
  45. data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
  46. data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
  47. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
  48. data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
  49. data/app/views/llm_cost_tracker/tags/show.html.erb +85 -104
  50. data/config/routes.rb +3 -3
  51. data/lib/llm_cost_tracker/budget.rb +25 -28
  52. data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
  53. data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +2 -1
  54. data/lib/llm_cost_tracker/capture/stream_collector.rb +30 -52
  55. data/lib/llm_cost_tracker/capture/stream_tracker.rb +18 -33
  56. data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
  57. data/lib/llm_cost_tracker/charges/cost.rb +27 -0
  58. data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
  59. data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +40 -44
  60. data/lib/llm_cost_tracker/check.rb +5 -0
  61. data/lib/llm_cost_tracker/configuration.rb +13 -61
  62. data/lib/llm_cost_tracker/currency.rb +5 -0
  63. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
  64. data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
  65. data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
  66. data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
  67. data/lib/llm_cost_tracker/doctor.rb +66 -64
  68. data/lib/llm_cost_tracker/engine.rb +4 -4
  69. data/lib/llm_cost_tracker/event.rb +12 -20
  70. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
  71. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
  72. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
  73. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -2
  74. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +4 -0
  75. data/lib/llm_cost_tracker/ingestion/batch.rb +39 -8
  76. data/lib/llm_cost_tracker/ingestion/inbox.rb +8 -9
  77. data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
  78. data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
  79. data/lib/llm_cost_tracker/ingestion.rb +24 -36
  80. data/lib/llm_cost_tracker/integrations/anthropic.rb +94 -116
  81. data/lib/llm_cost_tracker/integrations/base.rb +39 -57
  82. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
  83. data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
  84. data/lib/llm_cost_tracker/integrations/openai.rb +72 -332
  85. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +89 -145
  86. data/lib/llm_cost_tracker/integrations.rb +32 -25
  87. data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
  88. data/lib/llm_cost_tracker/ledger/period.rb +5 -10
  89. data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
  90. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
  91. data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
  92. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
  93. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
  94. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
  95. data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
  96. data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
  97. data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
  98. data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
  99. data/lib/llm_cost_tracker/ledger/store.rb +18 -42
  100. data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
  101. data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
  102. data/lib/llm_cost_tracker/ledger.rb +14 -11
  103. data/lib/llm_cost_tracker/logging.rb +4 -21
  104. data/lib/llm_cost_tracker/middleware/faraday.rb +63 -51
  105. data/lib/llm_cost_tracker/parsers.rb +140 -29
  106. data/lib/llm_cost_tracker/prices.json +1707 -1
  107. data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
  108. data/lib/llm_cost_tracker/pricing/calculation.rb +260 -0
  109. data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
  110. data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
  111. data/lib/llm_cost_tracker/pricing/matcher.rb +84 -0
  112. data/lib/llm_cost_tracker/pricing/mode.rb +53 -35
  113. data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
  114. data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
  115. data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
  116. data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
  117. data/lib/llm_cost_tracker/pricing/source.rb +7 -0
  118. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
  119. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
  120. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
  121. data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
  122. data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
  123. data/lib/llm_cost_tracker/pricing.rb +10 -295
  124. data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
  125. data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
  126. data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
  127. data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
  128. data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
  129. data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
  130. data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
  131. data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
  132. data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
  133. data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
  134. data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
  135. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
  136. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +181 -0
  137. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
  138. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
  139. data/lib/llm_cost_tracker/providers.rb +35 -0
  140. data/lib/llm_cost_tracker/railtie.rb +0 -7
  141. data/lib/llm_cost_tracker/report/data.rb +3 -4
  142. data/lib/llm_cost_tracker/report/formatter.rb +33 -20
  143. data/lib/llm_cost_tracker/report.rb +1 -1
  144. data/lib/llm_cost_tracker/retention.rb +6 -19
  145. data/lib/llm_cost_tracker/tags/context.rb +9 -6
  146. data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
  147. data/lib/llm_cost_tracker/timing.rb +2 -4
  148. data/lib/llm_cost_tracker/tracker.rb +24 -36
  149. data/lib/llm_cost_tracker/usage/catalog.rb +58 -0
  150. data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
  151. data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +24 -46
  152. data/lib/llm_cost_tracker/usage/source.rb +14 -0
  153. data/lib/llm_cost_tracker/usage/token_usage.rb +100 -0
  154. data/lib/llm_cost_tracker/version.rb +1 -1
  155. data/lib/llm_cost_tracker.rb +43 -52
  156. data/lib/tasks/llm_cost_tracker.rake +14 -73
  157. metadata +92 -58
  158. data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -106
  159. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
  160. data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
  161. data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
  162. data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
  163. data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -183
  164. data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
  165. data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
  166. data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
  167. data/lib/llm_cost_tracker/billing/components.rb +0 -95
  168. data/lib/llm_cost_tracker/capture/stream.rb +0 -9
  169. data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
  170. data/lib/llm_cost_tracker/doctor/check.rb +0 -7
  171. data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
  172. data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
  173. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
  174. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
  175. data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
  176. data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
  177. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
  178. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -32
  179. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -25
  180. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
  181. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
  182. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
  183. data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
  184. data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
  185. data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
  186. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
  187. data/lib/llm_cost_tracker/masking.rb +0 -39
  188. data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -193
  189. data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
  190. data/lib/llm_cost_tracker/parsers/base.rb +0 -131
  191. data/lib/llm_cost_tracker/parsers/gemini.rb +0 -232
  192. data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
  193. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -51
  194. data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -155
  195. data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
  196. data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
  197. data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
  198. data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
  199. data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
  200. data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -134
  201. data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
  202. data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
  203. data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
  204. data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -254
  205. data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -172
  206. data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
  207. data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -142
  208. data/lib/llm_cost_tracker/reconciliation.rb +0 -118
  209. data/lib/llm_cost_tracker/token_usage.rb +0 -93
@@ -1,206 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_support/core_ext/object/blank"
4
- require "bigdecimal"
5
- require "time"
6
- require "yaml"
7
-
8
- require_relative "../billing/components"
9
- require_relative "registry"
10
-
11
- module LlmCostTracker
12
- module Pricing
13
- module ServiceCharges
14
- extend self
15
-
16
- DEFAULT_CURRENCY = "USD"
17
- EMPTY_RATES = {}.freeze
18
- MUTEX = Mutex.new
19
-
20
- def reset!
21
- MUTEX.synchronize do
22
- @builtin_rates = nil
23
- @file_rates_cache = nil
24
- end
25
- end
26
-
27
- def builtin_rates
28
- cached = @builtin_rates
29
- return cached if cached
30
-
31
- MUTEX.synchronize do
32
- @builtin_rates ||= begin
33
- registry = YAML.safe_load_file(Registry::DEFAULT_PRICES_PATH, aliases: false) || {}
34
- rates_from_registry(registry).freeze
35
- end
36
- end
37
- end
38
-
39
- def file_rates(path)
40
- return EMPTY_RATES unless path
41
-
42
- cache_key = [path, File.mtime(path)]
43
- cached = @file_rates_cache
44
- return cached[:value] if cached && cached[:key] == cache_key
45
-
46
- MUTEX.synchronize do
47
- cached = @file_rates_cache
48
- return cached[:value] if cached && cached[:key] == cache_key
49
-
50
- registry = YAML.safe_load_file(path, aliases: false) || {}
51
- value = rates_from_registry(registry, context: path).freeze
52
- @file_rates_cache = { key: cache_key, value: value }.freeze
53
- value
54
- end
55
- rescue Errno::ENOENT, Psych::Exception, ArgumentError, TypeError => e
56
- raise Error, "Unable to load prices_file #{path.inspect}: #{e.message}"
57
- end
58
-
59
- def rates_from_registry(registry, context: "price registry")
60
- data = registry.fetch("service_charges", EMPTY_RATES)
61
- raise ArgumentError, "#{context} service_charges must be a hash" unless data.is_a?(Hash)
62
-
63
- currency = registry.dig("metadata", "currency") || DEFAULT_CURRENCY
64
- data.each_with_object({}) do |(provider, entries), rates|
65
- section_context = "#{context} service_charges.#{provider}"
66
- rates[provider] = rates_from_section(entries, currency: currency, context: section_context)
67
- end
68
- end
69
-
70
- def charge_rate(provider:, component:, pricing_mode:)
71
- pricing_mode = Pricing.normalize_mode(pricing_mode)
72
- match = charge_rate_match(provider: provider, component: component, pricing_mode: pricing_mode)
73
- return nil unless match
74
-
75
- rate = match.fetch(:rate)
76
- {
77
- amount: rate.fetch(:amount),
78
- quantity: rate.fetch(:quantity),
79
- currency: rate.fetch(:currency),
80
- source: match.fetch(:source),
81
- source_key: match.fetch(:key),
82
- source_version: rate_source_version_for(match.fetch(:source))
83
- }
84
- end
85
-
86
- private
87
-
88
- def rates_from_section(entries, currency:, context:)
89
- raise ArgumentError, "#{context} must be a hash" unless entries.is_a?(Hash)
90
-
91
- entries.each_with_object({}) do |(key, amount), rates|
92
- key = key.name if key.is_a?(Symbol)
93
- component, tier = component_and_tier_for(key, context: context)
94
- amount = amount_for(key, amount, context: context)
95
-
96
- rate = {
97
- amount: amount,
98
- quantity: rate_quantity(component),
99
- currency: currency,
100
- source_key: key
101
- }
102
- component_rates = rates[component.key] ||= { tiers: {} }
103
- (tier ? component_rates[:tiers] : component_rates)[tier || :default] = rate
104
- end
105
- end
106
-
107
- def component_and_tier_for(key, context:)
108
- Billing::Components::REGISTRY.each do |component|
109
- next if component.token_key
110
-
111
- return [component, nil] if key == component.key.name
112
-
113
- suffix = "_#{component.key.name}"
114
- next unless key.end_with?(suffix)
115
-
116
- tier = key.delete_suffix(suffix)
117
- return [component, :"#{tier}"] unless tier.empty?
118
- end
119
-
120
- raise ArgumentError, "service charge price key #{key.inspect} in #{context} uses unknown billing component"
121
- end
122
-
123
- def amount_for(key, amount, context:)
124
- value = BigDecimal(amount.to_s)
125
- if value.infinite? || value.nan?
126
- raise ArgumentError,
127
- "service charge price amount for #{key.inspect} in #{context} must be finite"
128
- end
129
- if value.negative?
130
- raise ArgumentError,
131
- "service charge price amount for #{key.inspect} in #{context} must be non-negative"
132
- end
133
-
134
- value
135
- end
136
-
137
- def rate_quantity(component)
138
- BigDecimal(Billing::RATE_BASIS_QUANTITIES.fetch(component.rate_basis, 1).to_s)
139
- end
140
-
141
- def charge_rate_match(provider:, component:, pricing_mode:)
142
- provider_name = provider.is_a?(Symbol) ? provider.name : provider.presence
143
- return nil unless provider_name
144
-
145
- component_key = charge_component_key(component)
146
-
147
- table = ServiceCharges.file_rates(LlmCostTracker.configuration.prices_file)
148
- provider_table = table.fetch(provider_name, EMPTY_RATES)
149
- rate = rate_for(provider_table, component_key: component_key, pricing_mode: pricing_mode)
150
- if rate
151
- return {
152
- source: :prices_file,
153
- key: "service_charges.#{provider_name}.#{rate.fetch(:source_key)}",
154
- rate: rate
155
- }
156
- end
157
-
158
- table = ServiceCharges.builtin_rates
159
- provider_table = table.fetch(provider_name, EMPTY_RATES)
160
- rate = rate_for(provider_table, component_key: component_key, pricing_mode: pricing_mode)
161
- return unless rate
162
-
163
- {
164
- source: :bundled,
165
- key: "service_charges.#{provider_name}.#{rate.fetch(:source_key)}",
166
- rate: rate
167
- }
168
- end
169
-
170
- def rate_for(provider_table, component_key:, pricing_mode:)
171
- component_rates = provider_table.fetch(component_key, EMPTY_RATES)
172
- tier_rates = component_rates.fetch(:tiers, EMPTY_RATES)
173
- if pricing_mode
174
- rate = tier_rates[pricing_mode]
175
- return rate if rate
176
-
177
- name = pricing_mode.name
178
- tier_rates.each do |candidate, candidate_rate|
179
- return candidate_rate if tier_includes?(name, candidate.name)
180
- end
181
- end
182
- component_rates[:default]
183
- end
184
-
185
- def tier_includes?(tier_name, candidate_name)
186
- tier_name == candidate_name ||
187
- tier_name.start_with?("#{candidate_name}_") ||
188
- tier_name.end_with?("_#{candidate_name}") ||
189
- tier_name.include?("_#{candidate_name}_")
190
- end
191
-
192
- def charge_component_key(component)
193
- billing_component = Billing::Components::BY_KEY[component]
194
- return billing_component.key if billing_component && billing_component.token_key.nil?
195
-
196
- raise Error, "Unknown billing component: #{component.inspect}"
197
- end
198
-
199
- def rate_source_version_for(source)
200
- return LlmCostTracker::VERSION if source == :bundled
201
-
202
- Lookup.prices_file_mtime_iso
203
- end
204
- end
205
- end
206
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module Providers
5
- module Anthropic
6
- module TierClassification
7
- DATA_RESIDENCY_GEOS = %w[us].freeze
8
- STANDARD_EQUIVALENT_SERVICE_TIERS = %w[standard standard_only priority].freeze
9
-
10
- module_function
11
-
12
- def data_residency_geo?(geo)
13
- DATA_RESIDENCY_GEOS.include?(geo.to_s.downcase)
14
- end
15
-
16
- def standard_equivalent_tier?(service_tier)
17
- STANDARD_EQUIVALENT_SERVICE_TIERS.include?(service_tier.to_s)
18
- end
19
- end
20
- end
21
- end
22
- end
@@ -1,134 +0,0 @@
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