llm_cost_tracker 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +11 -5
  4. data/app/assets/llm_cost_tracker/application.css +784 -802
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +14 -2
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +28 -21
  7. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -4
  8. data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
  9. data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
  10. data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
  11. data/app/helpers/llm_cost_tracker/application_helper.rb +19 -16
  12. data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
  13. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
  14. data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
  15. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
  16. data/app/models/llm_cost_tracker/call.rb +28 -63
  17. data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
  18. data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
  19. data/app/models/llm_cost_tracker/call_tag.rb +0 -2
  20. data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +2 -0
  21. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +64 -43
  22. data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
  23. data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
  24. data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
  25. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
  26. data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
  27. data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +81 -0
  28. data/app/services/llm_cost_tracker/dashboard/setup_state.rb +6 -68
  29. data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
  30. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +20 -12
  31. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
  32. data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
  33. data/app/views/layouts/llm_cost_tracker/application.html.erb +74 -17
  34. data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
  35. data/app/views/llm_cost_tracker/calls/show.html.erb +132 -125
  36. data/app/views/llm_cost_tracker/dashboard/index.html.erb +120 -159
  37. data/app/views/llm_cost_tracker/data_quality/index.html.erb +140 -194
  38. data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
  39. data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
  40. data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
  41. data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
  42. data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
  43. data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
  44. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
  45. data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
  46. data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
  47. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
  48. data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
  49. data/app/views/llm_cost_tracker/tags/show.html.erb +85 -104
  50. data/config/routes.rb +3 -3
  51. data/lib/llm_cost_tracker/budget.rb +25 -28
  52. data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
  53. data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +2 -1
  54. data/lib/llm_cost_tracker/capture/stream_collector.rb +30 -52
  55. data/lib/llm_cost_tracker/capture/stream_tracker.rb +18 -33
  56. data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
  57. data/lib/llm_cost_tracker/charges/cost.rb +27 -0
  58. data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
  59. data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +40 -44
  60. data/lib/llm_cost_tracker/check.rb +5 -0
  61. data/lib/llm_cost_tracker/configuration.rb +13 -61
  62. data/lib/llm_cost_tracker/currency.rb +5 -0
  63. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
  64. data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
  65. data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
  66. data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
  67. data/lib/llm_cost_tracker/doctor.rb +66 -64
  68. data/lib/llm_cost_tracker/engine.rb +4 -4
  69. data/lib/llm_cost_tracker/event.rb +12 -20
  70. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
  71. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
  72. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
  73. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -2
  74. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +4 -0
  75. data/lib/llm_cost_tracker/ingestion/batch.rb +39 -8
  76. data/lib/llm_cost_tracker/ingestion/inbox.rb +8 -9
  77. data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
  78. data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
  79. data/lib/llm_cost_tracker/ingestion.rb +24 -36
  80. data/lib/llm_cost_tracker/integrations/anthropic.rb +94 -116
  81. data/lib/llm_cost_tracker/integrations/base.rb +39 -57
  82. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
  83. data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
  84. data/lib/llm_cost_tracker/integrations/openai.rb +72 -332
  85. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +89 -145
  86. data/lib/llm_cost_tracker/integrations.rb +32 -25
  87. data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
  88. data/lib/llm_cost_tracker/ledger/period.rb +5 -10
  89. data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
  90. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
  91. data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
  92. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
  93. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
  94. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
  95. data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
  96. data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
  97. data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
  98. data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
  99. data/lib/llm_cost_tracker/ledger/store.rb +18 -42
  100. data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
  101. data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
  102. data/lib/llm_cost_tracker/ledger.rb +14 -11
  103. data/lib/llm_cost_tracker/logging.rb +4 -21
  104. data/lib/llm_cost_tracker/middleware/faraday.rb +63 -51
  105. data/lib/llm_cost_tracker/parsers.rb +140 -29
  106. data/lib/llm_cost_tracker/prices.json +1707 -1
  107. data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
  108. data/lib/llm_cost_tracker/pricing/calculation.rb +260 -0
  109. data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
  110. data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
  111. data/lib/llm_cost_tracker/pricing/matcher.rb +84 -0
  112. data/lib/llm_cost_tracker/pricing/mode.rb +53 -35
  113. data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
  114. data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
  115. data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
  116. data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
  117. data/lib/llm_cost_tracker/pricing/source.rb +7 -0
  118. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
  119. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
  120. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
  121. data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
  122. data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
  123. data/lib/llm_cost_tracker/pricing.rb +10 -295
  124. data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
  125. data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
  126. data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
  127. data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
  128. data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
  129. data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
  130. data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
  131. data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
  132. data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
  133. data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
  134. data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
  135. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
  136. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +181 -0
  137. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
  138. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
  139. data/lib/llm_cost_tracker/providers.rb +35 -0
  140. data/lib/llm_cost_tracker/railtie.rb +0 -7
  141. data/lib/llm_cost_tracker/report/data.rb +3 -4
  142. data/lib/llm_cost_tracker/report/formatter.rb +33 -20
  143. data/lib/llm_cost_tracker/report.rb +1 -1
  144. data/lib/llm_cost_tracker/retention.rb +6 -19
  145. data/lib/llm_cost_tracker/tags/context.rb +9 -6
  146. data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
  147. data/lib/llm_cost_tracker/timing.rb +2 -4
  148. data/lib/llm_cost_tracker/tracker.rb +24 -36
  149. data/lib/llm_cost_tracker/usage/catalog.rb +58 -0
  150. data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
  151. data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +24 -46
  152. data/lib/llm_cost_tracker/usage/source.rb +14 -0
  153. data/lib/llm_cost_tracker/usage/token_usage.rb +100 -0
  154. data/lib/llm_cost_tracker/version.rb +1 -1
  155. data/lib/llm_cost_tracker.rb +43 -52
  156. data/lib/tasks/llm_cost_tracker.rake +14 -73
  157. metadata +92 -58
  158. data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -106
  159. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
  160. data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
  161. data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
  162. data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
  163. data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -183
  164. data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
  165. data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
  166. data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
  167. data/lib/llm_cost_tracker/billing/components.rb +0 -95
  168. data/lib/llm_cost_tracker/capture/stream.rb +0 -9
  169. data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
  170. data/lib/llm_cost_tracker/doctor/check.rb +0 -7
  171. data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
  172. data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
  173. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
  174. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
  175. data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
  176. data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
  177. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
  178. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -32
  179. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -25
  180. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
  181. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
  182. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
  183. data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
  184. data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
  185. data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
  186. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
  187. data/lib/llm_cost_tracker/masking.rb +0 -39
  188. data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -193
  189. data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
  190. data/lib/llm_cost_tracker/parsers/base.rb +0 -131
  191. data/lib/llm_cost_tracker/parsers/gemini.rb +0 -232
  192. data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
  193. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -51
  194. data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -155
  195. data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
  196. data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
  197. data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
  198. data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
  199. data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
  200. data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -134
  201. data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
  202. data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
  203. data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
  204. data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -254
  205. data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -172
  206. data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
  207. data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -142
  208. data/lib/llm_cost_tracker/reconciliation.rb +0 -118
  209. data/lib/llm_cost_tracker/token_usage.rb +0 -93
