llm_cost_tracker 0.11.0 → 0.13.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 (196) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +73 -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/data_quality_controller.rb +1 -0
  8. data/app/controllers/llm_cost_tracker/pricing_controller.rb +1 -1
  9. data/app/helpers/llm_cost_tracker/application_helper.rb +12 -19
  10. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
  11. data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +4 -4
  12. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
  13. data/app/models/llm_cost_tracker/call.rb +28 -63
  14. data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
  15. data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
  16. data/app/models/llm_cost_tracker/call_tag.rb +0 -2
  17. data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +3 -0
  18. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +76 -43
  19. data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
  20. data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
  21. data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
  22. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
  23. data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
  24. data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +30 -44
  25. data/app/services/llm_cost_tracker/dashboard/setup_state.rb +4 -60
  26. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +1 -7
  27. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
  28. data/app/views/layouts/llm_cost_tracker/application.html.erb +0 -6
  29. data/app/views/llm_cost_tracker/calls/index.html.erb +8 -8
  30. data/app/views/llm_cost_tracker/calls/show.html.erb +33 -25
  31. data/app/views/llm_cost_tracker/dashboard/index.html.erb +8 -8
  32. data/app/views/llm_cost_tracker/data_quality/index.html.erb +73 -117
  33. data/app/views/llm_cost_tracker/models/index.html.erb +5 -5
  34. data/app/views/llm_cost_tracker/pricing/index.html.erb +3 -3
  35. data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -1
  36. data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -1
  37. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -1
  38. data/app/views/llm_cost_tracker/tags/index.html.erb +3 -3
  39. data/app/views/llm_cost_tracker/tags/show.html.erb +10 -10
  40. data/config/routes.rb +2 -3
  41. data/lib/llm_cost_tracker/budget.rb +24 -26
  42. data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
  43. data/lib/llm_cost_tracker/capture/sse.rb +1 -0
  44. data/lib/llm_cost_tracker/capture/stream_collector.rb +32 -39
  45. data/lib/llm_cost_tracker/capture/stream_tracker.rb +17 -28
  46. data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
  47. data/lib/llm_cost_tracker/charges/cost.rb +27 -0
  48. data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
  49. data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +41 -44
  50. data/lib/llm_cost_tracker/check.rb +5 -0
  51. data/lib/llm_cost_tracker/configuration.rb +13 -44
  52. data/lib/llm_cost_tracker/currency.rb +5 -0
  53. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
  54. data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
  55. data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
  56. data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
  57. data/lib/llm_cost_tracker/doctor.rb +5 -69
  58. data/lib/llm_cost_tracker/engine.rb +4 -4
  59. data/lib/llm_cost_tracker/event.rb +12 -18
  60. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
  61. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +4 -3
  64. data/lib/llm_cost_tracker/ingestion/batch.rb +61 -12
  65. data/lib/llm_cost_tracker/ingestion/inbox.rb +7 -8
  66. data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
  67. data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
  68. data/lib/llm_cost_tracker/ingestion.rb +24 -36
  69. data/lib/llm_cost_tracker/integrations/anthropic.rb +93 -106
  70. data/lib/llm_cost_tracker/integrations/base.rb +39 -57
  71. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
  72. data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
  73. data/lib/llm_cost_tracker/integrations/openai.rb +70 -276
  74. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +87 -99
  75. data/lib/llm_cost_tracker/integrations.rb +32 -25
  76. data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
  77. data/lib/llm_cost_tracker/ledger/period.rb +5 -10
  78. data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
  79. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
  80. data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
  81. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
  82. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
  83. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
  84. data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
  85. data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
  86. data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
  87. data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
  88. data/lib/llm_cost_tracker/ledger/store.rb +24 -41
  89. data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
  90. data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
  91. data/lib/llm_cost_tracker/ledger.rb +8 -18
  92. data/lib/llm_cost_tracker/logging.rb +4 -21
  93. data/lib/llm_cost_tracker/middleware/faraday.rb +61 -50
  94. data/lib/llm_cost_tracker/parsers.rb +139 -26
  95. data/lib/llm_cost_tracker/prices.json +1821 -13
  96. data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
  97. data/lib/llm_cost_tracker/pricing/calculation.rb +279 -0
  98. data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
  99. data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
  100. data/lib/llm_cost_tracker/pricing/matcher.rb +98 -0
  101. data/lib/llm_cost_tracker/pricing/mode.rb +44 -56
  102. data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
  103. data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
  104. data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
  105. data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
  106. data/lib/llm_cost_tracker/pricing/source.rb +7 -0
  107. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
  108. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
  109. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
  110. data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
  111. data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
  112. data/lib/llm_cost_tracker/pricing.rb +10 -278
  113. data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
  114. data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
  115. data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
  116. data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
  117. data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
  118. data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
  119. data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
  120. data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
  121. data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
  122. data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
  123. data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
  124. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
  125. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +63 -39
  126. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
  127. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
  128. data/lib/llm_cost_tracker/providers.rb +35 -0
  129. data/lib/llm_cost_tracker/railtie.rb +0 -3
  130. data/lib/llm_cost_tracker/report/data.rb +3 -4
  131. data/lib/llm_cost_tracker/report/formatter.rb +1 -1
  132. data/lib/llm_cost_tracker/report.rb +1 -1
  133. data/lib/llm_cost_tracker/retention.rb +6 -19
  134. data/lib/llm_cost_tracker/tags/context.rb +9 -6
  135. data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
  136. data/lib/llm_cost_tracker/timing.rb +2 -4
  137. data/lib/llm_cost_tracker/tracker.rb +23 -36
  138. data/lib/llm_cost_tracker/usage/catalog.rb +69 -0
  139. data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
  140. data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +6 -52
  141. data/lib/llm_cost_tracker/usage/source.rb +14 -0
  142. data/lib/llm_cost_tracker/usage/token_usage.rb +45 -0
  143. data/lib/llm_cost_tracker/version.rb +1 -1
  144. data/lib/llm_cost_tracker.rb +43 -52
  145. data/lib/tasks/llm_cost_tracker.rake +14 -73
  146. metadata +81 -55
  147. data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -100
  148. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
  149. data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
  150. data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
  151. data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
  152. data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -174
  153. data/lib/llm_cost_tracker/billing/components.rb +0 -95
  154. data/lib/llm_cost_tracker/capture/stream.rb +0 -9
  155. data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
  156. data/lib/llm_cost_tracker/doctor/check.rb +0 -7
  157. data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
  158. data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
  159. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
  160. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
  161. data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
  162. data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
  163. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
  164. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -36
  165. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -27
  166. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
  167. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
  168. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
  169. data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
  170. data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
  171. data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
  172. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
  173. data/lib/llm_cost_tracker/masking.rb +0 -39
  174. data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -176
  175. data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
  176. data/lib/llm_cost_tracker/parsers/base.rb +0 -131
  177. data/lib/llm_cost_tracker/parsers/gemini.rb +0 -230
  178. data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
  179. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -45
  180. data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
  181. data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
  182. data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
  183. data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
  184. data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +0 -15
  185. data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
  186. data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -131
  187. data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
  188. data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
  189. data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
  190. data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -249
  191. data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -148
  192. data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +0 -40
  193. data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
  194. data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -118
  195. data/lib/llm_cost_tracker/reconciliation.rb +0 -118
  196. data/lib/llm_cost_tracker/token_usage.rb +0 -93
