ruby_llm 0.1.0.pre35 → 0.1.0.pre37

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.
@@ -7,76 +7,108 @@ module RubyLLM
7
7
  module Capabilities # rubocop:disable Metrics/ModuleLength
8
8
  module_function
9
9
 
10
+ # Returns the context window size for the given model ID
11
+ # @param model_id [String] the model identifier
12
+ # @return [Integer] the context window size in tokens
10
13
  def context_window_for(model_id)
11
14
  case model_id
12
- when /o[13]-mini/, /o3-mini-2025/ then 200_000
13
- when /o1-2024/ then 200_000
14
- when /gpt-4o/, /gpt-4-turbo/ then 128_000
15
- when /gpt-4-0[0-9]{3}/ then 8_192
16
- when /gpt-3.5-turbo-instruct/ then 4_096
17
- when /gpt-3.5/ then 16_385
15
+ when /o1-2024/, /o3-mini/, /o3-mini-2025/ then 200_000
16
+ when /gpt-4o/, /gpt-4o-mini/, /gpt-4-turbo/, /o1-mini/ then 128_000
17
+ when /gpt-4-0[0-9]{3}/ then 8_192
18
+ when /gpt-3.5/ then 16_385
19
+ when /babbage-002/, /davinci-002/ then 16_384
18
20
  else 4_096
19
21
  end
20
22
  end
21
23
 
22
- def max_tokens_for(model_id) # rubocop:disable Metrics/CyclomaticComplexity
24
+ # Returns the maximum output tokens for the given model ID
25
+ # @param model_id [String] the model identifier
26
+ # @return [Integer] the maximum output tokens
27
+ def max_tokens_for(model_id)
23
28
  case model_id
24
- when /o1-2024/, /o3-mini/ then 100_000
25
- when /o1-mini-2024/ then 65_536
26
- when /gpt-4o-2024-05-13/ then 4_096
27
- when /gpt-4o/, /gpt-4o-mini/ then 16_384
28
- when /gpt-4o-realtime/ then 4_096
29
- when /gpt-4-0[0-9]{3}/ then 8_192
30
- when /gpt-3.5-turbo/ then 4_096
29
+ when /o1-2024/, /o3-mini/, /o3-mini-2025/ then 100_000
30
+ when /o1-mini-2024/ then 65_536
31
+ when /gpt-4o/, /gpt-4o-mini/, /gpt-4o-audio/, /gpt-4o-mini-audio/, /babbage-002/, /davinci-002/ then 16_384
32
+ when /gpt-4-0[0-9]{3}/ then 8_192
31
33
  else 4_096
32
34
  end
33
35
  end
34
36
 
37
+ # Returns the input price per million tokens for the given model ID
38
+ # @param model_id [String] the model identifier
39
+ # @return [Float] the price per million tokens for input
35
40
  def input_price_for(model_id)
36
41
  PRICES.dig(model_family(model_id), :input) || default_input_price
37
42
  end
38
43
 
44
+ # Returns the output price per million tokens for the given model ID
45
+ # @param model_id [String] the model identifier
46
+ # @return [Float] the price per million tokens for output
39
47
  def output_price_for(model_id)
40
48
  PRICES.dig(model_family(model_id), :output) || default_output_price
41
49
  end
42
50
 
51
+ # Determines if the model supports vision capabilities
52
+ # @param model_id [String] the model identifier
53
+ # @return [Boolean] true if the model supports vision
43
54
  def supports_vision?(model_id)
44
55
  model_id.match?(/gpt-4o|o1/) || model_id.match?(/gpt-4-(?!0314|0613)/)
45
56
  end
46
57
 
58
+ # Determines if the model supports function calling
59
+ # @param model_id [String] the model identifier
60
+ # @return [Boolean] true if the model supports functions
47
61
  def supports_functions?(model_id)
48
62
  !model_id.include?('instruct')
49
63
  end
50
64
 
65
+ # Determines if the model supports audio input/output
66
+ # @param model_id [String] the model identifier
67
+ # @return [Boolean] true if the model supports audio
51
68
  def supports_audio?(model_id)
52
69
  model_id.match?(/audio-preview|realtime-preview|whisper|tts/)
53
70
  end
54
71
 
72
+ # Determines if the model supports JSON mode
73
+ # @param model_id [String] the model identifier
74
+ # @return [Boolean] true if the model supports JSON mode
55
75
  def supports_json_mode?(model_id)
56
76
  model_id.match?(/gpt-4-\d{4}-preview/) ||
