Skip to content
AI Article

RubyLLM 1.16: Bringing Sanity to the Ruby AI Stack

The unified AI framework matures into a production-ready powerhouse with concurrent tool execution and Rails-native instrumentation.

Rachel Goldstein
Rachel Goldstein
Dev Tools Editor · Jun 24, 2026 · 5 min read
RubyLLM 1.16: Bringing Sanity to the Ruby AI Stack

For the past few years, developers building AI-powered applications have faced a frustrating choice. They could either swim in the bloated, un-pythonic waters of LangChain, or they could wrestle with a chaotic mess of vendor-specific SDKs, each with its own response formats, retry logic, and architectural quirks. For Ruby developers, the situation was even worse. Often treated as second-class citizens in the AI ecosystem, Rubyists were left to write custom HTTP wrappers or rely on thin, poorly maintained gems.

Enter RubyLLM. Created by Carmine Paolino, the framework has quietly amassed over 9 million downloads on RubyGems by offering a clean, idiomatic, and unified interface for OpenAI, Anthropic, Gemini, DeepSeek, Ollama, and more.

With the release of version 1.16, RubyLLM graduates from a convenient developer-experience wrapper to a serious production-grade tool. By introducing concurrent tool execution, native Rails-style instrumentation, and comprehensive custom API base configurations, this release directly addresses the performance, observability, and security requirements of enterprise applications.

The Anti-Bloat Philosophy

Many AI orchestration frameworks suffer from dependency creep. They pull in hundreds of transitive packages, slowing down boot times and introducing massive security attack surfaces. RubyLLM takes the opposite approach. It relies on a remarkably lean footprint, utilizing only a few core runtime dependencies including Faraday, Zeitwerk, and Marcel.

Despite this minimalism, it provides a comprehensive feature set that covers chat, multimodal vision, document extraction, audio transcription, image generation, and embeddings. Instead of writing separate client initializers for every model, a single unified interface handles the heavy lifting:

# A single interface for any model
chat = RubyLLM.chat
chat.ask "Analyze this document", with: "contract.pdf"

This abstraction is not just about saving lines of code. It decouples your application logic from the underlying model provider. If Anthropic's Claude Sonnet is your production default but you want to run local testing against Ollama or switch to a cheaper DeepSeek model for batch processing, the migration path requires changing a single configuration line rather than rewriting your entire ingestion pipeline.

Production-Grade Concurrency: Threads vs. Fibers

When an LLM decides to trigger multiple tool calls, it is signaling that these operations are independent. For example, if a user asks for the weather in Berlin and Tokyo, the model returns two tool calls. Running these calls sequentially is a massive waste of time, especially since tool calls are almost exclusively I/O-bound operations (database queries, external API requests, or microservice calls).

Prior to version 1.16, RubyLLM executed these tools one after another. In 1.16, concurrent tool execution is now native. Developers can enable this globally in their initializer:

RubyLLM.configure do |config|
  config.tool_concurrency = true # Defaults to :threads
end

Setting this to true uses Ruby's native threads, requiring zero external dependencies. However, for highly concurrent, I/O-heavy applications, spawning a thread per tool call can become expensive. To solve this, RubyLLM 1.16 introduces a :fibers mode, which utilizes the async gem to handle concurrency cooperatively.

This concurrency can also be overridden on a per-chat basis, allowing developers to fine-tune execution depending on the complexity of the tools being called:

# Use fibers for fast, concurrent API calls
chat.with_tools(Weather, StockPrice, concurrency: :fibers)

# Disable concurrency entirely for CPU-bound or rate-limited tools
chat.with_tools(HeavyProcessor, concurrency: false)

Crucially for Rails developers, concurrent tool calls run wrapped inside the Rails executor. This means database connection pools, CurrentAttributes, and code reloading behave exactly as they would in a standard controller or job, preventing the subtle thread-safety bugs that often plague custom concurrency implementations in Rails.

Observability Without Monkey Patching

