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


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.

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

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

@nwumnn
Copy link
Author

nwumnn commented Aug 29, 2025

For the tests, I'm not sure what specific tests you'd like me to add. Any guidance would be great, thanks!

@crmne
Copy link
Owner

crmne commented Aug 29, 2025

@nwumnn what's your experience with testing so far?

Here's how I would do it: add a couple tests that call the real APIs of the providers. Check chat_tools_spec.rb and add things there. One could be: add a tool then define tool choice none and check the tool didn't run. Another: add a tool that has no reason to run (weather while you ask when was the fall of Rome for example), but set it to required and check it has run. For parallel function calling set two tools that have to run because of the query and set parallel to false so the AI has to run one at a time.

@nwumnn
Copy link
Author

nwumnn commented Aug 30, 2025

Got it, thanks for the clear guidance! I'll add those tests to chat_tools_spec.rb

@nwumnn
Copy link
Author

nwumnn commented Sep 1, 2025

While writing tests, I found that some providers have limitations:

  • DeepSeek, Gemini, VertexAI, Bedrock: don't support disabling parallel tool calls
  • Ollama and GPUStack: supports neither tool choice nor parallel tool calls

Should I add methods like supports_tool_choice? and supports_tool_parallel_control? to the provider capability modules?

@nwumnn nwumnn requested a review from crmne September 25, 2025 16:59
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