llm_cost_tracker 0.14.1 → 0.14.2

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +16 -5
  4. data/app/assets/llm_cost_tracker/application.css +16 -0
  5. data/app/controllers/llm_cost_tracker/calls_controller.rb +1 -3
  6. data/app/controllers/llm_cost_tracker/models_controller.rb +2 -4
  7. data/app/controllers/llm_cost_tracker/tags_controller.rb +5 -4
  8. data/app/helpers/llm_cost_tracker/application_helper.rb +4 -25
  9. data/app/helpers/llm_cost_tracker/chart_helper.rb +6 -10
  10. data/app/helpers/llm_cost_tracker/token_usage_helper.rb +8 -28
  11. data/app/models/llm_cost_tracker/call.rb +10 -14
  12. data/app/services/llm_cost_tracker/dashboard/data_quality.rb +4 -15
  13. data/app/services/llm_cost_tracker/dashboard/filter.rb +5 -9
  14. data/app/services/llm_cost_tracker/dashboard/pagination.rb +2 -5
  15. data/app/services/llm_cost_tracker/dashboard/pricing_overview.rb +2 -1
  16. data/app/services/llm_cost_tracker/dashboard/spend_anomaly.rb +1 -1
  17. data/app/services/llm_cost_tracker/dashboard/tag_breakdown.rb +2 -3
  18. data/app/services/llm_cost_tracker/dashboard/tag_key_explorer.rb +1 -0
  19. data/app/services/llm_cost_tracker/dashboard/time_series.rb +3 -5
  20. data/app/services/llm_cost_tracker/dashboard/top_models.rb +1 -2
  21. data/app/views/llm_cost_tracker/calls/show.html.erb +8 -22
  22. data/app/views/llm_cost_tracker/data_quality/index.html.erb +1 -1
  23. data/app/views/llm_cost_tracker/shared/_bar.html.erb +1 -3
  24. data/app/views/llm_cost_tracker/shared/_filter_pill_date.html.erb +1 -4
  25. data/app/views/llm_cost_tracker/shared/_filter_pill_model.html.erb +1 -4
  26. data/app/views/llm_cost_tracker/shared/_filter_pill_provider.html.erb +1 -4
  27. data/app/views/llm_cost_tracker/shared/_filter_pill_stream.html.erb +1 -4
  28. data/app/views/llm_cost_tracker/tags/show.html.erb +5 -5
  29. data/lib/llm_cost_tracker/budget/per_tag.rb +15 -9
  30. data/lib/llm_cost_tracker/budget.rb +44 -55
  31. data/lib/llm_cost_tracker/capture/event_window.rb +3 -2
  32. data/lib/llm_cost_tracker/capture/sdk_payload.rb +5 -1
  33. data/lib/llm_cost_tracker/capture/stream_collector.rb +25 -26
  34. data/lib/llm_cost_tracker/capture/stream_tracker.rb +9 -23
  35. data/lib/llm_cost_tracker/capture_verifier.rb +1 -7
  36. data/lib/llm_cost_tracker/charges/line_item.rb +5 -1
  37. data/lib/llm_cost_tracker/configuration/budgets.rb +1 -1
  38. data/lib/llm_cost_tracker/configuration/pricing.rb +1 -1
  39. data/lib/llm_cost_tracker/configuration.rb +5 -9
  40. data/lib/llm_cost_tracker/doctor/price_check.rb +13 -3
  41. data/lib/llm_cost_tracker/doctor/schema_check.rb +1 -2
  42. data/lib/llm_cost_tracker/doctor.rb +19 -2
  43. data/lib/llm_cost_tracker/engine.rb +0 -1
  44. data/lib/llm_cost_tracker/event.rb +8 -0
  45. data/lib/llm_cost_tracker/generators/llm_cost_tracker/async_ingestion_generator.rb +0 -6
  46. data/lib/llm_cost_tracker/generators/llm_cost_tracker/call_rollups_generator.rb +5 -8
  47. data/lib/llm_cost_tracker/generators/llm_cost_tracker/install_generator.rb +0 -6
  48. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_async_ingestion.rb.erb +1 -1
  49. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_call_rollups.rb.erb +1 -1
  50. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/create_llm_cost_tracker_calls.rb.erb +1 -1
  51. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/initializer.rb.erb +11 -9
  52. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_rollups_provider.rb.erb +1 -1
  53. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_call_tags_key_value_index.rb.erb +1 -1
  54. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_image_tokens.rb.erb +1 -1
  55. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_indexes.rb.erb +1 -1
  56. data/lib/llm_cost_tracker/generators/llm_cost_tracker/templates/upgrade_per_tag_budgets.rb.erb +1 -1
  57. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_rollups_provider_generator.rb +0 -6
  58. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_call_tags_key_value_index_generator.rb +0 -6
  59. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_image_tokens_generator.rb +0 -6
  60. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_indexes_generator.rb +3 -8
  61. data/lib/llm_cost_tracker/generators/llm_cost_tracker/upgrade_per_tag_budgets_generator.rb +3 -8
  62. data/lib/llm_cost_tracker/ingestion/inbox.rb +2 -8
  63. data/lib/llm_cost_tracker/ingestion.rb +3 -1
  64. data/lib/llm_cost_tracker/integrations/anthropic.rb +63 -10
  65. data/lib/llm_cost_tracker/integrations/base.rb +39 -18
  66. data/lib/llm_cost_tracker/integrations/openai/batch_capture.rb +16 -13
  67. data/lib/llm_cost_tracker/integrations/openai/patches.rb +8 -0
  68. data/lib/llm_cost_tracker/integrations/openai.rb +42 -59
  69. data/lib/llm_cost_tracker/integrations/ruby_llm.rb +241 -76
  70. data/lib/llm_cost_tracker/integrations.rb +1 -20
  71. data/lib/llm_cost_tracker/ledger/isolation.rb +0 -7
  72. data/lib/llm_cost_tracker/ledger/period/totals.rb +14 -10
  73. data/lib/llm_cost_tracker/ledger/rollups.rb +14 -14
  74. data/lib/llm_cost_tracker/ledger/schema/adapter.rb +14 -3
  75. data/lib/llm_cost_tracker/ledger/schema/base.rb +5 -4
  76. data/lib/llm_cost_tracker/ledger/store.rb +2 -2
  77. data/lib/llm_cost_tracker/ledger/tags/breakdown.rb +1 -5
  78. data/lib/llm_cost_tracker/ledger/tags/encoding.rb +3 -12
  79. data/lib/llm_cost_tracker/ledger/tags/query.rb +0 -2
  80. data/lib/llm_cost_tracker/middleware/faraday.rb +36 -19
  81. data/lib/llm_cost_tracker/parsers.rb +7 -40
  82. data/lib/llm_cost_tracker/prices.json +2237 -154
  83. data/lib/llm_cost_tracker/pricing/backfill.rb +46 -13
  84. data/lib/llm_cost_tracker/pricing/calculation.rb +118 -75
  85. data/lib/llm_cost_tracker/pricing/effective_prices.rb +11 -10
  86. data/lib/llm_cost_tracker/pricing/estimator.rb +5 -2
  87. data/lib/llm_cost_tracker/pricing/matcher.rb +36 -22
  88. data/lib/llm_cost_tracker/pricing/mode.rb +2 -13
  89. data/lib/llm_cost_tracker/pricing/price_key.rb +5 -3
  90. data/lib/llm_cost_tracker/pricing/rate.rb +1 -0
  91. data/lib/llm_cost_tracker/pricing/registry.rb +5 -16
  92. data/lib/llm_cost_tracker/pricing/service_rates.rb +1 -8
  93. data/lib/llm_cost_tracker/pricing/sync/registry_diff.rb +13 -9
  94. data/lib/llm_cost_tracker/pricing/sync.rb +22 -71
  95. data/lib/llm_cost_tracker/providers/anthropic/parser.rb +33 -3
  96. data/lib/llm_cost_tracker/providers/anthropic/response_parser.rb +11 -5
  97. data/lib/llm_cost_tracker/providers/anthropic/usage_extractor.rb +81 -12
  98. data/lib/llm_cost_tracker/providers/azure/parser.rb +3 -7
  99. data/lib/llm_cost_tracker/providers/gemini/parser.rb +172 -48
  100. data/lib/llm_cost_tracker/providers/gemini/usage_extractor.rb +13 -22
  101. data/lib/llm_cost_tracker/providers/openai/hosts.rb +1 -1
  102. data/lib/llm_cost_tracker/providers/openai/parser.rb +11 -13
  103. data/lib/llm_cost_tracker/providers/openai/response_parser.rb +96 -26
  104. data/lib/llm_cost_tracker/providers/openai/service_charges.rb +57 -40
  105. data/lib/llm_cost_tracker/providers/openai/usage_extractor.rb +14 -1
  106. data/lib/llm_cost_tracker/providers/openai_compatible/parser.rb +3 -1
  107. data/lib/llm_cost_tracker/report/data.rb +1 -1
  108. data/lib/llm_cost_tracker/retention.rb +1 -3
  109. data/lib/llm_cost_tracker/tracker.rb +17 -9
  110. data/lib/llm_cost_tracker/usage/catalog.rb +13 -4
  111. data/lib/llm_cost_tracker/usage/dimension.rb +1 -1
  112. data/lib/llm_cost_tracker/usage/dimensions.yml +61 -0
  113. data/lib/llm_cost_tracker/version.rb +1 -1
  114. data/lib/tasks/llm_cost_tracker.rake +24 -21
  115. data/llm_cost_tracker.gemspec +63 -0
  116. metadata +8 -7
