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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +72 -1
- data/README.md +58 -221
- data/app/assets/llm_cost_tracker/application.css +218 -41
- data/app/controllers/llm_cost_tracker/application_controller.rb +30 -17
- data/app/controllers/llm_cost_tracker/assets_controller.rb +11 -1
- data/app/controllers/llm_cost_tracker/calls_controller.rb +19 -14
- data/app/controllers/llm_cost_tracker/data_quality_controller.rb +10 -2
- data/app/helpers/llm_cost_tracker/application_helper.rb +11 -24
- data/app/helpers/llm_cost_tracker/dashboard_filter_helper.rb +3 -21
- data/app/helpers/llm_cost_tracker/dashboard_filter_options_helper.rb +4 -4
- data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +1 -1
- data/app/helpers/llm_cost_tracker/token_usage_helper.rb +20 -7
- data/app/models/llm_cost_tracker/call.rb +169 -0
- data/app/models/llm_cost_tracker/call_line_item.rb +22 -0
- data/app/models/llm_cost_tracker/call_rollup.rb +9 -0
- data/app/models/llm_cost_tracker/call_tag.rb +16 -0
- data/app/models/llm_cost_tracker/ingestion/inbox_entry.rb +13 -0
- data/app/models/llm_cost_tracker/ingestion/lease.rb +1 -1
- data/app/models/llm_cost_tracker/provider_invoice.rb +9 -0
- data/app/services/llm_cost_tracker/dashboard/data_quality.rb +125 -34
- data/app/services/llm_cost_tracker/dashboard/date_range.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/filter.rb +2 -2
- data/app/services/llm_cost_tracker/dashboard/overview_stats.rb +74 -21
- data/app/services/llm_cost_tracker/dashboard/pagination.rb +6 -4
- data/app/services/llm_cost_tracker/dashboard/params.rb +8 -2
- data/app/services/llm_cost_tracker/dashboard/provider_breakdown.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +4 -3
- data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +42 -9
- data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +14 -37
- data/app/services/llm_cost_tracker/dashboard/time_series.rb +1 -1
- data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -1
- data/app/views/llm_cost_tracker/calls/index.html.erb +33 -75
- data/app/views/llm_cost_tracker/calls/show.html.erb +62 -7
- data/app/views/llm_cost_tracker/dashboard/index.html.erb +9 -50
- data/app/views/llm_cost_tracker/data_quality/index.html.erb +103 -126
- data/app/views/llm_cost_tracker/errors/database.html.erb +1 -1
- data/app/views/llm_cost_tracker/models/index.html.erb +18 -50
- data/app/views/llm_cost_tracker/shared/_filters.html.erb +63 -0
- data/app/views/llm_cost_tracker/shared/_sort.html.erb +13 -0
- data/app/views/llm_cost_tracker/shared/setup_required.html.erb +1 -1
- data/app/views/llm_cost_tracker/tags/index.html.erb +3 -34
- data/app/views/llm_cost_tracker/tags/show.html.erb +5 -37
- data/lib/llm_cost_tracker/billing/components.rb +53 -0
- data/lib/llm_cost_tracker/billing/components.yml +117 -0
- data/lib/llm_cost_tracker/billing/cost_status.rb +45 -0
- data/lib/llm_cost_tracker/billing/line_item.rb +189 -0
- data/lib/llm_cost_tracker/budget.rb +23 -35
- data/lib/llm_cost_tracker/capture/stream_collector.rb +47 -33
- data/lib/llm_cost_tracker/configuration.rb +36 -19
- data/lib/llm_cost_tracker/doctor/cost_drift_check.rb +54 -0
- data/lib/llm_cost_tracker/doctor/ingestion_check.rb +24 -32
- data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +36 -0
- data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +22 -0
- data/lib/llm_cost_tracker/doctor/price_check.rb +2 -2
- data/lib/llm_cost_tracker/doctor/pricing_snapshot_drift_check.rb +85 -0
- data/lib/llm_cost_tracker/doctor/probe.rb +17 -0
- data/lib/llm_cost_tracker/doctor/schema_check.rb +31 -0
- data/lib/llm_cost_tracker/doctor.rb +43 -45
- data/lib/llm_cost_tracker/errors.rb +5 -19
- data/lib/llm_cost_tracker/event.rb +10 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -2
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/prices_generator.rb +2 -6
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +157 -0
- data/lib/llm_cost_tracker/ingestion/batch.rb +11 -12
- data/lib/llm_cost_tracker/ingestion/inbox.rb +39 -23
- data/lib/llm_cost_tracker/ingestion/worker.rb +14 -5
- data/lib/llm_cost_tracker/ingestion.rb +28 -22
- data/lib/llm_cost_tracker/integrations/anthropic.rb +45 -38
- data/lib/llm_cost_tracker/integrations/base.rb +36 -29
- data/lib/llm_cost_tracker/integrations/openai.rb +85 -40
- data/lib/llm_cost_tracker/integrations/ruby_llm.rb +5 -5
- data/lib/llm_cost_tracker/integrations.rb +2 -2
- data/lib/llm_cost_tracker/ledger/period/totals.rb +12 -9
- data/lib/llm_cost_tracker/ledger/period.rb +5 -5
- data/lib/llm_cost_tracker/ledger/rollups/upsert_sql.rb +4 -10
- data/lib/llm_cost_tracker/ledger/rollups.rb +76 -25
- data/lib/llm_cost_tracker/ledger/schema/adapter.rb +18 -0
- data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +50 -0
- data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +37 -0
- data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +26 -0
- data/lib/llm_cost_tracker/ledger/schema/calls.rb +34 -23
- data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +57 -0
- data/lib/llm_cost_tracker/ledger/store.rb +110 -18
- data/lib/llm_cost_tracker/ledger/tags/query.rb +5 -11
- data/lib/llm_cost_tracker/ledger/tags/sql.rb +27 -14
- data/lib/llm_cost_tracker/ledger.rb +4 -2
- data/lib/llm_cost_tracker/logging.rb +2 -5
- data/lib/llm_cost_tracker/middleware/faraday.rb +7 -6
- data/lib/llm_cost_tracker/parsers/anthropic.rb +52 -7
- data/lib/llm_cost_tracker/parsers/base.rb +8 -3
- data/lib/llm_cost_tracker/parsers/gemini.rb +101 -15
- data/lib/llm_cost_tracker/parsers/openai_compatible.rb +10 -2
- data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +87 -0
- data/lib/llm_cost_tracker/parsers/openai_usage.rb +48 -21
- data/lib/llm_cost_tracker/parsers/sse.rb +1 -1
- data/lib/llm_cost_tracker/parsers.rb +1 -1
- data/lib/llm_cost_tracker/prices.json +105 -20
- data/lib/llm_cost_tracker/pricing/effective_prices.rb +57 -19
- data/lib/llm_cost_tracker/pricing/explainer.rb +4 -5
- data/lib/llm_cost_tracker/pricing/lookup.rb +38 -34
- data/lib/llm_cost_tracker/pricing/registry.rb +65 -45
- data/lib/llm_cost_tracker/pricing/service_charges.rb +204 -0
- data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +26 -17
- data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +6 -15
- data/lib/llm_cost_tracker/pricing/sync.rb +57 -10
- data/lib/llm_cost_tracker/pricing/sync_change_printer.rb +32 -0
- data/lib/llm_cost_tracker/pricing.rb +190 -26
- data/lib/llm_cost_tracker/railtie.rb +0 -8
- data/lib/llm_cost_tracker/report/data.rb +16 -8
- data/lib/llm_cost_tracker/report.rb +0 -4
- data/lib/llm_cost_tracker/retention.rb +8 -8
- data/lib/llm_cost_tracker/tags/context.rb +2 -4
- data/lib/llm_cost_tracker/tags/key.rb +4 -0
- data/lib/llm_cost_tracker/tags/sanitizer.rb +12 -17
- data/lib/llm_cost_tracker/timing.rb +15 -0
- data/lib/llm_cost_tracker/token_usage.rb +56 -42
- data/lib/llm_cost_tracker/tracker.rb +67 -24
- data/lib/llm_cost_tracker/usage_capture.rb +29 -8
- data/lib/llm_cost_tracker/version.rb +1 -1
- data/lib/llm_cost_tracker.rb +36 -35
- data/lib/tasks/llm_cost_tracker.rake +22 -17
- metadata +36 -41
- data/app/models/llm_cost_tracker/ingestion/event.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/call.rb +0 -45
- data/app/models/llm_cost_tracker/ledger/call_metrics.rb +0 -66
- data/app/models/llm_cost_tracker/ledger/period/grouping.rb +0 -71
- data/app/models/llm_cost_tracker/ledger/period/total.rb +0 -13
- data/app/models/llm_cost_tracker/ledger/tags/accessors.rb +0 -19
- data/lib/llm_cost_tracker/configuration/instrumentation.rb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_ingestion_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_latency_ms_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_period_totals_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_provider_response_id_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_streaming_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/add_token_usage_generator.rb +0 -42
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_ingestion_to_llm_cost_tracker.rb.erb +0 -33
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_latency_ms_to_llm_api_calls.rb.erb +0 -9
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_period_totals_to_llm_cost_tracker.rb.erb +0 -104
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_provider_response_id_to_llm_api_calls.rb.erb +0 -15
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_streaming_to_llm_api_calls.rb.erb +0 -21
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/add_token_usage_to_llm_api_calls.rb.erb +0 -22
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_api_calls.rb.erb +0 -83
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_cost_precision.rb.erb +0 -26
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_llm_api_call_tags_to_jsonb.rb.erb +0 -44
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_cost_precision_generator.rb +0 -29
- data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_tags_to_jsonb_generator.rb +0 -29
- data/lib/llm_cost_tracker/ledger/rollups/batch.rb +0 -43
- data/lib/llm_cost_tracker/ledger/schema/period_totals.rb +0 -32
- data/lib/llm_cost_tracker/pricing/components.rb +0 -37
- 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.
|
|
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:
|
|
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:
|
|
220
|
-
|
|
221
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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.
|
|
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.
|
|
408
|
-
signing_key:
|
|
403
|
+
rubygems_version: 3.6.9
|
|
409
404
|
specification_version: 4
|
|
410
|
-
summary:
|
|
405
|
+
summary: LLM API cost tracking for Rails applications
|
|
411
406
|
test_files: []
|
|
@@ -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,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
|