Skip to content

86: Add default limit for tools completions #87

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e7f7a0f
feat: add limit to tools calls
rhys117 Apr 1, 2025
47c8742
docs: add docs for removing limit
rhys117 Apr 1, 2025
190e9de
docs: Fix docs
rhys117 Apr 1, 2025
fe837de
docs: improve doc - more accurate & organisation
rhys117 Apr 1, 2025
0f7ce26
bug: reset tools calls for each new 'ask' call
rhys117 Apr 1, 2025
6b807cf
bug: move error so tool result is still added
rhys117 Apr 2, 2025
df12db9
merge: main
rhys117 Apr 3, 2025
3af500d
chore: rework approach to rely on completions instead of tool calls
rhys117 Apr 4, 2025
39894a8
bug: fix attr_reader declaration to match number_of_tool_completions
rhys117 Apr 4, 2025
4718e2d
test: add better example
rhys117 Apr 7, 2025
7885c4b
docs: correct docs after changing naming/strategy
rhys117 Apr 7, 2025
585af65
merge: Merge branch 'main' into max-tool-calls
rhys117 Apr 7, 2025
df26988
docs: add default to docs
rhys117 Apr 7, 2025
19c0cd1
chore: rename error to match new naming
rhys117 Apr 7, 2025
3ad25f4
Merge branch 'main' into max-tool-calls
rhys117 Apr 24, 2025
d94d6dd
chore: reorder configuration accessor and add comment
rhys117 Apr 24, 2025
71dba53
style: cops - disable / fix
rhys117 Apr 24, 2025
6455f08
test: add spec for configured limit
rhys117 Apr 24, 2025
5a44bff
docs: adjust tools docs
rhys117 Apr 24, 2025
17b55cc
merge: main
rhys117 May 3, 2025
d09ce92
bug: ensure with_max_tool_completions available through acts_as helpers
rhys117 May 3, 2025
69af539
Merge branch 'main' into max-tool-calls
rhys117 Jun 11, 2025
018cc3b
deps: remove faker gem
rhys117 Jun 11, 2025
4cb6765
chore: use existing context instead of with_max_tool_completions
rhys117 Jun 11, 2025
6cb8ec3
test: adjust spec for context use
rhys117 Jun 11, 2025
4cdb924
chore: rename to max_tool_llm_calls
rhys117 Jun 12, 2025
f8fc8a5
docs: minor doc correction after rename
rhys117 Jun 12, 2025
731fcc6
chore: rename error class
rhys117 Jun 12, 2025
092ff2c
test: ensure spec cases for openrouter, openai, anthropic passing
rhys117 Jun 12, 2025
a0a1fa8
bug: fix +1 issue for llm tool lomts
rhys117 Jun 12, 2025
397438e
docs: improve tool docs for max tool llm calls
rhys117 Jun 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/guides/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,52 @@ Proper error handling within your `execute` method is crucial.

