llm_cost_tracker 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +72 -1
  4. data/README.md +58 -221
  5. data/app/assets/llm_cost_tracker/application.css +218 -41
  6. data/app/controllers/llm_cost_tracker/application_controller.rb +30 -17
  7. data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
  8. data/app/controllers/llm_cost_tracker/calls_controller.rb +19 -14
  9. data/app/controllers/llm_cost_tracker/data_quality_controller.rb +10 -2
  10. data/app/helpers/llm_cost_tracker/application_helper.rb +11 -24
  11. data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
  12. data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
  13. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
  14. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +20 -7
  15. data/app/models/llm_cost_tracker/call.rb +169 -0
  16. data/app/models/llm_cost_tracker/call_line_item.rb +22 -0
  17. data/app/models/llm_cost_tracker/call_rollup.rb +9 -0
  18. data/app/models/llm_cost_tracker/call_tag.rb +16 -0
  19. data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +13 -0
  20. data/app/models/llm_cost_tracker/ingestion/lease.rb +1 -1
  21. data/app/models/llm_cost_tracker/provider_invoice.rb +9 -0
  22. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +125 -34
  23. data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
  24. data/app/services/llm_cost_tracker/dashboard/filter.rb +2 -2
  25. data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
  26. data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
  27. data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
  28. data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
  29. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
  30. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
  31. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
  32. data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
  33. data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
  34. data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
  35. data/app/views/llm_cost_tracker/calls/show.html.erb +62 -7
  36. data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -50
  37. data/app/views/llm_cost_tracker/data_quality/index.html.erb +103 -126
  38. data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
  39. data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
  40. data/app/views/llm_cost_tracker/shared/_filters.html.erb +63 -0
  41. data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
  42. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
  43. data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
  44. data/app/views/llm_cost_tracker/tags/show.html.erb +5 -37
  45. data/lib/llm_cost_tracker/billing/components.rb +53 -0
  46. data/lib/llm_cost_tracker/billing/components.yml +117 -0
  47. data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
  48. data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
  49. data/lib/llm_cost_tracker/budget.rb +23 -35
  50. data/lib/llm_cost_tracker/capture/stream_collector.rb +47 -33
  51. data/lib/llm_cost_tracker/configuration.rb +36 -19
  52. data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +54 -0
  53. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +24 -32
  54. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
  55. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
  56. data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
  57. data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
  58. data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
  59. data/lib/llm_cost_tracker/doctor/schema_check.rb +31 -0
  60. data/lib/llm_cost_tracker/doctor.rb +43 -45
  61. data/lib/llm_cost_tracker/errors.rb +5 -19
  62. data/lib/llm_cost_tracker/event.rb +10 -2
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -2
  64. data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
  65. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +157 -0
  66. data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
  67. data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -23
  68. data/lib/llm_cost_tracker/ingestion/worker.rb +14 -5
  69. data/lib/llm_cost_tracker/ingestion.rb +28 -22
  70. data/lib/llm_cost_tracker/integrations/anthropic.rb +45 -38
  71. data/lib/llm_cost_tracker/integrations/base.rb +36 -29
  72. data/lib/llm_cost_tracker/integrations/openai.rb +85 -40
  73. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +5 -5
  74. data/lib/llm_cost_tracker/integrations.rb +2 -2
  75. data/lib/llm_cost_tracker/ledger/period/totals.rb +12 -9
  76. data/lib/llm_cost_tracker/ledger/period.rb +5 -5
  77. data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +4 -10
  78. data/lib/llm_cost_tracker/ledger/rollups.rb +76 -25
  79. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
  80. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +50 -0
  81. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
  82. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +26 -0
  83. data/lib/llm_cost_tracker/ledger/schema/calls.rb +34 -23
  84. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
  85. data/lib/llm_cost_tracker/ledger/store.rb +110 -18
  86. data/lib/llm_cost_tracker/ledger/tags/query.rb +5 -11
  87. data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -14
  88. data/lib/llm_cost_tracker/ledger.rb +4 -2
  89. data/lib/llm_cost_tracker/logging.rb +2 -5
  90. data/lib/llm_cost_tracker/middleware/faraday.rb +7 -6
  91. data/lib/llm_cost_tracker/parsers/anthropic.rb +52 -7
  92. data/lib/llm_cost_tracker/parsers/base.rb +8 -3
  93. data/lib/llm_cost_tracker/parsers/gemini.rb +101 -15
  94. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +10 -2
  95. data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +87 -0
  96. data/lib/llm_cost_tracker/parsers/openai_usage.rb +48 -21
  97. data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
  98. data/lib/llm_cost_tracker/parsers.rb +1 -1
  99. data/lib/llm_cost_tracker/prices.json +105 -20
  100. data/lib/llm_cost_tracker/pricing/effective_prices.rb +57 -19
  101. data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
  102. data/lib/llm_cost_tracker/pricing/lookup.rb +38 -34
  103. data/lib/llm_cost_tracker/pricing/registry.rb +65 -45
  104. data/lib/llm_cost_tracker/pricing/service_charges.rb +204 -0
  105. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
  106. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
  107. data/lib/llm_cost_tracker/pricing/sync.rb +57 -10
  108. data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
  109. data/lib/llm_cost_tracker/pricing.rb +190 -26
  110. data/lib/llm_cost_tracker/railtie.rb +0 -8
  111. data/lib/llm_cost_tracker/report/data.rb +16 -8
  112. data/lib/llm_cost_tracker/report.rb +0 -4
  113. data/lib/llm_cost_tracker/retention.rb +8 -8
  114. data/lib/llm_cost_tracker/tags/context.rb +2 -4
  115. data/lib/llm_cost_tracker/tags/key.rb +4 -0
  116. data/lib/llm_cost_tracker/tags/sanitizer.rb +12 -17
  117. data/lib/llm_cost_tracker/timing.rb +15 -0
  118. data/lib/llm_cost_tracker/token_usage.rb +56 -42
  119. data/lib/llm_cost_tracker/tracker.rb +67 -24
  120. data/lib/llm_cost_tracker/usage_capture.rb +29 -8
  121. data/lib/llm_cost_tracker/version.rb +1 -1
  122. data/lib/llm_cost_tracker.rb +36 -35
  123. data/lib/tasks/llm_cost_tracker.rake +22 -17
  124. metadata +36 -41
  125. data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
  126. data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
  127. data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
  128. data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
  129. data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
  130. data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
  131. data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
  132. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
  133. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
  134. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
  135. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb +0 -29
  136. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +0 -29
  137. data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
  138. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
  139. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
  140. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
  141. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
  142. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
  143. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
  144. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
  145. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
  146. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
  147. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
  148. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
  149. data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
  150. data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
  151. data/lib/llm_cost_tracker/pricing/components.rb +0 -37
  152. data/lib/llm_cost_tracker/pricing/sync/registry_loader.rb +0 -63
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.2
4
+ version: 0.8.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
@@ -249,13 +247,13 @@ files:
249
247
  - app/helpers/llm_cost_tracker/dashboard_query_helper.rb
