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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +53 -0
- data/.rspec_status +7 -35
- data/.rubocop.yml +7 -2
- data/.yardopts +12 -0
- data/Gemfile +27 -0
- data/bin/console +4 -4
- data/docs/.gitignore +7 -0
- data/docs/Gemfile +11 -0
- data/docs/_config.yml +43 -0
- data/docs/_data/navigation.yml +25 -0
- data/docs/guides/chat.md +206 -0
- data/docs/guides/embeddings.md +325 -0
- data/docs/guides/error-handling.md +301 -0
- data/docs/guides/getting-started.md +164 -0
- data/docs/guides/image-generation.md +274 -0
- data/docs/guides/index.md +45 -0
- data/docs/guides/rails.md +401 -0
- data/docs/guides/streaming.md +242 -0
- data/docs/guides/tools.md +247 -0
- data/docs/index.md +53 -0
- data/docs/installation.md +98 -0
- data/lib/ruby_llm/active_record/acts_as.rb +2 -2
- data/lib/ruby_llm/chat.rb +7 -7
- data/lib/ruby_llm/models.json +27 -27
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +56 -19
- data/lib/ruby_llm/providers/anthropic/chat.rb +2 -3
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +39 -1
- data/lib/ruby_llm/providers/gemini/capabilities.rb +70 -8
- data/lib/ruby_llm/providers/openai/capabilities.rb +72 -24
- data/lib/ruby_llm/providers/openai/embeddings.rb +1 -1
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/tasks/models.rake +27 -5
- data/ruby_llm.gemspec +10 -32
- metadata +22 -296
@@ -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 /
|
13
|
-
when /o1-
|
14
|
-
when /gpt-
|
15
|
-
when /gpt-
|
16
|
-
when /
|
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
|
-
|
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/
|
25
|
-
when /o1-mini-2024/
|
26
|
-
when /gpt-4o-
|
27
|
-
when /gpt-
|
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(
|
182
|
-
.gsub(
|
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.
|
28
|
+
vectors = vectors.first if vectors.size == 1
|
29
29
|
|
30
30
|
Embedding.new(
|
31
31
|
vectors: vectors,
|
data/lib/ruby_llm/version.rb
CHANGED
data/lib/tasks/models.rake
CHANGED
@@ -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
|
71
|
-
config.deepseek_api_key = ENV
|
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
|
-
|
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,
|
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
|
-
'
|
14
|
-
'
|
15
|
-
'
|
16
|
-
spec.homepage = 'https://
|
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'] =
|
22
|
-
spec.metadata['changelog_uri'] = "#{spec.
|
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.
|
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
|