llm_cost_tracker 0.9.0 → 0.11.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 +55 -0
- data/README.md +6 -2
- data/app/assets/llm_cost_tracker/application.css +782 -801
- data/app/controllers/llm_cost_tracker/application_controller.rb +15 -3
- data/app/controllers/llm_cost_tracker/calls_controller.rb +39 -20
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +0 -3
- data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +13 -19
- data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +16 -4
- data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +9 -4
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +95 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +104 -0
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +19 -5
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
- data/app/views/layouts/llm_cost_tracker/application.html.erb +80 -17
- data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
- data/app/views/llm_cost_tracker/calls/show.html.erb +119 -120
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +119 -158
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +109 -108
- data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
- data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
- data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +49 -58
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
- data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
- data/app/views/llm_cost_tracker/tags/show.html.erb +83 -102
- data/config/routes.rb +1 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +21 -25
- data/lib/llm_cost_tracker/billing/line_item.rb +15 -49
- data/lib/llm_cost_tracker/budget.rb +29 -8
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +1 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +34 -42
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +2 -6
- data/lib/llm_cost_tracker/configuration.rb +30 -44
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +1 -1
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +8 -8
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -2
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -2
- data/lib/llm_cost_tracker/doctor.rb +80 -25
- data/lib/llm_cost_tracker/engine.rb +1 -2
- data/lib/llm_cost_tracker/errors.rb +3 -2
- data/lib/llm_cost_tracker/event.rb +47 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/{durable_ingestion_generator.rb → async_ingestion_generator.rb} +8 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -23
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/{create_llm_cost_tracker_durable_ingestion.rb.erb → create_llm_cost_tracker_async_ingestion.rb.erb} +3 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +6 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -7
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +27 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +5 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +36 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +27 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +31 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +31 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +5 -2
- data/lib/llm_cost_tracker/ingestion/inbox.rb +4 -25
- data/lib/llm_cost_tracker/ingestion/pool.rb +44 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +22 -36
- data/lib/llm_cost_tracker/ingestion.rb +8 -9
- data/lib/llm_cost_tracker/integrations/anthropic.rb +46 -68
- data/lib/llm_cost_tracker/integrations/base.rb +14 -11
- data/lib/llm_cost_tracker/integrations/openai.rb +104 -131
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +27 -73
- data/lib/llm_cost_tracker/integrations.rb +14 -13
- data/lib/llm_cost_tracker/ledger/period/totals.rb +5 -3
- data/lib/llm_cost_tracker/ledger/rollups.rb +4 -13
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +11 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +11 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +0 -4
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +13 -3
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +19 -9
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +26 -11
- data/lib/llm_cost_tracker/ledger/store.rb +21 -18
- data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -1
- data/lib/llm_cost_tracker/ledger.rb +13 -0
- data/lib/llm_cost_tracker/logging.rb +0 -4
- data/lib/llm_cost_tracker/middleware/faraday.rb +46 -17
- data/lib/llm_cost_tracker/parsers/anthropic.rb +35 -59
- data/lib/llm_cost_tracker/parsers/azure.rb +46 -0
- data/lib/llm_cost_tracker/parsers/base.rb +53 -47
- data/lib/llm_cost_tracker/parsers/gemini.rb +23 -27
- data/lib/llm_cost_tracker/parsers/openai.rb +8 -40
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +26 -49
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +19 -23
- data/lib/llm_cost_tracker/parsers.rb +29 -4
- data/lib/llm_cost_tracker/prices.json +567 -579
- data/lib/llm_cost_tracker/pricing/backfill.rb +140 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +2 -4
- data/lib/llm_cost_tracker/pricing/estimator.rb +33 -0
- data/lib/llm_cost_tracker/pricing/explainer.rb +5 -2
- data/lib/llm_cost_tracker/pricing/lookup.rb +37 -2
- data/lib/llm_cost_tracker/pricing/mode.rb +34 -4
- data/lib/llm_cost_tracker/pricing/registry.rb +0 -7
- data/lib/llm_cost_tracker/pricing/service_charges.rb +6 -10
- data/lib/llm_cost_tracker/pricing/{sync_change_printer.rb → sync/change_printer.rb} +3 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +14 -2
- data/lib/llm_cost_tracker/pricing/sync.rb +1 -9
- data/lib/llm_cost_tracker/pricing/unknown.rb +5 -2
- data/lib/llm_cost_tracker/pricing.rb +71 -43
- data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +15 -0
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +22 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +17 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +17 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +35 -0
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +51 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +157 -0
- data/lib/llm_cost_tracker/railtie.rb +3 -5
- data/lib/llm_cost_tracker/reconcile_tasks.rb +18 -21
- data/lib/llm_cost_tracker/reconciliation/diff.rb +26 -45
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -4
- data/lib/llm_cost_tracker/reconciliation/importer.rb +3 -7
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +10 -33
- data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +40 -0
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +7 -31
- data/lib/llm_cost_tracker/report/formatter.rb +32 -19
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +20 -8
- data/lib/llm_cost_tracker/tags/sanitizer.rb +13 -17
- data/lib/llm_cost_tracker/token_usage.rb +4 -0
- data/lib/llm_cost_tracker/tracker.rb +33 -74
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +11 -15
- data/lib/tasks/llm_cost_tracker.rake +16 -2
- metadata +31 -12
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
- data/lib/llm_cost_tracker/dashboard_setup_state.rb +0 -109
- data/lib/llm_cost_tracker/ingestion/inline.rb +0 -22
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -126
- data/lib/llm_cost_tracker/usage_capture.rb +0 -58
|
@@ -1,33 +1,30 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Experimental in v0.9.0 — public API may change in v0.9.x based on feedback.
|
|
10
|
-
<%= link_to "Open an issue", "https://github.com/sergey-homenko/llm_cost_tracker/issues", target: "_blank", rel: "noopener" %>
|
|
11
|
-
if you use it.
|
|
12
|
-
</p>
|
|
13
|
-
</section>
|
|
1
|
+
<h2 class="lct-page-title">Provider invoice reconciliation <span class="lct-page-title-meta">Experimental</span></h2>
|
|
2
|
+
<p class="lct-page-subtitle">
|
|
3
|
+
<% if @last_imported_at %>
|
|
4
|
+
<span>Last import: <%= @last_imported_at.utc.iso8601 %></span>
|
|
5
|
+
<span class="lct-page-subtitle-sep">·</span>
|
|
6
|
+
<% end %>
|
|
7
|
+
<span>Public API may change based on feedback — <%= link_to "open an issue", "https://github.com/sergey-homenko/llm_cost_tracker/issues", target: "_blank", rel: "noopener" %> if you use it.</span>
|
|
8
|
+
</p>
|
|
14
9
|
|
|
15
10
|
<% if flash[:notice] %>
|
|
16
|
-
<
|
|
11
|
+
<div class="lct-alert lct-alert-info"><span><%= flash[:notice] %></span></div>
|
|
17
12
|
<% end %>
|
|
18
13
|
<% if flash[:alert] %>
|
|
19
|
-
<
|
|
14
|
+
<div class="lct-alert lct-alert-danger"><span><%= flash[:alert] %></span></div>
|
|
20
15
|
<% end %>
|
|
21
16
|
|
|
22
17
|
<% if @configured_importers.any? %>
|
|
23
18
|
<section class="lct-panel">
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Trigger import</h2></div>
|
|
20
|
+
<div class="lct-panel-body">
|
|
21
|
+
<% @configured_importers.each_key do |source| %>
|
|
22
|
+
<%= button_to "Re-import #{source}",
|
|
23
|
+
reconciliation_import_path(source: source),
|
|
24
|
+
method: :post,
|
|
25
|
+
class: "lct-button lct-button-secondary" %>
|
|
26
|
+
<% end %>
|
|
27
|
+
</div>
|
|
31
28
|
</section>
|
|
32
29
|
<% end %>
|
|
33
30
|
|
|
@@ -35,36 +32,30 @@
|
|
|
35
32
|
<section class="lct-panel lct-empty">
|
|
36
33
|
<h2 class="lct-state-title">Reconciliation disabled</h2>
|
|
37
34
|
<p class="lct-state-copy">
|
|
38
|
-
Provider invoice reconciliation is opt-in because it requires admin/org-level
|
|
39
|
-
provider API keys (OpenAI <code>sk-admin-…</code>, Anthropic admin keys, GCP
|
|
40
|
-
<code>billing.viewer</code>) — separate from the runtime inference key the
|
|
41
|
-
tracker uses. Enable explicitly in the initializer:
|
|
35
|
+
Provider invoice reconciliation is opt-in because it requires admin/org-level provider API keys (OpenAI <code class="lct-code-id">sk-admin-…</code>, Anthropic admin keys, GCP <code class="lct-code-id">billing.viewer</code>) — separate from the runtime inference key the tracker uses. Enable explicitly in the initializer:
|
|
42
36
|
</p>
|
|
43
|
-
<pre class="lct-
|
|
37
|
+
<pre class="lct-pre">LlmCostTracker.configure do |config|
|
|
44
38
|
config.reconciliation_enabled = true
|
|
45
|
-
end</
|
|
39
|
+
end</pre>
|
|
46
40
|
</section>
|
|
47
41
|
<% elsif !@reconciliation_installed %>
|
|
48
42
|
<section class="lct-panel lct-empty">
|
|
49
43
|
<h2 class="lct-state-title">Reconciliation not installed</h2>
|
|
50
|
-
<p class="lct-state-copy">
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<pre class="lct-state-pre"><code>bin/rails generate llm_cost_tracker:reconciliation
|
|
54
|
-
bin/rails db:migrate</code></pre>
|
|
44
|
+
<p class="lct-state-copy">Run the optional migration to create the reconciliation tables:</p>
|
|
45
|
+
<pre class="lct-pre">bin/rails generate llm_cost_tracker:reconciliation
|
|
46
|
+
bin/rails db:migrate</pre>
|
|
55
47
|
</section>
|
|
56
48
|
<% elsif @diffs.empty? %>
|
|
57
49
|
<section class="lct-panel lct-empty">
|
|
58
50
|
<h2 class="lct-state-title">No invoices imported yet</h2>
|
|
59
51
|
<p class="lct-state-copy">
|
|
60
|
-
Reconciliation compares provider-side invoices against local cost. Once you import
|
|
61
|
-
invoice rows via <code>LlmCostTracker::Reconciliation.import</code>, they appear here.
|
|
52
|
+
Reconciliation compares provider-side invoices against local cost. Once you import invoice rows via <code class="lct-code-id">LlmCostTracker::Reconciliation.import</code>, they appear here.
|
|
62
53
|
</p>
|
|
63
54
|
</section>
|
|
64
55
|
<% else %>
|
|
65
56
|
<section class="lct-panel">
|
|
66
|
-
<
|
|
67
|
-
<table class="lct-
|
|
57
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Latest period per source / provider / currency</h2></div>
|
|
58
|
+
<table class="lct-tbl">
|
|
68
59
|
<thead>
|
|
69
60
|
<tr>
|
|
70
61
|
<th>Source</th>
|
|
@@ -81,8 +72,8 @@ bin/rails db:migrate</code></pre>
|
|
|
81
72
|
<tbody>
|
|
82
73
|
<% @diffs.each do |diff| %>
|
|
83
74
|
<tr>
|
|
84
|
-
<td><%= diff.source %></td>
|
|
85
|
-
<td><%= diff.provider %></td>
|
|
75
|
+
<td><code class="lct-code-id"><%= diff.source %></code></td>
|
|
76
|
+
<td><span class="lct-model-cell"><span class="lct-provider-dot lct-provider-dot-<%= diff.provider %>"></span><%= diff.provider %></span></td>
|
|
86
77
|
<td><%= diff.currency %></td>
|
|
87
78
|
<td><%= diff.period_start %> → <%= diff.period_end %></td>
|
|
88
79
|
<td class="lct-num"><%= money(diff.provider_total) %></td>
|
|
@@ -91,9 +82,9 @@ bin/rails db:migrate</code></pre>
|
|
|
91
82
|
<td class="lct-num"><%= diff.delta_percent.nil? ? "—" : "#{diff.delta_percent}%" %></td>
|
|
92
83
|
<td>
|
|
93
84
|
<% if diff.aligned?(threshold_percent: @threshold) %>
|
|
94
|
-
<span class="lct-
|
|
85
|
+
<span class="lct-status-pill lct-status-pill-ok">Aligned</span>
|
|
95
86
|
<% else %>
|
|
96
|
-
<span class="lct-
|
|
87
|
+
<span class="lct-status-pill lct-status-pill-warn">Drift</span>
|
|
97
88
|
<% end %>
|
|
98
89
|
</td>
|
|
99
90
|
</tr>
|
|
@@ -106,23 +97,23 @@ bin/rails db:migrate</code></pre>
|
|
|
106
97
|
<% next if diff.unmatched_provider_rows.empty? && diff.unmatched_local_calls.empty? && diff.non_cost_rows.empty? %>
|
|
107
98
|
|
|
108
99
|
<section class="lct-panel">
|
|
109
|
-
<
|
|
100
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title"><code class="lct-code-id"><%= diff.source %></code> / <%= diff.provider %> / <%= diff.currency %> — drill down</h2></div>
|
|
110
101
|
|
|
111
102
|
<% if diff.unmatched_provider_rows.any? %>
|
|
112
|
-
<
|
|
113
|
-
Provider rows without a matching local call
|
|
103
|
+
<p class="lct-panel-intro">
|
|
104
|
+
<strong>Provider rows without a matching local call</strong>
|
|
114
105
|
<% if diff.unmatched_provider_rows_truncated? %>
|
|
115
|
-
<
|
|
106
|
+
<span class="lct-num-muted">(showing <%= diff.unmatched_provider_rows.size %> of <%= diff.unmatched_provider_rows_total %>, ranked by billed amount)</span>
|
|
116
107
|
<% end %>
|
|
117
|
-
</
|
|
118
|
-
<table class="lct-
|
|
108
|
+
</p>
|
|
109
|
+
<table class="lct-tbl">
|
|
119
110
|
<thead>
|
|
120
111
|
<tr><th>External ID</th><th>Match basis</th><th>Attribution</th><th class="lct-num">Billed</th></tr>
|
|
121
112
|
</thead>
|
|
122
113
|
<tbody>
|
|
123
114
|
<% diff.unmatched_provider_rows.each do |row| %>
|
|
124
115
|
<tr>
|
|
125
|
-
<td><%= row[:external_id] %></td>
|
|
116
|
+
<td><code class="lct-code-id"><%= row[:external_id] %></code></td>
|
|
126
117
|
<td><%= row[:match_basis] %></td>
|
|
127
118
|
<td><%= attribution_summary(row[:attribution]) %></td>
|
|
128
119
|
<td class="lct-num"><%= optional_money(row[:billed_amount]) %></td>
|
|
@@ -133,13 +124,13 @@ bin/rails db:migrate</code></pre>
|
|
|
133
124
|
<% end %>
|
|
134
125
|
|
|
135
126
|
<% if diff.unmatched_local_calls.any? %>
|
|
136
|
-
<
|
|
137
|
-
Local calls no provider invoice can explain
|
|
127
|
+
<p class="lct-panel-intro">
|
|
128
|
+
<strong>Local calls no provider invoice can explain</strong>
|
|
138
129
|
<% if diff.unmatched_local_calls_truncated? %>
|
|
139
|
-
<
|
|
130
|
+
<span class="lct-num-muted">(showing <%= diff.unmatched_local_calls.size %> of <%= diff.unmatched_local_calls_total %>, ranked by total cost)</span>
|
|
140
131
|
<% end %>
|
|
141
|
-
</
|
|
142
|
-
<table class="lct-
|
|
132
|
+
</p>
|
|
133
|
+
<table class="lct-tbl">
|
|
143
134
|
<thead>
|
|
144
135
|
<tr><th>Attribution</th><th class="lct-num">Calls</th><th class="lct-num">Total cost</th></tr>
|
|
145
136
|
</thead>
|
|
@@ -156,13 +147,13 @@ bin/rails db:migrate</code></pre>
|
|
|
156
147
|
<% end %>
|
|
157
148
|
|
|
158
149
|
<% if diff.non_cost_rows.any? %>
|
|
159
|
-
<
|
|
160
|
-
Non-cost evidence (free quota, credits, adjustments)
|
|
150
|
+
<p class="lct-panel-intro">
|
|
151
|
+
<strong>Non-cost evidence</strong> (free quota, credits, adjustments)
|
|
161
152
|
<% if diff.non_cost_rows_truncated? %>
|
|
162
|
-
<
|
|
153
|
+
<span class="lct-num-muted">(showing <%= diff.non_cost_rows.size %> of <%= diff.non_cost_rows_total %>, ranked by amount)</span>
|
|
163
154
|
<% end %>
|
|
164
|
-
</
|
|
165
|
-
<table class="lct-
|
|
155
|
+
</p>
|
|
156
|
+
<table class="lct-tbl">
|
|
166
157
|
<thead>
|
|
167
158
|
<tr><th>Row type</th><th>Meter</th><th>Attribution</th><th class="lct-num">Amount</th></tr>
|
|
168
159
|
</thead>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<% extra_hidden = local_assigns.fetch(:extra_hidden, {}) %>
|
|
2
|
+
<% extra_except = local_assigns.fetch(:extra_except, []) %>
|
|
3
|
+
<details class="lct-filter-pop" name="lct-filter">
|
|
4
|
+
<summary class="lct-filter-pill">
|
|
5
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
|
|
6
|
+
<span class="lct-filter-pill-key">Date</span>
|
|
7
|
+
<span class="lct-filter-pill-value"><%= @from_date.strftime("%b %-d") %> – <%= @to_date.strftime("%b %-d") %></span>
|
|
8
|
+
<svg class="lct-chev" width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l4 4 4-4"/></svg>
|
|
9
|
+
</summary>
|
|
10
|
+
<%= form_with url: path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
11
|
+
<% extra_hidden.each do |k, v| %><%= hidden_field_tag k, v %><% end %>
|
|
12
|
+
<% current_query.except(:from, :to, :page, :per, *extra_except).each do |key, value| %>
|
|
13
|
+
<% Array(value).each do |v| %><%= hidden_field_tag(value.is_a?(Array) ? "#{key}[]" : key.to_s, v) %><% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<div class="lct-filter-pop-field"><label for="lct-filter-from">From</label><input type="date" name="from" id="lct-filter-from" value="<%= @from_date.iso8601 %>"></div>
|
|
16
|
+
<div class="lct-filter-pop-field"><label for="lct-filter-to">To</label><input type="date" name="to" id="lct-filter-to" value="<%= @to_date.iso8601 %>"></div>
|
|
17
|
+
<button type="submit" class="lct-button lct-button-primary">Apply</button>
|
|
18
|
+
<% end %>
|
|
19
|
+
</details>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% extra_hidden = local_assigns.fetch(:extra_hidden, {}) %>
|
|
2
|
+
<% extra_except = local_assigns.fetch(:extra_except, []) %>
|
|
3
|
+
<% active = params[:model].presence %>
|
|
4
|
+
<details class="lct-filter-pop" name="lct-filter">
|
|
5
|
+
<summary class="lct-filter-pill <%= 'lct-active' if active %>">
|
|
6
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>
|
|
7
|
+
<span class="lct-filter-pill-key">Model</span>
|
|
8
|
+
<span class="lct-filter-pill-value"><%= active || "All" %></span>
|
|
9
|
+
<svg class="lct-chev" width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l4 4 4-4"/></svg>
|
|
10
|
+
</summary>
|
|
11
|
+
<%= form_with url: path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
12
|
+
<% extra_hidden.each do |k, v| %><%= hidden_field_tag k, v %><% end %>
|
|
13
|
+
<% current_query.except(:model, :page, :per, *extra_except).each do |key, value| %>
|
|
14
|
+
<% Array(value).each do |v| %><%= hidden_field_tag(value.is_a?(Array) ? "#{key}[]" : key.to_s, v) %><% end %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<div class="lct-filter-pop-field">
|
|
17
|
+
<label for="lct-filter-model">Model</label>
|
|
18
|
+
<%= select_tag :model, options_for_select(model_filter_options, active), include_blank: "All models", id: "lct-filter-model" %>
|
|
19
|
+
</div>
|
|
20
|
+
<button type="submit" class="lct-button lct-button-primary">Apply</button>
|
|
21
|
+
<% end %>
|
|
22
|
+
</details>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<% extra_hidden = local_assigns.fetch(:extra_hidden, {}) %>
|
|
2
|
+
<% extra_except = local_assigns.fetch(:extra_except, []) %>
|
|
3
|
+
<% active = params[:provider].presence %>
|
|
4
|
+
<details class="lct-filter-pop" name="lct-filter">
|
|
5
|
+
<summary class="lct-filter-pill <%= 'lct-active' if active %>">
|
|
6
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
|
7
|
+
<span class="lct-filter-pill-key">Provider</span>
|
|
8
|
+
<span class="lct-filter-pill-value"><%= active || "All" %></span>
|
|
9
|
+
<svg class="lct-chev" width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l4 4 4-4"/></svg>
|
|
10
|
+
</summary>
|
|
11
|
+
<%= form_with url: path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
12
|
+
<% extra_hidden.each do |k, v| %><%= hidden_field_tag k, v %><% end %>
|
|
13
|
+
<% current_query.except(:provider, :page, :per, *extra_except).each do |key, value| %>
|
|
14
|
+
<% Array(value).each do |v| %><%= hidden_field_tag(value.is_a?(Array) ? "#{key}[]" : key.to_s, v) %><% end %>
|
|
15
|
+
<% end %>
|
|
16
|
+
<div class="lct-filter-pop-field">
|
|
17
|
+
<label for="lct-filter-provider">Provider</label>
|
|
18
|
+
<%= select_tag :provider, options_for_select(provider_filter_options, active), include_blank: "All providers", id: "lct-filter-provider" %>
|
|
19
|
+
</div>
|
|
20
|
+
<button type="submit" class="lct-button lct-button-primary">Apply</button>
|
|
21
|
+
<% end %>
|
|
22
|
+
</details>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<% extra_hidden = local_assigns.fetch(:extra_hidden, {}) %>
|
|
2
|
+
<% extra_except = local_assigns.fetch(:extra_except, []) %>
|
|
3
|
+
<% active = params[:stream].presence %>
|
|
4
|
+
<% display_value = case active when "yes" then "Streaming" when "no" then "Non-streaming" else "All" end %>
|
|
5
|
+
<details class="lct-filter-pop" name="lct-filter">
|
|
6
|
+
<summary class="lct-filter-pill <%= 'lct-active' if active %>">
|
|
7
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
|
|
8
|
+
<span class="lct-filter-pill-key">Stream</span>
|
|
9
|
+
<span class="lct-filter-pill-value"><%= display_value %></span>
|
|
10
|
+
<svg class="lct-chev" width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l4 4 4-4"/></svg>
|
|
11
|
+
</summary>
|
|
12
|
+
<%= form_with url: path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
13
|
+
<% extra_hidden.each do |k, v| %><%= hidden_field_tag k, v %><% end %>
|
|
14
|
+
<% current_query.except(:stream, :page, :per, *extra_except).each do |key, value| %>
|
|
15
|
+
<% Array(value).each do |v| %><%= hidden_field_tag(value.is_a?(Array) ? "#{key}[]" : key.to_s, v) %><% end %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<div class="lct-filter-pop-field">
|
|
18
|
+
<label for="lct-filter-stream">Stream</label>
|
|
19
|
+
<%= select_tag :stream, options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, active), include_blank: "All calls", id: "lct-filter-stream" %>
|
|
20
|
+
</div>
|
|
21
|
+
<button type="submit" class="lct-button lct-button-primary">Apply</button>
|
|
22
|
+
<% end %>
|
|
23
|
+
</details>
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
<% if series.blank? %>
|
|
2
|
-
<div class="lct-
|
|
2
|
+
<div class="lct-panel-body lct-muted">No spend in this range.</div>
|
|
3
3
|
<% else %>
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
<span><%= series.first[:label] %></span>
|
|
7
|
-
<% if local_assigns[:comparison_series].present? %>
|
|
8
|
-
<span class="lct-chart-legend-compare">
|
|
9
|
-
<span class="lct-chart-key"><span class="lct-chart-key-line"></span> Current</span>
|
|
10
|
-
<span class="lct-chart-key"><span class="lct-chart-key-line lct-chart-key-line-secondary"></span> Previous</span>
|
|
11
|
-
</span>
|
|
12
|
-
<% else %>
|
|
13
|
-
<span>Peak <%= money(series.map { |p| p[:cost] }.max) %></span>
|
|
14
|
-
<% end %>
|
|
15
|
-
<span><%= series.last[:label] %></span>
|
|
4
|
+
<div class="lct-panel-body">
|
|
5
|
+
<%= spend_chart_svg(series, comparison_points: local_assigns[:comparison_series]) %>
|
|
16
6
|
</div>
|
|
17
7
|
<% end %>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<% if entry[:more] %>
|
|
8
8
|
<span class="lct-tag-chip lct-tag-chip-more">+<%= entry[:more] %></span>
|
|
9
9
|
<% else %>
|
|
10
|
-
<span class="lct-tag-chip"><%= entry[:key]
|
|
10
|
+
<span class="lct-tag-chip"><span class="lct-tag-chip-key"><%= entry[:key] %></span>=<%= entry[:value] %></span>
|
|
11
11
|
<% end %>
|
|
12
12
|
<% end %>
|
|
13
13
|
</span>
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
<section class="lct-panel lct-
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<ul class="lct-state-copy">
|
|
13
|
-
<% @setup_details.each do |detail| %>
|
|
14
|
-
<li><code class="lct-code"><%= detail %></code></li>
|
|
1
|
+
<section class="lct-panel lct-setup-card">
|
|
2
|
+
<div class="lct-panel-head">
|
|
3
|
+
<h2 class="lct-panel-title">Setup required</h2>
|
|
4
|
+
</div>
|
|
5
|
+
<div class="lct-panel-body lct-setup-body">
|
|
6
|
+
<p><%= @setup_message || "The llm_cost_tracker_calls table is not available yet." %></p>
|
|
7
|
+
<p>
|
|
8
|
+
<% if @setup_details.present? %>
|
|
9
|
+
Run <code class="lct-code-id">bin/rails llm_cost_tracker:doctor</code>, apply the listed migrations, and migrate your database.
|
|
10
|
+
<% else %>
|
|
11
|
+
Run <code class="lct-code-id">rails generate llm_cost_tracker:install</code> and migrate your database.
|
|
15
12
|
<% end %>
|
|
16
|
-
</
|
|
13
|
+
</p>
|
|
14
|
+
<p>See <code class="lct-code-id">docs/upgrading.md</code> for the migration path.</p>
|
|
15
|
+
</div>
|
|
16
|
+
<% if @setup_details.present? %>
|
|
17
|
+
<pre class="lct-pre"><%= @setup_details.join("\n") %></pre>
|
|
17
18
|
<% end %>
|
|
18
19
|
</section>
|
|
@@ -1,46 +1,41 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<div class="lct-filter-row">
|
|
2
|
+
<%= render "llm_cost_tracker/shared/filter_pill_date", path: tags_path %>
|
|
3
|
+
<%= render "llm_cost_tracker/shared/filter_pill_provider", path: tags_path %>
|
|
4
|
+
<%= render "llm_cost_tracker/shared/filter_pill_model", path: tags_path %>
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
<% if params[:provider].present? || params[:model].present? %>
|
|
7
|
+
<%= link_to "× Clear filters", tags_path(current_query(provider: nil, model: nil, page: nil)), class: "lct-filter-clear" %>
|
|
8
|
+
<% end %>
|
|
9
9
|
|
|
10
|
-
<%=
|
|
11
|
-
</
|
|
10
|
+
<span class="lct-filter-row-meta"><%= number(@rows.size) %> tag key<%= "s" unless @rows.size == 1 %></span>
|
|
11
|
+
</div>
|
|
12
12
|
|
|
13
13
|
<% if @rows.empty? %>
|
|
14
14
|
<section class="lct-panel lct-empty">
|
|
15
15
|
<h2 class="lct-state-title">No tag keys found</h2>
|
|
16
16
|
<p class="lct-state-copy">Tag keys will appear here once tagged calls exist in the current slice.</p>
|
|
17
|
-
<div class="lct-state-actions">
|
|
18
|
-
<%= link_to "Clear filters", tags_path, class: "lct-button lct-button-secondary" %>
|
|
19
|
-
</div>
|
|
20
17
|
</section>
|
|
21
18
|
<% else %>
|
|
22
19
|
<section class="lct-panel">
|
|
23
|
-
<
|
|
24
|
-
<
|
|
25
|
-
<
|
|
20
|
+
<table class="lct-tbl">
|
|
21
|
+
<thead>
|
|
22
|
+
<tr>
|
|
23
|
+
<th>Tag key</th>
|
|
24
|
+
<th class="lct-num">Calls with this key</th>
|
|
25
|
+
<th class="lct-num">Distinct values</th>
|
|
26
|
+
<th></th>
|
|
27
|
+
</tr>
|
|
28
|
+
</thead>
|
|
29
|
+
<tbody>
|
|
30
|
+
<% @rows.each do |row| %>
|
|
26
31
|
<tr>
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
<
|
|
32
|
+
<td><code class="lct-code-id"><%= row.key %></code></td>
|
|
33
|
+
<td class="lct-num"><%= number(row.calls_count) %></td>
|
|
34
|
+
<td class="lct-num"><%= number(row.distinct_values) %></td>
|
|
35
|
+
<td class="lct-num"><%= link_to "Breakdown", tag_path(row.key, current_query), class: "lct-page-link" %></td>
|
|
31
36
|
</tr>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
<tr>
|
|
36
|
-
<td><code class="lct-code"><%= row.key %></code></td>
|
|
37
|
-
<td class="lct-num"><%= number(row.calls_count) %></td>
|
|
38
|
-
<td class="lct-num"><%= number(row.distinct_values) %></td>
|
|
39
|
-
<td><%= link_to "Breakdown", tag_path(row.key, current_query), class: "lct-button lct-button-secondary lct-button-compact" %></td>
|
|
40
|
-
</tr>
|
|
41
|
-
<% end %>
|
|
42
|
-
</tbody>
|
|
43
|
-
</table>
|
|
44
|
-
</div>
|
|
37
|
+
<% end %>
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
45
40
|
</section>
|
|
46
41
|
<% end %>
|