llm_cost_tracker 0.6.1 → 0.7.1

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +13 -12
  4. data/app/assets/llm_cost_tracker/application.css +3 -0
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +22 -4
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +6 -11
  7. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +2 -1
  8. data/app/controllers/llm_cost_tracker/data_quality_controller.rb +5 -1
  9. data/app/controllers/llm_cost_tracker/models_controller.rb +0 -1
  10. data/app/controllers/llm_cost_tracker/tags_controller.rb +1 -8
  11. data/app/helpers/llm_cost_tracker/application_helper.rb +2 -1
  12. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +1 -2
  13. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +1 -1
  14. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +10 -27
  15. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +58 -0
  16. data/app/models/llm_cost_tracker/ingestion/event.rb +13 -0
  17. data/app/models/llm_cost_tracker/ingestion/lease.rb +11 -0
  18. data/app/models/llm_cost_tracker/ledger/call.rb +45 -0
  19. data/app/models/llm_cost_tracker/ledger/call_metrics.rb +66 -0
  20. data/app/models/llm_cost_tracker/ledger/period/grouping.rb +71 -0
  21. data/app/models/llm_cost_tracker/ledger/period/total.rb +13 -0
  22. data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +19 -0
  23. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +111 -94
  24. data/app/services/llm_cost_tracker/dashboard/date_range.rb +2 -2
  25. data/app/services/llm_cost_tracker/dashboard/filter.rb +7 -18
  26. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +58 -67
  27. data/app/services/llm_cost_tracker/dashboard/pagination.rb +59 -0
  28. data/app/services/llm_cost_tracker/dashboard/params.rb +26 -0
  29. data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +18 -20
  30. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -13
  31. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +28 -61
  32. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +8 -37
  33. data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
  34. data/app/services/llm_cost_tracker/dashboard/top_models.rb +12 -47
  35. data/app/views/llm_cost_tracker/calls/index.html.erb +12 -18
  36. data/app/views/llm_cost_tracker/calls/show.html.erb +30 -32
  37. data/app/views/llm_cost_tracker/dashboard/index.html.erb +17 -19
  38. data/app/views/llm_cost_tracker/data_quality/index.html.erb +108 -135
  39. data/app/views/llm_cost_tracker/models/index.html.erb +8 -9
  40. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +13 -2
  41. data/app/views/llm_cost_tracker/tags/show.html.erb +20 -20
  42. data/config/routes.rb +1 -1
  43. data/lib/llm_cost_tracker/assets.rb +0 -6
  44. data/lib/llm_cost_tracker/budget.rb +10 -24
  45. data/lib/llm_cost_tracker/capture/stream.rb +9 -0
  46. data/lib/llm_cost_tracker/capture/stream_collector.rb +182 -0
  47. data/lib/llm_cost_tracker/{integrations → capture}/stream_tracker.rb +40 -72
  48. data/lib/llm_cost_tracker/configuration/instrumentation.rb +3 -7
  49. data/lib/llm_cost_tracker/configuration.rb +30 -45
  50. data/lib/llm_cost_tracker/doctor/capture_verifier.rb +61 -0
  51. data/lib/llm_cost_tracker/doctor/check.rb +7 -0
  52. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +22 -61
  53. data/lib/llm_cost_tracker/doctor/price_check.rb +60 -0
  54. data/lib/llm_cost_tracker/doctor.rb +66 -79
  55. data/lib/llm_cost_tracker/engine.rb +0 -3
  56. data/lib/llm_cost_tracker/errors.rb +4 -15
  57. data/lib/llm_cost_tracker/event.rb +6 -6
  58. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +42 -0
  59. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -0
  60. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +7 -7
  61. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +5 -5
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +22 -0
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +15 -14
  64. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +1 -21
  65. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +12 -1
  66. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +2 -2
  67. data/lib/llm_cost_tracker/{storage/active_record_inbox_batch.rb → ingestion/batch.rb} +21 -20
  68. data/lib/llm_cost_tracker/ingestion/inbox.rb +105 -0
  69. data/lib/llm_cost_tracker/{storage/active_record_ingestor_lease.rb → ingestion/lease_claim.rb} +5 -7
  70. data/lib/llm_cost_tracker/{storage/active_record_ingestor.rb → ingestion/worker.rb} +38 -48
  71. data/lib/llm_cost_tracker/ingestion.rb +129 -0
  72. data/lib/llm_cost_tracker/integrations/anthropic.rb +52 -34
  73. data/lib/llm_cost_tracker/integrations/base.rb +73 -34
  74. data/lib/llm_cost_tracker/integrations/openai.rb +45 -39
  75. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +40 -30
  76. data/lib/llm_cost_tracker/integrations.rb +43 -0
  77. data/lib/llm_cost_tracker/ledger/period/totals.rb +66 -0
  78. data/lib/llm_cost_tracker/{storage/active_record_periods.rb → ledger/period.rb} +2 -2
  79. data/lib/llm_cost_tracker/ledger/rollups/batch.rb +43 -0
  80. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +46 -0
  81. data/lib/llm_cost_tracker/ledger/rollups.rb +87 -0
  82. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +51 -0
  83. data/lib/llm_cost_tracker/ledger/schema/calls.rb +101 -0
  84. data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +32 -0
  85. data/lib/llm_cost_tracker/ledger/store.rb +60 -0
  86. data/lib/llm_cost_tracker/ledger/tags/query.rb +29 -0
  87. data/lib/llm_cost_tracker/ledger/tags/sql.rb +33 -0
  88. data/lib/llm_cost_tracker/ledger.rb +13 -0
  89. data/lib/llm_cost_tracker/logging.rb +3 -6
  90. data/lib/llm_cost_tracker/middleware/faraday.rb +35 -36
  91. data/lib/llm_cost_tracker/parsers/anthropic.rb +38 -27
  92. data/lib/llm_cost_tracker/parsers/base.rb +10 -19
  93. data/lib/llm_cost_tracker/parsers/gemini.rb +15 -16
  94. data/lib/llm_cost_tracker/parsers/openai_usage.rb +24 -19
  95. data/lib/llm_cost_tracker/parsers/sse.rb +4 -7
  96. data/lib/llm_cost_tracker/parsers.rb +20 -0
  97. data/lib/llm_cost_tracker/prices.json +52 -11
  98. data/lib/llm_cost_tracker/pricing/components.rb +37 -0
  99. data/lib/llm_cost_tracker/pricing/effective_prices.rb +40 -50
  100. data/lib/llm_cost_tracker/pricing/explainer.rb +12 -23
  101. data/lib/llm_cost_tracker/pricing/lookup.rb +24 -25
  102. data/lib/llm_cost_tracker/pricing/registry.rb +156 -0
  103. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +107 -0
  104. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +53 -0
  105. data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +63 -0
  106. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +31 -0
  107. data/lib/llm_cost_tracker/pricing/sync.rb +143 -0
  108. data/lib/llm_cost_tracker/pricing/unknown.rb +46 -0
  109. data/lib/llm_cost_tracker/pricing.rb +33 -32
  110. data/lib/llm_cost_tracker/railtie.rb +7 -10
  111. data/lib/llm_cost_tracker/report/data.rb +72 -0
  112. data/lib/llm_cost_tracker/report/formatter.rb +69 -0
  113. data/lib/llm_cost_tracker/report.rb +8 -10
  114. data/lib/llm_cost_tracker/retention.rb +27 -10
  115. data/lib/llm_cost_tracker/tags/context.rb +35 -0
  116. data/lib/llm_cost_tracker/tags/key.rb +18 -0
  117. data/lib/llm_cost_tracker/tags/sanitizer.rb +68 -0
  118. data/lib/llm_cost_tracker/token_usage.rb +67 -0
  119. data/lib/llm_cost_tracker/tracker.rb +38 -70
  120. data/lib/llm_cost_tracker/usage_capture.rb +37 -0
  121. data/lib/llm_cost_tracker/version.rb +1 -1
  122. data/lib/llm_cost_tracker.rb +56 -90
  123. data/lib/tasks/llm_cost_tracker.rake +18 -13
  124. metadata +85 -99
  125. data/app/services/llm_cost_tracker/dashboard/data_quality_aggregate.rb +0 -81
  126. data/app/services/llm_cost_tracker/pagination.rb +0 -57
  127. data/lib/llm_cost_tracker/active_record_adapter.rb +0 -49
  128. data/lib/llm_cost_tracker/capture_verifier.rb +0 -71
  129. data/lib/llm_cost_tracker/configuration/storage_backend.rb +0 -26
  130. data/lib/llm_cost_tracker/cost.rb +0 -12
  131. data/lib/llm_cost_tracker/doctor/capture_check.rb +0 -39
  132. data/lib/llm_cost_tracker/engine_compatibility.rb +0 -15
  133. data/lib/llm_cost_tracker/event_metadata.rb +0 -52
  134. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb +0 -29
  135. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb +0 -29
  136. data/lib/llm_cost_tracker/inbox_event.rb +0 -9
  137. data/lib/llm_cost_tracker/ingestor_lease.rb +0 -9
  138. data/lib/llm_cost_tracker/integrations/object_reader.rb +0 -56
  139. data/lib/llm_cost_tracker/integrations/registry.rb +0 -73
  140. data/lib/llm_cost_tracker/llm_api_call.rb +0 -60
  141. data/lib/llm_cost_tracker/llm_api_call_metrics.rb +0 -63
  142. data/lib/llm_cost_tracker/parameter_hash.rb +0 -33
  143. data/lib/llm_cost_tracker/parsed_usage.rb +0 -72
  144. data/lib/llm_cost_tracker/parsers/registry.rb +0 -58
  145. data/lib/llm_cost_tracker/period_grouping.rb +0 -69
  146. data/lib/llm_cost_tracker/period_total.rb +0 -9
  147. data/lib/llm_cost_tracker/price_freshness.rb +0 -38
  148. data/lib/llm_cost_tracker/price_registry.rb +0 -144
  149. data/lib/llm_cost_tracker/price_sync/fetcher.rb +0 -104
  150. data/lib/llm_cost_tracker/price_sync/registry_diff.rb +0 -51
  151. data/lib/llm_cost_tracker/price_sync/registry_loader.rb +0 -61
  152. data/lib/llm_cost_tracker/price_sync/registry_writer.rb +0 -29
  153. data/lib/llm_cost_tracker/price_sync.rb +0 -144
  154. data/lib/llm_cost_tracker/report_data.rb +0 -94
  155. data/lib/llm_cost_tracker/report_formatter.rb +0 -67
  156. data/lib/llm_cost_tracker/request_url.rb +0 -20
  157. data/lib/llm_cost_tracker/storage/active_record_backend.rb +0 -166
  158. data/lib/llm_cost_tracker/storage/active_record_connection_cleanup.rb +0 -13
  159. data/lib/llm_cost_tracker/storage/active_record_inbox.rb +0 -165
  160. data/lib/llm_cost_tracker/storage/active_record_period_totals.rb +0 -84
  161. data/lib/llm_cost_tracker/storage/active_record_rollup_batch.rb +0 -41
  162. data/lib/llm_cost_tracker/storage/active_record_rollup_upsert_sql.rb +0 -42
  163. data/lib/llm_cost_tracker/storage/active_record_rollups.rb +0 -146
  164. data/lib/llm_cost_tracker/storage/active_record_store.rb +0 -145
  165. data/lib/llm_cost_tracker/storage/custom_backend.rb +0 -32
  166. data/lib/llm_cost_tracker/storage/dispatcher.rb +0 -45
  167. data/lib/llm_cost_tracker/storage/log_backend.rb +0 -38
  168. data/lib/llm_cost_tracker/storage/registry.rb +0 -63
  169. data/lib/llm_cost_tracker/stream_capture.rb +0 -7
  170. data/lib/llm_cost_tracker/stream_collector.rb +0 -199
  171. data/lib/llm_cost_tracker/tag_accessors.rb +0 -15
  172. data/lib/llm_cost_tracker/tag_context.rb +0 -52
  173. data/lib/llm_cost_tracker/tag_key.rb +0 -16
  174. data/lib/llm_cost_tracker/tag_query.rb +0 -43
  175. data/lib/llm_cost_tracker/tag_sanitizer.rb +0 -81
  176. data/lib/llm_cost_tracker/tag_sql.rb +0 -34
  177. data/lib/llm_cost_tracker/tags_column.rb +0 -103
  178. data/lib/llm_cost_tracker/unknown_pricing.rb +0 -54
  179. data/lib/llm_cost_tracker/usage_breakdown.rb +0 -30
  180. data/lib/llm_cost_tracker/value_helpers.rb +0 -40
