Module: RubyLLM::Providers::OpenAI::Chat

Included in:
RubyLLM::Providers::OpenAI
Defined in:
lib/ruby_llm/providers/openai/chat.rb

Overview

Chat methods of the OpenAI API integration

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.extract_content_and_thinking(content) ⇒ Object



126
127
128
129
130
131
132
133
134
# File 'lib/ruby_llm/providers/openai/chat.rb', line 126

def extract_content_and_thinking(content)
  return extract_think_tag_content(content) if content.is_a?(String)
  return [content, nil] unless content.is_a?(Array)

  text = extract_text_from_blocks(content)
  thinking = extract_thinking_from_blocks(content)

  [text.empty? ? nil : text, thinking.empty? ? nil : thinking]
end

.extract_text_from_blocks(blocks) ⇒ Object



136
137
138
139
140
# File 'lib/ruby_llm/providers/openai/chat.rb', line 136

def extract_text_from_blocks(blocks)
  blocks.filter_map do |block|
    block['text'] if block['type'] == 'text' && block['text'].is_a?(String)
  end.join
end

.extract_think_tag_content(text) ⇒ Object



161
162
163
164
165
166
167
168
# File 'lib/ruby_llm/providers/openai/chat.rb', line 161

def extract_think_tag_content(text)
  return [text, nil] unless text.include?('<think>')

  thinking = text.scan(%r{<think>(.*?)</think>}m).join
  content = text.gsub(%r{<think>.*?</think>}m, '').strip

  [content.empty? ? nil : content, thinking.empty? ? nil : thinking]
end

.extract_thinking_from_blocks(blocks) ⇒ Object



142
143
144
145
146
147
148
# File 'lib/ruby_llm/providers/openai/chat.rb', line 142

def extract_thinking_from_blocks(blocks)
  blocks.filter_map do |block|
    next unless block['type'] == 'thinking'

    extract_thinking_text_from_block(block)
  end.join
end

.extract_thinking_signature(message_data) ⇒ Object



121
122
123
124
# File 'lib/ruby_llm/providers/openai/chat.rb', line 121

def extract_thinking_signature(message_data)
  candidate = message_data['reasoning_signature'] || message_data['signature']
  candidate.is_a?(String) ? candidate : nil
end

.extract_thinking_text(message_data) ⇒ Object



116
117
118
119
# File 'lib/ruby_llm/providers/openai/chat.rb', line 116

def extract_thinking_text(message_data)
  candidate = message_data['reasoning_content'] || message_data['reasoning'] || message_data['thinking']
  candidate.is_a?(String) ? candidate : nil
end

.extract_thinking_text_from_block(block) ⇒ Object



150
151
152
153
154
155
156
157
158
159
# File 'lib/ruby_llm/providers/openai/chat.rb', line 150

def extract_thinking_text_from_block(block)
  thinking_block = block['thinking']
  return thinking_block if thinking_block.is_a?(String)

  if thinking_block.is_a?(Array)
    return thinking_block.filter_map { |item| item['text'] if item['type'] == 'text' }.join
  end

  block['text'] if block['text'].is_a?(String)
end

.format_messages(messages) ⇒ Object



75
76
77
78
79
80
81
82
83
84
# File 'lib/ruby_llm/providers/openai/chat.rb', line 75

def format_messages(messages)
  messages.map do |msg|
    {
      role: format_role(msg.role),
      content: Media.format_content(msg.content),
      tool_calls: format_tool_calls(msg.tool_calls),
      tool_call_id: msg.tool_call_id
    }.compact.merge(format_thinking(msg))
  end
end

.format_role(role) ⇒ Object



86
87
88
89
90
91
92
93
# File 'lib/ruby_llm/providers/openai/chat.rb', line 86

def format_role(role)
  case role
  when :system
    @config.openai_use_system_role ? 'system' : 'developer'
  else
    role.to_s
  end
end

.format_thinking(msg) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/ruby_llm/providers/openai/chat.rb', line 101

def format_thinking(msg)
  return {} unless msg.role == :assistant

  thinking = msg.thinking
  return {} unless thinking

  payload = {}
  if thinking.text
    payload[:reasoning] = thinking.text
    payload[:reasoning_content] = thinking.text
  end
  payload[:reasoning_signature] = thinking.signature if thinking.signature
  payload
end

.parse_completion_response(response) ⇒ Object

Raises:



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/ruby_llm/providers/openai/chat.rb', line 44

def parse_completion_response(response)
  data = response.body
  return if data.empty?

  raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message')

  message_data = data.dig('choices', 0, 'message')
  return unless message_data

  usage = data['usage'] || {}
  cached_tokens = usage.dig('prompt_tokens_details', 'cached_tokens')
  thinking_tokens = usage.dig('completion_tokens_details', 'reasoning_tokens')
  content, thinking_from_blocks = extract_content_and_thinking(message_data['content'])
  thinking_text = thinking_from_blocks || extract_thinking_text(message_data)
  thinking_signature = extract_thinking_signature(message_data)

  Message.new(
    role: :assistant,
    content: content,
    thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
    tool_calls: parse_tool_calls(message_data['tool_calls']),
    input_tokens: usage['prompt_tokens'],
    output_tokens: usage['completion_tokens'],
    cached_tokens: cached_tokens,
    cache_creation_tokens: 0,
    thinking_tokens: thinking_tokens,
    model_id: data['model'],
    raw: response
  )
end

.render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) ⇒ Object

rubocop:disable Metrics/ParameterLists



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/ruby_llm/providers/openai/chat.rb', line 14

def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) # rubocop:disable Metrics/ParameterLists
  payload = {
    model: model.id,
    messages: format_messages(messages),
    stream: stream
  }

  payload[:temperature] = temperature unless temperature.nil?
  payload[:tools] = tools.map { |_, tool| tool_for(tool) } if tools.any?

  if schema
    strict = schema[:strict] != false

    payload[:response_format] = {
      type: 'json_schema',
      json_schema: {
        name: 'response',
        schema: schema,
        strict: strict
      }
    }
  end

  effort = resolve_effort(thinking)
  payload[:reasoning_effort] = effort if effort

  payload[:stream_options] = { include_usage: true } if stream
  payload
end

.resolve_effort(thinking) ⇒ Object



95
96
97
98
99
# File 'lib/ruby_llm/providers/openai/chat.rb', line 95

def resolve_effort(thinking)
  return nil unless thinking

  thinking.respond_to?(:effort) ? thinking.effort : thinking
end

Instance Method Details

#completion_urlObject



8
9
10
# File 'lib/ruby_llm/providers/openai/chat.rb', line 8

def completion_url
  'chat/completions'
end