RubyCanUseLLM
A unified Ruby client for multiple LLM providers with generators. One interface, every LLM.
The Problem
Every time a Ruby developer wants to add LLMs to their app, they start from scratch: pick a provider gem, learn its API, write a service object, handle errors, parse responses. Switch providers? Rewrite everything.
The Solution
RubyCanUseLLM gives you two things:
- Unified client — One interface that works the same across OpenAI, Anthropic, and more. Switch providers by changing a string, not your code.
- Generators — Commands that scaffold ready-to-use boilerplate. You don't start from zero, you start with something that works.
Installation
gem install rubycanusellm
Or add to your Gemfile:
gem "rubycanusellm"
Quick Start
1. Generate configuration
rubycanusellm generate:config
This creates a config file with your provider and API key. In Rails it goes to config/initializers/rubycanusellm.rb, otherwise to config/llm.rb.
2. Generate a completion service
rubycanusellm generate:completion
This creates a ready-to-use service object. In Rails it goes to app/services/, otherwise to lib/.
3. Use it
RubyCanUseLLM.configure do |config|
config.provider = :openai
config.api_key = ENV["LLM_API_KEY"]
end
response = RubyCanUseLLM.chat([
{ role: :user, content: "What is Ruby?" }
])
puts response.content
puts "Tokens: #{response.total_tokens}"
Switch providers in one line
config.provider = :anthropic
That's it. Same code, different provider.
Supported Providers
| Provider | Models | Status | Notes |
|---|---|---|---|
| OpenAI | gpt-4o-mini, gpt-4o, etc. | ✅ | Chat + Embeddings |
| Anthropic | claude-sonnet-4-20250514, etc. | ✅ | Chat only |
| Mistral | mistral-small-latest, mistral-large-latest, etc. | ✅ | Chat + Embeddings |
| Ollama | llama3.2, mistral, etc. | ✅ | Chat + Embeddings (local) |
| Voyage AI | voyage-3.5, voyage-4, etc. | ✅ | Embeddings only |
API Reference
Configuration
RubyCanUseLLM.configure do |config|
config.provider = :openai # :openai, :anthropic, :mistral, or :ollama
config.api_key = "your-key" # required (not needed for Ollama)
config.model = "gpt-4o-mini" # optional, has sensible defaults
config.timeout = 30 # optional, default 30s
config.base_url = "http://localhost:11434" # optional, for Ollama (default shown)
config. = :voyage # optional, for separate embedding provider
config. = "key" # required when embedding_provider is set
end
Ollama (local, no API key needed):
RubyCanUseLLM.configure do |config|
config.provider = :ollama
# config.base_url = "http://localhost:11434" # default, change if needed
end
Chat
response = RubyCanUseLLM.chat(, **)
messages — Array of hashes with :role and :content:
= [
{ role: :system, content: "You are helpful." },
{ role: :user, content: "Hello" }
]
options — Override config per request:
RubyCanUseLLM.chat(, model: "gpt-4o", temperature: 0.5)
Streaming
Pass stream: true with a block to receive tokens as they arrive:
RubyCanUseLLM.chat(, stream: true) do |chunk|
print chunk.content
end
Each chunk is a RubyCanUseLLM::Chunk with content (the token text) and role ("assistant"). Works with OpenAI, Anthropic, Mistral, and Ollama.
Response
response.content # "Hello! How can I help?"
response.model # "gpt-4o-mini"
response.input_tokens # 10
response.output_tokens # 5
response.total_tokens # 15
response.raw # original provider response
Embeddings
response = RubyCanUseLLM.("Hello world")
response. # [0.1, 0.2, ...]
response.tokens # 3
response.model # "text-embedding-3-small"
OpenAI users — embeddings work out of the box, no extra config needed:
RubyCanUseLLM.configure do |config|
config.provider = :openai
config.api_key = ENV["OPENAI_API_KEY"]
end
RubyCanUseLLM.("Hello world")
Anthropic users with Voyage AI (recommended by Anthropic):
RubyCanUseLLM.configure do |config|
config.provider = :anthropic
config.api_key = ENV["ANTHROPIC_API_KEY"]
config. = :voyage
config. = ENV["VOYAGE_API_KEY"]
end
RubyCanUseLLM.("Hello world")
Anthropic users with OpenAI for embeddings:
RubyCanUseLLM.configure do |config|
config.provider = :anthropic
config.api_key = ENV["ANTHROPIC_API_KEY"]
config. = :openai
config. = ENV["OPENAI_API_KEY"]
end
RubyCanUseLLM.("Hello world")
Cosine similarity:
a = RubyCanUseLLM.("cat")
b = RubyCanUseLLM.("dog")
a.cosine_similarity(b.) # 0.87
Error Handling
begin
RubyCanUseLLM.chat()
rescue RubyCanUseLLM::AuthenticationError
# invalid API key
rescue RubyCanUseLLM::RateLimitError
# too many requests
rescue RubyCanUseLLM::TimeoutError
# request timed out
rescue RubyCanUseLLM::ProviderError => e
# other provider error
end
Tool Calling
Define tools once, use them with any provider:
tools = [
{
name: "get_weather",
description: "Get current weather for a city",
parameters: {
type: "object",
properties: {
location: { type: "string", description: "City name" }
},
required: ["location"]
}
}
]
= [{ role: :user, content: "What's the weather in Paris?" }]
response = RubyCanUseLLM.chat(, tools: tools)
if response.tool_call?
tc = response.tool_calls.first
# tc.id => "call_abc123"
# tc.name => "get_weather"
# tc.arguments => { "location" => "Paris" }
# Execute the tool and continue the conversation
weather = fetch_weather(tc.arguments["location"])
<< { role: :assistant, content: response.content, tool_calls: response.tool_calls }
<< { role: :tool, tool_call_id: tc.id, name: tc.name, content: weather }
final_response = RubyCanUseLLM.chat(, tools: tools)
puts final_response.content
end
Works the same across providers — OpenAI, Anthropic, Mistral, and Ollama. Format differences (Anthropic uses input_schema and tool_result messages) are handled internally.
Prompt Templates
Keep prompts out of your Ruby code. Define them in YAML files with ERB for dynamic content:
# prompts/commodity_analysis.yml
system: |
You are an expert in <%= domain %> classification.
user: |
Analyze this item: <%= description %>
<% if references.any? %>
Similar references:
<% references.each do |ref| %>
- <%= ref %>
<% end %>
<% end %>
= RubyCanUseLLM::Prompt.load("prompts/commodity_analysis.yml",
domain: "electronics",
description: "capacitor 10uF",
references: ["ceramic", "electrolytic"]
)
RubyCanUseLLM.chat()
For inline prompts:
prompt = RubyCanUseLLM::Prompt.new(
system: "You are a <%= role %> assistant.",
user: "Help me with: <%= task %>"
)
= prompt.render(role: "coding", task: "fix this bug")
RubyCanUseLLM.chat()
ERB is supported in both cases — loops, conditionals, any Ruby expression.
Generators
| Command | Description |
|---|---|
rubycanusellm generate:config |
Configuration file with provider setup |
rubycanusellm generate:completion |
Completion service object |
rubycanusellm generate:embedding |
Embedding service object |
Roadmap
- [x] Project setup
- [x] Configuration module
- [x] OpenAI provider
- [x] Anthropic provider
- [x]
generate:configcommand - [x]
generate:completioncommand - [x] v0.1.0 release
- [x] Streaming support
- [x] Embeddings + configurable embedding provider
- [x] Voyage AI provider (embeddings)
- [x] Mistral provider (chat + embeddings)
- [x] Ollama provider (chat + embeddings, local)
- [x]
generate:embeddingcommand - [x] Prompt templates (ERB + YAML file-based)
- [x] Tool calling
Development
git clone https://github.com/mgznv/rubycanusellm.git
cd rubycanusellm
bin/setup
bundle exec rspec
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/mgznv/rubycanusellm.
License
The gem is available as open source under the terms of the MIT License.