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
@@ -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>
@@ -135,7 +143,7 @@ end %>
135
143
  <td><%= line_item.unit %></td>
136
144
  <td class="lct-num"><%= line_item.quantity %></td>
137
145
  <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 %>
146
+ <% unknown_cost = line_item.cost.nil? || line_item.cost_status.to_s == LlmCostTracker::Charges::CostStatus::UNKNOWN %>
139
147
  <td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(line_item.cost) %></td>
140
148
  <td><%= line_item.cost_status %></td>
141
149
  </tr>
@@ -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,7 +7,7 @@
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
13
  <% if @summary.total.zero? %>
@@ -20,19 +20,19 @@
20
20
  <div class="lct-stat-grid">
21
21
  <div class="lct-stat">
22
22
  <div class="lct-stat-head"><p class="lct-stat-label">Calls inspected</p></div>
23
- <p class="lct-stat-value"><%= number(@summary.total) %></p>
23
+ <p class="lct-stat-value"><%= number_with_delimiter(@summary.total) %></p>
24
24
  <p class="lct-stat-foot">in current slice</p>
25
25
  </div>
26
26
 
27
27
  <div class="lct-stat">
28
28
  <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>
29
+ <p class="lct-stat-value"><%= number_with_delimiter(@summary.streaming_count) %></p>
30
30
  <p class="lct-stat-foot"><%= percent(@summary.streaming_share) %> of calls</p>
31
31
  </div>
32
32
 
33
33
  <div class="lct-stat">
34
34
  <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>
35
+ <p class="lct-stat-value"><%= number_with_delimiter(@summary.calls_with_provider_response_id) %></p>
36
36
  <p class="lct-stat-foot"><%= percent(@summary.provider_response_id_coverage) %> of calls</p>
37
37
  </div>
38
38
  </div>
@@ -50,7 +50,7 @@
50
50
  <% if incomplete_pricing.positive? %>
51
51
  <div class="lct-stat lct-stat-warn">
52
52
  <div class="lct-stat-head"><p class="lct-stat-label">Incomplete pricing</p></div>
53
- <p class="lct-stat-value"><%= number(incomplete_pricing) %></p>
53
+ <p class="lct-stat-value"><%= number_with_delimiter(incomplete_pricing) %></p>
54
54
  <p class="lct-stat-foot"><%= percent(@summary.unknown_pricing_share) %> of calls</p>
55
55
  </div>
56
56
  <% end %>
@@ -58,7 +58,7 @@
58
58
  <% if untagged.positive? %>
59
59
  <div class="lct-stat lct-stat-warn">
60
60
  <div class="lct-stat-head"><p class="lct-stat-label">Calls without tags</p></div>
61
- <p class="lct-stat-value"><%= number(untagged) %></p>
61
+ <p class="lct-stat-value"><%= number_with_delimiter(untagged) %></p>
62
62
  <p class="lct-stat-foot"><%= percent(@summary.untagged_share) %> of calls</p>
63
63
  </div>
64
64
  <% end %>
@@ -66,7 +66,7 @@
66
66
  <% if missing_latency.positive? %>
67
67
  <div class="lct-stat lct-stat-warn">
68
68
  <div class="lct-stat-head"><p class="lct-stat-label">Missing latency</p></div>
69
- <p class="lct-stat-value"><%= number(missing_latency) %></p>
69
+ <p class="lct-stat-value"><%= number_with_delimiter(missing_latency) %></p>
70
70
  <p class="lct-stat-foot"><%= percent(@summary.missing_latency_share) %> of calls</p>
71
71
  </div>
72
72
  <% end %>
@@ -74,7 +74,7 @@
74
74
  <% if streams_missing_usage.positive? %>
75
75
  <div class="lct-stat lct-stat-warn">
76
76
  <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>
77
+ <p class="lct-stat-value"><%= number_with_delimiter(streams_missing_usage) %></p>
78
78
  <p class="lct-stat-foot"><%= percent(@summary.streaming_missing_usage_share) %> of streams</p>
79
79
  </div>
80
80
  <% end %>
@@ -83,118 +83,64 @@
83
83
  <div class="lct-stat lct-stat-warn">
84
84
  <div class="lct-stat-head"><p class="lct-stat-label">Hidden output share</p></div>
85
85
  <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>
86
+ <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
87
  </div>
88
88
  <% end %>
89
89
  </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
90
  <% end %>
