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-4-5') chat.with_instructions("Be terse.").with_tools(Weather)
ask runs the agentic loop to completion, executing tool calls until the model produces a final answer. ask_later, generate, run_tools, and step expose the individual moves of that loop.
A Chat is Enumerable over its messages.
Attributes
The prompt caching options set with with_caching, or nil.
The tool concurrency mode, or nil when tools run sequentially.
The Fallback models tried in order when generation fails.
Extra HTTP headers set with with_headers.
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.
Public Class Methods
# File lib/ruby_llm/chat.rb, line 69 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 = [] @tools = {} reset_tools @provider_options = {} @headers = {} @schema = nil @thinking = nil @citations = false @caching = nil @fallbacks = [] @fallback_errors = Fallback::DEFAULT_ERRORS @callbacks = Hash.new { |callbacks, name| callbacks[name] = [] } 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 567 def add_message(message_or_attributes) message = coerce_message(message_or_attributes) message = @provider.preprocess_message(message, model: @model, protocol: @protocol) if @provider messages << message message end
Source
# File lib/ruby_llm/chat.rb, line 524 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 496 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 511 def after_tool_result(&) add_callback(:after_tool_result, &) end
Registers a callback that receives each tool’s result after execution. Returns self.
# File lib/ruby_llm/chat.rb, line 103 def ask(message = nil, with: nil, &) ask_later(message, with: with) complete(&) end
Adds message to the conversation as a user message and runs the agentic loop to completion, executing tool calls along the way. Returns the final assistant Message. 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 118 def ask_later(message = nil, with: nil) 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)
Source
# File lib/ruby_llm/chat.rb, line 518 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 487 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 534 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 505 def before_tool_call(&) add_callback(:before_tool_call, &) end
Registers a callback that receives each 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 578 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 156 def complete(&) step(&) until complete? last_non_system_message || messages.last end
Source
# File lib/ruby_llm/chat.rb, line 163 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 model owes this chat nothing more: nothing is staged, or the model answered without calling a tool.
Source
# File lib/ruby_llm/chat.rb, line 550 def cost Cost.aggregate(messages.map { |message| message.cost(model: message.model_info || model) }) end
Returns a Cost aggregating the cost of every message in the conversation, priced by each message’s own model.
chat.cost.total
Source
# File lib/ruby_llm/chat.rb, line 541 def each(&) messages.each(&) end
Source
# File lib/ruby_llm/chat.rb, line 128 def generate(&) 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 557 def messages=(new_messages) @messages = message_list(new_messages).map { |message| coerce_message(message) } end
Source
# File lib/ruby_llm/chat.rb, line 598 def render @provider.render( messages, tools: @tools, tool_prefs: @tool_prefs, temperature: @temperature, max_output_tokens: @max_output_tokens, model: @model, provider_options: @provider_options, schema: @schema, thinking: @thinking, citations: @citations, caching: @caching, 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 138 def run_tools message = last_non_system_message execute_pending_tool_calls(message) if message&.tool_call? self end
Executes the tool calls pending in the latest response and appends their result messages, without asking the model to respond. 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 147 def step(&) return if complete? last_non_system_message&.tool_call? ? run_tools : generate(&) end
Advances the conversation by one move: runs the pending tool calls if the model asked for them, otherwise generates the next response. Returns nil once there is nothing left to do.
Source
# File lib/ruby_llm/chat.rb, line 382 def with_caching(options = {}) raise ArgumentError, 'To disable caching, use without_caching' if options.nil? @caching = 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: are passed through to providers that support them. Returns self.
chat.with_caching chat.with_caching(ttl: "1h")
Source
# File lib/ruby_llm/chat.rb, line 364 def with_citations @citations = true self end
Enables document citations, so the model backs its claims with quotes from attached files. Returns self.
chat.with_citations response = chat.ask "Who created Ruby?", with: "facts.txt" response.citations.each { |citation| puts citation.cited_text }
Source
# File lib/ruby_llm/chat.rb, line 397 def with_context(context) raise ArgumentError, 'To return to the global configuration, use without_context' if context.nil? @context = context @config = context.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. Returns self.
# File lib/ruby_llm/chat.rb, line 277 def with_fallbacks(*models, on: Fallback::DEFAULT_ERRORS) fallback_models = models.flatten.compact raise ArgumentError, 'To remove fallbacks, use without_fallbacks' if fallback_models.empty? @fallbacks = fallback_models.map { |model| Fallback.build(model) } @fallback_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. Returns self.
chat.with_fallbacks("gpt-4.1-mini", "claude-haiku-4-5")
Source
# File lib/ruby_llm/chat.rb, line 439 def with_headers(headers) raise ArgumentError, 'To clear headers, use without_headers' if headers.nil? @headers = headers.to_h self end
Sets extra HTTP headers sent with completion requests, replacing any previously set headers. Returns self.
chat.with_headers('anthropic-beta' => 'fine-grained-tool-streaming-2025-05-14')
# File lib/ruby_llm/chat.rb, line 179 def with_instructions(instructions, append: false) raise ArgumentError, 'To remove instructions, use without_instructions' if instructions.nil? without_instructions unless append @messages << Message.new(role: :system, content: instructions) 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. Returns self.
chat.with_instructions "You are a helpful Ruby tutor." chat.with_instructions "Use exactly one short paragraph.", append: true
# File lib/ruby_llm/chat.rb, line 318 def with_max_output_tokens(max_output_tokens) raise ArgumentError, 'To clear the limit, use without_max_output_tokens' if max_output_tokens.nil? @max_output_tokens = max_output_tokens self end
Caps the number of tokens the model may generate, mapping to each provider’s request field (max_tokens, max_output_tokens, maxOutputTokens, and so on). Returns self.
chat.with_max_output_tokens(1000)
# File lib/ruby_llm/chat.rb, line 263 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-4-5') chat.with_model('gpt-5.4', protocol: :chat_completions)
# File lib/ruby_llm/chat.rb, line 421 def with_provider_options(provider_options) raise ArgumentError, 'To clear provider options, use without_provider_options' if provider_options.nil? @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. Returns self.
chat.with_provider_options(max_output_tokens: 200)
Source
# File lib/ruby_llm/chat.rb, line 465 def with_schema(schema) raise ArgumentError, 'To remove the schema, use without_schema' if schema.nil? 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 RubyLLM::Schema class or instance, or any object responding to to_json_schema. Returns self.
class PersonSchema < RubyLLM::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}
# File lib/ruby_llm/chat.rb, line 298 def with_temperature(temperature) raise ArgumentError, 'To clear the temperature, use without_temperature' if temperature.nil? @temperature = temperature self end
Sets the sampling temperature for subsequent requests. Returns self.
chat.with_temperature(0.2)
# File lib/ruby_llm/chat.rb, line 341 def with_thinking(*args, effort: nil, budget: nil) raise ArgumentError, 'To clear the thinking configuration, use without_thinking' if args == [nil] raise ArgumentError, 'with_thinking accepts keyword options' unless args.empty? raise ArgumentError, 'with_thinking requires :effort or :budget' unless effort || budget @thinking = Thinking::Config.new(effort: effort, budget: budget) self end
Configures extended thinking for models that support it, with effort: (:low, :medium, :high, or :none) and/or budget: (a token count). Returns self.
Raises ArgumentError unless effort: or budget: is given.
chat.with_thinking(effort: :high, budget: 8000) chat.with_thinking(budget: 10_000)
# File lib/ruby_llm/chat.rb, line 231 def with_tool_options(choice: nil, calls: nil, concurrency: nil) update_tool_options(choice:, calls:) @concurrency = normalize_tool_concurrency(concurrency) unless concurrency.nil? 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. A nil option is left unchanged. Returns self.
chat.with_tools(Weather, Search).with_tool_options(choice: :required) chat.with_tool_options(calls: :one, concurrency: :threads)
Source
# File lib/ruby_llm/chat.rb, line 204 def with_tools(*tools) raise ArgumentError, 'To remove all tools, use without_tools' 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. Returns self.
chat.with_tools(Weather, Search) chat.with_tools(Weather).with_tool_options(choice: :required)
To replace the registered tools, clear them first with without_tools.
chat.without_tools.with_tools(NewTool)
Source
# File lib/ruby_llm/chat.rb, line 390 def without_caching @caching = nil self end
Disables prompt caching. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 370 def without_citations @citations = false self end
Disables document citations. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 408 def without_context @context = nil @config = RubyLLM.config with_model(@model.id, provider: @provider.slug, protocol: @protocol, assume_model_exists: true) self end
Removes the Context, returning the chat to the global RubyLLM.config. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 288 def without_fallbacks @fallbacks = [] @fallback_errors = Fallback::DEFAULT_ERRORS self end
Removes all fallback models and restores the default fallback error classes. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 447 def without_headers @headers = {} self end
Removes all extra HTTP headers. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 188 def without_instructions @messages.reject! { |message| message.role == :system } self end
Removes all system instructions from the conversation. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 327 def without_max_output_tokens @max_output_tokens = nil self end
Removes the output token limit, letting the provider use its default. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 429 def without_provider_options @provider_options = {} self end
Removes all provider request options. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 479 def without_schema @schema = nil self end
Removes the structured output schema, returning the chat to plain text responses. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 307 def without_temperature @temperature = nil self end
Removes the temperature override, returning the chat to the model’s default sampling behavior. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 352 def without_thinking @thinking = nil self end
Clears the thinking configuration, returning to the model’s default behavior. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 240 def without_tool_options @tool_prefs = { choice: nil, calls: nil } @concurrency = normalize_tool_concurrency(@config.tool_concurrency) self end
Resets the options set with with_tool_options: choice: and calls: return to unset, concurrency: to the configured default. Returns self.
Source
# File lib/ruby_llm/chat.rb, line 216 def without_tools @tools.clear self end
Removes all registered tools, leaving the options set with with_tool_options unchanged. Returns self.