# frozen_string_literal: true RSpec.describe Dispatch::Adapter::Claude::RequestBuilder::Thinking do # Shorthand: build params and apply thinking config. def apply(model_id:, thinking:, tool_choice: nil, max_tokens: nil, max_output_tokens: nil) params = {} params[:max_tokens] = max_tokens if max_tokens described_class.apply( params, model_id:, thinking:, tool_choice:, max_output_tokens: ) params end # ── nil / false → no-op ────────────────────────────────────────────────── describe "when thinking is nil or false" do it "does not add thinking or output_config keys for nil" do params = apply(model_id: "claude-opus-4-7", thinking: nil) expect(params).not_to have_key(:thinking) expect(params).not_to have_key(:output_config) end it "does not add thinking or output_config keys for false" do params = apply(model_id: "claude-opus-4-7", thinking: false) expect(params).not_to have_key(:thinking) expect(params).not_to have_key(:output_config) end end # ── Opus 4.7 adaptive with display ─────────────────────────────────────── describe "Opus 4.7 (adaptive + display: summarized)" do let(:model_id) { "claude-opus-4-7" } it 'sets thinking: {type: "adaptive", display: "summarized"} for effort string "high"' do params = apply(model_id:, thinking: "high") expect(params[:thinking]).to eq({ type: "adaptive", display: "summarized" }) end it "sets output_config.effort to the provided effort level" do params = apply(model_id:, thinking: "high") expect(params[:output_config]).to eq({ effort: "high" }) end it 'sets effort "low" when thinking: "low"' do params = apply(model_id:, thinking: "low") expect(params[:output_config]).to eq({ effort: "low" }) end it 'sets effort "medium" when thinking: "medium"' do params = apply(model_id:, thinking: "medium") expect(params[:output_config]).to eq({ effort: "medium" }) end it 'sets effort "max" when thinking: "max"' do params = apply(model_id:, thinking: "max") expect(params[:output_config]).to eq({ effort: "max" }) end it 'sets effort "xhigh" when thinking: "xhigh"' do params = apply(model_id:, thinking: "xhigh") expect(params[:output_config]).to eq({ effort: "xhigh" }) end it "also works with the dated model ID claude-opus-4-7-20251018" do params = apply(model_id: "claude-opus-4-7-20251018", thinking: "high") expect(params[:thinking]).to eq({ type: "adaptive", display: "summarized" }) expect(params[:output_config]).to eq({ effort: "high" }) end it "does not set output_config when thinking kwarg is unrecognised junk" do params = apply(model_id:, thinking: "turbo") expect(params[:thinking]).to eq({ type: "adaptive", display: "summarized" }) expect(params).not_to have_key(:output_config) end end # ── Opus 4.6 adaptive without display ──────────────────────────────────── describe "Opus 4.6 (adaptive, no display)" do let(:model_id) { "claude-opus-4-6" } it 'sets thinking: {type: "adaptive"} without a display key' do params = apply(model_id:, thinking: "high") expect(params[:thinking]).to eq({ type: "adaptive" }) expect(params[:thinking]).not_to have_key(:display) end it "sets output_config.effort = 'high'" do params = apply(model_id:, thinking: "high") expect(params[:output_config]).to eq({ effort: "high" }) end end # ── Sonnet 4.6 adaptive without display ────────────────────────────────── describe "Sonnet 4.6 (adaptive, no display)" do let(:model_id) { "claude-sonnet-4-6" } it 'sets thinking: {type: "adaptive"} without a display key' do params = apply(model_id:, thinking: "medium") expect(params[:thinking]).to eq({ type: "adaptive" }) expect(params[:thinking]).not_to have_key(:display) end it "sets output_config.effort = 'medium'" do params = apply(model_id:, thinking: "medium") expect(params[:output_config]).to eq({ effort: "medium" }) end end # ── Sonnet 4.5 and older → enabled mode ────────────────────────────────── describe "Sonnet 4.5 (enabled mode)" do let(:model_id) { "claude-sonnet-4-5" } context "with a Hash {type: :enabled, budget_tokens: 8000}" do let(:thinking) { { type: :enabled, budget_tokens: 8000 } } it 'sets thinking: {type: "enabled", budget_tokens: 8000}' do params = apply(model_id:, thinking:) expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 8000 }) end it "does not set output_config" do params = apply(model_id:, thinking:) expect(params).not_to have_key(:output_config) end it "raises max_tokens to at least budget_tokens + OUTPUT_FALLBACK_BUFFER" do params = apply(model_id:, thinking:, max_tokens: 100) expected = 8000 + described_class::OUTPUT_FALLBACK_BUFFER expect(params[:max_tokens]).to eq(expected) end it "does not lower an already-sufficient max_tokens" do big = 8000 + described_class::OUTPUT_FALLBACK_BUFFER + 1000 params = apply(model_id:, thinking:, max_tokens: big) expect(params[:max_tokens]).to eq(big) end it "clamps raised max_tokens to max_output_tokens when provided" do # max_output_tokens = 9000 < budget + buffer = 12096 params = apply(model_id:, thinking: { type: :enabled, budget_tokens: 8000 }, max_tokens: 100, max_output_tokens: 9000) expect(params[:max_tokens]).to eq(9000) end end context "with a string effort level (unusual for older models)" do it 'maps effort string to a proper budget: "high" => 10_000 tokens' do params = apply(model_id:, thinking: "high") expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 10_000 }) end it 'maps "low" => 1_024 tokens' do params = apply(model_id:, thinking: "low") expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 1_024 }) end it 'maps "medium" => 4_000 tokens' do params = apply(model_id:, thinking: "medium") expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 4_000 }) end it 'maps unknown string to the "high" fallback budget (10_000)' do params = apply(model_id:, thinking: "superduper") expect(params[:thinking]).to eq({ type: "enabled", budget_tokens: 10_000 }) end end end # ── Opus 4.5 (enabled mode, another older model) ────────────────────────── describe "Opus 4.5 (enabled mode)" do it "uses enabled mode for claude-opus-4-5" do params = apply(model_id: "claude-opus-4-5", thinking: { type: :enabled, budget_tokens: 2000 }) expect(params[:thinking][:type]).to eq("enabled") expect(params[:thinking][:budget_tokens]).to eq(2000) end end # ── max_tokens guard: zero budget_tokens ────────────────────────────────── describe "max_tokens guard when budget_tokens is 0" do it "does NOT modify max_tokens when budget_tokens is 0" do params = apply( model_id: "claude-sonnet-4-5", thinking: { type: :enabled, budget_tokens: 0 }, max_tokens: 100 ) expect(params[:max_tokens]).to eq(100) end end # ── tool_choice: :any strips thinking ──────────────────────────────────── describe "tool_choice: :any strips thinking and output_config" do it "removes thinking and output_config for tool_choice: :any" do params = apply(model_id: "claude-opus-4-7", thinking: "high", tool_choice: :any) expect(params).not_to have_key(:thinking) expect(params).not_to have_key(:output_config) end it 'removes thinking and output_config for tool_choice: {type: :tool, name: "edit"}' do params = apply(model_id: "claude-opus-4-7", thinking: "high", tool_choice: { type: :tool, name: "edit" }) expect(params).not_to have_key(:thinking) expect(params).not_to have_key(:output_config) end it 'removes thinking for tool_choice: {type: "any"}' do params = apply(model_id: "claude-opus-4-7", thinking: "high", tool_choice: { type: "any" }) expect(params).not_to have_key(:thinking) end it "does NOT remove thinking for tool_choice: :auto" do params = apply(model_id: "claude-opus-4-7", thinking: "high", tool_choice: :auto) expect(params).to have_key(:thinking) end it "does NOT remove thinking for tool_choice: :none" do params = apply(model_id: "claude-opus-4-7", thinking: "high", tool_choice: :none) expect(params).to have_key(:thinking) end end # ── Hash thinking kwarg with effort key ─────────────────────────────────── describe "Hash thinking kwarg with effort: key (adaptive model)" do it "extracts effort from the hash and sets output_config" do params = apply(model_id: "claude-opus-4-7", thinking: { effort: "medium" }) expect(params[:output_config]).to eq({ effort: "medium" }) end it "ignores effort from hash when value is unrecognised" do params = apply(model_id: "claude-opus-4-7", thinking: { effort: "extreme" }) expect(params).not_to have_key(:output_config) end end # ── OUTPUT_FALLBACK_BUFFER constant ────────────────────────────────────── describe "OUTPUT_FALLBACK_BUFFER" do it "equals 4096" do expect(described_class::OUTPUT_FALLBACK_BUFFER).to eq(4096) end end end