96
91
 
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>
92
+ <section class="lct-panel">
93
+ <div class="lct-panel-head"><h2 class="lct-panel-title">Coverage summary</h2></div>
143
94
 
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>
95
+ <table class="lct-tbl">
96
+ <thead>
97
+ <tr>
98
+ <th>Dimension</th>
99
+ <th class="lct-num">Coverage</th>
100
+ <th class="lct-num">Calls with data</th>
101
+ <th>Visual</th>
102
+ </tr>
103
+ </thead>
104
+ <tbody>
105
+ <tr>
106
+ <td>Cost (pricing known)</td>
107
+ <td class="lct-num"><%= percent(@summary.cost_coverage) %></td>
108
+ <td class="lct-num"><%= number_with_delimiter(@summary.calls_with_pricing) %></td>
109
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.cost_coverage, max: 100.0 %></td>
110
+ </tr>
147
111
 
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>
112
+ <tr>
113
+ <td>Tags (at least one tag)</td>
114
+ <td class="lct-num"><%= percent(@summary.tag_coverage) %></td>
115
+ <td class="lct-num"><%= number_with_delimiter(@summary.tagged_calls) %></td>
116
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.tag_coverage, max: 100.0 %></td>
117
+ </tr>
164
118
 
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>
119
+ <tr>
120
+ <td>Latency</td>
121
+ <td class="lct-num"><%= percent(@summary.latency_coverage) %></td>
122
+ <td class="lct-num"><%= number_with_delimiter(@summary.calls_with_latency) %></td>
123
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.latency_coverage, max: 100.0 %></td>
124
+ </tr>
171
125
 
126
+ <% if @summary.streaming_count.positive? %>
172
127
  <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>
128
+ <td>Streaming usage captured</td>
129
+ <td class="lct-num"><%= percent(@summary.stream_coverage) %></td>
130
+ <td class="lct-num"><%= number_with_delimiter(@summary.streams_with_usage) %> / <%= number_with_delimiter(@summary.streaming_count) %></td>
131
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.stream_coverage, max: 100.0 %></td>
177
132
  </tr>
133
+ <% end %>
178
134
 
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>
135
+ <tr>
136
+ <td>Provider response ID</td>
137
+ <td class="lct-num"><%= percent(@summary.provider_response_id_coverage) %></td>
138
+ <td class="lct-num"><%= number_with_delimiter(@summary.calls_with_provider_response_id) %></td>
139
+ <td><%= render "llm_cost_tracker/shared/bar", value: @summary.provider_response_id_coverage, max: 100.0 %></td>
140
+ </tr>
141
+ </tbody>
142
+ </table>
143
+ </section>
198
144
 
199
145
  <section class="lct-panel">
200
146
  <div class="lct-panel-head"><h2 class="lct-panel-title">Token usage</h2></div>
@@ -216,7 +162,7 @@
216
162
  <% cost_value = row.fetch(:cost_value) %>
217
163
  <tr>
218
164
  <td><%= LlmCostTracker::TokenUsageHelper::QUALITY_LABELS.fetch(token_key) %></td>
219
- <td class="lct-num"><%= number(row.fetch(:token_value)) %></td>
165
+ <td class="lct-num"><%= number_with_delimiter(row.fetch(:token_value)) %></td>
220
166
  <% if row.fetch(:share_basis) == :output %>
221
167
  <td class="lct-num"><%= percent(row.fetch(:share_percent)) %> of output</td>
222
168
  <% else %>
@@ -250,13 +196,13 @@
250
196
  </thead>
251
197
  <tbody>
252
198
  <% @service_charge_rows.each do |row| %>
253
- <% unknown_cost = row.cost_status.to_s == LlmCostTracker::Billing::CostStatus::UNKNOWN %>
199
+ <% unknown_cost = row.cost_status.to_s == LlmCostTracker::Charges::CostStatus::UNKNOWN %>
254
200
  <tr>
255
201
  <td><code class="lct-code-id"><%= row.provider %></code></td>
256
202
  <td><code class="lct-code-id"><%= row.component %></code></td>
257
203
  <td><%= row.cost_status %></td>
258
- <td class="lct-num"><%= number(row.charges_count) %></td>
259
- <td class="lct-num"><%= number(row.quantity) %></td>
204
+ <td class="lct-num"><%= number_with_delimiter(row.charges_count) %></td>
205
+ <td class="lct-num"><%= number_with_delimiter(row.quantity) %></td>
260
206
  <td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(row.total_cost) %></td>
261
207
  </tr>
262
208
  <% end %>
@@ -269,7 +215,6 @@
269
215
  <% if @streaming_health_rows.any? %>
270
216
  <section class="lct-panel">
