llm_cost_tracker 0.7.0 → 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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +11 -9
  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 -21
  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/lib/llm_cost_tracker/budget.rb +8 -20
  43. data/lib/llm_cost_tracker/capture/stream.rb +9 -0
  44. data/lib/llm_cost_tracker/capture/stream_collector.rb +182 -0
  45. data/lib/llm_cost_tracker/{integrations → capture}/stream_tracker.rb +40 -72
  46. data/lib/llm_cost_tracker/configuration/instrumentation.rb +3 -7
  47. data/lib/llm_cost_tracker/configuration.rb +28 -35
  48. data/lib/llm_cost_tracker/doctor/capture_verifier.rb +61 -0
  49. data/lib/llm_cost_tracker/doctor/check.rb +7 -0
  50. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +22 -59
  51. data/lib/llm_cost_tracker/doctor/price_check.rb +60 -0
  52. data/lib/llm_cost_tracker/doctor.rb +63 -71
  53. data/lib/llm_cost_tracker/errors.rb +4 -15
  54. data/lib/llm_cost_tracker/event.rb +6 -6
  55. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +42 -0
  56. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +2 -0
  57. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +7 -7
  58. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +3 -3
  59. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +22 -0
  60. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +9 -14
  61. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +0 -4
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +12 -1
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +2 -2
  64. data/lib/llm_cost_tracker/{storage/active_record_inbox_batch.rb → ingestion/batch.rb} +21 -20
  65. data/lib/llm_cost_tracker/ingestion/inbox.rb +105 -0
  66. data/lib/llm_cost_tracker/{storage/active_record_ingestor_lease.rb → ingestion/lease_claim.rb} +5 -7
  67. data/lib/llm_cost_tracker/{storage/active_record_ingestor.rb → ingestion/worker.rb} +38 -48
  68. data/lib/llm_cost_tracker/ingestion.rb +129 -0
  69. data/lib/llm_cost_tracker/integrations/anthropic.rb +52 -34
  70. data/lib/llm_cost_tracker/integrations/base.rb +73 -34
  71. data/lib/llm_cost_tracker/integrations/openai.rb +45 -39
  72. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +40 -30
  73. data/lib/llm_cost_tracker/integrations.rb +43 -0
  74. data/lib/llm_cost_tracker/ledger/period/totals.rb +66 -0
  75. data/lib/llm_cost_tracker/{storage/active_record_periods.rb → ledger/period.rb} +2 -2
  76. data/lib/llm_cost_tracker/ledger/rollups/batch.rb +43 -0
  77. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +46 -0
  78. data/lib/llm_cost_tracker/ledger/rollups.rb +87 -0
  79. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +51 -0
  80. data/lib/llm_cost_tracker/ledger/schema/calls.rb +101 -0
  81. data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +32 -0
  82. data/lib/llm_cost_tracker/ledger/store.rb +60 -0
  83. data/lib/llm_cost_tracker/ledger/tags/query.rb +29 -0
  84. data/lib/llm_cost_tracker/ledger/tags/sql.rb +33 -0
  85. data/lib/llm_cost_tracker/ledger.rb +13 -0
  86. data/lib/llm_cost_tracker/logging.rb +3 -6
  87. data/lib/llm_cost_tracker/middleware/faraday.rb +35 -36
  88. data/lib/llm_cost_tracker/parsers/anthropic.rb +38 -27
  89. data/lib/llm_cost_tracker/parsers/base.rb +10 -19
  90. data/lib/llm_cost_tracker/parsers/gemini.rb +15 -16
  91. data/lib/llm_cost_tracker/parsers/openai_usage.rb +24 -19
  92. data/lib/llm_cost_tracker/parsers/sse.rb +4 -7
  93. data/lib/llm_cost_tracker/parsers.rb +20 -0
  94. data/lib/llm_cost_tracker/prices.json +52 -11
  95. data/lib/llm_cost_tracker/pricing/components.rb +37 -0
  96. data/lib/llm_cost_tracker/pricing/effective_prices.rb +40 -50
  97. data/lib/llm_cost_tracker/pricing/explainer.rb +12 -23
  98. data/lib/llm_cost_tracker/pricing/lookup.rb +24 -25
  99. data/lib/llm_cost_tracker/pricing/registry.rb +156 -0
  100. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +107 -0
  101. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +53 -0
  102. data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +63 -0
  103. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +31 -0
  104. data/lib/llm_cost_tracker/pricing/sync.rb +143 -0
  105. data/lib/llm_cost_tracker/pricing/unknown.rb +46 -0
  106. data/lib/llm_cost_tracker/pricing.rb +33 -32
  107. data/lib/llm_cost_tracker/railtie.rb +7 -8
  108. data/lib/llm_cost_tracker/report/data.rb +72 -0
  109. data/lib/llm_cost_tracker/report/formatter.rb +69 -0
  110. data/lib/llm_cost_tracker/report.rb +8 -8
  111. data/lib/llm_cost_tracker/retention.rb +27 -10
  112. data/lib/llm_cost_tracker/tags/context.rb +35 -0
  113. data/lib/llm_cost_tracker/tags/key.rb +18 -0
  114. data/lib/llm_cost_tracker/tags/sanitizer.rb +68 -0
  115. data/lib/llm_cost_tracker/token_usage.rb +67 -0
  116. data/lib/llm_cost_tracker/tracker.rb +38 -70
  117. data/lib/llm_cost_tracker/usage_capture.rb +37 -0
  118. data/lib/llm_cost_tracker/version.rb +1 -1
  119. data/lib/llm_cost_tracker.rb +56 -78
  120. data/lib/tasks/llm_cost_tracker.rake +18 -13
  121. metadata +54 -58
  122. data/app/services/llm_cost_tracker/dashboard/data_quality_aggregate.rb +0 -81
  123. data/app/services/llm_cost_tracker/pagination.rb +0 -57
  124. data/lib/llm_cost_tracker/active_record_adapter.rb +0 -53
  125. data/lib/llm_cost_tracker/capture_verifier.rb +0 -64
  126. data/lib/llm_cost_tracker/cost.rb +0 -12
  127. data/lib/llm_cost_tracker/doctor/capture_check.rb +0 -39
  128. data/lib/llm_cost_tracker/event_metadata.rb +0 -52
  129. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_usage_breakdown_generator.rb +0 -29
  130. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_usage_breakdown_to_llm_api_calls.rb.erb +0 -29
  131. data/lib/llm_cost_tracker/inbox_event.rb +0 -9
  132. data/lib/llm_cost_tracker/ingestor_lease.rb +0 -9
  133. data/lib/llm_cost_tracker/integrations/object_reader.rb +0 -56
  134. data/lib/llm_cost_tracker/integrations/registry.rb +0 -71
  135. data/lib/llm_cost_tracker/llm_api_call.rb +0 -60
  136. data/lib/llm_cost_tracker/llm_api_call_metrics.rb +0 -63
  137. data/lib/llm_cost_tracker/parameter_hash.rb +0 -33
  138. data/lib/llm_cost_tracker/parsed_usage.rb +0 -72
  139. data/lib/llm_cost_tracker/parsers/registry.rb +0 -58
  140. data/lib/llm_cost_tracker/period_grouping.rb +0 -67
  141. data/lib/llm_cost_tracker/period_total.rb +0 -9
  142. data/lib/llm_cost_tracker/price_freshness.rb +0 -38
  143. data/lib/llm_cost_tracker/price_registry.rb +0 -144
  144. data/lib/llm_cost_tracker/price_sync/fetcher.rb +0 -104
  145. data/lib/llm_cost_tracker/price_sync/registry_diff.rb +0 -51
  146. data/lib/llm_cost_tracker/price_sync/registry_loader.rb +0 -61
  147. data/lib/llm_cost_tracker/price_sync/registry_writer.rb +0 -29
  148. data/lib/llm_cost_tracker/price_sync.rb +0 -144
  149. data/lib/llm_cost_tracker/report_data.rb +0 -94
  150. data/lib/llm_cost_tracker/report_formatter.rb +0 -67
  151. data/lib/llm_cost_tracker/request_url.rb +0 -20
  152. data/lib/llm_cost_tracker/storage/active_record_backend.rb +0 -167
  153. data/lib/llm_cost_tracker/storage/active_record_connection_cleanup.rb +0 -13
  154. data/lib/llm_cost_tracker/storage/active_record_inbox.rb +0 -160
  155. data/lib/llm_cost_tracker/storage/active_record_period_totals.rb +0 -84
  156. data/lib/llm_cost_tracker/storage/active_record_rollup_batch.rb +0 -41
  157. data/lib/llm_cost_tracker/storage/active_record_rollup_upsert_sql.rb +0 -42
  158. data/lib/llm_cost_tracker/storage/active_record_rollups.rb +0 -146
  159. data/lib/llm_cost_tracker/storage/active_record_store.rb +0 -145
  160. data/lib/llm_cost_tracker/storage/writer.rb +0 -35
  161. data/lib/llm_cost_tracker/stream_capture.rb +0 -7
  162. data/lib/llm_cost_tracker/stream_collector.rb +0 -199
  163. data/lib/llm_cost_tracker/tag_accessors.rb +0 -15
  164. data/lib/llm_cost_tracker/tag_context.rb +0 -52
  165. data/lib/llm_cost_tracker/tag_key.rb +0 -16
  166. data/lib/llm_cost_tracker/tag_query.rb +0 -43
  167. data/lib/llm_cost_tracker/tag_sanitizer.rb +0 -81
  168. data/lib/llm_cost_tracker/tag_sql.rb +0 -34
  169. data/lib/llm_cost_tracker/tags_column.rb +0 -105
  170. data/lib/llm_cost_tracker/unknown_pricing.rb +0 -54
  171. data/lib/llm_cost_tracker/usage_breakdown.rb +0 -30
  172. data/lib/llm_cost_tracker/value_helpers.rb +0 -40
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_cost_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
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
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -248,18 +248,26 @@ files:
248
248
  - app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb
249
249
  - app/helpers/llm_cost_tracker/dashboard_query_helper.rb
250
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
251
259
  - app/services/llm_cost_tracker/dashboard/data_quality.rb
252
- - app/services/llm_cost_tracker/dashboard/data_quality_aggregate.rb
253
260
  - app/services/llm_cost_tracker/dashboard/date_range.rb
254
261
  - app/services/llm_cost_tracker/dashboard/filter.rb
255
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
256
265
  - app/services/llm_cost_tracker/dashboard/provider_breakdown.rb
257
266
  - app/services/llm_cost_tracker/dashboard/spend_anomaly.rb
258
267
  - app/services/llm_cost_tracker/dashboard/tag_breakdown.rb
259
268
  - app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb
260
269
  - app/services/llm_cost_tracker/dashboard/time_series.rb
261
270
  - app/services/llm_cost_tracker/dashboard/top_models.rb
262
- - app/services/llm_cost_tracker/pagination.rb
263
271
  - app/views/layouts/llm_cost_tracker/application.html.erb
264
272
  - app/views/llm_cost_tracker/calls/index.html.erb
265
273
  - app/views/llm_cost_tracker/calls/show.html.erb
@@ -279,26 +287,27 @@ files:
279
287
  - app/views/llm_cost_tracker/tags/show.html.erb