250
248
  - app/helpers/llm_cost_tracker/pagination_helper.rb
251
249
  - app/helpers/llm_cost_tracker/token_usage_helper.rb
252
- - app/models/llm_cost_tracker/ingestion/event.rb
250
+ - app/models/llm_cost_tracker/call.rb
251
+ - app/models/llm_cost_tracker/call_line_item.rb
252
+ - app/models/llm_cost_tracker/call_rollup.rb
253
+ - app/models/llm_cost_tracker/call_tag.rb
254
+ - app/models/llm_cost_tracker/ingestion/inbox_entry.rb
253
255
  - 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
+ - app/models/llm_cost_tracker/provider_invoice.rb
259
257
  - app/services/llm_cost_tracker/dashboard/data_quality.rb
260
258
  - app/services/llm_cost_tracker/dashboard/date_range.rb
261
259
  - app/services/llm_cost_tracker/dashboard/filter.rb
@@ -279,7 +277,9 @@ files:
279
277
  - app/views/llm_cost_tracker/models/index.html.erb
280
278
  - app/views/llm_cost_tracker/shared/_active_filters.html.erb
281
279
  - app/views/llm_cost_tracker/shared/_bar.html.erb
280
+ - app/views/llm_cost_tracker/shared/_filters.html.erb
282
281
  - app/views/llm_cost_tracker/shared/_metric_stack.html.erb
