llm_cost_tracker 0.9.0 → 0.11.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 (145) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/README.md +6 -2
  4. data/app/assets/llm_cost_tracker/application.css +782 -801
  5. data/app/controllers/llm_cost_tracker/application_controller.rb +15 -3
  6. data/app/controllers/llm_cost_tracker/calls_controller.rb +39 -20
  7. data/app/controllers/llm_cost_tracker/dashboard_controller.rb +0 -3
  8. data/app/controllers/llm_cost_tracker/models_controller.rb +3 -1
  9. data/app/controllers/llm_cost_tracker/pricing_controller.rb +16 -0
  10. data/app/controllers/llm_cost_tracker/reconciliation_controller.rb +13 -19
  11. data/app/controllers/llm_cost_tracker/tags_controller.rb +3 -1
  12. data/app/helpers/llm_cost_tracker/application_helper.rb +16 -4
  13. data/app/helpers/llm_cost_tracker/chart_helper.rb +22 -6
  14. data/app/helpers/llm_cost_tracker/sortable_table_helper.rb +41 -0
  15. data/app/models/llm_cost_tracker/provider_invoice_import.rb +9 -4
  16. data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +95 -0
  17. data/app/services/llm_cost_tracker/dashboard/setup_state.rb +104 -0
  18. data/app/services/llm_cost_tracker/dashboard/sort.rb +9 -0
  19. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +19 -5
  20. data/app/services/llm_cost_tracker/dashboard/top_models.rb +34 -19
  21. data/app/views/layouts/llm_cost_tracker/application.html.erb +80 -17
  22. data/app/views/llm_cost_tracker/calls/index.html.erb +69 -90
  23. data/app/views/llm_cost_tracker/calls/show.html.erb +119 -120
  24. data/app/views/llm_cost_tracker/dashboard/index.html.erb +119 -158
  25. data/app/views/llm_cost_tracker/data_quality/index.html.erb +109 -108
  26. data/app/views/llm_cost_tracker/errors/database.html.erb +2 -2
  27. data/app/views/llm_cost_tracker/models/index.html.erb +39 -59
  28. data/app/views/llm_cost_tracker/pricing/index.html.erb +93 -0
  29. data/app/views/llm_cost_tracker/reconciliation/index.html.erb +49 -58
  30. data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +19 -0
  31. data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +22 -0
  32. data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +22 -0
  33. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +23 -0
  34. data/app/views/llm_cost_tracker/shared/_spend_chart.html.erb +3 -13
  35. data/app/views/llm_cost_tracker/shared/_tag_chips.html.erb +1 -1
  36. data/app/views/llm_cost_tracker/shared/setup_required.html.erb +16 -15
  37. data/app/views/llm_cost_tracker/tags/index.html.erb +27 -32
  38. data/app/views/llm_cost_tracker/tags/show.html.erb +83 -102
  39. data/config/routes.rb +1 -0
  40. data/lib/llm_cost_tracker/billing/cost_status.rb +21 -25
  41. data/lib/llm_cost_tracker/billing/line_item.rb +15 -49
  42. data/lib/llm_cost_tracker/budget.rb +29 -8
  43. data/lib/llm_cost_tracker/{parsers → capture}/sse.rb +1 -1
  44. data/lib/llm_cost_tracker/capture/stream_collector.rb +34 -42
  45. data/lib/llm_cost_tracker/capture/stream_tracker.rb +2 -6
  46. data/lib/llm_cost_tracker/configuration.rb +30 -44
  47. data/lib/llm_cost_tracker/doctor/capture_verifier.rb +1 -1
  48. data/lib/llm_cost_tracker/doctor/ingestion_check.rb +8 -8
  49. data/lib/llm_cost_tracker/doctor/legacy_audit_check.rb +0 -2
  50. data/lib/llm_cost_tracker/doctor/legacy_billing_status_check.rb +0 -2
  51. data/lib/llm_cost_tracker/doctor.rb +80 -25
  52. data/lib/llm_cost_tracker/engine.rb +1 -2
  53. data/lib/llm_cost_tracker/errors.rb +3 -2
  54. data/lib/llm_cost_tracker/event.rb +47 -0
  55. data/lib/llm_cost_tracker/generators/llm_cost_tracker/{durable_ingestion_generator.rb → async_ingestion_generator.rb} +8 -8
  56. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +4 -23
  57. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/{create_llm_cost_tracker_durable_ingestion.rb.erb → create_llm_cost_tracker_async_ingestion.rb.erb} +3 -3
  58. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_reconciliation.rb.erb +6 -1
  59. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +14 -7
  60. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +27 -8
  61. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +5 -5
  62. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoice_imports_provider.rb.erb +36 -0
  63. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_provider_invoices_metadata_index.rb.erb +27 -0
  64. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -9
  65. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoice_imports_provider_generator.rb +31 -0
  66. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_provider_invoices_metadata_index_generator.rb +31 -0
  67. data/lib/llm_cost_tracker/ingestion/batch.rb +5 -2
  68. data/lib/llm_cost_tracker/ingestion/inbox.rb +4 -25
  69. data/lib/llm_cost_tracker/ingestion/pool.rb +44 -0
  70. data/lib/llm_cost_tracker/ingestion/worker.rb +22 -36
  71. data/lib/llm_cost_tracker/ingestion.rb +8 -9
  72. data/lib/llm_cost_tracker/integrations/anthropic.rb +46 -68
  73. data/lib/llm_cost_tracker/integrations/base.rb +14 -11
  74. data/lib/llm_cost_tracker/integrations/openai.rb +104 -131
  75. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +27 -73
  76. data/lib/llm_cost_tracker/integrations.rb +14 -13
  77. data/lib/llm_cost_tracker/ledger/period/totals.rb +5 -3
  78. data/lib/llm_cost_tracker/ledger/rollups.rb +4 -13
  79. data/lib/llm_cost_tracker/ledger/schema/call_line_items.rb +11 -0
  80. data/lib/llm_cost_tracker/ledger/schema/call_rollups.rb +13 -3
  81. data/lib/llm_cost_tracker/ledger/schema/call_tags.rb +11 -0
  82. data/lib/llm_cost_tracker/ledger/schema/calls.rb +0 -4
  83. data/lib/llm_cost_tracker/ledger/schema/ingestion_inbox_entries.rb +13 -3
  84. data/lib/llm_cost_tracker/ledger/schema/ingestion_leases.rb +13 -3
  85. data/lib/llm_cost_tracker/ledger/schema/provider_invoice_imports.rb +19 -9
  86. data/lib/llm_cost_tracker/ledger/schema/provider_invoices.rb +26 -11
  87. data/lib/llm_cost_tracker/ledger/store.rb +21 -18
  88. data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -1
  89. data/lib/llm_cost_tracker/ledger.rb +13 -0
  90. data/lib/llm_cost_tracker/logging.rb +0 -4
  91. data/lib/llm_cost_tracker/middleware/faraday.rb +46 -17
  92. data/lib/llm_cost_tracker/parsers/anthropic.rb +35 -59
  93. data/lib/llm_cost_tracker/parsers/azure.rb +46 -0
  94. data/lib/llm_cost_tracker/parsers/base.rb +53 -47
  95. data/lib/llm_cost_tracker/parsers/gemini.rb +23 -27
  96. data/lib/llm_cost_tracker/parsers/openai.rb +8 -40
  97. data/lib/llm_cost_tracker/parsers/openai_compatible.rb +26 -49
  98. data/lib/llm_cost_tracker/parsers/openai_usage.rb +19 -23
  99. data/lib/llm_cost_tracker/parsers.rb +29 -4
  100. data/lib/llm_cost_tracker/prices.json +567 -579
  101. data/lib/llm_cost_tracker/pricing/backfill.rb +140 -0
  102. data/lib/llm_cost_tracker/pricing/effective_prices.rb +2 -4
  103. data/lib/llm_cost_tracker/pricing/estimator.rb +33 -0
  104. data/lib/llm_cost_tracker/pricing/explainer.rb +5 -2
  105. data/lib/llm_cost_tracker/pricing/lookup.rb +37 -2
  106. data/lib/llm_cost_tracker/pricing/mode.rb +34 -4
  107. data/lib/llm_cost_tracker/pricing/registry.rb +0 -7
  108. data/lib/llm_cost_tracker/pricing/service_charges.rb +6 -10
  109. data/lib/llm_cost_tracker/pricing/{sync_change_printer.rb → sync/change_printer.rb} +3 -3
  110. data/lib/llm_cost_tracker/pricing/sync/registry_writer.rb +14 -2
  111. data/lib/llm_cost_tracker/pricing/sync.rb +1 -9
  112. data/lib/llm_cost_tracker/pricing/unknown.rb +5 -2
  113. data/lib/llm_cost_tracker/pricing.rb +71 -43
  114. data/lib/llm_cost_tracker/providers/anthropic/server_tools.rb +15 -0
  115. data/lib/llm_cost_tracker/providers/anthropic/tier_classification.rb +22 -0
  116. data/lib/llm_cost_tracker/providers/azure/hosts.rb +17 -0
  117. data/lib/llm_cost_tracker/providers/gemini/model_families.rb +17 -0
  118. data/lib/llm_cost_tracker/providers/openai/hosts.rb +35 -0
  119. data/lib/llm_cost_tracker/providers/openai/model_families.rb +51 -0
  120. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +157 -0
  121. data/lib/llm_cost_tracker/railtie.rb +3 -5
  122. data/lib/llm_cost_tracker/reconcile_tasks.rb +18 -21
  123. data/lib/llm_cost_tracker/reconciliation/diff.rb +26 -45
  124. data/lib/llm_cost_tracker/reconciliation/diff_result.rb +0 -4
  125. data/lib/llm_cost_tracker/reconciliation/importer.rb +3 -7
  126. data/lib/llm_cost_tracker/reconciliation/sources/anthropic_usage.rb +10 -33
  127. data/lib/llm_cost_tracker/reconciliation/sources/coercion.rb +40 -0
  128. data/lib/llm_cost_tracker/reconciliation/sources/openai_usage.rb +7 -31
  129. data/lib/llm_cost_tracker/report/formatter.rb +32 -19
  130. data/lib/llm_cost_tracker/report.rb +0 -4
  131. data/lib/llm_cost_tracker/retention.rb +20 -8
  132. data/lib/llm_cost_tracker/tags/sanitizer.rb +13 -17
  133. data/lib/llm_cost_tracker/token_usage.rb +4 -0
  134. data/lib/llm_cost_tracker/tracker.rb +33 -74
  135. data/lib/llm_cost_tracker/version.rb +1 -1
  136. data/lib/llm_cost_tracker.rb +11 -15
  137. data/lib/tasks/llm_cost_tracker.rake +16 -2
  138. metadata +31 -12
  139. data/app/views/llm_cost_tracker/shared/_active_filters.html.erb +0 -16
  140. data/app/views/llm_cost_tracker/shared/_filters.html.erb +0 -66
  141. data/app/views/llm_cost_tracker/shared/_sort.html.erb +0 -13
  142. data/lib/llm_cost_tracker/dashboard_setup_state.rb +0 -109
  143. data/lib/llm_cost_tracker/ingestion/inline.rb +0 -22
  144. data/lib/llm_cost_tracker/parsers/openai_service_charges.rb +0 -126
  145. data/lib/llm_cost_tracker/usage_capture.rb +0 -58
