llm_cost_tracker 0.14.2 → 0.15.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/LICENSE.txt +28 -0
  4. data/README.md +33 -41
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +7 -1
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +12 -94
  7. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +11 -12
  8. data/app/controllers/llm_cost_tracker/pricing_controller.rb +15 -4
  9. data/app/controllers/llm_cost_tracker/tags_controller.rb +21 -14
  10. data/app/helpers/llm_cost_tracker/application_helper.rb +22 -31
  11. data/app/helpers/llm_cost_tracker/chart_helper.rb +57 -108
  12. data/app/helpers/llm_cost_tracker/dashboard_query_helper.rb +30 -17
  13. data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +18 -25
  14. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +30 -0
  15. data/app/models/llm_cost_tracker/call.rb +6 -4
  16. data/app/models/llm_cost_tracker/call_rollup.rb +6 -6
  17. data/app/services/llm_cost_tracker/dashboard/call_order.rb +36 -0
  18. data/app/services/llm_cost_tracker/dashboard/calls_export.rb +68 -0
  19. data/app/services/llm_cost_tracker/dashboard/data_quality/stats_query.rb +77 -0
  20. data/app/services/llm_cost_tracker/dashboard/data_quality/summary.rb +47 -0
  21. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +66 -210
  22. data/app/services/llm_cost_tracker/dashboard/filter.rb +2 -1
  23. data/app/services/llm_cost_tracker/dashboard/monthly_budget.rb +42 -35
  24. data/app/services/llm_cost_tracker/dashboard/percent.rb +12 -0
  25. data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
  26. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +19 -17
  27. data/app/services/llm_cost_tracker/dashboard/spend_chart.rb +50 -0
  28. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +7 -8
  29. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -4
  30. data/app/services/llm_cost_tracker/dashboard/top_models.rb +3 -4
  31. data/app/views/layouts/llm_cost_tracker/application.html.erb +6 -6
  32. data/app/views/llm_cost_tracker/calls/index.html.erb +2 -1
  33. data/app/views/llm_cost_tracker/calls/show.html.erb +3 -14
  34. data/app/views/llm_cost_tracker/errors/query.html.erb +6 -0
  35. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -1
  36. data/app/views/llm_cost_tracker/tags/show.html.erb +1 -1
  37. data/lib/llm_cost_tracker/budget/estimate.rb +35 -0
  38. data/lib/llm_cost_tracker/budget/global.rb +69 -0
  39. data/lib/llm_cost_tracker/budget/limit.rb +52 -0
  40. data/lib/llm_cost_tracker/budget/per_tag.rb +59 -27
  41. data/lib/llm_cost_tracker/budget/tagged.rb +74 -0
  42. data/lib/llm_cost_tracker/budget.rb +19 -157
  43. data/lib/llm_cost_tracker/capture/event_window.rb +16 -13
  44. data/lib/llm_cost_tracker/capture/sdk_payload.rb +22 -17
  45. data/lib/llm_cost_tracker/capture/sse.rb +7 -5
  46. data/lib/llm_cost_tracker/capture/stream_collector.rb +89 -96
  47. data/lib/llm_cost_tracker/capture/stream_tap.rb +2 -2
  48. data/lib/llm_cost_tracker/capture/stream_tracker.rb +32 -37
  49. data/lib/llm_cost_tracker/capture_verifier.rb +2 -3
  50. data/lib/llm_cost_tracker/charges/cost_status.rb +23 -19
  51. data/lib/llm_cost_tracker/charges/line_item.rb +31 -33
  52. data/lib/llm_cost_tracker/configuration/budgets.rb +49 -18
  53. data/lib/llm_cost_tracker/configuration/capture.rb +7 -1
  54. data/lib/llm_cost_tracker/configuration/mutability.rb +3 -3
  55. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +20 -2
  56. data/lib/llm_cost_tracker/doctor/price_check.rb +24 -35
  57. data/lib/llm_cost_tracker/doctor/probe.rb +1 -1
  58. data/lib/llm_cost_tracker/doctor.rb +23 -20
  59. data/lib/llm_cost_tracker/engine.rb +8 -0
  60. data/lib/llm_cost_tracker/errors.rb +2 -4
  61. data/lib/llm_cost_tracker/event.rb +22 -29
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +4 -2
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +6 -4
  64. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +1 -0
  65. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +1 -0
  66. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +2 -1
  67. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +1 -0
  68. data/lib/llm_cost_tracker/ingestion/batch.rb +33 -19
  69. data/lib/llm_cost_tracker/ingestion/inbox.rb +20 -11
  70. data/lib/llm_cost_tracker/ingestion/worker.rb +58 -37
  71. data/lib/llm_cost_tracker/ingestion.rb +20 -35
  72. data/lib/llm_cost_tracker/integrations/anthropic/batch_results_capture.rb +40 -0
  73. data/lib/llm_cost_tracker/integrations/anthropic/patches.rb +64 -0
  74. data/lib/llm_cost_tracker/integrations/anthropic.rb +59 -120
  75. data/lib/llm_cost_tracker/integrations/base/patch_target.rb +35 -0
  76. data/lib/llm_cost_tracker/integrations/base.rb +65 -81
  77. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +20 -19
  78. data/lib/llm_cost_tracker/integrations/openai/patches.rb +51 -40
  79. data/lib/llm_cost_tracker/integrations/openai/websocket_capture.rb +89 -0
  80. data/lib/llm_cost_tracker/integrations/openai.rb +114 -83
  81. data/lib/llm_cost_tracker/integrations/ruby_llm/v1/patches.rb +67 -0
  82. data/lib/llm_cost_tracker/integrations/ruby_llm/v1/reply.rb +155 -0
  83. data/lib/llm_cost_tracker/integrations/ruby_llm/v1.rb +154 -0
  84. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt/token_counts.rb +159 -0
  85. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt.rb +175 -0
  86. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/frame.rb +75 -0
  87. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/recorder.rb +70 -0
  88. data/lib/llm_cost_tracker/integrations/ruby_llm/v2/seams.rb +86 -0
  89. data/lib/llm_cost_tracker/integrations/ruby_llm/v2.rb +161 -0
  90. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +7 -408
  91. data/lib/llm_cost_tracker/ledger/isolation.rb +15 -2
  92. data/lib/llm_cost_tracker/ledger/period/totals.rb +1 -1
  93. data/lib/llm_cost_tracker/ledger/rollups.rb +24 -8
  94. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +23 -6
  95. data/lib/llm_cost_tracker/ledger/schema/base.rb +9 -5
  96. data/lib/llm_cost_tracker/ledger/store.rb +17 -35
  97. data/lib/llm_cost_tracker/ledger/tags/breakdown.rb +2 -2
  98. data/lib/llm_cost_tracker/middleware/faraday/body.rb +57 -0
  99. data/lib/llm_cost_tracker/middleware/faraday/exchange.rb +153 -0
  100. data/lib/llm_cost_tracker/middleware/faraday/response_reader.rb +75 -0
  101. data/lib/llm_cost_tracker/middleware/faraday/stream_tee.rb +47 -0
  102. data/lib/llm_cost_tracker/middleware/faraday.rb +2 -301
  103. data/lib/llm_cost_tracker/parsers.rb +12 -0
  104. data/lib/llm_cost_tracker/prices.json +1748 -379
  105. data/lib/llm_cost_tracker/pricing/backfill.rb +59 -29
  106. data/lib/llm_cost_tracker/pricing/calculation/iterations.rb +49 -0
  107. data/lib/llm_cost_tracker/pricing/calculation/quantities.rb +40 -0
  108. data/lib/llm_cost_tracker/pricing/calculation/snapshot.rb +51 -0
  109. data/lib/llm_cost_tracker/pricing/calculation/totals.rb +59 -0
  110. data/lib/llm_cost_tracker/pricing/calculation.rb +82 -173
  111. data/lib/llm_cost_tracker/pricing/matcher.rb +31 -14
  112. data/lib/llm_cost_tracker/pricing/mode.rb +6 -19
  113. data/lib/llm_cost_tracker/pricing/off_peak.rb +50 -0
  114. data/lib/llm_cost_tracker/pricing/rate.rb +1 -0
  115. data/lib/llm_cost_tracker/pricing/registry/normalizer.rb +110 -0
  116. data/lib/llm_cost_tracker/pricing/registry.rb +38 -111
  117. data/lib/llm_cost_tracker/pricing/sync/change_printer.rb +40 -20
  118. data/lib/llm_cost_tracker/pricing/sync/fetcher.rb +35 -34
  119. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +29 -18
  120. data/lib/llm_cost_tracker/pricing/sync/remote_snapshot.rb +78 -0
  121. data/lib/llm_cost_tracker/pricing/sync/snapshot_guard.rb +2 -0
  122. data/lib/llm_cost_tracker/pricing/sync.rb +7 -53
  123. data/lib/llm_cost_tracker/pricing/unknown.rb +1 -1
  124. data/lib/llm_cost_tracker/providers/anthropic/parser.rb +30 -25
  125. data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +5 -3
  126. data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +122 -101
  127. data/lib/llm_cost_tracker/providers/azure/hosts.rb +1 -1
  128. data/lib/llm_cost_tracker/providers/azure/parser.rb +3 -6
  129. data/lib/llm_cost_tracker/providers/gemini/grounding.rb +87 -0
  130. data/lib/llm_cost_tracker/providers/gemini/parser.rb +120 -183
  131. data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +66 -47
  132. data/lib/llm_cost_tracker/providers/openai/hosts.rb +11 -0
  133. data/lib/llm_cost_tracker/providers/openai/model_families.rb +1 -1
  134. data/lib/llm_cost_tracker/providers/openai/output_item.rb +84 -0
  135. data/lib/llm_cost_tracker/providers/openai/parser.rb +2 -1
  136. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +81 -149
  137. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +133 -156
  138. data/lib/llm_cost_tracker/providers/openai/stream_parser.rb +127 -0
  139. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +80 -62
  140. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +15 -4
  141. data/lib/llm_cost_tracker/providers.rb +3 -0
  142. data/lib/llm_cost_tracker/redaction.rb +13 -0
  143. data/lib/llm_cost_tracker/report/data.rb +25 -28
  144. data/lib/llm_cost_tracker/tags/context.rb +22 -7
  145. data/lib/llm_cost_tracker/tracker.rb +32 -26
  146. data/lib/llm_cost_tracker/usage/dimensions.yml +24 -0
  147. data/lib/llm_cost_tracker/usage/token_usage.rb +8 -4
  148. data/lib/llm_cost_tracker/version.rb +1 -1
  149. data/lib/llm_cost_tracker.rb +8 -4
  150. data/lib/tasks/llm_cost_tracker.rake +35 -5
  151. data/llm_cost_tracker.gemspec +7 -6
  152. metadata +66 -7
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmCostTracker
4
- VERSION = "0.14.2"
4
+ VERSION = "0.15.1"
5
5
  end
