Module: LlmCostTracker::Parsers::OpenaiServiceCharges

Included in:
OpenaiUsage
Defined in:
lib/llm_cost_tracker/parsers/openai_service_charges.rb

Constant Summary collapse

RESPONSE_OUTPUT_COMPONENTS =
{
  "web_search_call" => :web_search_request,
  "file_search_call" => :file_search_call,
  "code_interpreter_call" => :container_session,
  "mcp_call" => :mcp_call
}.freeze
CHAT_COMPLETIONS_ANNOTATION_PROVIDER_FIELD =
"choices.message.annotations.url_citation"
CHAT_COMPLETIONS_SEARCH_MODEL_PROVIDER_FIELD =
"request.model"

Class Method Summary collapse

Class Method Details

.billable?(item) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
63
64
65
66
67
68
69
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 60

def billable?(item)
  return false unless item.is_a?(Hash)

  component = RESPONSE_OUTPUT_COMPONENTS[item["type"]]
  return false unless component
  return true unless component == :web_search_request

  action_type = item.dig("action", "type")
  action_type.nil? || action_type == "search"
end

.build_line_item(item, request: nil, model: nil) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 83

def build_line_item(item, request: nil, model: nil)
  return nil unless item.is_a?(Hash)

  component_key = component_key_for(item, request: request, model: model)
  return nil unless component_key

  provider_item_id = if component_key == :container_session
                       item["container_id"] || item["id"]
                     else
                       item["id"]
                     end
  Billing::LineItem.build(
    component_key: component_key,
    quantity: 1,
    cost_status: Billing::CostStatus::UNKNOWN,
    pricing_basis: :provider_usage,
    provider_field: item["provider_field"] || "response.output.#{item['type']}",
    provider_item_id: provider_item_id,
    details: line_item_details(item)
  )
end

.chat_completions_search_model?(model) ⇒ Boolean

Returns:

  • (Boolean)


123
124
125
126
127
128
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 123

def chat_completions_search_model?(model)
  return false unless model

  name = model.to_s.split("/", 2).last
  LlmCostTracker::Providers::Openai::ModelFamilies.chat_completions_search?(name)
end

.chat_completions_search_provider_field(choices, model) ⇒ Object



45
46
47
48
49
50
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 45

def chat_completions_search_provider_field(choices, model)
  return CHAT_COMPLETIONS_ANNOTATION_PROVIDER_FIELD if chat_completions_used_web_search?(choices)
  return CHAT_COMPLETIONS_SEARCH_MODEL_PROVIDER_FIELD if chat_completions_search_model?(model)

  nil
end

.chat_completions_used_web_search?(choices) ⇒ Boolean

Returns:

  • (Boolean)


52
53
54
55
56
57
58
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 52

def chat_completions_used_web_search?(choices)
  Array(choices).any? do |choice|
    Array(choice.dig("message", "annotations")).any? do |annotation|
      annotation.is_a?(Hash) && annotation["type"] == "url_citation"
    end
  end
end

.chat_completions_web_search_items(response, model: nil) ⇒ Object



35
36
37
38
39
40
41
42
43
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 35

def chat_completions_web_search_items(response, model: nil)
  return [] unless response["choices"]

  provider_field = chat_completions_search_provider_field(response["choices"], model)
  return [] unless provider_field

  [{ "type" => "web_search_call", "id" => response["id"], "action" => { "type" => "search" },
     "provider_field" => provider_field }]
end

.component_key_for(item, request:, model:) ⇒ Object



105
106
107
108
109
110
111
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 105

def component_key_for(item, request:, model:)
  component = RESPONSE_OUTPUT_COMPONENTS[item["type"]]
  return component unless component == :web_search_request
  return component unless web_search_preview_used?(request) || chat_completions_search_model?(model)

  reasoning_model?(model) ? :web_search_preview_request_reasoning : :web_search_preview_request_non_reasoning
end

.line_item_details(item) ⇒ Object



137
138
139
140
141
142
143
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 137

def line_item_details(item)
  {
    "status" => item["status"],
    "action_type" => item.dig("action", "type"),
    "container_id" => item["container_id"]
  }.compact
end

.line_items_from_output(output_items, request: nil, model: nil) ⇒ Object



18
19
20
21
22
23
24
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 18

def line_items_from_output(output_items, request: nil, model: nil)
  deduped = {}
  Array(output_items).each { |item| store_output_item(deduped, item) }
  deduped.values
         .select { |item| billable?(item) }
         .filter_map { |item| build_line_item(item, request: request, model: model) }
end

.openai_stream_service_line_items(events, request: nil, model: nil) ⇒ Object



145
146
147
148
149
150
151
152
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 145

def openai_stream_service_line_items(events, request: nil, model: nil)
  output_items = []
  each_event_data(events) do |data|
    output_items.concat(Array(data.dig("response", "output")))
    output_items << data["item"] if data["item"]
  end
  line_items_from_output(output_items, request: request, model: model)
end

.reasoning_model?(model) ⇒ Boolean

Returns:

  • (Boolean)


130
131
132
133
134
135
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 130

def reasoning_model?(model)
  return false unless model

  name = model.to_s.split("/", 2).last
  LlmCostTracker::Providers::Openai::ModelFamilies.reasoning?(name)
end

.service_line_items_for(response, request: nil, model: nil) ⇒ Object



26
27
28
29
30
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 26

def service_line_items_for(response, request: nil, model: nil)
  output_items = Array(response["output"])
  output_items += chat_completions_web_search_items(response, model: model) if output_items.empty?
  line_items_from_output(output_items, request: request, model: model)
end

.store_output_item(output_items, item) ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 71

def store_output_item(output_items, item)
  return unless item.is_a?(Hash) && RESPONSE_OUTPUT_COMPONENTS.key?(item["type"])

  component = RESPONSE_OUTPUT_COMPONENTS[item["type"]]
  key = if component == :container_session && item["container_id"]
          "#{component}:#{item['container_id']}"
        else
          item["id"] || "#{item['type']}:#{output_items.length}"
        end
  output_items[key] = item
end

.web_search_preview_used?(request) ⇒ Boolean

Returns:

  • (Boolean)


113
114
115
116
117
118
119
120
121
# File 'lib/llm_cost_tracker/parsers/openai_service_charges.rb', line 113

def web_search_preview_used?(request)
  tools = request && (request[:tools] || request["tools"])
  return false unless tools.respond_to?(:each)

  tools.any? do |tool|
    type = tool.is_a?(Hash) ? (tool[:type] || tool["type"]) : tool
    type.to_s.include?("web_search_preview")
  end
end