metadata CHANGED
@@ -1,15 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_cost_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergii Khomenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-29 00:00:00.000000000 Z
11
+ date: 2026-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '7.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9.0'
13
33
  - !ruby/object:Gem::Dependency
14
34
  name: activesupport
15
35
  requirement: !ruby/object:Gem::Requirement
@@ -65,7 +85,7 @@ dependencies:
65
85
  - !ruby/object:Gem::Version
66
86
  version: '3.0'
67
87
  - !ruby/object:Gem::Dependency
68
- name: activerecord
88
+ name: railties
69
89
  requirement: !ruby/object:Gem::Requirement
70
90
  requirements:
71
91
  - - ">="
@@ -74,7 +94,7 @@ dependencies:
74
94
  - - "<"
75
95
  - !ruby/object:Gem::Version
76
96
  version: '9.0'
77
- type: :development
97
+ type: :runtime
78
98
  prerelease: false
79
99
  version_requirements: !ruby/object:Gem::Requirement
80
100
  requirements:
@@ -99,25 +119,19 @@ dependencies:
99
119
  - !ruby/object:Gem::Version
100
120
  version: '1.16'
101
121
  - !ruby/object:Gem::Dependency
102
- name: railties
122
+ name: pg
103
123
  requirement: !ruby/object:Gem::Requirement
