llm_cost_tracker 0.10.0 → 0.12.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 +82 -0
- data/README.md +11 -5
- data/app/assets/llm_cost_tracker/application.css +784 -802
- data/app/controllers/llm_cost_tracker/application_controller.rb +14 -2
- data/app/controllers/llm_cost_tracker/calls_controller.rb +28 -21
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -4
- 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/tags_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +19 -16
- data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
- data/app/models/llm_cost_tracker/call.rb +28 -63
- data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
- data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
- data/app/models/llm_cost_tracker/call_tag.rb +0 -2
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +2 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +64 -43
- data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
- data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
- data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +81 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +6 -68
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +20 -12
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
- data/app/views/layouts/llm_cost_tracker/application.html.erb +74 -17
- data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
- data/app/views/llm_cost_tracker/calls/show.html.erb +132 -125
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +120 -159
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +140 -194
- 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/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 +85 -104
- data/config/routes.rb +3 -3
- data/lib/llm_cost_tracker/budget.rb +25 -28
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +2 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +30 -52
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +18 -33
- data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
- data/lib/llm_cost_tracker/charges/cost.rb +27 -0
- data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
- data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +40 -44
- data/lib/llm_cost_tracker/check.rb +5 -0
- data/lib/llm_cost_tracker/configuration.rb +13 -61
- data/lib/llm_cost_tracker/currency.rb +5 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
- data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
- data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
- data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
- data/lib/llm_cost_tracker/doctor.rb +66 -64
- data/lib/llm_cost_tracker/engine.rb +4 -4
- data/lib/llm_cost_tracker/event.rb +12 -20
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +4 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +39 -8
- data/lib/llm_cost_tracker/ingestion/inbox.rb +8 -9
- data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
- data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
- data/lib/llm_cost_tracker/ingestion.rb +24 -36
- data/lib/llm_cost_tracker/integrations/anthropic.rb +94 -116
- data/lib/llm_cost_tracker/integrations/base.rb +39 -57
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +72 -332
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +89 -145
- data/lib/llm_cost_tracker/integrations.rb +32 -25
- data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
- data/lib/llm_cost_tracker/ledger/period.rb +5 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
- data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
- data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
- data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
- data/lib/llm_cost_tracker/ledger/store.rb +18 -42
- data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
- data/lib/llm_cost_tracker/ledger.rb +14 -11
- data/lib/llm_cost_tracker/logging.rb +4 -21
- data/lib/llm_cost_tracker/middleware/faraday.rb +63 -51
- data/lib/llm_cost_tracker/parsers.rb +140 -29
- data/lib/llm_cost_tracker/prices.json +1707 -1
- data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
- data/lib/llm_cost_tracker/pricing/calculation.rb +260 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
- data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +84 -0
- data/lib/llm_cost_tracker/pricing/mode.rb +53 -35
- data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
- data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
- data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
- data/lib/llm_cost_tracker/pricing/source.rb +7 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
- data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
- data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
- data/lib/llm_cost_tracker/pricing.rb +10 -295
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
- data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
- data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +181 -0
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
- data/lib/llm_cost_tracker/providers.rb +35 -0
- data/lib/llm_cost_tracker/railtie.rb +0 -7
- data/lib/llm_cost_tracker/report/data.rb +3 -4
- data/lib/llm_cost_tracker/report/formatter.rb +33 -20
- data/lib/llm_cost_tracker/report.rb +1 -1
- data/lib/llm_cost_tracker/retention.rb +6 -19
- data/lib/llm_cost_tracker/tags/context.rb +9 -6
- data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
- data/lib/llm_cost_tracker/timing.rb +2 -4
- data/lib/llm_cost_tracker/tracker.rb +24 -36
- data/lib/llm_cost_tracker/usage/catalog.rb +58 -0
- data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
- data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +24 -46
- data/lib/llm_cost_tracker/usage/source.rb +14 -0
- data/lib/llm_cost_tracker/usage/token_usage.rb +100 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +43 -52
- data/lib/tasks/llm_cost_tracker.rake +14 -73
- metadata +92 -58
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -106
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
- data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -183
- 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/billing/components.rb +0 -95
- data/lib/llm_cost_tracker/capture/stream.rb +0 -9
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
- data/lib/llm_cost_tracker/doctor/check.rb +0 -7
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -32
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -25
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
- data/lib/llm_cost_tracker/masking.rb +0 -39
- data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -193
- data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
- data/lib/llm_cost_tracker/parsers/base.rb +0 -131
- data/lib/llm_cost_tracker/parsers/gemini.rb +0 -232
- data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -51
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -155
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
- data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
- data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
- data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
- data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -134
- data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
- data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -254
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -172
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -142
- data/lib/llm_cost_tracker/reconciliation.rb +0 -118
- data/lib/llm_cost_tracker/token_usage.rb +0 -93
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<nav class="lct-tabs" aria-label="Price sources">
|
|
2
|
+
<% LlmCostTracker::Dashboard::PricingOverview::SOURCES.each do |source| %>
|
|
3
|
+
<% data = @overview.fetch(:sources)[source] %>
|
|
4
|
+
<% next unless data %>
|
|
5
|
+
<% active = source == @active_source %>
|
|
6
|
+
<%= link_to pricing_path(source: source), class: "lct-tab#{' lct-active' if active}", aria: (active ? { current: "page" } : {}) do %>
|
|
7
|
+
<%= data.fetch(:label) %><span class="lct-tab-count"><%= number_with_delimiter(data.fetch(:rows).size) %></span>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</nav>
|
|
11
|
+
|
|
12
|
+
<% if @active_source != @overview.fetch(:effective_source) %>
|
|
13
|
+
<% if @active_source == :bundled %>
|
|
14
|
+
<div class="lct-alert lct-alert-info">
|
|
15
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
|
|
16
|
+
<span><strong>Fallback source</strong> shipped with the gem. For production, pin a <code>prices_file</code> you control.</span>
|
|
17
|
+
</div>
|
|
18
|
+
<% else %>
|
|
19
|
+
<div class="lct-alert lct-alert-info">
|
|
20
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
|
|
21
|
+
<span>A higher-priority source is active — entries here only take effect when not overridden.</span>
|
|
22
|
+
</div>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<div class="lct-filter-row">
|
|
27
|
+
<details class="lct-filter-pop" name="lct-filter">
|
|
28
|
+
<summary class="lct-filter-pill <%= 'lct-active' if @provider_filter %>">
|
|
29
|
+
<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>
|
|
30
|
+
<span class="lct-filter-pill-key">Provider</span>
|
|
31
|
+
<span class="lct-filter-pill-value"><%= @provider_filter || "All" %></span>
|
|
32
|
+
<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>
|
|
33
|
+
</summary>
|
|
34
|
+
<%= form_with url: pricing_path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
35
|
+
<%= hidden_field_tag :source, @active_source %>
|
|
36
|
+
<div class="lct-filter-pop-field">
|
|
37
|
+
<label for="lct-filter-provider">Provider</label>
|
|
38
|
+
<%= select_tag :provider, options_for_select(@providers, @provider_filter), include_blank: "All providers", id: "lct-filter-provider" %>
|
|
39
|
+
</div>
|
|
40
|
+
<button type="submit" class="lct-button lct-button-primary">Apply</button>
|
|
41
|
+
<% end %>
|
|
42
|
+
</details>
|
|
43
|
+
|
|
44
|
+
<% if @provider_filter %>
|
|
45
|
+
<%= link_to "× Clear filters", pricing_path(source: @active_source), class: "lct-filter-clear" %>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<span class="lct-filter-row-meta">
|
|
49
|
+
<%= number_with_delimiter(@rows.size) %> entr<%= @rows.size == 1 ? "y" : "ies" %><% if @source_data.fetch(:currency) %> · <%= @source_data.fetch(:currency) %><% end %><% if @source_data.fetch(:updated_at) %> · Updated <%= @source_data.fetch(:updated_at) %><% end %>
|
|
50
|
+
</span>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<% if @rows.empty? %>
|
|
54
|
+
<section class="lct-panel lct-empty">
|
|
55
|
+
<h2 class="lct-state-title">No prices for this provider</h2>
|
|
56
|
+
<p class="lct-state-copy">Try a different provider filter or clear filters to see the full price table.</p>
|
|
57
|
+
</section>
|
|
58
|
+
<% else %>
|
|
59
|
+
<section class="lct-panel">
|
|
60
|
+
<table class="lct-tbl">
|
|
61
|
+
<thead>
|
|
62
|
+
<tr>
|
|
63
|
+
<th>Provider</th>
|
|
64
|
+
<th>Model</th>
|
|
65
|
+
<th class="lct-num">Input</th>
|
|
66
|
+
<th class="lct-num">Output</th>
|
|
67
|
+
<th class="lct-num">Cache read</th>
|
|
68
|
+
<th class="lct-num">Cache write</th>
|
|
69
|
+
<th class="lct-num">Batch input</th>
|
|
70
|
+
<th class="lct-num">Batch output</th>
|
|
71
|
+
</tr>
|
|
72
|
+
</thead>
|
|
73
|
+
<tbody>
|
|
74
|
+
<% @rows.each do |row| %>
|
|
75
|
+
<tr>
|
|
76
|
+
<td>
|
|
77
|
+
<% if row.provider %>
|
|
78
|
+
<span class="lct-model-cell"><span class="lct-provider-dot lct-provider-dot-<%= row.provider %>"></span><%= row.provider %></span>
|
|
79
|
+
<% else %>
|
|
80
|
+
<span class="lct-num-muted">—</span>
|
|
81
|
+
<% end %>
|
|
82
|
+
</td>
|
|
83
|
+
<td><code class="lct-code-id"><%= row.model %></code></td>
|
|
84
|
+
<% LlmCostTracker::Dashboard::PricingOverview::RATE_COLUMNS.each do |key| %>
|
|
85
|
+
<% value = row.rates[key] %>
|
|
86
|
+
<td class="lct-num<%= ' lct-num-muted' if value.nil? %>"><%= value ? money(value) : "—" %></td>
|
|
87
|
+
<% end %>
|
|
88
|
+
</tr>
|
|
89
|
+
<% end %>
|
|
90
|
+
</tbody>
|
|
91
|
+
</table>
|
|
92
|
+
</section>
|
|
93
|
+
<% end %>
|
|
@@ -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(filter_options_for(:model), 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(filter_options_for(:provider), 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::Dashboard::Filter::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_with_delimiter(@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_with_delimiter(row.calls_count) %></td>
|
|
34
|
+
<td class="lct-num"><%= number_with_delimiter(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 %>
|
|
@@ -1,150 +1,131 @@
|
|
|
1
|
+
<% form_url = tag_path(params[:key]) %>
|
|
2
|
+
|
|
1
3
|
<% if @value.present? %>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
<div>
|
|
5
|
-
<p class="lct-muted"><%= link_to "← All values for #{params[:key]}", tag_path(params[:key], current_query.except(:tag_value)) %></p>
|
|
6
|
-
<h2 class="lct-section-title">Tag: <code class="lct-code"><%= params[:key] %></code> = <code class="lct-code"><%= @value %></code></h2>
|
|
7
|
-
</div>
|
|
8
|
-
</div>
|
|
4
|
+
<p class="lct-breadcrumb-back"><%= link_to "← All values for #{params[:key]}", tag_path(params[:key], current_query.except(:tag_value)) %></p>
|
|
5
|
+
<h2 class="lct-page-title">Tag: <code class="lct-code-id"><%= params[:key] %></code> = <code class="lct-code-id"><%= @value %></code></h2>
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
<div class="lct-filter-row">
|
|
8
|
+
<%= render "llm_cost_tracker/shared/filter_pill_date", path: form_url, extra_hidden: { tag_value: @value }, extra_except: [:tag_value] %>
|
|
9
|
+
<%= render "llm_cost_tracker/shared/filter_pill_provider", path: form_url, extra_hidden: { tag_value: @value }, extra_except: [:tag_value] %>
|
|
10
|
+
<%= render "llm_cost_tracker/shared/filter_pill_model", path: form_url, extra_hidden: { tag_value: @value }, extra_except: [:tag_value] %>
|
|
11
|
+
|
|
12
|
+
<% if params[:provider].present? || params[:model].present? %>
|
|
13
|
+
<%= link_to "× Clear filters", tag_path(params[:key], current_query(provider: nil, model: nil, page: nil)), class: "lct-filter-clear" %>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<span class="lct-filter-row-meta"><%= number_with_delimiter(@value_calls) %> call<%= "s" unless @value_calls == 1 %> · <%= money(@value_total_cost) %></span>
|
|
17
|
+
</div>
|
|
16
18
|
|
|
17
19
|
<% if @value_calls.zero? %>
|
|
18
20
|
<section class="lct-panel lct-empty">
|
|
19
21
|
<h2 class="lct-state-title">No calls tagged with <%= params[:key] %>=<%= @value %></h2>
|
|
20
22
|
<p class="lct-state-copy">No matching calls in the current slice.</p>
|
|
21
|
-
<div class="lct-state-actions">
|
|
22
|
-
<%= link_to "Back to values", tag_path(params[:key]), class: "lct-button lct-button-secondary" %>
|
|
23
|
-
</div>
|
|
24
23
|
</section>
|
|
25
24
|
<% else %>
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
<p class="lct-stat-label">Total cost</p>
|
|
25
|
+
<div class="lct-stat-grid">
|
|
26
|
+
<div class="lct-stat">
|
|
27
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Total cost</p></div>
|
|
29
28
|
<p class="lct-stat-value"><%= money(@value_total_cost) %></p>
|
|
30
|
-
<p class="lct-stat-
|
|
31
|
-
</
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<p class="lct-stat-
|
|
35
|
-
<p class="lct-stat-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
<article class="lct-stat">
|
|
40
|
-
<p class="lct-stat-label">Avg cost / call</p>
|
|
29
|
+
<p class="lct-stat-foot">Across <%= number_with_delimiter(@value_calls) %> calls</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="lct-stat">
|
|
32
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Calls</p></div>
|
|
33
|
+
<p class="lct-stat-value"><%= number_with_delimiter(@value_calls) %></p>
|
|
34
|
+
<p class="lct-stat-foot">Tagged with <code class="lct-code-id"><%= @value %></code></p>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="lct-stat">
|
|
37
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Avg cost / call</p></div>
|
|
41
38
|
<p class="lct-stat-value"><%= money(@value_calls.positive? ? @value_total_cost / @value_calls : 0) %></p>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</section>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
45
41
|
|
|
46
42
|
<section class="lct-panel">
|
|
47
|
-
<div class="lct-
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
<p class="lct-section-copy">Daily total cost for calls tagged <code class="lct-code"><%= params[:key] %>=<%= @value %></code>.</p>
|
|
51
|
-
</div>
|
|
52
|
-
<%= link_to "Calls", calls_path(calls_query_for_tag(key: @key, value: @value)), class: "lct-button lct-button-secondary lct-button-compact" %>
|
|
43
|
+
<div class="lct-panel-head">
|
|
44
|
+
<h2 class="lct-panel-title">Spend over time</h2>
|
|
45
|
+
<span class="lct-panel-meta"><%= link_to "View calls →", calls_path(calls_query_for_tag(key: @key, value: @value)) %></span>
|
|
53
46
|
</div>
|
|
54
|
-
|
|
55
|
-
<%= render "llm_cost_tracker/shared/spend_chart", series: @value_points %>
|
|
47
|
+
<%= render "llm_cost_tracker/shared/spend_chart", series: @value_points, comparison_series: [] %>
|
|
56
48
|
</section>
|
|
57
49
|
<% end %>
|
|
50
|
+
|
|
58
51
|
<% else %>
|
|
59
|
-
<section class="lct-panel lct-toolbar">
|
|
60
|
-
<div class="lct-toolbar-head">
|
|
61
|
-
<div>
|
|
62
|
-
<p class="lct-muted"><%= link_to "← All tag keys", tags_path(current_query) %></p>
|
|
63
|
-
<h2 class="lct-section-title">Tag: <code class="lct-code"><%= params[:key] %></code></h2>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
52
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
fields: %i[from to provider model],
|
|
70
|
-
reset_path: tag_path(params[:key]) %>
|
|
53
|
+
<p class="lct-breadcrumb-back"><%= link_to "← All tag keys", tags_path(current_query) %></p>
|
|
54
|
+
<h2 class="lct-page-title">Tag: <code class="lct-code-id"><%= params[:key] %></code></h2>
|
|
71
55
|
|
|
72
|
-
|
|
56
|
+
<div class="lct-filter-row">
|
|
57
|
+
<%= render "llm_cost_tracker/shared/filter_pill_date", path: form_url %>
|
|
58
|
+
<%= render "llm_cost_tracker/shared/filter_pill_provider", path: form_url %>
|
|
59
|
+
<%= render "llm_cost_tracker/shared/filter_pill_model", path: form_url %>
|
|
73
60
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<span><strong><%= number(@breakdown.distinct_values) %></strong> distinct values</span>
|
|
78
|
-
</p>
|
|
61
|
+
<% if params[:provider].present? || params[:model].present? %>
|
|
62
|
+
<%= link_to "× Clear filters", tag_path(params[:key], current_query(provider: nil, model: nil, page: nil)), class: "lct-filter-clear" %>
|
|
63
|
+
<% end %>
|
|
79
64
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<% end %>
|
|
83
|
-
</section>
|
|
65
|
+
<span class="lct-filter-row-meta"><%= number_with_delimiter(@breakdown.tagged_calls) %> tagged call<%= "s" unless @breakdown.tagged_calls == 1 %> · <%= percent(coverage_percent(@breakdown.tagged_calls, @breakdown.total_calls)) %> coverage · <%= number_with_delimiter(@breakdown.distinct_values) %> distinct value<%= "s" unless @breakdown.distinct_values == 1 %></span>
|
|
66
|
+
</div>
|
|
84
67
|
|
|
85
|
-
<% if @breakdown.rows.empty? %>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
68
|
+
<% if @breakdown.rows.empty? %>
|
|
69
|
+
<section class="lct-panel lct-empty">
|
|
70
|
+
<h2 class="lct-state-title">No calls tagged with <%= params[:key] %></h2>
|
|
71
|
+
<p class="lct-state-copy">Values for this key will appear here once matching calls carry the tag in the current slice.</p>
|
|
72
|
+
</section>
|
|
73
|
+
<% else %>
|
|
74
|
+
<div class="lct-stat-grid">
|
|
75
|
+
<div class="lct-stat">
|
|
76
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Tagged calls</p></div>
|
|
77
|
+
<p class="lct-stat-value"><%= number_with_delimiter(@breakdown.tagged_calls) %></p>
|
|
78
|
+
<p class="lct-stat-foot">Rows that include <code class="lct-code-id"><%= params[:key] %></code></p>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="lct-stat">
|
|
81
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Coverage</p></div>
|
|
82
|
+
<p class="lct-stat-value"><%= percent(coverage_percent(@breakdown.tagged_calls, @breakdown.total_calls)) %></p>
|
|
83
|
+
<p class="lct-stat-foot"><%= number_with_delimiter(@breakdown.total_calls) %> total calls in this slice</p>
|
|
84
|
+
</div>
|
|
85
|
+
<div class="lct-stat">
|
|
86
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Distinct values</p></div>
|
|
87
|
+
<p class="lct-stat-value"><%= number_with_delimiter(@breakdown.distinct_values) %></p>
|
|
88
|
+
</div>
|
|
91
89
|
</div>
|
|
92
|
-
</section>
|
|
93
|
-
<% else %>
|
|
94
|
-
<section class="lct-stat-grid lct-stat-grid-spaced">
|
|
95
|
-
<article class="lct-stat">
|
|
96
|
-
<p class="lct-stat-label">Tagged calls</p>
|
|
97
|
-
<p class="lct-stat-value"><%= number(@breakdown.tagged_calls) %></p>
|
|
98
|
-
<p class="lct-stat-copy">Rows that include <code class="lct-code"><%= params[:key] %></code></p>
|
|
99
|
-
</article>
|
|
100
|
-
|
|
101
|
-
<article class="lct-stat">
|
|
102
|
-
<p class="lct-stat-label">Coverage</p>
|
|
103
|
-
<p class="lct-stat-value"><%= percent(coverage_percent(@breakdown.tagged_calls, @breakdown.total_calls)) %></p>
|
|
104
|
-
<p class="lct-stat-copy"><%= number(@breakdown.total_calls) %> total calls in this slice</p>
|
|
105
|
-
</article>
|
|
106
90
|
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
</section>
|
|
91
|
+
<% if @breakdown.distinct_values > @breakdown.rows.size %>
|
|
92
|
+
<div class="lct-alert lct-alert-info">
|
|
93
|
+
<span>Showing top <%= number_with_delimiter(@breakdown.limit) %> values by spend.</span>
|
|
94
|
+
</div>
|
|
95
|
+
<% end %>
|
|
113
96
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<table class="lct-table lct-table-compact">
|
|
97
|
+
<section class="lct-panel">
|
|
98
|
+
<table class="lct-tbl">
|
|
117
99
|
<thead>
|
|
118
100
|
<tr>
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
<%= sortable_header("Value", "value") %>
|
|
102
|
+
<%= sortable_header("Calls", "calls", num: true) %>
|
|
121
103
|
<th class="lct-num">Share</th>
|
|
122
|
-
|
|
123
|
-
|
|
104
|
+
<%= sortable_header("Total cost", "cost", num: true, default: true) %>
|
|
105
|
+
<%= sortable_header("Avg cost / call", "avg_cost", num: true) %>
|
|
124
106
|
<th></th>
|
|
125
107
|
</tr>
|
|
126
108
|
</thead>
|
|
127
109
|
<tbody>
|
|
128
110
|
<% @breakdown.rows.each do |row| %>
|
|
129
111
|
<tr>
|
|
130
|
-
<td><code class="lct-code"><%= row.value %></code></td>
|
|
131
|
-
<td class="lct-num"><%=
|
|
112
|
+
<td><code class="lct-code-id"><%= row.value %></code></td>
|
|
113
|
+
<td class="lct-num"><%= number_with_delimiter(row.calls) %></td>
|
|
132
114
|
<td class="lct-num"><%= percent(row.share_percent) %></td>
|
|
133
115
|
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
134
116
|
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
135
|
-
<td>
|
|
117
|
+
<td class="lct-num">
|
|
136
118
|
<% if row.value == "(untagged)" %>
|
|
137
|
-
<span class="lct-muted">n/a</span>
|
|
119
|
+
<span class="lct-num-muted">n/a</span>
|
|
138
120
|
<% else %>
|
|
139
|
-
<%= link_to "Trend", tag_path(params[:key], current_query.merge(tag_value: row.value)), class: "lct-
|
|
140
|
-
<%= link_to "Calls", calls_path(calls_query_for_tag(key: params[:key], value: row.value)), class: "lct-
|
|
121
|
+
<%= link_to "Trend", tag_path(params[:key], current_query.merge(tag_value: row.value)), class: "lct-page-link" %>
|
|
122
|
+
<%= link_to "Calls", calls_path(calls_query_for_tag(key: params[:key], value: row.value)), class: "lct-page-link" %>
|
|
141
123
|
<% end %>
|
|
142
124
|
</td>
|
|
143
125
|
</tr>
|
|
144
126
|
<% end %>
|
|
145
127
|
</tbody>
|
|
146
128
|
</table>
|
|
147
|
-
</
|
|
148
|
-
|
|
149
|
-
<% end %>
|
|
129
|
+
</section>
|
|
130
|
+
<% end %>
|
|
150
131
|
<% end %>
|
data/config/routes.rb
CHANGED
|
@@ -6,9 +6,9 @@ LlmCostTracker::Engine.routes.draw do
|
|
|
6
6
|
resources :models, only: :index
|
|
7
7
|
resources :tags, only: %i[index show], param: :key, format: false
|
|
8
8
|
get "data_quality", to: "data_quality#index", as: :data_quality
|
|
9
|
-
get "
|
|
10
|
-
post "reconciliation/import", to: "reconciliation#trigger_import", as: :reconciliation_import
|
|
9
|
+
get "pricing", to: "pricing#index", as: :pricing
|
|
11
10
|
|
|
12
11
|
get "assets/#{LlmCostTracker::Assets::STYLESHEET_FILENAME}",
|
|
13
|
-
to: "assets#stylesheet",
|
|
12
|
+
to: "assets#stylesheet",
|
|
13
|
+
as: :stylesheet
|
|
14
14
|
end
|