@@ -16,29 +16,31 @@ module LlmCostTracker
16
16
  MAX_IDLE_INTERVAL_SECONDS = 5.0
17
17
  LEASE_SECONDS = 10
18
18
  FLUSH_TIMEOUT_SECONDS = 10
19
+ MUTEX = Mutex.new
20
+
19
21
  class << self
20
22
  def ensure_started
21
- return unless Ingestion.durable?
23
+ return unless Ingestion.async?
22
24
 
23
- thread = mutex.synchronize do
25
+ thread = MUTEX.synchronize do
24
26
  reset_after_fork!
25
- unless @thread&.alive?
26
- @stop_requested = false
27
- @generation = @generation.to_i + 1
28
- generation = @generation
29
- @thread = Thread.new { run(generation) }
30
- @thread.name = "llm_cost_tracker_ingestor"
31
- @thread.report_on_exception = false
32
- end
27
+ break @thread if @stop_requested || @thread&.alive?
28
+
29
+ @generation = @generation.to_i + 1
30
+ generation = @generation
31
+ @thread = Thread.new { run(generation) }
32
+ @thread.name = "llm_cost_tracker_ingestor"
33
+ @thread.report_on_exception = false
33
34
  @thread
34
35
  end
35
36
  wake_thread(thread)
36
37
  end
