class RubyLLM::Chat
A Chat is a conversation with an AI model. It holds the messages exchanged so far, the tools the model may call, and the settings applied to each request. RubyLLM.chat is the usual way to create one.
chat = RubyLLM.chat chat.ask "What's the best way to learn Ruby?"
Configuration methods return self, so calls chain:
chat = RubyLLM.chat(model: 'claude-sonnet-5') chat.with_instructions("Be terse.").with_tools(Weather)
ask runs the conversation loop, executing tools until the model answers or a call needs approval. ask_later, generate, run_tools, and step expose individual operations. Resume an approval pause with approve or deny followed by complete.
A Chat is Enumerable over its messages.
Constants
- COMPACTION_OPTIONS
-
The provider-neutral options
with_compactionaccepts.
Attributes
The prompt caching options set with with_caching, false when explicitly disabled, or nil when not configured.
Whether with_citations asked the provider for citations.
The context compaction options set with with_compaction, false when explicitly disabled, or nil when not configured.
The tool concurrency mode, or nil when tools run sequentially.
The Context this chat sends requests through, or nil for the global configuration.
The opaque per-user identifier set with with_end_user, or nil.
The Fallback models tried in order when generation fails.
Extra HTTP headers set with with_headers.
The output cap set with with_max_output_tokens, or nil.
The Message objects exchanged so far, including system instructions.
The Provider instance handling requests for the current model.
Extra request options set with with_provider_options, expressed in the provider’s request vocabulary.
The normalized structured output schema set with with_schema, or nil.
The server tools enabled with with_server_tools, as an array of normalized entry Hashes.
The sampling temperature set with with_temperature, or nil to let the model use its default.
Public Class Methods
# File lib/ruby_llm/chat.rb, line 109 def initialize(model: nil, provider: nil, protocol: nil, assume_model_exists: false, context: nil) if assume_model_exists && !provider raise ArgumentError, 'Provider must be specified if assume_model_exists is true' end @context = context @config = context&.config || RubyLLM.config with_model(model, provider: provider, protocol: protocol, assume_model_exists: assume_model_exists) @temperature = nil @max_output_tokens = nil @messages = [] @usage_entries = [] @tools = {} @server_tools = [] @tool_prefs = { choice: nil, calls: nil } @concurrency = normalize_tool_concurrency(@config.tool_concurrency) @provider_options = {} @headers = {} @schema = nil @thinking = nil @citations = false @caching = nil @compaction = nil @end_user = nil @fallbacks = [] @fallback_errors = Fallback::DEFAULT_ERRORS @callbacks = Hash.new { |callbacks, name| callbacks[name] = [] } @cancelled = false @cancellation_checker = nil @tool_call_decisions = {} @approval_checker = nil end
Creates a chat with model:, or with the configured default model when model: is nil. Most code calls RubyLLM.chat instead.
A model is identified by its name, an optional provider:, and an optional protocol:. Pass provider: to disambiguate models available from several providers, and protocol: to override the wire protocol the provider would otherwise pick for the model. With assume_model_exists: true the registry lookup is skipped, which requires provider:. Pass a Context as context: to use its configuration instead of the global one.
Public Instance Methods
# File lib/ruby_llm/chat.rb, line 809 def add_message(message_or_attributes) message = coerce_message(message_or_attributes) messages << message message end
Source
# File lib/ruby_llm/chat.rb, line 694 def after_fallback(&) add_callback(:after_fallback, &) end
Registers a callback that receives the Fallback attempt once it has succeeded or failed. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 666 def after_message(&) add_callback(:after_message, &) end
Registers a callback that receives each assistant response and each tool result message once it has been appended. Returns self.
chat.after_message { |message| puts message.content }
Source
# File lib/ruby_llm/chat.rb, line 681 def after_tool_result(&) add_callback(:after_tool_result, &) end
Registers a callback that receives each local tool’s result after execution. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 252 def approve(tool_call) record_tool_call_decision(tool_call, true) end
# File lib/ruby_llm/chat.rb, line 153 def ask(message = nil, with: nil, &) ask_later(message, with: with) complete(&) end
Adds message as a user message and runs the conversation loop, executing tools until the model answers or a call needs approval. Returns the latest assistant Message; check awaiting_approval? before treating it as a final answer. Attach files with with:. A given block receives streamed Chunk objects as they arrive.
chat.ask "What's the best way to learn Ruby?" chat.ask "What's in this image?", with: "ruby_conf.jpg" chat.ask "Analyze these files", with: ["diagram.png", "report.pdf"] chat.ask("Tell me a story") { |chunk| print chunk.content }
# File lib/ruby_llm/chat.rb, line 171 def ask_later(message = nil, with: nil) raise_if_pending_tool_calls! add_message role: :user, content: message, attachments: with self end
Stages message as a user message without requesting a completion, leaving the chat ready for complete, a single step, or a provider-side batch via RubyLLM.batch. Accepts attachments with with: like ask. Returns self.
chats = tickets.map { |t| RubyLLM.chat.ask_later(t.body) } RubyLLM.batch(chats)
Raises PendingToolCallsError while the last response has unanswered tool calls: finish the round first, recording approve or deny decisions for calls that require approval.
Source
# File lib/ruby_llm/chat.rb, line 274 def awaiting_approval? response = pending_tool_response return false unless response pending = pending_tool_calls(response) pending.any? && pending.all? { |_, tool_call| approval_pending?(tool_call) } end
Returns whether the conversation can make no progress without an approval decision: every remaining pending tool call requires approval and has none recorded. While true, complete returns without executing them; record decisions with approve or deny, then call complete again. Tool calls that need no approval still execute before the loop pauses.
Consults each pending tool’s approval resolver when one is declared, so resolvers must be idempotent reads.
Source
# File lib/ruby_llm/chat.rb, line 688 def before_fallback(&) add_callback(:before_fallback, &) end
Registers a callback that receives the Fallback attempt after the current model fails and before the fallback model is tried. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 657 def before_message(&) add_callback(:before_message, &) end
Registers a callback that runs before each assistant response or tool result is appended to the conversation. Callbacks are additive: every registered block runs. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 704 def before_request(&) add_callback(:before_request, &) end
Registers a callback that receives the fully rendered request payload before it is sent and may mutate it in place. Runs after all RubyLLM formatting and with_provider_options merging. Returns self.
chat.before_request { |payload| logger.debug payload }
Source
# File lib/ruby_llm/chat.rb, line 675 def before_tool_call(&) add_callback(:before_tool_call, &) end
Registers a callback that receives each local ToolCall before the tool executes. Returns self.
chat.before_tool_call { |tool_call| puts tool_call.name }
Source
# File lib/ruby_llm/chat.rb, line 819 def cache_until_here message = messages.last raise ArgumentError, 'No messages to cache' unless message message.cache_until_here self end
Marks the latest message as an explicit prompt cache boundary, asking the provider to cache everything up to this point. Returns self.
Raises ArgumentError if the chat has no messages.
Source
# File lib/ruby_llm/chat.rb, line 299 def cancel @cancelled = true self end
Cancels the current in-flight chat operation. The next cancellation checkpoint raises CancelledError and clears the flag so the chat can be reused.
Source
# File lib/ruby_llm/chat.rb, line 305 def cancelled? @cancelled end
Returns whether this in-memory chat has been marked for cancellation.
Source
# File lib/ruby_llm/chat.rb, line 773 def compact raise_if_cancelled! raise_if_pending_tool_calls! usage_start = usage_entries.length payload = instrumentation_payload(streaming: false) RubyLLM.instrument('compaction.ruby_llm', payload, config: @config) do |event| result = provider_compaction record_out_of_band_usage(result) if usage_entries.length == usage_start record_generated_message(result, usage_start) record_completion_event(event, result) result end end
Compacts the conversation’s model context and returns an assistant Message. The message can have empty text and carries the provider’s compacted context internally. Every earlier message remains in messages, including on persisted Rails chats.
chat.ask "Remember these project requirements..." chat.compact chat.ask "Which requirement should we implement first?"
Uses the current instructions, headers, and request hooks. Records reported usage and runs the normal message callbacks. Raises Error when the provider has no manual compaction endpoint, and PendingToolCallsError until pending tool calls have been answered.
Source
# File lib/ruby_llm/chat.rb, line 229 def complete(&) step(&) until complete? || awaiting_approval? last_non_system_message || messages.last end
Runs the conversation loop until complete? or awaiting_approval? is true. Returns the last conversation Message, or nil for an empty chat. Used after ask_later; ask calls complete for you.
When a pending tool call requires approval and no decision has been recorded, the loop pauses. Record approve or deny decisions, then call complete again to continue.
Source
# File lib/ruby_llm/chat.rb, line 236 def complete? last = last_non_system_message case last&.role when nil then true when :user, :tool then false else !last.tool_call? end end
Returns whether the chat has no pending response or tool execution: nothing is staged, or the model answered without requesting tools.
Source
# File lib/ruby_llm/chat.rb, line 729 def cost Cost.aggregate(usage_entries.map(&:cost), complete: usage_entries.all?(&:cost_available?)) end
Returns a Cost aggregating every provider attempt this chat has made, including retries and attempts that produced no message.
chat.cost.total
Source
# File lib/ruby_llm/chat.rb, line 744 def count_tokens(message = nil) request_messages = messages.dup request_messages << coerce_message(role: :user, content: message) unless message.nil? @provider.count_tokens( preprocessed_messages(request_messages), model: @model, tools: @tools, tool_prefs: @tool_prefs, thinking: resolved_thinking, schema: @schema, citations: @citations, caching: @caching, protocol: @protocol ) end
Counts input tokens for the conversation, including instructions, function tools, structured output, thinking, and attachments. Pass message to include it as a staged user message without mutating the chat. Returns an Integer.
chat.with_instructions("Be terse.").with_tools(Weather) chat.count_tokens("What's the weather in Berlin?")
Server tools, provider_options, compaction, and before_request hooks are not included. Raises Error when the provider has no token counting endpoint.
Source
# File lib/ruby_llm/chat.rb, line 261 def deny(tool_call) record_tool_call_decision(tool_call, false) end
Source
# File lib/ruby_llm/chat.rb, line 711 def each(&) messages.each(&) end
Source
# File lib/ruby_llm/chat.rb, line 182 def generate(&) raise_if_cancelled! return generate_once(&) if fallbacks.empty? with_model_restored { generate_with_fallbacks(&) } end
Requests one completion from the model, appends the response to the conversation, and returns it as a Message. Honors the fallbacks configured with with_fallbacks. A given block receives streamed Chunk objects. Tool calls in the response are not executed; that is run_tools.
Source
# File lib/ruby_llm/chat.rb, line 790 def messages=(new_messages) @messages = message_list(new_messages).map { |message| coerce_message(message) } end
Source
# File lib/ruby_llm/chat.rb, line 289 def pending_approvals response = pending_tool_response return [] unless response pending_tool_calls(response).values.select { |tool_call| approval_pending?(tool_call) } end
Returns the tool calls from the latest response that require approval and have no recorded decision, as an array of ToolCall objects. Pairs with approve and deny. ToolCall#remote? identifies provider-executed calls.
chat.pending_approvals.each { |tool_call| puts tool_call.name } chat.approve(chat.pending_approvals.first)
Source
# File lib/ruby_llm/chat.rb, line 844 def render @provider.render( preprocessed_messages, tools: @tools, server_tools: @server_tools, tool_prefs: @tool_prefs, temperature: @temperature, max_output_tokens: @max_output_tokens, model: @model, provider_options: Support::Utils.deep_dup(@provider_options), schema: @schema, thinking: resolved_thinking, citations: @citations, caching: @caching, compaction: @compaction, end_user: @end_user, protocol: @protocol, before_request: @callbacks[:before_request] ) end
Returns the request payload this chat would send to the provider for its next completion, with before_request hooks applied. Useful for inspecting and testing request output.
Source
# File lib/ruby_llm/chat.rb, line 199 def run_tools raise_if_cancelled! message = pending_tool_response execute_pending_tool_calls(message) if message self end
Executes the tool calls pending in the latest response and appends their result messages, without asking the model to respond. Tool calls that already have results are skipped, so a chat reloaded mid-round resumes with only the remaining tools. Calls whose tool was declared with Tool.requires_approval only execute once approve records a decision; denied calls receive a structured denial result, and undecided calls stay pending. Does nothing when no tool calls are pending. The chat is then ready for the next generate, or the next batch round. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 211 def step(&) return if complete? raise_if_cancelled! return generate(&) unless pending_tool_response before = messages.length run_tools messages.last if messages.length > before end
Advances the conversation by one move: runs the pending tool calls if any are unanswered, otherwise generates the next response. Returns the Message that move produced, and nil once there is nothing left to do or the loop is parked on an approval.
Source
# File lib/ruby_llm/chat.rb, line 499 def thinking config = resolved_thinking return unless config { effort: config.effort, budget: config.budget, display: config.display, enabled: config.enabled }.compact end
Returns the thinking options resolved for the current model, or nil when thinking was not configured or needs no provider control.
Source
# File lib/ruby_llm/chat.rb, line 720 def tokens Tokens.aggregate(usage_entries.map(&:tokens)) end
Returns token usage aggregated across every provider attempt this chat has made, including retries and attempts that produced no message.
chat.tokens.input
Source
# File lib/ruby_llm/chat.rb, line 95 def tool_options { choice: tool_prefs[:choice], calls: tool_prefs[:calls], concurrency: concurrency } end
Returns the choice, calls, and concurrency set with with_tool_options, with nil for anything left at the default.
Source
# File lib/ruby_llm/chat.rb, line 540 def with_caching(options = {}) options = {} if options == true unless options == false || options.is_a?(Hash) raise ArgumentError, 'with_caching accepts true, false, or caching options' end @caching = options == false ? false : options.transform_keys(&:to_sym).freeze self end
Enables provider prompt caching. With no arguments the provider’s default behavior applies; options such as ttl: apply where supported. Pass id: with a CachedContent (or its name) from RubyLLM.cache to attach an explicit content cache. Pass false to stop RubyLLM from sending cache controls or rendering explicit cache boundaries. A provider may still cache prompts implicitly. Passing nil raises ArgumentError. Returns self.
chat.with_caching chat.with_caching(ttl: "1h") chat.with_caching(id: cache) chat.with_caching(false)
# File lib/ruby_llm/chat.rb, line 519 def with_citations(enabled = true) raise ArgumentError, 'with_citations accepts true or false' unless [true, false].include?(enabled) @citations = enabled self end
Enables document citations, so the model backs its claims with quotes from attached files. Pass false to disable. Passing nil raises ArgumentError. Returns self.
chat.with_citations response = chat.ask "Who created Ruby?", with: "facts.txt" response.citations.each { |citation| puts citation.cited_text }
# File lib/ruby_llm/chat.rb, line 574 def with_compaction(options = {}) options = {} if options == true unless options == false || options.is_a?(Hash) raise ArgumentError, 'with_compaction accepts true, false, or compaction options' end @compaction = options == false ? false : normalize_compaction(options) self end
Enables provider-side context compaction, so a long conversation keeps going instead of overflowing the context window. The provider condenses the earlier turns itself and returns a block that RubyLLM replays on later requests. With no arguments the provider’s own defaults apply. The options are provider-neutral:
at-
the input-token count that triggers compaction.
instructions-
a custom prompt for the summary the provider writes.
pause_after-
end the turn once compaction runs, instead of continuing straight into the answer.
Each provider applies the options it supports. Unsupported options are ignored with a debug log. Pass false to disable; passing nil raises ArgumentError. Returns self.
chat.with_compaction chat.with_compaction(at: 50_000) chat.with_compaction(at: 100_000, instructions: "Keep every decision.") chat.with_compaction(false)
What a provider does when the threshold is crossed differs. Anthropic and OpenAI summarize the compacted span into an opaque block that replaces it; OpenRouter drops messages from the middle of the conversation instead, and has no threshold of its own.
Source
# File lib/ruby_llm/chat.rb, line 601 def with_context(context) @context = context @config = context&.config || RubyLLM.config with_model(@model.id, provider: @provider.slug, protocol: @protocol, assume_model_exists: true) self end
Rebinds the chat to context, a Context built with RubyLLM.context, so subsequent requests use its configuration. Pass nil to return to the global RubyLLM.config. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 593 def with_end_user(end_user) @end_user = end_user self end
Identifies the end user behind the conversation for the provider’s abuse monitoring. Providers without an equivalent field omit it. Pass nil to remove it. Returns self.
chat.with_end_user("user-123").ask "Hello"
The value is sent as given, so use an opaque id such as a hash of your user id, never personal data.
# File lib/ruby_llm/chat.rb, line 433 def with_fallbacks(*models, on: Fallback::DEFAULT_ERRORS) fallback_models = models.flatten.compact @fallbacks = fallback_models.map { |model| Fallback.build(model) } @fallback_errors = fallback_models.empty? ? Fallback::DEFAULT_ERRORS : Array(on).flatten.compact self end
Sets fallback models to try, in order, when generation fails. on: selects the error classes that trigger a fallback; the default covers transient provider and network errors. Pass nil to remove all fallbacks and restore the default error classes. Returns self.
chat.with_fallbacks("gpt-4.1-mini", "claude-haiku-4-5") chat.with_fallbacks(nil)
Source
# File lib/ruby_llm/chat.rb, line 624 def with_headers(headers) @headers = headers.to_h self end
Sets extra HTTP headers sent with completion requests, replacing any previously set headers; nil clears them. Returns self.
chat.with_headers('anthropic-beta' => 'fine-grained-tool-streaming-2025-05-14')
# File lib/ruby_llm/chat.rb, line 319 def with_instructions(instructions, append: false, cache_until_here: false) @messages.reject! { |message| message.role == :system } unless append @messages << Message.new(role: :system, content: instructions) unless instructions.nil? @messages.last.cache_until_here if instructions && cache_until_here self end
Sets the system instructions for the conversation, replacing any existing system messages. With append: true the instructions are added alongside the existing ones. With cache_until_here: true the instruction becomes an explicit prompt cache boundary. Pass nil to remove all system instructions. Returns self.
chat.with_instructions "You are a helpful Ruby tutor." chat.with_instructions "Use exactly one short paragraph.", append: true chat.with_instructions nil
# File lib/ruby_llm/chat.rb, line 456 def with_max_output_tokens(max_output_tokens) @max_output_tokens = max_output_tokens self end
Caps the number of tokens the model may generate. Pass nil to remove the limit. Returns self.
chat.with_max_output_tokens(1000)
# File lib/ruby_llm/chat.rb, line 417 def with_model(model_id, provider: nil, protocol: nil, assume_model_exists: false) model_id ||= @config.default_model @model, @provider = Models.resolve(model_id, provider:, assume_model_exists:, config: @config) @connection = @provider.connection @protocol = protocol self end
Switches the chat to model_id and its provider. Pass provider: to disambiguate, and assume_model_exists: true to skip registry validation for custom or private models. Pass nil to return to the configured default model. Returns self.
protocol: overrides the wire protocol the provider would pick for the model, such as :responses or :chat_completions for OpenAI. It stays nil by default, meaning the provider chooses the protocol for each request. A bare with_model resets the override to nil, just as it re-resolves the provider from the model.
Raises ModelNotFoundError if model_id is not in the registry and assume_model_exists: is false.
chat.with_model('claude-sonnet-5') chat.with_model('gpt-5.6', protocol: :chat_completions)
# File lib/ruby_llm/chat.rb, line 614 def with_provider_options(provider_options) @provider_options = provider_options.to_h self end
Sets options in the provider’s request vocabulary, merged into the request payload as-is and overriding RubyLLM’s defaults. Replaces any previously set provider options; nil clears them. Returns self.
chat.with_provider_options(service_tier: "flex")
Source
# File lib/ruby_llm/chat.rb, line 644 def with_schema(schema) schema_instance = schema.is_a?(Class) ? schema.new : schema @schema = normalize_schema_payload( schema_instance.respond_to?(:to_json_schema) ? schema_instance.to_json_schema : schema_instance ) self end
Sets the schema for structured output. Accepts a JSON Schema Hash, a Schematist::Schema class or instance, or any object responding to to_json_schema. Returns self.
class PersonSchema < Schematist::Schema string :name integer :age end chat.with_schema(PersonSchema) response = chat.ask("Generate a person named Alice who is 30 years old") response.parsed # => {"name" => "Alice", "age" => 30}
Pass nil to remove the schema, returning the chat to plain text responses.
# File lib/ruby_llm/chat.rb, line 365 def with_server_tools(*tools, **tools_with_options) if tools == [nil] && tools_with_options.empty? @server_tools = [] return self end @server_tools += RubyLLM::Tools::ServerTools.normalize(tools, tools_with_options) self end
Enables tools that run on the provider’s servers, such as web search or code execution. Accepts portable alias Symbols, alias-with-options keywords whose options use the provider’s own vocabulary, and raw Hashes passed to the provider verbatim, so provider tools RubyLLM has no alias for yet work without a gem update. Entries add to any tools enabled earlier; pass nil to clear them all. Returns self.
chat.with_server_tools(:web_search) chat.with_server_tools(:web_search, :code_execution) chat.with_server_tools(web_search: { allowed_domains: ["ruby-lang.org"] }) chat.with_server_tools({ type: "web_search_20260318", name: "web_search" })
The tool steps the model ran come back on Message#server_tool_calls, citations from search tools on Message#citations, and per-use billing counters on message.tokens.server_tool_use.
Raises UnsupportedServerToolError at request time when the provider has no server-tool support or does not define a requested alias.
# File lib/ruby_llm/chat.rb, line 445 def with_temperature(temperature) @temperature = temperature self end
Sets the sampling temperature for subsequent requests. Pass nil to return to the model’s default sampling behavior. Returns self.
chat.with_temperature(0.2)
# File lib/ruby_llm/chat.rb, line 477 def with_thinking(enabled = true, **options) # rubocop:disable Metrics/PerceivedComplexity return with_thinking(**enabled.transform_keys(&:to_sym), **options) if enabled.is_a?(Hash) raise ArgumentError, 'with_thinking accepts false or thinking options' unless [true, false].include?(enabled) raise ArgumentError, 'with_thinking(false) does not accept options' if !enabled && options.any? raise ArgumentError, 'thinking options cannot be nil; pass false to disable' if options.value?(nil) if (unsupported = options.keys - THINKING_OPTIONS).any? raise ArgumentError, "with_thinking accepts #{format_option_keys(THINKING_OPTIONS)}, " \ "got #{format_option_keys(unsupported)}" end @thinking = if enabled options.empty? ? Thinking::Config.default : Thinking::Config.new(**options) else Thinking::Config.disabled end self end
Configures extended thinking for models that support it. With no arguments, RubyLLM uses the current model’s registered default. Pass false to disable thinking, or tune it with effort: (:low, :medium, :high, :none, or a provider-specific tier such as :minimal, :xhigh, or :max, passed through as-is), budget: (a token count), and display: (:summarized or :omitted, controlling whether providers that support it return readable thinking text). Accepts keywords or an options Hash. Passing nil raises ArgumentError. Returns self.
chat.with_thinking chat.with_thinking(false) chat.with_thinking(effort: :high) chat.with_thinking(budget: 10_000) chat.with_thinking(display: :summarized)
# File lib/ruby_llm/chat.rb, line 388 def with_tool_options(**options) options.each do |option, value| case option when :choice then apply_tool_choice(value) when :calls then @tool_prefs[:calls] = value.nil? ? nil : normalize_calls(value) when :concurrency then @concurrency = normalize_tool_concurrency(value.nil? ? @config.tool_concurrency : value) else raise ArgumentError, "Unknown tool option: #{option}. Valid options are: choice, calls, concurrency" end end self end
Configures how the model uses the registered tools. choice: constrains tool use to :auto, :none, :required, a tool name, or a Tool class. calls: limits how many tool calls one response may contain (:many or :one). concurrency: runs tool calls concurrently: true or :threads for threads, :fibers for fibers. An omitted option is left unchanged; passing nil explicitly resets that option (+concurrency: nil+ returns to the configured default). Returns self.
chat.with_tools(Weather, Search).with_tool_options(choice: :required) chat.with_tool_options(calls: :one, concurrency: :threads) chat.with_tool_options(choice: nil)
Source
# File lib/ruby_llm/chat.rb, line 337 def with_tools(*tools) @tools.clear if tools == [nil] tools.flatten.compact.each do |tool| tool_instance = tool.is_a?(Class) ? tool.new : tool @tools[tool_instance.name.to_sym] = tool_instance end self end
Registers tools, each a Tool class or instance, for the model to call. Configure how the model uses them with with_tool_options. Pass nil to remove all registered tools. Returns self.
chat.with_tools(Weather, Search) chat.with_tools(Weather).with_tool_options(choice: :required)
To replace the registered tools, clear them first:
chat.with_tools(nil).with_tools(NewTool)