104
124
  requirements:
105
- - - ">="
106
- - !ruby/object:Gem::Version
107
- version: '7.1'
108
- - - "<"
125
+ - - "~>"
109
126
  - !ruby/object:Gem::Version
110
- version: '9.0'
127
+ version: '1.6'
111
128
  type: :development
112
129
  prerelease: false
113
130
  version_requirements: !ruby/object:Gem::Requirement
114
131
  requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '7.1'
118
- - - "<"
132
+ - - "~>"
119
133
  - !ruby/object:Gem::Version
120
- version: '9.0'
134
+ version: '1.6'
121
135
  - !ruby/object:Gem::Dependency
122
136
  name: rake
123
137
  requirement: !ruby/object:Gem::Requirement
@@ -188,26 +202,6 @@ dependencies:
188
202
  - - "~>"
189
203
  - !ruby/object:Gem::Version
190
204
  version: '0.8'
191
- - !ruby/object:Gem::Dependency
192
- name: sqlite3
193
- requirement: !ruby/object:Gem::Requirement
194
- requirements:
195
- - - ">="
196
- - !ruby/object:Gem::Version
197
- version: '1.4'
198
- - - "<"
199
- - !ruby/object:Gem::Version
200
- version: '3.0'
201
- type: :development
202
- prerelease: false
203
- version_requirements: !ruby/object:Gem::Requirement
204
- requirements:
205
- - - ">="
206
- - !ruby/object:Gem::Version
207
- version: '1.4'
208
- - - "<"
209
- - !ruby/object:Gem::Version
210
- version: '3.0'
211
205
  - !ruby/object:Gem::Dependency
