RubyLLM 2.2.0 lets a model run the code it writes in a sandbox on your own servers, teaches it your workflows with skills, keeps files and long tool results out of the conversation, and makes persisted chats safe to run from several processes at once. Four more providers join, bringing the built-in total to twenty-three.
bundle add ruby_llm --version 2.2.0Upgrading from 2.1? Require 2.2 in your Gemfile, run bin/rails generate ruby_llm:upgrade, and run the migration before deploying. 2.2 removes and renames some names, listed under Changed in 2.2, and the upgrade guide gives the exact replacement for each. Applications on 2.0 upgrade to 2.1 first. See What's New in 2.2 for examples.
New
- Let a model run code on your own servers.
with_code_executiongives the model one tool that runs Ruby, Python, JavaScript, or shell in a sandbox built on the mxc gem, using Bubblewrap on Linux and Seatbelt on macOS. It needs mxc 0.4.1 or later, so runbundle update mxcif you already use it. Files the code writes come back as attachments, and runs emit an event, write a usage row, and report their progress. By @crmne, @stephankaag, and @yorzi. (93f582e0, 5209fa51, #1227, #1233, #1285, #1487, #1552, #1596) - Widen a sandbox one call at a time.
escalate_to:lets the model ask for a named sandbox for a single call, with a reason. The call waits for approval like any other, and nothing is remembered for the next one. By @crmne. (797814b4) - Teach the model your workflows with skills.
with_skillsloads folders in the open Agent Skills format only when a task needs them, and their scripts run in the sandbox. Theskillgem finds and installs them, withgem exec skill add, and installs RubyLLM's own skill for your coding assistant. By @crmne and @yorzi. (8e0b1470, #1226, #1232, #1235, #1237) - Keep files and long tool results out of the conversation.
with_file_storekeeps them in memory, a folder, or Active Storage, and the model works with them through tools shaped like a coding agent's. Word, Excel, and PowerPoint files need no setup. By @crmne. (#1238, #1443, #1462, #1464, #1469, #1571) - Run each persisted chat in one process at a time.
completeand the other loop verbs claim the chat first, so a double-submitted approval no longer runs a tool twice. A process that dies gives up its claims within a minute,Chat.interruptedfinds the chats it left, and the chat UI generator resumes them. A tool call that had started never runs again by itself. By @crmne. (bf1ff089, #1517, #1572, #1583, #1586, #1589) - Queue messages while a chat runs.
queueholds a message until the loop can take it, from any thread or process. A message joins the running turn, or the next one withsteer: false, and can be withdrawn until it does. By @crmne. (#1223, #1417, #1442, #1534) - Let a tool ask the person who runs the chat.
askin a tool pauses the call with no process waiting, andexecuteruns again with the answer. Approvals and MCP server questions share the samepending_questionslist, and any tool can pause on a long-running task. By @crmne. (#1249, #1257, #1540, #1546) - Control the agent loop.
with_tool_options(rounds:)stops a model that loops on its tools by raisingRubyLLM::ToolRoundLimitError, with every call answered. Context budgets keep tool results within the model's window,exclusivetools run alone in a round,around_tool_callwraps each tool in your block, andhaltends the turn from a tool. An agent class now goes wherever tools go, as a subagent. By @crmne. (#1453, #1457, #1460, #1461, #1467, #1562, #1602) - Generate every building block. Generators now cover MCP servers, skills, sandboxes, evaluations, and judges.
ruby_llm:installaddsApplicationAgent, and the chat UI gains a Stop button, costs, files, a model picker, and a card for each question a tool asks. Its Stimulus controller and view helpers ship with the gem, sobundle updatekeeps them current. By @crmne. (1cb5c713, #1488, #1495, #1527, #1531, #1541, #1581) - Test your agents without a provider.
RubyLLM::TestHelperscripts what the model replies, so your tests run real chats, agents, and pages with no API key. By @crmne. (#1498) - Moonshot AI, MiniMax, Z.ai, and Meta join the built-in providers. That makes twenty-three. MiniMax and Z.ai also bring images, video, speech, or OCR, and Moonshot brings files, web search, and batches. By @crmne. (#1252, #1253, #1254, #1255, #1608, #1612, #1613, #1636)
- Fill in what each provider offers. RubyLLM now covers 518 of the 524 shared features its providers offer, each checked against the provider's own documentation. New this release: judgments on OpenRouter and Azure, Chirp 3: HD voices on Vertex AI, Azure AI Speech transcription, Mistral OCR on Azure and Vertex AI, Perplexity file uploads and batches, Meta transcription, Deepgram chat, video and audio input on Meta, MiniMax, Kimi, and Z.ai, and DeepSeek, Z.ai, Perplexity, and Bedrock Nova hosted tools. By @crmne, @mickey, and @cupatea. (#1264, #1437, #1408, #1523, #1626, #1631, #1632, #1635, #1609, #1607, #1620, #1622, #1623, #1624, #1483, #1601)
- Choose your own timeouts and certificates.
config.stream_idle_timeoutends a stream that goes quiet,config.connect_timeoutlimits opening a connection, andconfig.vertexai_token_timeoutlimits waiting for a Vertex AI token.config.sslsets TLS options for every connection, globally or per context. By @crmne and @wdcyxxycdw; thanks @josh-sanders. (#1598, #1591, #1590, #1559) - Fall back with less to configure.
with_fallbacks(drop_unsupported: true)retries a model without an OpenAI parameter it rejects, fallbacks work with prompt caching across providers,RubyLLM.painttakesfallbacks:, and a timeout or a long retry wait moves to the next model at once. By @crmne. (#1468, #1529, #1553, #1561, #1597, #1599) - Read where answers came from.
response.web_sourceslists the pages a hosted web search returned, documents a tool returns can be cited, and search results can carry asource_id. By @crmne and @s-andringa. (#1243, #1250, #1455, #1478) - Trace, rescue, and shape what you need. Traces can record prompts and responses on request,
rescue_fromworks on tools and MCP servers,split_think_tagsmoves reasoning a server leaves in the answer intothinking, and prompt templates trim like Rails. A long video can useprocessing: :agentic, and Azure's content filter verdicts land on messages. By @crmne, @kalicki, and @yorzi; thanks @clarissalimab and @mvysny. (#1333, #1548, #1405, #1086, #1082, #1284)
Fixed
- Stop runs cleanly. A cancel reaches every concurrent tool, stops retries and fallbacks, and keeps the text streamed so far, and a stale cancel no longer ends a later reply. By @crmne, @kieranklaassen, and @yorzi. (#1104, #1149, #1173, #1326, #1328, #1329, #1354, #1413, #1470, #1486, #1500, #1509)
- Keep working when an MCP server misbehaves. A server that is down, rate limited, or answering errors sits out the turn or gives the model a tool error instead of raising from
ask. OAuth keeps its resource, query, and credentials through refreshes and across processes, and stdio servers get a short, safe environment. By @crmne, @kieranklaassen, @yorzi, @ningyixi, and @ammadakram; thanks @juneira and @stephankaag. (#1080, #1096, #1098, #1125, #1170, #1171, #1188, #1292, #1293, #1359, #1363, #1401, #1402, #1425, #1426, #1432, #1493, #1564, #1577, #1578, #1616) - Keep streams whole. Attachments, citations, tool calls without usable IDs, and thinking survive streaming, and a retried Bedrock or binary stream starts from a clean buffer. By @crmne, @kieranklaassen, and @bruno-costanzo. (#1061, #1093, #1123, #1153, #1154, #1212, #1213, #1215, #1427)
- Report tokens and costs that match the invoice. Each request prices at its own tier, characters, audio seconds, and searches get prices, unknown usage stays unknown, and Cohere, Azure Grok, vLLM, and Bedrock Claude count correctly. By @crmne, @kieranklaassen, @yorzi, @bruno-costanzo, and @merlin-croain; thanks @sheredega303. (#1066, #1070, #1074, #1076, #1099, #1126, #1156, #1161, #1162, #1228, #1267, #1273, #1300, #1409, #1418, #1421, #1454)
- Keep Rails records right. Duplicated chats get their own state, cache boundaries and serialization survive reloads, chat JSON works again, approvals come back in order from the database, and an empty models table loads once per process. By @crmne, @kieranklaassen, @yorzi, @bruno-costanzo, @monsterlag, and @lukasedw; thanks @Boux, @jordan-brough, and @stephankaag. (#1059, #1063, #1069, #1078, #1122, #1152, #1263, #1298, #1341, #1360, #1384, #1406, #1555)
- Keep batches paired with their inputs. Results land on the right input, cancelled batches read as cancelled, one bad line fails only itself, and batches that mix credentials are refused. By @crmne and @kieranklaassen. (#1088, #1092, #1158, #1160, #1175, #1176, #1177, #1178, #1187, #1357, #1364, #1629)
- Speak each provider's protocol correctly. Claude on Bedrock uses Anthropic Messages, Gemini takes deeply nested schemas and long-decode replies, Vertex AI signs each attempt with current credentials, OpenRouter reads vision from input modalities, and Responses keeps message text separate and sends document URLs as files. By @crmne, @yorzi, @jordanyeo, @alextakitani, and @wdcyxxycdw; thanks @frostmark, @patodevilla, and @bjorn-mb. (#1248, #1259, #1282, #1295, #1302, #1335, #1343, #1347, #1434, #1452, #1456)
- Send, read, and download files more reliably. Downloads follow redirects, invalid bytes in text attachments are scrubbed, extensionless media works in folder stores, and tool attachments reach Mistral Conversations and Responses replays. By @crmne, @kieranklaassen, and @yorzi. (#1115, #1130, #1134, #1168, #1216, #1217, #1342)
- Raise what you can rescue. Network failures and malformed responses raise
RubyLLM::Errorsubclasses instead of Faraday's, and options a provider cannot carry raise instead of disappearing. Response objects no longer hold your request headers. By @crmne. (#1353, #1379, #1382, #1407, #1410, #1603, #1605) - Resolve models and sandboxes as you expect. Models resolve to configured providers first, and code execution keeps Ruby's bundled gems and runs on macOS with the libraries Ruby links. By @crmne; thanks @stephankaag. (#1294, #1296, #1299)
Changed in 2.2
These need attention when upgrading. The upgrade guide has the replacement for each.
waiting?,pending_questions, andpending_taskssay what a chat waits on.pending_approvals,awaiting_approval?,pending_inputs,awaiting_input?, andawaiting_tasks?are gone, anddeclinereplacesdeny. By @crmne. (#1540, #1546)tool_callsis a collection of calls, and jobs usecomplete?andsucceeded?. The Agentchat_modelmacro is nowchat_class. By @crmne. (#1367, #1368, #1380)RubyLLM.chatdefaults togpt-6.1-sol. Setconfig.default_modelto keep your current model. By @crmne. (47b2427e)- A message sent during a reply waits in the queue, and a cancel with nothing to stop does nothing. A cancelled stream keeps its text. By @crmne. (#1223, #1470)
- Provider gems and custom providers rename protocol methods. Methods that build a request start with
render_, methods that read a response start withparse_, and capability predicates dropsupports_. By @kieranklaassen. (#1136) - Deprecated, and removed in 3.0: iterating a chat directly and
chat.cache_until_here, in favor ofchat.messagesand marking the message itself, andRubyLLM.with_usage_owner, nowRubyLLM.with_owner. The old names still work and warn. By @crmne. (#1440, #1448)
Known limitations
drop_unsupportedrecognizes OpenAI's rejections only. Anthropic and Gemini name the parameter only in free text, so their rejections stayBadRequestError.- A context budget only cuts the text of tool results. Attachments and long tool call arguments still count against the window.
- Code execution needs a host that can start its sandbox. On Linux that means Bubblewrap with unprivileged user namespaces; the Sandboxes guide covers hosts that block them.
- A few provider routes are not verified on a live account yet. Bedrock Mantle's
/openai/v1path,judgeon Ollama Cloud, andgpt-image-2on Azure follow each provider's documentation but have no recording. - Perplexity Orchestrator is unverified.
protocol: :router_chat_completionsfollows Perplexity's documentation, but Orchestrator is in limited preview and our key gets HTTP 403, so its tool choice, parallel tool control and cache boundaries have no live recording until Perplexity grants access. - On Gemini's Interactions API, tools plus
with_schemacan loop. The model can keep calling the tool after each result. Use tools or a schema alone, or Gemini's default protocol. - xAI batches can be slow. A batch can stay pending for a long time before xAI processes it, so poll with patience.
- MCP Apps need a UI supplied by your application. The conformance baseline still excludes the older-server elicitation-defaults scenario.
- Provider access still determines availability. Hosted tools, media, and judgments depend on the selected service and account. See provider coverage for what each one supports.
Thanks
Thanks to @kieranklaassen for 132 pull requests fixing bugs across the framework, and to @yorzi for 33 pull requests of fixes and features and for the reports behind many of them.
Thanks to @bruno-costanzo, @wdcyxxycdw, @s-andringa, @stephankaag, @merlin-croain, @mickey, @monsterlag, @alextakitani, @ammadakram, @jordanyeo, @kalicki, @ningyixi, @lukasedw, and @cupatea for their contributions.
Thanks to @stephankaag, @sheredega303, @juneira, @Boux, @patodevilla, @bjorn-mb, @clarissalimab, @mvysny, @frostmark, @josh-sanders, and @jordan-brough for the reports fixed in this release.
Full changelog: v2.1.1...v2.2.0