omniai-openai 3.0.1 → 3.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf857f3e19a92c38074533033d27f0c72c38b8b241821964c6a5afdd43c9c0ea
4
- data.tar.gz: 99bf66f3fa309e921eb585ad4462090456d5eec6f55886695fd5214a78825ec6
3
+ metadata.gz: 6c84d3becf5f93c58078e182e156861262bafc112e59ccb2c67d6ff08d0bb399
4
+ data.tar.gz: 31cb3de11d80088728a2f80193903f2740db4fd13c058b72db780561b25fc317
5
5
  SHA512:
6
- metadata.gz: e5661128cc6388ae20b5eebdcac7eb31d7760fac041cd153ba8ceefa5f9c7461b9834f2c425d712ff15f87ce5f20ad83cf07187be99d7d22a999e56a49081eb4
7
- data.tar.gz: 9d3a37d7108178cb19dfe8573888eee036c4e635759f0c7b3ed6932d84dad16b1ce19cfbd3db36947587bf5e82d3d51ba6ec36736b97a120966ee451d8930d1a
6
+ metadata.gz: 8b789a962479764a1de021ee658cf27d82550e8d2699777bf7cc8294ffe099c9748ec4411d2296ece309e3dbd039685d0d2da0ed1f4380c573180fe042a7aa94
7
+ data.tar.gz: 180e1daaebcddb697f9e63d2b41e2adcd25a3e22e2845a184d43669497d415e350afeced8aa8cc99ec55c19dd67233685d034e138e80dcc0a26b7a089d216ca0
data/README.md CHANGED
@@ -146,6 +146,44 @@ JSON.parse(completion.content) # { "name": "Ringo" }
146
146
 
147
147
  > When using JSON mode, you must also instruct the model to produce JSON yourself via a system or user message.
148
148
 