282
+ - app/views/llm_cost_tracker/shared/_sort.html.erb
283
283
  - app/views/llm_cost_tracker/shared/_spend_chart.html.erb
284
284
  - app/views/llm_cost_tracker/shared/_tag_chips.html.erb
285
285
  - app/views/llm_cost_tracker/shared/setup_required.html.erb
@@ -288,40 +288,33 @@ files:
288
288
  - config/routes.rb
289
289
  - lib/llm_cost_tracker.rb
290
290
  - lib/llm_cost_tracker/assets.rb
291
+ - lib/llm_cost_tracker/billing/components.rb
292
+ - lib/llm_cost_tracker/billing/components.yml
293
+ - lib/llm_cost_tracker/billing/cost_status.rb
294
+ - lib/llm_cost_tracker/billing/line_item.rb
291
295
  - lib/llm_cost_tracker/budget.rb
292
296
  - lib/llm_cost_tracker/capture/stream.rb
293
297
  - lib/llm_cost_tracker/capture/stream_collector.rb
294
298
  - lib/llm_cost_tracker/capture/stream_tracker.rb
295
299
  - lib/llm_cost_tracker/configuration.rb
296
- - lib/llm_cost_tracker/configuration/instrumentation.rb
297
300
  - lib/llm_cost_tracker/doctor.rb
298
301
  - lib/llm_cost_tracker/doctor/capture_verifier.rb
299
302
  - lib/llm_cost_tracker/doctor/check.rb
303
+ - lib/llm_cost_tracker/doctor/cost_drift_check.rb
300
304
  - lib/llm_cost_tracker/doctor/ingestion_check.rb
305
+ - lib/llm_cost_tracker/doctor/legacy_audit_check.rb
306
+ - lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb
301
307
  - lib/llm_cost_tracker/doctor/price_check.rb
308
+ - lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb
309
+ - lib/llm_cost_tracker/doctor/probe.rb
310
+ - lib/llm_cost_tracker/doctor/schema_check.rb
302
311
  - lib/llm_cost_tracker/engine.rb
303
312
  - lib/llm_cost_tracker/errors.rb
304
313
  - 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
311
314
  - lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb
312
315
  - 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
316
+ - lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb
320
317
  - 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
325
318
  - lib/llm_cost_tracker/ingestion.rb
326
319
  - lib/llm_cost_tracker/ingestion/batch.rb
327
320
  - lib/llm_cost_tracker/ingestion/inbox.rb
@@ -336,11 +329,13 @@ files:
336
329
  - lib/llm_cost_tracker/ledger/period.rb
337
330
  - lib/llm_cost_tracker/ledger/period/totals.rb
338
331
  - lib/llm_cost_tracker/ledger/rollups.rb
339
- - lib/llm_cost_tracker/ledger/rollups/batch.rb
340
332
  - lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb
341
333
  - lib/llm_cost_tracker/ledger/schema/adapter.rb
334
+ - lib/llm_cost_tracker/ledger/schema/call_line_items.rb
335
+ - lib/llm_cost_tracker/ledger/schema/call_rollups.rb
336
+ - lib/llm_cost_tracker/ledger/schema/call_tags.rb
342
337
  - lib/llm_cost_tracker/ledger/schema/calls.rb
