Module: LlmCostTracker::Integrations::Openai

Extended by:
Base
Defined in:
lib/llm_cost_tracker/integrations/openai.rb,
lib/llm_cost_tracker/integrations/openai/patches.rb,
lib/llm_cost_tracker/integrations/openai/batch_capture.rb

Defined Under Namespace

Modules: BatchCapture, BatchesPatch, ChatCompletionsPatch, PatchBuilder, ResponsesPatch

Constant Summary collapse

EmbeddingsPatch =
PatchBuilder.build(record_method: :record_response, methods: %i[create])
ImagesPatch =
PatchBuilder.build(record_method: :record_image, methods: %i[generate edit create_variation])
TranscriptionsPatch =
PatchBuilder.build(record_method: :record_transcription, methods: %i[create])
TranslationsPatch =
PatchBuilder.build(record_method: :record_transcription, methods: %i[create])
SpeechPatch =
PatchBuilder.build(record_method: :record_speech, methods: %i[create])
ModerationsPatch =
PatchBuilder.build(record_method: :record_moderation, methods: %i[create])
StreamingImagesPatch =
PatchBuilder.build_stream(methods: %i[generate_stream_raw edit_stream_raw])
StreamingTranscriptionsPatch =
PatchBuilder.build_stream(methods: %i[create_streaming])

Class Method Summary collapse

Methods included from Base

active?, enforce_budget!, gem_version, install, integration_name, minimum_version, patch_target, patch_targets, provider, record_safely, request_params, status, stream_collector, stream_pricing_mode, track_stream, wrap_blocking, wrap_stream

Class Method Details

.auxiliary_patch_targetsObject



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 67

def auxiliary_patch_targets
  [
    patch_target("OpenAI::Resources::Embeddings", with: EmbeddingsPatch, optional: true),
    patch_target("OpenAI::Resources::Images", with: ImagesPatch, optional: true),
    patch_target("OpenAI::Resources::Images",
                 with: StreamingImagesPatch,
                                                                       optional: true,
                 skip_when_methods_missing: true),
    patch_target("OpenAI::Resources::Audio::Transcriptions", with: TranscriptionsPatch, optional: true),
    patch_target("OpenAI::Resources::Audio::Transcriptions",
                 with: StreamingTranscriptionsPatch,
                                                                                      optional: true,
                 skip_when_methods_missing: true),
    patch_target("OpenAI::Resources::Audio::Translations", with: TranslationsPatch, optional: true),
    patch_target("OpenAI::Resources::Audio::Speech", with: SpeechPatch, optional: true),
    patch_target("OpenAI::Resources::Moderations", with: ModerationsPatch, optional: true),
    patch_target("OpenAI::Resources::Batches", with: BatchesPatch, optional: true)
  ]
end

.client_host_for(resource) ⇒ Object



46
47
48
49
50
51
52
53
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 46

def client_host_for(resource)
  client = resource.instance_variable_get(:@client)
  return nil unless client

  URI.parse(client.base_url.to_s).host
rescue URI::InvalidURIError
  nil
end

.patch_targetsObject



59
60
61
62
63
64
65
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 59

def patch_targets
  [
    patch_target("OpenAI::Resources::Responses", with: ResponsesPatch),
    patch_target("OpenAI::Resources::Chat::Completions", with: ChatCompletionsPatch),
    *auxiliary_patch_targets
  ]
end

.provider_for_host(host) ⇒ Object



55
56
57
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 55

def provider_for_host(host)
  LlmCostTracker::Providers::Azure::Hosts.openai?(host) ? "azure_openai" : "openai"
end

.record_image(response, request:, latency_ms:, host: nil) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 110

def record_image(response, request:, latency_ms:, host: nil)
  usage = usage_hash_from(response) || {}
  raw_input = usage[:input_tokens].to_i
  image_input = LlmCostTracker::Providers::Openai::UsageExtractor.image_input_tokens(usage)
  cache_read = LlmCostTracker::Providers::Openai::UsageExtractor.cache_read_input_tokens(usage)
  image_output, text_output = LlmCostTracker::Providers::Openai::UsageExtractor.split_output(
    output_tokens: usage[:output_tokens].to_i,
    image_output_details: LlmCostTracker::Providers::Openai::UsageExtractor.image_output_tokens(usage),
    text_output_details: LlmCostTracker::Providers::Openai::UsageExtractor.text_output_tokens(usage),
    audio_output: 0,
    default_to_image: true
  )
  record_passthrough(
    model: request[:model],
    response: response,
    latency_ms: latency_ms,
    host: host,
    input_tokens: [raw_input - image_input - cache_read, 0].max,
    image_input_tokens: image_input,
    output_tokens: text_output,
    image_output_tokens: image_output,
    cache_read_input_tokens: cache_read
  )
end

.record_moderation(response, request:, latency_ms:, host: nil) ⇒ Object



185
186
187
188
189
190
191
192
193
194
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 185

