ruby_llm 0.1.0.pre49 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,22 +12,26 @@ module RubyLLM
12
12
  class Models
13
13
  include Enumerable
14
14
 
15
- def self.instance
16
- @instance ||= new
17
- end
15
+ # Delegate class methods to the singleton instance
16
+ class << self
17
+ def instance
18
+ @instance ||= new
19
+ end
18
20
 
19
- def self.provider_for(model)
20
- Provider.for(model)
21
- end
21
+ def provider_for(model)
22
+ Provider.for(model)
23
+ end
22
24
 
23
- # Class method to refresh model data
24
- def self.refresh!
25
- models = RubyLLM.providers.flat_map(&:list_models).sort_by(&:id)
26
- @instance = new(models)
27
- end
25
+ def models_file
26
+ File.expand_path('models.json', __dir__)
27
+ end
28
+
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)
33
+ end
28
34
 
29
- # Delegate class methods to the singleton instance
30
- class << self
31
35
  def method_missing(method, ...)
32
36
  if instance.respond_to?(method)
33
37
  instance.send(method, ...)
@@ -48,12 +52,16 @@ module RubyLLM
48
52
 
49
53
  # Load models from the JSON file
50
54
  def load_models
51
- data = JSON.parse(File.read(File.expand_path('models.json', __dir__)))
55
+ data = JSON.parse(File.read(self.class.models_file))
52
56
  data.map { |model| ModelInfo.new(model.transform_keys(&:to_sym)) }
53
57
  rescue Errno::ENOENT
54
58
  [] # Return empty array if file doesn't exist yet
55
59
  end
56
60
 
61
+ def save_models
62
+ File.write(self.class.models_file, JSON.pretty_generate(all.map(&:to_h)))
63
+ end
64
+
57
65
  # Return all models in the collection
58
66
  def all
59
67
  @models
@@ -103,8 +111,6 @@ module RubyLLM
103
111
  # Instance method to refresh models
104
112
  def refresh!
105
113
  self.class.refresh!
106
- # Return self for method chaining
107
- self
108
114
  end
109
115
  end
110
116
  end
@@ -8,8 +8,18 @@ module RubyLLM
8
8
  # Common functionality for all LLM providers. Implements the core provider
9
9
  # interface so specific providers only need to implement a few key methods.
10
10
  module Methods # rubocop:disable Metrics/ModuleLength
11
- def complete(messages, tools:, temperature:, model:, &block)
12
- payload = render_payload messages, tools: tools, temperature: temperature, model: model, stream: block_given?
11
+ def complete(messages, tools:, temperature:, model:, &block) # rubocop:disable Metrics/MethodLength
12
+ normalized_temperature = if capabilities.respond_to?(:normalize_temperature)
13
+ capabilities.normalize_temperature(temperature, model)
14
+ else
15
+ temperature
16
+ end
17
+
18
+ payload = render_payload(messages,
19
+ tools: tools,
20
+ temperature: normalized_temperature,
21
+ model: model,
22
+ stream: block_given?)
13
23
 
14
24
  if block_given?
15
25
  stream_response payload, &block
@@ -10,14 +10,6 @@ module RubyLLM
10
10
  'models'
11
11
  end
12
12
 
13
- def list_models
14
- response = connection.get("models?key=#{RubyLLM.config.gemini_api_key}") do |req|
15
- req.headers.merge! headers
16
- end
17
-
18
- parse_list_models_response(response, slug, capabilities)
19
- end
20
-
21
13
  private
22
14
 
23
15
  def parse_list_models_response(response, slug, capabilities) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
@@ -50,15 +50,42 @@ module RubyLLM
50
50
  # Determines if the model supports vision capabilities
51
51
  # @param model_id [String] the model identifier
52
52
  # @return [Boolean] true if the model supports vision
53
- def supports_vision?(model_id)
54
- model_id.match?(/gpt-4o|o1/) || model_id.match?(/gpt-4-(?!0314|0613)/)
53
+ def supports_vision?(model_id) # rubocop:disable Metrics/MethodLength
54
+ supporting_patterns = [
55
+ /^o1$/,
56
+ /^o1-(?!.*mini|.*preview).*$/,
57
+ /gpt-4\.5/,
58
+ /^gpt-4o$/,
59
+ /gpt-4o-2024/,
60
+ /gpt-4o-search/,
61
+ /^gpt-4o-mini$/,
62
+ /gpt-4o-mini-2024/,
63
+ /gpt-4o-mini-search/,
64
+ /chatgpt-4o/,
65
+ /gpt-4-turbo-2024/,
66
+ /computer-use-preview/,
67
+ /omni-moderation/
68
+ ]
69
+ supporting_patterns.any? { |regex| model_id.match?(regex) }
55
70
  end
