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 rubycanusellmOr add to your Gemfile:
gem "rubycanusellm"Quick Start
1. Generate configuration
rubycanusellm generate:configThis 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:completionThis 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 = :anthropicThat'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.embedding_provider = :voyage # optional, for separate embedding provider
config.embedding_api_key = "key" # required when embedding_provider is set
endOllama (local, no API key needed):
RubyCanUseLLM.configure do |config|
config.provider = :ollama
# config.base_url = "http://localhost:11434" # default, change if needed
endChat
response = RubyCanUseLLM.chat(messages, **options)messages — Array of hashes with :role and :content:
messages = [
{ role: :system, content: "You are helpful." },
{ role: :user, content: "Hello" }
]options — Override config per request:
RubyCanUseLLM.chat(messages, model: "gpt-4o", temperature: 0.5)Streaming
Pass stream: true with a block to receive tokens as they arrive:
RubyCanUseLLM.chat(messages, stream: true) do |chunk|
print chunk.content
endEach 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 responseEmbeddings
response = RubyCanUseLLM.embed("Hello world")
response.embedding # [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.embed("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.embedding_provider = :voyage
config.embedding_api_key = ENV["VOYAGE_API_KEY"]
end
RubyCanUseLLM.embed("Hello world")Anthropic users with OpenAI for embeddings:
RubyCanUseLLM.configure do |config|
config.provider = :anthropic
config.api_key = ENV["ANTHROPIC_API_KEY"]
config.embedding_provider = :openai
config.embedding_api_key = ENV["OPENAI_API_KEY"]
end
RubyCanUseLLM.embed("Hello world")Cosine similarity:
a = RubyCanUseLLM.embed("cat")
b = RubyCanUseLLM.embed("dog")
a.cosine_similarity(b.embedding) # 0.87Error Handling
begin
RubyCanUseLLM.chat(messages)
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
endTool 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"]
}
}
]
messages = [{ role: :user, content: "What's the weather in Paris?" }]
response = RubyCanUseLLM.chat(messages, 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"])
messages << { role: :assistant, content: response.content, tool_calls: response.tool_calls }
messages << { role: :tool, tool_call_id: tc.id, name: tc.name, content: weather }
final_response = RubyCanUseLLM.chat(messages, tools: tools)
puts final_response.content
endWorks 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 %>messages = RubyCanUseLLM::Prompt.load("prompts/commodity_analysis.yml",
domain: "electronics",
description: "capacitor 10uF",
references: ["ceramic", "electrolytic"]
)
RubyCanUseLLM.chat(messages)For inline prompts:
prompt = RubyCanUseLLM::Prompt.new(
system: "You are a <%= role %> assistant.",
user: "Help me with: <%= task %>"
)
messages = prompt.render(role: "coding", task: "fix this bug")
RubyCanUseLLM.chat(messages)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
- Project setup
- Configuration module
- OpenAI provider
- Anthropic provider
-
generate:configcommand -
generate:completioncommand - v0.1.0 release
- Streaming support
- Embeddings + configurable embedding provider
- Voyage AI provider (embeddings)
- Mistral provider (chat + embeddings)
- Ollama provider (chat + embeddings, local)
-
generate:embeddingcommand - Prompt templates (ERB + YAML file-based)
- Tool calling
Development
git clone https://github.com/mgznv/rubycanusellm.git
cd rubycanusellm
bin/setup
bundle exec rspecContributing
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.