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
|
@@ -1,195 +1,152 @@
|
|
|
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_with_delimiter(@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
|
-
|
|
23
|
-
<p class="lct-stat-
|
|
24
|
-
<p class="lct-stat-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<article class="lct-stat">
|
|
28
|
-
<p class="lct-stat-label">Unknown pricing</p>
|
|
29
|
-
<p class="lct-stat-value"><%= number(@summary.unknown_pricing_count) %></p>
|
|
30
|
-
<p class="lct-stat-sub"><%= percent(@summary.unknown_pricing_share) %> of calls</p>
|
|
31
|
-
</article>
|
|
32
|
-
|
|
33
|
-
<article class="lct-stat">
|
|
34
|
-
<p class="lct-stat-label">Calls without tags</p>
|
|
35
|
-
<p class="lct-stat-value"><%= number(@summary.untagged_calls_count) %></p>
|
|
36
|
-
<p class="lct-stat-sub"><%= percent(@summary.untagged_share) %> of calls</p>
|
|
37
|
-
</article>
|
|
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
|
+
<p class="lct-stat-value"><%= number_with_delimiter(@summary.total) %></p>
|
|
24
|
+
<p class="lct-stat-foot">in current slice</p>
|
|
25
|
+
</div>
|
|
38
26
|
|
|
39
|
-
<
|
|
40
|
-
<p class="lct-stat-label">
|
|
41
|
-
<p class="lct-stat-value"><%=
|
|
42
|
-
<p class="lct-stat-
|
|
43
|
-
</
|
|
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_with_delimiter(@summary.streaming_count) %></p>
|
|
30
|
+
<p class="lct-stat-foot"><%= percent(@summary.streaming_share) %> of calls</p>
|
|
31
|
+
</div>
|
|
44
32
|
|
|
45
|
-
<
|
|
46
|
-
<p class="lct-stat-label">
|
|
47
|
-
<p class="lct-stat-value"><%=
|
|
48
|
-
<p class="lct-stat-
|
|
49
|
-
</
|
|
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_with_delimiter(@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>
|
|
50
39
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<% end %>
|
|
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? %>
|
|
58
46
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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_with_delimiter(incomplete_pricing) %></p>
|
|
54
|
+
<p class="lct-stat-foot"><%= percent(@summary.unknown_pricing_share) %> of calls</p>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</section>
|
|
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_with_delimiter(untagged) %></p>
|
|
62
|
+
<p class="lct-stat-foot"><%= percent(@summary.untagged_share) %> of calls</p>
|
|
63
|
+
</div>
|
|
64
|
+
<% end %>
|
|
73
65
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
<p class="lct-section-copy">Use these fixes to improve the trustworthiness of downstream cost reports.</p>
|
|
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_with_delimiter(missing_latency) %></p>
|
|
70
|
+
<p class="lct-stat-foot"><%= percent(@summary.missing_latency_share) %> of calls</p>
|
|
80
71
|
</div>
|
|
81
|
-
|
|
72
|
+
<% end %>
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
<
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</thead>
|
|
91
|
-
<tbody>
|
|
92
|
-
<tr>
|
|
93
|
-
<td>Unknown pricing</td>
|
|
94
|
-
<td>Cost stays <code class="lct-code">nil</code>, so totals undercount.</td>
|
|
95
|
-
<td>Update <code class="lct-code">pricing_overrides</code> or <code class="lct-code">prices_file</code>.</td>
|
|
96
|
-
</tr>
|
|
97
|
-
<tr>
|
|
98
|
-
<td>Missing tags</td>
|
|
99
|
-
<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>
|
|
101
|
-
</tr>
|
|
102
|
-
<tr>
|
|
103
|
-
<td>Missing latency</td>
|
|
104
|
-
<td>Slow requests become harder to isolate on the calls page.</td>
|
|
105
|
-
<td>Make sure latency capture is enabled on every tracked request.</td>
|
|
106
|
-
</tr>
|
|
107
|
-
<% if @summary.streaming_missing_usage.positive? %>
|
|
108
|
-
<tr>
|
|
109
|
-
<td>Streams without usage</td>
|
|
110
|
-
<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>
|
|
112
|
-
</tr>
|
|
113
|
-
<% end %>
|
|
114
|
-
<% if @summary.missing_provider_response_id_count.positive? %>
|
|
115
|
-
<tr>
|
|
116
|
-
<td>Missing provider response IDs</td>
|
|
117
|
-
<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>
|
|
119
|
-
</tr>
|
|
120
|
-
<% end %>
|
|
121
|
-
</tbody>
|
|
122
|
-
</table>
|
|
123
|
-
</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_with_delimiter(streams_missing_usage) %></p>
|
|
78
|
+
<p class="lct-stat-foot"><%= percent(@summary.streaming_missing_usage_share) %> of streams</p>
|
|
79
|
+
</div>
|
|
80
|
+
<% end %>
|
|
124
81
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
<p class="lct-
|
|
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_with_delimiter(@hidden_output_summary.fetch(:hidden_output_tokens)) %> of <%= number_with_delimiter(@hidden_output_summary.fetch(:output_tokens)) %> output tokens</p>
|
|
130
87
|
</div>
|
|
131
|
-
|
|
88
|
+
<% end %>
|
|
89
|
+
</div>
|
|
90
|
+
<% end %>
|
|
132
91
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<tr>
|
|
136
|
-
<th>Dimension</th>
|
|
137
|
-
<th class="lct-num">Coverage</th>
|
|
138
|
-
<th class="lct-num">Calls with data</th>
|
|
139
|
-
<th>Visual</th>
|
|
140
|
-
</tr>
|
|
141
|
-
</thead>
|
|
142
|
-
<tbody>
|
|
143
|
-
<tr>
|
|
144
|
-
<td>Cost (pricing known)</td>
|
|
145
|
-
<td class="lct-num"><%= percent(@summary.cost_coverage) %></td>
|
|
146
|
-
<td class="lct-num"><%= number(@summary.calls_with_pricing) %></td>
|
|
147
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.cost_coverage, max: 100.0 %></td>
|
|
148
|
-
</tr>
|
|
92
|
+
<section class="lct-panel">
|
|
93
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Coverage summary</h2></div>
|
|
149
94
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
</
|
|
95
|
+
<table class="lct-tbl">
|
|
96
|
+
<thead>
|
|
97
|
+
<tr>
|
|
98
|
+
<th>Dimension</th>
|
|
99
|
+
<th class="lct-num">Coverage</th>
|
|
100
|
+
<th class="lct-num">Calls with data</th>
|
|
101
|
+
<th>Visual</th>
|
|
102
|
+
</tr>
|
|
103
|
+
</thead>
|
|
104
|
+
<tbody>
|
|
105
|
+
<tr>
|
|
106
|
+
<td>Cost (pricing known)</td>
|
|
107
|
+
<td class="lct-num"><%= percent(@summary.cost_coverage) %></td>
|
|
108
|
+
<td class="lct-num"><%= number_with_delimiter(@summary.calls_with_pricing) %></td>
|
|
109
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.cost_coverage, max: 100.0 %></td>
|
|
110
|
+
</tr>
|
|
156
111
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
112
|
+
<tr>
|
|
113
|
+
<td>Tags (at least one tag)</td>
|
|
114
|
+
<td class="lct-num"><%= percent(@summary.tag_coverage) %></td>
|
|
115
|
+
<td class="lct-num"><%= number_with_delimiter(@summary.tagged_calls) %></td>
|
|
116
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.tag_coverage, max: 100.0 %></td>
|
|
117
|
+
</tr>
|
|
163
118
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
</tr>
|
|
171
|
-
<% end %>
|
|
119
|
+
<tr>
|
|
120
|
+
<td>Latency</td>
|
|
121
|
+
<td class="lct-num"><%= percent(@summary.latency_coverage) %></td>
|
|
122
|
+
<td class="lct-num"><%= number_with_delimiter(@summary.calls_with_latency) %></td>
|
|
123
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.latency_coverage, max: 100.0 %></td>
|
|
124
|
+
</tr>
|
|
172
125
|
|
|
126
|
+
<% if @summary.streaming_count.positive? %>
|
|
173
127
|
<tr>
|
|
174
|
-
<td>
|
|
175
|
-
<td class="lct-num"><%= percent(@summary.
|
|
176
|
-
<td class="lct-num"><%=
|
|
177
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.
|
|
128
|
+
<td>Streaming usage captured</td>
|
|
129
|
+
<td class="lct-num"><%= percent(@summary.stream_coverage) %></td>
|
|
130
|
+
<td class="lct-num"><%= number_with_delimiter(@summary.streams_with_usage) %> / <%= number_with_delimiter(@summary.streaming_count) %></td>
|
|
131
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.stream_coverage, max: 100.0 %></td>
|
|
178
132
|
</tr>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
133
|
+
<% end %>
|
|
134
|
+
|
|
135
|
+
<tr>
|
|
136
|
+
<td>Provider response ID</td>
|
|
137
|
+
<td class="lct-num"><%= percent(@summary.provider_response_id_coverage) %></td>
|
|
138
|
+
<td class="lct-num"><%= number_with_delimiter(@summary.calls_with_provider_response_id) %></td>
|
|
139
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.provider_response_id_coverage, max: 100.0 %></td>
|
|
140
|
+
</tr>
|
|
141
|
+
</tbody>
|
|
142
|
+
</table>
|
|
182
143
|
</section>
|
|
183
144
|
|
|
184
145
|
<section class="lct-panel">
|
|
185
|
-
<div class="lct-
|
|
186
|
-
<div>
|
|
187
|
-
<h2 class="lct-section-title">Token usage</h2>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
146
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Token usage</h2></div>
|
|
190
147
|
|
|
191
148
|
<div class="lct-table-wrap">
|
|
192
|
-
<table class="lct-
|
|
149
|
+
<table class="lct-tbl">
|
|
193
150
|
<thead>
|
|
194
151
|
<tr>
|
|
195
152
|
<th>Bucket</th>
|
|
@@ -205,7 +162,7 @@
|
|
|
205
162
|
<% cost_value = row.fetch(:cost_value) %>
|
|
206
163
|
<tr>
|
|
207
164
|
<td><%= LlmCostTracker::TokenUsageHelper::QUALITY_LABELS.fetch(token_key) %></td>
|
|
208
|
-
<td class="lct-num"><%=
|
|
165
|
+
<td class="lct-num"><%= number_with_delimiter(row.fetch(:token_value)) %></td>
|
|
209
166
|
<% if row.fetch(:share_basis) == :output %>
|
|
210
167
|
<td class="lct-num"><%= percent(row.fetch(:share_percent)) %> of output</td>
|
|
211
168
|
<% else %>
|
|
@@ -223,14 +180,10 @@
|
|
|
223
180
|
|
|
224
181
|
<% if @service_charge_rows.any? %>
|
|
225
182
|
<section class="lct-panel">
|
|
226
|
-
<div class="lct-
|
|
227
|
-
<div>
|
|
228
|
-
<h2 class="lct-section-title">Service charges</h2>
|
|
229
|
-
</div>
|
|
230
|
-
</div>
|
|
183
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Service charges</h2></div>
|
|
231
184
|
|
|
232
185
|
<div class="lct-table-wrap">
|
|
233
|
-
<table class="lct-
|
|
186
|
+
<table class="lct-tbl">
|
|
234
187
|
<thead>
|
|
235
188
|
<tr>
|
|
236
189
|
<th>Provider</th>
|
|
@@ -243,13 +196,13 @@
|
|
|
243
196
|
</thead>
|
|
244
197
|
<tbody>
|
|
245
198
|
<% @service_charge_rows.each do |row| %>
|
|
246
|
-
<% unknown_cost = row.cost_status.to_s == LlmCostTracker::
|
|
199
|
+
<% unknown_cost = row.cost_status.to_s == LlmCostTracker::Charges::CostStatus::UNKNOWN %>
|
|
247
200
|
<tr>
|
|
248
|
-
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
249
|
-
<td><code class="lct-code"><%= row.component %></code></td>
|
|
201
|
+
<td><code class="lct-code-id"><%= row.provider %></code></td>
|
|
202
|
+
<td><code class="lct-code-id"><%= row.component %></code></td>
|
|
250
203
|
<td><%= row.cost_status %></td>
|
|
251
|
-
<td class="lct-num"><%=
|
|
252
|
-
<td class="lct-num"><%=
|
|
204
|
+
<td class="lct-num"><%= number_with_delimiter(row.charges_count) %></td>
|
|
205
|
+
<td class="lct-num"><%= number_with_delimiter(row.quantity) %></td>
|
|
253
206
|
<td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(row.total_cost) %></td>
|
|
254
207
|
</tr>
|
|
255
208
|
<% end %>
|
|
@@ -261,15 +214,10 @@
|
|
|
261
214
|
|
|
262
215
|
<% if @streaming_health_rows.any? %>
|
|
263
216
|
<section class="lct-panel">
|
|
264
|
-
<div class="lct-
|
|
265
|
-
<div>
|
|
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>
|
|
217
|
+
<div class="lct-panel-head"><h2 class="lct-panel-title">Streaming health by provider</h2></div>
|
|
270
218
|
|
|
271
219
|
<div class="lct-table-wrap">
|
|
272
|
-
<table class="lct-
|
|
220
|
+
<table class="lct-tbl">
|
|
273
221
|
<thead>
|
|
274
222
|
<tr>
|
|
275
223
|
<th>Provider</th>
|
|
@@ -282,10 +230,10 @@
|
|
|
282
230
|
<tbody>
|
|
283
231
|
<% @streaming_health_rows.each do |row| %>
|
|
284
232
|
<tr>
|
|
285
|
-
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
286
|
-
<td class="lct-num"><%=
|
|
287
|
-
<td class="lct-num"><%=
|
|
288
|
-
<td class="lct-num"><%=
|
|
233
|
+
<td><code class="lct-code-id"><%= row.provider %></code></td>
|
|
234
|
+
<td class="lct-num"><%= number_with_delimiter(row.streams) %></td>
|
|
235
|
+
<td class="lct-num"><%= number_with_delimiter(row.with_usage) %></td>
|
|
236
|
+
<td class="lct-num"><%= number_with_delimiter(row.unknown) %></td>
|
|
289
237
|
<td class="lct-num"><%= percent(row.unknown_share) %></td>
|
|
290
238
|
</tr>
|
|
291
239
|
<% end %>
|
|
@@ -297,16 +245,14 @@
|
|
|
297
245
|
|
|
298
246
|
<% unless @unknown_pricing_by_model.empty? %>
|
|
299
247
|
<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. After the next price refresh or a <code class="lct-code">pricing_overrides</code> update, run <code class="lct-code">bin/rails llm_cost_tracker:backfill_unknown_pricing</code> to recompute these calls.</p>
|
|
304
|
-
</div>
|
|
305
|
-
<%= link_to "Calls", calls_path(current_query(sort: "unknown_pricing")), class: "lct-button lct-button-secondary" %>
|
|
248
|
+
<div class="lct-panel-head">
|
|
249
|
+
<h2 class="lct-panel-title">Incomplete pricing by model</h2>
|
|
250
|
+
<span class="lct-panel-meta"><%= link_to "View calls →", calls_path(current_query(cost_status: "incomplete", sort: nil)) %></span>
|
|
306
251
|
</div>
|
|
252
|
+
<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
253
|
|
|
308
254
|
<div class="lct-table-wrap">
|
|
309
|
-
<table class="lct-
|
|
255
|
+
<table class="lct-tbl">
|
|
310
256
|
<thead>
|
|
311
257
|
<tr>
|
|
312
258
|
<th>Provider</th>
|
|
@@ -318,9 +264,9 @@
|
|
|
318
264
|
<tbody>
|
|
319
265
|
<% @unknown_pricing_by_model.each do |row| %>
|
|
320
266
|
<tr>
|
|
321
|
-
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
322
|
-
<td><code class="lct-code"><%= row.model %></code></td>
|
|
323
|
-
<td class="lct-num"><%=
|
|
267
|
+
<td><code class="lct-code-id"><%= row.provider %></code></td>
|
|
268
|
+
<td><code class="lct-code-id"><%= row.model %></code></td>
|
|
269
|
+
<td class="lct-num"><%= number_with_delimiter(row.calls) %></td>
|
|
324
270
|
<td class="lct-num"><%= percent(row.share_percent) %></td>
|
|
325
271
|
</tr>
|
|
326
272
|
<% end %>
|
|
@@ -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_with_delimiter(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, default: 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_with_delimiter(row.calls) %></td>
|
|
40
|
+
<td class="lct-num"><%= number_with_delimiter(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_with_delimiter(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 %>
|