@@ -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? %>
@@ -12,156 +12,163 @@ end %>
12
12
  { label: component.fetch(:label), value: value, formatted_value: optional_money(value), css_class: component.fetch(:css_class) }
13
13
  end %>
14
14
  <% end %>
15
+ <% service_line_items = @call.line_items.reject { |li| li.unit == "token" }.sort_by(&:position) %>
15
16
 
16
- <section class="lct-panel">
17
- <nav class="lct-breadcrumb" aria-label="Breadcrumb">
18
- <%= link_to "Calls", calls_path, class: "lct-breadcrumb-link" %>
19
- <span class="lct-breadcrumb-sep" aria-hidden="true">›</span>
20
- <span class="lct-breadcrumb-current">#<%= @call.id %></span>
21
- </nav>
22
- <div class="lct-call-hero">
23
- <div>
24
- <h2 class="lct-section-title lct-call-title">
25
- <code class="lct-code"><%= @call.model %></code>
26
- </h2>
27
- <p class="lct-call-subtitle">
28
- <code class="lct-code"><%= @call.provider %></code>
29
- <span>·</span>
30
- <span><%= format_date(@call.tracked_at) %></span>
31
- </p>
32
- </div>
33
-
34
- <div class="lct-call-summary">
35
- <div class="lct-call-summary-item">
36
- <span class="lct-call-summary-label">Pricing</span>
37
- <strong><%= pricing_status(@call) %></strong>
38
- </div>
39
- <div class="lct-call-summary-item">
40
- <span class="lct-call-summary-label">Total cost</span>
41
- <strong><%= optional_money(@call.total_cost) %></strong>
42
- </div>
43
- <div class="lct-call-summary-item">
44
- <span class="lct-call-summary-label">Total tokens</span>
45
- <strong><%= number(@call.total_tokens) %></strong>
46
- </div>
47
- <div class="lct-call-summary-item">
48
- <span class="lct-call-summary-label">Latency</span>
49
- <strong><%= @call.latency_ms ? "#{optional_number(@call.latency_ms)}ms" : "n/a" %></strong>
50
- </div>
51
- </div>
17
+ <p class="lct-breadcrumb-back"><%= link_to "← All calls", calls_path %></p>
18
+ <h2 class="lct-page-title"><code class="lct-code-id"><%= @call.model %></code> <span class="lct-page-title-meta">#<%= @call.id %></span></h2>
19
+ <p class="lct-page-subtitle">
20
+ <span class="lct-model-cell"><span class="lct-provider-dot lct-provider-dot-<%= @call.provider %>"></span><%= @call.provider %></span>
21
+ <span class="lct-page-subtitle-sep">·</span>
22
+ <span><%= format_date(@call.tracked_at) %></span>
23
+ </p>
24
+
25
+ <div class="lct-stat-grid">
26
+ <div class="lct-stat">
27
+ <div class="lct-stat-head"><p class="lct-stat-label">Pricing</p></div>
28
+ <p class="lct-stat-value-sm"><%= pricing_status(@call) %></p>
29
+ </div>
30
+ <div class="lct-stat">
31
+ <div class="lct-stat-head"><p class="lct-stat-label">Total cost</p></div>
32
+ <p class="lct-stat-value<%= ' lct-num-muted' if @call.total_cost.nil? %>"><%= optional_money(@call.total_cost) %></p>
52
33
  </div>