@@ -35,7 +35,7 @@ module LlmCostTracker
35
35
  calls: calls,
36
36
  total_cost: row.total_cost,
37
37
  average_cost_per_call: row.average_cost_per_call,
38
- share_percent: percentage(calls, total)
38
+ share_percent: total.positive? ? (calls.to_f / total) * 100.0 : 0.0
39
39
  )
40
40
  end
41
41
  end
@@ -115,12 +115,6 @@ module LlmCostTracker
115
115
  def quoted_key
116
116
  scope.connection.quote(key)
117
117
  end
118
-
119
- def percentage(numerator, denominator)
120
- return 0.0 unless denominator.positive?
121
-
122
- (numerator / denominator.to_f) * 100.0
123
- end
124
118
  end
125
119
  end
126
120
  end
@@ -20,7 +20,7 @@ module LlmCostTracker
20
20
 
21
21
  def rows
22
22
  scope.klass.find_by_sql(build_sql)
23
- rescue StandardError => e
23
+ rescue ActiveRecord::StatementInvalid => e
24
24
  LlmCostTracker::Logging.warn("Tag key discovery failed (#{connection.adapter_name}): #{e.class}: #{e.message}")
25
25
  []
26
26
  end
@@ -79,12 +79,6 @@
79
79
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
80
80
  Pricing
81
81
  <% end %>
82
- <% if LlmCostTracker.reconciliation_enabled? %>
83
- <%= link_to reconciliation_path, class: ("lct-sidebar-link lct-active" if dashboard_section == :reconciliation) || "lct-sidebar-link", aria: (dashboard_section == :reconciliation ? { current: "page" } : {}) do %>
84
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
85
- Reconciliation
86
- <% end %>
87
- <% end %>
88
82
  </div>
89
83
  </aside>
90
84
 
@@ -18,8 +18,8 @@
18
18
  <%= link_to "× Clear filters", calls_path(current_query(provider: nil, model: nil, stream: nil, cost_status: nil, page: nil)), class: "lct-filter-clear" %>
19
19
  <% end %>
20
20
 
21
- <span class="lct-filter-row-meta"><%= number(@calls_count) %> call<%= "s" unless @calls_count == 1 %> · <%= money(@calls_total_cost) %></span>
22
- <%= link_to "Export CSV", calls_path(current_query(format: :csv)), class: "lct-page-link", title: "Capped at #{number(LlmCostTracker::CallsController::CSV_EXPORT_LIMIT)} rows per request" %>
21
+ <span class="lct-filter-row-meta"><%= number_with_delimiter(@calls_count) %> call<%= "s" unless @calls_count == 1 %> · <%= money(@calls_total_cost) %></span>
22
+ <%= link_to "Export CSV", calls_path(current_query(format: :csv)), class: "lct-page-link", title: "Capped at #{number_with_delimiter(LlmCostTracker::CallsController::CSV_EXPORT_LIMIT)} rows per request" %>
23
23
  </div>
24
24
 
25
25
  <% if @calls_count.zero? %>
@@ -32,7 +32,7 @@
32
32
  <table class="lct-tbl">
33
33
  <thead>
34
34
  <tr>
35
- <%= sortable_header("Tracked at", "tracked_at", num: true) %>
35
+ <%= sortable_header("Tracked at", "tracked_at", num: true, default: true) %>
36
36
  <%= sortable_header("Provider", "provider") %>
37
37
  <%= sortable_header("Model", "model") %>
38
38
  <%= sortable_header("Input", "input", num: true) %>
@@ -49,11 +49,11 @@
49
49
  <td><%= format_date(call.tracked_at) %></td>
50
50
  <td><span class="lct-model-cell"><span class="lct-provider-dot lct-provider-dot-<%= call.provider %>"></span><%= call.provider %></span></td>
51
51
  <td><code class="lct-code-id"><%= call.model %></code></td>
52
- <td class="lct-num"><%= number(call.input_tokens) %></td>
53
- <td class="lct-num"><%= number(call.output_tokens) %></td>
52
+ <td class="lct-num"><%= number_with_delimiter(call.input_tokens) %></td>
53
+ <td class="lct-num"><%= number_with_delimiter(call.output_tokens) %></td>
54
54
  <td class="lct-num<%= ' lct-num-muted' if call.total_cost.nil? %>"><%= optional_money(call.total_cost) %></td>
