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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmCostTracker
4
- VERSION = "0.7.3"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -16,7 +16,11 @@ require_relative "llm_cost_tracker/logging"
16
16
  require_relative "llm_cost_tracker/tags/key"
17
17
  require_relative "llm_cost_tracker/tags/context"
18
18
  require_relative "llm_cost_tracker/tags/sanitizer"
19
+ require_relative "llm_cost_tracker/masking"
19
20
  require_relative "llm_cost_tracker/token_usage"
21
+ require_relative "llm_cost_tracker/billing/components"
22
+ require_relative "llm_cost_tracker/billing/line_item"
23
+ require_relative "llm_cost_tracker/billing/cost_status"
20
24
  require_relative "llm_cost_tracker/event"
21
25
  require_relative "llm_cost_tracker/pricing"
22
26
  require_relative "llm_cost_tracker/usage_capture"
@@ -42,19 +46,31 @@ require_relative "llm_cost_tracker/doctor"
42
46
  require_relative "llm_cost_tracker/doctor/capture_verifier"
43
47
 
44
48
  module LlmCostTracker
49
+ autoload :Reconciliation, "llm_cost_tracker/reconciliation"
50
+ autoload :ReconcileTasks, "llm_cost_tracker/reconcile_tasks"
51
+
45
52
  @configuration = Configuration.new
46
53
 
47
54
  class << self
48
55
  attr_reader :configuration
49
56
 
57
+ def table_name_prefix
58
+ "llm_cost_tracker_"
59
+ end
60
+
61
+ def reconciliation_enabled?
62
+ configuration.reconciliation_enabled
63
+ end
64
+
50
65
  def configure
51
66
  config = configuration
52
67
  raise Error, "LlmCostTracker is already configured" if config.finalized?
53
68
 
54
69
  yield(config)
55
- config.openai_compatible_providers = config.openai_compatible_providers.dup
56
70
  config.finalize!
57
71
  Pricing::Lookup.reset!
72
+ Pricing::Registry.reset!
73
+ Pricing::ServiceCharges.reset!
58
74
  Integrations.install!
59
75
  config
60
76
  end
@@ -63,67 +79,61 @@ module LlmCostTracker
63
79
  Ingestion::Worker.shutdown!(drain: false)
64
80
  @configuration = Configuration.new
65
81
  Pricing::Lookup.reset!
82
+ Pricing::Registry.reset!
83
+ Pricing::ServiceCharges.reset!
66
84
  Pricing::Unknown.reset!
67
85
  Ingestion::Worker.reset!
68
86
  Tags::Context.clear!
69
- end
70
-
71
- def flush!(timeout: nil)
72
- if timeout
73
- Ingestion::Worker.flush!(timeout: timeout)
74
- else
75
- Ingestion::Worker.flush!
76
- end
77
- end
78
-
79
- def shutdown!(timeout: nil, drain: true)
80
- if timeout
81
- Ingestion::Worker.shutdown!(timeout: timeout, drain: drain)
82
- else
83
- Ingestion::Worker.shutdown!(drain: drain)
84
- end
85
- end
86
-
87
- def enforce_budget!
88
- Tracker.enforce_budget!
87
+ DashboardSetupState.reset! if defined?(DashboardSetupState)
89
88
  end
90
89
 
91
90
  def with_tags(tags = nil, **kwargs, &)
92
- merged = (tags || {}).to_h.merge(kwargs)
93
- Tags::Context.with(merged, &)
91
+ Tags::Context.with((tags || {}).merge(kwargs), &)
94
92
  end
95
93
 
96
- def track(provider:, input_tokens:, output_tokens:, model: nil, latency_ms: nil, stream: false,
97
- usage_source: :manual, enforce_budget: false, provider_response_id: nil, pricing_mode: nil, **metadata)
98
- enforce_budget! if enforce_budget
99
- token_usage = TokenUsage.from_hash(metadata.merge(input_tokens: input_tokens, output_tokens: output_tokens))
94
+ def track(provider:, tokens:, model: nil, tags: {}, latency_ms: nil, stream: false,
95
+ usage_source: :manual, enforce_budget: false,
96
+ provider_response_id: nil, provider_project_id: nil, provider_api_key_id: nil,
97
+ provider_workspace_id: nil, batch: nil, pricing_mode: nil, service_line_items: [])
98
+ Tracker.enforce_budget! if enforce_budget
100
99
 