34
+ <div class="lct-stat">
35
+ <div class="lct-stat-head"><p class="lct-stat-label">Total tokens</p></div>
36
+ <p class="lct-stat-value"><%= number_with_delimiter(@call.total_tokens) %></p>
37
+ </div>
38
+ <div class="lct-stat">
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_with_delimiter(@call.latency_ms) : "n/a" %><% if @call.latency_ms %><span class="lct-stat-unit">ms</span><% end %></p>
41
+ </div>
42
+ </div>
53
43
 
54
- <div class="lct-call-breakdown-grid">
55
- <section class="lct-call-breakdown">
56
- <h3 class="lct-call-breakdown-title">Token Mix</h3>
44
+ <div class="lct-grid-2">
45
+ <section class="lct-panel">
46
+ <div class="lct-panel-head"><h2 class="lct-panel-title">Token mix</h2></div>
47
+ <div class="lct-panel-body">
57
48
  <%= render "llm_cost_tracker/shared/metric_stack", segments: token_segments, empty_message: "No token data for this call." %>
58
- </section>
49
+ </div>
50
+ </section>
59
51
 
60
- <section class="lct-call-breakdown">
61
- <h3 class="lct-call-breakdown-title">Cost Mix</h3>
52
+ <section class="lct-panel">
53
+ <div class="lct-panel-head"><h2 class="lct-panel-title">Cost mix</h2></div>
54
+ <div class="lct-panel-body">
62
55
  <% if @call.total_cost.nil? %>
63
- <p class="lct-call-breakdown-empty">Pricing not available for this call.</p>
56
+ <p class="lct-stack-empty">Pricing not available for this call.</p>
64
57
  <% else %>
65
58
  <%= render "llm_cost_tracker/shared/metric_stack", segments: cost_segments, empty_message: "No cost breakdown for this call." %>
66
59
  <% end %>
67
- </section>
68
- </div>
69
-
70
- <div class="lct-detail-grid">
71
- <dl class="lct-dl">
72
- <dt>Cost Status</dt>
73
- <dd><%= @call.cost_status.presence || "n/a" %></dd>
74
-
75
- <dt>Latency</dt>
76
- <dd><%= @call.latency_ms ? "#{optional_number(@call.latency_ms)}ms" : "n/a" %></dd>
60
+ </div>
61
+ </section>
62
+ </div>
77
63
 
64
+ <section class="lct-panel">
65
+ <div class="lct-panel-head"><h2 class="lct-panel-title">Provider context</h2></div>
66
+ <dl class="lct-meta-strip">
67
+ <div class="lct-meta-strip-item">
78
68
  <dt>Batch</dt>
79
69
  <dd><%= @call.batch? ? "yes" : "no" %></dd>
70
+ </div>
71
+ <% if @call.has_attribute?(:stream) %>
72
+ <div class="lct-meta-strip-item">
73
+ <dt>Stream</dt>
74
+ <dd><%= @call.stream? ? "yes" : "no" %></dd>
75
+ </div>
76
+ <% end %>
77
+ <% if @call.has_attribute?(:usage_source) && @call.usage_source.present? %>
78
+ <div class="lct-meta-strip-item">
79
+ <dt>Usage source</dt>
80
+ <dd><code class="lct-code-id"><%= @call.usage_source %></code></dd>
81
+ </div>
82
+ <% end %>
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 %>
107
+ </dl>
108
+ </section>
80
109
 
