Skip to content
Merged
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: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Ruby Setup
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2.1"
ruby-version: "3.3"
bundler-cache: true

- name: Build
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- **Default model**: Changed from `gpt-5.2` to `gpt-5.6-terra`, the cost-balanced tier of OpenAI's GPT-5.6 family. Pricing is roughly the same as `gpt-5.2` ($2/$12 vs $1.75/$14 per 1M input/output tokens) with a newer model and a 1M-token context window. `generate_schema!` uses the same model.

- **Default `reasoning_effort` is now `"none"` instead of `nil`**: GPT-5.5 and newer models fall back to `"medium"` reasoning when the `reasoning` parameter is omitted, so keeping the old `nil` default would have silently made every default call slower and more expensive. The gem now sends `reasoning: { effort: "none" }` by default, which matches the previous behavior on `gpt-5.2`. Setting `reasoning_effort = nil` still omits the parameter entirely, which you will need for models that do not accept it (e.g., `gpt-4o`).

- **No reasoning summary requested when `reasoning_effort` is `"none"`**: `summary: "auto"` is only sent alongside a non-`"none"` effort.

- **Requires Ruby 3.3 or newer** (was 3.2). Ruby 3.2 reached end-of-life on March 31, 2026, and newer versions of the `openai` gem require 3.3+. CI now runs on Ruby 3.3. (Breaking for Ruby 3.2 users.)

- **Bumped `openai` runtime dependency from `~> 0.59` to `~> 0.80`**: Picks up the GPT-5.6 model slugs and a summer's worth of upstream SDK fixes. No gem code changes were required for the bump.

## [0.6.1] - 2026-04-22

### Added
Expand Down
7 changes: 4 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ PATH
base64 (~> 0.1, > 0.1.1)
json (~> 2.0)
marcel (~> 1.0)
openai (~> 0.59)
openai (~> 0.80)
tty-spinner (~> 0.9.3)

GEM
Expand Down Expand Up @@ -66,10 +66,11 @@ GEM
lint_roller (1.1.0)
logger (1.7.0)
marcel (1.1.0)
openai (0.59.0)
openai (0.80.0)
base64
cgi
connection_pool
connection_pool (>= 2.2.3)
logger
ostruct (0.6.3)
parallel (1.27.0)
parser (3.3.10.1)
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ ap response
# => {
# :role => "assistant",
# :content => "Matz is nice and so we are nice.",
# :response => { id: "resp_abc...", model: "gpt-5.2", ... }
# :response => { id: "resp_abc...", model: "gpt-5.6-terra", ... }
# }

ap chat.messages
Expand All @@ -68,7 +68,7 @@ ap chat.messages
# {
# :role => "assistant",
# :content => "Matz is nice and so we are nice.",
# :response => { id: "resp_abc...", model: "gpt-5.2", ... }
# :response => { id: "resp_abc...", model: "gpt-5.6-terra", ... }
# }
# ]
```
Expand Down Expand Up @@ -131,11 +131,11 @@ chat.add("Here's what I think...", role: "assistant")

### Model

The gem defaults to `gpt-5.2`. You can change it:
The gem defaults to `gpt-5.6-terra`. You can change it:

```ruby
chat = AI::Chat.new
chat.model = "gpt-4o"
chat.model = "gpt-5.6-luna"
```

### API Key
Expand Down Expand Up @@ -403,13 +403,15 @@ Control how much reasoning the model does before responding:

```ruby
chat = AI::Chat.new
chat.reasoning_effort = "high" # "low", "medium", or "high"
chat.reasoning_effort = "high" # "none", "low", "medium", "high", "xhigh", or "max"

chat.user("Explain the tradeoffs between microservices and monoliths.")
chat.generate!
```

By default, `reasoning_effort` is `nil` (no reasoning parameter is sent). For `gpt-5.2`, this is equivalent to no reasoning.
By default, `reasoning_effort` is `"none"`, so the model answers directly without spending reasoning tokens. This matters because GPT-5.5 and newer models (including the default `gpt-5.6-terra`) fall back to `"medium"` reasoning when the parameter is omitted, which is slower and more expensive.

Set `reasoning_effort = nil` to omit the reasoning parameter entirely — you will need this if you switch to a model that does not support reasoning (e.g., `gpt-4o`) or one that predates the `"none"` option.

### Verbosity

Expand Down Expand Up @@ -474,7 +476,7 @@ chat.generate!

response = chat.last[:response]
response[:id] # => "resp_abc123..."
response[:model] # => "gpt-5.2"
response[:model] # => "gpt-5.6-terra"
response[:usage] # => { input_tokens: 5, output_tokens: 7, total_tokens: 12 }
```

Expand Down
4 changes: 2 additions & 2 deletions ai-chat.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ Gem::Specification.new do |spec|
"source_code_uri" => "https://github.com/firstdraft/ai-chat"
}