37
38
 
38
39
  def flush!(timeout: nil, require_lease: false)
39
- return true unless Ingestion.durable?
40
+ return true unless Ingestion.async?
40
41
 
41
42
  Ingestion.ensure_current_schema!
43
+ MUTEX.synchronize { reset_after_fork! }
42
44
 
43
45
  deadline = Time.now.utc + flush_timeout_seconds(timeout)
44
46
  loop do
@@ -56,10 +58,10 @@ module LlmCostTracker
56
58
  end
57
59
 
58
60
  def shutdown!(timeout: nil, drain: true)
59
- return true unless Ingestion.durable?
61
+ return true unless Ingestion.async?
60
62
 
61
63
  timeout ||= FLUSH_TIMEOUT_SECONDS
62
- thread = mutex.synchronize do
64
+ thread = MUTEX.synchronize do
63
65
  @stop_requested = true
64
66
  @generation = @generation.to_i + 1
65
67
  @thread
@@ -71,14 +73,14 @@ module LlmCostTracker
71
73
  handle_error(e)
72
74
  false
73
75
  ensure
74
- mutex.synchronize do
76
+ MUTEX.synchronize do
75
77
  @thread = nil if @thread.equal?(thread) && !thread&.alive?
76
78
  end
77
79
  end
78
80
 
