llm_cost_tracker 0.10.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 +82 -0
- data/README.md +11 -5
- data/app/assets/llm_cost_tracker/application.css +784 -802
- data/app/controllers/llm_cost_tracker/application_controller.rb +14 -2
- data/app/controllers/llm_cost_tracker/calls_controller.rb +28 -21
- data/app/controllers/llm_cost_tracker/dashboard_controller.rb +1 -4
- data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
- data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
- data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
- data/app/helpers/llm_cost_tracker/application_helper.rb +19 -16
- data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -11
- data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
- 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 +81 -0
- data/app/services/llm_cost_tracker/dashboard/setup_state.rb +6 -68
- data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +20 -12
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
- data/app/views/layouts/llm_cost_tracker/application.html.erb +74 -17
- data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
- data/app/views/llm_cost_tracker/calls/show.html.erb +132 -125
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +120 -159
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +140 -194
- data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
- data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
- data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
- data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
- data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
- data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
- data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
- data/app/views/llm_cost_tracker/tags/show.html.erb +85 -104
- data/config/routes.rb +3 -3
- data/lib/llm_cost_tracker/budget.rb +25 -28
- data/lib/llm_cost_tracker/capture/sdk_payload.rb +34 -0
- data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +2 -1
- data/lib/llm_cost_tracker/capture/stream_collector.rb +30 -52
- data/lib/llm_cost_tracker/capture/stream_tracker.rb +18 -33
- 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 -61
- 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 +66 -64
- 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/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +4 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +39 -8
- data/lib/llm_cost_tracker/ingestion/inbox.rb +8 -9
- 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 +94 -116
- 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 +72 -332
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +89 -145
- 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 +14 -11
- data/lib/llm_cost_tracker/logging.rb +4 -21
- data/lib/llm_cost_tracker/middleware/faraday.rb +63 -51
- data/lib/llm_cost_tracker/parsers.rb +140 -29
- 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 +53 -35
- 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 -295
- 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 +181 -0
- 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 -7
- data/lib/llm_cost_tracker/report/data.rb +3 -4
- data/lib/llm_cost_tracker/report/formatter.rb +33 -20
- 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 +92 -58
- data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +0 -106
- 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 -183
- data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
- 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 -32
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +0 -25
- 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 -193
- 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 -232
- data/lib/llm_cost_tracker/parsers/openai.rb +0 -41
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +0 -51
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -155
- 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/tier_classification.rb +0 -22
- data/lib/llm_cost_tracker/reconcile_tasks.rb +0 -134
- 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 -254
- data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +0 -172
- data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +0 -20
- data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +0 -142
- data/lib/llm_cost_tracker/reconciliation.rb +0 -118
- data/lib/llm_cost_tracker/token_usage.rb +0 -93
|
@@ -10,6 +10,7 @@ module LlmCostTracker
|
|
|
10
10
|
|
|
11
11
|
before_action :set_dashboard_security_headers
|
|
12
12
|
before_action :ensure_current_schema
|
|
13
|
+
before_action :assign_dashboard_date_range
|
|
13
14
|
|
|
14
15
|
helper_method :dashboard_csp_nonce
|
|
15
16
|
|
|
@@ -30,6 +31,12 @@ module LlmCostTracker
|
|
|
30
31
|
render template: "llm_cost_tracker/shared/setup_required"
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
def assign_dashboard_date_range
|
|
35
|
+
range = LlmCostTracker::Dashboard::DateRange.call(params: params)
|
|
36
|
+
@from_date = range.from
|
|
37
|
+
@to_date = range.to
|
|
38
|
+
end
|
|
39
|
+
|
|
33
40
|
def render_database_error(error)
|
|
34
41
|
@error = error
|
|
35
42
|
render "llm_cost_tracker/errors/database", status: :internal_server_error
|
|
@@ -48,8 +55,13 @@ module LlmCostTracker
|
|
|
48
55
|
nonce = dashboard_csp_nonce
|
|
49
56
|
response.headers["X-Frame-Options"] = "DENY"
|
|
50
57
|
response.headers["Referrer-Policy"] = "same-origin"
|
|
51
|
-
response.headers["Content-Security-Policy"] =
|
|
52
|
-
"default-src 'self'
|
|
58
|
+
response.headers["Content-Security-Policy"] = [
|
|
59
|
+
"default-src 'self'",
|
|
60
|
+
"script-src 'self' 'nonce-#{nonce}'",
|
|
61
|
+
"style-src 'self' 'nonce-#{nonce}'",
|
|
62
|
+
"img-src 'self' data:",
|
|
63
|
+
"frame-ancestors 'none'"
|
|
64
|
+
].join("; ")
|
|
53
65
|
end
|
|
54
66
|
|
|
55
67
|
def dashboard_csp_nonce
|
|
@@ -8,19 +8,25 @@ module LlmCostTracker
|
|
|
8
8
|
CSV_EXPORT_LIMIT = 10_000
|
|
9
9
|
CSV_EXPORT_BATCH_SIZE = 500
|
|
10
10
|
CSV_FORMULA_PREFIXES = ["=", "+", "-", "@", "\t", "\r"].freeze
|
|
11
|
-
|
|
11
|
+
DEFAULT_TIEBREAKER = { tracked_at: :desc, id: :desc }.freeze
|
|
12
|
+
SORT_OPTIONS = %w[tracked_at provider model input output cost latency].freeze
|
|
13
|
+
NULLS_LAST_GUARD = {
|
|
14
|
+
total_cost: Arel.sql("CASE WHEN total_cost IS NULL THEN 1 ELSE 0 END ASC"),
|
|
15
|
+
latency_ms: Arel.sql("CASE WHEN latency_ms IS NULL THEN 1 ELSE 0 END ASC")
|
|
16
|
+
}.freeze
|
|
12
17
|
|
|
13
18
|
def index
|
|
14
19
|
@sort = params[:sort].to_s
|
|
20
|
+
@dir = params[:dir].to_s
|
|
15
21
|
scope = Dashboard::Filter.call(params: params)
|
|
16
|
-
scope = scope.unknown_pricing if
|
|
17
|
-
ordered_scope = scope.order(
|
|
22
|
+
scope = scope.unknown_pricing if params[:cost_status].to_s == "incomplete"
|
|
23
|
+
ordered_scope = scope.order(*calls_order(@sort, @dir))
|
|
18
24
|
|
|
19
25
|
respond_to do |format|
|
|
20
26
|
format.html do
|
|
21
27
|
@page = Dashboard::Pagination.call(params)
|
|
22
|
-
@calls_count = scope.
|
|
23
|
-
@calls = ordered_scope.includes(:tag_records).limit(@page.
|
|
28
|
+
@calls_count, @calls_total_cost = scope.pick(Arel.sql("COUNT(*), COALESCE(SUM(total_cost), 0)"))
|
|
29
|
+
@calls = ordered_scope.includes(:tag_records).limit(@page.per).offset(@page.offset).to_a
|
|
24
30
|
end
|
|
25
31
|
format.csv do
|
|
26
32
|
response.headers["Cache-Control"] = "no-store"
|
|
@@ -37,18 +43,19 @@ module LlmCostTracker
|
|
|
37
43
|
|
|
38
44
|
private
|
|
39
45
|
|
|
40
|
-
def calls_order(sort)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
when
|
|
47
|
-
|
|
48
|
-
when
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
def calls_order(sort, dir)
|
|
47
|
+
column = SORT_OPTIONS.include?(sort) ? sort.to_sym : :tracked_at
|
|
48
|
+
natural = %i[provider model].include?(column) ? :asc : :desc
|
|
49
|
+
direction = Dashboard::Sort::DIRECTIONS.include?(dir.downcase) ? dir.downcase.to_sym : natural
|
|
50
|
+
|
|
51
|
+
case column
|
|
52
|
+
when :tracked_at then [{ tracked_at: direction, id: direction }]
|
|
53
|
+
when :provider then [{ provider: direction, model: :asc, **DEFAULT_TIEBREAKER }]
|
|
54
|
+
when :model then [{ model: direction, **DEFAULT_TIEBREAKER }]
|
|
55
|
+
when :input then [{ input_tokens: direction, **DEFAULT_TIEBREAKER }]
|
|
56
|
+
when :output then [{ output_tokens: direction, **DEFAULT_TIEBREAKER }]
|
|
57
|
+
when :cost then [NULLS_LAST_GUARD[:total_cost], { total_cost: direction, **DEFAULT_TIEBREAKER }]
|
|
58
|
+
when :latency then [NULLS_LAST_GUARD[:latency_ms], { latency_ms: direction, **DEFAULT_TIEBREAKER }]
|
|
52
59
|
end
|
|
53
60
|
end
|
|
54
61
|
|
|
@@ -76,7 +83,7 @@ module LlmCostTracker
|
|
|
76
83
|
|
|
77
84
|
def csv_fields
|
|
78
85
|
%i[tracked_at provider model] +
|
|
79
|
-
TokenUsage.members +
|
|
86
|
+
Usage::TokenUsage.members +
|
|
80
87
|
%i[
|
|
81
88
|
total_cost cost_status pricing_snapshot latency_ms provider_response_id provider_project_id
|
|
82
89
|
provider_api_key_id provider_workspace_id batch tags
|
|
@@ -86,15 +93,15 @@ module LlmCostTracker
|
|
|
86
93
|
def csv_value(field, call)
|
|
87
94
|
case field
|
|
88
95
|
when :tracked_at
|
|
89
|
-
call.tracked_at
|
|
96
|
+
call.tracked_at.utc.iso8601
|
|
90
97
|
when :provider_api_key_id, :provider_workspace_id, :provider_project_id
|
|
91
|
-
csv_safe(LlmCostTracker::Masking.mask_value(field, call[field]))
|
|
98
|
+
csv_safe(LlmCostTracker::Dashboard::Masking.mask_value(field, call[field]))
|
|
92
99
|
when :provider, :model, :provider_response_id, :cost_status
|
|
93
100
|
csv_safe(call[field])
|
|
94
101
|
when :pricing_snapshot
|
|
95
102
|
csv_safe(csv_json(call.pricing_snapshot))
|
|
96
103
|
when :tags
|
|
97
|
-
csv_safe(call.
|
|
104
|
+
csv_safe(call.tag_pairs.to_json)
|
|
98
105
|
else
|
|
99
106
|
call[field]
|
|
100
107
|
end
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
module LlmCostTracker
|
|
4
4
|
class DashboardController < ApplicationController
|
|
5
5
|
def index
|
|
6
|
-
range = Dashboard::DateRange.call(params: params)
|
|
7
|
-
@from_date = range.from
|
|
8
|
-
@to_date = range.to
|
|
9
6
|
prev_from, prev_to = previous_range
|
|
10
7
|
filter_params = LlmCostTracker::Dashboard::Params.to_hash(params)
|
|
11
8
|
scope = Dashboard::Filter.call(
|
|
@@ -16,7 +13,7 @@ module LlmCostTracker
|
|
|
16
13
|
)
|
|
17
14
|
|
|
18
15
|
@stats = Dashboard::OverviewStats.call(scope: scope, previous_scope: previous_scope)
|
|
19
|
-
@monthly_budget_status = Dashboard::
|
|
16
|
+
@monthly_budget_status = Dashboard::MonthlyBudget.status
|
|
20
17
|
@time_series = Dashboard::TimeSeries.call(scope: scope, from: @from_date, to: @to_date)
|
|
21
18
|
@comparison_series = Dashboard::TimeSeries.call(scope: previous_scope, from: prev_from, to: prev_to)
|
|
22
19
|
@spend_anomaly = Dashboard::SpendAnomaly.call(from: @from_date, to: @to_date, scope: scope)
|
|
@@ -4,10 +4,12 @@ module LlmCostTracker
|
|
|
4
4
|
class ModelsController < ApplicationController
|
|
5
5
|
def index
|
|
6
6
|
@sort = params[:sort].to_s
|
|
7
|
+
@dir = params[:dir].to_s
|
|
7
8
|
@rows = Dashboard::TopModels.call(
|
|
8
9
|
scope: Dashboard::Filter.call(params: params),
|
|
9
10
|
limit: nil,
|
|
10
|
-
sort: @sort
|
|
11
|
+
sort: @sort,
|
|
12
|
+
direction: @dir
|
|
11
13
|
)
|
|
12
14
|
end
|
|
13
15
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
class PricingController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@overview = Dashboard::PricingOverview.call
|
|
7
|
+
requested = params[:source]&.to_sym
|
|
8
|
+
@active_source = @overview.fetch(:sources).key?(requested) ? requested : @overview.fetch(:effective_source)
|
|
9
|
+
@source_data = @overview.fetch(:sources).fetch(@active_source)
|
|
10
|
+
@provider_filter = params[:provider].to_s.presence
|
|
11
|
+
@rows = @source_data.fetch(:rows)
|
|
12
|
+
@rows = @rows.select { |row| row.provider == @provider_filter } if @provider_filter
|
|
13
|
+
@providers = @source_data.fetch(:rows).map(&:provider).compact.uniq.sort
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -11,7 +11,9 @@ module LlmCostTracker
|
|
|
11
11
|
@value = params[:tag_value].to_s
|
|
12
12
|
|
|
13
13
|
if @value.empty?
|
|
14
|
-
@
|
|
14
|
+
@sort = params[:sort].to_s
|
|
15
|
+
@dir = params[:dir].to_s
|
|
16
|
+
@breakdown = Dashboard::TagBreakdown.call(scope: scope, key: params[:key], sort: @sort, direction: @dir)
|
|
15
17
|
else
|
|
16
18
|
@key = LlmCostTracker::Tags::Key.validate!(
|
|
17
19
|
params[:key],
|
|
@@ -7,7 +7,6 @@ module LlmCostTracker
|
|
|
7
7
|
TAG_VALUE_SUMMARY_BYTES = 80
|
|
8
8
|
TAG_TOOLTIP_BYTES = 512
|
|
9
9
|
|
|
10
|
-
include DashboardFilterHelper
|
|
11
10
|
include DashboardFilterOptionsHelper
|
|
12
11
|
include DashboardQueryHelper
|
|
13
12
|
include ChartHelper
|
|
@@ -15,6 +14,17 @@ module LlmCostTracker
|
|
|
15
14
|
include TokenUsageHelper
|
|
16
15
|
include InlineStyleHelper
|
|
17
16
|
|
|
17
|
+
def dashboard_section
|
|
18
|
+
path = request.path.to_s
|
|
19
|
+
return :models if path.start_with?(models_path)
|
|
20
|
+
return :calls if path.start_with?(calls_path)
|
|
21
|
+
return :tags if path.start_with?(tags_path)
|
|
22
|
+
return :data_quality if path.start_with?(data_quality_path)
|
|
23
|
+
return :pricing if path.start_with?(pricing_path)
|
|
24
|
+
|
|
25
|
+
:overview
|
|
26
|
+
end
|
|
27
|
+
|
|
18
28
|
def coverage_percent(numerator, denominator)
|
|
19
29
|
denominator = denominator.to_f
|
|
20
30
|
return 0.0 unless denominator.positive?
|
|
@@ -33,27 +43,20 @@ module LlmCostTracker
|
|
|
33
43
|
value.nil? ? "n/a" : money(value)
|
|
34
44
|
end
|
|
35
45
|
|
|
36
|
-
def optional_number(value)
|
|
37
|
-
value.nil? ? "n/a" : number(value)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def number(value)
|
|
41
|
-
number_with_delimiter(value)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
46
|
def format_date(value)
|
|
45
|
-
|
|
47
|
+
return "" if value.nil?
|
|
48
|
+
|
|
49
|
+
value.strftime("%Y-%m-%d %H:%M")
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
def pricing_status(call)
|
|
49
|
-
return "Unknown
|
|
50
|
-
return "Estimated" unless call.has_attribute?(:cost_status)
|
|
53
|
+
return "Unknown" if call.total_cost.nil?
|
|
51
54
|
|
|
52
55
|
{
|
|
53
|
-
LlmCostTracker::
|
|
54
|
-
LlmCostTracker::
|
|
55
|
-
LlmCostTracker::
|
|
56
|
-
}.fetch(call.cost_status, "Unknown
|
|
56
|
+
LlmCostTracker::Charges::CostStatus::COMPLETE => "Estimated",
|
|
57
|
+
LlmCostTracker::Charges::CostStatus::FREE => "Free",
|
|
58
|
+
LlmCostTracker::Charges::CostStatus::PARTIAL => "Partial"
|
|
59
|
+
}.fetch(call.cost_status, "Unknown")
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
def percent(value)
|
|
@@ -6,7 +6,7 @@ module LlmCostTracker
|
|
|
6
6
|
return nil if points.blank?
|
|
7
7
|
|
|
8
8
|
cfg = chart_config(points, comparison_points, height, y_ticks)
|
|
9
|
-
parts = [chart_svg_open(cfg)]
|
|
9
|
+
parts = [chart_svg_open(cfg), "<title>Daily spend trend</title>", chart_area_gradient_def]
|
|
10
10
|
parts.concat(chart_grid_and_axis(cfg))
|
|
11
11
|
parts << chart_paths(cfg)
|
|
12
12
|
parts.concat(chart_dots(cfg))
|
|
@@ -22,7 +22,7 @@ module LlmCostTracker
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def chart_config(points, comparison_points, height, y_ticks)
|
|
25
|
-
width =
|
|
25
|
+
width = 1180
|
|
26
26
|
pad = { top: 16, right: 16, bottom: 28, left: 56 }
|
|
27
27
|
plot_w = width - pad[:left] - pad[:right]
|
|
28
28
|
plot_h = height - pad[:top] - pad[:bottom]
|
|
@@ -31,9 +31,11 @@ module LlmCostTracker
|
|
|
31
31
|
coords = chart_coords(points, pad, plot_w, plot_h, max_cost)
|
|
32
32
|
comparison_coords = chart_coords(comparison_points, pad, plot_w, plot_h, max_cost) if comparison_points.present?
|
|
33
33
|
|
|
34
|
+
peak_index = points.each_with_index.max_by { |point, _| point[:cost].to_f }&.last
|
|
34
35
|
{ width: width, height: height, pad: pad, plot_w: plot_w, plot_h: plot_h,
|
|
35
36
|
max_cost: max_cost, n: points.size, y_ticks: y_ticks, points: points, coords: coords,
|
|
36
|
-
comparison_points: comparison_points, comparison_coords: comparison_coords
|
|
37
|
+
comparison_points: comparison_points, comparison_coords: comparison_coords,
|
|
38
|
+
peak_index: peak_index }
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
def chart_coords(points, pad, plot_w, plot_h, max_cost)
|
|
@@ -51,7 +53,7 @@ module LlmCostTracker
|
|
|
51
53
|
attrs = [
|
|
52
54
|
%(class="lct-chart"),
|
|
53
55
|
%(viewBox="0 0 #{cfg[:width]} #{cfg[:height]}"),
|
|
54
|
-
%(preserveAspectRatio="
|
|
56
|
+
%(preserveAspectRatio="xMidYMid meet"),
|
|
55
57
|
%(role="img"),
|
|
56
58
|
%(aria-label="Daily spend trend")
|
|
57
59
|
].join(" ")
|
|
@@ -114,10 +116,20 @@ module LlmCostTracker
|
|
|
114
116
|
def chart_dot(cfg, pt_x, pt_y, idx)
|
|
115
117
|
point = cfg[:points][idx]
|
|
116
118
|
title = ERB::Util.html_escape("#{point[:label]}: #{money(point[:cost])}")
|
|
117
|
-
|
|
119
|
+
peak = idx == cfg[:peak_index]
|
|
120
|
+
klass = peak ? "lct-chart-peak" : "lct-chart-dot"
|
|
121
|
+
radius = peak ? 4 : 3
|
|
122
|
+
circle = %(<circle class="#{klass}" cx="#{chart_fmt(pt_x)}" cy="#{chart_fmt(pt_y)}" r="#{radius}"/>)
|
|
118
123
|
"<g>#{circle}<title>#{title}</title></g>"
|
|
119
124
|
end
|
|
120
125
|
|
|
126
|
+
def chart_area_gradient_def
|
|
127
|
+
"<defs><linearGradient id=\"lct-chart-grad\" x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">" \
|
|
128
|
+
"<stop offset=\"0%\" stop-color=\"var(--lct-accent)\" stop-opacity=\"0.28\"/>" \
|
|
129
|
+
"<stop offset=\"100%\" stop-color=\"var(--lct-accent)\" stop-opacity=\"0.02\"/>" \
|
|
130
|
+
"</linearGradient></defs>"
|
|
131
|
+
end
|
|
132
|
+
|
|
121
133
|
def chart_x_labels(cfg)
|
|
122
134
|
indexes = cfg[:n] <= 2 ? (0...cfg[:n]).to_a : [0, cfg[:n] / 2, cfg[:n] - 1].uniq
|
|
123
135
|
label_y = chart_fmt(cfg[:height] - 8)
|
|
@@ -127,7 +139,11 @@ module LlmCostTracker
|
|
|
127
139
|
def chart_x_label(cfg, idx, label_y)
|
|
128
140
|
pt_x, = cfg[:coords][idx]
|
|
129
141
|
label = ERB::Util.html_escape(cfg[:points][idx][:label])
|
|
130
|
-
|
|
142
|
+
anchor = if idx.zero? then "start"
|
|
143
|
+
elsif idx == cfg[:n] - 1 then "end"
|
|
144
|
+
else "middle"
|
|
145
|
+
end
|
|
146
|
+
%(<text class="lct-chart-axis" x="#{chart_fmt(pt_x)}" y="#{label_y}" text-anchor="#{anchor}">#{label}</text>)
|
|
131
147
|
end
|
|
132
148
|
end
|
|
133
149
|
end
|
|
@@ -4,17 +4,7 @@ module LlmCostTracker
|
|
|
4
4
|
module DashboardFilterOptionsHelper
|
|
5
5
|
MAX_FILTER_OPTIONS = 100
|
|
6
6
|
|
|
7
|
-
def
|
|
8
|
-
filter_options_for(:provider, filter_params: filter_params)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def model_filter_options(filter_params: params)
|
|
12
|
-
filter_options_for(:model, filter_params: filter_params)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
private
|
|
16
|
-
|
|
17
|
-
def filter_options_for(column, filter_params:)
|
|
7
|
+
def filter_options_for(column, filter_params: params)
|
|
18
8
|
source = LlmCostTracker::Dashboard::Params.to_hash(filter_params).symbolize_keys
|
|
19
9
|
scope_params = source.merge(
|
|
20
10
|
column => nil, format: nil, page: nil, per: nil, sort: nil
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmCostTracker
|
|
4
|
+
module SortableTableHelper
|
|
5
|
+
def sortable_header(label, column, num: false, default: false)
|
|
6
|
+
state = sortable_state(column, num: num, default: default)
|
|
7
|
+
classes = ["lct-sortable"]
|
|
8
|
+
classes << "lct-num" if num
|
|
9
|
+
classes << "lct-sorted" if state[:active]
|
|
10
|
+
|
|
11
|
+
href = dashboard_filter_path(current_query(sort: column, dir: state[:next_dir], page: nil))
|
|
12
|
+
tag.th(class: classes.join(" "), "aria-sort": state[:aria_sort]) do
|
|
13
|
+
link_to(href) { safe_join([label, " ", tag.span(state[:arrow], class: "lct-sort-ind")]) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def sortable_state(column, num:, default: false)
|
|
20
|
+
current_sort = params[:sort].presence || (default ? column : nil)
|
|
21
|
+
current_dir = Dashboard::Sort::DIRECTIONS.include?(params[:dir].to_s) ? params[:dir].to_s : nil
|
|
22
|
+
natural_dir = num ? "desc" : "asc"
|
|
23
|
+
active = current_sort == column
|
|
24
|
+
effective_dir = active ? (current_dir || natural_dir) : natural_dir
|
|
25
|
+
flipped = effective_dir == "asc" ? "desc" : "asc"
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
active: active,
|
|
29
|
+
next_dir: active ? flipped : natural_dir,
|
|
30
|
+
arrow: active && effective_dir == "asc" ? "▲" : "▼",
|
|
31
|
+
aria_sort: sortable_aria(active, effective_dir)
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sortable_aria(active, effective_dir)
|
|
36
|
+
return "none" unless active
|
|
37
|
+
|
|
38
|
+
effective_dir == "asc" ? "ascending" : "descending"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -40,11 +40,9 @@ module LlmCostTracker
|
|
|
40
40
|
|
|
41
41
|
def call_line_item_costs_by_component(call)
|
|
42
42
|
call.line_items.each_with_object({}) do |line_item, accumulator|
|
|
43
|
-
component = LlmCostTracker::
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
item.cache_state.to_s == line_item.cache_state.to_s
|
|
47
|
-
end
|
|
43
|
+
component = LlmCostTracker::Usage::Catalog.token_priced_for(
|
|
44
|
+
kind: line_item.kind, direction: line_item.direction, cache_state: line_item.cache_state
|
|
45
|
+
)
|
|
48
46
|
accumulator[component.key] = line_item.cost if component && line_item.cost
|
|
49
47
|
end
|
|
50
48
|
end
|
|
@@ -52,7 +50,7 @@ module LlmCostTracker
|
|
|
52
50
|
private
|
|
53
51
|
|
|
54
52
|
def token_usage_display_components(labels:)
|
|
55
|
-
LlmCostTracker::
|
|
53
|
+
LlmCostTracker::Usage::Catalog.token_priced.map do |component|
|
|
56
54
|
token_key = component.token_key
|
|
57
55
|
{
|
|
58
56
|
token_key: token_key,
|
|
@@ -2,43 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
|
-
require "llm_cost_tracker/billing/cost_status"
|
|
6
|
-
require "llm_cost_tracker/ledger/schema/adapter"
|
|
7
|
-
require "llm_cost_tracker/ledger/tags/sql"
|
|
8
|
-
|
|
9
5
|
module LlmCostTracker
|
|
10
6
|
class Call < ActiveRecord::Base
|
|
11
7
|
before_validation :assign_event_id
|
|
12
8
|
|
|
13
|
-
PERIOD_FORMATS = {
|
|
14
|
-
day: {
|
|
15
|
-
postgres: "YYYY-MM-DD",
|
|
16
|
-
mysql: "%Y-%m-%d"
|
|
17
|
-
},
|
|
18
|
-
month: {
|
|
19
|
-
postgres: "YYYY-MM",
|
|
20
|
-
mysql: "%Y-%m"
|
|
21
|
-
}
|
|
22
|
-
}.freeze
|
|
23
|
-
|
|
24
|
-
private_constant :PERIOD_FORMATS
|
|
25
|
-
|
|
26
9
|
scope :with_cost, -> { where.not(total_cost: nil) }
|
|
27
10
|
scope :without_cost, -> { where(total_cost: nil) }
|
|
28
|
-
scope :unknown_pricing,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
11
|
+
scope :unknown_pricing,
|
|
12
|
+
lambda {
|
|
13
|
+
where(Charges::CostStatus.unknown_pricing_sql)
|
|
14
|
+
}
|
|
33
15
|
scope :with_latency, -> { where.not(latency_ms: nil) }
|
|
34
16
|
scope :streaming, -> { where(stream: true) }
|
|
35
17
|
scope :non_streaming, -> { where(stream: [false, nil]) }
|
|
36
18
|
scope :by_usage_source, ->(source) { where(usage_source: source.to_s) }
|
|
37
19
|
scope :with_provider_response_id, -> { where.not(provider_response_id: [nil, ""]) }
|
|
38
20
|
scope :missing_provider_response_id, -> { where(provider_response_id: [nil, ""]) }
|
|
39
|
-
scope :streaming_missing_usage,
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
scope :streaming_missing_usage,
|
|
22
|
+
lambda {
|
|
23
|
+
where(stream: true).where(usage_source: [Usage::Source::UNKNOWN, nil])
|
|
24
|
+
}
|
|
42
25
|
|
|
43
26
|
has_many :line_items,
|
|
44
27
|
class_name: "LlmCostTracker::CallLineItem",
|
|
@@ -58,6 +41,12 @@ module LlmCostTracker
|
|
|
58
41
|
scope :between, ->(from, to) { where(tracked_at: from..to) }
|
|
59
42
|
|
|
60
43
|
class << self
|
|
44
|
+
def already_recorded?(provider:, provider_response_id:)
|
|
45
|
+
return false if provider_response_id.to_s.empty?
|
|
46
|
+
|
|
47
|
+
where(provider: provider, provider_response_id: provider_response_id).exists?
|
|
48
|
+
end
|
|
49
|
+
|
|
61
50
|
def by_tag(key, value) = by_tags(key => value)
|
|
62
51
|
|
|
63
52
|
def by_tags(tags) = Ledger::Tags::Query.apply(tags)
|
|
@@ -71,20 +60,20 @@ module LlmCostTracker
|
|
|
71
60
|
def cost_by_provider(limit: nil) = cost_by_column(:provider, limit: limit)
|
|
72
61
|
|
|
73
62
|
def group_by_tag(key)
|
|
74
|
-
Ledger::Tags::
|
|
63
|
+
Ledger::Tags::Breakdown.join_relation(self, key).group(Ledger::Tags::Breakdown.value_arel)
|
|
75
64
|
end
|
|
76
65
|
|
|
77
66
|
def cost_by_tag(key, limit: nil)
|
|
78
|
-
label = Ledger::Tags::
|
|
79
|
-
raw_value = Ledger::Tags::
|
|
80
|
-
relation = Ledger::Tags::
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
67
|
+
label = Ledger::Tags::Breakdown.label_sql(connection)
|
|
68
|
+
raw_value = Ledger::Tags::Breakdown.raw_value_sql(connection)
|
|
69
|
+
relation = Ledger::Tags::Breakdown.join_relation(self, key)
|
|
70
|
+
.select("#{label} AS name", "COALESCE(SUM(total_cost), 0) AS total_cost")
|
|
71
|
+
.group(Arel.sql(label))
|
|
72
|
+
.order(
|
|
73
|
+
Arel.sql("COALESCE(SUM(total_cost), 0) DESC"),
|
|
74
|
+
Arel.sql("MAX(CASE WHEN #{raw_value} IS NULL THEN 1 ELSE 0 END) ASC"),
|
|
75
|
+
Arel.sql("#{label} DESC")
|
|
76
|
+
)
|
|
88
77
|
relation = relation.limit(limit) if limit
|
|
89
78
|
relation
|
|
90
79
|
end
|
|
@@ -108,8 +97,7 @@ module LlmCostTracker
|
|
|
108
97
|
private
|
|
109
98
|
|
|
110
99
|
def cost_by_column(column, limit:)
|
|
111
|
-
|
|
112
|
-
relation = select("#{quoted_column} AS name, COALESCE(SUM(total_cost), 0) AS total_cost")
|
|
100
|
+
relation = select(arel_table[column].as("name"), "COALESCE(SUM(total_cost), 0) AS total_cost")
|
|
113
101
|
.group(column)
|
|
114
102
|
.order(Arel.sql("COALESCE(SUM(total_cost), 0) DESC"))
|
|
115
103
|
relation = relation.limit(limit) if limit
|
|
@@ -117,30 +105,7 @@ module LlmCostTracker
|
|
|
117
105
|
end
|
|
118
106
|
|
|
119
107
|
def period_group_expression(period, column:)
|
|
120
|
-
period
|
|
121
|
-
column = period_column_expression(column)
|
|
122
|
-
formats = PERIOD_FORMATS.fetch(period)
|
|
123
|
-
|
|
124
|
-
if Ledger::Schema::Adapter.postgresql?(connection)
|
|
125
|
-
postgres_period_expression(period, column, formats)
|
|
126
|
-
elsif Ledger::Schema::Adapter.mysql?(connection)
|
|
127
|
-
"DATE_FORMAT(#{column}, #{connection.quote(formats.fetch(:mysql))})"
|
|
128
|
-
else
|
|
129
|
-
Ledger::Schema::Adapter.ensure_supported!(connection)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def postgres_period_expression(period, column, formats)
|
|
134
|
-
"TO_CHAR(" \
|
|
135
|
-
"DATE_TRUNC(#{connection.quote(period.name)}, #{column}), " \
|
|
136
|
-
"#{connection.quote(formats.fetch(:postgres))}" \
|
|
137
|
-
")"
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def validated_period(period)
|
|
141
|
-
return period if PERIOD_FORMATS.key?(period)
|
|
142
|
-
|
|
143
|
-
raise ArgumentError, "invalid period: #{period.inspect}"
|
|
108
|
+
Ledger::Schema::Adapter.period_bucket_sql(connection, period, period_column_expression(column))
|
|
144
109
|
end
|
|
145
110
|
|
|
146
111
|
def period_column_expression(column)
|
|
@@ -151,7 +116,7 @@ module LlmCostTracker
|
|
|
151
116
|
end
|
|
152
117
|
end
|
|
153
118
|
|
|
154
|
-
def
|
|
119
|
+
def tag_pairs
|
|
155
120
|
tag_records.to_h do |record|
|
|
156
121
|
[record.key, record.value]
|
|
157
122
|
end
|
|
@@ -12,7 +12,7 @@ module LlmCostTracker
|
|
|
12
12
|
scope :by_direction, ->(direction) { where(direction: direction.to_s) }
|
|
13
13
|
scope :by_modality, ->(modality) { where(modality: modality.to_s) }
|
|
14
14
|
scope :cached, -> { where.not(cache_state: ["none", nil]) }
|
|
15
|
-
scope :priced, -> { where(cost_status:
|
|
16
|
-
scope :unpriced, -> { where(cost_status:
|
|
15
|
+
scope :priced, -> { where(cost_status: [Charges::CostStatus::COMPLETE, Charges::CostStatus::FREE]) }
|
|
16
|
+
scope :unpriced, -> { where(cost_status: Charges::CostStatus::UNKNOWN) }
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -2,5 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
module LlmCostTracker
|
|
4
4
|
class CallRollup < ActiveRecord::Base
|
|
5
|
+
class << self
|
|
6
|
+
def increment_all(rows)
|
|
7
|
+
upsert_all(rows, on_duplicate: increment_on_duplicate, record_timestamps: true, unique_by: increment_unique_by)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def decrement(buckets)
|
|
11
|
+
now = Time.now.utc
|
|
12
|
+
buckets.each do |(period, period_start, currency, provider), amount|
|
|
13
|
+
where(period: period, period_start: period_start, currency: currency, provider: provider)
|
|
14
|
+
.update_all(["total_cost = GREATEST(0, total_cost - ?), updated_at = ?", amount, now])
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def increment_on_duplicate
|
|
21
|
+
return Arel.sql(mysql_increment_sql) if Ledger::Schema::Adapter.mysql?(connection)
|
|
22
|
+
return Arel.sql(postgres_increment_sql) if Ledger::Schema::Adapter.postgresql?(connection)
|
|
23
|
+
|
|
24
|
+
Ledger::Schema::Adapter.ensure_supported!(connection)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def postgres_increment_sql
|
|
28
|
+
total = connection.quote_column_name("total_cost")
|
|
29
|
+
updated = connection.quote_column_name("updated_at")
|
|
30
|
+
"#{total} = #{quoted_table_name}.#{total} + excluded.#{total}, #{updated} = excluded.#{updated}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mysql_increment_sql
|
|
34
|
+
"total_cost = total_cost + VALUES(total_cost), updated_at = VALUES(updated_at)"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def increment_unique_by
|
|
38
|
+
return unless connection.supports_insert_conflict_target?
|
|
39
|
+
|
|
40
|
+
%i[period period_start currency provider]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
5
43
|
end
|
|
6
44
|
end
|