llm_cost_tracker 0.7.3 → 0.8.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +66 -1
  4. data/README.md +58 -225
  5. data/app/assets/llm_cost_tracker/application.css +218 -41
  6. data/app/controllers/llm_cost_tracker/application_controller.rb +30 -17
  7. data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
  8. data/app/controllers/llm_cost_tracker/calls_controller.rb +19 -14
  9. data/app/controllers/llm_cost_tracker/data_quality_controller.rb +10 -2
  10. data/app/helpers/llm_cost_tracker/application_helper.rb +11 -24
  11. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
  12. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
  13. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
  14. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +20 -7
  15. data/app/models/llm_cost_tracker/call.rb +169 -0
  16. data/app/models/llm_cost_tracker/call_line_item.rb +22 -0
  17. data/app/models/llm_cost_tracker/call_rollup.rb +9 -0
  18. data/app/models/llm_cost_tracker/call_tag.rb +16 -0
  19. data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +13 -0
  20. data/app/models/llm_cost_tracker/ingestion/lease.rb +1 -1
  21. data/app/models/llm_cost_tracker/provider_invoice.rb +9 -0
  22. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +121 -30
  23. data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
  24. data/app/services/llm_cost_tracker/dashboard/filter.rb +2 -2
  25. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
  26. data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
  27. data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
  28. data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
  29. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
  30. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
  31. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
  32. data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
  33. data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
  34. data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
  35. data/app/views/llm_cost_tracker/calls/show.html.erb +62 -7
  36. data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -50
  37. data/app/views/llm_cost_tracker/data_quality/index.html.erb +103 -126
  38. data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
  39. data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
  40. data/app/views/llm_cost_tracker/shared/_filters.html.erb +63 -0
  41. data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
  42. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
  43. data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
  44. data/app/views/llm_cost_tracker/tags/show.html.erb +5 -37
  45. data/lib/llm_cost_tracker/billing/components.rb +53 -0
  46. data/lib/llm_cost_tracker/billing/components.yml +117 -0
  47. data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
  48. data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
  49. data/lib/llm_cost_tracker/budget.rb +23 -35
  50. data/lib/llm_cost_tracker/capture/stream_collector.rb +47 -33
  51. data/lib/llm_cost_tracker/configuration.rb +36 -19
  52. data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +54 -0
  53. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +24 -32
  54. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
  55. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
  56. data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
  57. data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
  58. data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
  59. data/lib/llm_cost_tracker/doctor/schema_check.rb +31 -0
  60. data/lib/llm_cost_tracker/doctor.rb +43 -45
  61. data/lib/llm_cost_tracker/errors.rb +5 -19
  62. data/lib/llm_cost_tracker/event.rb +10 -2
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -2
  64. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
  65. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +157 -0
  66. data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
  67. data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -23
  68. data/lib/llm_cost_tracker/ingestion/worker.rb +14 -5
  69. data/lib/llm_cost_tracker/ingestion.rb +28 -22
  70. data/lib/llm_cost_tracker/integrations/anthropic.rb +45 -38
  71. data/lib/llm_cost_tracker/integrations/base.rb +36 -29
  72. data/lib/llm_cost_tracker/integrations/openai.rb +85 -40
  73. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +5 -5
  74. data/lib/llm_cost_tracker/integrations.rb +2 -2
  75. data/lib/llm_cost_tracker/ledger/period/totals.rb +12 -9
  76. data/lib/llm_cost_tracker/ledger/period.rb +5 -5
  77. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +2 -2
  78. data/lib/llm_cost_tracker/ledger/rollups.rb +76 -25
  79. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
  80. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +50 -0
  81. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
  82. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +26 -0
  83. data/lib/llm_cost_tracker/ledger/schema/calls.rb +34 -23
  84. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
  85. data/lib/llm_cost_tracker/ledger/store.rb +96 -13
  86. data/lib/llm_cost_tracker/ledger/tags/query.rb +4 -10
  87. data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -15
  88. data/lib/llm_cost_tracker/ledger.rb +4 -2
  89. data/lib/llm_cost_tracker/logging.rb +2 -5
  90. data/lib/llm_cost_tracker/middleware/faraday.rb +7 -6
  91. data/lib/llm_cost_tracker/parsers/anthropic.rb +52 -7
  92. data/lib/llm_cost_tracker/parsers/base.rb +8 -3
  93. data/lib/llm_cost_tracker/parsers/gemini.rb +101 -15
  94. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +10 -2
  95. data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +87 -0
  96. data/lib/llm_cost_tracker/parsers/openai_usage.rb +48 -21
  97. data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
  98. data/lib/llm_cost_tracker/parsers.rb +1 -1
  99. data/lib/llm_cost_tracker/prices.json +105 -20
  100. data/lib/llm_cost_tracker/pricing/effective_prices.rb +57 -19
  101. data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
  102. data/lib/llm_cost_tracker/pricing/lookup.rb +38 -34
  103. data/lib/llm_cost_tracker/pricing/registry.rb +65 -45
  104. data/lib/llm_cost_tracker/pricing/service_charges.rb +204 -0
  105. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
  106. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
  107. data/lib/llm_cost_tracker/pricing/sync.rb +57 -10
  108. data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
  109. data/lib/llm_cost_tracker/pricing.rb +190 -26
  110. data/lib/llm_cost_tracker/railtie.rb +0 -8
  111. data/lib/llm_cost_tracker/report/data.rb +16 -8
  112. data/lib/llm_cost_tracker/report.rb +0 -4
  113. data/lib/llm_cost_tracker/retention.rb +8 -8
  114. data/lib/llm_cost_tracker/tags/context.rb +2 -4
  115. data/lib/llm_cost_tracker/tags/key.rb +4 -0
  116. data/lib/llm_cost_tracker/tags/sanitizer.rb +12 -17
  117. data/lib/llm_cost_tracker/timing.rb +15 -0
  118. data/lib/llm_cost_tracker/token_usage.rb +56 -42
  119. data/lib/llm_cost_tracker/tracker.rb +67 -24
  120. data/lib/llm_cost_tracker/usage_capture.rb +29 -8
  121. data/lib/llm_cost_tracker/version.rb +1 -1
  122. data/lib/llm_cost_tracker.rb +36 -35
  123. data/lib/tasks/llm_cost_tracker.rake +22 -17
  124. metadata +36 -41
  125. data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
  126. data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
  127. data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
  128. data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
  129. data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
  130. data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
  131. data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
  132. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
  133. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
  134. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
  135. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb +0 -29
  136. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +0 -29
  137. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
  138. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
  139. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
  140. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
  141. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
  142. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
  143. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
  144. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
  145. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
  146. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
  147. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
  148. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
  149. data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
  150. data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
  151. data/lib/llm_cost_tracker/pricing/components.rb +0 -37
  152. data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +0 -63
