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.
Files changed (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +7 -4
  4. data/app/assets/llm_cost_tracker/application.css +8 -7
  5. data/app/controllers/llm_cost_tracker/calls_controller.rb +5 -5
  6. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -1
  7. data/app/controllers/llm_cost_tracker/pricing_controller.rb +1 -1
  8. data/app/helpers/llm_cost_tracker/application_helper.rb +6 -15
  9. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
  10. data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +4 -4
  11. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
  12. data/app/models/llm_cost_tracker/call.rb +28 -63
  13. data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
  14. data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
  15. data/app/models/llm_cost_tracker/call_tag.rb +0 -2
  16. data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +2 -0
  17. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +64 -43
  18. data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
  19. data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
  20. data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
  21. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
  22. data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
  23. data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +30 -44
  24. data/app/services/llm_cost_tracker/dashboard/setup_state.rb +4 -60
  25. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +1 -7
  26. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
  27. data/app/views/layouts/llm_cost_tracker/application.html.erb +0 -6
  28. data/app/views/llm_cost_tracker/calls/index.html.erb +8 -8
  29. data/app/views/llm_cost_tracker/calls/show.html.erb +31 -23
  30. data/app/views/llm_cost_tracker/dashboard/index.html.erb +8 -8
  31. data/app/views/llm_cost_tracker/data_quality/index.html.erb +62 -117
  32. data/app/views/llm_cost_tracker/models/index.html.erb +5 -5
  33. data/app/views/llm_cost_tracker/pricing/index.html.erb +2 -2
  34. data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -1
  35. data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -1
  36. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -1
  37. data/app/views/llm_cost_tracker/tags/index.html.erb +3 -3
  38. data/app/views/llm_cost_tracker/tags/show.html.erb +10 -10
  39. data/config/routes.rb +2 -3
  40. data/lib/llm_cost_tracker/budget.rb +24 -26
  41. data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
  42. data/lib/llm_cost_tracker/capture/sse.rb +1 -0
  43. data/lib/llm_cost_tracker/capture/stream_collector.rb +28 -36
  44. data/lib/llm_cost_tracker/capture/stream_tracker.rb +17 -28
  45. data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
  46. data/lib/llm_cost_tracker/charges/cost.rb +27 -0
  47. data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
  48. data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +40 -44
  49. data/lib/llm_cost_tracker/check.rb +5 -0
  50. data/lib/llm_cost_tracker/configuration.rb +13 -44
  51. data/lib/llm_cost_tracker/currency.rb +5 -0
  52. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
  53. data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
  54. data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
  55. data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
  56. data/lib/llm_cost_tracker/doctor.rb +5 -69
  57. data/lib/llm_cost_tracker/engine.rb +4 -4
  58. data/lib/llm_cost_tracker/event.rb +12 -20
  59. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
  60. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
  61. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -2
  63. data/lib/llm_cost_tracker/ingestion/batch.rb +39 -8
  64. data/lib/llm_cost_tracker/ingestion/inbox.rb +7 -8
  65. data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
  66. data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
  67. data/lib/llm_cost_tracker/ingestion.rb +24 -36
  68. data/lib/llm_cost_tracker/integrations/anthropic.rb +92 -106
  69. data/lib/llm_cost_tracker/integrations/base.rb +39 -57
  70. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
  71. data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
  72. data/lib/llm_cost_tracker/integrations/openai.rb +70 -276
  73. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +87 -99
  74. data/lib/llm_cost_tracker/integrations.rb +32 -25
  75. data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
  76. data/lib/llm_cost_tracker/ledger/period.rb +5 -10
  77. data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
  78. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
  79. data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
  80. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
  81. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
  82. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
  83. data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
  84. data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
  85. data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
  86. data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
  87. data/lib/llm_cost_tracker/ledger/store.rb +18 -42
  88. data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
  89. data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
  90. data/lib/llm_cost_tracker/ledger.rb +8 -18
  91. data/lib/llm_cost_tracker/logging.rb +4 -21
  92. data/lib/llm_cost_tracker/middleware/faraday.rb +61 -50
  93. data/lib/llm_cost_tracker/parsers.rb +139 -26
  94. data/lib/llm_cost_tracker/prices.json +1707 -1
  95. data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
  96. data/lib/llm_cost_tracker/pricing/calculation.rb +260 -0
  97. data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
  98. data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
  99. data/lib/llm_cost_tracker/pricing/matcher.rb +84 -0
  100. data/lib/llm_cost_tracker/pricing/mode.rb +40 -52
  101. data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
  102. data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
  103. data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
  104. data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
  105. data/lib/llm_cost_tracker/pricing/source.rb +7 -0
  106. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
  107. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
  108. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
  109. data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
  110. data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
  111. data/lib/llm_cost_tracker/pricing.rb +10 -278
  112. data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
  113. data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
  114. data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
  115. data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
  116. data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
  117. data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
  118. data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
  119. data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
  120. data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
  121. data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
  122. data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
  123. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
  124. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +63 -39
  125. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
  126. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
  127. data/lib/llm_cost_tracker/providers.rb +35 -0
  128. data/lib/llm_cost_tracker/railtie.rb +0 -3
  129. data/lib/llm_cost_tracker/report/data.rb +3 -4
  130. data/lib/llm_cost_tracker/report/formatter.rb +1 -1
  131. data/lib/llm_cost_tracker/report.rb +1 -1
  132. data/lib/llm_cost_tracker/retention.rb +6 -19
  133. data/lib/llm_cost_tracker/tags/context.rb +9 -6
  134. data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
  135. data/lib/llm_cost_tracker/timing.rb +2 -4
  136. data/lib/llm_cost_tracker/tracker.rb +24 -36
  137. data/lib/llm_cost_tracker/usage/catalog.rb +58 -0
  138. data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
  139. data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +24 -46
  140. data/lib/llm_cost_tracker/usage/source.rb +14 -0
  141. data/lib/llm_cost_tracker/usage/token_usage.rb +100 -0
  142. data/lib/llm_cost_tracker/version.rb +1 -1
  143. data/lib/llm_cost_tracker.rb +43 -52
  144. data/lib/tasks/llm_cost_tracker.rake +14 -73
  145. metadata +81 -55
  146. data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -100
  147. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
  148. data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
  149. data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
  150. data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
  151. data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -174
  152. data/lib/llm_cost_tracker/billing/components.rb +0 -95
  153. data/lib/llm_cost_tracker/capture/stream.rb +0 -9
  154. data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
  155. data/lib/llm_cost_tracker/doctor/check.rb +0 -7
  156. data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
  157. data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
  158. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
  159. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
  160. data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
  161. data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
  162. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
  163. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -36
  164. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -27
  165. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
  166. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
  167. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
  168. data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
  169. data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
  170. data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
  171. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
  172. data/lib/llm_cost_tracker/masking.rb +0 -39
  173. data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -176
  174. data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
  175. data/lib/llm_cost_tracker/parsers/base.rb +0 -131
  176. data/lib/llm_cost_tracker/parsers/gemini.rb +0 -230
  177. data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
  178. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -45
  179. data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
  180. data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
  181. data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
  182. data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
  183. data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +0 -15
  184. data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
  185. data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -131
  186. data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
  187. data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
  188. data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
  189. data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -249
  190. data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -148
  191. data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +0 -40
  192. data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
  193. data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -118
  194. data/lib/llm_cost_tracker/reconciliation.rb +0 -118
  195. data/lib/llm_cost_tracker/token_usage.rb +0 -93
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_cost_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergii Khomenko
@@ -103,6 +103,20 @@ dependencies:
103
103
  - - "<"
104
104
  - !ruby/object:Gem::Version
105
105
  version: '9.0'
106
+ - !ruby/object:Gem::Dependency
107
+ name: anthropic
108
+ requirement: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - "~>"
111
+ - !ruby/object:Gem::Version
112
+ version: '1.42'
113
+ type: :development
114
+ prerelease: false
115
+ version_requirements: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - "~>"
118
+ - !ruby/object:Gem::Version
119
+ version: '1.42'
106
120
  - !ruby/object:Gem::Dependency
107
121
  name: nokogiri
108
122
  requirement: !ruby/object:Gem::Requirement
@@ -117,6 +131,20 @@ dependencies:
117
131
  - - "~>"
118
132
  - !ruby/object:Gem::Version
119
133
  version: '1.16'
134
+ - !ruby/object:Gem::Dependency
135
+ name: openai
136
+ requirement: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - "~>"
139
+ - !ruby/object:Gem::Version
140
+ version: '0.63'
141
+ type: :development
142
+ prerelease: false
143
+ version_requirements: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - "~>"
146
+ - !ruby/object:Gem::Version
147
+ version: '0.63'
120
148
  - !ruby/object:Gem::Dependency
121
149
  name: pg
122
150
  requirement: !ruby/object:Gem::Requirement
@@ -173,6 +201,20 @@ dependencies:
173
201
  - - "~>"
174
202
  - !ruby/object:Gem::Version
175
203
  version: '1.0'
204
+ - !ruby/object:Gem::Dependency
205
+ name: ruby_llm
206
+ requirement: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - "~>"
209
+ - !ruby/object:Gem::Version
210
+ version: '1.15'
211
+ type: :development
212
+ prerelease: false
213
+ version_requirements: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - "~>"
216
+ - !ruby/object:Gem::Version
217
+ version: '1.15'
176
218
  - !ruby/object:Gem::Dependency
177
219
  name: simplecov
178
220
  requirement: !ruby/object:Gem::Requirement
@@ -240,16 +282,13 @@ files:
240
282
  - app/controllers/llm_cost_tracker/data_quality_controller.rb
241
283
  - app/controllers/llm_cost_tracker/models_controller.rb
242
284
  - app/controllers/llm_cost_tracker/pricing_controller.rb
243
- - app/controllers/llm_cost_tracker/reconciliation_controller.rb
244
285
  - app/controllers/llm_cost_tracker/tags_controller.rb
245
286
  - app/helpers/llm_cost_tracker/application_helper.rb
246
287
  - app/helpers/llm_cost_tracker/chart_helper.rb
247
- - app/helpers/llm_cost_tracker/dashboard_filter_helper.rb
248
288
  - app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb
249
289
  - app/helpers/llm_cost_tracker/dashboard_query_helper.rb
250
290
  - app/helpers/llm_cost_tracker/inline_style_helper.rb
251
291
  - app/helpers/llm_cost_tracker/pagination_helper.rb
252
- - app/helpers/llm_cost_tracker/reconciliation_helper.rb
253
292
  - app/helpers/llm_cost_tracker/sortable_table_helper.rb
254
293
  - app/helpers/llm_cost_tracker/token_usage_helper.rb
255
294
  - app/models/llm_cost_tracker/call.rb
@@ -258,11 +297,11 @@ files:
258
297
  - app/models/llm_cost_tracker/call_tag.rb
259
298
  - app/models/llm_cost_tracker/ingestion/inbox_entry.rb
260
299
  - app/models/llm_cost_tracker/ingestion/lease.rb
261
- - app/models/llm_cost_tracker/provider_invoice.rb
262
- - app/models/llm_cost_tracker/provider_invoice_import.rb
263
300
  - app/services/llm_cost_tracker/dashboard/data_quality.rb
264
301
  - app/services/llm_cost_tracker/dashboard/date_range.rb
265
302
  - app/services/llm_cost_tracker/dashboard/filter.rb
303
+ - app/services/llm_cost_tracker/dashboard/masking.rb
304
+ - app/services/llm_cost_tracker/dashboard/monthly_budget.rb
266
305
  - app/services/llm_cost_tracker/dashboard/overview_stats.rb
267
306
  - app/services/llm_cost_tracker/dashboard/pagination.rb
268
307
  - app/services/llm_cost_tracker/dashboard/params.rb
@@ -285,7 +324,6 @@ files:
285
324
  - app/views/llm_cost_tracker/errors/not_found.html.erb
286
325
  - app/views/llm_cost_tracker/models/index.html.erb
287
326
  - app/views/llm_cost_tracker/pricing/index.html.erb
288
- - app/views/llm_cost_tracker/reconciliation/index.html.erb
289
327
  - app/views/llm_cost_tracker/shared/_bar.html.erb
290
328
  - app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb
291
329
  - app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb
@@ -300,26 +338,21 @@ files:
300
338
  - config/routes.rb
301
339
  - lib/llm_cost_tracker.rb
302
340
  - lib/llm_cost_tracker/assets.rb
303
- - lib/llm_cost_tracker/billing/components.rb
304
- - lib/llm_cost_tracker/billing/components.yml
305
- - lib/llm_cost_tracker/billing/cost_status.rb
306
- - lib/llm_cost_tracker/billing/line_item.rb
307
341
  - lib/llm_cost_tracker/budget.rb
342
+ - lib/llm_cost_tracker/capture/sdk_payload.rb
308
343
  - lib/llm_cost_tracker/capture/sse.rb
309
- - lib/llm_cost_tracker/capture/stream.rb
310
344
  - lib/llm_cost_tracker/capture/stream_collector.rb
311
345
  - lib/llm_cost_tracker/capture/stream_tracker.rb
346
+ - lib/llm_cost_tracker/capture_verifier.rb
347
+ - lib/llm_cost_tracker/charges/cost.rb
348
+ - lib/llm_cost_tracker/charges/cost_status.rb
349
+ - lib/llm_cost_tracker/charges/line_item.rb
350
+ - lib/llm_cost_tracker/check.rb
312
351
  - lib/llm_cost_tracker/configuration.rb
352
+ - lib/llm_cost_tracker/currency.rb
313
353
  - lib/llm_cost_tracker/doctor.rb
314
- - lib/llm_cost_tracker/doctor/capture_verifier.rb
315
- - lib/llm_cost_tracker/doctor/check.rb
316
- - lib/llm_cost_tracker/doctor/cost_drift_check.rb
317
354
  - lib/llm_cost_tracker/doctor/ingestion_check.rb
318
- - lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb
319
- - lib/llm_cost_tracker/doctor/legacy_audit_check.rb
320
- - lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb
321
355
  - lib/llm_cost_tracker/doctor/price_check.rb
322
- - lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb
323
356
  - lib/llm_cost_tracker/doctor/probe.rb
324
357
  - lib/llm_cost_tracker/doctor/schema_check.rb
325
358
  - lib/llm_cost_tracker/engine.rb
@@ -329,22 +362,16 @@ files:
329
362
  - lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb
330
363
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
331
364
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
332
- - lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb
333
365
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb
334
366
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb
335
367
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb
336
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb
337
368
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
338
369
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb
339
370
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb
340
371
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb
341
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb
342
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb
343
372
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb
344
373
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb
345
374
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb
346
- - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb
347
- - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb
348
375
  - lib/llm_cost_tracker/ingestion.rb
349
376
  - lib/llm_cost_tracker/ingestion/batch.rb
350
377
  - lib/llm_cost_tracker/ingestion/inbox.rb
@@ -355,70 +382,65 @@ files:
355
382
  - lib/llm_cost_tracker/integrations/anthropic.rb
356
383
  - lib/llm_cost_tracker/integrations/base.rb
357
384
  - lib/llm_cost_tracker/integrations/openai.rb
385
+ - lib/llm_cost_tracker/integrations/openai/batch_capture.rb
386
+ - lib/llm_cost_tracker/integrations/openai/patches.rb
358
387
  - lib/llm_cost_tracker/integrations/ruby_llm.rb
359
388
  - lib/llm_cost_tracker/ledger.rb
360
389
  - lib/llm_cost_tracker/ledger/period.rb
361
390
  - lib/llm_cost_tracker/ledger/period/totals.rb
362
391
  - lib/llm_cost_tracker/ledger/rollups.rb
363
- - lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb
392
+ - lib/llm_cost_tracker/ledger/schema.rb
364
393
  - lib/llm_cost_tracker/ledger/schema/adapter.rb
394
+ - lib/llm_cost_tracker/ledger/schema/base.rb
365
395
  - lib/llm_cost_tracker/ledger/schema/call_line_items.rb
366
396
  - lib/llm_cost_tracker/ledger/schema/call_rollups.rb
367
397
  - lib/llm_cost_tracker/ledger/schema/call_tags.rb
368
398
  - lib/llm_cost_tracker/ledger/schema/calls.rb
369
- - lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb
370
- - lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb
371
- - lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb
372
- - lib/llm_cost_tracker/ledger/schema/provider_invoices.rb
399
+ - lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb
400
+ - lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb
373
401
  - lib/llm_cost_tracker/ledger/store.rb
402
+ - lib/llm_cost_tracker/ledger/tags/breakdown.rb
374
403
  - lib/llm_cost_tracker/ledger/tags/encoding.rb
375
404
  - lib/llm_cost_tracker/ledger/tags/query.rb
376
- - lib/llm_cost_tracker/ledger/tags/sql.rb
377
405
  - lib/llm_cost_tracker/logging.rb
378
- - lib/llm_cost_tracker/masking.rb
379
406
  - lib/llm_cost_tracker/middleware/faraday.rb
380
407
  - lib/llm_cost_tracker/parsers.rb
381
- - lib/llm_cost_tracker/parsers/anthropic.rb
382
- - lib/llm_cost_tracker/parsers/azure.rb
383
- - lib/llm_cost_tracker/parsers/base.rb
384
- - lib/llm_cost_tracker/parsers/gemini.rb
385
- - lib/llm_cost_tracker/parsers/openai.rb
386
- - lib/llm_cost_tracker/parsers/openai_compatible.rb
387
- - lib/llm_cost_tracker/parsers/openai_usage.rb
388
408
  - lib/llm_cost_tracker/prices.json
389
409
  - lib/llm_cost_tracker/pricing.rb
390
410
  - lib/llm_cost_tracker/pricing/backfill.rb
411
+ - lib/llm_cost_tracker/pricing/calculation.rb
391
412
  - lib/llm_cost_tracker/pricing/effective_prices.rb
392
413
  - lib/llm_cost_tracker/pricing/estimator.rb
393
- - lib/llm_cost_tracker/pricing/explainer.rb
394
- - lib/llm_cost_tracker/pricing/lookup.rb
414
+ - lib/llm_cost_tracker/pricing/matcher.rb
395
415
  - lib/llm_cost_tracker/pricing/mode.rb
416
+ - lib/llm_cost_tracker/pricing/price_key.rb
417
+ - lib/llm_cost_tracker/pricing/rate.rb
396
418
  - lib/llm_cost_tracker/pricing/registry.rb
397
- - lib/llm_cost_tracker/pricing/service_charges.rb
419
+ - lib/llm_cost_tracker/pricing/service_rates.rb
420
+ - lib/llm_cost_tracker/pricing/source.rb
398
421
  - lib/llm_cost_tracker/pricing/sync.rb
399
422
  - lib/llm_cost_tracker/pricing/sync/change_printer.rb
400
423
  - lib/llm_cost_tracker/pricing/sync/fetcher.rb
401
424
  - lib/llm_cost_tracker/pricing/sync/registry_diff.rb
402
425
  - lib/llm_cost_tracker/pricing/sync/registry_writer.rb
403
426
  - lib/llm_cost_tracker/pricing/unknown.rb
404
- - lib/llm_cost_tracker/providers/anthropic/server_tools.rb
405
- - lib/llm_cost_tracker/providers/anthropic/tier_classification.rb
427
+ - lib/llm_cost_tracker/providers.rb
428
+ - lib/llm_cost_tracker/providers/anthropic/parser.rb
429
+ - lib/llm_cost_tracker/providers/anthropic/response_parser.rb
430
+ - lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb
406
431
  - lib/llm_cost_tracker/providers/azure/hosts.rb
432
+ - lib/llm_cost_tracker/providers/azure/parser.rb
407
433
  - lib/llm_cost_tracker/providers/gemini/model_families.rb
434
+ - lib/llm_cost_tracker/providers/gemini/parser.rb
435
+ - lib/llm_cost_tracker/providers/gemini/usage_extractor.rb
408
436
  - lib/llm_cost_tracker/providers/openai/hosts.rb
409
437
  - lib/llm_cost_tracker/providers/openai/model_families.rb
438
+ - lib/llm_cost_tracker/providers/openai/parser.rb
439
+ - lib/llm_cost_tracker/providers/openai/response_parser.rb
410
440
  - lib/llm_cost_tracker/providers/openai/service_charges.rb
441
+ - lib/llm_cost_tracker/providers/openai/usage_extractor.rb
442
+ - lib/llm_cost_tracker/providers/openai_compatible/parser.rb
411
443
  - lib/llm_cost_tracker/railtie.rb
412
- - lib/llm_cost_tracker/reconcile_tasks.rb
413
- - lib/llm_cost_tracker/reconciliation.rb
414
- - lib/llm_cost_tracker/reconciliation/diff.rb
415
- - lib/llm_cost_tracker/reconciliation/diff_result.rb
416
- - lib/llm_cost_tracker/reconciliation/import_result.rb
417
- - lib/llm_cost_tracker/reconciliation/importer.rb
418
- - lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb
419
- - lib/llm_cost_tracker/reconciliation/sources/coercion.rb
420
- - lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb
421
- - lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb
422
444
  - lib/llm_cost_tracker/report.rb
423
445
  - lib/llm_cost_tracker/report/data.rb
424
446
  - lib/llm_cost_tracker/report/formatter.rb
@@ -427,8 +449,12 @@ files:
427
449
  - lib/llm_cost_tracker/tags/key.rb
428
450
  - lib/llm_cost_tracker/tags/sanitizer.rb
429
451
  - lib/llm_cost_tracker/timing.rb
430
- - lib/llm_cost_tracker/token_usage.rb
431
452
  - lib/llm_cost_tracker/tracker.rb
453
+ - lib/llm_cost_tracker/usage/catalog.rb
454
+ - lib/llm_cost_tracker/usage/dimension.rb
455
+ - lib/llm_cost_tracker/usage/dimensions.yml
456
+ - lib/llm_cost_tracker/usage/source.rb
457
+ - lib/llm_cost_tracker/usage/token_usage.rb
432
458
  - lib/llm_cost_tracker/version.rb
433
459
  - lib/tasks/llm_cost_tracker.rake
434
460
  homepage: https://github.com/sergey-homenko/llm_cost_tracker
@@ -1,100 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- class ReconciliationController < ApplicationController
5
- def index
6
- @reconciliation_enabled = LlmCostTracker::Reconciliation.enabled?
7
- @reconciliation_installed = LlmCostTracker::ProviderInvoice.table_exists?
8
- if @reconciliation_enabled && @reconciliation_installed
9
- @scopes = invoice_scopes
10
- @sources = @scopes.map { |scope| scope[:source] }.uniq
11
- @diffs = @scopes.filter_map { |scope| diff_for(scope) }
12
- @last_imported_at = LlmCostTracker::ProviderInvoice.maximum(:imported_at)
13
- else
14
- @scopes = []
15
- @sources = []
16
- @diffs = []
17
- @last_imported_at = nil
18
- end
19
- @threshold = LlmCostTracker::Reconciliation::DEFAULT_THRESHOLD_PERCENT
20
- @configured_importers = @reconciliation_enabled ? configured_importers : {}
21
- end
22
-
23
- def trigger_import
24
- unless LlmCostTracker::Reconciliation.enabled?
25
- return redirect_to reconciliation_path, alert: "Reconciliation is disabled"
26
- end
27
-
28
- source = params[:source].to_s
29
- importer = configured_importers[source.to_sym]
30
- return redirect_to reconciliation_path, alert: "No importer configured for #{source}" if importer.nil?
31
-
32
- result = importer.call
33
- if result.errors.any?
34
- LlmCostTracker::Logging.warn(
35
- "Reconciliation import for #{source} returned #{result.errors.size} row error(s)"
36
- )
37
- return redirect_to(
38
- reconciliation_path,
39
- alert: "Imported #{result.total_imported} #{source} rows " \
40
- "with #{result.errors.size} row error(s); see Rails logs for details."
41
- )
42
- end
43
- redirect_to reconciliation_path,
44
- notice: "Imported #{result.total_imported} #{source} rows"
45
- rescue StandardError => e
46
- LlmCostTracker::Logging.warn("Reconciliation import failed for #{source}: #{e.class}: #{e.message}")
47
- redirect_to reconciliation_path,
48
- alert: "Import failed (#{e.class.name}); see Rails logs for details."
49
- end
50
-
51
- private
52
-
53
- def configured_importers
54
- LlmCostTracker.configuration.reconciliation_importers
55
- end
56
-
57
- def invoice_scopes
58
- provider_expr = Arel.sql(metadata_provider_sql)
59
- LlmCostTracker::ProviderInvoice
60
- .group(:source, provider_expr, :currency)
61
- .order(:source, :currency)
62
- .pluck(:source, provider_expr, :currency)
63
- .map { |source, provider, currency| { source: source, provider: provider, currency: currency.upcase } }
64
- end
65
-
66
- def diff_for(scope)
67
- window = scope_invoices(scope)
68
- .order(period_end: :desc, period_start: :desc)
69
- .limit(1)
70
- .pick(:period_start, :period_end)
71
- return nil unless window
72
-
73
- LlmCostTracker::Reconciliation.diff(
74
- source: scope[:source], provider: scope[:provider], currency: scope[:currency],
75
- period_start: window[0], period_end: window[1]
76
- )
77
- rescue ArgumentError => e
78
- LlmCostTracker::Logging.warn("Reconciliation diff skipped for #{scope.inspect}: #{e.message}")
79
- nil
80
- end
81
-
82
- def scope_invoices(scope)
83
- relation = LlmCostTracker::ProviderInvoice
84
- .where(source: scope[:source], currency: scope[:currency])
85
- provider = scope[:provider]
86
- return relation if provider.nil? || provider.empty?
87
-
88
- relation.where("#{metadata_provider_sql} = ?", provider)
89
- end
90
-
91
- def metadata_provider_sql
92
- connection = LlmCostTracker::ProviderInvoice.connection
93
- if LlmCostTracker::Ledger::Schema::Adapter.postgresql?(connection)
94
- "metadata->>'provider'"
95
- else
96
- "JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.provider'))"
97
- end
98
- end
99
- end
100
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module DashboardFilterHelper
5
- FILTER_PARAM_KEYS = %i[from to provider model stream usage_source tag].freeze
6
-
7
- STREAM_FILTER_OPTIONS = [
8
- ["Streaming only", "yes"],
9
- ["Non-streaming only", "no"]
10
- ].freeze
11
-
12
- def any_filter_applied?
13
- FILTER_PARAM_KEYS.any? { |key| params[key].present? }
14
- end
15
-
16
- def active_tag_filters
17
- tag_params = LlmCostTracker::Dashboard::Params.tag_query(params[:tag])
18
-
19
- tag_params.filter_map do |key, value|
20
- {
21
- label: "Tag",
22
- value: "#{key}=#{value}",
23
- path: dashboard_filter_path(current_query(tag: tag_params.except(key).presence, page: nil))
24
- }
25
- end
26
- end
27
- end
28
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module ReconciliationHelper
5
- def attribution_summary(attribution)
6
- LlmCostTracker::Masking.format_attribution(attribution)
7
- end
8
-
9
- def mask_secret(value)
10
- LlmCostTracker::Masking.mask_value(:provider_api_key_id, value)
11
- end
12
- end
13
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- class ProviderInvoice < ActiveRecord::Base
5
- before_validation :normalize_currency
6
-
7
- private
8
-
9
- def normalize_currency
10
- self.currency = currency.to_s.upcase if currency.present?
11
- end
12
- end
13
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- class ProviderInvoiceImport < ActiveRecord::Base
5
- STATE_RUNNING = "running"
6
- STATE_COMPLETED = "completed"
7
- STATE_FAILED = "failed"
8
- STATES = [STATE_RUNNING, STATE_COMPLETED, STATE_FAILED].freeze
9
-
10
- scope :for_source, ->(source) { where(source: source.to_s) }
11
- scope :for_provider, ->(provider) { where(provider: provider.to_s) }
12
- scope :running, -> { where(state: STATE_RUNNING) }
13
- scope :completed, -> { where(state: STATE_COMPLETED) }
14
- scope :failed, -> { where(state: STATE_FAILED) }
15
- scope :latest, -> { order(started_at: :desc, id: :desc) }
16
-
17
- def self.resume_cursor_for(source, provider: nil)
18
- scope = for_source(source)
19
- scope = scope.for_provider(provider) if provider
20
- scope.latest.limit(1).pick(:cursor)
21
- end
22
-
23
- def self.last_completed_window_for(source, provider: nil)
24
- scope = for_source(source)
25
- scope = scope.for_provider(provider) if provider
26
- scope.completed.latest.limit(1).pick(:window_start, :window_end)
27
- end
28
- end
29
- end
@@ -1,174 +0,0 @@
1
- <h2 class="lct-page-title">Provider invoice reconciliation <span class="lct-page-title-meta">Experimental</span></h2>
2
- <p class="lct-page-subtitle">
3
- <% if @last_imported_at %>
4
- <span>Last import: <%= @last_imported_at.utc.iso8601 %></span>
5
- <span class="lct-page-subtitle-sep">·</span>
6
- <% end %>
7
- <span>Public API may change based on feedback — <%= link_to "open an issue", "https://github.com/sergey-homenko/llm_cost_tracker/issues", target: "_blank", rel: "noopener" %> if you use it.</span>
8
- </p>
9
-
10
- <% if flash[:notice] %>
11
- <div class="lct-alert lct-alert-info"><span><%= flash[:notice] %></span></div>
12
- <% end %>
13
- <% if flash[:alert] %>
14
- <div class="lct-alert lct-alert-danger"><span><%= flash[:alert] %></span></div>
15
- <% end %>
16
-
17
- <% if @configured_importers.any? %>
18
- <section class="lct-panel">
19
- <div class="lct-panel-head"><h2 class="lct-panel-title">Trigger import</h2></div>
20
- <div class="lct-panel-body">
21
- <% @configured_importers.each_key do |source| %>
22
- <%= button_to "Re-import #{source}",
23
- reconciliation_import_path(source: source),
24
- method: :post,
25
- class: "lct-button lct-button-secondary" %>
26
- <% end %>
27
- </div>
28
- </section>
29
- <% end %>
30
-
31
- <% if !@reconciliation_enabled %>
32
- <section class="lct-panel lct-empty">
33
- <h2 class="lct-state-title">Reconciliation disabled</h2>
34
- <p class="lct-state-copy">
35
- Provider invoice reconciliation is opt-in because it requires admin/org-level provider API keys (OpenAI <code class="lct-code-id">sk-admin-…</code>, Anthropic admin keys, GCP <code class="lct-code-id">billing.viewer</code>) — separate from the runtime inference key the tracker uses. Enable explicitly in the initializer:
36
- </p>
37
- <pre class="lct-pre">LlmCostTracker.configure do |config|
38
- config.reconciliation_enabled = true
39
- end</pre>
40
- </section>
41
- <% elsif !@reconciliation_installed %>
42
- <section class="lct-panel lct-empty">
43
- <h2 class="lct-state-title">Reconciliation not installed</h2>
44
- <p class="lct-state-copy">Run the optional migration to create the reconciliation tables:</p>
45
- <pre class="lct-pre">bin/rails generate llm_cost_tracker:reconciliation
46
- bin/rails db:migrate</pre>
47
- </section>
48
- <% elsif @diffs.empty? %>
49
- <section class="lct-panel lct-empty">
50
- <h2 class="lct-state-title">No invoices imported yet</h2>
51
- <p class="lct-state-copy">
52
- Reconciliation compares provider-side invoices against local cost. Once you import invoice rows via <code class="lct-code-id">LlmCostTracker::Reconciliation.import</code>, they appear here.
53
- </p>
54
- </section>
55
- <% else %>
56
- <section class="lct-panel">
57
- <div class="lct-panel-head"><h2 class="lct-panel-title">Latest period per source / provider / currency</h2></div>
58
- <table class="lct-tbl">
59
- <thead>
60
- <tr>
61
- <th>Source</th>
62
- <th>Provider</th>
63
- <th>Currency</th>
64
- <th>Period</th>
65
- <th class="lct-num">Provider total</th>
66
- <th class="lct-num">Local total</th>
67
- <th class="lct-num">Delta</th>
68
- <th class="lct-num">%</th>
69
- <th>Status</th>
70
- </tr>
71
- </thead>
72
- <tbody>
73
- <% @diffs.each do |diff| %>
74
- <tr>
75
- <td><code class="lct-code-id"><%= diff.source %></code></td>
76
- <td><span class="lct-model-cell"><span class="lct-provider-dot lct-provider-dot-<%= diff.provider %>"></span><%= diff.provider %></span></td>
77
- <td><%= diff.currency %></td>
78
- <td><%= diff.period_start %> → <%= diff.period_end %></td>
79
- <td class="lct-num"><%= money(diff.provider_total) %></td>
80
- <td class="lct-num"><%= money(diff.local_total) %></td>
81
- <td class="lct-num"><%= money(diff.delta_amount) %></td>
82
- <td class="lct-num"><%= diff.delta_percent.nil? ? "—" : "#{diff.delta_percent}%" %></td>
83
- <td>
84
- <% if diff.aligned?(threshold_percent: @threshold) %>
85
- <span class="lct-status-pill lct-status-pill-ok">Aligned</span>
86
- <% else %>
87
- <span class="lct-status-pill lct-status-pill-warn">Drift</span>
88
- <% end %>
89
- </td>
90
- </tr>
91
- <% end %>
92
- </tbody>
93
- </table>
94
- </section>
95
-
96
- <% @diffs.each do |diff| %>
97
- <% next if diff.unmatched_provider_rows.empty? && diff.unmatched_local_calls.empty? && diff.non_cost_rows.empty? %>
98
-
99
- <section class="lct-panel">
100
- <div class="lct-panel-head"><h2 class="lct-panel-title"><code class="lct-code-id"><%= diff.source %></code> / <%= diff.provider %> / <%= diff.currency %> — drill down</h2></div>
101
-
102
- <% if diff.unmatched_provider_rows.any? %>
103
- <p class="lct-panel-intro">
104
- <strong>Provider rows without a matching local call</strong>
105
- <% if diff.unmatched_provider_rows_truncated? %>
106
- <span class="lct-num-muted">(showing <%= diff.unmatched_provider_rows.size %> of <%= diff.unmatched_provider_rows_total %>, ranked by billed amount)</span>
107
- <% end %>
108
- </p>
109
- <table class="lct-tbl">
110
- <thead>
111
- <tr><th>External ID</th><th>Match basis</th><th>Attribution</th><th class="lct-num">Billed</th></tr>
112
- </thead>
113
- <tbody>
114
- <% diff.unmatched_provider_rows.each do |row| %>
115
- <tr>
116
- <td><code class="lct-code-id"><%= row[:external_id] %></code></td>
117
- <td><%= row[:match_basis] %></td>
118
- <td><%= attribution_summary(row[:attribution]) %></td>
119
- <td class="lct-num"><%= optional_money(row[:billed_amount]) %></td>
120
- </tr>
121
- <% end %>
122
- </tbody>
123
- </table>
124
- <% end %>
125
-
126
- <% if diff.unmatched_local_calls.any? %>
127
- <p class="lct-panel-intro">
128
- <strong>Local calls no provider invoice can explain</strong>
129
- <% if diff.unmatched_local_calls_truncated? %>
130
- <span class="lct-num-muted">(showing <%= diff.unmatched_local_calls.size %> of <%= diff.unmatched_local_calls_total %>, ranked by total cost)</span>
131
- <% end %>
132
- </p>
133
- <table class="lct-tbl">
134
- <thead>
135
- <tr><th>Attribution</th><th class="lct-num">Calls</th><th class="lct-num">Total cost</th></tr>
136
- </thead>
137
- <tbody>
138
- <% diff.unmatched_local_calls.each do |row| %>
139
- <tr>
140
- <td><%= attribution_summary(row[:attribution]) %></td>
141
- <td class="lct-num"><%= number(row[:count]) %></td>
142
- <td class="lct-num"><%= money(row[:total_cost]) %></td>
143
- </tr>
144
- <% end %>
145
- </tbody>
146
- </table>
147
- <% end %>
148
-
149
- <% if diff.non_cost_rows.any? %>
150
- <p class="lct-panel-intro">
151
- <strong>Non-cost evidence</strong> (free quota, credits, adjustments)
152
- <% if diff.non_cost_rows_truncated? %>
153
- <span class="lct-num-muted">(showing <%= diff.non_cost_rows.size %> of <%= diff.non_cost_rows_total %>, ranked by amount)</span>
154
- <% end %>
155
- </p>
156
- <table class="lct-tbl">
157
- <thead>
158
- <tr><th>Row type</th><th>Meter</th><th>Attribution</th><th class="lct-num">Amount</th></tr>
159
- </thead>
160
- <tbody>
161
- <% diff.non_cost_rows.each do |row| %>
162
- <tr>
163
- <td><%= row[:row_type] %></td>
164
- <td><%= row[:meter] %></td>
165
- <td><%= attribution_summary(row[:attribution]) %></td>
166
- <td class="lct-num"><%= optional_money(row[:billed_amount]) %></td>
167
- </tr>
168
- <% end %>
169
- </tbody>
170
- </table>
171
- <% end %>
172
- </section>
173
- <% end %>
174
- <% end %>