@@ -128,12 +128,16 @@ module LlmCostTracker
128
128
  pricing_mode: pricing_mode,
129
129
  metadata: tags
130
130
  )
131
+ completed = false
131
132
  yield collector
132
- rescue Exception # rubocop:disable Lint/RescueException -- record the spend and keep the caller's exception
133
- collector&.finish!(errored: true)
134
- raise
135
- else
133
+ completed = true
136
134
  collector.finish!
135
+ rescue Exception # rubocop:disable Lint/RescueException
136
+ collector&.finish!(errored: true) unless completed
137
+ completed = true
138
+ raise
139
+ ensure
140
+ collector&.finish! unless completed
137
141
  end
138
142
  end
139
143
  end
@@ -12,6 +12,34 @@ module LlmCostTrackerTasks
12
12
  FileUtils.mkdir_p(File.dirname(path))
13
13
  path
14
14
  end
15
+
16
+ def self.batch_size(default)
17
+ text = ENV.fetch("BATCH_SIZE", default.to_s)
18
+ size = Integer(text, 10, exception: false)
19
+ return size if size&.positive?
20
+
21
+ abort("llm_cost_tracker: BATCH_SIZE=#{text} is not a positive integer, e.g. BATCH_SIZE=#{default}")
22
+ end
23
+
24
+ def self.backfill_after_refresh(path)
25
+ file = LlmCostTracker.configuration.pricing.file
26
+ return unless file && File.expand_path(file) == File.expand_path(path)
27
+
28
+ if ledger_reachable?
29
+ Rake::Task["llm_cost_tracker:backfill_unknown_pricing"].execute
30
+ else
31
+ puts "llm_cost_tracker: calls ledger not reachable; " \
32
+ "run bin/rails llm_cost_tracker:backfill_unknown_pricing where it is"
33
+ end
34
+ end
35
+
36
+ def self.ledger_reachable?
37
+ return false unless Rake::Task.task_defined?("environment")
38
+
39
+ LlmCostTracker::Call.with_connection { |connection| connection.table_exists?(LlmCostTracker::Call.table_name) }
40
+ rescue ActiveRecord::NoDatabaseError, ActiveRecord::ConnectionNotEstablished
41
+ false
42
+ end
15
43
  end