81
- <dt>Response ID</dt>
82
- <dd><%= @call.provider_response_id.presence || "n/a" %></dd>
83
-
84
- <dt>Project ID</dt>
85
- <dd><%= @call.provider_project_id.present? ? LlmCostTracker::Masking.mask_value(:provider_project_id, @call.provider_project_id) : "n/a" %></dd>
86
-
87
- <dt>API Key ID</dt>
88
- <dd><%= @call.provider_api_key_id.present? ? LlmCostTracker::Masking.mask_value(:provider_api_key_id, @call.provider_api_key_id) : "n/a" %></dd>
89
-
90
- <dt>Workspace ID</dt>
91
- <dd><%= @call.provider_workspace_id.present? ? LlmCostTracker::Masking.mask_value(:provider_workspace_id, @call.provider_workspace_id) : "n/a" %></dd>
92
- </dl>
93
-
94
- <dl class="lct-dl">
95
- <% priced_components.each do |component| %>
96
- <dt><%= component.fetch(:label).titleize %> Tokens</dt>
97
- <dd><%= number(@call[component.fetch(:token_key)]) %></dd>
98
- <% end %>
99
-
100
- <dt>Total Tokens</dt>
101
- <dd><%= number(@call.total_tokens) %></dd>
102
- </dl>
103
-
104
- <dl class="lct-dl">
105
- <% priced_components.each do |component| %>
106
- <dt><%= component.fetch(:label).titleize %> Cost</dt>
107
- <dd><%= optional_money(line_item_costs_by_component[component.fetch(:price_key)]) %></dd>
108
- <% end %>
109
-
110
- <dt>Total Cost</dt>
111
- <dd><%= optional_money(@call.total_cost) %></dd>
112
- </dl>
110
+ <section class="lct-panel">
111
+ <div class="lct-panel-head"><h2 class="lct-panel-title">Tags</h2></div>
112
+ <div class="lct-panel-body">
113
+ <% if @call.tag_pairs.empty? %>
114
+ <p class="lct-stack-empty">(untagged)</p>
115
+ <% else %>
116
+ <span class="lct-tag-chips">
117
+ <% @call.tag_pairs.each do |k, v| %>
118
+ <span class="lct-tag-chip"><span class="lct-tag-chip-key"><%= k %></span>=<%= v %></span>
119
+ <% end %>
120
+ </span>
121
+ <% end %>
113
122
  </div>
114
123
  </section>
115
124
 
116
- <% service_line_items = @call.line_items.reject { |li| li.unit == "token" }.sort_by(&:position) %>
117
125
  <% if service_line_items.any? %>
118
126
  <section class="lct-panel">
119
- <h2 class="lct-section-title">Service Charges</h2>
120
- <div class="lct-table-wrap">
121
- <table class="lct-table lct-table-compact">
122
- <thead>
127
+ <div class="lct-panel-head"><h2 class="lct-panel-title">Service charges</h2></div>
128
+ <table class="lct-tbl">
129
+ <thead>
130
+ <tr>
131
+ <th>Component</th>
132
+ <th>Unit</th>
133
+ <th class="lct-num">Quantity</th>
134
+ <th class="lct-num">Rate</th>
135
+ <th class="lct-num">Cost</th>
136
+ <th>Status</th>
137
+ </tr>
138
+ </thead>
139
+ <tbody>
140
+ <% service_line_items.each do |line_item| %>
123
141
  <tr>
124
- <th>Component</th>
125
- <th>Unit</th>
126
- <th class="lct-num">Quantity</th>
127
- <th class="lct-num">Rate</th>
128
- <th class="lct-num">Cost</th>
129
- <th>Status</th>
142
+ <td><code class="lct-code-id"><%= line_item.kind %></code></td>
143
+ <td><%= line_item.unit %></td>
144
+ <td class="lct-num"><%= line_item.quantity %></td>
145
+ <td class="lct-num"><%= line_item.rate_amount ? "#{optional_money(line_item.rate_amount)} / #{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) %></td>
148
+ <td><%= line_item.cost_status %></td>
130
149
  </tr>
131
- </thead>
132
- <tbody>
133
- <% service_line_items.each do |line_item| %>
134
- <tr>
135
- <td><code class="lct-code"><%= line_item.kind %></code></td>
136
- <td><%= line_item.unit %></td>
137
- <td class="lct-num"><%= line_item.quantity %></td>
138
- <td class="lct-num"><%= line_item.rate_amount ? "#{optional_money(line_item.rate_amount)} / #{line_item.rate_quantity}" : "n/a" %></td>
139
- <% unknown_cost = line_item.cost.nil? || line_item.cost_status.to_s == LlmCostTracker::Billing::CostStatus::UNKNOWN %>
140
- <td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(line_item.cost) %></td>
141
- <td><%= line_item.cost_status %></td>
142
- </tr>
143
- <% end %>
144
- </tbody>
145
- </table>
146
- </div>
150
+ <% end %>
151
+ </tbody>
152
+ </table>
147
153
  </section>
148
154
  <% end %>
149
155
 
150
156
  <% if @call.pricing_snapshot.present? %>
