llm_cost_tracker 0.2.0.alpha2 → 0.3.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 +48 -1
- data/README.md +114 -70
- data/Rakefile +2 -0
- 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 +12 -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 +47 -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/data_quality.rb +16 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +22 -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 +116 -74
- data/app/views/llm_cost_tracker/calls/show.html.erb +58 -1
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +211 -111
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +224 -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 +19 -0
- data/lib/llm_cost_tracker/configuration.rb +78 -42
- data/lib/llm_cost_tracker/engine.rb +2 -0
- data/lib/llm_cost_tracker/event.rb +2 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +25 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +4 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/llm_cost_tracker_prices.yml.erb +8 -1
- data/lib/llm_cost_tracker/llm_api_call.rb +9 -1
- data/lib/llm_cost_tracker/middleware/faraday.rb +57 -9
- data/lib/llm_cost_tracker/parsed_usage.rb +7 -3
- data/lib/llm_cost_tracker/parsers/anthropic.rb +79 -1
- data/lib/llm_cost_tracker/parsers/base.rb +17 -5
- data/lib/llm_cost_tracker/parsers/gemini.rb +59 -6
- data/lib/llm_cost_tracker/parsers/openai.rb +8 -0
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +8 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +55 -1
- data/lib/llm_cost_tracker/parsers/registry.rb +15 -3
- data/lib/llm_cost_tracker/parsers/sse.rb +81 -0
- data/lib/llm_cost_tracker/price_registry.rb +18 -7
- data/lib/llm_cost_tracker/price_sync/fetcher.rb +72 -0
- data/lib/llm_cost_tracker/price_sync/merger.rb +72 -0
- data/lib/llm_cost_tracker/price_sync/model_catalog.rb +77 -0
- data/lib/llm_cost_tracker/price_sync/raw_price.rb +35 -0
- data/lib/llm_cost_tracker/price_sync/source.rb +29 -0
- data/lib/llm_cost_tracker/price_sync/source_result.rb +7 -0
- data/lib/llm_cost_tracker/price_sync/sources/litellm.rb +91 -0
- data/lib/llm_cost_tracker/price_sync/sources/open_router.rb +94 -0
- data/lib/llm_cost_tracker/price_sync/validator.rb +66 -0
- data/lib/llm_cost_tracker/price_sync.rb +310 -0
- data/lib/llm_cost_tracker/pricing.rb +19 -6
- data/lib/llm_cost_tracker/retention.rb +34 -0
- data/lib/llm_cost_tracker/storage/active_record_store.rb +3 -1
- data/lib/llm_cost_tracker/stream_collector.rb +158 -0
- data/lib/llm_cost_tracker/tag_query.rb +7 -2
- data/lib/llm_cost_tracker/tags_column.rb +21 -1
- data/lib/llm_cost_tracker/tracker.rb +15 -12
- data/lib/llm_cost_tracker/value_helpers.rb +40 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +51 -29
- data/lib/tasks/llm_cost_tracker.rake +124 -0
- data/llm_cost_tracker.gemspec +9 -8
- metadata +40 -12
- data/PLAN_0.2.md +0 -488
|
@@ -1,142 +1,242 @@
|
|
|
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
|
+
<% if LlmCostTracker::LlmApiCall.stream_column? %>
|
|
33
|
+
<div class="lct-field">
|
|
34
|
+
<label for="lct-overview-stream">Stream</label>
|
|
35
|
+
<%= select_tag :stream,
|
|
36
|
+
options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, params[:stream]),
|
|
37
|
+
include_blank: "All calls",
|
|
38
|
+
id: "lct-overview-stream" %>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<div class="lct-filter-actions">
|
|
43
|
+
<button class="lct-button" type="submit">Apply</button>
|
|
44
|
+
<%= link_to("Reset", root_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</form>
|
|
48
|
+
|
|
49
|
+
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: root_path %>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
1
52
|
<% if @stats.total_calls.zero? %>
|
|
2
53
|
<section class="lct-panel lct-empty">
|
|
3
|
-
<h2 class="lct-
|
|
4
|
-
<p class="lct-
|
|
54
|
+
<h2 class="lct-state-title">No LLM calls yet</h2>
|
|
55
|
+
<p class="lct-state-copy">Tracked requests will appear here after your application records its first LLM API call.</p>
|
|
56
|
+
<div class="lct-state-actions">
|
|
57
|
+
<%= link_to "View calls", calls_path, class: "lct-button lct-button-secondary" %>
|
|
58
|
+
</div>
|
|
5
59
|
</section>
|
|
6
60
|
<% else %>
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
61
|
+
<% if @stats.unknown_pricing_count.positive? %>
|
|
62
|
+
<aside class="lct-banner lct-banner-warning" role="status">
|
|
63
|
+
<div class="lct-banner-body">
|
|
64
|
+
<p class="lct-banner-title">
|
|
65
|
+
<%= number(@stats.unknown_pricing_count) %> call<%= "s" unless @stats.unknown_pricing_count == 1 %> missing pricing
|
|
66
|
+
<span class="lct-banner-muted">· <%= percent(coverage_percent(@stats.unknown_pricing_count, @stats.total_calls)) %> of the slice</span>
|
|
67
|
+
</p>
|
|
68
|
+
<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>
|
|
69
|
+
</div>
|
|
70
|
+
<%= link_to "Fix now →", data_quality_path, class: "lct-button lct-button-secondary" %>
|
|
71
|
+
</aside>
|
|
72
|
+
<% end %>
|
|
14
73
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
74
|
+
<% if @spend_anomaly %>
|
|
75
|
+
<aside class="lct-banner lct-banner-danger" role="status">
|
|
76
|
+
<div class="lct-banner-body">
|
|
77
|
+
<p class="lct-banner-title">
|
|
78
|
+
Spend anomaly detected
|
|
79
|
+
<span class="lct-banner-muted">· <code class="lct-code"><%= @spend_anomaly.model %></code> on <%= @spend_anomaly.day.strftime("%b %-d") %></span>
|
|
80
|
+
</p>
|
|
81
|
+
<p class="lct-banner-copy">
|
|
82
|
+
<% if @spend_anomaly.ratio %>
|
|
83
|
+
<%= number_with_precision(@spend_anomaly.ratio, precision: 1) %>× its prior 7-day average in this slice
|
|
84
|
+
<% else %>
|
|
85
|
+
<%= money(@spend_anomaly.latest_spend) %> after seven quiet days in this slice
|
|
86
|
+
<% end %>
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
<%= link_to "Review calls →",
|
|
90
|
+
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)),
|
|
91
|
+
class: "lct-button lct-button-secondary" %>
|
|
92
|
+
</aside>
|
|
93
|
+
<% end %>
|
|
21
94
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
95
|
+
<section class="lct-hero">
|
|
96
|
+
<article class="lct-panel lct-hero-primary">
|
|
97
|
+
<div>
|
|
98
|
+
<p class="lct-stat-label">Total spend</p>
|
|
99
|
+
<p class="lct-hero-value"><%= money(@stats.total_cost) %></p>
|
|
100
|
+
<% badge = delta_badge(@stats.cost_delta_percent) %>
|
|
101
|
+
<span class="<%= badge[:css_class] %>"><span class="lct-delta-dot" aria-hidden="true"></span><%= badge[:text] %></span>
|
|
102
|
+
</div>
|
|
25
103
|
</article>
|
|
26
104
|
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
105
|
+
<div class="lct-hero-side">
|
|
106
|
+
<div class="lct-stat-grid">
|
|
107
|
+
<article class="lct-stat">
|
|
108
|
+
<p class="lct-stat-label">Calls</p>
|
|
109
|
+
<p class="lct-stat-value"><%= number(@stats.total_calls) %></p>
|
|
110
|
+
<% badge = delta_badge(@stats.calls_delta_percent, mode: :neutral) %>
|
|
111
|
+
<span class="<%= badge[:css_class] %>"><span class="lct-delta-dot" aria-hidden="true"></span><%= badge[:text] %></span>
|
|
112
|
+
</article>
|
|
33
113
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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>
|
|
114
|
+
<article class="lct-stat">
|
|
115
|
+
<p class="lct-stat-label">Avg cost / call</p>
|
|
116
|
+
<p class="lct-stat-value"><%= money(@stats.average_cost_per_call) %></p>
|
|
117
|
+
</article>
|
|
45
118
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 %>
|
|
70
|
-
|
|
71
|
-
<section class="lct-panel">
|
|
72
|
-
<h2 class="lct-section-title">Daily Spend</h2>
|
|
73
|
-
<% max_cost = @time_series.map { |point| point[:cost] }.max.to_f %>
|
|
74
|
-
<table class="lct-table">
|
|
75
|
-
<thead>
|
|
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>
|
|
119
|
+
<% if @stats.average_latency_ms %>
|
|
120
|
+
<article class="lct-stat">
|
|
121
|
+
<p class="lct-stat-label">Avg latency</p>
|
|
122
|
+
<p class="lct-stat-value"><%= number(@stats.average_latency_ms.round) %>ms</p>
|
|
123
|
+
</article>
|
|
89
124
|
<% end %>
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<% if @stats.monthly_budget_status %>
|
|
129
|
+
<% budget = @stats.monthly_budget_status %>
|
|
130
|
+
<% fill_mod = budget_fill_modifier(budget[:percent_used]) %>
|
|
131
|
+
<% projected_marker = [[budget[:projected_percent_used].to_f, 0.0].max, 100.0].min %>
|
|
132
|
+
<% projected_delta = budget[:projected_delta].to_f %>
|
|
133
|
+
<section class="lct-panel lct-panel-tight">
|
|
134
|
+
<div class="lct-section-head">
|
|
135
|
+
<div>
|
|
136
|
+
<h2 class="lct-section-title">Monthly Budget</h2>
|
|
137
|
+
<p class="lct-section-copy">Current-month spend across <strong>all</strong> calls.</p>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="lct-budget">
|
|
141
|
+
<div class="lct-budget-head">
|
|
142
|
+
<span>
|
|
143
|
+
<span class="lct-budget-spent"><%= money(budget[:spent]) %></span>
|
|
144
|
+
<span class="lct-budget-of"> of <%= money(budget[:budget]) %></span>
|
|
145
|
+
</span>
|
|
146
|
+
<span class="lct-budget-percent"><%= percent(budget[:percent_used]) %></span>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="lct-budget-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<%= [budget[:percent_used].to_f, 100.0].min.round %>">
|
|
149
|
+
<div class="lct-budget-fill <%= fill_mod %>" style="width: <%= [budget[:percent_used].to_f, 100.0].min %>%"></div>
|
|
150
|
+
<% if budget[:projected_spent].positive? %>
|
|
151
|
+
<span class="lct-budget-marker" aria-hidden="true" style="left: calc(<%= projected_marker %>% - 1px)"></span>
|
|
152
|
+
<% end %>
|
|
153
|
+
</div>
|
|
154
|
+
<p class="lct-budget-projection">
|
|
155
|
+
<span>Projected <strong><%= money(budget[:projected_spent]) %></strong> by <%= budget[:projection_end_label] %></span>
|
|
156
|
+
<span class="lct-budget-projection-status <%= projected_delta.positive? ? "lct-budget-projection-status--over" : "lct-budget-projection-status--under" %>">
|
|
157
|
+
<%= money(projected_delta.abs) %> <%= projected_delta.positive? ? "over" : "under" %> budget
|
|
158
|
+
</span>
|
|
159
|
+
</p>
|
|
160
|
+
<p class="lct-budget-meta">Soft limit: blocking is not atomic under concurrency.</p>
|
|
161
|
+
</div>
|
|
162
|
+
</section>
|
|
163
|
+
<% end %>
|
|
164
|
+
</div>
|
|
92
165
|
</section>
|
|
93
166
|
|
|
94
|
-
|
|
167
|
+
<section class="lct-grid lct-two-col">
|
|
95
168
|
<section class="lct-panel">
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
169
|
+
<div class="lct-section-head">
|
|
170
|
+
<div>
|
|
171
|
+
<h2 class="lct-section-title">Daily Spend</h2>
|
|
172
|
+
<p class="lct-section-copy">Current slice vs. previous <%= number(@comparison_series.size) %>-day slice.</p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<%= render "llm_cost_tracker/shared/spend_chart", series: @time_series, comparison_series: @comparison_series %>
|
|
176
|
+
</section>
|
|
177
|
+
|
|
178
|
+
<% if @providers.any? %>
|
|
179
|
+
<section class="lct-panel">
|
|
180
|
+
<div class="lct-section-head">
|
|
181
|
+
<div>
|
|
182
|
+
<h2 class="lct-section-title">By Provider</h2>
|
|
183
|
+
<p class="lct-section-copy">Spend share across the selected slice.</p>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
<table class="lct-table lct-table-compact">
|
|
187
|
+
<thead>
|
|
108
188
|
<tr>
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
<
|
|
189
|
+
<th>Provider</th>
|
|
190
|
+
<th class="lct-num">Calls</th>
|
|
191
|
+
<th class="lct-num">Spend</th>
|
|
192
|
+
<th class="lct-num">Share</th>
|
|
193
|
+
<th></th>
|
|
113
194
|
</tr>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
195
|
+
</thead>
|
|
196
|
+
<tbody>
|
|
197
|
+
<% @providers.each do |row| %>
|
|
198
|
+
<tr>
|
|
199
|
+
<td><%= row.provider %></td>
|
|
200
|
+
<td class="lct-num"><%= number(row.calls) %></td>
|
|
201
|
+
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
202
|
+
<td class="lct-num"><%= percent(row.share_percent) %></td>
|
|
203
|
+
<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>
|
|
204
|
+
</tr>
|
|
205
|
+
<% end %>
|
|
206
|
+
</tbody>
|
|
207
|
+
</table>
|
|
208
|
+
</section>
|
|
209
|
+
<% end %>
|
|
210
|
+
</section>
|
|
119
211
|
|
|
120
212
|
<section class="lct-panel">
|
|
121
|
-
<
|
|
122
|
-
|
|
213
|
+
<div class="lct-section-head">
|
|
214
|
+
<div>
|
|
215
|
+
<h2 class="lct-section-title">Top Models</h2>
|
|
216
|
+
<p class="lct-section-copy">The heaviest contributors in the current slice.</p>
|
|
217
|
+
</div>
|
|
218
|
+
<%= link_to "View all models", models_path(current_query), class: "lct-button lct-button-secondary lct-button-compact" %>
|
|
219
|
+
</div>
|
|
220
|
+
<table class="lct-table lct-table-compact">
|
|
123
221
|
<thead>
|
|
124
222
|
<tr>
|
|
125
223
|
<th>Provider</th>
|
|
126
224
|
<th>Model</th>
|
|
127
|
-
<th>Calls</th>
|
|
128
|
-
<th>Spend</th>
|
|
129
|
-
<th>Avg cost / call</th>
|
|
225
|
+
<th class="lct-num">Calls</th>
|
|
226
|
+
<th class="lct-num">Spend</th>
|
|
227
|
+
<th class="lct-num">Avg cost / call</th>
|
|
228
|
+
<th></th>
|
|
130
229
|
</tr>
|
|
131
230
|
</thead>
|
|
132
231
|
<tbody>
|
|
133
232
|
<% @top_models.each do |row| %>
|
|
134
233
|
<tr>
|
|
135
234
|
<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>
|
|
235
|
+
<td><code class="lct-code"><%= row.model %></code></td>
|
|
236
|
+
<td class="lct-num"><%= number(row.calls) %></td>
|
|
237
|
+
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
238
|
+
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
239
|
+
<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
240
|
</tr>
|
|
141
241
|
<% end %>
|
|
142
242
|
</tbody>
|