Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## [Unreleased]

- Always send reasoning parameter with summary=auto to enable reasoning results for all models

## [0.0.7] - 2025-04-25

- Added support for reasoning effort parameter (`reasoning_effort`) with the OpenAI Responses API
Expand Down
38 changes: 28 additions & 10 deletions lib/ai/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
module AI
class Chat
attr_accessor :messages, :schema, :model
attr_reader :reasoning_effort
attr_reader :reasoning_effort, :reasoning_output
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new attr_reader 'reasoning_output' is declared but never used in the class. Consider removing or implementing its usage.

Suggested change
attr_reader :reasoning_effort, :reasoning_output
attr_reader :reasoning_effort


VALID_REASONING_EFFORTS = [:low, :medium, :high].freeze

Expand Down Expand Up @@ -125,13 +125,10 @@ def assistant!
"input" => messages
}

# Add reasoning parameter if specified
if !@reasoning_effort.nil?
# Convert symbol back to string for the API request
request_body_hash["reasoning"] = {
"effort" => @reasoning_effort.to_s
}
end
# Always add reasoning with summary=auto, and add effort if specified
reasoning_params = { "summary" => "auto" }
reasoning_params["effort"] = @reasoning_effort.to_s unless @reasoning_effort.nil?
request_body_hash["reasoning"] = reasoning_params

# Handle structured output (JSON schema)
if !schema.nil?
Expand Down Expand Up @@ -178,11 +175,18 @@ def assistant!
raise "OpenAI API Error: #{error_message}"
end

# Extract the text content from the response
# Extract the text content and reasoning from the response
content = ""
reasoning_data = nil

# Parse response according to the documented structure
if parsed_response.key?("output") && parsed_response["output"].is_a?(Array) && !parsed_response["output"].empty?
# Extract reasoning data if available
reasoning_item = parsed_response["output"].find { |item| item["type"] == "reasoning" }
if reasoning_item && reasoning_item.key?("summary")
reasoning_data = reasoning_item["summary"]
end

# Find the message output item, which may not be the first item when reasoning is used
message_output_item = parsed_response["output"].find { |item| item["type"] == "message" }

Expand All @@ -199,11 +203,25 @@ def assistant!
raise "Failed to extract content from response: #{parsed_response.inspect}"
end

messages.push({role: "assistant", content: content})
# Store the assistant message with reasoning data if available
message_data = {role: "assistant", content: content}
message_data[:reasoning] = reasoning_data if reasoning_data
messages.push(message_data)

schema.nil? ? content : JSON.parse(content)
end

# Get reasoning data for the last assistant message or a specific index
def reasoning(index = nil)
if index.nil?
# Get reasoning from the last assistant message
messages.reverse.find { |msg| msg[:role] == "assistant" && msg.key?(:reasoning) }&.fetch(:reasoning)
else
# Get reasoning from a specific message index
messages[index][:reasoning] if messages[index] && messages[index][:role] == "assistant" && messages[index].key?(:reasoning)
end
end

def inspect
"#<#{self.class.name} @messages=#{messages.inspect} @model=#{@model.inspect} @schema=#{@schema.inspect} @reasoning_effort=#{@reasoning_effort.inspect}>"
end
Expand Down