280
288
  - config/routes.rb
281
289
  - lib/llm_cost_tracker.rb
282
- - lib/llm_cost_tracker/active_record_adapter.rb
283
290
  - lib/llm_cost_tracker/assets.rb
284
291
  - lib/llm_cost_tracker/budget.rb
285
- - 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
286
295
  - lib/llm_cost_tracker/configuration.rb
287
296
  - lib/llm_cost_tracker/configuration/instrumentation.rb
288
- - lib/llm_cost_tracker/cost.rb
289
297
  - lib/llm_cost_tracker/doctor.rb
290
- - 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
291
300
  - lib/llm_cost_tracker/doctor/ingestion_check.rb
301
+ - lib/llm_cost_tracker/doctor/price_check.rb
292
302
  - lib/llm_cost_tracker/engine.rb
293
303
  - lib/llm_cost_tracker/errors.rb
294
304
  - lib/llm_cost_tracker/event.rb
295
- - lib/llm_cost_tracker/event_metadata.rb
296
305
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb
297
306
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb
298
307
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb
299
308
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb
300
309
  - lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb
301
- - 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
302
311
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
303
312
  - lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb
304
313
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb
@@ -306,82 +315,69 @@ files:
306
315
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb
307
316
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb
308
317
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb
309
- - 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
310
319
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb
311
320
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb
312
321
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb
313
322
  - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb
314
323
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb
315
324
  - lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb
316
- - lib/llm_cost_tracker/inbox_event.rb
317
- - 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
318
331
  - lib/llm_cost_tracker/integrations/anthropic.rb
319
332
  - lib/llm_cost_tracker/integrations/base.rb
320
- - lib/llm_cost_tracker/integrations/object_reader.rb
321
333
  - lib/llm_cost_tracker/integrations/openai.rb
322
- - lib/llm_cost_tracker/integrations/registry.rb
323
334
  - lib/llm_cost_tracker/integrations/ruby_llm.rb
324
- - lib/llm_cost_tracker/integrations/stream_tracker.rb
325
- - lib/llm_cost_tracker/llm_api_call.rb
326
- - 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
327
347
  - lib/llm_cost_tracker/logging.rb
328
348
  - lib/llm_cost_tracker/middleware/faraday.rb
329
- - lib/llm_cost_tracker/parameter_hash.rb
330
- - lib/llm_cost_tracker/parsed_usage.rb
349
+ - lib/llm_cost_tracker/parsers.rb
331
350
  - lib/llm_cost_tracker/parsers/anthropic.rb
332
351
  - lib/llm_cost_tracker/parsers/base.rb
333
352
  - lib/llm_cost_tracker/parsers/gemini.rb
334
353
  - lib/llm_cost_tracker/parsers/openai.rb
335
354
  - lib/llm_cost_tracker/parsers/openai_compatible.rb
336
355
  - lib/llm_cost_tracker/parsers/openai_usage.rb
337
- - lib/llm_cost_tracker/parsers/registry.rb
338
356
  - lib/llm_cost_tracker/parsers/sse.rb
339
- - lib/llm_cost_tracker/period_grouping.rb
340
- - lib/llm_cost_tracker/period_total.rb
341
- - lib/llm_cost_tracker/price_freshness.rb
342
- - lib/llm_cost_tracker/price_registry.rb
343
- - lib/llm_cost_tracker/price_sync.rb
344
- - lib/llm_cost_tracker/price_sync/fetcher.rb
345
- - lib/llm_cost_tracker/price_sync/registry_diff.rb
346
- - lib/llm_cost_tracker/price_sync/registry_loader.rb
347
- - lib/llm_cost_tracker/price_sync/registry_writer.rb
348
357
  - lib/llm_cost_tracker/prices.json
349
358
  - lib/llm_cost_tracker/pricing.rb
359
+ - lib/llm_cost_tracker/pricing/components.rb
350
360
  - lib/llm_cost_tracker/pricing/effective_prices.rb
351
361
  - lib/llm_cost_tracker/pricing/explainer.rb
352
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
353
370
  - lib/llm_cost_tracker/railtie.rb
354
371
  - lib/llm_cost_tracker/report.rb
355
- - lib/llm_cost_tracker/report_data.rb
356
- - lib/llm_cost_tracker/report_formatter.rb
357
- - lib/llm_cost_tracker/request_url.rb
372
+ - lib/llm_cost_tracker/report/data.rb
373
+ - lib/llm_cost_tracker/report/formatter.rb
358
374
  - lib/llm_cost_tracker/retention.rb