@@ -8,7 +8,7 @@ module LlmCostTracker
8
8
  DEFAULT_SORT = "cost"
9
9
 
10
10
  class << self
11
- def call(scope: LlmCostTracker::Ledger::Call.all, limit: DEFAULT_LIMIT, sort: DEFAULT_SORT)
11
+ def call(scope: LlmCostTracker::Call.all, limit: DEFAULT_LIMIT, sort: DEFAULT_SORT)
12
12
  new(scope: scope, limit: limit, sort: sort).rows
13
13
  end
14
14
  end
@@ -2,83 +2,16 @@
2
2
  <div class="lct-toolbar-head">
3
3
  <h2 class="lct-section-title">Calls</h2>
4
4
  <div class="lct-toolbar-actions">
5
- <%= link_to "Export CSV", calls_path(current_query(format: :csv)), class: "lct-button lct-button-secondary" %>
5
+ <%= link_to "Export CSV",
6
+ calls_path(current_query(format: :csv)),
7
+ class: "lct-button lct-button-secondary",
8
+ title: "Capped at #{number(LlmCostTracker::CallsController::CSV_EXPORT_LIMIT)} rows per request — narrow the date range to export larger slices." %>
6
9
  </div>
7
10
  </div>
8
11
 
9
- <form class="lct-filters" action="<%= calls_path %>" method="get">
10
- <div class="lct-filter-row lct-filter-row-with-sort">
11
- <div class="lct-field">
12
- <label for="lct-from">From</label>
13
- <input id="lct-from" type="date" name="from" value="<%= params[:from] %>">
14
- </div>
15
-
16
- <div class="lct-field">
17
- <label for="lct-to">To</label>
18
- <input id="lct-to" type="date" name="to" value="<%= params[:to] %>">
19
- </div>
20
-
21
- <div class="lct-field">
22
- <label for="lct-provider">Provider</label>
23
- <%= select_tag :provider,
24
- options_for_select(provider_filter_options, params[:provider]),
25
- include_blank: "All providers",
26
- id: "lct-provider" %>
27
- </div>
28
-
29
- <div class="lct-field">
30
- <label for="lct-model">Model</label>
31
- <%= select_tag :model,
32
- options_for_select(model_filter_options, params[:model]),
33
- include_blank: "All models",
34
- id: "lct-model" %>
35
- </div>
36
-
37
- <div class="lct-field">
38
- <label for="lct-stream">Stream</label>
39
- <%= select_tag :stream,
40
- options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, params[:stream]),
41
- include_blank: "All calls",
42
- id: "lct-stream" %>
43
- </div>
44
-
45
- <div class="lct-field">
46
- <label for="lct-sort">Sort</label>
47
- <%= select_tag :sort,
48
- options_for_select(
49
- [["Recent first", ""],
50
- ["Most expensive", "expensive"],
51
- ["Largest input", "input"],
52
- ["Largest output", "output"],
53
- ["Slowest", "slow"],
54
- ["Unknown pricing only", "unknown_pricing"]],
55
- @sort
56
- ),
57
- id: "lct-sort" %>
58
- </div>
59
-
60
- <div class="lct-filter-actions">
61
- <button class="lct-button" type="submit">Apply</button>
62
- <%= link_to("Reset", calls_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
63
- </div>
64
- </div>
65
- </form>
12
+ <%= render "llm_cost_tracker/shared/filters", path: calls_path %>
66
13
 
67
14
  <%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: calls_path %>
68
-
69
- <p class="lct-summary-row">
70
- <span class="lct-pagination-per">
71
- <span class="lct-pagination-per-label">Per page:</span>
72
- <% LlmCostTracker::PaginationHelper::PER_PAGE_CHOICES.each do |choice| %>
73
- <% if choice == @page.per %>
74
- <span class="lct-pagination-per-option is-active" aria-current="true"><%= choice %></span>
75
- <% else %>
76
- <%= link_to choice, calls_path(current_query(per: choice, page: 1)), class: "lct-pagination-per-option" %>
77
- <% end %>
78
- <% end %>
79
- </span>
80
- </p>
81
- <p class="lct-toolbar-note">CSV export is capped at <%= number(LlmCostTracker::CallsController::CSV_EXPORT_LIMIT) %> rows per request — narrow the date range to export larger slices.</p>
82
15
  </section>
83
16
 
84
17
  <% if @calls_count.zero? %>
@@ -91,6 +24,31 @@
91
24
  </section>
92
25
  <% else %>
93
26
  <section class="lct-panel">
27
+ <div class="lct-results-toolbar">
28
+ <span class="lct-pagination-per">
29
+ <span class="lct-pagination-per-label">Per page:</span>
30
+ <% LlmCostTracker::PaginationHelper::PER_PAGE_CHOICES.each do |choice| %>
31
+ <% if choice == @page.per %>
32
+ <span class="lct-pagination-per-option is-active" aria-current="true"><%= choice %></span>
33
+ <% else %>
34
+ <%= link_to choice, calls_path(current_query(per: choice, page: 1)), class: "lct-pagination-per-option" %>
35
+ <% end %>
36
+ <% end %>
37
+ </span>
38
+
39
+ <%= render "llm_cost_tracker/shared/sort",
40
+ current: @sort,
41
+ options: [
42
+ ["Recent", ""],
43
+ ["Most expensive", "expensive"],
44
+ ["Largest input", "input"],
45
+ ["Largest output", "output"],
46
+ ["Slowest", "slow"],
47
+ ["Unknown pricing", "unknown_pricing"]
48
+ ],
49
+ path_for_sort: ->(value) { calls_path(current_query(sort: value.presence, page: nil)) } %>
50
+ </div>
51
+
94
52
  <div class="lct-table-wrap">
95
53
  <table class="lct-table lct-table-compact lct-calls-table">
96
54
  <thead>
@@ -113,9 +71,9 @@
113
71
  <td class="lct-nowrap"><%= format_date(call.tracked_at) %></td>
114
72
  <td><%= call.provider %></td>
115
73
  <td><code class="lct-code"><%= call.model %></code></td>
116
- <td class="lct-num"><%= format_tokens(call.input_tokens) %></td>
117
- <td class="lct-num"><%= format_tokens(call.output_tokens) %></td>
118
- <td class="lct-num"><%= format_tokens(call.total_tokens) %></td>
74
+ <td class="lct-num"><%= number(call.input_tokens) %></td>
75
+ <td class="lct-num"><%= number(call.output_tokens) %></td>
76
+ <td class="lct-num"><%= number(call.total_tokens) %></td>
119
77
  <td class="lct-num<%= ' lct-num-muted' if call.total_cost.nil? %>"><%= optional_money(call.total_cost) %></td>
120
78
  <td class="lct-num<%= ' lct-num-muted' if call.latency_ms.nil? %>"><%= call.latency_ms ? "#{number(call.latency_ms)}ms" : "n/a" %></td>
121
79
  <td><%= render "llm_cost_tracker/shared/tag_chips", tags: call.parsed_tags %></td>
@@ -1,14 +1,14 @@
1
1
  <% priced_components = token_usage_stack_components %>
2
+ <% line_item_costs_by_component = call_line_item_costs_by_component(@call) %>
2
3
  <% token_segments = priced_components.map do |component|
3
4
  token_key = component.fetch(:token_key)
4
5
  value = @call.has_attribute?(token_key) ? @call[token_key] : 0
5
- { label: component.fetch(:label), value: value, formatted_value: format_tokens(value), css_class: component.fetch(:css_class) }
6
+ { label: component.fetch(:label), value: value, formatted_value: number(value), css_class: component.fetch(:css_class) }
6
7
  end %>
7
8
  <% cost_segments = [] %>
8
9
  <% unless @call.total_cost.nil? %>
9
10
  <% cost_segments = priced_components.map do |component|
10
- cost_key = component.fetch(:cost_key)
11
- value = @call.has_attribute?(cost_key) ? @call[cost_key] : nil
11
+ value = line_item_costs_by_component[component.fetch(:price_key)]
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 %>
@@ -38,7 +38,7 @@ end %>
38
38
  </div>
39
39
  <div class="lct-call-summary-item">
40
40
  <span class="lct-call-summary-label">Total tokens</span>
41
- <strong><%= format_tokens(@call.total_tokens) %></strong>
41
+ <strong><%= number(@call.total_tokens) %></strong>
42
42
  </div>
43
43
  <div class="lct-call-summary-item">
44
44
  <span class="lct-call-summary-label">Latency</span>
@@ -77,9 +77,24 @@ end %>
77
77
  <dt>Pricing Status</dt>
78
78
  <dd><%= pricing_status(@call) %></dd>
79
79
 
80
+ <dt>Cost Status</dt>
81
+ <dd><%= @call.cost_status.presence || "n/a" %></dd>
82
+
80
83
  <dt>Provider Response ID</dt>
81
84
  <dd><%= @call.provider_response_id.presence || "n/a" %></dd>
82
85
 
86
+ <dt>Provider Project ID</dt>
87
+ <dd><%= @call.provider_project_id.presence || "n/a" %></dd>
88
+
89
+ <dt>Provider API Key ID</dt>
90
+ <dd><%= @call.provider_api_key_id.presence || "n/a" %></dd>
91
+
92
+ <dt>Provider Workspace ID</dt>
93
+ <dd><%= @call.provider_workspace_id.presence || "n/a" %></dd>
94
+
95
+ <dt>Batch</dt>
96
+ <dd><%= @call.batch? ? "yes" : "no" %></dd>
97
+
83
98
  <% if @call.has_attribute?("created_at") %>
84
99
  <dt>Created At</dt>
85
100
  <dd><%= format_date(@call.created_at) %></dd>
@@ -94,15 +109,15 @@ end %>
94
109
  <dl class="lct-dl">
95
110
  <% priced_components.each do |component| %>
96
111
  <dt><%= component.fetch(:label).titleize %> Tokens</dt>
97
- <dd><%= format_tokens(@call[component.fetch(:token_key)]) %></dd>
112
+ <dd><%= number(@call[component.fetch(:token_key)]) %></dd>
98
113
  <% end %>
99
114
 
100
115
  <dt>Total Tokens</dt>
101
- <dd><%= format_tokens(@call.total_tokens) %></dd>
116
+ <dd><%= number(@call.total_tokens) %></dd>
102
117
 
103
118
  <% priced_components.each do |component| %>
104
119
  <dt><%= component.fetch(:label).titleize %> Cost</dt>
105
- <dd><%= optional_money(@call[component.fetch(:cost_key)]) %></dd>
120
+ <dd><%= optional_money(line_item_costs_by_component[component.fetch(:price_key)]) %></dd>
106
121
  <% end %>
107
122
 
108
123
  <dt>Total Cost</dt>
@@ -114,6 +129,46 @@ end %>
114
129
  </div>
115
130
  </section>
116
131
 
132
+ <% service_line_items = @call.line_items.where.not(unit: "token").order(:position).to_a %>
133
+ <% if service_line_items.any? %>
134
+ <section class="lct-panel">
135
+ <h2 class="lct-section-title">Service Charges</h2>
136
+ <div class="lct-table-wrap">
137
+ <table class="lct-table lct-table-compact">
138
+ <thead>
139
+ <tr>
140
+ <th>Component</th>
141
+ <th>Unit</th>
142
+ <th class="lct-num">Quantity</th>
143
+ <th class="lct-num">Rate</th>
144
+ <th class="lct-num">Cost</th>
145
+ <th>Status</th>
146
+ </tr>
147
+ </thead>
148
+ <tbody>
149
+ <% service_line_items.each do |line_item| %>
150
+ <tr>
151
+ <td><code class="lct-code"><%= line_item.kind %></code></td>
152
+ <td><%= line_item.unit %></td>
153
+ <td class="lct-num"><%= line_item.quantity %></td>
154
+ <td class="lct-num"><%= line_item.rate_amount ? "#{optional_money(line_item.rate_amount)} / #{line_item.rate_quantity}" : "n/a" %></td>
155
+ <td class="lct-num<%= ' lct-num-muted' if line_item.cost.nil? %>"><%= optional_money(line_item.cost) %></td>
156
+ <td><%= line_item.cost_status %></td>
157
+ </tr>
158
+ <% end %>
159
+ </tbody>
160
+ </table>
161
+ </div>
162
+ </section>
163
+ <% end %>
164
+
165
+ <% if @call.pricing_snapshot.present? %>
166
+ <section class="lct-panel">
167
+ <h2 class="lct-section-title">Pricing Snapshot</h2>
168
+ <pre class="lct-pre"><%= safe_json(@call.pricing_snapshot) %></pre>
169
+ </section>
170
+ <% end %>
171
+
117
172
  <section class="lct-panel">
118
173
  <h2 class="lct-section-title">Tags</h2>
119
174
  <pre class="lct-pre"><%= safe_json(@call.parsed_tags) %></pre>
@@ -1,48 +1,10 @@
1
1
  <% overview_filter_scope = current_query(from: @from_date.iso8601, to: @to_date.iso8601) %>
2
2
 
3
3
  <section class="lct-panel lct-toolbar">
4
- <form class="lct-filters" action="<%= root_path %>" method="get">
5
- <div class="lct-filter-row lct-filter-row-basic">
6
- <div class="lct-field">
7
- <label for="lct-overview-from">From</label>
8
- <input id="lct-overview-from" type="date" name="from" value="<%= params[:from] || @from_date.iso8601 %>">
9
- </div>
10
-
11
- <div class="lct-field">
12
- <label for="lct-overview-to">To</label>
13
- <input id="lct-overview-to" type="date" name="to" value="<%= params[:to] || @to_date.iso8601 %>">
14
- </div>
15
-
16
- <div class="lct-field">
17
- <label for="lct-overview-provider">Provider</label>
18
- <%= select_tag :provider,
19
- options_for_select(provider_filter_options(filter_params: overview_filter_scope), params[:provider]),
20
- include_blank: "All providers",
21
- id: "lct-overview-provider" %>
22
- </div>
23
-
24
- <div class="lct-field">
25
- <label for="lct-overview-model">Model</label>
26
- <%= select_tag :model,
27
- options_for_select(model_filter_options(filter_params: overview_filter_scope), params[:model]),
28
- include_blank: "All models",
29
- id: "lct-overview-model" %>
30
- </div>
31
-
32
- <div class="lct-field">
33
- <label for="lct-overview-stream">Stream</label>
34
- <%= select_tag :stream,
35
- options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, params[:stream]),
36
- include_blank: "All calls",
37
- id: "lct-overview-stream" %>
38
- </div>
39
-
40
- <div class="lct-filter-actions">
41
- <button class="lct-button" type="submit">Apply</button>
42
- <%= link_to("Reset", root_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
43
- </div>
44
- </div>
45
- </form>
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 } %>
46
8
 