212
206
  name: webmock
213
207
  requirement: !ruby/object:Gem::Requirement
@@ -224,8 +218,9 @@ dependencies:
224
218
  version: '3.0'
225
219
  description: Tracks token usage, latency, and estimated costs for RubyLLM, OpenAI,
226
220
  Anthropic, Google Gemini, OpenRouter, DeepSeek, and OpenAI-compatible APIs. Works
227
- through Faraday middleware or explicit track/track_stream helpers, with ActiveRecord
228
- storage, tag-based attribution, price sync tasks, and budget guardrails.
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.
229
224
  email:
230
225
  - sergey@mm.st
231
226
  executables: []
@@ -253,18 +248,26 @@ files:
253
248
  - app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb
254
249
  - app/helpers/llm_cost_tracker/dashboard_query_helper.rb
255
250
  - app/helpers/llm_cost_tracker/pagination_helper.rb
251
+ - app/helpers/llm_cost_tracker/token_usage_helper.rb
252
+ - app/models/llm_cost_tracker/ingestion/event.rb
253
+ - 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
256
259
  - app/services/llm_cost_tracker/dashboard/data_quality.rb
257
- - app/services/llm_cost_tracker/dashboard/data_quality_aggregate.rb
258
260
  - app/services/llm_cost_tracker/dashboard/date_range.rb
259
261
  - app/services/llm_cost_tracker/dashboard/filter.rb
260
262
  - app/services/llm_cost_tracker/dashboard/overview_stats.rb
263
+ - app/services/llm_cost_tracker/dashboard/pagination.rb
264
+ - app/services/llm_cost_tracker/dashboard/params.rb
261
265
  - app/services/llm_cost_tracker/dashboard/provider_breakdown.rb
262
266
  - app/services/llm_cost_tracker/dashboard/spend_anomaly.rb
263
267
  - app/services/llm_cost_tracker/dashboard/tag_breakdown.rb
264
268
  - app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb
265
269
  - app/services/llm_cost_tracker/dashboard/time_series.rb
266
270
  - app/services/llm_cost_tracker/dashboard/top_models.rb
267
- - app/services/llm_cost_tracker/pagination.rb
268
271
  - app/views/layouts/llm_cost_tracker/application.html.erb
269
272
  - app/views/llm_cost_tracker/calls/index.html.erb
270
273
  - app/views/llm_cost_tracker/calls/show.html.erb
@@ -284,28 +287,27 @@ files:
284
287
  - app/views/llm_cost_tracker/tags/show.html.erb
285
288
  - config/routes.rb
286
289
  - lib/llm_cost_tracker.rb
287
- - lib/llm_cost_tracker/active_record_adapter.rb
288
290
  - lib/llm_cost_tracker/assets.rb
289
291
  - lib/llm_cost_tracker/budget.rb
290
- - lib/llm_cost_tracker/capture_verifier.rb
292
+ - lib/llm_cost_tracker/capture/stream.rb
293
+ - lib/llm_cost_tracker/capture/stream_collector.rb
294
+ - lib/llm_cost_tracker/capture/stream_tracker.rb
291
295
  - lib/llm_cost_tracker/configuration.rb
292
296
  - lib/llm_cost_tracker/configuration/instrumentation.rb
293
- - lib/llm_cost_tracker/configuration/storage_backend.rb
294
- - lib/llm_cost_tracker/cost.rb
295
297
  - lib/llm_cost_tracker/doctor.rb
296
- - lib/llm_cost_tracker/doctor/capture_check.rb
298
+ - lib/llm_cost_tracker/doctor/capture_verifier.rb
299
+ - lib/llm_cost_tracker/doctor/check.rb
297
300
  - lib/llm_cost_tracker/doctor/ingestion_check.rb
301
+ - lib/llm_cost_tracker/doctor/price_check.rb
298
302
  - lib/llm_cost_tracker/engine.rb
299
- - lib/llm_cost_tracker/engine_compatibility.rb
300
303
  - lib/llm_cost_tracker/errors.rb
301
304
  - lib/llm_cost_tracker/event.rb