359
- - lib/llm_cost_tracker/storage/active_record_backend.rb
360
- - lib/llm_cost_tracker/storage/active_record_connection_cleanup.rb
361
- - lib/llm_cost_tracker/storage/active_record_inbox.rb
362
- - lib/llm_cost_tracker/storage/active_record_inbox_batch.rb
363
- - lib/llm_cost_tracker/storage/active_record_ingestor.rb
364
- - lib/llm_cost_tracker/storage/active_record_ingestor_lease.rb
365
- - lib/llm_cost_tracker/storage/active_record_period_totals.rb
366
- - lib/llm_cost_tracker/storage/active_record_periods.rb
367
- - lib/llm_cost_tracker/storage/active_record_rollup_batch.rb
368
- - lib/llm_cost_tracker/storage/active_record_rollup_upsert_sql.rb
369
- - lib/llm_cost_tracker/storage/active_record_rollups.rb
370
- - lib/llm_cost_tracker/storage/active_record_store.rb
371
- - lib/llm_cost_tracker/storage/writer.rb
372
- - lib/llm_cost_tracker/stream_capture.rb
373
- - lib/llm_cost_tracker/stream_collector.rb
374
- - lib/llm_cost_tracker/tag_accessors.rb
375
- - lib/llm_cost_tracker/tag_context.rb
376
- - lib/llm_cost_tracker/tag_key.rb
377
- - lib/llm_cost_tracker/tag_query.rb
378
- - lib/llm_cost_tracker/tag_sanitizer.rb
379
- - lib/llm_cost_tracker/tag_sql.rb
380
- - 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
381
379
  - lib/llm_cost_tracker/tracker.rb
382
- - lib/llm_cost_tracker/unknown_pricing.rb
383
- - lib/llm_cost_tracker/usage_breakdown.rb
384
- - lib/llm_cost_tracker/value_helpers.rb
380
+ - lib/llm_cost_tracker/usage_capture.rb
385
381
  - lib/llm_cost_tracker/version.rb
386
382
  - lib/tasks/llm_cost_tracker.rake
387
383
  homepage: https://github.com/sergey-homenko/llm_cost_tracker