151
- <section class="lct-panel">
152
- <h2 class="lct-section-title">Pricing Snapshot</h2>
157
+ <details class="lct-panel lct-disclose">
158
+ <summary class="lct-panel-head lct-disclose-summary">
159
+ <h2 class="lct-panel-title">Pricing snapshot</h2>
160
+ <span class="lct-disclose-hint">show JSON</span>
161
+ </summary>
153
162
  <pre class="lct-pre"><%= safe_json(@call.pricing_snapshot) %></pre>
154
- </section>
163
+ </details>
155
164
  <% end %>
156
165
 
157
- <section class="lct-panel">
158
- <h2 class="lct-section-title">Tags</h2>
159
- <pre class="lct-pre"><%= safe_json(@call.parsed_tags) %></pre>
160
- </section>
161
-
162
166
  <% if @call.has_attribute?("metadata") %>
163
- <section class="lct-panel">
164
- <h2 class="lct-section-title">Metadata</h2>
165
- <pre class="lct-pre"><%= safe_json(LlmCostTracker::Masking.mask_hash(masked_metadata_hash(@call.read_attribute("metadata")))) %></pre>
166
- </section>
167
+ <details class="lct-panel lct-disclose">
168
+ <summary class="lct-panel-head lct-disclose-summary">
169
+ <h2 class="lct-panel-title">Metadata</h2>
170
+ <span class="lct-disclose-hint">show JSON</span>
171
+ </summary>
172
+ <pre class="lct-pre"><%= safe_json(LlmCostTracker::Dashboard::Masking.mask_hash(masked_metadata_hash(@call.read_attribute("metadata")))) %></pre>
173
+ </details>
167
174
  <% end %>
@@ -1,202 +1,163 @@
1
- <% overview_filter_scope = current_query(from: @from_date.iso8601, to: @to_date.iso8601) %>
1
+ <div class="lct-filter-row">
2
+ <%= render "llm_cost_tracker/shared/filter_pill_date", path: root_path %>
3
+ <%= render "llm_cost_tracker/shared/filter_pill_provider", path: root_path %>
4
+ <%= render "llm_cost_tracker/shared/filter_pill_model", path: root_path %>
2
5
 
3
- <section class="lct-panel lct-toolbar">
4
- <%= render "llm_cost_tracker/shared/filters",
5
- path: root_path,
6
- filter_scope: overview_filter_scope,
7
- defaults: { from: @from_date.iso8601, to: @to_date.iso8601 } %>
6
+ <% if params[:provider].present? || params[:model].present? %>
7
+ <%= link_to "× Clear filters", root_path(current_query(provider: nil, model: nil, page: nil)), class: "lct-filter-clear" %>
8
+ <% end %>
8
9
 
9
- <%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: root_path %>
10
- </section>
10
+ <span class="lct-filter-row-meta">
11
+ <%= number_with_delimiter(@stats.total_calls) %> call<%= "s" unless @stats.total_calls.to_i == 1 %> · <%= money(@stats.total_cost) %>
12
+ </span>
13
+ </div>
11
14
 
12
15
  <% if @stats.total_calls.to_i.zero? %>
13
16
  <section class="lct-panel lct-empty">
14
17
  <h2 class="lct-state-title">No LLM calls yet</h2>
15
18
  <p class="lct-state-copy">Tracked requests will appear here after your application records its first LLM API call.</p>
16
- <div class="lct-state-actions">
17
- <%= link_to "Calls", calls_path, class: "lct-button lct-button-secondary" %>
18
- </div>
19
19
  </section>
20
20
  <% else %>
21
21
  <% if @stats.unknown_pricing_count.to_i.positive? %>
22
- <aside class="lct-banner lct-banner-warning" role="status">
23
- <div class="lct-banner-body">
24
- <p class="lct-banner-title">
25
- <%= number(@stats.unknown_pricing_count) %> call<%= "s" unless @stats.unknown_pricing_count.to_i == 1 %> missing pricing
26
- <span class="lct-banner-muted">· <%= percent(coverage_percent(@stats.unknown_pricing_count, @stats.total_calls)) %> of the slice</span>
27
- </p>
28
- <p class="lct-banner-copy">Totals undercount until every model has a known price. Update <code class="lct-code">pricing_overrides</code> or <code class="lct-code">prices_file</code> to fix.</p>
29
- </div>
30
- <%= link_to "Fix now →", data_quality_path, class: "lct-button lct-button-secondary" %>
31
- </aside>
22
+ <div class="lct-alert lct-alert-warn">
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
+ <span>
25
+ <strong><%= number_with_delimiter(@stats.unknown_pricing_count) %> call<%= "s" unless @stats.unknown_pricing_count.to_i == 1 %> with incomplete pricing</strong>
26
+ · <%= percent(coverage_percent(@stats.unknown_pricing_count, @stats.total_calls)) %> of slice
27
+ — update <code>pricing_overrides</code> or <code>prices_file</code>.
28
+ </span>
29
+ <%= link_to "Fix now →", data_quality_path, class: "lct-alert-action" %>
30
+ </div>
32
31
  <% end %>