55
- <td class="lct-num<%= ' lct-num-muted' if call.latency_ms.nil? %>"><%= call.latency_ms ? "#{number(call.latency_ms)}ms" : "n/a" %></td>
56
- <td><%= render "llm_cost_tracker/shared/tag_chips", tags: call.parsed_tags %></td>
55
+ <td class="lct-num<%= ' lct-num-muted' if call.latency_ms.nil? %>"><%= call.latency_ms ? "#{number_with_delimiter(call.latency_ms)}ms" : "n/a" %></td>
56
+ <td><%= render "llm_cost_tracker/shared/tag_chips", tags: call.tag_pairs %></td>
57
57
  <td class="lct-num"><%= link_to "Details", call_path(call), class: "lct-page-link" %></td>
58
58
  </tr>
59
59
  <% end %>
@@ -64,7 +64,7 @@
64
64
  <% last_row = [@page.offset + @calls.length, @calls_count].min %>
65
65
  <% total_pages = @page.total_pages(@calls_count) %>
66
66
  <div class="lct-pagination">
67
- <span class="lct-pagination-info">Showing <strong><%= number(first_row) %></strong>–<strong><%= number(last_row) %></strong> of <strong><%= number(@calls_count) %></strong></span>
67
+ <span class="lct-pagination-info">Showing <strong><%= number_with_delimiter(first_row) %></strong>–<strong><%= number_with_delimiter(last_row) %></strong> of <strong><%= number_with_delimiter(@calls_count) %></strong></span>
68
68
  <span class="lct-seg lct-pagination-perpage" title="Rows per page">
69
69
  <% LlmCostTracker::PaginationHelper::PER_PAGE_CHOICES.each do |choice| %>
70
70
  <% if choice == @page.per %>
@@ -3,7 +3,7 @@
3
3
  <% token_segments = priced_components.map do |component|
4
4
  token_key = component.fetch(:token_key)
5
5
  value = @call.has_attribute?(token_key) ? @call[token_key] : 0
6
- { label: component.fetch(:label), value: value, formatted_value: number(value), css_class: component.fetch(:css_class) }
6
+ { label: component.fetch(:label), value: value, formatted_value: number_with_delimiter(value), css_class: component.fetch(:css_class) }
7
7
  end %>
8
8
  <% cost_segments = [] %>
9
9
  <% unless @call.total_cost.nil? %>
@@ -33,11 +33,11 @@ end %>
33
33
  </div>
34
34
  <div class="lct-stat">
35
35
  <div class="lct-stat-head"><p class="lct-stat-label">Total tokens</p></div>
36
- <p class="lct-stat-value"><%= number(@call.total_tokens) %></p>
36
+ <p class="lct-stat-value"><%= number_with_delimiter(@call.total_tokens) %></p>
37
37
  </div>
38
38
  <div class="lct-stat">
39
39
  <div class="lct-stat-head"><p class="lct-stat-label">Latency</p></div>
40
- <p class="lct-stat-value<%= ' lct-num-muted' if @call.latency_ms.nil? %>"><%= @call.latency_ms ? number(@call.latency_ms) : "n/a" %><% if @call.latency_ms %><span class="lct-stat-unit">ms</span><% end %></p>
40
+ <p class="lct-stat-value<%= ' lct-num-muted' if @call.latency_ms.nil? %>"><%= @call.latency_ms ? number_with_delimiter(@call.latency_ms) : "n/a" %><% if @call.latency_ms %><span class="lct-stat-unit">ms</span><% end %></p>
41
41
  </div>
42
42
  </div>
43
43
 
@@ -80,33 +80,41 @@ end %>
80
80
  <dd><code class="lct-code-id"><%= @call.usage_source %></code></dd>
81
81
  </div>
82
82
  <% end %>
83
- <div class="lct-meta-strip-item">
84
- <dt>Response ID</dt>
85
- <dd><%= @call.provider_response_id.present? ? content_tag(:code, @call.provider_response_id, class: "lct-code-id") : "n/a" %></dd>
86
- </div>
87
- <div class="lct-meta-strip-item">
88
- <dt>Project ID</dt>
89
- <dd><%= @call.provider_project_id.present? ? content_tag(:code, LlmCostTracker::Masking.mask_value(:provider_project_id, @call.provider_project_id), class: "lct-code-id") : "n/a" %></dd>
90
- </div>
91
- <div class="lct-meta-strip-item">
92
- <dt>API Key ID</dt>
93
- <dd><%= @call.provider_api_key_id.present? ? content_tag(:code, LlmCostTracker::Masking.mask_value(:provider_api_key_id, @call.provider_api_key_id), class: "lct-code-id") : "n/a" %></dd>
94
- </div>
95
- <div class="lct-meta-strip-item">
96
- <dt>Workspace ID</dt>
97
- <dd><%= @call.provider_workspace_id.present? ? content_tag(:code, LlmCostTracker::Masking.mask_value(:provider_workspace_id, @call.provider_workspace_id), class: "lct-code-id") : "n/a" %></dd>
98
- </div>
83
+ <% if @call.provider_response_id.present? %>
84
+ <div class="lct-meta-strip-item">
85
+ <dt>Response ID</dt>
86
+ <dd><%= content_tag(:code, @call.provider_response_id, class: "lct-code-id") %></dd>
87
+ </div>
88
+ <% end %>
89
+ <% if @call.provider_project_id.present? %>
90
+ <div class="lct-meta-strip-item">
91
+ <dt>Project ID</dt>
92
+ <dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(:provider_project_id, @call.provider_project_id), class: "lct-code-id") %></dd>
93
+ </div>
94
+ <% end %>
95
+ <% if @call.provider_api_key_id.present? %>
96
+ <div class="lct-meta-strip-item">
97
+ <dt>API Key ID</dt>
98
+ <dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(:provider_api_key_id, @call.provider_api_key_id), class: "lct-code-id") %></dd>
99
+ </div>
100
+ <% end %>
101
+ <% if @call.provider_workspace_id.present? %>
102
+ <div class="lct-meta-strip-item">
103
+ <dt>Workspace ID</dt>
104
+ <dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(:provider_workspace_id, @call.provider_workspace_id), class: "lct-code-id") %></dd>
105
+ </div>
106
+ <% end %>
99
107
  </dl>