302
- - lib/llm_cost_tracker/event_metadata.rb
303
305
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb
304
306
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
305
307
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb
306
308
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb
307
309
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb
308
- - lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb
310
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb
309
311
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
310
312
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
311
313
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb
@@ -313,85 +315,69 @@ files:
313
315
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb
314
316
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb
315
317
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb
316
- - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_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
317
319
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
318
320
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
319
321
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb
320
322
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb
321
323
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb
322
324
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb
323
- - lib/llm_cost_tracker/inbox_event.rb
324
- - lib/llm_cost_tracker/ingestor_lease.rb
325
+ - lib/llm_cost_tracker/ingestion.rb
326
+ - lib/llm_cost_tracker/ingestion/batch.rb
327
+ - lib/llm_cost_tracker/ingestion/inbox.rb
328
+ - lib/llm_cost_tracker/ingestion/lease_claim.rb
329
+ - lib/llm_cost_tracker/ingestion/worker.rb
330
+ - lib/llm_cost_tracker/integrations.rb
325
331
  - lib/llm_cost_tracker/integrations/anthropic.rb
326
332
  - lib/llm_cost_tracker/integrations/base.rb
327
- - lib/llm_cost_tracker/integrations/object_reader.rb
328
333
  - lib/llm_cost_tracker/integrations/openai.rb
329
- - lib/llm_cost_tracker/integrations/registry.rb
330
334
  - lib/llm_cost_tracker/integrations/ruby_llm.rb
331
- - lib/llm_cost_tracker/integrations/stream_tracker.rb
332
- - lib/llm_cost_tracker/llm_api_call.rb
333
- - lib/llm_cost_tracker/llm_api_call_metrics.rb
335
+ - lib/llm_cost_tracker/ledger.rb
336
+ - lib/llm_cost_tracker/ledger/period.rb
337
+ - lib/llm_cost_tracker/ledger/period/totals.rb
338
+ - lib/llm_cost_tracker/ledger/rollups.rb
339
+ - lib/llm_cost_tracker/ledger/rollups/batch.rb
340
+ - lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb
341
+ - lib/llm_cost_tracker/ledger/schema/adapter.rb
342
+ - lib/llm_cost_tracker/ledger/schema/calls.rb
343
+ - lib/llm_cost_tracker/ledger/schema/period_totals.rb
344
+ - lib/llm_cost_tracker/ledger/store.rb
345
+ - lib/llm_cost_tracker/ledger/tags/query.rb
346
+ - lib/llm_cost_tracker/ledger/tags/sql.rb
334
347
  - lib/llm_cost_tracker/logging.rb
335
348
  - lib/llm_cost_tracker/middleware/faraday.rb
336
- - lib/llm_cost_tracker/parameter_hash.rb
337
- - lib/llm_cost_tracker/parsed_usage.rb
349
+ - lib/llm_cost_tracker/parsers.rb
338
350
  - lib/llm_cost_tracker/parsers/anthropic.rb
339
351
  - lib/llm_cost_tracker/parsers/base.rb
340
352
  - lib/llm_cost_tracker/parsers/gemini.rb
341
353
  - lib/llm_cost_tracker/parsers/openai.rb
342
354
  - lib/llm_cost_tracker/parsers/openai_compatible.rb
343
355
  - lib/llm_cost_tracker/parsers/openai_usage.rb
344
- - lib/llm_cost_tracker/parsers/registry.rb
345
356
  - lib/llm_cost_tracker/parsers/sse.rb
346
- - lib/llm_cost_tracker/period_grouping.rb
347
- - lib/llm_cost_tracker/period_total.rb
348
- - lib/llm_cost_tracker/price_freshness.rb
349
- - lib/llm_cost_tracker/price_registry.rb
350
- - lib/llm_cost_tracker/price_sync.rb
351
- - lib/llm_cost_tracker/price_sync/fetcher.rb
352
- - lib/llm_cost_tracker/price_sync/registry_diff.rb
353
- - lib/llm_cost_tracker/price_sync/registry_loader.rb
354
- - lib/llm_cost_tracker/price_sync/registry_writer.rb
355
357
  - lib/llm_cost_tracker/prices.json
356
358
  - lib/llm_cost_tracker/pricing.rb
359
+ - lib/llm_cost_tracker/pricing/components.rb
357
360
  - lib/llm_cost_tracker/pricing/effective_prices.rb
358
361
  - lib/llm_cost_tracker/pricing/explainer.rb
359
362
  - lib/llm_cost_tracker/pricing/lookup.rb
363
+ - lib/llm_cost_tracker/pricing/registry.rb
364
+ - lib/llm_cost_tracker/pricing/sync.rb
365
+ - lib/llm_cost_tracker/pricing/sync/fetcher.rb
366
+ - lib/llm_cost_tracker/pricing/sync/registry_diff.rb
367
+ - lib/llm_cost_tracker/pricing/sync/registry_loader.rb
368
+ - lib/llm_cost_tracker/pricing/sync/registry_writer.rb
369
+ - lib/llm_cost_tracker/pricing/unknown.rb
360
370
  - lib/llm_cost_tracker/railtie.rb