spec.required_ruby_version = ">= 3.2"
spec.add_runtime_dependency "openai", "~> 0.59"
spec.required_ruby_version = ">= 3.3"
spec.add_runtime_dependency "openai", "~> 0.80"
spec.add_runtime_dependency "marcel", "~> 1.0"
spec.add_runtime_dependency "base64", "~> 0.1", "> 0.1.1"
spec.add_runtime_dependency "json", "~> 2.0"
Expand Down
2 changes: 1 addition & 1 deletion examples/15_proxy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

unless ENV["AICHAT_PROXY_KEY"]
puts "Skipping proxy tests - set AICHAT_PROXY_KEY environment variable to run these examples"
exit 0
return # not exit: all.rb require_relatives this file, and exit would skip the remaining examples
end

puts "\n=== AI::Chat Proxy Examples ==="
Expand Down
2 changes: 2 additions & 0 deletions examples/17_verbosity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
puts "-" * 30
chat6 = AI::Chat.new
chat6.model = "gpt-4.1-nano"
chat6.reasoning_effort = nil # gpt-4.1 models reject the reasoning parameter
chat6.verbosity = :medium
chat6.user("How high do planes typically fly?")
response = chat6.generate![:content]
Expand All @@ -91,6 +92,7 @@
puts "-" * 30
chat7 = AI::Chat.new
chat7.model = "gpt-4.1-nano"
chat7.reasoning_effort = nil
chat7.verbosity = :low
chat7.user("How high do planes typically fly?")
begin
Expand Down
15 changes: 11 additions & 4 deletions lib/ai/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ def initialize(api_key: nil, api_key_env_var: nil, proxy: nil)
@proxy = proxy.nil? ? ENV[PROXY_ENV]&.downcase == "true" : !!proxy
@api_key = resolve_api_key
@messages = []
@reasoning_effort = nil
@model = "gpt-5.2"
@reasoning_effort = "none"
@model = "gpt-5.6-terra"
client_options = {api_key: @api_key}
client_options[:base_url] = BASE_PROXY_URL if @proxy
@client = OpenAI::Client.new(**client_options)
Expand All @@ -57,7 +57,7 @@ def self.generate_schema!(description, location: "schema.json", api_key: nil, ap

client = OpenAI::Client.new(**options)
response = client.responses.create(
model: "gpt-5.2",
model: "gpt-5.6-terra",
input: [
{role: :system, content: system_prompt},
{role: :user, content: description}
Expand Down Expand Up @@ -307,6 +307,13 @@ def extract_filename(obj)
end
end

# Reasoning summaries are only meaningful when the model actually reasons.
def reasoning_parameters
params = {effort: reasoning_effort}
params[:summary] = "auto" unless reasoning_effort.to_s == "none"
params
end

def create_conversation
conversation = client.conversations.create
self.conversation_id = conversation.id
Expand All @@ -321,7 +328,7 @@ def create_response
parameters[:background] = background if background
parameters[:tools] = tools unless tools.empty?
parameters[:text] = schema if schema
parameters[:reasoning] = {effort: reasoning_effort, summary: "auto"} if reasoning_effort
parameters[:reasoning] = reasoning_parameters if reasoning_effort

create_conversation unless conversation_id
parameters[:conversation] = conversation_id
Expand Down
45 changes: 45 additions & 0 deletions spec/unit/chat_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -758,3 +758,48 @@ def schema_client_double
end
end
end

RSpec.describe AI::Chat, "request parameters" do
let(:chat) { AI::Chat.new(api_key: "test-key") }
let(:responses) { double("responses") }

before do
chat.conversation_id = "conv_123"
allow(chat).to receive(:client).and_return(double("client", responses: responses))
allow(responses).to receive(:create).and_return(double("response"))
end

it "defaults to gpt-5.6-terra" do
expect(chat.model).to eq("gpt-5.6-terra")
end

it "defaults reasoning_effort to \"none\"" do
expect(chat.reasoning_effort).to eq("none")
end

it "sends reasoning effort none without a summary by default" do
chat.send(:create_response)

expect(responses).to have_received(:create).with(
hash_including(model: "gpt-5.6-terra", reasoning: {effort: "none"})
)
end

it "requests a reasoning summary when reasoning_effort is set" do
chat.reasoning_effort = "high"
chat.send(:create_response)

expect(responses).to have_received(:create).with(
hash_including(reasoning: {effort: "high", summary: "auto"})
)
end

it "omits the reasoning parameter when reasoning_effort is nil" do
chat.reasoning_effort = nil
chat.send(:create_response)

expect(responses).to have_received(:create) do |params|
expect(params).not_to have_key(:reasoning)
end
end
end
Loading