100
108
  </section>
101
109
 
102
110
  <section class="lct-panel">
103
111
  <div class="lct-panel-head"><h2 class="lct-panel-title">Tags</h2></div>
104
112
  <div class="lct-panel-body">
105
- <% if @call.parsed_tags.empty? %>
113
+ <% if @call.tag_pairs.empty? %>
106
114
  <p class="lct-stack-empty">(untagged)</p>
107
115
  <% else %>
108
116
  <span class="lct-tag-chips">
109
- <% @call.parsed_tags.each do |k, v| %>
117
+ <% @call.tag_pairs.each do |k, v| %>
110
118
  <span class="lct-tag-chip"><span class="lct-tag-chip-key"><%= k %></span>=<%= v %></span>
111
119
  <% end %>
112
120
  </span>
@@ -134,9 +142,9 @@ end %>
134
142
  <td><code class="lct-code-id"><%= line_item.kind %></code></td>
135
143
  <td><%= line_item.unit %></td>
136
144
  <td class="lct-num"><%= line_item.quantity %></td>
137
- <td class="lct-num"><%= line_item.rate_amount ? "#{optional_money(line_item.rate_amount)} / #{line_item.rate_quantity}" : "n/a" %></td>
138
- <% unknown_cost = line_item.cost.nil? || line_item.cost_status.to_s == LlmCostTracker::Billing::CostStatus::UNKNOWN %>
139
- <td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(line_item.cost) %></td>
145
+ <td class="lct-num"><%= line_item.rate_amount ? "#{optional_money(line_item.rate_amount, currency: line_item.currency)} / #{line_item.rate_quantity}" : "n/a" %></td>
146
+ <% unknown_cost = line_item.cost.nil? || line_item.cost_status.to_s == LlmCostTracker::Charges::CostStatus::UNKNOWN %>
147
+ <td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(line_item.cost, currency: line_item.currency) %></td>
140
148
  <td><%= line_item.cost_status %></td>
141
149
  </tr>
142
150
  <% end %>
@@ -161,6 +169,6 @@ end %>
161
169
  <h2 class="lct-panel-title">Metadata</h2>
162
170
  <span class="lct-disclose-hint">show JSON</span>
163
171
  </summary>
164
- <pre class="lct-pre"><%= safe_json(LlmCostTracker::Masking.mask_hash(masked_metadata_hash(@call.read_attribute("metadata")))) %></pre>
172
+ <pre class="lct-pre"><%= safe_json(LlmCostTracker::Dashboard::Masking.mask_hash(masked_metadata_hash(@call.read_attribute("metadata")))) %></pre>
165
173
  </details>
166
174
  <% end %>
@@ -8,7 +8,7 @@
8
8
  <% end %>
9
9
 
10
10
  <span class="lct-filter-row-meta">
11
- <%= number(@stats.total_calls) %> call<%= "s" unless @stats.total_calls.to_i == 1 %> · <%= money(@stats.total_cost) %>
11
+ <%= number_with_delimiter(@stats.total_calls) %> call<%= "s" unless @stats.total_calls.to_i == 1 %> · <%= money(@stats.total_cost) %>
12
12
  </span>
13
13
  </div>
14
14
 
@@ -22,7 +22,7 @@
22
22
  <div class="lct-alert lct-alert-warn">
23
23
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4M12 17h.01M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
24
24
  <span>
25
- <strong><%= number(@stats.unknown_pricing_count) %> call<%= "s" unless @stats.unknown_pricing_count.to_i == 1 %> with incomplete pricing</strong>
25
+ <strong><%= number_with_delimiter(@stats.unknown_pricing_count) %> call<%= "s" unless @stats.unknown_pricing_count.to_i == 1 %> with incomplete pricing</strong>
26
26
  · <%= percent(coverage_percent(@stats.unknown_pricing_count, @stats.total_calls)) %> of slice
27
27
  — update <code>pricing_overrides</code> or <code>prices_file</code>.
28
28
  </span>
@@ -64,12 +64,12 @@
64
64
  <% badge = delta_badge(@stats.calls_delta_percent, mode: :neutral) %>
65
65
  <span class="<%= badge[:css_class] %>"><%= badge[:text] %></span>
66
66
  </div>
67
- <p class="lct-stat-value"><%= number(@stats.total_calls) %></p>
67
+ <p class="lct-stat-value"><%= number_with_delimiter(@stats.total_calls) %></p>
68
68
  </div>
69
69
  <% if @stats.average_latency_ms %>
70
70
  <div class="lct-stat lct-stat-ok">
71
71
  <div class="lct-stat-head"><p class="lct-stat-label">Avg latency</p></div>
72
- <p class="lct-stat-value"><%= number(@stats.average_latency_ms.round) %><span class="lct-stat-unit">ms</span></p>
72
+ <p class="lct-stat-value"><%= number_with_delimiter(@stats.average_latency_ms.round) %><span class="lct-stat-unit">ms</span></p>
73
73
  </div>
74
74
  <% else %>
75
75
  <div class="lct-stat">
@@ -79,7 +79,7 @@
79
79
  <% end %>