16
44
 
17
45
  # rubocop:disable-next Metrics/BlockLength
@@ -58,7 +86,7 @@ namespace :llm_cost_tracker do
58
86
  "Use BATCH_SIZE=N to tune."
59
87
  task backfill_unknown_pricing: :environment do
60
88
  require_relative "../llm_cost_tracker/pricing/backfill"
61
- batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Pricing::Backfill::DEFAULT_BATCH_SIZE).to_i
89
+ batch_size = LlmCostTrackerTasks.batch_size(LlmCostTracker::Pricing::Backfill::DEFAULT_BATCH_SIZE)
62
90
  result = LlmCostTracker::Pricing::Backfill.call(batch_size: batch_size)
63
91
  puts "llm_cost_tracker: examined #{result.examined} calls, recomputed #{result.recomputed}, " \
64
92
  "still unknown #{result.still_unknown}"
@@ -73,7 +101,7 @@ namespace :llm_cost_tracker do
73
101
  to_text = ENV["TO"].presence
74
102
  to = to_text && Time.zone.parse(to_text)
75
103
  abort("llm_cost_tracker: TO=#{to_text} is not a date, e.g. TO=2026-10-01") if to_text && !to
76
- batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Pricing::Backfill::DEFAULT_BATCH_SIZE).to_i
104
+ batch_size = LlmCostTrackerTasks.batch_size(LlmCostTracker::Pricing::Backfill::DEFAULT_BATCH_SIZE)
77
105
  scope = LlmCostTracker::Pricing::Backfill.reprice_scope(from...to)