271
217
  <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
218
 
274
219
  <div class="lct-table-wrap">
275
220
  <table class="lct-tbl">
@@ -286,9 +231,9 @@
286
231
  <% @streaming_health_rows.each do |row| %>
287
232
  <tr>
288
233
  <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>
234
+ <td class="lct-num"><%= number_with_delimiter(row.streams) %></td>
235
+ <td class="lct-num"><%= number_with_delimiter(row.with_usage) %></td>
236
+ <td class="lct-num"><%= number_with_delimiter(row.unknown) %></td>
292
237
  <td class="lct-num"><%= percent(row.unknown_share) %></td>
293
238
  </tr>
294
239
  <% end %>
@@ -321,7 +266,7 @@
321
266
  <tr>
322
267
  <td><code class="lct-code-id"><%= row.provider %></code></td>
323
268
  <td><code class="lct-code-id"><%= row.model %></code></td>
324
- <td class="lct-num"><%= number(row.calls) %></td>
269
+ <td class="lct-num"><%= number_with_delimiter(row.calls) %></td>
325
270
  <td class="lct-num"><%= percent(row.share_percent) %></td>
326
271
  </tr>
327
272
  <% 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>
@@ -4,7 +4,7 @@
4
4
  <% next unless data %>
5
5
  <% active = source == @active_source %>
6
6
  <%= link_to pricing_path(source: source), class: "lct-tab#{' lct-active' if active}", aria: (active ? { current: "page" } : {}) do %>
7
- <%= data.fetch(:label) %><span class="lct-tab-count"><%= number(data.fetch(:rows).size) %></span>
7
+ <%= data.fetch(:label) %><span class="lct-tab-count"><%= number_with_delimiter(data.fetch(:rows).size) %></span>
8
8
  <% end %>
9
9
  <% end %>
10
10
  </nav>
@@ -46,7 +46,7 @@
46
46
  <% end %>
47
47
 
48
48
  <span class="lct-filter-row-meta">
49
- <%= number(@rows.size) %> entr<%= @rows.size == 1 ? "y" : "ies" %><% if @source_data.fetch(:currency) %> · <%= @source_data.fetch(:currency) %><% end %><% if @source_data.fetch(:updated_at) %> · Updated <%= @source_data.fetch(:updated_at) %><% end %>
49
+ <%= number_with_delimiter(@rows.size) %> entr<%= @rows.size == 1 ? "y" : "ies" %><% if @source_data.fetch(:currency) %> · <%= @source_data.fetch(:currency) %><% end %><% if @source_data.fetch(:updated_at) %> · Updated <%= @source_data.fetch(:updated_at) %><% end %>
50
50
  </span>
51
51
  </div>
52
52
 
@@ -15,7 +15,7 @@
15
15
  <% end %>
16
16
  <div class="lct-filter-pop-field">
17
17
  <label for="lct-filter-model">Model</label>
18
- <%= select_tag :model, options_for_select(model_filter_options, active), include_blank: "All models", id: "lct-filter-model" %>
18
+ <%= select_tag :model, options_for_select(filter_options_for(:model), active), include_blank: "All models", id: "lct-filter-model" %>
19
19
  </div>
20
20
  <button type="submit" class="lct-button lct-button-primary">Apply</button>
21
21
  <% end %>
@@ -15,7 +15,7 @@
15
15
  <% end %>
16
16
  <div class="lct-filter-pop-field">
17
17
  <label for="lct-filter-provider">Provider</label>
18
- <%= select_tag :provider, options_for_select(provider_filter_options, active), include_blank: "All providers", id: "lct-filter-provider" %>
18
+ <%= select_tag :provider, options_for_select(filter_options_for(:provider), active), include_blank: "All providers", id: "lct-filter-provider" %>
19
19
  </div>
20
20
  <button type="submit" class="lct-button lct-button-primary">Apply</button>
21
21
  <% end %>
@@ -16,7 +16,7 @@
16
16
  <% end %>
17
17
  <div class="lct-filter-pop-field">
18
18
  <label for="lct-filter-stream">Stream</label>
19
- <%= select_tag :stream, options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, active), include_blank: "All calls", id: "lct-filter-stream" %>
19
+ <%= select_tag :stream, options_for_select(LlmCostTracker::Dashboard::Filter::STREAM_FILTER_OPTIONS, active), include_blank: "All calls", id: "lct-filter-stream" %>
20
20
  </div>
21
21
  <button type="submit" class="lct-button lct-button-primary">Apply</button>
22
22
  <% end %>