56
71
 
57
72
  # Determines if the model supports function calling
58
73
  # @param model_id [String] the model identifier
59
74
  # @return [Boolean] true if the model supports functions
60
- def supports_functions?(model_id)
61
- !model_id.include?('instruct')
75
+ def supports_functions?(model_id) # rubocop:disable Metrics/MethodLength
76
+ supporting_patterns = [
77
+ /^o1$/,
78
+ /gpt-4o/,
79
+ /gpt-4\.5/,
80
+ /chatgpt-4o/,
81
+ /gpt-4-turbo/,
82
+ /computer-use-preview/,
83
+ /o1-preview/,
84
+ /o1-\d{4}-\d{2}-\d{2}/,
85
+ /o1-pro/,
86
+ /o3-mini/
87
+ ]
88
+ supporting_patterns.any? { |regex| model_id.match?(regex) }
62
89
  end
63
90
 
64
91
  # Determines if the model supports audio input/output
@@ -230,6 +257,16 @@ module RubyLLM
230
257
  .gsub('Omni Moderation', 'Omni-Moderation')
231
258
  .gsub('Text Moderation', 'Text-Moderation')
232
259
  end
260
+
261
+ def normalize_temperature(temperature, model_id)
262
+ if model_id.match?(/o[13]/)
263
+ # O1/O3 models always use temperature 1.0
264
+ RubyLLM.logger.debug "Model #{model_id} requires temperature=1.0, ignoring provided value"
265
+ 1.0
266
+ else
267
+ temperature
268
+ end
269
+ end
233
270
  end
234
271
  end
235
272
  end
data/lib/ruby_llm/tool.rb CHANGED
@@ -18,14 +18,19 @@ module RubyLLM
18
18
  # interface for defining parameters and implementing tool behavior.
19
19
  #
20
20
  # Example:
21
- # class Calculator < RubyLLM::Tool
22
- # description "Performs arithmetic calculations"
23
- # param :expression, type: :string, desc: "Math expression to evaluate"
21
+ # require 'tzinfo'
24
22
  #
25
- # def execute(expression:)
26
- # eval(expression).to_s
27
- # end
28
- # end
23
+ # class TimeInfo < RubyLLM::Tool
24
+ # description 'Gets the current time in various timezones'
25
+ # param :timezone, desc: "Timezone name (e.g., 'UTC', 'America/New_York')"
26
+ #
27
+ # def execute(timezone:)
28
+ # time = TZInfo::Timezone.get(timezone).now.strftime('%Y-%m-%d %H:%M:%S')
29
+ # "Current time in #{timezone}: #{time}"
30
+ # rescue StandardError => e
31
+ # { error: e.message }
32
+ # end
33
+ # end
29
34
  class Tool
30
35
  class << self
31
36
  def description(text = nil)
@@ -45,6 +50,9 @@ module RubyLLM
45
50
 
46
51
  def name
47
52
  self.class.name
53
+ .unicode_normalize(:nfkd)
54
+ .encode('ASCII', replace: '')
55
+ .gsub(/[^a-zA-Z0-9_-]/, '-')
48
56
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
49
57
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
50
58
  .downcase
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '0.1.0.pre49'
4
+ VERSION = '1.0.1'
5
5
  end
data/lib/ruby_llm.rb CHANGED
@@ -17,6 +17,8 @@ loader.inflector.inflect(
17
17
  'api' => 'API',
18
18
  'deepseek' => 'DeepSeek'
19
19
  )
20
+ loader.ignore("#{__dir__}/ruby_llm/railtie")
21
+ loader.ignore("#{__dir__}/ruby_llm/active_record")
20
22
  loader.setup
21
23
 
22
24
  # A delightful Ruby interface to modern AI language models.
@@ -2,7 +2,9 @@
2
2
 
3
3
  require 'English'
4
4
  require 'faraday'
5
+ require 'fileutils'
5
6
  require 'nokogiri'
7
+ require 'ruby_llm'
6
8
 
7
9
  # URLs to process