79
81
  def reset!
80
- thread = mutex.synchronize do
81
- @stop_requested = true
82
+ thread = MUTEX.synchronize do
83
+ @stop_requested = false
82
84
  @generation = @generation.to_i + 1
83
85
  thread = @thread
84
86
  @thread = nil
@@ -98,6 +100,7 @@ module LlmCostTracker
98
100
 
99
101
  def ingest_once(require_lease: true)
100
102
  Ingestion.ensure_current_schema!
103
+ MUTEX.synchronize { reset_after_fork! }
101
104
  batch = Ingestion::Batch.new(identity: identity)
102
105
  return 0 unless batch.claimable?
103
106
  return 0 if require_lease && !Ingestion::LeaseClaim.new(identity: identity, seconds: LEASE_SECONDS).acquire
@@ -110,16 +113,12 @@ module LlmCostTracker
110
113
 
111
114
  private
112
115
 
113
- def mutex
114
- @mutex ||= Mutex.new
115
- end
116
-
117
116
  def run(generation)
118
117
  idle_interval = IDLE_INTERVAL_SECONDS
119
118
  loop do
120
- break if mutex.synchronize { @stop_requested || generation != @generation }
119
+ break if MUTEX.synchronize { @stop_requested || generation != @generation }
121
120
 
122
- processed = executor_wrap { ingest_once }
121
+ processed = Rails.application.executor.wrap { ingest_once }
123
122
  release_connection!
124
123
  if processed.zero?
125
124
  sleep(idle_interval)
@@ -134,7 +133,7 @@ module LlmCostTracker
134
133
  end
135
134
  ensure
136
135
  release_connection!
137
- mutex.synchronize { @thread = nil if @thread.equal?(Thread.current) }
136
+ MUTEX.synchronize { @thread = nil if @thread.equal?(Thread.current) }
138
137
  end
139
138
 
140
139
  def reset_after_fork!
@@ -151,19 +150,6 @@ module LlmCostTracker
151
150
  nil
152
151
  end
153
152
 
154
- def executor_wrap(&)
155
- executor = rails_executor
156
- return yield unless executor
157
-
158
- executor.wrap(&)
159
- end
160
-
161
- def rails_executor
162
- Rails.application.try(:executor)
163
- rescue StandardError
164
- nil
165
- end
166
-
167
153
  def identity
168
154
  @identity ||= "pid-#{Process.pid}-#{SecureRandom.hex(6)}"
169
155
  end
@@ -2,11 +2,10 @@
2
2
 
3
3
  require "securerandom"
4
4
 
5
- require_relative "doctor/check"
6
5
  require_relative "errors"
7
6
  require_relative "ledger"
8
- require_relative "ingestion/inline"
9
7
  require_relative "ingestion/lease_claim"
8
+ require_relative "ingestion/pool"
10
9
  require_relative "ingestion/inbox"
11
10
  require_relative "ingestion/batch"
12
11
  require_relative "ingestion/worker"
@@ -28,7 +27,7 @@ module LlmCostTracker
28
27
 
29
28
  ROLLUPS_SCHEMA_GUARD = ["llm_cost_tracker_call_rollups", Ledger::Schema::CallRollups].freeze
30
29
 