@@ -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,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "errors"
4
-
5
- module LlmCostTracker
6
- module ActiveRecordAdapter
7
- MYSQL_ADAPTERS = %w[
8
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
9
- ActiveRecord::ConnectionAdapters::Mysql2Adapter
10
- ActiveRecord::ConnectionAdapters::TrilogyAdapter
11
- ].freeze
12
- POSTGRESQL_ADAPTERS = %w[
13
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
14
- ].freeze
15
- MYSQL_PATTERN = /mysql|trilogy|mariadb/i
16
- POSTGRESQL_PATTERN = /postgres/i
17
-
18
- class << self
19
- def mysql?(value) = adapter_instance?(value, MYSQL_ADAPTERS) || adapter_name(value).match?(MYSQL_PATTERN)
20
-
21
- def postgresql?(value)
22
- adapter_instance?(value, POSTGRESQL_ADAPTERS) || adapter_name(value).match?(POSTGRESQL_PATTERN)
23
- end
24
-
25
- def supported?(value) = mysql?(value) || postgresql?(value)
26
-
27
- def ensure_supported!(value)
28
- return if supported?(value)
29
-
30
- raise Error, "Unsupported database adapter: #{adapter_name(value)}. Use PostgreSQL or MySQL."
31
- end
32
-
33
- private
34
-
35
- def adapter_instance?(value, class_names)
36
- class_names.any? do |class_name|
37
- adapter_class = constantize(class_name)
38
- adapter_class && value.is_a?(adapter_class)
39
- end
40
- end
41
-
42
- def constantize(name)
43
- name.split("::").reduce(Object) { |namespace, part| namespace.const_get(part, false) }
44
- rescue NameError
45
- nil
46
- end
47
-
48
- def adapter_name(value)
49
- value.respond_to?(:adapter_name) ? value.adapter_name.to_s : value.to_s
50
- end
51
- end
52
- end
53
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "storage/active_record_backend"
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::Registry.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
- LlmCostTracker::Storage::ActiveRecordBackend.verify.map do |check|
58
- Check.new(check.status, check.name, check.message)
59
- end
60
- rescue LlmCostTracker::Error => e
61
- [Check.new(:error, "storage", e.message)]
62
- end
63
- end
64
- 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,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module EventMetadata
5
- INTERNAL_TAG_KEYS = %w[
6
- cache_read_input_tokens
7
- cache_write_input_tokens
8
- hidden_output_tokens
9
- input_tokens
10
- output_tokens
11
- pricing_mode
12
- provider_response_id
13
- total_tokens
14
- ].freeze
15
-
16
- class << self
17
- def usage_data(input_tokens, output_tokens, metadata)
18
- metadata = metadata.to_h.symbolize_keys
19
- cache_read = first_integer(metadata, :cache_read_input_tokens)
20
- cache_write = first_integer(metadata, :cache_write_input_tokens)
21
- hidden_output = first_integer(metadata, :hidden_output_tokens)
22
- breakdown = UsageBreakdown.build(
23
- input_tokens: input_tokens,
24
- output_tokens: output_tokens,
25
- cache_read_input_tokens: cache_read,
26
- cache_write_input_tokens: cache_write,
27
- hidden_output_tokens: hidden_output
28
- )
29
-
30
- breakdown.to_h.merge(pricing_mode: normalized_pricing_mode(metadata[:pricing_mode])).compact
31
- end
32
-
33
- def tags(metadata)
34
- metadata.reject { |key, _value| INTERNAL_TAG_KEYS.include?(key.to_s) }
35
- end
36
-
37
- private
38
-
39
- def first_integer(metadata, *keys)
40
- keys.each { |key| return metadata[key].to_i unless metadata[key].nil? }
41
- 0
42
- end
43
-
44
- def normalized_pricing_mode(value)
45
- return nil if value.nil?
46
-
47
- mode = value.to_s.strip
48
- mode.empty? || mode == "standard" ? nil : mode
49
- end
50
- end
51
- end
52
- 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 AddUsageBreakdownGenerator < 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 usage and cost breakdown columns to llm_api_calls"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_usage_breakdown_to_llm_api_calls.rb.erb",
18
- "db/migrate/add_usage_breakdown_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
- class AddUsageBreakdownToLlmApiCalls < ActiveRecord::Migration<%= migration_version %>
2
- def up
3
- unless column_exists?(:llm_api_calls, :cache_read_input_tokens)
4
- add_column :llm_api_calls, :cache_read_input_tokens, :integer, null: false, default: 0
5
- end
6
- unless column_exists?(:llm_api_calls, :cache_write_input_tokens)
7
- add_column :llm_api_calls, :cache_write_input_tokens, :integer, null: false, default: 0
8
- end
9
- unless column_exists?(:llm_api_calls, :hidden_output_tokens)
10
- add_column :llm_api_calls, :hidden_output_tokens, :integer, null: false, default: 0
11
- end
12
- unless column_exists?(:llm_api_calls, :cache_read_input_cost)
13
- add_column :llm_api_calls, :cache_read_input_cost, :decimal, precision: 20, scale: 8
14
- end
15
- unless column_exists?(:llm_api_calls, :cache_write_input_cost)
16
- add_column :llm_api_calls, :cache_write_input_cost, :decimal, precision: 20, scale: 8
17
- end
18
- add_column :llm_api_calls, :pricing_mode, :string unless column_exists?(:llm_api_calls, :pricing_mode)
19
- end
20
-
21
- def down
22
- remove_column :llm_api_calls, :pricing_mode if column_exists?(:llm_api_calls, :pricing_mode)
23
- remove_column :llm_api_calls, :cache_write_input_cost if column_exists?(:llm_api_calls, :cache_write_input_cost)
24
- remove_column :llm_api_calls, :cache_read_input_cost if column_exists?(:llm_api_calls, :cache_read_input_cost)
25
- remove_column :llm_api_calls, :hidden_output_tokens if column_exists?(:llm_api_calls, :hidden_output_tokens)
26
- remove_column :llm_api_calls, :cache_write_input_tokens if column_exists?(:llm_api_calls, :cache_write_input_tokens)
27
- remove_column :llm_api_calls, :cache_read_input_tokens if column_exists?(:llm_api_calls, :cache_read_input_tokens)
28
- end
29
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
-
5
- module LlmCostTracker
6
- class InboxEvent < ActiveRecord::Base
7
- self.table_name = "llm_cost_tracker_inbox_events"
8
- end
9
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
-
5
- module LlmCostTracker
6
- class IngestorLease < ActiveRecord::Base
7
- self.table_name = "llm_cost_tracker_ingestor_leases"
8
- end
9
- end