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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +66 -1
- data/README.md +58 -225
- data/app/assets/llm_cost_tracker/application.css +218 -41
- data/app/controllers/llm_cost_tracker/application_controller.rb +30 -17
- data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +19 -14
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +10 -2
- data/app/helpers/llm_cost_tracker/application_helper.rb +11 -24
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +20 -7
- data/app/models/llm_cost_tracker/call.rb +169 -0
- data/app/models/llm_cost_tracker/call_line_item.rb +22 -0
- data/app/models/llm_cost_tracker/call_rollup.rb +9 -0
- data/app/models/llm_cost_tracker/call_tag.rb +16 -0
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +13 -0
- data/app/models/llm_cost_tracker/ingestion/lease.rb +1 -1
- data/app/models/llm_cost_tracker/provider_invoice.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +121 -30
- data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +2 -2
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
- data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
- data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
- data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
- data/app/views/llm_cost_tracker/calls/show.html.erb +62 -7
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -50
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +103 -126
- data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
- data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +63 -0
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
- data/app/views/llm_cost_tracker/tags/show.html.erb +5 -37
- data/lib/llm_cost_tracker/billing/components.rb +53 -0
- data/lib/llm_cost_tracker/billing/components.yml +117 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
- data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
- data/lib/llm_cost_tracker/budget.rb +23 -35
- data/lib/llm_cost_tracker/capture/stream_collector.rb +47 -33
- data/lib/llm_cost_tracker/configuration.rb +36 -19
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +54 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +24 -32
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
- data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
- data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +31 -0
- data/lib/llm_cost_tracker/doctor.rb +43 -45
- data/lib/llm_cost_tracker/errors.rb +5 -19
- data/lib/llm_cost_tracker/event.rb +10 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +157 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
- data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -23
- data/lib/llm_cost_tracker/ingestion/worker.rb +14 -5
- data/lib/llm_cost_tracker/ingestion.rb +28 -22
- data/lib/llm_cost_tracker/integrations/anthropic.rb +45 -38
- data/lib/llm_cost_tracker/integrations/base.rb +36 -29
- data/lib/llm_cost_tracker/integrations/openai.rb +85 -40
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +5 -5
- data/lib/llm_cost_tracker/integrations.rb +2 -2
- data/lib/llm_cost_tracker/ledger/period/totals.rb +12 -9
- data/lib/llm_cost_tracker/ledger/period.rb +5 -5
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +2 -2
- data/lib/llm_cost_tracker/ledger/rollups.rb +76 -25
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +50 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +34 -23
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
- data/lib/llm_cost_tracker/ledger/store.rb +96 -13
- data/lib/llm_cost_tracker/ledger/tags/query.rb +4 -10
- data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -15
- data/lib/llm_cost_tracker/ledger.rb +4 -2
- data/lib/llm_cost_tracker/logging.rb +2 -5
- data/lib/llm_cost_tracker/middleware/faraday.rb +7 -6
- data/lib/llm_cost_tracker/parsers/anthropic.rb +52 -7
- data/lib/llm_cost_tracker/parsers/base.rb +8 -3
- data/lib/llm_cost_tracker/parsers/gemini.rb +101 -15
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +10 -2
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +87 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +48 -21
- data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
- data/lib/llm_cost_tracker/parsers.rb +1 -1
- data/lib/llm_cost_tracker/prices.json +105 -20
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +57 -19
- data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
- data/lib/llm_cost_tracker/pricing/lookup.rb +38 -34
- data/lib/llm_cost_tracker/pricing/registry.rb +65 -45
- data/lib/llm_cost_tracker/pricing/service_charges.rb +204 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
- data/lib/llm_cost_tracker/pricing/sync.rb +57 -10
- data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
- data/lib/llm_cost_tracker/pricing.rb +190 -26
- data/lib/llm_cost_tracker/railtie.rb +0 -8
- data/lib/llm_cost_tracker/report/data.rb +16 -8
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +8 -8
- data/lib/llm_cost_tracker/tags/context.rb +2 -4
- data/lib/llm_cost_tracker/tags/key.rb +4 -0
- data/lib/llm_cost_tracker/tags/sanitizer.rb +12 -17
- data/lib/llm_cost_tracker/timing.rb +15 -0
- data/lib/llm_cost_tracker/token_usage.rb +56 -42
- data/lib/llm_cost_tracker/tracker.rb +67 -24
- data/lib/llm_cost_tracker/usage_capture.rb +29 -8
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +36 -35
- data/lib/tasks/llm_cost_tracker.rake +22 -17
- metadata +36 -41
- data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
- data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
- data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
- data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
- data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
- data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
- data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
- data/lib/llm_cost_tracker/pricing/components.rb +0 -37
- data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +0 -63
|
@@ -1,64 +1,14 @@
|
|
|
1
|
-
<% total = @stats.total_calls.to_i %>
|
|
2
|
-
<% unknown_pricing_count = @stats.unknown_pricing_count.to_i %>
|
|
3
|
-
<% untagged_calls_count = @stats.untagged_calls_count.to_i %>
|
|
4
|
-
<% missing_latency_count = @stats.missing_latency_count&.to_i %>
|
|
5
|
-
<% streaming_count = @stats.streaming_count&.to_i %>
|
|
6
|
-
<% streaming_missing_usage = @stats.streaming_missing_usage_count&.to_i %>
|
|
7
|
-
<% missing_provider_response_id_count = @stats.missing_provider_response_id_count&.to_i %>
|
|
8
|
-
<% calls_with_provider_response_id = total - missing_provider_response_id_count %>
|
|
9
|
-
|
|
10
1
|
<section class="lct-panel lct-toolbar">
|
|
11
2
|
<div class="lct-toolbar-head">
|
|
12
3
|
<h2 class="lct-section-title">Data Quality</h2>
|
|
13
4
|
</div>
|
|
14
5
|
|
|
15
|
-
|
|
16
|
-
<div class="lct-filter-row lct-filter-row-basic">
|
|
17
|
-
<div class="lct-field">
|
|
18
|
-
<label for="lct-quality-from">From</label>
|
|
19
|
-
<input id="lct-quality-from" type="date" name="from" value="<%= params[:from] %>">
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<div class="lct-field">
|
|
23
|
-
<label for="lct-quality-to">To</label>
|
|
24
|
-
<input id="lct-quality-to" type="date" name="to" value="<%= params[:to] %>">
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<div class="lct-field">
|
|
28
|
-
<label for="lct-quality-provider">Provider</label>
|
|
29
|
-
<%= select_tag :provider,
|
|
30
|
-
options_for_select(provider_filter_options, params[:provider]),
|
|
31
|
-
include_blank: "All providers",
|
|
32
|
-
id: "lct-quality-provider" %>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<div class="lct-field">
|
|
36
|
-
<label for="lct-quality-model">Model</label>
|
|
37
|
-
<%= select_tag :model,
|
|
38
|
-
options_for_select(model_filter_options, params[:model]),
|
|
39
|
-
include_blank: "All models",
|
|
40
|
-
id: "lct-quality-model" %>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<div class="lct-field">
|
|
44
|
-
<label for="lct-quality-stream">Stream</label>
|
|
45
|
-
<%= select_tag :stream,
|
|
46
|
-
options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, params[:stream]),
|
|
47
|
-
include_blank: "All calls",
|
|
48
|
-
id: "lct-quality-stream" %>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<div class="lct-filter-actions">
|
|
52
|
-
<button class="lct-button" type="submit">Apply</button>
|
|
53
|
-
<%= link_to("Reset", data_quality_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</form>
|
|
6
|
+
<%= render "llm_cost_tracker/shared/filters", path: data_quality_path %>
|
|
57
7
|
|
|
58
8
|
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: data_quality_path %>
|
|
59
9
|
</section>
|
|
60
10
|
|
|
61
|
-
<% if total.zero? %>
|
|
11
|
+
<% if @summary.total.zero? %>
|
|
62
12
|
<section class="lct-panel lct-empty">
|
|
63
13
|
<h2 class="lct-state-title">No data yet</h2>
|
|
64
14
|
<p class="lct-state-copy">Quality metrics will appear here once calls are recorded in the current slice.</p>
|
|
@@ -67,63 +17,58 @@
|
|
|
67
17
|
</div>
|
|
68
18
|
</section>
|
|
69
19
|
<% else %>
|
|
70
|
-
<section class="lct-
|
|
71
|
-
<article class="lct-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</div>
|
|
20
|
+
<section class="lct-stat-grid lct-stat-grid-spaced">
|
|
21
|
+
<article class="lct-stat">
|
|
22
|
+
<p class="lct-stat-label">Calls inspected</p>
|
|
23
|
+
<p class="lct-stat-value"><%= number(@summary.total) %></p>
|
|
24
|
+
<p class="lct-stat-sub">in current slice</p>
|
|
76
25
|
</article>
|
|
77
26
|
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<p class="lct-stat-sub"><%= percent(coverage_percent(unknown_pricing_count, total)) %> of calls</p>
|
|
84
|
-
</article>
|
|
27
|
+
<article class="lct-stat">
|
|
28
|
+
<p class="lct-stat-label">Unknown pricing</p>
|
|
29
|
+
<p class="lct-stat-value"><%= number(@summary.unknown_pricing_count) %></p>
|
|
30
|
+
<p class="lct-stat-sub"><%= percent(@summary.unknown_pricing_share) %> of calls</p>
|
|
31
|
+
</article>
|
|
85
32
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
33
|
+
<article class="lct-stat">
|
|
34
|
+
<p class="lct-stat-label">Calls without tags</p>
|
|
35
|
+
<p class="lct-stat-value"><%= number(@summary.untagged_calls_count) %></p>
|
|
36
|
+
<p class="lct-stat-sub"><%= percent(@summary.untagged_share) %> of calls</p>
|
|
37
|
+
</article>
|
|
91
38
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
39
|
+
<article class="lct-stat">
|
|
40
|
+
<p class="lct-stat-label">Missing latency</p>
|
|
41
|
+
<p class="lct-stat-value"><%= number(@summary.missing_latency_count) %></p>
|
|
42
|
+
<p class="lct-stat-sub"><%= percent(@summary.missing_latency_share) %> of calls</p>
|
|
43
|
+
</article>
|
|
97
44
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
45
|
+
<article class="lct-stat">
|
|
46
|
+
<p class="lct-stat-label">Streaming calls</p>
|
|
47
|
+
<p class="lct-stat-value"><%= number(@summary.streaming_count) %></p>
|
|
48
|
+
<p class="lct-stat-sub"><%= percent(@summary.streaming_share) %> of calls</p>
|
|
49
|
+
</article>
|
|
103
50
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
51
|
+
<% if @summary.streaming_count.positive? %>
|
|
52
|
+
<article class="lct-stat">
|
|
53
|
+
<p class="lct-stat-label">Streams without usage</p>
|
|
54
|
+
<p class="lct-stat-value"><%= number(@summary.streaming_missing_usage) %></p>
|
|
55
|
+
<p class="lct-stat-sub"><%= percent(@summary.streaming_missing_usage_share) %> of streams</p>
|
|
56
|
+
</article>
|
|
57
|
+
<% end %>
|
|
111
58
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
59
|
+
<article class="lct-stat">
|
|
60
|
+
<p class="lct-stat-label">Calls with provider response ID</p>
|
|
61
|
+
<p class="lct-stat-value"><%= number(@summary.calls_with_provider_response_id) %></p>
|
|
62
|
+
<p class="lct-stat-sub"><%= percent(@summary.provider_response_id_coverage) %> of calls</p>
|
|
63
|
+
</article>
|
|
117
64
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
65
|
+
<% if @hidden_output_summary %>
|
|
66
|
+
<article class="lct-stat">
|
|
67
|
+
<p class="lct-stat-label">Hidden output share</p>
|
|
68
|
+
<p class="lct-stat-value"><%= percent(@hidden_output_summary.fetch(:share_percent)) %></p>
|
|
69
|
+
<p class="lct-stat-sub"><%= number(@hidden_output_summary.fetch(:hidden_output_tokens)) %> of <%= number(@hidden_output_summary.fetch(:output_tokens)) %> output tokens</p>
|
|
70
|
+
</article>
|
|
71
|
+
<% end %>
|
|
127
72
|
</section>
|
|
128
73
|
|
|
129
74
|
<section class="lct-grid lct-two-col">
|
|
@@ -145,46 +90,41 @@
|
|
|
145
90
|
</tr>
|
|
146
91
|
</thead>
|
|
147
92
|
<tbody>
|
|
148
|
-
<% cost_coverage = coverage_percent(total - unknown_pricing_count, total) %>
|
|
149
93
|
<tr>
|
|
150
94
|
<td>Cost (pricing known)</td>
|
|
151
|
-
<td class="lct-num"><%= percent(cost_coverage) %></td>
|
|
152
|
-
<td class="lct-num"><%= number(
|
|
153
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: cost_coverage, max: 100.0 %></td>
|
|
95
|
+
<td class="lct-num"><%= percent(@summary.cost_coverage) %></td>
|
|
96
|
+
<td class="lct-num"><%= number(@summary.calls_with_pricing) %></td>
|
|
97
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.cost_coverage, max: 100.0 %></td>
|
|
154
98
|
</tr>
|
|
155
99
|
|
|
156
|
-
<% tag_coverage = coverage_percent(total - untagged_calls_count, total) %>
|
|
157
100
|
<tr>
|
|
158
101
|
<td>Tags (at least one tag)</td>
|
|
159
|
-
<td class="lct-num"><%= percent(tag_coverage) %></td>
|
|
160
|
-
<td class="lct-num"><%= number(
|
|
161
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: tag_coverage, max: 100.0 %></td>
|
|
102
|
+
<td class="lct-num"><%= percent(@summary.tag_coverage) %></td>
|
|
103
|
+
<td class="lct-num"><%= number(@summary.tagged_calls) %></td>
|
|
104
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.tag_coverage, max: 100.0 %></td>
|
|
162
105
|
</tr>
|
|
163
106
|
|
|
164
|
-
<% latency_coverage = coverage_percent(total - missing_latency_count, total) %>
|
|
165
107
|
<tr>
|
|
166
108
|
<td>Latency</td>
|
|
167
|
-
<td class="lct-num"><%= percent(latency_coverage) %></td>
|
|
168
|
-
<td class="lct-num"><%= number(
|
|
169
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: latency_coverage, max: 100.0 %></td>
|
|
109
|
+
<td class="lct-num"><%= percent(@summary.latency_coverage) %></td>
|
|
110
|
+
<td class="lct-num"><%= number(@summary.calls_with_latency) %></td>
|
|
111
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.latency_coverage, max: 100.0 %></td>
|
|
170
112
|
</tr>
|
|
171
113
|
|
|
172
|
-
<% if streaming_count.
|
|
173
|
-
<% stream_coverage = coverage_percent(streaming_count - streaming_missing_usage, streaming_count) %>
|
|
114
|
+
<% if @summary.streaming_count.positive? %>
|
|
174
115
|
<tr>
|
|
175
116
|
<td>Streaming usage captured</td>
|
|
176
|
-
<td class="lct-num"><%= percent(stream_coverage) %></td>
|
|
177
|
-
<td class="lct-num"><%= number(
|
|
178
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: stream_coverage, max: 100.0 %></td>
|
|
117
|
+
<td class="lct-num"><%= percent(@summary.stream_coverage) %></td>
|
|
118
|
+
<td class="lct-num"><%= number(@summary.streams_with_usage) %> / <%= number(@summary.streaming_count) %></td>
|
|
119
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.stream_coverage, max: 100.0 %></td>
|
|
179
120
|
</tr>
|
|
180
121
|
<% end %>
|
|
181
122
|
|
|
182
|
-
<% provider_response_id_coverage = coverage_percent(calls_with_provider_response_id, total) %>
|
|
183
123
|
<tr>
|
|
184
124
|
<td>Provider response ID</td>
|
|
185
|
-
<td class="lct-num"><%= percent(provider_response_id_coverage) %></td>
|
|
186
|
-
<td class="lct-num"><%= number(calls_with_provider_response_id) %></td>
|
|
187
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: provider_response_id_coverage, max: 100.0 %></td>
|
|
125
|
+
<td class="lct-num"><%= percent(@summary.provider_response_id_coverage) %></td>
|
|
126
|
+
<td class="lct-num"><%= number(@summary.calls_with_provider_response_id) %></td>
|
|
127
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.provider_response_id_coverage, max: 100.0 %></td>
|
|
188
128
|
</tr>
|
|
189
129
|
</tbody>
|
|
190
130
|
</table>
|
|
@@ -222,14 +162,14 @@
|
|
|
222
162
|
<td>Slow requests become harder to isolate on the calls page.</td>
|
|
223
163
|
<td>Make sure latency capture is enabled on every tracked request.</td>
|
|
224
164
|
</tr>
|
|
225
|
-
<% if streaming_missing_usage.
|
|
165
|
+
<% if @summary.streaming_missing_usage.positive? %>
|
|
226
166
|
<tr>
|
|
227
167
|
<td>Streams without usage</td>
|
|
228
168
|
<td>Token totals undercount when streaming responses drop the final usage event.</td>
|
|
229
169
|
<td>Send OpenAI requests with <code class="lct-code">stream_options: { include_usage: true }</code>, or wrap custom clients with <code class="lct-code">LlmCostTracker.track_stream</code>.</td>
|
|
230
170
|
</tr>
|
|
231
171
|
<% end %>
|
|
232
|
-
<% if missing_provider_response_id_count.
|
|
172
|
+
<% if @summary.missing_provider_response_id_count.positive? %>
|
|
233
173
|
<tr>
|
|
234
174
|
<td>Missing provider response IDs</td>
|
|
235
175
|
<td>Proof of provider-issued responses is weaker when calls cannot be tied back to provider objects.</td>
|
|
@@ -264,7 +204,7 @@
|
|
|
264
204
|
<% cost_key = row.fetch(:cost_key) %>
|
|
265
205
|
<% cost_value = row.fetch(:cost_value) %>
|
|
266
206
|
<tr>
|
|
267
|
-
<td><%=
|
|
207
|
+
<td><%= LlmCostTracker::TokenUsageHelper::QUALITY_LABELS.fetch(token_key) %></td>
|
|
268
208
|
<td class="lct-num"><%= number(row.fetch(:token_value)) %></td>
|
|
269
209
|
<% if row.fetch(:share_basis) == :output %>
|
|
270
210
|
<td class="lct-num"><%= percent(row.fetch(:share_percent)) %> of output</td>
|
|
@@ -281,6 +221,43 @@
|
|
|
281
221
|
</div>
|
|
282
222
|
</section>
|
|
283
223
|
|
|
224
|
+
<% if @service_charge_rows.any? %>
|
|
225
|
+
<section class="lct-panel">
|
|
226
|
+
<div class="lct-section-head">
|
|
227
|
+
<div>
|
|
228
|
+
<h2 class="lct-section-title">Service charges</h2>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="lct-table-wrap">
|
|
233
|
+
<table class="lct-table lct-table-compact">
|
|
234
|
+
<thead>
|
|
235
|
+
<tr>
|
|
236
|
+
<th>Provider</th>
|
|
237
|
+
<th>Component</th>
|
|
238
|
+
<th>Status</th>
|
|
239
|
+
<th class="lct-num">Rows</th>
|
|
240
|
+
<th class="lct-num">Quantity</th>
|
|
241
|
+
<th class="lct-num">Cost</th>
|
|
242
|
+
</tr>
|
|
243
|
+
</thead>
|
|
244
|
+
<tbody>
|
|
245
|
+
<% @service_charge_rows.each do |row| %>
|
|
246
|
+
<tr>
|
|
247
|
+
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
248
|
+
<td><code class="lct-code"><%= row.component %></code></td>
|
|
249
|
+
<td><%= row.cost_status %></td>
|
|
250
|
+
<td class="lct-num"><%= number(row.charges_count) %></td>
|
|
251
|
+
<td class="lct-num"><%= number(row.quantity) %></td>
|
|
252
|
+
<td class="lct-num"><%= optional_money(row.total_cost) %></td>
|
|
253
|
+
</tr>
|
|
254
|
+
<% end %>
|
|
255
|
+
</tbody>
|
|
256
|
+
</table>
|
|
257
|
+
</div>
|
|
258
|
+
</section>
|
|
259
|
+
<% end %>
|
|
260
|
+
|
|
284
261
|
<% unless @unknown_pricing_by_model.empty? %>
|
|
285
262
|
<section class="lct-panel">
|
|
286
263
|
<div class="lct-section-head">
|
|
@@ -305,7 +282,7 @@
|
|
|
305
282
|
<tr>
|
|
306
283
|
<td><code class="lct-code"><%= row.model %></code></td>
|
|
307
284
|
<td class="lct-num"><%= number(row.calls) %></td>
|
|
308
|
-
<td class="lct-num"><%= percent(
|
|
285
|
+
<td class="lct-num"><%= percent(row.share_percent) %></td>
|
|
309
286
|
</tr>
|
|
310
287
|
<% end %>
|
|
311
288
|
</tbody>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<section class="lct-panel lct-empty">
|
|
2
2
|
<h2 class="lct-state-title">Database unavailable</h2>
|
|
3
3
|
<p class="lct-state-copy">
|
|
4
|
-
llm_cost_tracker could not read the <span class="lct-code">
|
|
4
|
+
llm_cost_tracker could not read the <span class="lct-code">llm_cost_tracker_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.
|
|
7
7
|
</p>
|
|
@@ -3,53 +3,9 @@
|
|
|
3
3
|
<h2 class="lct-section-title">Models</h2>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<label for="lct-models-from">From</label>
|
|
10
|
-
<input id="lct-models-from" type="date" name="from" value="<%= params[:from] %>">
|
|
11
|
-
</div>
|
|
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>
|
|
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>
|
|
25
|
-
|
|
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>
|
|
33
|
-
|
|
34
|
-
<div class="lct-field">
|
|
35
|
-
<label for="lct-models-sort">Sort</label>
|
|
36
|
-
<%= select_tag :sort,
|
|
37
|
-
options_for_select(
|
|
38
|
-
[["Total spend", "cost"],
|
|
39
|
-
["Call volume", "calls"],
|
|
40
|
-
["Avg cost / call", "avg_cost"],
|
|
41
|
-
["Avg latency", "latency"]],
|
|
42
|
-
@sort.presence || "cost"
|
|
43
|
-
),
|
|
44
|
-
id: "lct-models-sort" %>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<div class="lct-filter-actions">
|
|
48
|
-
<button class="lct-button" type="submit">Apply</button>
|
|
49
|
-
<%= link_to("Reset", models_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</form>
|
|
6
|
+
<%= render "llm_cost_tracker/shared/filters",
|
|
7
|
+
path: models_path,
|
|
8
|
+
fields: %i[from to provider model] %>
|
|
53
9
|
|
|
54
10
|
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: models_path %>
|
|
55
11
|
</section>
|
|
@@ -64,6 +20,18 @@
|
|
|
64
20
|
</section>
|
|
65
21
|
<% else %>
|
|
66
22
|
<section class="lct-panel">
|
|
23
|
+
<div class="lct-results-toolbar">
|
|
24
|
+
<%= render "llm_cost_tracker/shared/sort",
|
|
25
|
+
current: (@sort.presence || "cost"),
|
|
26
|
+
options: [
|
|
27
|
+
["Total spend", "cost"],
|
|
28
|
+
["Call volume", "calls"],
|
|
29
|
+
["Avg cost / call", "avg_cost"],
|
|
30
|
+
["Avg latency", "latency"]
|
|
31
|
+
],
|
|
32
|
+
path_for_sort: ->(value) { models_path(current_query(sort: value)) } %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
67
35
|
<div class="lct-table-wrap">
|
|
68
36
|
<table class="lct-table lct-table-compact">
|
|
69
37
|
<thead>
|
|
@@ -86,9 +54,9 @@
|
|
|
86
54
|
<td><%= row.provider %></td>
|
|
87
55
|
<td><code class="lct-code"><%= row.model %></code></td>
|
|
88
56
|
<td class="lct-num"><%= number(row.calls) %></td>
|
|
89
|
-
<td class="lct-num"><%=
|
|
90
|
-
<td class="lct-num"><%=
|
|
91
|
-
<td class="lct-num"><%=
|
|
57
|
+
<td class="lct-num"><%= number(row.total_tokens) %></td>
|
|
58
|
+
<td class="lct-num"><%= number(row.input_tokens) %></td>
|
|
59
|
+
<td class="lct-num"><%= number(row.output_tokens) %></td>
|
|
92
60
|
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
93
61
|
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
94
62
|
<% average_latency_ms = row.average_latency_ms %>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<%
|
|
2
|
+
fields = local_assigns.fetch(:fields, %i[from to provider model stream])
|
|
3
|
+
default_range = LlmCostTracker::Dashboard::DateRange.call(params: params)
|
|
4
|
+
defaults = {
|
|
5
|
+
from: default_range.from.iso8601,
|
|
6
|
+
to: default_range.to.iso8601
|
|
7
|
+
}.merge(local_assigns.fetch(:defaults, {}))
|
|
8
|
+
reset_path = local_assigns.fetch(:reset_path, path)
|
|
9
|
+
filter_scope = local_assigns.fetch(:filter_scope, params)
|
|
10
|
+
%>
|
|
11
|
+
|
|
12
|
+
<form class="lct-filters" action="<%= path %>" method="get">
|
|
13
|
+
<div class="lct-filter-row">
|
|
14
|
+
<% if fields.include?(:from) %>
|
|
15
|
+
<div class="lct-field">
|
|
16
|
+
<label for="lct-filter-from">From</label>
|
|
17
|
+
<input id="lct-filter-from" type="date" name="from" value="<%= params[:from] || defaults[:from] %>">
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<% if fields.include?(:to) %>
|
|
22
|
+
<div class="lct-field">
|
|
23
|
+
<label for="lct-filter-to">To</label>
|
|
24
|
+
<input id="lct-filter-to" type="date" name="to" value="<%= params[:to] || defaults[:to] %>">
|
|
25
|
+
</div>
|
|
26
|
+
<% end %>
|
|
27
|
+
|
|
28
|
+
<% if fields.include?(:provider) %>
|
|
29
|
+
<div class="lct-field">
|
|
30
|
+
<label for="lct-filter-provider">Provider</label>
|
|
31
|
+
<%= select_tag :provider,
|
|
32
|
+
options_for_select(provider_filter_options(filter_params: filter_scope), params[:provider]),
|
|
33
|
+
include_blank: "All providers",
|
|
34
|
+
id: "lct-filter-provider" %>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<% if fields.include?(:model) %>
|
|
39
|
+
<div class="lct-field">
|
|
40
|
+
<label for="lct-filter-model">Model</label>
|
|
41
|
+
<%= select_tag :model,
|
|
42
|
+
options_for_select(model_filter_options(filter_params: filter_scope), params[:model]),
|
|
43
|
+
include_blank: "All models",
|
|
44
|
+
id: "lct-filter-model" %>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<% if fields.include?(:stream) %>
|
|
49
|
+
<div class="lct-field">
|
|
50
|
+
<label for="lct-filter-stream">Stream</label>
|
|
51
|
+
<%= select_tag :stream,
|
|
52
|
+
options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, params[:stream]),
|
|
53
|
+
include_blank: "All calls",
|
|
54
|
+
id: "lct-filter-stream" %>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
57
|
+
|
|
58
|
+
<div class="lct-filter-actions">
|
|
59
|
+
<button class="lct-button" type="submit">Apply</button>
|
|
60
|
+
<%= link_to("Reset", reset_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</form>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%
|
|
2
|
+
current = local_assigns.fetch(:current).to_s
|
|
3
|
+
options = local_assigns.fetch(:options)
|
|
4
|
+
%>
|
|
5
|
+
|
|
6
|
+
<nav class="lct-sort" aria-label="Sort by">
|
|
7
|
+
<% options.each do |label, value| %>
|
|
8
|
+
<%= link_to label,
|
|
9
|
+
path_for_sort.call(value),
|
|
10
|
+
class: ["lct-sort-option", ("is-active" if current == value.to_s)].compact.join(" "),
|
|
11
|
+
aria: ({ current: "true" } if current == value.to_s) %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</nav>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<section class="lct-panel lct-empty">
|
|
2
2
|
<h2 class="lct-state-title">Setup required</h2>
|
|
3
3
|
<p class="lct-state-copy">
|
|
4
|
-
<%= @setup_message || "The
|
|
4
|
+
<%= @setup_message || "The llm_cost_tracker_calls table is not available yet." %>
|
|
5
5
|
<% if @setup_details.present? %>
|
|
6
6
|
Run <span class="lct-code">bin/rails llm_cost_tracker:doctor</span>, apply the listed migrations, and migrate your database.
|
|
7
7
|
<% else %>
|
|
@@ -3,40 +3,9 @@
|
|
|
3
3
|
<h2 class="lct-section-title">Tag keys</h2>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<label for="lct-tags-from">From</label>
|
|
10
|
-
<input id="lct-tags-from" 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>
|
|
6
|
+
<%= render "llm_cost_tracker/shared/filters",
|
|
7
|
+
path: tags_path,
|
|
8
|
+
fields: %i[from to provider model] %>
|
|
40
9
|
|
|
41
10
|
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: tags_path %>
|
|
42
11
|
</section>
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<% share_base = @breakdown.tagged_calls.positive? ? @breakdown.tagged_calls.to_f : 1.0 %>
|
|
2
|
-
|
|
3
1
|
<section class="lct-panel lct-toolbar">
|
|
4
2
|
<div class="lct-toolbar-head">
|
|
5
3
|
<div>
|
|
@@ -8,40 +6,10 @@
|
|
|
8
6
|
</div>
|
|
9
7
|
</div>
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<input id="lct-tag-show-from" type="date" name="from" value="<%= params[:from] %>">
|
|
16
|
-
</div>
|
|
17
|
-
|
|
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>
|
|
38
|
-
|
|
39
|
-
<div class="lct-filter-actions">
|
|
40
|
-
<button class="lct-button" type="submit">Apply</button>
|
|
41
|
-
<%= link_to("Reset", tag_path(params[:key]), class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
44
|
-
</form>
|
|
9
|
+
<%= render "llm_cost_tracker/shared/filters",
|
|
10
|
+
path: tag_path(params[:key]),
|
|
11
|
+
fields: %i[from to provider model],
|
|
12
|
+
reset_path: tag_path(params[:key]) %>
|
|
45
13
|
|
|
46
14
|
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: tag_path(params[:key]) %>
|
|
47
15
|
|
|
@@ -103,7 +71,7 @@
|
|
|
103
71
|
<tr>
|
|
104
72
|
<td><code class="lct-code"><%= row.value %></code></td>
|
|
105
73
|
<td class="lct-num"><%= number(row.calls) %></td>
|
|
106
|
-
<td class="lct-num"><%= percent(
|
|
74
|
+
<td class="lct-num"><%= percent(row.share_percent) %></td>
|
|
107
75
|
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
108
76
|
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
109
77
|
<td>
|