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
|
@@ -4,9 +4,10 @@ module LlmCostTracker
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
class TagBreakdown
|
|
6
6
|
DEFAULT_LIMIT = 100
|
|
7
|
+
Row = Data.define(:value, :calls, :total_cost, :average_cost_per_call, :share_percent)
|
|
7
8
|
|
|
8
9
|
class << self
|
|
9
|
-
def call(key:, scope: LlmCostTracker::
|
|
10
|
+
def call(key:, scope: LlmCostTracker::Call.all, limit: DEFAULT_LIMIT)
|
|
10
11
|
new(scope: scope, key: key, limit: limit)
|
|
11
12
|
end
|
|
12
13
|
end
|
|
@@ -21,7 +22,19 @@ module LlmCostTracker
|
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def rows
|
|
24
|
-
@rows ||=
|
|
25
|
+
@rows ||= begin
|
|
26
|
+
total = tagged_calls
|
|
27
|
+
scope.klass.find_by_sql(rows_sql).map do |row|
|
|
28
|
+
calls = row.calls.to_i
|
|
29
|
+
Row.new(
|
|
30
|
+
value: row.value,
|
|
31
|
+
calls: calls,
|
|
32
|
+
total_cost: row.total_cost,
|
|
33
|
+
average_cost_per_call: row.average_cost_per_call,
|
|
34
|
+
share_percent: percentage(calls, total)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
25
38
|
end
|
|
26
39
|
|
|
27
40
|
def total_calls
|
|
@@ -46,13 +59,14 @@ module LlmCostTracker
|
|
|
46
59
|
|
|
47
60
|
def rows_sql
|
|
48
61
|
<<~SQL.squish
|
|
49
|
-
SELECT #{
|
|
62
|
+
SELECT #{tag_value_column} AS value,
|
|
50
63
|
COUNT(*) AS calls,
|
|
51
64
|
COALESCE(SUM(sub.total_cost), 0) AS total_cost,
|
|
52
65
|
COALESCE(SUM(sub.total_cost), 0) / NULLIF(COUNT(*), 0) AS average_cost_per_call
|
|
53
66
|
FROM (#{scope.to_sql}) AS sub
|
|
67
|
+
INNER JOIN #{call_tag_table} t ON t.llm_cost_tracker_call_id = sub.id AND t.#{quote_column('key')} = #{quoted_key}
|
|
54
68
|
WHERE #{tag_present_predicate}
|
|
55
|
-
GROUP BY #{
|
|
69
|
+
GROUP BY #{tag_value_column}
|
|
56
70
|
ORDER BY total_cost DESC, calls DESC, value ASC
|
|
57
71
|
LIMIT #{limit}
|
|
58
72
|
SQL
|
|
@@ -61,18 +75,37 @@ module LlmCostTracker
|
|
|
61
75
|
def summary_sql
|
|
62
76
|
<<~SQL.squish
|
|
63
77
|
SELECT COUNT(*) AS total_calls,
|
|
64
|
-
|
|
65
|
-
COUNT(DISTINCT CASE WHEN #{tag_present_predicate} THEN #{
|
|
78
|
+
COUNT(t.#{quote_column('value')}) AS tagged_calls,
|
|
79
|
+
COUNT(DISTINCT CASE WHEN #{tag_present_predicate} THEN #{tag_value_column} END) AS distinct_values
|
|
66
80
|
FROM (#{scope.to_sql}) AS sub
|
|
81
|
+
LEFT OUTER JOIN #{call_tag_table} t ON t.llm_cost_tracker_call_id = sub.id AND t.#{quote_column('key')} = #{quoted_key}
|
|
67
82
|
SQL
|
|
68
83
|
end
|
|
69
84
|
|
|
70
85
|
def tag_present_predicate
|
|
71
|
-
"#{
|
|
86
|
+
"#{tag_value_column} IS NOT NULL AND #{tag_value_column} != ''"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def tag_value_column
|
|
90
|
+
"t.#{quote_column('value')}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def call_tag_table
|
|
94
|
+
LlmCostTracker::CallTag.quoted_table_name
|
|
72
95
|
end
|
|
73
96
|
|
|
74
|
-
def
|
|
75
|
-
|
|
97
|
+
def quote_column(name)
|
|
98
|
+
scope.connection.quote_column_name(name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def quoted_key
|
|
102
|
+
scope.connection.quote(key)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def percentage(numerator, denominator)
|
|
106
|
+
return 0.0 unless denominator.positive?
|
|
107
|
+
|
|
108
|
+
(numerator / denominator.to_f) * 100.0
|
|
76
109
|
end
|
|
77
110
|
end
|
|
78
111
|
end
|
|
@@ -6,14 +6,14 @@ module LlmCostTracker
|
|
|
6
6
|
DEFAULT_LIMIT = 100
|
|
7
7
|
|
|
8
8
|
class << self
|
|
9
|
-
def call(scope: LlmCostTracker::
|
|
9
|
+
def call(scope: LlmCostTracker::Call.all, limit: DEFAULT_LIMIT)
|
|
10
10
|
new(scope: scope, limit: limit).rows
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def initialize(scope:, limit:)
|
|
15
15
|
@scope = scope
|
|
16
|
-
@connection = LlmCostTracker::
|
|
16
|
+
@connection = LlmCostTracker::Call.connection
|
|
17
17
|
limit = limit.to_i
|
|
18
18
|
@limit = limit.positive? ? [limit, DEFAULT_LIMIT].min : DEFAULT_LIMIT
|
|
19
19
|
end
|
|
@@ -29,50 +29,27 @@ module LlmCostTracker
|
|
|
29
29
|
|
|
30
30
|
attr_reader :scope, :connection, :limit
|
|
31
31
|
|
|
32
|
-
def subquery
|
|
33
|
-
scope.to_sql
|
|
34
|
-
end
|
|
35
|
-
|
|
36
32
|
def build_sql
|
|
37
|
-
|
|
38
|
-
return mysql_sql if Ledger::Schema::Adapter.mysql?(connection)
|
|
39
|
-
|
|
40
|
-
Ledger::Schema::Adapter.ensure_supported!(connection)
|
|
41
|
-
end
|
|
33
|
+
tags_table = LlmCostTracker::CallTag.quoted_table_name
|
|
42
34
|
|
|
43
|
-
def mysql_sql
|
|
44
35
|
<<~SQL.squish
|
|
45
|
-
SELECT
|
|
36
|
+
SELECT t.#{key_column} AS #{key_column},
|
|
46
37
|
COUNT(*) AS calls_count,
|
|
47
|
-
COUNT(DISTINCT
|
|
48
|
-
FROM (#{
|
|
49
|
-
JOIN
|
|
50
|
-
|
|
51
|
-
'$[*]' COLUMNS(
|
|
52
|
-
key VARCHAR(255) PATH '$'
|
|
53
|
-
)
|
|
54
|
-
) AS jt
|
|
55
|
-
WHERE sub.tags IS NOT NULL
|
|
56
|
-
AND sub.tags != ''
|
|
57
|
-
GROUP BY jt.key
|
|
38
|
+
COUNT(DISTINCT t.#{value_column}) AS distinct_values
|
|
39
|
+
FROM (#{scope.to_sql}) AS sub
|
|
40
|
+
INNER JOIN #{tags_table} t ON t.llm_cost_tracker_call_id = sub.id
|
|
41
|
+
GROUP BY t.#{key_column}
|
|
58
42
|
ORDER BY calls_count DESC
|
|
59
43
|
LIMIT #{limit}
|
|
60
44
|
SQL
|
|
61
45
|
end
|
|
62
46
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
jsonb_object_keys(sub.tags::jsonb) AS key
|
|
70
|
-
WHERE sub.tags IS NOT NULL
|
|
71
|
-
AND sub.tags::jsonb <> '{}'::jsonb
|
|
72
|
-
GROUP BY key
|
|
73
|
-
ORDER BY calls_count DESC
|
|
74
|
-
LIMIT #{limit}
|
|
75
|
-
SQL
|
|
47
|
+
def key_column
|
|
48
|
+
connection.quote_column_name("key")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def value_column
|
|
52
|
+
connection.quote_column_name("value")
|
|
76
53
|
end
|
|
77
54
|
end
|
|
78
55
|
end
|
|
@@ -8,7 +8,7 @@ module LlmCostTracker
|
|
|
8
8
|
DEFAULT_DAYS = 30
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
|
-
def call(scope: LlmCostTracker::
|
|
11
|
+
def call(scope: LlmCostTracker::Call.all, from: nil, to: Date.current)
|
|
12
12
|
new(scope: scope, from: from, to: to).points
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -8,7 +8,7 @@ module LlmCostTracker
|
|
|
8
8
|
DEFAULT_SORT = "cost"
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
|
-
def call(scope: LlmCostTracker::
|
|
11
|
+
def call(scope: LlmCostTracker::Call.all, limit: DEFAULT_LIMIT, sort: DEFAULT_SORT)
|
|
12
12
|
new(scope: scope, limit: limit, sort: sort).rows
|
|
13
13
|
end
|
|
14
14
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
<% body = capture { yield } %>
|
|
1
2
|
<!DOCTYPE html>
|
|
2
3
|
<html lang="en">
|
|
3
4
|
<head>
|
|
@@ -5,6 +6,7 @@
|
|
|
5
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
7
|
<title>LLM Cost Tracker</title>
|
|
7
8
|
<%= stylesheet_link_tag stylesheet_path %>
|
|
9
|
+
<%= inline_style_block %>
|
|
8
10
|
</head>
|
|
9
11
|
<body class="lct-body">
|
|
10
12
|
<div class="lct-app">
|
|
@@ -19,10 +21,13 @@
|
|
|
19
21
|
<%= link_to "Calls", calls_path, class: ("lct-active" if request.path.start_with?(calls_path)) %>
|
|
20
22
|
<%= link_to "Tags", tags_path, class: ("lct-active" if request.path.start_with?(tags_path)) %>
|
|
21
23
|
<%= link_to "Data Quality", data_quality_path, class: ("lct-active" if request.path.start_with?(data_quality_path)) %>
|
|
24
|
+
<% if LlmCostTracker.reconciliation_enabled? %>
|
|
25
|
+
<%= link_to "Reconciliation", reconciliation_path, class: ("lct-active" if request.path.start_with?(reconciliation_path)) %>
|
|
26
|
+
<% end %>
|
|
22
27
|
</nav>
|
|
23
28
|
</header>
|
|
24
29
|
|
|
25
|
-
<%=
|
|
30
|
+
<%= body %>
|
|
26
31
|
</main>
|
|
27
32
|
</div>
|
|
28
33
|
</body>
|
|
@@ -2,83 +2,16 @@
|
|
|
2
2
|
<div class="lct-toolbar-head">
|
|
3
3
|
<h2 class="lct-section-title">Calls</h2>
|
|
4
4
|
<div class="lct-toolbar-actions">
|
|
5
|
-
<%= link_to "Export CSV",
|
|
5
|
+
<%= link_to "Export CSV",
|
|
6
|
+
calls_path(current_query(format: :csv)),
|
|
7
|
+
class: "lct-button lct-button-secondary",
|
|
8
|
+
title: "Capped at #{number(LlmCostTracker::CallsController::CSV_EXPORT_LIMIT)} rows per request — narrow the date range to export larger slices." %>
|
|
6
9
|
</div>
|
|
7
10
|
</div>
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
<div class="lct-filter-row lct-filter-row-with-sort">
|
|
11
|
-
<div class="lct-field">
|
|
12
|
-
<label for="lct-from">From</label>
|
|
13
|
-
<input id="lct-from" type="date" name="from" value="<%= params[:from] %>">
|
|
14
|
-
</div>
|
|
15
|
-
|
|
16
|
-
<div class="lct-field">
|
|
17
|
-
<label for="lct-to">To</label>
|
|
18
|
-
<input id="lct-to" type="date" name="to" value="<%= params[:to] %>">
|
|
19
|
-
</div>
|
|
20
|
-
|
|
21
|
-
<div class="lct-field">
|
|
22
|
-
<label for="lct-provider">Provider</label>
|
|
23
|
-
<%= select_tag :provider,
|
|
24
|
-
options_for_select(provider_filter_options, params[:provider]),
|
|
25
|
-
include_blank: "All providers",
|
|
26
|
-
id: "lct-provider" %>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<div class="lct-field">
|
|
30
|
-
<label for="lct-model">Model</label>
|
|
31
|
-
<%= select_tag :model,
|
|
32
|
-
options_for_select(model_filter_options, params[:model]),
|
|
33
|
-
include_blank: "All models",
|
|
34
|
-
id: "lct-model" %>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<div class="lct-field">
|
|
38
|
-
<label for="lct-stream">Stream</label>
|
|
39
|
-
<%= select_tag :stream,
|
|
40
|
-
options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, params[:stream]),
|
|
41
|
-
include_blank: "All calls",
|
|
42
|
-
id: "lct-stream" %>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<div class="lct-field">
|
|
46
|
-
<label for="lct-sort">Sort</label>
|
|
47
|
-
<%= select_tag :sort,
|
|
48
|
-
options_for_select(
|
|
49
|
-
[["Recent first", ""],
|
|
50
|
-
["Most expensive", "expensive"],
|
|
51
|
-
["Largest input", "input"],
|
|
52
|
-
["Largest output", "output"],
|
|
53
|
-
["Slowest", "slow"],
|
|
54
|
-
["Unknown pricing only", "unknown_pricing"]],
|
|
55
|
-
@sort
|
|
56
|
-
),
|
|
57
|
-
id: "lct-sort" %>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<div class="lct-filter-actions">
|
|
61
|
-
<button class="lct-button" type="submit">Apply</button>
|
|
62
|
-
<%= link_to("Reset", calls_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</form>
|
|
12
|
+
<%= render "llm_cost_tracker/shared/filters", path: calls_path %>
|
|
66
13
|
|
|
67
14
|
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: calls_path %>
|
|
68
|
-
|
|
69
|
-
<p class="lct-summary-row">
|
|
70
|
-
<span class="lct-pagination-per">
|
|
71
|
-
<span class="lct-pagination-per-label">Per page:</span>
|
|
72
|
-
<% LlmCostTracker::PaginationHelper::PER_PAGE_CHOICES.each do |choice| %>
|
|
73
|
-
<% if choice == @page.per %>
|
|
74
|
-
<span class="lct-pagination-per-option is-active" aria-current="true"><%= choice %></span>
|
|
75
|
-
<% else %>
|
|
76
|
-
<%= link_to choice, calls_path(current_query(per: choice, page: 1)), class: "lct-pagination-per-option" %>
|
|
77
|
-
<% end %>
|
|
78
|
-
<% end %>
|
|
79
|
-
</span>
|
|
80
|
-
</p>
|
|
81
|
-
<p class="lct-toolbar-note">CSV export is capped at <%= number(LlmCostTracker::CallsController::CSV_EXPORT_LIMIT) %> rows per request — narrow the date range to export larger slices.</p>
|
|
82
15
|
</section>
|
|
83
16
|
|
|
84
17
|
<% if @calls_count.zero? %>
|
|
@@ -91,6 +24,31 @@
|
|
|
91
24
|
</section>
|
|
92
25
|
<% else %>
|
|
93
26
|
<section class="lct-panel">
|
|
27
|
+
<div class="lct-results-toolbar">
|
|
28
|
+
<span class="lct-pagination-per">
|
|
29
|
+
<span class="lct-pagination-per-label">Per page:</span>
|
|
30
|
+
<% LlmCostTracker::PaginationHelper::PER_PAGE_CHOICES.each do |choice| %>
|
|
31
|
+
<% if choice == @page.per %>
|
|
32
|
+
<span class="lct-pagination-per-option is-active" aria-current="true"><%= choice %></span>
|
|
33
|
+
<% else %>
|
|
34
|
+
<%= link_to choice, calls_path(current_query(per: choice, page: 1)), class: "lct-pagination-per-option" %>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% end %>
|
|
37
|
+
</span>
|
|
38
|
+
|
|
39
|
+
<%= render "llm_cost_tracker/shared/sort",
|
|
40
|
+
current: @sort,
|
|
41
|
+
options: [
|
|
42
|
+
["Recent", ""],
|
|
43
|
+
["Most expensive", "expensive"],
|
|
44
|
+
["Largest input", "input"],
|
|
45
|
+
["Largest output", "output"],
|
|
46
|
+
["Slowest", "slow"],
|
|
47
|
+
["Unknown pricing", "unknown_pricing"]
|
|
48
|
+
],
|
|
49
|
+
path_for_sort: ->(value) { calls_path(current_query(sort: value.presence, page: nil)) } %>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
94
52
|
<div class="lct-table-wrap">
|
|
95
53
|
<table class="lct-table lct-table-compact lct-calls-table">
|
|
96
54
|
<thead>
|
|
@@ -113,9 +71,9 @@
|
|
|
113
71
|
<td class="lct-nowrap"><%= format_date(call.tracked_at) %></td>
|
|
114
72
|
<td><%= call.provider %></td>
|
|
115
73
|
<td><code class="lct-code"><%= call.model %></code></td>
|
|
116
|
-
<td class="lct-num"><%=
|
|
117
|
-
<td class="lct-num"><%=
|
|
118
|
-
<td class="lct-num"><%=
|
|
74
|
+
<td class="lct-num"><%= number(call.input_tokens) %></td>
|
|
75
|
+
<td class="lct-num"><%= number(call.output_tokens) %></td>
|
|
76
|
+
<td class="lct-num"><%= number(call.total_tokens) %></td>
|
|
119
77
|
<td class="lct-num<%= ' lct-num-muted' if call.total_cost.nil? %>"><%= optional_money(call.total_cost) %></td>
|
|
120
78
|
<td class="lct-num<%= ' lct-num-muted' if call.latency_ms.nil? %>"><%= call.latency_ms ? "#{number(call.latency_ms)}ms" : "n/a" %></td>
|
|
121
79
|
<td><%= render "llm_cost_tracker/shared/tag_chips", tags: call.parsed_tags %></td>
|
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
<% priced_components = token_usage_stack_components %>
|
|
2
|
+
<% line_item_costs_by_component = call_line_item_costs_by_component(@call) %>
|
|
2
3
|
<% token_segments = priced_components.map do |component|
|
|
3
4
|
token_key = component.fetch(:token_key)
|
|
4
5
|
value = @call.has_attribute?(token_key) ? @call[token_key] : 0
|
|
5
|
-
{ label: component.fetch(:label), value: value, formatted_value:
|
|
6
|
+
{ label: component.fetch(:label), value: value, formatted_value: number(value), css_class: component.fetch(:css_class) }
|
|
6
7
|
end %>
|
|
7
8
|
<% cost_segments = [] %>
|
|
8
9
|
<% unless @call.total_cost.nil? %>
|
|
9
10
|
<% cost_segments = priced_components.map do |component|
|
|
10
|
-
|
|
11
|
-
value = @call.has_attribute?(cost_key) ? @call[cost_key] : nil
|
|
11
|
+
value = line_item_costs_by_component[component.fetch(:price_key)]
|
|
12
12
|
{ label: component.fetch(:label), value: value, formatted_value: optional_money(value), css_class: component.fetch(:css_class) }
|
|
13
13
|
end %>
|
|
14
14
|
<% end %>
|
|
15
15
|
|
|
16
16
|
<section class="lct-panel">
|
|
17
|
-
<
|
|
17
|
+
<nav class="lct-breadcrumb" aria-label="Breadcrumb">
|
|
18
|
+
<%= link_to "Calls", calls_path, class: "lct-breadcrumb-link" %>
|
|
19
|
+
<span class="lct-breadcrumb-sep" aria-hidden="true">›</span>
|
|
20
|
+
<span class="lct-breadcrumb-current">#<%= @call.id %></span>
|
|
21
|
+
</nav>
|
|
18
22
|
<div class="lct-call-hero">
|
|
19
23
|
<div>
|
|
20
|
-
<h2 class="lct-section-title lct-call-title">
|
|
24
|
+
<h2 class="lct-section-title lct-call-title">
|
|
25
|
+
<code class="lct-code"><%= @call.model %></code>
|
|
26
|
+
</h2>
|
|
21
27
|
<p class="lct-call-subtitle">
|
|
22
28
|
<code class="lct-code"><%= @call.provider %></code>
|
|
23
29
|
<span>·</span>
|
|
24
|
-
<code class="lct-code"><%= @call.model %></code>
|
|
25
|
-
<span>·</span>
|
|
26
30
|
<span><%= format_date(@call.tracked_at) %></span>
|
|
27
31
|
</p>
|
|
28
32
|
</div>
|
|
@@ -38,7 +42,7 @@ end %>
|
|
|
38
42
|
</div>
|
|
39
43
|
<div class="lct-call-summary-item">
|
|
40
44
|
<span class="lct-call-summary-label">Total tokens</span>
|
|
41
|
-
<strong><%=
|
|
45
|
+
<strong><%= number(@call.total_tokens) %></strong>
|
|
42
46
|
</div>
|
|
43
47
|
<div class="lct-call-summary-item">
|
|
44
48
|
<span class="lct-call-summary-label">Latency</span>
|
|
@@ -65,55 +69,91 @@ end %>
|
|
|
65
69
|
|
|
66
70
|
<div class="lct-detail-grid">
|
|
67
71
|
<dl class="lct-dl">
|
|
68
|
-
<dt>
|
|
69
|
-
<dd><%=
|
|
72
|
+
<dt>Cost Status</dt>
|
|
73
|
+
<dd><%= @call.cost_status.presence || "n/a" %></dd>
|
|
70
74
|
|
|
71
|
-
<dt>
|
|
72
|
-
<dd><%= @call.
|
|
73
|
-
|
|
74
|
-
<dt>Model</dt>
|
|
75
|
-
<dd><%= @call.model %></dd>
|
|
75
|
+
<dt>Latency</dt>
|
|
76
|
+
<dd><%= @call.latency_ms ? "#{optional_number(@call.latency_ms)}ms" : "n/a" %></dd>
|
|
76
77
|
|
|
77
|
-
<dt>
|
|
78
|
-
<dd><%=
|
|
78
|
+
<dt>Batch</dt>
|
|
79
|
+
<dd><%= @call.batch? ? "yes" : "no" %></dd>
|
|
79
80
|
|
|
80
|
-
<dt>
|
|
81
|
+
<dt>Response ID</dt>
|
|
81
82
|
<dd><%= @call.provider_response_id.presence || "n/a" %></dd>
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<dd><%= format_date(@call.created_at) %></dd>
|
|
86
|
-
<% end %>
|
|
84
|
+
<dt>Project ID</dt>
|
|
85
|
+
<dd><%= @call.provider_project_id.present? ? LlmCostTracker::Masking.mask_value(:provider_project_id, @call.provider_project_id) : "n/a" %></dd>
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
<dt>API Key ID</dt>
|
|
88
|
+
<dd><%= @call.provider_api_key_id.present? ? LlmCostTracker::Masking.mask_value(:provider_api_key_id, @call.provider_api_key_id) : "n/a" %></dd>
|
|
89
|
+
|
|
90
|
+
<dt>Workspace ID</dt>
|
|
91
|
+
<dd><%= @call.provider_workspace_id.present? ? LlmCostTracker::Masking.mask_value(:provider_workspace_id, @call.provider_workspace_id) : "n/a" %></dd>
|
|
92
92
|
</dl>
|
|
93
93
|
|
|
94
94
|
<dl class="lct-dl">
|
|
95
95
|
<% priced_components.each do |component| %>
|
|
96
96
|
<dt><%= component.fetch(:label).titleize %> Tokens</dt>
|
|
97
|
-
<dd><%=
|
|
97
|
+
<dd><%= number(@call[component.fetch(:token_key)]) %></dd>
|
|
98
98
|
<% end %>
|
|
99
99
|
|
|
100
100
|
<dt>Total Tokens</dt>
|
|
101
|
-
<dd><%=
|
|
101
|
+
<dd><%= number(@call.total_tokens) %></dd>
|
|
102
|
+
</dl>
|
|
102
103
|
|
|
104
|
+
<dl class="lct-dl">
|
|
103
105
|
<% priced_components.each do |component| %>
|
|
104
106
|
<dt><%= component.fetch(:label).titleize %> Cost</dt>
|
|
105
|
-
<dd><%= optional_money(
|
|
107
|
+
<dd><%= optional_money(line_item_costs_by_component[component.fetch(:price_key)]) %></dd>
|
|
106
108
|
<% end %>
|
|
107
109
|
|
|
108
110
|
<dt>Total Cost</dt>
|
|
109
111
|
<dd><%= optional_money(@call.total_cost) %></dd>
|
|
110
|
-
|
|
111
|
-
<dt>Latency</dt>
|
|
112
|
-
<dd><%= @call.latency_ms ? "#{optional_number(@call.latency_ms)}ms" : "n/a" %></dd>
|
|
113
112
|
</dl>
|
|
114
113
|
</div>
|
|
115
114
|
</section>
|
|
116
115
|
|
|
116
|
+
<% service_line_items = @call.line_items.where.not(unit: "token").order(:position).to_a %>
|
|
117
|
+
<% if service_line_items.any? %>
|
|
118
|
+
<section class="lct-panel">
|
|
119
|
+
<h2 class="lct-section-title">Service Charges</h2>
|
|
120
|
+
<div class="lct-table-wrap">
|
|
121
|
+
<table class="lct-table lct-table-compact">
|
|
122
|
+
<thead>
|
|
123
|
+
<tr>
|
|
124
|
+
<th>Component</th>
|
|
125
|
+
<th>Unit</th>
|
|
126
|
+
<th class="lct-num">Quantity</th>
|
|
127
|
+
<th class="lct-num">Rate</th>
|
|
128
|
+
<th class="lct-num">Cost</th>
|
|
129
|
+
<th>Status</th>
|
|
130
|
+
</tr>
|
|
131
|
+
</thead>
|
|
132
|
+
<tbody>
|
|
133
|
+
<% service_line_items.each do |line_item| %>
|
|
134
|
+
<tr>
|
|
135
|
+
<td><code class="lct-code"><%= line_item.kind %></code></td>
|
|
136
|
+
<td><%= line_item.unit %></td>
|
|
137
|
+
<td class="lct-num"><%= line_item.quantity %></td>
|
|
138
|
+
<td class="lct-num"><%= line_item.rate_amount ? "#{optional_money(line_item.rate_amount)} / #{line_item.rate_quantity}" : "n/a" %></td>
|
|
139
|
+
<% unknown_cost = line_item.cost.nil? || line_item.cost_status.to_s == LlmCostTracker::Billing::CostStatus::UNKNOWN %>
|
|
140
|
+
<td class="lct-num<%= ' lct-num-muted' if unknown_cost %>"><%= unknown_cost ? "n/a" : money(line_item.cost) %></td>
|
|
141
|
+
<td><%= line_item.cost_status %></td>
|
|
142
|
+
</tr>
|
|
143
|
+
<% end %>
|
|
144
|
+
</tbody>
|
|
145
|
+
</table>
|
|
146
|
+
</div>
|
|
147
|
+
</section>
|
|
148
|
+
<% end %>
|
|
149
|
+
|
|
150
|
+
<% if @call.pricing_snapshot.present? %>
|
|
151
|
+
<section class="lct-panel">
|
|
152
|
+
<h2 class="lct-section-title">Pricing Snapshot</h2>
|
|
153
|
+
<pre class="lct-pre"><%= safe_json(@call.pricing_snapshot) %></pre>
|
|
154
|
+
</section>
|
|
155
|
+
<% end %>
|
|
156
|
+
|
|
117
157
|
<section class="lct-panel">
|
|
118
158
|
<h2 class="lct-section-title">Tags</h2>
|
|
119
159
|
<pre class="lct-pre"><%= safe_json(@call.parsed_tags) %></pre>
|
|
@@ -122,6 +162,6 @@ end %>
|
|
|
122
162
|
<% if @call.has_attribute?("metadata") %>
|
|
123
163
|
<section class="lct-panel">
|
|
124
164
|
<h2 class="lct-section-title">Metadata</h2>
|
|
125
|
-
<pre class="lct-pre"><%= safe_json(@call.read_attribute("metadata")) %></pre>
|
|
165
|
+
<pre class="lct-pre"><%= safe_json(LlmCostTracker::Masking.mask_hash(masked_metadata_hash(@call.read_attribute("metadata")))) %></pre>
|
|
126
166
|
</section>
|
|
127
167
|
<% end %>
|
|
@@ -1,48 +1,10 @@
|
|
|
1
1
|
<% overview_filter_scope = current_query(from: @from_date.iso8601, to: @to_date.iso8601) %>
|
|
2
2
|
|
|
3
3
|
<section class="lct-panel lct-toolbar">
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<input id="lct-overview-from" type="date" name="from" value="<%= params[:from] || @from_date.iso8601 %>">
|
|
9
|
-
</div>
|
|
10
|
-
|
|
11
|
-
<div class="lct-field">
|
|
12
|
-
<label for="lct-overview-to">To</label>
|
|
13
|
-
<input id="lct-overview-to" type="date" name="to" value="<%= params[:to] || @to_date.iso8601 %>">
|
|
14
|
-
</div>
|
|
15
|
-
|
|
16
|
-
<div class="lct-field">
|
|
17
|
-
<label for="lct-overview-provider">Provider</label>
|
|
18
|
-
<%= select_tag :provider,
|
|
19
|
-
options_for_select(provider_filter_options(filter_params: overview_filter_scope), params[:provider]),
|
|
20
|
-
include_blank: "All providers",
|
|
21
|
-
id: "lct-overview-provider" %>
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
<div class="lct-field">
|
|
25
|
-
<label for="lct-overview-model">Model</label>
|
|
26
|
-
<%= select_tag :model,
|
|
27
|
-
options_for_select(model_filter_options(filter_params: overview_filter_scope), params[:model]),
|
|
28
|
-
include_blank: "All models",
|
|
29
|
-
id: "lct-overview-model" %>
|
|
30
|
-
</div>
|
|
31
|
-
|
|
32
|
-
<div class="lct-field">
|
|
33
|
-
<label for="lct-overview-stream">Stream</label>
|
|
34
|
-
<%= select_tag :stream,
|
|
35
|
-
options_for_select(LlmCostTracker::DashboardFilterHelper::STREAM_FILTER_OPTIONS, params[:stream]),
|
|
36
|
-
include_blank: "All calls",
|
|
37
|
-
id: "lct-overview-stream" %>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<div class="lct-filter-actions">
|
|
41
|
-
<button class="lct-button" type="submit">Apply</button>
|
|
42
|
-
<%= link_to("Reset", root_path, class: "lct-button lct-button-secondary") if any_filter_applied? %>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
</form>
|
|
4
|
+
<%= render "llm_cost_tracker/shared/filters",
|
|
5
|
+
path: root_path,
|
|
6
|
+
filter_scope: overview_filter_scope,
|
|
7
|
+
defaults: { from: @from_date.iso8601, to: @to_date.iso8601 } %>
|
|
46
8
|
|
|
47
9
|
<%= render "llm_cost_tracker/shared/active_filters", chips: active_tag_filters, clear_path: root_path %>
|
|
48
10
|
</section>
|
|
@@ -52,7 +14,7 @@
|
|
|
52
14
|
<h2 class="lct-state-title">No LLM calls yet</h2>
|
|
53
15
|
<p class="lct-state-copy">Tracked requests will appear here after your application records its first LLM API call.</p>
|
|
54
16
|
<div class="lct-state-actions">
|
|
55
|
-
<%= link_to "
|
|
17
|
+
<%= link_to "Calls", calls_path, class: "lct-button lct-button-secondary" %>
|
|
56
18
|
</div>
|
|
57
19
|
</section>
|
|
58
20
|
<% else %>
|
|
@@ -84,7 +46,7 @@
|
|
|
84
46
|
<% end %>
|
|
85
47
|
</p>
|
|
86
48
|
</div>
|
|
87
|
-
<%= link_to "
|
|
49
|
+
<%= link_to "Calls",
|
|
88
50
|
calls_path(current_query(provider: @spend_anomaly.fetch(:provider), model: @spend_anomaly.fetch(:model), from: @to_date.iso8601, to: @to_date.iso8601, page: nil, per: nil, format: nil)),
|
|
89
51
|
class: "lct-button lct-button-secondary" %>
|
|
90
52
|
</aside>
|
|
@@ -102,6 +64,11 @@
|
|
|
102
64
|
|
|
103
65
|
<div class="lct-hero-side">
|
|
104
66
|
<div class="lct-stat-grid">
|
|
67
|
+
<article class="lct-stat">
|
|
68
|
+
<p class="lct-stat-label">Avg cost / call</p>
|
|
69
|
+
<p class="lct-stat-value"><%= money(@stats.average_cost_per_call) %></p>
|
|
70
|
+
</article>
|
|
71
|
+
|
|
105
72
|
<article class="lct-stat">
|
|
106
73
|
<p class="lct-stat-label">Calls</p>
|
|
107
74
|
<p class="lct-stat-value"><%= number(@stats.total_calls) %></p>
|
|
@@ -109,11 +76,6 @@
|
|
|
109
76
|
<span class="<%= badge[:css_class] %>"><span class="lct-delta-dot" aria-hidden="true"></span><%= badge[:text] %></span>
|
|
110
77
|
</article>
|
|
111
78
|
|
|
112
|
-
<article class="lct-stat">
|
|
113
|
-
<p class="lct-stat-label">Avg cost / call</p>
|
|
114
|
-
<p class="lct-stat-value"><%= money(@stats.average_cost_per_call) %></p>
|
|
115
|
-
</article>
|
|
116
|
-
|
|
117
79
|
<% if @stats.average_latency_ms %>
|
|
118
80
|
<article class="lct-stat">
|
|
119
81
|
<p class="lct-stat-label">Avg latency</p>
|
|
@@ -125,9 +87,6 @@
|
|
|
125
87
|
|
|
126
88
|
<% if @monthly_budget_status %>
|
|
127
89
|
<% budget = @monthly_budget_status %>
|
|
128
|
-
<% fill_mod = budget_fill_modifier(budget[:percent_used]) %>
|
|
129
|
-
<% projected_marker = [[budget[:projected_percent_used].to_f, 0.0].max, 100.0].min %>
|
|
130
|
-
<% projected_delta = budget[:projected_delta].to_f %>
|
|
131
90
|
<section class="lct-panel lct-panel-tight">
|
|
132
91
|
<div class="lct-section-head">
|
|
133
92
|
<div>
|
|
@@ -143,16 +102,16 @@
|
|
|
143
102
|
</span>
|
|
144
103
|
<span class="lct-budget-percent"><%= percent(budget[:percent_used]) %></span>
|
|
145
104
|
</div>
|
|
146
|
-
<div class="lct-budget-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<%=
|
|
147
|
-
<div
|
|
105
|
+
<div class="lct-budget-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<%= budget[:progress_percent].round %>">
|
|
106
|
+
<div data-lct-style="<%= inline_style("width: #{budget[:progress_percent]}%") %>" class="lct-budget-fill <%= budget[:fill_modifier] %>"></div>
|
|
148
107
|
<% if budget[:projected_spent].positive? %>
|
|
149
|
-
<span class="lct-budget-marker" aria-hidden="true"
|
|
108
|
+
<span data-lct-style="<%= inline_style("left: calc(#{budget[:projected_marker_percent]}% - 1px)") %>" class="lct-budget-marker" aria-hidden="true"></span>
|
|
150
109
|
<% end %>
|
|
151
110
|
</div>
|
|
152
111
|
<p class="lct-budget-projection">
|
|
153
112
|
<span>Projected <strong><%= money(budget[:projected_spent]) %></strong> by <%= budget[:projection_end_label] %></span>
|
|
154
|
-
<span class="lct-budget-projection-status <%=
|
|
155
|
-
<%= money(
|
|
113
|
+
<span class="lct-budget-projection-status <%= budget[:projected_delta_status_class] %>">
|
|
114
|
+
<%= money(budget[:projected_delta_amount]) %> <%= budget[:projected_delta_direction] %> budget
|
|
156
115
|
</span>
|
|
157
116
|
</p>
|
|
158
117
|
<p class="lct-budget-meta">Soft limit: blocking is not atomic under concurrency.</p>
|