Skip to content

Conversation

nwumnn
Copy link

@nwumnn nwumnn commented Aug 15, 2025

What this does

Adds tool choice and parallel tool calls options to the with_tool and with_tools method.

Users can now specify:

  • choice: control how the model should use tools (auto, none, required, or specific tool name)
  • parallel: boolean parameter to allow/disallow parallel tool calls
chat.with_tools(MyTool, choice: :required, parallel: false)
chat.with_tool(SpecificTool, choice: :specific_tool, parallel: false)

Also updates the handle_tool_calls method. With :required or specific tool choices, the tool_choice is automatically reset to nil after tool execution to prevent infinite loops.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Performance improvement

Scope check

  • I read the Contributing Guide
  • This aligns with RubyLLM's focus on LLM communication
  • This isn't application-specific logic that belongs in user code
  • This benefits most users, not just my specific use case

Quality check

  • I ran overcommit --install and all hooks pass
  • I tested my changes thoroughly
  • I updated documentation if needed
  • I didn't modify auto-generated files manually (models.json, aliases.json)

API changes

  • Breaking change
  • New public methods/classes
  • Changed method signatures
  • No API changes

Related issues

Fixes #343

nwumnn added 3 commits August 14, 2025 16:55
- Implement provider-specific tool choice handling
- Add InvalidToolChoiceError for validation
- Enhance tool execution flow to prevent infinite loops with non-auto choices
Comment on lines 194 to 203
# 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
```
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both examples of with_tool and with_tools should contain both choice and parallel parameters otherwise people get the false sense that one parameter is for one call only.

Comment on lines 199 to 209
halt_result || complete(&)
return halt_result if halt_result

should_continue_after_tools? ? complete(&) : response
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The solution to this is not to halt the conversation, but to reset tool_choice to auto after tools have been called.

Halting the conversation is not normal behavior and it's for very specific use cases. More info here: https://community.openai.com/t/infinite-loop-with-tool-choice-required-or-type-function/755129

@@ -205,6 +215,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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as mentioned in the issue, I want the API to look like auto, none or required as required captures better what's going to happen.

@@ -5,7 +5,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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to compress these two into one tool_prefs = {choice: ..., parallel: ...}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should choice and parallel default to nil (like temperature) to use provider defaults, or should we set them explicitly to :auto and true?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely set them to nil

Copy link
Owner

@crmne crmne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good. These are finishing touches. It now needs tests.

Comment on lines +189 to +215
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')

# 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)

# Force specific tool
chat.with_tool(Weather, choice: :weather, parallel: true)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer that to be code first, then parameter values. Also when specifying a tool in choice, it should accept the tool class too.

Comment on lines +20 to 22
@tool_prefs = { choice: nil, parallel: nil }
@messages = []
@tools = {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small thing, but make it after @tools

@@ -130,6 +135,7 @@ def complete(&) # rubocop:disable Metrics/PerceivedComplexity
params: @params,
headers: @headers,
schema: @schema,
tool_prefs: @tool_prefs,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pass it after tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEATURE] Add built-in support for tool control parameters
2 participants