101
100
  Tracker.record(
102
101
  capture: UsageCapture.build(
103
102
  provider: provider,
104
103
  model: model,
105
- token_usage: token_usage,
104
+ token_usage: TokenUsage.build_from_tokens(tokens),
106
105
  stream: stream,
107
106
  usage_source: usage_source,
108
- provider_response_id: provider_response_id
107
+ provider_response_id: provider_response_id,
108
+ provider_project_id: provider_project_id,
109
+ provider_api_key_id: provider_api_key_id,
110
+ provider_workspace_id: provider_workspace_id,
111
+ batch: batch,
112
+ pricing_mode: pricing_mode,
113
+ service_line_items: service_line_items
109
114
  ),
110
115
  latency_ms: latency_ms,
111
116
  pricing_mode: pricing_mode,
112
- metadata: metadata
117
+ metadata: tags
113
118
  )
114
119
  end
115
120
 
116
- def track_stream(provider:, model: nil, latency_ms: nil, enforce_budget: false, provider_response_id: nil,
117
- pricing_mode: nil, **metadata)
121
+ def track_stream(provider:, model: nil, tags: {}, latency_ms: nil, enforce_budget: false,
122
+ provider_response_id: nil, provider_project_id: nil, provider_api_key_id: nil,
123
+ provider_workspace_id: nil, batch: nil, pricing_mode: nil)
118
124
  require_relative "llm_cost_tracker/capture/stream_collector"
119
- enforce_budget! if enforce_budget
125
+ Tracker.enforce_budget! if enforce_budget
120
126
  collector = Capture::StreamCollector.new(
121
127
  provider: provider.to_s,
122
128
  model: model,
123
129
  latency_ms: latency_ms,
124
130
  provider_response_id: provider_response_id,
131
+ provider_project_id: provider_project_id,
132
+ provider_api_key_id: provider_api_key_id,
133
+ provider_workspace_id: provider_workspace_id,
134
+ batch: batch,
125
135
  pricing_mode: pricing_mode,
126
- metadata: metadata
136
+ metadata: tags
127
137
  )
128
138
  yield collector
129
139
  collector.finish!
@@ -140,4 +150,4 @@ Faraday::Middleware.register_middleware(
140
150
  llm_cost_tracker: LlmCostTracker::Middleware::Faraday
141
151
  )
142
152
 
143
- at_exit { LlmCostTracker.shutdown!(drain: false) }
153
+ at_exit { LlmCostTracker::Ingestion::Worker.shutdown!(drain: false) }
@@ -1,9 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
+ require "rails/generators"
5
+
6
+ require_relative "../llm_cost_tracker/generators/llm_cost_tracker/install_generator"
7
+ require_relative "../llm_cost_tracker/pricing/sync_change_printer"
4
8
 
5
9
  # rubocop:disable Metrics/BlockLength
6
10
  namespace :llm_cost_tracker do
11
+ desc "Install LLM Cost Tracker with dashboard and prices, migrate, and run doctor"
12
+ task :setup do
13
+ Rails::Generators.invoke("llm_cost_tracker:install", %w[--dashboard --prices --skip])
14
+ begin
15
+ Rake::Task["db:migrate"].invoke
16
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished => e
17
+ abort(
18
+ "llm_cost_tracker: database is not reachable (#{e.class}). " \
19
+ "Start your database, run 'rails db:create db:migrate', then re-run 'rails llm_cost_tracker:setup'."
20
+ )
21
+ end
22
+ Rake::Task["llm_cost_tracker:doctor"].invoke
23
+ end
24
+
7
25
  desc "Check LLM Cost Tracker setup"
8
26
  task :doctor do