See the [Error Handling Guide]({% link guides/error-handling.md %}#handling-errors-within-tools) for more discussion.

## Maximum Tool LLM Requests

When including tools it is important to consider if the response could trigger unintended recursive calls to the provider. RubyLLM provides built-in protection by providing a default limit of 25, which can be overridden or turned off entirely.

Note this is a limit on the number of requests made to the provider, not the number of tool calls made by the application. **The limit is checked after all the requested tool executions have been performed**, this is to prevent the chat from
becoming invalid if you wish to continue the conversation after the error.

This can be performed on a per chat basis using `RubyLLM.context` (see configuration documentation) or provided in the global configuration.

```ruby
RubyLLM.configure do |config|
config.max_tool_llm_calls = 5
end
chat.ask "Question that triggers tools loop"
# => `execute_tool': Tool LLM calls limit reached: (RubyLLM::ToolCallLimitReachedError)
```

If you wish to remove this safe-guard you can set the max_tool_llm_calls to `nil`.
```ruby
RubyLLM.configure do |config|
config.max_tool_llm_calls = nil # No limit
end
chat = RubyLLM.chat
chat.ask "Question that triggers tools loop"
# Loops until you've used all your credit...
```

### Global Configuration

You can set a default maximum tool completion limit for all chats through the global configuration:

```ruby
RubyLLM.configure do |config|
# Default is 25 calls per conversation
config.max_tool_llm_calls = 10 # Set a more conservative limit
end
```

This setting can still be overridden per-chat when needed:

```ruby
# Override the global setting for this specific chat
context = RubyLLM.context { |ctx| ctx.max_tool_llm_calls = 5 }
chat = RubyLLM.chat.with_context(context)
```

## Security Considerations

{: .warning }
Expand All @@ -208,6 +254,7 @@ Treat any arguments passed to your `execute` method as potentially untrusted use
* **NEVER** use methods like `eval`, `system`, `send`, or direct SQL interpolation with raw arguments from the AI.
* **Validate and Sanitize:** Always validate parameter types, ranges, formats, and allowed values. Sanitize strings to prevent injection attacks if they are used in database queries or system commands (though ideally, avoid direct system commands).
* **Principle of Least Privilege:** Ensure the code within `execute` only has access to the resources it absolutely needs.
* **Cost-based Denial of Service:** Ensure protection against malicious input or usage, particularly when used in conjunction with tool calls if you remove the default limit.

## Next Steps

Expand Down
22 changes: 21 additions & 1 deletion lib/ruby_llm/chat.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ def initialize(model: nil, provider: nil, assume_model_exists: false, context: n
new_message: nil,
end_message: nil
}
@max_tool_llm_calls = @config.max_tool_llm_calls
@number_of_tool_llm_calls = 0
end

def ask(message = nil, with: nil, &)
@number_of_tool_llm_calls = 0

add_message role: :user, content: Content.new(message, with)
complete(&)
end
Expand Down Expand Up @@ -73,7 +77,10 @@ def with_temperature(temperature)

def with_context(context)
@context = context
@config = context.config
if context.config
@config = context.config
@max_tool_llm_calls = @config.max_tool_llm_calls
end
Comment on lines +80 to +83
Copy link
Contributor Author

@rhys117 rhys117 Jun 12, 2025

Choose a reason for hiding this comment

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

Wrapped this in a conditional to ensure that the config was present on the context. This more closely aligns with the assignment in the initialiser (@config = context&.config || RubyLLM.config)|

If I've made a mistake here, please let me know.

with_model(@model.id, provider: @provider.slug, assume_exists: true)
self
end
Expand Down Expand Up @@ -125,13 +132,20 @@ def reset_messages!
private

def handle_tool_calls(response, &)
@number_of_tool_llm_calls += 1

response.tool_calls.each_value do |tool_call|
@on[:new_message]&.call
result = execute_tool tool_call
message = add_tool_result tool_call.id, result
@on[:end_message]&.call(message)
end

# Perform this afterwards to ensure messages remain valid for the next call
if max_tool_llm_calls_reached?
raise ToolCallLimitReachedError, "Tool LLM calls limit reached: #{@max_tool_llm_calls}"
end

complete(&)
end

Expand All @@ -148,5 +162,11 @@ def add_tool_result(tool_use_id, result)
tool_call_id: tool_use_id
)
end

def max_tool_llm_calls_reached?
return false unless @max_tool_llm_calls

@number_of_tool_llm_calls >= @max_tool_llm_calls
end
end
end
5 changes: 5 additions & 0 deletions lib/ruby_llm/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class Configuration
:bedrock_session_token,
:openrouter_api_key,
:ollama_api_base,
# Default tool configuration
:max_tool_llm_calls,
# Default models
:default_model,
:default_embedding_model,
Expand Down Expand Up @@ -54,6 +56,9 @@ def initialize
@default_embedding_model = 'text-embedding-3-small'
@default_image_model = 'dall-e-3'

# Default restrictions
@max_tool_llm_calls = 25

# Logging configuration
@log_file = $stdout
@log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_llm/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ConfigurationError < StandardError; end
class InvalidRoleError < StandardError; end
class ModelNotFoundError < StandardError; end
class UnsupportedFunctionsError < StandardError; end
class ToolCallLimitReachedError < StandardError; end
class UnsupportedAttachmentError < StandardError; end

# Error classes for different HTTP status codes
Expand Down
Loading