78
106
  result = LlmCostTracker::Pricing::Backfill.call(scope: scope, batch_size: batch_size, reprice: true)
79
107
  puts "llm_cost_tracker: examined #{result.examined} calls, repriced #{result.recomputed}"
@@ -82,7 +110,7 @@ namespace :llm_cost_tracker do
82
110
  desc "Delete llm_cost_tracker_calls and ingestion inbox rows older than DAYS (default: 90). Use BATCH_SIZE=N to tune."
83
111
  task prune: :environment do
84
112
  days = (ENV["DAYS"] || 90).to_i
85
- batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Retention::DEFAULT_BATCH_SIZE).to_i
113
+ batch_size = LlmCostTrackerTasks.batch_size(LlmCostTracker::Retention::DEFAULT_BATCH_SIZE)
86
114
  deleted = LlmCostTracker::Retention.prune(older_than: days, batch_size: batch_size)
87
115
  puts "llm_cost_tracker: pruned #{deleted} calls older than #{days} days"
88
116
  inbox_pruned = LlmCostTracker::Retention.prune_inbox(older_than: days)
@@ -105,14 +133,15 @@ namespace :llm_cost_tracker do
105
133
  abort("llm_cost_tracker: llm_cost_tracker_call_tags is missing the cost columns; " \
106
134
  "run the upgrade_per_tag_budgets generator and migrate first")
