From af21430853086d8656e7c4707275821687772baa Mon Sep 17 00:00:00 2001 From: Sophia Date: Thu, 14 Aug 2025 16:54:46 -0700 Subject: [PATCH 01/11] feat: add tool choice and parallel tool calls control - Implement provider-specific tool choice handling - Add InvalidToolChoiceError for validation - Enhance tool execution flow to prevent infinite loops with non-auto choices --- lib/ruby_llm/chat.rb | 44 +++++++++++++++++++---- lib/ruby_llm/error.rb | 1 + lib/ruby_llm/provider.rb | 7 +++- lib/ruby_llm/providers/anthropic/chat.rb | 15 +++++--- lib/ruby_llm/providers/anthropic/tools.rb | 9 +++++ lib/ruby_llm/providers/bedrock/chat.rb | 8 +++-- lib/ruby_llm/providers/gemini/chat.rb | 12 +++++-- lib/ruby_llm/providers/gemini/tools.rb | 15 ++++++++ lib/ruby_llm/providers/mistral/chat.rb | 3 +- lib/ruby_llm/providers/openai/chat.rb | 11 ++++-- lib/ruby_llm/providers/openai/tools.rb | 16 +++++++++ 11 files changed, 122 insertions(+), 19 deletions(-) diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index 7131aeb7a..f9ec5150a 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -11,7 +11,7 @@ module RubyLLM class Chat include Enumerable - attr_reader :model, :messages, :tools, :params, :headers, :schema + attr_reader :model, :messages, :tools, :tool_choice, :parallel_tool_calls, :params, :headers, :schema def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil) if assume_model_exists && !provider @@ -23,6 +23,8 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n model_id = model || @config.default_model with_model(model_id, provider: provider, assume_exists: assume_model_exists) @temperature = 0.7 + @tool_choice = nil + @parallel_tool_calls = nil @messages = [] @tools = {} @params = {} @@ -50,15 +52,19 @@ def with_instructions(instructions, replace: false) self end - def with_tool(tool) - tool_instance = tool.is_a?(Class) ? tool.new : tool - @tools[tool_instance.name.to_sym] = tool_instance + def with_tool(tool, choice: nil, parallel: nil) + unless tool.nil? + tool_instance = tool.is_a?(Class) ? tool.new : tool + @tools[tool_instance.name.to_sym] = tool_instance + end + update_tool_options(choice:, parallel:) self end - def with_tools(*tools, replace: false) + def with_tools(*tools, replace: false, choice: nil, parallel: nil) @tools.clear if replace tools.compact.each { |tool| with_tool tool } + update_tool_options(choice:, parallel:) self end @@ -136,6 +142,8 @@ def complete(&) # rubocop:disable Metrics/PerceivedComplexity params: @params, headers: @headers, schema: @schema, + tool_choice: @tool_choice, + parallel_tool_calls: @parallel_tool_calls, &wrap_streaming_block(&) ) @@ -189,7 +197,7 @@ def wrap_streaming_block(&block) end end - def handle_tool_calls(response, &) + def handle_tool_calls(response, &) # rubocop:disable Metrics/PerceivedComplexity halt_result = nil response.tool_calls.each_value do |tool_call| @@ -203,7 +211,9 @@ def handle_tool_calls(response, &) halt_result = result if result.is_a?(Tool::Halt) end - halt_result || complete(&) + return halt_result if halt_result + + should_continue_after_tools? ? complete(&) : response end def execute_tool(tool_call) @@ -212,6 +222,26 @@ def execute_tool(tool_call) tool.call(args) end + def update_tool_options(choice:, parallel:) + unless choice.nil? + valid_tool_choices = %i[auto none any] + tools.keys + unless valid_tool_choices.include?(choice.to_sym) + raise InvalidToolChoiceError, + "Invalid tool choice: #{choice}. Valid choices are: #{valid_tool_choices.join(', ')}" + end + + @tool_choice = choice.to_sym + end + + @parallel_tool_calls = !!parallel unless parallel.nil? + end + + def should_continue_after_tools? + # Continue conversation only with :auto tool choice to avoid infinite loops. + # With :any or specific tool choices, the model would keep calling tools repeatedly. + tool_choice.nil? || tool_choice == :auto + end + def instance_variables super - %i[@connection @config] end diff --git a/lib/ruby_llm/error.rb b/lib/ruby_llm/error.rb index 1bcc018bc..a20fa0f25 100644 --- a/lib/ruby_llm/error.rb +++ b/lib/ruby_llm/error.rb @@ -22,6 +22,7 @@ def initialize(response = nil, message = nil) # Error classes for non-HTTP errors class ConfigurationError < StandardError; end class InvalidRoleError < StandardError; end + class InvalidToolChoiceError < StandardError; end class ModelNotFoundError < StandardError; end class UnsupportedAttachmentError < StandardError; end diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index ed0c2ac8a..be8c3127a 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -40,7 +40,9 @@ def configuration_requirements self.class.configuration_requirements end - def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, &) # rubocop:disable Metrics/ParameterLists + # rubocop:disable Metrics/ParameterLists + def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, + tool_choice: nil, parallel_tool_calls: nil, &) normalized_temperature = maybe_normalize_temperature(temperature, model) payload = Utils.deep_merge( @@ -48,6 +50,8 @@ def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, sc render_payload( messages, tools: tools, + tool_choice: tool_choice, + parallel_tool_calls: parallel_tool_calls, temperature: normalized_temperature, model: model, stream: block_given?, @@ -61,6 +65,7 @@ def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, sc sync_response @connection, payload, headers end end + # rubocop:enable Metrics/ParameterLists def list_models response = @connection.get models_url diff --git a/lib/ruby_llm/providers/anthropic/chat.rb b/lib/ruby_llm/providers/anthropic/chat.rb index 64cd764b0..cbc0db922 100644 --- a/lib/ruby_llm/providers/anthropic/chat.rb +++ b/lib/ruby_llm/providers/anthropic/chat.rb @@ -11,14 +11,17 @@ def completion_url '/v1/messages' end - def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument + # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument + def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, + temperature:, model:, stream: false, schema: nil) system_messages, chat_messages = separate_messages(messages) system_content = build_system_content(system_messages) build_base_payload(chat_messages, model, stream).tap do |payload| - add_optional_fields(payload, system_content:, tools:, temperature:) + add_optional_fields(payload, system_content:, tools:, tool_choice:, parallel_tool_calls:, temperature:) end end + # rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument def separate_messages(messages) messages.partition { |msg| msg.role == :system } @@ -44,8 +47,12 @@ def build_base_payload(chat_messages, model, stream) } end - def add_optional_fields(payload, system_content:, tools:, temperature:) - payload[:tools] = tools.values.map { |t| Tools.function_for(t) } if tools.any? + def add_optional_fields(payload, system_content:, tools:, tool_choice:, parallel_tool_calls:, temperature:) # rubocop:disable Metrics/ParameterLists + if tools.any? + payload[:tools] = tools.values.map { |t| Tools.function_for(t) } + payload[:tool_choice] = build_tool_choice(tool_choice, parallel_tool_calls) unless tool_choice.nil? + end + payload[:system] = system_content unless system_content.empty? payload[:temperature] = temperature unless temperature.nil? end diff --git a/lib/ruby_llm/providers/anthropic/tools.rb b/lib/ruby_llm/providers/anthropic/tools.rb index 0b4d1c7c4..8f26cb799 100644 --- a/lib/ruby_llm/providers/anthropic/tools.rb +++ b/lib/ruby_llm/providers/anthropic/tools.rb @@ -102,6 +102,15 @@ def clean_parameters(parameters) def required_parameters(parameters) parameters.select { |_, param| param.required }.keys end + + def build_tool_choice(tool_choice, parallel_tool_calls) + { + type: %i[auto any none].include?(tool_choice) ? tool_choice : :tool + }.tap do |tc| + tc[:name] = tool_choice if tc[:type] == :tool + tc[:disable_parallel_tool_use] = !parallel_tool_calls unless tc[:type] == :none || parallel_tool_calls.nil? + end + end end end end diff --git a/lib/ruby_llm/providers/bedrock/chat.rb b/lib/ruby_llm/providers/bedrock/chat.rb index 94655d117..a4d5220f3 100644 --- a/lib/ruby_llm/providers/bedrock/chat.rb +++ b/lib/ruby_llm/providers/bedrock/chat.rb @@ -40,7 +40,9 @@ 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 + # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument + def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, + temperature:, model:, stream: false, schema: nil) # Hold model_id in instance variable for use in completion_url and stream_url @model_id = model @@ -48,9 +50,11 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema system_content = Anthropic::Chat.build_system_content(system_messages) build_base_payload(chat_messages, model).tap do |payload| - Anthropic::Chat.add_optional_fields(payload, system_content:, tools:, temperature:) + Anthropic::Chat.add_optional_fields(payload, system_content:, tools:, tool_choice:, + parallel_tool_calls:, temperature:) end end + # rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument def build_base_payload(chat_messages, model) { diff --git a/lib/ruby_llm/providers/gemini/chat.rb b/lib/ruby_llm/providers/gemini/chat.rb index 5254722a8..d25dd8c22 100644 --- a/lib/ruby_llm/providers/gemini/chat.rb +++ b/lib/ruby_llm/providers/gemini/chat.rb @@ -11,7 +11,9 @@ def completion_url "models/#{@model}:generateContent" end - def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument + # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument + def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, + temperature:, model:, stream: false, schema: nil) @model = model # Store model for completion_url/stream_url payload = { contents: format_messages(messages), @@ -25,9 +27,15 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema payload[:generationConfig][:responseSchema] = convert_schema_to_gemini(schema) end - payload[:tools] = format_tools(tools) if tools.any? + if tools.any? + payload[:tools] = format_tools(tools) + # Gemini doesn't support controlling parallel tool calls + payload[:toolConfig] = build_tool_config(tool_choice) unless tool_choice.nil? + end + payload end + # rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument private diff --git a/lib/ruby_llm/providers/gemini/tools.rb b/lib/ruby_llm/providers/gemini/tools.rb index fd178d24c..7a31a0a13 100644 --- a/lib/ruby_llm/providers/gemini/tools.rb +++ b/lib/ruby_llm/providers/gemini/tools.rb @@ -76,6 +76,21 @@ def param_type_for_gemini(type) else 'STRING' end end + + def build_tool_config(tool_choice) + { + functionCallingConfig: { + mode: specific_tool_choice?(tool_choice) ? 'any' : tool_choice + }.tap do |config| + # Use allowedFunctionNames to simulate specific tool choice + config[:allowedFunctionNames] = [tool_choice] if specific_tool_choice?(tool_choice) + end + } + end + + def specific_tool_choice?(tool_choice) + !%i[auto none any].include?(tool_choice) + end end end end diff --git a/lib/ruby_llm/providers/mistral/chat.rb b/lib/ruby_llm/providers/mistral/chat.rb index 74d508d19..c2a6321e0 100644 --- a/lib/ruby_llm/providers/mistral/chat.rb +++ b/lib/ruby_llm/providers/mistral/chat.rb @@ -13,7 +13,8 @@ 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:, tool_choice:, parallel_tool_calls:, temperature:, model:, stream: false, + schema: nil) payload = super # Mistral doesn't support stream_options payload.delete(:stream_options) diff --git a/lib/ruby_llm/providers/openai/chat.rb b/lib/ruby_llm/providers/openai/chat.rb index 9ed7e170b..7d0a5fd6d 100644 --- a/lib/ruby_llm/providers/openai/chat.rb +++ b/lib/ruby_llm/providers/openai/chat.rb @@ -11,7 +11,9 @@ def completion_url module_function - def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists + # rubocop:disable Metrics/ParameterLists + def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, + temperature:, model:, stream: false, schema: nil) payload = { model: model, messages: format_messages(messages), @@ -21,7 +23,11 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema # Only include temperature if it's not nil (some models don't accept it) payload[:temperature] = temperature unless temperature.nil? - payload[:tools] = tools.map { |_, tool| tool_for(tool) } if tools.any? + if tools.any? + payload[:tools] = tools.map { |_, tool| tool_for(tool) } + payload[:tool_choice] = build_tool_choice(tool_choice) unless tool_choice.nil? + payload[:parallel_tool_calls] = parallel_tool_calls unless parallel_tool_calls.nil? + end if schema # Use strict mode from schema if specified, default to true @@ -40,6 +46,7 @@ def render_payload(messages, tools:, temperature:, model:, stream: false, schema payload[:stream_options] = { include_usage: true } if stream payload end + # rubocop:enable Metrics/ParameterLists def parse_completion_response(response) data = response.body diff --git a/lib/ruby_llm/providers/openai/tools.rb b/lib/ruby_llm/providers/openai/tools.rb index e4b76c0cf..7cf87d89d 100644 --- a/lib/ruby_llm/providers/openai/tools.rb +++ b/lib/ruby_llm/providers/openai/tools.rb @@ -67,6 +67,22 @@ def parse_tool_calls(tool_calls, parse_arguments: true) ] end end + + def build_tool_choice(tool_choice) + case tool_choice + when :auto, :none + tool_choice + when :any + :required + else + { + type: 'function', + function: { + name: tool_choice + } + } + end + end end end end From 47923bce1257bad450cad69a012e51a726e04a8e Mon Sep 17 00:00:00 2001 From: Sophia Date: Thu, 14 Aug 2025 18:00:05 -0700 Subject: [PATCH 02/11] docs: add Tool Choice Control section to tools.md --- docs/_core_features/tools.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/_core_features/tools.md b/docs/_core_features/tools.md index aadb73f61..3e352804c 100644 --- a/docs/_core_features/tools.md +++ b/docs/_core_features/tools.md @@ -136,6 +136,27 @@ puts response.content # => "Current weather at 52.52, 13.4: Temperature: 12.5°C, Wind Speed: 8.3 km/h, Conditions: Mainly clear, partly cloudy, and overcast." ``` +### Tool Choice Control + +Control when and how tools are called using `choice` and `parallel` options: + +```ruby +chat = RubyLLM.chat(model: 'gpt-4o') + +# Choice options +chat.with_tool(Weather, choice: :auto) # Model decides whether to call any provided tools or not (default) +chat.with_tool(Weather, choice: :any) # Model must use one of the provided tools +chat.with_tool(Weather, choice: :none) # No tools +chat.with_tool(Weather, choice: :weather) # Force specific tool + +# Parallel tool calls +chat.with_tools(Weather, Calculator, parallel: true) # Model can output multiple tool calls at once (default) +chat.with_tools(Weather, Calculator, parallel: false) # At most one tool call +``` + +> With `:any` or specific tool choices, tool results are not automatically sent back to the AI model (see The Tool Execution Flow section below) to prevent infinite loops. +{: .note } + ### Model Compatibility {: .d-inline-block } From 4a9efc44677340717b1c2eb074447c347e64a2ef Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 26 Aug 2025 22:04:50 -0700 Subject: [PATCH 03/11] Reset tool_choice after forced tool use to prevent infinite loops --- lib/ruby_llm/chat.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index 9d064f0c9..e011296de 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -204,9 +204,8 @@ def handle_tool_calls(response, &) # rubocop:disable Metrics/PerceivedComplexity halt_result = result if result.is_a?(Tool::Halt) end - return halt_result if halt_result - - should_continue_after_tools? ? complete(&) : response + reset_tool_choice if forced_tool_choice? + halt_result || complete(&) end def execute_tool(tool_call) @@ -229,10 +228,12 @@ def update_tool_options(choice:, parallel:) @parallel_tool_calls = !!parallel unless parallel.nil? end - def should_continue_after_tools? - # Continue conversation only with :auto tool choice to avoid infinite loops. - # With :any or specific tool choices, the model would keep calling tools repeatedly. - tool_choice.nil? || tool_choice == :auto + def forced_tool_choice? + @tool_choice && !%i[auto none].include?(@tool_choice) + end + + def reset_tool_choice + @tool_choice = nil end def instance_variables From 70831d1099d37d1da743809b7492be902a3a9646 Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 26 Aug 2025 22:24:42 -0700 Subject: [PATCH 04/11] Replace :any with :required --- lib/ruby_llm/chat.rb | 2 +- lib/ruby_llm/providers/anthropic/tools.rb | 9 ++++++++- lib/ruby_llm/providers/gemini/tools.rb | 8 ++++++-- lib/ruby_llm/providers/openai/tools.rb | 4 +--- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index e011296de..9133f7671 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -216,7 +216,7 @@ def execute_tool(tool_call) def update_tool_options(choice:, parallel:) unless choice.nil? - valid_tool_choices = %i[auto none any] + tools.keys + valid_tool_choices = %i[auto none required] + tools.keys unless valid_tool_choices.include?(choice.to_sym) raise InvalidToolChoiceError, "Invalid tool choice: #{choice}. Valid choices are: #{valid_tool_choices.join(', ')}" diff --git a/lib/ruby_llm/providers/anthropic/tools.rb b/lib/ruby_llm/providers/anthropic/tools.rb index edce8a787..77743944c 100644 --- a/lib/ruby_llm/providers/anthropic/tools.rb +++ b/lib/ruby_llm/providers/anthropic/tools.rb @@ -104,7 +104,14 @@ def required_parameters(parameters) def build_tool_choice(tool_choice, parallel_tool_calls) { - type: %i[auto any none].include?(tool_choice) ? tool_choice : :tool + type: case tool_choice + when :auto, :none + tool_choice + when :required + :any + else + :tool + end }.tap do |tc| tc[:name] = tool_choice if tc[:type] == :tool tc[:disable_parallel_tool_use] = !parallel_tool_calls unless tc[:type] == :none || parallel_tool_calls.nil? diff --git a/lib/ruby_llm/providers/gemini/tools.rb b/lib/ruby_llm/providers/gemini/tools.rb index a257345c4..9fbc77029 100644 --- a/lib/ruby_llm/providers/gemini/tools.rb +++ b/lib/ruby_llm/providers/gemini/tools.rb @@ -75,7 +75,7 @@ def param_type_for_gemini(type) def build_tool_config(tool_choice) { functionCallingConfig: { - mode: specific_tool_choice?(tool_choice) ? 'any' : tool_choice + mode: forced_tool_choice?(tool_choice) ? 'any' : tool_choice }.tap do |config| # Use allowedFunctionNames to simulate specific tool choice config[:allowedFunctionNames] = [tool_choice] if specific_tool_choice?(tool_choice) @@ -83,8 +83,12 @@ def build_tool_config(tool_choice) } end + def forced_tool_choice?(tool_choice) + tool_choice == :required || specific_tool_choice?(tool_choice) + end + def specific_tool_choice?(tool_choice) - !%i[auto none any].include?(tool_choice) + !%i[auto none required].include?(tool_choice) end end end diff --git a/lib/ruby_llm/providers/openai/tools.rb b/lib/ruby_llm/providers/openai/tools.rb index 7cf87d89d..2f93b9722 100644 --- a/lib/ruby_llm/providers/openai/tools.rb +++ b/lib/ruby_llm/providers/openai/tools.rb @@ -70,10 +70,8 @@ def parse_tool_calls(tool_calls, parse_arguments: true) def build_tool_choice(tool_choice) case tool_choice - when :auto, :none + when :auto, :none, :required tool_choice - when :any - :required else { type: 'function', From a932b78f59f6968bfb101ab70523dc3be4484033 Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 26 Aug 2025 22:39:47 -0700 Subject: [PATCH 05/11] Refactor tool options into unified tool_prefs hash --- lib/ruby_llm/chat.rb | 16 +++++++--------- lib/ruby_llm/provider.rb | 5 ++--- lib/ruby_llm/providers/anthropic/chat.rb | 8 ++++---- lib/ruby_llm/providers/anthropic/tools.rb | 5 ++++- lib/ruby_llm/providers/bedrock/chat.rb | 8 ++++---- lib/ruby_llm/providers/gemini/chat.rb | 5 ++--- lib/ruby_llm/providers/mistral/chat.rb | 2 +- lib/ruby_llm/providers/openai/chat.rb | 7 +++---- 8 files changed, 27 insertions(+), 29 deletions(-) diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index 9133f7671..48a8c8e6d 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -5,7 +5,7 @@ module RubyLLM class Chat include Enumerable - attr_reader :model, :messages, :tools, :tool_choice, :parallel_tool_calls, :params, :headers, :schema + attr_reader :model, :messages, :tools, :tool_prefs, :params, :headers, :schema def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil) if assume_model_exists && !provider @@ -17,8 +17,7 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n model_id = model || @config.default_model with_model(model_id, provider: provider, assume_exists: assume_model_exists) @temperature = nil - @tool_choice = nil - @parallel_tool_calls = nil + @tool_prefs = { choice: nil, parallel: nil } @messages = [] @tools = {} @params = {} @@ -136,8 +135,7 @@ def complete(&) # rubocop:disable Metrics/PerceivedComplexity params: @params, headers: @headers, schema: @schema, - tool_choice: @tool_choice, - parallel_tool_calls: @parallel_tool_calls, + tool_prefs: @tool_prefs, &wrap_streaming_block(&) ) @@ -222,18 +220,18 @@ def update_tool_options(choice:, parallel:) "Invalid tool choice: #{choice}. Valid choices are: #{valid_tool_choices.join(', ')}" end - @tool_choice = choice.to_sym + @tool_prefs[:choice] = choice.to_sym end - @parallel_tool_calls = !!parallel unless parallel.nil? + @tool_prefs[:parallel] = !!parallel unless parallel.nil? end def forced_tool_choice? - @tool_choice && !%i[auto none].include?(@tool_choice) + @tool_prefs[:choice] && !%i[auto none].include?(@tool_prefs[:choice]) end def reset_tool_choice - @tool_choice = nil + @tool_prefs[:choice] = nil end def instance_variables diff --git a/lib/ruby_llm/provider.rb b/lib/ruby_llm/provider.rb index ff3564b21..5daa126f7 100644 --- a/lib/ruby_llm/provider.rb +++ b/lib/ruby_llm/provider.rb @@ -39,7 +39,7 @@ def configuration_requirements # rubocop:disable Metrics/ParameterLists def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, - tool_choice: nil, parallel_tool_calls: nil, &) + tool_prefs: nil, &) normalized_temperature = maybe_normalize_temperature(temperature, model) payload = Utils.deep_merge( @@ -47,8 +47,7 @@ def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, sc render_payload( messages, tools: tools, - tool_choice: tool_choice, - parallel_tool_calls: parallel_tool_calls, + tool_prefs: tool_prefs, temperature: normalized_temperature, model: model, stream: block_given?, diff --git a/lib/ruby_llm/providers/anthropic/chat.rb b/lib/ruby_llm/providers/anthropic/chat.rb index cbc0db922..4f307b4bb 100644 --- a/lib/ruby_llm/providers/anthropic/chat.rb +++ b/lib/ruby_llm/providers/anthropic/chat.rb @@ -12,13 +12,13 @@ def completion_url end # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument - def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, + def render_payload(messages, tools:, tool_prefs:, temperature:, model:, stream: false, schema: nil) system_messages, chat_messages = separate_messages(messages) system_content = build_system_content(system_messages) build_base_payload(chat_messages, model, stream).tap do |payload| - add_optional_fields(payload, system_content:, tools:, tool_choice:, parallel_tool_calls:, temperature:) + add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:) end end # rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument @@ -47,10 +47,10 @@ def build_base_payload(chat_messages, model, stream) } end - def add_optional_fields(payload, system_content:, tools:, tool_choice:, parallel_tool_calls:, temperature:) # rubocop:disable Metrics/ParameterLists + def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:) if tools.any? payload[:tools] = tools.values.map { |t| Tools.function_for(t) } - payload[:tool_choice] = build_tool_choice(tool_choice, parallel_tool_calls) unless tool_choice.nil? + payload[:tool_choice] = build_tool_choice(tool_prefs) unless tool_prefs[:choice].nil? end payload[:system] = system_content unless system_content.empty? diff --git a/lib/ruby_llm/providers/anthropic/tools.rb b/lib/ruby_llm/providers/anthropic/tools.rb index 77743944c..c8949c6f4 100644 --- a/lib/ruby_llm/providers/anthropic/tools.rb +++ b/lib/ruby_llm/providers/anthropic/tools.rb @@ -102,7 +102,10 @@ def required_parameters(parameters) parameters.select { |_, param| param.required }.keys end - def build_tool_choice(tool_choice, parallel_tool_calls) + def build_tool_choice(tool_prefs) + tool_choice = tool_prefs[:choice] + parallel_tool_calls = tool_prefs[:parallel] + { type: case tool_choice when :auto, :none diff --git a/lib/ruby_llm/providers/bedrock/chat.rb b/lib/ruby_llm/providers/bedrock/chat.rb index 37181cae8..90974f635 100644 --- a/lib/ruby_llm/providers/bedrock/chat.rb +++ b/lib/ruby_llm/providers/bedrock/chat.rb @@ -40,16 +40,16 @@ def completion_url end # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument - def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, - temperature:, model:, stream: false, schema: nil) + def render_payload(messages, tools:, tool_prefs:, temperature:, model:, stream: false, + schema: nil) @model_id = model system_messages, chat_messages = Anthropic::Chat.separate_messages(messages) system_content = Anthropic::Chat.build_system_content(system_messages) build_base_payload(chat_messages, model).tap do |payload| - Anthropic::Chat.add_optional_fields(payload, system_content:, tools:, tool_choice:, - parallel_tool_calls:, temperature:) + Anthropic::Chat.add_optional_fields(payload, system_content:, tools:, tool_prefs:, + temperature:) end end # rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument diff --git a/lib/ruby_llm/providers/gemini/chat.rb b/lib/ruby_llm/providers/gemini/chat.rb index ac044f034..e6571f020 100644 --- a/lib/ruby_llm/providers/gemini/chat.rb +++ b/lib/ruby_llm/providers/gemini/chat.rb @@ -12,8 +12,7 @@ def completion_url end # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument - def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, - temperature:, model:, stream: false, schema: nil) + def render_payload(messages, tools:, tool_prefs:, temperature:, model:, stream: false, schema: nil) @model = model payload = { contents: format_messages(messages), @@ -30,7 +29,7 @@ def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, if tools.any? payload[:tools] = format_tools(tools) # Gemini doesn't support controlling parallel tool calls - payload[:toolConfig] = build_tool_config(tool_choice) unless tool_choice.nil? + payload[:toolConfig] = build_tool_config(tool_prefs[:choice]) unless tool_prefs[:choice].nil? end payload diff --git a/lib/ruby_llm/providers/mistral/chat.rb b/lib/ruby_llm/providers/mistral/chat.rb index 3a39673fc..a48eeaadc 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:, tool_choice:, parallel_tool_calls:, temperature:, model:, stream: false, + def render_payload(messages, tools:, tool_prefs:, temperature:, model:, stream: false, schema: nil) payload = super payload.delete(:stream_options) diff --git a/lib/ruby_llm/providers/openai/chat.rb b/lib/ruby_llm/providers/openai/chat.rb index fcbd7865c..79be33080 100644 --- a/lib/ruby_llm/providers/openai/chat.rb +++ b/lib/ruby_llm/providers/openai/chat.rb @@ -12,8 +12,7 @@ def completion_url module_function # rubocop:disable Metrics/ParameterLists - def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, - temperature:, model:, stream: false, schema: nil) + def render_payload(messages, tools:, tool_prefs:, temperature:, model:, stream: false, schema: nil) payload = { model: model, messages: format_messages(messages), @@ -24,8 +23,8 @@ def render_payload(messages, tools:, tool_choice:, parallel_tool_calls:, if tools.any? payload[:tools] = tools.map { |_, tool| tool_for(tool) } - payload[:tool_choice] = build_tool_choice(tool_choice) unless tool_choice.nil? - payload[:parallel_tool_calls] = parallel_tool_calls unless parallel_tool_calls.nil? + payload[:tool_choice] = build_tool_choice(tool_prefs[:choice]) unless tool_prefs[:choice].nil? + payload[:parallel_tool_calls] = tool_prefs[:parallel] unless tool_prefs[:parallel].nil? end if schema From 3a2b830e24cdde3556e8a1beb928756c6d864c1f Mon Sep 17 00:00:00 2001 From: Sophia Date: Wed, 27 Aug 2025 10:19:48 -0700 Subject: [PATCH 06/11] Update docs --- docs/_core_features/tools.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/_core_features/tools.md b/docs/_core_features/tools.md index b50ac8301..6022ec1ed 100644 --- a/docs/_core_features/tools.md +++ b/docs/_core_features/tools.md @@ -186,23 +186,36 @@ puts response.content ### Tool Choice Control -Control when and how tools are called using `choice` and `parallel` options: +Control when and how tools are called using `choice` and `parallel` options. + +**Parameter Values:** +- **`choice`**: Controls tool choice behavior + - `:auto` Model decides whether to use any tools + - `:required` - Model must use one of the provided tools + - `:none` - Disable all tools + - `"tool_name"` - Force a specific tool (e.g., `:weather` for `Weather` tool) +- **`parallel`**: Controls parallel tool calls + - `true` Allow multiple tool calls simultaneously + - `false` - One at a time + +If not provided, RubyLLM will use the provider's default behavior for tool choice and parallel tool calls. + +**Examples:** ```ruby chat = RubyLLM.chat(model: 'gpt-4o') -# Choice options -chat.with_tool(Weather, choice: :auto) # Model decides whether to call any provided tools or not (default) -chat.with_tool(Weather, choice: :any) # Model must use one of the provided tools -chat.with_tool(Weather, choice: :none) # No tools -chat.with_tool(Weather, choice: :weather) # Force specific tool +# Basic usage with defaults +chat.with_tools(Weather, Calculator) # uses provider defaults + +# Force tool usage, one at a time +chat.with_tools(Weather, Calculator, choice: :required, parallel: false) -# Parallel tool calls -chat.with_tools(Weather, Calculator, parallel: true) # Model can output multiple tool calls at once (default) -chat.with_tools(Weather, Calculator, parallel: false) # At most one tool call +# Force specific tool +chat.with_tool(Weather, choice: :weather, parallel: true) ``` -> With `:any` or specific tool choices, tool results are not automatically sent back to the AI model (see The Tool Execution Flow section below) to prevent infinite loops. +> With `:required` or specific tool choices, the tool_choice is automatically reset to `nil` after tool execution to prevent infinite loops. {: .note } ### Model Compatibility From fb06e7d85544527aa83a2c4b3b20d4f692c5303e Mon Sep 17 00:00:00 2001 From: Sophia Date: Fri, 29 Aug 2025 13:28:42 -0700 Subject: [PATCH 07/11] Pass `tool_prefs` after `tools` --- lib/ruby_llm/chat.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ruby_llm/chat.rb b/lib/ruby_llm/chat.rb index 41e7df01f..e13a60d88 100644 --- a/lib/ruby_llm/chat.rb +++ b/lib/ruby_llm/chat.rb @@ -17,9 +17,9 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n model_id = model || @config.default_model with_model(model_id, provider: provider, assume_exists: assume_model_exists) @temperature = nil - @tool_prefs = { choice: nil, parallel: nil } @messages = [] @tools = {} + @tool_prefs = { choice: nil, parallel: nil } @params = {} @headers = {} @schema = nil @@ -130,12 +130,12 @@ def complete(&) # rubocop:disable Metrics/PerceivedComplexity response = @provider.complete( messages, tools: @tools, + tool_prefs: @tool_prefs, temperature: @temperature, model: @model, params: @params, headers: @headers, schema: @schema, - tool_prefs: @tool_prefs, &wrap_streaming_block(&) ) From fefd1a2daa2942f2075825ba6b2698e7d2331b1a Mon Sep 17 00:00:00 2001 From: Sophia Date: Fri, 29 Aug 2025 13:30:49 -0700 Subject: [PATCH 08/11] Update docs --- docs/_core_features/tools.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/docs/_core_features/tools.md b/docs/_core_features/tools.md index 6022ec1ed..c0367cb4e 100644 --- a/docs/_core_features/tools.md +++ b/docs/_core_features/tools.md @@ -188,20 +188,6 @@ puts response.content Control when and how tools are called using `choice` and `parallel` options. -**Parameter Values:** -- **`choice`**: Controls tool choice behavior - - `:auto` Model decides whether to use any tools - - `:required` - Model must use one of the provided tools - - `:none` - Disable all tools - - `"tool_name"` - Force a specific tool (e.g., `:weather` for `Weather` tool) -- **`parallel`**: Controls parallel tool calls - - `true` Allow multiple tool calls simultaneously - - `false` - One at a time - -If not provided, RubyLLM will use the provider's default behavior for tool choice and parallel tool calls. - -**Examples:** - ```ruby chat = RubyLLM.chat(model: 'gpt-4o') @@ -215,6 +201,18 @@ chat.with_tools(Weather, Calculator, choice: :required, parallel: false) chat.with_tool(Weather, choice: :weather, parallel: true) ``` +**Parameter Values:** +- **`choice`**: Controls tool choice behavior + - `:auto` Model decides whether to use any tools + - `:required` - Model must use one of the provided tools + - `:none` - Disable all tools + - `"tool_name"` - Force a specific tool (e.g., `:weather` for `Weather` tool) +- **`parallel`**: Controls parallel tool calls + - `true` Allow multiple tool calls simultaneously + - `false` - One at a time + +If not provided, RubyLLM will use the provider's default behavior for tool choice and parallel tool calls. + > With `:required` or specific tool choices, the tool_choice is automatically reset to `nil` after tool execution to prevent infinite loops. {: .note } From 5359c79a951904ccdac279fb865c5750e6d5ebfd Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 23 Sep 2025 14:55:43 -0700 Subject: [PATCH 09/11] Namespace build_tool_choice with Tools module --- lib/ruby_llm/providers/anthropic/chat.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ruby_llm/providers/anthropic/chat.rb b/lib/ruby_llm/providers/anthropic/chat.rb index d61c2123d..e08e3e7d8 100644 --- a/lib/ruby_llm/providers/anthropic/chat.rb +++ b/lib/ruby_llm/providers/anthropic/chat.rb @@ -50,7 +50,7 @@ def build_base_payload(chat_messages, model, stream) def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:) if tools.any? payload[:tools] = tools.values.map { |t| Tools.function_for(t) } - payload[:tool_choice] = build_tool_choice(tool_prefs) unless tool_prefs[:choice].nil? + payload[:tool_choice] = Tools.build_tool_choice(tool_prefs) unless tool_prefs[:choice].nil? end payload[:system] = system_content unless system_content.empty? From cef8c06f06302615825d0da1fc39ae656d0c15fe Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 23 Sep 2025 15:12:08 -0700 Subject: [PATCH 10/11] Add tool control capability methods to providers --- .../providers/anthropic/capabilities.rb | 8 ++++++++ .../providers/bedrock/capabilities.rb | 8 ++++++++ .../providers/deepseek/capabilities.rb | 8 ++++++++ lib/ruby_llm/providers/gemini/capabilities.rb | 8 ++++++++ lib/ruby_llm/providers/gpustack.rb | 4 ++++ .../providers/gpustack/capabilities.rb | 20 +++++++++++++++++++ .../providers/mistral/capabilities.rb | 8 ++++++++ lib/ruby_llm/providers/ollama.rb | 4 ++++ lib/ruby_llm/providers/ollama/capabilities.rb | 20 +++++++++++++++++++ lib/ruby_llm/providers/openai/capabilities.rb | 8 ++++++++ 10 files changed, 96 insertions(+) create mode 100644 lib/ruby_llm/providers/gpustack/capabilities.rb create mode 100644 lib/ruby_llm/providers/ollama/capabilities.rb diff --git a/lib/ruby_llm/providers/anthropic/capabilities.rb b/lib/ruby_llm/providers/anthropic/capabilities.rb index 710b55386..fdf86924c 100644 --- a/lib/ruby_llm/providers/anthropic/capabilities.rb +++ b/lib/ruby_llm/providers/anthropic/capabilities.rb @@ -34,6 +34,14 @@ def supports_functions?(model_id) model_id.match?(/claude-3/) end + def supports_tool_choice?(_model_id) + true + end + + def supports_tool_parallel_control?(_model_id) + true + end + def supports_json_mode?(model_id) model_id.match?(/claude-3/) end diff --git a/lib/ruby_llm/providers/bedrock/capabilities.rb b/lib/ruby_llm/providers/bedrock/capabilities.rb index 94b8e1438..757f02be2 100644 --- a/lib/ruby_llm/providers/bedrock/capabilities.rb +++ b/lib/ruby_llm/providers/bedrock/capabilities.rb @@ -46,6 +46,14 @@ def supports_functions?(model_id) model_id.match?(/anthropic\.claude/) end + def supports_tool_choice?(model_id) + model_id.match?(/anthropic\.claude/) + end + + def supports_tool_parallel_control?(_model_id) + false + end + def supports_audio?(_model_id) false end diff --git a/lib/ruby_llm/providers/deepseek/capabilities.rb b/lib/ruby_llm/providers/deepseek/capabilities.rb index 975f0df9a..8a3bad737 100644 --- a/lib/ruby_llm/providers/deepseek/capabilities.rb +++ b/lib/ruby_llm/providers/deepseek/capabilities.rb @@ -41,6 +41,14 @@ def supports_functions?(model_id) model_id.match?(/deepseek-chat/) end + def supports_tool_choice?(_model_id) + true + end + + def supports_tool_parallel_control?(_model_id) + false + end + def supports_json_mode?(_model_id) false end diff --git a/lib/ruby_llm/providers/gemini/capabilities.rb b/lib/ruby_llm/providers/gemini/capabilities.rb index 41b046d9e..528ae9e2f 100644 --- a/lib/ruby_llm/providers/gemini/capabilities.rb +++ b/lib/ruby_llm/providers/gemini/capabilities.rb @@ -62,6 +62,14 @@ def supports_functions?(model_id) model_id.match?(/gemini|pro|flash/) end + def supports_tool_choice?(_model_id) + true + end + + def supports_tool_parallel_control?(_model_id) + false + end + def supports_json_mode?(model_id) if model_id.match?(/text-embedding|embedding-001|aqa|imagen|gemini-2\.0-flash-lite|gemini-2\.5-pro-exp-03-25/) return false diff --git a/lib/ruby_llm/providers/gpustack.rb b/lib/ruby_llm/providers/gpustack.rb index fed40ba99..c91d1a06f 100644 --- a/lib/ruby_llm/providers/gpustack.rb +++ b/lib/ruby_llm/providers/gpustack.rb @@ -28,6 +28,10 @@ def local? def configuration_requirements %i[gpustack_api_base] end + + def capabilities + GPUStack::Capabilities + end end end end diff --git a/lib/ruby_llm/providers/gpustack/capabilities.rb b/lib/ruby_llm/providers/gpustack/capabilities.rb new file mode 100644 index 000000000..ad6cb4a28 --- /dev/null +++ b/lib/ruby_llm/providers/gpustack/capabilities.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyLLM + module Providers + class GPUStack + # Determines capabilities for GPUStack models + module Capabilities + module_function + + def supports_tool_choice?(_model_id) + false + end + + def supports_tool_parallel_control?(_model_id) + false + end + end + end + end +end diff --git a/lib/ruby_llm/providers/mistral/capabilities.rb b/lib/ruby_llm/providers/mistral/capabilities.rb index 85616a95a..26a21edc7 100644 --- a/lib/ruby_llm/providers/mistral/capabilities.rb +++ b/lib/ruby_llm/providers/mistral/capabilities.rb @@ -15,6 +15,14 @@ def supports_tools?(model_id) !model_id.match?(/embed|moderation|ocr|voxtral|transcriptions|mistral-(tiny|small)-(2312|2402)/) end + def supports_tool_choice?(_model_id) + true + end + + def supports_tool_parallel_control?(_model_id) + true + end + def supports_vision?(model_id) model_id.match?(/pixtral|mistral-small-(2503|2506)|mistral-medium/) end diff --git a/lib/ruby_llm/providers/ollama.rb b/lib/ruby_llm/providers/ollama.rb index ea20c4953..2c4182291 100644 --- a/lib/ruby_llm/providers/ollama.rb +++ b/lib/ruby_llm/providers/ollama.rb @@ -24,6 +24,10 @@ def configuration_requirements def local? true end + + def capabilities + Ollama::Capabilities + end end end end diff --git a/lib/ruby_llm/providers/ollama/capabilities.rb b/lib/ruby_llm/providers/ollama/capabilities.rb new file mode 100644 index 000000000..225181db7 --- /dev/null +++ b/lib/ruby_llm/providers/ollama/capabilities.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module RubyLLM + module Providers + class Ollama + # Determines capabilities for Ollama models + module Capabilities + module_function + + def supports_tool_choice?(_model_id) + false + end + + def supports_tool_parallel_control?(_model_id) + false + end + end + end + end +end diff --git a/lib/ruby_llm/providers/openai/capabilities.rb b/lib/ruby_llm/providers/openai/capabilities.rb index 37c320da8..cbfd57cca 100644 --- a/lib/ruby_llm/providers/openai/capabilities.rb +++ b/lib/ruby_llm/providers/openai/capabilities.rb @@ -97,6 +97,14 @@ def supports_functions?(model_id) end end + def supports_tool_choice?(_model_id) + true + end + + def supports_tool_parallel_control?(_model_id) + true + end + def supports_structured_output?(model_id) case model_family(model_id) when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4o', From 9a3d68256f535175769e7fd36b80fa50edf59b58 Mon Sep 17 00:00:00 2001 From: Sophia Date: Tue, 23 Sep 2025 16:45:54 -0700 Subject: [PATCH 11/11] Add tests --- ...-5-haiku-20241022_respects_choice_none.yml | 86 +++++ ..._choice_required_for_unrelated_queries.yml | 183 ++++++++++ ...arallel_false_for_sequential_execution.yml | 272 ++++++++++++++ ...20241022_respects_specific_tool_choice.yml | 186 ++++++++++ ..._choice_required_for_unrelated_queries.yml | 128 +++++++ ...022-v1_0_respects_specific_tool_choice.yml | 131 +++++++ ...eek_deepseek-chat_respects_choice_none.yml | 60 +++ ..._choice_required_for_unrelated_queries.yml | 118 ++++++ ...eek-chat_respects_specific_tool_choice.yml | 118 ++++++ ..._gemini-2_5-flash_respects_choice_none.yml | 85 +++++ ..._choice_required_for_unrelated_queries.yml | 146 ++++++++ ..._5-flash_respects_specific_tool_choice.yml | 174 +++++++++ ...tral-small-latest_respects_choice_none.yml | 81 +++++ ..._choice_required_for_unrelated_queries.yml | 165 +++++++++ ...arallel_false_for_sequential_execution.yml | 166 +++++++++ ...l-latest_respects_specific_tool_choice.yml | 176 +++++++++ ...enai_gpt-4_1-nano_respects_choice_none.yml | 118 ++++++ ..._choice_required_for_unrelated_queries.yml | 253 +++++++++++++ ...arallel_false_for_sequential_execution.yml | 344 ++++++++++++++++++ ...4_1-nano_respects_specific_tool_choice.yml | 248 +++++++++++++ ..._claude-3_5-haiku_respects_choice_none.yml | 57 +++ ..._choice_required_for_unrelated_queries.yml | 126 +++++++ ...arallel_false_for_sequential_execution.yml | 186 ++++++++++ ..._5-haiku_respects_specific_tool_choice.yml | 112 ++++++ ..._gemini-2_5-flash_respects_choice_none.yml | 111 ++++++ ..._choice_required_for_unrelated_queries.yml | 302 +++++++++++++++ ..._5-flash_respects_specific_tool_choice.yml | 300 +++++++++++++++ spec/ruby_llm/chat_tools_spec.rb | 102 ++++++ 28 files changed, 4534 insertions(+) create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_choice_none.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_choice_required_for_unrelated_queries.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_parallel_false_for_sequential_execution.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_specific_tool_choice.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_respects_choice_required_for_unrelated_queries.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_respects_specific_tool_choice.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_choice_none.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_choice_required_for_unrelated_queries.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_specific_tool_choice.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_choice_none.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_choice_required_for_unrelated_queries.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_specific_tool_choice.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_choice_none.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_choice_required_for_unrelated_queries.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_parallel_false_for_sequential_execution.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_specific_tool_choice.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_choice_none.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_choice_required_for_unrelated_queries.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_parallel_false_for_sequential_execution.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_specific_tool_choice.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_choice_none.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_choice_required_for_unrelated_queries.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_parallel_false_for_sequential_execution.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_specific_tool_choice.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_choice_none.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_choice_required_for_unrelated_queries.yml create mode 100644 spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_specific_tool_choice.yml diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_choice_none.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_choice_none.yml new file mode 100644 index 000000000..ea830aab3 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_choice_none.yml @@ -0,0 +1,86 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"What''s + the weather in Berlin? (52.5200, 13.4050)"}]}],"stream":false,"max_tokens":8192,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}],"tool_choice":{"type":"none"}}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Sun, 31 Aug 2025 01:12:54 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-08-31T01:12:53Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-08-31T01:12:54Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-08-31T01:12:53Z' + Anthropic-Ratelimit-Tokens-Limit: + - '480000' + Anthropic-Ratelimit-Tokens-Remaining: + - '480000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-08-31T01:12:53Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + X-Envoy-Upstream-Service-Time: + - '934' + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01CeSiqgUDBk58hT5GPRDtZm","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"Let + me check the current weather for Berlin using the provided coordinates."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":389,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":22,"service_tier":"standard"}}' + recorded_at: Sun, 31 Aug 2025 01:12:54 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_choice_required_for_unrelated_queries.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_choice_required_for_unrelated_queries.yml new file mode 100644 index 000000000..0dad56001 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_choice_required_for_unrelated_queries.yml @@ -0,0 +1,183 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"When + was the fall of Rome?"}]}],"stream":false,"max_tokens":8192,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}],"tool_choice":{"type":"any"}}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Sun, 31 Aug 2025 01:12:55 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-08-31T01:12:55Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-08-31T01:12:55Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-08-31T01:12:54Z' + Anthropic-Ratelimit-Tokens-Limit: + - '480000' + Anthropic-Ratelimit-Tokens-Remaining: + - '480000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-08-31T01:12:55Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + X-Envoy-Upstream-Service-Time: + - '1151' + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_011ZXkF13Ept7roaZ7uy8S2Z","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"tool_use","id":"toolu_01T7eCvB7yP2hayppNh61uW3","name":"weather","input":{"latitude":"41.9028","longitude":"12.4964"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":468,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":59,"service_tier":"standard"}}' + recorded_at: Sun, 31 Aug 2025 01:12:55 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"When + was the fall of Rome?"}]},{"role":"assistant","content":[{"type":"tool_use","id":"toolu_01T7eCvB7yP2hayppNh61uW3","name":"weather","input":{"latitude":"41.9028","longitude":"12.4964"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01T7eCvB7yP2hayppNh61uW3","content":[{"type":"text","text":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h"}]}]}],"stream":false,"max_tokens":8192,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Sun, 31 Aug 2025 01:13:00 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-08-31T01:12:56Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-08-31T01:13:01Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-08-31T01:12:55Z' + Anthropic-Ratelimit-Tokens-Limit: + - '480000' + Anthropic-Ratelimit-Tokens-Remaining: + - '480000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-08-31T01:12:56Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + X-Envoy-Upstream-Service-Time: + - '5158' + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01FJYoxLea1LN4z76aqyH2gX","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"I + apologize, but the weather tool is not appropriate for answering a historical + question about the fall of Rome. Let me provide a historical answer instead.\n\nThe + fall of Rome is typically dated to 476 CE, when the last Roman Emperor in + the West, Romulus Augustulus, was deposed by the Germanic king Odoacer. This + marked the end of the Western Roman Empire, though the Eastern Roman Empire + (Byzantine Empire) continued to exist for nearly another thousand years until + Constantinople fell to the Ottoman Turks in 1453.\n\nHowever, historians debate + the exact moment of \"fall,\" as the decline of the Roman Empire was a gradual + process that occurred over several centuries. Some key events in this decline + include:\n\n1. The division of the Empire into Western and Eastern halves + in 285 CE\n2. The sack of Rome by the Visigoths in 410 CE\n3. The final deposition + of Romulus Augustulus in 476 CE\n4. The end of the Western Roman Empire''s + political structures\n\nThe fall of Rome marked the beginning of the Medieval + period in European history and had profound implications for the political, + social, and cultural landscape of Europe."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":491,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":258,"service_tier":"standard"}}' + recorded_at: Sun, 31 Aug 2025 01:13:01 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_parallel_false_for_sequential_execution.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_parallel_false_for_sequential_execution.yml new file mode 100644 index 000000000..1e2ade981 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_parallel_false_for_sequential_execution.yml @@ -0,0 +1,272 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"What''s + the weather in Berlin and what''s the best programming language?"}]}],"stream":false,"max_tokens":8192,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}},{"name":"best_language_to_learn","description":"Gets + the best language to learn","input_schema":{"type":"object","properties":{},"required":[]}}],"system":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Sun, 31 Aug 2025 01:13:03 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-08-31T01:13:01Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-08-31T01:13:03Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-08-31T01:13:01Z' + Anthropic-Ratelimit-Tokens-Limit: + - '480000' + Anthropic-Ratelimit-Tokens-Remaining: + - '480000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-08-31T01:13:01Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + X-Envoy-Upstream-Service-Time: + - '1923' + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01JnHdgM7UnVJrDWEAy51pZs","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"I''ll + help you find out the weather in Berlin and the best programming language + to learn. I''ll use the available tools to get this information for you.\n\nFirst, + let''s check the weather in Berlin:"},{"type":"tool_use","id":"toolu_01MgneaQ2SYw3piJ14QaVaNi","name":"weather","input":{"latitude":"52.5200","longitude":"13.4050"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":460,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":117,"service_tier":"standard"}}' + recorded_at: Sun, 31 Aug 2025 01:13:03 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"What''s + the weather in Berlin and what''s the best programming language?"}]},{"role":"assistant","content":[{"type":"text","text":"I''ll + help you find out the weather in Berlin and the best programming language + to learn. I''ll use the available tools to get this information for you.\n\nFirst, + let''s check the weather in Berlin:"},{"type":"tool_use","id":"toolu_01MgneaQ2SYw3piJ14QaVaNi","name":"weather","input":{"latitude":"52.5200","longitude":"13.4050"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01MgneaQ2SYw3piJ14QaVaNi","content":[{"type":"text","text":"Current + weather at 52.5200, 13.4050: 15°C, Wind: 10 km/h"}]}]}],"stream":false,"max_tokens":8192,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}},{"name":"best_language_to_learn","description":"Gets + the best language to learn","input_schema":{"type":"object","properties":{},"required":[]}}],"system":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Sun, 31 Aug 2025 01:13:04 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-08-31T01:13:03Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-08-31T01:13:04Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-08-31T01:13:03Z' + Anthropic-Ratelimit-Tokens-Limit: + - '480000' + Anthropic-Ratelimit-Tokens-Remaining: + - '480000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-08-31T01:13:03Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + X-Envoy-Upstream-Service-Time: + - '1081' + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01GysMofP1j2yodwkPx3HrCw","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"Now, + let''s find out the best programming language to learn:"},{"type":"tool_use","id":"toolu_01A7KoRqUe7Y3BrthnxUGbAo","name":"best_language_to_learn","input":{}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":616,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":55,"service_tier":"standard"}}' + recorded_at: Sun, 31 Aug 2025 01:13:04 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"What''s + the weather in Berlin and what''s the best programming language?"}]},{"role":"assistant","content":[{"type":"text","text":"I''ll + help you find out the weather in Berlin and the best programming language + to learn. I''ll use the available tools to get this information for you.\n\nFirst, + let''s check the weather in Berlin:"},{"type":"tool_use","id":"toolu_01MgneaQ2SYw3piJ14QaVaNi","name":"weather","input":{"latitude":"52.5200","longitude":"13.4050"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01MgneaQ2SYw3piJ14QaVaNi","content":[{"type":"text","text":"Current + weather at 52.5200, 13.4050: 15°C, Wind: 10 km/h"}]}]},{"role":"assistant","content":[{"type":"text","text":"Now, + let''s find out the best programming language to learn:"},{"type":"tool_use","id":"toolu_01A7KoRqUe7Y3BrthnxUGbAo","name":"best_language_to_learn","input":{}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01A7KoRqUe7Y3BrthnxUGbAo","content":[{"type":"text","text":"Ruby"}]}]}],"stream":false,"max_tokens":8192,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}},{"name":"best_language_to_learn","description":"Gets + the best language to learn","input_schema":{"type":"object","properties":{},"required":[]}}],"system":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Sun, 31 Aug 2025 01:13:07 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-08-31T01:13:05Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-08-31T01:13:07Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-08-31T01:13:04Z' + Anthropic-Ratelimit-Tokens-Limit: + - '480000' + Anthropic-Ratelimit-Tokens-Remaining: + - '480000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-08-31T01:13:05Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + X-Envoy-Upstream-Service-Time: + - '2376' + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6Im1zZ18wMVN6OFVHTnhRdHdSRlJqQ01yQXRaZm4iLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJtb2RlbCI6ImNsYXVkZS0zLTUtaGFpa3UtMjAyNDEwMjIiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJIZXJlJ3MgdGhlIGluZm9ybWF0aW9uIHlvdSBhc2tlZCBmb3I6XG4xLiBXZWF0aGVyIGluIEJlcmxpbjogSXQncyBjdXJyZW50bHkgMTXCsEMgd2l0aCBhIHdpbmQgc3BlZWQgb2YgMTAga20vaC5cbjIuIEJlc3QgUHJvZ3JhbW1pbmcgTGFuZ3VhZ2UgdG8gTGVhcm46IFJ1YnlcblxuVGhlIHdlYXRoZXIgc2VlbXMgbWlsZCBhbmQgcGxlYXNhbnQsIGFuZCBSdWJ5IGlzIHJlY29tbWVuZGVkIGFzIGEgZ3JlYXQgcHJvZ3JhbW1pbmcgbGFuZ3VhZ2UgdG8gbGVhcm4uIFJ1YnkgaXMga25vd24gZm9yIGl0cyBzaW1wbGljaXR5IGFuZCByZWFkYWJpbGl0eSwgbWFraW5nIGl0IGEgZ29vZCBjaG9pY2UgZm9yIGJlZ2lubmVycyBhbmQgZXhwZXJpZW5jZWQgZGV2ZWxvcGVycyBhbGlrZS5cblxuSXMgdGhlcmUgYW55dGhpbmcgZWxzZSBJIGNhbiBoZWxwIHlvdSB3aXRoPyJ9XSwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6NjgzLCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MTAzLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9fQ== + recorded_at: Sun, 31 Aug 2025 01:13:07 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_specific_tool_choice.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_specific_tool_choice.yml new file mode 100644 index 000000000..17116fb8f --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_anthropic_claude-3-5-haiku-20241022_respects_specific_tool_choice.yml @@ -0,0 +1,186 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"What''s + the fall of Rome?"}]}],"stream":false,"max_tokens":8192,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}],"tool_choice":{"type":"tool","name":"weather"}}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Tue, 23 Sep 2025 22:59:27 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-09-23T22:59:26Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-09-23T22:59:27Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-09-23T22:59:26Z' + Anthropic-Ratelimit-Tokens-Limit: + - '480000' + Anthropic-Ratelimit-Tokens-Remaining: + - '480000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-09-23T22:59:26Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + X-Envoy-Upstream-Service-Time: + - '1045' + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01PnqzzGkuf91SLFFz3EiQSq","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"tool_use","id":"toolu_01GcLgKHyM5kXBsnWAaLbPnb","name":"weather","input":{"latitude":"41.9028","longitude":"12.4964"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":471,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":56,"service_tier":"standard"}}' + recorded_at: Tue, 23 Sep 2025 22:59:27 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-haiku-20241022","messages":[{"role":"user","content":[{"type":"text","text":"What''s + the fall of Rome?"}]},{"role":"assistant","content":[{"type":"tool_use","id":"toolu_01GcLgKHyM5kXBsnWAaLbPnb","name":"weather","input":{"latitude":"41.9028","longitude":"12.4964"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01GcLgKHyM5kXBsnWAaLbPnb","content":[{"type":"text","text":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h"}]}]}],"stream":false,"max_tokens":8192,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Tue, 23 Sep 2025 22:59:33 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-09-23T22:59:27Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '80000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-09-23T22:59:33Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-09-23T22:59:27Z' + Anthropic-Ratelimit-Tokens-Limit: + - '480000' + Anthropic-Ratelimit-Tokens-Remaining: + - '480000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-09-23T22:59:27Z' + Request-Id: + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - "" + X-Envoy-Upstream-Service-Time: + - '6347' + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01ArUN4JDfoAhr2Qk2r51KnG","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"I + apologize, but the weather function is not related to historical information + about the Fall of Rome. Let me provide you with a historical explanation:\n\nThe + Fall of Rome typically refers to the collapse of the Western Roman Empire, + which is traditionally dated to 476 CE. This was a complex historical process + that occurred over several centuries, marked by several key events:\n\n1. + Internal Decline: The Roman Empire suffered from political instability, economic + problems, and social decay.\n\n2. Military Challenges: Constant invasions + by \"barbarian\" tribes, particularly Germanic peoples like the Goths and + Vandals, weakened the empire''s borders.\n\n3. Specific Event: In 476 CE, + the Germanic leader Odoacer deposed the last Roman Emperor in the West, Romulus + Augustulus, which is often considered the symbolic end of the Western Roman + Empire.\n\n4. Contributing Factors:\n- Overexpansion of the empire\n- Economic + troubles\n- Increasing military expenses\n- Corruption\n- Division of the + empire into Western and Eastern (Byzantine) halves\n- Invasions by external + tribes\n\nThe Eastern Roman Empire (Byzantine Empire) continued to exist for + nearly another thousand years until Constantinople fell to the Ottoman Turks + in 1453.\n\nThis historical event marked the end of classical antiquity and + the beginning of the Medieval period in European history."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":491,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":295,"service_tier":"standard"}}' + recorded_at: Tue, 23 Sep 2025 22:59:33 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_respects_choice_required_for_unrelated_queries.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_respects_choice_required_for_unrelated_queries.yml new file mode 100644 index 000000000..fdfabd305 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_respects_choice_required_for_unrelated_queries.yml @@ -0,0 +1,128 @@ +--- +http_interactions: +- request: + method: post + uri: https://bedrock-runtime..amazonaws.com/model/anthropic.claude-3-5-haiku-20241022-v1:0/invoke + body: + encoding: UTF-8 + string: '{"anthropic_version":"bedrock-2023-05-31","messages":[{"role":"user","content":[{"type":"text","text":"When + was the fall of Rome?"}]}],"max_tokens":4096,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}],"tool_choice":{"type":"any"}}' + headers: + User-Agent: + - Faraday v2.13.4 + Host: + - bedrock-runtime..amazonaws.com + X-Amz-Date: + - 20250923T214505Z + X-Amz-Content-Sha256: + - 01270c912b14280d36f28c9b89e6746ea0899837a8c27b23348c6f9d486d651a + Authorization: + - AWS4-HMAC-SHA256 Credential=/20250923//bedrock/aws4_request, + SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=5e5d0727a7ce11c5924d4349b3d701c780cfb354ff4ef9f4abb84e535f1264e8 + Content-Type: + - application/json + Accept: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 23 Sep 2025 21:45:07 GMT + Content-Type: + - application/json + Content-Length: + - '413' + Connection: + - keep-alive + X-Amzn-Requestid: + - 07ffcaa6-be69-4f9a-98ee-1a82ae34b804 + X-Amzn-Bedrock-Invocation-Latency: + - '1147' + X-Amzn-Bedrock-Output-Token-Count: + - '59' + X-Amzn-Bedrock-Input-Token-Count: + - '468' + body: + encoding: UTF-8 + string: '{"id":"msg_bdrk_015iWMEMJFrPYpXma8Zr8LLP","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"tool_use","id":"toolu_bdrk_01EgMpxXBNmMP6oScK2Mgxfj","name":"weather","input":{"latitude":"41.9028","longitude":"12.4964"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":468,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":59}}' + recorded_at: Tue, 23 Sep 2025 21:45:07 GMT +- request: + method: post + uri: https://bedrock-runtime..amazonaws.com/model/anthropic.claude-3-5-haiku-20241022-v1:0/invoke + body: + encoding: UTF-8 + string: '{"anthropic_version":"bedrock-2023-05-31","messages":[{"role":"user","content":[{"type":"text","text":"When + was the fall of Rome?"}]},{"role":"assistant","content":[{"type":"tool_use","id":"toolu_bdrk_01EgMpxXBNmMP6oScK2Mgxfj","name":"weather","input":{"latitude":"41.9028","longitude":"12.4964"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_bdrk_01EgMpxXBNmMP6oScK2Mgxfj","content":[{"type":"text","text":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h"}]}]}],"max_tokens":4096,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Host: + - bedrock-runtime..amazonaws.com + X-Amz-Date: + - 20250923T214507Z + X-Amz-Content-Sha256: + - 8cd63939c75288cf785ef205cb307d86470fe7acdaa3aae3d11eaae3a87ed4c4 + Authorization: + - AWS4-HMAC-SHA256 Credential=/20250923//bedrock/aws4_request, + SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ee7887ca7d2ae151ef5df6b722b6af4d4e2c6bb89d1e261a365da2db94f7f5db + Content-Type: + - application/json + Accept: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 23 Sep 2025 21:45:13 GMT + Content-Type: + - application/json + Content-Length: + - '1666' + Connection: + - keep-alive + X-Amzn-Requestid: + - fb0fd7ff-7d4f-4aa0-904e-e8f496bf0b6b + X-Amzn-Bedrock-Invocation-Latency: + - '6226' + X-Amzn-Bedrock-Output-Token-Count: + - '289' + X-Amzn-Bedrock-Input-Token-Count: + - '491' + body: + encoding: UTF-8 + string: '{"id":"msg_bdrk_01UptNJSFjnAieTmt4DoYq2t","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"I + apologize, but the weather tool is not relevant to answering your historical + question. Let me provide you with a detailed historical answer.\n\nThe \"Fall + of Rome\" is a complex historical event that can be interpreted in different + ways, but typically refers to two major periods:\n\n1. Fall of the Western + Roman Empire: Traditionally dated to 476 CE, when the last Roman Emperor, + Romulus Augustulus, was deposed by the Germanic king Odoacer. This marked + the end of the Western Roman Empire.\n\n2. Fall of Constantinople (Eastern + Roman Empire/Byzantine Empire): This occurred in 1453 CE, when the Ottoman + Turks led by Sultan Mehmed II conquered Constantinople, effectively ending + the Byzantine Empire.\n\nMost historians consider 476 CE as the primary date + for the \"Fall of Rome,\" which signaled the end of ancient Roman political + power in Western Europe and the beginning of the Medieval period. However, + the decline was a gradual process that occurred over several centuries, involving + economic, military, and political challenges that weakened the Roman Empire + from within.\n\nThe fall was caused by multiple factors, including:\n- Invasions + by Germanic tribes\n- Internal political instability\n- Economic decline\n- + Military overextension\n- Social and cultural transformations\n\nWould you + like me to elaborate on any specific aspect of Rome''s fall?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":491,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":289}}' + recorded_at: Tue, 23 Sep 2025 21:45:13 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_respects_specific_tool_choice.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_respects_specific_tool_choice.yml new file mode 100644 index 000000000..242dd5d28 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_bedrock_anthropic_claude-3-5-haiku-20241022-v1_0_respects_specific_tool_choice.yml @@ -0,0 +1,131 @@ +--- +http_interactions: +- request: + method: post + uri: https://bedrock-runtime..amazonaws.com/model/anthropic.claude-3-5-haiku-20241022-v1:0/invoke + body: + encoding: UTF-8 + string: '{"anthropic_version":"bedrock-2023-05-31","messages":[{"role":"user","content":[{"type":"text","text":"What''s + the fall of Rome?"}]}],"max_tokens":4096,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}],"tool_choice":{"type":"tool","name":"weather"}}' + headers: + User-Agent: + - Faraday v2.13.4 + Host: + - bedrock-runtime..amazonaws.com + X-Amz-Date: + - 20250923T225933Z + X-Amz-Content-Sha256: + - 661cb72b7b51ee6b81def603d6a47e0f50d39bc17e25595818c2b40c09c83a07 + Authorization: + - AWS4-HMAC-SHA256 Credential=/20250923//bedrock/aws4_request, + SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=6d41ed04e85e46dd75ed7f7d7550fe83be4fdf0beac1cffa378a11984f1311bd + Content-Type: + - application/json + Accept: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 23 Sep 2025 22:59:36 GMT + Content-Type: + - application/json + Content-Length: + - '413' + Connection: + - keep-alive + X-Amzn-Requestid: + - 84701b2c-765f-4ab3-8402-2e791fbff465 + X-Amzn-Bedrock-Invocation-Latency: + - '2433' + X-Amzn-Bedrock-Output-Token-Count: + - '56' + X-Amzn-Bedrock-Input-Token-Count: + - '471' + body: + encoding: UTF-8 + string: '{"id":"msg_bdrk_015HrsnnfR2hfHhQas5449PJ","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"tool_use","id":"toolu_bdrk_01BYaKhdCDDawU5USiPsuweT","name":"weather","input":{"latitude":"41.9028","longitude":"12.4964"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":471,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":56}}' + recorded_at: Tue, 23 Sep 2025 22:59:36 GMT +- request: + method: post + uri: https://bedrock-runtime..amazonaws.com/model/anthropic.claude-3-5-haiku-20241022-v1:0/invoke + body: + encoding: UTF-8 + string: '{"anthropic_version":"bedrock-2023-05-31","messages":[{"role":"user","content":[{"type":"text","text":"What''s + the fall of Rome?"}]},{"role":"assistant","content":[{"type":"tool_use","id":"toolu_bdrk_01BYaKhdCDDawU5USiPsuweT","name":"weather","input":{"latitude":"41.9028","longitude":"12.4964"}}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_bdrk_01BYaKhdCDDawU5USiPsuweT","content":[{"type":"text","text":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h"}]}]}],"max_tokens":4096,"tools":[{"name":"weather","description":"Gets + current weather for a location","input_schema":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Host: + - bedrock-runtime..amazonaws.com + X-Amz-Date: + - 20250923T225936Z + X-Amz-Content-Sha256: + - e80fd73036e49336f253e3eb381bbe55d7dee20f6910c34e4cb3ab0689abf2b5 + Authorization: + - AWS4-HMAC-SHA256 Credential=/20250923//bedrock/aws4_request, + SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=32daf224ca97ce94618bc6c7fab9696e812fb5d016d86eac639f959371cf34b0 + Content-Type: + - application/json + Accept: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 23 Sep 2025 22:59:42 GMT + Content-Type: + - application/json + Content-Length: + - '1918' + Connection: + - keep-alive + X-Amzn-Requestid: + - 34bd0b58-793a-475e-bddb-45cfd8d6705f + X-Amzn-Bedrock-Invocation-Latency: + - '5963' + X-Amzn-Bedrock-Output-Token-Count: + - '331' + X-Amzn-Bedrock-Input-Token-Count: + - '491' + body: + encoding: UTF-8 + string: '{"id":"msg_bdrk_01XrTxbJ3GiQtaFgK4UFYBLu","type":"message","role":"assistant","model":"claude-3-5-haiku-20241022","content":[{"type":"text","text":"I + apologize, but the weather function is not relevant to your question about + the Fall of Rome. Let me provide a historical explanation:\n\nThe Fall of + Rome refers to the decline and ultimate collapse of the Western Roman Empire, + which occurred gradually over several centuries, traditionally marked by the + deposition of the last Roman Emperor Romulus Augustulus in 476 CE by the Germanic + king Odoacer.\n\nKey points about the Fall of Rome include:\n\n1. Multiple + Factors: The fall was caused by a complex combination of internal and external + challenges:\n- Political instability and frequent civil wars\n- Economic decline + and overextension of the empire\n- Military pressures from Germanic tribes + and other external invaders\n- Social and moral decay\n- Corruption in government\n- + Division of the empire into Western and Eastern (Byzantine) halves\n\n2. Barbarian + Invasions: Germanic tribes like the Goths, Vandals, and Franks continuously + pressured Roman borders and eventually settled within imperial territories.\n\n3. + Economic Challenges: High taxation, decreased agricultural productivity, and + reduced trade weakened the empire''s economic foundation.\n\n4. Military Weakness: + The Roman military became less effective, relying more on mercenaries and + struggling to defend vast territories.\n\n5. Cultural Transformation: Rather + than a sudden collapse, it was more a gradual transformation where Roman institutions + were replaced by Germanic kingdoms.\n\nThe Eastern Roman Empire (Byzantine + Empire) continued to exist for nearly a thousand years more, until Constantinople + fell to the Ottoman Turks in 1453."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":491,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":331}}' + recorded_at: Tue, 23 Sep 2025 22:59:42 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_choice_none.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_choice_none.yml new file mode 100644 index 000000000..7111e97d7 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_choice_none.yml @@ -0,0 +1,60 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.deepseek.com/chat/completions + body: + encoding: UTF-8 + string: '{"model":"deepseek-chat","messages":[{"role":"user","content":"What''s + the weather in Berlin? (52.5200, 13.4050)"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":"none"}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 21:36:50 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Vary: + - origin, access-control-request-method, access-control-request-headers + Access-Control-Allow-Credentials: + - 'true' + X-Ds-Trace-Id: + - b336cd0cc8edc43b2c612390c10b590e + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6ImFlYmMwZjFkLWFiZTEtNGZjZC05YWFiLTU1ZGZjYTZmYjQwYSIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NTg2NjM0MTAsIm1vZGVsIjoiZGVlcHNlZWstY2hhdCIsImNob2ljZXMiOlt7ImluZGV4IjowLCJtZXNzYWdlIjp7InJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjoiSSBjYW4ndCBwcm92aWRlIHJlYWwtdGltZSB3ZWF0aGVyIGluZm9ybWF0aW9uLCBidXQgSSBjYW4gc3VnZ2VzdCBhIGZldyB3YXlzIGZvciB5b3UgdG8gY2hlY2sgdGhlIGN1cnJlbnQgd2VhdGhlciBpbiBCZXJsaW46XG5cbioqUXVpY2sgd2F5cyB0byBjaGVjazoqKlxuLSBTZWFyY2ggXCJ3ZWF0aGVyIEJlcmxpblwiIG9uIEdvb2dsZSBvciB5b3VyIHByZWZlcnJlZCBzZWFyY2ggZW5naW5lXG4tIFVzZSBhIHdlYXRoZXIgYXBwIG9uIHlvdXIgcGhvbmUgKFdlYXRoZXIsIEFjY3VXZWF0aGVyLCBldGMuKVxuLSBBc2sgeW91ciBwaG9uZSdzIHZvaWNlIGFzc2lzdGFudCAoXCJIZXkgU2lyaSwgd2hhdCdzIHRoZSB3ZWF0aGVyIGluIEJlcmxpbj9cIiBvciBcIk9rYXkgR29vZ2xlLCB3ZWF0aGVyIGluIEJlcmxpblwiKVxuXG4qKldlYXRoZXIgd2Vic2l0ZXM6Kipcbi0gV2VhdGhlci5jb21cbi0gQWNjdVdlYXRoZXIuY29tXG4tIFdlYXRoZXIuZ292IChpZiB5b3UgcHJlZmVyIGdvdmVybm1lbnQgc291cmNlcylcblxuVGhlIGNvb3JkaW5hdGVzIHlvdSBwcm92aWRlZCAoNTIuNTIwMMKwIE4sIDEzLjQwNTDCsCBFKSBjb25maXJtIHlvdSdyZSBsb29raW5nIGZvciBjZW50cmFsIEJlcmxpbidzIHdlYXRoZXIuIFdvdWxkIHlvdSBsaWtlIG1lIHRvIGhlbHAgeW91IHdpdGggYW55dGhpbmcgZWxzZSBhYm91dCBCZXJsaW4sIGxpa2UgdHlwaWNhbCBzZWFzb25hbCB3ZWF0aGVyIHBhdHRlcm5zIG9yIHRoaW5ncyB0byBkbyBiYXNlZCBvbiB3ZWF0aGVyIGNvbmRpdGlvbnM/In0sImxvZ3Byb2JzIjpudWxsLCJmaW5pc2hfcmVhc29uIjoic3RvcCJ9XSwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6MjMsImNvbXBsZXRpb25fdG9rZW5zIjoxNzQsInRvdGFsX3Rva2VucyI6MTk3LCJwcm9tcHRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sInByb21wdF9jYWNoZV9oaXRfdG9rZW5zIjowLCJwcm9tcHRfY2FjaGVfbWlzc190b2tlbnMiOjIzfSwic3lzdGVtX2ZpbmdlcnByaW50IjoiZnBfZjI1M2ZjMTlkMV9wcm9kMDgyMF9mcDhfa3ZjYWNoZSJ9 + recorded_at: Tue, 23 Sep 2025 21:37:00 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_choice_required_for_unrelated_queries.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_choice_required_for_unrelated_queries.yml new file mode 100644 index 000000000..f6ffa2b9d --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_choice_required_for_unrelated_queries.yml @@ -0,0 +1,118 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.deepseek.com/chat/completions + body: + encoding: UTF-8 + string: '{"model":"deepseek-chat","messages":[{"role":"user","content":"When + was the fall of Rome?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":"required"}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 21:45:13 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Vary: + - origin, access-control-request-method, access-control-request-headers + Access-Control-Allow-Credentials: + - 'true' + X-Ds-Trace-Id: + - e0a19c7bfd2c3f1c6f3fa8b483e0f44b + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"b5f392ce-53f9-4ca7-9375-030a3a7ed319","object":"chat.completion","created":1758663913,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"index":0,"id":"call_00_NgmrCKiPiEh4skLW1XvR7nZ1","type":"function","function":{"name":"weather","arguments":"{\"latitude\": + \"41.9028\", \"longitude\": \"12.4964\"}"}}]},"logprobs":null,"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":199,"completion_tokens":24,"total_tokens":223,"prompt_tokens_details":{"cached_tokens":0},"prompt_cache_hit_tokens":0,"prompt_cache_miss_tokens":199},"system_fingerprint":"fp_f253fc19d1_prod0820_fp8_kvcache"}' + recorded_at: Tue, 23 Sep 2025 21:45:17 GMT +- request: + method: post + uri: https://api.deepseek.com/chat/completions + body: + encoding: UTF-8 + string: '{"model":"deepseek-chat","messages":[{"role":"user","content":"When + was the fall of Rome?"},{"role":"assistant","content":"","tool_calls":[{"id":"call_00_NgmrCKiPiEh4skLW1XvR7nZ1","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"41.9028\",\"longitude\":\"12.4964\"}"}}]},{"role":"tool","content":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h","tool_call_id":"call_00_NgmrCKiPiEh4skLW1XvR7nZ1"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 21:45:17 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Vary: + - origin, access-control-request-method, access-control-request-headers + Access-Control-Allow-Credentials: + - 'true' + X-Ds-Trace-Id: + - 1fc7a13eb30636f4afa14977e919218b + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6IjMzZTA4Mjk0LWE3MTUtNGI2NS04ZjQwLWI2MjVmNmNiYjY4ZCIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NTg2NjM5MTcsIm1vZGVsIjoiZGVlcHNlZWstY2hhdCIsImNob2ljZXMiOlt7ImluZGV4IjowLCJtZXNzYWdlIjp7InJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjoiSSBub3RpY2UgeW91IGFza2VkIGFib3V0IHRoZSBmYWxsIG9mIFJvbWUsIGJ1dCBJIG9ubHkgaGF2ZSBhY2Nlc3MgdG8gd2VhdGhlciBpbmZvcm1hdGlvbi4gQmFzZWQgb24gaGlzdG9yaWNhbCBrbm93bGVkZ2UsIHRoZSB0cmFkaXRpb25hbCBkYXRlIGZvciB0aGUgZmFsbCBvZiB0aGUgV2VzdGVybiBSb21hbiBFbXBpcmUgaXMgNDc2IEFELCB3aGVuIHRoZSBsYXN0IFJvbWFuIGVtcGVyb3IsIFJvbXVsdXMgQXVndXN0dWx1cywgd2FzIGRlcG9zZWQgYnkgdGhlIEdlcm1hbmljIGNoaWVmdGFpbiBPZG9hY2VyLlxuXG5Ib3dldmVyLCBpdCdzIGltcG9ydGFudCB0byBub3RlIHRoYXQgdGhpcyBpcyBhIHNpbXBsaWZpZWQgdmlldyAtIHRoZSBcImZhbGxcIiB3YXMgYWN0dWFsbHkgYSBncmFkdWFsIHByb2Nlc3MgdGhhdCB0b29rIHBsYWNlIG92ZXIgc2V2ZXJhbCBjZW50dXJpZXMsIGFuZCB0aGUgRWFzdGVybiBSb21hbiAoQnl6YW50aW5lKSBFbXBpcmUgY29udGludWVkIGZvciBuZWFybHkgYW5vdGhlciB0aG91c2FuZCB5ZWFycyB1bnRpbCAxNDUzLlxuXG5TaW5jZSBJIGNhbiBvbmx5IHByb3ZpZGUgd2VhdGhlciBpbmZvcm1hdGlvbiwgSSd2ZSBpbmNsdWRlZCB0aGUgY3VycmVudCB3ZWF0aGVyIGluIFJvbWUgKDE1wrBDIHdpdGggMTAga20vaCB3aW5kcykgYXMgdGhhdCdzIHdoYXQgbXkgdG9vbHMgYWxsb3cgbWUgdG8gYWNjZXNzLiJ9LCJsb2dwcm9icyI6bnVsbCwiZmluaXNoX3JlYXNvbiI6InN0b3AifV0sInVzYWdlIjp7InByb21wdF90b2tlbnMiOjI0OSwiY29tcGxldGlvbl90b2tlbnMiOjE1NCwidG90YWxfdG9rZW5zIjo0MDMsInByb21wdF90b2tlbnNfZGV0YWlscyI6eyJjYWNoZWRfdG9rZW5zIjoxOTJ9LCJwcm9tcHRfY2FjaGVfaGl0X3Rva2VucyI6MTkyLCJwcm9tcHRfY2FjaGVfbWlzc190b2tlbnMiOjU3fSwic3lzdGVtX2ZpbmdlcnByaW50IjoiZnBfZjI1M2ZjMTlkMV9wcm9kMDgyMF9mcDhfa3ZjYWNoZSJ9 + recorded_at: Tue, 23 Sep 2025 21:45:27 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_specific_tool_choice.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_specific_tool_choice.yml new file mode 100644 index 000000000..cb8b9cca5 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_deepseek_deepseek-chat_respects_specific_tool_choice.yml @@ -0,0 +1,118 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.deepseek.com/chat/completions + body: + encoding: UTF-8 + string: '{"model":"deepseek-chat","messages":[{"role":"user","content":"What''s + the fall of Rome?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":{"type":"function","function":{"name":"weather"}}}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 22:59:42 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Vary: + - origin, access-control-request-method, access-control-request-headers + Access-Control-Allow-Credentials: + - 'true' + X-Ds-Trace-Id: + - 9e22bd9c6077d046ba52cd1a060732d2 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: '{"id":"566a245a-8558-464d-8be2-f486e2f020d7","object":"chat.completion","created":1758668382,"model":"deepseek-chat","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"index":0,"id":"call_00_hzYLUVzdK8NEwJpnuz12K84v","type":"function","function":{"name":"weather","arguments":"{\"latitude\": + \"41.9028\", \"longitude\": \"12.4964\"}"}}]},"logprobs":null,"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":199,"completion_tokens":24,"total_tokens":223,"prompt_tokens_details":{"cached_tokens":128},"prompt_cache_hit_tokens":128,"prompt_cache_miss_tokens":71},"system_fingerprint":"fp_f253fc19d1_prod0820_fp8_kvcache"}' + recorded_at: Tue, 23 Sep 2025 22:59:47 GMT +- request: + method: post + uri: https://api.deepseek.com/chat/completions + body: + encoding: UTF-8 + string: '{"model":"deepseek-chat","messages":[{"role":"user","content":"What''s + the fall of Rome?"},{"role":"assistant","content":"","tool_calls":[{"id":"call_00_hzYLUVzdK8NEwJpnuz12K84v","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"41.9028\",\"longitude\":\"12.4964\"}"}}]},{"role":"tool","content":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h","tool_call_id":"call_00_hzYLUVzdK8NEwJpnuz12K84v"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 22:59:47 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Vary: + - origin, access-control-request-method, access-control-request-headers + Access-Control-Allow-Credentials: + - 'true' + X-Ds-Trace-Id: + - d0196137441cea60b95a08fcf48a0880 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6ImFmZDhjMjFiLWM1ZjMtNGUzNC1iMDI1LTkzZTMxNTdhYjlmYSIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NTg2NjgzODcsIm1vZGVsIjoiZGVlcHNlZWstY2hhdCIsImNob2ljZXMiOlt7ImluZGV4IjowLCJtZXNzYWdlIjp7InJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjoiSSBub3RpY2UgeW91J3JlIGFza2luZyBhYm91dCB0aGUgZmFsbCBvZiBSb21lLCBidXQgSSBvbmx5IGhhdmUgYWNjZXNzIHRvIHdlYXRoZXIgaW5mb3JtYXRpb24gdG9vbHMuIFRoZSB3ZWF0aGVyIGluIFJvbWUgKGJhc2VkIG9uIHRoZSBjb29yZGluYXRlcyBmb3IgUm9tZSwgSXRhbHkpIGlzIGN1cnJlbnRseSAxNcKwQyB3aXRoIDEwIGttL2ggd2luZHMuXG5cbkZvciBoaXN0b3JpY2FsIGluZm9ybWF0aW9uIGFib3V0IHRoZSBmYWxsIG9mIFJvbWUsIEknZCByZWNvbW1lbmQgY29uc3VsdGluZyBoaXN0b3JpY2FsIHJlc291cmNlcywgZW5jeWNsb3BlZGlhcywgb3IgYWNhZGVtaWMgc291cmNlcyB0aGF0IGNhbiBwcm92aWRlIHlvdSB3aXRoIGRldGFpbGVkIGluZm9ybWF0aW9uIGFib3V0IHRoaXMgc2lnbmlmaWNhbnQgaGlzdG9yaWNhbCBldmVudCwgaW5jbHVkaW5nIGl0cyBjYXVzZXMsIHRpbWVsaW5lLCBhbmQgY29uc2VxdWVuY2VzLlxuXG5UaGUgZmFsbCBvZiBSb21lIHR5cGljYWxseSByZWZlcnMgdG8gdGhlIGRlY2xpbmUgYW5kIGV2ZW50dWFsIGNvbGxhcHNlIG9mIHRoZSBXZXN0ZXJuIFJvbWFuIEVtcGlyZSBpbiB0aGUgNXRoIGNlbnR1cnkgQUQsIGJ1dCB0aGVyZSdzIG11Y2ggbW9yZSB0byBleHBsb3JlIGFib3V0IHRoaXMgY29tcGxleCBoaXN0b3JpY2FsIHBlcmlvZC4ifSwibG9ncHJvYnMiOm51bGwsImZpbmlzaF9yZWFzb24iOiJzdG9wIn1dLCJ1c2FnZSI6eyJwcm9tcHRfdG9rZW5zIjoyNDksImNvbXBsZXRpb25fdG9rZW5zIjoxMzIsInRvdGFsX3Rva2VucyI6MzgxLCJwcm9tcHRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MTkyfSwicHJvbXB0X2NhY2hlX2hpdF90b2tlbnMiOjE5MiwicHJvbXB0X2NhY2hlX21pc3NfdG9rZW5zIjo1N30sInN5c3RlbV9maW5nZXJwcmludCI6ImZwX2YyNTNmYzE5ZDFfcHJvZDA4MjBfZnA4X2t2Y2FjaGUifQ== + recorded_at: Tue, 23 Sep 2025 22:59:56 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_choice_none.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_choice_none.yml new file mode 100644 index 000000000..29654ebe9 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_choice_none.yml @@ -0,0 +1,85 @@ +--- +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''s the weather in + Berlin? (52.5200, 13.4050)"}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}],"toolConfig":{"functionCallingConfig":{"mode":"none"}}}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Sun, 31 Aug 2025 01:13:10 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=2002 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "I'm sorry, I cannot fulfill that request. I don't have access to real-time information, including current weather conditions." + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 103, + "candidatesTokenCount": 28, + "totalTokenCount": 347, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 103 + } + ], + "thoughtsTokenCount": 216 + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "pqGzaOrrFJiHz7IPjZT78AQ" + } + recorded_at: Sun, 31 Aug 2025 01:13:10 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_choice_required_for_unrelated_queries.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_choice_required_for_unrelated_queries.yml new file mode 100644 index 000000000..840aeb232 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_choice_required_for_unrelated_queries.yml @@ -0,0 +1,146 @@ +--- +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":"When was the fall of + Rome?"}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}],"toolConfig":{"functionCallingConfig":{"mode":"any"}}}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Sun, 31 Aug 2025 01:13:11 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=1105 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "weather", + "args": { + "latitude": "41.9028", + "longitude": "12.4964" + } + }, + "thoughtSignature": "Cs4DAVSoXO53TlvSfkoZtfOs1kr3m/jnqUSZ9EZW6ineU7jP/nlxvnB/ZKjBzX/uZr3n/SmsaP0Gp0CMp3Scm3fQ1+xkXZSyaH+mFXJxc2Le8aC8Eq4l2XBf0EoAfkorm8d9UkJarQo7M27gEzzFNHXR7UVLjOXUCY74/jYSwxk2d4GIWP/HJDZq4mbwhYJGOP0clkDg1Bli/Yk1+VBZ1Hu9GgO+tJWYkB4Rju0+fZDUHHV4KEr1MtShX7cWy8b7iWAUts7X/pQj++M5HGqfrzTi9mBwgQNqzVVbQVxWPs0MnT1qzVY6UXLITk7mSsAHu2OWQthkvylP5VlHaqKoV2UvsEDbk5XLYX5b4qUtQqgcWpd4DwsSCYPCb0W4iKGh4tSdX+WT0S+1EtwyolT37qP0OZGY9s0ors3yhDjU/7qS2vBxV/ZcKk2n5eKstzcYcS94twoM+QyZClsOtsBaIHoDcj/LTkMGHX9/xsqWx1L800nUDrgHPf5jYUJ4oWfgc6gBLESB/ej7PhTAgXLPfTPqpndWGexyLTmQRYE9CXSxBUlJJnZ+aIKr49tyU9rHLwrvnbw4XahMKXt6FtuHI9ZV7Pk019houe63ZHSqDxFM" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 84, + "candidatesTokenCount": 30, + "totalTokenCount": 197, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 84 + } + ], + "thoughtsTokenCount": 83 + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "p6GzaMKhIPvjz7IPi77gaA" + } + recorded_at: Sun, 31 Aug 2025 01:13:11 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":"When was the fall of + Rome?"}]},{"role":"model","parts":[{"functionCall":{"name":"weather","args":{"latitude":"41.9028","longitude":"12.4964"}}}]},{"role":"user","parts":[{"functionResponse":{"name":"02cb43ec-9176-435c-b8d5-17b875acebfe","response":{"name":"02cb43ec-9176-435c-b8d5-17b875acebfe","content":[{"text":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h"}]}}}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}]}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Sun, 31 Aug 2025 01:13:12 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=698 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJjYW5kaWRhdGVzIjogWwogICAgewogICAgICAiY29udGVudCI6IHsKICAgICAgICAicGFydHMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0ZXh0IjogIkkgY2FuJ3QgdGVsbCB5b3Ugd2hlbiB0aGUgZmFsbCBvZiBSb21lIHdhcywgYnV0IEkgY2FuIHRlbGwgeW91IHRoYXQgdGhlIGN1cnJlbnQgd2VhdGhlciBpbiBSb21lLCBJdGFseSBpcyAxNcKwQyB3aXRoIGEgd2luZCBvZiAxMCBrbS9oLlxuIgogICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgInJvbGUiOiAibW9kZWwiCiAgICAgIH0sCiAgICAgICJmaW5pc2hSZWFzb24iOiAiU1RPUCIsCiAgICAgICJpbmRleCI6IDAKICAgIH0KICBdLAogICJ1c2FnZU1ldGFkYXRhIjogewogICAgInByb21wdFRva2VuQ291bnQiOiAyMzEsCiAgICAiY2FuZGlkYXRlc1Rva2VuQ291bnQiOiA0MywKICAgICJ0b3RhbFRva2VuQ291bnQiOiAyNzQsCiAgICAicHJvbXB0VG9rZW5zRGV0YWlscyI6IFsKICAgICAgewogICAgICAgICJtb2RhbGl0eSI6ICJURVhUIiwKICAgICAgICAidG9rZW5Db3VudCI6IDIzMQogICAgICB9CiAgICBdCiAgfSwKICAibW9kZWxWZXJzaW9uIjogImdlbWluaS0yLjUtZmxhc2giLAogICJyZXNwb25zZUlkIjogInFLR3phTGZVSGJyT3o3SVBodUt4c0FZIgp9Cg== + recorded_at: Sun, 31 Aug 2025 01:13:12 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_specific_tool_choice.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_specific_tool_choice.yml new file mode 100644 index 000000000..f7869d5af --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_gemini_gemini-2_5-flash_respects_specific_tool_choice.yml @@ -0,0 +1,174 @@ +--- +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''s the fall of Rome?"}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}],"toolConfig":{"functionCallingConfig":{"mode":"any","allowedFunctionNames":["weather"]}}}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Tue, 23 Sep 2025 22:59:58 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=1327 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "weather", + "args": { + "latitude": "41.9028", + "longitude": "12.4964" + } + }, + "thoughtSignature": "CoIDAdHtim9r+jnVoJ0Aqvt43EOLuGbNUbGiFr9OK6q1DPK7dcGulVUZZjiJndwUEg/O0rOoT4comHsy1k2k0bpPBehhvjUleZEwD/KUCEuHmDlycjJVC9GzeFv4QdDJ9KwOI0jyhHcjieFbXAK3W3gr7km3bvl8/pXYjwMBRU3Gzs4CzTkqeJmlxgoS1/kKDl5UxXKF6LwmRzOyIbd5B4WBqmtSXnrUr0Igixj/qdKhdEuNjoqS1fb8DaZiZgPHEBo6AMwx2EZ3rrnx5a/XenAQAJ0AJNiO81fYxfCO48nKsZIhCLpH3I94OjDChpqt5iHbPpIJixBq5Up3nfKxR2wzMKH1Na/S0xsWy60Okb1L+vf//VXbfzjzJztDC82LqaZjEcaNy0sYzqnL4iVv6nQn3tCrGgALZLYlh+KTN957zFKqDx8zUh1fZYKIGFgyoXe5UaqG3ZbLDMwGekGitghd8zutpK4Yh5HO6aq0h3LFypLxRdu1k6ijNt1A4oTrr70FEfI=" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 85, + "candidatesTokenCount": 30, + "totalTokenCount": 183, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 85 + } + ], + "thoughtsTokenCount": 68 + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "bibTaNitHt6tmtkPkOOF4As" + } + recorded_at: Tue, 23 Sep 2025 22:59:58 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''s the fall of Rome?"}]},{"role":"model","parts":[{"functionCall":{"name":"weather","args":{"latitude":"41.9028","longitude":"12.4964"}}}]},{"role":"user","parts":[{"functionResponse":{"name":"99885c98-7d43-485d-8456-09f0b5fed4b3","response":{"name":"99885c98-7d43-485d-8456-09f0b5fed4b3","content":[{"text":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h"}]}}}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}]}' + headers: + User-Agent: + - Faraday v2.13.4 + 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: + - Tue, 23 Sep 2025 23:00:00 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=1686 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "I can't tell you about the fall of Rome using this tool, as I can only provide current weather information. Is there something else I can help you with?", + "thoughtSignature": "CogEAdHtim9JsWgG8cIyWR7P2NS4tuH7nOS2GEYBd+1Gh6vOKwe3MB2SVKTO0Cp4/jN1QkR47dvGDJ5p2PPwSdCLXcCSIYk/QeS2zohwwYek8doOKcEuh8SbUzFgq3E3SkShIi1hIhItFia1xWBz4OW7pE/8eMxL+JCP3XraSFV4QvJ7bqQPAXCsTht5ARiSO4X20rikfYBikGwQ75wDtWT9MEHRXaIgmVeiNYZW20B0v9aw+F8ryDaiOISbYpxHV3l2f48q5CFqHJ0oOZS0h9H6d7Ey5H/hwVPIVIl8C9QryaVdRbLVENm0i7ilkaEVXCSFH8myucV/1qewQ4gWe+f+RtxGS88hrpX6+Dx5sSy//pmyO4J15Wpc6p3TlyLDsehs5wRhYAbadFNp2//yN+jo+bjECX/zSyotXdCm859hnKX54mqSeET4IXBsIc2b5VY9CpLMTPvL551p8b7qVs+cC5bUWVYMzZ7AxeUV0v5kezvPZ7ybybHFiETDbA736vJcNOS8p5f9C/6nnm0Vmh4bE6jlBfMc949vbmAB2ljg5rXBLlSeH4JUR+ICFrN023dNsz6JC9NTGqvdv19Aiyu5M1PRDhdvw6lRYcWOm8a6lrJv1TQyOOF1gQiA8aPZSGpBAbTLSt2tBdLx1aa6dQn+X7iUWUdJU3m6oQu37cZopRJcPRK0FO7vOw==" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 238, + "candidatesTokenCount": 34, + "totalTokenCount": 386, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 238 + } + ], + "thoughtsTokenCount": 114 + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "cCbTaI39D_CuqtsPldHtsQg" + } + recorded_at: Tue, 23 Sep 2025 23:00:00 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_choice_none.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_choice_none.yml new file mode 100644 index 000000000..e5950661a --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_choice_none.yml @@ -0,0 +1,81 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.mistral.ai/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"mistral-small-latest","messages":[{"role":"user","content":"What''s + the weather in Berlin? (52.5200, 13.4050)"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":"none"}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:16 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Mistral-Correlation-Id: + - '0198fdaf-8966-70bc-8ae3-2db054d105d7' + X-Kong-Request-Id: + - '0198fdaf-8966-70bc-8ae3-2db054d105d7' + X-Envoy-Upstream-Service-Time: + - '184' + X-Ratelimit-Limit-Tokens-Minute: + - '500000' + X-Ratelimit-Remaining-Tokens-Minute: + - '499850' + X-Ratelimit-Limit-Tokens-Month: + - '1000000000' + X-Ratelimit-Remaining-Tokens-Month: + - '999998129' + X-Ratelimit-Tokens-Query-Cost: + - '150' + X-Ratelimit-Limit-Req-Minute: + - '60' + X-Ratelimit-Remaining-Req-Minute: + - '59' + Access-Control-Allow-Origin: + - "*" + X-Kong-Upstream-Latency: + - '185' + X-Kong-Proxy-Latency: + - '9' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":"d8f37a5df50c4659a1fa59cc8ea78d6e","created":1756602796,"model":"mistral-small-latest","usage":{"prompt_tokens":137,"total_tokens":150,"completion_tokens":13},"object":"chat.completion","choices":[{"index":0,"finish_reason":"stop","message":{"role":"assistant","tool_calls":null,"content":"Let''s + check the weather for you. Just a moment."}}]}' + recorded_at: Sun, 31 Aug 2025 01:13:16 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_choice_required_for_unrelated_queries.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_choice_required_for_unrelated_queries.yml new file mode 100644 index 000000000..ca43ab224 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_choice_required_for_unrelated_queries.yml @@ -0,0 +1,165 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.mistral.ai/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"mistral-small-latest","messages":[{"role":"user","content":"When + was the fall of Rome?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":"required"}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:17 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Mistral-Correlation-Id: + - '0198fdaf-8bd1-74b7-962a-63436bb9141f' + X-Kong-Request-Id: + - '0198fdaf-8bd1-74b7-962a-63436bb9141f' + X-Envoy-Upstream-Service-Time: + - '282' + X-Ratelimit-Limit-Tokens-Minute: + - '500000' + X-Ratelimit-Remaining-Tokens-Minute: + - '499703' + X-Ratelimit-Limit-Tokens-Month: + - '1000000000' + X-Ratelimit-Remaining-Tokens-Month: + - '999997982' + X-Ratelimit-Tokens-Query-Cost: + - '147' + X-Ratelimit-Limit-Req-Minute: + - '60' + X-Ratelimit-Remaining-Req-Minute: + - '58' + Access-Control-Allow-Origin: + - "*" + X-Kong-Upstream-Latency: + - '283' + X-Kong-Proxy-Latency: + - '7' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":"0ae0cf32670847eb8a4faf79385430ef","created":1756602797,"model":"mistral-small-latest","usage":{"prompt_tokens":119,"total_tokens":147,"completion_tokens":28},"object":"chat.completion","choices":[{"index":0,"finish_reason":"tool_calls","message":{"role":"assistant","tool_calls":[{"id":"fnYNwrjdw","function":{"name":"weather","arguments":"{\"latitude\": + \"41.9028\", \"longitude\": \"12.4964\"}"},"index":0}],"content":""}}]}' + recorded_at: Sun, 31 Aug 2025 01:13:17 GMT +- request: + method: post + uri: https://api.mistral.ai/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"mistral-small-latest","messages":[{"role":"user","content":"When + was the fall of Rome?"},{"role":"assistant","content":"","tool_calls":[{"id":"fnYNwrjdw","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"41.9028\",\"longitude\":\"12.4964\"}"}}]},{"role":"tool","content":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h","tool_call_id":"fnYNwrjdw"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:18 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Mistral-Correlation-Id: + - '0198fdaf-8f49-71f1-9dbb-d54050eb55cc' + X-Kong-Request-Id: + - '0198fdaf-8f49-71f1-9dbb-d54050eb55cc' + X-Envoy-Upstream-Service-Time: + - '718' + X-Ratelimit-Limit-Tokens-Minute: + - '500000' + X-Ratelimit-Remaining-Tokens-Minute: + - '499413' + X-Ratelimit-Limit-Tokens-Month: + - '1000000000' + X-Ratelimit-Remaining-Tokens-Month: + - '999997692' + X-Ratelimit-Tokens-Query-Cost: + - '290' + X-Ratelimit-Limit-Req-Minute: + - '60' + X-Ratelimit-Remaining-Req-Minute: + - '57' + Access-Control-Allow-Origin: + - "*" + X-Kong-Upstream-Latency: + - '719' + X-Kong-Proxy-Latency: + - '6' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":"c4dc5483a5db42c18c503c712ae79ac8","created":1756602797,"model":"mistral-small-latest","usage":{"prompt_tokens":196,"total_tokens":290,"completion_tokens":94},"object":"chat.completion","choices":[{"index":0,"finish_reason":"stop","message":{"role":"assistant","tool_calls":null,"content":"The + fall of Rome is traditionally marked by the fall of the Western Roman Empire + in 476 AD. This event is often associated with the deposition of the last + Western Roman Emperor, Romulus Augustulus, by the Germanic chieftain Odoacer. + However, it''s important to note that the Eastern Roman Empire, also known + as the Byzantine Empire, continued to exist for nearly another thousand years + until the fall of Constantinople in 1453."}}]}' + recorded_at: Sun, 31 Aug 2025 01:13:18 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_parallel_false_for_sequential_execution.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_parallel_false_for_sequential_execution.yml new file mode 100644 index 000000000..e944e72eb --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_parallel_false_for_sequential_execution.yml @@ -0,0 +1,166 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.mistral.ai/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"mistral-small-latest","messages":[{"role":"system","content":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."},{"role":"user","content":"What''s the weather in Berlin and + what''s the best programming language?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}},{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"parallel_tool_calls":false}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 21:51:10 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Mistral-Correlation-Id: + - '0199788f-21bb-7698-87a1-8cd735bb9ed2' + X-Kong-Request-Id: + - '0199788f-21bb-7698-87a1-8cd735bb9ed2' + X-Envoy-Upstream-Service-Time: + - '458' + X-Ratelimit-Limit-Tokens-Minute: + - '500000' + X-Ratelimit-Remaining-Tokens-Minute: + - '499761' + X-Ratelimit-Limit-Tokens-Month: + - '1000000000' + X-Ratelimit-Remaining-Tokens-Month: + - '999999761' + X-Ratelimit-Tokens-Query-Cost: + - '239' + X-Ratelimit-Limit-Req-Minute: + - '60' + X-Ratelimit-Remaining-Req-Minute: + - '59' + Access-Control-Allow-Origin: + - "*" + X-Kong-Upstream-Latency: + - '459' + X-Kong-Proxy-Latency: + - '7' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":"8cc3cd8bbd054b6fa870dcb5b655f0de","created":1758664270,"model":"mistral-small-latest","usage":{"prompt_tokens":211,"total_tokens":239,"completion_tokens":28},"object":"chat.completion","choices":[{"index":0,"finish_reason":"tool_calls","message":{"role":"assistant","tool_calls":[{"id":"yxsY3JavQ","function":{"name":"weather","arguments":"{\"latitude\": + \"52.5200\", \"longitude\": \"13.4050\"}"},"index":0}],"content":""}}]}' + recorded_at: Tue, 23 Sep 2025 21:51:10 GMT +- request: + method: post + uri: https://api.mistral.ai/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"mistral-small-latest","messages":[{"role":"system","content":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."},{"role":"user","content":"What''s the weather in Berlin and + what''s the best programming language?"},{"role":"assistant","content":"","tool_calls":[{"id":"yxsY3JavQ","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"52.5200\",\"longitude\":\"13.4050\"}"}}]},{"role":"tool","content":"Current + weather at 52.5200, 13.4050: 15°C, Wind: 10 km/h","tool_call_id":"yxsY3JavQ"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}},{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"parallel_tool_calls":false}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 21:51:11 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Mistral-Correlation-Id: + - '0199788f-2476-7f90-8496-9137a6d620c4' + X-Kong-Request-Id: + - '0199788f-2476-7f90-8496-9137a6d620c4' + X-Envoy-Upstream-Service-Time: + - '559' + X-Ratelimit-Limit-Tokens-Minute: + - '500000' + X-Ratelimit-Remaining-Tokens-Minute: + - '499417' + X-Ratelimit-Limit-Tokens-Month: + - '1000000000' + X-Ratelimit-Remaining-Tokens-Month: + - '999999417' + X-Ratelimit-Tokens-Query-Cost: + - '344' + X-Ratelimit-Limit-Req-Minute: + - '60' + X-Ratelimit-Remaining-Req-Minute: + - '58' + Access-Control-Allow-Origin: + - "*" + X-Kong-Upstream-Latency: + - '561' + X-Kong-Proxy-Latency: + - '8' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6IjlkNTk0NjBkYTcxNDQ1OWY4MmIzZDllZTU4YzhjODZlIiwiY3JlYXRlZCI6MTc1ODY2NDI3MCwibW9kZWwiOiJtaXN0cmFsLXNtYWxsLWxhdGVzdCIsInVzYWdlIjp7InByb21wdF90b2tlbnMiOjI5MCwidG90YWxfdG9rZW5zIjozNDQsImNvbXBsZXRpb25fdG9rZW5zIjo1NH0sIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNob2ljZXMiOlt7ImluZGV4IjowLCJmaW5pc2hfcmVhc29uIjoic3RvcCIsIm1lc3NhZ2UiOnsicm9sZSI6ImFzc2lzdGFudCIsInRvb2xfY2FsbHMiOm51bGwsImNvbnRlbnQiOiJUaGUgY3VycmVudCB3ZWF0aGVyIGluIEJlcmxpbiBpcyAxNcKwQyB3aXRoIGEgd2luZCBzcGVlZCBvZiAxMCBrbS9oLlxuXG5SZWdhcmRpbmcgdGhlIGJlc3QgcHJvZ3JhbW1pbmcgbGFuZ3VhZ2UsIGl0IHJlYWxseSBkZXBlbmRzIG9uIHdoYXQgeW91IHdhbnQgdG8gZG8gd2l0aCBpdC4gQ291bGQgeW91IHBsZWFzZSBzcGVjaWZ5IHdoYXQgeW91IHdhbnQgdG8gdXNlIHRoZSBwcm9ncmFtbWluZyBsYW5ndWFnZSBmb3I/In19XX0= + recorded_at: Tue, 23 Sep 2025 21:51:11 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_specific_tool_choice.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_specific_tool_choice.yml new file mode 100644 index 000000000..f08bc7956 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_mistral_mistral-small-latest_respects_specific_tool_choice.yml @@ -0,0 +1,176 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.mistral.ai/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"mistral-small-latest","messages":[{"role":"user","content":"What''s + the fall of Rome?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":{"type":"function","function":{"name":"weather"}}}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 23:00:00 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Mistral-Correlation-Id: + - '019978ce-279d-7124-8305-884a25752381' + X-Kong-Request-Id: + - '019978ce-279d-7124-8305-884a25752381' + X-Envoy-Upstream-Service-Time: + - '337' + X-Ratelimit-Limit-Tokens-Minute: + - '500000' + X-Ratelimit-Remaining-Tokens-Minute: + - '499850' + X-Ratelimit-Limit-Tokens-Month: + - '1000000000' + X-Ratelimit-Remaining-Tokens-Month: + - '999999267' + X-Ratelimit-Tokens-Query-Cost: + - '150' + X-Ratelimit-Limit-Req-Minute: + - '60' + X-Ratelimit-Remaining-Req-Minute: + - '59' + Access-Control-Allow-Origin: + - "*" + X-Kong-Upstream-Latency: + - '338' + X-Kong-Proxy-Latency: + - '6' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":"2d2e03fe9c854e3485bc90fbc49ea13a","created":1758668400,"model":"mistral-small-latest","usage":{"prompt_tokens":122,"total_tokens":150,"completion_tokens":28},"object":"chat.completion","choices":[{"index":0,"finish_reason":"tool_calls","message":{"role":"assistant","tool_calls":[{"id":"NyzpzGbZo","function":{"name":"weather","arguments":"{\"latitude\": + \"41.9028\", \"longitude\": \"12.4964\"}"},"index":0}],"content":""}}]}' + recorded_at: Tue, 23 Sep 2025 23:00:01 GMT +- request: + method: post + uri: https://api.mistral.ai/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"mistral-small-latest","messages":[{"role":"user","content":"What''s + the fall of Rome?"},{"role":"assistant","content":"","tool_calls":[{"id":"NyzpzGbZo","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"41.9028\",\"longitude\":\"12.4964\"}"}}]},{"role":"tool","content":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h","tool_call_id":"NyzpzGbZo"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 23:00:04 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Mistral-Correlation-Id: + - '019978ce-2a89-7f65-a78f-af43c0dd7c27' + X-Kong-Request-Id: + - '019978ce-2a89-7f65-a78f-af43c0dd7c27' + X-Envoy-Upstream-Service-Time: + - '3555' + X-Ratelimit-Limit-Tokens-Minute: + - '500000' + X-Ratelimit-Remaining-Tokens-Minute: + - '499372' + X-Ratelimit-Limit-Tokens-Month: + - '1000000000' + X-Ratelimit-Remaining-Tokens-Month: + - '999998789' + X-Ratelimit-Tokens-Query-Cost: + - '478' + X-Ratelimit-Limit-Req-Minute: + - '60' + X-Ratelimit-Remaining-Req-Minute: + - '58' + Access-Control-Allow-Origin: + - "*" + X-Kong-Upstream-Latency: + - '3557' + X-Kong-Proxy-Latency: + - '6' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=15552000; includeSubDomains + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: '{"id":"d02b4655671b45e5ba1328b3f81a61db","created":1758668401,"model":"mistral-small-latest","usage":{"prompt_tokens":200,"total_tokens":478,"completion_tokens":278},"object":"chat.completion","choices":[{"index":0,"finish_reason":"stop","message":{"role":"assistant","tool_calls":null,"content":"The + fall of Rome typically refers to the fall of the Western Roman Empire, which + occurred in 476 AD. This event marked the end of the ancient Roman state and + the beginning of the Middle Ages in Western Europe. The fall was a complex + process that involved a combination of internal and external factors, including:\n\n1. + **Military Overextension**: The Roman Empire had expanded too much, making + it difficult to defend all its territories effectively.\n2. **Economic Troubles**: + Heavy taxation, reliance on slave labor, and a widening gap between the rich + and the poor weakened the economy.\n3. **Political Corruption and Instability**: + Frequent changes in leadership and political corruption undermined the empire''s + stability.\n4. **Barbarian Invasions**: Invasions by various Germanic tribes, + such as the Visigoths, Vandals, and Ostrogoths, put significant pressure on + the empire.\n5. **Social and Cultural Decline**: There was a decline in civic + pride and a shift in cultural values, which affected the social fabric of + the empire.\n\nThe final blow came when the Germanic chieftain Odoacer deposed + the last Western Roman Emperor, Romulus Augustulus, in 476 AD, and declared + himself King of Italy. This event is often seen as the traditional end of + the Western Roman Empire."}}]}' + recorded_at: Tue, 23 Sep 2025 23:00:04 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_choice_none.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_choice_none.yml new file mode 100644 index 000000000..47c9e431c --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_choice_none.yml @@ -0,0 +1,118 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1-nano","messages":[{"role":"user","content":"What''s + the weather in Berlin? (52.5200, 13.4050)"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":"none"}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:21 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '183' + Openai-Project: + - proj_CT47WB5LbNTGaasdAh4lTjTl + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '186' + X-Ratelimit-Limit-Requests: + - '500' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '499' + X-Ratelimit-Remaining-Tokens: + - '199986' + X-Ratelimit-Reset-Requests: + - 120ms + X-Ratelimit-Reset-Tokens: + - 4ms + X-Request-Id: + - "" + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CAQvBQc491J7zUXS44pNB0ZKfDzsj", + "object": "chat.completion", + "created": 1756602801, + "model": "gpt-4.1-nano-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "I'll check the current weather in Berlin for you.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 89, + "completion_tokens": 10, + "total_tokens": 99, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_c4c155951e" + } + recorded_at: Sun, 31 Aug 2025 01:13:21 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_choice_required_for_unrelated_queries.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_choice_required_for_unrelated_queries.yml new file mode 100644 index 000000000..2c60b0e0c --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_choice_required_for_unrelated_queries.yml @@ -0,0 +1,253 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1-nano","messages":[{"role":"user","content":"When + was the fall of Rome?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":"required"}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:23 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '490' + Openai-Project: + - proj_CT47WB5LbNTGaasdAh4lTjTl + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '493' + X-Ratelimit-Limit-Requests: + - '500' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '499' + X-Ratelimit-Remaining-Tokens: + - '199991' + X-Ratelimit-Reset-Requests: + - 120ms + X-Ratelimit-Reset-Tokens: + - 2ms + X-Request-Id: + - "" + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CAQvCBTwD89xBM16o1OvbkHFRf1Px", + "object": "chat.completion", + "created": 1756602802, + "model": "gpt-4.1-nano-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_a75vr5O6KEZ1vRKngigOiIY3", + "type": "function", + "function": { + "name": "weather", + "arguments": "{\"latitude\": \"41.9\", \"longitude\": \"12.5\"}" + } + }, + { + "id": "call_qLBIbaaDzs3oI8zKm9muABvX", + "type": "function", + "function": { + "name": "weather", + "arguments": "{\"latitude\": \"40.7\", \"longitude\": \"-73.9\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 77, + "completion_tokens": 59, + "total_tokens": 136, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_c4c155951e" + } + recorded_at: Sun, 31 Aug 2025 01:13:22 GMT +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1-nano","messages":[{"role":"user","content":"When + was the fall of Rome?"},{"role":"assistant","tool_calls":[{"id":"call_a75vr5O6KEZ1vRKngigOiIY3","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"41.9\",\"longitude\":\"12.5\"}"}},{"id":"call_qLBIbaaDzs3oI8zKm9muABvX","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"40.7\",\"longitude\":\"-73.9\"}"}}]},{"role":"tool","content":"Current + weather at 41.9, 12.5: 15°C, Wind: 10 km/h","tool_call_id":"call_a75vr5O6KEZ1vRKngigOiIY3"},{"role":"tool","content":"Current + weather at 40.7, -73.9: 15°C, Wind: 10 km/h","tool_call_id":"call_qLBIbaaDzs3oI8zKm9muABvX"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:25 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '561' + Openai-Project: + - proj_CT47WB5LbNTGaasdAh4lTjTl + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '567' + X-Ratelimit-Limit-Requests: + - '500' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '499' + X-Ratelimit-Remaining-Tokens: + - '199962' + X-Ratelimit-Reset-Requests: + - 120ms + X-Ratelimit-Reset-Tokens: + - 11ms + X-Request-Id: + - "" + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CAQvE4OIDI2bCF3iLQLQbyl3nkyxC", + "object": "chat.completion", + "created": 1756602804, + "model": "gpt-4.1-nano-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The fall of Rome is traditionally marked in AD 476, when the last Roman emperor of the Western Roman Empire, Romulus Augustulus, was deposed. This event signifies the end of ancient Rome and the decline of the Western Roman Empire.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 204, + "completion_tokens": 50, + "total_tokens": 254, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_c4c155951e" + } + recorded_at: Sun, 31 Aug 2025 01:13:25 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_parallel_false_for_sequential_execution.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_parallel_false_for_sequential_execution.yml new file mode 100644 index 000000000..99c1f9546 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_parallel_false_for_sequential_execution.yml @@ -0,0 +1,344 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1-nano","messages":[{"role":"developer","content":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."},{"role":"user","content":"What''s the weather in Berlin and + what''s the best programming language?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}},{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"parallel_tool_calls":false}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:25 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '437' + Openai-Project: + - proj_CT47WB5LbNTGaasdAh4lTjTl + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '441' + X-Ratelimit-Limit-Requests: + - '500' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '499' + X-Ratelimit-Remaining-Tokens: + - '199956' + X-Ratelimit-Reset-Requests: + - 120ms + X-Ratelimit-Reset-Tokens: + - 13ms + X-Request-Id: + - "" + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CAQvFx3vsphtCMyQGP9SrKhHWucp6", + "object": "chat.completion", + "created": 1756602805, + "model": "gpt-4.1-nano-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_LAG7dZXozlUvleaEaeH3URwb", + "type": "function", + "function": { + "name": "weather", + "arguments": "{\"latitude\":\"52.5200\",\"longitude\":\"13.4050\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 127, + "completion_tokens": 23, + "total_tokens": 150, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_7c233bf9d1" + } + recorded_at: Sun, 31 Aug 2025 01:13:25 GMT +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1-nano","messages":[{"role":"developer","content":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."},{"role":"user","content":"What''s the weather in Berlin and + what''s the best programming language?"},{"role":"assistant","tool_calls":[{"id":"call_LAG7dZXozlUvleaEaeH3URwb","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"52.5200\",\"longitude\":\"13.4050\"}"}}]},{"role":"tool","content":"Current + weather at 52.5200, 13.4050: 15°C, Wind: 10 km/h","tool_call_id":"call_LAG7dZXozlUvleaEaeH3URwb"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}},{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"parallel_tool_calls":false}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:27 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '343' + Openai-Project: + - proj_CT47WB5LbNTGaasdAh4lTjTl + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '345' + X-Ratelimit-Limit-Requests: + - '500' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '499' + X-Ratelimit-Remaining-Tokens: + - '199940' + X-Ratelimit-Reset-Requests: + - 120ms + X-Ratelimit-Reset-Tokens: + - 18ms + X-Request-Id: + - "" + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CAQvHX72BcSTozOXGl4Lvaa8G9Qn4", + "object": "chat.completion", + "created": 1756602807, + "model": "gpt-4.1-nano-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_BvMMs1DpscvHI7ay9DwRk2aC", + "type": "function", + "function": { + "name": "best_language_to_learn", + "arguments": "{}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 182, + "completion_tokens": 13, + "total_tokens": 195, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_7c233bf9d1" + } + recorded_at: Sun, 31 Aug 2025 01:13:27 GMT +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1-nano","messages":[{"role":"developer","content":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."},{"role":"user","content":"What''s the weather in Berlin and + what''s the best programming language?"},{"role":"assistant","tool_calls":[{"id":"call_LAG7dZXozlUvleaEaeH3URwb","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"52.5200\",\"longitude\":\"13.4050\"}"}}]},{"role":"tool","content":"Current + weather at 52.5200, 13.4050: 15°C, Wind: 10 km/h","tool_call_id":"call_LAG7dZXozlUvleaEaeH3URwb"},{"role":"assistant","tool_calls":[{"id":"call_BvMMs1DpscvHI7ay9DwRk2aC","type":"function","function":{"name":"best_language_to_learn","arguments":"{}"}}]},{"role":"tool","content":"Ruby","tool_call_id":"call_BvMMs1DpscvHI7ay9DwRk2aC"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}},{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"parallel_tool_calls":false}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:28 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '316' + Openai-Project: + - proj_CT47WB5LbNTGaasdAh4lTjTl + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '319' + X-Ratelimit-Limit-Requests: + - '500' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '499' + X-Ratelimit-Remaining-Tokens: + - '199937' + X-Ratelimit-Reset-Requests: + - 120ms + X-Ratelimit-Reset-Tokens: + - 18ms + X-Request-Id: + - "" + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjaGF0Y21wbC1DQVF2SWxBNHE5RG1hTnVaVGNpUXZIeGVxdjFuRSIsCiAgIm9iamVjdCI6ICJjaGF0LmNvbXBsZXRpb24iLAogICJjcmVhdGVkIjogMTc1NjYwMjgwOCwKICAibW9kZWwiOiAiZ3B0LTQuMS1uYW5vLTIwMjUtMDQtMTQiLAogICJjaG9pY2VzIjogWwogICAgewogICAgICAiaW5kZXgiOiAwLAogICAgICAibWVzc2FnZSI6IHsKICAgICAgICAicm9sZSI6ICJhc3Npc3RhbnQiLAogICAgICAgICJjb250ZW50IjogIlRoZSBjdXJyZW50IHdlYXRoZXIgaW4gQmVybGluIGlzIDE1wrBDIHdpdGggYSB3aW5kIG9mIDEwIGttL2guIFRoZSBiZXN0IHByb2dyYW1taW5nIGxhbmd1YWdlIHRvIGxlYXJuIHJpZ2h0IG5vdyBpcyBSdWJ5LiIsCiAgICAgICAgInJlZnVzYWwiOiBudWxsLAogICAgICAgICJhbm5vdGF0aW9ucyI6IFtdCiAgICAgIH0sCiAgICAgICJsb2dwcm9icyI6IG51bGwsCiAgICAgICJmaW5pc2hfcmVhc29uIjogInN0b3AiCiAgICB9CiAgXSwKICAidXNhZ2UiOiB7CiAgICAicHJvbXB0X3Rva2VucyI6IDIwNywKICAgICJjb21wbGV0aW9uX3Rva2VucyI6IDMwLAogICAgInRvdGFsX3Rva2VucyI6IDIzNywKICAgICJwcm9tcHRfdG9rZW5zX2RldGFpbHMiOiB7CiAgICAgICJjYWNoZWRfdG9rZW5zIjogMCwKICAgICAgImF1ZGlvX3Rva2VucyI6IDAKICAgIH0sCiAgICAiY29tcGxldGlvbl90b2tlbnNfZGV0YWlscyI6IHsKICAgICAgInJlYXNvbmluZ190b2tlbnMiOiAwLAogICAgICAiYXVkaW9fdG9rZW5zIjogMCwKICAgICAgImFjY2VwdGVkX3ByZWRpY3Rpb25fdG9rZW5zIjogMCwKICAgICAgInJlamVjdGVkX3ByZWRpY3Rpb25fdG9rZW5zIjogMAogICAgfQogIH0sCiAgInNlcnZpY2VfdGllciI6ICJkZWZhdWx0IiwKICAic3lzdGVtX2ZpbmdlcnByaW50IjogImZwX2U5MWE1MThkZGIiCn0K + recorded_at: Sun, 31 Aug 2025 01:13:28 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_specific_tool_choice.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_specific_tool_choice.yml new file mode 100644 index 000000000..38cc4728c --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openai_gpt-4_1-nano_respects_specific_tool_choice.yml @@ -0,0 +1,248 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1-nano","messages":[{"role":"user","content":"What''s + the fall of Rome?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":{"type":"function","function":{"name":"weather"}}}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 23:06:03 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '334' + Openai-Project: + - proj_CT47WB5LbNTGaasdAh4lTjTl + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '551' + X-Ratelimit-Limit-Requests: + - '500' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '499' + X-Ratelimit-Remaining-Tokens: + - '199992' + X-Ratelimit-Reset-Requests: + - 120ms + X-Ratelimit-Reset-Tokens: + - 2ms + X-Request-Id: + - "" + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CJ6N9EPejqMpgKLnYv7BhmYbVT0aV", + "object": "chat.completion", + "created": 1758668763, + "model": "gpt-4.1-nano-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_7M77PqB14XibKL6C23aSDm8S", + "type": "function", + "function": { + "name": "weather", + "arguments": "{\"latitude\":\"41.9028\",\"longitude\":\"12.4964\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 84, + "completion_tokens": 15, + "total_tokens": 99, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_7c233bf9d1" + } + recorded_at: Tue, 23 Sep 2025 23:06:03 GMT +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1-nano","messages":[{"role":"user","content":"What''s + the fall of Rome?"},{"role":"assistant","tool_calls":[{"id":"call_7M77PqB14XibKL6C23aSDm8S","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"41.9028\",\"longitude\":\"12.4964\"}"}}]},{"role":"tool","content":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h","tool_call_id":"call_7M77PqB14XibKL6C23aSDm8S"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 23:06:05 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - "" + Openai-Processing-Ms: + - '1036' + Openai-Project: + - proj_CT47WB5LbNTGaasdAh4lTjTl + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '1298' + X-Ratelimit-Limit-Requests: + - '500' + X-Ratelimit-Limit-Tokens: + - '200000' + X-Ratelimit-Remaining-Requests: + - '499' + X-Ratelimit-Remaining-Tokens: + - '199975' + X-Ratelimit-Reset-Requests: + - 120ms + X-Ratelimit-Reset-Tokens: + - 7ms + X-Request-Id: + - "" + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - "" + - "" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CJ6NAtbBfcPENk5puUC9eabd4j3c2", + "object": "chat.completion", + "created": 1758668764, + "model": "gpt-4.1-nano-2025-04-14", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "The fall of Rome is historically considered to have occurred in September 476 AD, when the last Roman emperor of the West, Romulus Augustulus, was deposed by the Germanic chieftain Odoacer. This event marks the end of the Western Roman Empire and is often used to signify the transition from antiquity to the Middle Ages in Europe.", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 131, + "completion_tokens": 73, + "total_tokens": 204, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_7c233bf9d1" + } + recorded_at: Tue, 23 Sep 2025 23:06:05 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_choice_none.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_choice_none.yml new file mode 100644 index 000000000..30d71055e --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_choice_none.yml @@ -0,0 +1,57 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"anthropic/claude-3.5-haiku","messages":[{"role":"user","content":"What''s + the weather in Berlin? (52.5200, 13.4050)"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":"none"}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:30 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: "\n \n\n \n{\"id\":\"gen-1756602809-IBEAh5fts2Ov5bpcIrN2\",\"provider\":\"Google\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1756602809,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"I'll + help you check the current weather in Berlin using the provided coordinates.\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":389,\"completion_tokens\":24,\"total_tokens\":413}}" + recorded_at: Sun, 31 Aug 2025 01:13:30 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_choice_required_for_unrelated_queries.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_choice_required_for_unrelated_queries.yml new file mode 100644 index 000000000..e90ba03e2 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_choice_required_for_unrelated_queries.yml @@ -0,0 +1,126 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"anthropic/claude-3.5-haiku","messages":[{"role":"user","content":"When + was the fall of Rome?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":"required"}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:31 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: "\n \n\n \n{\"id\":\"gen-1756602810-hpQDX95J40A6Cal5jhu6\",\"provider\":\"Google\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1756602810,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"tool_calls\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null,\"reasoning\":null,\"tool_calls\":[{\"id\":\"toolu_vrtx_01Fw5bHBFooudxXQxsUpSpnS\",\"index\":0,\"type\":\"function\",\"function\":{\"name\":\"weather\",\"arguments\":\"{\\\"latitude\\\": + \\\"41.9028\\\", \\\"longitude\\\": \\\"12.4964\\\"}\"}}]}}],\"usage\":{\"prompt_tokens\":468,\"completion_tokens\":59,\"total_tokens\":527}}" + recorded_at: Sun, 31 Aug 2025 01:13:31 GMT +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"anthropic/claude-3.5-haiku","messages":[{"role":"user","content":"When + was the fall of Rome?"},{"role":"assistant","content":"","tool_calls":[{"id":"toolu_vrtx_01Fw5bHBFooudxXQxsUpSpnS","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"41.9028\",\"longitude\":\"12.4964\"}"}}]},{"role":"tool","content":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h","tool_call_id":"toolu_vrtx_01Fw5bHBFooudxXQxsUpSpnS"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:33 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: "\n \n\n \n\n \n\n \n\n \n\n + \ \n\n \n\n \n\n \n\n \n\n \n{\"id\":\"gen-1756602812-ZrGP9hBw9qoCq6tGrpnU\",\"provider\":\"Google\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1756602812,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"stop\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"I + apologize, but the weather function is not designed to provide historical + information. Let me answer your question about the fall of Rome directly.\\n\\nThe + fall of Rome is typically considered to have occurred in 476 CE, when the + last Roman Emperor in the West, Romulus Augustulus, was deposed by the Germanic + king Odoacer. This marked the end of the Western Roman Empire, though the + Eastern Roman Empire (Byzantine Empire) continued to exist for nearly another + thousand years until Constantinople fell to the Ottoman Turks in 1453.\\n\\nHowever, + historians debate the exact moment of \\\"fall,\\\" as the decline of the + Roman Empire was a gradual process that occurred over several centuries. Some + key events include:\\n\\n1. The division of the Empire into Western and Eastern + halves in 285 CE\\n2. The sack of Rome by the Visigoths in 410 CE\\n3. The + final deposition of Romulus Augustulus in 476 CE\\n\\nThe fall of Rome had + profound consequences, marking the end of classical antiquity and the beginning + of the medieval period in European history.\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":491,\"completion_tokens\":238,\"total_tokens\":729}}" + recorded_at: Sun, 31 Aug 2025 01:13:37 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_parallel_false_for_sequential_execution.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_parallel_false_for_sequential_execution.yml new file mode 100644 index 000000000..0ca002bb7 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_parallel_false_for_sequential_execution.yml @@ -0,0 +1,186 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"anthropic/claude-3.5-haiku","messages":[{"role":"developer","content":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."},{"role":"user","content":"What''s the weather in Berlin and + what''s the best programming language?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}},{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"parallel_tool_calls":false}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:38 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: "\n \n\n \n\n \n\n \n\n \n{\"id\":\"gen-1756602817-VMtwmlgbk82l96FTejKC\",\"provider\":\"Google\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1756602817,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"tool_calls\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"I'll + help you find out the weather in Berlin and the best programming language + to learn. I'll use the available tools to get this information for you.\\n\\nFirst, + let's check the weather in Berlin:\",\"refusal\":null,\"reasoning\":null,\"tool_calls\":[{\"id\":\"toolu_vrtx_018hpaBSASAcq6oSYRRwxTXv\",\"index\":0,\"type\":\"function\",\"function\":{\"name\":\"weather\",\"arguments\":\"{\\\"latitude\\\": + \\\"52.5200\\\", \\\"longitude\\\": \\\"13.4050\\\"}\"}}]}}],\"usage\":{\"prompt_tokens\":462,\"completion_tokens\":108,\"total_tokens\":570}}" + recorded_at: Sun, 31 Aug 2025 01:13:39 GMT +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"anthropic/claude-3.5-haiku","messages":[{"role":"developer","content":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."},{"role":"user","content":"What''s the weather in Berlin and + what''s the best programming language?"},{"role":"assistant","content":"I''ll + help you find out the weather in Berlin and the best programming language + to learn. I''ll use the available tools to get this information for you.\n\nFirst, + let''s check the weather in Berlin:","tool_calls":[{"id":"toolu_vrtx_018hpaBSASAcq6oSYRRwxTXv","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"52.5200\",\"longitude\":\"13.4050\"}"}}]},{"role":"tool","content":"Current + weather at 52.5200, 13.4050: 15°C, Wind: 10 km/h","tool_call_id":"toolu_vrtx_018hpaBSASAcq6oSYRRwxTXv"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}},{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"parallel_tool_calls":false}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:41 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: "\n \n\n \n{\"id\":\"gen-1756602820-Si0n0YPvus1luifj7TXx\",\"provider\":\"Anthropic\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1756602820,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"tool_calls\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"Now, + let's find out the best programming language to learn:\",\"refusal\":null,\"reasoning\":null,\"tool_calls\":[{\"id\":\"toolu_01P4w3SrLRhxkbmaWWGoMeGy\",\"index\":0,\"type\":\"function\",\"function\":{\"name\":\"best_language_to_learn\",\"arguments\":\"\"}}]}}],\"usage\":{\"prompt_tokens\":618,\"completion_tokens\":46,\"total_tokens\":664}}" + recorded_at: Sun, 31 Aug 2025 01:13:41 GMT +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"anthropic/claude-3.5-haiku","messages":[{"role":"developer","content":"You + must use both the weather tool for Berlin (52.5200, 13.4050) and the best + language tool."},{"role":"user","content":"What''s the weather in Berlin and + what''s the best programming language?"},{"role":"assistant","content":"I''ll + help you find out the weather in Berlin and the best programming language + to learn. I''ll use the available tools to get this information for you.\n\nFirst, + let''s check the weather in Berlin:","tool_calls":[{"id":"toolu_vrtx_018hpaBSASAcq6oSYRRwxTXv","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"52.5200\",\"longitude\":\"13.4050\"}"}}]},{"role":"tool","content":"Current + weather at 52.5200, 13.4050: 15°C, Wind: 10 km/h","tool_call_id":"toolu_vrtx_018hpaBSASAcq6oSYRRwxTXv"},{"role":"assistant","content":"Now, + let''s find out the best programming language to learn:","tool_calls":[{"id":"toolu_01P4w3SrLRhxkbmaWWGoMeGy","type":"function","function":{"name":"best_language_to_learn","arguments":"{}"}}]},{"role":"tool","content":"Ruby","tool_call_id":"toolu_01P4w3SrLRhxkbmaWWGoMeGy"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}},{"type":"function","function":{"name":"best_language_to_learn","description":"Gets + the best language to learn","parameters":{"type":"object","properties":{},"required":[]}}}],"parallel_tool_calls":false}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 01:13:42 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + CiAgICAgICAgIAoKICAgICAgICAgCgogICAgICAgICAKCiAgICAgICAgIAp7ImlkIjoiZ2VuLTE3NTY2MDI4MjItUmE0RjlpenFOMnNKRTdURjdnWlgiLCJwcm92aWRlciI6IkFudGhyb3BpYyIsIm1vZGVsIjoiYW50aHJvcGljL2NsYXVkZS0zLjUtaGFpa3UiLCJvYmplY3QiOiJjaGF0LmNvbXBsZXRpb24iLCJjcmVhdGVkIjoxNzU2NjAyODIyLCJjaG9pY2VzIjpbeyJsb2dwcm9icyI6bnVsbCwiZmluaXNoX3JlYXNvbiI6InN0b3AiLCJuYXRpdmVfZmluaXNoX3JlYXNvbiI6InN0b3AiLCJpbmRleCI6MCwibWVzc2FnZSI6eyJyb2xlIjoiYXNzaXN0YW50IiwiY29udGVudCI6IkxldCBtZSBzdW1tYXJpemUgdGhlIHJlc3VsdHMgZm9yIHlvdTpcbjEuIFdlYXRoZXIgaW4gQmVybGluOiBDdXJyZW50bHkgMTXCsEMgd2l0aCBhIHdpbmQgc3BlZWQgb2YgMTAga20vaC4gSXQgc2VlbXMgbGlrZSBhIG1pbGQgZGF5LlxuMi4gQmVzdCBQcm9ncmFtbWluZyBMYW5ndWFnZTogVGhlIHRvb2wgc3VnZ2VzdHMgUnVieSBhcyB0aGUgYmVzdCBsYW5ndWFnZSB0byBsZWFybi5cblxuSXMgdGhlcmUgYW55dGhpbmcgZWxzZSBJIGNhbiBoZWxwIHlvdSB3aXRoPyIsInJlZnVzYWwiOm51bGwsInJlYXNvbmluZyI6bnVsbH19XSwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6Njg1LCJjb21wbGV0aW9uX3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6NzU4fX0= + recorded_at: Sun, 31 Aug 2025 01:13:44 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_specific_tool_choice.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_specific_tool_choice.yml new file mode 100644 index 000000000..d0e485338 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_openrouter_anthropic_claude-3_5-haiku_respects_specific_tool_choice.yml @@ -0,0 +1,112 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"anthropic/claude-3.5-haiku","messages":[{"role":"user","content":"What''s + the fall of Rome?"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}],"tool_choice":{"type":"function","function":{"name":"weather"}}}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 23:00:07 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: "\n \n\n \n\n \n{\"id\":\"gen-1758668406-7YToyLJa9lLnIE72IZO5\",\"provider\":\"Google\",\"model\":\"anthropic/claude-3.5-haiku\",\"object\":\"chat.completion\",\"created\":1758668406,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"tool_calls\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null,\"reasoning\":null,\"tool_calls\":[{\"id\":\"toolu_vrtx_01Y9uiETpFzuYnVZuav5vj4q\",\"index\":0,\"type\":\"function\",\"function\":{\"name\":\"weather\",\"arguments\":\"{\\\"latitude\\\": + \\\"41.9028\\\", \\\"longitude\\\": \\\"12.4964\\\"}\"}}]}}],\"usage\":{\"prompt_tokens\":471,\"completion_tokens\":56,\"total_tokens\":527}}" + recorded_at: Tue, 23 Sep 2025 23:00:07 GMT +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"anthropic/claude-3.5-haiku","messages":[{"role":"user","content":"What''s + the fall of Rome?"},{"role":"assistant","content":"","tool_calls":[{"id":"toolu_vrtx_01Y9uiETpFzuYnVZuav5vj4q","type":"function","function":{"name":"weather","arguments":"{\"latitude\":\"41.9028\",\"longitude\":\"12.4964\"}"}}]},{"role":"tool","content":"Current + weather at 41.9028, 12.4964: 15°C, Wind: 10 km/h","tool_call_id":"toolu_vrtx_01Y9uiETpFzuYnVZuav5vj4q"}],"stream":false,"tools":[{"type":"function","function":{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"object","properties":{"latitude":{"type":"string","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"string","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 23:00:08 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - "" + body: + encoding: ASCII-8BIT + string: !binary |- + CiAgICAgICAgIAoKICAgICAgICAgCgogICAgICAgICAKCiAgICAgICAgIAoKICAgICAgICAgCgogICAgICAgICAKCiAgICAgICAgIAoKICAgICAgICAgCgogICAgICAgICAKCiAgICAgICAgIAoKICAgICAgICAgCgogICAgICAgICAKCiAgICAgICAgIAoKICAgICAgICAgCgogICAgICAgICAKeyJpZCI6Imdlbi0xNzU4NjY4NDA3LU94bWphaHQ4RmJSSjU4dUswcUJpIiwicHJvdmlkZXIiOiJBbnRocm9waWMiLCJtb2RlbCI6ImFudGhyb3BpYy9jbGF1ZGUtMy41LWhhaWt1Iiwib2JqZWN0IjoiY2hhdC5jb21wbGV0aW9uIiwiY3JlYXRlZCI6MTc1ODY2ODQwNywiY2hvaWNlcyI6W3sibG9ncHJvYnMiOm51bGwsImZpbmlzaF9yZWFzb24iOiJzdG9wIiwibmF0aXZlX2ZpbmlzaF9yZWFzb24iOiJzdG9wIiwiaW5kZXgiOjAsIm1lc3NhZ2UiOnsicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOiJJIGFwb2xvZ2l6ZSwgYnV0IHRoZSB3ZWF0aGVyIGZ1bmN0aW9uIGlzIG5vdCByZWxhdGVkIHRvIGhpc3RvcmljYWwgaW5mb3JtYXRpb24uIExldCBtZSBwcm92aWRlIHlvdSB3aXRoIGEgZGV0YWlsZWQgZXhwbGFuYXRpb24gYWJvdXQgdGhlIEZhbGwgb2YgUm9tZTpcblxuVGhlIEZhbGwgb2YgUm9tZSByZWZlcnMgdG8gdGhlIGRlY2xpbmUgYW5kIHVsdGltYXRlIGNvbGxhcHNlIG9mIHRoZSBXZXN0ZXJuIFJvbWFuIEVtcGlyZSwgd2hpY2ggaXMgdHJhZGl0aW9uYWxseSBkYXRlZCB0byA0NzYgQ0UuIFRoaXMgd2FzIGEgY29tcGxleCBoaXN0b3JpY2FsIHByb2Nlc3MgdGhhdCBvY2N1cnJlZCBvdmVyIHNldmVyYWwgY2VudHVyaWVzLCBpbnZvbHZpbmcgbXVsdGlwbGUgZmFjdG9yczpcblxuMS4gSW50ZXJuYWwgQ2hhbGxlbmdlczpcbi0gUG9saXRpY2FsIGluc3RhYmlsaXR5IGFuZCBmcmVxdWVudCBjaXZpbCB3YXJzXG4tIENvcnJ1cHRpb24gaW4gZ292ZXJubWVudFxuLSBFY29ub21pYyBkZWNsaW5lXG4tIE92ZXJleHBhbnNpb24gb2YgdGhlIGVtcGlyZVxuXG4yLiBFeHRlcm5hbCBQcmVzc3VyZXM6XG4tIENvbnN0YW50IGludmFzaW9ucyBieSBHZXJtYW5pYyB0cmliZXMgbGlrZSB0aGUgR290aHMsIFZhbmRhbHMsIGFuZCBIdW5zXG4tIE1pZ3JhdGlvbiBwZXJpb2QgKGtub3duIGFzIHRoZSBcIlbDtmxrZXJ3YW5kZXJ1bmdcIilcbi0gV2Vha2VuaW5nIG9mIFJvbWFuIG1pbGl0YXJ5IGNhcGFiaWxpdGllc1xuXG4zLiBLZXkgRXZlbnRzOlxuLSBJbiA0MTAgQ0UsIHRoZSBWaXNpZ290aHMgdW5kZXIgQWxhcmljIHNhY2tlZCBSb21lXG4tIEluIDQ1NSBDRSwgdGhlIFZhbmRhbHMgYWxzbyBzYWNrZWQgUm9tZVxuLSBJbiA0NzYgQ0UsIHRoZSBHZXJtYW5pYyBsZWFkZXIgT2RvYWNlciBkZXBvc2VkIHRoZSBsYXN0IFJvbWFuIEVtcGVyb3IgUm9tdWx1cyBBdWd1c3R1bHVzLCB3aGljaCBpcyBvZnRlbiBjb25zaWRlcmVkIHRoZSBkZWZpbml0aXZlIGVuZCBvZiB0aGUgV2VzdGVybiBSb21hbiBFbXBpcmVcblxuVGhlIEVhc3Rlcm4gUm9tYW4gRW1waXJlIChCeXphbnRpbmUgRW1waXJlKSBjb250aW51ZWQgdG8gZXhpc3QgZm9yIG5lYXJseSBhbm90aGVyIHRob3VzYW5kIHllYXJzIHVudGlsIENvbnN0YW50aW5vcGxlIGZlbGwgdG8gdGhlIE90dG9tYW4gVHVya3MgaW4gMTQ1My5cblxuVGhlIEZhbGwgb2YgUm9tZSBtYXJrZWQgdGhlIGVuZCBvZiBjbGFzc2ljYWwgYW50aXF1aXR5IGFuZCB0aGUgYmVnaW5uaW5nIG9mIHRoZSBNZWRpZXZhbCBwZXJpb2QgaW4gRXVyb3BlYW4gaGlzdG9yeSwgcHJvZm91bmRseSBjaGFuZ2luZyB0aGUgcG9saXRpY2FsLCBzb2NpYWwsIGFuZCBjdWx0dXJhbCBsYW5kc2NhcGUgb2YgRXVyb3BlLiIsInJlZnVzYWwiOm51bGwsInJlYXNvbmluZyI6bnVsbH19XSwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6NDkxLCJjb21wbGV0aW9uX3Rva2VucyI6MzIzLCJ0b3RhbF90b2tlbnMiOjgxNH19 + recorded_at: Tue, 23 Sep 2025 23:00:14 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_choice_none.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_choice_none.yml new file mode 100644 index 000000000..d94df47c0 --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_choice_none.yml @@ -0,0 +1,111 @@ +--- +http_interactions: +- request: + method: post + uri: https://oauth2.googleapis.com/token + body: + encoding: ASCII-8BIT + string: grant_type=refresh_token&refresh_token=&client_id=&client_secret= + headers: + User-Agent: + - Faraday v2.13.4 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Expires: + - Mon, 01 Jan 1990 00:00:00 GMT + Date: + - Tue, 23 Sep 2025 23:40:16 GMT + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Vary: + - Origin + - Referer + - X-Origin + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: |- + { + "access_token": "", + "expires_in": 3599, + "scope": "https://www.googleapis.com/auth/sqlservice.login openid https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/userinfo.email", + "token_type": "Bearer", + "id_token": "" + } + recorded_at: Tue, 23 Sep 2025 23:40:16 GMT +- request: + method: post + uri: https://-aiplatform.googleapis.com/v1beta1/projects//locations//publishers/google/models/gemini-2.5-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"What''s the weather in + Berlin? (52.5200, 13.4050)"}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}],"toolConfig":{"functionCallingConfig":{"mode":"none"}}}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 23:40:18 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJjYW5kaWRhdGVzIjogWwogICAgewogICAgICAiY29udGVudCI6IHsKICAgICAgICAicm9sZSI6ICJtb2RlbCIsCiAgICAgICAgInBhcnRzIjogWwogICAgICAgICAgewogICAgICAgICAgICAidGV4dCI6ICJUaGUgd2VhdGhlciBpbiBCZXJsaW4sIEdlcm1hbnkgKDUyLjUyMDAsIDEzLjQwNTApIGlzIGN1cnJlbnRseSAqKnBhcnRseSBjbG91ZHkqKiB3aXRoIGEgdGVtcGVyYXR1cmUgb2YgKioxMsKwQyAoNTTCsEYpKiouXG5cblRoZSB3aW5kIGlzIGJsb3dpbmcgZnJvbSB0aGUgd2VzdCBhdCBhYm91dCAxMSBrbS9oICg3IG1waCksIGFuZCB0aGUgaHVtaWRpdHkgaXMgNzYlLiIKICAgICAgICAgIH0KICAgICAgICBdCiAgICAgIH0sCiAgICAgICJmaW5pc2hSZWFzb24iOiAiU1RPUCIsCiAgICAgICJhdmdMb2dwcm9icyI6IC0xLjQ4ODU1ODc5NTAwMTA1NTcKICAgIH0KICBdLAogICJ1c2FnZU1ldGFkYXRhIjogewogICAgInByb21wdFRva2VuQ291bnQiOiA3MCwKICAgICJjYW5kaWRhdGVzVG9rZW5Db3VudCI6IDc0LAogICAgInRvdGFsVG9rZW5Db3VudCI6IDM5NiwKICAgICJ0cmFmZmljVHlwZSI6ICJPTl9ERU1BTkQiLAogICAgInByb21wdFRva2Vuc0RldGFpbHMiOiBbCiAgICAgIHsKICAgICAgICAibW9kYWxpdHkiOiAiVEVYVCIsCiAgICAgICAgInRva2VuQ291bnQiOiA3MAogICAgICB9CiAgICBdLAogICAgImNhbmRpZGF0ZXNUb2tlbnNEZXRhaWxzIjogWwogICAgICB7CiAgICAgICAgIm1vZGFsaXR5IjogIlRFWFQiLAogICAgICAgICJ0b2tlbkNvdW50IjogNzQKICAgICAgfQogICAgXSwKICAgICJ0aG91Z2h0c1Rva2VuQ291bnQiOiAyNTIKICB9LAogICJtb2RlbFZlcnNpb24iOiAiZ2VtaW5pLTIuNS1mbGFzaCIsCiAgImNyZWF0ZVRpbWUiOiAiMjAyNS0wOS0yM1QyMzo0MDoxNi43NDg4NzFaIiwKICAicmVzcG9uc2VJZCI6ICI0Q19UYU1mYUxiejVsZDhQd3ZPaTRRbyIKfQo= + recorded_at: Tue, 23 Sep 2025 23:40:19 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_choice_required_for_unrelated_queries.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_choice_required_for_unrelated_queries.yml new file mode 100644 index 000000000..6a4510b0b --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_choice_required_for_unrelated_queries.yml @@ -0,0 +1,302 @@ +--- +http_interactions: +- request: + method: post + uri: https://oauth2.googleapis.com/token + body: + encoding: ASCII-8BIT + string: grant_type=refresh_token&refresh_token=&client_id=&client_secret= + headers: + User-Agent: + - Faraday v2.13.4 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Sun, 31 Aug 2025 04:21:18 GMT + Expires: + - Mon, 01 Jan 1990 00:00:00 GMT + Pragma: + - no-cache + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Content-Type: + - application/json; charset=utf-8 + Vary: + - Origin + - Referer + - X-Origin + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: |- + { + "access_token": "", + "expires_in": 3599, + "scope": "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/sqlservice.login https://www.googleapis.com/auth/userinfo.email openid", + "token_type": "Bearer", + "id_token": "" + } + recorded_at: Sun, 31 Aug 2025 04:21:18 GMT +- request: + method: post + uri: https://-aiplatform.googleapis.com/v1beta1/projects//locations//publishers/google/models/gemini-2.5-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"When was the fall of + Rome?"}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}],"toolConfig":{"functionCallingConfig":{"mode":"any"}}}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 04:21:19 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "functionCall": { + "name": "weather", + "args": { + "latitude": "41.9028", + "longitude": "12.496417" + } + }, + "thoughtSignature": "CvsCAcu98PAb08Q8kb5yHHg7PU0CAjOKLdn8Yxi2SwzpHYUVjGPiedU6mdgPOZdN9tWjBNESIFkhfj2x2vCWqXbVy7/kvVQ8RvC2nCBatPli1p6fGNNyxhFD8kYeE1hPgru/3WQlGcs0tZxYXu0ba62mojAtIt2xI3OSp7egZL0+nsBwfjQNP+0/bbCEFU+yfulkv68tClVXYaQasM7BOewLQJPiTh6/psJEsf7E/GKuQMzKwqyXILBLkp2lk3DjDM5wVmrh4Y4uite5JL6zBD4xcMaEyzIfdMW6UFYGZfTwgwWdeVpCBiQbkjprCXxGdYytGlnY1m5dyrHO0YVh+LcuO22GbZcTMIGIU6ktVXroW178MxaEoT/T99gw6jVfwZF07W1DhzovLjpmeOxGI2yFv3rLaoVdaEXiNKRSjRPam+3bIxcJ8T+4lMlLDceY5FN/LaN6j/meZuUwe08pOv/RXV7++XAARrQgti71qetOrLH0FuFsBVb3OG9tGw==" + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -1.0042402367842824 + } + ], + "usageMetadata": { + "promptTokenCount": 51, + "candidatesTokenCount": 19, + "totalTokenCount": 137, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 51 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 19 + } + ], + "thoughtsTokenCount": 67 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-08-31T04:21:18.762574Z", + "responseId": "vs2zaM7FLvKOsLUP1Kyl8A4" + } + recorded_at: Sun, 31 Aug 2025 04:21:19 GMT +- request: + method: post + uri: https://oauth2.googleapis.com/token + body: + encoding: ASCII-8BIT + string: grant_type=refresh_token&refresh_token=&client_id=&client_secret= + headers: + User-Agent: + - Faraday v2.13.4 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Date: + - Sun, 31 Aug 2025 04:21:20 GMT + Expires: + - Mon, 01 Jan 1990 00:00:00 GMT + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Vary: + - Origin + - Referer + - X-Origin + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: |- + { + "access_token": "", + "expires_in": 3599, + "scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/sqlservice.login openid https://www.googleapis.com/auth/cloud-platform", + "token_type": "Bearer", + "id_token": "" + } + recorded_at: Sun, 31 Aug 2025 04:21:20 GMT +- request: + method: post + uri: https://-aiplatform.googleapis.com/v1beta1/projects//locations//publishers/google/models/gemini-2.5-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"When was the fall of + Rome?"}]},{"role":"model","parts":[{"functionCall":{"name":"weather","args":{"latitude":"41.9028","longitude":"12.496417"}}}]},{"role":"user","parts":[{"functionResponse":{"name":"5ae7d84b-2f68-4db0-b691-3ec7e5bb8dc6","response":{"name":"5ae7d84b-2f68-4db0-b691-3ec7e5bb8dc6","content":[{"text":"Current + weather at 41.9028, 12.496417: 15°C, Wind: 10 km/h"}]}}}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Sun, 31 Aug 2025 04:21:21 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "I do not have information on the fall of Rome. My capabilities are limited to providing current weather information given a latitude and longitude.", + "thoughtSignature": "CuwDAcu98PA8YopxQKMu7+a//uFi4cbEWiGyxy6kT/HGyDV5V8CcgKtKpSBtwixRYAuoPd7JRtN/az8OAR2liQXrPsQy+TOX2dyO0kg8c35V3o7DOnevQWnwtlVSS5QoDYZga5s2MUC7Fw4342N7Rs6uO0tJgn+Cz4mNmJ3UexyAyi9+0DNGd9sHpoJknwlSry/PHAahszwYeKJ22HpnLuA85T9Z8Ce4078veM/W/UMYsqQVxE78uuZXrI9TJO6sLiwyC9Nf83VEey+IovdKIVaxg5r3bCvDKhXNn9LOGInyFodJTs1Sa33AvyoIo+kej6kMq035Bij3xF5cbSw0jHxrCBRaDDamgd4zy8B2TFM0XeHJ9bPjmu/xQGjYP7yH7a8ZjWg+Jyxc+1qlmqqi5GFgaYQbXvjRRbbPMBBmmo3eBVX9OIGzL798zzzqTU0HfbMfCFFDpiB2DwwoxvTGFuF+Wsy5iFm3LO+5HXi0fyrrYCcuFkejOuQSdV7eoYJq1A33rUFg7gi6cW31hyxGEjirQRcUOaNbKYTLnXUXN/KfwbEU2cwL7OjZ44A6CDMNdnFzl6zqIMtA6co6DKU0NfFV1wcmZ99LCNfL9wPZaSacN5Z8M7mm8UXbmBvFWk9AeZ+Loc6wGV5bcfuC9tE9" + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -1.7103475423959584 + } + ], + "usageMetadata": { + "promptTokenCount": 172, + "candidatesTokenCount": 26, + "totalTokenCount": 291, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 172 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 26 + } + ], + "thoughtsTokenCount": 93 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-08-31T04:21:20.578012Z", + "responseId": "wM2zaNyjI-C82PwP8uWe4A4" + } + recorded_at: Sun, 31 Aug 2025 04:21:21 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_specific_tool_choice.yml b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_specific_tool_choice.yml new file mode 100644 index 000000000..713d7e83b --- /dev/null +++ b/spec/fixtures/vcr_cassettes/chat_tool_choice_and_parallel_control_vertexai_gemini-2_5-flash_respects_specific_tool_choice.yml @@ -0,0 +1,300 @@ +--- +http_interactions: +- request: + method: post + uri: https://oauth2.googleapis.com/token + body: + encoding: ASCII-8BIT + string: grant_type=refresh_token&refresh_token=&client_id=&client_secret= + headers: + User-Agent: + - Faraday v2.13.4 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Date: + - Tue, 23 Sep 2025 23:40:19 GMT + Expires: + - Mon, 01 Jan 1990 00:00:00 GMT + Pragma: + - no-cache + Content-Type: + - application/json; charset=utf-8 + Vary: + - Origin + - Referer + - X-Origin + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: |- + { + "access_token": "", + "expires_in": 3599, + "scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/sqlservice.login openid https://www.googleapis.com/auth/cloud-platform", + "token_type": "Bearer", + "id_token": "" + } + recorded_at: Tue, 23 Sep 2025 23:40:19 GMT +- request: + method: post + uri: https://-aiplatform.googleapis.com/v1beta1/projects//locations//publishers/google/models/gemini-2.5-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"What''s the fall of Rome?"}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}],"toolConfig":{"functionCallingConfig":{"mode":"any","allowedFunctionNames":["weather"]}}}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 23:40:20 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "functionCall": { + "name": "weather", + "args": { + "longitude": "13.4050", + "latitude": "52.5200" + } + }, + "thoughtSignature": "Ct8CAR/MhbYiyJv/zjhmLZKv3KGwS6Ub8a+VgkWbOiqAiqZni0RcbOZKsBRaWAqO7ZujffAeNN2DlLr25BnU624oQINT98/UFUA9rTnslQAcUqnlMd5psNjzf2nv1SvGrMH4/Naac1L0pAbn2NpecBYfJxVuy1yHRK4W2qe4V77sZMT5ktzY0NC6n9QyNrR/9QhyJKRwfMTK9NAoENRzb5smcBcM/ZTRxGswltsr3BW2tGespKhqhn8CqUxl3+Ll6VZl1GO3C9ARsSqXAJUBI1SXHQ2ayIgHtGy38xDIy578N3hpbr9TqMV5E32HRjxVT03MuoGnK+5zZPQsZLouC6FrZ+KotfEfympgmUjFOKA35bsFUJ/+tP4rr3TR/rmAXdkje/g/rRqfuoiahiwN8/HMpo/CWhhmjCI9TszGjPXByZuwuytP5CxTs+oSO5kT98z8TcDkMQILHR85oyQtzU1x" + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -0.73671425090116616 + } + ], + "usageMetadata": { + "promptTokenCount": 52, + "candidatesTokenCount": 17, + "totalTokenCount": 133, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 52 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 17 + } + ], + "thoughtsTokenCount": 64 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-09-23T23:40:19.403263Z", + "responseId": "4y_TaL_OGIL9ld8PqbTR0Qo" + } + recorded_at: Tue, 23 Sep 2025 23:40:20 GMT +- request: + method: post + uri: https://oauth2.googleapis.com/token + body: + encoding: ASCII-8BIT + string: grant_type=refresh_token&refresh_token=&client_id=&client_secret= + headers: + User-Agent: + - Faraday v2.13.4 + Content-Type: + - application/x-www-form-urlencoded + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + response: + status: + code: 200 + message: OK + headers: + Pragma: + - no-cache + Cache-Control: + - no-cache, no-store, max-age=0, must-revalidate + Date: + - Tue, 23 Sep 2025 23:40:20 GMT + Expires: + - Mon, 01 Jan 1990 00:00:00 GMT + Content-Type: + - application/json; charset=utf-8 + Vary: + - Origin + - Referer + - X-Origin + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: |- + { + "access_token": "", + "expires_in": 3599, + "scope": "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/cloud-platform openid https://www.googleapis.com/auth/sqlservice.login", + "token_type": "Bearer", + "id_token": "" + } + recorded_at: Tue, 23 Sep 2025 23:40:20 GMT +- request: + method: post + uri: https://-aiplatform.googleapis.com/v1beta1/projects//locations//publishers/google/models/gemini-2.5-flash:generateContent + body: + encoding: UTF-8 + string: '{"contents":[{"role":"user","parts":[{"text":"What''s the fall of Rome?"}]},{"role":"model","parts":[{"functionCall":{"name":"weather","args":{"longitude":"13.4050","latitude":"52.5200"}}}]},{"role":"user","parts":[{"functionResponse":{"name":"3ec84e41-bb7b-4578-a062-a92cf3e3e4bb","response":{"name":"3ec84e41-bb7b-4578-a062-a92cf3e3e4bb","content":[{"text":"Current + weather at 52.5200, 13.4050: 15°C, Wind: 10 km/h"}]}}}]}],"generationConfig":{},"tools":[{"functionDeclarations":[{"name":"weather","description":"Gets + current weather for a location","parameters":{"type":"OBJECT","properties":{"latitude":{"type":"STRING","description":"Latitude + (e.g., 52.5200)"},"longitude":{"type":"STRING","description":"Longitude (e.g., + 13.4050)"}},"required":["latitude","longitude"]}}]}]}' + headers: + User-Agent: + - Faraday v2.13.4 + Authorization: + - Bearer + 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: + - Tue, 23 Sep 2025 23:40:21 GMT + Server: + - scaffolding on HTTPServer2 + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "role": "model", + "parts": [ + { + "text": "I am sorry, I cannot tell you about the fall of Rome with the available tools.", + "thoughtSignature": "CtsCAR/Mhba7U8MzvRalU8+ez57BrQ9yzdjTG/Tohpkadsnk013vgS3b0W3Wu5eY2smCrhDTbOaExj/r7qAXy81icgO29x0NIA96DH3T8MMqH7Xm/WSv8wQnibCAP6P8fc8seZG9ARBYbAw+GstPfHx/thiAyy6ToFZDTyGbi2JevJhsyxaKgomxaFgFm/0NreyjJ46abAYHVdd6zpro4R9KnFKQZiOQ1iWR34d9cst8QUsRll6rub+LyTwuhS9kco4PKbtoP+PHWs62df6v3XLbyBnv2sUu8GiI2bAe7Mzty5jZgotW4NDtWW1oWusv6eL8T4U8/uU8IG9kHKyrbm0QSHwdZREbNy6h6vtodi08W6K8jfJa8A2c6JUSsUzraYpv72jzOttTfNwwSEYgnkafLBZGcywDbG5L2CaFKK8xuk1M/aESDDTGdnp71IqnlOme5xSOaTY9OjATMfk=" + } + ] + }, + "finishReason": "STOP", + "avgLogprobs": -1.2237757576836481 + } + ], + "usageMetadata": { + "promptTokenCount": 171, + "candidatesTokenCount": 18, + "totalTokenCount": 253, + "trafficType": "ON_DEMAND", + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 171 + } + ], + "candidatesTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 18 + } + ], + "thoughtsTokenCount": 64 + }, + "modelVersion": "gemini-2.5-flash", + "createTime": "2025-09-23T23:40:20.524965Z", + "responseId": "5C_TaKWFIIL9ld8PqbTR0Qo" + } + recorded_at: Tue, 23 Sep 2025 23:40:21 GMT +recorded_with: VCR 6.3.1 diff --git a/spec/ruby_llm/chat_tools_spec.rb b/spec/ruby_llm/chat_tools_spec.rb index cff534172..483d5a0e1 100644 --- a/spec/ruby_llm/chat_tools_spec.rb +++ b/spec/ruby_llm/chat_tools_spec.rb @@ -377,6 +377,108 @@ def execute(query:) end end + describe 'tool choice and parallel control' do + CHAT_MODELS.each do |model_info| + model = model_info[:model] + provider = model_info[:provider] + + it "#{provider}/#{model} respects choice: :none" do + unless RubyLLM::Provider.providers[provider]&.local? + model_info = RubyLLM.models.find(model) + skip "#{model} doesn't support function calling" unless model_info&.supports_functions? + end + + provider_class = provider ? RubyLLM::Provider.providers[provider.to_sym] : nil + skip "#{provider} doesn't support tool choice" unless provider_class&.capabilities&.supports_tool_choice?(model) + + skip "Bedrock doesn't support :none tool choice" if provider == :bedrock + + chat = RubyLLM.chat(model: model, provider: provider) + .with_tool(Weather, choice: :none) + + tool_called = false + chat.on_tool_call do |_tool_call| + tool_called = true + end + + response = chat.ask("What's the weather in Berlin? (52.5200, 13.4050)") + + expect(tool_called).to be(false) + expect(response.content).not_to include('15°C') # Should not contain tool result + end + + it "#{provider}/#{model} respects choice: :required for unrelated queries" do + unless RubyLLM::Provider.providers[provider]&.local? + model_info = RubyLLM.models.find(model) + skip "#{model} doesn't support function calling" unless model_info&.supports_functions? + end + + provider_class = provider ? RubyLLM::Provider.providers[provider.to_sym] : nil + skip "#{provider} doesn't support tool choice" unless provider_class&.capabilities&.supports_tool_choice?(model) + + chat = RubyLLM.chat(model: model, provider: provider) + .with_tool(Weather, choice: :required) + + tool_called = false + chat.on_tool_call do |_tool_call| + tool_called = true + end + + # Ask about Roman history - completely unrelated to weather + chat.ask('When was the fall of Rome?') + + expect(tool_called).to be(true) # Tool should be forced to run + end + + it "#{provider}/#{model} respects specific tool choice" do + unless RubyLLM::Provider.providers[provider]&.local? + model_info = RubyLLM.models.find(model) + skip "#{model} doesn't support function calling" unless model_info&.supports_functions? + end + + provider_class = provider ? RubyLLM::Provider.providers[provider.to_sym] : nil + skip "#{provider} doesn't support tool choice" unless provider_class&.capabilities&.supports_tool_choice?(model) + + chat = RubyLLM.chat(model: model, provider: provider) + .with_tool(Weather, choice: :weather) + + tool_called = false + chat.on_tool_call do |_tool_call| + tool_called = true + end + + # Ask about Roman history - completely unrelated to weather + chat.ask("What's the fall of Rome?") + + expect(tool_called).to be(true) + end + + it "#{provider}/#{model} respects parallel: false for sequential execution" do + unless RubyLLM::Provider.providers[provider]&.local? + model_info = RubyLLM.models.find(model) + skip "#{model} doesn't support function calling" unless model_info&.supports_functions? + end + + provider_class = provider ? RubyLLM::Provider.providers[provider.to_sym] : nil + unless provider_class&.capabilities&.supports_tool_parallel_control?(model) + skip "#{provider} doesn't support tool parallel control" + end + + chat = RubyLLM.chat(model: model, provider: provider) + .with_tools(Weather, BestLanguageToLearn, parallel: false) + .with_instructions( + 'You must use both the weather tool for Berlin (52.5200, 13.4050) and the best language tool.' + ) + + chat.on_end_message do |message| + expect(message.tool_calls.length).to eq(1) if message.tool_call? + end + + chat.ask("What's the weather in Berlin and what's the best programming language?") + end + end + end + describe 'error handling' do it 'raises an error when tool execution fails' do chat = RubyLLM.chat.with_tool(BrokenTool)