llm_cost_tracker 0.14.1 → 0.15.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 +198 -0
- data/LICENSE.txt +28 -0
- data/README.md +38 -35
- data/app/assets/llm_cost_tracker/application.css +16 -0
- data/app/controllers/llm_cost_tracker/application_controller.rb +7 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +12 -96
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +11 -12
- data/app/controllers/llm_cost_tracker/models_controller.rb +2 -4
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +15 -4
- data/app/controllers/llm_cost_tracker/tags_controller.rb +21 -13
- data/app/helpers/llm_cost_tracker/application_helper.rb +24 -54
- data/app/helpers/llm_cost_tracker/chart_helper.rb +58 -113
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +30 -17
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +18 -25
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +36 -26
- data/app/models/llm_cost_tracker/call.rb +15 -17
- data/app/models/llm_cost_tracker/call_rollup.rb +6 -6
- data/app/services/llm_cost_tracker/dashboard/call_order.rb +36 -0
- data/app/services/llm_cost_tracker/dashboard/calls_export.rb +68 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality/stats_query.rb +77 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality/summary.rb +47 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +67 -222
- data/app/services/llm_cost_tracker/dashboard/filter.rb +7 -10
- data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +42 -35
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/percent.rb +12 -0
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +2 -1
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +20 -18
- data/app/services/llm_cost_tracker/dashboard/spend_chart.rb +50 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +8 -10
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +2 -4
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +3 -5
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +3 -5
- data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -6
- data/app/views/llm_cost_tracker/calls/index.html.erb +2 -1
- data/app/views/llm_cost_tracker/calls/show.html.erb +7 -32
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +1 -1
- data/app/views/llm_cost_tracker/errors/query.html.erb +6 -0
- data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -3
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -4
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +2 -5
- data/app/views/llm_cost_tracker/tags/show.html.erb +6 -6
- data/lib/llm_cost_tracker/budget/estimate.rb +35 -0
- data/lib/llm_cost_tracker/budget/global.rb +69 -0
- data/lib/llm_cost_tracker/budget/limit.rb +52 -0
- data/lib/llm_cost_tracker/budget/per_tag.rb +63 -25
- data/lib/llm_cost_tracker/budget/tagged.rb +74 -0
- data/lib/llm_cost_tracker/budget.rb +22 -171
- data/lib/llm_cost_tracker/capture/event_window.rb +17 -13
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +26 -17
- data/lib/llm_cost_tracker/capture/sse.rb +7 -5
- data/lib/llm_cost_tracker/capture/stream_collector.rb +99 -107
- data/lib/llm_cost_tracker/capture/stream_tap.rb +2 -2
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +36 -55
- data/lib/llm_cost_tracker/capture_verifier.rb +3 -10
- data/lib/llm_cost_tracker/charges/cost_status.rb +23 -19
- data/lib/llm_cost_tracker/charges/line_item.rb +36 -34
- data/lib/llm_cost_tracker/configuration/budgets.rb +50 -19
- data/lib/llm_cost_tracker/configuration/capture.rb +7 -1
- data/lib/llm_cost_tracker/configuration/mutability.rb +3 -3
- data/lib/llm_cost_tracker/configuration/pricing.rb +1 -1
- data/lib/llm_cost_tracker/configuration.rb +5 -9
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +20 -2
- data/lib/llm_cost_tracker/doctor/price_check.rb +26 -27
- data/lib/llm_cost_tracker/doctor/probe.rb +1 -1
- data/lib/llm_cost_tracker/doctor/schema_check.rb +1 -2
- data/lib/llm_cost_tracker/doctor.rb +42 -22
- data/lib/llm_cost_tracker/engine.rb +8 -1
- data/lib/llm_cost_tracker/errors.rb +2 -4
- data/lib/llm_cost_tracker/event.rb +28 -27
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +5 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +5 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -10
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +2 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +2 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +1 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +3 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +2 -1
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb +0 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb +3 -8
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb +3 -8
- data/lib/llm_cost_tracker/ingestion/batch.rb +33 -19
- data/lib/llm_cost_tracker/ingestion/inbox.rb +22 -19
- data/lib/llm_cost_tracker/ingestion/worker.rb +58 -37
- data/lib/llm_cost_tracker/ingestion.rb +23 -36
- data/lib/llm_cost_tracker/integrations/anthropic/batch_results_capture.rb +40 -0
- data/lib/llm_cost_tracker/integrations/anthropic/patches.rb +56 -0
- data/lib/llm_cost_tracker/integrations/anthropic.rb +82 -90
- data/lib/llm_cost_tracker/integrations/base/patch_target.rb +35 -0
- data/lib/llm_cost_tracker/integrations/base.rb +98 -93
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +27 -23
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +54 -35
- data/lib/llm_cost_tracker/integrations/openai/websocket_capture.rb +89 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +135 -121
- data/lib/llm_cost_tracker/integrations/ruby_llm/v1/patches.rb +67 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v1/reply.rb +155 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v1.rb +154 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt/token_counts.rb +159 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt.rb +175 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2/frame.rb +75 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2/recorder.rb +70 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2/seams.rb +86 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm/v2.rb +161 -0
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +7 -243
- data/lib/llm_cost_tracker/integrations.rb +1 -20
- data/lib/llm_cost_tracker/ledger/isolation.rb +15 -9
- data/lib/llm_cost_tracker/ledger/period/totals.rb +15 -11
- data/lib/llm_cost_tracker/ledger/rollups.rb +34 -18
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +33 -5
- data/lib/llm_cost_tracker/ledger/schema/base.rb +9 -4
- data/lib/llm_cost_tracker/ledger/store.rb +19 -37
- data/lib/llm_cost_tracker/ledger/tags/breakdown.rb +2 -6
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +3 -12
- data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -2
- data/lib/llm_cost_tracker/middleware/faraday/body.rb +57 -0
- data/lib/llm_cost_tracker/middleware/faraday/exchange.rb +153 -0
- data/lib/llm_cost_tracker/middleware/faraday/response_reader.rb +75 -0
- data/lib/llm_cost_tracker/middleware/faraday/stream_tee.rb +47 -0
- data/lib/llm_cost_tracker/middleware/faraday.rb +2 -284
- data/lib/llm_cost_tracker/parsers.rb +19 -40
- data/lib/llm_cost_tracker/prices.json +3241 -187
- data/lib/llm_cost_tracker/pricing/backfill.rb +98 -35
- data/lib/llm_cost_tracker/pricing/calculation/iterations.rb +49 -0
- data/lib/llm_cost_tracker/pricing/calculation/quantities.rb +40 -0
- data/lib/llm_cost_tracker/pricing/calculation/snapshot.rb +51 -0
- data/lib/llm_cost_tracker/pricing/calculation/totals.rb +59 -0
- data/lib/llm_cost_tracker/pricing/calculation.rb +113 -161
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +11 -10
- data/lib/llm_cost_tracker/pricing/estimator.rb +5 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +61 -30
- data/lib/llm_cost_tracker/pricing/mode.rb +8 -32
- data/lib/llm_cost_tracker/pricing/off_peak.rb +50 -0
- data/lib/llm_cost_tracker/pricing/price_key.rb +5 -3
- data/lib/llm_cost_tracker/pricing/rate.rb +2 -0
- data/lib/llm_cost_tracker/pricing/registry/normalizer.rb +110 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +39 -123
- data/lib/llm_cost_tracker/pricing/service_rates.rb +1 -8
- data/lib/llm_cost_tracker/pricing/sync/change_printer.rb +40 -20
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +35 -34
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +13 -9
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +29 -18
- data/lib/llm_cost_tracker/pricing/sync/remote_snapshot.rb +78 -0
- data/lib/llm_cost_tracker/pricing/sync/snapshot_guard.rb +2 -0
- data/lib/llm_cost_tracker/pricing/sync.rb +24 -119
- data/lib/llm_cost_tracker/pricing/unknown.rb +1 -1
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +59 -24
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +12 -4
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +140 -50
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -1
- data/lib/llm_cost_tracker/providers/azure/parser.rb +6 -13
- data/lib/llm_cost_tracker/providers/gemini/grounding.rb +87 -0
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +167 -106
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +65 -55
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +12 -1
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +1 -1
- data/lib/llm_cost_tracker/providers/openai/output_item.rb +84 -0
- data/lib/llm_cost_tracker/providers/openai/parser.rb +12 -13
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +100 -98
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +131 -137
- data/lib/llm_cost_tracker/providers/openai/stream_parser.rb +127 -0
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +78 -55
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +17 -4
- data/lib/llm_cost_tracker/providers.rb +3 -0
- data/lib/llm_cost_tracker/redaction.rb +13 -0
- data/lib/llm_cost_tracker/report/data.rb +25 -28
- data/lib/llm_cost_tracker/retention.rb +1 -3
- data/lib/llm_cost_tracker/tags/context.rb +22 -7
- data/lib/llm_cost_tracker/tracker.rb +45 -31
- data/lib/llm_cost_tracker/usage/catalog.rb +13 -4
- data/lib/llm_cost_tracker/usage/dimension.rb +1 -1
- data/lib/llm_cost_tracker/usage/dimensions.yml +85 -0
- data/lib/llm_cost_tracker/usage/token_usage.rb +8 -4
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +8 -4
- data/lib/tasks/llm_cost_tracker.rake +58 -25
- data/llm_cost_tracker.gemspec +64 -0
- metadata +65 -11
|
@@ -11,6 +11,7 @@ module LlmCostTracker
|
|
|
11
11
|
["Streaming only", "yes"],
|
|
12
12
|
["Non-streaming only", "no"]
|
|
13
13
|
].freeze
|
|
14
|
+
STREAM_FILTER_LABELS = { "yes" => "Streaming", "no" => "Non-streaming" }.freeze
|
|
14
15
|
|
|
15
16
|
class << self
|
|
16
17
|
def call(scope: LlmCostTracker::Call.all, params: {}, tags: {})
|
|
@@ -39,17 +40,13 @@ module LlmCostTracker
|
|
|
39
40
|
attr_reader :scope, :params, :extra_tags
|
|
40
41
|
|
|
41
42
|
def apply_date_filters(relation)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Dashboard::DateRange.
|
|
45
|
-
|
|
46
|
-
default_range = Dashboard::DateRange.call(params: params)
|
|
47
|
-
from_date ||= default_range.from
|
|
48
|
-
to_date ||= default_range.to
|
|
43
|
+
Dashboard::DateRange.validate!(from: Dashboard::DateRange.parse(params, :from),
|
|
44
|
+
to: Dashboard::DateRange.parse(params, :to))
|
|
45
|
+
range = Dashboard::DateRange.call(params: params)
|
|
49
46
|
|
|
50
47
|
relation
|
|
51
|
-
.where(tracked_at:
|
|
52
|
-
.where(tracked_at: ..
|
|
48
|
+
.where(tracked_at: range.from.beginning_of_day..)
|
|
49
|
+
.where(tracked_at: ..range.to.end_of_day)
|
|
53
50
|
end
|
|
54
51
|
|
|
55
52
|
def apply_exact_filter(relation, key)
|
|
@@ -94,7 +91,7 @@ module LlmCostTracker
|
|
|
94
91
|
|
|
95
92
|
def matchable?(values)
|
|
96
93
|
values.none? { |value| value.include?("\0") } ||
|
|
97
|
-
!LlmCostTracker::Ledger::Schema::Adapter.postgresql?(scope.
|
|
94
|
+
!LlmCostTracker::Ledger::Schema::Adapter.postgresql?(scope.lease_connection)
|
|
98
95
|
end
|
|
99
96
|
|
|
100
97
|
def normalized_string(value, name)
|
|
@@ -2,29 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module LlmCostTracker
|
|
4
4
|
module Dashboard
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def status
|
|
5
|
+
class MonthlyBudget
|
|
6
|
+
def self.status
|
|
9
7
|
budget = LlmCostTracker.configuration.budgets.monthly
|
|
10
8
|
return nil unless budget
|
|
11
9
|
|
|
12
|
-
budget
|
|
13
|
-
|
|
14
|
-
month_start = now.beginning_of_month
|
|
15
|
-
month_end = now.end_of_month
|
|
16
|
-
spent = LlmCostTracker::Ledger::Period::Totals.call(%i[month], time: now).fetch(:month)
|
|
17
|
-
elapsed_seconds = now - month_start
|
|
18
|
-
total_seconds = month_end - month_start
|
|
19
|
-
projected_spent = if spent.zero? || !elapsed_seconds.positive?
|
|
20
|
-
spent
|
|
21
|
-
else
|
|
22
|
-
spent * (total_seconds / elapsed_seconds)
|
|
23
|
-
end
|
|
24
|
-
percent_used = budget.positive? ? (spent / budget) * 100.0 : 0.0
|
|
25
|
-
projected_percent_used = budget.positive? ? (projected_spent / budget) * 100.0 : 0.0
|
|
26
|
-
projected_delta = projected_spent - budget
|
|
10
|
+
new(budget.to_f, Time.now.utc).status
|
|
11
|
+
end
|
|
27
12
|
|
|
13
|
+
def initialize(budget, now)
|
|
14
|
+
@budget = budget
|
|
15
|
+
@now = now
|
|
16
|
+
@spent = LlmCostTracker::Ledger::Period::Totals.call(%i[month], time: now).fetch(:month)
|
|
17
|
+
@projected_spent = project(@spent)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def status
|
|
28
21
|
{
|
|
29
22
|
budget: budget,
|
|
30
23
|
spent: spent,
|
|
@@ -32,31 +25,45 @@ module LlmCostTracker
|
|
|
32
25
|
projected_spent: projected_spent,
|
|
33
26
|
projected_percent_used: projected_percent_used,
|
|
34
27
|
projected_delta: projected_delta,
|
|
35
|
-
projection_end_label:
|
|
36
|
-
fill_modifier: fill_modifier
|
|
37
|
-
progress_percent:
|
|
38
|
-
projected_marker_percent:
|
|
39
|
-
|
|
40
|
-
projected_delta_direction: projected_delta.positive? ? "over" : "under",
|
|
41
|
-
projected_delta_status_class: projected_delta_status_class(projected_delta)
|
|
28
|
+
projection_end_label: now.end_of_month.strftime("%b %-d"),
|
|
29
|
+
fill_modifier: fill_modifier,
|
|
30
|
+
progress_percent: percent_used.clamp(0.0, 100.0),
|
|
31
|
+
projected_marker_percent: projected_percent_used.clamp(0.0, 100.0),
|
|
32
|
+
**projected_delta_labels
|
|
42
33
|
}
|
|
43
34
|
end
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :budget, :now, :spent, :projected_spent
|
|
39
|
+
|
|
40
|
+
def project(amount)
|
|
41
|
+
month_start = now.beginning_of_month
|
|
42
|
+
elapsed_seconds = now - month_start
|
|
43
|
+
return amount if amount.zero? || !elapsed_seconds.positive?
|
|
44
|
+
|
|
45
|
+
amount * ((now.end_of_month - month_start) / elapsed_seconds)
|
|
47
46
|
end
|
|
48
47
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
def percent_used = share_of_budget(spent)
|
|
49
|
+
def projected_percent_used = share_of_budget(projected_spent)
|
|
50
|
+
def share_of_budget(amount) = budget.positive? ? (amount / budget) * 100.0 : 0.0
|
|
51
|
+
def projected_delta = projected_spent - budget
|
|
52
|
+
|
|
53
|
+
def fill_modifier
|
|
54
|
+
return "lct-budget-fill--over" if percent_used >= 100.0
|
|
55
|
+
return "lct-budget-fill--warn" if percent_used >= 80.0
|
|
52
56
|
|
|
53
57
|
""
|
|
54
58
|
end
|
|
55
59
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
def projected_delta_labels
|
|
61
|
+
direction = projected_delta.positive? ? "over" : "under"
|
|
62
|
+
{
|
|
63
|
+
projected_delta_amount: projected_delta.abs,
|
|
64
|
+
projected_delta_direction: direction,
|
|
65
|
+
projected_delta_status_class: "lct-budget-projection-status--#{direction}"
|
|
66
|
+
}
|
|
60
67
|
end
|
|
61
68
|
end
|
|
62
69
|
end
|
|
@@ -19,11 +19,8 @@ module LlmCostTracker
|
|
|
19
19
|
)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def self.integer_param(params, key, default:, min:, max:
|
|
23
|
-
|
|
24
|
-
value = [value, min].max
|
|
25
|
-
value = [value, max].min if max
|
|
26
|
-
value
|
|
22
|
+
def self.integer_param(params, key, default:, min:, max:)
|
|
23
|
+
Integer(params[key], 10).clamp(min, max)
|
|
27
24
|
rescue ArgumentError, TypeError
|
|
28
25
|
default
|
|
29
26
|
end
|
|
@@ -63,9 +63,10 @@ module LlmCostTracker
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def build_rows(prices)
|
|
66
|
+
today = Time.now.utc.to_date.iso8601
|
|
66
67
|
rows = prices.map do |key, rates|
|
|
67
68
|
provider, model = split_key(key.to_s)
|
|
68
|
-
Row.new(provider: provider, model: model, rates: rates)
|
|
69
|
+
Row.new(provider: provider, model: model, rates: Pricing::Matcher.prices_on(rates, today))
|
|
69
70
|
end
|
|
70
71
|
rows.sort_by { |row| [row.provider || "~", row.model] }
|
|
71
72
|
end
|
|
@@ -4,6 +4,15 @@ module LlmCostTracker
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
module Sort
|
|
6
6
|
DIRECTIONS = %w[asc desc].freeze
|
|
7
|
+
|
|
8
|
+
Choice = Data.define(:column, :direction)
|
|
9
|
+
|
|
10
|
+
def self.resolve(column, direction, natural_directions:, fallback:)
|
|
11
|
+
column = natural_directions.key?(column.to_s) ? column.to_s : fallback
|
|
12
|
+
direction = direction.to_s
|
|
13
|
+
direction = natural_directions.fetch(column) unless DIRECTIONS.include?(direction)
|
|
14
|
+
Choice.new(column: column, direction: direction)
|
|
15
|
+
end
|
|
7
16
|
end
|
|
8
17
|
end
|
|
9
18
|
end
|
|
@@ -28,18 +28,14 @@ module LlmCostTracker
|
|
|
28
28
|
attr_reader :scope, :from, :to
|
|
29
29
|
|
|
30
30
|
def alerts
|
|
31
|
-
|
|
32
|
-
daily_spend_by_model.each_with_object([]) do |((provider, model), daily_costs), rows|
|
|
31
|
+
daily_spend_by_model.filter_map do |(provider, model), daily_costs|
|
|
33
32
|
latest_spend = daily_costs.fetch(to, 0.0)
|
|
34
33
|
next unless latest_spend.positive?
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
mean
|
|
38
|
-
variance = baseline_days.sum { |value| (value - mean)**2 } / window_days
|
|
39
|
-
threshold = mean + (2 * Math.sqrt(variance))
|
|
40
|
-
next unless latest_spend > threshold
|
|
35
|
+
mean, deviation = baseline(daily_costs)
|
|
36
|
+
next unless latest_spend > mean + (2 * deviation)
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
{
|
|
43
39
|
provider: provider,
|
|
44
40
|
model: model,
|
|
45
41
|
day: to,
|
|
@@ -50,22 +46,28 @@ module LlmCostTracker
|
|
|
50
46
|
end
|
|
51
47
|
end
|
|
52
48
|
|
|
53
|
-
def
|
|
54
|
-
|
|
49
|
+
def baseline(daily_costs)
|
|
50
|
+
days = ((to - WINDOW_DAYS)...to).map { |day| daily_costs.fetch(day, 0.0) }
|
|
51
|
+
mean = days.sum / WINDOW_DAYS.to_f
|
|
52
|
+
variance = days.sum { |value| (value - mean)**2 } / WINDOW_DAYS.to_f
|
|
53
|
+
[mean, Math.sqrt(variance)]
|
|
54
|
+
end
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
def daily_spend_by_model
|
|
57
|
+
daily_costs = Hash.new { |hash, key| hash[key] = Hash.new(0.0) }
|
|
58
|
+
daily_totals.each do |(provider, model, day), total_cost|
|
|
59
|
+
daily_costs[[provider, model]][Date.iso8601(day.to_s)] += total_cost.to_f
|
|
60
|
+
end
|
|
61
|
+
daily_costs
|
|
62
|
+
end
|
|
57
63
|
|
|
64
|
+
def daily_totals
|
|
58
65
|
scope
|
|
59
|
-
.where(tracked_at:
|
|
66
|
+
.where(tracked_at: (to - WINDOW_DAYS).beginning_of_day..to.end_of_day)
|
|
60
67
|
.where.not(total_cost: nil)
|
|
61
68
|
.group(:provider, :model)
|
|
62
|
-
.group_by_period(:day)
|
|
69
|
+
.group_by_period(:day, time_zone: Time.zone)
|
|
63
70
|
.sum(:total_cost)
|
|
64
|
-
.each do |(provider, model, day), total_cost|
|
|
65
|
-
grouped[[provider, model]][Date.iso8601(day.to_s)] += total_cost.to_f
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
grouped
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Dashboard
|
|
5
|
+
class SpendChart
|
|
6
|
+
WIDTH = 1180
|
|
7
|
+
PADDING = { top: 16, right: 16, bottom: 28, left: 56 }.freeze
|
|
8
|
+
MIN_MAX_COST = 0.0001
|
|
9
|
+
|
|
10
|
+
attr_reader :points, :height, :y_ticks, :max_cost, :coords, :comparison_coords
|
|
11
|
+
|
|
12
|
+
def initialize(points, comparison_points: nil, height: 180, y_ticks: 3)
|
|
13
|
+
@points = points
|
|
14
|
+
@height = height
|
|
15
|
+
@y_ticks = y_ticks
|
|
16
|
+
costs = points.map { |point| point[:cost].to_f } + Array(comparison_points).map { |point| point[:cost].to_f }
|
|
17
|
+
@max_cost = [costs.max.to_f, MIN_MAX_COST].max
|
|
18
|
+
@coords = coords_for(points)
|
|
19
|
+
@comparison_coords = coords_for(comparison_points) if comparison_points.present?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def peak_index
|
|
23
|
+
@peak_index ||= points.each_with_index.max_by { |point, _| point[:cost].to_f }.last
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def left = PADDING[:left]
|
|
27
|
+
def right = left + plot_width
|
|
28
|
+
def baseline = PADDING[:top] + plot_height
|
|
29
|
+
def tick_y(index) = PADDING[:top] + (plot_height * index.to_f / y_ticks)
|
|
30
|
+
def tick_value(index) = max_cost * (y_ticks - index).to_f / y_ticks
|
|
31
|
+
|
|
32
|
+
def label_indexes
|
|
33
|
+
count = points.size
|
|
34
|
+
count <= 2 ? (0...count).to_a : [0, count / 2, count - 1].uniq
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def plot_width = WIDTH - PADDING[:left] - PADDING[:right]
|
|
40
|
+
def plot_height = height - PADDING[:top] - PADDING[:bottom]
|
|
41
|
+
|
|
42
|
+
def coords_for(series)
|
|
43
|
+
step = series.size > 1 ? plot_width.to_f / (series.size - 1) : 0.0
|
|
44
|
+
series.each_with_index.map do |point, index|
|
|
45
|
+
[left + (index * step), baseline - ((point[:cost].to_f / max_cost) * plot_height)]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -4,7 +4,6 @@ module LlmCostTracker
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
class TagBreakdown
|
|
6
6
|
DEFAULT_LIMIT = 100
|
|
7
|
-
SORT_OPTIONS = %w[value calls cost avg_cost].freeze
|
|
8
7
|
DEFAULT_DIRECTIONS = { "value" => "asc", "calls" => "desc", "cost" => "desc", "avg_cost" => "desc" }.freeze
|
|
9
8
|
Row = Data.define(:value, :calls, :total_cost, :average_cost_per_call, :share_percent)
|
|
10
9
|
|
|
@@ -21,8 +20,7 @@ module LlmCostTracker
|
|
|
21
20
|
@key = LlmCostTracker::Tags::Key.validate!(key, error_class: LlmCostTracker::InvalidFilterError)
|
|
22
21
|
limit = limit.to_i
|
|
23
22
|
@limit = limit.positive? ? [limit, DEFAULT_LIMIT].min : DEFAULT_LIMIT
|
|
24
|
-
@
|
|
25
|
-
@direction = Sort::DIRECTIONS.include?(direction.to_s) ? direction.to_s : DEFAULT_DIRECTIONS[@sort]
|
|
23
|
+
@order = Sort.resolve(sort, direction, natural_directions: DEFAULT_DIRECTIONS, fallback: "cost")
|
|
26
24
|
end
|
|
27
25
|
|
|
28
26
|
def rows
|
|
@@ -35,7 +33,7 @@ module LlmCostTracker
|
|
|
35
33
|
calls: calls,
|
|
36
34
|
total_cost: row.total_cost,
|
|
37
35
|
average_cost_per_call: row.average_cost_per_call,
|
|
38
|
-
share_percent:
|
|
36
|
+
share_percent: Percent.of(calls, total)
|
|
39
37
|
)
|
|
40
38
|
end
|
|
41
39
|
end
|
|
@@ -55,7 +53,7 @@ module LlmCostTracker
|
|
|
55
53
|
|
|
56
54
|
private
|
|
57
55
|
|
|
58
|
-
attr_reader :scope, :key, :
|
|
56
|
+
attr_reader :scope, :key, :order
|
|
59
57
|
|
|
60
58
|
def summary_counts
|
|
61
59
|
@summary_counts ||= scope.klass.find_by_sql(summary_sql).first
|
|
@@ -77,8 +75,8 @@ module LlmCostTracker
|
|
|
77
75
|
end
|
|
78
76
|
|
|
79
77
|
def order_clause
|
|
80
|
-
dir = direction.upcase
|
|
81
|
-
case
|
|
78
|
+
dir = order.direction.upcase
|
|
79
|
+
case order.column
|
|
82
80
|
when "value" then "#{tag_value_column} #{dir}"
|
|
83
81
|
when "calls" then "COUNT(*) #{dir}, total_cost DESC"
|
|
84
82
|
when "avg_cost" then "average_cost_per_call #{dir}, total_cost DESC"
|
|
@@ -89,7 +87,7 @@ module LlmCostTracker
|
|
|
89
87
|
def summary_sql
|
|
90
88
|
<<~SQL.squish
|
|
91
89
|
SELECT COUNT(*) AS total_calls,
|
|
92
|
-
COUNT(
|
|
90
|
+
COUNT(CASE WHEN #{tag_present_predicate} THEN 1 END) AS tagged_calls,
|
|
93
91
|
COUNT(DISTINCT CASE WHEN #{tag_present_predicate} THEN #{tag_value_column} END) AS distinct_values
|
|
94
92
|
FROM (#{scope.to_sql}) AS sub
|
|
95
93
|
LEFT OUTER JOIN #{call_tag_table} t ON t.llm_cost_tracker_call_id = sub.id AND t.#{quote_column('key')} = #{quoted_key}
|
|
@@ -109,11 +107,11 @@ module LlmCostTracker
|
|
|
109
107
|
end
|
|
110
108
|
|
|
111
109
|
def quote_column(name)
|
|
112
|
-
scope.
|
|
110
|
+
scope.lease_connection.quote_column_name(name)
|
|
113
111
|
end
|
|
114
112
|
|
|
115
113
|
def quoted_key
|
|
116
|
-
scope.
|
|
114
|
+
scope.lease_connection.quote(key)
|
|
117
115
|
end
|
|
118
116
|
end
|
|
119
117
|
end
|
|
@@ -13,16 +13,13 @@ module LlmCostTracker
|
|
|
13
13
|
|
|
14
14
|
def initialize(scope:, limit:)
|
|
15
15
|
@scope = scope
|
|
16
|
-
@connection = LlmCostTracker::Call.
|
|
16
|
+
@connection = LlmCostTracker::Call.lease_connection
|
|
17
17
|
limit = limit.to_i
|
|
18
18
|
@limit = limit.positive? ? [limit, DEFAULT_LIMIT].min : DEFAULT_LIMIT
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def rows
|
|
22
22
|
scope.klass.find_by_sql(build_sql)
|
|
23
|
-
rescue ActiveRecord::StatementInvalid => e
|
|
24
|
-
LlmCostTracker::Logging.warn("Tag key discovery failed (#{connection.adapter_name}): #{e.class}: #{e.message}")
|
|
25
|
-
[]
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
private
|
|
@@ -38,6 +35,7 @@ module LlmCostTracker
|
|
|
38
35
|
COUNT(DISTINCT t.#{value_column}) AS distinct_values
|
|
39
36
|
FROM (#{scope.to_sql}) AS sub
|
|
40
37
|
INNER JOIN #{tags_table} t ON t.llm_cost_tracker_call_id = sub.id
|
|
38
|
+
WHERE t.#{value_column} != ''
|
|
41
39
|
GROUP BY t.#{key_column}
|
|
42
40
|
ORDER BY calls_count DESC
|
|
43
41
|
LIMIT #{limit}
|
|
@@ -5,18 +5,16 @@ require "date"
|
|
|
5
5
|
module LlmCostTracker
|
|
6
6
|
module Dashboard
|
|
7
7
|
class TimeSeries
|
|
8
|
-
DEFAULT_DAYS = 30
|
|
9
|
-
|
|
10
8
|
class << self
|
|
11
|
-
def call(scope: LlmCostTracker::Call.all
|
|
9
|
+
def call(from:, to:, scope: LlmCostTracker::Call.all)
|
|
12
10
|
new(scope: scope, from: from, to: to).points
|
|
13
11
|
end
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
def initialize(scope:, from:, to:)
|
|
17
15
|
@scope = scope
|
|
16
|
+
@from = from.to_date
|
|
18
17
|
@to = to.to_date
|
|
19
|
-
@from = from ? from.to_date : (@to - (DEFAULT_DAYS - 1))
|
|
20
18
|
end
|
|
21
19
|
|
|
22
20
|
def points
|
|
@@ -35,7 +33,7 @@ module LlmCostTracker
|
|
|
35
33
|
def scoped_costs
|
|
36
34
|
scope
|
|
37
35
|
.where(tracked_at: from.beginning_of_day..to.end_of_day)
|
|
38
|
-
.group_by_period(:day)
|
|
36
|
+
.group_by_period(:day, time_zone: Time.zone)
|
|
39
37
|
.sum(:total_cost)
|
|
40
38
|
.transform_values(&:to_f)
|
|
41
39
|
end
|
|
@@ -4,7 +4,6 @@ module LlmCostTracker
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
class TopModels
|
|
6
6
|
DEFAULT_LIMIT = 5
|
|
7
|
-
SORT_OPTIONS = %w[cost calls avg_cost latency tokens provider name].freeze
|
|
8
7
|
DEFAULT_SORT = "cost"
|
|
9
8
|
DEFAULT_DIRECTIONS = {
|
|
10
9
|
"provider" => "asc",
|
|
@@ -43,21 +42,20 @@ module LlmCostTracker
|
|
|
43
42
|
def initialize(scope:, limit:, sort: DEFAULT_SORT, direction: nil)
|
|
44
43
|
@scope = scope
|
|
45
44
|
@limit = limit
|
|
46
|
-
@
|
|
47
|
-
@direction = Sort::DIRECTIONS.include?(direction.to_s) ? direction.to_s : DEFAULT_DIRECTIONS[@sort]
|
|
45
|
+
@order = Sort.resolve(sort, direction, natural_directions: DEFAULT_DIRECTIONS, fallback: DEFAULT_SORT)
|
|
48
46
|
end
|
|
49
47
|
|
|
50
48
|
def rows
|
|
51
49
|
scope
|
|
52
50
|
.group(:provider, :model)
|
|
53
51
|
.select(selects)
|
|
54
|
-
.order(*ORDER_NODES.fetch([
|
|
52
|
+
.order(*ORDER_NODES.fetch([order.column, order.direction]))
|
|
55
53
|
.then { |r| limit ? r.limit(limit) : r }
|
|
56
54
|
end
|
|
57
55
|
|
|
58
56
|
private
|
|
59
57
|
|
|
60
|
-
attr_reader :scope, :limit, :
|
|
58
|
+
attr_reader :scope, :limit, :order
|
|
61
59
|
|
|
62
60
|
def selects
|
|
63
61
|
columns = [
|
|
@@ -54,28 +54,28 @@
|
|
|
54
54
|
<aside class="lct-sidebar" aria-label="Dashboard">
|
|
55
55
|
<div class="lct-sidebar-inner">
|
|
56
56
|
<div class="lct-sidebar-section">Insights</div>
|
|
57
|
-
<%= link_to root_path,
|
|
57
|
+
<%= link_to root_path, sidebar_link_options(:overview) do %>
|
|
58
58
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg>
|
|
59
59
|
Overview
|
|
60
60
|
<% end %>
|
|
61
|
-
<%= link_to models_path,
|
|
61
|
+
<%= link_to models_path, sidebar_link_options(:models) do %>
|
|
62
62
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>
|
|
63
63
|
Models
|
|
64
64
|
<% end %>
|
|
65
|
-
<%= link_to calls_path,
|
|
65
|
+
<%= link_to calls_path, sidebar_link_options(:calls) do %>
|
|
66
66
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
67
67
|
Calls
|
|
68
68
|
<% end %>
|
|
69
|
-
<%= link_to tags_path,
|
|
69
|
+
<%= link_to tags_path, sidebar_link_options(:tags) do %>
|
|
70
70
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>
|
|
71
71
|
Tags
|
|
72
72
|
<% end %>
|
|
73
|
-
<%= link_to data_quality_path,
|
|
73
|
+
<%= link_to data_quality_path, sidebar_link_options(:data_quality) do %>
|
|
74
74
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
|
75
75
|
Data Quality
|
|
76
76
|
<% end %>
|
|
77
77
|
<div class="lct-sidebar-section">Reference</div>
|
|
78
|
-
<%= link_to pricing_path,
|
|
78
|
+
<%= link_to pricing_path, sidebar_link_options(:pricing) do %>
|
|
79
79
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
|
80
80
|
Pricing
|
|
81
81
|
<% end %>
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
<% end %>
|
|
20
20
|
|
|
21
21
|
<span class="lct-filter-row-meta"><%= number_with_delimiter(@calls_count) %> call<%= "s" unless @calls_count == 1 %> · <%= money(@calls_total_cost) %></span>
|
|
22
|
-
|
|
22
|
+
<% export_limit = LlmCostTracker::CallsController::CSV_EXPORT_LIMIT %>
|
|
23
|
+
<%= link_to(@calls_count > export_limit ? "Export CSV (first #{number_with_delimiter(export_limit)})" : "Export CSV", calls_path(current_query(format: :csv)), class: "lct-page-link", title: "Capped at #{number_with_delimiter(export_limit)} rows per request") %>
|
|
23
24
|
</div>
|
|
24
25
|
|
|
25
26
|
<% if @calls_count.zero? %>
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
<%
|
|
2
|
-
<% line_item_costs_by_component = call_line_item_costs_by_component(@call) %>
|
|
3
|
-
<% token_segments = priced_components.map do |component|
|
|
4
|
-
token_key = component.fetch(:token_key)
|
|
5
|
-
value = @call[token_key]
|
|
6
|
-
{ label: component.fetch(:label), value: value, formatted_value: number_with_delimiter(value), css_class: component.fetch(:css_class) }
|
|
7
|
-
end %>
|
|
8
|
-
<% cost_segments = [] %>
|
|
9
|
-
<% unless @call.total_cost.nil? %>
|
|
10
|
-
<% cost_segments = priced_components.map do |component|
|
|
11
|
-
value = line_item_costs_by_component[component.fetch(:price_key)]
|
|
12
|
-
{ label: component.fetch(:label), value: value, formatted_value: optional_money(value), css_class: component.fetch(:css_class) }
|
|
13
|
-
end %>
|
|
14
|
-
<% end %>
|
|
15
|
-
<% service_line_items = @call.line_items.reject { |li| li.unit == "token" }.sort_by(&:position) %>
|
|
1
|
+
<% service_line_items = call_service_line_items(@call) %>
|
|
16
2
|
|
|
17
3
|
<p class="lct-breadcrumb-back"><%= link_to "← All calls", calls_path %></p>
|
|
18
4
|
<h2 class="lct-page-title"><code class="lct-code-id"><%= @call.model %></code> <span class="lct-page-title-meta">#<%= @call.id %></span></h2>
|
|
@@ -45,7 +31,7 @@ end %>
|
|
|
45
31
|
<section class="lct-panel">
|
|
46
32
|
<div class="lct-panel-head"><h2 class="lct-panel-title">Token mix</h2></div>
|
|
47
33
|
<div class="lct-panel-body">
|
|
48
|
-
<%= render "llm_cost_tracker/shared/metric_stack", segments:
|
|
34
|
+
<%= render "llm_cost_tracker/shared/metric_stack", segments: call_token_segments(@call), empty_message: "No token data for this call." %>
|
|
49
35
|
</div>
|
|
50
36
|
</section>
|
|
51
37
|
|
|
@@ -55,7 +41,7 @@ end %>
|
|
|
55
41
|
<% if @call.total_cost.nil? %>
|
|
56
42
|
<p class="lct-stack-empty">Pricing not available for this call.</p>
|
|
57
43
|
<% else %>
|
|
58
|
-
<%= render "llm_cost_tracker/shared/metric_stack", segments:
|
|
44
|
+
<%= render "llm_cost_tracker/shared/metric_stack", segments: call_cost_segments(@call), empty_message: "No cost breakdown for this call." %>
|
|
59
45
|
<% end %>
|
|
60
46
|
</div>
|
|
61
47
|
</section>
|
|
@@ -84,22 +70,11 @@ end %>
|
|
|
84
70
|
<dd><%= content_tag(:code, @call.provider_response_id, class: "lct-code-id") %></dd>
|
|
85
71
|
</div>
|
|
86
72
|
<% end %>
|
|
87
|
-
<%
|
|
88
|
-
|
|
89
|
-
<dt>Project ID</dt>
|
|
90
|
-
<dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(:provider_project_id, @call.provider_project_id), class: "lct-code-id") %></dd>
|
|
91
|
-
</div>
|
|
92
|
-
<% end %>
|
|
93
|
-
<% if @call.provider_api_key_id.present? %>
|
|
94
|
-
<div class="lct-meta-strip-item">
|
|
95
|
-
<dt>API Key ID</dt>
|
|
96
|
-
<dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(:provider_api_key_id, @call.provider_api_key_id), class: "lct-code-id") %></dd>
|
|
97
|
-
</div>
|
|
98
|
-
<% end %>
|
|
99
|
-
<% if @call.provider_workspace_id.present? %>
|
|
73
|
+
<% { provider_project_id: "Project ID", provider_api_key_id: "API Key ID", provider_workspace_id: "Workspace ID" }.each do |attribute, label| %>
|
|
74
|
+
<% next if @call[attribute].blank? %>
|
|
100
75
|
<div class="lct-meta-strip-item">
|
|
101
|
-
<dt
|
|
102
|
-
<dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(
|
|
76
|
+
<dt><%= label %></dt>
|
|
77
|
+
<dd><%= content_tag(:code, LlmCostTracker::Dashboard::Masking.mask_value(attribute, @call[attribute]), class: "lct-code-id") %></dd>
|
|
103
78
|
</div>
|
|
104
79
|
<% end %>
|
|
105
80
|
</dl>
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
<h2 class="lct-panel-title">Incomplete pricing by model</h2>
|
|
272
272
|
<span class="lct-panel-meta"><%= link_to "View calls →", calls_path(current_query(cost_status: "incomplete", sort: nil)) %></span>
|
|
273
273
|
</div>
|
|
274
|
-
<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>
|
|
274
|
+
<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. A partial call is left as recorded when a rate it already used has changed since.</p>
|
|
275
275
|
|
|
276
276
|
<div class="lct-table-wrap">
|
|
277
277
|
<table class="lct-tbl">
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<% variant_class = local_assigns[:variant] == "budget" ? " lct-budget-fill" : "" %>
|
|
2
|
-
|
|
3
1
|
<div class="lct-bar-track" aria-hidden="true">
|
|
4
|
-
<div data-lct-style="<%= inline_style("width: #{bar_width(value, max)}") %>" class="lct-bar-fill
|
|
2
|
+
<div data-lct-style="<%= inline_style("width: #{bar_width(value, max)}") %>" class="lct-bar-fill"></div>
|
|
5
3
|
</div>
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<% extra_hidden = local_assigns.fetch(:extra_hidden, {}) %>
|
|
2
|
-
<% extra_except = local_assigns.fetch(:extra_except, []) %>
|
|
3
1
|
<details class="lct-filter-pop" name="lct-filter">
|
|
4
2
|
<summary class="lct-filter-pill">
|
|
5
3
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>
|
|
@@ -8,8 +6,7 @@
|
|
|
8
6
|
<svg class="lct-chev" width="10" height="6" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M1 1l4 4 4-4"/></svg>
|
|
9
7
|
</summary>
|
|
10
8
|
<%= form_with url: path, method: :get, local: true, html: { class: "lct-filter-pop-body" } do %>
|
|
11
|
-
|
|
12
|
-
<%= hidden_query_fields(current_query.except(:from, :to, :page, :per, *extra_except)) %>
|
|
9
|
+
<%= hidden_query_fields(current_query.except(:from, :to, :page, :per)) %>
|
|
13
10
|
<div class="lct-filter-pop-field"><label for="lct-filter-from">From</label><input type="date" name="from" id="lct-filter-from" value="<%= @from_date.iso8601 %>"></div>
|
|
14
11
|
<div class="lct-filter-pop-field"><label for="lct-filter-to">To</label><input type="date" name="to" id="lct-filter-to" value="<%= @to_date.iso8601 %>"></div>
|
|
15
12
|
<button type="submit" class="lct-button lct-button-primary">Apply</button>
|