57
77
  model_id.include?('turbo') ||
58
78
  model_id.match?(/gpt-3.5-turbo-(?!0301|0613)/)
59
79
  end
60
80
 
81
+ # Formats the model ID into a human-readable display name
82
+ # @param model_id [String] the model identifier
83
+ # @return [String] the formatted display name
61
84
  def format_display_name(model_id)
62
85
  model_id.then { |id| humanize(id) }
63
86
  .then { |name| apply_special_formatting(name) }
64
87
  end
65
88
 
89
+ # Determines the type of model
90
+ # @param model_id [String] the model identifier
91
+ # @return [String] the model type (chat, embedding, image, audio, moderation)
66
92
  def model_type(model_id)
67
93
  case model_id
68
94
  when /text-embedding|embedding/ then 'embedding'
69
95
  when /dall-e/ then 'image'
70
96
  when /tts|whisper/ then 'audio'
71
- when /omni-moderation/ then 'moderation'
97
+ when /omni-moderation|text-moderation/ then 'moderation'
72
98
  else 'chat'
73
99
  end
74
100
  end
75
101
 
102
+ # Determines if the model supports structured output
103
+ # @param model_id [String] the model identifier
104
+ # @return [Boolean] true if the model supports structured output
76
105
  def supports_structured_output?(model_id)
77
- model_id.match?(/gpt-4o|o[13]-mini|o1/)
106
+ model_id.match?(/gpt-4o|o[13]-mini|o1|o3-mini/)
78
107
  end
79
108
 
109
+ # Determines the model family for pricing and capability lookup
110
+ # @param model_id [String] the model identifier
111
+ # @return [Symbol] the model family identifier
80
112
  def model_family(model_id) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
81
113
  case model_id
82
114
  when /o3-mini/ then 'o3_mini'
@@ -100,13 +132,14 @@ module RubyLLM
100
132
  when /tts-1-hd/ then 'tts1_hd'
101
133
  when /tts-1/ then 'tts1'
102
134
  when /whisper/ then 'whisper1'
103
- when /omni-moderation/ then 'moderation'
135
+ when /omni-moderation|text-moderation/ then 'moderation'
104
136
  when /babbage/ then 'babbage'
105
137
  when /davinci/ then 'davinci'
106
138
  else 'other'
107
139
  end
108
140
  end
109
141
 
142
+ # Pricing information for OpenAI models (per million tokens unless otherwise specified)
110
143
  PRICES = {
111
144
  o1: { input: 15.0, cached_input: 7.5, output: 60.0 },
112
145
  o1_mini: { input: 1.10, cached_input: 0.55, output: 4.40 },
@@ -150,38 +183,53 @@ module RubyLLM
150
183
  embedding2: { price: 0.10 },
151
184
  davinci: { input: 2.0, output: 2.0 },
152
185
  babbage: { input: 0.40, output: 0.40 },
153
- tts1: { price: 15.0 },
154
- tts1_hd: { price: 30.0 },
155
- whisper1: { price: 0.006 }
186
+ tts1: { price: 15.0 }, # per million characters
187
+ tts1_hd: { price: 30.0 }, # per million characters
188
+ whisper1: { price: 0.006 }, # per minute
189
+ moderation: { price: 0.0 } # free
156
190
  }.freeze
157
191
 
192
+ # Default input price when model-specific pricing is not available
193
+ # @return [Float] the default price per million tokens
158
194
  def default_input_price
159
195
  0.50
160
196
  end
161
197
 
198
+ # Default output price when model-specific pricing is not available
199
+ # @return [Float] the default price per million tokens
162
200
  def default_output_price
163
201
  1.50
164
202
  end
165
203
 
204
+ # Converts a model ID to a human-readable format
205
+ # @param id [String] the model identifier
206
+ # @return [String] the humanized model name
166
207
  def humanize(id)
167
208
  id.tr('-', ' ')
168
- .split(' ')
209
+ .split
169
210
  .map(&:capitalize)
170
211
  .join(' ')
171
212
  end
172
213
 
214
+ # Applies special formatting rules to model names
215
+ # @param name [String] the humanized model name
216
+ # @return [String] the specially formatted model name
173
217
  def apply_special_formatting(name) # rubocop:disable Metrics/MethodLength
174
218
  name
175
219
  .gsub(/(\d{4}) (\d{2}) (\d{2})/, '\1\2\3')
176
220
  .gsub(/^Gpt /, 'GPT-')