@@ -16,13 +16,10 @@ module LlmCostTracker
16
16
  @integration_name ||= name.demodulize.underscore.to_sym
17
17
  end
18
18
 
19
- def provider(value = nil)
20
- @provider = value.to_s if value
21
- @provider ||= integration_name.to_s
22
- end
19
+ def provider = integration_name.to_s
23
20
 
24
21
  def active?
25
- LlmCostTracker.configuration.instrumented?(integration_name)
22
+ LlmCostTracker.configuration.enabled && LlmCostTracker.configuration.instrumented?(integration_name)
26
23
  end
27
24
 
28
25
  def install
@@ -30,7 +27,8 @@ module LlmCostTracker
30
27
  Logging.warn(untested_version_message) if untested_version?
31
28
  patch_targets.each do |target|
32
29
  target_class = target.fetch(:constant_name).to_s.safe_constantize
33
- install_patch(target_class, target.fetch(:patch)) if target_class
30
+ patch = target.fetch(:patch)
31
+ target_class.prepend(patch) if target_class && !target_class.ancestors.include?(patch)
34
32
  end
35
33
  end
36
34
 
@@ -68,6 +66,40 @@ module LlmCostTracker
68
66
  Logging.warn("#{integration_name} integration failed to record usage: #{e.class}: #{e.message}")
