llm_cost_tracker 0.2.0.alpha1 → 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 +29 -2
- 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,4 +1,4 @@
|
|
|
1
|
-
<section class="lct-panel">
|
|
2
|
-
<h2>Invalid filter</h2>
|
|
3
|
-
<p class="lct-
|
|
1
|
+
<section class="lct-panel lct-empty">
|
|
2
|
+
<h2 class="lct-state-title">Invalid filter</h2>
|
|
3
|
+
<p class="lct-state-copy"><%= @error_message %></p>
|
|
4
4
|
</section>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
<section class="lct-panel">
|
|
2
|
-
<h2>Call not found</h2>
|
|
3
|
-
<p class="lct-
|
|
1
|
+
<section class="lct-panel lct-empty">
|
|
2
|
+
<h2 class="lct-state-title">Call not found</h2>
|
|
3
|
+
<p class="lct-state-copy">The requested LLM API call could not be found.</p>
|
|
4
4
|
<p><%= link_to "Back to calls", calls_path, class: "lct-button lct-button-secondary" %></p>
|
|
5
5
|
</section>
|
|
@@ -1,91 +1,99 @@
|
|
|
1
|
-
<section class="lct-panel">
|
|
2
|
-
<
|
|
1
|
+
<section class="lct-panel lct-toolbar">
|
|
2
|
+
<div class="lct-toolbar-head">
|
|
3
|
+
<h2 class="lct-section-title">Models</h2>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
3
6
|
<form class="lct-filters" action="<%= models_path %>" method="get">
|
|
4
|
-
<div class="lct-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
<div class="lct-filter-row lct-filter-row-with-sort">
|
|
8
|
+
<div class="lct-field">
|
|
9
|
+
<label for="lct-models-from">From</label>
|
|
10
|
+
<input id="lct-models-from" data-lct-filter-input type="date" name="from" value="<%= params[:from] %>">
|
|
11
|
+
</div>
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
<div class="lct-field">
|
|
14
|
+
<label for="lct-models-to">To</label>
|
|
15
|
+
<input id="lct-models-to" type="date" name="to" value="<%= params[:to] %>">
|
|
16
|
+
</div>
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
<div class="lct-field">
|
|
19
|
+
<label for="lct-models-provider">Provider</label>
|
|
20
|
+
<%= select_tag :provider,
|
|
21
|
+
options_for_select(provider_filter_options, params[:provider]),
|
|
22
|
+
include_blank: "All providers",
|
|
23
|
+
id: "lct-models-provider" %>
|
|
24
|
+
</div>
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<input id="lct-tag-key" type="text" name="tag_key" value="<%= params[:tag_key] %>">
|
|
27
|
-
</div>
|
|
26
|
+
<div class="lct-field">
|
|
27
|
+
<label for="lct-models-model">Model</label>
|
|
28
|
+
<%= select_tag :model,
|
|
29
|
+
options_for_select(model_filter_options, params[:model]),
|
|
30
|
+
include_blank: "All models",
|
|
31
|
+
id: "lct-models-model" %>
|
|
32
|
+
</div>
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<% if @latency_available %>
|
|
41
|
-
<option value="latency" <%= "selected" if @sort == "latency" %>>Avg latency</option>
|
|
42
|
-
<% end %>
|
|
43
|
-
</select>
|
|
44
|
-
</div>
|
|
34
|
+
<div class="lct-field">
|
|
35
|
+
<label for="lct-models-sort">Sort</label>
|
|
36
|
+
<select id="lct-models-sort" name="sort">
|
|
37
|
+
<option value="cost" <%= "selected" if @sort.blank? || @sort == "cost" %>>Total spend</option>
|
|
38
|
+
<option value="calls" <%= "selected" if @sort == "calls" %>>Call volume</option>
|
|
39
|
+
<option value="avg_cost" <%= "selected" if @sort == "avg_cost" %>>Avg cost / call</option>
|
|
40
|
+
<% if @latency_available %>
|
|
41
|
+
<option value="latency" <%= "selected" if @sort == "latency" %>>Avg latency</option>
|
|
42
|
+
<% end %>
|
|
43
|
+
</select>
|
|
44
|
+
</div>
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
<div class="lct-filter-actions">
|
|
47
|
+
<button class="lct-button" type="submit">Apply</button>
|
|
48
|
+
<%= link_to("Reset", models_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
49
|
+
</div>
|
|
49
50
|
</div>
|
|
50
51
|
</form>
|
|
52
|
+
|
|
53
|
+
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: models_path %>
|
|
51
54
|
</section>
|
|
52
55
|
|
|
53
56
|
<% if @rows.empty? %>
|
|
54
57
|
<section class="lct-panel lct-empty">
|
|
55
|
-
<h2 class="lct-
|
|
56
|
-
<p class="lct-
|
|
58
|
+
<h2 class="lct-state-title">No models in this slice</h2>
|
|
59
|
+
<p class="lct-state-copy">Tracked models will appear here once calls match the current provider, model, and date filters.</p>
|
|
60
|
+
<div class="lct-state-actions">
|
|
61
|
+
<%= link_to "Clear filters", models_path, class: "lct-button lct-button-secondary" %>
|
|
62
|
+
</div>
|
|
57
63
|
</section>
|
|
58
64
|
<% else %>
|
|
59
65
|
<section class="lct-panel">
|
|
60
66
|
<div class="lct-table-wrap">
|
|
61
|
-
<table class="lct-table">
|
|
67
|
+
<table class="lct-table lct-table-compact">
|
|
62
68
|
<thead>
|
|
63
69
|
<tr>
|
|
64
70
|
<th>Provider</th>
|
|
65
71
|
<th>Model</th>
|
|
66
|
-
<th>Calls</th>
|
|
67
|
-
<th>Input</th>
|
|
68
|
-
<th>Output</th>
|
|
69
|
-
<th>Total cost</th>
|
|
70
|
-
<th>Avg cost / call</th>
|
|
72
|
+
<th class="lct-num">Calls</th>
|
|
73
|
+
<th class="lct-num">Input</th>
|
|
74
|
+
<th class="lct-num">Output</th>
|
|
75
|
+
<th class="lct-num">Total cost</th>
|
|
76
|
+
<th class="lct-num">Avg cost / call</th>
|
|
71
77
|
<% if @latency_available %>
|
|
72
|
-
<th>Avg latency</th>
|
|
78
|
+
<th class="lct-num">Avg latency</th>
|
|
73
79
|
<% end %>
|
|
80
|
+
<th></th>
|
|
74
81
|
</tr>
|
|
75
82
|
</thead>
|
|
76
83
|
<tbody>
|
|
77
84
|
<% @rows.each do |row| %>
|
|
78
85
|
<tr>
|
|
79
86
|
<td><%= row.provider %></td>
|
|
80
|
-
<td><%= row.model %></td>
|
|
81
|
-
<td><%= number(row.calls) %></td>
|
|
82
|
-
<td><%= format_tokens(row.input_tokens) %></td>
|
|
83
|
-
<td><%= format_tokens(row.output_tokens) %></td>
|
|
84
|
-
<td><%= money(row.total_cost) %></td>
|
|
85
|
-
<td><%= money(row.average_cost_per_call) %></td>
|
|
87
|
+
<td><code class="lct-code"><%= row.model %></code></td>
|
|
88
|
+
<td class="lct-num"><%= number(row.calls) %></td>
|
|
89
|
+
<td class="lct-num"><%= format_tokens(row.input_tokens) %></td>
|
|
90
|
+
<td class="lct-num"><%= format_tokens(row.output_tokens) %></td>
|
|
91
|
+
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
92
|
+
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
86
93
|
<% if @latency_available %>
|
|
87
|
-
<td><%= row.average_latency_ms ? "#{number(row.average_latency_ms.round)}ms" : "n/a" %></td>
|
|
94
|
+
<td class="lct-num<%= ' lct-num-muted' if row.average_latency_ms.nil? %>"><%= row.average_latency_ms ? "#{number(row.average_latency_ms.round)}ms" : "n/a" %></td>
|
|
88
95
|
<% end %>
|
|
96
|
+
<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>
|
|
89
97
|
</tr>
|
|
90
98
|
<% end %>
|
|
91
99
|
</tbody>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<% if chips.any? %>
|
|
2
|
+
<div class="lct-chip-row" aria-label="Active filters">
|
|
3
|
+
<% chips.each do |chip| %>
|
|
4
|
+
<span class="lct-chip">
|
|
5
|
+
<span class="lct-chip-label"><%= chip[:label] %></span>
|
|
6
|
+
<span><%= chip[:value] %></span>
|
|
7
|
+
<% if chip[:path] %>
|
|
8
|
+
<%= link_to "×", chip[:path], class: "lct-chip-remove", aria: { label: "Remove #{chip[:label]} #{chip[:value]}" } %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</span>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% if local_assigns[:clear_path] %>
|
|
13
|
+
<%= link_to "Clear all", clear_path, class: "lct-clear-link" %>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<% visible_segments = stack_segments(segments) %>
|
|
2
|
+
|
|
3
|
+
<% if visible_segments.empty? %>
|
|
4
|
+
<p class="lct-call-breakdown-empty"><%= empty_message %></p>
|
|
5
|
+
<% else %>
|
|
6
|
+
<div class="lct-stack-track" aria-hidden="true">
|
|
7
|
+
<% visible_segments.each do |segment| %>
|
|
8
|
+
<span class="lct-stack-fill <%= segment[:css_class] %>" style="width: <%= segment[:percent].round(2) %>%"></span>
|
|
9
|
+
<% end %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="lct-stack-legend">
|
|
13
|
+
<% visible_segments.each do |segment| %>
|
|
14
|
+
<div class="lct-stack-legend-item">
|
|
15
|
+
<span class="lct-stack-key">
|
|
16
|
+
<span class="lct-stack-swatch <%= segment[:css_class] %>"></span>
|
|
17
|
+
<span><%= segment[:label] %></span>
|
|
18
|
+
</span>
|
|
19
|
+
<span class="lct-stack-meta"><%= segment[:formatted_value] %> · <%= percent(segment[:percent]) %></span>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%# locals: series: Array[{ label:, cost: }], comparison_series: nil %>
|
|
2
|
+
<% if series.blank? %>
|
|
3
|
+
<div class="lct-chart-empty">No spend in this range.</div>
|
|
4
|
+
<% else %>
|
|
5
|
+
<%= spend_chart_svg(series, comparison_points: local_assigns[:comparison_series]) %>
|
|
6
|
+
<div class="lct-chart-legend">
|
|
7
|
+
<span><%= series.first[:label] %></span>
|
|
8
|
+
<% if local_assigns[:comparison_series].present? %>
|
|
9
|
+
<span class="lct-chart-legend-compare">
|
|
10
|
+
<span class="lct-chart-key"><span class="lct-chart-key-line"></span> Current</span>
|
|
11
|
+
<span class="lct-chart-key"><span class="lct-chart-key-line lct-chart-key-line-secondary"></span> Previous</span>
|
|
12
|
+
</span>
|
|
13
|
+
<% else %>
|
|
14
|
+
<span>Peak <%= money(series.map { |p| p[:cost] }.max) %></span>
|
|
15
|
+
<% end %>
|
|
16
|
+
<span><%= series.last[:label] %></span>
|
|
17
|
+
</div>
|
|
18
|
+
<% end %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%# locals: tags: Hash, limit: Integer (optional) %>
|
|
2
|
+
<% entries = tag_chip_entries(tags, limit: local_assigns.fetch(:limit, 3)) %>
|
|
3
|
+
<% if entries.empty? %>
|
|
4
|
+
<span class="lct-tag-empty">(untagged)</span>
|
|
5
|
+
<% else %>
|
|
6
|
+
<span class="lct-tag-chips" title="<%= safe_json(tags) %>">
|
|
7
|
+
<% entries.each do |entry| %>
|
|
8
|
+
<% if entry[:more] %>
|
|
9
|
+
<span class="lct-tag-chip lct-tag-chip-more">+<%= entry[:more] %></span>
|
|
10
|
+
<% else %>
|
|
11
|
+
<span class="lct-tag-chip"><%= entry[:key] %>=<%= entry[:value] %></span>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</span>
|
|
15
|
+
<% end %>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
<section class="lct-panel">
|
|
2
|
-
<
|
|
1
|
+
<section class="lct-panel lct-empty">
|
|
2
|
+
<h2 class="lct-state-title">Setup required</h2>
|
|
3
|
+
<p class="lct-state-copy">
|
|
3
4
|
The <span class="lct-code">llm_api_calls</span> table is not available yet.
|
|
4
5
|
Run <span class="lct-code">rails generate llm_cost_tracker:install</span> and migrate your database.
|
|
5
6
|
</p>
|
|
@@ -1,19 +1,63 @@
|
|
|
1
|
+
<section class="lct-panel lct-toolbar">
|
|
2
|
+
<div class="lct-toolbar-head">
|
|
3
|
+
<h2 class="lct-section-title">Tag keys</h2>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<form class="lct-filters" action="<%= tags_path %>" method="get">
|
|
7
|
+
<div class="lct-filter-row lct-filter-row-basic">
|
|
8
|
+
<div class="lct-field">
|
|
9
|
+
<label for="lct-tags-from">From</label>
|
|
10
|
+
<input id="lct-tags-from" data-lct-filter-input type="date" name="from" value="<%= params[:from] %>">
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="lct-field">
|
|
14
|
+
<label for="lct-tags-to">To</label>
|
|
15
|
+
<input id="lct-tags-to" type="date" name="to" value="<%= params[:to] %>">
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="lct-field">
|
|
19
|
+
<label for="lct-tags-provider">Provider</label>
|
|
20
|
+
<%= select_tag :provider,
|
|
21
|
+
options_for_select(provider_filter_options, params[:provider]),
|
|
22
|
+
include_blank: "All providers",
|
|
23
|
+
id: "lct-tags-provider" %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="lct-field">
|
|
27
|
+
<label for="lct-tags-model">Model</label>
|
|
28
|
+
<%= select_tag :model,
|
|
29
|
+
options_for_select(model_filter_options, params[:model]),
|
|
30
|
+
include_blank: "All models",
|
|
31
|
+
id: "lct-tags-model" %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="lct-filter-actions">
|
|
35
|
+
<button class="lct-button" type="submit">Apply</button>
|
|
36
|
+
<%= link_to("Reset", tags_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</form>
|
|
40
|
+
|
|
41
|
+
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: tags_path %>
|
|
42
|
+
</section>
|
|
43
|
+
|
|
1
44
|
<% if @rows.empty? %>
|
|
2
45
|
<section class="lct-panel lct-empty">
|
|
3
|
-
<h2 class="lct-
|
|
4
|
-
<p class="lct-
|
|
46
|
+
<h2 class="lct-state-title">No tag keys found</h2>
|
|
47
|
+
<p class="lct-state-copy">Tag keys will appear here once tagged calls exist in the current slice.</p>
|
|
48
|
+
<div class="lct-state-actions">
|
|
49
|
+
<%= link_to "Clear filters", tags_path, class: "lct-button lct-button-secondary" %>
|
|
50
|
+
</div>
|
|
5
51
|
</section>
|
|
6
52
|
<% else %>
|
|
7
53
|
<section class="lct-panel">
|
|
8
|
-
<h2 class="lct-section-title">Tag Keys</h2>
|
|
9
|
-
<p class="lct-muted">All tag keys present in the dataset. Click a key to explore spend and call distribution by value.</p>
|
|
10
54
|
<div class="lct-table-wrap">
|
|
11
|
-
<table class="lct-table">
|
|
55
|
+
<table class="lct-table lct-table-compact">
|
|
12
56
|
<thead>
|
|
13
57
|
<tr>
|
|
14
|
-
<th>Tag
|
|
15
|
-
<th>Calls with this key</th>
|
|
16
|
-
<th>Distinct values</th>
|
|
58
|
+
<th>Tag key</th>
|
|
59
|
+
<th class="lct-num">Calls with this key</th>
|
|
60
|
+
<th class="lct-num">Distinct values</th>
|
|
17
61
|
<th></th>
|
|
18
62
|
</tr>
|
|
19
63
|
</thead>
|
|
@@ -21,9 +65,9 @@
|
|
|
21
65
|
<% @rows.each do |row| %>
|
|
22
66
|
<tr>
|
|
23
67
|
<td><code class="lct-code"><%= row.key %></code></td>
|
|
24
|
-
<td><%= number(row.calls_count) %></td>
|
|
25
|
-
<td><%= number(row.distinct_values) %></td>
|
|
26
|
-
<td><%= link_to "Breakdown
|
|
68
|
+
<td class="lct-num"><%= number(row.calls_count) %></td>
|
|
69
|
+
<td class="lct-num"><%= number(row.distinct_values) %></td>
|
|
70
|
+
<td><%= link_to "Breakdown", tag_path(row.key, current_query), class: "lct-button lct-button-secondary lct-button-compact" %></td>
|
|
27
71
|
</tr>
|
|
28
72
|
<% end %>
|
|
29
73
|
</tbody>
|
|
@@ -31,4 +75,3 @@
|
|
|
31
75
|
</div>
|
|
32
76
|
</section>
|
|
33
77
|
<% end %>
|
|
34
|
-
|
|
@@ -1,65 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
<p class="lct-muted"><%= link_to "← All tag keys", tags_path %></p>
|
|
3
|
-
<h2 class="lct-section-title">Tag key: <code class="lct-code"><%= @tag_key %></code></h2>
|
|
4
|
-
<% if @total_calls.positive? %>
|
|
5
|
-
<p class="lct-muted">
|
|
6
|
-
<%= number(@tagged_calls) %> of <%= number(@total_calls) %> calls have this key
|
|
7
|
-
(<%= percent(coverage_percent(@tagged_calls, @total_calls)) %> coverage) ·
|
|
8
|
-
<%= number(@distinct_values) %> distinct <%= @distinct_values == 1 ? "value" : "values" %>
|
|
9
|
-
</p>
|
|
10
|
-
<% end %>
|
|
11
|
-
<form class="lct-filters" action="<%= tag_path(@tag_key) %>" method="get">
|
|
12
|
-
<div class="lct-field">
|
|
13
|
-
<label for="lct-from">From</label>
|
|
14
|
-
<input id="lct-from" type="date" name="from" value="<%= params[:from] %>">
|
|
15
|
-
</div>
|
|
1
|
+
<% share_base = @tagged_calls.positive? ? @tagged_calls.to_f : 1.0 %>
|
|
16
2
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
<section class="lct-panel lct-toolbar">
|
|
4
|
+
<div class="lct-toolbar-head">
|
|
5
|
+
<div>
|
|
6
|
+
<p class="lct-muted"><%= link_to "← All tag keys", tags_path(current_query) %></p>
|
|
7
|
+
<h2 class="lct-section-title">Tag: <code class="lct-code"><%= @tag_key %></code></h2>
|
|
20
8
|
</div>
|
|
9
|
+
</div>
|
|
21
10
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
11
|
+
<form class="lct-filters" action="<%= tag_path(@tag_key) %>" method="get">
|
|
12
|
+
<div class="lct-filter-row lct-filter-row-basic">
|
|
13
|
+
<div class="lct-field">
|
|
14
|
+
<label for="lct-tag-show-from">From</label>
|
|
15
|
+
<input id="lct-tag-show-from" data-lct-filter-input type="date" name="from" value="<%= params[:from] %>">
|
|
16
|
+
</div>
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
<div class="lct-field">
|
|
19
|
+
<label for="lct-tag-show-to">To</label>
|
|
20
|
+
<input id="lct-tag-show-to" type="date" name="to" value="<%= params[:to] %>">
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="lct-field">
|
|
24
|
+
<label for="lct-tag-show-provider">Provider</label>
|
|
25
|
+
<%= select_tag :provider,
|
|
26
|
+
options_for_select(provider_filter_options, params[:provider]),
|
|
27
|
+
include_blank: "All providers",
|
|
28
|
+
id: "lct-tag-show-provider" %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="lct-field">
|
|
32
|
+
<label for="lct-tag-show-model">Model</label>
|
|
33
|
+
<%= select_tag :model,
|
|
34
|
+
options_for_select(model_filter_options, params[:model]),
|
|
35
|
+
include_blank: "All models",
|
|
36
|
+
id: "lct-tag-show-model" %>
|
|
37
|
+
</div>
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
<div class="lct-filter-actions">
|
|
40
|
+
<button class="lct-button" type="submit">Apply</button>
|
|
41
|
+
<%= link_to("Reset", tag_path(@tag_key), class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
42
|
+
</div>
|
|
35
43
|
</div>
|
|
36
44
|
</form>
|
|
45
|
+
|
|
46
|
+
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: tag_path(@tag_key) %>
|
|
47
|
+
|
|
48
|
+
<p class="lct-summary-row">
|
|
49
|
+
<span><strong><%= number(@tagged_calls) %></strong> tagged calls</span>
|
|
50
|
+
<span><strong><%= percent(coverage_percent(@tagged_calls, @total_calls)) %></strong> coverage</span>
|
|
51
|
+
<span><strong><%= number(@distinct_values) %></strong> distinct values</span>
|
|
52
|
+
</p>
|
|
37
53
|
</section>
|
|
38
54
|
|
|
39
55
|
<% if @rows.empty? %>
|
|
40
56
|
<section class="lct-panel lct-empty">
|
|
41
|
-
<h2 class="lct-
|
|
42
|
-
<p class="lct-
|
|
57
|
+
<h2 class="lct-state-title">No calls tagged with <%= @tag_key %></h2>
|
|
58
|
+
<p class="lct-state-copy">Values for this key will appear here once matching calls carry the tag in the current slice.</p>
|
|
59
|
+
<div class="lct-state-actions">
|
|
60
|
+
<%= link_to "Clear filters", tag_path(@tag_key), class: "lct-button lct-button-secondary" %>
|
|
61
|
+
</div>
|
|
43
62
|
</section>
|
|
44
63
|
<% else %>
|
|
64
|
+
<section class="lct-stat-grid lct-stat-grid-spaced">
|
|
65
|
+
<article class="lct-stat">
|
|
66
|
+
<p class="lct-stat-label">Tagged calls</p>
|
|
67
|
+
<p class="lct-stat-value"><%= number(@tagged_calls) %></p>
|
|
68
|
+
<p class="lct-stat-copy">Rows that include <code class="lct-code"><%= @tag_key %></code></p>
|
|
69
|
+
</article>
|
|
70
|
+
|
|
71
|
+
<article class="lct-stat">
|
|
72
|
+
<p class="lct-stat-label">Coverage</p>
|
|
73
|
+
<p class="lct-stat-value"><%= percent(coverage_percent(@tagged_calls, @total_calls)) %></p>
|
|
74
|
+
<p class="lct-stat-copy"><%= number(@total_calls) %> total calls in this slice</p>
|
|
75
|
+
</article>
|
|
76
|
+
|
|
77
|
+
<article class="lct-stat">
|
|
78
|
+
<p class="lct-stat-label">Distinct values</p>
|
|
79
|
+
<p class="lct-stat-value"><%= number(@distinct_values) %></p>
|
|
80
|
+
<p class="lct-stat-copy">Unique values currently visible</p>
|
|
81
|
+
</article>
|
|
82
|
+
</section>
|
|
83
|
+
|
|
45
84
|
<section class="lct-panel">
|
|
46
85
|
<div class="lct-table-wrap">
|
|
47
|
-
<table class="lct-table">
|
|
86
|
+
<table class="lct-table lct-table-compact">
|
|
48
87
|
<thead>
|
|
49
88
|
<tr>
|
|
50
89
|
<th>Value</th>
|
|
51
|
-
<th>Calls</th>
|
|
52
|
-
<th>
|
|
53
|
-
<th>
|
|
90
|
+
<th class="lct-num">Calls</th>
|
|
91
|
+
<th class="lct-num">Share</th>
|
|
92
|
+
<th class="lct-num">Total cost</th>
|
|
93
|
+
<th class="lct-num">Avg cost / call</th>
|
|
94
|
+
<th></th>
|
|
54
95
|
</tr>
|
|
55
96
|
</thead>
|
|
56
97
|
<tbody>
|
|
57
98
|
<% @rows.each do |row| %>
|
|
58
99
|
<tr>
|
|
59
|
-
<td><%= row.value %></td>
|
|
60
|
-
<td><%= number(row.calls) %></td>
|
|
61
|
-
<td><%=
|
|
62
|
-
<td><%= money(row.
|
|
100
|
+
<td><code class="lct-code"><%= row.value %></code></td>
|
|
101
|
+
<td class="lct-num"><%= number(row.calls) %></td>
|
|
102
|
+
<td class="lct-num"><%= percent((row.calls / share_base) * 100.0) %></td>
|
|
103
|
+
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
104
|
+
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
105
|
+
<td>
|
|
106
|
+
<% if row.value == "(untagged)" %>
|
|
107
|
+
<span class="lct-muted">n/a</span>
|
|
108
|
+
<% else %>
|
|
109
|
+
<%= link_to "Calls", calls_path(calls_query_for_tag(key: @tag_key, value: row.value)), class: "lct-button lct-button-secondary lct-button-compact" %>
|
|
110
|
+
<% end %>
|
|
111
|
+
</td>
|
|
63
112
|
</tr>
|
|
64
113
|
<% end %>
|
|
65
114
|
</tbody>
|
data/config/routes.rb
CHANGED
|
@@ -7,4 +7,7 @@ LlmCostTracker::Engine.routes.draw do
|
|
|
7
7
|
get "tags", to: "tags#index", as: :tags
|
|
8
8
|
get "tags/:key", to: "tags#show", as: :tag, format: false
|
|
9
9
|
get "data_quality", to: "data_quality#index", as: :data_quality
|
|
10
|
+
|
|
11
|
+
get "assets/#{LlmCostTracker::Assets.stylesheet_filename}",
|
|
12
|
+
to: "assets#stylesheet", as: :stylesheet
|
|
10
13
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module LlmCostTracker
|
|
6
|
+
module Assets
|
|
7
|
+
ROOT = File.expand_path("../../app/assets/llm_cost_tracker", __dir__)
|
|
8
|
+
STYLESHEET = "application.css"
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def root
|
|
12
|
+
ROOT
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def stylesheet_fingerprint
|
|
16
|
+
@stylesheet_fingerprint ||= Digest::SHA256.file(File.join(ROOT, STYLESHEET)).hexdigest[0, 12]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def stylesheet_filename
|
|
20
|
+
"application-#{stylesheet_fingerprint}.css"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -100,7 +100,7 @@ module LlmCostTracker
|
|
|
100
100
|
|
|
101
101
|
case connection.adapter_name
|
|
102
102
|
when /postgres/i
|
|
103
|
-
json_column =
|
|
103
|
+
json_column = tags_jsonb_column? ? column : "(#{column})::jsonb"
|
|
104
104
|
"#{json_column}->>#{connection.quote(key)}"
|
|
105
105
|
when /mysql/i
|
|
106
106
|
"JSON_UNQUOTE(JSON_EXTRACT(#{column}, #{connection.quote(json_path(key))}))"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "monitor"
|
|
4
5
|
require "yaml"
|
|
5
6
|
|
|
6
7
|
require_relative "logging"
|
|
@@ -11,14 +12,17 @@ module LlmCostTracker
|
|
|
11
12
|
EMPTY_PRICES = {}.freeze
|
|
12
13
|
PRICE_KEYS = %w[input cached_input output cache_read_input cache_creation_input].freeze
|
|
13
14
|
METADATA_KEYS = %w[_source _updated _notes].freeze
|
|
15
|
+
MUTEX = Monitor.new
|
|
14
16
|
|
|
15
17
|
class << self
|
|
16
18
|
def builtin_prices
|
|
17
|
-
@builtin_prices ||=
|
|
19
|
+
@builtin_prices ||= MUTEX.synchronize do
|
|
20
|
+
@builtin_prices || normalize_price_table(raw_registry.fetch("models", {})).freeze
|
|
21
|
+
end
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def metadata
|
|
21
|
-
@metadata ||= raw_registry.fetch("metadata", {}).freeze
|
|
25
|
+
@metadata ||= MUTEX.synchronize { @metadata || raw_registry.fetch("metadata", {}).freeze }
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
def normalize_price_table(table)
|
|
@@ -35,9 +39,14 @@ module LlmCostTracker
|
|
|
35
39
|
cached = @file_prices_cache
|
|
36
40
|
return cached[:value] if cached && cached[:key] == cache_key
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
MUTEX.synchronize do
|
|
43
|
+
cached = @file_prices_cache
|
|
44
|
+
return cached[:value] if cached && cached[:key] == cache_key
|
|
45
|
+
|
|
46
|
+
value = normalize_file_prices(price_file_models(load_price_file(path)), path: path).freeze
|
|
47
|
+
@file_prices_cache = { key: cache_key, value: value }.freeze
|
|
48
|
+
value
|
|
49
|
+
end
|
|
41
50
|
rescue Errno::ENOENT, JSON::ParserError, Psych::Exception, ArgumentError, TypeError, NoMethodError => e
|
|
42
51
|
raise Error, "Unable to load prices_file #{path.inspect}: #{e.message}"
|
|
43
52
|
end
|
|
@@ -45,7 +54,9 @@ module LlmCostTracker
|
|
|
45
54
|
private
|
|
46
55
|
|
|
47
56
|
def raw_registry
|
|
48
|
-
@raw_registry ||=
|
|
57
|
+
@raw_registry ||= MUTEX.synchronize do
|
|
58
|
+
@raw_registry || JSON.parse(File.read(DEFAULT_PRICES_PATH)).freeze
|
|
59
|
+
end
|
|
49
60
|
end
|
|
50
61
|
|
|
51
62
|
def normalize_price_entry(price)
|