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
|
@@ -45,7 +45,10 @@ module LlmCostTracker
|
|
|
45
45
|
def already_recorded?(provider:, provider_response_id:)
|
|
46
46
|
return false if provider_response_id.to_s.empty?
|
|
47
47
|
|
|
48
|
-
Ledger::Isolation.guard(self)
|
|
48
|
+
Ledger::Isolation.guard(self) do
|
|
49
|
+
where(provider: provider, provider_response_id: provider_response_id)
|
|
50
|
+
.where.not(usage_source: Usage::Source::UNKNOWN).exists?
|
|
51
|
+
end
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
def by_tag(key, value) = by_tags(key => value)
|
|
@@ -66,8 +69,8 @@ module LlmCostTracker
|
|
|
66
69
|
|
|
67
70
|
def cost_by_tag(key, limit: nil)
|
|
68
71
|
cost = qualified(:total_cost)
|
|
69
|
-
label = Ledger::Tags::Breakdown.label_sql(
|
|
70
|
-
raw_value = Ledger::Tags::Breakdown.raw_value_sql(
|
|
72
|
+
label = Ledger::Tags::Breakdown.label_sql(lease_connection)
|
|
73
|
+
raw_value = Ledger::Tags::Breakdown.raw_value_sql(lease_connection)
|
|
71
74
|
relation = Ledger::Tags::Breakdown.join_relation(self, key)
|
|
72
75
|
.select("#{label} AS name", "COALESCE(SUM(#{cost}), 0) AS total_cost")
|
|
73
76
|
.group(Arel.sql(label))
|
|
@@ -86,8 +89,14 @@ module LlmCostTracker
|
|
|
86
89
|
|
|
87
90
|
def latency_by_provider = group(:provider).average(:latency_ms).transform_values(&:to_f)
|
|
88
91
|
|
|
89
|
-
def group_by_period(period, column: :tracked_at)
|
|
90
|
-
|
|
92
|
+
def group_by_period(period, column: :tracked_at, time_zone: nil)
|
|
93
|
+
column = column.to_s
|
|
94
|
+
raise ArgumentError, "invalid period column: #{column.inspect}" unless column_names.include?(column)
|
|
95
|
+
|
|
96
|
+
bucket = Ledger::Schema::Adapter.period_bucket_sql(
|
|
97
|
+
lease_connection, period, qualified(column), time_zone: time_zone
|
|
98
|
+
)
|
|
99
|
+
group(Arel.sql(bucket))
|
|
91
100
|
end
|
|
92
101
|
|
|
93
102
|
def daily_costs(days: 30)
|
|
@@ -97,7 +106,7 @@ module LlmCostTracker
|
|
|
97
106
|
end
|
|
98
107
|
|
|
99
108
|
def qualified(column)
|
|
100
|
-
"#{quoted_table_name}.#{
|
|
109
|
+
"#{quoted_table_name}.#{lease_connection.quote_column_name(column)}"
|
|
101
110
|
end
|
|
102
111
|
|
|
103
112
|
private
|
|
@@ -109,17 +118,6 @@ module LlmCostTracker
|
|
|
109
118
|
relation = relation.limit(limit) if limit
|
|
110
119
|
relation
|
|
111
120
|
end
|
|
112
|
-
|
|
113
|
-
def period_group_expression(period, column:)
|
|
114
|
-
Ledger::Schema::Adapter.period_bucket_sql(connection, period, period_column_expression(column))
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def period_column_expression(column)
|
|
118
|
-
column = column.to_s
|
|
119
|
-
return "#{quoted_table_name}.#{connection.quote_column_name(column)}" if column_names.include?(column)
|
|
120
|
-
|
|
121
|
-
raise ArgumentError, "invalid period column: #{column.inspect}"
|
|
122
|
-
end
|
|
123
121
|
end
|
|
124
122
|
|
|
125
123
|
def tag_pairs
|
|
@@ -34,15 +34,15 @@ module LlmCostTracker
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def increment_on_duplicate
|
|
37
|
-
return Arel.sql(mysql_increment_sql) if Ledger::Schema::Adapter.mysql?(
|
|
38
|
-
return Arel.sql(postgres_increment_sql) if Ledger::Schema::Adapter.postgresql?(
|
|
37
|
+
return Arel.sql(mysql_increment_sql) if Ledger::Schema::Adapter.mysql?(lease_connection)
|
|
38
|
+
return Arel.sql(postgres_increment_sql) if Ledger::Schema::Adapter.postgresql?(lease_connection)
|
|
39
39
|
|
|
40
|
-
Ledger::Schema::Adapter.ensure_supported!(
|
|
40
|
+
Ledger::Schema::Adapter.ensure_supported!(lease_connection)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def postgres_increment_sql
|
|
44
|
-
total =
|
|
45
|
-
updated =
|
|
44
|
+
total = lease_connection.quote_column_name("total_cost")
|
|
45
|
+
updated = lease_connection.quote_column_name("updated_at")
|
|
46
46
|
"#{total} = #{quoted_table_name}.#{total} + excluded.#{total}, #{updated} = excluded.#{updated}"
|
|
47
47
|
end
|
|
48
48
|
|
|
@@ -51,7 +51,7 @@ module LlmCostTracker
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def increment_unique_by
|
|
54
|
-
return unless
|
|
54
|
+
return unless lease_connection.supports_insert_conflict_target?
|
|
55
55
|
|
|
56
56
|
%i[period period_start currency provider]
|
|
57
57
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Dashboard
|
|
5
|
+
module CallOrder
|
|
6
|
+
NATURAL_DIRECTIONS = {
|
|
7
|
+
"tracked_at" => "desc",
|
|
8
|
+
"provider" => "asc",
|
|
9
|
+
"model" => "asc",
|
|
10
|
+
"input" => "desc",
|
|
11
|
+
"output" => "desc",
|
|
12
|
+
"cost" => "desc",
|
|
13
|
+
"latency" => "desc"
|
|
14
|
+
}.freeze
|
|
15
|
+
NEWEST_FIRST = { tracked_at: :desc, id: :desc }.freeze
|
|
16
|
+
COST_NULLS_LAST = Arel.sql("CASE WHEN total_cost IS NULL THEN 1 ELSE 0 END ASC")
|
|
17
|
+
LATENCY_NULLS_LAST = Arel.sql("CASE WHEN latency_ms IS NULL THEN 1 ELSE 0 END ASC")
|
|
18
|
+
ORDERS = {
|
|
19
|
+
"tracked_at" => ->(dir) { [{ tracked_at: dir, id: dir }] },
|
|
20
|
+
"provider" => ->(dir) { [{ provider: dir, model: :asc, **NEWEST_FIRST }] },
|
|
21
|
+
"model" => ->(dir) { [{ model: dir, **NEWEST_FIRST }] },
|
|
22
|
+
"input" => ->(dir) { [{ input_tokens: dir, **NEWEST_FIRST }] },
|
|
23
|
+
"output" => ->(dir) { [{ output_tokens: dir, **NEWEST_FIRST }] },
|
|
24
|
+
"cost" => ->(dir) { [COST_NULLS_LAST, { total_cost: dir, **NEWEST_FIRST }] },
|
|
25
|
+
"latency" => ->(dir) { [LATENCY_NULLS_LAST, { latency_ms: dir, **NEWEST_FIRST }] }
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
def self.call(scope, sort:, direction:)
|
|
29
|
+
choice = Sort.resolve(
|
|
30
|
+
sort, direction.to_s.downcase, natural_directions: NATURAL_DIRECTIONS, fallback: "tracked_at"
|
|
31
|
+
)
|
|
32
|
+
scope.order(*ORDERS.fetch(choice.column).call(choice.direction.to_sym))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module LlmCostTracker
|
|
7
|
+
module Dashboard
|
|
8
|
+
class CallsExport
|
|
9
|
+
MYSQL_HINT = "NO_SKIP_SCAN(llm_cost_tracker_calls)"
|
|
10
|
+
FORMULA_PREFIXES = ["=", "+", "-", "@", "\t", "\r"].freeze
|
|
11
|
+
MASKED_FIELDS = %i[provider_api_key_id provider_workspace_id provider_project_id].freeze
|
|
12
|
+
TEXT_FIELDS = %i[provider model provider_response_id cost_status].freeze
|
|
13
|
+
FIELDS = [
|
|
14
|
+
:tracked_at, :provider, :model, *Usage::TokenUsage.members, :total_cost, :cost_status, :pricing_snapshot,
|
|
15
|
+
:latency_ms, :provider_response_id, :provider_project_id, :provider_api_key_id, :provider_workspace_id,
|
|
16
|
+
:batch, :tags
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
def self.call(relation, limit:, batch_size:)
|
|
20
|
+
new(relation, limit: limit, batch_size: batch_size).to_s
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(relation, limit:, batch_size:)
|
|
24
|
+
@relation = relation
|
|
25
|
+
@limit = limit
|
|
26
|
+
@batch_size = batch_size
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_s
|
|
30
|
+
CSV.generate do |csv|
|
|
31
|
+
csv << FIELDS.map(&:to_s)
|
|
32
|
+
each_call { |call| csv << FIELDS.map { |field| value(field, call) } }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :relation, :limit, :batch_size
|
|
39
|
+
|
|
40
|
+
def each_call(&block)
|
|
41
|
+
scope = relation.limit(limit)
|
|
42
|
+
scope = scope.optimizer_hints(MYSQL_HINT) if Ledger::Schema::Adapter.mysql?(scope.lease_connection)
|
|
43
|
+
scope.pluck(:id).each_slice(batch_size) do |ids|
|
|
44
|
+
calls = LlmCostTracker::Call.where(id: ids).preload(:tag_records).index_by(&:id)
|
|
45
|
+
calls.values_at(*ids).compact.each(&block)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def value(field, call)
|
|
50
|
+
case field
|
|
51
|
+
when :tracked_at then call.tracked_at.utc.iso8601
|
|
52
|
+
when *MASKED_FIELDS then formula_safe(Masking.mask_value(field, call[field]))
|
|
53
|
+
when *TEXT_FIELDS then formula_safe(call[field])
|
|
54
|
+
when :pricing_snapshot then formula_safe(Hash(call.pricing_snapshot).deep_stringify_keys.to_json)
|
|
55
|
+
when :tags then formula_safe(call.tag_pairs.to_json)
|
|
56
|
+
else call[field]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def formula_safe(value)
|
|
61
|
+
return if value.nil?
|
|
62
|
+
|
|
63
|
+
string = value.to_s
|
|
64
|
+
FORMULA_PREFIXES.include?(string.lstrip[0]) ? "'#{string}" : string
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Dashboard
|
|
5
|
+
module DataQuality
|
|
6
|
+
class StatsQuery
|
|
7
|
+
MISSING_RESPONSE_ID = "provider_response_id IS NULL OR provider_response_id = ''"
|
|
8
|
+
|
|
9
|
+
def self.call(scope)
|
|
10
|
+
new(scope).call
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(scope)
|
|
14
|
+
@scope = scope
|
|
15
|
+
@connection = scope.lease_connection
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
scope.unscope(:order).select(selects.join(", ")).take
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :scope, :connection
|
|
25
|
+
|
|
26
|
+
def selects
|
|
27
|
+
[
|
|
28
|
+
"COUNT(*) AS total_calls",
|
|
29
|
+
"#{count_where(Charges::CostStatus.unknown_pricing_sql)} AS unknown_pricing_count",
|
|
30
|
+
"COUNT(*) - #{count_where(tagged_predicate)} AS untagged_calls_count",
|
|
31
|
+
"#{count_where('latency_ms IS NULL')} AS missing_latency_count",
|
|
32
|
+
"#{count_where('stream')} AS streaming_count",
|
|
33
|
+
"#{count_where(streaming_missing_usage_predicate)} AS streaming_missing_usage_count",
|
|
34
|
+
"#{count_where(MISSING_RESPONSE_ID)} AS missing_provider_response_id_count",
|
|
35
|
+
*token_sums,
|
|
36
|
+
"#{billable_tokens} AS billable_tokens",
|
|
37
|
+
"#{hidden_output_share} AS hidden_output_share"
|
|
38
|
+
]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def token_sums
|
|
42
|
+
[*Usage::Catalog.token_priced.map(&:token_key), :hidden_output_tokens].map do |column|
|
|
43
|
+
"#{column_sum(column)} AS #{column}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def billable_tokens
|
|
48
|
+
Usage::Catalog.token_priced.map { |component| column_sum(component.token_key) }.join(" + ")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def hidden_output_share
|
|
52
|
+
output = column_sum(:output_tokens)
|
|
53
|
+
"CASE WHEN #{output} > 0 THEN #{column_sum(:hidden_output_tokens)} * 100.0 / #{output} ELSE 0 END"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def tagged_predicate
|
|
57
|
+
tags = LlmCostTracker::CallTag.quoted_table_name
|
|
58
|
+
calls = scope.klass.quoted_table_name
|
|
59
|
+
"EXISTS (SELECT 1 FROM #{tags} WHERE #{tags}.llm_cost_tracker_call_id = #{calls}.id " \
|
|
60
|
+
"AND #{tags}.#{connection.quote_column_name('value')} != '')"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def streaming_missing_usage_predicate
|
|
64
|
+
"stream AND (#{DataQuality.unknown_usage_sql(connection)})"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def column_sum(column)
|
|
68
|
+
"COALESCE(SUM(#{connection.quote_column_name(column)}), 0)"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def count_where(predicate)
|
|
72
|
+
"COALESCE(SUM(CASE WHEN #{predicate} THEN 1 ELSE 0 END), 0)"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Dashboard
|
|
5
|
+
module DataQuality
|
|
6
|
+
Summary = ::Data.define(
|
|
7
|
+
:total,
|
|
8
|
+
:unknown_pricing_count,
|
|
9
|
+
:untagged_calls_count,
|
|
10
|
+
:missing_latency_count,
|
|
11
|
+
:streaming_count,
|
|
12
|
+
:streaming_missing_usage,
|
|
13
|
+
:missing_provider_response_id_count
|
|
14
|
+
) do
|
|
15
|
+
def self.from_stats(stats)
|
|
16
|
+
new(
|
|
17
|
+
total: stats.total_calls.to_i,
|
|
18
|
+
unknown_pricing_count: stats.unknown_pricing_count.to_i,
|
|
19
|
+
untagged_calls_count: stats.untagged_calls_count.to_i,
|
|
20
|
+
missing_latency_count: stats.missing_latency_count.to_i,
|
|
21
|
+
streaming_count: stats.streaming_count.to_i,
|
|
22
|
+
streaming_missing_usage: stats.streaming_missing_usage_count.to_i,
|
|
23
|
+
missing_provider_response_id_count: stats.missing_provider_response_id_count.to_i
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def calls_with_pricing = total - unknown_pricing_count
|
|
28
|
+
def tagged_calls = total - untagged_calls_count
|
|
29
|
+
def calls_with_latency = total - missing_latency_count
|
|
30
|
+
def streams_with_usage = streaming_count - streaming_missing_usage
|
|
31
|
+
def calls_with_provider_response_id = total - missing_provider_response_id_count
|
|
32
|
+
|
|
33
|
+
def unknown_pricing_share = Percent.of(unknown_pricing_count, total)
|
|
34
|
+
def untagged_share = Percent.of(untagged_calls_count, total)
|
|
35
|
+
def missing_latency_share = Percent.of(missing_latency_count, total)
|
|
36
|
+
def streaming_share = Percent.of(streaming_count, total)
|
|
37
|
+
def streaming_missing_usage_share = Percent.of(streaming_missing_usage, streaming_count)
|
|
38
|
+
|
|
39
|
+
def cost_coverage = Percent.of(calls_with_pricing, total)
|
|
40
|
+
def tag_coverage = Percent.of(tagged_calls, total)
|
|
41
|
+
def latency_coverage = Percent.of(calls_with_latency, total)
|
|
42
|
+
def stream_coverage = Percent.of(streams_with_usage, streaming_count)
|
|
43
|
+
def provider_response_id_coverage = Percent.of(calls_with_provider_response_id, total)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -7,37 +7,22 @@ module LlmCostTracker
|
|
|
7
7
|
QuarantinedInbox = ::Data.define(:count, :total_cost)
|
|
8
8
|
UnseenBudgetTags = ::Data.define(:keys)
|
|
9
9
|
StreamingHealthRow = ::Data.define(:provider, :streams, :with_usage, :unknown, :unknown_share)
|
|
10
|
-
Summary = ::Data.define(:total,
|
|
11
|
-
:unknown_pricing_count,
|
|
12
|
-
:untagged_calls_count,
|
|
13
|
-
:missing_latency_count,
|
|
14
|
-
:streaming_count,
|
|
15
|
-
:streaming_missing_usage,
|
|
16
|
-
:missing_provider_response_id_count,
|
|
17
|
-
:calls_with_pricing,
|
|
18
|
-
:tagged_calls,
|
|
19
|
-
:calls_with_latency,
|
|
20
|
-
:streams_with_usage,
|
|
21
|
-
:calls_with_provider_response_id,
|
|
22
|
-
:unknown_pricing_share,
|
|
23
|
-
:untagged_share,
|
|
24
|
-
:missing_latency_share,
|
|
25
|
-
:streaming_share,
|
|
26
|
-
:streaming_missing_usage_share,
|
|
27
|
-
:cost_coverage,
|
|
28
|
-
:tag_coverage,
|
|
29
|
-
:latency_coverage,
|
|
30
|
-
:stream_coverage,
|
|
31
|
-
:provider_response_id_coverage)
|
|
32
10
|
|
|
33
11
|
class << self
|
|
34
12
|
def call(scope: LlmCostTracker::Call.all)
|
|
35
|
-
|
|
13
|
+
StatsQuery.call(scope)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def summary(stats)
|
|
17
|
+
Summary.from_stats(stats)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def unknown_usage_sql(connection)
|
|
21
|
+
"usage_source = #{connection.quote(LlmCostTracker::Usage::Source::UNKNOWN)} OR usage_source IS NULL"
|
|
36
22
|
end
|
|
37
23
|
|
|
38
24
|
def quarantined_inbox
|
|
39
|
-
return nil unless Ingestion.async?
|
|
40
|
-
return nil unless Ingestion::InboxEntry.table_exists?
|
|
25
|
+
return nil unless Ingestion.async? && Ingestion::InboxEntry.table_exists?
|
|
41
26
|
|
|
42
27
|
row = Ingestion::InboxEntry
|
|
43
28
|
.quarantined
|
|
@@ -48,54 +33,10 @@ module LlmCostTracker
|
|
|
48
33
|
|
|
49
34
|
def unseen_budget_tags
|
|
50
35
|
budgeted = Budget::PerTag.configured
|
|
51
|
-
return nil if budgeted.empty?
|
|
52
|
-
return nil unless Budget::PerTag.columns?
|
|
53
|
-
return nil unless LlmCostTracker::CallTag.exists?
|
|
54
|
-
|
|
55
|
-
unseen = budgeted.keys.reject { |key| LlmCostTracker::CallTag.exists?(key: key) }
|
|
56
|
-
return nil if unseen.empty?
|
|
57
|
-
|
|
58
|
-
UnseenBudgetTags.new(keys: unseen)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def summary(stats)
|
|
62
|
-
total = stats.total_calls.to_i
|
|
63
|
-
unknown_pricing_count = stats.unknown_pricing_count.to_i
|
|
64
|
-
untagged_calls_count = stats.untagged_calls_count.to_i
|
|
65
|
-
missing_latency_count = stats.missing_latency_count.to_i
|
|
66
|
-
streaming_count = stats.streaming_count.to_i
|
|
67
|
-
streaming_missing_usage = stats.streaming_missing_usage_count.to_i
|
|
68
|
-
missing_provider_response_id_count = stats.missing_provider_response_id_count.to_i
|
|
69
|
-
calls_with_pricing = total - unknown_pricing_count
|
|
70
|
-
tagged_calls = total - untagged_calls_count
|
|
71
|
-
calls_with_latency = total - missing_latency_count
|
|
72
|
-
streams_with_usage = streaming_count - streaming_missing_usage
|
|
73
|
-
calls_with_provider_response_id = total - missing_provider_response_id_count
|
|
36
|
+
return nil if budgeted.empty? || !Budget::PerTag.columns? || !LlmCostTracker::CallTag.exists?
|
|
74
37
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
unknown_pricing_count,
|
|
78
|
-
untagged_calls_count,
|
|
79
|
-
missing_latency_count,
|
|
80
|
-
streaming_count,
|
|
81
|
-
streaming_missing_usage,
|
|
82
|
-
missing_provider_response_id_count,
|
|
83
|
-
calls_with_pricing,
|
|
84
|
-
tagged_calls,
|
|
85
|
-
calls_with_latency,
|
|
86
|
-
streams_with_usage,
|
|
87
|
-
calls_with_provider_response_id,
|
|
88
|
-
percentage(unknown_pricing_count, total),
|
|
89
|
-
percentage(untagged_calls_count, total),
|
|
90
|
-
percentage(missing_latency_count, total),
|
|
91
|
-
percentage(streaming_count, total),
|
|
92
|
-
percentage(streaming_missing_usage, streaming_count),
|
|
93
|
-
percentage(calls_with_pricing, total),
|
|
94
|
-
percentage(tagged_calls, total),
|
|
95
|
-
percentage(calls_with_latency, total),
|
|
96
|
-
percentage(streams_with_usage, streaming_count),
|
|
97
|
-
percentage(calls_with_provider_response_id, total)
|
|
98
|
-
)
|
|
38
|
+
unseen = budgeted.keys.reject { |key| LlmCostTracker::CallTag.where(key: key).where.not(value: "").exists? }
|
|
39
|
+
UnseenBudgetTags.new(keys: unseen) unless unseen.empty?
|
|
99
40
|
end
|
|
100
41
|
|
|
101
42
|
def unknown_pricing_by_model(scope, total_calls:)
|
|
@@ -109,103 +50,57 @@ module LlmCostTracker
|
|
|
109
50
|
UnknownPricingRow.new(provider: row.provider,
|
|
110
51
|
model: row.model,
|
|
111
52
|
calls: calls,
|
|
112
|
-
share_percent:
|
|
53
|
+
share_percent: Percent.of(calls, total_calls))
|
|
113
54
|
end
|
|
114
55
|
end
|
|
115
56
|
|
|
116
57
|
def service_charge_rows(scope)
|
|
117
58
|
call_table = LlmCostTracker::Call.quoted_table_name
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
.
|
|
126
|
-
.order(Arel.sql("COALESCE(SUM(#{line_item_table}.cost), 0) DESC"), Arel.sql("COUNT(*) DESC"))
|
|
59
|
+
item_table = LlmCostTracker::CallLineItem.quoted_table_name
|
|
60
|
+
|
|
61
|
+
LlmCostTracker::CallLineItem
|
|
62
|
+
.where.not(unit: "token")
|
|
63
|
+
.joins(:call)
|
|
64
|
+
.merge(scope.unscope(:select, :order))
|
|
65
|
+
.group("#{call_table}.provider", "#{item_table}.kind", "#{item_table}.cost_status")
|
|
66
|
+
.order(Arel.sql("COALESCE(SUM(#{item_table}.cost), 0) DESC"), Arel.sql("COUNT(*) DESC"))
|
|
127
67
|
.select(
|
|
128
68
|
"#{call_table}.provider AS provider",
|
|
129
|
-
"#{
|
|
130
|
-
"#{
|
|
69
|
+
"#{item_table}.kind AS component",
|
|
70
|
+
"#{item_table}.cost_status AS cost_status",
|
|
131
71
|
"COUNT(*) AS charges_count",
|
|
132
|
-
"COALESCE(SUM(#{
|
|
133
|
-
"COALESCE(SUM(#{
|
|
72
|
+
"COALESCE(SUM(#{item_table}.quantity), 0) AS quantity",
|
|
73
|
+
"COALESCE(SUM(#{item_table}.cost), 0) AS total_cost"
|
|
134
74
|
)
|
|
135
75
|
.limit(10)
|
|
136
76
|
end
|
|
137
77
|
|
|
78
|
+
def component_costs(scope)
|
|
79
|
+
item_table = LlmCostTracker::CallLineItem.quoted_table_name
|
|
80
|
+
dimensions = %w[kind direction cache_state].map { |column| "#{item_table}.#{column}" }
|
|
81
|
+
rows = LlmCostTracker::CallLineItem
|
|
82
|
+
.where(unit: "token")
|
|
83
|
+
.joins(:call)
|
|
84
|
+
.merge(scope.unscope(:select, :order, :group))
|
|
85
|
+
.group(*dimensions)
|
|
86
|
+
.pluck(*dimensions.map { |column| Arel.sql(column) }, Arel.sql("COALESCE(SUM(#{item_table}.cost), 0)"))
|
|
87
|
+
Usage::Catalog.costs_by_component(rows)
|
|
88
|
+
end
|
|
89
|
+
|
|
138
90
|
def usage_rows(stats, component_costs: {})
|
|
139
91
|
billable_tokens = stats.billable_tokens.to_f
|
|
140
|
-
|
|
141
92
|
rows = Usage::Catalog.token_priced.map do |component|
|
|
142
93
|
token_value = stats[component.token_key].to_i
|
|
143
|
-
|
|
144
94
|
{
|
|
145
|
-
price_key: component.key,
|
|
146
95
|
token_key: component.token_key,
|
|
147
96
|
cost_key: component.cost_key,
|
|
148
97
|
token_value: token_value,
|
|
149
98
|
cost_value: component_costs[component.key],
|
|
150
|
-
share_percent:
|
|
99
|
+
share_percent: Percent.of(token_value, billable_tokens),
|
|
151
100
|
share_basis: nil
|
|
152
101
|
}
|
|
153
102
|
end
|
|
154
|
-
|
|
155
|
-
rows + [
|
|
156
|
-
{
|
|
157
|
-
price_key: nil,
|
|
158
|
-
token_key: :hidden_output_tokens,
|
|
159
|
-
cost_key: nil,
|
|
160
|
-
token_value: stats.hidden_output_tokens.to_i,
|
|
161
|
-
cost_value: nil,
|
|
162
|
-
share_percent: stats.hidden_output_share.to_f,
|
|
163
|
-
share_basis: :output
|
|
164
|
-
}
|
|
165
|
-
]
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def component_costs(scope)
|
|
169
|
-
line_item_table = LlmCostTracker::CallLineItem.quoted_table_name
|
|
170
|
-
rows = LlmCostTracker::CallLineItem
|
|
171
|
-
.where(unit: "token")
|
|
172
|
-
.joins(:call)
|
|
173
|
-
.merge(scope.unscope(:select, :order, :group))
|
|
174
|
-
.group("#{line_item_table}.kind",
|
|
175
|
-
"#{line_item_table}.direction",
|
|
176
|
-
"#{line_item_table}.cache_state")
|
|
177
|
-
.pluck(Arel.sql("#{line_item_table}.kind"),
|
|
178
|
-
Arel.sql("#{line_item_table}.direction"),
|
|
179
|
-
Arel.sql("#{line_item_table}.cache_state"),
|
|
180
|
-
Arel.sql("COALESCE(SUM(#{line_item_table}.cost), 0)"))
|
|
181
|
-
index_costs_by_component(rows)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def streaming_health_rows(scope, total_streaming:)
|
|
185
|
-
return [] unless total_streaming.positive?
|
|
186
|
-
|
|
187
|
-
unknown_predicate = unknown_usage_source_predicate(scope)
|
|
188
|
-
rows = scope.unscope(:select, :order, :group)
|
|
189
|
-
.where(stream: true)
|
|
190
|
-
.group(:provider)
|
|
191
|
-
.order(Arel.sql("COUNT(*) DESC"), :provider)
|
|
192
|
-
.pluck(
|
|
193
|
-
:provider,
|
|
194
|
-
Arel.sql("COUNT(*)"),
|
|
195
|
-
Arel.sql("SUM(CASE WHEN #{unknown_predicate} THEN 1 ELSE 0 END)")
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
rows.map do |provider, streams, unknown|
|
|
199
|
-
streams_count = streams.to_i
|
|
200
|
-
unknown_count = unknown.to_i
|
|
201
|
-
StreamingHealthRow.new(
|
|
202
|
-
provider: provider,
|
|
203
|
-
streams: streams_count,
|
|
204
|
-
with_usage: streams_count - unknown_count,
|
|
205
|
-
unknown: unknown_count,
|
|
206
|
-
unknown_share: percentage(unknown_count, streams_count)
|
|
207
|
-
)
|
|
208
|
-
end
|
|
103
|
+
rows << hidden_output_usage_row(stats)
|
|
209
104
|
end
|
|
210
105
|
|
|
211
106
|
def hidden_output_summary(stats)
|
|
@@ -219,89 +114,39 @@ module LlmCostTracker
|
|
|
219
114
|
}
|
|
220
115
|
end
|
|
221
116
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def index_costs_by_component(rows)
|
|
225
|
-
rows.each_with_object({}) do |(kind, direction, cache_state, cost), accumulator|
|
|
226
|
-
component = Usage::Catalog.token_priced_for(kind: kind, direction: direction, cache_state: cache_state)
|
|
227
|
-
accumulator[component.key] = cost if component
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def percentage(numerator, denominator)
|
|
232
|
-
return 0.0 unless denominator.positive?
|
|
233
|
-
|
|
234
|
-
(numerator.to_f / denominator) * 100.0
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def aggregate_selects(scope)
|
|
238
|
-
unknown_pricing = Charges::CostStatus.unknown_pricing_sql
|
|
239
|
-
selects = [
|
|
240
|
-
"COUNT(*) AS total_calls",
|
|
241
|
-
"#{conditional_count_sql(unknown_pricing)} AS unknown_pricing_count",
|
|
242
|
-
"#{tagged_calls_sql(scope)} AS tagged_calls_count",
|
|
243
|
-
"COUNT(*) - #{tagged_calls_sql(scope)} AS untagged_calls_count",
|
|
244
|
-
"#{conditional_count_sql('latency_ms IS NULL')} AS missing_latency_count",
|
|
245
|
-
"#{conditional_count_sql('stream')} AS streaming_count",
|
|
246
|
-
"#{streaming_missing_usage_select(scope)} AS streaming_missing_usage_count",
|
|
247
|
-
"#{provider_response_id_select} AS missing_provider_response_id_count"
|
|
248
|
-
]
|
|
249
|
-
|
|
250
|
-
usage_sum_columns.each do |column|
|
|
251
|
-
selects << "#{column_sum(scope, column)} AS #{column}"
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
selects << "#{billable_tokens_select(scope)} AS billable_tokens"
|
|
255
|
-
selects << "#{hidden_output_share_select(scope)} AS hidden_output_share"
|
|
256
|
-
|
|
257
|
-
selects.join(", ")
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def usage_sum_columns
|
|
261
|
-
Usage::Catalog.token_priced.map(&:token_key) + [:hidden_output_tokens]
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def billable_tokens_select(scope)
|
|
265
|
-
Usage::Catalog.token_priced
|
|
266
|
-
.map { |component| column_sum(scope, component.token_key) }
|
|
267
|
-
.join(" + ")
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def hidden_output_share_select(scope)
|
|
271
|
-
hidden_output = column_sum(scope, :hidden_output_tokens)
|
|
272
|
-
output = column_sum(scope, :output_tokens)
|
|
273
|
-
|
|
274
|
-
"CASE WHEN #{output} > 0 THEN #{hidden_output} * 100.0 / #{output} ELSE 0 END"
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def unknown_usage_source_predicate(scope)
|
|
278
|
-
quoted = scope.connection.quote(LlmCostTracker::Usage::Source::UNKNOWN)
|
|
279
|
-
"usage_source = #{quoted} OR usage_source IS NULL"
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
def column_sum(scope, column)
|
|
283
|
-
"COALESCE(SUM(#{scope.connection.quote_column_name(column)}), 0)"
|
|
284
|
-
end
|
|
117
|
+
def streaming_health_rows(scope, total_streaming:)
|
|
118
|
+
return [] unless total_streaming.positive?
|
|
285
119
|
|
|
286
|
-
|
|
287
|
-
|
|
120
|
+
unknown_count = Arel.sql("SUM(CASE WHEN #{unknown_usage_sql(scope.lease_connection)} THEN 1 ELSE 0 END)")
|
|
121
|
+
scope.unscope(:select, :order, :group)
|
|
122
|
+
.where(stream: true)
|
|
123
|
+
.group(:provider)
|
|
124
|
+
.order(Arel.sql("COUNT(*) DESC"), :provider)
|
|
125
|
+
.pluck(:provider, Arel.sql("COUNT(*)"), unknown_count)
|
|
126
|
+
.map { |provider, streams, unknown| streaming_health_row(provider, streams.to_i, unknown.to_i) }
|
|
288
127
|
end
|
|
289
128
|
|
|
290
|
-
|
|
291
|
-
conditional_count_sql("stream AND (#{unknown_usage_source_predicate(scope)})")
|
|
292
|
-
end
|
|
129
|
+
private
|
|
293
130
|
|
|
294
|
-
def
|
|
295
|
-
|
|
296
|
-
|
|
131
|
+
def hidden_output_usage_row(stats)
|
|
132
|
+
{
|
|
133
|
+
token_key: :hidden_output_tokens,
|
|
134
|
+
cost_key: nil,
|
|
135
|
+
token_value: stats.hidden_output_tokens.to_i,
|
|
136
|
+
cost_value: nil,
|
|
137
|
+
share_percent: stats.hidden_output_share.to_f,
|
|
138
|
+
share_basis: :output
|
|
139
|
+
}
|
|
297
140
|
end
|
|
298
141
|
|
|
299
|
-
def
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
142
|
+
def streaming_health_row(provider, streams, unknown)
|
|
143
|
+
StreamingHealthRow.new(
|
|
144
|
+
provider: provider,
|
|
145
|
+
streams: streams,
|
|
146
|
+
with_usage: streams - unknown,
|
|
147
|
+
unknown: unknown,
|
|
148
|
+
unknown_share: Percent.of(unknown, streams)
|
|
149
|
+
)
|
|
305
150
|
end
|
|
306
151
|
end
|
|
307
152
|
end
|