69
67
  end
70
68
 
69
+ def record_passthrough(provider:,
70
+ model:,
71
+ response:,
72
+ latency_ms:,
73
+ service_line_items: [],
74
+ usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE,
75
+ pricing_mode: nil,
76
+ **token_attributes)
77
+ return unless active?
78
+
79
+ record_safely do
80
+ LlmCostTracker::Tracker.record(
81
+ event: LlmCostTracker::Event.build(
82
+ provider: provider,
83
+ model: model,
84
+ token_usage: LlmCostTracker::Usage::TokenUsage.build(**token_attributes),
85
+ usage_source: usage_source,
86
+ pricing_mode: pricing_mode,
87
+ provider_response_id: provider_response_id_for(response),
88
+ service_line_items: service_line_items
89
+ ),
90
+ latency_ms: latency_ms
91
+ )
92
+ end
93
+ end
94
+
95
+ def record_once(event)
96
+ LlmCostTracker::Tracker.record(event: event.keyed_by_response_id)
97
+ rescue ActiveRecord::RecordNotUnique
98
+ nil
99
+ end
100
+
101
+ def provider_response_id_for(response) = response&.try(:id)
102
+
71
103
  def request_params(args, kwargs)
72
104
  params =
73
105
  case args.first
@@ -108,19 +140,14 @@ module LlmCostTracker
108
140
  ).wrap
109
141
  end
110
142
 
111
- def stream_collector(request, provider: self.provider)
143
+ def stream_collector(request)
112
144
  LlmCostTracker::Capture::StreamCollector.new(
113
145
  provider: provider,
114
146
  model: request[:model],
115
- pricing_mode: stream_pricing_mode(request),
116
147
  request: request
117
148
  )
118
149
  end
119
150
 