80
80
  <div class="lct-stat <%= @stats.unknown_pricing_count.to_i.positive? ? 'lct-stat-warn' : 'lct-stat-ok' %>">
81
81
  <div class="lct-stat-head"><p class="lct-stat-label">Incomplete pricing</p></div>
82
- <p class="lct-stat-value"><%= number(@stats.unknown_pricing_count) %></p>
82
+ <p class="lct-stat-value"><%= number_with_delimiter(@stats.unknown_pricing_count) %></p>
83
83
  <p class="lct-stat-foot"><%= percent(coverage_percent(@stats.unknown_pricing_count, @stats.total_calls)) %> of slice</p>
84
84
  </div>
85
85
  </div>
@@ -115,7 +115,7 @@
115
115
  <h2 class="lct-panel-title">Daily spend</h2>
116
116
  <span class="lct-panel-meta">
117
117
  <span class="lct-legend"><span class="lct-legend-dot lct-legend-dot-current"></span>current</span>
118
- <% if @comparison_series.any? %><span class="lct-legend"><span class="lct-legend-dot lct-legend-dot-prior"></span>prior <%= number(@comparison_series.size) %>d</span><% end %>
118
+ <% if @comparison_series.any? %><span class="lct-legend"><span class="lct-legend-dot lct-legend-dot-prior"></span>prior <%= number_with_delimiter(@comparison_series.size) %>d</span><% end %>
119
119
  </span>
120
120
  </div>
121
121
  <%= render "llm_cost_tracker/shared/spend_chart", series: @time_series, comparison_series: @comparison_series %>
@@ -133,7 +133,7 @@
133
133
  <% @top_models.first(5).each do |row| %>
134
134
  <tr>
135
135
  <td><span class="lct-model-cell"><span class="lct-provider-dot lct-provider-dot-<%= row.provider %>"></span><%= link_to calls_path(calls_query_for_model(provider: row.provider, model: row.model)), class: "lct-code-id" do %><%= row.model %><% end %></span></td>
136
- <td class="lct-num"><%= number(row.calls) %></td>
136
+ <td class="lct-num"><%= number_with_delimiter(row.calls) %></td>
137
137
  <td class="lct-num"><%= money(row.total_cost) %></td>
138
138
  </tr>
139
139
  <% end %>
@@ -150,7 +150,7 @@
150
150
  <% @providers.each do |row| %>
151
151
  <tr>
152
152
  <td><span class="lct-model-cell"><span class="lct-provider-dot lct-provider-dot-<%= row.provider %>"></span><%= row.provider %></span></td>
153
- <td class="lct-num"><%= number(row.calls) %></td>
153
+ <td class="lct-num"><%= number_with_delimiter(row.calls) %></td>
154
154
  <td class="lct-num"><%= money(row.total_cost) %></td>
155
155
  <td class="lct-num"><%= percent(row.share_percent) %></td>
156
156
  </tr>
@@ -7,9 +7,20 @@
7
7
  <%= link_to "× Clear filters", data_quality_path(current_query(provider: nil, model: nil, page: nil)), class: "lct-filter-clear" %>
8
8
  <% end %>
9
9
 
10
- <span class="lct-filter-row-meta"><%= number(@summary.total) %> call<%= "s" unless @summary.total == 1 %> inspected</span>
10
+ <span class="lct-filter-row-meta"><%= number_with_delimiter(@summary.total) %> call<%= "s" unless @summary.total == 1 %> inspected</span>
11
11
  </div>
12
12
 
13
+ <% if @quarantined_inbox && @quarantined_inbox.count.positive? %>
14
+ <h3 class="lct-stat-section-label">Async inbox</h3>
15
+ <div class="lct-stat-grid">
16
+ <div class="lct-stat lct-stat-warn">
17
+ <div class="lct-stat-head"><p class="lct-stat-label">Quarantined inbox rows</p></div>
18
+ <p class="lct-stat-value"><%= number_with_delimiter(@quarantined_inbox.count) %></p>
19
+ <p class="lct-stat-foot"><%= money(@quarantined_inbox.total_cost) %> excluded from the ledger and totals</p>
20
+ </div>
21
+ </div>
22
+ <% end %>
23
+
13
24
  <% if @summary.total.zero? %>
14
25
  <section class="lct-panel lct-empty">
15
26
  <h2 class="lct-state-title">No data yet</h2>
@@ -20,19 +31,19 @@
20
31
  <div class="lct-stat-grid">
21
32
  <div class="lct-stat">
22
33
  <div class="lct-stat-head"><p class="lct-stat-label">Calls inspected</p></div>
23
- <p class="lct-stat-value"><%= number(@summary.total) %></p>
34
+ <p class="lct-stat-value"><%= number_with_delimiter(@summary.total) %></p>
24
35
  <p class="lct-stat-foot">in current slice</p>
25
36
  </div>
26
37
 
27
38
  <div class="lct-stat">
28
39
  <div class="lct-stat-head"><p class="lct-stat-label">Streaming calls</p></div>
29
- <p class="lct-stat-value"><%= number(@summary.streaming_count) %></p>
40
+ <p class="lct-stat-value"><%= number_with_delimiter(@summary.streaming_count) %></p>
30
41
  <p class="lct-stat-foot"><%= percent(@summary.streaming_share) %> of calls</p>
31
42
  </div>
32
43
 
33
44
  <div class="lct-stat">
34
45
  <div class="lct-stat-head"><p class="lct-stat-label">Calls with provider response ID</p></div>
35
- <p class="lct-stat-value"><%= number(@summary.calls_with_provider_response_id) %></p>
46
+ <p class="lct-stat-value"><%= number_with_delimiter(@summary.calls_with_provider_response_id) %></p>
36
47
  <p class="lct-stat-foot"><%= percent(@summary.provider_response_id_coverage) %> of calls</p>
