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,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "llm_cost_tracker/ledger/tags/sql"
4
-
5
- module LlmCostTracker
6
- module Ledger
7
- module CallMetrics
8
- def total_cost
9
- sum(:total_cost).to_f
10
- end
11
-
12
- def total_tokens
13
- sum(:total_tokens).to_i
14
- end
15
-
16
- def cost_by_model(limit: nil)
17
- cost_by_column(:model, limit: limit)
18
- end
19
-
20
- def cost_by_provider(limit: nil)
21
- cost_by_column(:provider, limit: limit)
22
- end
23
-
24
- def group_by_tag(key)
25
- group(Arel.sql(tag_value_expression(key)))
26
- end
27
-
28
- def cost_by_tag(key, limit: nil)
29
- expression = tag_value_expression(key)
30
- label_expression = "COALESCE(NULLIF(#{expression}, ''), #{connection.quote('(untagged)')})"
31
- relation = select("#{label_expression} AS name, COALESCE(SUM(total_cost), 0) AS total_cost")
32
- .group(Arel.sql(label_expression))
33
- .order(Arel.sql("COALESCE(SUM(total_cost), 0) DESC"))
34
- relation = relation.limit(limit) if limit
35
- relation
36
- end
37
-
38
- def average_latency_ms
39
- average(:latency_ms)&.to_f
40
- end
41
-
42
- def latency_by_model
43
- group(:model).average(:latency_ms).transform_values(&:to_f)
44
- end
45
-
46
- def latency_by_provider
47
- group(:provider).average(:latency_ms).transform_values(&:to_f)
48
- end
49
-
50
- def tag_value_expression(key, table_name: quoted_table_name)
51
- Ledger::Tags::Sql.value_expression(key, table_name: table_name)
52
- end
53
-
54
- private
55
-
56
- def cost_by_column(column, limit:)
57
- quoted_column = "#{quoted_table_name}.#{connection.quote_column_name(column)}"
58
- relation = select("#{quoted_column} AS name, COALESCE(SUM(total_cost), 0) AS total_cost")
59
- .group(column)
60
- .order(Arel.sql("COALESCE(SUM(total_cost), 0) DESC"))
61
- relation = relation.limit(limit) if limit
62
- relation
63
- end
64
- end
65
- end
66
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "llm_cost_tracker/ledger/schema/adapter"
4
-
5
- module LlmCostTracker
6
- module Ledger
7
- module Period
8
- module Grouping
9
- PERIOD_FORMATS = {
10
- day: {
11
- postgres: "YYYY-MM-DD",
12
- mysql: "%Y-%m-%d"
13
- },
14
- month: {
15
- postgres: "YYYY-MM",
16
- mysql: "%Y-%m"
17
- }
18
- }.freeze
19
-
20
- private_constant :PERIOD_FORMATS
21
-
22
- def group_by_period(period, column: :tracked_at)
23
- group(Arel.sql(period_group_expression(period, column: column)))
24
- end
25
-
26
- def daily_costs(days: 30)
27
- where(tracked_at: days.days.ago..)
28
- .group_by_period(:day)
29
- .sum(:total_cost)
30
- end
31
-
32
- private
33
-
34
- def period_group_expression(period, column:)
35
- period = validated_period(period)
36
- column = period_column_expression(column)
37
- formats = PERIOD_FORMATS.fetch(period)
38
-
39
- if Ledger::Schema::Adapter.postgresql?(connection)
40
- postgres_period_expression(period, column, formats)
41
- elsif Ledger::Schema::Adapter.mysql?(connection)
42
- "DATE_FORMAT(#{column}, #{connection.quote(formats.fetch(:mysql))})"
43
- else
44
- Ledger::Schema::Adapter.ensure_supported!(connection)
45
- end
46
- end
47
-
48
- def postgres_period_expression(period, column, formats)
49
- "TO_CHAR(" \
50
- "DATE_TRUNC(#{connection.quote(period.to_s)}, #{column}), " \
51
- "#{connection.quote(formats.fetch(:postgres))}" \
52
- ")"
53
- end
54
-
55
- def validated_period(period)
56
- normalized_period = period.try(:to_sym)
57
- return normalized_period if PERIOD_FORMATS.key?(normalized_period)
58
-
59
- raise ArgumentError, "invalid period: #{period.inspect}"
60
- end
61
-
62
- def period_column_expression(column)
63
- column = column.to_s
64
- return "#{quoted_table_name}.#{connection.quote_column_name(column)}" if column_names.include?(column)
65
-
66
- raise ArgumentError, "invalid period column: #{column.inspect}"
67
- end
68
- end
69
- end
70
- end
71
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
-
5
- module LlmCostTracker
6
- module Ledger
7
- module Period
8
- class Total < ActiveRecord::Base
9
- self.table_name = "llm_cost_tracker_period_totals"
10
- end
11
- end
12
- end
13
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module LlmCostTracker
6
- module Ledger
7
- module Tags
8
- module Accessors
9
- def parsed_tags
10
- return tags.transform_keys(&:to_s) if tags.is_a?(Hash)
11
-
12
- JSON.parse(tags || "{}")
13
- rescue JSON::ParserError
14
- {}
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module ConfigurationInstrumentation
5
- def instrument(*names)
6
- ensure_shared_configuration_mutable!
7
- @instrumented_integrations = (@instrumented_integrations + normalize_instrumentation_names(names)).uniq
8
- end
9
-
10
- def instrumented?(name)
11
- @instrumented_integrations.include?(name.to_sym)
12
- end
13
-
14
- private
15
-
16
- def normalize_instrumentation_names(names)
17
- names.flatten.flat_map do |name|
18
- key = name.to_sym
19
- next Integrations.names if key == :all
20
-
21
- validate_instrumentation_name!(key)
22
- key
23
- end
24
- end
25
-
26
- def validate_instrumentation_name!(name)
27
- return if Integrations.names.include?(name)
28
-
29
- raise Error, "Unknown integration: #{name.inspect}. " \
30
- "Use one of: #{Integrations.names.join(', ')}"
31
- end
32
- end
33
- 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 AddIngestionGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add durable ActiveRecord ingestion"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_ingestion_to_llm_cost_tracker.rb.erb",
18
- "db/migrate/add_ingestion_to_llm_cost_tracker.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 AddLatencyMsGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add llm_api_calls.latency_ms"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_latency_ms_to_llm_api_calls.rb.erb",
18
- "db/migrate/add_latency_ms_to_llm_api_calls.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 AddPeriodTotalsGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add llm_cost_tracker_period_totals"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_period_totals_to_llm_cost_tracker.rb.erb",
18
- "db/migrate/add_period_totals_to_llm_cost_tracker.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 AddProviderResponseIdGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add llm_api_calls.provider_response_id"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_provider_response_id_to_llm_api_calls.rb.erb",
18
- "db/migrate/add_provider_response_id_to_llm_api_calls.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 AddStreamingGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add llm_api_calls.stream and llm_api_calls.usage_source"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_streaming_to_llm_api_calls.rb.erb",
18
- "db/migrate/add_streaming_to_llm_api_calls.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,42 +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 AddTokenUsageGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- TOKEN_COLUMNS = %w[
12
- cache_read_input_tokens
13
- cache_write_input_tokens
14
- cache_write_1h_input_tokens
15
- hidden_output_tokens
16
- ].freeze
17
- COST_COLUMNS = %w[
18
- cache_read_input_cost
19
- cache_write_input_cost
20
- cache_write_1h_input_cost
21
- ].freeze
22
- COLUMN_NAMES = (TOKEN_COLUMNS + COST_COLUMNS + %w[pricing_mode]).freeze
23
-
24
- source_root File.expand_path("templates", __dir__)
25
-
26
- desc "Creates a migration to add token usage and token cost columns to llm_api_calls"
27
-
28
- def create_migration_file
29
- migration_template(
30
- "add_token_usage_to_llm_api_calls.rb.erb",
31
- "db/migrate/add_token_usage_to_llm_api_calls.rb"
32
- )
33
- end
34
-
35
- private
36
-
37
- def migration_version
38
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
39
- end
40
- end
41
- end
42
- end
@@ -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