diff --git a/docs/_core_features/thinking.md b/docs/_core_features/thinking.md new file mode 100644 index 000000000..89b5e122a --- /dev/null +++ b/docs/_core_features/thinking.md @@ -0,0 +1,234 @@ +--- +layout: default +title: Extended Thinking +nav_order: 8 +description: Access the model's internal reasoning process with Extended Thinking +redirect_from: + - /guides/thinking + - /guides/reasoning +--- + +# {{ page.title }} +{: .no_toc } + +{{ page.description }} +{: .fs-6 .fw-300 } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +After reading this guide, you will know: + +* How to enable Extended Thinking for supported models. +* How to access thinking content in responses. +* How to stream thinking content in real-time. +* How thinking works across different providers. +* How to persist thinking content with ActiveRecord. + +## What is Extended Thinking? + +Extended Thinking (also known as "reasoning") is a feature that exposes the model's internal reasoning process. When enabled, models will "think through" problems step-by-step before providing their final response. This is particularly useful for: + +* Complex mathematical or logical problems +* Multi-step reasoning tasks +* Debugging and understanding model behavior +* Applications where transparency in reasoning is valuable + +## Enabling Extended Thinking + +Use the `with_thinking` method to enable Extended Thinking on a chat: + +```ruby +chat = RubyLLM.chat(model: 'claude-opus-4-5-20251101') + .with_thinking(budget: :medium) + +response = chat.ask("What is 15 * 23? Show your reasoning.") + +puts "Thinking: #{response.thinking}" +# => "Let me break this down step by step. 15 * 23 = 15 * 20 + 15 * 3..." + +puts "Answer: #{response.content}" +# => "The answer is 345." +``` + +### Budget Options + +The `budget` parameter controls how much "thinking" the model should do: + +| Budget | Description | +|--------|-------------| +| `:low` | Minimal thinking, faster responses | +| `:medium` | Balanced thinking (default) | +| `:high` | Maximum thinking, most thorough | +| Integer | Specific token budget (provider-dependent) | + +```ruby +# Symbol budgets +chat.with_thinking(budget: :low) +chat.with_thinking(budget: :medium) +chat.with_thinking(budget: :high) + +# Integer budget (tokens) +chat.with_thinking(budget: 10_000) +``` + +### Checking if Thinking is Enabled + +```ruby +chat = RubyLLM.chat(model: 'claude-opus-4-5-20251101') + +chat.thinking_enabled? # => false + +chat.with_thinking(budget: :medium) + +chat.thinking_enabled? # => true +``` + +## Streaming with Thinking + +When streaming, thinking content is available on each chunk: + +```ruby +chat = RubyLLM.chat(model: 'claude-opus-4-5-20251101') + .with_thinking(budget: :medium) + +chat.ask("Solve this step by step: What is 127 * 43?") do |chunk| + # Print thinking content as it streams + if chunk.thinking + print "[Thinking] #{chunk.thinking}" + end + + # Print response content + if chunk.content + print chunk.content + end +end +``` + +### Separating Thinking from Response + +For UI applications, you may want to display thinking separately: + +```ruby +thinking_content = "" +response_content = "" + +chat.ask("Complex question here...") do |chunk| + thinking_content << chunk.thinking if chunk.thinking + response_content << chunk.content if chunk.content + + # Update UI with separated content + update_thinking_panel(thinking_content) + update_response_panel(response_content) +end +``` + +## Supported Models + +Extended Thinking requires models with the `reasoning` capability. Use `with_thinking` only on supported models: + +```ruby +# Check if a model supports thinking +model = RubyLLM::Models.find('claude-opus-4-5-20251101') +model.supports?('reasoning') # => true + +# Using with_thinking on unsupported models raises an error +chat = RubyLLM.chat(model: 'gpt-4o') +chat.with_thinking(budget: :medium) +# => raises RubyLLM::UnsupportedFeatureError +``` + +### Provider-Specific Behavior + +| Provider | Models | Implementation | +|----------|--------|----------------| +| Anthropic | claude-opus-4-*, claude-sonnet-4-* | `thinking` block with `budget_tokens` | +| Gemini | gemini-2.5-*, gemini-3-* | `thinkingConfig` with budget or effort level | +| OpenAI/Grok | grok-* models | `reasoning_effort` parameter | + +Budget symbols are automatically translated to provider-specific values: + +| Symbol | Anthropic | Gemini 2.5 | Gemini 3 | Grok | +|--------|-----------|------------|----------|------| +| `:low` | 1,024 tokens | 1,024 tokens | "low" | "low" | +| `:medium` | 10,000 tokens | 8,192 tokens | "medium" | "high" | +| `:high` | 32,000 tokens | 24,576 tokens | "high" | "high" | + +## Multi-Turn Conversations + +Extended Thinking works seamlessly in multi-turn conversations. The model maintains context of its previous reasoning: + +```ruby +chat = RubyLLM.chat(model: 'claude-opus-4-5-20251101') + .with_thinking(budget: :medium) + +response1 = chat.ask("What is 15 * 23?") +puts response1.thinking # Shows step-by-step calculation + +response2 = chat.ask("Now multiply that result by 2") +puts response2.thinking # References previous calculation +puts response2.content # => "690" +``` + +## ActiveRecord Integration + +When using `acts_as_chat` and `acts_as_message`, thinking content is automatically persisted: + +```ruby +# Migration (generated automatically with new installs) +# t.text :thinking +# t.text :thinking_signature + +# Using thinking with persisted chats +chat_record = Chat.create! +chat_record.with_thinking(budget: :medium) + +response = chat_record.ask("Explain quantum entanglement") + +# Thinking is saved to the message record +last_message = chat_record.messages.last +last_message.thinking # => "Let me break down quantum entanglement..." +``` + +### Upgrading Existing Installations + +If you have an existing RubyLLM installation, add the thinking columns: + +```ruby +class AddThinkingToMessages < ActiveRecord::Migration[7.0] + def change + add_column :messages, :thinking, :text + add_column :messages, :thinking_signature, :text + end +end +``` + +## Error Handling + +```ruby +begin + chat = RubyLLM.chat(model: 'gpt-4o') # Doesn't support thinking + chat.with_thinking(budget: :medium) +rescue RubyLLM::UnsupportedFeatureError => e + puts "This model doesn't support Extended Thinking" + puts e.message # => "Model 'gpt-4o' does not support extended thinking" +end +``` + +## Best Practices + +1. **Choose appropriate budgets**: Use `:low` for simple tasks, `:high` for complex reasoning +2. **Stream for long responses**: Thinking can be lengthy; streaming provides better UX +3. **Don't always display thinking**: Consider whether users need to see the reasoning +4. **Handle gracefully**: Check `thinking_enabled?` before relying on thinking content + +## Next Steps + +* [Streaming Responses]({% link _core_features/streaming.md %}) +* [Rails Integration]({% link _advanced/rails.md %}) +* [Error Handling]({% link _advanced/error-handling.md %}) diff --git a/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt b/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt index b8882febf..ddd90def2 100644 --- a/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +++ b/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt @@ -4,6 +4,8 @@ class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::M t.string :role, null: false t.text :content t.json :content_raw + t.text :thinking + t.text :thinking_signature t.integer :input_tokens t.integer :output_tokens t.integer :cached_tokens diff --git a/lib/ruby_llm/active_record/chat_methods.rb b/lib/ruby_llm/active_record/chat_methods.rb index 41930548c..85a6c4beb 100644 --- a/lib/ruby_llm/active_record/chat_methods.rb +++ b/lib/ruby_llm/active_record/chat_methods.rb @@ -124,6 +124,11 @@ def with_temperature(...) self end + def with_thinking(...) + to_llm.with_thinking(...) + self + end + def with_params(...) to_llm.with_params(...) self @@ -262,6 +267,8 @@ def persist_message_completion(message) if @message.has_attribute?(:cache_creation_tokens) attrs[:cache_creation_tokens] = message.cache_creation_tokens end + attrs[:thinking] = message.thinking if @message.has_attribute?(:thinking) + attrs[:thinking_signature] = Messages.signature_for(message) if @message.has_attribute?(:thinking_signature) # Add model association dynamically attrs[self.class.model_association_name] = model_association diff --git a/lib/ruby_llm/active_record/message_methods.rb b/lib/ruby_llm/active_record/message_methods.rb index 334352409..eede61830 100644 --- a/lib/ruby_llm/active_record/message_methods.rb +++ b/lib/ruby_llm/active_record/message_methods.rb @@ -11,24 +11,39 @@ module MessageMethods end def to_llm - cached = has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil - cache_creation = has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil - RubyLLM::Message.new( role: role.to_sym, content: extract_content, + thinking: thinking_value, + thinking_signature: thinking_signature_value, tool_calls: extract_tool_calls, tool_call_id: extract_tool_call_id, input_tokens: input_tokens, output_tokens: output_tokens, - cached_tokens: cached, - cache_creation_tokens: cache_creation, + cached_tokens: cached_value, + cache_creation_tokens: cache_creation_value, model_id: model_association&.model_id ) end private + def thinking_value + has_attribute?(:thinking) ? self[:thinking] : nil + end + + def thinking_signature_value + has_attribute?(:thinking_signature) ? self[:thinking_signature] : nil + end + + def cached_value + has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil + end + + def cache_creation_value + has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil + end + def extract_tool_calls tool_calls_association.to_h do |tool_call| [ diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index d03d872ca..e0bce5dde 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -22,6 +22,7 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n @params = {} @headers = {} @schema = nil + @thinking_budget = nil @on = { new_message: nil, end_message: nil, @@ -67,6 +68,16 @@ def with_temperature(temperature) self end + def with_thinking(budget: :medium) + validate_thinking_support! + @thinking_budget = budget + self + end + + def thinking_enabled? + !@thinking_budget.nil? + end + def with_context(context) @context = context @config = context.config @@ -130,6 +141,7 @@ def complete(&) # rubocop:disable Metrics/PerceivedComplexity params: @params, headers: @headers, schema: @schema, + thinking: @thinking_budget, &wrap_streaming_block(&) ) @@ -169,6 +181,18 @@ def instance_variables private + def validate_thinking_support! + return if @model.supports?('reasoning') + return if gemini_thinking_model? + + raise UnsupportedFeatureError, + "Model '#{@model.id}' does not support extended thinking" + end + + def gemini_thinking_model? + @model.id.to_s.match?(/gemini-[23]|gemini-2\.\d-.*thinking/) + end + def wrap_streaming_block(&block) return nil unless block_given? diff --git a/lib/ruby_llm/error.rb b/lib/ruby_llm/error.rb index 3908bee27..631f5f21d 100644 --- a/lib/ruby_llm/error.rb +++ b/lib/ruby_llm/error.rb @@ -18,6 +18,13 @@ class InvalidRoleError < StandardError; end class ModelNotFoundError < StandardError; end class UnsupportedAttachmentError < StandardError; end + # Error raised when a feature is not supported by a model + class UnsupportedFeatureError < Error + def initialize(message) + super(nil, message) + end + end + # Error classes for different HTTP status codes class BadRequestError < Error; end class ForbiddenError < Error; end diff --git a/lib/ruby_llm/message.rb b/lib/ruby_llm/message.rb index 0a710b15f..64510bf03 100644 --- a/lib/ruby_llm/message.rb +++ b/lib/ruby_llm/message.rb @@ -6,7 +6,7 @@ class Message ROLES = %i[system user assistant tool].freeze attr_reader :role, :model_id, :tool_calls, :tool_call_id, :input_tokens, :output_tokens, - :cached_tokens, :cache_creation_tokens, :raw + :cached_tokens, :cache_creation_tokens, :raw, :thinking attr_writer :content def initialize(options = {}) @@ -20,6 +20,8 @@ def initialize(options = {}) @cached_tokens = options[:cached_tokens] @cache_creation_tokens = options[:cache_creation_tokens] @raw = options[:raw] + @thinking = options[:thinking] + @thinking_signature = options[:thinking_signature] ensure_valid_role end @@ -54,7 +56,8 @@ def to_h input_tokens: input_tokens, output_tokens: output_tokens, cached_tokens: cached_tokens, - cache_creation_tokens: cache_creation_tokens + cache_creation_tokens: cache_creation_tokens, + thinking: thinking }.compact end @@ -62,6 +65,10 @@ def instance_variables super - [:@raw] end + protected + + attr_reader :thinking_signature + private def normalize_content(content) diff --git a/lib/ruby_llm/messages.rb b/lib/ruby_llm/messages.rb new file mode 100644 index 000000000..435064a63 --- /dev/null +++ b/lib/ruby_llm/messages.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module RubyLLM + # Internal helper module for accessing protected message attributes + module Messages + module_function + + def signature_for(message) + message.send(:thinking_signature) if message.respond_to?(:thinking_signature, true) + end + end +end diff --git a/lib/ruby_llm/models.json b/lib/ruby_llm/models.json index 2e6033e17..52b077e80 100644 --- a/lib/ruby_llm/models.json +++ b/lib/ruby_llm/models.json @@ -316,6 +316,43 @@ }, "metadata": {} }, + { + "id": "claude-opus-4-5-20251101", + "name": "Claude Opus 4.5", + "provider": "anthropic", + "family": "claude-2", + "created_at": "2025-11-24 00:00:00 UTC", + "context_window": 200000, + "max_output_tokens": 4096, + "knowledge_cutoff": null, + "modalities": { + "input": [ + "text", + "image", + "pdf" + ], + "output": [ + "text" + ] + }, + "capabilities": [ + "streaming", + "reasoning" + ], + "pricing": { + "text_tokens": { + "standard": { + "input_per_million": 3.0, + "output_per_million": 15.0 + }, + "batch": { + "input_per_million": 1.5, + "output_per_million": 7.5 + } + } + }, + "metadata": {} + }, { "id": "claude-sonnet-4-20250514", "name": "Claude Sonnet 4", @@ -2775,7 +2812,8 @@ "capabilities": [ "batch", "function_calling", - "structured_output" + "structured_output", + "reasoning" ], "pricing": { "text_tokens": { @@ -31394,7 +31432,8 @@ "capabilities": [ "streaming", "function_calling", - "structured_output" + "structured_output", + "reasoning" ], "pricing": { "text_tokens": { diff --git a/lib/ruby_llm/models.rb b/lib/ruby_llm/models.rb index 54d7cc6d8..8d624269a 100644 --- a/lib/ruby_llm/models.rb +++ b/lib/ruby_llm/models.rb @@ -19,7 +19,7 @@ def load_models(file = RubyLLM.config.model_registry_file) end def read_from_json(file = RubyLLM.config.model_registry_file) - data = File.exist?(file) ? File.read(file) : '[]' + data = File.exist?(file) ? File.read(file, encoding: 'UTF-8') : '[]' JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) } rescue JSON::ParserError [] diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index 2fdb3ca11..feaedbac2 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -37,7 +37,7 @@ def configuration_requirements self.class.configuration_requirements end - def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, &) # rubocop:disable Metrics/ParameterLists + def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil, &) # rubocop:disable Metrics/ParameterLists normalized_temperature = maybe_normalize_temperature(temperature, model) payload = Utils.deep_merge( @@ -47,7 +47,8 @@ def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, sc temperature: normalized_temperature, model: model, stream: block_given?, - schema: schema + schema: schema, + thinking: thinking ), params ) diff --git a/lib/ruby_llm/providers/anthropic/chat.rb b/lib/ruby_llm/providers/anthropic/chat.rb index cbc9f1161..741d9574d 100644 --- a/lib/ruby_llm/providers/anthropic/chat.rb +++ b/lib/ruby_llm/providers/anthropic/chat.rb @@ -3,7 +3,7 @@ module RubyLLM module Providers class Anthropic - # Chat methods of the OpenAI API integration + # Chat methods for the Anthropic API implementation module Chat module_function @@ -11,11 +11,11 @@ def completion_url '/v1/messages' end - def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument + def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument system_messages, chat_messages = separate_messages(messages) system_content = build_system_content(system_messages) - build_base_payload(chat_messages, model, stream).tap do |payload| + build_base_payload(chat_messages, model, stream, thinking).tap do |payload| add_optional_fields(payload, system_content:, tools:, temperature:) end end @@ -45,13 +45,41 @@ def build_system_content(system_messages) end end - def build_base_payload(chat_messages, model, stream) - { + def build_base_payload(chat_messages, model, stream, thinking) + payload = { model: model.id, - messages: chat_messages.map { |msg| format_message(msg) }, + messages: chat_messages.map { |msg| format_message(msg, thinking_enabled: thinking) }, stream: stream, - max_tokens: model.max_tokens || 4096 + max_tokens: calculate_max_tokens(model, thinking) } + + if thinking + payload[:thinking] = { + type: 'enabled', + budget_tokens: resolve_budget(thinking, model) + } + end + + payload + end + + def calculate_max_tokens(model, thinking) + base = model.max_tokens || 4096 + return base unless thinking + + budget = resolve_budget(thinking, model) + [base, budget + 8000].max + end + + def resolve_budget(thinking, _model) + return thinking if thinking.is_a?(Integer) + + case thinking + when :low then 1024 + when :medium then 10_000 + when :high then 32_000 + else 10_000 # rubocop:disable Lint/DuplicateBranch + end end def add_optional_fields(payload, system_content:, tools:, temperature:) @@ -65,9 +93,11 @@ def parse_completion_response(response) content_blocks = data['content'] || [] text_content = extract_text_content(content_blocks) + thinking_content = extract_thinking_content(content_blocks) + thinking_sig = extract_thinking_signature(content_blocks) tool_use_blocks = Tools.find_tool_uses(content_blocks) - build_message(data, text_content, tool_use_blocks, response) + build_message(data, text_content, thinking_content, thinking_sig, tool_use_blocks, response) end def extract_text_content(blocks) @@ -75,7 +105,18 @@ def extract_text_content(blocks) text_blocks.map { |c| c['text'] }.join end - def build_message(data, content, tool_use_blocks, response) + def extract_thinking_content(blocks) + thinking_blocks = blocks.select { |c| c['type'] == 'thinking' } + thoughts = thinking_blocks.map { |c| c['thinking'] }.join + thoughts.empty? ? nil : thoughts + end + + def extract_thinking_signature(blocks) + thinking_block = blocks.find { |c| c['type'] == 'thinking' } + thinking_block&.dig('signature') + end + + def build_message(data, content, thinking, thinking_sig, tool_use_blocks, response) # rubocop:disable Metrics/ParameterLists usage = data['usage'] || {} cached_tokens = usage['cache_read_input_tokens'] cache_creation_tokens = usage['cache_creation_input_tokens'] @@ -86,6 +127,8 @@ def build_message(data, content, tool_use_blocks, response) Message.new( role: :assistant, content: content, + thinking: thinking, + thinking_signature: thinking_sig, tool_calls: Tools.parse_tool_calls(tool_use_blocks), input_tokens: usage['input_tokens'], output_tokens: usage['output_tokens'], @@ -96,20 +139,68 @@ def build_message(data, content, tool_use_blocks, response) ) end - def format_message(msg) + def format_message(msg, thinking_enabled: false) if msg.tool_call? - Tools.format_tool_call(msg) + format_tool_call_with_thinking(msg, thinking_enabled) elsif msg.tool_result? Tools.format_tool_result(msg) else - format_basic_message(msg) + format_basic_message_with_thinking(msg, thinking_enabled) end end - def format_basic_message(msg) + def format_basic_message_with_thinking(msg, thinking_enabled) + content_blocks = [] + + if msg.role == :assistant && thinking_enabled + sig = Messages.signature_for(msg) + + if msg.thinking && !msg.thinking.empty? + content_blocks << { + type: 'thinking', + thinking: msg.thinking, + signature: sig + }.compact + elsif sig + content_blocks << { + type: 'redacted_thinking', + data: sig + } + end + end + + content_blocks.concat(Media.format_content(msg.content)) + { role: convert_role(msg.role), - content: Media.format_content(msg.content) + content: content_blocks + } + end + + def format_tool_call_with_thinking(msg, thinking_enabled) + content_blocks = [] + + if thinking_enabled && msg.thinking && !msg.thinking.empty? + sig = Messages.signature_for(msg) + content_blocks << { + type: 'thinking', + thinking: msg.thinking, + signature: sig + }.compact + end + + msg.tool_calls.each_value do |tool_call| + content_blocks << { + type: 'tool_use', + id: tool_call.id, + name: tool_call.name, + input: tool_call.arguments + } + end + + { + role: 'assistant', + content: content_blocks } end diff --git a/lib/ruby_llm/providers/anthropic/streaming.rb b/lib/ruby_llm/providers/anthropic/streaming.rb index 5fed14710..f634471c8 100644 --- a/lib/ruby_llm/providers/anthropic/streaming.rb +++ b/lib/ruby_llm/providers/anthropic/streaming.rb @@ -12,10 +12,14 @@ def stream_url end def build_chunk(data) + delta_type = data.dig('delta', 'type') + Chunk.new( role: :assistant, model_id: extract_model_id(data), - content: data.dig('delta', 'text'), + content: extract_content_delta(data, delta_type), + thinking: extract_thinking_delta(data, delta_type), + thinking_signature: extract_signature_delta(data, delta_type), input_tokens: extract_input_tokens(data), output_tokens: extract_output_tokens(data), cached_tokens: extract_cached_tokens(data), @@ -24,6 +28,24 @@ def build_chunk(data) ) end + def extract_content_delta(data, delta_type) + return data.dig('delta', 'text') if delta_type == 'text_delta' + + nil + end + + def extract_thinking_delta(data, delta_type) + return data.dig('delta', 'thinking') if delta_type == 'thinking_delta' + + nil + end + + def extract_signature_delta(data, delta_type) + return data.dig('delta', 'signature') if delta_type == 'signature_delta' + + nil + end + def json_delta?(data) data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta' end diff --git a/lib/ruby_llm/providers/bedrock/chat.rb b/lib/ruby_llm/providers/bedrock/chat.rb index 4abdae3f3..68570ce47 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, schema: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/ParameterLists + def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/ParameterLists @model_id = model.id system_messages, chat_messages = Anthropic::Chat.separate_messages(messages) diff --git a/lib/ruby_llm/providers/gemini/chat.rb b/lib/ruby_llm/providers/gemini/chat.rb index bab33f79a..3a9cdea48 100644 --- a/lib/ruby_llm/providers/gemini/chat.rb +++ b/lib/ruby_llm/providers/gemini/chat.rb @@ -14,7 +14,7 @@ def completion_url "models/#{@model}:generateContent" end - def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument + def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument @model = model.id payload = { contents: format_messages(messages), @@ -25,10 +25,52 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema payload[:generationConfig].merge!(structured_output_config(schema, model)) if schema + payload[:generationConfig][:thinkingConfig] = build_thinking_config(model, thinking) if thinking + payload[:tools] = format_tools(tools) if tools.any? payload end + def build_thinking_config(model, thinking) + config = { includeThoughts: true } + + if gemini_3_model?(model) + config[:thinkingLevel] = resolve_effort_level(thinking) + else + config[:thinkingBudget] = resolve_budget(thinking) + end + + config + end + + # Gemini 3 uses thinkingLevel (string) while Gemini 2.5 uses thinkingBudget (integer). + # String matching is unavoidable until Google provides a capability flag or stabilizes + # their API versioning. This detection is confined to the Gemini provider where the + # coupling is appropriate. + def gemini_3_model?(model) + model.id.to_s.include?('gemini-3') + end + + def resolve_effort_level(thinking) + case thinking + when :low then 'low' + when :medium then 'medium' + when :high then 'high' + when Integer then thinking > 16_000 ? 'high' : 'low' + else 'high' # rubocop:disable Lint/DuplicateBranch + end + end + + def resolve_budget(thinking) + case thinking + when Integer then thinking + when :low then 1024 + when :medium then 8192 + when :high then 24_576 + else 8192 # rubocop:disable Lint/DuplicateBranch + end + end + private def format_messages(messages) @@ -62,11 +104,16 @@ def format_parts(msg) def parse_completion_response(response) data = response.body + parts = data.dig('candidates', 0, 'content', 'parts') || [] + + text_content = extract_text_parts(parts) + thinking_content = extract_thought_parts(parts) tool_calls = extract_tool_calls(data) Message.new( role: :assistant, - content: parse_content(data), + content: text_content, + thinking: thinking_content, tool_calls: tool_calls, input_tokens: data.dig('usageMetadata', 'promptTokenCount'), output_tokens: calculate_output_tokens(data), @@ -75,6 +122,18 @@ def parse_completion_response(response) ) end + def extract_text_parts(parts) + text_parts = parts.reject { |p| p['thought'] } + content = text_parts.filter_map { |p| p['text'] }.join + content.empty? ? nil : content + end + + def extract_thought_parts(parts) + thought_parts = parts.select { |p| p['thought'] } + thoughts = thought_parts.filter_map { |p| p['text'] }.join + thoughts.empty? ? nil : thoughts + end + def convert_schema_to_gemini(schema) return nil unless schema diff --git a/lib/ruby_llm/providers/gemini/streaming.rb b/lib/ruby_llm/providers/gemini/streaming.rb index 8aa630b27..ec2288243 100644 --- a/lib/ruby_llm/providers/gemini/streaming.rb +++ b/lib/ruby_llm/providers/gemini/streaming.rb @@ -10,10 +10,13 @@ def stream_url end def build_chunk(data) + parts = data.dig('candidates', 0, 'content', 'parts') || [] + Chunk.new( role: :assistant, model_id: extract_model_id(data), - content: extract_content(data), + content: extract_text_content(parts), + thinking: extract_thought_content(parts), input_tokens: extract_input_tokens(data), output_tokens: extract_output_tokens(data), tool_calls: extract_tool_calls(data) @@ -26,6 +29,18 @@ def extract_model_id(data) data['modelVersion'] end + def extract_text_content(parts) + text_parts = parts.reject { |p| p['thought'] } + text = text_parts.filter_map { |p| p['text'] }.join + text.empty? ? nil : text + end + + def extract_thought_content(parts) + thought_parts = parts.select { |p| p['thought'] } + thoughts = thought_parts.filter_map { |p| p['text'] }.join + thoughts.empty? ? nil : thoughts + end + def extract_content(data) return nil unless data['candidates']&.any? diff --git a/lib/ruby_llm/providers/mistral/chat.rb b/lib/ruby_llm/providers/mistral/chat.rb index 10ec965e4..816733504 100644 --- a/lib/ruby_llm/providers/mistral/chat.rb +++ b/lib/ruby_llm/providers/mistral/chat.rb @@ -12,7 +12,7 @@ def format_role(role) end # rubocop:disable Metrics/ParameterLists - def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) + def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) payload = super payload.delete(:stream_options) payload diff --git a/lib/ruby_llm/providers/openai/capabilities.rb b/lib/ruby_llm/providers/openai/capabilities.rb index 37c320da8..317c24ea2 100644 --- a/lib/ruby_llm/providers/openai/capabilities.rb +++ b/lib/ruby_llm/providers/openai/capabilities.rb @@ -224,12 +224,12 @@ def special_prefix_format(prefix) end def self.normalize_temperature(temperature, model_id) - if model_id.match?(/^(o\d|gpt-5)/) - RubyLLM.logger.debug "Model #{model_id} requires temperature=1.0, ignoring provided value" - 1.0 - elsif model_id.match?(/-search/) + if model_id.match?(/-search/) RubyLLM.logger.debug "Model #{model_id} does not accept temperature parameter, removing" nil + elsif model_id.match?(/^(o\d|gpt-5)/) + RubyLLM.logger.debug "Model #{model_id} requires temperature=1.0, ignoring provided value" + 1.0 else temperature end diff --git a/lib/ruby_llm/providers/openai/chat.rb b/lib/ruby_llm/providers/openai/chat.rb index 68522811e..f774b52c7 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, schema: nil) # rubocop:disable Metrics/ParameterLists + def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil, thinking: nil) # rubocop:disable Metrics/ParameterLists payload = { model: model.id, messages: format_messages(messages), @@ -34,10 +34,26 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema } end + payload[:reasoning_effort] = resolve_effort(thinking) if thinking && grok_model?(model) + payload[:stream_options] = { include_usage: true } if stream payload end + # Grok models are accessed via OpenRouter, which inherits from OpenAI. + # They support the reasoning_effort parameter for extended thinking. + def grok_model?(model) + model.id.to_s.include?('grok') + end + + def resolve_effort(thinking) + case thinking + when :low then 'low' + when Integer then thinking > 10_000 ? 'high' : 'low' + else 'high' + end + end + def parse_completion_response(response) data = response.body return if data.empty? @@ -53,6 +69,7 @@ def parse_completion_response(response) Message.new( role: :assistant, content: message_data['content'], + thinking: message_data['reasoning_content'], tool_calls: parse_tool_calls(message_data['tool_calls']), input_tokens: usage['prompt_tokens'], output_tokens: usage['completion_tokens'], diff --git a/lib/ruby_llm/providers/openai/streaming.rb b/lib/ruby_llm/providers/openai/streaming.rb index 99d7866df..f76fae74b 100644 --- a/lib/ruby_llm/providers/openai/streaming.rb +++ b/lib/ruby_llm/providers/openai/streaming.rb @@ -14,12 +14,14 @@ def stream_url def build_chunk(data) usage = data['usage'] || {} cached_tokens = usage.dig('prompt_tokens_details', 'cached_tokens') + delta = data.dig('choices', 0, 'delta') || {} Chunk.new( role: :assistant, model_id: data['model'], - content: data.dig('choices', 0, 'delta', 'content'), - tool_calls: parse_tool_calls(data.dig('choices', 0, 'delta', 'tool_calls'), parse_arguments: false), + content: delta['content'], + thinking: delta['reasoning_content'], + tool_calls: parse_tool_calls(delta['tool_calls'], parse_arguments: false), input_tokens: usage['prompt_tokens'], output_tokens: usage['completion_tokens'], cached_tokens: cached_tokens, diff --git a/lib/ruby_llm/stream_accumulator.rb b/lib/ruby_llm/stream_accumulator.rb index 33bb9730d..e19b3047e 100644 --- a/lib/ruby_llm/stream_accumulator.rb +++ b/lib/ruby_llm/stream_accumulator.rb @@ -3,10 +3,12 @@ module RubyLLM # Assembles streaming responses from LLMs into complete messages. class StreamAccumulator - attr_reader :content, :model_id, :tool_calls + attr_reader :content, :model_id, :tool_calls, :thinking def initialize @content = +'' + @thinking = +'' + @thinking_signature = nil @tool_calls = {} @input_tokens = nil @output_tokens = nil @@ -15,7 +17,7 @@ def initialize @latest_tool_call_id = nil end - def add(chunk) + def add(chunk) # rubocop:disable Metrics/PerceivedComplexity RubyLLM.logger.debug chunk.inspect if RubyLLM.config.log_stream_debug @model_id ||= chunk.model_id @@ -23,8 +25,11 @@ def add(chunk) accumulate_tool_calls chunk.tool_calls else @content << (chunk.content || '') + @thinking << (chunk.thinking || '') end + @thinking_signature = Messages.signature_for(chunk) || @thinking_signature + count_tokens chunk RubyLLM.logger.debug inspect if RubyLLM.config.log_stream_debug end @@ -33,6 +38,8 @@ def to_message(response) Message.new( role: :assistant, content: content.empty? ? nil : content, + thinking: thinking.empty? ? nil : thinking, + thinking_signature: @thinking_signature, model_id: model_id, tool_calls: tool_calls_from_stream, input_tokens: @input_tokens, diff --git a/lib/ruby_llm/tool.rb b/lib/ruby_llm/tool.rb index 2846a995d..24d2468c7 100644 --- a/lib/ruby_llm/tool.rb +++ b/lib/ruby_llm/tool.rb @@ -186,7 +186,7 @@ def resolve_schema def resolve_direct_schema(schema) return extract_schema(schema.to_json_schema) if schema.respond_to?(:to_json_schema) return RubyLLM::Utils.deep_dup(schema) if schema.is_a?(Hash) - if schema.is_a?(Class) && schema.instance_methods.include?(:to_json_schema) + if schema.is_a?(Class) && schema.method_defined?(:to_json_schema) return extract_schema(schema.new.to_json_schema) end diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-20250514_maintains_thinking_across_multi-turn_conversation.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-20250514_maintains_thinking_across_multi-turn_conversation.yml new file mode 100644 index 000000000..c8931e622 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-20250514_maintains_thinking_across_multi-turn_conversation.yml @@ -0,0 +1,163 @@ +--- +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 + is 5 + 3?"}]}],"stream":false,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 17:23:48 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T17:23:47Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T17:23:48Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2026-01-02T17:23:46Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T17:23:47Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '1709' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"model":"claude-opus-4-20250514","id":"msg_018yPka2cXUnko1tS8fTffEA","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"This + is a simple addition problem. 5 + 3 = 8.","signature":"EtUBCkYICxgCKkBTKgGEUioRSsJoxhuzo1AT0IuxJmwIxAkRRNe15z10h5e8CMTYppvz7xt2wwXONyqpRoWYmGBQfSlspsv8kJpBEgzfxXZ1wQu4sH36WBkaDEtyQwE6aidRDab8tiIw0SeAOeWHV3Kz9igF+uxfN/ww/WHadiRgHbyPPcJL/sTUXxp28q+SEsYCLUan2R//Kj2OqqUYy2+MMbcNwgkU+UM9NcfIrkO3R9JOLsCrWtvuiajR2DIoqIdYICkvgW2sgcTI50hMOT8rl+uHy7qDGAE="},{"type":"text","text":"5 + + 3 = 8"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":45,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":38,"service_tier":"standard"}}' + recorded_at: Fri, 02 Jan 2026 17:23:48 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 + is 5 + 3?"}]},{"role":"assistant","content":[{"type":"thinking","thinking":"This + is a simple addition problem. 5 + 3 = 8.","signature":"EtUBCkYICxgCKkBTKgGEUioRSsJoxhuzo1AT0IuxJmwIxAkRRNe15z10h5e8CMTYppvz7xt2wwXONyqpRoWYmGBQfSlspsv8kJpBEgzfxXZ1wQu4sH36WBkaDEtyQwE6aidRDab8tiIw0SeAOeWHV3Kz9igF+uxfN/ww/WHadiRgHbyPPcJL/sTUXxp28q+SEsYCLUan2R//Kj2OqqUYy2+MMbcNwgkU+UM9NcfIrkO3R9JOLsCrWtvuiajR2DIoqIdYICkvgW2sgcTI50hMOT8rl+uHy7qDGAE="},{"type":"text","text":"5 + + 3 = 8"}]},{"role":"user","content":[{"type":"text","text":"Now multiply + that by 2"}]}],"stream":false,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 17:23:50 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T17:23:49Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T17:23:50Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2026-01-02T17:23:48Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T17:23:49Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '2086' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1vcHVzLTQtMjAyNTA1MTQiLCJpZCI6Im1zZ18wMVhwcHpxVHNYUW5GUENMcEpjekIyQ0QiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGhpbmtpbmciLCJ0aGlua2luZyI6IlRoZSBwcmV2aW91cyBhbnN3ZXIgd2FzIDggKDUgKyAzID0gOCkuXG5Ob3cgSSBuZWVkIHRvIG11bHRpcGx5IDggYnkgMi5cbjggw5cgMiA9IDE2Iiwic2lnbmF0dXJlIjoiRXZrQkNrWUlDeGdDS2tEaTY2c0tuMlNyV2xGT2xGbXBhRkQveTYxTEIzNlcyZVZiM2ZjR3FTczdoQ1VVc1pLNWc1clpyOWgzaitEZ2tEV1kyYk1GelpuVVFjQmJ2Q1NnVitCNkVneitzVEJXK1hqZm0vU1dteDRhRE41d3dtK1dha3hQKzIyTzZpSXdsdHN2RkN1Q2haNmVGb2x2RmhURVpySTQweEExcmhMTzVMMmJSczB5UXNJSDNTcUtNOXNBVURzNHdNdWhuZHM5S21ITm91eVFQc3Nia1AxTTZTRzZiYU5EZ2E3UHcxeWt1eGFEbmdBTVRoUE5nNlhvUUdTUDJ5OFVIb1QwcWlUVFArT3drU1ZXNXRWZHVYTXdnK0k2clRyNTFHbTNNNkl2SG9YQXFFcWNScDBpTHdQY3NKRTNpTFk1VXc4TTB5TE9uUXJZR0FFPSJ9LHsidHlwZSI6InRleHQiLCJ0ZXh0IjoiOCDDlyAyID0gMTYifV0sInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjY3LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NjEsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn19 + recorded_at: Fri, 02 Jan 2026 17:23:50 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-20250514_returns_thinking_content_when_streaming.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-20250514_returns_thinking_content_when_streaming.yml new file mode 100644 index 000000000..191440e2e --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-20250514_returns_thinking_content_when_streaming.yml @@ -0,0 +1,83 @@ +--- +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 + is 8 * 7? Show your work."}]}],"stream":true,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 17:23:40 GMT + Content-Type: + - text/event-stream; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cache-Control: + - no-cache + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T17:23:39Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T17:23:39Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2026-01-02T17:23:39Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T17:23:39Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '1267' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtb3B1cy00LTIwMjUwNTE0IiwiaWQiOiJtc2dfMDFMS2FyNG9QUXVSM0ZLQ2g3aGdEaFhHIiwidHlwZSI6Im1lc3NhZ2UiLCJyb2xlIjoiYXNzaXN0YW50IiwiY29udGVudCI6W10sInN0b3BfcmVhc29uIjpudWxsLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjQ5LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6OCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifX0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdGFydApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0YXJ0IiwiaW5kZXgiOjAsImNvbnRlbnRfYmxvY2siOnsidHlwZSI6InRoaW5raW5nIiwidGhpbmtpbmciOiIiLCJzaWduYXR1cmUiOiIifSAgICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiJUaGUgdXNlciBpcyBhc2tpbmcgbWUgdG8gY2FsY3VsYXRlICJ9ICAgICAgICAgfQoKZXZlbnQ6IHBpbmcKZGF0YTogeyJ0eXBlIjogInBpbmcifQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IjggKiA3IGFuZCBzaG93In0gICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgbXkgd29yay4gVGhpcyBpcyBhIHNpbXBsZSBtdWx0aXBsaWNhdGlvbiJ9ICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgcHJvYmxlbS5cblxuOCAqIDcifSAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiA9IDU2XG5cbkkgc2hvdWxkIHNob3cifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiB0aGUgd29yayJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIsIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIHdoaWNoIGNvdWwifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiZCBiZSJ9ICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIGRvbmUifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIGluIGEifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgZmV3IHdheXM6XG4xLiBKdXN0In0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgc3RhdGluZyJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiB0aGUgbXVsdGlwbGljYXRpb24ifSAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IlxuMi4gU2hvd2luZyBpdCJ9ICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBhcyJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIHJlcGVhdGUifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiZCBhZGRpdGlvbiJ9ICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiXG4zLiBVc2luZyJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiB0aGUgZGlzdHJpYnV0aXZlIHByb3BlcnR5In0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiXG40LiBPciJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIG90aGVyIn0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIG1ldGhvZHNcblxuSSdsbCBzaG93IGEifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIGNvdXBsZSJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIG9mIGFwcHJvYWNoZXMgdG8gZGVtb25zdHJhdGUifSB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIHRoZSB3b3JrIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiLiJ9ICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoic2lnbmF0dXJlX2RlbHRhIiwic2lnbmF0dXJlIjoiRW9rRUNrWUlDeGdDS2tBNFRDNHQwS2NQRG9Da2FtQ2NIcTlTVHlMT3FLSHpBZWt6M2F1dHI3aHJPM2laVlNCdzdVbUtIdmRDY29sb3F5QzdBME5jcFowWGE4K1VvL2Q2WU5sc0VnekZhY3MxWUsxVkkzZjVWRElhRENzVUV3MTNSaDZYRkZJcU1pSXc3RW9oNXgyWVlCTlpXRnF0K1hVaW9SUEdOMFVUZFQ3VUJ6Z29oRGlQSnZCMG1IMkRhUC9CUk5UWmxFb2RXeGYxS3ZBQ1p3dFdvWVMzWWhhVWVHZ2xRak5WcjhodTJzTy9XSTF2ZERuZDlnN2MyT0hwTk9hUGVRejkxMlJNd2JSYURTS25SU3AwRENxSnJ1RVJaTFYzOWttNzlnblVJcFJmOXZUNlI3aFQxVGpkMVg2ZlErSmxmMEhSaE04cG1CM3BuVGNQRzVhNklpSWJsVU5XSDFmRWhMVWRiUDJLZW1SV3JyZUdYRi9VbjM4S04yOVNzNE5qaWNqQm1Odmk1ZDhaU2RSWFRjYWkybk9xZk1KbXBnNThaU3g5YVNWZ3h0bEJJMUxTeUxrOWZFZTkyUTBoeVpMSmk3TTJGcTVpdTdibm96RVQ3QXR0b2lCNWROb3d3aDZSLzZXSmttVVc2OHduWG1maUo5dVVWRzVvRzE0UEJOSjd5MFIvRktYUUw4ZjJha3gwU0tyVjhLcHdIS2IwVlRkcm9wN2FJS2lVem15ZVhJc3FvT1NDZ3VCRlpqSEROK1p0ZmpDa1Z0U2JJVHZlaGptYVpaZzBneThZcmtZbFk1ZGtPYWd3VTVyT2ZiZkdZaEtHTmF1b01LZ1dmdmc4aG1Kek9RTkw3Q2tGNkRtUERYQ1kzK3U2dERpL1ZBSGhpQzZxYjNQdEgwaXlnc2poOVVBd3BFQ0JhWEhObjVBWUFRPT0ifSAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RvcApkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX3N0b3AiLCJpbmRleCI6MCAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfc3RhcnQKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdGFydCIsImluZGV4IjoxLCJjb250ZW50X2Jsb2NrIjp7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IiJ9ICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiVG8gZmluIn0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6ImQgOCDDlyJ9ICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiA3LCBJIGNhbiBzb2x2ZSJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiB0aGlzIGluIn0gICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgYSBjb3VwbGUifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBvZiB3YXlzOlxuXG4qKk1ldGhvZCAxIn19CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IjogRGlyZWN0IG11bHRpcGxpY2F0aW9uKipcbmBgYCJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlxuICAifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiI4XG7DlyA3XG4tLS0ifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiXG4gIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IjU2XG5gYGBcblxuKipNZXRob2QgMiJ9ICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiI6IFJlcGVhdGVkIGFkZGl0aW9uKipcbjgifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgw5cgNyBtZWFucyJ9ICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIGFkZGluZyA4IHNldmVuIHRpbWVzOiJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlxuOCArIDggKyAifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiOCArIDggKyAifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiOCArIDggKyAifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiOCA9IDU2XG5cbioqTWV0aG8ifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJkIDM6IFVzaW5nIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBrbm93biJ9ICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBmYWN0cyJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIqKlxuSSJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIGNhbiBicmVhayB0aGlzIGRvd24gdXNpbmcifSAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIHRoZSJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIGRpc3RyaWJ1dGl2ZSBwcm9wZXJ0eTpcbi0ifSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiA4IMOXIDcgPSJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgOCDDlyAoNSArIn0gICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiAyKSJ9ICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlxuLSA4IMOXIDUifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiA9IDQwIn0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJcbi0gOCDDlyAyICJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6Ij0gMTZcbi0gNDAgIn0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiKyAxNiA9IDU2In0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJcblxuKioifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IkFuc3dlciJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiI6IDgifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiDDlyA3ID0gNTYifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IioqIn19CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjoxICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2VfZGVsdGEKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9kZWx0YSIsImRlbHRhIjp7InN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsfSwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo0OSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwib3V0cHV0X3Rva2VucyI6Mjk5fSB9CgpldmVudDogbWVzc2FnZV9zdG9wCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2Vfc3RvcCIgICAgfQoK + recorded_at: Fri, 02 Jan 2026 17:23:46 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-20250514_returns_thinking_content_with_response.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-20250514_returns_thinking_content_with_response.yml new file mode 100644 index 000000000..368ab15a2 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-20250514_returns_thinking_content_with_response.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 + is 15 * 23? Think through this step by step."}]}],"stream":false,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 17:23:39 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T17:23:33Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T17:23:39Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2026-01-02T17:23:32Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T17:23:33Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '6691' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1vcHVzLTQtMjAyNTA1MTQiLCJpZCI6Im1zZ18wMUhDeW8xUm9NRWZCS2VNTHh4Q1ZETlciLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGhpbmtpbmciLCJ0aGlua2luZyI6IkkgbmVlZCB0byBjYWxjdWxhdGUgMTUgKiAyMy4gTGV0IG1lIHRoaW5rIHRocm91Z2ggdGhpcyBzdGVwIGJ5IHN0ZXAuXG5cbk1ldGhvZCAxOiBCcmVha2luZyBpdCBkb3duXG4xNSAqIDIzID0gMTUgKiAoMjAgKyAzKVxuPSAxNSAqIDIwICsgMTUgKiAzXG49IDMwMCArIDQ1XG49IDM0NVxuXG5MZXQgbWUgdmVyaWZ5IHdpdGggYW5vdGhlciBtZXRob2QuXG5cbk1ldGhvZCAyOiBCcmVha2luZyBkb3duIDE1XG4xNSAqIDIzID0gKDEwICsgNSkgKiAyM1xuPSAxMCAqIDIzICsgNSAqIDIzXG49IDIzMCArIDExNVxuPSAzNDVcblxuQm90aCBtZXRob2RzIGdpdmUgbWUgMzQ1LCBzbyB0aGF0IHNob3VsZCBiZSBjb3JyZWN0LiIsInNpZ25hdHVyZSI6IkV2Z0RDa1lJQ3hnQ0trQk11QXo1dFUwR3R6Yk1yWkYzSnBmNzloSEljcXlNcHY0N3ZjaE16UVBNUmp2TDRJYXhuOU5kRE0yeWhZM3RHZEgzSjl4V0ErOWJRRlc4eE9ubkNNTjFFZ3oydWwxZ0hWclUwbnZOV3FjYURMemNXSE1QUFlQem41c3YzQ0l3WFBZbXV6elZoNlJsMDZDNU8wM3BDQnRUMUJzMWorNGJnZzVISGFpSjFWdm40ZzBqS0pub0Ira3hsdFdkWU5LRkt0OENQRE9XSmJhdlFpZjVaQ3FnODBiSEZ6cjBvL01WTE1YbTRXODE4U3A3T1BibjRYOENVdzJGRTRBa3ZlNHlTVFdKUlZQcllwL011bFBibCtLWFQ4dzlOME9FM0owWEpzZmRhc2Z6dzdPL0QweXNZSEdzcmlZQlV0bEVzOGtOZ0w5N3lwdVAzRFpZTnY4b1RDcWZHSjYrbzF6RjBlcTlEUTIrRnU4dVpmQThsWXJ0aDdsM1ZLd2NYN2ZnelRIOTQzVjJaK25lRFh4L0loWWJWN2ZncDl1SkVHMUxtNzl3cCtpVTV3YTNvdHhNaFlTMUVuZXRMSzhoQjNRTXlzdCtORDk1QVdEeXhWSkY1QWxjdmZWT0ptandOZGVnU2FTRXhRaEF4VzV2ZmtiWHZlZlZWcmQ1UlBTTGRjWUdMcE9Qb0s0WHZCcEtiTnFCVHJEbHZBR0szTWk5eUwyRjBYMW90dmlhbTFPV3hwdjE4U1lUSGpweVRSdkZJeGExd210eHhyVGh2MVdWUVI1UkZnMkVFK1F0Q3MrWEViakZGRkNybktwa25MTXIwcVh1dTlQSldlT1c1K2tPODB6RHRsNi9rSk52ZUduRkM4Wk5XQW1JT0FJeGROQy9HQUU9In0seyJ0eXBlIjoidGV4dCIsInRleHQiOiJJJ2xsIHNvbHZlIDE1IMOXIDIzIHN0ZXAgYnkgc3RlcC5cblxuTWV0aG9kIDE6IEJyZWFrIGRvd24gMjMgaW50byAyMCArIDNcbi0gMTUgw5cgMjMgPSAxNSDDlyAoMjAgKyAzKVxuLSAxNSDDlyAyMCA9IDMwMFxuLSAxNSDDlyAzID0gNDVcbi0gMzAwICsgNDUgPSAzNDVcblxuTWV0aG9kIDI6IExldCBtZSB2ZXJpZnkgYnkgYnJlYWtpbmcgZG93biAxNSBpbnRvIDEwICsgNVxuLSAxNSDDlyAyMyA9ICgxMCArIDUpIMOXIDIzXG4tIDEwIMOXIDIzID0gMjMwXG4tIDUgw5cgMjMgPSAxMTVcbi0gMjMwICsgMTE1ID0gMzQ1XG5cblRoZXJlZm9yZSwgMTUgw5cgMjMgPSAzNDUifV0sInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjUyLCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MzQxLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9fQ== + recorded_at: Fri, 02 Jan 2026 17:23:39 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-5-20251101_maintains_thinking_across_multi-turn_conversation.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-5-20251101_maintains_thinking_across_multi-turn_conversation.yml new file mode 100644 index 000000000..5dbb30d6e --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-5-20251101_maintains_thinking_across_multi-turn_conversation.yml @@ -0,0 +1,163 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-opus-4-5-20251101","messages":[{"role":"user","content":[{"type":"text","text":"What + is 5 + 3?"}]}],"stream":false,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 21:47:19 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T21:47:18Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T21:47:19Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2026-01-02T21:47:17Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T21:47:18Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '1398' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"model":"claude-opus-4-5-20251101","id":"msg_01719FsPNBYWetWpxFGoQiza","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"The + user is asking a simple arithmetic question: 5 + 3.\n\n5 + 3 = 8","signature":"EuoBCkYICxgCKkCuo1dP5TeGHNcxk0Hzj2E/ShHQQDMOH3/Rp6EgZxXJHGGPdkCNYyBQlS6utB5CbWIUlUJp0t6mK1l7UeGCNjIuEgzsIvdAoXSYZw0peVUaDGaOrx0au9UUwQWH1CIwHSqXMR2ewV1Ii79ncaszVQ6plNIpyykK67uRhee4FnU2EPQ5q0/JpfQI/gQo/PnVKlI7kAeiWcEdUvn+r1VonXc4Ermbj/YJ62IIr6VLEPnD4jOWODzLS2l07VhBjVdq+LPImscRTNh+gaKV6y11mxIiDceTlgplsgKv8lNLCNJML9ACGAE="},{"type":"text","text":"5 + + 3 = 8"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":45,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":46,"service_tier":"standard"}}' + recorded_at: Fri, 02 Jan 2026 21:47:19 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-opus-4-5-20251101","messages":[{"role":"user","content":[{"type":"text","text":"What + is 5 + 3?"}]},{"role":"assistant","content":[{"type":"thinking","thinking":"The + user is asking a simple arithmetic question: 5 + 3.\n\n5 + 3 = 8","signature":"EuoBCkYICxgCKkCuo1dP5TeGHNcxk0Hzj2E/ShHQQDMOH3/Rp6EgZxXJHGGPdkCNYyBQlS6utB5CbWIUlUJp0t6mK1l7UeGCNjIuEgzsIvdAoXSYZw0peVUaDGaOrx0au9UUwQWH1CIwHSqXMR2ewV1Ii79ncaszVQ6plNIpyykK67uRhee4FnU2EPQ5q0/JpfQI/gQo/PnVKlI7kAeiWcEdUvn+r1VonXc4Ermbj/YJ62IIr6VLEPnD4jOWODzLS2l07VhBjVdq+LPImscRTNh+gaKV6y11mxIiDceTlgplsgKv8lNLCNJML9ACGAE="},{"type":"text","text":"5 + + 3 = 8"}]},{"role":"user","content":[{"type":"text","text":"Now multiply + that by 2"}]}],"stream":false,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 21:47:21 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T21:47:20Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T21:47:21Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2026-01-02T21:47:19Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T21:47:20Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '2657' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1vcHVzLTQtNS0yMDI1MTEwMSIsImlkIjoibXNnXzAxSlduM0M5Y2JzWDNpcG8xcFFHc0N3bSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOlt7InR5cGUiOiJ0aGlua2luZyIsInRoaW5raW5nIjoiVGhlIHVzZXIgd2FudHMgbWUgdG8gdGFrZSB0aGUgcHJldmlvdXMgcmVzdWx0ICg4KSBhbmQgbXVsdGlwbHkgaXQgYnkgMi5cblxuOCDDlyAyID0gMTYiLCJzaWduYXR1cmUiOiJFdndCQ2tZSUN4Z0NLa0JKa1pCYjVlOVAySkZaL0tvYUQxT1R0Q2tsWmpGRzBUZVJrNExBdTBXNm9tWU5CWWtHK3h0eHpVVVlBVFZDTlZ3a0QzNFdnV1Jjb1I5RjFJaWgwMTh2RWd3V2RwRXFkazMvbi8wRGJIVWFETEJhM1FXei9OblZqeGlJbVNJdzJUQnJoSkZvWHgxN2pHUFZIZVd3MTY2Z1NGQjhYTDdyOTlmaEtBbnBTOVRzTFZ6UElhQmRhQ0ljSllwcUdlU2xLbVRQNkxacEV5NEdnUnhjWjFMelJuQ2dHekxiMVQ5b3lPaWJCTlM5Kzl0TE5teitXaVorMGd6RldBY1lQNUc2WUlZZ3RBYXNaWC94UE82a0IrM1ZVdmZaa3Ewb0NmR1pBZ1FyRXMrSjlJbUdsV01iTU5zT1c3RzdZZmxuTHFCUUwreUQvWkFNR0FFPSJ9LHsidHlwZSI6InRleHQiLCJ0ZXh0IjoiOCDDlyAyID0gMTYifV0sInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEwNywiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjQ5LCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9fQ== + recorded_at: Fri, 02 Jan 2026 21:47:21 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-5-20251101_returns_thinking_content_when_streaming.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-5-20251101_returns_thinking_content_when_streaming.yml new file mode 100644 index 000000000..b758d5d7d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-5-20251101_returns_thinking_content_when_streaming.yml @@ -0,0 +1,83 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-opus-4-5-20251101","messages":[{"role":"user","content":[{"type":"text","text":"What + is 8 * 7? Show your work."}]}],"stream":true,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 21:47:14 GMT + Content-Type: + - text/event-stream; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cache-Control: + - no-cache + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T21:47:13Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T21:47:13Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2026-01-02T21:47:13Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T21:47:13Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '808' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtb3B1cy00LTUtMjAyNTExMDEiLCJpZCI6Im1zZ18wMUNnZVpjakYyRHVwRFgzR0p1RWpMTlAiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbXSwic3RvcF9yZWFzb24iOm51bGwsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6NDksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoxLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9fSAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MCwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidGhpbmtpbmciLCJ0aGlua2luZyI6IiIsInNpZ25hdHVyZSI6IiJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiJUaGUifSAgICAgICB9CgpldmVudDogcGluZwpkYXRhOiB7InR5cGUiOiAicGluZyJ9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIHVzZXIgaXMgYXNraW5nIG1lIHRvIn0gICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBjYWxjdWxhdGUgOCAqIn0gICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiA3IGFuZCBzaG93In0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgbXkgd29yay4ifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiXG5cbjggKiAifSAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiI3ID0gNTYifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiJcblxuVG8ifSB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIHNob3cgd29yayJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiwgSSBjYW4ifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgYnJlYWsifX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgdGhpcyBkb3duOlxuLSJ9ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDggKiAifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IjcgbWVhbnMifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBhZGRpbmcifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiA4IHNldmVuIn0gICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiB0aW1lczoifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiA4ICsgIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiOCArIDgifSB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiICsgOCAifSAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIrIDggKyJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgOCArICJ9ICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiI4In0gICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiJcbi0gT3IifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBJIn0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIGNhbiB0aGluayJ9fQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBvZiBpdCBhcyAoIn0gICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiI4ICogNSJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIpICsgKDggIn0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIqIDIpID0ifSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDQwICsgIn0gfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IjE2ID0gNTYifSAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiJ9ICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoic2lnbmF0dXJlX2RlbHRhIiwic2lnbmF0dXJlIjoiRW9rRENrWUlDeGdDS2tEcmxoR2tJR0JzOVZMZjlFREhBQzRXVVc4Z2xRcUo2b3hZZThIQ1h5TTQ3U20wZ1o0OXQzV1RMUTBQdHpoaERoWWFEUmU2dW9XK055NEwwTFB1ekJHZUVnenF2RkczR3R2TDZVMDlPdU1hREMwNldPTm5KNlhSa2o3cDBDSXdzVUFZTitqMUVyWHA4K09qMzRjN28rNkFuMmxJNmdlS0phVkltWS9keVVkMnVhNUpobS9waTNwSTgyZzduOTRxS3ZBQllqdzRRbFBEMDJldG1wNkcyOGRaQ2JsaHcwK3h1VmJ0WVp4ZUNvN0IwcDk1K3FUUTRZdGhBNmJVbFVsc3k1dm14RFcwSm5OWjV6Y3BqVDFRcmpYRW9kbnRaRlFKcG1Cb1ZwYzZHbzc3elNvVkpIdEtGNFNCMHdIeTcvM1BMOHE0Rk90b0t3UDdETEpQRG9PdUtkWk1Wc2ZPVnpzYmVxUUh5Q2JvZ3pHYUE4SXFTUldhcklTVDJTdmtxQ1hhZTFxZnR0NmlLazNWQnkrODNGYXVMbmNuT2JuVnAwZjBDK2pJcDNLTHNMNnFzN09PZW5PblNBd2JZTUIwTUVDV2VkOHRmdUg3WFFkaTMxTkNsUzRTaElPc3RGdnJ6b1N3RTlwRUM2Wm9EOXZ5bDRMOWJYS1BQVjZaN0U5VGh1Qm0xSGtYR0FFPSJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MSwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidGV4dCIsInRleHQiOiIifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiMifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiAifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiI4IMOXIn0gICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIDcifSAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJcblxuKioifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJNZXRobyJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6ImQ6KioifSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBJIn0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgY2FuIGJyZWFrIn0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiB0aGlzIGludG8ifSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBzaW0ifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6InBsZXIgcGFydHM6In0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiXG5cbjggw5cgNyJ9ICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgPSA4ICJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0Ijoiw5cgKDUifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgKyAyKSJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJcbjgifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIMOXIDUifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgPSA0MCJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJcbjggw5cgMiJ9IH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiID0gMTYifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlxuNDAifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiArIDE2ICJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6Ij0gKioifSAgICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiNTYqKlxuXG4qKiJ9ICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJBbnN3ZXIifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiOiA1NioqIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjoxICAgIH0KCmV2ZW50OiBtZXNzYWdlX2RlbHRhCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2VfZGVsdGEiLCJkZWx0YSI6eyJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbH0sInVzYWdlIjp7ImlucHV0X3Rva2VucyI6NDksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsIm91dHB1dF90b2tlbnMiOjIwNn19CgpldmVudDogbWVzc2FnZV9zdG9wCmRhdGE6IHsidHlwZSI6Im1lc3NhZ2Vfc3RvcCIgICAgIH0KCg== + recorded_at: Fri, 02 Jan 2026 21:47:17 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-5-20251101_returns_thinking_content_with_response.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-5-20251101_returns_thinking_content_with_response.yml new file mode 100644 index 000000000..6b785ed51 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-opus-4-5-20251101_returns_thinking_content_with_response.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-5-20251101","messages":[{"role":"user","content":[{"type":"text","text":"What + is 15 * 23? Think through this step by step."}]}],"stream":false,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 21:47:13 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T21:47:10Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T21:47:13Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2026-01-02T21:47:08Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T21:47:10Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '4808' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1vcHVzLTQtNS0yMDI1MTEwMSIsImlkIjoibXNnXzAxMW5VQ2N5UG03RFd2cmU4UjhkVUgxSyIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOlt7InR5cGUiOiJ0aGlua2luZyIsInRoaW5raW5nIjoiSSBuZWVkIHRvIGNhbGN1bGF0ZSAxNSAqIDIzIHN0ZXAgYnkgc3RlcC5cblxuSSBjYW4gYnJlYWsgdGhpcyBkb3duIGluIGEgZmV3IHdheXM6XG5cbk1ldGhvZCAxOiBCcmVhayBkb3duIDIzXG4xNSAqIDIzID0gMTUgKiAoMjAgKyAzKVxuPSAxNSAqIDIwICsgMTUgKiAzXG49IDMwMCArIDQ1XG49IDM0NVxuXG5NZXRob2QgMjogQnJlYWsgZG93biAxNVxuMTUgKiAyMyA9ICgxMCArIDUpICogMjNcbj0gMTAgKiAyMyArIDUgKiAyM1xuPSAyMzAgKyAxMTVcbj0gMzQ1XG5cbkJvdGggbWV0aG9kcyBnaXZlIDM0NS4iLCJzaWduYXR1cmUiOiJFcnNEQ2tZSUN4Z0NLa0RVM0VJY1dGOXI4QTEvSkJMNjYyZ3Y2eGwwQnZSSTVBVUZiOFd3QU1nbDlocmpOOGs2a0pic0hzQStxeG5pN0ZqS0Jpd3RKdS81NmRkdjh2eS8rWEl6RWd4RXpheDVtRUttZ2ZSbHMySWFEUFNDb3NJM3lVczlwa2YrVVNJd1RiQjhiQStQVjQ4UmNVUzZYVTN3SDNaeTNWMlhoOTlBMWx0M2dKL2lUbFZadTg2Q05neCtZbTJFa0Q0b1BselhLcUlDY3RjaUU1d1JSMkY5OU95OFU5YkpTZnN4MVBteGFxb2tlOW9VdVJOZ2RTK0VFbVZIWG5xVUlkbFRCcWpjRTlINXdJTkFMRTlFalphcTNpSEhRR1pZdWw2RE1QaXE1TWsvRGZZYVNlV2lCTkJtVXJBMHpoeVB1WjBlQW9OcUtxVmYvQko5MXN6YWE2M2tKMWVvWUhWTElhaXhVRUM2Tk1PMGVmRDB2NDJTaVdQZUpmVTZwelovbm94dEI4VUNadmZkamlKVjljeGNKTW1kWmMxNFdxTDIvbzd1eGxIbWh1UlYrYmdQRml1RXp4R1J4VWpLdU83NTQ3REVvWnBXMFpmT0IyTEErWjBCMGE3c3NLbUJ0QTVoVDVBS2VkSmg4NjUvbytPWmxMNGIzSG8rUGdtUmJYTDRwSnNFUGQ4NlZvUWE4azg2eWtVS1p3cjczVkFLaXBsM2VzcElqWDR6WUJ6MFNNMVVBalpkT1pXbDlwd1BzRExkVUl3aUZaQ3BlWXdKQWw4WUFRPT0ifSx7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IiMgQ2FsY3VsYXRpbmcgMTUgw5cgMjNcblxuSSdsbCBicmVhayB0aGlzIGRvd24gaW50byBzaW1wbGVyIHBhcnRzOlxuXG4qKlN0ZXAgMToqKiBCcmVhayAyMyBpbnRvIDIwICsgM1xuXG4qKlN0ZXAgMjoqKiBNdWx0aXBseSBlYWNoIHBhcnQgYnkgMTVcbi0gMTUgw5cgMjAgPSAzMDBcbi0gMTUgw5cgMyA9IDQ1XG5cbioqU3RlcCAzOioqIEFkZCB0aGUgcmVzdWx0c1xuLSAzMDAgKyA0NSA9ICoqMzQ1KipcblxuVGhlIGFuc3dlciBpcyAqKjM0NSoqLiJ9XSwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6NTIsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjoyNTgsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn19 + recorded_at: Fri, 02 Jan 2026 21:47:13 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-sonnet-4-20250514_maintains_thinking_across_multi-turn_conversation.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-sonnet-4-20250514_maintains_thinking_across_multi-turn_conversation.yml new file mode 100644 index 000000000..c681ab814 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-sonnet-4-20250514_maintains_thinking_across_multi-turn_conversation.yml @@ -0,0 +1,151 @@ +--- +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 + is 5 + 3?"}]}],"stream":false,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 12:40:24 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T12:40:24Z' + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T12:40:23Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T12:40:23Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '1403' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"model":"claude-sonnet-4-20250514","id":"msg_01Cms2n7oDouTyLLt1JVGTWu","type":"message","role":"assistant","content":[{"type":"thinking","thinking":"This + is a simple arithmetic question. 5 + 3 = 8.","signature":"EtgBCkYICxgCKkCyqtg4YSovHjJWjT5xWNBV0HDNY0NkeiSISwchPehu+JHqF14GKTlprSnmlk1ohL26KlGnQRhwg33jqkxTjsJiEgyJp6r0bv+e1FkyczMaDIHYPD0JGDX98H7YHCIwuMvyp3BNE6CeOnVdzkaYHtV3ff26cg/CVde+9WSWggmubj3VqchQcWlrFhjYwgisKkCuVkiIk/mf1BnRuZJeYL67NBl9dF/fzbl5anoX9rBM7Q2iqKwVh0ekM8zI2gILGEqG1xsRIuR8em/bzOKjnJuJGAE="},{"type":"text","text":"5 + + 3 = 8"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":45,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":38,"service_tier":"standard"}}' + recorded_at: Fri, 02 Jan 2026 12:40:24 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 + is 5 + 3?"}]},{"role":"assistant","content":[{"type":"thinking","thinking":"This + is a simple arithmetic question. 5 + 3 = 8.","signature":"EtgBCkYICxgCKkCyqtg4YSovHjJWjT5xWNBV0HDNY0NkeiSISwchPehu+JHqF14GKTlprSnmlk1ohL26KlGnQRhwg33jqkxTjsJiEgyJp6r0bv+e1FkyczMaDIHYPD0JGDX98H7YHCIwuMvyp3BNE6CeOnVdzkaYHtV3ff26cg/CVde+9WSWggmubj3VqchQcWlrFhjYwgisKkCuVkiIk/mf1BnRuZJeYL67NBl9dF/fzbl5anoX9rBM7Q2iqKwVh0ekM8zI2gILGEqG1xsRIuR8em/bzOKjnJuJGAE="},{"type":"text","text":"5 + + 3 = 8"}]},{"role":"user","content":[{"type":"text","text":"Now multiply + that by 2"}]}],"stream":false,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 12:40:26 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T12:40:25Z' + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T12:40:25Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T12:40:25Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '1620' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1zb25uZXQtNC0yMDI1MDUxNCIsImlkIjoibXNnXzAxNHc1bWVaYXR0TTF1M3lUTVE0NzZtMSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOlt7InR5cGUiOiJ0aGlua2luZyIsInRoaW5raW5nIjoiVGhlIHVzZXIgYXNrZWQgbWUgdG8gbXVsdGlwbHkgdGhlIHJlc3VsdCBmcm9tIHRoZSBwcmV2aW91cyBjYWxjdWxhdGlvbiAod2hpY2ggd2FzIDgpIGJ5IDIuXG5cbjggw5cgMiA9IDE2Iiwic2lnbmF0dXJlIjoiRW84Q0NrWUlDeGdDS2tERGFDNDVQNkhyQjltRmkzUGViaVVsS3VDeHJkYzlPTStqejdvNFlmeFlkamM5TGE4SGNLZ25hNjlheFlYT3RVS05XWVQzTzExQVV2V1dXbzM4RnFuYkVneWwwaHczVGh5blNHR3FDaDRhREVKTjJkM3pjM2c3YXZpd0RDSXd5bFQwSWtoMjZHQUNubFErQzUzTGpsWTU0Zkl4ejkrZUJhb2ZFVGwzL0dRUkdHeUZybmRweXZHQzJ1VWRmeEE5S25mT3lxQnVSMjlmV3pXT3hCOXplR1I5bmZZUk1SdVpXTFAySDkxeVBLUk45OEVtMEdGZkFHcUJsU21WUVRIOFdDQlh3Y3FBTFgycG1iN0VORW1pRzlocWlFMXpPOThCRnJUaXV2aGNKMUhpQWJTRFQxSzRzenYybC9iTjhHaWkrTTFuS3ZpYkdEM0k1TnNmSytjMlBWUVV1Y3JRMU4rSllSZ0IifSx7InR5cGUiOiJ0ZXh0IiwidGV4dCI6Ijggw5cgMiA9IDE2In1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo2NywiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjUyLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9fQ== + recorded_at: Fri, 02 Jan 2026 12:40:25 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-sonnet-4-20250514_returns_thinking_content_when_streaming.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-sonnet-4-20250514_returns_thinking_content_when_streaming.yml new file mode 100644 index 000000000..d74e56dee --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-sonnet-4-20250514_returns_thinking_content_when_streaming.yml @@ -0,0 +1,77 @@ +--- +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 + is 8 * 7? Show your work."}]}],"stream":true,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 12:40:17 GMT + Content-Type: + - text/event-stream; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Cache-Control: + - no-cache + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T12:40:17Z' + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T12:40:17Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T12:40:17Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '658' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + ZXZlbnQ6IG1lc3NhZ2Vfc3RhcnQKZGF0YTogeyJ0eXBlIjoibWVzc2FnZV9zdGFydCIsIm1lc3NhZ2UiOnsibW9kZWwiOiJjbGF1ZGUtc29ubmV0LTQtMjAyNTA1MTQiLCJpZCI6Im1zZ18wMVNLRkszcDZRZm9EeFJDQzVCaFRVclUiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbXSwic3RvcF9yZWFzb24iOm51bGwsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6NDksImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo4LCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9fX0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MCwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidGhpbmtpbmciLCJ0aGlua2luZyI6IiIsInNpZ25hdHVyZSI6IiJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiSSBuZWVkIHRvIGNhbGN1bGF0ZSA4ICoifX0KCmV2ZW50OiBwaW5nCmRhdGE6IHsidHlwZSI6ICJwaW5nIn0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgNy4ifSB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIFRoaXMifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBpcyBhIn0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBiYXNpYyBtdWx0aXBsaWNhdGlvbiBwcm9ibGVtLiJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiXG5cbjggKiA3IG1lYW5zIn0gICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDggZ3JvdXBzIn0gICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIG9mIDcsIn0gICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIG9yIn0gICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDcifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIGdyb3VwcyBvZiA4LlxuXG5JIn0gICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBjYW4ifSAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgdGhpbmsifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIG9mIHRoaXMgYXM6XG43In0gfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiArIn0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgNyArIDcgKyJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDcgKyA3ICsifSAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDcgKyA3ICsifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDcgPSA/In0gICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiXG5cbkxldCBtZSBhZGQgc3RlcCJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBieSBzdGVwOlxuNyJ9ICAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiArIDcgPSAxNCJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiJcbjE0ICsgNyA9In0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDIxXG4yMSArIDcifSB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiID0gMjhcbjI4ICsifSAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDcgPSAzNVxuMzUifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiArIDcgPSA0MiJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiXG40MiArIDcgPSJ9ICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIDQ5XG40OSArIDcifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiID0gNTYifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IlxuXG5TbyA4ICogNyAifSAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiI9IDU2In0gICAgICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiXG5cbkFsdGVybmF0aXZlbHksIEkga25vdyJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIHRoYXQgOCAqIDcgIn0gICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6Ij0gNTYgIn0gICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiZnJvbSJ9ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjAsImRlbHRhIjp7InR5cGUiOiJ0aGlua2luZ19kZWx0YSIsInRoaW5raW5nIjoiIHRoZSJ9fQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjowLCJkZWx0YSI6eyJ0eXBlIjoidGhpbmtpbmdfZGVsdGEiLCJ0aGlua2luZyI6IiBtdWx0aXBsaWNhdGlvbiJ9ICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIgdGFibGUuIn0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InRoaW5raW5nX2RlbHRhIiwidGhpbmtpbmciOiIifSAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MCwiZGVsdGEiOnsidHlwZSI6InNpZ25hdHVyZV9kZWx0YSIsInNpZ25hdHVyZSI6IkVvd0VDa1lJQ3hnQ0trQW1LT3c1aVA5STJIVXduZEZwdTJtSjVTQ2trZjEzcDdIaUc1dlUwZTZBdCt3ZjVwaHFMTlY1Z0JuRmhwUHNpWmp4bVVsVDArN3pDMEl2ZzUrZG9ibTBFZ3dMZkQ0ak5DVVl2YkRWVjNrYURETTlibmwzbU5iRXh6b1NlaUl3U3pKQ3dINjNrdUhkcHgrOWhxcVZTSFNnSG40NFFyMjE1c0F3N250UUd4UXhRZlZnb21GOWl5eEJWV0h5dm5TbEt2TUNmMzAyMCtHUk1UaHJ6ck13L0MyZDRpWlp3bTFaMEFrWmJXeVZxZVBGUW9iZFdUcHFEVncwcE9RTTd3VkMxUFVVNlVkSnQ3VWNiQzMxOGg5aGR4THFSVkZteklyOEZkWkRza0JaY3ViRXRrcDFuVVhCTUgzTHBqSDFTSkRrWUZHeWVtZUREbENiZnRsNTZzMmdPT0RqTmtzU2pMbWtwQzJBRm51THJacHdkRzRnKzdIU3FxOXZmZzNGVm9uZVhuTFdVb1ZlMFB0U3RGNy85RnBFdmNtR0g1NG9tS2lVRlFodTdMSlZUYkYyazdQT05DYThpczlid2UrQmMvdWhQZ21lYWNBVVp4TnF2cm9JdVRJd3RDQ01VWEMybGJNUEFTTnVpSXVPeUNUdTJWY05wKy9qb2JieUR0TjhmcWxNbFNJNTFmTU0rZGJTYjBhZ1BrODhGd1hWdVpkakdDYXJrWm5DdjE5TEJ1R1U3NjN2NjdkRHJCYTJHbEE5TjlKZDdjM0xMRVNxSG5xYnhHRVJtRk5yQ2hsazNsKy94U1lGTU1ZcFZ2MlAyK0hSdjcwZUJmYWNIcjZabEtqSFI2OHFEejliWk5EVCt4VXlOc0phSTdmeFVVUS96eFpkY3ExZGtDSWtjQXdDWDg4M0NzOXRRNjRZQVE9PSJ9ICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0b3AKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19zdG9wIiwiaW5kZXgiOjAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX3N0YXJ0CmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RhcnQiLCJpbmRleCI6MSwiY29udGVudF9ibG9jayI6eyJ0eXBlIjoidGV4dCIsInRleHQiOiIifSAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiSSJ9fQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiInbGwifSAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgc29sdmUgIn0gICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6Ijggw5cifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgNyBieSBzaG93aW5nIn0gICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiIHRoZSBtdWx0aXBsaWNhdGlvbjpcblxuKioifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJNZXRobyJ9ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6ImQgMTogUmVwZWF0ZWQgQWRkaXRpb24qKiJ9ICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiXG44IMOXIDcgbWVhbnMifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBhZGRpbmcifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgNyJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiBlaWdodCJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiB0aW1lczpcbjcifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgKyA3ICsgNyJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiICsgNyArIDcifSAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgKyA3ICsgNyJ9fQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgKyA3In0gICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiXG5cbkxldCBtZSBhZGQgc3RlcCBieSBzdGVwOiJ9ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlxuLSA3ICsgNyAifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiPSAxNFxuLSAxNCAifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiKyA3ID0gMjEgICJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlxuLSAyMSArIDcifSAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiID0gMjhcbi0gMjgifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgKyA3ID0gMzUifSAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiXG4tIDM1ICsgNyJ9ICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiIgPSA0MlxuLSA0MiJ9ICAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiArIDcgPSA0OSJ9ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlxuLSA0OSArIDcifSAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiID0gNTZcblxuKioifSAgICAgICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJNZXRob2QgMjogU3RhbmRhciJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiZCBNdWx0aXBsaWNhdGlvbioqXG5gYGAifSAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiXG4gICAgIn0gICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiNyJ9ICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6Ilxuw5cifSAgICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IiAgICJ9ICAgICAgICAgICAgIH0KCmV2ZW50OiBjb250ZW50X2Jsb2NrX2RlbHRhCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfZGVsdGEiLCJpbmRleCI6MSwiZGVsdGEiOnsidHlwZSI6InRleHRfZGVsdGEiLCJ0ZXh0IjoiOCJ9ICAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6IlxuLS0tLS0ifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJcbiAgICJ9fQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiI1NlxuYGBgXG5cbioqIn0gICAgICAgICAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiJBbnN3ZXIifSAgfQoKZXZlbnQ6IGNvbnRlbnRfYmxvY2tfZGVsdGEKZGF0YTogeyJ0eXBlIjoiY29udGVudF9ibG9ja19kZWx0YSIsImluZGV4IjoxLCJkZWx0YSI6eyJ0eXBlIjoidGV4dF9kZWx0YSIsInRleHQiOiI6IDggw5cgNyAifSAgICAgICB9CgpldmVudDogY29udGVudF9ibG9ja19kZWx0YQpkYXRhOiB7InR5cGUiOiJjb250ZW50X2Jsb2NrX2RlbHRhIiwiaW5kZXgiOjEsImRlbHRhIjp7InR5cGUiOiJ0ZXh0X2RlbHRhIiwidGV4dCI6Ij0gNTYqKiJ9ICB9CgpldmVudDogY29udGVudF9ibG9ja19zdG9wCmRhdGE6IHsidHlwZSI6ImNvbnRlbnRfYmxvY2tfc3RvcCIsImluZGV4IjoxICB9CgpldmVudDogbWVzc2FnZV9kZWx0YQpkYXRhOiB7InR5cGUiOiJtZXNzYWdlX2RlbHRhIiwiZGVsdGEiOnsic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGx9LCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjQ5LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJvdXRwdXRfdG9rZW5zIjo0MTJ9ICAgICAgICAgICAgfQoKZXZlbnQ6IG1lc3NhZ2Vfc3RvcApkYXRhOiB7InR5cGUiOiJtZXNzYWdlX3N0b3AiICAgICAgIH0KCg== + recorded_at: Fri, 02 Jan 2026 12:40:22 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-sonnet-4-20250514_returns_thinking_content_with_response.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-sonnet-4-20250514_returns_thinking_content_with_response.yml new file mode 100644 index 000000000..87917aada --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_anthropic_claude-sonnet-4-20250514_returns_thinking_content_with_response.yml @@ -0,0 +1,75 @@ +--- +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 + is 15 * 23? Think through this step by step."}]}],"stream":false,"max_tokens":9024,"thinking":{"type":"enabled","budget_tokens":1024}}' + headers: + User-Agent: + - Faraday v2.14.0 + 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, 02 Jan 2026 12:40:17 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '399000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2026-01-02T12:40:17Z' + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2026-01-02T12:40:10Z' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2399000' + Anthropic-Ratelimit-Tokens-Reset: + - '2026-01-02T12:40:10Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + Server: + - cloudflare + X-Envoy-Upstream-Service-Time: + - '7485' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1zb25uZXQtNC0yMDI1MDUxNCIsImlkIjoibXNnXzAxOTZVUHVIRXRIZEt6SnJCSG9XSFA0TSIsInR5cGUiOiJtZXNzYWdlIiwicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOlt7InR5cGUiOiJ0aGlua2luZyIsInRoaW5raW5nIjoiSSBuZWVkIHRvIGNhbGN1bGF0ZSAxNSAqIDIzLiBMZXQgbWUgZG8gdGhpcyBzdGVwIGJ5IHN0ZXAgdXNpbmcgdGhlIHN0YW5kYXJkIG11bHRpcGxpY2F0aW9uIG1ldGhvZC5cblxuMTUgKiAyM1xuXG5JIGNhbiBicmVhayB0aGlzIGRvd24gYXM6XG4xNSAqIDIzID0gMTUgKiAoMjAgKyAzKSA9ICgxNSAqIDIwKSArICgxNSAqIDMpXG5cbkZpcnN0LCBsZXQgbWUgY2FsY3VsYXRlIDE1ICogMjA6XG4xNSAqIDIwID0gMTUgKiAyICogMTAgPSAzMCAqIDEwID0gMzAwXG5cblRoZW4sIGxldCBtZSBjYWxjdWxhdGUgMTUgKiAzOlxuMTUgKiAzID0gKDEwICsgNSkgKiAzID0gKDEwICogMykgKyAoNSAqIDMpID0gMzAgKyAxNSA9IDQ1XG5cblNvOiAxNSAqIDIzID0gMzAwICsgNDUgPSAzNDVcblxuTGV0IG1lIGRvdWJsZS1jaGVjayB0aGlzIHVzaW5nIHRoZSBzdGFuZGFyZCBtdWx0aXBsaWNhdGlvbiBhbGdvcml0aG06XG4gICAgMjNcbiAgw5cgMTVcbiAgLS0tLVxuICAgMTE1ICAoMjMgw5cgNSlcbiAgMjMwICAgKDIzIMOXIDEwLCBzaGlmdGVkIG9uZSBwbGFjZSlcbiAgLS0tLVxuICAzNDVcblxuMjMgw5cgNSA9IDExNVxuMjMgw5cgMSAoaW4gdGhlIHRlbnMgcGxhY2UpID0gMjMsIGJ1dCBzaW5jZSBpdCdzIGluIHRoZSB0ZW5zIHBsYWNlLCBpdCdzIGFjdHVhbGx5IDIzIMOXIDEwID0gMjMwXG5cbjExNSArIDIzMCA9IDM0NVxuXG5ZZXMsIHRoYXQncyBjb3JyZWN0LiIsInNpZ25hdHVyZSI6IkV0Y0dDa1lJQ3hnQ0trRCtaYnRLdWpYa0RMOEs2bGFFNHZMcFNvSy9aY0ZWMUMzVncvdzJpT0cwYUVFZkVORmhSNVVReTdTc1BtNXZUdmM0NHZ5Q3F1aHRoNmRJWmJJMnlTUlhFZ3dST2RVdk1iVHg5bWZyUkhJYURCTzBJTTYwZytKWktHKzNwQ0l3d2NpeUxrOVkyaVRyN3dtMnRXVmF1TVYzbi9ZTElZbzl4eWFZd1NGU3lPdk9oRG1pem5PMmR2KzMxZFJ0Z2NNdUtyNEZDbHVJczlDanJkblMrOFZzVVNyMWNYbTNRa3ozMVlYbUNveWtFZjUwc0xCRldITVZBWlA0L2tVMFhJbzd5TDdUa3lSWTNYWHBIVFVGQVpQQ3BBYW1OWEZCY2V6a1Z4Z0QycS9yMk5sN2hMcEsramhlZHQrbHZ6aFY5YWF4TERlLzhOR0krMnhLYk11cEw5YVNqa05vN1VpaVV0K3lFa1NMRDVPbWZldGNaU205V21uMk04NFdLUVVaTitXbUo2MEdHL3V0VTByYUQ5YXhkUjVmWEZBUnJPTTVVNEJYVHBZdzBySGRZZldlSjV3U052bTRzdERvd2ppMEpSK3dTcisybm5Eamd4NThkSDRUcmpUTFdhcXRoV1psRWRtcHhjekVKb1JYT0E1TmVDUm9MR1FjRlFkb3hSRnI2NHpDcjRDYnRiZmoyUjBibFN2M1gyc3ZYV1ZBWWtYWDlvSXR0Y2VRV0E4L0ZoZS82SDdZVlBxT3VKLysyUGVPZUdvekwxWkluaUcrcEFuem10a1NCK1JLVjU3SHYzczZMRmtGWDRONFo1aG1pUjQwMmxoWTlmVGUzdm5BNi9OaUlGYUtBdklhUk9VL0Frc1FZdW43Q3JSTkRWMkNtQ21UOXFEQlhvZWtuemQ4bDEzTi92MHQxVmlvK1RESjJlMFk1NGJmSHlZV0g3ZW5DUTVTbmlHL2FWUHBYZ3FuRGo3cWIwMml1b1oweTE1Mm9KbkVtZEVYSmovcHo0bjE5ck5zM2pYeThYQUFIQldJY01QU09CTHRkMWlFdmhDenZDT3krOTJZOUJXY1Z3TEYwNVFDSEZsMDV6MkJlY2VRM051U2czdGJheW1sWWNRZStReW9EREdXTWFyRHdibDhhdGFqbW45ZnpDSm9sSThMdDlaenB1T1FQdTVsL1EzNTQzUWZTTDBaOFdTbTdsMk1aaEtrRnVITVJDS0V4NktUNmlLNWo5OTV0RUord3hwNGZKTVpOYnlzTCtBV283MkxxUmEyd0dsNFNxcnZaY1Z5REZWU3RBMk5UVzFqUXQ2Q043VFk5RERDM1IzblVLODdTbEtmTllRd0U1eVFyRjhibkMvaTZoREtEbUhJVU96bUlLazRDZHBvMUpueVBLcHY0WlB0QTNEQVM2VkhpYzZZaTVGTFpBaHpBdzNUVi9Ta3JNZ3AyM2lRL2VYOUdhajhYUTU1eEJKcEp2Q3BRNmlwdVZYejRnL2VqMGpLdGxPVUl5Q2lHQUU9In0seyJ0eXBlIjoidGV4dCIsInRleHQiOiJJJ2xsIHNvbHZlIDE1IMOXIDIzIHN0ZXAgYnkgc3RlcCB1c2luZyB0aGUgZGlzdHJpYnV0aXZlIHByb3BlcnR5LlxuXG4qKk1ldGhvZCAxOiBCcmVha2luZyBkb3duIDIzKipcbjE1IMOXIDIzID0gMTUgw5cgKDIwICsgMylcbj0gKDE1IMOXIDIwKSArICgxNSDDlyAzKVxuPSAzMDAgKyA0NVxuPSAqKjM0NSoqXG5cbioqTGV0IG1lIHZlcmlmeSB3aXRoIHRoZSBzdGFuZGFyZCBhbGdvcml0aG06KipcbmBgYFxuICAgIDIzXG4gIMOXIDE1XG4gIC0tLS1cbiAgIDExNSAgKDIzIMOXIDUpXG4gIDIzMCAgICgyMyDDlyAxMClcbiAgLS0tLVxuICAzNDVcbmBgYFxuXG4tIDIzIMOXIDUgPSAxMTVcbi0gMjMgw5cgMTAgPSAyMzAgIFxuLSAxMTUgKyAyMzAgPSAzNDVcblxuVGhlcmVmb3JlLCAxNSDDlyAyMyA9ICoqMzQ1KioifV0sInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjUyLCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NTE2LCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9fQ== + recorded_at: Fri, 02 Jan 2026 12:40:16 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_gemini_gemini-2_5-flash_maintains_thinking_across_multi-turn_conversation.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_gemini_gemini-2_5-flash_maintains_thinking_across_multi-turn_conversation.yml new file mode 100644 index 000000000..efeb1be60 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_gemini_gemini-2_5-flash_maintains_thinking_across_multi-turn_conversation.yml @@ -0,0 +1,168 @@ +--- +http_interactions: +- request: + method: post + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"What is 5 + 3?"}]}],"generationConfig":{"thinkingConfig":{"includeThoughts":true,"thinkingBudget":1024}}}' + headers: + User-Agent: + - Faraday v2.14.0 + X-Goog-Api-Key: + - "" + 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: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Fri, 02 Jan 2026 21:49:33 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=1786 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "**A Simple Addition Problem**\n\nOkay, so I'm looking at this simple math problem. First thing I see is that plus sign, that's addition, no doubt about it. Then I spot the numbers: 5 and 3. Alright, let's do this. I'll start with 5, and I need to add 3 to it. I can easily do this in my head, but let's make it explicit for demonstration's sake. So, 5... then count up three: 6, 7, 8. And that's it! The sum, or the answer, is 8. Elementary.\n", + "thought": true + }, + { + "text": "5 + 3 = 8" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 9, + "candidatesTokenCount": 8, + "totalTokenCount": 96, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 9 + } + ], + "thoughtsTokenCount": 79 + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "bT1YaavfAbCkkdUPjdvRiAo" + } + recorded_at: Fri, 02 Jan 2026 21:49:33 GMT +- request: + method: post + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"What is 5 + 3?"}]},{"role":"model","parts":[{"text":"5 + + 3 = 8"}]},{"role":"user","parts":[{"text":"Now multiply that by 2"}]}],"generationConfig":{"thinkingConfig":{"includeThoughts":true,"thinkingBudget":1024}}}' + headers: + User-Agent: + - Faraday v2.14.0 + X-Goog-Api-Key: + - "" + 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: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Fri, 02 Jan 2026 21:49:34 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=1043 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "**Calculating the Next Step**\n\nOkay, so we've got our previous result, which is 8. Now, the goal is to multiply that by 2. Let's keep it simple and just do the math: 8 multiplied by 2. That's a straightforward calculation. I know it's 16. Done. Next!\n", + "thought": true + }, + { + "text": "Okay, 8 multiplied by 2 is 16." + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 24, + "candidatesTokenCount": 13, + "totalTokenCount": 62, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 24 + } + ], + "thoughtsTokenCount": 25 + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "bj1YabylCrCkkdUPjdvRiAo" + } + recorded_at: Fri, 02 Jan 2026 21:49:34 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_gemini_gemini-2_5-flash_returns_thinking_content_when_streaming.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_gemini_gemini-2_5-flash_returns_thinking_content_when_streaming.yml new file mode 100644 index 000000000..321817ba6 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_gemini_gemini-2_5-flash_returns_thinking_content_when_streaming.yml @@ -0,0 +1,134 @@ +--- +http_interactions: +- request: + method: post + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"What is 8 * 7? Show your + work."}]}],"generationConfig":{"thinkingConfig":{"includeThoughts":true,"thinkingBudget":1024}}}' + headers: + User-Agent: + - Faraday v2.14.0 + X-Goog-Api-Key: + - "" + 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: + Content-Type: + - text/event-stream + Content-Disposition: + - attachment + Vary: + - Origin + - Referer + - X-Origin + Transfer-Encoding: + - chunked + Date: + - Fri, 02 Jan 2026 21:49:27 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=1508 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + body: + encoding: UTF-8 + string: "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"**Decomposing + the Problem**\\n\\nI've homed in on the core of this task: a simple multiplication + problem (8 * 7). The challenge is providing the \\\"work\\\" to solve it. + I've successfully recalled the answer: 56. Now, I'm brainstorming the best + way to demonstrate the steps, even for such a basic calculation.\\n\\n\\n\",\"thought\": + true}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": + 13,\"totalTokenCount\": 78,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 13}],\"thoughtsTokenCount\": 65},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: {\"candidates\": [{\"content\": + {\"parts\": [{\"text\": \"**Exploring Work Solutions**\\n\\nI'm now zeroing + in on the best ways to present the \\\"work\\\" for this problem. While repeated + addition clearly defines multiplication, I also think I can effectively use + the distributive property to showcase a more complex method of thinking. I'm + focusing on the clarity of demonstration. I'm aiming for methods that convey + the steps a person might take to solve this problem, rather than just provide + the final answer.\\n\\n\\n\",\"thought\": true}],\"role\": \"model\"},\"index\": + 0}],\"usageMetadata\": {\"promptTokenCount\": 13,\"totalTokenCount\": 419,\"promptTokensDetails\": + [{\"modality\": \"TEXT\",\"tokenCount\": 13}],\"thoughtsTokenCount\": 406},\"modelVersion\": + \"gemini-2.5-flash\",\"responseId\": \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: + {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"**Crafting the Final + Response**\\n\\nI've finalized my presentation. After exploring several options, + I've selected repeated addition and the distributive property as the strongest + ways to illustrate the process for the user's request. My emphasis is on clarity + and showing common mental math strategies. The \\\"work\\\" is now in the + final stages of the draft, and I'm ready to present my complete response.\\n\\n\\n\",\"thought\": + true}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": + 13,\"totalTokenCount\": 815,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 13}],\"thoughtsTokenCount\": 802},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: {\"candidates\": [{\"content\": + {\"parts\": [{\"text\": \"**Formulating a Response**\\n\\nI'm now putting + together the complete answer, starting with the direct result (56). Next, + I'll present repeated addition and break-down methods, including explanations. + I'll make sure to clearly and accurately illustrate the steps I came up with. + I'm prioritizing providing a complete and helpful response that addresses + the core requirements.\\n\\n\\n\",\"thought\": true}],\"role\": \"model\"},\"index\": + 0}],\"usageMetadata\": {\"promptTokenCount\": 13,\"totalTokenCount\": 849,\"promptTokensDetails\": + [{\"modality\": \"TEXT\",\"tokenCount\": 13}],\"thoughtsTokenCount\": 836},\"modelVersion\": + \"gemini-2.5-flash\",\"responseId\": \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: + {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"To find 8 * 7, we + can use a few methods:\\n\\n**Method 1: Repeated Addition**\\nMultiplication + is essentially a quicker way of doing repeated addition.\\nYou can add the + number 7 to itself 8 times\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": + {\"promptTokenCount\": 13,\"candidatesTokenCount\": 46,\"totalTokenCount\": + 895,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 13}],\"thoughtsTokenCount\": + 836},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: + {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \":\\n7 + 7 + 7 + + 7 + 7 + 7 + 7 + 7 = 56\\n\\nOr, you can add the number 8 to itself 7 times:\\n8 + + 8 +\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": + 13,\"candidatesTokenCount\": 96,\"totalTokenCount\": 945,\"promptTokensDetails\": + [{\"modality\": \"TEXT\",\"tokenCount\": 13}],\"thoughtsTokenCount\": 836},\"modelVersion\": + \"gemini-2.5-flash\",\"responseId\": \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: + {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \" 8 + 8 + 8 + 8 + + 8 = 56\\n\\n**Method 2: Breaking Down One Number (Distributive Property)**\\nYou + can break one of the numbers into parts that are easier to multiply\"}],\"role\": + \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 13,\"candidatesTokenCount\": + 143,\"totalTokenCount\": 992,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 13}],\"thoughtsTokenCount\": 836},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: {\"candidates\": [{\"content\": + {\"parts\": [{\"text\": \". For example, let's break 7 into 5 + 2:\\n\\n8 + * 7 = 8 * (5 + 2)\\nNow, distribute the 8 to each part:\\n= (8 * \"}],\"role\": + \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 13,\"candidatesTokenCount\": + 191,\"totalTokenCount\": 1040,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 13}],\"thoughtsTokenCount\": 836},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: {\"candidates\": [{\"content\": + {\"parts\": [{\"text\": \"5) + (8 * 2)\\n= 40 + 16\\n= 56\\n\\nAlternatively, + you could break 8 into 4 + 4:\\n8 * 7 = (4 + 4) *\"}],\"role\": \"model\"},\"index\": + 0}],\"usageMetadata\": {\"promptTokenCount\": 13,\"candidatesTokenCount\": + 242,\"totalTokenCount\": 1091,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 13}],\"thoughtsTokenCount\": 836},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: {\"candidates\": [{\"content\": + {\"parts\": [{\"text\": \" 7\\n= (4 * 7) + (4 * 7)\\n= 28 + 28\\n= 56\\n\\n---\\n\\nUsing + any of these methods, we find that:\\n\\n**8 * 7 =\"}],\"role\": \"model\"},\"index\": + 0}],\"usageMetadata\": {\"promptTokenCount\": 13,\"candidatesTokenCount\": + 292,\"totalTokenCount\": 1141,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 13}],\"thoughtsTokenCount\": 836},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\ndata: {\"candidates\": [{\"content\": + {\"parts\": [{\"text\": \" 56**\"}],\"role\": \"model\"},\"finishReason\": + \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 13,\"candidatesTokenCount\": + 295,\"totalTokenCount\": 1144,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 13}],\"thoughtsTokenCount\": 836},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"Zj1Yae2KBcXrkdUPhdPckQg\"}\r\n\r\n" + recorded_at: Fri, 02 Jan 2026 21:49:31 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_gemini_gemini-2_5-flash_returns_thinking_content_with_response.yml b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_gemini_gemini-2_5-flash_returns_thinking_content_with_response.yml new file mode 100644 index 000000000..ee57bef1d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_extended_thinking_integration_with_gemini_gemini-2_5-flash_returns_thinking_content_with_response.yml @@ -0,0 +1,53 @@ +--- +http_interactions: +- request: + method: post + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"What is 15 * 23? Think + through this step by step."}]}],"generationConfig":{"thinkingConfig":{"includeThoughts":true,"thinkingBudget":1024}}}' + headers: + User-Agent: + - Faraday v2.14.0 + X-Goog-Api-Key: + - "" + 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: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Fri, 02 Jan 2026 21:49:25 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=7574 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJjYW5kaWRhdGVzIjogWwogICAgewogICAgICAiY29udGVudCI6IHsKICAgICAgICAicGFydHMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0ZXh0IjogIioqRGVjb25zdHJ1Y3RpbmcgTXVsdGlwbGljYXRpb246IEEgU3RlcC1ieS1TdGVwIEFwcHJvYWNoKipcblxuT2theSwgc28gSSd2ZSBnb3QgdG8gY2FsY3VsYXRlIDE1IG11bHRpcGxpZWQgYnkgMjMuIE15IGZpcnN0IHRob3VnaHQgaXMgdG8gY29uc2lkZXIgdGhlIGJlc3QgYXBwcm9hY2gg4oCTIEkgbmVlZCB0byBjaG9vc2UgYW4gZWZmaWNpZW50IGFuZCByZWxpYWJsZSBtZXRob2QuICBUaGUgc3RhbmRhcmQgYWxnb3JpdGhtLCBvZiBjb3Vyc2UsIGlzIGEgc29saWQgZm91bmRhdGlvbiwgYW5kIEknbGwgcHJvYmFibHkgcmVseSBvbiBpdCBmb3IgYWNjdXJhY3kgYW5kIHZlcmlmaWNhdGlvbi4gSG93ZXZlciwgZm9yIGV4cGxhbmF0aW9uIHB1cnBvc2VzLCBhbmQgYmVjYXVzZSBpdCBvZmZlcnMgc29tZSB2YWx1YWJsZSBpbnNpZ2h0cywgSSdtIGxlYW5pbmcgdG93YXJkcyB0aGUgZGlzdHJpYnV0aXZlIHByb3BlcnR5IGhlcmUuXG5cblRoZSBkaXN0cmlidXRpdmUgcHJvcGVydHkgYWxsb3dzIG1lIHRvIGJyZWFrIGRvd24gdGhlIG11bHRpcGxpY2F0aW9uIGludG8gbW9yZSBtYW5hZ2VhYmxlIHBhcnRzLiBJJ2xsIHRha2UgMjMgYW5kIGRlY29tcG9zZSBpdCBpbnRvIDIwICsgMy4gTm93LCBJIGhhdmUgMTUgKiAoMjAgKyAzKSwgd2hpY2gsIHdoZW4gZXhwYW5kZWQsIGJlY29tZXMgKDE1ICogMjApICsgKDE1ICogMykuXG5cbkNhbGN1bGF0aW5nIGVhY2ggcGFydDogMTUgKiAyMCBpcyBzdHJhaWdodGZvcndhcmQuIDE1ICogMiBpcyAzMCwgYW5kIHRoZW4gSSBhZGQgYSB6ZXJvIGZvciB0aGUgZmFjdG9yIG9mIDEwLCBnaXZpbmcgbWUgMzAwLiBGb3IgMTUgKiAzLCBJIGtub3cgMTUgKiAxID0gMTUsIGFuZCBkb3VibGluZyB0aGF0IGdpdmVzIG1lIDMwICgxNSAqIDIpLiBUaGVuLCBhZGRpbmcgYW5vdGhlciAxNSBnZXRzIG1lIDQ1LCBvciwgYXMgYSBtZW50YWwgc2hvcnRjdXQsIDE1ICogMyBpcyBzaW1wbHkgNDUuXG5cbkFkZGluZyB0aGVzZSByZXN1bHRzIHRvZ2V0aGVyLCAzMDAgKyA0NSwgZ2l2ZXMgbWUgMzQ1LlxuXG5Ob3csIHRvIGRvdWJsZS1jaGVjayBteSB3b3JrLCBJJ2xsIHF1aWNrbHkgYXBwbHkgdGhlIHN0YW5kYXJkIGFsZ29yaXRobSwgd2hpY2ggc2hvdWxkIGdpdmUgbWUgdGhlIHNhbWUgcmVzdWx0LiBNdWx0aXBseWluZyAyMyBieSA1LCBJIGdldCAxMTUuIFRoZW4sIG11bHRpcGx5aW5nIDIzIGJ5IDEwIChvciBqdXN0IDIzLCBidXQgc2hpZnRlZCBvbmUgcGxhY2Ugb3ZlciB0byBhY2NvdW50IGZvciB0aGUgdGVucyBwbGFjZSksIEkgZ2V0IDIzMC4gQWRkaW5nIDExNSBhbmQgMjMwLCBJIGFycml2ZSBhdCAzNDUgYWdhaW4hXG5cblNvLCBib3RoIG1ldGhvZHMgY29udmVyZ2Ugb24gdGhlIHNhbWUgYW5zd2VyOiAzNDUuIEl0J3MgYSBjbGVhbiByZXN1bHQsIHdoaWNoIGNvbmZpcm1zIHRoZSBpbnRlZ3JpdHkgb2YgbXkgdGhpbmtpbmcuXG4iLAogICAgICAgICAgICAidGhvdWdodCI6IHRydWUKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0ZXh0IjogIkxldCdzIGJyZWFrIGRvd24gdGhlIG11bHRpcGxpY2F0aW9uIG9mIDE1ICogMjMgc3RlcCBieSBzdGVwIHVzaW5nIGEgY29tbW9uIG1ldGhvZCB0aGF0IGludm9sdmVzIGJyZWFraW5nIG9uZSBudW1iZXIgaW50byBpdHMgdGVucyBhbmQgb25lcyBjb21wb25lbnRzIChkaXN0cmlidXRpdmUgcHJvcGVydHkpLlxuXG4qKlN0ZXAgMTogQnJlYWsgZG93biBvbmUgb2YgdGhlIG51bWJlcnMuKipcbkxldCdzIGJyZWFrIGRvd24gMjMgaW50byAyMCBhbmQgMy5cblNvLCAxNSAqIDIzIGlzIHRoZSBzYW1lIGFzIDE1ICogKDIwICsgMykuXG5cbioqU3RlcCAyOiBNdWx0aXBseSB0aGUgZmlyc3QgbnVtYmVyIGJ5IHRoZSB0ZW5zIGNvbXBvbmVudC4qKlxuTXVsdGlwbHkgMTUgYnkgMjAuXG4qICAgRmlyc3QsIG11bHRpcGx5IDE1IGJ5IDI6IDE1ICogMiA9IDMwLlxuKiAgIFNpbmNlIHdlIG11bHRpcGxpZWQgYnkgMjAgKG5vdCAyKSwgYWRkIGEgemVybyB0byB0aGUgcmVzdWx0OiAzMDAuXG5TbywgMTUgKiAyMCA9IDMwMC5cblxuKipTdGVwIDM6IE11bHRpcGx5IHRoZSBmaXJzdCBudW1iZXIgYnkgdGhlIG9uZXMgY29tcG9uZW50LioqXG5NdWx0aXBseSAxNSBieSAzLlxuKiAgIDE1ICogMyA9IDQ1LlxuXG4qKlN0ZXAgNDogQWRkIHRoZSByZXN1bHRzIGZyb20gU3RlcCAyIGFuZCBTdGVwIDMuKipcbkFkZCAzMDAgKGZyb20gMTUgKiAyMCkgYW5kIDQ1IChmcm9tIDE1ICogMykuXG4qICAgMzAwICsgNDUgPSAzNDUuXG5cblNvLCAxNSAqIDIzID0gMzQ1LiIKICAgICAgICAgIH0KICAgICAgICBdLAogICAgICAgICJyb2xlIjogIm1vZGVsIgogICAgICB9LAogICAgICAiZmluaXNoUmVhc29uIjogIlNUT1AiLAogICAgICAiaW5kZXgiOiAwCiAgICB9CiAgXSwKICAidXNhZ2VNZXRhZGF0YSI6IHsKICAgICJwcm9tcHRUb2tlbkNvdW50IjogMTgsCiAgICAiY2FuZGlkYXRlc1Rva2VuQ291bnQiOiAzMDEsCiAgICAidG90YWxUb2tlbkNvdW50IjogMTEyNCwKICAgICJwcm9tcHRUb2tlbnNEZXRhaWxzIjogWwogICAgICB7CiAgICAgICAgIm1vZGFsaXR5IjogIlRFWFQiLAogICAgICAgICJ0b2tlbkNvdW50IjogMTgKICAgICAgfQogICAgXSwKICAgICJ0aG91Z2h0c1Rva2VuQ291bnQiOiA4MDUKICB9LAogICJtb2RlbFZlcnNpb24iOiAiZ2VtaW5pLTIuNS1mbGFzaCIsCiAgInJlc3BvbnNlSWQiOiAiWlQxWWFibjZJci1PMjhvUGxNU1kwUTAiCn0K + recorded_at: Fri, 02 Jan 2026 21:49:25 GMT +recorded_with: VCR 6.4.0 diff --git a/spec/ruby_llm/thinking_spec.rb b/spec/ruby_llm/thinking_spec.rb new file mode 100644 index 000000000..a07d10afe --- /dev/null +++ b/spec/ruby_llm/thinking_spec.rb @@ -0,0 +1,551 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Extended Thinking is a cross-cutting feature that spans multiple classes. +# This spec file tests the feature holistically rather than per-class. +# rubocop:disable RSpec/MultipleDescribes + +# Models with reasoning capability for integration tests +# Note: Grok via OpenRouter doesn't expose reasoning_content, so it's not included +THINKING_MODELS = [ + { provider: :anthropic, model: 'claude-sonnet-4-20250514' }, + { provider: :anthropic, model: 'claude-opus-4-20250514' }, + { provider: :anthropic, model: 'claude-opus-4-5-20251101' }, + { provider: :gemini, model: 'gemini-2.5-flash' } +].freeze + +RSpec.describe RubyLLM::Message do + include_context 'with configured RubyLLM' + + describe 'thinking attribute' do + it 'stores thinking content' do + message = described_class.new( + role: :assistant, + content: 'The answer is 42', + thinking: 'Let me calculate this carefully...' + ) + + expect(message.thinking).to eq('Let me calculate this carefully...') + end + + it 'can be nil when not present' do + message = described_class.new( + role: :assistant, + content: 'The answer is 42' + ) + + expect(message.thinking).to be_nil + end + + it 'includes thinking in to_h output' do + message = described_class.new( + role: :assistant, + content: 'The answer is 42', + thinking: 'Let me calculate this carefully...' + ) + + hash = message.to_h + + expect(hash[:thinking]).to eq('Let me calculate this carefully...') + expect(hash[:content]).to eq('The answer is 42') + expect(hash[:role]).to eq(:assistant) + end + + it 'excludes thinking from to_h when nil' do + message = described_class.new( + role: :assistant, + content: 'The answer is 42' + ) + + hash = message.to_h + + expect(hash).not_to have_key(:thinking) + end + + it 'stores thinking_signature as protected attribute' do + message = described_class.new( + role: :assistant, + content: 'Response', + thinking: 'Thoughts', + thinking_signature: 'sig-abc123' + ) + + expect(message.respond_to?(:thinking_signature, true)).to be true + expect { message.thinking_signature }.to raise_error(NoMethodError) + end + end +end + +RSpec.describe RubyLLM::Messages do + include_context 'with configured RubyLLM' + + describe '.signature_for' do + it 'returns thinking_signature when present' do + message = RubyLLM::Message.new( + role: :assistant, + content: 'Response', + thinking: 'Thoughts', + thinking_signature: 'sig-xyz789' + ) + + signature = described_class.signature_for(message) + + expect(signature).to eq('sig-xyz789') + end + + it 'returns nil when thinking_signature is not present' do + message = RubyLLM::Message.new( + role: :assistant, + content: 'Response', + thinking: 'Thoughts' + ) + + signature = described_class.signature_for(message) + + expect(signature).to be_nil + end + + it 'returns nil for messages without thinking' do + message = RubyLLM::Message.new( + role: :assistant, + content: 'Response' + ) + + signature = described_class.signature_for(message) + + expect(signature).to be_nil + end + + it 'handles non-message objects gracefully' do + object = Object.new + + signature = described_class.signature_for(object) + + expect(signature).to be_nil + end + end +end + +RSpec.describe RubyLLM::StreamAccumulator do + include_context 'with configured RubyLLM' + + describe 'thinking accumulation' do + it 'accumulates thinking from multiple chunks' do + accumulator = described_class.new + + chunk1 = RubyLLM::Chunk.new( + role: :assistant, + content: 'First ', + thinking: 'Let me think... ', + model_id: 'claude-sonnet-4-20250514' + ) + chunk2 = RubyLLM::Chunk.new( + role: :assistant, + content: 'second', + thinking: 'about this problem', + model_id: 'claude-sonnet-4-20250514' + ) + + accumulator.add(chunk1) + accumulator.add(chunk2) + + expect(accumulator.thinking).to eq('Let me think... about this problem') + expect(accumulator.content).to eq('First second') + end + + it 'returns empty string for thinking when no chunks have thinking' do + accumulator = described_class.new + + chunk = RubyLLM::Chunk.new( + role: :assistant, + content: 'Regular response', + model_id: 'claude-sonnet-4-20250514' + ) + + accumulator.add(chunk) + + expect(accumulator.thinking).to eq('') + end + + it 'preserves thinking_signature from chunks' do + accumulator = described_class.new + + chunk1 = RubyLLM::Chunk.new( + role: :assistant, + content: 'Response', + thinking: 'Thinking', + thinking_signature: 'sig-first', + model_id: 'claude-sonnet-4-20250514' + ) + chunk2 = RubyLLM::Chunk.new( + role: :assistant, + content: ' more', + thinking: ' more', + model_id: 'claude-sonnet-4-20250514' + ) + + accumulator.add(chunk1) + accumulator.add(chunk2) + + # Build a mock response object for to_message + mock_env = Struct.new(:request_body).new('{}') + mock_response = Struct.new(:headers, :body, :status, :env).new({}, {}, 200, mock_env) + message = accumulator.to_message(mock_response) + + expect(RubyLLM::Messages.signature_for(message)).to eq('sig-first') + end + + it 'converts empty thinking to nil in message' do + accumulator = described_class.new + + chunk = RubyLLM::Chunk.new( + role: :assistant, + content: 'Response', + model_id: 'claude-sonnet-4-20250514' + ) + + accumulator.add(chunk) + + mock_env = Struct.new(:request_body).new('{}') + mock_response = Struct.new(:headers, :body, :status, :env).new({}, {}, 200, mock_env) + message = accumulator.to_message(mock_response) + + expect(message.thinking).to be_nil + end + + it 'preserves non-empty thinking in message' do + accumulator = described_class.new + + chunk = RubyLLM::Chunk.new( + role: :assistant, + content: 'Answer', + thinking: 'Internal reasoning', + model_id: 'claude-sonnet-4-20250514' + ) + + accumulator.add(chunk) + + mock_env = Struct.new(:request_body).new('{}') + mock_response = Struct.new(:headers, :body, :status, :env).new({}, {}, 200, mock_env) + message = accumulator.to_message(mock_response) + + expect(message.thinking).to eq('Internal reasoning') + end + end +end + +RSpec.describe RubyLLM::Chat do + include_context 'with configured RubyLLM' + + describe '#with_thinking configuration' do + THINKING_MODELS.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + + context "with #{provider}/#{model}" do + it 'returns self for method chaining' do + chat = RubyLLM.chat(model: model, provider: provider) + result = chat.with_thinking(budget: :medium) + + expect(result).to be(chat) + end + + it 'accepts symbol budgets' do + chat = RubyLLM.chat(model: model, provider: provider) + + expect { chat.with_thinking(budget: :low) }.not_to raise_error + end + + it 'accepts integer budgets' do + chat = RubyLLM.chat(model: model, provider: provider) + + expect { chat.with_thinking(budget: 5000) }.not_to raise_error + end + + it 'defaults to medium budget when not specified' do + chat = RubyLLM.chat(model: model, provider: provider) + + expect { chat.with_thinking }.not_to raise_error + end + end + end + + it 'raises UnsupportedFeatureError for models without reasoning capability' do + chat = RubyLLM.chat(model: 'gpt-4.1-nano', provider: :openai) + + expect { chat.with_thinking(budget: :medium) } + .to raise_error(RubyLLM::UnsupportedFeatureError, /does not support extended thinking/) + end + end + + describe '#thinking_enabled?' do + THINKING_MODELS.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + + context "with #{provider}/#{model}" do + it 'returns false by default' do + chat = RubyLLM.chat(model: model, provider: provider) + + expect(chat.thinking_enabled?).to be false + end + + it 'returns true after with_thinking is called' do + chat = RubyLLM.chat(model: model, provider: provider) + chat.with_thinking(budget: :medium) + + expect(chat.thinking_enabled?).to be true + end + end + end + end + + describe 'extended thinking integration' do + THINKING_MODELS.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + + context "with #{provider}/#{model}" do + it 'returns thinking content with response' do + chat = RubyLLM.chat(model: model, provider: provider) + .with_thinking(budget: :low) + + response = chat.ask('What is 15 * 23? Think through this step by step.') + + expect(response.content).to be_present + expect(response.thinking).to be_present + expect(response.thinking.length).to be > 10 + end + + it 'returns thinking content when streaming' do + chat = RubyLLM.chat(model: model, provider: provider) + .with_thinking(budget: :low) + + chunks = [] + response = chat.ask('What is 8 * 7? Show your work.') do |chunk| + chunks << chunk + end + + expect(response.content).to be_present + expect(response.thinking).to be_present + expect(chunks).not_to be_empty + end + + it 'maintains thinking across multi-turn conversation' do + chat = RubyLLM.chat(model: model, provider: provider) + .with_thinking(budget: :low) + + first_response = chat.ask('What is 5 + 3?') + expect(first_response.thinking).to be_present + + second_response = chat.ask('Now multiply that by 2') + expect(second_response.thinking).to be_present + expect(second_response.content).to be_present + end + end + end + end +end + +RSpec.describe RubyLLM::Providers::Anthropic::Chat do + include_context 'with configured RubyLLM' + + describe '.resolve_budget' do + let(:model) { RubyLLM.models.find('claude-sonnet-4-20250514') } + + it 'resolves :low to 1024 tokens' do + expect(described_class.resolve_budget(:low, model)).to eq(1024) + end + + it 'resolves :medium to 10,000 tokens' do + expect(described_class.resolve_budget(:medium, model)).to eq(10_000) + end + + it 'resolves :high to 32,000 tokens' do + expect(described_class.resolve_budget(:high, model)).to eq(32_000) + end + + it 'passes through integer budgets unchanged' do + expect(described_class.resolve_budget(15_000, model)).to eq(15_000) + end + + it 'defaults unknown symbols to 10,000' do + expect(described_class.resolve_budget(:unknown, model)).to eq(10_000) + end + end +end + +RSpec.describe RubyLLM::Providers::Gemini::Chat do + include_context 'with configured RubyLLM' + + describe '.resolve_budget' do + it 'resolves :low to 1024 tokens' do + expect(described_class.resolve_budget(:low)).to eq(1024) + end + + it 'resolves :medium to 8192 tokens' do + expect(described_class.resolve_budget(:medium)).to eq(8192) + end + + it 'resolves :high to 24,576 tokens' do + expect(described_class.resolve_budget(:high)).to eq(24_576) + end + + it 'passes through integer budgets unchanged' do + expect(described_class.resolve_budget(12_000)).to eq(12_000) + end + + it 'defaults unknown symbols to 8192' do + expect(described_class.resolve_budget(:unknown)).to eq(8192) + end + end + + describe '.resolve_effort_level' do + it 'resolves :low to "low"' do + expect(described_class.resolve_effort_level(:low)).to eq('low') + end + + it 'resolves :medium to "medium"' do + expect(described_class.resolve_effort_level(:medium)).to eq('medium') + end + + it 'resolves :high to "high"' do + expect(described_class.resolve_effort_level(:high)).to eq('high') + end + + it 'resolves large integers to "high"' do + expect(described_class.resolve_effort_level(20_000)).to eq('high') + end + + it 'resolves small integers to "low"' do + expect(described_class.resolve_effort_level(5000)).to eq('low') + end + + it 'defaults unknown symbols to "high"' do + expect(described_class.resolve_effort_level(:unknown)).to eq('high') + end + end + + describe '.gemini_3_model?' do + it 'detects Gemini 3 models by name' do + model = RubyLLM::Model::Info.new( + id: 'gemini-3-flash', + name: 'Gemini 3 Flash', + provider: 'gemini' + ) + + expect(described_class.gemini_3_model?(model)).to be true + end + + it 'does not detect Gemini 2.5 as Gemini 3' do + model = RubyLLM::Model::Info.new( + id: 'gemini-2.5-pro', + name: 'Gemini 2.5 Pro', + provider: 'gemini' + ) + + expect(described_class.gemini_3_model?(model)).to be false + end + end +end + +RSpec.describe RubyLLM::Providers::OpenAI::Chat do + include_context 'with configured RubyLLM' + + describe '.resolve_effort' do + it 'resolves :low to "low"' do + expect(described_class.resolve_effort(:low)).to eq('low') + end + + it 'resolves :medium to "high"' do + expect(described_class.resolve_effort(:medium)).to eq('high') + end + + it 'resolves :high to "high"' do + expect(described_class.resolve_effort(:high)).to eq('high') + end + + it 'resolves large integers to "high"' do + expect(described_class.resolve_effort(15_000)).to eq('high') + end + + it 'resolves small integers to "low"' do + expect(described_class.resolve_effort(5000)).to eq('low') + end + + it 'defaults unknown symbols to "high"' do + expect(described_class.resolve_effort(:unknown)).to eq('high') + end + end + + describe '.grok_model?' do + it 'detects Grok models by name' do + model = RubyLLM::Model::Info.new( + id: 'grok-4', + name: 'Grok 4', + provider: 'openai' + ) + + expect(described_class.grok_model?(model)).to be true + end + + it 'does not detect regular OpenAI models as Grok' do + model = RubyLLM::Model::Info.new( + id: 'gpt-4o', + name: 'GPT-4o', + provider: 'openai' + ) + + expect(described_class.grok_model?(model)).to be false + end + end +end + +RSpec.describe RubyLLM::Chunk do + include_context 'with configured RubyLLM' + + it 'can include thinking content' do + chunk = described_class.new( + role: :assistant, + content: 'Answer', + thinking: 'Reasoning', + model_id: 'claude-sonnet-4-20250514' + ) + + expect(chunk.thinking).to eq('Reasoning') + end + + it 'can include thinking_signature' do + chunk = described_class.new( + role: :assistant, + content: 'Answer', + thinking: 'Reasoning', + thinking_signature: 'sig-123', + model_id: 'claude-sonnet-4-20250514' + ) + + expect(RubyLLM::Messages.signature_for(chunk)).to eq('sig-123') + end +end + +RSpec.describe RubyLLM::UnsupportedFeatureError do + include_context 'with configured RubyLLM' + + it 'is a subclass of RubyLLM::Error' do + expect(described_class).to be < RubyLLM::Error + end + + it 'can be raised with a custom message' do + expect { raise described_class, 'Feature not supported' } + .to raise_error(described_class, 'Feature not supported') + end + + it 'does not require a response object' do + error = described_class.new('Test error') + + expect(error.response).to be_nil + expect(error.message).to eq('Test error') + end +end +# rubocop:enable RSpec/MultipleDescribes