37
48
  </div>
38
49
  </div>
@@ -50,7 +61,7 @@
50
61
  <% if incomplete_pricing.positive? %>
51
62
  <div class="lct-stat lct-stat-warn">
52
63
  <div class="lct-stat-head"><p class="lct-stat-label">Incomplete pricing</p></div>
53
- <p class="lct-stat-value"><%= number(incomplete_pricing) %></p>
64
+ <p class="lct-stat-value"><%= number_with_delimiter(incomplete_pricing) %></p>
54
65
  <p class="lct-stat-foot"><%= percent(@summary.unknown_pricing_share) %> of calls</p>
55
66
  </div>
56
67
  <% end %>
@@ -58,7 +69,7 @@
58
69
  <% if untagged.positive? %>
59
70
  <div class="lct-stat lct-stat-warn">
60
71
  <div class="lct-stat-head"><p class="lct-stat-label">Calls without tags</p></div>
61
- <p class="lct-stat-value"><%= number(untagged) %></p>
72
+ <p class="lct-stat-value"><%= number_with_delimiter(untagged) %></p>
62
73
  <p class="lct-stat-foot"><%= percent(@summary.untagged_share) %> of calls</p>
63
74
  </div>
64
75
  <% end %>
@@ -66,7 +77,7 @@
66
77
  <% if missing_latency.positive? %>
67
78
  <div class="lct-stat lct-stat-warn">
68
79
  <div class="lct-stat-head"><p class="lct-stat-label">Missing latency</p></div>
69
- <p class="lct-stat-value"><%= number(missing_latency) %></p>
80
+ <p class="lct-stat-value"><%= number_with_delimiter(missing_latency) %></p>
70
81
  <p class="lct-stat-foot"><%= percent(@summary.missing_latency_share) %> of calls</p>
71
82
  </div>
72
83
  <% end %>
@@ -74,7 +85,7 @@
74
85
  <% if streams_missing_usage.positive? %>
75
86
  <div class="lct-stat lct-stat-warn">
76
87
  <div class="lct-stat-head"><p class="lct-stat-label">Streams without usage</p></div>
77
- <p class="lct-stat-value"><%= number(streams_missing_usage) %></p>
88
+ <p class="lct-stat-value"><%= number_with_delimiter(streams_missing_usage) %></p>
78
89
  <p class="lct-stat-foot"><%= percent(@summary.streaming_missing_usage_share) %> of streams</p>
79
90
  </div>
80
91
  <% end %>
@@ -83,118 +94,64 @@
83
94
  <div class="lct-stat lct-stat-warn">
84
95
  <div class="lct-stat-head"><p class="lct-stat-label">Hidden output share</p></div>
85
96
  <p class="lct-stat-value"><%= percent(hidden_output_share) %></p>
86
- <p class="lct-stat-foot"><%= number(@hidden_output_summary.fetch(:hidden_output_tokens)) %> of <%= number(@hidden_output_summary.fetch(:output_tokens)) %> output tokens</p>
97
+ <p class="lct-stat-foot"><%= number_with_delimiter(@hidden_output_summary.fetch(:hidden_output_tokens)) %> of <%= number_with_delimiter(@hidden_output_summary.fetch(:output_tokens)) %> output tokens</p>
87
98
  </div>
88
99
  <% end %>
89
100
  </div>
90
- <% else %>
91
- <div class="lct-alert lct-alert-info">
92
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
93
- <span>No data-quality issues in this slice.</span>
94
- </div>
95
101
  <% end %>
96
102
 
97
- <div class="lct-grid-2">
98
- <section class="lct-panel">
99
- <div class="lct-panel-head"><h2 class="lct-panel-title">Next actions</h2></div>
100
- <p class="lct-panel-intro">Use these fixes to improve the trustworthiness of downstream cost reports.</p>
101
-
102
- <table class="lct-tbl">
103
- <thead>
104
- <tr>
105
- <th>Issue</th>
106
- <th>Why it matters</th>
107
- <th>Suggested action</th>
108
- </tr>
109
- </thead>
110
- <tbody>
111
- <tr>
112
- <td>Incomplete pricing</td>
113
- <td>Some or all line items have no configured rate, so totals undercount.</td>
114
- <td>Update <code class="lct-code-id">pricing_overrides</code> or <code class="lct-code-id">prices_file</code>.</td>
115
- </tr>
116
- <tr>
117
- <td>Missing tags</td>
118
- <td>Attribution by tenant, user, or feature becomes less useful.</td>
119
- <td>Pass <code class="lct-code-id">tags:</code> from middleware using request context.</td>
120
- </tr>
121
- <tr>
122
- <td>Missing latency</td>
123
- <td>Slow requests become harder to isolate on the calls page.</td>
124
- <td>Make sure latency capture is enabled on every tracked request.</td>
125
- </tr>
126
- <% if @summary.streaming_missing_usage.positive? %>
127
- <tr>
128
- <td>Streams without usage</td>
129
- <td>Token totals undercount when streaming responses drop the final usage event.</td>
130
- <td>Send OpenAI requests with <code class="lct-code-id">stream_options: { include_usage: true }</code>, or wrap custom clients with <code class="lct-code-id">LlmCostTracker.track_stream</code>.</td>
131
- </tr>
132
- <% end %>
133
- <% if @summary.missing_provider_response_id_count.positive? %>
134
- <tr>
135
- <td>Missing provider response IDs</td>
136
- <td>Proof of provider-issued responses is weaker when calls cannot be tied back to provider objects.</td>
137
- <td>Upgrade to the latest parser coverage and pass <code class="lct-code-id">provider_response_id:</code> for custom clients when the provider exposes one.</td>
138
- </tr>
139
- <% end %>
140
- </tbody>
141
- </table>
142
- </section>
103
+ <section class="lct-panel">
104
+ <div class="lct-panel-head"><h2 class="lct-panel-title">Coverage summary</h2></div>
143
105
 