9
27
  Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
@@ -30,7 +48,7 @@ namespace :llm_cost_tracker do
30
48
  puts LlmCostTracker::Report.generate(days: days)
31
49
  end
32
50
 
33
- desc "Delete llm_api_calls older than DAYS (default: 90). Use BATCH_SIZE=N to tune."
51
+ desc "Delete llm_cost_tracker_calls older than DAYS (default: 90). Use BATCH_SIZE=N to tune."
34
52
  task prune: :environment do
35
53
  days = (ENV["DAYS"] || 90).to_i
36
54
  batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Retention::DEFAULT_BATCH_SIZE).to_i
@@ -98,19 +116,27 @@ namespace :llm_cost_tracker do
98
116
  abort("llm_cost_tracker: price is incomplete or unknown") unless explanation.complete?
99
117
  end
100
118
  end
119
+
120
+ namespace :reconcile do
121
+ desc "Import provider invoice rows from a JSON INPUT file. " \
122
+ "Use SOURCE=openai INPUT=path/to/file.json. Pass PROVIDER=openai for unmapped sources (csv, ...)."
123
+ task(:import) { reconcile_run(:run_import) }
124
+
125
+ desc "Print a reconciliation diff. " \
126
+ "Use SOURCE=openai PERIOD_START=YYYY-MM-DD PERIOD_END=YYYY-MM-DD. PROVIDER=openai for unmapped sources."
127
+ task(:diff) { reconcile_run(:run_diff) }
128
+ end
101
129
  end
102
130
  # rubocop:enable Metrics/BlockLength
103
131
 
104
- def print_changes(changes)
105
- puts " changed models: #{changes.size}"
106
- return if changes.empty?
132
+ def reconcile_run(method)
133
+ Rake::Task["environment"].invoke if Rake::Task.task_defined?("environment")
134
+ require_relative "../llm_cost_tracker"
135
+ LlmCostTracker::ReconcileTasks.public_send(method, env: ENV)
136
+ end
107
137
 
108
- changes.each do |model, fields|
109
- puts " - #{model}"
110
- fields.each do |field, values|
111
- puts " #{field}: #{values['from'].inspect} -> #{values['to'].inspect}"
112
- end
113
- end
138
+ def print_changes(changes)
139
+ LlmCostTracker::Pricing::SyncChangePrinter.call(changes)
114
140
  end
115
141
 
116
142
  def price_refresh_output_path
@@ -128,13 +154,15 @@ def price_explanation_from_env
128
154
  provider: provider,
129
155
  model: model,
130
156
  pricing_mode: ENV.fetch("PRICING_MODE", nil),
131
- token_usage: LlmCostTracker::TokenUsage.build(
132
- input_tokens: ENV.fetch("INPUT_TOKENS", 1).to_i,
133
- output_tokens: ENV.fetch("OUTPUT_TOKENS", 1).to_i,
134
- cache_read_input_tokens: ENV.fetch("CACHE_READ_INPUT_TOKENS", 0).to_i,
135
- cache_write_input_tokens: ENV.fetch("CACHE_WRITE_INPUT_TOKENS", 0).to_i,
136
- cache_write_1h_input_tokens: ENV.fetch("CACHE_WRITE_1H_INPUT_TOKENS", 0).to_i
137
- )
157
+ tokens: {
158
+ input: ENV.fetch("INPUT_TOKENS", 1).to_i,
159
+ output: ENV.fetch("OUTPUT_TOKENS", 1).to_i,
160
+ cache_read_input: ENV.fetch("CACHE_READ_INPUT_TOKENS", 0).to_i,
161
+ cache_write_input: ENV.fetch("CACHE_WRITE_INPUT_TOKENS", 0).to_i,
162
+ cache_write_extended_input: ENV.fetch("CACHE_WRITE_EXTENDED_INPUT_TOKENS", 0).to_i,
163
+ audio_input: ENV.fetch("AUDIO_INPUT_TOKENS", 0).to_i,
164
+ audio_output: ENV.fetch("AUDIO_OUTPUT_TOKENS", 0).to_i
165
+ }
138
166
  )
