llm_cost_tracker 0.2.0.alpha2 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +28 -1
- data/README.md +4 -3
- data/app/assets/llm_cost_tracker/application.css +760 -0
- data/app/controllers/llm_cost_tracker/application_controller.rb +1 -7
- data/app/controllers/llm_cost_tracker/assets_controller.rb +13 -0
- data/app/controllers/llm_cost_tracker/calls_controller.rb +29 -12
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +5 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +46 -5
- data/app/helpers/llm_cost_tracker/chart_helper.rb +133 -0
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +42 -0
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +34 -0
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +58 -0
- data/app/helpers/llm_cost_tracker/pagination_helper.rb +18 -0
- data/app/services/llm_cost_tracker/dashboard/filter.rb +0 -3
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +16 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +79 -0
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +19 -46
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +17 -8
- data/app/services/llm_cost_tracker/pagination.rb +6 -0
- data/app/views/layouts/llm_cost_tracker/application.html.erb +35 -333
- data/app/views/llm_cost_tracker/calls/index.html.erb +106 -74
- data/app/views/llm_cost_tracker/calls/show.html.erb +58 -1
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +201 -111
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +178 -78
- data/app/views/llm_cost_tracker/errors/database.html.erb +3 -3
- data/app/views/llm_cost_tracker/errors/invalid_filter.html.erb +3 -3
- data/app/views/llm_cost_tracker/errors/not_found.html.erb +3 -3
- data/app/views/llm_cost_tracker/models/index.html.erb +66 -58
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +16 -0
- data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +18 -0
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +15 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +3 -2
- data/app/views/llm_cost_tracker/tags/index.html.erb +55 -12
- data/app/views/llm_cost_tracker/tags/show.html.erb +88 -39
- data/config/routes.rb +3 -0
- data/lib/llm_cost_tracker/assets.rb +24 -0
- data/lib/llm_cost_tracker/engine.rb +2 -0
- data/lib/llm_cost_tracker/llm_api_call.rb +1 -1
- data/lib/llm_cost_tracker/price_registry.rb +17 -6
- data/lib/llm_cost_tracker/pricing.rb +19 -6
- data/lib/llm_cost_tracker/retention.rb +34 -0
- data/lib/llm_cost_tracker/tag_query.rb +7 -2
- data/lib/llm_cost_tracker/tags_column.rb +13 -1
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +1 -0
- data/lib/tasks/llm_cost_tracker.rake +8 -0
- data/llm_cost_tracker.gemspec +1 -2
- metadata +17 -5
- data/PLAN_0.2.md +0 -488
|
@@ -1,142 +1,232 @@
|
|
|
1
|
+
<% overview_filter_scope = current_query(from: @from_date.iso8601, to: @to_date.iso8601) %>
|
|
2
|
+
|
|
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" data-lct-filter-input 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-filter-actions">
|
|
33
|
+
<button class="lct-button" type="submit">Apply</button>
|
|
34
|
+
<%= link_to("Reset", root_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</form>
|
|
38
|
+
|
|
39
|
+
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: root_path %>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
1
42
|
<% if @stats.total_calls.zero? %>
|
|
2
43
|
<section class="lct-panel lct-empty">
|
|
3
|
-
<h2 class="lct-
|
|
4
|
-
<p class="lct-
|
|
44
|
+
<h2 class="lct-state-title">No LLM calls yet</h2>
|
|
45
|
+
<p class="lct-state-copy">Tracked requests will appear here after your application records its first LLM API call.</p>
|
|
46
|
+
<div class="lct-state-actions">
|
|
47
|
+
<%= link_to "View calls", calls_path, class: "lct-button lct-button-secondary" %>
|
|
48
|
+
</div>
|
|
5
49
|
</section>
|
|
6
50
|
<% else %>
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
51
|
+
<% if @stats.unknown_pricing_count.positive? %>
|
|
52
|
+
<aside class="lct-banner lct-banner-warning" role="status">
|
|
53
|
+
<div class="lct-banner-body">
|
|
54
|
+
<p class="lct-banner-title">
|
|
55
|
+
<%= number(@stats.unknown_pricing_count) %> call<%= "s" unless @stats.unknown_pricing_count == 1 %> missing pricing
|
|
56
|
+
<span class="lct-banner-muted">· <%= percent(coverage_percent(@stats.unknown_pricing_count, @stats.total_calls)) %> of the slice</span>
|
|
57
|
+
</p>
|
|
58
|
+
<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>
|
|
59
|
+
</div>
|
|
60
|
+
<%= link_to "Fix now →", data_quality_path, class: "lct-button lct-button-secondary" %>
|
|
61
|
+
</aside>
|
|
62
|
+
<% end %>
|
|
14
63
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
64
|
+
<% if @spend_anomaly %>
|
|
65
|
+
<aside class="lct-banner lct-banner-danger" role="status">
|
|
66
|
+
<div class="lct-banner-body">
|
|
67
|
+
<p class="lct-banner-title">
|
|
68
|
+
Spend anomaly detected
|
|
69
|
+
<span class="lct-banner-muted">· <code class="lct-code"><%= @spend_anomaly.model %></code> on <%= @spend_anomaly.day.strftime("%b %-d") %></span>
|
|
70
|
+
</p>
|
|
71
|
+
<p class="lct-banner-copy">
|
|
72
|
+
<% if @spend_anomaly.ratio %>
|
|
73
|
+
<%= number_with_precision(@spend_anomaly.ratio, precision: 1) %>× its prior 7-day average in this slice
|
|
74
|
+
<% else %>
|
|
75
|
+
<%= money(@spend_anomaly.latest_spend) %> after seven quiet days in this slice
|
|
76
|
+
<% end %>
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
<%= link_to "Review calls →",
|
|
80
|
+
calls_path(current_query(provider: @spend_anomaly.provider, model: @spend_anomaly.model, from: @to_date.iso8601, to: @to_date.iso8601, page: nil, per: nil, format: nil)),
|
|
81
|
+
class: "lct-button lct-button-secondary" %>
|
|
82
|
+
</aside>
|
|
83
|
+
<% end %>
|
|
21
84
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
85
|
+
<section class="lct-hero">
|
|
86
|
+
<article class="lct-panel lct-hero-primary">
|
|
87
|
+
<div>
|
|
88
|
+
<p class="lct-stat-label">Total spend</p>
|
|
89
|
+
<p class="lct-hero-value"><%= money(@stats.total_cost) %></p>
|
|
90
|
+
<% badge = delta_badge(@stats.cost_delta_percent) %>
|
|
91
|
+
<span class="<%= badge[:css_class] %>"><span class="lct-delta-dot" aria-hidden="true"></span><%= badge[:text] %></span>
|
|
92
|
+
</div>
|
|
25
93
|
</article>
|
|
26
94
|
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<article class="lct-stat">
|
|
36
|
-
<p class="lct-stat-label">Unknown pricing</p>
|
|
37
|
-
<p class="lct-stat-value"><%= number(@stats.unknown_pricing_count) %></p>
|
|
38
|
-
<p class="lct-stat-sub">
|
|
39
|
-
<%= percent(coverage_percent(@stats.unknown_pricing_count, @stats.total_calls)) %> of calls ·
|
|
40
|
-
<%= link_to "Review →", data_quality_path %>
|
|
41
|
-
</p>
|
|
42
|
-
</article>
|
|
43
|
-
<% end %>
|
|
44
|
-
</section>
|
|
95
|
+
<div class="lct-hero-side">
|
|
96
|
+
<div class="lct-stat-grid">
|
|
97
|
+
<article class="lct-stat">
|
|
98
|
+
<p class="lct-stat-label">Calls</p>
|
|
99
|
+
<p class="lct-stat-value"><%= number(@stats.total_calls) %></p>
|
|
100
|
+
<% badge = delta_badge(@stats.calls_delta_percent, mode: :neutral) %>
|
|
101
|
+
<span class="<%= badge[:css_class] %>"><span class="lct-delta-dot" aria-hidden="true"></span><%= badge[:text] %></span>
|
|
102
|
+
</article>
|
|
45
103
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<p class="lct-muted">Current-month spend across <strong>all</strong> calls — overview filters do not apply. Soft limit: blocking is not atomic under concurrency.</p>
|
|
51
|
-
<table class="lct-table">
|
|
52
|
-
<tbody>
|
|
53
|
-
<tr>
|
|
54
|
-
<th>Spent</th>
|
|
55
|
-
<td><%= money(budget[:spent]) %></td>
|
|
56
|
-
</tr>
|
|
57
|
-
<tr>
|
|
58
|
-
<th>Budget</th>
|
|
59
|
-
<td><%= money(budget[:budget]) %></td>
|
|
60
|
-
</tr>
|
|
61
|
-
<tr>
|
|
62
|
-
<th>Used</th>
|
|
63
|
-
<td><%= percent(budget[:percent_used]) %></td>
|
|
64
|
-
</tr>
|
|
65
|
-
</tbody>
|
|
66
|
-
</table>
|
|
67
|
-
<%= render "llm_cost_tracker/shared/bar", value: budget[:percent_used], max: 100.0, variant: "budget" %>
|
|
68
|
-
</section>
|
|
69
|
-
<% end %>
|
|
104
|
+
<article class="lct-stat">
|
|
105
|
+
<p class="lct-stat-label">Avg cost / call</p>
|
|
106
|
+
<p class="lct-stat-value"><%= money(@stats.average_cost_per_call) %></p>
|
|
107
|
+
</article>
|
|
70
108
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<tr>
|
|
77
|
-
<th>Day</th>
|
|
78
|
-
<th>Spend</th>
|
|
79
|
-
<th>Trend</th>
|
|
80
|
-
</tr>
|
|
81
|
-
</thead>
|
|
82
|
-
<tbody>
|
|
83
|
-
<% @time_series.each do |point| %>
|
|
84
|
-
<tr>
|
|
85
|
-
<td><%= point[:label] %></td>
|
|
86
|
-
<td><%= money(point[:cost]) %></td>
|
|
87
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: point[:cost], max: max_cost %></td>
|
|
88
|
-
</tr>
|
|
109
|
+
<% if @stats.average_latency_ms %>
|
|
110
|
+
<article class="lct-stat">
|
|
111
|
+
<p class="lct-stat-label">Avg latency</p>
|
|
112
|
+
<p class="lct-stat-value"><%= number(@stats.average_latency_ms.round) %>ms</p>
|
|
113
|
+
</article>
|
|
89
114
|
<% end %>
|
|
90
|
-
|
|
91
|
-
|
|
115
|
+
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<% if @stats.monthly_budget_status %>
|
|
119
|
+
<% budget = @stats.monthly_budget_status %>
|
|
120
|
+
<% fill_mod = budget_fill_modifier(budget[:percent_used]) %>
|
|
121
|
+
<% projected_marker = [[budget[:projected_percent_used].to_f, 0.0].max, 100.0].min %>
|
|
122
|
+
<% projected_delta = budget[:projected_delta].to_f %>
|
|
123
|
+
<section class="lct-panel lct-panel-tight">
|
|
124
|
+
<div class="lct-section-head">
|
|
125
|
+
<div>
|
|
126
|
+
<h2 class="lct-section-title">Monthly Budget</h2>
|
|
127
|
+
<p class="lct-section-copy">Current-month spend across <strong>all</strong> calls.</p>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="lct-budget">
|
|
131
|
+
<div class="lct-budget-head">
|
|
132
|
+
<span>
|
|
133
|
+
<span class="lct-budget-spent"><%= money(budget[:spent]) %></span>
|
|
134
|
+
<span class="lct-budget-of"> of <%= money(budget[:budget]) %></span>
|
|
135
|
+
</span>
|
|
136
|
+
<span class="lct-budget-percent"><%= percent(budget[:percent_used]) %></span>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="lct-budget-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<%= [budget[:percent_used].to_f, 100.0].min.round %>">
|
|
139
|
+
<div class="lct-budget-fill <%= fill_mod %>" style="width: <%= [budget[:percent_used].to_f, 100.0].min %>%"></div>
|
|
140
|
+
<% if budget[:projected_spent].positive? %>
|
|
141
|
+
<span class="lct-budget-marker" aria-hidden="true" style="left: calc(<%= projected_marker %>% - 1px)"></span>
|
|
142
|
+
<% end %>
|
|
143
|
+
</div>
|
|
144
|
+
<p class="lct-budget-projection">
|
|
145
|
+
<span>Projected <strong><%= money(budget[:projected_spent]) %></strong> by <%= budget[:projection_end_label] %></span>
|
|
146
|
+
<span class="lct-budget-projection-status <%= projected_delta.positive? ? "lct-budget-projection-status--over" : "lct-budget-projection-status--under" %>">
|
|
147
|
+
<%= money(projected_delta.abs) %> <%= projected_delta.positive? ? "over" : "under" %> budget
|
|
148
|
+
</span>
|
|
149
|
+
</p>
|
|
150
|
+
<p class="lct-budget-meta">Soft limit: blocking is not atomic under concurrency.</p>
|
|
151
|
+
</div>
|
|
152
|
+
</section>
|
|
153
|
+
<% end %>
|
|
154
|
+
</div>
|
|
92
155
|
</section>
|
|
93
156
|
|
|
94
|
-
|
|
157
|
+
<section class="lct-grid lct-two-col">
|
|
95
158
|
<section class="lct-panel">
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
159
|
+
<div class="lct-section-head">
|
|
160
|
+
<div>
|
|
161
|
+
<h2 class="lct-section-title">Daily Spend</h2>
|
|
162
|
+
<p class="lct-section-copy">Current slice vs. previous <%= number(@comparison_series.size) %>-day slice.</p>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
<%= render "llm_cost_tracker/shared/spend_chart", series: @time_series, comparison_series: @comparison_series %>
|
|
166
|
+
</section>
|
|
167
|
+
|
|
168
|
+
<% if @providers.any? %>
|
|
169
|
+
<section class="lct-panel">
|
|
170
|
+
<div class="lct-section-head">
|
|
171
|
+
<div>
|
|
172
|
+
<h2 class="lct-section-title">By Provider</h2>
|
|
173
|
+
<p class="lct-section-copy">Spend share across the selected slice.</p>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<table class="lct-table lct-table-compact">
|
|
177
|
+
<thead>
|
|
108
178
|
<tr>
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
<
|
|
179
|
+
<th>Provider</th>
|
|
180
|
+
<th class="lct-num">Calls</th>
|
|
181
|
+
<th class="lct-num">Spend</th>
|
|
182
|
+
<th class="lct-num">Share</th>
|
|
183
|
+
<th></th>
|
|
113
184
|
</tr>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
185
|
+
</thead>
|
|
186
|
+
<tbody>
|
|
187
|
+
<% @providers.each do |row| %>
|
|
188
|
+
<tr>
|
|
189
|
+
<td><%= row.provider %></td>
|
|
190
|
+
<td class="lct-num"><%= number(row.calls) %></td>
|
|
191
|
+
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
192
|
+
<td class="lct-num"><%= percent(row.share_percent) %></td>
|
|
193
|
+
<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>
|
|
194
|
+
</tr>
|
|
195
|
+
<% end %>
|
|
196
|
+
</tbody>
|
|
197
|
+
</table>
|
|
198
|
+
</section>
|
|
199
|
+
<% end %>
|
|
200
|
+
</section>
|
|
119
201
|
|
|
120
202
|
<section class="lct-panel">
|
|
121
|
-
<
|
|
122
|
-
|
|
203
|
+
<div class="lct-section-head">
|
|
204
|
+
<div>
|
|
205
|
+
<h2 class="lct-section-title">Top Models</h2>
|
|
206
|
+
<p class="lct-section-copy">The heaviest contributors in the current slice.</p>
|
|
207
|
+
</div>
|
|
208
|
+
<%= link_to "View all models", models_path(current_query), class: "lct-button lct-button-secondary lct-button-compact" %>
|
|
209
|
+
</div>
|
|
210
|
+
<table class="lct-table lct-table-compact">
|
|
123
211
|
<thead>
|
|
124
212
|
<tr>
|
|
125
213
|
<th>Provider</th>
|
|
126
214
|
<th>Model</th>
|
|
127
|
-
<th>Calls</th>
|
|
128
|
-
<th>Spend</th>
|
|
129
|
-
<th>Avg cost / call</th>
|
|
215
|
+
<th class="lct-num">Calls</th>
|
|
216
|
+
<th class="lct-num">Spend</th>
|
|
217
|
+
<th class="lct-num">Avg cost / call</th>
|
|
218
|
+
<th></th>
|
|
130
219
|
</tr>
|
|
131
220
|
</thead>
|
|
132
221
|
<tbody>
|
|
133
222
|
<% @top_models.each do |row| %>
|
|
134
223
|
<tr>
|
|
135
224
|
<td><%= row.provider %></td>
|
|
136
|
-
<td><%= row.model %></td>
|
|
137
|
-
<td><%= number(row.calls) %></td>
|
|
138
|
-
<td><%= money(row.total_cost) %></td>
|
|
139
|
-
<td><%= money(row.average_cost_per_call) %></td>
|
|
225
|
+
<td><code class="lct-code"><%= row.model %></code></td>
|
|
226
|
+
<td class="lct-num"><%= number(row.calls) %></td>
|
|
227
|
+
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
228
|
+
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
229
|
+
<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>
|
|
140
230
|
</tr>
|
|
141
231
|
<% end %>
|
|
142
232
|
</tbody>
|
|
@@ -1,110 +1,210 @@
|
|
|
1
1
|
<% total = @stats.total_calls %>
|
|
2
|
+
<% known_pricing_calls = total - @stats.unknown_pricing_count %>
|
|
3
|
+
<% tagged_calls = total - @stats.untagged_calls_count %>
|
|
4
|
+
<% latency_calls = @stats.latency_column_present ? total - @stats.missing_latency_count : nil %>
|
|
5
|
+
|
|
6
|
+
<section class="lct-panel lct-toolbar">
|
|
7
|
+
<div class="lct-toolbar-head">
|
|
8
|
+
<h2 class="lct-section-title">Data Quality</h2>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<form class="lct-filters" action="<%= data_quality_path %>" method="get">
|
|
12
|
+
<div class="lct-filter-row lct-filter-row-basic">
|
|
13
|
+
<div class="lct-field">
|
|
14
|
+
<label for="lct-quality-from">From</label>
|
|
15
|
+
<input id="lct-quality-from" data-lct-filter-input type="date" name="from" value="<%= params[:from] %>">
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="lct-field">
|
|
19
|
+
<label for="lct-quality-to">To</label>
|
|
20
|
+
<input id="lct-quality-to" type="date" name="to" value="<%= params[:to] %>">
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="lct-field">
|
|
24
|
+
<label for="lct-quality-provider">Provider</label>
|
|
25
|
+
<%= select_tag :provider,
|
|
26
|
+
options_for_select(provider_filter_options, params[:provider]),
|
|
27
|
+
include_blank: "All providers",
|
|
28
|
+
id: "lct-quality-provider" %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="lct-field">
|
|
32
|
+
<label for="lct-quality-model">Model</label>
|
|
33
|
+
<%= select_tag :model,
|
|
34
|
+
options_for_select(model_filter_options, params[:model]),
|
|
35
|
+
include_blank: "All models",
|
|
36
|
+
id: "lct-quality-model" %>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="lct-filter-actions">
|
|
40
|
+
<button class="lct-button" type="submit">Apply</button>
|
|
41
|
+
<%= link_to("Reset", data_quality_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</form>
|
|
45
|
+
|
|
46
|
+
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: data_quality_path %>
|
|
47
|
+
</section>
|
|
2
48
|
|
|
3
49
|
<% if total.zero? %>
|
|
4
50
|
<section class="lct-panel lct-empty">
|
|
5
|
-
<h2 class="lct-
|
|
6
|
-
<p class="lct-
|
|
51
|
+
<h2 class="lct-state-title">No data yet</h2>
|
|
52
|
+
<p class="lct-state-copy">Quality metrics will appear here once calls are recorded in the current slice.</p>
|
|
53
|
+
<div class="lct-state-actions">
|
|
54
|
+
<%= link_to "Clear filters", data_quality_path, class: "lct-button lct-button-secondary" %>
|
|
55
|
+
</div>
|
|
7
56
|
</section>
|
|
8
57
|
<% else %>
|
|
9
|
-
<section class="lct-
|
|
10
|
-
<article class="lct-
|
|
11
|
-
<
|
|
12
|
-
|
|
58
|
+
<section class="lct-hero">
|
|
59
|
+
<article class="lct-panel lct-hero-primary">
|
|
60
|
+
<div>
|
|
61
|
+
<p class="lct-stat-label">Calls inspected</p>
|
|
62
|
+
<p class="lct-hero-value"><%= number(total) %></p>
|
|
63
|
+
</div>
|
|
13
64
|
</article>
|
|
14
65
|
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
66
|
+
<div class="lct-hero-side">
|
|
67
|
+
<div class="lct-stat-grid">
|
|
68
|
+
<article class="lct-stat">
|
|
69
|
+
<p class="lct-stat-label">Unknown pricing</p>
|
|
70
|
+
<p class="lct-stat-value"><%= number(@stats.unknown_pricing_count) %></p>
|
|
71
|
+
<p class="lct-stat-sub"><%= percent(coverage_percent(@stats.unknown_pricing_count, total)) %> of calls</p>
|
|
72
|
+
</article>
|
|
20
73
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
74
|
+
<article class="lct-stat">
|
|
75
|
+
<p class="lct-stat-label">Calls without tags</p>
|
|
76
|
+
<p class="lct-stat-value"><%= number(@stats.untagged_calls_count) %></p>
|
|
77
|
+
<p class="lct-stat-sub"><%= percent(coverage_percent(@stats.untagged_calls_count, total)) %> of calls</p>
|
|
78
|
+
</article>
|
|
26
79
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
80
|
+
<% if @stats.latency_column_present %>
|
|
81
|
+
<article class="lct-stat">
|
|
82
|
+
<p class="lct-stat-label">Missing latency</p>
|
|
83
|
+
<p class="lct-stat-value"><%= number(@stats.missing_latency_count) %></p>
|
|
84
|
+
<p class="lct-stat-sub"><%= percent(coverage_percent(@stats.missing_latency_count, total)) %> of calls</p>
|
|
85
|
+
</article>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
<section class="lct-grid lct-two-col">
|
|
92
|
+
<section class="lct-panel">
|
|
93
|
+
<div class="lct-section-head">
|
|
94
|
+
<div>
|
|
95
|
+
<h2 class="lct-section-title">Coverage summary</h2>
|
|
96
|
+
<p class="lct-section-copy">Good dashboards start with clean pricing, tags, and latency coverage.</p>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<table class="lct-table lct-table-compact">
|
|
101
|
+
<thead>
|
|
102
|
+
<tr>
|
|
103
|
+
<th>Dimension</th>
|
|
104
|
+
<th class="lct-num">Coverage</th>
|
|
105
|
+
<th class="lct-num">Calls with data</th>
|
|
106
|
+
<th>Visual</th>
|
|
107
|
+
</tr>
|
|
108
|
+
</thead>
|
|
109
|
+
<tbody>
|
|
110
|
+
<% cost_coverage = coverage_percent(known_pricing_calls, total) %>
|
|
111
|
+
<tr>
|
|
112
|
+
<td>Cost (pricing known)</td>
|
|
113
|
+
<td class="lct-num"><%= percent(cost_coverage) %></td>
|
|
114
|
+
<td class="lct-num"><%= number(known_pricing_calls) %></td>
|
|
115
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: cost_coverage, max: 100.0 %></td>
|
|
116
|
+
</tr>
|
|
117
|
+
|
|
118
|
+
<% tag_coverage = coverage_percent(tagged_calls, total) %>
|
|
119
|
+
<tr>
|
|
120
|
+
<td>Tags (at least one tag)</td>
|
|
121
|
+
<td class="lct-num"><%= percent(tag_coverage) %></td>
|
|
122
|
+
<td class="lct-num"><%= number(tagged_calls) %></td>
|
|
123
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: tag_coverage, max: 100.0 %></td>
|
|
124
|
+
</tr>
|
|
125
|
+
|
|
126
|
+
<% if @stats.latency_column_present %>
|
|
127
|
+
<% latency_coverage = coverage_percent(latency_calls, total) %>
|
|
128
|
+
<tr>
|
|
129
|
+
<td>Latency</td>
|
|
130
|
+
<td class="lct-num"><%= percent(latency_coverage) %></td>
|
|
131
|
+
<td class="lct-num"><%= number(latency_calls) %></td>
|
|
132
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: latency_coverage, max: 100.0 %></td>
|
|
133
|
+
</tr>
|
|
134
|
+
<% end %>
|
|
135
|
+
</tbody>
|
|
136
|
+
</table>
|
|
137
|
+
</section>
|
|
138
|
+
|
|
139
|
+
<section class="lct-panel">
|
|
140
|
+
<div class="lct-section-head">
|
|
141
|
+
<div>
|
|
142
|
+
<h2 class="lct-section-title">Next actions</h2>
|
|
143
|
+
<p class="lct-section-copy">Use these fixes to improve the trustworthiness of downstream cost reports.</p>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<table class="lct-table lct-table-compact">
|
|
148
|
+
<thead>
|
|
149
|
+
<tr>
|
|
150
|
+
<th>Issue</th>
|
|
151
|
+
<th>Why it matters</th>
|
|
152
|
+
<th>Suggested action</th>
|
|
153
|
+
</tr>
|
|
154
|
+
</thead>
|
|
155
|
+
<tbody>
|
|
156
|
+
<tr>
|
|
157
|
+
<td>Unknown pricing</td>
|
|
158
|
+
<td>Cost stays <code class="lct-code">nil</code>, so totals undercount.</td>
|
|
159
|
+
<td>Update <code class="lct-code">pricing_overrides</code> or <code class="lct-code">prices_file</code>.</td>
|
|
160
|
+
</tr>
|
|
161
|
+
<tr>
|
|
162
|
+
<td>Missing tags</td>
|
|
163
|
+
<td>Attribution by tenant, user, or feature becomes less useful.</td>
|
|
164
|
+
<td>Pass <code class="lct-code">tags:</code> from middleware using request context.</td>
|
|
165
|
+
</tr>
|
|
166
|
+
<% if @stats.latency_column_present %>
|
|
167
|
+
<tr>
|
|
168
|
+
<td>Missing latency</td>
|
|
169
|
+
<td>Slow requests become harder to isolate on the calls page.</td>
|
|
170
|
+
<td>Make sure latency capture is enabled on every tracked request.</td>
|
|
171
|
+
</tr>
|
|
172
|
+
<% end %>
|
|
173
|
+
</tbody>
|
|
174
|
+
</table>
|
|
175
|
+
</section>
|
|
34
176
|
</section>
|
|
35
177
|
|
|
36
178
|
<% unless @stats.unknown_pricing_by_model.empty? %>
|
|
37
179
|
<section class="lct-panel">
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
180
|
+
<div class="lct-section-head">
|
|
181
|
+
<div>
|
|
182
|
+
<h2 class="lct-section-title">Unknown pricing by model</h2>
|
|
183
|
+
<p class="lct-section-copy">These models have token counts but no configured rates, so cost stays unknown.</p>
|
|
184
|
+
</div>
|
|
185
|
+
<%= link_to "Review calls", calls_path(current_query(sort: "unknown_pricing")), class: "lct-button lct-button-secondary" %>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
43
188
|
<div class="lct-table-wrap">
|
|
44
|
-
<table class="lct-table">
|
|
189
|
+
<table class="lct-table lct-table-compact">
|
|
45
190
|
<thead>
|
|
46
191
|
<tr>
|
|
47
192
|
<th>Model</th>
|
|
48
|
-
<th>Calls without cost</th>
|
|
49
|
-
<th>Share of total</th>
|
|
193
|
+
<th class="lct-num">Calls without cost</th>
|
|
194
|
+
<th class="lct-num">Share of total</th>
|
|
50
195
|
</tr>
|
|
51
196
|
</thead>
|
|
52
197
|
<tbody>
|
|
53
198
|
<% @stats.unknown_pricing_by_model.each do |model, count| %>
|
|
54
199
|
<tr>
|
|
55
200
|
<td><code class="lct-code"><%= model %></code></td>
|
|
56
|
-
<td><%= number(count) %></td>
|
|
57
|
-
<td><%= percent(total.positive? ? (count.to_f / total) * 100 : 0) %></td>
|
|
201
|
+
<td class="lct-num"><%= number(count) %></td>
|
|
202
|
+
<td class="lct-num"><%= percent(total.positive? ? (count.to_f / total) * 100.0 : 0.0) %></td>
|
|
58
203
|
</tr>
|
|
59
204
|
<% end %>
|
|
60
205
|
</tbody>
|
|
61
206
|
</table>
|
|
62
207
|
</div>
|
|
63
|
-
<p class="lct-muted" style="margin-top:12px">
|
|
64
|
-
<%= link_to "View all unknown pricing calls →", calls_path(sort: "unknown_pricing") %>
|
|
65
|
-
</p>
|
|
66
208
|
</section>
|
|
67
209
|
<% end %>
|
|
68
|
-
|
|
69
|
-
<section class="lct-panel">
|
|
70
|
-
<h2 class="lct-section-title">Coverage Summary</h2>
|
|
71
|
-
<table class="lct-table">
|
|
72
|
-
<thead>
|
|
73
|
-
<tr>
|
|
74
|
-
<th>Dimension</th>
|
|
75
|
-
<th>Coverage</th>
|
|
76
|
-
<th>Calls with data</th>
|
|
77
|
-
<th>Visual</th>
|
|
78
|
-
</tr>
|
|
79
|
-
</thead>
|
|
80
|
-
<tbody>
|
|
81
|
-
<% cost_coverage = coverage_percent(total - @stats.unknown_pricing_count, total) %>
|
|
82
|
-
<tr>
|
|
83
|
-
<td>Cost (pricing known)</td>
|
|
84
|
-
<td><%= percent(cost_coverage) %></td>
|
|
85
|
-
<td><%= number(total - @stats.unknown_pricing_count) %></td>
|
|
86
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: cost_coverage, max: 100.0 %></td>
|
|
87
|
-
</tr>
|
|
88
|
-
|
|
89
|
-
<% tag_coverage = coverage_percent(total - @stats.untagged_calls_count, total) %>
|
|
90
|
-
<tr>
|
|
91
|
-
<td>Tags (at least one tag)</td>
|
|
92
|
-
<td><%= percent(tag_coverage) %></td>
|
|
93
|
-
<td><%= number(total - @stats.untagged_calls_count) %></td>
|
|
94
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: tag_coverage, max: 100.0 %></td>
|
|
95
|
-
</tr>
|
|
96
|
-
|
|
97
|
-
<% if @stats.latency_column_present %>
|
|
98
|
-
<% latency_coverage = coverage_percent(total - @stats.missing_latency_count, total) %>
|
|
99
|
-
<tr>
|
|
100
|
-
<td>Latency</td>
|
|
101
|
-
<td><%= percent(latency_coverage) %></td>
|
|
102
|
-
<td><%= number(total - @stats.missing_latency_count) %></td>
|
|
103
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: latency_coverage, max: 100.0 %></td>
|
|
104
|
-
</tr>
|
|
105
|
-
<% end %>
|
|
106
|
-
</tbody>
|
|
107
|
-
</table>
|
|
108
|
-
</section>
|
|
109
210
|
<% end %>
|
|
110
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<section class="lct-panel">
|
|
2
|
-
<h2>Database unavailable</h2>
|
|
3
|
-
<p class="lct-
|
|
1
|
+
<section class="lct-panel lct-empty">
|
|
2
|
+
<h2 class="lct-state-title">Database unavailable</h2>
|
|
3
|
+
<p class="lct-state-copy">
|
|
4
4
|
llm_cost_tracker could not read the <span class="lct-code">llm_api_calls</span> table.
|
|
5
5
|
Check that ActiveRecord is connected, then run
|
|
6
6
|
<span class="lct-code">rails generate llm_cost_tracker:install</span> and migrate your database.
|