144
- <section class="lct-panel">
145
- <div class="lct-panel-head"><h2 class="lct-panel-title">Coverage summary</h2></div>
146
- <p class="lct-panel-intro">Good dashboards start with clean pricing, tags, and latency coverage.</p>
106
+ <table class="lct-tbl">
107
+ <thead>
108
+ <tr>
109
+ <th>Dimension</th>
110
+ <th class="lct-num">Coverage</th>
111
+ <th class="lct-num">Calls with data</th>
112
+ <th>Visual</th>
113
+ </tr>
114
+ </thead>
115
+ <tbody>
116
+ <tr>
117
+ <td>Cost (pricing known)</td>
118
+ <td class="lct-num"><%= percent(@summary.cost_coverage) %></td>
119
+ <td class="lct-num"><%= number_with_delimiter(@summary.calls_with_pricing) %></td>
120
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.cost_coverage, max: 100.0 %></td>
121
+ </tr>
147
122
 
148
- <table class="lct-tbl">
149
- <thead>
150
- <tr>
151
- <th>Dimension</th>
152
- <th class="lct-num">Coverage</th>
153
- <th class="lct-num">Calls with data</th>
154
- <th>Visual</th>
155
- </tr>
156
- </thead>
157
- <tbody>
158
- <tr>
159
- <td>Cost (pricing known)</td>
160
- <td class="lct-num"><%= percent(@summary.cost_coverage) %></td>
161
- <td class="lct-num"><%= number(@summary.calls_with_pricing) %></td>
162
- <td><%= render "llm_cost_tracker/shared/bar", value: @summary.cost_coverage, max: 100.0 %></td>
163
- </tr>
123
+ <tr>
124
+ <td>Tags (at least one tag)</td>
125
+ <td class="lct-num"><%= percent(@summary.tag_coverage) %></td>
126
+ <td class="lct-num"><%= number_with_delimiter(@summary.tagged_calls) %></td>
127
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.tag_coverage, max: 100.0 %></td>
128
+ </tr>
164
129
 
165
- <tr>
166
- <td>Tags (at least one tag)</td>
167
- <td class="lct-num"><%= percent(@summary.tag_coverage) %></td>
168
- <td class="lct-num"><%= number(@summary.tagged_calls) %></td>
169
- <td><%= render "llm_cost_tracker/shared/bar", value: @summary.tag_coverage, max: 100.0 %></td>
170
- </tr>
130
+ <tr>
131
+ <td>Latency</td>
132
+ <td class="lct-num"><%= percent(@summary.latency_coverage) %></td>
133
+ <td class="lct-num"><%= number_with_delimiter(@summary.calls_with_latency) %></td>
134
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.latency_coverage, max: 100.0 %></td>
135
+ </tr>
171
136
 
137
+ <% if @summary.streaming_count.positive? %>
172
138
  <tr>
173
- <td>Latency</td>
174
- <td class="lct-num"><%= percent(@summary.latency_coverage) %></td>
175
- <td class="lct-num"><%= number(@summary.calls_with_latency) %></td>
176
- <td><%= render "llm_cost_tracker/shared/bar", value: @summary.latency_coverage, max: 100.0 %></td>
139
+ <td>Streaming usage captured</td>
140
+ <td class="lct-num"><%= percent(@summary.stream_coverage) %></td>
141
+ <td class="lct-num"><%= number_with_delimiter(@summary.streams_with_usage) %> / <%= number_with_delimiter(@summary.streaming_count) %></td>
142
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.stream_coverage, max: 100.0 %></td>
177
143
  </tr>
144
+ <% end %>
178
145
 
179
- <% if @summary.streaming_count.positive? %>
180
- <tr>
181
- <td>Streaming usage captured</td>
182
- <td class="lct-num"><%= percent(@summary.stream_coverage) %></td>
183
- <td class="lct-num"><%= number(@summary.streams_with_usage) %> / <%= number(@summary.streaming_count) %></td>
184
- <td><%= render "llm_cost_tracker/shared/bar", value: @summary.stream_coverage, max: 100.0 %></td>
185
- </tr>
186
- <% end %>
187
-
188
- <tr>
189
- <td>Provider response ID</td>
190
- <td class="lct-num"><%= percent(@summary.provider_response_id_coverage) %></td>
191
- <td class="lct-num"><%= number(@summary.calls_with_provider_response_id) %></td>
192
- <td><%= render "llm_cost_tracker/shared/bar", value: @summary.provider_response_id_coverage, max: 100.0 %></td>
193
- </tr>
194
- </tbody>
195
- </table>
196
- </section>
197
- </div>
146
+ <tr>
147
+ <td>Provider response ID</td>
148
+ <td class="lct-num"><%= percent(@summary.provider_response_id_coverage) %></td>
149
+ <td class="lct-num"><%= number_with_delimiter(@summary.calls_with_provider_response_id) %></td>
150
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.provider_response_id_coverage, max: 100.0 %></td>
151
+ </tr>
152
+ </tbody>
153
+ </table>
154
+ </section>
198
155
 
199
156
  <section class="lct-panel">
200
157
  <div class="lct-panel-head"><h2 class="lct-panel-title">Token usage</h2></div>
@@ -216,7 +173,7 @@
216
173
  <% cost_value = row.fetch(:cost_value) %>
217
174
  <tr>
218
175
  <td><%= LlmCostTracker::TokenUsageHelper::QUALITY_LABELS.fetch(token_key) %></td>
219
- <td class="lct-num"><%= number(row.fetch(:token_value)) %></td>
176
+ <td class="lct-num"><%= number_with_delimiter(row.fetch(:token_value)) %></td>
220
177
  <% if row.fetch(:share_basis) == :output %>