139
167
  end
140
168
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_cost_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergii Khomenko
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-05-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -216,11 +215,9 @@ dependencies:
216
215
  - - "~>"
217
216
  - !ruby/object:Gem::Version
218
217
  version: '3.0'
219
- description: Tracks token usage, latency, and estimated costs for RubyLLM, OpenAI,
220
- Anthropic, Google Gemini, OpenRouter, DeepSeek, and OpenAI-compatible APIs. Works
221
- through Rails SDK integrations, Faraday middleware, or explicit track/track_stream
222
- helpers, with ActiveRecord storage, tag-based attribution, price sync tasks, and
223
- budget guardrails.
218
+ description: 'Logs every call your Rails app makes to OpenAI, Anthropic, Gemini, RubyLLM,
219
+ or an OpenAI-compatible API: tokens, cost, latency, tags. Calls go straight to the
220
+ provider no proxy. Includes price sync, budget guardrails, and a mountable dashboard.'
224
221
  email:
225
222
  - sergey@mm.st
226
223
  executables: []
@@ -228,6 +225,7 @@ extensions: []
228
225
  extra_rdoc_files: []
229
226
  files:
230
227
  - ".rspec"
228
+ - ".ruby-version"
231
229
  - CHANGELOG.md
232
230
  - CODE_OF_CONDUCT.md
233
231
  - LICENSE.txt
@@ -241,21 +239,25 @@ files:
241
239
  - app/controllers/llm_cost_tracker/dashboard_controller.rb
242
240
  - app/controllers/llm_cost_tracker/data_quality_controller.rb
243
241
  - app/controllers/llm_cost_tracker/models_controller.rb
242
+ - app/controllers/llm_cost_tracker/reconciliation_controller.rb
244
243
  - app/controllers/llm_cost_tracker/tags_controller.rb
245
244
  - app/helpers/llm_cost_tracker/application_helper.rb
246
245
  - app/helpers/llm_cost_tracker/chart_helper.rb
247
246
  - app/helpers/llm_cost_tracker/dashboard_filter_helper.rb
248
247
  - app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb
249
248
  - app/helpers/llm_cost_tracker/dashboard_query_helper.rb
249
+ - app/helpers/llm_cost_tracker/inline_style_helper.rb
250
250
  - app/helpers/llm_cost_tracker/pagination_helper.rb
251
+ - app/helpers/llm_cost_tracker/reconciliation_helper.rb
251
252
  - app/helpers/llm_cost_tracker/token_usage_helper.rb
252
- - app/models/llm_cost_tracker/ingestion/event.rb
253
+ - app/models/llm_cost_tracker/call.rb
254
+ - app/models/llm_cost_tracker/call_line_item.rb
255
+ - app/models/llm_cost_tracker/call_rollup.rb
256
+ - app/models/llm_cost_tracker/call_tag.rb
257
+ - app/models/llm_cost_tracker/ingestion/inbox_entry.rb
253
258
  - app/models/llm_cost_tracker/ingestion/lease.rb
254
- - app/models/llm_cost_tracker/ledger/call.rb
255
- - app/models/llm_cost_tracker/ledger/call_metrics.rb
256
- - app/models/llm_cost_tracker/ledger/period/grouping.rb
257
- - app/models/llm_cost_tracker/ledger/period/total.rb
258
- - app/models/llm_cost_tracker/ledger/tags/accessors.rb
259
+ - app/models/llm_cost_tracker/provider_invoice.rb
260
+ - app/models/llm_cost_tracker/provider_invoice_import.rb
259
261
  - app/services/llm_cost_tracker/dashboard/data_quality.rb
260
262
  - app/services/llm_cost_tracker/dashboard/date_range.rb
261
263
  - app/services/llm_cost_tracker/dashboard/filter.rb
@@ -277,9 +279,12 @@ files:
277
279
  - app/views/llm_cost_tracker/errors/invalid_filter.html.erb