343
- - lib/llm_cost_tracker/ledger/schema/period_totals.rb
338
+ - lib/llm_cost_tracker/ledger/schema/provider_invoices.rb
344
339
  - lib/llm_cost_tracker/ledger/store.rb
345
340
  - lib/llm_cost_tracker/ledger/tags/query.rb
346
341
  - lib/llm_cost_tracker/ledger/tags/sql.rb
@@ -352,20 +347,21 @@ files:
352
347
  - lib/llm_cost_tracker/parsers/gemini.rb
353
348
  - lib/llm_cost_tracker/parsers/openai.rb
354
349
  - lib/llm_cost_tracker/parsers/openai_compatible.rb
350
+ - lib/llm_cost_tracker/parsers/openai_service_charges.rb
355
351
  - lib/llm_cost_tracker/parsers/openai_usage.rb
356
352
  - lib/llm_cost_tracker/parsers/sse.rb
357
353
  - lib/llm_cost_tracker/prices.json
358
354
  - lib/llm_cost_tracker/pricing.rb
359
- - lib/llm_cost_tracker/pricing/components.rb
360
355
  - lib/llm_cost_tracker/pricing/effective_prices.rb
361
356
  - lib/llm_cost_tracker/pricing/explainer.rb
362
357
  - lib/llm_cost_tracker/pricing/lookup.rb
363
358
  - lib/llm_cost_tracker/pricing/registry.rb
359
+ - lib/llm_cost_tracker/pricing/service_charges.rb
364
360
  - lib/llm_cost_tracker/pricing/sync.rb
365
361
  - lib/llm_cost_tracker/pricing/sync/fetcher.rb
366
362
  - lib/llm_cost_tracker/pricing/sync/registry_diff.rb
367
- - lib/llm_cost_tracker/pricing/sync/registry_loader.rb
368
363
  - lib/llm_cost_tracker/pricing/sync/registry_writer.rb
364
+ - lib/llm_cost_tracker/pricing/sync_change_printer.rb
369
365
  - lib/llm_cost_tracker/pricing/unknown.rb
370
366
  - lib/llm_cost_tracker/railtie.rb
371
367
  - lib/llm_cost_tracker/report.rb
@@ -375,6 +371,7 @@ files:
375
371
  - lib/llm_cost_tracker/tags/context.rb
376
372
  - lib/llm_cost_tracker/tags/key.rb
377
373
  - lib/llm_cost_tracker/tags/sanitizer.rb
374
+ - lib/llm_cost_tracker/timing.rb
378
375
  - lib/llm_cost_tracker/token_usage.rb
379
376
  - lib/llm_cost_tracker/tracker.rb
380
377
  - lib/llm_cost_tracker/usage_capture.rb
@@ -389,7 +386,6 @@ metadata:
389
386
  source_code_uri: https://github.com/sergey-homenko/llm_cost_tracker
390
387
  documentation_uri: https://github.com/sergey-homenko/llm_cost_tracker#readme
391
388
  rubygems_mfa_required: 'true'
392
- post_install_message:
393
389
  rdoc_options: []
394
390
  require_paths:
395
391
  - lib
@@ -397,15 +393,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
397
393
  requirements:
398
394
  - - ">="
399
395
  - !ruby/object:Gem::Version
400
- version: 3.3.0
396
+ version: 3.4.0
401
397
  required_rubygems_version: !ruby/object:Gem::Requirement
402
398
  requirements:
403
399
  - - ">="
404
400
  - !ruby/object:Gem::Version
405
401
  version: '0'
406
402
  requirements: []
407
- rubygems_version: 3.5.22
408
- signing_key:
403
+ rubygems_version: 3.6.9
409
404
  specification_version: 4
