Skip to content

Commit 75f99a1

Browse files
committed
Add provider configuration checks and selective model refresh
- Add configuration_requirements for each provider to specify needed configs - Show helpful error messages with exact config code needed - Make Models.refresh! ignore unconfigured providers while preserving their models - Add informative logging about which providers are being refreshed/skipped
1 parent 0478a09 commit 75f99a1

File tree

9 files changed

+87
-16
lines changed

9 files changed

+87
-16
lines changed

lib/ruby_llm/error.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def initialize(response = nil, message = nil)
2222
class ModelNotFoundError < StandardError; end
2323
class InvalidRoleError < StandardError; end
2424
class UnsupportedFunctionsError < StandardError; end
25+
class ConfigurationError < StandardError; end
2526
class UnauthorizedError < Error; end
2627
class PaymentRequiredError < Error; end
2728
class ServiceUnavailableError < Error; end

lib/ruby_llm/models.rb

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,21 @@ def models_file
2626
File.expand_path('models.json', __dir__)
2727
end
2828

29-
# Class method to refresh model data
30-
def refresh!
31-
models = RubyLLM.providers.flat_map(&:list_models).sort_by(&:id)
32-
@instance = new(models)
29+
def refresh! # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
30+
configured = Provider.configured_providers
31+
32+
# Log provider status
33+
skipped = Provider.providers.values - configured
34+
RubyLLM.logger.info "Refreshing models from #{configured.map(&:slug).join(', ')}" if configured.any?
35+
RubyLLM.logger.info "Skipping #{skipped.map(&:slug).join(', ')} - providers not configured" if skipped.any?
36+
37+
# Store current models except from configured providers
38+
current = instance.load_models
39+
preserved = current.reject { |m| configured.map(&:slug).include?(m.provider) }
40+
41+
@instance = new(preserved + configured.flat_map(&:list_models))
42+
@instance.save_models
43+
@instance
3344
end
3445

3546
def method_missing(method, ...)
@@ -52,10 +63,10 @@ def initialize(models = nil)
5263

5364
# Load models from the JSON file
5465
def load_models
55-
data = JSON.parse(File.read(self.class.models_file))
56-
data.map { |model| ModelInfo.new(model.transform_keys(&:to_sym)) }
57-
rescue Errno::ENOENT
58-
[] # Return empty array if file doesn't exist yet
66+
data = File.exist?(self.class.models_file) ? File.read(self.class.models_file) : '[]'
67+
JSON.parse(data).map { |model| ModelInfo.new(model.transform_keys(&:to_sym)) }
68+
rescue JSON::ParserError
69+
[]
5970
end
6071

6172
def save_models

lib/ruby_llm/provider.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,32 @@ def paint(prompt, model:, size:)
4949
parse_image_response(response)
5050
end
5151

52+
def configured?
53+
missing_configs.empty?
54+
end
55+
5256
private
5357

58+
def missing_configs
59+
configuration_requirements.select do |key|
60+
value = RubyLLM.config.send(key)
61+
value.nil? || value.empty?
62+
end
63+
end
64+
65+
def ensure_configured!
66+
return if configured?
67+
68+
config_block = <<~RUBY
69+
RubyLLM.configure do |config|
70+
#{missing_configs.map { |key| "config.#{key} = ENV['#{key.to_s.upcase}']" }.join("\n ")}
71+
end
72+
RUBY
73+
74+
raise ConfigurationError,
75+
"#{slug} provider is not configured. Add this to your initialization:\n\n#{config_block}"
76+
end
77+
5478
def sync_response(payload)
5579
response = post completion_url, payload
5680
parse_completion_response response
@@ -77,6 +101,8 @@ def post(url, payload)
77101
end
78102

79103
def connection # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
104+
ensure_configured!
105+
80106
@connection ||= Faraday.new(api_base) do |f| # rubocop:disable Metrics/BlockLength
81107
f.options.timeout = RubyLLM.config.request_timeout
82108

@@ -178,6 +204,19 @@ def parse_data_uri(uri)
178204
nil
179205
end
180206

207+
def configured?
208+
missing_configs = configuration_requirements.select do |key|
209+
value = RubyLLM.config.send(key)
210+
value.nil? || value.empty?
211+
end
212+
213+
return true if missing_configs.empty?
214+
215+
required = missing_configs.map { |key| "config.#{key}" }.join(', ')
216+
@configuration_error = "#{slug} provider is not configured. Please set: #{required}"
217+
false
218+
end
219+
181220
class << self
182221
def extended(base)
183222
base.extend(Methods)
@@ -195,6 +234,10 @@ def for(model)
195234
def providers
196235
@providers ||= {}
197236
end
237+
238+
def configured_providers
239+
providers.select { |_name, provider| provider.configured? }.values
240+
end
198241
end
199242
end
200243
end

lib/ruby_llm/providers/anthropic.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ def capabilities
3333
def slug
3434
'anthropic'
3535
end
36+
37+
def configuration_requirements
38+
%i[anthropic_api_key]
39+
end
3640
end
3741
end
3842
end

lib/ruby_llm/providers/deepseek.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def capabilities
2525
def slug
2626
'deepseek'
2727
end
28+
29+
def configuration_requirements
30+
%i[deepseek_api_key]
31+
end
2832
end
2933
end
3034
end

lib/ruby_llm/providers/gemini.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ def capabilities
3232
def slug
3333
'gemini'
3434
end
35+
36+
def configuration_requirements
37+
%i[gemini_api_key]
38+
end
3539
end
3640
end
3741
end

lib/ruby_llm/providers/openai.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ def capabilities
4545
def slug
4646
'openai'
4747
end
48+
49+
def configuration_requirements
50+
%i[openai_api_key]
51+
end
4852
end
4953
end
5054
end

lib/tasks/models.rake

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ namespace :models do # rubocop:disable Metrics/BlockLength
6868
task :update do
6969
# Configure API keys
7070
RubyLLM.configure do |config|
71-
config.openai_api_key = ENV.fetch('OPENAI_API_KEY')
72-
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY')
73-
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY')
74-
config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY')
71+
config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
72+
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
73+
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
74+
config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
7575
end
7676

7777
models = RubyLLM.models.refresh!

spec/spec_helper.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@
8484
RSpec.shared_context 'with configured RubyLLM' do
8585
before do
8686
RubyLLM.configure do |config|
87-
config.openai_api_key = ENV.fetch('OPENAI_API_KEY')
88-
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY')
89-
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY')
90-
config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY')
87+
config.openai_api_key = ENV.fetch('OPENAI_API_KEY', 'test')
88+
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', 'test')
89+
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', 'test')
90+
config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', 'test')
9191
config.max_retries = 50
9292
end
9393
end

0 commit comments

Comments
 (0)