221
178
  <td class="lct-num"><%= percent(row.fetch(:share_percent)) %> of output</td>
222
179
  <% else %>
@@ -250,13 +207,13 @@
250
207
  </thead>
251
208
  <tbody>
252
209
  <% @service_charge_rows.each do |row| %>
253
- <% unknown_cost = row.cost_status.to_s == LlmCostTracker::Billing::CostStatus::UNKNOWN %>
210
+ <% unknown_cost = row.cost_status.to_s == LlmCostTracker::Charges::CostStatus::UNKNOWN %>
254
211
  <tr>
255
212
  <td><code class="lct-code-id"><%= row.provider %></code></td>
256
213
  <td><code class="lct-code-id"><%= row.component %></code></td>
257
214
  <td><%= row.cost_status %></td>
258
- <td class="lct-num"><%= number(row.charges_count) %></td>
259
- <td class="lct-num"><%= number(row.quantity) %></td>
215
+ <td class="lct-num"><%= number_with_delimiter(row.charges_count) %></td>
216
+ <td class="lct-num"><%= number_with_delimiter(row.quantity) %></td>
260
217
  <td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(row.total_cost) %></td>
261
218
  </tr>
262
219
  <% end %>
@@ -269,7 +226,6 @@
269
226
  <% if @streaming_health_rows.any? %>
270
227
  <section class="lct-panel">
271
228
  <div class="lct-panel-head"><h2 class="lct-panel-title">Streaming health by provider</h2></div>
272
- <p class="lct-panel-intro">Streams without a final usage chunk land as <code>usage_source: unknown</code> and undercount tokens. A high unknown share for an OpenAI-compatible provider usually means <code>stream_options: { include_usage: true }</code> is not being injected for that host.</p>
273
229
 
274
230
  <div class="lct-table-wrap">
275
231
  <table class="lct-tbl">
@@ -286,9 +242,9 @@
286
242
  <% @streaming_health_rows.each do |row| %>
287
243
  <tr>
288
244
  <td><code class="lct-code-id"><%= row.provider %></code></td>
289
- <td class="lct-num"><%= number(row.streams) %></td>
290
- <td class="lct-num"><%= number(row.with_usage) %></td>
291
- <td class="lct-num"><%= number(row.unknown) %></td>
245
+ <td class="lct-num"><%= number_with_delimiter(row.streams) %></td>
246
+ <td class="lct-num"><%= number_with_delimiter(row.with_usage) %></td>
247
+ <td class="lct-num"><%= number_with_delimiter(row.unknown) %></td>
292
248
  <td class="lct-num"><%= percent(row.unknown_share) %></td>
293
249
  </tr>
294
250
  <% end %>
@@ -321,7 +277,7 @@
321
277
  <tr>
322
278
  <td><code class="lct-code-id"><%= row.provider %></code></td>
323
279
  <td><code class="lct-code-id"><%= row.model %></code></td>
324
- <td class="lct-num"><%= number(row.calls) %></td>
280
+ <td class="lct-num"><%= number_with_delimiter(row.calls) %></td>
325
281
  <td class="lct-num"><%= percent(row.share_percent) %></td>
326
282
  </tr>
327
283
  <% end %>
@@ -8,7 +8,7 @@
8
8
  <%= link_to "× Clear filters", models_path(current_query(provider: nil, model: nil, page: nil)), class: "lct-filter-clear" %>
9
9
  <% end %>
10
10
 
11
- <span class="lct-filter-row-meta"><%= number(rows.size) %> model<%= "s" unless rows.size == 1 %></span>
11
+ <span class="lct-filter-row-meta"><%= number_with_delimiter(rows.size) %> model<%= "s" unless rows.size == 1 %></span>
12
12
  </div>
13
13
 
14
14
  <% if rows.empty? %>
@@ -27,7 +27,7 @@
27
27
  <%= sortable_header("Tokens", "tokens", num: true) %>
28
28
  <%= sortable_header("Avg latency", "latency", num: true) %>
29
29
  <%= sortable_header("Avg cost / call", "avg_cost", num: true) %>
30
- <%= sortable_header("Total cost", "cost", num: true) %>
30
+ <%= sortable_header("Total cost", "cost", num: true, default: true) %>
31
31
  <th></th>
32
32
  </tr>
33
33
  </thead>
@@ -36,10 +36,10 @@
36
36
  <tr>
37
37
  <td><span class="lct-model-cell"><span class="lct-provider-dot lct-provider-dot-<%= row.provider %>"></span><%= row.provider %></span></td>
38
38
  <td><code class="lct-code-id"><%= row.model %></code></td>
39
- <td class="lct-num"><%= number(row.calls) %></td>
40
- <td class="lct-num"><%= number(row.total_tokens) %></td>
39
+ <td class="lct-num"><%= number_with_delimiter(row.calls) %></td>
40
+ <td class="lct-num"><%= number_with_delimiter(row.total_tokens) %></td>
41
41
  <% avg_latency = row.average_latency_ms %>
42
- <td class="lct-num<%= ' lct-num-muted' if avg_latency.nil? %>"><%= avg_latency ? "#{number(avg_latency.round)}ms" : "n/a" %></td>
42
+ <td class="lct-num<%= ' lct-num-muted' if avg_latency.nil? %>"><%= avg_latency ? "#{number_with_delimiter(avg_latency.round)}ms" : "n/a" %></td>
43
43
  <td class="lct-num"><%= money(row.average_cost_per_call) %></td>
44
44
  <td class="lct-num"><%= money(row.total_cost) %></td>
45
45
  <td class="lct-num"><%= link_to "Calls", calls_path(calls_query_for_model(provider: row.provider, model: row.model)), class: "lct-page-link" %></td>