- Install deps
bundle install- Create .env
cp .env.example .env
# then edit .env and set at least one key
# OPENAI_API_KEY=...
# GEMINI_API_KEY=...- Smoke test (one liner)
ruby -e "require_relative 'ruby_llm_resilient_client'; require 'ruby_llm'; chat = RubyLLM.chat; r = ask_with_failover(chat, 'Say hello in five words.'); puts 'Model: ' + r.model_id; puts r.content"- Interactive test with IRB
irb -r ./ruby_llm_resilient_client.rb -r ruby_llmThen inside IRB:
chat = RubyLLM.chat
r = ask_with_failover(chat, "What is the Circuit Breaker pattern in one sentence?")
puts r.model_id
puts r.content
# Follow-up (preserves context)
r2 = ask_with_failover(chat, "Give me an example use case.")
puts r2.contentExit IRB with exit or Ctrl+D.
ruby_llm_resilient_client.rb— failover logic using RubyLLM + Stoplightstoplight_config.rb— Stoplight v5 config (memory-only datastore)ai_provider_settings.rb— thresholds and tracked errorsruby_llm_config.rb— RubyLLM keys from ENV
Use realistic, available model IDs to avoid unknown-model errors. Default priority in this repo favors widely available models:
MODEL_PRIORITY = ['gpt-4o', 'gemini-2.5-flash', 'gpt-4o-mini']You can reorder to prefer Gemini first:
MODEL_PRIORITY = ['gemini-2.5-flash', 'gpt-4o', 'gpt-4o-mini']- We configure Stoplight with a memory datastore (no Redis).
- For each model, we create a circuit breaker with:
threshold: number of failures to trip the circuitcool_off_time: time window before a recovery probetracked_errors: errors that count as failures (e.g.,RubyLLM::Error)
- The chat call loops through
MODEL_PRIORITYand returns the first success. - Circuit status uses
light.color(Stoplight v5 public API):- GREEN = closed
- YELLOW = half open
- RED = open
# stoplight_config.rb
require 'stoplight'
Stoplight.configure do |config|
config.error_notifier = ->(_) {}
config.notifiers = []
memory_store = Stoplight::DataStore::Memory.new
config.data_store = memory_store
$STOPLIGHT_DATA_STORE = memory_store
end# ruby_llm_config.rb
require 'dotenv/load'
require 'ruby_llm'
RubyLLM.configure do |config|
config.openai_api_key = ENV['OPENAI_API_KEY'] if ENV['OPENAI_API_KEY']
config.gemini_api_key = ENV['GEMINI_API_KEY'] if ENV['GEMINI_API_KEY']
end# ai_provider_settings.rb
module AIProviderSettings
CIRCUIT_THRESHOLD = 3
FAILURE_COOLDOWN_S = 30
TRACKING_ERRORS = [RubyLLM::Error, StandardError]
end# excerpt from ruby_llm_resilient_client.rb
def ask_with_failover(chat, prompt)
last_error = nil
MODEL_PRIORITY.each do |model_name|
light = Stoplight("ai_models:#{model_name}",
threshold: AIProviderSettings::CIRCUIT_THRESHOLD,
cool_off_time: AIProviderSettings::FAILURE_COOLDOWN_S,
tracked_errors: AIProviderSettings::TRACKING_ERRORS,
data_store: ($STOPLIGHT_DATA_STORE || Stoplight::DataStore::Memory.new)
)
begin
return light.run do
chat.with_model(model_name)
chat.ask(prompt)
end
rescue Stoplight::Error::RedLight, *AIProviderSettings::TRACKING_ERRORS => e
last_error = e
next
end
end
raise StandardError, "All AI models failed. Last error: #{last_error&.message}"
end- "Unknown model" — switch to
gpt-4o/gemini-2.5-flash/gpt-4o-minior reorder priority. - "No API keys found" — create
.envand set at least one key. - Circuit remains RED — wait
FAILURE_COOLDOWN_Sseconds for half-open or adjust threshold.
- This project intentionally uses Stoplight’s in-memory store (no Redis) for simplicity.
- For production, you may switch to Redis data store, add observability, retries, and cost controls.
MIT