llm_cost_tracker 0.7.3 → 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 +66 -1
- data/README.md +58 -225
- 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 +121 -30
- 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 +2 -2
- 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 +96 -13
- data/lib/llm_cost_tracker/ledger/tags/query.rb +4 -10
- data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -15
- 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,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
|