diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index dc7cf84e..2d8744f8 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -22,7 +22,9 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n @config = context&.config || RubyLLM.config model_id = model || @config.default_model with_model(model_id, provider: provider, assume_exists: assume_model_exists) - @temperature = 0.7 + @thinking = @config.default_thinking + @thinking_budget = @config.default_thinking_budget + @temperature = @config.default_temperature @messages = [] @tools = {} @on = { @@ -63,6 +65,8 @@ def with_tools(*tools) def with_model(model_id, provider: nil, assume_exists: false) @model, @provider = Models.resolve(model_id, provider:, assume_exists:) @connection = @context ? @context.connection_for(@provider) : @provider.connection(@config) + # TODO: Currently the unsupported errors will not retrigger after model reassignment. + self end @@ -71,6 +75,18 @@ def with_temperature(temperature) self end + def with_thinking(thinking: true, budget: nil, temperature: 1) + raise UnsupportedThinkingError, "Model #{@model.id} doesn't support thinking" if thinking && !@model.thinking? + + @thinking = thinking + + # Most thinking models require set temperature so force it 1 here, however allowing override via param. + @temperature = temperature + @thinking_budget = budget if budget + + self + end + def with_context(context) @context = context @config = context.config @@ -98,6 +114,8 @@ def complete(&) tools: @tools, temperature: @temperature, model: @model.id, + thinking: @thinking, + thinking_budget: @thinking_budget, connection: @connection, &wrap_streaming_block(&) ) @@ -123,6 +141,10 @@ def reset_messages! @messages.clear end + def thinking? + @thinking + end + private def wrap_streaming_block(&block) diff --git a/lib/ruby_llm/configuration.rb b/lib/ruby_llm/configuration.rb index e8b4a663..42411824 100644 --- a/lib/ruby_llm/configuration.rb +++ b/lib/ruby_llm/configuration.rb @@ -28,6 +28,10 @@ class Configuration :default_model, :default_embedding_model, :default_image_model, + # Default model settings + :default_temperature, + :default_thinking, + :default_thinking_budget, # Connection configuration :request_timeout, :max_retries, @@ -55,6 +59,11 @@ def initialize @default_embedding_model = 'text-embedding-3-small' @default_image_model = 'dall-e-3' + # Default model settings + @default_thinking = false + @default_thinking_budget = 2048 + @default_temperature = 0.7 + # Logging configuration @log_file = $stdout @log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO diff --git a/lib/ruby_llm/error.rb b/lib/ruby_llm/error.rb index 228053a5..01948dbb 100644 --- a/lib/ruby_llm/error.rb +++ b/lib/ruby_llm/error.rb @@ -25,6 +25,7 @@ class InvalidRoleError < StandardError; end class ModelNotFoundError < StandardError; end class UnsupportedFunctionsError < StandardError; end class UnsupportedAttachmentError < StandardError; end + class UnsupportedThinkingError < StandardError; end # Error classes for different HTTP status codes class BadRequestError < Error; end diff --git a/lib/ruby_llm/message.rb b/lib/ruby_llm/message.rb index 74a9ac9a..cb337fa3 100644 --- a/lib/ruby_llm/message.rb +++ b/lib/ruby_llm/message.rb @@ -7,11 +7,12 @@ module RubyLLM class Message ROLES = %i[system user assistant tool].freeze - attr_reader :role, :tool_calls, :tool_call_id, :input_tokens, :output_tokens, :model_id + attr_reader :role, :tool_calls, :tool_call_id, :input_tokens, :output_tokens, :model_id, :thinking def initialize(options = {}) @role = options.fetch(:role).to_sym @content = normalize_content(options.fetch(:content)) + @thinking = options[:thinking] @tool_calls = options[:tool_calls] @input_tokens = options[:input_tokens] @output_tokens = options[:output_tokens] diff --git a/lib/ruby_llm/model/info.rb b/lib/ruby_llm/model/info.rb index 9c72bcdf..cdee5a2a 100644 --- a/lib/ruby_llm/model/info.rb +++ b/lib/ruby_llm/model/info.rb @@ -35,7 +35,7 @@ def supports?(capability) capabilities.include?(capability.to_s) end - %w[function_calling structured_output batch reasoning citations streaming].each do |cap| + %w[function_calling structured_output batch reasoning citations streaming thinking].each do |cap| define_method "#{cap}?" do supports?(cap) end diff --git a/lib/ruby_llm/models.json b/lib/ruby_llm/models.json index 5fd7a65c..f36d9e8d 100644 --- a/lib/ruby_llm/models.json +++ b/lib/ruby_llm/models.json @@ -182,7 +182,8 @@ ] }, "capabilities": [ - "function_calling" + "function_calling", + "thinking" ], "pricing": { "text_tokens": { @@ -317,7 +318,8 @@ ] }, "capabilities": [ - "function_calling" + "function_calling", + "thinking" ], "pricing": { "text_tokens": { @@ -349,7 +351,8 @@ ] }, "capabilities": [ - "function_calling" + "function_calling", + "thinking" ], "pricing": { "text_tokens": { @@ -1754,7 +1757,7 @@ "streaming", "function_calling", "structured_output", - "reasoning", + "thinking", "batch", "citations" ], @@ -1807,7 +1810,8 @@ "capabilities": [ "streaming", "function_calling", - "structured_output" + "structured_output", + "thinking" ], "pricing": { "text_tokens": { @@ -1858,7 +1862,8 @@ "capabilities": [ "streaming", "function_calling", - "structured_output" + "structured_output", + "thinking" ], "pricing": { "text_tokens": { @@ -27601,4 +27606,4 @@ ] } } -] \ No newline at end of file +] diff --git a/lib/ruby_llm/models.rb b/lib/ruby_llm/models.rb index b1fa08d7..035c6145 100644 --- a/lib/ruby_llm/models.rb +++ b/lib/ruby_llm/models.rb @@ -57,7 +57,7 @@ def resolve(model_id, provider: nil, assume_exists: false) # rubocop:disable Met id: model_id, name: model_id.gsub('-', ' ').capitalize, provider: provider.slug, - capabilities: %w[function_calling streaming], + capabilities: %w[function_calling streaming thinking], modalities: { input: %w[text image], output: %w[text] }, metadata: { warning: 'Assuming model exists, capabilities may not be accurate' } ) diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index 11b52f1a..5a59d874 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -10,13 +10,15 @@ module Provider module Methods extend Streaming - def complete(messages, tools:, temperature:, model:, connection:, &) + def complete(messages, tools:, temperature:, model:, thinking:, thinking_budget:, connection:, &) # rubocop:disable Metrics/ParameterLists normalized_temperature = maybe_normalize_temperature(temperature, model) payload = render_payload(messages, tools: tools, temperature: normalized_temperature, model: model, + thinking: thinking, + thinking_budget: thinking_budget, stream: block_given?) if block_given? diff --git a/lib/ruby_llm/providers/anthropic/capabilities.rb b/lib/ruby_llm/providers/anthropic/capabilities.rb index 78cbf50c..0cb8bd94 100644 --- a/lib/ruby_llm/providers/anthropic/capabilities.rb +++ b/lib/ruby_llm/providers/anthropic/capabilities.rb @@ -65,7 +65,7 @@ def supports_json_mode?(model_id) # @param model_id [String] the model identifier # @return [Boolean] true if the model supports extended thinking def supports_extended_thinking?(model_id) - model_id.match?(/claude-3-7-sonnet/) + model_id.match?(/claude-3-7-sonnet|claude-sonnet-4|claude-opus-4/) end # Determines the model family for a given model ID @@ -73,6 +73,8 @@ def supports_extended_thinking?(model_id) # @return [Symbol] the model family identifier def model_family(model_id) case model_id + when /claude-sonnet-4/ then 'claude-sonnet-4' + when /claude-opus-4/ then 'claude-opus-4' when /claude-3-7-sonnet/ then 'claude-3-7-sonnet' when /claude-3-5-sonnet/ then 'claude-3-5-sonnet' when /claude-3-5-haiku/ then 'claude-3-5-haiku' @@ -131,17 +133,17 @@ def capabilities_for(model_id) capabilities = ['streaming'] # Function calling for Claude 3+ - if model_id.match?(/claude-3/) + if model_id.match?(/claude-3|claude-sonnet-4|claude-opus-4/) capabilities << 'function_calling' capabilities << 'structured_output' capabilities << 'batch' end - # Extended thinking (reasoning) for Claude 3.7 - capabilities << 'reasoning' if model_id.match?(/claude-3-7/) + # Extended thinking for Claude 3.7 and Claude 4 + capabilities << 'thinking' if supports_extended_thinking?(model_id) # Citations - capabilities << 'citations' if model_id.match?(/claude-3\.5|claude-3-7/) + capabilities << 'citations' if model_id.match?(/claude-3\.5|claude-3-7|claude-sonnet-4|claude-opus-4/) capabilities end @@ -161,10 +163,10 @@ def pricing_for(model_id) output_per_million: prices[:output] * 0.5 } - # Add reasoning output pricing for 3.7 models - if model_id.match?(/claude-3-7/) - standard_pricing[:reasoning_output_per_million] = prices[:output] * 2.5 - batch_pricing[:reasoning_output_per_million] = prices[:output] * 1.25 + # Add thinking output pricing for 3.7 and 4 models + if model_id.match?(/claude-3-7|claude-sonnet-4|claude-opus-4/) + standard_pricing[:thinking_output_per_million] = prices[:output] * 2.5 + batch_pricing[:thinking_output_per_million] = prices[:output] * 1.25 end { diff --git a/lib/ruby_llm/providers/anthropic/chat.rb b/lib/ruby_llm/providers/anthropic/chat.rb index 2ba96009..3b7f7995 100644 --- a/lib/ruby_llm/providers/anthropic/chat.rb +++ b/lib/ruby_llm/providers/anthropic/chat.rb @@ -11,12 +11,12 @@ def completion_url '/v1/messages' end - def render_payload(messages, tools:, temperature:, model:, stream: false) + def render_payload(messages, tools:, temperature:, model:, thinking:, thinking_budget:, stream: false) # rubocop:disable Metrics/ParameterLists system_messages, chat_messages = separate_messages(messages) system_content = build_system_content(system_messages) build_base_payload(chat_messages, temperature, model, stream).tap do |payload| - add_optional_fields(payload, system_content:, tools:) + add_optional_fields(payload, system_content:, tools:, thinking:, thinking_budget:) end end @@ -45,19 +45,33 @@ def build_base_payload(chat_messages, temperature, model, stream) } end - def add_optional_fields(payload, system_content:, tools:) + def add_optional_fields(payload, system_content:, tools:, thinking:, thinking_budget:) payload[:tools] = tools.values.map { |t| Tools.function_for(t) } if tools.any? payload[:system] = system_content unless system_content.empty? + return unless thinking + + payload[:thinking] = { + type: 'enabled', + budget_tokens: thinking_budget + } end def parse_completion_response(response) data = response.body + RubyLLM.logger.debug("Anthropic response: #{data}") + content_blocks = data['content'] || [] + thinking_content = extract_thinking_content(content_blocks) text_content = extract_text_content(content_blocks) tool_use = Tools.find_tool_use(content_blocks) - build_message(data, text_content, tool_use) + build_message(data, text_content, tool_use, thinking_content) + end + + def extract_thinking_content(blocks) + thinking_blocks = blocks.select { |c| c['type'] == 'thinking' } + thinking_blocks.map { |c| c['thinking'] }.join end def extract_text_content(blocks) @@ -65,10 +79,11 @@ def extract_text_content(blocks) text_blocks.map { |c| c['text'] }.join end - def build_message(data, content, tool_use) + def build_message(data, content, tool_use, thinking_content) Message.new( role: :assistant, content: content, + thinking: thinking_content, tool_calls: Tools.parse_tool_calls(tool_use), input_tokens: data.dig('usage', 'input_tokens'), output_tokens: data.dig('usage', 'output_tokens'), diff --git a/lib/ruby_llm/providers/anthropic/streaming.rb b/lib/ruby_llm/providers/anthropic/streaming.rb index 3bf84215..5a0bc01f 100644 --- a/lib/ruby_llm/providers/anthropic/streaming.rb +++ b/lib/ruby_llm/providers/anthropic/streaming.rb @@ -16,6 +16,7 @@ def build_chunk(data) role: :assistant, model_id: extract_model_id(data), content: data.dig('delta', 'text'), + thinking: data.dig('delta', 'thinking'), input_tokens: extract_input_tokens(data), output_tokens: extract_output_tokens(data), tool_calls: extract_tool_calls(data) diff --git a/lib/ruby_llm/providers/bedrock/capabilities.rb b/lib/ruby_llm/providers/bedrock/capabilities.rb index 342c6789..97626300 100644 --- a/lib/ruby_llm/providers/bedrock/capabilities.rb +++ b/lib/ruby_llm/providers/bedrock/capabilities.rb @@ -108,6 +108,10 @@ def supports_structured_output?(model_id) model_id.match?(/anthropic\.claude/) end + def supports_extended_thinking?(model_id) + model_id.match?(/claude-3-7-sonnet|claude-sonnet-4|claude-opus-4/) + end + # Model family patterns for capability lookup MODEL_FAMILIES = { /anthropic\.claude-3-opus/ => :claude3_opus, @@ -117,7 +121,9 @@ def supports_structured_output?(model_id) /anthropic\.claude-3-haiku/ => :claude3_haiku, /anthropic\.claude-3-5-haiku/ => :claude3_5_haiku, /anthropic\.claude-v2/ => :claude2, - /anthropic\.claude-instant/ => :claude_instant + /anthropic\.claude-instant/ => :claude_instant, + /anthropic\.claude-sonnet-4/ => :claude_sonnet4, + /anthropic\.claude-opus-4/ => :claude_opus4 }.freeze # Determines the model family for pricing and capability lookup @@ -187,8 +193,8 @@ def capabilities_for(model_id) capabilities << 'structured_output' if supports_json_mode?(model_id) - # Extended thinking for 3.7 models - capabilities << 'reasoning' if model_id.match?(/claude-3-7/) + # Extended thinking for 3.7, and 4 models + capabilities << 'thinking' if supports_extended_thinking?(model_id) # Batch capabilities for newer Claude models if model_id.match?(/claude-3\.5|claude-3-7/) diff --git a/lib/ruby_llm/providers/bedrock/chat.rb b/lib/ruby_llm/providers/bedrock/chat.rb index 74257955..9d5486d4 100644 --- a/lib/ruby_llm/providers/bedrock/chat.rb +++ b/lib/ruby_llm/providers/bedrock/chat.rb @@ -39,7 +39,7 @@ def completion_url "model/#{@model_id}/invoke" end - def render_payload(messages, tools:, temperature:, model:, stream: false) # rubocop:disable Lint/UnusedMethodArgument + def render_payload(messages, tools:, temperature:, model:, thinking:, thinking_budget:, **) # rubocop:disable Metrics/ParameterLists # Hold model_id in instance variable for use in completion_url and stream_url @model_id = model @@ -47,7 +47,7 @@ def render_payload(messages, tools:, temperature:, model:, stream: false) # rubo system_content = Anthropic::Chat.build_system_content(system_messages) build_base_payload(chat_messages, temperature, model).tap do |payload| - Anthropic::Chat.add_optional_fields(payload, system_content:, tools:) + Anthropic::Chat.add_optional_fields(payload, system_content:, tools:, thinking:, thinking_budget:) end end diff --git a/lib/ruby_llm/providers/gemini/chat.rb b/lib/ruby_llm/providers/gemini/chat.rb index d6ba1696..bb0bd525 100644 --- a/lib/ruby_llm/providers/gemini/chat.rb +++ b/lib/ruby_llm/providers/gemini/chat.rb @@ -11,7 +11,7 @@ def completion_url "models/#{@model}:generateContent" end - def render_payload(messages, tools:, temperature:, model:, stream: false) # rubocop:disable Lint/UnusedMethodArgument + def render_payload(messages, tools:, temperature:, model:, **) @model = model # Store model for completion_url/stream_url payload = { contents: format_messages(messages), diff --git a/lib/ruby_llm/providers/openai/chat.rb b/lib/ruby_llm/providers/openai/chat.rb index 697442b2..b33cfffc 100644 --- a/lib/ruby_llm/providers/openai/chat.rb +++ b/lib/ruby_llm/providers/openai/chat.rb @@ -11,7 +11,7 @@ def completion_url module_function - def render_payload(messages, tools:, temperature:, model:, stream: false) + def render_payload(messages, tools:, temperature:, model:, stream: false, **) # rubocop:disable Metrics/ParameterLists payload = { model: model, messages: format_messages(messages), diff --git a/lib/ruby_llm/stream_accumulator.rb b/lib/ruby_llm/stream_accumulator.rb index 7fca306a..9eeb10b2 100644 --- a/lib/ruby_llm/stream_accumulator.rb +++ b/lib/ruby_llm/stream_accumulator.rb @@ -9,6 +9,7 @@ class StreamAccumulator def initialize @content = String.new + @thinking = String.new @tool_calls = {} @input_tokens = 0 @output_tokens = 0 @@ -23,6 +24,7 @@ def add(chunk) accumulate_tool_calls chunk.tool_calls else @content << (chunk.content || '') + @thinking << (chunk.thinking || '') end count_tokens chunk @@ -33,6 +35,7 @@ def to_message Message.new( role: :assistant, content: content.empty? ? nil : content, + thinking: @thinking.empty? ? nil : @thinking, model_id: model_id, tool_calls: tool_calls_from_stream, input_tokens: @input_tokens.positive? ? @input_tokens : nil, diff --git a/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-3-7-sonnet_can_handle_basic_conversation_with_thinking_enabled.yml b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-3-7-sonnet_can_handle_basic_conversation_with_thinking_enabled.yml new file mode 100644 index 00000000..9130d4f3 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-3-7-sonnet_can_handle_basic_conversation_with_thinking_enabled.yml @@ -0,0 +1,88 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-7-sonnet-20250219","messages":[{"role":"user","content":[{"type":"text","text":"What''s + 2 + 2? Think through this step by step."}]}],"temperature":1,"stream":false,"max_tokens":64000,"thinking":{"type":"enabled","budget_tokens":2048}}' + headers: + User-Agent: + - Faraday v2.13.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 18 Jul 2025 05:38:50 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-07-18T05:38:47Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-07-18T05:38:51Z' + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-07-18T05:38:47Z' + Anthropic-Ratelimit-Tokens-Limit: + - '28000' + Anthropic-Ratelimit-Tokens-Remaining: + - '28000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-07-18T05:38:47Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - a2bc4b1c-6bf0-4b62-b1f6-a1d9821a5e3a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01H8phMkgUJm2jRb6mjh1TyE","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[{"type":"thinking","thinking":"This + is a very basic arithmetic problem. Let''s solve it step by step.\n\n2 + 2 + means I need to add the number 2 with the number 2.\n\nTo add these numbers, + I can think of it as:\n- Starting with 2\n- Then adding 2 more\n\nSo:\n2 + + 2 = 4\n\nThat''s the answer: 4.","signature":"ErUBCkYIBRgCIkDYEWR6TDzVtqVJyMdFWFx9CUzT61wYklKVzM6g2GYLr0biNf88UKUY851WNn5+NWQ5BImkpVnBXuPrXYTLXLeoEgwajyIH8En4csCcQXMaDLvR03WVgYn4llf4SyIw9obvmcjnLQiqW1pJbZAYyBVChfUdu+4geQF/17LMVmH0j5hHgJOFlxeJRkKixyZQKh3F2jQ9AjbxG8PVSclPOFIi/2Ckm7Pgy8dzCm/fNxgC"},{"type":"text","text":"To + solve 2 + 2, I''ll break it down:\n\n1. I need to add the number 2 with another + number 2\n2. Addition means combining quantities together\n3. If I have 2 + items and get 2 more items, I''ll have a total of 4 items\n4. Therefore, 2 + + 2 = 4\n\nThe answer is 4."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":51,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":190,"service_tier":"standard"}}' + recorded_at: Fri, 18 Jul 2025 05:38:50 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-3-7-sonnet_maintains_thinking_mode_across_multiple_turns.yml b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-3-7-sonnet_maintains_thinking_mode_across_multiple_turns.yml new file mode 100644 index 00000000..40756a8c --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-3-7-sonnet_maintains_thinking_mode_across_multiple_turns.yml @@ -0,0 +1,162 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-7-sonnet-20250219","messages":[{"role":"user","content":[{"type":"text","text":"What''s + 5 + 3?"}]}],"temperature":1,"stream":false,"max_tokens":64000,"thinking":{"type":"enabled","budget_tokens":2048}}' + headers: + User-Agent: + - Faraday v2.13.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 18 Jul 2025 05:38:52 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-07-18T05:38:51Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-07-18T05:38:52Z' + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-07-18T05:38:51Z' + Anthropic-Ratelimit-Tokens-Limit: + - '28000' + Anthropic-Ratelimit-Tokens-Remaining: + - '28000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-07-18T05:38:51Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - a2bc4b1c-6bf0-4b62-b1f6-a1d9821a5e3a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01UVtt5CXncUaY5eMdZz5yXk","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[{"type":"thinking","thinking":"This + is a simple addition problem.\n\n5 + 3 = 8\n\nSo the answer is 8.","signature":"ErUBCkYIBRgCIkCj2IP3xc6RELPu8t9/N1bormyfIlJHWjS35zy3nE/PwNDRooJm43bzvCxFsJji6R7cAImgnsSKdSadE5UTSxybEgxzjihH9btpHvFvYd4aDM2QOkD1VQddEb3W+yIwiBdK30embODvJqFB4RG45ySacd2jmju/7B8PWp2NyZkdtlKXnrA7U3eqqgGqY2rCKh1JUx4LQqzYQjbVSvZjQ7bOCIkln9tg6ZUJFiWCvBgC"},{"type":"text","text":"The + sum of 5 + 3 is 8."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":44,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":51,"service_tier":"standard"}}' + recorded_at: Fri, 18 Jul 2025 05:38:52 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-7-sonnet-20250219","messages":[{"role":"user","content":[{"type":"text","text":"What''s + 5 + 3?"}]},{"role":"assistant","content":[{"type":"text","text":"The sum of + 5 + 3 is 8."}]},{"role":"user","content":[{"type":"text","text":"Now multiply + that result by 2"}]}],"temperature":1,"stream":false,"max_tokens":64000,"thinking":{"type":"enabled","budget_tokens":2048}}' + headers: + User-Agent: + - Faraday v2.13.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 18 Jul 2025 05:38:54 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-07-18T05:38:53Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-07-18T05:38:54Z' + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-07-18T05:38:53Z' + Anthropic-Ratelimit-Tokens-Limit: + - '28000' + Anthropic-Ratelimit-Tokens-Remaining: + - '28000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-07-18T05:38:53Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - a2bc4b1c-6bf0-4b62-b1f6-a1d9821a5e3a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6Im1zZ18wMUVHVjc0ZTlhOUpkaXFHY0J6Z1hGNWoiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJtb2RlbCI6ImNsYXVkZS0zLTctc29ubmV0LTIwMjUwMjE5IiwiY29udGVudCI6W3sidHlwZSI6InRoaW5raW5nIiwidGhpbmtpbmciOiJJIG5lZWQgdG8gbXVsdGlwbHkgdGhlIHByZXZpb3VzIHJlc3VsdCAoOCkgYnkgMi5cblxuOCDDlyAyID0gMTYiLCJzaWduYXR1cmUiOiJFclVCQ2tZSUJSZ0NJa0NDeEhjQXVEcU1MMTlCVytFSjNxQmhjKyt0S0w2TlpHNlprN1h6NTNqeDQyVGxybi9xSTNVenVMZE9ZeVdFVm1OekR5T1cvdUhLMS9oMEV1TWxQZVBoRWd3TzlzV25IclJCV0lVOGZJOGFERHVjbXcxcnZndXNKVzRXS2lJdys1elY1NGw2dlRkaU9sOWhsRGVTNTlMUk9od2Q5d2VUb1Y0QTRFbmpKYTJSWlFuTjJGY3VHTVNBK3FvNE5PVy9LaDNyT3RiQzNlUGVvQ25LdkMyS01zaVNpTkliOWxKY2hQT01tQ0hYaFJnQyJ9LHsidHlwZSI6InRleHQiLCJ0ZXh0IjoiVG8gbXVsdGlwbHkgdGhlIHByZXZpb3VzIHJlc3VsdCBieSAyOlxuXG44IMOXIDIgPSAxNlxuXG5UaGUgYW5zd2VyIGlzIDE2LiJ9XSwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6NzEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnMiOjYyLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9fQ== + recorded_at: Fri, 18 Jul 2025 05:38:54 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-opus-4_can_handle_basic_conversation_with_thinking_enabled.yml b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-opus-4_can_handle_basic_conversation_with_thinking_enabled.yml new file mode 100644 index 00000000..b3a4e092 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-opus-4_can_handle_basic_conversation_with_thinking_enabled.yml @@ -0,0 +1,81 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-opus-4-20250514","messages":[{"role":"user","content":[{"type":"text","text":"What''s + 2 + 2? Think through this step by step."}]}],"temperature":1,"stream":false,"max_tokens":32000,"thinking":{"type":"enabled","budget_tokens":2048}}' + headers: + User-Agent: + - Faraday v2.13.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 18 Jul 2025 05:39:23 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-07-18T05:39:17Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '4000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '4000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-07-18T05:39:27Z' + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-07-18T05:39:16Z' + Anthropic-Ratelimit-Tokens-Limit: + - '24000' + Anthropic-Ratelimit-Tokens-Remaining: + - '24000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-07-18T05:39:17Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - a2bc4b1c-6bf0-4b62-b1f6-a1d9821a5e3a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6Im1zZ18wMTlDMnA3cVdudzQ3RFpBeXNyc2JlUGkiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJtb2RlbCI6ImNsYXVkZS1vcHVzLTQtMjAyNTA1MTQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGhpbmtpbmciLCJ0aGlua2luZyI6IlRoaXMgaXMgYSB2ZXJ5IHNpbXBsZSBhcml0aG1ldGljIHF1ZXN0aW9uLiBUaGUgdXNlciBpcyBhc2tpbmcgbWUgdG8gYWRkIDIgKyAyLCBidXQgdGhleSd2ZSBhbHNvIGFza2VkIG1lIHRvIHRoaW5rIHRocm91Z2ggaXQgc3RlcCBieSBzdGVwLiBFdmVuIHRob3VnaCB0aGlzIGlzIGVsZW1lbnRhcnksIEkgc2hvdWxkIGhvbm9yIHRoZWlyIHJlcXVlc3QgdG8gc2hvdyB0aGUgc3RlcHMuXG5cbjIgKyAyID0gNFxuXG5JIGNhbiBleHBsYWluIHRoaXMgaW4gYSBmZXcgd2F5czpcbi0gQ291bnRpbmcgdXA6IFN0YXJ0aW5nIGF0IDIsIGNvdW50IHVwIDIgbW9yZTogMywgNFxuLSBHcm91cGluZzogSWYgeW91IGhhdmUgMiBpdGVtcyBhbmQgYWRkIDIgbW9yZSBpdGVtcywgeW91IGhhdmUgNCBpdGVtcyB0b3RhbFxuLSBOdW1iZXIgbGluZTogU3RhcnRpbmcgYXQgMiBvbiBhIG51bWJlciBsaW5lLCBtb3ZlIDIgc3BhY2VzIHRvIHRoZSByaWdodCwgbGFuZGluZyBvbiA0Iiwic2lnbmF0dXJlIjoiRW9vRkNrWUlCUmdDS2tBWEpSVXVJdTlXT00za1NySTl0WHFJRkxwZEhudHg2NWxuc1N3cmIzRU0vbjZpTFd2MmZTVDlFSERvWU1seEtaLzlWZGx3bm04bnhLSHFtYVVwb0NOakVnekJIWVREYlBzajNVQTZ5STRhRExhL2IxSGtaWE5QVmU3U2Z5SXc5OTlucm0zazN1azZkczlES3dSMmhQOFdrbHpGZnBoWHptOFZYdkNJK1AySHRLUmJmYncwc0hrNlFVTGdhQXVoS3ZFRHFITVovd3I0OTFEMHh4Y1hyL2JmRUxuYXFyUkhlbVZvZ2NMc2hlTWxWeDVNdUJURThLM3pYekZQZ1B1ZU9XeFNZb1FFcnVCUnQ4QzN1VjQwTTArMjFsZE5zYndwbEl4QWJhQ2s3dTdteHR2eTRPeFhzVllRTU05UVYwZzlyWFBEY1lFZzR1YTJ1N1B5RXZZTkNhRDZHWVZ2MFp2TU9mSGxxTm11T1FuZGRIYlRac1EreGhJeE5Cb1pwbWVQeU9uSFQwRXkvbUxxN0ppOWdBVlZ2QThlejcwODdlMjIxdW5sS0NMZFVGSjFPNlFWd2ZZTGRIZjBqYzNRR3l4ODlOWXVZamE2MUlRTTlReEJkekx5dUFaYmV4QjBTK3hreWVKdk9xNFA5N05CdjBWT29Zblc5bklPZW1rdDc2Sis5U3RBVnJjT01CRzdaa1ZtNWNPa0ZVS0VsT0o5SVI3SzFPWTZHd2dLbDJpcUFCLzBpcVB1Y01qZnhaQkV4OFRIcjVpMmtLeUtUbHFRK3k5WUpmeWlKcS9BRXMrRW5wTUJDNUxRK2I1dERud0lTeEV2eDNGcnpUNnE0WVcxMU8yVWgvVkdGS0RUUEY4NkZyUDVFaUJBNXFPVW9LUW55NWtlK2trbTM5MWo3Q0dXY1FHWmxkSkpEZnNhUHQ0a0x0WGdvcXR6ZWZjcE9FR1R5dGFRRWxidGhBWjFyaS91ZHh1Rk9FRnZyWm41VExnY0Zpd0lnd2xKamIrb2dnbHkxWmRJUGcrRFI3blVrSEllbHB4L2gvamhZR1dlM3drYXFUMEpINzZ5L3JGTkd0NHpZZ0dVMElwM2xydXdFS0NVbEtYVC90Q1lCZzhBL1N1TkNMNk9IUGFOVFRVaFZORVlBUT09In0seyJ0eXBlIjoidGV4dCIsInRleHQiOiJJJ2xsIHdvcmsgdGhyb3VnaCB0aGlzIGFkZGl0aW9uIHN0ZXAgYnkgc3RlcC5cblxuU3RhcnRpbmcgd2l0aDogMiArIDJcblxuU3RlcCAxOiBUYWtlIHRoZSBmaXJzdCBudW1iZXIsIHdoaWNoIGlzIDJcblN0ZXAgMjogQWRkIHRoZSBzZWNvbmQgbnVtYmVyLCB3aGljaCBpcyBhbHNvIDJcblN0ZXAgMzogQ291bnQgdXAgZnJvbSAyOiBcbi0gU3RhcnQgYXQgMlxuLSBBZGQgMSDihpIgZ2V0IDNcbi0gQWRkIDEgbW9yZSDihpIgZ2V0IDRcblxuVGhlcmVmb3JlOiAyICsgMiA9IDRcblxuQW5vdGhlciB3YXkgdG8gdGhpbmsgYWJvdXQgaXQ6IElmIHlvdSBoYXZlIDIgb2JqZWN0cyBhbmQgc29tZW9uZSBnaXZlcyB5b3UgMiBtb3JlIG9iamVjdHMsIHlvdSBub3cgaGF2ZSA0IG9iamVjdHMgdG90YWwuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo1MSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2VucyI6Mjk4LCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9fQ== + recorded_at: Fri, 18 Jul 2025 05:39:23 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-opus-4_maintains_thinking_mode_across_multiple_turns.yml b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-opus-4_maintains_thinking_mode_across_multiple_turns.yml new file mode 100644 index 00000000..64f17018 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-opus-4_maintains_thinking_mode_across_multiple_turns.yml @@ -0,0 +1,161 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-opus-4-20250514","messages":[{"role":"user","content":[{"type":"text","text":"What''s + 5 + 3?"}]}],"temperature":1,"stream":false,"max_tokens":32000,"thinking":{"type":"enabled","budget_tokens":2048}}' + headers: + User-Agent: + - Faraday v2.13.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 18 Jul 2025 05:39:26 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-07-18T05:39:26Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '4000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '4000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-07-18T05:39:27Z' + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-07-18T05:39:25Z' + Anthropic-Ratelimit-Tokens-Limit: + - '24000' + Anthropic-Ratelimit-Tokens-Remaining: + - '24000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-07-18T05:39:26Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - a2bc4b1c-6bf0-4b62-b1f6-a1d9821a5e3a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01YXCT7MgRWQbxwwfs1bWhN2","type":"message","role":"assistant","model":"claude-opus-4-20250514","content":[{"type":"thinking","thinking":"This + is a simple arithmetic question. 5 + 3 = 8.","signature":"EtgBCkYIBRgCKkCyqtg4YSovHjJWjT5xWNBV0HDNY0NkeiSISwchPehu+JHqF14GKTlprSnmlk1ohL26KlGnQRhwg33jqkxTjsJiEgz7IAVT6nqF9r6eMC8aDDtFLpYkLlDKnJjnpSIwvHR9G483A2OajVNq3vWQr7SfmZ7p5CnDQNuZp/QkVIQMc8IGCOtLX15SWVC2HKeaKkAcQTrZsHVQM5K8hKFfSDAEngoOyzJ0kus67m+ETlZZL4r1WFKIc9VoOMlD0yej7XnEFDlG1Ck5oCzPH1qrNKD1GAE="},{"type":"text","text":"5 + + 3 = 8"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":44,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":39,"service_tier":"standard"}}' + recorded_at: Fri, 18 Jul 2025 05:39:26 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-opus-4-20250514","messages":[{"role":"user","content":[{"type":"text","text":"What''s + 5 + 3?"}]},{"role":"assistant","content":[{"type":"text","text":"5 + 3 = 8"}]},{"role":"user","content":[{"type":"text","text":"Now + multiply that result by 2"}]}],"temperature":1,"stream":false,"max_tokens":32000,"thinking":{"type":"enabled","budget_tokens":2048}}' + headers: + User-Agent: + - Faraday v2.13.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 18 Jul 2025 05:39:29 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '20000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-07-18T05:39:28Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '4000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '4000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-07-18T05:39:29Z' + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-07-18T05:39:28Z' + Anthropic-Ratelimit-Tokens-Limit: + - '24000' + Anthropic-Ratelimit-Tokens-Remaining: + - '24000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-07-18T05:39:28Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - a2bc4b1c-6bf0-4b62-b1f6-a1d9821a5e3a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6Im1zZ18wMVdLY24zeGNYMlJ1Y2JieHdGdTQ0NWgiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJtb2RlbCI6ImNsYXVkZS1vcHVzLTQtMjAyNTA1MTQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGhpbmtpbmciLCJ0aGlua2luZyI6IlRoZSBwcmV2aW91cyByZXN1bHQgd2FzIDggKGZyb20gNSArIDMpLiBOb3cgSSBuZWVkIHRvIG11bHRpcGx5IDggYnkgMi5cbjggw5cgMiA9IDE2Iiwic2lnbmF0dXJlIjoiRXZvQkNrWUlCUmdDS2tDSDBkcWh3Qi9rRUttajBUcHJlellxVmpuOHZES1FLQ3FwK2NNeFBCbHoxb096TFdrNDF0ZUFRYkJ0cnd3bkp4QXA4OUQ4VkdNemxIWEZIa21xYTZKTUVnd2w0MENqSExySDVmV2hxRGdhREptdzdDWXNPTUNUMnVhZDZ5SXdLcmhIb3N3dnNsUmE2UFMxYmZ4VVlhVTk2Rk92WSt2Y2QwRThWV1RYQUN3bHFqWHZkZFJ5d01qckJDbWpPenhZS21LL1N1dnM1dGo4ZWE3Wll0THdzODBYYXNrQ0E5OGw2N0JVVktYZys0cWpQRWZvNjYzZWJSRkRsZEpvR2F5L1ByUjJULzlVL1ZjZStvZGdleCtlSXB4KzcxWTBRdC95T2dJZ2NvbG9XckthaWpKemdFdkltd1RwVXNRWENjK2JJMUV0SUJnQiJ9LHsidHlwZSI6InRleHQiLCJ0ZXh0IjoiOCDDlyAyID0gMTYifV0sInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjY3LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zIjo1OSwic2VydmljZV90aWVyIjoic3RhbmRhcmQifX0= + recorded_at: Fri, 18 Jul 2025 05:39:29 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-sonnet-4_can_handle_basic_conversation_with_thinking_enabled.yml b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-sonnet-4_can_handle_basic_conversation_with_thinking_enabled.yml new file mode 100644 index 00000000..f2d9ca25 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-sonnet-4_can_handle_basic_conversation_with_thinking_enabled.yml @@ -0,0 +1,93 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-sonnet-4-20250514","messages":[{"role":"user","content":[{"type":"text","text":"What''s + 2 + 2? Think through this step by step."}]}],"temperature":1,"stream":false,"max_tokens":64000,"thinking":{"type":"enabled","budget_tokens":2048}}' + headers: + User-Agent: + - Faraday v2.13.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 18 Jul 2025 05:39:01 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '30000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '30000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-07-18T05:38:55Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-07-18T05:39:03Z' + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-07-18T05:38:55Z' + Anthropic-Ratelimit-Tokens-Limit: + - '38000' + Anthropic-Ratelimit-Tokens-Remaining: + - '38000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-07-18T05:38:55Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - a2bc4b1c-6bf0-4b62-b1f6-a1d9821a5e3a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01ELjQtoVzofUkfT2MG4NnvY","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"thinking","thinking":"This + is a very basic arithmetic question. Let me think through it step by step + as requested.\n\n2 + 2\n\nThis is asking me to add the number 2 to itself.\n\nStep + 1: I start with the first number, which is 2.\nStep 2: I need to add 2 to + it.\nStep 3: 2 + 2 = 4\n\nI can think of this in several ways:\n- Counting: + If I have 2 items, and I add 2 more items, I have 4 items total\n- On a number + line: Starting at 2, if I move 2 spaces to the right, I land on 4\n- Basic + addition fact: This is one of the fundamental addition facts that 2 + 2 = + 4","signature":"ErkFCkYIBRgCKkAGD5ERRvwl/3LTfinLm7VjiqqC4SUrYGpZtWlYkibMcIFwHN/sTGd8t4tNZmDLjZrsHp4qLIeQ2ZaXrx/cdekJEgwdoMEt+uVUTc41uWkaDNgGvIHy6w6qICAlXiIwPii9Ia+JG/IRms/CawNXQ8IutTtFXpmc5cE0XCSXq3BEohpEaGHVUwzMbnG2KH5OKqAE/u5S+Nw+AsmuwpxOzd+CnFM6yvqYDBbRCZRDJqlWaffXEmeZp5066Yl9CePSoBqP7MXRbJE35kvwrfZZXuwFH88Xmkxv7WOZkL2L8keUXX9LamLnxrQthqSqivuhskIJUiup5gsUTQUztKOztDxcwEfGDOeBIVeZ8GRqXusIy2cZxQNF87fPvj712KyXkrUej05qAAmyLLlRF2U+e0Rmuq2uKVqifmk6uvL+qM/5nVxknMAlBgU3h8wPR0NYQdVGC8Y6qi+IDn5vahBSADCnraBXv/PeY/dLzyIq1WsBFbnOriSIYNhVhulBlGm6e3dIk6L54j1xT41d5RxFXYXZi5ROSAgyapLN0eX/XI+m6Fd09Y953n0YSSKlBiEJvet6ZB/9PD06oQf7zWrQ4Edqm0QBc97UZjPVZp/7aAbpfkSmYq3JkyT0YLUGbDOvEQTdaJLN5dlPkB5fmihZKA2DrqhOrI4+/bBtj4wO66M3wvnQqIBJkivzHVOqwJz4eJ7ZjZFw8dHTmb7/lEY8f3dn42kNwfY735j7vvrvpHCXw5ST3h4fW88/IHymeWtr0U7ohFMbzhxo2vY7nFah5wHd7RUqEOwtSlDvicXj4O0tvp/0cITCn6dqYCAk1fh1EBiw+OfLLRypTfFGSH5KV0Wb3TF3m944/KeSx8Jetmd2xVSBEwfhYolr9UacGENv8eoQAvMm1hMM5lBC+DGTLJZErRgB"},{"type":"text","text":"I''ll + work through this step by step:\n\n**Step 1:** Start with the first number: + 2\n\n**Step 2:** Add the second number: 2\n\n**Step 3:** Combine them: 2 + + 2\n\n**Step 4:** Calculate the sum: 4\n\nSo 2 + 2 = 4.\n\nYou can think of + this as having 2 objects, then adding 2 more objects, which gives you 4 objects + total."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":51,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":298,"service_tier":"standard"}}' + recorded_at: Fri, 18 Jul 2025 05:39:01 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-sonnet-4_maintains_thinking_mode_across_multiple_turns.yml b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-sonnet-4_maintains_thinking_mode_across_multiple_turns.yml new file mode 100644 index 00000000..fa8068ea --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_thinking_mode_functionality_thinking_mode_integration_with_chat_anthropic_claude-sonnet-4_maintains_thinking_mode_across_multiple_turns.yml @@ -0,0 +1,161 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-sonnet-4-20250514","messages":[{"role":"user","content":[{"type":"text","text":"What''s + 5 + 3?"}]}],"temperature":1,"stream":false,"max_tokens":64000,"thinking":{"type":"enabled","budget_tokens":2048}}' + headers: + User-Agent: + - Faraday v2.13.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 18 Jul 2025 05:39:03 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '30000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '30000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-07-18T05:39:03Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-07-18T05:39:03Z' + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-07-18T05:39:03Z' + Anthropic-Ratelimit-Tokens-Limit: + - '38000' + Anthropic-Ratelimit-Tokens-Remaining: + - '38000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-07-18T05:39:03Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - a2bc4b1c-6bf0-4b62-b1f6-a1d9821a5e3a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01BzuwUNCKCLie4m3CUR9sRr","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"thinking","thinking":"This + is a simple addition problem. 5 + 3 = 8.","signature":"EtUBCkYIBRgCKkBTKgGEUioRSsJoxhuzo1AT0IuxJmwIxAkRRNe15z10h5e8CMTYppvz7xt2wwXONyqpRoWYmGBQfSlspsv8kJpBEgzBiQiuQU2am3MN3YQaDJow1tAgVZRXRLLi3CIw67uUejmUfdAYNzynxnnHwd6Ba6C4OgDvQ8yqX7Au6xBHmHFYDhUYzbinIo/WSGLjKj0jXruNlLkGAoGxJ0IGvpuX0979sQApiBQaoBJFXMoa4ryX9E77kbLyprbcwg76uE6yCTbh41h0yxU/G+/BGAE="},{"type":"text","text":"5 + + 3 = 8"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":44,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":39,"service_tier":"standard"}}' + recorded_at: Fri, 18 Jul 2025 05:39:03 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-sonnet-4-20250514","messages":[{"role":"user","content":[{"type":"text","text":"What''s + 5 + 3?"}]},{"role":"assistant","content":[{"type":"text","text":"5 + 3 = 8"}]},{"role":"user","content":[{"type":"text","text":"Now + multiply that result by 2"}]}],"temperature":1,"stream":false,"max_tokens":64000,"thinking":{"type":"enabled","budget_tokens":2048}}' + headers: + User-Agent: + - Faraday v2.13.2 + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Fri, 18 Jul 2025 05:39:14 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '30000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '30000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-07-18T05:39:14Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '8000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-07-18T05:39:15Z' + Anthropic-Ratelimit-Requests-Limit: + - '50' + Anthropic-Ratelimit-Requests-Remaining: + - '49' + Anthropic-Ratelimit-Requests-Reset: + - '2025-07-18T05:39:05Z' + Anthropic-Ratelimit-Tokens-Limit: + - '38000' + Anthropic-Ratelimit-Tokens-Remaining: + - '38000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-07-18T05:39:14Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - a2bc4b1c-6bf0-4b62-b1f6-a1d9821a5e3a + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6Im1zZ18wMThabnY0d1BGYWtWeHJFaUs5Yjd2a3ciLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQtNC0yMDI1MDUxNCIsImNvbnRlbnQiOlt7InR5cGUiOiJ0aGlua2luZyIsInRoaW5raW5nIjoiVGhlIHVzZXIgYXNrZWQgbWUgdG8gbXVsdGlwbHkgdGhlIHJlc3VsdCBmcm9tIHRoZSBwcmV2aW91cyBjYWxjdWxhdGlvbiBieSAyLiBUaGUgcHJldmlvdXMgcmVzdWx0IHdhcyA4LCBzbyBJIG5lZWQgdG8gY2FsY3VsYXRlIDggw5cgMi5cblxuOCDDlyAyID0gMTYiLCJzaWduYXR1cmUiOiJFcndDQ2tZSUJSZ0NLa0J4TGdyNWdYVTVyOEJJN000cXVkUFRsdmdUaTBheGFFRWZLdmttaWxkMFQxYjBCajB6cHB6UUc4WE1GU1gyOC91WldnL21QM1UyK1JGZy9PRmpON2RjRWd5ZW0xMDMvbXg1UENhN1QrUWFESGEvVk5oR2RBd2ZwMEc1ZFNJd1lKdU5MbXNBMHptR01IZy8wNjFpZWVxcXRiN3lKQVkxSGE0NU5aVElDZ1M5SkQ1NC9lQ2dJMnIvVlJxckZQbHdLcU1CWDltenU3UU12Q3pXNWJrMjhPRVp5MDA4TS9LSVVqZkZNWHZlSE1DbzFMWWJUaWxFLzBPbEJ5RFdpRVNQRzVPVTE5R3Q3SWpqT3dwUkFSaERGcnNJSTJiNE9WeHhXMzdZc0x2RG1vclFodkdxYTBHQWFYNjQ5dUVFNWkxZExzQURUdTN4a3lWZGRsZGJKeXYyQ290TllNR0JBNW9nNk1FRlpNNVlxUlJCcmdSUlBpVDNFaUNZK2R4TWxKRjRmejZ1U3lHeFRvaktMTEM5L211c0NrYitWQ09vZ2hnQiJ9LHsidHlwZSI6InRleHQiLCJ0ZXh0IjoiOCDDlyAyID0gMTYifV0sInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjY3LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zIjo2Niwic2VydmljZV90aWVyIjoic3RhbmRhcmQifX0= + recorded_at: Fri, 18 Jul 2025 05:39:14 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/ruby_llm/chat_thinking_spec.rb b/spec/ruby_llm/chat_thinking_spec.rb new file mode 100644 index 00000000..4f968f25 --- /dev/null +++ b/spec/ruby_llm/chat_thinking_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RubyLLM::Chat do + include_context 'with configured RubyLLM' + + describe 'thinking mode functionality' do + describe '#with_thinking' do + context 'with thinking-capable models' do # rubocop:disable RSpec/NestedGroups + THINKING_MODELS.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + + it "#{provider}/#{model} enables thinking mode successfully" do # rubocop:disable RSpec/MultipleExpectations + chat = RubyLLM.chat(model: model, provider: provider) + + expect { chat.with_thinking }.not_to raise_error + expect(chat.instance_variable_get(:@thinking)).to be true + expect(chat.instance_variable_get(:@temperature)).to eq 1 + end + + it "#{provider}/#{model} accepts custom thinking parameters" do # rubocop:disable RSpec/MultipleExpectations + chat = RubyLLM.chat(model: model, provider: provider) + + chat.with_thinking(budget: 20_000, temperature: 0.8) + + expect(chat.instance_variable_get(:@thinking)).to be true + expect(chat.instance_variable_get(:@thinking_budget)).to eq 20_000 + expect(chat.instance_variable_get(:@temperature)).to eq 0.8 + end + + it "#{provider}/#{model} can disable thinking mode" do + chat = RubyLLM.chat(model: model, provider: provider) + + chat.with_thinking(thinking: false) + + expect(chat.instance_variable_get(:@thinking)).to be false + end + + it "#{provider}/#{model} can chain with other methods" do # rubocop:disable RSpec/MultipleExpectations + chat = RubyLLM.chat(model: model, provider: provider) + + result = chat.with_thinking.with_temperature(0.5) + + expect(result).to be_a(described_class) + expect(chat.instance_variable_get(:@thinking)).to be true + # Temperature should be overridden by the subsequent with_temperature call + expect(chat.instance_variable_get(:@temperature)).to eq 0.5 + end + end + end + + context 'with non-thinking models' do # rubocop:disable RSpec/NestedGroups + NON_THINKING_MODELS.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + + it "#{provider}/#{model} raises UnsupportedThinkingError when enabling thinking" do + chat = RubyLLM.chat(model: model, provider: provider) + + expect { chat.with_thinking }.to raise_error(RubyLLM::UnsupportedThinkingError) + end + + it "#{provider}/#{model} allows disabling thinking without error" do # rubocop:disable RSpec/MultipleExpectations + chat = RubyLLM.chat(model: model, provider: provider) + + expect { chat.with_thinking(thinking: false) }.not_to raise_error + expect(chat.instance_variable_get(:@thinking)).to be false + end + end + end + end + + describe 'thinking mode integration with chat' do + THINKING_MODELS.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + + it "#{provider}/#{model} can handle basic conversation with thinking enabled" do # rubocop:disable RSpec/MultipleExpectations, RSpec/ExampleLength + chat = RubyLLM.chat(model: model, provider: provider) + chat.with_thinking + + response = chat.ask("What's 2 + 2? Think through this step by step.") + + expect(response.content).to be_present + expect(response.thinking).to be_present + expect(response.role).to eq(:assistant) + expect(response.input_tokens).to be_positive + expect(response.output_tokens).to be_positive + end + + it "#{provider}/#{model} maintains thinking mode across multiple turns" do # rubocop:disable RSpec/MultipleExpectations, RSpec/ExampleLength + chat = RubyLLM.chat(model: model, provider: provider) + chat.with_thinking + + first = chat.ask("What's 5 + 3?") + expect(first.content).to include('8') + + second = chat.ask('Now multiply that result by 2') + expect(second.content).to include('16') + + # Thinking mode should still be enabled + expect(chat.instance_variable_get(:@thinking)).to be true + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f3ac86c0..5d2d6bc7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -160,3 +160,14 @@ AUDIO_MODELS = [ { provider: :openai, model: 'gpt-4o-mini-audio-preview' } ].freeze + +THINKING_MODELS = [ + { model: 'claude-3-7-sonnet', provider: 'anthropic' }, + { model: 'claude-sonnet-4', provider: 'anthropic' }, + { model: 'claude-opus-4', provider: 'anthropic' } +].freeze + +NON_THINKING_MODELS = [ + { model: 'claude-3-haiku', provider: 'anthropic' }, + { model: 'claude-3-sonnet', provider: 'anthropic' } +].freeze