107
135
  end
108
- batch_size = (ENV["BATCH_SIZE"] || LlmCostTracker::Budget::PerTag::DEFAULT_BACKFILL_BATCH).to_i
136
+ batch_size = LlmCostTrackerTasks.batch_size(LlmCostTracker::Budget::PerTag::DEFAULT_BACKFILL_BATCH)
109
137
  filled = LlmCostTracker::Budget::PerTag.backfill(batch_size: batch_size)
110
138
  puts "llm_cost_tracker: filled cost and time on #{filled} tag rows"
111
139
  end
112
140
 
113
141
  namespace :prices do
114
142
  desc(
115
- "Refresh the configured pricing file from the maintained LLM Cost Tracker price snapshot. " \
143
+ "Refresh the configured pricing file from the maintained LLM Cost Tracker price snapshot; after writing a new " \
144
+ "config.pricing.file, run llm_cost_tracker:backfill_unknown_pricing if the calls ledger is reachable. " \
116
145
  "Review changes first with llm_cost_tracker:prices:check. Use FORCE=1 to accept suspicious price changes, " \
117
146
  "URL=... to override the source, or OUTPUT=path/to/file.json."
118
147
  )
@@ -130,6 +159,7 @@ namespace :llm_cost_tracker do
130
159
  puts " source: #{result.source_url}"
131
160
  puts " version: #{result.source_version.inspect}" if result.source_version
132
161
  LlmCostTracker::Pricing::Sync::ChangePrinter.call(result.changes, suspicious: result.suspicious)
162
+ LlmCostTrackerTasks.backfill_after_refresh(result.path) if result.written
133
163
  end
134
164
 
135
165
  desc "Compare the current pricing file with the maintained LLM Cost Tracker price snapshot."
@@ -8,11 +8,11 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Sergii Khomenko"]
9
9
  spec.email = ["sergey@mm.st"]
10
10
 
11
- spec.summary = "LLM API cost tracking for Rails applications"
12
- spec.description = "Logs every call your Rails app makes to OpenAI, Anthropic, Gemini, RubyLLM, " \
13
- "or an OpenAI-compatible API: tokens, cost, latency, tags. Calls go straight " \
14
- "to the provider — no proxy. Includes price sync, budget guardrails, and a " \
15
- "mountable dashboard."
11
+ spec.summary = "LLM spend tracking and budgets for Rails, in your own database"
12
+ spec.description = "Records every LLM call your Rails app makes through RubyLLM, the official OpenAI " \
13
+ "and Anthropic SDKs, Faraday, or any OpenAI-compatible API: tokens and cost, attributed " \
14
+ "by user, feature, or any tag. Calls go straight to the provider — no proxy. " \
15
+ "Includes budgets that can block the next call, bundled prices, and a dashboard."
16
16
  spec.homepage = "https://github.com/sergey-homenko/llm_cost_tracker"
17
17
  spec.license = "MIT"
18
18
 
@@ -49,7 +49,8 @@ Gem::Specification.new do |spec|
49
49
  spec.add_dependency "faraday", ">= 2.0", "< 3.0"
50
50
  spec.add_dependency "railties", ">= 8.0", "< 9.0"
51
51
 
52
- spec.add_development_dependency "anthropic", "~> 1.61"
52
+ spec.add_development_dependency "anthropic", ">= 1.61", "< 3"
53
+ spec.add_development_dependency "aws-eventstream", "~> 1.4"
53
54
  spec.add_development_dependency "nokogiri", "~> 1.19"
54
55
  spec.add_development_dependency "openai", "~> 0.80"
55
56
  spec.add_development_dependency "pg", "~> 1.6"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_cost_tracker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.2
4
+ version: 0.15.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergii Khomenko
@@ -107,16 +107,36 @@ dependencies:
107
107
  name: anthropic
108
108
  requirement: !ruby/object:Gem::Requirement
109
109
  requirements:
110
- - - "~>"
110
+ - - ">="
111
111
  - !ruby/object:Gem::Version
112
112
  version: '1.61'
113
+ - - "<"
114
+ - !ruby/object:Gem::Version
115
+ version: '3'
113
116
  type: :development
114
117
  prerelease: false
115
118
  version_requirements: !ruby/object:Gem::Requirement
116
119
  requirements:
117
- - - "~>"
120
+ - - ">="
118
121
  - !ruby/object:Gem::Version
119
122
  version: '1.61'
123
+ - - "<"
124
+ - !ruby/object:Gem::Version
125
+ version: '3'
126
+ - !ruby/object:Gem::Dependency
127
+ name: aws-eventstream
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '1.4'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '1.4'
120
140
  - !ruby/object:Gem::Dependency
121
141
  name: nokogiri
122
142
  requirement: !ruby/object:Gem::Requirement
@@ -263,9 +283,10 @@ dependencies:
263
283
  - - "~>"
264
284
  - !ruby/object:Gem::Version
265
285
  version: '3.0'
266
- description: 'Logs every call your Rails app makes to OpenAI, Anthropic, Gemini, RubyLLM,
267
- or an OpenAI-compatible API: tokens, cost, latency, tags. Calls go straight to the
268
- provider — no proxy. Includes price sync, budget guardrails, and a mountable dashboard.'
286
+ description: 'Records every LLM call your Rails app makes through RubyLLM, the official
287
+ OpenAI and Anthropic SDKs, Faraday, or any OpenAI-compatible API: tokens and cost,
288
+ attributed by user, feature, or any tag. Calls go straight to the provider — no
289
+ proxy. Includes budgets that can block the next call, bundled prices, and a dashboard.'
269
290
  email:
270
291
  - sergey@mm.st
271
292
  executables: []
@@ -303,7 +324,11 @@ files:
303
324
  - app/models/llm_cost_tracker/call_tag.rb
304
325
  - app/models/llm_cost_tracker/ingestion/inbox_entry.rb
305
326
  - app/models/llm_cost_tracker/ingestion/lease.rb
327
+ - app/services/llm_cost_tracker/dashboard/call_order.rb
328
+ - app/services/llm_cost_tracker/dashboard/calls_export.rb
306
329
  - app/services/llm_cost_tracker/dashboard/data_quality.rb
330
+ - app/services/llm_cost_tracker/dashboard/data_quality/stats_query.rb
331
+ - app/services/llm_cost_tracker/dashboard/data_quality/summary.rb
307
332
  - app/services/llm_cost_tracker/dashboard/date_range.rb
308
333
  - app/services/llm_cost_tracker/dashboard/filter.rb
309
334
  - app/services/llm_cost_tracker/dashboard/masking.rb
@@ -311,11 +336,13 @@ files:
311
336
  - app/services/llm_cost_tracker/dashboard/overview_stats.rb
312
337
  - app/services/llm_cost_tracker/dashboard/pagination.rb
313
338
  - app/services/llm_cost_tracker/dashboard/params.rb
339
+ - app/services/llm_cost_tracker/dashboard/percent.rb
314
340
  - app/services/llm_cost_tracker/dashboard/pricing_overview.rb
315
341
  - app/services/llm_cost_tracker/dashboard/provider_breakdown.rb
316
342
  - app/services/llm_cost_tracker/dashboard/setup_state.rb
317
343
  - app/services/llm_cost_tracker/dashboard/sort.rb
318
344
  - app/services/llm_cost_tracker/dashboard/spend_anomaly.rb
345
+ - app/services/llm_cost_tracker/dashboard/spend_chart.rb
319
346
  - app/services/llm_cost_tracker/dashboard/tag_breakdown.rb
320
347
  - app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb
321
348
  - app/services/llm_cost_tracker/dashboard/time_series.rb
@@ -328,6 +355,7 @@ files:
328
355
  - app/views/llm_cost_tracker/errors/database.html.erb
329
356
  - app/views/llm_cost_tracker/errors/invalid_filter.html.erb
330
357
  - app/views/llm_cost_tracker/errors/not_found.html.erb
358
+ - app/views/llm_cost_tracker/errors/query.html.erb
331
359
  - app/views/llm_cost_tracker/models/index.html.erb
332
360
  - app/views/llm_cost_tracker/pricing/index.html.erb
333
361
  - app/views/llm_cost_tracker/shared/_bar.html.erb
@@ -345,7 +373,11 @@ files:
345
373
  - lib/llm_cost_tracker.rb
346
374
  - lib/llm_cost_tracker/assets.rb
347
375
  - lib/llm_cost_tracker/budget.rb
376
+ - lib/llm_cost_tracker/budget/estimate.rb
377
+ - lib/llm_cost_tracker/budget/global.rb
378
+ - lib/llm_cost_tracker/budget/limit.rb
348
379
  - lib/llm_cost_tracker/budget/per_tag.rb
380
+ - lib/llm_cost_tracker/budget/tagged.rb
349
381
  - lib/llm_cost_tracker/capture/event_window.rb
350
382
  - lib/llm_cost_tracker/capture/sdk_payload.rb
351
383
  - lib/llm_cost_tracker/capture/sse.rb
@@ -401,11 +433,24 @@ files:
401
433
  - lib/llm_cost_tracker/ingestion/worker.rb
402
434
  - lib/llm_cost_tracker/integrations.rb
403
435
  - lib/llm_cost_tracker/integrations/anthropic.rb
436
+ - lib/llm_cost_tracker/integrations/anthropic/batch_results_capture.rb
437
+ - lib/llm_cost_tracker/integrations/anthropic/patches.rb
404
438
  - lib/llm_cost_tracker/integrations/base.rb
439
+ - lib/llm_cost_tracker/integrations/base/patch_target.rb
405
440
  - lib/llm_cost_tracker/integrations/openai.rb
406
441
  - lib/llm_cost_tracker/integrations/openai/batch_capture.rb
407
442
  - lib/llm_cost_tracker/integrations/openai/patches.rb
443
+ - lib/llm_cost_tracker/integrations/openai/websocket_capture.rb
408
444
  - lib/llm_cost_tracker/integrations/ruby_llm.rb
445
+ - lib/llm_cost_tracker/integrations/ruby_llm/v1.rb
446
+ - lib/llm_cost_tracker/integrations/ruby_llm/v1/patches.rb
447
+ - lib/llm_cost_tracker/integrations/ruby_llm/v1/reply.rb
448
+ - lib/llm_cost_tracker/integrations/ruby_llm/v2.rb
449
+ - lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt.rb
450
+ - lib/llm_cost_tracker/integrations/ruby_llm/v2/attempt/token_counts.rb
451
+ - lib/llm_cost_tracker/integrations/ruby_llm/v2/frame.rb
452
+ - lib/llm_cost_tracker/integrations/ruby_llm/v2/recorder.rb
453
+ - lib/llm_cost_tracker/integrations/ruby_llm/v2/seams.rb
409
454
  - lib/llm_cost_tracker/ledger.rb
410
455
  - lib/llm_cost_tracker/ledger/isolation.rb
411
456
  - lib/llm_cost_tracker/ledger/period.rb
@@ -427,18 +472,28 @@ files:
427
472
  - lib/llm_cost_tracker/ledger/tags/query.rb
428
473
  - lib/llm_cost_tracker/logging.rb
429
474
  - lib/llm_cost_tracker/middleware/faraday.rb
475
+ - lib/llm_cost_tracker/middleware/faraday/body.rb
476
+ - lib/llm_cost_tracker/middleware/faraday/exchange.rb
477
+ - lib/llm_cost_tracker/middleware/faraday/response_reader.rb
478
+ - lib/llm_cost_tracker/middleware/faraday/stream_tee.rb
430
479
  - lib/llm_cost_tracker/parsers.rb