31
- DURABLE_SCHEMA_GUARDS = [
30
+ ASYNC_SCHEMA_GUARDS = [
32
31
  ["llm_cost_tracker_ingestion_inbox_entries", Ledger::Schema::IngestionInboxEntries],
33
32
  ["llm_cost_tracker_ingestion_leases", Ledger::Schema::IngestionLeases]
34
33
  ].freeze
@@ -47,8 +46,8 @@ module LlmCostTracker
47
46
  end
48
47
  end
49
48
 
50
- def durable?
51
- LlmCostTracker.configuration.durable_ingestion
49
+ def async?
50
+ LlmCostTracker.configuration.ingestion == :async
52
51
  end
53
52
 
54
53
  def cache_rollups?
@@ -58,7 +57,7 @@ module LlmCostTracker
58
57
  def guards_for_current_config
59
58
  guards = CORE_SCHEMA_GUARDS.dup
60
59
  guards << ROLLUPS_SCHEMA_GUARD if cache_rollups?
61
- guards += DURABLE_SCHEMA_GUARDS if durable?
60
+ guards += ASYNC_SCHEMA_GUARDS if async?
62
61
  guards
63
62
  end
64
63
 
@@ -93,7 +92,7 @@ module LlmCostTracker
93
92
  provider_response_id: response_id,
94
93
  tags: { feature: VERIFY_TAG }
95
94
  )
96
- LlmCostTracker::Ingestion::Worker.flush! if durable?
95
+ LlmCostTracker::Ingestion::Worker.flush! if async?
97
96
  persisted = LlmCostTracker::Call.where(provider_response_id: response_id).exists?
98
97
 
99
98
  return capture_success if persisted && notifications.any?
@@ -122,7 +121,7 @@ module LlmCostTracker
122
121
  end
123
122
 
124
123
  def capture_success