120
- def stream_pricing_mode(_request)
121
- nil
122
- end
123
-
124
151
  def minimum_version(value = nil)
125
152
  @minimum_version = value if value
126
153
  @minimum_version
@@ -196,12 +223,6 @@ module LlmCostTracker
196
223
  "#{target.fetch(:constant_name)}##{method_name} is not available"
197
224
  end
198
225
  end
199
-
200
- def install_patch(target, patch)
201
- return if target&.ancestors&.include?(patch)
202
-
203
- target.prepend(patch)
204
- end
205
226
  end
206
227
  end
207
228
  end
@@ -13,15 +13,15 @@ module LlmCostTracker
13
13
  class << self
14
14
  def maybe_capture(batch, resource:)
15
15
  return unless Openai.active?
16
- return unless batch.status.to_s == "completed"
16
+ return unless %w[completed expired cancelled].include?(batch.status.to_s)
17
17
  return unless batch.output_file_id && batch.id
18
18
  return if captured?(batch.id)
19
19
 
20
20
  client = resource.instance_variable_get(:@client)
21
21
  host = Openai.client_host_for(resource)
22
22
  Openai.record_safely do
23
- io = client.files.content(batch.output_file_id)
24
- deferred = capture_jsonl(io.respond_to?(:read) ? io.read : io.to_s, host: host)
23
+ jsonl = client.files.content(batch.output_file_id).read
24
+ deferred = capture_jsonl(jsonl, host: host, model: batch.model)
25
25
  mark_captured(batch.id)
26
26
  raise deferred if deferred
27
27
  end
@@ -41,19 +41,16 @@ module LlmCostTracker
41
41
  end
42
42
  end
43
43
 
44
- def capture_jsonl(jsonl, host:)
44
+ def capture_jsonl(jsonl, host:, model:)
45
45
  deferred = nil
46
46
  jsonl.each_line do |line|
47
- line = line.strip
48
- next if line.empty?
49
-
50
47
  entry = parse_line(line)
51
48
  next unless entry
52
49
 
53
50
  response = entry.dig("response", "body")
54
51
  next unless response.is_a?(Hash) && response["usage"]
55
52
 
56
- record_result(response, host: host)
53
+ record_result({ "id" => entry["id"] }.merge(response), host: host, model: model)
57
54
  rescue LlmCostTracker::BudgetExceededError, LlmCostTracker::UnknownPricingError => e
58
55
  deferred ||= e
59
56
  end
@@ -66,19 +63,25 @@ module LlmCostTracker
66
63
  nil
67
64
  end
68
65
 
69
- def record_result(response, host:)
66
+ def record_result(response, host:, model:)
70
67
  provider = Openai.provider_for_host(host)
71
68
  return if LlmCostTracker::Call.already_recorded?(provider: provider, provider_response_id: response["id"])
72
69
 
