Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/_getting_started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,26 @@ Defaults if not configured:
- Embeddings: `{{ site.models.default_embedding }}`
- Images: `{{ site.models.default_image }}`

## Default Providers

Route models to specific providers by default, so you don't have to pass `provider:` on every call:

```ruby
RubyLLM.configure do |config|
config.default_providers = {
'claude' => :bedrock, # all claude models route to bedrock
'claude-haiku' => :anthropic, # except haiku, which goes direct to anthropic
'gpt' => :azure, # all gpt models route to azure
'gemini' => :vertexai, # all gemini models route to vertex ai
'claude-sonnet-4-5' => :openrouter, # one specific model via openrouter
}
end
```

Keys are matched against the start of the model ID. When multiple keys match, the longest (most specific) one wins. Passing an explicit `provider:` argument always overrides `default_providers`.

This applies to all model types: chat, embeddings, images, transcription, and moderation.

## Model Registry File

By default, RubyLLM reads model information from the bundled `models.json` file. If your gem directory is read-only, you can configure a writable location:
Expand Down Expand Up @@ -513,6 +533,9 @@ RubyLLM.configure do |config|
config.default_moderation_model = String
config.default_transcription_model = String

# Default Providers
config.default_providers = Hash # Route models to providers by model ID prefix

# Model Registry
config.model_registry_file = String # Path to model registry JSON file (v1.9.0+)
config.model_registry_class = String
Expand Down
2 changes: 2 additions & 0 deletions lib/ruby_llm/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def defaults = @defaults ||= {}
option :default_image_model, 'gpt-image-1.5'
option :default_transcription_model, 'whisper-1'

option :default_providers, -> { {} }

option :model_registry_file, -> { File.expand_path('models.json', __dir__) }
option :model_registry_class, 'Model'

Expand Down
12 changes: 12 additions & 0 deletions lib/ruby_llm/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def fetch_from_providers(remote_only: true)

def resolve(model_id, provider: nil, assume_exists: false, config: nil) # rubocop:disable Metrics/PerceivedComplexity
config ||= RubyLLM.config
provider ||= resolve_default_provider(model_id, config)
provider_class = provider ? Provider.providers[provider.to_sym] : nil

if provider_class
Expand Down Expand Up @@ -136,6 +137,17 @@ def resolve(model_id, provider: nil, assume_exists: false, config: nil) # ruboco
[model, provider_instance]
end

def resolve_default_provider(model_id, config)
default_providers = config.default_providers
return nil if default_providers.nil? || default_providers.empty?

model_id_str = model_id.to_s
best_key = default_providers.keys
.select { |key| model_id_str.start_with?(key.to_s) }
.max_by { |key| key.to_s.length }
default_providers[best_key] if best_key
end

def method_missing(method, ...)
if instance.respond_to?(method)
instance.send(method, ...)
Expand Down
1 change: 1 addition & 0 deletions spec/ruby_llm/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
expect(config.retry_interval).to eq(0.1)
expect(config.retry_backoff_factor).to eq(2)
expect(config.retry_interval_randomness).to eq(0.5)
expect(config.default_providers).to eq({})
end

it 'exposes a discoverable options API' do
Expand Down
107 changes: 107 additions & 0 deletions spec/ruby_llm/models_default_providers_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe RubyLLM::Models do
include_context 'with configured RubyLLM'

let(:anthropic_model) do
RubyLLM::Model::Info.new(
id: 'claude-sonnet-4-5',
name: 'Claude Sonnet 4.5',
provider: 'anthropic',
family: 'claude-sonnet'
)
end

let(:bedrock_model) do
RubyLLM::Model::Info.new(
id: 'claude-sonnet-4-5',
name: 'Claude Sonnet 4.5',
provider: 'bedrock',
family: 'claude-sonnet'
)
end

let(:haiku_anthropic) do
RubyLLM::Model::Info.new(
id: 'claude-haiku-4-5',
name: 'Claude Haiku 4.5',
provider: 'anthropic',
family: 'claude-haiku'
)
end

let(:haiku_bedrock) do
RubyLLM::Model::Info.new(
id: 'claude-haiku-4-5',
name: 'Claude Haiku 4.5',
provider: 'bedrock',
family: 'claude-haiku'
)
end

after do
described_class.instance_variable_set(:@instance, nil)
RubyLLM.configure do |config|
config.default_providers = {}
end
end

describe 'default_providers' do
it 'routes models to the configured provider by prefix match' do
models = described_class.new([anthropic_model, bedrock_model])

RubyLLM.configure do |config|
config.default_providers = { 'claude' => :bedrock }
end

model, provider = models.resolve('claude-sonnet-4-5')
expect(model.provider).to eq('bedrock')
expect(provider).to be_a(RubyLLM::Provider)
end

it 'prefers longer (more specific) keys' do
models = described_class.new([haiku_anthropic, haiku_bedrock, anthropic_model, bedrock_model])

RubyLLM.configure do |config|
config.default_providers = {
'claude' => :bedrock,
'claude-haiku' => :anthropic
}
end

# claude-haiku matches the longer key → anthropic
model, _provider = models.resolve('claude-haiku-4-5')
expect(model.provider).to eq('anthropic')

# claude-sonnet only matches 'claude' → bedrock
model, _provider = models.resolve('claude-sonnet-4-5')
expect(model.provider).to eq('bedrock')
end

it 'does not apply when an explicit provider: is passed' do
models = described_class.new([anthropic_model, bedrock_model])

RubyLLM.configure do |config|
config.default_providers = { 'claude' => :bedrock }
end

model, _provider = models.resolve('claude-sonnet-4-5', provider: :anthropic)
expect(model.provider).to eq('anthropic')
end

it 'falls through to normal resolution when no prefix matches' do
models = described_class.new([anthropic_model, bedrock_model])

RubyLLM.configure do |config|
config.default_providers = { 'gpt' => :azure }
end

# 'claude-sonnet-4-5' doesn't match 'gpt', so default_providers is skipped
# and normal PROVIDER_PREFERENCE applies (anthropic before bedrock)
model, _provider = models.resolve('claude-sonnet-4-5')
expect(model.provider).to eq('anthropic')
end
end
end