33
32
 
34
33
  <% if @spend_anomaly %>
35
- <aside class="lct-banner lct-banner-danger" role="status">
36
- <div class="lct-banner-body">
37
- <p class="lct-banner-title">
38
- Spend anomaly detected
39
- <span class="lct-banner-muted">· <code class="lct-code"><%= @spend_anomaly.fetch(:model) %></code> on <%= @spend_anomaly.fetch(:day).strftime("%b %-d") %></span>
40
- </p>
41
- <p class="lct-banner-copy">
42
- <% if @spend_anomaly.fetch(:ratio) %>
43
- <%= number_with_precision(@spend_anomaly.fetch(:ratio), precision: 1) %>× its prior 7-day average in this slice
44
- <% else %>
45
- <%= money(@spend_anomaly.fetch(:latest_spend)) %> after seven quiet days in this slice
46
- <% end %>
47
- </p>
48
- </div>
49
- <%= link_to "Calls",
34
+ <div class="lct-alert lct-alert-danger">
35
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
36
+ <span>
37
+ <strong>Spend anomaly detected</strong>
38
+ · <code><%= @spend_anomaly.fetch(:model) %></code> on <%= @spend_anomaly.fetch(:day).strftime("%b %-d") %>
39
+ <% if @spend_anomaly.fetch(:ratio) %>
40
+ <%= number_with_precision(@spend_anomaly.fetch(:ratio), precision: 1) %>× its prior 7-day average.
41
+ <% else %>
42
+ <%= money(@spend_anomaly.fetch(:latest_spend)) %> after seven quiet days.
43
+ <% end %>
44
+ </span>
45
+ <%= link_to "View calls →",
50
46
  calls_path(current_query(provider: @spend_anomaly.fetch(:provider), model: @spend_anomaly.fetch(:model), from: @to_date.iso8601, to: @to_date.iso8601, page: nil, per: nil, format: nil)),
51
- class: "lct-button lct-button-secondary" %>
52
- </aside>
47
+ class: "lct-alert-action" %>
48
+ </div>
53
49
  <% end %>
54
50
 
55
- <section class="lct-hero">
56
- <article class="lct-panel lct-hero-primary">
57
- <div>
51
+ <div class="lct-stat-grid">
52
+ <div class="lct-stat">
53
+ <div class="lct-stat-head">
58
54
  <p class="lct-stat-label">Total spend</p>
59
- <p class="lct-hero-value"><%= money(@stats.total_cost) %></p>
60
55
  <% badge = delta_badge(@stats.cost_delta_percent) %>
61
- <span class="<%= badge[:css_class] %>"><span class="lct-delta-dot" aria-hidden="true"></span><%= badge[:text] %></span>
56
+ <span class="<%= badge[:css_class] %>"><%= badge[:text] %></span>
62
57
  </div>
63
- </article>
64
-
65
- <div class="lct-hero-side">
66
- <div class="lct-stat-grid">
67
- <article class="lct-stat">
68
- <p class="lct-stat-label">Avg cost / call</p>
69
- <p class="lct-stat-value"><%= money(@stats.average_cost_per_call) %></p>
70
- </article>
71
-
72
- <article class="lct-stat">
73
- <p class="lct-stat-label">Calls</p>
74
- <p class="lct-stat-value"><%= number(@stats.total_calls) %></p>
75
- <% badge = delta_badge(@stats.calls_delta_percent, mode: :neutral) %>
76
- <span class="<%= badge[:css_class] %>"><span class="lct-delta-dot" aria-hidden="true"></span><%= badge[:text] %></span>
77
- </article>
78
-
79
- <% if @stats.average_latency_ms %>
80
- <article class="lct-stat">
81
- <p class="lct-stat-label">Avg latency</p>
82
- <p class="lct-stat-value"><%= number(@stats.average_latency_ms.round) %>ms</p>
83
- </article>
84
- <% end %>
58
+ <p class="lct-stat-value"><%= money(@stats.total_cost) %></p>
59
+ <p class="lct-stat-foot">Avg <%= money(@stats.average_cost_per_call) %> / call</p>
60
+ </div>
61
+ <div class="lct-stat lct-stat-ok">
62
+ <div class="lct-stat-head">
63
+ <p class="lct-stat-label">Calls</p>
64
+ <% badge = delta_badge(@stats.calls_delta_percent, mode: :neutral) %>
65
+ <span class="<%= badge[:css_class] %>"><%= badge[:text] %></span>
66
+ </div>
67
+ <p class="lct-stat-value"><%= number_with_delimiter(@stats.total_calls) %></p>
68
+ </div>
69
+ <% if @stats.average_latency_ms %>
70
+ <div class="lct-stat lct-stat-ok">
71
+ <div class="lct-stat-head"><p class="lct-stat-label">Avg latency</p></div>
72
+ <p class="lct-stat-value"><%= number_with_delimiter(@stats.average_latency_ms.round) %><span class="lct-stat-unit">ms</span></p>
73
+ </div>
74
+ <% else %>
75
+ <div class="lct-stat">
76
+ <div class="lct-stat-head"><p class="lct-stat-label">Avg latency</p></div>
77
+ <p class="lct-stat-value lct-num-muted">n/a</p>
78
+ </div>
79
+ <% end %>
80
+ <div class="lct-stat <%= @stats.unknown_pricing_count.to_i.positive? ? 'lct-stat-warn' : 'lct-stat-ok' %>">
81
+ <div class="lct-stat-head"><p class="lct-stat-label">Incomplete pricing</p></div>
82
+ <p class="lct-stat-value"><%= number_with_delimiter(@stats.unknown_pricing_count) %></p>
83
+ <p class="lct-stat-foot"><%= percent(coverage_percent(@stats.unknown_pricing_count, @stats.total_calls)) %> of slice</p>
84
+ </div>
85
+ </div>
85
86
 