149
+ #### Reasoning
150
+
151
+ OpenAI o1 and o3 models support reasoning, which provides a summary of the model's thought process.
152
+
153
+ ```ruby
154
+ # Enable reasoning with unified thinking API
155
+ response = client.chat("What is 25 * 25?", model: "o3-mini", thinking: true)
156
+
157
+ # Or use OpenAI-specific reasoning options
158
+ response = client.chat("What is 25 * 25?", model: "o3-mini", reasoning: { effort: "high", summary: "auto" })
159
+ ```
160
+
161
+ **Reasoning Effort Levels:**
162
+ - `low` - Minimal reasoning
163
+ - `medium` - Balanced reasoning
164
+ - `high` - Maximum reasoning effort
165
+
166
+ #### Accessing Reasoning Content
167
+
168
+ ```ruby
169
+ response.choices.first.message.contents.each do |content|
170
+ case content
171
+ when OmniAI::Chat::Thinking
172
+ puts "Reasoning: #{content.thinking}"
173
+ when OmniAI::Chat::Text
174
+ puts "Response: #{content.text}"
175
+ end
176
+ end
177
+ ```
178
+
179
+ #### Streaming with Reasoning
180
+
181
+ ```ruby
182
+ client.chat("What are the prime factors of 1234567?", model: "o3-mini", thinking: true, stream: $stdout)
183
+ ```
184
+
185
+ [OpenAI API Reference `reasoning`](https://platform.openai.com/docs/guides/reasoning)
186
+
149
187
  ### Transcribe
150
188
 
151
189
  A transcription is generated by passing in a path to a file:
@@ -8,10 +8,11 @@ module OmniAI
8
8
  # @param data [Hash]
9
9
  # @param context [Context]
10
10
  #
11
- # @return [OmniAI::Chat::Text, OmniAI::Chat::ToolCall]
11
+ # @return [OmniAI::Chat::Text, OmniAI::Chat::Thinking, OmniAI::Chat::ToolCall]
12
12
  def self.deserialize(data, context:)
13
13
  case data["type"]
14
14
  when /(input|output)_text/ then OmniAI::Chat::Text.deserialize(data, context:)
15
+ when "reasoning" then OmniAI::Chat::Thinking.deserialize(data, context:)
15
16
  end
16
17
  end
17
18
  end
@@ -30,11 +30,14 @@ module OmniAI
30
30
  # @param message [OmniAI::Chat::Message]
31
31
  # @param context [OmniAI::Context]
32
32
  #
33
- # @return [Hash]
33
+ # @return [Hash, nil]
34
34
  def self.serialize_for_content(message, context:)
35
35
  role = message.role
36
36
  direction = message.direction
37
- parts = arrayify(message.content)
37
+ # Filter out thinking content - OpenAI doesn't accept it in input messages
38
+ parts = arrayify(message.content).reject { |part| part.is_a?(OmniAI::Chat::Thinking) }
39
+
40
+ return if parts.empty?
38
41
 
39
42
  content = parts.map do |part|
40
43
  case part
@@ -54,9 +57,19 @@ module OmniAI
54
57
  case data["type"]
55
58
  when "message" then deserialize_for_content(data, context:)
56
59
  when "function_call" then deserialize_for_tool_call(data, context:)
60
+ when "reasoning" then deserialize_for_reasoning(data, context:)
57
61
  end
58
62
  end
59
63
 
64
+ # @param data [Hash]
65
+ # @param context [OmniAI::Context]
66
+ #
67
+ # @return [OmniAI::Chat::Message]
68
+ def self.deserialize_for_reasoning(data, context:)
69
+ thinking = OmniAI::Chat::Thinking.deserialize(data, context:)
70
+ OmniAI::Chat::Message.new(role: OmniAI::Chat::Role::ASSISTANT, content: [thinking])
71
+ end
72
+
60
73
  # @param data [Hash]
61
74
  # @param context [OmniAI::Context]
62
75
  #
@@ -25,7 +25,7 @@ module OmniAI
25
25
  # @return [OmniAI::Chat::Response]
26
26
  def self.deserialize(data, context:)
27
27
  usage = OmniAI::Chat::Usage.deserialize(data["usage"], context:) if data["usage"]
28
- choices = data["output"].map { |choice_data| OmniAI::Chat::Choice.deserialize(choice_data, context:) }
28
+ choices = data["output"].filter_map { |choice_data| OmniAI::Chat::Choice.deserialize(choice_data, context:) }
29
29
 
30
30
  OmniAI::Chat::Response.new(data:, choices:, usage:)
31
31
  end
@@ -13,10 +13,12 @@ module OmniAI
13
13
  response = {}
14
14
 
15
15
  @chunks.each do |chunk|
16
- parser.feed(chunk) do |type, data, _id|
16
+ parser.feed(chunk.b) do |type, data, _id|
17
17
  case type
18
- when /response\.(.*)_text\.delta/
18
+ when "response.output_text.delta"
19
19
  block.call(OmniAI::Chat::Delta.new(text: JSON.parse(data)["delta"]))
20
+ when "response.reasoning_summary_text.delta"
21
+ block.call(OmniAI::Chat::Delta.new(thinking: JSON.parse(data)["delta"]))
20
22
  when "response.completed"
21
23
  response = JSON.parse(data)["response"]
22
24
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniAI
4
+ module OpenAI
5
+ class Chat
6
+ # Overrides thinking serialize / deserialize.
7
+ module ThinkingSerializer
8
+ # @param data [Hash]
9
+ # @param context [Context]
10
+ #
11
+ # @return [OmniAI::Chat::Thinking]
12
+ def self.deserialize(data, context: nil) # rubocop:disable Lint/UnusedMethodArgument
13
+ summary = data["summary"]
14
+
15
+ thinking = case summary
16
+ when Array
17
+ summary.filter_map { |item| item["text"] if item["type"] == "summary_text" }.join("\n")
18
+ when String
19
+ summary
20
+ else
21
+ data["thinking"]
22
+ end
23
+
24
+ OmniAI::Chat::Thinking.new(thinking)
25
+ end
26
+
27
+ # @param thinking [OmniAI::Chat::Thinking]
28
+ # @param context [Context]
29
+ #
30
+ # @return [Hash]
31
+ def self.serialize(thinking, context: nil) # rubocop:disable Lint/UnusedMethodArgument
32
+ { type: "reasoning", summary: thinking.thinking }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -74,6 +74,8 @@ module OmniAI
74
74
  context.deserializers[:tool_call_result] = ToolCallResultSerializer.method(:deserialize)
75
75
  context.serializers[:tool_call_message] = ToolCallMessageSerializer.method(:serialize)
76
76
  context.deserializers[:tool_call_message] = ToolCallMessageSerializer.method(:deserialize)
77
+ context.serializers[:thinking] = ThinkingSerializer.method(:serialize)
78
+ context.deserializers[:thinking] = ThinkingSerializer.method(:deserialize)
77
79
  end
78
80
 
79
81
  protected
@@ -88,7 +90,7 @@ module OmniAI
88
90
  @prompt
89
91
  .messages
90
92
  .reject(&:system?)
91
- .map { |message| message.serialize(context:) }
93
+ .filter_map { |message| message.serialize(context:) }
92
94
  end
93
95
 
94
96
  # @return [String, nil]
@@ -155,9 +157,24 @@ module OmniAI
155
157
  end
156
158
 
157
159
  # @return [Hash]
160
+ # Accepts unified `thinking:` option and translates to OpenAI's `reasoning:` format.
161
+ # Example: `thinking: { effort: "high" }` becomes `reasoning: { effort: "high", summary: "auto" }`
158
162
  def reasoning
159
- options = @options.fetch(:reasoning, {})
160
- options unless options.empty?
163
+ # Support both native `reasoning:` and unified `thinking:` options
164
+ options = @options[:reasoning] || translate_thinking_to_reasoning
165
+ options unless options.nil? || options.empty?
166
+ end
167
+
168
+ # Translates unified thinking option to OpenAI reasoning format
169
+ # @return [Hash, nil]
170
+ def translate_thinking_to_reasoning
171
+ thinking = @options[:thinking]
172
+ return unless thinking
173
+
174
+ case thinking
175
+ when true then { effort: ReasoningEffort::HIGH, summary: "auto" }
176
+ when Hash then { summary: "auto" }.merge(thinking)
177
+ end
161
178
  end
162
179
 
163
180
  # @raise [ArgumentError]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAI
4
4
  module OpenAI
5
- VERSION = "3.0.1"
5
+ VERSION = "3.1.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniai-openai
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
@@ -83,6 +83,7 @@ files:
83
83
  - lib/omniai/openai/chat/response_serializer.rb
84
84
  - lib/omniai/openai/chat/stream.rb
85
85
  - lib/omniai/openai/chat/text_serializer.rb
86
+ - lib/omniai/openai/chat/thinking_serializer.rb
86
87
  - lib/omniai/openai/chat/tool_call_message_serializer.rb
87
88
  - lib/omniai/openai/chat/tool_call_result_serializer.rb
88
89
  - lib/omniai/openai/chat/tool_call_serializer.rb