llm_cost_tracker 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +7 -4
- data/app/assets/llm_cost_tracker/application.css +8 -7
- data/app/controllers/llm_cost_tracker/calls_controller.rb +5 -5
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -1
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +1 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +6 -15
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +4 -4
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +4 -6
- data/app/models/llm_cost_tracker/call.rb +28 -63
- data/app/models/llm_cost_tracker/call_line_item.rb +2 -2
- data/app/models/llm_cost_tracker/call_rollup.rb +38 -0
- data/app/models/llm_cost_tracker/call_tag.rb +0 -2
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +2 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +64 -43
- data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -0
- data/app/services/llm_cost_tracker/dashboard/masking.rb +31 -0
- data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +63 -0
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +5 -71
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
- data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +30 -44
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +4 -60
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +1 -7
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
- data/app/views/layouts/llm_cost_tracker/application.html.erb +0 -6
- data/app/views/llm_cost_tracker/calls/index.html.erb +8 -8
- data/app/views/llm_cost_tracker/calls/show.html.erb +31 -23
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +8 -8
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +62 -117
- data/app/views/llm_cost_tracker/models/index.html.erb +5 -5
- data/app/views/llm_cost_tracker/pricing/index.html.erb +2 -2
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -3
- data/app/views/llm_cost_tracker/tags/show.html.erb +10 -10
- data/config/routes.rb +2 -3
- data/lib/llm_cost_tracker/budget.rb +24 -26
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
- data/lib/llm_cost_tracker/capture/sse.rb +1 -0
- data/lib/llm_cost_tracker/capture/stream_collector.rb +28 -36
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +17 -28
- data/lib/llm_cost_tracker/capture_verifier.rb +59 -0
- data/lib/llm_cost_tracker/charges/cost.rb +27 -0
- data/lib/llm_cost_tracker/{billing → charges}/cost_status.rb +14 -4
- data/lib/llm_cost_tracker/{billing → charges}/line_item.rb +40 -44
- data/lib/llm_cost_tracker/check.rb +5 -0
- data/lib/llm_cost_tracker/configuration.rb +13 -44
- data/lib/llm_cost_tracker/currency.rb +5 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +15 -49
- data/lib/llm_cost_tracker/doctor/price_check.rb +1 -1
- data/lib/llm_cost_tracker/doctor/probe.rb +3 -4
- data/lib/llm_cost_tracker/doctor/schema_check.rb +3 -6
- data/lib/llm_cost_tracker/doctor.rb +5 -69
- data/lib/llm_cost_tracker/engine.rb +4 -4
- data/lib/llm_cost_tracker/event.rb +12 -20
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -3
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +5 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -5
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +3 -2
- data/lib/llm_cost_tracker/ingestion/batch.rb +39 -8
- data/lib/llm_cost_tracker/ingestion/inbox.rb +7 -8
- data/lib/llm_cost_tracker/ingestion/pool.rb +3 -11
- data/lib/llm_cost_tracker/ingestion/worker.rb +7 -17
- data/lib/llm_cost_tracker/ingestion.rb +24 -36
- data/lib/llm_cost_tracker/integrations/anthropic.rb +92 -106
- data/lib/llm_cost_tracker/integrations/base.rb +39 -57
- data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +84 -0
- data/lib/llm_cost_tracker/integrations/openai/patches.rb +81 -0
- data/lib/llm_cost_tracker/integrations/openai.rb +70 -276
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +87 -99
- data/lib/llm_cost_tracker/integrations.rb +32 -25
- data/lib/llm_cost_tracker/ledger/period/totals.rb +27 -42
- data/lib/llm_cost_tracker/ledger/period.rb +5 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +67 -98
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +12 -13
- data/lib/llm_cost_tracker/ledger/schema/base.rb +51 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +24 -79
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +3 -35
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +4 -41
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +30 -99
- data/lib/llm_cost_tracker/ledger/schema/ingestion/inbox_entries.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/ingestion/leases.rb +17 -0
- data/lib/llm_cost_tracker/ledger/schema.rb +26 -0
- data/lib/llm_cost_tracker/ledger/store.rb +18 -42
- data/lib/llm_cost_tracker/ledger/tags/{sql.rb → breakdown.rb} +1 -1
- data/lib/llm_cost_tracker/ledger/tags/encoding.rb +4 -6
- data/lib/llm_cost_tracker/ledger.rb +8 -18
- data/lib/llm_cost_tracker/logging.rb +4 -21
- data/lib/llm_cost_tracker/middleware/faraday.rb +61 -50
- data/lib/llm_cost_tracker/parsers.rb +139 -26
- data/lib/llm_cost_tracker/prices.json +1707 -1
- data/lib/llm_cost_tracker/pricing/backfill.rb +52 -80
- data/lib/llm_cost_tracker/pricing/calculation.rb +260 -0
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +17 -18
- data/lib/llm_cost_tracker/pricing/estimator.rb +2 -2
- data/lib/llm_cost_tracker/pricing/matcher.rb +84 -0
- data/lib/llm_cost_tracker/pricing/mode.rb +40 -52
- data/lib/llm_cost_tracker/pricing/price_key.rb +56 -0
- data/lib/llm_cost_tracker/pricing/rate.rb +18 -0
- data/lib/llm_cost_tracker/pricing/registry.rb +189 -100
- data/lib/llm_cost_tracker/pricing/service_rates.rb +69 -0
- data/lib/llm_cost_tracker/pricing/source.rb +7 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +2 -3
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +4 -10
- data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +10 -3
- data/lib/llm_cost_tracker/pricing/sync.rb +9 -11
- data/lib/llm_cost_tracker/pricing/unknown.rb +1 -5
- data/lib/llm_cost_tracker/pricing.rb +10 -278
- data/lib/llm_cost_tracker/providers/anthropic/parser.rb +93 -0
- data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +30 -0
- data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -4
- data/lib/llm_cost_tracker/providers/azure/parser.rb +44 -0
- data/lib/llm_cost_tracker/providers/gemini/model_families.rb +1 -4
- data/lib/llm_cost_tracker/providers/gemini/parser.rb +177 -0
- data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +76 -0
- data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -7
- data/lib/llm_cost_tracker/providers/openai/model_families.rb +5 -8
- data/lib/llm_cost_tracker/providers/openai/parser.rb +39 -0
- data/lib/llm_cost_tracker/providers/openai/response_parser.rb +152 -0
- data/lib/llm_cost_tracker/providers/openai/service_charges.rb +63 -39
- data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +72 -0
- data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +36 -0
- data/lib/llm_cost_tracker/providers.rb +35 -0
- data/lib/llm_cost_tracker/railtie.rb +0 -3
- data/lib/llm_cost_tracker/report/data.rb +3 -4
- data/lib/llm_cost_tracker/report/formatter.rb +1 -1
- data/lib/llm_cost_tracker/report.rb +1 -1
- data/lib/llm_cost_tracker/retention.rb +6 -19
- data/lib/llm_cost_tracker/tags/context.rb +9 -6
- data/lib/llm_cost_tracker/tags/sanitizer.rb +10 -0
- data/lib/llm_cost_tracker/timing.rb +2 -4
- data/lib/llm_cost_tracker/tracker.rb +24 -36
- data/lib/llm_cost_tracker/usage/catalog.rb +58 -0
- data/lib/llm_cost_tracker/usage/dimension.rb +21 -0
- data/lib/llm_cost_tracker/{billing/components.yml → usage/dimensions.yml} +24 -46
- data/lib/llm_cost_tracker/usage/source.rb +14 -0
- data/lib/llm_cost_tracker/usage/token_usage.rb +100 -0
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +43 -52
- data/lib/tasks/llm_cost_tracker.rake +14 -73
- metadata +81 -55
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -100
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +0 -28
- data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice.rb +0 -13
- data/app/models/llm_cost_tracker/provider_invoice_import.rb +0 -29
- data/app/views/llm_cost_tracker/reconciliation/index.html.erb +0 -174
- data/lib/llm_cost_tracker/billing/components.rb +0 -95
- data/lib/llm_cost_tracker/capture/stream.rb +0 -9
- data/lib/llm_cost_tracker/doctor/capture_verifier.rb +0 -61
- data/lib/llm_cost_tracker/doctor/check.rb +0 -7
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +0 -56
- data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +0 -164
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -34
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -20
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +0 -85
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +0 -34
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +0 -60
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +0 -36
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -27
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +0 -31
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +0 -31
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +0 -40
- data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +0 -57
- data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +0 -52
- data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +0 -56
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +0 -72
- data/lib/llm_cost_tracker/masking.rb +0 -39
- data/lib/llm_cost_tracker/parsers/anthropic.rb +0 -176
- data/lib/llm_cost_tracker/parsers/azure.rb +0 -46
- data/lib/llm_cost_tracker/parsers/base.rb +0 -131
- data/lib/llm_cost_tracker/parsers/gemini.rb +0 -230
- data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -45
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +0 -228
- data/lib/llm_cost_tracker/pricing/explainer.rb +0 -74
- data/lib/llm_cost_tracker/pricing/lookup.rb +0 -236
- data/lib/llm_cost_tracker/pricing/service_charges.rb +0 -206
- data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +0 -15
- data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +0 -22
- data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -131
- data/lib/llm_cost_tracker/reconciliation/diff.rb +0 -409
- data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -44
- data/lib/llm_cost_tracker/reconciliation/import_result.rb +0 -19
- data/lib/llm_cost_tracker/reconciliation/importer.rb +0 -249
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -148
- data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +0 -40
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -118
- data/lib/llm_cost_tracker/reconciliation.rb +0 -118
- data/lib/llm_cost_tracker/token_usage.rb +0 -93
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "llm_cost_tracker/billing/components"
|
|
4
|
-
require "llm_cost_tracker/ledger/schema/adapter"
|
|
5
|
-
|
|
6
3
|
module LlmCostTracker
|
|
7
4
|
module Dashboard
|
|
8
|
-
|
|
5
|
+
module DataQuality
|
|
9
6
|
UnknownPricingRow = ::Data.define(:provider, :model, :calls, :share_percent)
|
|
10
7
|
StreamingHealthRow = ::Data.define(:provider, :streams, :with_usage, :unknown, :unknown_share)
|
|
11
|
-
Summary = ::Data.define(:total,
|
|
12
|
-
:
|
|
13
|
-
:
|
|
14
|
-
:
|
|
15
|
-
:
|
|
16
|
-
:
|
|
8
|
+
Summary = ::Data.define(:total,
|
|
9
|
+
:unknown_pricing_count,
|
|
10
|
+
:untagged_calls_count,
|
|
11
|
+
:missing_latency_count,
|
|
12
|
+
:streaming_count,
|
|
13
|
+
:streaming_missing_usage,
|
|
14
|
+
:missing_provider_response_id_count,
|
|
15
|
+
:calls_with_pricing,
|
|
16
|
+
:tagged_calls,
|
|
17
|
+
:calls_with_latency,
|
|
18
|
+
:streams_with_usage,
|
|
19
|
+
:calls_with_provider_response_id,
|
|
20
|
+
:unknown_pricing_share,
|
|
21
|
+
:untagged_share,
|
|
22
|
+
:missing_latency_share,
|
|
23
|
+
:streaming_share,
|
|
24
|
+
:streaming_missing_usage_share,
|
|
25
|
+
:cost_coverage,
|
|
26
|
+
:tag_coverage,
|
|
27
|
+
:latency_coverage,
|
|
28
|
+
:stream_coverage,
|
|
17
29
|
:provider_response_id_coverage)
|
|
18
30
|
|
|
19
31
|
class << self
|
|
@@ -36,14 +48,28 @@ module LlmCostTracker
|
|
|
36
48
|
calls_with_provider_response_id = total - missing_provider_response_id_count
|
|
37
49
|
|
|
38
50
|
Summary.new(
|
|
39
|
-
total,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
total,
|
|
52
|
+
unknown_pricing_count,
|
|
53
|
+
untagged_calls_count,
|
|
54
|
+
missing_latency_count,
|
|
55
|
+
streaming_count,
|
|
56
|
+
streaming_missing_usage,
|
|
57
|
+
missing_provider_response_id_count,
|
|
58
|
+
calls_with_pricing,
|
|
59
|
+
tagged_calls,
|
|
60
|
+
calls_with_latency,
|
|
61
|
+
streams_with_usage,
|
|
62
|
+
calls_with_provider_response_id,
|
|
63
|
+
percentage(unknown_pricing_count, total),
|
|
64
|
+
percentage(untagged_calls_count, total),
|
|
65
|
+
percentage(missing_latency_count, total),
|
|
66
|
+
percentage(streaming_count, total),
|
|
67
|
+
percentage(streaming_missing_usage, streaming_count),
|
|
68
|
+
percentage(calls_with_pricing, total),
|
|
69
|
+
percentage(tagged_calls, total),
|
|
70
|
+
percentage(calls_with_latency, total),
|
|
71
|
+
percentage(streams_with_usage, streaming_count),
|
|
72
|
+
percentage(calls_with_provider_response_id, total)
|
|
47
73
|
)
|
|
48
74
|
end
|
|
49
75
|
|
|
@@ -55,7 +81,9 @@ module LlmCostTracker
|
|
|
55
81
|
.limit(10)
|
|
56
82
|
.map do |row|
|
|
57
83
|
calls = row.calls.to_i
|
|
58
|
-
UnknownPricingRow.new(provider: row.provider,
|
|
84
|
+
UnknownPricingRow.new(provider: row.provider,
|
|
85
|
+
model: row.model,
|
|
86
|
+
calls: calls,
|
|
59
87
|
share_percent: percentage(calls, total_calls))
|
|
60
88
|
end
|
|
61
89
|
end
|
|
@@ -85,7 +113,7 @@ module LlmCostTracker
|
|
|
85
113
|
def usage_rows(stats, component_costs: {})
|
|
86
114
|
billable_tokens = stats.billable_tokens.to_f
|
|
87
115
|
|
|
88
|
-
rows =
|
|
116
|
+
rows = Usage::Catalog.token_priced.map do |component|
|
|
89
117
|
token_value = stats[component.token_key].to_i
|
|
90
118
|
|
|
91
119
|
{
|
|
@@ -118,7 +146,8 @@ module LlmCostTracker
|
|
|
118
146
|
.where(unit: "token")
|
|
119
147
|
.joins(:call)
|
|
120
148
|
.merge(scope.unscope(:select, :order, :group))
|
|
121
|
-
.group("#{line_item_table}.kind",
|
|
149
|
+
.group("#{line_item_table}.kind",
|
|
150
|
+
"#{line_item_table}.direction",
|
|
122
151
|
"#{line_item_table}.cache_state")
|
|
123
152
|
.pluck(Arel.sql("#{line_item_table}.kind"),
|
|
124
153
|
Arel.sql("#{line_item_table}.direction"),
|
|
@@ -130,7 +159,7 @@ module LlmCostTracker
|
|
|
130
159
|
def streaming_health_rows(scope, total_streaming:)
|
|
131
160
|
return [] unless total_streaming.positive?
|
|
132
161
|
|
|
133
|
-
unknown_predicate =
|
|
162
|
+
unknown_predicate = unknown_usage_source_predicate(scope)
|
|
134
163
|
rows = scope.unscope(:select, :order, :group)
|
|
135
164
|
.where(stream: true)
|
|
136
165
|
.group(:provider)
|
|
@@ -169,11 +198,7 @@ module LlmCostTracker
|
|
|
169
198
|
|
|
170
199
|
def index_costs_by_component(rows)
|
|
171
200
|
rows.each_with_object({}) do |(kind, direction, cache_state, cost), accumulator|
|
|
172
|
-
component =
|
|
173
|
-
item.kind.to_s == kind.to_s &&
|
|
174
|
-
item.direction.to_s == direction.to_s &&
|
|
175
|
-
item.cache_state.to_s == cache_state.to_s
|
|
176
|
-
end
|
|
201
|
+
component = Usage::Catalog.token_priced_for(kind: kind, direction: direction, cache_state: cache_state)
|
|
177
202
|
accumulator[component.key] = cost if component
|
|
178
203
|
end
|
|
179
204
|
end
|
|
@@ -185,14 +210,15 @@ module LlmCostTracker
|
|
|
185
210
|
end
|
|
186
211
|
|
|
187
212
|
def aggregate_selects(scope)
|
|
213
|
+
unknown_pricing = Charges::CostStatus.unknown_pricing_sql
|
|
188
214
|
selects = [
|
|
189
215
|
"COUNT(*) AS total_calls",
|
|
190
|
-
"#{conditional_count_sql(
|
|
216
|
+
"#{conditional_count_sql(unknown_pricing)} AS unknown_pricing_count",
|
|
191
217
|
"#{tagged_calls_sql(scope)} AS tagged_calls_count",
|
|
192
218
|
"COUNT(*) - #{tagged_calls_sql(scope)} AS untagged_calls_count",
|
|
193
219
|
"#{conditional_count_sql('latency_ms IS NULL')} AS missing_latency_count",
|
|
194
220
|
"#{conditional_count_sql('stream')} AS streaming_count",
|
|
195
|
-
"#{streaming_missing_usage_select} AS streaming_missing_usage_count",
|
|
221
|
+
"#{streaming_missing_usage_select(scope)} AS streaming_missing_usage_count",
|
|
196
222
|
"#{provider_response_id_select} AS missing_provider_response_id_count"
|
|
197
223
|
]
|
|
198
224
|
|
|
@@ -207,13 +233,13 @@ module LlmCostTracker
|
|
|
207
233
|
end
|
|
208
234
|
|
|
209
235
|
def usage_sum_columns
|
|
210
|
-
|
|
236
|
+
Usage::Catalog.token_priced.map(&:token_key) + [:hidden_output_tokens]
|
|
211
237
|
end
|
|
212
238
|
|
|
213
239
|
def billable_tokens_select(scope)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
240
|
+
Usage::Catalog.token_priced
|
|
241
|
+
.map { |component| column_sum(scope, component.token_key) }
|
|
242
|
+
.join(" + ")
|
|
217
243
|
end
|
|
218
244
|
|
|
219
245
|
def hidden_output_share_select(scope)
|
|
@@ -223,13 +249,9 @@ module LlmCostTracker
|
|
|
223
249
|
"CASE WHEN #{output} > 0 THEN #{hidden_output} * 100.0 / #{output} ELSE 0 END"
|
|
224
250
|
end
|
|
225
251
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
LlmCostTracker::Billing::CostStatus::PARTIAL
|
|
230
|
-
].map { |value| scope.connection.quote(value) }
|
|
231
|
-
|
|
232
|
-
"total_cost IS NULL OR cost_status IN (#{values.join(', ')})"
|
|
252
|
+
def unknown_usage_source_predicate(scope)
|
|
253
|
+
quoted = scope.connection.quote(LlmCostTracker::Usage::Source::UNKNOWN)
|
|
254
|
+
"usage_source = #{quoted} OR usage_source IS NULL"
|
|
233
255
|
end
|
|
234
256
|
|
|
235
257
|
def column_sum(scope, column)
|
|
@@ -240,9 +262,8 @@ module LlmCostTracker
|
|
|
240
262
|
"COALESCE(SUM(CASE WHEN #{predicate} THEN 1 ELSE 0 END), 0)"
|
|
241
263
|
end
|
|
242
264
|
|
|
243
|
-
def streaming_missing_usage_select
|
|
244
|
-
|
|
245
|
-
conditional_count_sql(predicate)
|
|
265
|
+
def streaming_missing_usage_select(scope)
|
|
266
|
+
conditional_count_sql("stream AND (#{unknown_usage_source_predicate(scope)})")
|
|
246
267
|
end
|
|
247
268
|
|
|
248
269
|
def provider_response_id_select
|
|
@@ -5,6 +5,11 @@ require "date"
|
|
|
5
5
|
module LlmCostTracker
|
|
6
6
|
module Dashboard
|
|
7
7
|
class Filter
|
|
8
|
+
STREAM_FILTER_OPTIONS = [
|
|
9
|
+
["Streaming only", "yes"],
|
|
10
|
+
["Non-streaming only", "no"]
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
8
13
|
class << self
|
|
9
14
|
def call(scope: LlmCostTracker::Call.all, params: {})
|
|
10
15
|
new(scope: scope, params: params).relation
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Dashboard
|
|
5
|
+
module Masking
|
|
6
|
+
SENSITIVE_KEYS = %w[provider_api_key_id provider_workspace_id provider_project_id].freeze
|
|
7
|
+
MASK_TAIL_LENGTH = 4
|
|
8
|
+
|
|
9
|
+
def self.mask_value(key, value)
|
|
10
|
+
string = value.to_s
|
|
11
|
+
return string unless SENSITIVE_KEYS.include?(key.to_s)
|
|
12
|
+
return string if string.length <= MASK_TAIL_LENGTH
|
|
13
|
+
|
|
14
|
+
"***#{string[-MASK_TAIL_LENGTH, MASK_TAIL_LENGTH]}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.mask_hash(hash)
|
|
18
|
+
return hash unless hash.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
hash.each_with_object({}) do |(key, value), masked|
|
|
21
|
+
masked[key] = case value
|
|
22
|
+
when Hash then mask_hash(value)
|
|
23
|
+
when Array then value.map { |entry| entry.is_a?(Hash) ? mask_hash(entry) : entry }
|
|
24
|
+
else
|
|
25
|
+
mask_value(key, value)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module Dashboard
|
|
5
|
+
module MonthlyBudget
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def status
|
|
9
|
+
budget = LlmCostTracker.configuration.monthly_budget
|
|
10
|
+
return nil unless budget
|
|
11
|
+
|
|
12
|
+
budget = budget.to_f
|
|
13
|
+
now = Time.now.utc
|
|
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
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
budget: budget,
|
|
30
|
+
spent: spent,
|
|
31
|
+
percent_used: percent_used,
|
|
32
|
+
projected_spent: projected_spent,
|
|
33
|
+
projected_percent_used: projected_percent_used,
|
|
34
|
+
projected_delta: projected_delta,
|
|
35
|
+
projection_end_label: month_end.strftime("%b %-d"),
|
|
36
|
+
fill_modifier: fill_modifier(percent_used),
|
|
37
|
+
progress_percent: clamped_percent(percent_used),
|
|
38
|
+
projected_marker_percent: clamped_percent(projected_percent_used),
|
|
39
|
+
projected_delta_amount: projected_delta.abs,
|
|
40
|
+
projected_delta_direction: projected_delta.positive? ? "over" : "under",
|
|
41
|
+
projected_delta_status_class: projected_delta_status_class(projected_delta)
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def clamped_percent(value)
|
|
46
|
+
value.clamp(0.0, 100.0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fill_modifier(percent)
|
|
50
|
+
return "lct-budget-fill--over" if percent >= 100.0
|
|
51
|
+
return "lct-budget-fill--warn" if percent >= 80.0
|
|
52
|
+
|
|
53
|
+
""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def projected_delta_status_class(delta)
|
|
57
|
+
return "lct-budget-projection-status--over" if delta.positive?
|
|
58
|
+
|
|
59
|
+
"lct-budget-projection-status--under"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "llm_cost_tracker/ledger"
|
|
4
|
-
|
|
5
3
|
module LlmCostTracker
|
|
6
4
|
module Dashboard
|
|
7
|
-
|
|
5
|
+
module OverviewStats
|
|
8
6
|
class << self
|
|
9
7
|
def call(scope: LlmCostTracker::Call.all, previous_scope: nil)
|
|
10
8
|
return scope.select(aggregate_selects).take unless previous_scope
|
|
@@ -16,48 +14,6 @@ module LlmCostTracker
|
|
|
16
14
|
.take
|
|
17
15
|
end
|
|
18
16
|
|
|
19
|
-
def monthly_budget_status
|
|
20
|
-
budget = LlmCostTracker.configuration.monthly_budget
|
|
21
|
-
return nil unless budget
|
|
22
|
-
|
|
23
|
-
budget = budget.to_f
|
|
24
|
-
now = Time.now.utc
|
|
25
|
-
month_start = now.beginning_of_month
|
|
26
|
-
month_end = now.end_of_month
|
|
27
|
-
spent = LlmCostTracker::Ledger::Period::Totals.call(%i[month], time: now).fetch(:month)
|
|
28
|
-
elapsed_seconds = now - month_start
|
|
29
|
-
total_seconds = month_end - month_start
|
|
30
|
-
projected_spent = if spent.zero? || !elapsed_seconds.positive?
|
|
31
|
-
spent
|
|
32
|
-
else
|
|
33
|
-
spent * (total_seconds / elapsed_seconds)
|
|
34
|
-
end
|
|
35
|
-
percent_used = budget.positive? ? (spent / budget) * 100.0 : 0.0
|
|
36
|
-
projected_percent_used = budget.positive? ? (projected_spent / budget) * 100.0 : 0.0
|
|
37
|
-
projected_delta = projected_spent - budget
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
budget: budget,
|
|
41
|
-
spent: spent,
|
|
42
|
-
percent_used: percent_used,
|
|
43
|
-
projected_spent: projected_spent,
|
|
44
|
-
projected_percent_used: projected_percent_used,
|
|
45
|
-
projected_delta: projected_delta,
|
|
46
|
-
projection_end_label: month_end.strftime("%b %-d"),
|
|
47
|
-
fill_modifier: budget_fill_modifier(percent_used),
|
|
48
|
-
progress_percent: clamped_percent(percent_used),
|
|
49
|
-
projected_marker_percent: clamped_percent(projected_percent_used),
|
|
50
|
-
projected_delta_amount: projected_delta.abs,
|
|
51
|
-
projected_delta_direction: projected_delta.positive? ? "over" : "under",
|
|
52
|
-
projected_delta_status_class: projected_delta_status_class(projected_delta)
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
UNKNOWN_PRICING_COST_STATUSES = [
|
|
57
|
-
LlmCostTracker::Billing::CostStatus::UNKNOWN,
|
|
58
|
-
LlmCostTracker::Billing::CostStatus::PARTIAL
|
|
59
|
-
].freeze
|
|
60
|
-
|
|
61
17
|
private
|
|
62
18
|
|
|
63
19
|
def aggregate_selects(table_name: nil, previous: false)
|
|
@@ -69,11 +25,10 @@ module LlmCostTracker
|
|
|
69
25
|
THEN COALESCE(SUM(#{total_cost}), 0) * 1.0 / COUNT(*)
|
|
70
26
|
ELSE 0 END
|
|
71
27
|
SQL
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
SQL
|
|
28
|
+
predicate = LlmCostTracker::Charges::CostStatus.unknown_pricing_sql(
|
|
29
|
+
total_cost: total_cost, cost_status: cost_status
|
|
30
|
+
)
|
|
31
|
+
unknown_pricing_sql = "SUM(CASE WHEN #{predicate} THEN 1 ELSE 0 END)"
|
|
77
32
|
selects = [
|
|
78
33
|
"COUNT(*) AS total_calls",
|
|
79
34
|
"COALESCE(SUM(#{total_cost}), 0) AS total_cost",
|
|
@@ -85,10 +40,6 @@ module LlmCostTracker
|
|
|
85
40
|
selects.join(", ")
|
|
86
41
|
end
|
|
87
42
|
|
|
88
|
-
def connection
|
|
89
|
-
LlmCostTracker::Call.connection
|
|
90
|
-
end
|
|
91
|
-
|
|
92
43
|
def previous_selects(previous)
|
|
93
44
|
unless previous
|
|
94
45
|
return [
|
|
@@ -125,23 +76,6 @@ module LlmCostTracker
|
|
|
125
76
|
.select("COALESCE(SUM(total_cost), 0) AS total_cost", "COUNT(*) AS total_calls")
|
|
126
77
|
.to_sql
|
|
127
78
|
end
|
|
128
|
-
|
|
129
|
-
def clamped_percent(value)
|
|
130
|
-
value.clamp(0.0, 100.0)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def budget_fill_modifier(percent)
|
|
134
|
-
return "lct-budget-fill--over" if percent >= 100.0
|
|
135
|
-
return "lct-budget-fill--warn" if percent >= 80.0
|
|
136
|
-
|
|
137
|
-
""
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def projected_delta_status_class(delta)
|
|
141
|
-
return "lct-budget-projection-status--over" if delta.positive?
|
|
142
|
-
|
|
143
|
-
"lct-budget-projection-status--under"
|
|
144
|
-
end
|
|
145
79
|
end
|
|
146
80
|
end
|
|
147
81
|
end
|
|
@@ -6,6 +6,7 @@ module LlmCostTracker
|
|
|
6
6
|
DEFAULT_PER = 50
|
|
7
7
|
MAX_PER = 200
|
|
8
8
|
MIN_PAGE = 1
|
|
9
|
+
MIN_PER = 1
|
|
9
10
|
|
|
10
11
|
attr_reader :page, :per
|
|
11
12
|
|
|
@@ -13,7 +14,7 @@ module LlmCostTracker
|
|
|
13
14
|
params = Params.to_hash(params).symbolize_keys
|
|
14
15
|
new(
|
|
15
16
|
page: integer_param(params, :page, default: MIN_PAGE, min: MIN_PAGE),
|
|
16
|
-
per: integer_param(params, :per, default: DEFAULT_PER, min:
|
|
17
|
+
per: integer_param(params, :per, default: DEFAULT_PER, min: MIN_PER, max: MAX_PER)
|
|
17
18
|
)
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -33,10 +34,6 @@ module LlmCostTracker
|
|
|
33
34
|
freeze
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
def limit
|
|
37
|
-
per
|
|
38
|
-
end
|
|
39
|
-
|
|
40
37
|
def offset
|
|
41
38
|
(page - 1) * per
|
|
42
39
|
end
|
|
@@ -4,9 +4,13 @@ module LlmCostTracker
|
|
|
4
4
|
module Dashboard
|
|
5
5
|
class PricingOverview
|
|
6
6
|
SOURCES = %i[overrides file bundled].freeze
|
|
7
|
-
RATE_COLUMNS = %
|
|
7
|
+
RATE_COLUMNS = %w[input output cache_read_input cache_write_input batch_input batch_output].freeze
|
|
8
8
|
Row = Data.define(:provider, :model, :rates)
|
|
9
9
|
|
|
10
|
+
SOURCE_NAME = { overrides: "pricing_overrides", file: "prices_file", bundled: "bundled" }.freeze
|
|
11
|
+
LABEL = { overrides: "Overrides", file: "Custom file", bundled: "Bundled" }.freeze
|
|
12
|
+
private_constant :SOURCE_NAME, :LABEL
|
|
13
|
+
|
|
10
14
|
class << self
|
|
11
15
|
def call
|
|
12
16
|
new.call
|
|
@@ -14,9 +18,9 @@ module LlmCostTracker
|
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
def call
|
|
17
|
-
sources = SOURCES.each_with_object({}) do |
|
|
18
|
-
|
|
19
|
-
acc[
|
|
21
|
+
sources = SOURCES.each_with_object({}) do |key, acc|
|
|
22
|
+
source = sources_by_name.fetch(SOURCE_NAME.fetch(key))
|
|
23
|
+
acc[key] = present(key, source) unless source.prices.empty?
|
|
20
24
|
end
|
|
21
25
|
{
|
|
22
26
|
sources: sources,
|
|
@@ -26,54 +30,36 @@ module LlmCostTracker
|
|
|
26
30
|
|
|
27
31
|
private
|
|
28
32
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
when :overrides then build_overrides
|
|
32
|
-
when :file then build_file
|
|
33
|
-
when :bundled then build_bundled
|
|
34
|
-
end
|
|
33
|
+
def sources_by_name
|
|
34
|
+
@sources_by_name ||= Pricing::Registry.sources.to_h { |source| [source.name, source] }
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def
|
|
38
|
-
prices = LlmCostTracker.configuration.pricing_overrides
|
|
39
|
-
return nil if prices.nil? || prices.empty?
|
|
40
|
-
|
|
37
|
+
def present(key, source)
|
|
41
38
|
{
|
|
42
|
-
label:
|
|
43
|
-
subtitle:
|
|
44
|
-
updated_at:
|
|
45
|
-
currency:
|
|
46
|
-
rows: build_rows(prices)
|
|
39
|
+
label: LABEL.fetch(key),
|
|
40
|
+
subtitle: subtitle_for(key),
|
|
41
|
+
updated_at: updated_at_for(key),
|
|
42
|
+
currency: source.currency,
|
|
43
|
+
rows: build_rows(source.prices)
|
|
47
44
|
}
|
|
48
45
|
end
|
|
49
46
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
meta = Pricing::Registry.file_metadata(path)
|
|
58
|
-
{
|
|
59
|
-
label: "Custom file",
|
|
60
|
-
subtitle: path.to_s,
|
|
61
|
-
updated_at: meta["updated_at"] || Pricing::Lookup.prices_file_mtime_iso,
|
|
62
|
-
currency: meta["currency"] || Pricing::Lookup::DEFAULT_CURRENCY,
|
|
63
|
-
rows: build_rows(prices)
|
|
64
|
-
}
|
|
47
|
+
def subtitle_for(key)
|
|
48
|
+
case key
|
|
49
|
+
when :overrides then "config.pricing_overrides"
|
|
50
|
+
when :file then LlmCostTracker.configuration.prices_file.to_s
|
|
51
|
+
when :bundled then "ships with the gem"
|
|
52
|
+
end
|
|
65
53
|
end
|
|
66
54
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
rows: build_rows(prices)
|
|
76
|
-
}
|
|
55
|
+
def updated_at_for(key)
|
|
56
|
+
case key
|
|
57
|
+
when :file
|
|
58
|
+
path = LlmCostTracker.configuration.prices_file
|
|
59
|
+
Pricing::Registry.file_metadata(path)["updated_at"] || Pricing::Registry.prices_file_mtime_iso
|
|
60
|
+
when :bundled
|
|
61
|
+
Pricing::Registry.metadata["updated_at"]
|
|
62
|
+
end
|
|
77
63
|
end
|
|
78
64
|
|
|
79
65
|
def build_rows(prices)
|
|
@@ -1,65 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "llm_cost_tracker/ledger"
|
|
4
|
-
|
|
5
3
|
module LlmCostTracker
|
|
6
4
|
module Dashboard
|
|
7
5
|
module SetupState
|
|
8
6
|
SetupRequired = Data.define(:message, :details)
|
|
9
|
-
DOCS_HINT = "See docs/upgrading.md for the migration path."
|
|
10
|
-
MUTEX = Mutex.new
|
|
11
|
-
|
|
12
|
-
private_constant :MUTEX, :DOCS_HINT
|
|
13
7
|
|
|
14
8
|
class << self
|
|
15
9
|
def current
|
|
16
|
-
|
|
10
|
+
return @current if defined?(@current)
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
if !defined?(@cache_fingerprint) || @cache_fingerprint != fingerprint
|
|
20
|
-
LlmCostTracker::Call.reset_column_information
|
|
21
|
-
@cached = compute
|
|
22
|
-
@cache_fingerprint = fingerprint
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
@cached
|
|
12
|
+
@current = compute
|
|
26
13
|
end
|
|
27
14
|
|
|
28
15
|
def reset!
|
|
29
|
-
|
|
30
|
-
remove_instance_variable(:@cached) if defined?(@cached)
|
|
31
|
-
remove_instance_variable(:@cache_fingerprint) if defined?(@cache_fingerprint)
|
|
32
|
-
end
|
|
16
|
+
remove_instance_variable(:@current) if defined?(@current)
|
|
33
17
|
end
|
|
34
18
|
|
|
35
19
|
private
|
|
36
20
|
|
|
37
|
-
SCHEMA_MIGRATIONS_TABLE = "schema_migrations"
|
|
38
|
-
private_constant :SCHEMA_MIGRATIONS_TABLE
|
|
39
|
-
|
|
40
|
-
def schema_fingerprint
|
|
41
|
-
connection = ActiveRecord::Base.connection
|
|
42
|
-
quoted = connection.quote_table_name(SCHEMA_MIGRATIONS_TABLE)
|
|
43
|
-
connection.query_value("SELECT MAX(version) FROM #{quoted}")
|
|
44
|
-
rescue ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished
|
|
45
|
-
nil
|
|
46
|
-
end
|
|
47
|
-
|
|
48
21
|
def compute
|
|
49
22
|
LlmCostTracker::Logging.debug("Dashboard::SetupState recomputing")
|
|
50
23
|
return calls_table_missing unless LlmCostTracker::Call.table_exists?
|
|
51
24
|
|
|
52
|
-
|
|
53
|
-
return core_drift if core_drift
|
|
54
|
-
return nil unless LlmCostTracker.reconciliation_enabled?
|
|
55
|
-
|
|
56
|
-
reconciliation_drift
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def schema_checks_for_current_config
|
|
60
|
-
return LlmCostTracker::Ledger::Schema::CORE_SCHEMAS unless LlmCostTracker.configuration.cache_rollups
|
|
61
|
-
|
|
62
|
-
LlmCostTracker::Ledger::Schema::CORE_SCHEMAS + [LlmCostTracker::Ledger::Schema::CACHE_ROLLUPS_SCHEMA]
|
|
25
|
+
drift_in(LlmCostTracker::Ingestion.guards_for_current_config)
|
|
63
26
|
end
|
|
64
27
|
|
|
65
28
|
def drift_in(checks)
|
|
@@ -73,25 +36,6 @@ module LlmCostTracker
|
|
|
73
36
|
nil
|
|
74
37
|
end
|
|
75
38
|
|
|
76
|
-
def reconciliation_drift
|
|
77
|
-
connection = ActiveRecord::Base.connection
|
|
78
|
-
LlmCostTracker::Reconciliation::SCHEMA_TABLES.each do |schema, table|
|
|
79
|
-
unless connection.data_source_exists?(table)
|
|
80
|
-
return SetupRequired.new(
|
|
81
|
-
message: "The #{table} table is required when reconciliation is enabled.",
|
|
82
|
-
details: ["bin/rails generate llm_cost_tracker:reconciliation && bin/rails db:migrate"]
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
errors = schema.current_schema_errors
|
|
87
|
-
next if errors.empty?
|
|
88
|
-
|
|
89
|
-
message = "The #{table} table does not match the current LLM Cost Tracker schema."
|
|
90
|
-
return SetupRequired.new(message: message, details: errors)
|
|
91
|
-
end
|
|
92
|
-
nil
|
|
93
|
-
end
|
|
94
|
-
|
|
95
39
|
def calls_table_missing
|
|
96
40
|
SetupRequired.new(
|
|
97
41
|
message: "The llm_cost_tracker_calls table is not available yet.",
|
|
@@ -35,7 +35,7 @@ module LlmCostTracker
|
|
|
35
35
|
calls: calls,
|
|
36
36
|
total_cost: row.total_cost,
|
|
37
37
|
average_cost_per_call: row.average_cost_per_call,
|
|
38
|
-
share_percent:
|
|
38
|
+
share_percent: total.positive? ? (calls.to_f / total) * 100.0 : 0.0
|
|
39
39
|
)
|
|
40
40
|
end
|
|
41
41
|
end
|
|
@@ -115,12 +115,6 @@ module LlmCostTracker
|
|
|
115
115
|
def quoted_key
|
|
116
116
|
scope.connection.quote(key)
|
|
117
117
|
end
|
|
118
|
-
|
|
119
|
-
def percentage(numerator, denominator)
|
|
120
|
-
return 0.0 unless denominator.positive?
|
|
121
|
-
|
|
122
|
-
(numerator / denominator.to_f) * 100.0
|
|
123
|
-
end
|
|
124
118
|
end
|
|
125
119
|
end
|
|
126
120
|
end
|
|
@@ -20,7 +20,7 @@ module LlmCostTracker
|
|
|
20
20
|
|
|
21
21
|
def rows
|
|
22
22
|
scope.klass.find_by_sql(build_sql)
|
|
23
|
-
rescue
|
|
23
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
24
24
|
LlmCostTracker::Logging.warn("Tag key discovery failed (#{connection.adapter_name}): #{e.class}: #{e.message}")
|
|
25
25
|
[]
|
|
26
26
|
end
|