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,86 +1,105 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<div class="lct-filter-row">
|
|
2
|
+
<%= render "llm_cost_tracker/shared/filter_pill_date", path: data_quality_path %>
|
|
3
|
+
<%= render "llm_cost_tracker/shared/filter_pill_provider", path: data_quality_path %>
|
|
4
|
+
<%= render "llm_cost_tracker/shared/filter_pill_model", path: data_quality_path %>
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
<% if params[:provider].present? || params[:model].present? %>
|
|
7
|
+
<%= link_to "× Clear filters", data_quality_path(current_query(provider: nil, model: nil, page: nil)), class: "lct-filter-clear" %>
|
|
8
|
+
<% end %>
|
|
7
9
|
|
|
8
|
-
<%=
|
|
9
|
-
</
|
|
10
|
+
<span class="lct-filter-row-meta"><%= number(@summary.total) %> call<%= "s" unless @summary.total == 1 %> inspected</span>
|
|
11
|
+
</div>
|
|
10
12
|
|
|
11
13
|
<% if @summary.total.zero? %>
|
|
12
14
|
<section class="lct-panel lct-empty">
|
|
13
15
|
<h2 class="lct-state-title">No data yet</h2>
|
|
14
16
|
<p class="lct-state-copy">Quality metrics will appear here once calls are recorded in the current slice.</p>
|
|
15
|
-
<div class="lct-state-actions">
|
|
16
|
-
<%= link_to "Clear filters", data_quality_path, class: "lct-button lct-button-secondary" %>
|
|
17
|
-
</div>
|
|
18
17
|
</section>
|
|
19
18
|
<% else %>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
<h3 class="lct-stat-section-label">Volume</h3>
|
|
20
|
+
<div class="lct-stat-grid">
|
|
21
|
+
<div class="lct-stat">
|
|
22
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Calls inspected</p></div>
|
|
23
23
|
<p class="lct-stat-value"><%= number(@summary.total) %></p>
|
|
24
|
-
<p class="lct-stat-
|
|
25
|
-
</
|
|
24
|
+
<p class="lct-stat-foot">in current slice</p>
|
|
25
|
+
</div>
|
|
26
26
|
|
|
27
|
-
<
|
|
28
|
-
<p class="lct-stat-label">
|
|
29
|
-
<p class="lct-stat-value"><%= number(@summary.
|
|
30
|
-
<p class="lct-stat-
|
|
31
|
-
</
|
|
27
|
+
<div class="lct-stat">
|
|
28
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Streaming calls</p></div>
|
|
29
|
+
<p class="lct-stat-value"><%= number(@summary.streaming_count) %></p>
|
|
30
|
+
<p class="lct-stat-foot"><%= percent(@summary.streaming_share) %> of calls</p>
|
|
31
|
+
</div>
|
|
32
32
|
|
|
33
|
-
<
|
|
34
|
-
<p class="lct-stat-label">Calls
|
|
35
|
-
<p class="lct-stat-value"><%= number(@summary.
|
|
36
|
-
<p class="lct-stat-
|
|
37
|
-
</
|
|
33
|
+
<div class="lct-stat">
|
|
34
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Calls with provider response ID</p></div>
|
|
35
|
+
<p class="lct-stat-value"><%= number(@summary.calls_with_provider_response_id) %></p>
|
|
36
|
+
<p class="lct-stat-foot"><%= percent(@summary.provider_response_id_coverage) %> of calls</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
<% incomplete_pricing = @summary.unknown_pricing_count.to_i %>
|
|
41
|
+
<% untagged = @summary.untagged_calls_count.to_i %>
|
|
42
|
+
<% missing_latency = @summary.missing_latency_count.to_i %>
|
|
43
|
+
<% streams_missing_usage = @summary.streaming_missing_usage.to_i %>
|
|
44
|
+
<% hidden_output_share = @hidden_output_summary&.fetch(:share_percent).to_f %>
|
|
45
|
+
<% has_issues = incomplete_pricing.positive? || untagged.positive? || missing_latency.positive? || streams_missing_usage.positive? || hidden_output_share.positive? %>
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
<% if has_issues %>
|
|
48
|
+
<h3 class="lct-stat-section-label">Issues</h3>
|
|
49
|
+
<div class="lct-stat-grid">
|
|
50
|
+
<% if incomplete_pricing.positive? %>
|
|
51
|
+
<div class="lct-stat lct-stat-warn">
|
|
52
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Incomplete pricing</p></div>
|
|
53
|
+
<p class="lct-stat-value"><%= number(incomplete_pricing) %></p>
|
|
54
|
+
<p class="lct-stat-foot"><%= percent(@summary.unknown_pricing_share) %> of calls</p>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
<% if untagged.positive? %>
|
|
59
|
+
<div class="lct-stat lct-stat-warn">
|
|
60
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Calls without tags</p></div>
|
|
61
|
+
<p class="lct-stat-value"><%= number(untagged) %></p>
|
|
62
|
+
<p class="lct-stat-foot"><%= percent(@summary.untagged_share) %> of calls</p>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
<% if missing_latency.positive? %>
|
|
67
|
+
<div class="lct-stat lct-stat-warn">
|
|
68
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Missing latency</p></div>
|
|
69
|
+
<p class="lct-stat-value"><%= number(missing_latency) %></p>
|
|
70
|
+
<p class="lct-stat-foot"><%= percent(@summary.missing_latency_share) %> of calls</p>
|
|
71
|
+
</div>
|
|
72
|
+
<% end %>
|
|
64
73
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</section>
|
|
74
|
+
<% if streams_missing_usage.positive? %>
|
|
75
|
+
<div class="lct-stat lct-stat-warn">
|
|
76
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Streams without usage</p></div>
|
|
77
|
+
<p class="lct-stat-value"><%= number(streams_missing_usage) %></p>
|
|
78
|
+
<p class="lct-stat-foot"><%= percent(@summary.streaming_missing_usage_share) %> of streams</p>
|
|
79
|
+
</div>
|
|
80
|
+
<% end %>
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
<p class="lct-section-copy">Use these fixes to improve the trustworthiness of downstream cost reports.</p>
|
|
82
|
+
<% if hidden_output_share.positive? %>
|
|
83
|
+
<div class="lct-stat lct-stat-warn">
|
|
84
|
+
<div class="lct-stat-head"><p class="lct-stat-label">Hidden output share</p></div>
|
|
85
|
+
<p class="lct-stat-value"><%= percent(hidden_output_share) %></p>
|
|
86
|
+
<p class="lct-stat-foot"><%= number(@hidden_output_summary.fetch(:hidden_output_tokens)) %> of <%= number(@hidden_output_summary.fetch(:output_tokens)) %> output tokens</p>
|
|
80
87
|
</div>
|
|
81
|
-
|
|
88
|
+
<% end %>
|
|
89
|
+
</div>
|
|
90
|
+
<% else %>
|
|
91
|
+
<div class="lct-alert lct-alert-info">
|
|
92
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
93
|
+
<span>No data-quality issues in this slice.</span>
|
|
94
|
+
</div>
|
|
95
|
+
<% end %>
|
|
82
96
|
|
|
83
|
-
|
|
97
|
+
<div class="lct-grid-2">
|
|
98
|
+
<section class="lct-panel">
|
|
99
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Next actions</h2></div>
|
|
100
|
+
<p class="lct-panel-intro">Use these fixes to improve the trustworthiness of downstream cost reports.</p>
|
|
101
|
+
|
|
102
|
+
<table class="lct-tbl">
|
|
84
103
|
<thead>
|
|
85
104
|
<tr>
|
|
86
105
|
<th>Issue</th>
|
|
@@ -90,14 +109,14 @@
|
|
|
90
109
|
</thead>
|
|
91
110
|
<tbody>
|
|
92
111
|
<tr>
|
|
93
|
-
<td>
|
|
94
|
-
<td>
|
|
95
|
-
<td>Update <code class="lct-code">pricing_overrides</code> or <code class="lct-code">prices_file</code>.</td>
|
|
112
|
+
<td>Incomplete pricing</td>
|
|
113
|
+
<td>Some or all line items have no configured rate, so totals undercount.</td>
|
|
114
|
+
<td>Update <code class="lct-code-id">pricing_overrides</code> or <code class="lct-code-id">prices_file</code>.</td>
|
|
96
115
|
</tr>
|
|
97
116
|
<tr>
|
|
98
117
|
<td>Missing tags</td>
|
|
99
118
|
<td>Attribution by tenant, user, or feature becomes less useful.</td>
|
|
100
|
-
<td>Pass <code class="lct-code">tags:</code> from middleware using request context.</td>
|
|
119
|
+
<td>Pass <code class="lct-code-id">tags:</code> from middleware using request context.</td>
|
|
101
120
|
</tr>
|
|
102
121
|
<tr>
|
|
103
122
|
<td>Missing latency</td>
|
|
@@ -108,14 +127,14 @@
|
|
|
108
127
|
<tr>
|
|
109
128
|
<td>Streams without usage</td>
|
|
110
129
|
<td>Token totals undercount when streaming responses drop the final usage event.</td>
|
|
111
|
-
<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>
|
|
130
|
+
<td>Send OpenAI requests with <code class="lct-code-id">stream_options: { include_usage: true }</code>, or wrap custom clients with <code class="lct-code-id">LlmCostTracker.track_stream</code>.</td>
|
|
112
131
|
</tr>
|
|
113
132
|
<% end %>
|
|
114
133
|
<% if @summary.missing_provider_response_id_count.positive? %>
|
|
115
134
|
<tr>
|
|
116
135
|
<td>Missing provider response IDs</td>
|
|
117
136
|
<td>Proof of provider-issued responses is weaker when calls cannot be tied back to provider objects.</td>
|
|
118
|
-
<td>Upgrade to the latest parser coverage and pass <code class="lct-code">provider_response_id:</code> for custom clients when the provider exposes one.</td>
|
|
137
|
+
<td>Upgrade to the latest parser coverage and pass <code class="lct-code-id">provider_response_id:</code> for custom clients when the provider exposes one.</td>
|
|
119
138
|
</tr>
|
|
120
139
|
<% end %>
|
|
121
140
|
</tbody>
|
|
@@ -123,14 +142,10 @@
|
|
|
123
142
|
</section>
|
|
124
143
|
|
|
125
144
|
<section class="lct-panel">
|
|
126
|
-
<div class="lct-
|
|
127
|
-
|
|
128
|
-
<h2 class="lct-section-title">Coverage summary</h2>
|
|
129
|
-
<p class="lct-section-copy">Good dashboards start with clean pricing, tags, and latency coverage.</p>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
145
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Coverage summary</h2></div>
|
|
146
|
+
<p class="lct-panel-intro">Good dashboards start with clean pricing, tags, and latency coverage.</p>
|
|
132
147
|
|
|
133
|
-
<table class="lct-
|
|
148
|
+
<table class="lct-tbl">
|
|
134
149
|
<thead>
|
|
135
150
|
<tr>
|
|
136
151
|
<th>Dimension</th>
|
|
@@ -179,17 +194,13 @@
|
|
|
179
194
|
</tbody>
|
|
180
195
|
</table>
|
|
181
196
|
</section>
|
|
182
|
-
</
|
|
197
|
+
</div>
|
|
183
198
|
|
|
184
199
|
<section class="lct-panel">
|
|
185
|
-
<div class="lct-
|
|
186
|
-
<div>
|
|
187
|
-
<h2 class="lct-section-title">Token usage</h2>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
200
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Token usage</h2></div>
|
|
190
201
|
|
|
191
202
|
<div class="lct-table-wrap">
|
|
192
|
-
<table class="lct-
|
|
203
|
+
<table class="lct-tbl">
|
|
193
204
|
<thead>
|
|
194
205
|
<tr>
|
|
195
206
|
<th>Bucket</th>
|
|
@@ -223,14 +234,10 @@
|
|
|
223
234
|
|
|
224
235
|
<% if @service_charge_rows.any? %>
|
|
225
236
|
<section class="lct-panel">
|
|
226
|
-
<div class="lct-
|
|
227
|
-
<div>
|
|
228
|
-
<h2 class="lct-section-title">Service charges</h2>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
237
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Service charges</h2></div>
|
|
231
238
|
|
|
232
239
|
<div class="lct-table-wrap">
|
|
233
|
-
<table class="lct-
|
|
240
|
+
<table class="lct-tbl">
|
|
234
241
|
<thead>
|
|
235
242
|
<tr>
|
|
236
243
|
<th>Provider</th>
|
|
@@ -245,8 +252,8 @@
|
|
|
245
252
|
<% @service_charge_rows.each do |row| %>
|
|
246
253
|
<% unknown_cost = row.cost_status.to_s == LlmCostTracker::Billing::CostStatus::UNKNOWN %>
|
|
247
254
|
<tr>
|
|
248
|
-
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
249
|
-
<td><code class="lct-code"><%= row.component %></code></td>
|
|
255
|
+
<td><code class="lct-code-id"><%= row.provider %></code></td>
|
|
256
|
+
<td><code class="lct-code-id"><%= row.component %></code></td>
|
|
250
257
|
<td><%= row.cost_status %></td>
|
|
251
258
|
<td class="lct-num"><%= number(row.charges_count) %></td>
|
|
252
259
|
<td class="lct-num"><%= number(row.quantity) %></td>
|
|
@@ -261,15 +268,11 @@
|
|
|
261
268
|
|
|
262
269
|
<% if @streaming_health_rows.any? %>
|
|
263
270
|
<section class="lct-panel">
|
|
264
|
-
<div class="lct-
|
|
265
|
-
|
|
266
|
-
<h2 class="lct-section-title">Streaming health by provider</h2>
|
|
267
|
-
<p class="lct-section-copy">Streams without a final usage chunk land as <code class="lct-code">usage_source: unknown</code> and undercount tokens. A high unknown share for an OpenAI-compatible provider usually means <code class="lct-code">stream_options: { include_usage: true }</code> is not being injected for that host.</p>
|
|
268
|
-
</div>
|
|
269
|
-
</div>
|
|
271
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Streaming health by provider</h2></div>
|
|
272
|
+
<p class="lct-panel-intro">Streams without a final usage chunk land as <code>usage_source: unknown</code> and undercount tokens. A high unknown share for an OpenAI-compatible provider usually means <code>stream_options: { include_usage: true }</code> is not being injected for that host.</p>
|
|
270
273
|
|
|
271
274
|
<div class="lct-table-wrap">
|
|
272
|
-
<table class="lct-
|
|
275
|
+
<table class="lct-tbl">
|
|
273
276
|
<thead>
|
|
274
277
|
<tr>
|
|
275
278
|
<th>Provider</th>
|
|
@@ -282,7 +285,7 @@
|
|
|
282
285
|
<tbody>
|
|
283
286
|
<% @streaming_health_rows.each do |row| %>
|
|
284
287
|
<tr>
|
|
285
|
-
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
288
|
+
<td><code class="lct-code-id"><%= row.provider %></code></td>
|
|
286
289
|
<td class="lct-num"><%= number(row.streams) %></td>
|
|
287
290
|
<td class="lct-num"><%= number(row.with_usage) %></td>
|
|
288
291
|
<td class="lct-num"><%= number(row.unknown) %></td>
|
|
@@ -297,16 +300,14 @@
|
|
|
297
300
|
|
|
298
301
|
<% unless @unknown_pricing_by_model.empty? %>
|
|
299
302
|
<section class="lct-panel">
|
|
300
|
-
<div class="lct-
|
|
301
|
-
<
|
|
302
|
-
|
|
303
|
-
<p class="lct-section-copy">These models have token counts but no configured rates, so cost stays unknown.</p>
|
|
304
|
-
</div>
|
|
305
|
-
<%= link_to "Calls", calls_path(current_query(sort: "unknown_pricing")), class: "lct-button lct-button-secondary" %>
|
|
303
|
+
<div class="lct-panel-head">
|
|
304
|
+
<h2 class="lct-panel-title">Incomplete pricing by model</h2>
|
|
305
|
+
<span class="lct-panel-meta"><%= link_to "View calls →", calls_path(current_query(cost_status: "incomplete", sort: nil)) %></span>
|
|
306
306
|
</div>
|
|
307
|
+
<p class="lct-panel-intro">These models have line items without configured rates, so totals undercount (a full row is missing all rates; a partial row has some line items priced and others not). After the next price refresh or a <code>pricing_overrides</code> update, run <code>bin/rails llm_cost_tracker:backfill_unknown_pricing</code> to recompute these calls.</p>
|
|
307
308
|
|
|
308
309
|
<div class="lct-table-wrap">
|
|
309
|
-
<table class="lct-
|
|
310
|
+
<table class="lct-tbl">
|
|
310
311
|
<thead>
|
|
311
312
|
<tr>
|
|
312
313
|
<th>Provider</th>
|
|
@@ -318,8 +319,8 @@
|
|
|
318
319
|
<tbody>
|
|
319
320
|
<% @unknown_pricing_by_model.each do |row| %>
|
|
320
321
|
<tr>
|
|
321
|
-
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
322
|
-
<td><code class="lct-code"><%= row.model %></code></td>
|
|
322
|
+
<td><code class="lct-code-id"><%= row.provider %></code></td>
|
|
323
|
+
<td><code class="lct-code-id"><%= row.model %></code></td>
|
|
323
324
|
<td class="lct-num"><%= number(row.calls) %></td>
|
|
324
325
|
<td class="lct-num"><%= percent(row.share_percent) %></td>
|
|
325
326
|
</tr>
|
|
@@ -1,8 +1,8 @@
|
|
|
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 <
|
|
4
|
+
llm_cost_tracker could not read the <code class="lct-code-id">llm_cost_tracker_calls</code> table.
|
|
5
5
|
Check that ActiveRecord is connected, then run
|
|
6
|
-
<
|
|
6
|
+
<code class="lct-code-id">rails generate llm_cost_tracker:install</code> and migrate your database.
|
|
7
7
|
</p>
|
|
8
8
|
</section>
|
|
@@ -1,71 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
<% rows = @rows.to_a %>
|
|
2
|
+
<div class="lct-filter-row">
|
|
3
|
+
<%= render "llm_cost_tracker/shared/filter_pill_date", path: models_path %>
|
|
4
|
+
<%= render "llm_cost_tracker/shared/filter_pill_provider", path: models_path %>
|
|
5
|
+
<%= render "llm_cost_tracker/shared/filter_pill_model", path: models_path %>
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
<% if params[:provider].present? || params[:model].present? %>
|
|
8
|
+
<%= link_to "× Clear filters", models_path(current_query(provider: nil, model: nil, page: nil)), class: "lct-filter-clear" %>
|
|
9
|
+
<% end %>
|
|
9
10
|
|
|
10
|
-
<%=
|
|
11
|
-
</
|
|
11
|
+
<span class="lct-filter-row-meta"><%= number(rows.size) %> model<%= "s" unless rows.size == 1 %></span>
|
|
12
|
+
</div>
|
|
12
13
|
|
|
13
|
-
<% if
|
|
14
|
+
<% if rows.empty? %>
|
|
14
15
|
<section class="lct-panel lct-empty">
|
|
15
16
|
<h2 class="lct-state-title">No models in this slice</h2>
|
|
16
|
-
<p class="lct-state-copy">Tracked models will appear here once calls match the current
|
|
17
|
-
<div class="lct-state-actions">
|
|
18
|
-
<%= link_to "Clear filters", models_path, class: "lct-button lct-button-secondary" %>
|
|
19
|
-
</div>
|
|
17
|
+
<p class="lct-state-copy">Tracked models will appear here once calls match the current filters.</p>
|
|
20
18
|
</section>
|
|
21
19
|
<% else %>
|
|
22
20
|
<section class="lct-panel">
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
21
|
+
<table class="lct-tbl">
|
|
22
|
+
<thead>
|
|
23
|
+
<tr>
|
|
24
|
+
<%= sortable_header("Provider", "provider") %>
|
|
25
|
+
<%= sortable_header("Model", "name") %>
|
|
26
|
+
<%= sortable_header("Calls", "calls", num: true) %>
|
|
27
|
+
<%= sortable_header("Tokens", "tokens", num: true) %>
|
|
28
|
+
<%= sortable_header("Avg latency", "latency", num: true) %>
|
|
29
|
+
<%= sortable_header("Avg cost / call", "avg_cost", num: true) %>
|
|
30
|
+
<%= sortable_header("Total cost", "cost", num: true) %>
|
|
31
|
+
<th></th>
|
|
32
|
+
</tr>
|
|
33
|
+
</thead>
|
|
34
|
+
<tbody>
|
|
35
|
+
<% rows.each do |row| %>
|
|
38
36
|
<tr>
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
<th></th>
|
|
37
|
+
<td><span class="lct-model-cell"><span class="lct-provider-dot lct-provider-dot-<%= row.provider %>"></span><%= row.provider %></span></td>
|
|
38
|
+
<td><code class="lct-code-id"><%= row.model %></code></td>
|
|
39
|
+
<td class="lct-num"><%= number(row.calls) %></td>
|
|
40
|
+
<td class="lct-num"><%= number(row.total_tokens) %></td>
|
|
41
|
+
<% avg_latency = row.average_latency_ms %>
|
|
42
|
+
<td class="lct-num<%= ' lct-num-muted' if avg_latency.nil? %>"><%= avg_latency ? "#{number(avg_latency.round)}ms" : "n/a" %></td>
|
|
43
|
+
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
44
|
+
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
45
|
+
<td class="lct-num"><%= link_to "Calls", calls_path(calls_query_for_model(provider: row.provider, model: row.model)), class: "lct-page-link" %></td>
|
|
49
46
|
</tr>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<tr>
|
|
54
|
-
<td><%= row.provider %></td>
|
|
55
|
-
<td><code class="lct-code"><%= row.model %></code></td>
|
|
56
|
-
<td class="lct-num"><%= number(row.calls) %></td>
|
|
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>
|
|
60
|
-
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
61
|
-
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
62
|
-
<% average_latency_ms = row.average_latency_ms %>
|
|
63
|
-
<td class="lct-num<%= ' lct-num-muted' if average_latency_ms.nil? %>"><%= average_latency_ms ? "#{number(average_latency_ms.round)}ms" : "n/a" %></td>
|
|
64
|
-
<td><%= link_to "Calls", calls_path(calls_query_for_model(provider: row.provider, model: row.model)), class: "lct-button lct-button-secondary lct-button-compact" %></td>
|
|
65
|
-
</tr>
|
|
66
|
-
<% end %>
|
|
67
|
-
</tbody>
|
|
68
|
-
</table>
|
|
69
|
-
</div>
|
|
47
|
+
<% end %>
|
|
48
|
+
</tbody>
|
|
49
|
+
</table>
|
|
70
50
|
</section>
|
|
71
51
|
<% end %>
|
|
@@ -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(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(@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 %>
|