361
371
  - lib/llm_cost_tracker/report.rb
362
- - lib/llm_cost_tracker/report_data.rb
363
- - lib/llm_cost_tracker/report_formatter.rb
364
- - lib/llm_cost_tracker/request_url.rb
372
+ - lib/llm_cost_tracker/report/data.rb
373
+ - lib/llm_cost_tracker/report/formatter.rb
365
374
  - lib/llm_cost_tracker/retention.rb
366
- - lib/llm_cost_tracker/storage/active_record_backend.rb
367
- - lib/llm_cost_tracker/storage/active_record_connection_cleanup.rb
368
- - lib/llm_cost_tracker/storage/active_record_inbox.rb
369
- - lib/llm_cost_tracker/storage/active_record_inbox_batch.rb
370
- - lib/llm_cost_tracker/storage/active_record_ingestor.rb
371
- - lib/llm_cost_tracker/storage/active_record_ingestor_lease.rb
372
- - lib/llm_cost_tracker/storage/active_record_period_totals.rb
373
- - lib/llm_cost_tracker/storage/active_record_periods.rb
374
- - lib/llm_cost_tracker/storage/active_record_rollup_batch.rb
375
- - lib/llm_cost_tracker/storage/active_record_rollup_upsert_sql.rb
376
- - lib/llm_cost_tracker/storage/active_record_rollups.rb
377
- - lib/llm_cost_tracker/storage/active_record_store.rb
378
- - lib/llm_cost_tracker/storage/custom_backend.rb
379
- - lib/llm_cost_tracker/storage/dispatcher.rb
380
- - lib/llm_cost_tracker/storage/log_backend.rb
381
- - lib/llm_cost_tracker/storage/registry.rb
382
- - lib/llm_cost_tracker/stream_capture.rb
383
- - lib/llm_cost_tracker/stream_collector.rb
384
- - lib/llm_cost_tracker/tag_accessors.rb
385
- - lib/llm_cost_tracker/tag_context.rb
386
- - lib/llm_cost_tracker/tag_key.rb
387
- - lib/llm_cost_tracker/tag_query.rb
388
- - lib/llm_cost_tracker/tag_sanitizer.rb
389
- - lib/llm_cost_tracker/tag_sql.rb
390
- - lib/llm_cost_tracker/tags_column.rb
375
+ - lib/llm_cost_tracker/tags/context.rb
376
+ - lib/llm_cost_tracker/tags/key.rb
377
+ - lib/llm_cost_tracker/tags/sanitizer.rb
378
+ - lib/llm_cost_tracker/token_usage.rb
391
379
  - lib/llm_cost_tracker/tracker.rb
392
- - lib/llm_cost_tracker/unknown_pricing.rb
393
- - lib/llm_cost_tracker/usage_breakdown.rb
394
- - lib/llm_cost_tracker/value_helpers.rb
380
+ - lib/llm_cost_tracker/usage_capture.rb
395
381
  - lib/llm_cost_tracker/version.rb
396
382
  - lib/tasks/llm_cost_tracker.rake
397
383
  homepage: https://github.com/sergey-homenko/llm_cost_tracker
@@ -421,5 +407,5 @@ requirements: []
421
407
  rubygems_version: 3.5.22
422
408
  signing_key:
423
409
  specification_version: 4