125
- path = durable? ? "durable inbox" : "inline writer"
124
+ path = async? ? "async inbox" : "inline writer"
126
125
  LlmCostTracker::Doctor::Check.new(
127
126
  :ok,
128
127
  "active_record capture",
@@ -149,7 +148,7 @@ module LlmCostTracker
149
148
  end
150
149
 
151
150
  def cleanup_verification_inbox(event:, response_id:)
152
- return unless durable? && LlmCostTracker::Ingestion::InboxEntry.table_exists?
151
+ return unless async? && LlmCostTracker::Ingestion::InboxEntry.table_exists?
153
152
 
154
153
  if event
155
154
  LlmCostTracker::Ingestion::InboxEntry.where(event_id: event.event_id).delete_all
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative "base"
4
4
  require_relative "../billing/line_item"
5
+ require_relative "../providers/anthropic/server_tools"
6
+ require_relative "../providers/anthropic/tier_classification"
5
7
 
6
8
  module LlmCostTracker
7
9
  module Integrations
@@ -45,10 +47,10 @@ module LlmCostTracker
45
47
  next if input_tokens.nil? && output_tokens.nil?
46
48
 
47
49
  LlmCostTracker::Tracker.record(
48
- capture: UsageCapture.build(
50
+ event: Event.build(
49
51
  provider: "anthropic",
50
52
  model: object_value(message, :model) || request[:model],
51
- pricing_mode: pricing_mode(message: message, request: request, usage: usage),
53
+ pricing_mode: pricing_mode(request: request, usage: usage),
52
54
  token_usage: token_usage(usage: usage, input_tokens: input_tokens, output_tokens: output_tokens),
53
55
  usage_source: :sdk_response,
54
56
  provider_response_id: object_value(message, :id),
@@ -63,35 +65,18 @@ module LlmCostTracker
63
65
  server_tool_use = object_value(usage, :server_tool_use)
64
66
  return [] unless server_tool_use
65
67
 
66
- [
67
- line_item_for_server_tool(server_tool_use, :web_search_request, :web_search_requests,
68
- "usage.server_tool_use.web_search_requests"),
69
- line_item_for_server_tool(server_tool_use, :web_fetch_request, :web_fetch_requests,
70
- "usage.server_tool_use.web_fetch_requests"),
71
- line_item_for_server_tool(server_tool_use, :code_execution_request, :code_execution_requests,
72
- "usage.server_tool_use.code_execution_requests")
73
- ].compact
74
- end
75
-
76
- def line_item_for_server_tool(server_tool_use, component_key, count_key, provider_field)
77
- quantity = server_tool_count(server_tool_use, count_key)
78
- return nil if quantity.zero?
79
-
80
- Billing::LineItem.build(
81
- component_key: component_key,
82
- quantity: quantity,
83
- cost_status: Billing::CostStatus::UNKNOWN,
84
- pricing_basis: :provider_usage,
85
- provider_field: provider_field
86
- )
87
- end
88
-
89
- def server_tool_count(server_tool_use, count_key)
90
- direct = object_value(server_tool_use, count_key).to_i
91
- return direct if direct.positive?
92
- return 0 unless server_tool_use.respond_to?(:to_h)
68
+ Providers::Anthropic::ServerTools::LINE_ITEMS.filter_map do |component_key, count_key|
69
+ quantity = object_value(server_tool_use, count_key).to_i
70
+ next if quantity.zero?
93
71
 
94
- server_tool_use.to_h[count_key].to_i
72
+ Billing::LineItem.build(
73
+ component_key: component_key,
74
+ quantity: quantity,
75
+ cost_status: Billing::CostStatus::UNKNOWN,
76
+ pricing_basis: :provider_usage,
77
+ provider_field: "usage.server_tool_use.#{count_key}"
78
+ )
79
+ end
95
80
  end
96
81
 
97
82
  def token_usage(usage:, input_tokens:, output_tokens:)
@@ -118,65 +103,58 @@ module LlmCostTracker
118
103
  )
119
104
  end
120
105
 
121
- DATA_RESIDENCY_GEOS = %w[us].freeze
122
- # Anthropic Priority Tier is committed throughput (tokens/min capacity), not a per-token
123
- # surcharge. Treat it as standard pricing so cost_status doesn't fall to :unknown.
124
- STANDARD_EQUIVALENT_SERVICE_TIERS = %w[standard standard_only priority].freeze
125
-
126
- def pricing_mode(message:, request:, usage:)
127
- service_tier = object_value(usage, :service_tier) ||
128
- object_value(message, :service_tier) ||
129
- request[:service_tier]
130
- service_tier = nil if STANDARD_EQUIVALENT_SERVICE_TIERS.include?(service_tier.to_s)
106
+ def pricing_mode(request:, usage:)
107
+ service_tier = object_value(usage, :service_tier) || request[:service_tier]
108
+ tier = Providers::Anthropic::TierClassification
109
+ service_tier = nil if tier.standard_equivalent_tier?(service_tier)
131
110
 
132
111
  modes = [
133
- Pricing.normalize_mode(object_value(usage, :speed) || object_value(message, :speed) || request[:speed]),
134
- Pricing.normalize_mode(service_tier)
112
+ Pricing::Mode.normalize(object_value(usage, :speed) || request[:speed]),
113
+ Pricing::Mode.normalize(service_tier)
135
114
  ]
136
- geo = inference_geo(message: message, request: request, usage: usage).to_s.downcase
137
- modes << "data_residency" if DATA_RESIDENCY_GEOS.include?(geo)
115
+ geo = inference_geo(request: request, usage: usage).to_s.downcase
116
+ modes << "data_residency" if tier.data_residency_geo?(geo)
138
117
  modes = modes.compact.uniq
139
118
  modes.empty? ? nil : modes.join("_")
140
119
  end
141
120
 
142
121
  def stream_pricing_mode(request)
143
- pricing_mode(message: nil, request: request || {}, usage: nil)
122
+ pricing_mode(request: request || {}, usage: nil)
144
123
  end
145
124
 
146
- def inference_geo(message:, request:, usage:)
147
- object_value(usage, :inference_geo) ||
148
- object_value(message, :inference_geo) ||
149
- request[:inference_geo]
125
+ def inference_geo(request:, usage:)
126
+ object_value(usage, :inference_geo) || request[:inference_geo]
127
+ end
128
+
129
+ def wrap_stream_call(args, kwargs)
130
+ request = request_params(args, kwargs)
131
+ enforce_budget!(request: request)
132
+ collector = stream_collector(request)
133
+ stream = yield
134
+ track_stream(stream, collector: collector)
135
+ end
136
+
137
+ def wrap_blocking_call(args, kwargs)
138
+ request = request_params(args, kwargs)
139
+ enforce_budget!(request: request)
140
+ started_at = LlmCostTracker::Timing.now_monotonic
141
+ message = yield
142
+ record_message(message, request: request, latency_ms: LlmCostTracker::Timing.elapsed_ms(started_at))
143
+ message
150
144
  end
151
145
  end
152
146
 
153
147
  module MessagesPatch
154
148
  def create(*args, **kwargs)
155
- LlmCostTracker::Integrations::Anthropic.enforce_budget!
156
- started_at = LlmCostTracker::Timing.now_monotonic
157
- message = super
158
- LlmCostTracker::Integrations::Anthropic.record_message(
159
- message,
160
- request: LlmCostTracker::Integrations::Anthropic.request_params(args, kwargs),
161
- latency_ms: LlmCostTracker::Integrations::Anthropic.elapsed_ms(started_at)
162
- )
163
- message
149
+ LlmCostTracker::Integrations::Anthropic.wrap_blocking_call(args, kwargs) { super }
164
150
  end
165
151
 
166
152
  def stream(*args, **kwargs)
167
- request = LlmCostTracker::Integrations::Anthropic.request_params(args, kwargs)
168
- LlmCostTracker::Integrations::Anthropic.enforce_budget!
169
- collector = LlmCostTracker::Integrations::Anthropic.stream_collector(request)
170
- stream = super
171
- LlmCostTracker::Integrations::Anthropic.track_stream(stream, collector: collector)
153
+ LlmCostTracker::Integrations::Anthropic.wrap_stream_call(args, kwargs) { super }
172
154
  end
173
155
 
174
156
  def stream_raw(*args, **kwargs)
175
- request = LlmCostTracker::Integrations::Anthropic.request_params(args, kwargs)
176
- LlmCostTracker::Integrations::Anthropic.enforce_budget!
177
- collector = LlmCostTracker::Integrations::Anthropic.stream_collector(request)
178
- stream = super
179
- LlmCostTracker::Integrations::Anthropic.track_stream(stream, collector: collector)
157
+ LlmCostTracker::Integrations::Anthropic.wrap_stream_call(args, kwargs) { super }
180
158
  end
181
159
  end
182
160
  end
@@ -3,6 +3,7 @@
3
3
  require "active_support/core_ext/hash/indifferent_access"
4
4
  require "active_support/core_ext/string/inflections"
5
5
 
6
+ require_relative "../doctor/check"
6
7
  require_relative "../logging"
7
8
  require_relative "../timing"
8
9
  require_relative "../capture/stream_collector"
@@ -11,7 +12,7 @@ require_relative "../capture/stream_tracker"
11
12
  module LlmCostTracker
12
13
  module Integrations
13
14
  module Base
14
- Result = Data.define(:name, :status, :message)
15
+ Result = LlmCostTracker::Doctor::Check
15
16
 
16
17
  def active?
17
18
  LlmCostTracker.configuration.instrumented?(integration_name)
@@ -26,26 +27,28 @@ module LlmCostTracker
26
27
  end
27
28
 
28
29
  def status
29
- name = integration_name
30
+ name = integration_name.to_s
30
31
  problems = version_problems + target_problems
31
32
  if problems.any?
32
- return Result.new(name, :warn, "#{name} integration cannot be installed: #{problems.join('; ')}")
33
+ return Result.new(:warn, name, "#{name} integration cannot be installed: #{problems.join('; ')}")
33
34
  end
34
35
 
35
36
  installed = patch_targets.reject { |target| target.fetch(:optional) }.all? do |target|
36
37
  target.fetch(:constant_name).to_s.safe_constantize&.ancestors&.include?(target.fetch(:patch))
37
38
  end
38
- return Result.new(name, :ok, "#{name} integration installed") if installed
39
+ return Result.new(:ok, name, "#{name} integration installed") if installed
39
40
 
40
- Result.new(name, :warn, "#{name} integration is enabled but not installed")
41
+ Result.new(:warn, name, "#{name} integration is enabled but not installed")
41
42
  end
42
43
 
43
- def elapsed_ms(started_at)
44
- Timing.elapsed_ms(started_at)
45
- end
44
+ def enforce_budget!(request:)
45
+ return unless active?
46
46
 
47
- def enforce_budget!
48
- LlmCostTracker::Tracker.enforce_budget! if active?
47
+ LlmCostTracker::Tracker.enforce_budget!(
48
+ provider: integration_name.to_s,
49
+ model: request[:model],
50
+ request: request
51
+ )
49
52
  end
50
53
 
51
54
  def record_safely
@@ -61,7 +64,7 @@ module LlmCostTracker
61
64
  case args.first
62
65
  when Hash then args.first
63
66
  when nil then {}
64
- else args.first.respond_to?(:to_h) ? args.first.to_h : {}
67
+ else args.first.to_h
65
68
  end
66
69
  params.merge(kwargs).with_indifferent_access
67
70
  rescue StandardError