431
480
  - lib/llm_cost_tracker/prices.json
432
481
  - lib/llm_cost_tracker/pricing.rb
433
482
  - lib/llm_cost_tracker/pricing/backfill.rb
434
483
  - lib/llm_cost_tracker/pricing/calculation.rb
484
+ - lib/llm_cost_tracker/pricing/calculation/iterations.rb
485
+ - lib/llm_cost_tracker/pricing/calculation/quantities.rb
486
+ - lib/llm_cost_tracker/pricing/calculation/snapshot.rb
487
+ - lib/llm_cost_tracker/pricing/calculation/totals.rb
435
488
  - lib/llm_cost_tracker/pricing/effective_prices.rb
436
489
  - lib/llm_cost_tracker/pricing/estimator.rb
437
490
  - lib/llm_cost_tracker/pricing/matcher.rb
438
491
  - lib/llm_cost_tracker/pricing/mode.rb
492
+ - lib/llm_cost_tracker/pricing/off_peak.rb
439
493
  - lib/llm_cost_tracker/pricing/price_key.rb
440
494
  - lib/llm_cost_tracker/pricing/rate.rb
441
495
  - lib/llm_cost_tracker/pricing/registry.rb
496
+ - lib/llm_cost_tracker/pricing/registry/normalizer.rb
442
497
  - lib/llm_cost_tracker/pricing/service_rates.rb
443
498
  - lib/llm_cost_tracker/pricing/source.rb
444
499
  - lib/llm_cost_tracker/pricing/sync.rb
@@ -446,6 +501,7 @@ files:
446
501
  - lib/llm_cost_tracker/pricing/sync/fetcher.rb
447
502
  - lib/llm_cost_tracker/pricing/sync/registry_diff.rb
448
503
  - lib/llm_cost_tracker/pricing/sync/registry_writer.rb
504
+ - lib/llm_cost_tracker/pricing/sync/remote_snapshot.rb
449
505
  - lib/llm_cost_tracker/pricing/sync/snapshot_guard.rb
450
506
  - lib/llm_cost_tracker/pricing/unknown.rb
451
507
  - lib/llm_cost_tracker/providers.rb
@@ -454,14 +510,17 @@ files:
454
510
  - lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb
455
511
  - lib/llm_cost_tracker/providers/azure/hosts.rb
456
512
  - lib/llm_cost_tracker/providers/azure/parser.rb
513
+ - lib/llm_cost_tracker/providers/gemini/grounding.rb
457
514
  - lib/llm_cost_tracker/providers/gemini/model_families.rb
458
515
  - lib/llm_cost_tracker/providers/gemini/parser.rb
459
516
  - lib/llm_cost_tracker/providers/gemini/usage_extractor.rb
460
517
  - lib/llm_cost_tracker/providers/openai/hosts.rb
461
518
  - lib/llm_cost_tracker/providers/openai/model_families.rb
519
+ - lib/llm_cost_tracker/providers/openai/output_item.rb
462
520
  - lib/llm_cost_tracker/providers/openai/parser.rb
463
521
  - lib/llm_cost_tracker/providers/openai/response_parser.rb
464
522
  - lib/llm_cost_tracker/providers/openai/service_charges.rb
523
+ - lib/llm_cost_tracker/providers/openai/stream_parser.rb
465
524
  - lib/llm_cost_tracker/providers/openai/usage_extractor.rb
466
525
  - lib/llm_cost_tracker/providers/openai_compatible/parser.rb
467
526
  - lib/llm_cost_tracker/railtie.rb
@@ -508,5 +567,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
508
567
  requirements: []
509
568
  rubygems_version: 3.6.9
510
569
  specification_version: 4
511
- summary: LLM API cost tracking for Rails applications
570
+ summary: LLM spend tracking and budgets for Rails, in your own database
512
571
  test_files: []