424
- summary: Self-hosted LLM usage and cost tracking for Ruby and Rails
410
+ summary: Rails-native LLM usage and cost tracking with ActiveRecord storage
425
411
  test_files: []
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module Dashboard
5
- class DataQualityAggregate
6
- class << self
7
- def call(scope:)
8
- model = scope.klass
9
- expressions = aggregate_expressions(scope, model:)
10
- values = Array(scope.unscope(:order).pick(*expressions.values))
11
-
12
- expressions.keys.zip(values).to_h
13
- end
14
-
15
- private
16
-
17
- def aggregate_expressions(scope, model:)
18
- usage_breakdown_present = model.usage_breakdown_columns?
19
- usage_breakdown_cost_present = model.usage_breakdown_cost_columns?
20
-
21
- expressions = {
22
- total_calls: Arel.sql("COUNT(*)"),
23
- unknown_pricing_count: conditional_count_expression("total_cost IS NULL"),
24
- tagged_calls_count: tagged_calls_expression(model)
25
- }
26
-
27
- if model.latency_column?
28
- expressions[:missing_latency_count] = conditional_count_expression("latency_ms IS NULL")
29
- end
30
- expressions[:streaming_count] = conditional_count_expression("stream") if model.stream_column?
31
- if model.stream_column? && model.usage_source_column?
32
- expressions[:streaming_missing_usage_count] =
33
- conditional_count_expression("stream AND (usage_source = 'unknown' OR usage_source IS NULL)")
34
- end
35
- if model.provider_response_id_column?
36
- expressions[:missing_provider_response_id_count] =
37
- conditional_count_expression("provider_response_id IS NULL OR provider_response_id = ''")
38
- end
39
-
40
- usage_sum_columns(usage_breakdown_present, usage_breakdown_cost_present).each do |column|
41
- expressions[column] = sum_expression(scope, column)
42
- end
43
-
44
- expressions
45
- end
46
-
47
- def usage_sum_columns(usage_breakdown_present, usage_breakdown_cost_present)
48
- columns = %i[input_tokens output_tokens input_cost output_cost]
49
- if usage_breakdown_present
50
- columns += %i[cache_read_input_tokens cache_write_input_tokens hidden_output_tokens]
51
- end
52
- columns += %i[cache_read_input_cost cache_write_input_cost] if usage_breakdown_cost_present
53
- columns
54
- end
55
-
56
- def conditional_count_expression(predicate)
57
- Arel.sql("COALESCE(SUM(CASE WHEN #{predicate} THEN 1 ELSE 0 END), 0)")
58
- end
59
-
60
- def tagged_calls_expression(model)
61
- table = model.quoted_table_name
62
- column = "#{table}.#{model.connection.quote_column_name('tags')}"
63
-
64
- Arel.sql(case
65
- when model.tags_jsonb_column?
66
- "COALESCE(SUM(CASE WHEN #{column} <> '{}'::jsonb THEN 1 ELSE 0 END), 0)"
67
- when model.tags_mysql_json_column?
68
- "COALESCE(SUM(CASE WHEN JSON_LENGTH(#{column}) > 0 THEN 1 ELSE 0 END), 0)"
69
- else
70
- "COALESCE(SUM(CASE WHEN #{column} IS NOT NULL AND #{column} <> '' " \
71
- "AND #{column} <> '{}' THEN 1 ELSE 0 END), 0)"
72
- end)
73
- end
74
-
75
- def sum_expression(scope, column)
76
- Arel.sql("COALESCE(SUM(#{scope.connection.quote_column_name(column)}), 0)")
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- class Pagination
5
- DEFAULT_PER = 50
6
- MAX_PER = 200
7
- MIN_PAGE = 1
8
-
9
- attr_reader :page, :per
10
-
11
- def self.call(params)
12
- params = LlmCostTracker::ParameterHash.with_indifferent_access(params)
13
- new(
14
- page: integer_param(params, :page, default: MIN_PAGE, min: MIN_PAGE),
15
- per: integer_param(params, :per, default: DEFAULT_PER, min: 1, max: MAX_PER)
16
- )
17
- end
18
-
19
- def self.integer_param(params, key, default:, min:, max: nil)
20
- value = Integer(params[key], 10)
21
- value = [value, min].max
22
- value = [value, max].min if max
23
- value
24
- rescue ArgumentError, TypeError
25
- default
26
- end
27
- private_class_method :integer_param
28
-
29
- def initialize(page:, per:)
30
- @page = page
31
- @per = per
32
- freeze
33
- end
34
-
35
- def limit
36
- per
37
- end
38
-
39
- def offset
40
- (page - 1) * per
41
- end
42
-
43
- def prev_page?
44
- page > MIN_PAGE
45
- end
46
-
47
- def next_page?(total_count)
48
- offset + per < total_count.to_i
49
- end
50
-
51
- def total_pages(total_count)
52
- return MIN_PAGE if total_count.to_i <= 0
53
-
54
- [(total_count.to_f / per).ceil, MIN_PAGE].max
55
- end
56
- end
57
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module ActiveRecordAdapter
5
- MYSQL_ADAPTERS = %w[
6
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
7
- ActiveRecord::ConnectionAdapters::Mysql2Adapter
8
- ActiveRecord::ConnectionAdapters::TrilogyAdapter
9
- ].freeze
10
- POSTGRESQL_ADAPTERS = %w[
11
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
12
- ].freeze
13
- SQLITE_ADAPTERS = %w[
14
- ActiveRecord::ConnectionAdapters::SQLite3Adapter
15
- ].freeze
16
- MYSQL_PATTERN = /mysql|trilogy|mariadb/i
17
- POSTGRESQL_PATTERN = /postgres/i
18
- SQLITE_PATTERN = /sqlite/i
19
-
20
- class << self
21
- def mysql?(value) = adapter_instance?(value, MYSQL_ADAPTERS) || adapter_name(value).match?(MYSQL_PATTERN)
22
-
23
- def postgresql?(value)
24
- adapter_instance?(value, POSTGRESQL_ADAPTERS) || adapter_name(value).match?(POSTGRESQL_PATTERN)
25
- end
26
-
27
- def sqlite?(value) = adapter_instance?(value, SQLITE_ADAPTERS) || adapter_name(value).match?(SQLITE_PATTERN)
28
-
29
- private
30
-
31
- def adapter_instance?(value, class_names)
32
- class_names.any? do |class_name|
33
- adapter_class = constantize(class_name)
34
- adapter_class && value.is_a?(adapter_class)
35
- end
36
- end
37
-
38
- def constantize(name)
39
- name.split("::").reduce(Object) { |namespace, part| namespace.const_get(part, false) }
40
- rescue NameError
41
- nil
42
- end
43
-
44
- def adapter_name(value)
45
- value.respond_to?(:adapter_name) ? value.adapter_name.to_s : value.to_s
46
- end
47
- end
48
- end
49
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "storage/dispatcher"
4
-
5
- module LlmCostTracker
6
- class CaptureVerifier
7
- Check = Data.define(:status, :name, :message)
8
-
9
- class << self
10
- def call = new.checks
11
-
12
- def report(checks = call)
13
- (["LLM Cost Tracker capture verification"] + checks.map { |check| format_check(check) }).join("\n")
14
- end
15
-
16
- def healthy?(checks = call)
17
- checks.none? { |check| check.status == :error }
18
- end
19
-
20
- private
21
-
22
- def format_check(check)
23
- "[#{check.status}] #{check.name}: #{check.message}"
24
- end
25
- end
26
-
27
- def checks
28
- [
29
- enabled_check,
30
- *integration_checks,
31
- *storage_checks
32
- ].compact
33
- end
34
-
35
- private
36
-
37
- def enabled_check
38
- return Check.new(:ok, "tracking", "enabled") if LlmCostTracker.configuration.enabled
39
-
40
- Check.new(:error, "tracking", "disabled; set config.enabled = true before verifying capture")
41
- end
42
-
43
- def integration_checks
44
- enabled = LlmCostTracker.configuration.instrumented_integrations
45
- if enabled.empty?
46
- return [
47
- Check.new(:ok, "sdk integrations", "none enabled; Faraday middleware and manual capture remain available")
48
- ]
49
- end
50
-
51
- LlmCostTracker::Integrations.checks.map do |check|
52
- Check.new(check.status, "sdk integration #{check.name}", check.message)
53
- end
54
- end
55
-
56
- def storage_checks
57
- backend = LlmCostTracker::Storage::Registry.fetch(LlmCostTracker.configuration.storage_backend)
58
- unless backend.respond_to?(:verify)
59
- return [
60
- Check.new(:warn, "storage", "#{LlmCostTracker.configuration.storage_backend} backend has no verifier")
61
- ]
62
- end
63
-
64
- backend.verify.map do |check|
65
- Check.new(check.status, check.name, check.message)
66
- end
67
- rescue LlmCostTracker::Error => e
68
- [Check.new(:error, "storage", e.message)]
69
- end
70
- end
71
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module ConfigurationStorageBackend
5
- def storage_backend=(value)
6
- ensure_shared_configuration_mutable!
7
- @storage_backend = normalize_storage_backend(value)
8
- end
9
-
10
- private
11
-
12
- def normalize_storage_backend(value)
13
- value = :log if value.nil?
14
- value = value.to_sym
15
- return value if self.class::STORAGE_BACKENDS.include?(value)
16
- return value if defined?(Storage::Registry) && Storage::Registry.registered?(value)
17
-
18
- names = if defined?(Storage::Registry)
19
- Storage::Registry.names
20
- else
21
- self.class::STORAGE_BACKENDS
22
- end
23
- raise Error, "Unknown storage_backend: #{value.inspect}. Use one of: #{names.join(', ')}"
24
- end
25
- end
26
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- Cost = Data.define(
5
- :input_cost,
6
- :cache_read_input_cost,
7
- :cache_write_input_cost,
8
- :output_cost,
9
- :total_cost,
10
- :currency
11
- )
12
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- class Doctor
5
- class CaptureCheck
6
- def self.call(check_class)
7
- new(check_class).call
8
- end
9
-
10
- def initialize(check_class)
11
- @check_class = check_class
12
- end
13
-
14
- def call
15
- config = LlmCostTracker.configuration
16
- return disabled_check unless config.enabled
17
- return integrations_check(config.instrumented_integrations) if config.instrumented_integrations.any?
18
-
19
- check(:ok, "no SDK integrations enabled; Faraday middleware and manual capture remain available")
20
- end
21
-
22
- private
23
-
24
- attr_reader :check_class
25
-
26
- def disabled_check
27
- check(:warn, "tracking is disabled; set config.enabled = true to record calls")
28
- end
29
-
30
- def integrations_check(integrations)
31
- check(:ok, "SDK integrations enabled: #{integrations.join(', ')}")
32
- end
33
-
34
- def check(status, message)
35
- check_class.new(status, "capture", message)
36
- end
37
- end
38
- end
39
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module EngineCompatibility
5
- REQUIRED_RAILS_VERSION = Gem::Version.new("7.1.0")
6
-
7
- class << self
8
- def check_rails_version!(version)
9
- return if Gem::Version.new(version) >= REQUIRED_RAILS_VERSION
10
-
11
- raise LlmCostTracker::Error, "LlmCostTracker::Engine requires Rails 7.1+"
12
- end
13
- end
14
- end
15
- end