llm_cost_tracker 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +72 -1
- data/README.md +58 -221
- data/app/assets/llm_cost_tracker/application.css +218 -41
- data/app/controllers/llm_cost_tracker/application_controller.rb +30 -17
- data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +19 -14
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +10 -2
- data/app/helpers/llm_cost_tracker/application_helper.rb +11 -24
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +20 -7
- data/app/models/llm_cost_tracker/call.rb +169 -0
- data/app/models/llm_cost_tracker/call_line_item.rb +22 -0
- data/app/models/llm_cost_tracker/call_rollup.rb +9 -0
- data/app/models/llm_cost_tracker/call_tag.rb +16 -0
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +13 -0
- data/app/models/llm_cost_tracker/ingestion/lease.rb +1 -1
- data/app/models/llm_cost_tracker/provider_invoice.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +125 -34
- data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +2 -2
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
- data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
- data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
- data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
- data/app/views/llm_cost_tracker/calls/show.html.erb +62 -7
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -50
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +103 -126
- data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
- data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +63 -0
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
- data/app/views/llm_cost_tracker/tags/show.html.erb +5 -37
- data/lib/llm_cost_tracker/billing/components.rb +53 -0
- data/lib/llm_cost_tracker/billing/components.yml +117 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
- data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
- data/lib/llm_cost_tracker/budget.rb +23 -35
- data/lib/llm_cost_tracker/capture/stream_collector.rb +47 -33
- data/lib/llm_cost_tracker/configuration.rb +36 -19
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +54 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +24 -32
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
- data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
- data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +31 -0
- data/lib/llm_cost_tracker/doctor.rb +43 -45
- data/lib/llm_cost_tracker/errors.rb +5 -19
- data/lib/llm_cost_tracker/event.rb +10 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +157 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
- data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -23
- data/lib/llm_cost_tracker/ingestion/worker.rb +14 -5
- data/lib/llm_cost_tracker/ingestion.rb +28 -22
- data/lib/llm_cost_tracker/integrations/anthropic.rb +45 -38
- data/lib/llm_cost_tracker/integrations/base.rb +36 -29
- data/lib/llm_cost_tracker/integrations/openai.rb +85 -40
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +5 -5
- data/lib/llm_cost_tracker/integrations.rb +2 -2
- data/lib/llm_cost_tracker/ledger/period/totals.rb +12 -9
- data/lib/llm_cost_tracker/ledger/period.rb +5 -5
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +4 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +76 -25
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +50 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +34 -23
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
- data/lib/llm_cost_tracker/ledger/store.rb +110 -18
- data/lib/llm_cost_tracker/ledger/tags/query.rb +5 -11
- data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -14
- data/lib/llm_cost_tracker/ledger.rb +4 -2
- data/lib/llm_cost_tracker/logging.rb +2 -5
- data/lib/llm_cost_tracker/middleware/faraday.rb +7 -6
- data/lib/llm_cost_tracker/parsers/anthropic.rb +52 -7
- data/lib/llm_cost_tracker/parsers/base.rb +8 -3
- data/lib/llm_cost_tracker/parsers/gemini.rb +101 -15
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +10 -2
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +87 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +48 -21
- data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
- data/lib/llm_cost_tracker/parsers.rb +1 -1
- data/lib/llm_cost_tracker/prices.json +105 -20
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +57 -19
- data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
- data/lib/llm_cost_tracker/pricing/lookup.rb +38 -34
- data/lib/llm_cost_tracker/pricing/registry.rb +65 -45
- data/lib/llm_cost_tracker/pricing/service_charges.rb +204 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
- data/lib/llm_cost_tracker/pricing/sync.rb +57 -10
- data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
- data/lib/llm_cost_tracker/pricing.rb +190 -26
- data/lib/llm_cost_tracker/railtie.rb +0 -8
- data/lib/llm_cost_tracker/report/data.rb +16 -8
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +8 -8
- data/lib/llm_cost_tracker/tags/context.rb +2 -4
- data/lib/llm_cost_tracker/tags/key.rb +4 -0
- data/lib/llm_cost_tracker/tags/sanitizer.rb +12 -17
- data/lib/llm_cost_tracker/timing.rb +15 -0
- data/lib/llm_cost_tracker/token_usage.rb +56 -42
- data/lib/llm_cost_tracker/tracker.rb +67 -24
- data/lib/llm_cost_tracker/usage_capture.rb +29 -8
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +36 -35
- data/lib/tasks/llm_cost_tracker.rake +22 -17
- metadata +36 -41
- data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
- data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
- data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
- data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
- data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
- data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
- data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
- data/lib/llm_cost_tracker/pricing/components.rb +0 -37
- data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +0 -63
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
class AddIngestionToLlmCostTracker < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
-
def change
|
|
3
|
-
add_column :llm_api_calls, :event_id, :string unless column_exists?(:llm_api_calls, :event_id)
|
|
4
|
-
add_index :llm_api_calls, :event_id, unique: true if column_exists?(:llm_api_calls, :event_id) &&
|
|
5
|
-
!index_exists?(:llm_api_calls, :event_id)
|
|
6
|
-
|
|
7
|
-
create_table :llm_cost_tracker_inbox_events do |t|
|
|
8
|
-
t.string :event_id, null: false
|
|
9
|
-
t.decimal :total_cost, precision: 20, scale: 8
|
|
10
|
-
t.datetime :tracked_at, null: false
|
|
11
|
-
t.text :payload, null: false
|
|
12
|
-
t.datetime :locked_at
|
|
13
|
-
t.string :locked_by
|
|
14
|
-
t.integer :attempts, null: false, default: 0
|
|
15
|
-
t.text :last_error
|
|
16
|
-
|
|
17
|
-
t.timestamps
|
|
18
|
-
end unless table_exists?(:llm_cost_tracker_inbox_events)
|
|
19
|
-
|
|
20
|
-
create_table :llm_cost_tracker_ingestor_leases do |t|
|
|
21
|
-
t.string :name, null: false
|
|
22
|
-
t.string :locked_by
|
|
23
|
-
t.datetime :locked_until
|
|
24
|
-
|
|
25
|
-
t.timestamps
|
|
26
|
-
end unless table_exists?(:llm_cost_tracker_ingestor_leases)
|
|
27
|
-
|
|
28
|
-
add_index :llm_cost_tracker_inbox_events, :event_id, unique: true unless index_exists?(:llm_cost_tracker_inbox_events, :event_id)
|
|
29
|
-
add_index :llm_cost_tracker_inbox_events, :tracked_at unless index_exists?(:llm_cost_tracker_inbox_events, :tracked_at)
|
|
30
|
-
add_index :llm_cost_tracker_inbox_events, [:locked_at, :id] unless index_exists?(:llm_cost_tracker_inbox_events, [:locked_at, :id])
|
|
31
|
-
add_index :llm_cost_tracker_ingestor_leases, :name, unique: true unless index_exists?(:llm_cost_tracker_ingestor_leases, :name)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
class AddLatencyMsToLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
-
def up
|
|
3
|
-
add_column :llm_api_calls, :latency_ms, :integer unless column_exists?(:llm_api_calls, :latency_ms)
|
|
4
|
-
end
|
|
5
|
-
|
|
6
|
-
def down
|
|
7
|
-
remove_column :llm_api_calls, :latency_ms if column_exists?(:llm_api_calls, :latency_ms)
|
|
8
|
-
end
|
|
9
|
-
end
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
require "llm_cost_tracker/ledger/schema/adapter"
|
|
2
|
-
|
|
3
|
-
class AddPeriodTotalsToLlmCostTracker < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
-
def up
|
|
5
|
-
create_table :llm_cost_tracker_period_totals do |t|
|
|
6
|
-
t.string :period, null: false
|
|
7
|
-
t.date :period_start, null: false
|
|
8
|
-
t.decimal :total_cost, precision: 20, scale: 8, null: false, default: 0
|
|
9
|
-
|
|
10
|
-
t.timestamps
|
|
11
|
-
end unless table_exists?(:llm_cost_tracker_period_totals)
|
|
12
|
-
|
|
13
|
-
backfill_period_totals
|
|
14
|
-
|
|
15
|
-
add_index :llm_cost_tracker_period_totals, [:period, :period_start],
|
|
16
|
-
unique: true unless index_exists?(:llm_cost_tracker_period_totals, [:period, :period_start])
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def down
|
|
20
|
-
remove_index :llm_cost_tracker_period_totals, [:period, :period_start] if index_exists?(:llm_cost_tracker_period_totals, [:period, :period_start])
|
|
21
|
-
drop_table :llm_cost_tracker_period_totals if table_exists?(:llm_cost_tracker_period_totals)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def backfill_period_totals
|
|
27
|
-
backfill_legacy_monthly_totals if table_exists?(:llm_cost_tracker_monthly_totals)
|
|
28
|
-
return unless table_exists?(:llm_api_calls)
|
|
29
|
-
|
|
30
|
-
backfill_period_total("day", day_bucket_sql)
|
|
31
|
-
backfill_period_total("month", month_bucket_sql)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def backfill_legacy_monthly_totals
|
|
35
|
-
execute <<~SQL
|
|
36
|
-
INSERT INTO llm_cost_tracker_period_totals (period, period_start, total_cost, created_at, updated_at)
|
|
37
|
-
SELECT #{connection.quote("month")} AS period,
|
|
38
|
-
month AS period_start,
|
|
39
|
-
total_cost,
|
|
40
|
-
CURRENT_TIMESTAMP,
|
|
41
|
-
CURRENT_TIMESTAMP
|
|
42
|
-
FROM llm_cost_tracker_monthly_totals legacy
|
|
43
|
-
WHERE NOT EXISTS (
|
|
44
|
-
SELECT 1
|
|
45
|
-
FROM llm_cost_tracker_period_totals existing
|
|
46
|
-
WHERE existing.period = #{connection.quote("month")}
|
|
47
|
-
AND existing.period_start = legacy.month
|
|
48
|
-
)
|
|
49
|
-
SQL
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def backfill_period_total(period, bucket_sql)
|
|
53
|
-
execute <<~SQL
|
|
54
|
-
INSERT INTO llm_cost_tracker_period_totals (period, period_start, total_cost, created_at, updated_at)
|
|
55
|
-
SELECT aggregated.period,
|
|
56
|
-
aggregated.period_start,
|
|
57
|
-
aggregated.total_cost,
|
|
58
|
-
CURRENT_TIMESTAMP,
|
|
59
|
-
CURRENT_TIMESTAMP
|
|
60
|
-
FROM (
|
|
61
|
-
SELECT #{connection.quote(period)} AS period,
|
|
62
|
-
#{bucket_sql} AS period_start,
|
|
63
|
-
SUM(total_cost) AS total_cost
|
|
64
|
-
FROM llm_api_calls
|
|
65
|
-
WHERE total_cost IS NOT NULL
|
|
66
|
-
GROUP BY #{bucket_sql}
|
|
67
|
-
) aggregated
|
|
68
|
-
WHERE NOT EXISTS (
|
|
69
|
-
SELECT 1
|
|
70
|
-
FROM llm_cost_tracker_period_totals existing
|
|
71
|
-
WHERE existing.period = aggregated.period
|
|
72
|
-
AND existing.period_start = aggregated.period_start
|
|
73
|
-
)
|
|
74
|
-
SQL
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def day_bucket_sql
|
|
78
|
-
if postgresql?
|
|
79
|
-
"DATE_TRUNC('day', tracked_at)::date"
|
|
80
|
-
elsif mysql?
|
|
81
|
-
"DATE(tracked_at)"
|
|
82
|
-
else
|
|
83
|
-
raise "LLM Cost Tracker supports PostgreSQL and MySQL only"
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def month_bucket_sql
|
|
88
|
-
if postgresql?
|
|
89
|
-
"DATE_TRUNC('month', tracked_at)::date"
|
|
90
|
-
elsif mysql?
|
|
91
|
-
"DATE_FORMAT(tracked_at, '%Y-%m-01')"
|
|
92
|
-
else
|
|
93
|
-
raise "LLM Cost Tracker supports PostgreSQL and MySQL only"
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def postgresql?
|
|
98
|
-
LlmCostTracker::Ledger::Schema::Adapter.postgresql?(connection)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def mysql?
|
|
102
|
-
LlmCostTracker::Ledger::Schema::Adapter.mysql?(connection)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
class AddProviderResponseIdToLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
-
def up
|
|
3
|
-
return if column_exists?(:llm_api_calls, :provider_response_id)
|
|
4
|
-
|
|
5
|
-
add_column :llm_api_calls, :provider_response_id, :string
|
|
6
|
-
add_index :llm_api_calls, :provider_response_id
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def down
|
|
10
|
-
return unless column_exists?(:llm_api_calls, :provider_response_id)
|
|
11
|
-
|
|
12
|
-
remove_index :llm_api_calls, :provider_response_id if index_exists?(:llm_api_calls, :provider_response_id)
|
|
13
|
-
remove_column :llm_api_calls, :provider_response_id
|
|
14
|
-
end
|
|
15
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
class AddStreamingToLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
-
def up
|
|
3
|
-
unless column_exists?(:llm_api_calls, :stream)
|
|
4
|
-
add_column :llm_api_calls, :stream, :boolean, null: false, default: false
|
|
5
|
-
end
|
|
6
|
-
|
|
7
|
-
unless column_exists?(:llm_api_calls, :usage_source)
|
|
8
|
-
add_column :llm_api_calls, :usage_source, :string
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def down
|
|
13
|
-
if column_exists?(:llm_api_calls, :usage_source)
|
|
14
|
-
remove_column :llm_api_calls, :usage_source
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
if column_exists?(:llm_api_calls, :stream)
|
|
18
|
-
remove_column :llm_api_calls, :stream
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
class AddTokenUsageToLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
-
def up
|
|
3
|
-
<% LlmCostTracker::Generators::AddTokenUsageGenerator::TOKEN_COLUMNS.each do |column| -%>
|
|
4
|
-
unless column_exists?(:llm_api_calls, :<%= column %>)
|
|
5
|
-
add_column :llm_api_calls, :<%= column %>, :integer, null: false, default: 0
|
|
6
|
-
end
|
|
7
|
-
<% end -%>
|
|
8
|
-
<% LlmCostTracker::Generators::AddTokenUsageGenerator::COST_COLUMNS.each do |column| -%>
|
|
9
|
-
unless column_exists?(:llm_api_calls, :<%= column %>)
|
|
10
|
-
add_column :llm_api_calls, :<%= column %>, :decimal, precision: 20, scale: 8
|
|
11
|
-
end
|
|
12
|
-
<% end -%>
|
|
13
|
-
add_column :llm_api_calls, :pricing_mode, :string unless column_exists?(:llm_api_calls, :pricing_mode)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def down
|
|
17
|
-
remove_column :llm_api_calls, :pricing_mode if column_exists?(:llm_api_calls, :pricing_mode)
|
|
18
|
-
<% (LlmCostTracker::Generators::AddTokenUsageGenerator::COST_COLUMNS + LlmCostTracker::Generators::AddTokenUsageGenerator::TOKEN_COLUMNS).reverse.each do |column| -%>
|
|
19
|
-
remove_column :llm_api_calls, :<%= column %> if column_exists?(:llm_api_calls, :<%= column %>)
|
|
20
|
-
<% end -%>
|
|
21
|
-
end
|
|
22
|
-
end
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
require "llm_cost_tracker/ledger/schema/adapter"
|
|
2
|
-
|
|
3
|
-
class CreateLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
-
def change
|
|
5
|
-
create_table :llm_api_calls do |t|
|
|
6
|
-
t.string :event_id, null: false
|
|
7
|
-
t.string :provider, null: false
|
|
8
|
-
t.string :model, null: false
|
|
9
|
-
<% LlmCostTracker::TokenUsage::STORED_KEYS.each do |column| -%>
|
|
10
|
-
t.integer :<%= column %>, null: false, default: 0
|
|
11
|
-
<% end -%>
|
|
12
|
-
<% LlmCostTracker::Pricing::COST_KEYS.each do |column| -%>
|
|
13
|
-
t.decimal :<%= column %>, precision: 20, scale: 8
|
|
14
|
-
<% end -%>
|
|
15
|
-
t.integer :latency_ms
|
|
16
|
-
t.boolean :stream, null: false, default: false
|
|
17
|
-
t.string :usage_source
|
|
18
|
-
t.string :provider_response_id
|
|
19
|
-
t.string :pricing_mode
|
|
20
|
-
if postgresql?
|
|
21
|
-
t.jsonb :tags, null: false, default: {}
|
|
22
|
-
elsif mysql?
|
|
23
|
-
t.json :tags, null: false
|
|
24
|
-
else
|
|
25
|
-
raise "LLM Cost Tracker supports PostgreSQL and MySQL only"
|
|
26
|
-
end
|
|
27
|
-
t.datetime :tracked_at, null: false
|
|
28
|
-
|
|
29
|
-
t.timestamps
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
create_table :llm_cost_tracker_period_totals do |t|
|
|
33
|
-
t.string :period, null: false
|
|
34
|
-
t.date :period_start, null: false
|
|
35
|
-
t.decimal :total_cost, precision: 20, scale: 8, null: false, default: 0
|
|
36
|
-
|
|
37
|
-
t.timestamps
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
create_table :llm_cost_tracker_inbox_events do |t|
|
|
41
|
-
t.string :event_id, null: false
|
|
42
|
-
t.decimal :total_cost, precision: 20, scale: 8
|
|
43
|
-
t.datetime :tracked_at, null: false
|
|
44
|
-
t.text :payload, null: false
|
|
45
|
-
t.datetime :locked_at
|
|
46
|
-
t.string :locked_by
|
|
47
|
-
t.integer :attempts, null: false, default: 0
|
|
48
|
-
t.text :last_error
|
|
49
|
-
|
|
50
|
-
t.timestamps
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
create_table :llm_cost_tracker_ingestor_leases do |t|
|
|
54
|
-
t.string :name, null: false
|
|
55
|
-
t.string :locked_by
|
|
56
|
-
t.datetime :locked_until
|
|
57
|
-
|
|
58
|
-
t.timestamps
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
add_index :llm_api_calls, :event_id, unique: true
|
|
62
|
-
add_index :llm_api_calls, :tracked_at
|
|
63
|
-
add_index :llm_api_calls, [:provider, :tracked_at]
|
|
64
|
-
add_index :llm_api_calls, [:model, :tracked_at]
|
|
65
|
-
add_index :llm_api_calls, :provider_response_id
|
|
66
|
-
add_index :llm_api_calls, :tags, using: :gin if postgresql?
|
|
67
|
-
add_index :llm_cost_tracker_period_totals, [:period, :period_start], unique: true
|
|
68
|
-
add_index :llm_cost_tracker_inbox_events, :event_id, unique: true
|
|
69
|
-
add_index :llm_cost_tracker_inbox_events, :tracked_at
|
|
70
|
-
add_index :llm_cost_tracker_inbox_events, [:locked_at, :id]
|
|
71
|
-
add_index :llm_cost_tracker_ingestor_leases, :name, unique: true
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
private
|
|
75
|
-
|
|
76
|
-
def postgresql?
|
|
77
|
-
LlmCostTracker::Ledger::Schema::Adapter.postgresql?(connection)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def mysql?
|
|
81
|
-
LlmCostTracker::Ledger::Schema::Adapter.mysql?(connection)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
class UpgradeLlmApiCallCostPrecision < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
-
COST_COLUMNS = %i[
|
|
3
|
-
input_cost
|
|
4
|
-
cache_read_input_cost
|
|
5
|
-
cache_write_input_cost
|
|
6
|
-
cache_write_1h_input_cost
|
|
7
|
-
output_cost
|
|
8
|
-
total_cost
|
|
9
|
-
].freeze
|
|
10
|
-
|
|
11
|
-
def up
|
|
12
|
-
COST_COLUMNS.each do |column|
|
|
13
|
-
next unless column_exists?(:llm_api_calls, column)
|
|
14
|
-
|
|
15
|
-
change_column :llm_api_calls, column, :decimal, precision: 20, scale: 8
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def down
|
|
20
|
-
COST_COLUMNS.each do |column|
|
|
21
|
-
next unless column_exists?(:llm_api_calls, column)
|
|
22
|
-
|
|
23
|
-
change_column :llm_api_calls, column, :decimal, precision: 12, scale: 8
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
require "llm_cost_tracker/ledger/schema/adapter"
|
|
2
|
-
|
|
3
|
-
class UpgradeLlmApiCallTagsToJsonb < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
-
def up
|
|
5
|
-
unless postgresql?
|
|
6
|
-
say "Skipping llm_api_calls.tags JSONB upgrade: database adapter is #{connection.adapter_name}."
|
|
7
|
-
return
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
return if tags_jsonb?
|
|
11
|
-
|
|
12
|
-
remove_index :llm_api_calls, :tags if index_exists?(:llm_api_calls, :tags)
|
|
13
|
-
say "Upgrading llm_api_calls.tags to jsonb rewrites the table on PostgreSQL. Run this migration during a maintenance window on large datasets."
|
|
14
|
-
|
|
15
|
-
change_column(
|
|
16
|
-
:llm_api_calls,
|
|
17
|
-
:tags,
|
|
18
|
-
:jsonb,
|
|
19
|
-
using: "CASE WHEN tags IS NULL OR tags = '' THEN '{}'::jsonb ELSE tags::jsonb END",
|
|
20
|
-
default: {},
|
|
21
|
-
null: false
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
add_index :llm_api_calls, :tags, using: :gin unless index_exists?(:llm_api_calls, :tags)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def down
|
|
28
|
-
return unless postgresql?
|
|
29
|
-
|
|
30
|
-
remove_index :llm_api_calls, :tags if index_exists?(:llm_api_calls, :tags)
|
|
31
|
-
change_column :llm_api_calls, :tags, :text, using: "tags::text"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def postgresql?
|
|
37
|
-
LlmCostTracker::Ledger::Schema::Adapter.postgresql?(connection)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def tags_jsonb?
|
|
41
|
-
column = connection.columns(:llm_api_calls).find { |candidate| candidate.name == "tags" }
|
|
42
|
-
column&.sql_type.to_s.downcase == "jsonb"
|
|
43
|
-
end
|
|
44
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rails/generators"
|
|
4
|
-
require "rails/generators/active_record"
|
|
5
|
-
|
|
6
|
-
module LlmCostTracker
|
|
7
|
-
module Generators
|
|
8
|
-
class UpgradeCostPrecisionGenerator < Rails::Generators::Base
|
|
9
|
-
include ActiveRecord::Generators::Migration
|
|
10
|
-
|
|
11
|
-
source_root File.expand_path("templates", __dir__)
|
|
12
|
-
|
|
13
|
-
desc "Creates a migration to widen llm_api_calls cost decimal precision"
|
|
14
|
-
|
|
15
|
-
def create_migration_file
|
|
16
|
-
migration_template(
|
|
17
|
-
"upgrade_llm_api_call_cost_precision.rb.erb",
|
|
18
|
-
"db/migrate/upgrade_llm_api_call_cost_precision.rb"
|
|
19
|
-
)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def migration_version
|
|
25
|
-
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "rails/generators"
|
|
4
|
-
require "rails/generators/active_record"
|
|
5
|
-
|
|
6
|
-
module LlmCostTracker
|
|
7
|
-
module Generators
|
|
8
|
-
class UpgradeTagsToJsonbGenerator < Rails::Generators::Base
|
|
9
|
-
include ActiveRecord::Generators::Migration
|
|
10
|
-
|
|
11
|
-
source_root File.expand_path("templates", __dir__)
|
|
12
|
-
|
|
13
|
-
desc "Creates a migration to upgrade llm_api_calls.tags to PostgreSQL JSONB"
|
|
14
|
-
|
|
15
|
-
def create_migration_file
|
|
16
|
-
migration_template(
|
|
17
|
-
"upgrade_llm_api_call_tags_to_jsonb.rb.erb",
|
|
18
|
-
"db/migrate/upgrade_llm_api_call_tags_to_jsonb.rb"
|
|
19
|
-
)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def migration_version
|
|
25
|
-
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bigdecimal"
|
|
4
|
-
|
|
5
|
-
require_relative "../period"
|
|
6
|
-
|
|
7
|
-
module LlmCostTracker
|
|
8
|
-
module Ledger
|
|
9
|
-
class Rollups
|
|
10
|
-
class Batch
|
|
11
|
-
def self.rows(events)
|
|
12
|
-
new(events).rows
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def initialize(events)
|
|
16
|
-
@events = events
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def rows
|
|
20
|
-
totals.map do |(period, period_start), total_cost|
|
|
21
|
-
{
|
|
22
|
-
period: period,
|
|
23
|
-
period_start: period_start,
|
|
24
|
-
total_cost: total_cost
|
|
25
|
-
}
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
attr_reader :events
|
|
32
|
-
|
|
33
|
-
def totals
|
|
34
|
-
events.each_with_object(Hash.new { |hash, key| hash[key] = BigDecimal("0") }) do |event, rows|
|
|
35
|
-
Period::PERIODS.each do |period, name|
|
|
36
|
-
rows[[name, Period.bucket(period, event.tracked_at)]] += BigDecimal(event.total_cost.to_s)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LlmCostTracker
|
|
4
|
-
module Ledger
|
|
5
|
-
module Schema
|
|
6
|
-
module PeriodTotals
|
|
7
|
-
REQUIRED_COLUMNS = %w[period period_start total_cost].freeze
|
|
8
|
-
UNIQUE_COLUMNS = %i[period period_start].freeze
|
|
9
|
-
|
|
10
|
-
class << self
|
|
11
|
-
def current_schema_errors
|
|
12
|
-
connection = Ledger::Call.connection
|
|
13
|
-
table_name = Ledger::Period::Total.table_name
|
|
14
|
-
return ["llm_cost_tracker_period_totals table is missing"] unless connection.data_source_exists?(table_name)
|
|
15
|
-
|
|
16
|
-
errors = []
|
|
17
|
-
missing = REQUIRED_COLUMNS - Ledger::Period::Total.columns_hash.keys
|
|
18
|
-
errors << "missing columns: #{missing.join(', ')}" if missing.any?
|
|
19
|
-
errors << "missing unique index: period, period_start" unless unique_period_index?(connection, table_name)
|
|
20
|
-
errors
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def unique_period_index?(connection, table_name)
|
|
26
|
-
connection.index_exists?(table_name, UNIQUE_COLUMNS, unique: true)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LlmCostTracker
|
|
4
|
-
module Pricing
|
|
5
|
-
Component = Data.define(:price_key, :token_key, :cost_key)
|
|
6
|
-
|
|
7
|
-
COMPONENTS = [
|
|
8
|
-
Component.new(
|
|
9
|
-
price_key: :input,
|
|
10
|
-
token_key: :input_tokens,
|
|
11
|
-
cost_key: :input_cost
|
|
12
|
-
),
|
|
13
|
-
Component.new(
|
|
14
|
-
price_key: :cache_read_input,
|
|
15
|
-
token_key: :cache_read_input_tokens,
|
|
16
|
-
cost_key: :cache_read_input_cost
|
|
17
|
-
),
|
|
18
|
-
Component.new(
|
|
19
|
-
price_key: :cache_write_input,
|
|
20
|
-
token_key: :cache_write_input_tokens,
|
|
21
|
-
cost_key: :cache_write_input_cost
|
|
22
|
-
),
|
|
23
|
-
Component.new(
|
|
24
|
-
price_key: :cache_write_1h_input,
|
|
25
|
-
token_key: :cache_write_1h_input_tokens,
|
|
26
|
-
cost_key: :cache_write_1h_input_cost
|
|
27
|
-
),
|
|
28
|
-
Component.new(
|
|
29
|
-
price_key: :output,
|
|
30
|
-
token_key: :output_tokens,
|
|
31
|
-
cost_key: :output_cost
|
|
32
|
-
)
|
|
33
|
-
].freeze
|
|
34
|
-
|
|
35
|
-
COST_KEYS = (COMPONENTS.map(&:cost_key) + %i[total_cost]).freeze
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
require "yaml"
|
|
5
|
-
|
|
6
|
-
require_relative "../registry"
|
|
7
|
-
|
|
8
|
-
module LlmCostTracker
|
|
9
|
-
module Pricing
|
|
10
|
-
module Sync
|
|
11
|
-
class RegistryLoader
|
|
12
|
-
YAML_EXTENSIONS = %w[.yml .yaml].freeze
|
|
13
|
-
|
|
14
|
-
def call(path:, seed_path:)
|
|
15
|
-
source_path = File.exist?(path.to_s) ? path.to_s : seed_path.to_s
|
|
16
|
-
normalize_registry(load_registry_file(source_path))
|
|
17
|
-
rescue Errno::ENOENT, JSON::ParserError, Psych::Exception, ArgumentError, TypeError => e
|
|
18
|
-
raise Error, "Unable to load pricing registry #{source_path.inspect}: #{e.message}"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def load_registry_file(path)
|
|
24
|
-
if File.size(path) > Registry::MAX_FILE_BYTES
|
|
25
|
-
raise ArgumentError, "pricing registry exceeds #{Registry::MAX_FILE_BYTES} bytes"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
contents = File.read(path)
|
|
29
|
-
registry = yaml_file?(path) ? (YAML.safe_load(contents, aliases: false) || {}) : JSON.parse(contents)
|
|
30
|
-
raise ArgumentError, "pricing registry must be a hash" unless registry.is_a?(Hash)
|
|
31
|
-
|
|
32
|
-
registry
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def normalize_registry(registry)
|
|
36
|
-
{
|
|
37
|
-
"metadata" => normalize_hash(registry.fetch("metadata", {}), label: "pricing metadata"),
|
|
38
|
-
"models" => normalize_models(registry.fetch("models", {}))
|
|
39
|
-
}
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def normalize_models(models)
|
|
43
|
-
normalize_hash(models, label: "pricing models").each_with_object({}) do |(model, entry), normalized|
|
|
44
|
-
normalized[model.to_s] = normalize_hash(entry, label: "pricing model entry")
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def normalize_hash(hash, label:)
|
|
49
|
-
return {} if hash.nil?
|
|
50
|
-
raise ArgumentError, "#{label} must be a hash" unless hash.is_a?(Hash)
|
|
51
|
-
|
|
52
|
-
hash.each_with_object({}) do |(key, value), normalized|
|
|
53
|
-
normalized[key.to_s] = value
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def yaml_file?(path)
|
|
58
|
-
YAML_EXTENSIONS.include?(File.extname(path).downcase)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|