177
221
  .gsub(/^O([13]) /, 'O\1-')
222
+ .gsub(/^O3 Mini/, 'O3-Mini')
223
+ .gsub(/^O1 Mini/, 'O1-Mini')
178
224
  .gsub(/^Chatgpt /, 'ChatGPT-')
179
225
  .gsub(/^Tts /, 'TTS-')
180
226
  .gsub(/^Dall E /, 'DALL-E-')
181
- .gsub(/3\.5 /, '3.5-')
182
- .gsub(/4 /, '4-')
183
- .gsub(/4o (?=Mini|Preview|Turbo|Audio)/, '4o-')
227
+ .gsub('3.5 ', '3.5-')
228
+ .gsub('4 ', '4-')
229
+ .gsub(/4o (?=Mini|Preview|Turbo|Audio|Realtime)/, '4o-')
184
230
  .gsub(/\bHd\b/, 'HD')
231
+ .gsub('Omni Moderation', 'Omni-Moderation')
232
+ .gsub('Text Moderation', 'Text-Moderation')
185
233
  end
186
234
  end
187
235
  end
@@ -25,7 +25,7 @@ module RubyLLM
25
25
  vectors = data['data'].map { |d| d['embedding'] }
26
26
 
27
27
  # If we only got one embedding, return it as a single vector
28
- vectors = vectors.size == 1 ? vectors.first : vectors
28
+ vectors = vectors.first if vectors.size == 1
29
29
 
30
30
  Embedding.new(
31
31
  vectors: vectors,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '0.1.0.pre35'
4
+ VERSION = '0.1.0.pre37'
5
5
  end
@@ -16,6 +16,9 @@ PROVIDER_DOCS = {
16
16
  },
17
17
  deepseek: {
18
18
  models: 'https://api-docs.deepseek.com/quick_start/pricing/'
19
+ },
20
+ anthropic: {
21
+ models: 'https://docs.anthropic.com/en/docs/about-claude/models/all-models'
19
22
  }
20
23
  }.freeze
21
24
 
@@ -67,8 +70,8 @@ namespace :models do # rubocop:disable Metrics/BlockLength
67
70
  RubyLLM.configure do |config|
68
71
  config.openai_api_key = ENV.fetch('OPENAI_API_KEY')
69
72
  config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY')
70
- config.gemini_api_key = ENV['GEMINI_API_KEY']
71
- config.deepseek_api_key = ENV['DEEPSEEK_API_KEY']
73
+ config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
74
+ config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
72
75
  end
73
76
 
74
77
  # Get all models
@@ -85,8 +88,10 @@ namespace :models do # rubocop:disable Metrics/BlockLength
85
88
  end
86
89
  end
87
90
 
88
- desc 'Update model capabilities modules by scraping provider documentation'
91
+ desc 'Update model capabilities modules by scraping provider documentation (use PROVIDER=name to update only one)'
89
92
  task :update_capabilities do # rubocop:disable Metrics/BlockLength
93
+ # Check if a specific provider was requested
94
+ target_provider = ENV['PROVIDER']&.to_sym
90
95
  require 'ruby_llm'
91
96
  require 'fileutils'
92
97
 
@@ -97,8 +102,15 @@ namespace :models do # rubocop:disable Metrics/BlockLength
97
102
  config.gemini_api_key = ENV.fetch('GEMINI_API_KEY')
98
103
  end
99
104
 
105
+ # Filter providers if a specific one was requested
106
+ providers_to_process = if target_provider && PROVIDER_DOCS.key?(target_provider)
107
+ { target_provider => PROVIDER_DOCS[target_provider] }
108
+ else
109
+ PROVIDER_DOCS
110
+ end
111
+
100
112
  # Process each provider
101
- PROVIDER_DOCS.each do |provider, urls| # rubocop:disable Metrics/BlockLength
113
+ providers_to_process.each do |provider, urls| # rubocop:disable Metrics/BlockLength
102
114
  puts "Processing #{provider}..."
103
115
 
104
116
  # Initialize our AI assistants
@@ -175,12 +187,22 @@ namespace :models do # rubocop:disable Metrics/BlockLength
175
187
 
176
188
  response = claude.ask(code_prompt)
177
189
 
190
+ # Extract Ruby code from Claude's response
191
+ puts " Extracting Ruby code from Claude's response..."
192
+ ruby_code = nil
193
+
194
+ # Look for Ruby code block
195
+ ruby_code = Regexp.last_match(1).strip if response.content =~ /```ruby\s*(.*?)```/m
196
+
197
+ # Verify we found Ruby code
198
+ raise "No Ruby code block found in Claude's response" if ruby_code.nil? || ruby_code.empty?
199
+
178
200
  # Save the file
