I released RubyLLM 1.0 today.
When I started building Chat with Work, I wanted to write this:
chat = RubyLLM.chat
chat.ask "What's the best way to learn Ruby?"
And have it work regardless of model or provider. No provider-specific client classes, no different response formats, no ceremony. Just a conversation.
That’s RubyLLM. One API for OpenAI, Claude, Gemini, DeepSeek, and more.
What it looks like
chat = RubyLLM.chat
embedding = RubyLLM.embed("Ruby is elegant")
image = RubyLLM.paint("a sunset over mountains")
Switch models whenever you want. Don’t specify one and you get a sensible default:
chat = RubyLLM.chat(model: 'claude-3-5-sonnet')
chat.with_model('gpt-4o-mini')
Tool calling is a Ruby class, not JSON Schema gymnastics:
class Search < RubyLLM::Tool
description "Searches our knowledge base"
param :query, desc: "Search query"
param :limit, type: :integer, desc: "Max results", required: false
def execute(query:, limit: 5)
Document.search(query).limit(limit).map(&:title)
end
end
chat.with_tool(Search).ask "Find our product documentation"
Streaming works the same way everywhere:
chat.ask "Write a story about Ruby" do |chunk|
print chunk.content
end
Token tracking is built in:
response = chat.ask "Explain Ruby modules"
puts "This cost #{response.input_tokens + response.output_tokens} tokens"
Rails is a first-class citizen:
class Chat < ApplicationRecord
acts_as_chat
end
chat = Chat.create!(model_id: 'gemini-2.0-flash')
chat.ask "Hello" # Everything persisted automatically
Vision, PDFs, and audio through the same interface:
chat.ask "What's in this image?", with: { image: "photo.jpg" }
chat.ask "Summarize this document", with: { pdf: "contract.pdf" }
chat.ask "Transcribe this recording", with: { audio: "meeting.wav" }
Dependencies: Faraday, Zeitwerk, and a tiny event parser. That’s it.
RubyLLM already powers Chat with Work in production. gem install ruby_llm and rubyllm.com has the rest.