47
9
  <%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: root_path %>
48
10
  </section>
@@ -125,9 +87,6 @@
125
87
 
126
88
  <% if @monthly_budget_status %>
127
89
  <% budget = @monthly_budget_status %>
128
- <% fill_mod = budget_fill_modifier(budget[:percent_used]) %>
129
- <% projected_marker = [[budget[:projected_percent_used].to_f, 0.0].max, 100.0].min %>
130
- <% projected_delta = budget[:projected_delta].to_f %>
131
90
  <section class="lct-panel lct-panel-tight">
132
91
  <div class="lct-section-head">
133
92
  <div>
@@ -143,16 +102,16 @@
143
102
  </span>
144
103
  <span class="lct-budget-percent"><%= percent(budget[:percent_used]) %></span>
145
104
  </div>
146
- <div class="lct-budget-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<%= [budget[:percent_used].to_f, 100.0].min.round %>">
147
- <div class="lct-budget-fill <%= fill_mod %>" style="width: <%= [budget[:percent_used].to_f, 100.0].min %>%"></div>
105
+ <div class="lct-budget-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<%= budget[:progress_percent].round %>">
106
+ <div class="lct-budget-fill <%= budget[:fill_modifier] %>" style="width: <%= budget[:progress_percent] %>%"></div>
148
107
  <% if budget[:projected_spent].positive? %>
149
- <span class="lct-budget-marker" aria-hidden="true" style="left: calc(<%= projected_marker %>% - 1px)"></span>
108
+ <span class="lct-budget-marker" aria-hidden="true" style="left: calc(<%= budget[:projected_marker_percent] %>% - 1px)"></span>
150
109
  <% end %>
151
110
  </div>
152
111
  <p class="lct-budget-projection">
153
112
  <span>Projected <strong><%= money(budget[:projected_spent]) %></strong> by <%= budget[:projection_end_label] %></span>
154
- <span class="lct-budget-projection-status <%= projected_delta.positive? ? "lct-budget-projection-status--over" : "lct-budget-projection-status--under" %>">
155
- <%= money(projected_delta.abs) %> <%= projected_delta.positive? ? "over" : "under" %> budget
113
+ <span class="lct-budget-projection-status <%= budget[:projected_delta_status_class] %>">
114
+ <%= money(budget[:projected_delta_amount]) %> <%= budget[:projected_delta_direction] %> budget
156
115
  </span>
157
116
  </p>
158
117
  <p class="lct-budget-meta">Soft limit: blocking is not atomic under concurrency.</p>