diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82f7a55..b7f9808 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a3cdbc0..c7abe41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index dd22c1a..ad92604 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 @@ -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) diff --git a/README.md b/README.md index 1c8e12a..b3d77fc 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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", ... } # } # ] ``` @@ -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 @@ -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 @@ -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 } ``` diff --git a/ai-chat.gemspec b/ai-chat.gemspec index af9416b..2b71f02 100644 --- a/ai-chat.gemspec +++ b/ai-chat.gemspec @@ -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" diff --git a/examples/15_proxy.rb b/examples/15_proxy.rb index 2755502..64299f2 100644 --- a/examples/15_proxy.rb +++ b/examples/15_proxy.rb @@ -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 ===" diff --git a/examples/17_verbosity.rb b/examples/17_verbosity.rb index 360ff39..ff461a8 100644 --- a/examples/17_verbosity.rb +++ b/examples/17_verbosity.rb @@ -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] @@ -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 diff --git a/lib/ai/chat.rb b/lib/ai/chat.rb index 10ddc6c..6c18271 100644 --- a/lib/ai/chat.rb +++ b/lib/ai/chat.rb @@ -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) @@ -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} @@ -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 @@ -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 diff --git a/spec/unit/chat_spec.rb b/spec/unit/chat_spec.rb index 0d13b14..2c5af60 100644 --- a/spec/unit/chat_spec.rb +++ b/spec/unit/chat_spec.rb @@ -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