llm_cost_tracker 0.7.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +173 -0
- data/README.md +60 -220
- data/app/assets/llm_cost_tracker/application.css +282 -45
- data/app/controllers/llm_cost_tracker/application_controller.rb +25 -20
- data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +22 -19
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +14 -2
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +106 -0
- data/app/controllers/llm_cost_tracker/tags_controller.rb +15 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +18 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
- data/app/helpers/llm_cost_tracker/inline_style_helper.rb +28 -0
- data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +13 -0
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +24 -7
- data/app/models/llm_cost_tracker/call.rb +166 -0
- data/app/models/llm_cost_tracker/call_line_item.rb +18 -0
- data/app/models/llm_cost_tracker/call_rollup.rb +6 -0
- data/app/models/llm_cost_tracker/call_tag.rb +12 -0
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +9 -0
- data/app/models/llm_cost_tracker/ingestion/lease.rb +0 -3
- data/app/models/llm_cost_tracker/provider_invoice.rb +13 -0
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +24 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +152 -32
- data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +8 -6
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
- data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
- data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
- data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -1
- data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
- data/app/views/llm_cost_tracker/calls/show.html.erb +73 -33
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +16 -57
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +183 -167
- data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
- data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +183 -0
- data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +66 -0
- data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
- data/app/views/llm_cost_tracker/tags/show.html.erb +64 -36
- data/config/routes.rb +3 -2
- data/lib/llm_cost_tracker/billing/components.rb +95 -0
- data/lib/llm_cost_tracker/billing/components.yml +188 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
- data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
- data/lib/llm_cost_tracker/budget.rb +26 -36
- data/lib/llm_cost_tracker/capture/stream_collector.rb +125 -38
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +40 -5
- data/lib/llm_cost_tracker/configuration.rb +86 -17
- data/lib/llm_cost_tracker/dashboard_setup_state.rb +109 -0
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +56 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +48 -30
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +164 -0
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
- data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
- data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +34 -0
- data/lib/llm_cost_tracker/doctor.rb +111 -44
- data/lib/llm_cost_tracker/engine.rb +9 -0
- data/lib/llm_cost_tracker/errors.rb +5 -19
- data/lib/llm_cost_tracker/event.rb +11 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +43 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/durable_ingestion_generator.rb +43 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +17 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +34 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +15 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +104 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_durable_ingestion.rb.erb +29 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +55 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +28 -25
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +20 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +32 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +18 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +38 -0
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/{add_provider_response_id_generator.rb → upgrade_call_tags_key_value_index_generator.rb} +5 -4
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/{add_streaming_generator.rb → upgrade_image_tokens_generator.rb} +4 -4
- data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
- data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -24
- data/lib/llm_cost_tracker/ingestion/inline.rb +22 -0
- data/lib/llm_cost_tracker/ingestion/worker.rb +24 -7
- data/lib/llm_cost_tracker/ingestion.rb +66 -22
- data/lib/llm_cost_tracker/integrations/anthropic.rb +68 -42
- data/lib/llm_cost_tracker/integrations/base.rb +56 -32
- data/lib/llm_cost_tracker/integrations/openai.rb +342 -63
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +110 -11
- data/lib/llm_cost_tracker/integrations.rb +21 -3
- data/lib/llm_cost_tracker/ledger/period/totals.rb +30 -11
- data/lib/llm_cost_tracker/ledger/period.rb +5 -5
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +2 -2
- data/lib/llm_cost_tracker/ledger/rollups.rb +90 -25
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +79 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +41 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +36 -23
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +47 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +42 -0
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +46 -0
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
- data/lib/llm_cost_tracker/ledger/store.rb +103 -20
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +37 -0
- data/lib/llm_cost_tracker/ledger/tags/query.rb +6 -11
- data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -15
- data/lib/llm_cost_tracker/ledger.rb +5 -2
- data/lib/llm_cost_tracker/logging.rb +2 -5
- data/lib/llm_cost_tracker/masking.rb +39 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +95 -35
- data/lib/llm_cost_tracker/parsers/anthropic.rb +74 -14
- data/lib/llm_cost_tracker/parsers/base.rb +13 -4
- data/lib/llm_cost_tracker/parsers/gemini.rb +105 -15
- data/lib/llm_cost_tracker/parsers/openai.rb +16 -2
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +15 -3
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +126 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +157 -59
- data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
- data/lib/llm_cost_tracker/parsers.rb +1 -1
- data/lib/llm_cost_tracker/prices.json +198 -22
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +28 -21
- data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
- data/lib/llm_cost_tracker/pricing/lookup.rb +73 -36
- data/lib/llm_cost_tracker/pricing/mode.rb +76 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +67 -45
- data/lib/llm_cost_tracker/pricing/service_charges.rb +210 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +50 -1
- data/lib/llm_cost_tracker/pricing/sync.rb +59 -10
- data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
- data/lib/llm_cost_tracker/pricing.rb +220 -28
- data/lib/llm_cost_tracker/railtie.rb +6 -8
- data/lib/llm_cost_tracker/reconcile_tasks.rb +134 -0
- data/lib/llm_cost_tracker/reconciliation/diff.rb +428 -0
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +48 -0
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +19 -0
- data/lib/llm_cost_tracker/reconciliation/importer.rb +253 -0
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +171 -0
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +20 -0
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +142 -0
- data/lib/llm_cost_tracker/reconciliation.rb +118 -0
- data/lib/llm_cost_tracker/report/data.rb +19 -8
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +22 -9
- data/lib/llm_cost_tracker/tags/context.rb +2 -5
- data/lib/llm_cost_tracker/tags/key.rb +4 -0
- data/lib/llm_cost_tracker/tags/sanitizer.rb +71 -20
- data/lib/llm_cost_tracker/timing.rb +15 -0
- data/lib/llm_cost_tracker/token_usage.rb +64 -42
- data/lib/llm_cost_tracker/tracker.rb +97 -27
- data/lib/llm_cost_tracker/usage_capture.rb +29 -8
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +45 -35
- data/lib/tasks/llm_cost_tracker.rake +45 -17
- metadata +71 -41
- data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
- data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
- data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
- data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
- data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
- data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
- data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
- data/lib/llm_cost_tracker/pricing/components.rb +0 -37
- data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +0 -63
|
@@ -1,64 +1,14 @@
|
|
|
1
|
-
<% total = @stats.total_calls.to_i %>
|
|
2
|
-
<% unknown_pricing_count = @stats.unknown_pricing_count.to_i %>
|
|
3
|
-
<% untagged_calls_count = @stats.untagged_calls_count.to_i %>
|
|
4
|
-
<% missing_latency_count = @stats.missing_latency_count&.to_i %>
|
|
5
|
-
<% streaming_count = @stats.streaming_count&.to_i %>
|
|
6
|
-
<% streaming_missing_usage = @stats.streaming_missing_usage_count&.to_i %>
|
|
7
|
-
<% missing_provider_response_id_count = @stats.missing_provider_response_id_count&.to_i %>
|
|
8
|
-
<% calls_with_provider_response_id = total - missing_provider_response_id_count %>
|
|
9
|
-
|
|
10
1
|
<section class="lct-panel lct-toolbar">
|
|
11
2
|
<div class="lct-toolbar-head">
|
|
12
3
|
<h2 class="lct-section-title">Data Quality</h2>
|
|
13
4
|
</div>
|
|
14
5
|
|
|
15
|
-
|
|
16
|
-
<div class="lct-filter-row lct-filter-row-basic">
|
|
17
|
-
<div class="lct-field">
|
|
18
|
-
<label for="lct-quality-from">From</label>
|
|
19
|
-
<input id="lct-quality-from" type="date" name="from" value="<%= params[:from] %>">
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
<div class="lct-field">
|
|
23
|
-
<label for="lct-quality-to">To</label>
|
|
24
|
-
<input id="lct-quality-to" type="date" name="to" value="<%= params[:to] %>">
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<div class="lct-field">
|
|
28
|
-
<label for="lct-quality-provider">Provider</label>
|
|
29
|
-
<%= select_tag :provider,
|
|
30
|
-
options_for_select(provider_filter_options, params[:provider]),
|
|
31
|
-
include_blank: "All providers",
|
|
32
|
-
id: "lct-quality-provider" %>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<div class="lct-field">
|
|
36
|
-
<label for="lct-quality-model">Model</label>
|
|
37
|
-
<%= select_tag :model,
|
|
38
|
-
options_for_select(model_filter_options, params[:model]),
|
|
39
|
-
include_blank: "All models",
|
|
40
|
-
id: "lct-quality-model" %>
|
|
41
|
-
</div>
|
|
42
|
-
|
|
43
|
-
<div class="lct-field">
|
|
44
|
-
<label for="lct-quality-stream">Stream</label>
|
|
45
|
-
<%= select_tag :stream,
|
|
46
|
-
options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, params[:stream]),
|
|
47
|
-
include_blank: "All calls",
|
|
48
|
-
id: "lct-quality-stream" %>
|
|
49
|
-
</div>
|
|
50
|
-
|
|
51
|
-
<div class="lct-filter-actions">
|
|
52
|
-
<button class="lct-button" type="submit">Apply</button>
|
|
53
|
-
<%= link_to("Reset", data_quality_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
</form>
|
|
6
|
+
<%= render "llm_cost_tracker/shared/filters", path: data_quality_path %>
|
|
57
7
|
|
|
58
8
|
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: data_quality_path %>
|
|
59
9
|
</section>
|
|
60
10
|
|
|
61
|
-
<% if total.zero? %>
|
|
11
|
+
<% if @summary.total.zero? %>
|
|
62
12
|
<section class="lct-panel lct-empty">
|
|
63
13
|
<h2 class="lct-state-title">No data yet</h2>
|
|
64
14
|
<p class="lct-state-copy">Quality metrics will appear here once calls are recorded in the current slice.</p>
|
|
@@ -67,125 +17,107 @@
|
|
|
67
17
|
</div>
|
|
68
18
|
</section>
|
|
69
19
|
<% else %>
|
|
70
|
-
<section class="lct-
|
|
71
|
-
<article class="lct-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</div>
|
|
20
|
+
<section class="lct-stat-grid lct-stat-grid-spaced">
|
|
21
|
+
<article class="lct-stat">
|
|
22
|
+
<p class="lct-stat-label">Calls inspected</p>
|
|
23
|
+
<p class="lct-stat-value"><%= number(@summary.total) %></p>
|
|
24
|
+
<p class="lct-stat-sub">in current slice</p>
|
|
76
25
|
</article>
|
|
77
26
|
|
|
78
|
-
<
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<p class="lct-stat-sub"><%= percent(coverage_percent(unknown_pricing_count, total)) %> of calls</p>
|
|
84
|
-
</article>
|
|
27
|
+
<article class="lct-stat">
|
|
28
|
+
<p class="lct-stat-label">Unknown pricing</p>
|
|
29
|
+
<p class="lct-stat-value"><%= number(@summary.unknown_pricing_count) %></p>
|
|
30
|
+
<p class="lct-stat-sub"><%= percent(@summary.unknown_pricing_share) %> of calls</p>
|
|
31
|
+
</article>
|
|
85
32
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
33
|
+
<article class="lct-stat">
|
|
34
|
+
<p class="lct-stat-label">Calls without tags</p>
|
|
35
|
+
<p class="lct-stat-value"><%= number(@summary.untagged_calls_count) %></p>
|
|
36
|
+
<p class="lct-stat-sub"><%= percent(@summary.untagged_share) %> of calls</p>
|
|
37
|
+
</article>
|
|
91
38
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
39
|
+
<article class="lct-stat">
|
|
40
|
+
<p class="lct-stat-label">Missing latency</p>
|
|
41
|
+
<p class="lct-stat-value"><%= number(@summary.missing_latency_count) %></p>
|
|
42
|
+
<p class="lct-stat-sub"><%= percent(@summary.missing_latency_share) %> of calls</p>
|
|
43
|
+
</article>
|
|
97
44
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
45
|
+
<article class="lct-stat">
|
|
46
|
+
<p class="lct-stat-label">Streaming calls</p>
|
|
47
|
+
<p class="lct-stat-value"><%= number(@summary.streaming_count) %></p>
|
|
48
|
+
<p class="lct-stat-sub"><%= percent(@summary.streaming_share) %> of calls</p>
|
|
49
|
+
</article>
|
|
103
50
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
51
|
+
<% if @summary.streaming_count.positive? %>
|
|
52
|
+
<article class="lct-stat">
|
|
53
|
+
<p class="lct-stat-label">Streams without usage</p>
|
|
54
|
+
<p class="lct-stat-value"><%= number(@summary.streaming_missing_usage) %></p>
|
|
55
|
+
<p class="lct-stat-sub"><%= percent(@summary.streaming_missing_usage_share) %> of streams</p>
|
|
56
|
+
</article>
|
|
57
|
+
<% end %>
|
|
111
58
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
59
|
+
<article class="lct-stat">
|
|
60
|
+
<p class="lct-stat-label">Calls with provider response ID</p>
|
|
61
|
+
<p class="lct-stat-value"><%= number(@summary.calls_with_provider_response_id) %></p>
|
|
62
|
+
<p class="lct-stat-sub"><%= percent(@summary.provider_response_id_coverage) %> of calls</p>
|
|
63
|
+
</article>
|
|
117
64
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
65
|
+
<% if @hidden_output_summary %>
|
|
66
|
+
<article class="lct-stat">
|
|
67
|
+
<p class="lct-stat-label">Hidden output share</p>
|
|
68
|
+
<p class="lct-stat-value"><%= percent(@hidden_output_summary.fetch(:share_percent)) %></p>
|
|
69
|
+
<p class="lct-stat-sub"><%= number(@hidden_output_summary.fetch(:hidden_output_tokens)) %> of <%= number(@hidden_output_summary.fetch(:output_tokens)) %> output tokens</p>
|
|
70
|
+
</article>
|
|
71
|
+
<% end %>
|
|
127
72
|
</section>
|
|
128
73
|
|
|
129
74
|
<section class="lct-grid lct-two-col">
|
|
130
75
|
<section class="lct-panel">
|
|
131
76
|
<div class="lct-section-head">
|
|
132
77
|
<div>
|
|
133
|
-
<h2 class="lct-section-title">
|
|
134
|
-
<p class="lct-section-copy">
|
|
78
|
+
<h2 class="lct-section-title">Next actions</h2>
|
|
79
|
+
<p class="lct-section-copy">Use these fixes to improve the trustworthiness of downstream cost reports.</p>
|
|
135
80
|
</div>
|
|
136
81
|
</div>
|
|
137
82
|
|
|
138
83
|
<table class="lct-table lct-table-compact">
|
|
139
84
|
<thead>
|
|
140
85
|
<tr>
|
|
141
|
-
<th>
|
|
142
|
-
<th
|
|
143
|
-
<th
|
|
144
|
-
<th>Visual</th>
|
|
86
|
+
<th>Issue</th>
|
|
87
|
+
<th>Why it matters</th>
|
|
88
|
+
<th>Suggested action</th>
|
|
145
89
|
</tr>
|
|
146
90
|
</thead>
|
|
147
91
|
<tbody>
|
|
148
|
-
<% cost_coverage = coverage_percent(total - unknown_pricing_count, total) %>
|
|
149
92
|
<tr>
|
|
150
|
-
<td>
|
|
151
|
-
<td class="lct-
|
|
152
|
-
<td class="lct-
|
|
153
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: cost_coverage, max: 100.0 %></td>
|
|
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>
|
|
154
96
|
</tr>
|
|
155
|
-
|
|
156
|
-
<% tag_coverage = coverage_percent(total - untagged_calls_count, total) %>
|
|
157
97
|
<tr>
|
|
158
|
-
<td>
|
|
159
|
-
<td
|
|
160
|
-
<td class="lct-
|
|
161
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: tag_coverage, max: 100.0 %></td>
|
|
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>
|
|
162
101
|
</tr>
|
|
163
|
-
|
|
164
|
-
<% latency_coverage = coverage_percent(total - missing_latency_count, total) %>
|
|
165
102
|
<tr>
|
|
166
|
-
<td>
|
|
167
|
-
<td
|
|
168
|
-
<td
|
|
169
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: latency_coverage, max: 100.0 %></td>
|
|
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>
|
|
170
106
|
</tr>
|
|
171
|
-
|
|
172
|
-
<% if streaming_count.to_i.positive? %>
|
|
173
|
-
<% stream_coverage = coverage_percent(streaming_count - streaming_missing_usage, streaming_count) %>
|
|
107
|
+
<% if @summary.streaming_missing_usage.positive? %>
|
|
174
108
|
<tr>
|
|
175
|
-
<td>
|
|
176
|
-
<td
|
|
177
|
-
<td class="lct-
|
|
178
|
-
|
|
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>
|
|
179
119
|
</tr>
|
|
180
120
|
<% end %>
|
|
181
|
-
|
|
182
|
-
<% provider_response_id_coverage = coverage_percent(calls_with_provider_response_id, total) %>
|
|
183
|
-
<tr>
|
|
184
|
-
<td>Provider response ID</td>
|
|
185
|
-
<td class="lct-num"><%= percent(provider_response_id_coverage) %></td>
|
|
186
|
-
<td class="lct-num"><%= number(calls_with_provider_response_id) %></td>
|
|
187
|
-
<td><%= render "llm_cost_tracker/shared/bar", value: provider_response_id_coverage, max: 100.0 %></td>
|
|
188
|
-
</tr>
|
|
189
121
|
</tbody>
|
|
190
122
|
</table>
|
|
191
123
|
</section>
|
|
@@ -193,49 +125,57 @@
|
|
|
193
125
|
<section class="lct-panel">
|
|
194
126
|
<div class="lct-section-head">
|
|
195
127
|
<div>
|
|
196
|
-
<h2 class="lct-section-title">
|
|
197
|
-
<p class="lct-section-copy">
|
|
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>
|
|
198
130
|
</div>
|
|
199
131
|
</div>
|
|
200
132
|
|
|
201
133
|
<table class="lct-table lct-table-compact">
|
|
202
134
|
<thead>
|
|
203
135
|
<tr>
|
|
204
|
-
<th>
|
|
205
|
-
<th>
|
|
206
|
-
<th>
|
|
136
|
+
<th>Dimension</th>
|
|
137
|
+
<th class="lct-num">Coverage</th>
|
|
138
|
+
<th class="lct-num">Calls with data</th>
|
|
139
|
+
<th>Visual</th>
|
|
207
140
|
</tr>
|
|
208
141
|
</thead>
|
|
209
142
|
<tbody>
|
|
210
143
|
<tr>
|
|
211
|
-
<td>
|
|
212
|
-
<td
|
|
213
|
-
<td
|
|
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>
|
|
214
148
|
</tr>
|
|
149
|
+
|
|
215
150
|
<tr>
|
|
216
|
-
<td>
|
|
217
|
-
<td
|
|
218
|
-
<td
|
|
151
|
+
<td>Tags (at least one tag)</td>
|
|
152
|
+
<td class="lct-num"><%= percent(@summary.tag_coverage) %></td>
|
|
153
|
+
<td class="lct-num"><%= number(@summary.tagged_calls) %></td>
|
|
154
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.tag_coverage, max: 100.0 %></td>
|
|
219
155
|
</tr>
|
|
156
|
+
|
|
220
157
|
<tr>
|
|
221
|
-
<td>
|
|
222
|
-
<td
|
|
223
|
-
<td
|
|
158
|
+
<td>Latency</td>
|
|
159
|
+
<td class="lct-num"><%= percent(@summary.latency_coverage) %></td>
|
|
160
|
+
<td class="lct-num"><%= number(@summary.calls_with_latency) %></td>
|
|
161
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.latency_coverage, max: 100.0 %></td>
|
|
224
162
|
</tr>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
<td>Streams without usage</td>
|
|
228
|
-
<td>Token totals undercount when streaming responses drop the final usage event.</td>
|
|
229
|
-
<td>Send OpenAI requests with <code class="lct-code">stream_options: { include_usage: true }</code>, or wrap custom clients with <code class="lct-code">LlmCostTracker.track_stream</code>.</td>
|
|
230
|
-
</tr>
|
|
231
|
-
<% end %>
|
|
232
|
-
<% if missing_provider_response_id_count.to_i.positive? %>
|
|
163
|
+
|
|
164
|
+
<% if @summary.streaming_count.positive? %>
|
|
233
165
|
<tr>
|
|
234
|
-
<td>
|
|
235
|
-
<td
|
|
236
|
-
<td
|
|
166
|
+
<td>Streaming usage captured</td>
|
|
167
|
+
<td class="lct-num"><%= percent(@summary.stream_coverage) %></td>
|
|
168
|
+
<td class="lct-num"><%= number(@summary.streams_with_usage) %> / <%= number(@summary.streaming_count) %></td>
|
|
169
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.stream_coverage, max: 100.0 %></td>
|
|
237
170
|
</tr>
|
|
238
171
|
<% end %>
|
|
172
|
+
|
|
173
|
+
<tr>
|
|
174
|
+
<td>Provider response ID</td>
|
|
175
|
+
<td class="lct-num"><%= percent(@summary.provider_response_id_coverage) %></td>
|
|
176
|
+
<td class="lct-num"><%= number(@summary.calls_with_provider_response_id) %></td>
|
|
177
|
+
<td><%= render "llm_cost_tracker/shared/bar", value: @summary.provider_response_id_coverage, max: 100.0 %></td>
|
|
178
|
+
</tr>
|
|
239
179
|
</tbody>
|
|
240
180
|
</table>
|
|
241
181
|
</section>
|
|
@@ -264,7 +204,7 @@
|
|
|
264
204
|
<% cost_key = row.fetch(:cost_key) %>
|
|
265
205
|
<% cost_value = row.fetch(:cost_value) %>
|
|
266
206
|
<tr>
|
|
267
|
-
<td><%=
|
|
207
|
+
<td><%= LlmCostTracker::TokenUsageHelper::QUALITY_LABELS.fetch(token_key) %></td>
|
|
268
208
|
<td class="lct-num"><%= number(row.fetch(:token_value)) %></td>
|
|
269
209
|
<% if row.fetch(:share_basis) == :output %>
|
|
270
210
|
<td class="lct-num"><%= percent(row.fetch(:share_percent)) %> of output</td>
|
|
@@ -281,6 +221,80 @@
|
|
|
281
221
|
</div>
|
|
282
222
|
</section>
|
|
283
223
|
|
|
224
|
+
<% if @service_charge_rows.any? %>
|
|
225
|
+
<section class="lct-panel">
|
|
226
|
+
<div class="lct-section-head">
|
|
227
|
+
<div>
|
|
228
|
+
<h2 class="lct-section-title">Service charges</h2>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="lct-table-wrap">
|
|
233
|
+
<table class="lct-table lct-table-compact">
|
|
234
|
+
<thead>
|
|
235
|
+
<tr>
|
|
236
|
+
<th>Provider</th>
|
|
237
|
+
<th>Component</th>
|
|
238
|
+
<th>Status</th>
|
|
239
|
+
<th class="lct-num">Rows</th>
|
|
240
|
+
<th class="lct-num">Quantity</th>
|
|
241
|
+
<th class="lct-num">Cost</th>
|
|
242
|
+
</tr>
|
|
243
|
+
</thead>
|
|
244
|
+
<tbody>
|
|
245
|
+
<% @service_charge_rows.each do |row| %>
|
|
246
|
+
<% unknown_cost = row.cost_status.to_s == LlmCostTracker::Billing::CostStatus::UNKNOWN %>
|
|
247
|
+
<tr>
|
|
248
|
+
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
249
|
+
<td><code class="lct-code"><%= row.component %></code></td>
|
|
250
|
+
<td><%= row.cost_status %></td>
|
|
251
|
+
<td class="lct-num"><%= number(row.charges_count) %></td>
|
|
252
|
+
<td class="lct-num"><%= number(row.quantity) %></td>
|
|
253
|
+
<td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(row.total_cost) %></td>
|
|
254
|
+
</tr>
|
|
255
|
+
<% end %>
|
|
256
|
+
</tbody>
|
|
257
|
+
</table>
|
|
258
|
+
</div>
|
|
259
|
+
</section>
|
|
260
|
+
<% end %>
|
|
261
|
+
|
|
262
|
+
<% if @streaming_health_rows.any? %>
|
|
263
|
+
<section class="lct-panel">
|
|
264
|
+
<div class="lct-section-head">
|
|
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>
|
|
270
|
+
|
|
271
|
+
<div class="lct-table-wrap">
|
|
272
|
+
<table class="lct-table lct-table-compact">
|
|
273
|
+
<thead>
|
|
274
|
+
<tr>
|
|
275
|
+
<th>Provider</th>
|
|
276
|
+
<th class="lct-num">Streams</th>
|
|
277
|
+
<th class="lct-num">With usage</th>
|
|
278
|
+
<th class="lct-num">Unknown</th>
|
|
279
|
+
<th class="lct-num">Unknown share</th>
|
|
280
|
+
</tr>
|
|
281
|
+
</thead>
|
|
282
|
+
<tbody>
|
|
283
|
+
<% @streaming_health_rows.each do |row| %>
|
|
284
|
+
<tr>
|
|
285
|
+
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
286
|
+
<td class="lct-num"><%= number(row.streams) %></td>
|
|
287
|
+
<td class="lct-num"><%= number(row.with_usage) %></td>
|
|
288
|
+
<td class="lct-num"><%= number(row.unknown) %></td>
|
|
289
|
+
<td class="lct-num"><%= percent(row.unknown_share) %></td>
|
|
290
|
+
</tr>
|
|
291
|
+
<% end %>
|
|
292
|
+
</tbody>
|
|
293
|
+
</table>
|
|
294
|
+
</div>
|
|
295
|
+
</section>
|
|
296
|
+
<% end %>
|
|
297
|
+
|
|
284
298
|
<% unless @unknown_pricing_by_model.empty? %>
|
|
285
299
|
<section class="lct-panel">
|
|
286
300
|
<div class="lct-section-head">
|
|
@@ -288,13 +302,14 @@
|
|
|
288
302
|
<h2 class="lct-section-title">Unknown pricing by model</h2>
|
|
289
303
|
<p class="lct-section-copy">These models have token counts but no configured rates, so cost stays unknown.</p>
|
|
290
304
|
</div>
|
|
291
|
-
<%= link_to "
|
|
305
|
+
<%= link_to "Calls", calls_path(current_query(sort: "unknown_pricing")), class: "lct-button lct-button-secondary" %>
|
|
292
306
|
</div>
|
|
293
307
|
|
|
294
308
|
<div class="lct-table-wrap">
|
|
295
309
|
<table class="lct-table lct-table-compact">
|
|
296
310
|
<thead>
|
|
297
311
|
<tr>
|
|
312
|
+
<th>Provider</th>
|
|
298
313
|
<th>Model</th>
|
|
299
314
|
<th class="lct-num">Calls without cost</th>
|
|
300
315
|
<th class="lct-num">Share of total</th>
|
|
@@ -303,9 +318,10 @@
|
|
|
303
318
|
<tbody>
|
|
304
319
|
<% @unknown_pricing_by_model.each do |row| %>
|
|
305
320
|
<tr>
|
|
321
|
+
<td><code class="lct-code"><%= row.provider %></code></td>
|
|
306
322
|
<td><code class="lct-code"><%= row.model %></code></td>
|
|
307
323
|
<td class="lct-num"><%= number(row.calls) %></td>
|
|
308
|
-
<td class="lct-num"><%= percent(
|
|
324
|
+
<td class="lct-num"><%= percent(row.share_percent) %></td>
|
|
309
325
|
</tr>
|
|
310
326
|
<% end %>
|
|
311
327
|
</tbody>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<section class="lct-panel lct-empty">
|
|
2
2
|
<h2 class="lct-state-title">Database unavailable</h2>
|
|
3
3
|
<p class="lct-state-copy">
|
|
4
|
-
llm_cost_tracker could not read the <span class="lct-code">
|
|
4
|
+
llm_cost_tracker could not read the <span class="lct-code">llm_cost_tracker_calls</span> table.
|
|
5
5
|
Check that ActiveRecord is connected, then run
|
|
6
6
|
<span class="lct-code">rails generate llm_cost_tracker:install</span> and migrate your database.
|
|
7
7
|
</p>
|
|
@@ -3,53 +3,9 @@
|
|
|
3
3
|
<h2 class="lct-section-title">Models</h2>
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<label for="lct-models-from">From</label>
|
|
10
|
-
<input id="lct-models-from" type="date" name="from" value="<%= params[:from] %>">
|
|
11
|
-
</div>
|
|
12
|
-
|
|
13
|
-
<div class="lct-field">
|
|
14
|
-
<label for="lct-models-to">To</label>
|
|
15
|
-
<input id="lct-models-to" type="date" name="to" value="<%= params[:to] %>">
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<div class="lct-field">
|
|
19
|
-
<label for="lct-models-provider">Provider</label>
|
|
20
|
-
<%= select_tag :provider,
|
|
21
|
-
options_for_select(provider_filter_options, params[:provider]),
|
|
22
|
-
include_blank: "All providers",
|
|
23
|
-
id: "lct-models-provider" %>
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
|
-
<div class="lct-field">
|
|
27
|
-
<label for="lct-models-model">Model</label>
|
|
28
|
-
<%= select_tag :model,
|
|
29
|
-
options_for_select(model_filter_options, params[:model]),
|
|
30
|
-
include_blank: "All models",
|
|
31
|
-
id: "lct-models-model" %>
|
|
32
|
-
</div>
|
|
33
|
-
|
|
34
|
-
<div class="lct-field">
|
|
35
|
-
<label for="lct-models-sort">Sort</label>
|
|
36
|
-
<%= select_tag :sort,
|
|
37
|
-
options_for_select(
|
|
38
|
-
[["Total spend", "cost"],
|
|
39
|
-
["Call volume", "calls"],
|
|
40
|
-
["Avg cost / call", "avg_cost"],
|
|
41
|
-
["Avg latency", "latency"]],
|
|
42
|
-
@sort.presence || "cost"
|
|
43
|
-
),
|
|
44
|
-
id: "lct-models-sort" %>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
<div class="lct-filter-actions">
|
|
48
|
-
<button class="lct-button" type="submit">Apply</button>
|
|
49
|
-
<%= link_to("Reset", models_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</form>
|
|
6
|
+
<%= render "llm_cost_tracker/shared/filters",
|
|
7
|
+
path: models_path,
|
|
8
|
+
fields: %i[from to provider model] %>
|
|
53
9
|
|
|
54
10
|
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: models_path %>
|
|
55
11
|
</section>
|
|
@@ -64,6 +20,18 @@
|
|
|
64
20
|
</section>
|
|
65
21
|
<% else %>
|
|
66
22
|
<section class="lct-panel">
|
|
23
|
+
<div class="lct-results-toolbar">
|
|
24
|
+
<%= render "llm_cost_tracker/shared/sort",
|
|
25
|
+
current: (@sort.presence || "cost"),
|
|
26
|
+
options: [
|
|
27
|
+
["Total spend", "cost"],
|
|
28
|
+
["Call volume", "calls"],
|
|
29
|
+
["Avg cost / call", "avg_cost"],
|
|
30
|
+
["Avg latency", "latency"]
|
|
31
|
+
],
|
|
32
|
+
path_for_sort: ->(value) { models_path(current_query(sort: value)) } %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
67
35
|
<div class="lct-table-wrap">
|
|
68
36
|
<table class="lct-table lct-table-compact">
|
|
69
37
|
<thead>
|
|
@@ -86,9 +54,9 @@
|
|
|
86
54
|
<td><%= row.provider %></td>
|
|
87
55
|
<td><code class="lct-code"><%= row.model %></code></td>
|
|
88
56
|
<td class="lct-num"><%= number(row.calls) %></td>
|
|
89
|
-
<td class="lct-num"><%=
|
|
90
|
-
<td class="lct-num"><%=
|
|
91
|
-
<td class="lct-num"><%=
|
|
57
|
+
<td class="lct-num"><%= number(row.total_tokens) %></td>
|
|
58
|
+
<td class="lct-num"><%= number(row.input_tokens) %></td>
|
|
59
|
+
<td class="lct-num"><%= number(row.output_tokens) %></td>
|
|
92
60
|
<td class="lct-num"><%= money(row.total_cost) %></td>
|
|
93
61
|
<td class="lct-num"><%= money(row.average_cost_per_call) %></td>
|
|
94
62
|
<% average_latency_ms = row.average_latency_ms %>
|