Extended Thinking
Give reasoning models more time and budget to deliberate, with optional access to thinking output
After reading this guide, you will know:
- How to control extended thinking with
with_thinking - How effort and budget are sent to providers
- How to access thinking output in responses and streams
- How to persist thinking data with ActiveRecord
What is Extended Thinking?
Extended Thinking gives supported models more time and a larger computation budget to deliberate before answering. It can improve results on multi-step tasks like coding, math, and logic, at the expense of latency and cost. Some providers can also return a thinking trace or signature alongside the final answer.
Controlling Extended Thinking
Call with_thinking without options to enable thinking with the current model’s registered controls:
chat = RubyLLM.chat(model: 'claude-opus-4-5').with_thinking
RubyLLM resolves those controls when it builds the request, so it follows later model changes and fallbacks. The registry may select an effort, a token budget, a provider toggle, or no request option for a model that always thinks. If the registry does not know how to enable thinking for the selected model, RubyLLM raises an ArgumentError instead of guessing.
When the registry lists several controls, RubyLLM prefers an explicit default, then a provider toggle, then medium effort when available, then the smallest positive token budget the model accepts. Each protocol then sends what its provider needs. Claude never turns thinking on from an effort alone, so RubyLLM adds the thinking block that generation takes. See Provider Notes.
Pass keyword options or a Hash to configure thinking explicitly:
chat = RubyLLM.chat(model: 'claude-opus-4-5')
.with_thinking(effort: :high, budget: 8000)
response = chat.ask("What is 15 * 23?")
response.thinking&.text
response.thinking&.signature
response.content
response.thinking.text combines the visible thinking text. RubyLLM also keeps the individual signed and encrypted blocks for later turns. When you restore a conversation, keep the complete messages instead of rebuilding thinking from text and signature alone.
Thinking stays with the provider that produced it. When with_model moves a chat to another provider, earlier turns replay without their thinking, because no provider can verify another provider’s signature. The messages themselves keep it.
Pass effort, budget, or both:
chat.with_thinking(effort: :low)
chat.with_thinking(budget: 10_000)
chat.with_thinking({ effort: :high })
chat.with_thinking(effort: :none)
Pass false to disable thinking:
chat.with_thinking(false)
Disabling is model-specific too. RubyLLM sends the registered off control and raises an ArgumentError when the model does not expose one. Passing nil is invalid.
Effort and Budget
Use effort to pick a qualitative depth (:low, :medium, :high) and budget for models that accept a token cap.
RubyLLM sends effort and budget exactly as provided, and never rewrites a value to one the registry believes a model takes. Check your provider’s docs for supported values. When a model does not accept the value you picked, the request fails with the provider’s own error, which names the parameter and usually lists the values it takes.
Display
Some providers let you choose how much of the thinking comes back with the response. Pass display: and RubyLLM forwards the value in the provider’s request shape:
chat.with_thinking(effort: :high, display: :summarized) # a summary of the thinking
chat.with_thinking(effort: :high, display: :omitted) # think, but return no thinking text
Streaming with Thinking
Thinking content is delivered alongside normal content in streaming chunks:
chat = RubyLLM.chat(model: 'claude-opus-5')
.with_thinking(effort: :medium, display: :summarized)
chat.ask("Solve this step by step: What is 127 * 43?") do |chunk|
print chunk.thinking&.text
print chunk.content
end
Some providers only expose thinking in the final response. In those cases, response.thinking is populated after the stream completes, and chunk.thinking stays empty.
ActiveRecord Integration
When using acts_as_chat and acts_as_message, thinking content is persisted with the message while thinking-token accounting is stored in RubyLLM’s internal usage ledger:
# Migration (generated automatically with new installs)
# t.text :thinking_text
# t.text :thinking_signature
response = chat_record.ask("Explain quantum entanglement")
response.thinking&.text
response.tokens.thinking
tokens.thinking reports reasoning work separately. tokens.output is already the billable output bucket, so do not add tokens.thinking to it when calculating costs. When a model has distinct reasoning-token pricing, the cost is exposed separately as response.cost.thinking.
Apps upgrading from 1.16 get the thinking columns from the 2.0 upgrade.
The generated schema also preserves the complete thinking blocks when you reload a conversation. Upgrading cannot recover blocks that an older version already discarded.
Provider Notes
- Anthropic sends each option to its own parameter:
budgetbecomes a thinking budget,effortbecomes Claude’s effort setting, anddisplaychooses how much thinking text comes back. Effort alone never turns thinking on, so RubyLLM always sends a thinking block with it. Generations that take a budget (Opus 4.5, Opus and Sonnet 4.6, Sonnet and Haiku 4.5) use thebudget:you set, or one sized from the effort with the levels Bedrock publishes: 1024 for low, 40000 for medium, 63999 for high, capped undermax_tokens. Generations without a budget option (Opus 4.7 and later, Sonnet 5, Fable 5) think adaptively and decide how much to think.with_thinking(false)sends a disabled block on generations that accept it. Claude Sonnet 5.5 rejectsdisabled, including regional Bedrock ids such asus.anthropic.claude-sonnet-5-5;between_toolsis its lowest thinking setting, not fully off, and still allows thinking between tool calls. Newer Claude models hide their thinking text, so passdisplay: :summarizedto read it. - Bedrock thinking params are model-dependent. Claude generations on Bedrock that take a budget get the budget level the model advertises for your
effort, andbudgetsets the exact number of tokens. Generations without a budget option (Opus 4.7 and later, Sonnet 5, Fable 5) think adaptively, as they do on Anthropic. - Gemini 2.5 uses a token budget; Gemini 3 uses effort levels.
- OpenAI reasoning models accept
effort, return an encrypted signature, and only return thinking text when you passdisplay: :summarized, which asks the Responses API for a reasoning summary. - Perplexity’s Agent API folds reasoning into the answer text and returns no separate thinking.
- Mistral sends
effortasreasoning_effortfor every model. Its reasoning models takehighandnone, and models without reasoning reject the parameter. Mistral has no thinking budget, sobudgethas no effect. - DeepSeek accepts a wider range of effort levels than most providers and has no thinking budget. Its error lists the levels a model takes.
- Cohere reasoning models think by default.
with_thinking(budget:)caps the thinking tokens, andwith_thinking(effort: :none)turns the default off. Models without reasoning reject the request. Cohere returns thinking text but no signature, and reports no separate thinking token count. - Ollama and GPUStack local-model thinking controls vary by backend and model. RubyLLM does not translate them; pass backend params explicitly with
with_provider_options. - Ollama does not report thinking token counts.