73
- event = LlmCostTracker::Providers::Openai::ResponseParser.event_from_response(
70
+ parser = LlmCostTracker::Providers::Openai::ResponseParser
71
+ event = parser.event_from_response(
74
72
  response: response,
75
- request: {},
73
+ request: { "model" => model },
76
74
  provider: provider,
77
75
  host: host,
78
76
  usage_source: LlmCostTracker::Usage::Source::SDK_BATCH_RESULT,
79
- pricing_mode: "batch"
77
+ # /v1/batches runs regional processing only on the us and eu hosts.
78
+ pricing_mode: parser.combined_pricing_mode(
79
+ host: (host if host.to_s.match?(/\A(?:us|eu)\./i)),
80
+ model: response["model"] || model,
81
+ service_tier: "batch"
82
+ )
80
83
  )
81
- LlmCostTracker::Tracker.record(event: event) if event
84
+ Openai.record_once(event)
82
85
  end
83
86
  end
84
87
  end
@@ -45,6 +45,14 @@ module LlmCostTracker
45
45
  include PatchBuilder.build(record_method: :record_response, methods: %i[create])
46
46
  include PatchBuilder.build_stream(methods: %i[stream stream_raw])
47
47
 
48
+ def retrieve(response_id, *args, **kwargs)
49
+ response = super
50
+ LlmCostTracker::Integrations::Openai.record_retrieved_response(
51
+ response, host: LlmCostTracker::Integrations::Openai.client_host_for(self)
52
+ )
53
+ response
54
+ end
55
+
48
56
  def retrieve_streaming(response_id, *args, **kwargs)
49
57
  LlmCostTracker::Integrations::Openai.wrap_stream(
50
58
  args, kwargs, **LlmCostTracker::Integrations::Openai.stream_seam(self)
@@ -18,19 +18,12 @@ module LlmCostTracker
18
18
  minimum_version "0.59.0"
19
19
 
20
20
  class << self
21
- def stream_pricing_mode(request, host: nil)
22
- LlmCostTracker::Providers::Openai::ResponseParser.combined_pricing_mode(
23
- host: host,
24
- model: (request || {})[:model],
25
- service_tier: (request || {})[:service_tier]
26
- )
27
- end
28
-
29
21
  def stream_collector(request, host: nil)
30
22
  LlmCostTracker::Capture::StreamCollector.new(
31
23
  provider: provider_for_host(host),
32
24
  model: request[:model],
33
- pricing_mode: stream_pricing_mode(request, host: host),
25
+ # Host part only: the parser prefers the served tier over the requested one, e.g. after a downgrade.
26
+ pricing_mode: host_pricing_mode(host, request),
34
27
  request: request
35
28
  )
36
29
  end
@@ -53,29 +46,25 @@ module LlmCostTracker
53
46
  end
54
47
 
55
48
  def provider_for_host(host)
56
- LlmCostTracker::Providers::Azure::Hosts.openai?(host) ? "azure_openai" : "openai"
49
+ return "azure_openai" if LlmCostTracker::Providers::Azure::Hosts.openai?(host)
50
+
51
+ LlmCostTracker.configuration.capture.openai_compatible_providers[host.to_s.downcase] || "openai"
57
52
  end
58
53
 
59
54
  def patch_targets
60
55
  [
61
56
  patch_target("OpenAI::Resources::Responses", with: ResponsesPatch),
62
57
  patch_target("OpenAI::Resources::Chat::Completions", with: ChatCompletionsPatch),
63
- *auxiliary_patch_targets
64
- ]
65
- end
66
-
67
- def auxiliary_patch_targets
68
- [
69
58
  patch_target("OpenAI::Resources::Embeddings", with: EmbeddingsPatch, optional: true),
70
59
  patch_target("OpenAI::Resources::Images", with: ImagesPatch, optional: true),
71
60
  patch_target("OpenAI::Resources::Images",
72
61
  with: StreamingImagesPatch,
73
- optional: true,
62
+ optional: true,
74
63
  skip_when_methods_missing: true),
75
64
  patch_target("OpenAI::Resources::Audio::Transcriptions", with: TranscriptionsPatch, optional: true),
76
65
  patch_target("OpenAI::Resources::Audio::Transcriptions",
77
66
  with: StreamingTranscriptionsPatch,
78
- optional: true,
67
+ optional: true,
79
68
  skip_when_methods_missing: true),
80
69
  patch_target("OpenAI::Resources::Audio::Translations", with: TranslationsPatch, optional: true),
81
70
  patch_target("OpenAI::Resources::Audio::Speech", with: SpeechPatch, optional: true),
@@ -89,11 +78,14 @@ module LlmCostTracker
89
78
 
90
79
  record_safely do
91
80
  normalized = LlmCostTracker::Capture::SdkPayload.normalize(response)
92
- usage = normalized["usage"]
93
- if usage
94
- input_tokens = usage["input_tokens"] || usage["prompt_tokens"]
95
- output_tokens = usage["output_tokens"] || usage["completion_tokens"]
96
- next if input_tokens.nil? && output_tokens.nil?
81
+ usage = normalized["usage"] || {}
82
+ input_tokens = usage["input_tokens"] || usage["prompt_tokens"]
83
+ output_tokens = usage["output_tokens"] || usage["completion_tokens"]
84
+ if input_tokens.nil? && output_tokens.nil?
85
+ unless normalized["background"]
86
+ Logging.warn("OpenAI response #{normalized['id']} has no usage; not recorded")
87
+ end
88
+ next
97
89
  end
98
90
 
99
91
  event = LlmCostTracker::Providers::Openai::ResponseParser.event_from_response(
@@ -103,7 +95,21 @@ module LlmCostTracker
103
95
  host: host,
104
96
  usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE
105
97
  )
106
- LlmCostTracker::Tracker.record(event: event, latency_ms: latency_ms) if event
98
+ LlmCostTracker::Tracker.record(event: event, latency_ms: latency_ms)
99
+ end
100
+ end
101
+
102
+ def record_retrieved_response(response, host:)
103
+ return unless active?
104
+
105
+ record_safely do
106
+ event = LlmCostTracker::Providers::Openai::ResponseParser.retrieved_event(
107
+ response: LlmCostTracker::Capture::SdkPayload.normalize(response),
108
+ provider: provider_for_host(host),
109
+ host: host,
110
+ usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE
111
+ )
112
+ record_once(event) if event
107
113
  end
108
114
  end
109
115
 
@@ -123,7 +129,8 @@ module LlmCostTracker
123
129
  model: request[:model],
124
130
  response: response,
125
131
  latency_ms: latency_ms,
126
- host: host,
132
+ provider: provider_for_host(host),
133
+ pricing_mode: host_pricing_mode(host, request),
127
134
  input_tokens: [raw_input - image_input - cache_read, 0].max,
128
135
  image_input_tokens: image_input,
129
136
  output_tokens: text_output,
@@ -138,8 +145,10 @@ module LlmCostTracker
138
145
  model: request[:model],
139
146
  response: response,
140
147
  latency_ms: latency_ms,
141
- host: host,
148
+ provider: provider_for_host(host),
149
+ pricing_mode: host_pricing_mode(host, request),
142
150
  service_line_items: LlmCostTracker::Providers::Openai::ServiceCharges.transcription_line_items(usage),
151
+ usage_source: usage ? LlmCostTracker::Usage::Source::SDK_RESPONSE : LlmCostTracker::Usage::Source::UNKNOWN,
143
152
  **transcription_token_attributes(usage)
144
153
  )
145
154
  end
@@ -161,54 +170,28 @@ module LlmCostTracker
161
170
  model: request[:model],
162
171
  response: nil,
163
172
  latency_ms: latency_ms,
164
- host: host,
173
+ provider: provider_for_host(host),
165
174
  input_tokens: 0,
166
175
  output_tokens: 0,
167
- service_line_items: speech_line_items(request)
176
+ service_line_items: LlmCostTracker::Providers::Openai::ServiceCharges.speech_line_items(request)
168
177
  )
169
178
  end
170
179
 
171
- def speech_line_items(request)
172
- input = request[:input]
173
- return [] unless input.is_a?(String)
174
- return [] unless LlmCostTracker::Providers::Openai::ModelFamilies.character_billed_tts?(request[:model])
175
-
176
- [LlmCostTracker::Charges::LineItem.build(
177
- dimension_key: "text_to_speech_character",
178
- quantity: input.length,
179
- cost_status: LlmCostTracker::Charges::CostStatus::UNKNOWN,
180
- pricing_basis: "provider_usage",
181
- provider_field: "request.input"
182
- )]
183
- end
184
-
185
180
  def record_moderation(response, request:, latency_ms:, host: nil)
186
181
  record_passthrough(
187
182
  model: response.model || request[:model],
188
183
  response: response,
189
184
  latency_ms: latency_ms,
190
- host: host,
185
+ provider: provider_for_host(host),
191
186
  input_tokens: 0,
192
187
  output_tokens: 0
193
188
  )
194
189
  end
195
190
 
196
- def record_passthrough(model:, response:, latency_ms:, host: nil, service_line_items: [], **token_attributes)
197
- return unless active?
198
-
199
- record_safely do
200
- LlmCostTracker::Tracker.record(
201
- event: Event.build(
202
- provider: provider_for_host(host),
203
- model: model,
204
- token_usage: Usage::TokenUsage.build(**token_attributes),
205
- usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE,
206
- provider_response_id: response&.try(:id),
207
- service_line_items: service_line_items
208
- ),
209
- latency_ms: latency_ms
210
- )
211
- end
191
+ def host_pricing_mode(host, request)
192
+ LlmCostTracker::Providers::Openai::ResponseParser.combined_pricing_mode(
193
+ provider: provider_for_host(host), host: host, model: request[:model], service_tier: nil
194
+ )
212
195
  end
213
196
 
214
197
  def usage_hash_from(response)