179
201
  file_path = "lib/ruby_llm/providers/#{provider}/capabilities.rb"
180
202
  puts " Writing #{file_path}..."
181
203
 
182
204
  FileUtils.mkdir_p(File.dirname(file_path))
183
- File.write(file_path, response.content)
205
+ File.write(file_path, ruby_code)
184
206
  rescue StandardError => e
185
207
  raise "Failed to process #{provider}: #{e.message}"
186
208
  end
data/ruby_llm.gemspec CHANGED
@@ -9,19 +9,21 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ['carmine@paolino.me']
10
10
 
11
11
  spec.summary = 'Beautiful Ruby interface to modern AI'
12
- spec.description = 'A delightful Ruby way to work with AI. Chat in text, analyze and generate images, understand' \
13
- ' audio, and use tools through a unified interface to OpenAI, Anthropic, Google, and DeepSeek.' \
14
- ' Built for developer happiness with automatic token counting, proper streaming, and Rails' \
15
- ' integration. No wrapping your head around multiple APIs - just clean Ruby code that works.'
16
- spec.homepage = 'https://github.com/crmne/ruby_llm'
12
+ spec.description = 'A delightful Ruby way to work with AI. Chat in text, analyze and generate images, understand ' \
13
+ 'audio, and use tools through a unified interface to OpenAI, Anthropic, Google, and DeepSeek. ' \
14
+ 'Built for developer happiness with automatic token counting, proper streaming, and Rails ' \
15
+ 'integration. No wrapping your head around multiple APIs - just clean Ruby code that works.'
16
+ spec.homepage = 'https://rubyllm.com'
17
17
  spec.license = 'MIT'
18
18
  spec.required_ruby_version = Gem::Requirement.new('>= 3.1.0')
19
19
 
20
20
  spec.metadata['homepage_uri'] = spec.homepage
21
- spec.metadata['source_code_uri'] = spec.homepage
22
- spec.metadata['changelog_uri'] = "#{spec.homepage}/commits/main"
21
+ spec.metadata['source_code_uri'] = 'https://github.com/crmne/ruby_llm'
22
+ spec.metadata['changelog_uri'] = "#{spec.metadata['source_code_uri']}/commits/main"
23
23
  spec.metadata['documentation_uri'] = spec.homepage
24
- spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
24
+ spec.metadata['bug_tracker_uri'] = "#{spec.metadata['source_code_uri']}/issues"
25
+
26
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
27
 
26
28
  # Specify which files should be added to the gem when it is released.
27
29
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -38,28 +40,4 @@ Gem::Specification.new do |spec|
38
40
  spec.add_dependency 'faraday-multipart', '>= 1.0'
39
41
  spec.add_dependency 'faraday-retry', '>= 2.0'
40
42
  spec.add_dependency 'zeitwerk', '>= 2.6'
41
-
42
- # Rails integration dependencies
43
- spec.add_development_dependency 'activerecord', '>= 6.0', '< 9.0'
44
- spec.add_development_dependency 'activesupport', '>= 6.0', '< 9.0'
45
-
46
- # Development dependencies
47
- spec.add_development_dependency 'bundler', '>= 2.0'
48
- spec.add_development_dependency 'codecov'
49
- spec.add_development_dependency 'dotenv'
50
- spec.add_development_dependency 'irb'
51
- spec.add_development_dependency 'nokogiri'
52
- spec.add_development_dependency 'overcommit', '>= 0.66'
53
- spec.add_development_dependency 'pry', '>= 0.14'
54
- spec.add_development_dependency 'rake', '>= 13.0'
55
- spec.add_development_dependency 'rdoc'
56
- spec.add_development_dependency 'reline'
57
- spec.add_development_dependency 'rspec', '~> 3.12'
58
- spec.add_development_dependency 'rubocop', '>= 1.0'
59
- spec.add_development_dependency 'rubocop-rake', '>= 0.6'
60
- spec.add_development_dependency 'simplecov', '>= 0.21'
61
- spec.add_development_dependency 'simplecov-cobertura'
62
- spec.add_development_dependency 'sqlite3'
63
- spec.add_development_dependency 'webmock', '~> 3.18'
64
- spec.add_development_dependency 'yard', '>= 0.9'
65
43
  end