278
280
  - app/views/llm_cost_tracker/errors/not_found.html.erb
279
281
  - app/views/llm_cost_tracker/models/index.html.erb
282
+ - app/views/llm_cost_tracker/reconciliation/index.html.erb
280
283
  - app/views/llm_cost_tracker/shared/_active_filters.html.erb
281
284
  - app/views/llm_cost_tracker/shared/_bar.html.erb
285
+ - app/views/llm_cost_tracker/shared/_filters.html.erb
282
286
  - app/views/llm_cost_tracker/shared/_metric_stack.html.erb
287
+ - app/views/llm_cost_tracker/shared/_sort.html.erb
283
288
  - app/views/llm_cost_tracker/shared/_spend_chart.html.erb
284
289
  - app/views/llm_cost_tracker/shared/_tag_chips.html.erb
285
290
  - app/views/llm_cost_tracker/shared/setup_required.html.erb
@@ -288,43 +293,51 @@ files:
288
293
  - config/routes.rb
289
294
  - lib/llm_cost_tracker.rb
290
295
  - lib/llm_cost_tracker/assets.rb
296
+ - lib/llm_cost_tracker/billing/components.rb
297
+ - lib/llm_cost_tracker/billing/components.yml
298
+ - lib/llm_cost_tracker/billing/cost_status.rb
299
+ - lib/llm_cost_tracker/billing/line_item.rb
291
300
  - lib/llm_cost_tracker/budget.rb
292
301
  - lib/llm_cost_tracker/capture/stream.rb
293
302
  - lib/llm_cost_tracker/capture/stream_collector.rb
294
303
  - lib/llm_cost_tracker/capture/stream_tracker.rb
295
304
  - lib/llm_cost_tracker/configuration.rb
296
- - lib/llm_cost_tracker/configuration/instrumentation.rb
305
+ - lib/llm_cost_tracker/dashboard_setup_state.rb
297
306
  - lib/llm_cost_tracker/doctor.rb
298
307
  - lib/llm_cost_tracker/doctor/capture_verifier.rb
299
308
  - lib/llm_cost_tracker/doctor/check.rb
309
+ - lib/llm_cost_tracker/doctor/cost_drift_check.rb
300
310
  - lib/llm_cost_tracker/doctor/ingestion_check.rb
311
+ - lib/llm_cost_tracker/doctor/invoice_reconciliation_check.rb
312
+ - lib/llm_cost_tracker/doctor/legacy_audit_check.rb
313
+ - lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb
301
314
  - lib/llm_cost_tracker/doctor/price_check.rb
315
+ - lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb
316
+ - lib/llm_cost_tracker/doctor/probe.rb
317
+ - lib/llm_cost_tracker/doctor/schema_check.rb
302
318
  - lib/llm_cost_tracker/engine.rb
303
319
  - lib/llm_cost_tracker/errors.rb
304
320
  - lib/llm_cost_tracker/event.rb
305
- - lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb
306
- - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
307
- - lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb
308
- - lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb
309
- - lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb
310
- - lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb
321
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb
322
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/durable_ingestion_generator.rb
311
323
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
312
324
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
313
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb
314
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb
315
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb
316
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb
317
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb
318
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb
319
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
325
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/reconciliation_generator.rb
326
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb
327
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb
328
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_durable_ingestion.rb.erb
329
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb
320
330
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
321
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb
322
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb
323
- - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb
324
- - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb
331
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb
332
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb
333
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb
334
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb
335
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb
336
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb
325
337
  - lib/llm_cost_tracker/ingestion.rb
326
338
  - lib/llm_cost_tracker/ingestion/batch.rb
327
339
  - lib/llm_cost_tracker/ingestion/inbox.rb
340
+ - lib/llm_cost_tracker/ingestion/inline.rb
328
341
  - lib/llm_cost_tracker/ingestion/lease_claim.rb
329
342
  - lib/llm_cost_tracker/ingestion/worker.rb
330
343
  - lib/llm_cost_tracker/integrations.rb