Operating LLMs in production is notoriously difficult because of their non-deterministic nature. When a request fails, latency spikes, or token usage balloons, developers need immediate visibility. Historically, adding telemetry to Ruby AI libraries required monkey patching the underlying HTTP clients, an approach that inevitably breaks during minor version upgrades.

RubyLLM 1.16 solves this by adopting the standard Rails pattern of structured event emission. It publishes events that can be easily consumed via ActiveSupport::Notifications:

# config/initializers/ruby_llm_instrumentation.rb
ActiveSupport::Notifications.subscribe('chat.ruby_llm') do |_name, _start, _finish, _id, payload|
  Rails.logger.info(
    provider: payload[:provider],
    model: payload[:model],
    input_tokens: payload[:input_tokens],
    output_tokens: payload[:output_tokens]
  )
end

For non-Rails applications, developers can point config.instrumenter to any object that responds to the instrument method, making it straightforward to pipe metrics directly into OpenTelemetry, Datadog, or StatsD. The payload exposes everything from raw HTTP requests and tool arguments to token counts and model registry refreshes, allowing teams to build comprehensive dashboards without modifying their core application code.

Infrastructure Control and Custom Gateways

In an enterprise environment, sending traffic directly to a public AI provider's endpoint is rarely acceptable. Security compliance, rate limiting, caching, and cost-tracking usually require routing requests through an internal gateway or reverse proxy.

While RubyLLM previously supported custom base URLs for some providers, version 1.16 completes the coverage. Every native provider now supports a configurable api_base:

RubyLLM.configure do |config|
  config.bedrock_api_base = ENV['INTERNAL_BEDROCK_PROXY']
  config.vertexai_api_base = ENV['INTERNAL_VERTEX_PROXY']
  config.perplexity_api_base = ENV['INTERNAL_PERPLEXITY_PROXY']
end

Additionally, the underlying Faraday adapter is now fully configurable. If your organization standardizes on a specific HTTP client for connection pooling or HTTP/2 support, you can swap the default Net::HTTP adapter easily:

RubyLLM.configure do |config|
  config.faraday_adapter = :async_http # Or :typhoeus, :httpx, etc.
end

The Trade-offs: Abstraction vs. Control

While RubyLLM is an exceptional tool for unifying the developer experience, any abstraction layer introduces trade-offs.

First, there is the risk of abstraction leakage. Different LLM providers handle concepts like system prompts, tool calling, and structured JSON outputs in fundamentally different ways. While RubyLLM's model registry (which tracks over 800 models and their capabilities) does an admirable job of normalizing these differences, developers must still test their prompts and tool schemas across different backends. A prompt optimized for GPT-4o might fail spectacularly when run against Claude or a local Llama model via Ollama.

Second, there is the project's bus factor. While RubyLLM is highly active and has a growing community, it remains heavily driven by its primary author, Carmine Paolino. Enterprise teams must weigh the benefit of a highly elegant, unified community framework against the security of official, vendor-backed SDKs, even if those official SDKs are significantly more bloated.

The Verdict

For Ruby and Rails teams building AI features today, RubyLLM is the most compelling option on the market. It respects Ruby's philosophy of developer happiness and elegant syntax while avoiding the architectural bloat that has ruined similar frameworks in other language ecosystems.

With the performance, observability, and network control features introduced in version 1.16, the framework has proven it is ready for serious production workloads. It is time to delete your custom API wrappers and standardize your Ruby AI stack.

Sources & further reading

  1. RubyLLM: A single, beautiful Ruby framework for all major AI providers — rubyllm.com
  2. GitHub - crmne/ruby_llm: One delightful Ruby framework for every major AI provider. Build AI agents, chatbots, RAG apps, and multimodal workflows in beautiful, expressive code. · GitHub — github.com
  3. ruby_llm | RubyGems.org | your community gem host — rubygems.org
  4. RubyLLM 1.16: Concurrent Tool Execution, Rails-Style Instrumentation, and api_base for Every Provider — paolino.me
  5. Ruby Community — Carmine Paolino — rubycommunity.org
Rachel Goldstein
Written by
Rachel Goldstein · Dev Tools Editor

Rachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading