llm_cost_tracker 0.7.3 → 0.9.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 (195) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +173 -0
  4. data/README.md +60 -220
  5. data/app/assets/llm_cost_tracker/application.css +282 -45
  6. data/app/controllers/llm_cost_tracker/application_controller.rb +25 -20
  7. data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
  8. data/app/controllers/llm_cost_tracker/calls_controller.rb +22 -19
  9. data/app/controllers/llm_cost_tracker/data_quality_controller.rb +14 -2
  10. data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +106 -0
  11. data/app/controllers/llm_cost_tracker/tags_controller.rb +15 -1
  12. data/app/helpers/llm_cost_tracker/application_helper.rb +18 -21
  13. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
  14. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
  15. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
  16. data/app/helpers/llm_cost_tracker/inline_style_helper.rb +28 -0
  17. data/app/helpers/llm_cost_tracker/reconciliation_helper.rb +13 -0
  18. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +24 -7
  19. data/app/models/llm_cost_tracker/call.rb +166 -0
  20. data/app/models/llm_cost_tracker/call_line_item.rb +18 -0
  21. data/app/models/llm_cost_tracker/call_rollup.rb +6 -0
  22. data/app/models/llm_cost_tracker/call_tag.rb +12 -0
  23. data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +9 -0
  24. data/app/models/llm_cost_tracker/ingestion/lease.rb +0 -3
  25. data/app/models/llm_cost_tracker/provider_invoice.rb +13 -0
  26. data/app/models/llm_cost_tracker/provider_invoice_import.rb +24 -0
  27. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +152 -32
  28. data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
  29. data/app/services/llm_cost_tracker/dashboard/filter.rb +8 -6
  30. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
  31. data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
  32. data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
  33. data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
  34. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
  35. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
  36. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
  37. data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
  38. data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
  39. data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -1
  40. data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
  41. data/app/views/llm_cost_tracker/calls/show.html.erb +73 -33
  42. data/app/views/llm_cost_tracker/dashboard/index.html.erb +16 -57
  43. data/app/views/llm_cost_tracker/data_quality/index.html.erb +183 -167
  44. data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
  45. data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
  46. data/app/views/llm_cost_tracker/reconciliation/index.html.erb +183 -0
  47. data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -1
  48. data/app/views/llm_cost_tracker/shared/_filters.html.erb +66 -0
  49. data/app/views/llm_cost_tracker/shared/_metric_stack.html.erb +1 -1
  50. data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
  51. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
  52. data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
  53. data/app/views/llm_cost_tracker/tags/show.html.erb +64 -36
  54. data/config/routes.rb +3 -2
  55. data/lib/llm_cost_tracker/billing/components.rb +95 -0
  56. data/lib/llm_cost_tracker/billing/components.yml +188 -0
  57. data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
  58. data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
  59. data/lib/llm_cost_tracker/budget.rb +26 -36
  60. data/lib/llm_cost_tracker/capture/stream_collector.rb +125 -38
  61. data/lib/llm_cost_tracker/capture/stream_tracker.rb +40 -5
  62. data/lib/llm_cost_tracker/configuration.rb +86 -17
  63. data/lib/llm_cost_tracker/dashboard_setup_state.rb +109 -0
  64. data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +56 -0
  65. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +48 -30
  66. data/lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb +164 -0
  67. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
  68. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
  69. data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
  70. data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
  71. data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
  72. data/lib/llm_cost_tracker/doctor/schema_check.rb +34 -0
  73. data/lib/llm_cost_tracker/doctor.rb +111 -44
  74. data/lib/llm_cost_tracker/engine.rb +9 -0
  75. data/lib/llm_cost_tracker/errors.rb +5 -19
  76. data/lib/llm_cost_tracker/event.rb +11 -3
  77. data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +43 -0
  78. data/lib/llm_cost_tracker/generators/llm_cost_tracker/durable_ingestion_generator.rb +43 -0
  79. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +17 -5
  80. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
  81. data/lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb +34 -0
  82. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +15 -0
  83. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +104 -0
  84. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_durable_ingestion.rb.erb +29 -0
  85. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +55 -0
  86. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +28 -25
  87. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +20 -0
  88. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +32 -0
  89. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +18 -0
  90. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +38 -0
  91. data/lib/llm_cost_tracker/generators/llm_cost_tracker/{add_provider_response_id_generator.rb → upgrade_call_tags_key_value_index_generator.rb} +5 -4
  92. data/lib/llm_cost_tracker/generators/llm_cost_tracker/{add_streaming_generator.rb → upgrade_image_tokens_generator.rb} +4 -4
  93. data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
  94. data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -24
  95. data/lib/llm_cost_tracker/ingestion/inline.rb +22 -0
  96. data/lib/llm_cost_tracker/ingestion/worker.rb +24 -7
  97. data/lib/llm_cost_tracker/ingestion.rb +66 -22
  98. data/lib/llm_cost_tracker/integrations/anthropic.rb +68 -42
  99. data/lib/llm_cost_tracker/integrations/base.rb +56 -32
  100. data/lib/llm_cost_tracker/integrations/openai.rb +342 -63
  101. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +110 -11
  102. data/lib/llm_cost_tracker/integrations.rb +21 -3
  103. data/lib/llm_cost_tracker/ledger/period/totals.rb +30 -11
  104. data/lib/llm_cost_tracker/ledger/period.rb +5 -5
  105. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +2 -2
  106. data/lib/llm_cost_tracker/ledger/rollups.rb +90 -25
  107. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
  108. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +79 -0
  109. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
  110. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +41 -0
  111. data/lib/llm_cost_tracker/ledger/schema/calls.rb +36 -23
  112. data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +47 -0
  113. data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +42 -0
  114. data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +46 -0
  115. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
  116. data/lib/llm_cost_tracker/ledger/store.rb +103 -20
  117. data/lib/llm_cost_tracker/ledger/tags/encoding.rb +37 -0
  118. data/lib/llm_cost_tracker/ledger/tags/query.rb +6 -11
  119. data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -15
  120. data/lib/llm_cost_tracker/ledger.rb +5 -2
  121. data/lib/llm_cost_tracker/logging.rb +2 -5
  122. data/lib/llm_cost_tracker/masking.rb +39 -0
  123. data/lib/llm_cost_tracker/middleware/faraday.rb +95 -35
  124. data/lib/llm_cost_tracker/parsers/anthropic.rb +74 -14
  125. data/lib/llm_cost_tracker/parsers/base.rb +13 -4
  126. data/lib/llm_cost_tracker/parsers/gemini.rb +105 -15
  127. data/lib/llm_cost_tracker/parsers/openai.rb +16 -2
  128. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +15 -3
  129. data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +126 -0
  130. data/lib/llm_cost_tracker/parsers/openai_usage.rb +157 -59
  131. data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
  132. data/lib/llm_cost_tracker/parsers.rb +1 -1
  133. data/lib/llm_cost_tracker/prices.json +198 -22
  134. data/lib/llm_cost_tracker/pricing/effective_prices.rb +28 -21
  135. data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
  136. data/lib/llm_cost_tracker/pricing/lookup.rb +73 -36
  137. data/lib/llm_cost_tracker/pricing/mode.rb +76 -0
  138. data/lib/llm_cost_tracker/pricing/registry.rb +67 -45
  139. data/lib/llm_cost_tracker/pricing/service_charges.rb +210 -0
  140. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
  141. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
  142. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +50 -1
  143. data/lib/llm_cost_tracker/pricing/sync.rb +59 -10
  144. data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
  145. data/lib/llm_cost_tracker/pricing.rb +220 -28
  146. data/lib/llm_cost_tracker/railtie.rb +6 -8
  147. data/lib/llm_cost_tracker/reconcile_tasks.rb +134 -0
  148. data/lib/llm_cost_tracker/reconciliation/diff.rb +428 -0
  149. data/lib/llm_cost_tracker/reconciliation/diff_result.rb +48 -0
  150. data/lib/llm_cost_tracker/reconciliation/import_result.rb +19 -0
  151. data/lib/llm_cost_tracker/reconciliation/importer.rb +253 -0
  152. data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +171 -0
  153. data/lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb +20 -0
  154. data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +142 -0
  155. data/lib/llm_cost_tracker/reconciliation.rb +118 -0
  156. data/lib/llm_cost_tracker/report/data.rb +19 -8
  157. data/lib/llm_cost_tracker/report.rb +0 -4
  158. data/lib/llm_cost_tracker/retention.rb +22 -9
  159. data/lib/llm_cost_tracker/tags/context.rb +2 -5
  160. data/lib/llm_cost_tracker/tags/key.rb +4 -0
  161. data/lib/llm_cost_tracker/tags/sanitizer.rb +71 -20
  162. data/lib/llm_cost_tracker/timing.rb +15 -0
  163. data/lib/llm_cost_tracker/token_usage.rb +64 -42
  164. data/lib/llm_cost_tracker/tracker.rb +97 -27
  165. data/lib/llm_cost_tracker/usage_capture.rb +29 -8
  166. data/lib/llm_cost_tracker/version.rb +1 -1
  167. data/lib/llm_cost_tracker.rb +45 -35
  168. data/lib/tasks/llm_cost_tracker.rake +45 -17
  169. metadata +71 -41
  170. data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
  171. data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
  172. data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
  173. data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
  174. data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
  175. data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
  176. data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
  177. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
  178. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
  179. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
  180. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
  181. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
  182. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
  183. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
  184. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
  185. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
  186. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
  187. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
  188. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
  189. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
  190. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
  191. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
  192. data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
  193. data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
  194. data/lib/llm_cost_tracker/pricing/components.rb +0 -37
  195. data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +0 -63
@@ -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,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
@@ -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