8
10
  PROVIDER_DOCS = {
@@ -64,8 +66,6 @@ end
64
66
  namespace :models do # rubocop:disable Metrics/BlockLength
65
67
  desc 'Update available models from providers'
66
68
  task :update do
67
- require 'ruby_llm'
68
-
69
69
  # Configure API keys
70
70
  RubyLLM.configure do |config|
71
71
  config.openai_api_key = ENV.fetch('OPENAI_API_KEY')
@@ -74,15 +74,13 @@ namespace :models do # rubocop:disable Metrics/BlockLength
74
74
  config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY')
75
75
  end
76
76
 
77
- # Refresh models (now returns self instead of models array)
78
- models = RubyLLM.models.refresh!.all
79
- # Write to models.json
80
- File.write(File.expand_path('../ruby_llm/models.json', __dir__), JSON.pretty_generate(models.map(&:to_h)))
77
+ models = RubyLLM.models.refresh!
78
+ models.save_models
81
79
 
82
- puts "Updated models.json with #{models.size} models:"
80
+ puts "Updated models.json with #{models.all.size} models:"
83
81
  RubyLLM::Provider.providers.each do |provider_sym, provider_module|
84
82
  provider_name = provider_module.to_s.split('::').last
85
- provider_models = models.select { |m| m.provider == provider_sym.to_s }
83
+ provider_models = models.all.select { |m| m.provider == provider_sym.to_s }
86
84
  puts "#{provider_name} models: #{provider_models.size}"
87
85
  end
88
86
  end
@@ -91,8 +89,6 @@ namespace :models do # rubocop:disable Metrics/BlockLength
91
89
  task :update_capabilities do # rubocop:disable Metrics/BlockLength
92
90
  # Check if a specific provider was requested
93
91
  target_provider = ENV['PROVIDER']&.to_sym
94
- require 'ruby_llm'
95
- require 'fileutils'
96
92
 
97
93
  # Configure API keys
98
94
  RubyLLM.configure do |config|
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Helper functions at the top level
4
+ def record_all_cassettes(cassette_dir)
5
+ # Re-record all cassettes
6
+ FileUtils.rm_rf(cassette_dir)
7
+ FileUtils.mkdir_p(cassette_dir)
8
+
9
+ puts 'Recording cassettes for all providers...'
10
+ run_tests
11
+ puts 'Done recording. Please review the new cassettes.'
12
+ end
13
+
14
+ def record_for_providers(providers, cassette_dir) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
15
+ # Get the list of available providers from RubyLLM itself
16
+ all_providers = RubyLLM::Provider.providers.keys.map(&:to_s)
17
+
18
+ # Check for valid providers
19
+ if providers.empty?
20
+ puts "Please specify providers or 'all'. Example: rake vcr:record[openai,anthropic]"
21
+ puts "Available providers: #{all_providers.join(', ')}"
22
+ return
23
+ end
24
+
25
+ invalid_providers = providers - all_providers
26
+ if invalid_providers.any?
27
+ puts "Invalid providers: #{invalid_providers.join(', ')}"
28
+ puts "Available providers: #{all_providers.join(', ')}"
29
+ return
30
+ end
31
+
32
+ # Get URL patterns from the providers themselves
33
+ provider_patterns = get_provider_patterns(providers)
34
+
35
+ puts "Finding cassettes for providers: #{providers.join(', ')}"
36
+
37
+ # Find and delete matching cassettes
38
+ cassettes_to_delete = find_matching_cassettes(cassette_dir, provider_patterns)
39
+
40
+ if cassettes_to_delete.empty?
41
+ puts 'No cassettes found for the specified providers.'
42
+ puts 'Running tests to record new cassettes...'
43
+ else
44
+ delete_cassettes(cassettes_to_delete)
45
+ puts "\nRunning tests to record new cassettes..."
46
+ end
47
+
48
+ run_tests
49
+
50
+ puts "\nDone recording cassettes for #{providers.join(', ')}."
51
+ puts 'Please review the updated cassettes for sensitive information.'
52
+ end
53
+
54
+ def get_provider_patterns(providers) # rubocop:disable Metrics/MethodLength
55
+ provider_patterns = {}
56
+
57
+ providers.each do |provider_name|
58
+ provider_module = RubyLLM::Provider.providers[provider_name.to_sym]
59
+ next unless provider_module
60
+
61
+ # Extract the base URL from the provider's api_base method
62
+ api_base = provider_module.api_base.to_s
63
+
64
+ # Create a regex pattern from the domain
65
+ next unless api_base && !api_base.empty?
66
+
67
+ domain = URI.parse(api_base).host
68
+ pattern = Regexp.new(Regexp.escape(domain))
69
+ provider_patterns[provider_name] = pattern
70
+ end
71
+
72
+ provider_patterns
73
+ end
74
+
75
+ def find_matching_cassettes(dir, patterns)
76
+ cassettes = []
77
+
78
+ Dir.glob("#{dir}/**/*.yml").each do |file|
79
+ content = File.read(file)
80
+ cassettes << file if patterns.values.any? { |pattern| content.match?(pattern) }
81
+ end
82
+
83
+ cassettes
84
+ end
85
+
86
+ def delete_cassettes(cassettes)
87
+ puts "Deleting #{cassettes.size} cassettes for re-recording:"
88
+ cassettes.each do |file|
89
+ puts " - #{File.basename(file)}"
90
+ File.delete(file)
91
+ end
92
+ end
93
+
94
+ def run_tests
95
+ system('bundle exec rspec') || abort('Tests failed')
96
+ end
97
+
98
+ namespace :vcr do
99
+ desc 'Record VCR cassettes (rake vcr:record[all] or vcr:record[openai,anthropic])'
100
+ task :record, [:providers] do |_, args|
101
+ require 'fileutils'
102
+ require 'ruby_llm'
103
+
104
+ providers = (args[:providers] || '').downcase.split(',')
105
+ cassette_dir = 'spec/fixtures/vcr_cassettes'
106
+ FileUtils.mkdir_p(cassette_dir)
107
+
108
+ if providers.include?('all')
109
+ record_all_cassettes(cassette_dir)
110
+ else
111
+ record_for_providers(providers, cassette_dir)
112
+ end
113
+ end
114
+ end
data/ruby_llm.gemspec CHANGED
@@ -35,6 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.require_paths = ['lib']
36
36
 
37
37
  # Runtime dependencies
38
+ spec.add_dependency 'base64'
38
39
  spec.add_dependency 'event_stream_parser', '~> 1'
39
40
  spec.add_dependency 'faraday', '~> 2'
40
41
  spec.add_dependency 'faraday-multipart', '~> 1'
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre49
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-28 00:00:00.000000000 Z
11
+ date: 2025-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: base64
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: event_stream_parser
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -96,9 +110,9 @@ files:
96
110
  - ".gitignore"
97
111
  - ".overcommit.yml"
98
112
  - ".rspec"
99
- - ".rspec_status"
100
113
  - ".rubocop.yml"
101
114
  - ".yardopts"
115
+ - CONTRIBUTING.md
102
116
  - Gemfile
103
117
  - LICENSE
104
118
  - README.md
@@ -153,6 +167,7 @@ files:
153
167
  - lib/ruby_llm/tool_call.rb
154
168
  - lib/ruby_llm/version.rb
155
169
  - lib/tasks/models.rake
170
+ - lib/tasks/vcr.rake
156
171
  - ruby_llm.gemspec
157
172
  homepage: https://rubyllm.com
158
173
  licenses:
data/.rspec_status DELETED
@@ -1,50 +0,0 @@
1
- example_id | status | run_time |
2
- -------------------------------------------------- | ------ | --------------- |
3
- ./spec/ruby_llm/active_record/acts_as_spec.rb[1:1] | passed | 3.38 seconds |
4
- ./spec/ruby_llm/active_record/acts_as_spec.rb[1:2] | passed | 2.48 seconds |
5
- ./spec/ruby_llm/chat_content_spec.rb[1:1:1] | passed | 2.74 seconds |
6
- ./spec/ruby_llm/chat_content_spec.rb[1:1:2] | passed | 1.29 seconds |
7
- ./spec/ruby_llm/chat_content_spec.rb[1:1:3] | passed | 2.54 seconds |
8
- ./spec/ruby_llm/chat_content_spec.rb[1:2:1] | passed | 2.77 seconds |
9
- ./spec/ruby_llm/chat_content_spec.rb[1:2:2] | passed | 2.1 seconds |
10
- ./spec/ruby_llm/chat_pdf_spec.rb[1:1:1] | passed | 7.75 seconds |
11
- ./spec/ruby_llm/chat_pdf_spec.rb[1:1:2] | passed | 13.88 seconds |
12
- ./spec/ruby_llm/chat_spec.rb[1:1:1:1] | passed | 1.02 seconds |
13
- ./spec/ruby_llm/chat_spec.rb[1:1:1:2] | passed | 3.95 seconds |
14
- ./spec/ruby_llm/chat_spec.rb[1:1:2:1] | passed | 0.4854 seconds |
15
- ./spec/ruby_llm/chat_spec.rb[1:1:2:2] | passed | 1.37 seconds |
16
- ./spec/ruby_llm/chat_spec.rb[1:1:3:1] | passed | 7.34 seconds |
17
- ./spec/ruby_llm/chat_spec.rb[1:1:3:2] | passed | 19.22 seconds |
18
- ./spec/ruby_llm/chat_spec.rb[1:1:4:1] | passed | 3.15 seconds |
19
- ./spec/ruby_llm/chat_spec.rb[1:1:4:2] | passed | 2.51 seconds |
20
- ./spec/ruby_llm/chat_streaming_spec.rb[1:1:1:1] | passed | 0.91374 seconds |
21
- ./spec/ruby_llm/chat_streaming_spec.rb[1:1:2:1] | passed | 0.50088 seconds |
22
- ./spec/ruby_llm/chat_streaming_spec.rb[1:1:3:1] | passed | 5.69 seconds |
23
- ./spec/ruby_llm/chat_streaming_spec.rb[1:1:4:1] | passed | 1.22 seconds |
24
- ./spec/ruby_llm/chat_tools_spec.rb[1:1:1] | passed | 4.26 seconds |
25
- ./spec/ruby_llm/chat_tools_spec.rb[1:1:2] | passed | 6.16 seconds |
26
- ./spec/ruby_llm/chat_tools_spec.rb[1:1:3] | passed | 11.15 seconds |
27
- ./spec/ruby_llm/chat_tools_spec.rb[1:1:4] | passed | 1.3 seconds |
28
- ./spec/ruby_llm/chat_tools_spec.rb[1:1:5] | passed | 2.71 seconds |
29
- ./spec/ruby_llm/chat_tools_spec.rb[1:1:6] | passed | 2.43 seconds |
30
- ./spec/ruby_llm/chat_tools_spec.rb[1:1:7] | passed | 2.71 seconds |
31
- ./spec/ruby_llm/chat_tools_spec.rb[1:1:8] | passed | 4 seconds |
32
- ./spec/ruby_llm/chat_tools_spec.rb[1:1:9] | passed | 2.81 seconds |
33
- ./spec/ruby_llm/embeddings_spec.rb[1:1:1:1] | passed | 0.33357 seconds |
34
- ./spec/ruby_llm/embeddings_spec.rb[1:1:1:2] | passed | 0.43632 seconds |
35
- ./spec/ruby_llm/embeddings_spec.rb[1:1:2:1] | passed | 0.65614 seconds |
36
- ./spec/ruby_llm/embeddings_spec.rb[1:1:2:2] | passed | 2.16 seconds |
37
- ./spec/ruby_llm/error_handling_spec.rb[1:1] | passed | 0.29366 seconds |
38
- ./spec/ruby_llm/image_generation_spec.rb[1:1:1] | passed | 14.16 seconds |
39
- ./spec/ruby_llm/image_generation_spec.rb[1:1:2] | passed | 16.22 seconds |
40
- ./spec/ruby_llm/image_generation_spec.rb[1:1:3] | passed | 9.1 seconds |
41
- ./spec/ruby_llm/image_generation_spec.rb[1:1:4] | passed | 0.00138 seconds |
42
- ./spec/ruby_llm/models_spec.rb[1:1:1] | passed | 0.01071 seconds |
43
- ./spec/ruby_llm/models_spec.rb[1:1:2] | passed | 0.00056 seconds |
44
- ./spec/ruby_llm/models_spec.rb[1:1:3] | passed | 0.00336 seconds |
45
- ./spec/ruby_llm/models_spec.rb[1:2:1] | passed | 0.00016 seconds |
46
- ./spec/ruby_llm/models_spec.rb[1:2:2] | passed | 0.00085 seconds |
47
- ./spec/ruby_llm/models_spec.rb[1:3:1] | passed | 1.44 seconds |
48
- ./spec/ruby_llm/models_spec.rb[1:3:2] | passed | 1.23 seconds |
49
- ./spec/ruby_llm/models_spec.rb[1:4:1] | passed | 0.0003 seconds |
50
- ./spec/ruby_llm/models_spec.rb[1:4:2] | passed | 0.00175 seconds |