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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +66 -1
  4. data/README.md +58 -225
  5. data/app/assets/llm_cost_tracker/application.css +218 -41
  6. data/app/controllers/llm_cost_tracker/application_controller.rb +30 -17
  7. data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
  8. data/app/controllers/llm_cost_tracker/calls_controller.rb +19 -14
  9. data/app/controllers/llm_cost_tracker/data_quality_controller.rb +10 -2
  10. data/app/helpers/llm_cost_tracker/application_helper.rb +11 -24
  11. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
  12. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
  13. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
  14. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +20 -7
  15. data/app/models/llm_cost_tracker/call.rb +169 -0
  16. data/app/models/llm_cost_tracker/call_line_item.rb +22 -0
  17. data/app/models/llm_cost_tracker/call_rollup.rb +9 -0
  18. data/app/models/llm_cost_tracker/call_tag.rb +16 -0
  19. data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +13 -0
  20. data/app/models/llm_cost_tracker/ingestion/lease.rb +1 -1
  21. data/app/models/llm_cost_tracker/provider_invoice.rb +9 -0
  22. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +121 -30
  23. data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
  24. data/app/services/llm_cost_tracker/dashboard/filter.rb +2 -2
  25. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
  26. data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
  27. data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
  28. data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
  29. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
  30. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
  31. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
  32. data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
  33. data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
  34. data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
  35. data/app/views/llm_cost_tracker/calls/show.html.erb +62 -7
  36. data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -50
  37. data/app/views/llm_cost_tracker/data_quality/index.html.erb +103 -126
  38. data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
  39. data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
  40. data/app/views/llm_cost_tracker/shared/_filters.html.erb +63 -0
  41. data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
  42. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
  43. data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
  44. data/app/views/llm_cost_tracker/tags/show.html.erb +5 -37
  45. data/lib/llm_cost_tracker/billing/components.rb +53 -0
  46. data/lib/llm_cost_tracker/billing/components.yml +117 -0
  47. data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
  48. data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
  49. data/lib/llm_cost_tracker/budget.rb +23 -35
  50. data/lib/llm_cost_tracker/capture/stream_collector.rb +47 -33
  51. data/lib/llm_cost_tracker/configuration.rb +36 -19
  52. data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +54 -0
  53. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +24 -32
  54. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
  55. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
  56. data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
  57. data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
  58. data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
  59. data/lib/llm_cost_tracker/doctor/schema_check.rb +31 -0
  60. data/lib/llm_cost_tracker/doctor.rb +43 -45
  61. data/lib/llm_cost_tracker/errors.rb +5 -19
  62. data/lib/llm_cost_tracker/event.rb +10 -2
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -2
  64. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
  65. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +157 -0
  66. data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
  67. data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -23
  68. data/lib/llm_cost_tracker/ingestion/worker.rb +14 -5
  69. data/lib/llm_cost_tracker/ingestion.rb +28 -22
  70. data/lib/llm_cost_tracker/integrations/anthropic.rb +45 -38
  71. data/lib/llm_cost_tracker/integrations/base.rb +36 -29
  72. data/lib/llm_cost_tracker/integrations/openai.rb +85 -40
  73. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +5 -5
  74. data/lib/llm_cost_tracker/integrations.rb +2 -2
  75. data/lib/llm_cost_tracker/ledger/period/totals.rb +12 -9
  76. data/lib/llm_cost_tracker/ledger/period.rb +5 -5
  77. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +2 -2
  78. data/lib/llm_cost_tracker/ledger/rollups.rb +76 -25
  79. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
  80. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +50 -0
  81. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
  82. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +26 -0
  83. data/lib/llm_cost_tracker/ledger/schema/calls.rb +34 -23
  84. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
  85. data/lib/llm_cost_tracker/ledger/store.rb +96 -13
  86. data/lib/llm_cost_tracker/ledger/tags/query.rb +4 -10
  87. data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -15
  88. data/lib/llm_cost_tracker/ledger.rb +4 -2
  89. data/lib/llm_cost_tracker/logging.rb +2 -5
  90. data/lib/llm_cost_tracker/middleware/faraday.rb +7 -6
  91. data/lib/llm_cost_tracker/parsers/anthropic.rb +52 -7
  92. data/lib/llm_cost_tracker/parsers/base.rb +8 -3
  93. data/lib/llm_cost_tracker/parsers/gemini.rb +101 -15
  94. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +10 -2
  95. data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +87 -0
  96. data/lib/llm_cost_tracker/parsers/openai_usage.rb +48 -21
  97. data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
  98. data/lib/llm_cost_tracker/parsers.rb +1 -1
  99. data/lib/llm_cost_tracker/prices.json +105 -20
  100. data/lib/llm_cost_tracker/pricing/effective_prices.rb +57 -19
  101. data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
  102. data/lib/llm_cost_tracker/pricing/lookup.rb +38 -34
  103. data/lib/llm_cost_tracker/pricing/registry.rb +65 -45
  104. data/lib/llm_cost_tracker/pricing/service_charges.rb +204 -0
  105. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
  106. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
  107. data/lib/llm_cost_tracker/pricing/sync.rb +57 -10
  108. data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
  109. data/lib/llm_cost_tracker/pricing.rb +190 -26
  110. data/lib/llm_cost_tracker/railtie.rb +0 -8
  111. data/lib/llm_cost_tracker/report/data.rb +16 -8
  112. data/lib/llm_cost_tracker/report.rb +0 -4
  113. data/lib/llm_cost_tracker/retention.rb +8 -8
  114. data/lib/llm_cost_tracker/tags/context.rb +2 -4
  115. data/lib/llm_cost_tracker/tags/key.rb +4 -0
  116. data/lib/llm_cost_tracker/tags/sanitizer.rb +12 -17
  117. data/lib/llm_cost_tracker/timing.rb +15 -0
  118. data/lib/llm_cost_tracker/token_usage.rb +56 -42
  119. data/lib/llm_cost_tracker/tracker.rb +67 -24
  120. data/lib/llm_cost_tracker/usage_capture.rb +29 -8
  121. data/lib/llm_cost_tracker/version.rb +1 -1
  122. data/lib/llm_cost_tracker.rb +36 -35
  123. data/lib/tasks/llm_cost_tracker.rake +22 -17
  124. metadata +36 -41
  125. data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
  126. data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
  127. data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
  128. data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
  129. data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
  130. data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
  131. data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
  132. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
  133. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
  134. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
  135. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb +0 -29
  136. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +0 -29
  137. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
  138. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
  139. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
  140. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
  141. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
  142. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
  143. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
  144. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
  145. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
  146. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
  147. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
  148. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
  149. data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
  150. data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
  151. data/lib/llm_cost_tracker/pricing/components.rb +0 -37
  152. 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