def record_moderation(response, request:, latency_ms:, host: nil)
  record_passthrough(
    model: response.model || request[:model],
    response: response,
    latency_ms: latency_ms,
    host: host,
    input_tokens: 0,
    output_tokens: 0
  )
end

.record_passthrough(model:, response:, latency_ms:, host: nil, service_line_items: [], **token_attributes) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 196

def record_passthrough(model:, response:, latency_ms:, host: nil, service_line_items: [], **token_attributes)
  return unless active?

  record_safely do
    LlmCostTracker::Tracker.record(
      event: Event.build(
        provider: provider_for_host(host),
        model: model,
        token_usage: Usage::TokenUsage.build(**token_attributes),
        usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE,
        provider_response_id: response&.try(:id),
        service_line_items: service_line_items
      ),
      latency_ms: latency_ms
    )
  end
end

.record_response(response, request:, latency_ms:, host: nil) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 87

def record_response(response, request:, latency_ms:, host: nil)
  return unless active?

  record_safely do
    normalized = LlmCostTracker::Capture::SdkPayload.normalize(response)
    usage = normalized["usage"]
    if usage
      input_tokens = usage["input_tokens"] || usage["prompt_tokens"]
      output_tokens = usage["output_tokens"] || usage["completion_tokens"]
      next if input_tokens.nil? && output_tokens.nil?
    end

    event = LlmCostTracker::Providers::Openai::ResponseParser.event_from_response(
      response: normalized,
      request: request,
      provider: provider_for_host(host),
      host: host,
      usage_source: LlmCostTracker::Usage::Source::SDK_RESPONSE
    )
    LlmCostTracker::Tracker.record(event: event, latency_ms: latency_ms) if event
  end
end

.record_speech(_response, request:, latency_ms:, host: nil) ⇒ Object



159
160
161
162
163
164
165
166
167
168
169
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 159

def record_speech(_response, request:, latency_ms:, host: nil)
  record_passthrough(
    model: request[:model],
    response: nil,
    latency_ms: latency_ms,
    host: host,
    input_tokens: 0,
    output_tokens: 0,
    service_line_items: speech_line_items(request)
  )
end

.record_transcription(response, request:, latency_ms:, host: nil) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 135

def record_transcription(response, request:, latency_ms:, host: nil)
  usage = usage_hash_from(response)
  record_passthrough(
    model: request[:model],
    response: response,
    latency_ms: latency_ms,
    host: host,
    service_line_items: LlmCostTracker::Providers::Openai::ServiceCharges.transcription_line_items(usage),
    **transcription_token_attributes(usage)
  )
end

.speech_line_items(request) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 171

def speech_line_items(request)
  input = request[:input]
  return [] unless input.is_a?(String)
  return [] unless LlmCostTracker::Providers::Openai::ModelFamilies.character_billed_tts?(request[:model])

  [LlmCostTracker::Charges::LineItem.build(
    dimension_key: "text_to_speech_character",
    quantity: input.length,
    cost_status: LlmCostTracker::Charges::CostStatus::UNKNOWN,
    pricing_basis: "provider_usage",
    provider_field: "request.input"
  )]
end

.stream_collector(request, host: nil) ⇒ Object



29
30
31
32
33
34
35
36
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 29

def stream_collector(request, host: nil)
  LlmCostTracker::Capture::StreamCollector.new(
    provider: provider_for_host(host),
    model: request[:model],
    pricing_mode: stream_pricing_mode(request, host: host),
    request: request
  )
end

.stream_pricing_mode(request, host: nil) ⇒ Object



21
22
23
24
25
26
27
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 21

def stream_pricing_mode(request, host: nil)
  LlmCostTracker::Providers::Openai::ResponseParser.combined_pricing_mode(
    host: host,
    model: (request || {})[:model],
    service_tier: (request || {})[:service_tier]
  )
end

.stream_seam(resource) ⇒ Object



38
39
40
41
42
43
44
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 38

def stream_seam(resource)
  host = client_host_for(resource)
  {
    provider: provider_for_host(host),
    collector: ->(request) { stream_collector(request, host: host) }
  }
end

.transcription_token_attributes(usage) ⇒ Object



147
148
149
150
151
152
153
154
155
156
157
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 147

def transcription_token_attributes(usage)
  return { input_tokens: 0, output_tokens: 0 } unless usage && usage[:type].to_s == "tokens"

  raw_input = usage[:input_tokens].to_i
  audio_input = LlmCostTracker::Providers::Openai::UsageExtractor.audio_input_tokens(usage)
  {
    input_tokens: [raw_input - audio_input, 0].max,
    audio_input_tokens: audio_input,
    output_tokens: usage[:output_tokens].to_i
  }
end

.usage_hash_from(response) ⇒ Object



214
215
216
# File 'lib/llm_cost_tracker/integrations/openai.rb', line 214

def usage_hash_from(response)
  response.try(:usage)&.deep_to_h
end