@@ -336,15 +349,22 @@ files:
336
349
  - lib/llm_cost_tracker/ledger/period.rb
337
350
  - lib/llm_cost_tracker/ledger/period/totals.rb
338
351
  - lib/llm_cost_tracker/ledger/rollups.rb
339
- - lib/llm_cost_tracker/ledger/rollups/batch.rb
340
352
  - lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb
341
353
  - lib/llm_cost_tracker/ledger/schema/adapter.rb
354
+ - lib/llm_cost_tracker/ledger/schema/call_line_items.rb
355
+ - lib/llm_cost_tracker/ledger/schema/call_rollups.rb
356
+ - lib/llm_cost_tracker/ledger/schema/call_tags.rb
342
357
  - lib/llm_cost_tracker/ledger/schema/calls.rb
343
- - lib/llm_cost_tracker/ledger/schema/period_totals.rb
358
+ - lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb
359
+ - lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb
360
+ - lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb
361
+ - lib/llm_cost_tracker/ledger/schema/provider_invoices.rb
344
362
  - lib/llm_cost_tracker/ledger/store.rb
363
+ - lib/llm_cost_tracker/ledger/tags/encoding.rb
345
364
  - lib/llm_cost_tracker/ledger/tags/query.rb
346
365
  - lib/llm_cost_tracker/ledger/tags/sql.rb
347
366
  - lib/llm_cost_tracker/logging.rb
367
+ - lib/llm_cost_tracker/masking.rb
348
368
  - lib/llm_cost_tracker/middleware/faraday.rb
349
369
  - lib/llm_cost_tracker/parsers.rb
350
370
  - lib/llm_cost_tracker/parsers/anthropic.rb
@@ -352,22 +372,33 @@ files:
352
372
  - lib/llm_cost_tracker/parsers/gemini.rb
353
373
  - lib/llm_cost_tracker/parsers/openai.rb
354
374
  - lib/llm_cost_tracker/parsers/openai_compatible.rb
375
+ - lib/llm_cost_tracker/parsers/openai_service_charges.rb
355
376
  - lib/llm_cost_tracker/parsers/openai_usage.rb
356
377
  - lib/llm_cost_tracker/parsers/sse.rb
357
378
  - lib/llm_cost_tracker/prices.json
358
379
  - lib/llm_cost_tracker/pricing.rb
359
- - lib/llm_cost_tracker/pricing/components.rb
360
380
  - lib/llm_cost_tracker/pricing/effective_prices.rb
361
381
  - lib/llm_cost_tracker/pricing/explainer.rb
362
382
  - lib/llm_cost_tracker/pricing/lookup.rb
383
+ - lib/llm_cost_tracker/pricing/mode.rb
363
384
  - lib/llm_cost_tracker/pricing/registry.rb
385
+ - lib/llm_cost_tracker/pricing/service_charges.rb
364
386
  - lib/llm_cost_tracker/pricing/sync.rb
365
387
  - lib/llm_cost_tracker/pricing/sync/fetcher.rb
366
388
  - lib/llm_cost_tracker/pricing/sync/registry_diff.rb
367
- - lib/llm_cost_tracker/pricing/sync/registry_loader.rb
368
389
  - lib/llm_cost_tracker/pricing/sync/registry_writer.rb
390
+ - lib/llm_cost_tracker/pricing/sync_change_printer.rb
369
391
  - lib/llm_cost_tracker/pricing/unknown.rb
370
392
  - lib/llm_cost_tracker/railtie.rb
393
+ - lib/llm_cost_tracker/reconcile_tasks.rb
394
+ - lib/llm_cost_tracker/reconciliation.rb
395
+ - lib/llm_cost_tracker/reconciliation/diff.rb
396
+ - lib/llm_cost_tracker/reconciliation/diff_result.rb
397
+ - lib/llm_cost_tracker/reconciliation/import_result.rb
398
+ - lib/llm_cost_tracker/reconciliation/importer.rb
399
+ - lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb
400
+ - lib/llm_cost_tracker/reconciliation/sources/fingerprint.rb
401
+ - lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb
371
402
  - lib/llm_cost_tracker/report.rb