410
- summary: Rails-native LLM usage and cost tracking with ActiveRecord storage
405
+ summary: LLM API cost tracking for Rails applications
411
406
  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(self, 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(self, key, table_name: table_name)
52
- end
53
-
54
- private
55
-
56
- def cost_by_column(column, limit:)
57
- quoted_column = "#{quoted_table_name}.#{connection.quote_column_name(column)}"
58
- relation = select("#{quoted_column} AS name, COALESCE(SUM(total_cost), 0) AS total_cost")
59
- .group(column)
60
- .order(Arel.sql("COALESCE(SUM(total_cost), 0) DESC"))
61
- relation = relation.limit(limit) if limit
62
- relation
63
- end
64
- end
65
- end
66
- end
@@ -1,71 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "llm_cost_tracker/ledger/schema/adapter"
4
-
5
- module LlmCostTracker
6
- module Ledger
7
- module Period
8
- module Grouping
9
- PERIOD_FORMATS = {
10
- day: {
11
- postgres: "YYYY-MM-DD",
12
- mysql: "%Y-%m-%d"
13
- },
14
- month: {
15
- postgres: "YYYY-MM",
16
- mysql: "%Y-%m"
17
- }
18
- }.freeze
19
-
20
- private_constant :PERIOD_FORMATS
21
-
22
- def group_by_period(period, column: :tracked_at)
23
- group(Arel.sql(period_group_expression(period, column: column)))
24
- end
25
-
26
- def daily_costs(days: 30)
27
- where(tracked_at: days.days.ago..)
28
- .group_by_period(:day)
29
- .sum(:total_cost)
30
- end
31
-
32
- private
33
-
34
- def period_group_expression(period, column:)
35
- period = validated_period(period)
36
- column = period_column_expression(column)
37
- formats = PERIOD_FORMATS.fetch(period)
38
-
39
- if Ledger::Schema::Adapter.postgresql?(connection)
40
- postgres_period_expression(period, column, formats)
41
- elsif Ledger::Schema::Adapter.mysql?(connection)
42
- "DATE_FORMAT(#{column}, #{connection.quote(formats.fetch(:mysql))})"
43
- else
44
- Ledger::Schema::Adapter.ensure_supported!(connection)
45
- end
46
- end
47
-
48
- def postgres_period_expression(period, column, formats)
49
- "TO_CHAR(" \
50
- "DATE_TRUNC(#{connection.quote(period.to_s)}, #{column}), " \
51
- "#{connection.quote(formats.fetch(:postgres))}" \
52
- ")"
53
- end
54
-
55
- def validated_period(period)
56
- normalized_period = period.try(:to_sym)
57
- return normalized_period if PERIOD_FORMATS.key?(normalized_period)
58
-
59
- raise ArgumentError, "invalid period: #{period.inspect}"
60
- end
61
-
62
- def period_column_expression(column)
63
- column = column.to_s
64
- return "#{quoted_table_name}.#{connection.quote_column_name(column)}" if column_names.include?(column)
65
-
66
- raise ArgumentError, "invalid period column: #{column.inspect}"
67
- end
68
- end
69
- end
70
- end
71
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record"
4
-
5
- module LlmCostTracker
6
- module Ledger
7
- module Period
8
- class Total < ActiveRecord::Base
9
- self.table_name = "llm_cost_tracker_period_totals"
10
- end
11
- end
12
- end
13
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module LlmCostTracker
6
- module Ledger
7
- module Tags
8
- module Accessors
9
- def parsed_tags
10
- return tags.transform_keys(&:to_s) if tags.is_a?(Hash)
11
-
12
- JSON.parse(tags || "{}")
13
- rescue JSON::ParserError
14
- {}
15
- end
16
- end
17
- end
18
- end
19
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmCostTracker
4
- module ConfigurationInstrumentation
5
- def instrument(*names)
6
- ensure_shared_configuration_mutable!
7
- @instrumented_integrations = (@instrumented_integrations + normalize_instrumentation_names(names)).uniq
8
- end
9
-
10
- def instrumented?(name)
11
- @instrumented_integrations.include?(name.to_sym)
12
- end
13
-
14
- private
15
-
16
- def normalize_instrumentation_names(names)
17
- names.flatten.flat_map do |name|
18
- key = name.to_sym
19
- next Integrations.names if key == :all
20
-
21
- validate_instrumentation_name!(key)
22
- key
23
- end
24
- end
25
-
26
- def validate_instrumentation_name!(name)
27
- return if Integrations.names.include?(name)
28
-
29
- raise Error, "Unknown integration: #{name.inspect}. " \
30
- "Use one of: #{Integrations.names.join(', ')}"
31
- end
32
- end
33
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module LlmCostTracker
7
- module Generators
8
- class AddIngestionGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add durable ActiveRecord ingestion"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_ingestion_to_llm_cost_tracker.rb.erb",
18
- "db/migrate/add_ingestion_to_llm_cost_tracker.rb"
19
- )
20
- end
21
-
22
- private
23
-
24
- def migration_version
25
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
26
- end
27
- end
28
- end
29
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module LlmCostTracker
7
- module Generators
8
- class AddLatencyMsGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add llm_api_calls.latency_ms"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_latency_ms_to_llm_api_calls.rb.erb",
18
- "db/migrate/add_latency_ms_to_llm_api_calls.rb"
19
- )
20
- end
21
-
22
- private
23
-
24
- def migration_version
25
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
26
- end
27
- end
28
- end
29
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module LlmCostTracker
7
- module Generators
8
- class AddPeriodTotalsGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add llm_cost_tracker_period_totals"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_period_totals_to_llm_cost_tracker.rb.erb",
18
- "db/migrate/add_period_totals_to_llm_cost_tracker.rb"
19
- )
20
- end
21
-
22
- private
23
-
24
- def migration_version
25
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
26
- end
27
- end
28
- end
29
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module LlmCostTracker
7
- module Generators
8
- class AddProviderResponseIdGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add llm_api_calls.provider_response_id"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_provider_response_id_to_llm_api_calls.rb.erb",
18
- "db/migrate/add_provider_response_id_to_llm_api_calls.rb"
19
- )
20
- end
21
-
22
- private
23
-
24
- def migration_version
25
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
26
- end
27
- end
28
- end
29
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module LlmCostTracker
7
- module Generators
8
- class AddStreamingGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- source_root File.expand_path("templates", __dir__)
12
-
13
- desc "Creates a migration to add llm_api_calls.stream and llm_api_calls.usage_source"
14
-
15
- def create_migration_file
16
- migration_template(
17
- "add_streaming_to_llm_api_calls.rb.erb",
18
- "db/migrate/add_streaming_to_llm_api_calls.rb"
19
- )
20
- end
21
-
22
- private
23
-
24
- def migration_version
25
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
26
- end
27
- end
28
- end
29
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators"
4
- require "rails/generators/active_record"
5
-
6
- module LlmCostTracker
7
- module Generators
8
- class AddTokenUsageGenerator < Rails::Generators::Base
9
- include ActiveRecord::Generators::Migration
10
-
11
- TOKEN_COLUMNS = %w[
12
- cache_read_input_tokens
13
- cache_write_input_tokens
14
- cache_write_1h_input_tokens
15
- hidden_output_tokens
16
- ].freeze
17
- COST_COLUMNS = %w[
18
- cache_read_input_cost
19
- cache_write_input_cost
20
- cache_write_1h_input_cost
21
- ].freeze
22
- COLUMN_NAMES = (TOKEN_COLUMNS + COST_COLUMNS + %w[pricing_mode]).freeze
23
-
24
- source_root File.expand_path("templates", __dir__)
25
-
26
- desc "Creates a migration to add token usage and token cost columns to llm_api_calls"
27
-
28
- def create_migration_file
29
- migration_template(
30
- "add_token_usage_to_llm_api_calls.rb.erb",
31
- "db/migrate/add_token_usage_to_llm_api_calls.rb"
32
- )
33
- end
34
-
35
- private
36
-
37
- def migration_version
38
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
39
- end
40
- end
41
- end
42
- end