87
+ <% if @monthly_budget_status %>
88
+ <% budget = @monthly_budget_status %>
89
+ <section class="lct-panel">
90
+ <div class="lct-panel-head">
91
+ <h2 class="lct-panel-title">Monthly budget</h2>
92
+ <span class="lct-panel-meta">
93
+ <span><%= money(budget[:spent]) %> of <%= money(budget[:budget]) %> · <%= percent(budget[:percent_used]) %></span>
94
+ </span>
95
+ </div>
96
+ <div class="lct-panel-body">
97
+ <div class="lct-budget-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<%= budget[:progress_percent].round %>">
98
+ <div data-lct-style="<%= inline_style("width: #{budget[:progress_percent]}%") %>" class="lct-budget-fill <%= budget[:fill_modifier] %>"></div>
99
+ <% if budget[:projected_spent].positive? %>
100
+ <span data-lct-style="<%= inline_style("left: calc(#{budget[:projected_marker_percent]}% - 1px)") %>" class="lct-budget-marker" aria-hidden="true"></span>
101
+ <% end %>
102
+ </div>
103
+ <p class="lct-budget-projection">
104
+ <span>Projected <strong><%= money(budget[:projected_spent]) %></strong> by <%= budget[:projection_end_label] %></span>
105
+ <span class="lct-budget-projection-status <%= budget[:projected_delta_status_class] %>">
106
+ <%= money(budget[:projected_delta_amount]) %> <%= budget[:projected_delta_direction] %> budget
107
+ </span>
108
+ </p>
86
109
  </div>
110
+ </section>
111
+ <% end %>
87
112
 
88
- <% if @monthly_budget_status %>
89
- <% budget = @monthly_budget_status %>
90
- <section class="lct-panel lct-panel-tight">
91
- <div class="lct-section-head">
92
- <div>
93
- <h2 class="lct-section-title">Monthly Budget</h2>
94
- <p class="lct-section-copy">Current-month spend across <strong>all</strong> calls.</p>
95
- </div>
96
- </div>
97
- <div class="lct-budget">
98
- <div class="lct-budget-head">
99
- <span>
100
- <span class="lct-budget-spent"><%= money(budget[:spent]) %></span>
101
- <span class="lct-budget-of"> of <%= money(budget[:budget]) %></span>
102
- </span>
103
- <span class="lct-budget-percent"><%= percent(budget[:percent_used]) %></span>
104
- </div>
105
- <div class="lct-budget-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<%= budget[:progress_percent].round %>">
106
- <div data-lct-style="<%= inline_style("width: #{budget[:progress_percent]}%") %>" class="lct-budget-fill <%= budget[:fill_modifier] %>"></div>
107
- <% if budget[:projected_spent].positive? %>
108
- <span data-lct-style="<%= inline_style("left: calc(#{budget[:projected_marker_percent]}% - 1px)") %>" class="lct-budget-marker" aria-hidden="true"></span>
109
- <% end %>
110
- </div>
111
- <p class="lct-budget-projection">
112
- <span>Projected <strong><%= money(budget[:projected_spent]) %></strong> by <%= budget[:projection_end_label] %></span>
113
- <span class="lct-budget-projection-status <%= budget[:projected_delta_status_class] %>">
114
- <%= money(budget[:projected_delta_amount]) %> <%= budget[:projected_delta_direction] %> budget
115
- </span>
116
- </p>
117
- <p class="lct-budget-meta">Soft limit: blocking is not atomic under concurrency.</p>
118
- </div>
119
- </section>
120
- <% end %>
113
+ <section class="lct-panel">
114
+ <div class="lct-panel-head">
115
+ <h2 class="lct-panel-title">Daily spend</h2>
116
+ <span class="lct-panel-meta">
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_with_delimiter(@comparison_series.size) %>d</span><% end %>
119
+ </span>
121
120
  </div>
121
+ <%= render "llm_cost_tracker/shared/spend_chart", series: @time_series, comparison_series: @comparison_series %>
122
122
  </section>
123
123
 
124
- <section class="lct-grid lct-two-col">
124
+ <div class="lct-grid-2">
125
125
  <section class="lct-panel">
126
- <div class="lct-section-head">
127
- <div>
128
- <h2 class="lct-section-title">Daily Spend</h2>
129
- <p class="lct-section-copy">Current slice vs. previous <%= number(@comparison_series.size) %>-day slice.</p>
130
- </div>
126
+ <div class="lct-panel-head">
127
+ <h2 class="lct-panel-title">Top models</h2>
128
+ <span class="lct-panel-meta"><%= link_to "View all →", models_path(current_query) %></span>
131
129
  </div>
132
- <%= render "llm_cost_tracker/shared/spend_chart", series: @time_series, comparison_series: @comparison_series %>
130
+ <table class="lct-tbl">
131
+ <thead><tr><th>Model</th><th class="lct-num">Calls</th><th class="lct-num">Cost</th></tr></thead>
132
+ <tbody>
133
+ <% @top_models.first(5).each do |row| %>
134
+ <tr>
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_with_delimiter(row.calls) %></td>
137
+ <td class="lct-num"><%= money(row.total_cost) %></td>
138
+ </tr>
139
+ <% end %>
140
+ </tbody>
141
+ </table>
133
142
  </section>
134
143
 
135
144
  <% if @providers.any? %>
136
145
  <section class="lct-panel">
137
- <div class="lct-section-head">
138
- <div>
139
- <h2 class="lct-section-title">By Provider</h2>
140
- <p class="lct-section-copy">Spend share across the selected slice.</p>
141
- </div>
142
- </div>
143
- <table class="lct-table lct-table-compact">
144
- <thead>
145
- <tr>
146
- <th>Provider</th>
147
- <th class="lct-num">Calls</th>
148
- <th class="lct-num">Spend</th>
149
- <th class="lct-num">Share</th>
150
- <th></th>
151
- </tr>
152
- </thead>
146
+ <div class="lct-panel-head"><h2 class="lct-panel-title">By provider</h2></div>
147
+ <table class="lct-tbl">
148
+ <thead><tr><th>Provider</th><th class="lct-num">Calls</th><th class="lct-num">Cost</th><th class="lct-num">Share</th></tr></thead>
153
149
  <tbody>
154
150
  <% @providers.each do |row| %>
155
151
  <tr>
156
- <td><%= row.provider %></td>
157
- <td class="lct-num"><%= number(row.calls) %></td>
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_with_delimiter(row.calls) %></td>
158
154
  <td class="lct-num"><%= money(row.total_cost) %></td>
159
155
  <td class="lct-num"><%= percent(row.share_percent) %></td>
160
- <td><%= link_to "Calls", calls_path(current_query(provider: row.provider, page: nil, per: nil, format: nil)), class: "lct-button lct-button-secondary lct-button-compact" %></td>
161
156
  </tr>
162
157
  <% end %>
163
158
  </tbody>
164
159
  </table>
165
160
  </section>
166
161
  <% end %>
167
- </section>
168
-
169
- <section class="lct-panel">
170
- <div class="lct-section-head">
171
- <div>
172
- <h2 class="lct-section-title">Top Models</h2>
173
- <p class="lct-section-copy">The heaviest contributors in the current slice.</p>
174
- </div>
175
- <%= link_to "View all models", models_path(current_query), class: "lct-button lct-button-secondary lct-button-compact" %>
176
- </div>
177
- <table class="lct-table lct-table-compact">
178
- <thead>
179
- <tr>
180
- <th>Provider</th>
181
- <th>Model</th>
182
- <th class="lct-num">Calls</th>
183
- <th class="lct-num">Spend</th>
184
- <th class="lct-num">Avg cost / call</th>
185
- <th></th>
186
- </tr>
187
- </thead>
188
- <tbody>
189
- <% @top_models.each do |row| %>
190
- <tr>
191
- <td><%= row.provider %></td>
192
- <td><code class="lct-code"><%= row.model %></code></td>
193
- <td class="lct-num"><%= number(row.calls) %></td>
194
- <td class="lct-num"><%= money(row.total_cost) %></td>
195
- <td class="lct-num"><%= money(row.average_cost_per_call) %></td>
196
- <td><%= link_to "Calls", calls_path(calls_query_for_model(provider: row.provider, model: row.model)), class: "lct-button lct-button-secondary lct-button-compact" %></td>
197
- </tr>
198
- <% end %>
199
- </tbody>
200
- </table>
201
- </section>
162
+ </div>
202
163
  <% end %>