372
403
  - lib/llm_cost_tracker/report/data.rb
373
404
  - lib/llm_cost_tracker/report/formatter.rb
@@ -375,6 +406,7 @@ files:
375
406
  - lib/llm_cost_tracker/tags/context.rb
376
407
  - lib/llm_cost_tracker/tags/key.rb
377
408
  - lib/llm_cost_tracker/tags/sanitizer.rb
409
+ - lib/llm_cost_tracker/timing.rb
378
410
  - lib/llm_cost_tracker/token_usage.rb
379
411
  - lib/llm_cost_tracker/tracker.rb
380
412
  - lib/llm_cost_tracker/usage_capture.rb
@@ -389,7 +421,6 @@ metadata:
389
421
  source_code_uri: https://github.com/sergey-homenko/llm_cost_tracker
390
422
  documentation_uri: https://github.com/sergey-homenko/llm_cost_tracker#readme
391
423
  rubygems_mfa_required: 'true'
392
- post_install_message:
393
424
  rdoc_options: []
394
425
  require_paths:
395
426
  - lib
@@ -397,15 +428,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
397
428
  requirements:
398
429
  - - ">="
399
430
  - !ruby/object:Gem::Version
400
- version: 3.3.0
431
+ version: 3.4.0
401
432
  required_rubygems_version: !ruby/object:Gem::Requirement
402
433
  requirements:
403
434
  - - ">="
404
435
  - !ruby/object:Gem::Version
405
436
  version: '0'
406
437
  requirements: []
407
- rubygems_version: 3.5.22
408
- signing_key:
438
+ rubygems_version: 3.6.9
409
439
  specification_version: 4
410
- summary: Rails-native LLM usage and cost tracking with ActiveRecord storage
440
+ summary: LLM API cost tracking for Rails applications
411
441
  test_files: []
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
-
5
- module LlmCostTracker
6
- module Ingestion
7
- class Event < ActiveRecord::Base
8
- MAX_ATTEMPTS = 5
9
-
10
- self.table_name = "llm_cost_tracker_inbox_events"
11
- end
12
- end
13
- end
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
-
5
- module LlmCostTracker
6
- module Ledger
7
- class Call < ActiveRecord::Base
8
- extend Period::Grouping
9
- extend Ledger::CallMetrics
10
- include Ledger::Tags::Accessors
11
-
12
- self.table_name = "llm_api_calls"
13
-
14
- scope :with_cost, -> { where.not(total_cost: nil) }
15
- scope :without_cost, -> { where(total_cost: nil) }
16
- scope :unknown_pricing, -> { without_cost }
17
- scope :with_latency, -> { where.not(latency_ms: nil) }
18
- scope :streaming, -> { where(stream: true) }
19
- scope :non_streaming, -> { where(stream: [false, nil]) }
20
- scope :by_usage_source, ->(source) { where(usage_source: source.to_s) }
21
- scope :with_provider_response_id, -> { where.not(provider_response_id: [nil, ""]) }
22
- scope :missing_provider_response_id, -> { where(provider_response_id: [nil, ""]) }
23
- scope :streaming_missing_usage, lambda {
24
- where(stream: true).where(usage_source: ["unknown", nil])
25
- }
26
-
27
- scope :with_json_tags, lambda {
28
- where.not(tags: {})
29
- }
30
-
31
- scope :today, -> { where(tracked_at: Time.now.utc.beginning_of_day..) }
32
- scope :this_week, -> { where(tracked_at: Time.now.utc.beginning_of_week..) }
33
- scope :this_month, -> { where(tracked_at: Time.now.utc.beginning_of_month..) }
34
- scope :between, ->(from, to) { where(tracked_at: from..to) }
35
-
36
- def self.by_tag(key, value)
37
- by_tags(key => value)
38
- end
39
-
40
- def self.by_tags(tags)
41
- Ledger::Tags::Query.apply(tags)
42
- end
43
- end
44
- end
45
- end
@@ -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