RubyLLM 1.4-1.5.1: Three Releases in Three Days

We shipped three releases in three days. Wednesday, Friday, and Friday again. Each one fixed real problems. Each one shipped as soon as it was ready.

1.4.0: The Structured Output Release (Wednesday)

One of the biggest problem with LLMs is getting them to return data in the format you need.

We all had code like this:

# The old struggle
response = chat.ask("Return user data as JSON. ONLY JSON. NO MARKDOWN.")
begin
  data = JSON.parse(response.content.gsub(/```json\n?/, '').gsub(/```\n?/, ''))
rescue JSON::ParserError
  # Hope and pray
end

Now with structured output:

# Define your schema with the RubyLLM::Schema DSL
class PersonSchema < RubyLLM::Schema
  string :name
  integer :age
  array :skills, of: :string
end

# Get perfectly structured JSON every time
chat = RubyLLM.chat.with_schema(PersonSchema)
response = chat.ask("Generate a Ruby developer profile")

# => {"name" => "Yukihiro", "age" => 59, "skills" => ["Ruby", "C", "Language Design"]}

No more regex. No more parsing. Just data structures that work.

Oh, and Daniel Friis released RubyLLM::Schema just for the occasion, but you can use any gem you want with RubyLLM, or even write your own JSON schema from scratch.

Rails Generators: From Zero to Chat

We didn’t have Rails generators before. Now we do:

rails generate ruby_llm:install

This creates everything you need:

  • Migrations
  • Models with acts_as_chat, acts_as_message, and acts_as_tool_call
  • A clean initializer

Your Chat model works like any Rails model:

chat = Chat.create!(model: "gpt-4.1-nano")
response = chat.ask("Explain Ruby blocks")
# Messages are automatically persisted with proper associations

From rails new to working chat in under 5 minutes.

Tool Call Transparency

New callback to see what your AI is doing:

chat.on_tool_call do |tool_call|
  puts "🔧 AI is calling: #{tool_call.name}"
  puts "   Arguments: #{tool_call.arguments}"

  Rails.logger.info "[AI Tool] #{tool_call.name}: #{tool_call.arguments}"
end

chat.ask("What's the weather in Tokyo?").with_tools([weather_tool])
# => 🔧 AI is calling: get_weather
#    Arguments: {"location": "Tokyo"}

Essential for debugging and auditing AI behavior.

Direct Parameter Provider Access

Need that one weird parameter? Use with_params:

# OpenAI's JSON mode
chat.with_params(response_format: { type: "json_object" })
     .ask("List Ruby features as JSON")

No waiting for us to wrap every provider option.

Critical Bug Fixes and Other Improvements in 1.4.0

  • Anthropic multiple tool calls: Was only processing the first tool call, silently ignoring the rest
  • Streaming errors: Now handled properly in both Faraday V1 and V2
  • Test fixtures: Removed 60MB of unnecessary test data
  • Message ordering: Fixed race conditions in streaming responses
  • JRuby support: Now officially tested and supported
  • Direct access to raw responses: Get the raw responses from Faraday for debugging
  • GPUStack support: A production-ready alternative to Ollama

Full release notes for 1.4.0 available on GitHub.

1.5.0: Two New Providers (Friday)

Mistral AI

63 models from France, from tiny to massive:

RubyLLM.configure do |config|
  config.mistral_api_key = ENV['MISTRAL_API_KEY']
end

# Efficient small model
chat = RubyLLM.chat(model: 'ministral-3b-latest')

# Their flagship model
chat = RubyLLM.chat(model: 'mistral-large-latest')

# Vision with Pixtral
vision = RubyLLM.chat(model: 'pixtral-12b-latest')
vision.ask("What's in this image?", with: "path/to/image.jpg")

Perplexity

Real-time web search meets LLMs:

RubyLLM.configure do |config|
  config.perplexity_api_key = ENV['PERPLEXITY_API_KEY']
end

# Get current information with web search
chat = RubyLLM.chat(model: 'sonar-pro')
response = chat.ask("What are the latest Ruby 3.4 features?")
# Searches the web and returns current information

Full release notes for 1.5.0 available on GitHub.

Rails Generator Fixes

  • Fixed migration order (Chats → Messages → Tool Calls)
  • Fixed PostgreSQL detection that was broken by namespace collision
  • PostgreSQL users now get jsonb columns instead of json

1.5.1: Quick Fixes (Also Friday)

Found issues Friday afternoon. Fixed them. Shipped them. That’s it.

Why make users wait through the weekend with broken code?

  • Fixed Mistral model capabilities (was a Hash, should be Array)
  • Fixed Google Imagen output modality
  • Updated to JRuby 10.0.1.0
  • Added JSON schema validation for model registry

Full release notes for 1.5.1 available on GitHub.

The Philosophy: Ship When Ready

Three days. Three releases. Each one made someone’s code work better.

We could have waited. We could have bundled everything into one release next week. But every moment we wait is a moment someone’s dealing with a bug we already fixed.

The structured output in 1.4.0? People needed that since before RubyLLM existed. The PostgreSQL fix in 1.5.0? Someone’s migrations were failing Thursday. The Mistral capability fix? Breaking someone’s code Friday morning.

When code is ready, you ship.

What You Can Build Now

With structured output and multiple providers, you can build real features:

# Extract structured data from any text
class InvoiceSchema < RubyLLM::Schema
  string :invoice_number
  date :date
  float :total
  array :line_items do
    object do
      string :description
      float :amount
    end
  end
end

# Use Mistral for cost-effective extraction
extractor = RubyLLM.chat(model: 'ministral-8b-latest')
                    .with_schema(InvoiceSchema)

invoice_data = extractor.ask("Extract invoice details from: #{pdf_text}")
# Reliable data extraction at a fraction of GPT-4's cost

# Use Perplexity for current information
researcher = RubyLLM.chat(model: 'sonar-deep-research')
market_data = researcher.ask("Current Ruby job market trends in 2025")
# Real-time data, not training cutoff guesses

Use It

gem 'ruby_llm', '~> 1.5'

Full backward compatibility. Your 1.0 code still runs. These releases just made everything better.

Newsletter