ruby_llm 1.2.0 → 1.3.0
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/README.md +80 -133
- data/lib/ruby_llm/active_record/acts_as.rb +144 -47
- data/lib/ruby_llm/aliases.json +187 -17
- data/lib/ruby_llm/attachment.rb +164 -0
- data/lib/ruby_llm/chat.rb +31 -20
- data/lib/ruby_llm/configuration.rb +34 -1
- data/lib/ruby_llm/connection.rb +121 -0
- data/lib/ruby_llm/content.rb +27 -79
- data/lib/ruby_llm/context.rb +30 -0
- data/lib/ruby_llm/embedding.rb +13 -5
- data/lib/ruby_llm/error.rb +2 -1
- data/lib/ruby_llm/image.rb +15 -8
- data/lib/ruby_llm/message.rb +14 -6
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +101 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +51 -0
- data/lib/ruby_llm/model/pricing_category.rb +48 -0
- data/lib/ruby_llm/model/pricing_tier.rb +34 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +26279 -2362
- data/lib/ruby_llm/models.rb +95 -14
- data/lib/ruby_llm/provider.rb +48 -90
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
- data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
- data/lib/ruby_llm/providers/anthropic/media.rb +49 -28
- data/lib/ruby_llm/providers/anthropic/models.rb +16 -16
- data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
- data/lib/ruby_llm/providers/anthropic.rb +3 -3
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
- data/lib/ruby_llm/providers/bedrock/media.rb +59 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
- data/lib/ruby_llm/providers/bedrock.rb +14 -25
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
- data/lib/ruby_llm/providers/deepseek.rb +3 -3
- data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
- data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
- data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
- data/lib/ruby_llm/providers/gemini/images.rb +4 -3
- data/lib/ruby_llm/providers/gemini/media.rb +28 -111
- data/lib/ruby_llm/providers/gemini/models.rb +17 -23
- data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
- data/lib/ruby_llm/providers/gemini.rb +3 -3
- data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
- data/lib/ruby_llm/providers/ollama/media.rb +48 -0
- data/lib/ruby_llm/providers/ollama.rb +34 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
- data/lib/ruby_llm/providers/openai/chat.rb +6 -4
- data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
- data/lib/ruby_llm/providers/openai/images.rb +3 -2
- data/lib/ruby_llm/providers/openai/media.rb +48 -21
- data/lib/ruby_llm/providers/openai/models.rb +17 -18
- data/lib/ruby_llm/providers/openai/tools.rb +9 -5
- data/lib/ruby_llm/providers/openai.rb +7 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
- data/lib/ruby_llm/providers/openrouter.rb +31 -0
- data/lib/ruby_llm/stream_accumulator.rb +4 -4
- data/lib/ruby_llm/streaming.rb +48 -13
- data/lib/ruby_llm/utils.rb +27 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +15 -5
- data/lib/tasks/aliases.rake +235 -0
- data/lib/tasks/models_docs.rake +164 -121
- data/lib/tasks/models_update.rake +79 -0
- data/lib/tasks/release.rake +32 -0
- data/lib/tasks/vcr.rake +4 -2
- metadata +56 -32
- data/lib/ruby_llm/model_info.rb +0 -56
- data/lib/tasks/browser_helper.rb +0 -97
- data/lib/tasks/capability_generator.rb +0 -123
- data/lib/tasks/capability_scraper.rb +0 -224
- data/lib/tasks/cli_helper.rb +0 -22
- data/lib/tasks/code_validator.rb +0 -29
- data/lib/tasks/model_updater.rb +0 -66
- data/lib/tasks/models.rake +0 -43
@@ -1,224 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'ruby_llm'
|
4
|
-
require 'uri'
|
5
|
-
require_relative 'browser_helper'
|
6
|
-
require_relative 'cli_helper'
|
7
|
-
|
8
|
-
class CapabilityScraper # rubocop:disable Metrics/ClassLength,Style/Documentation
|
9
|
-
PROVIDER_DOCS = {
|
10
|
-
openai: {
|
11
|
-
url: 'https://platform.openai.com/docs/models'
|
12
|
-
},
|
13
|
-
anthropic: {
|
14
|
-
url: 'https://docs.anthropic.com/claude/docs/models-overview'
|
15
|
-
},
|
16
|
-
gemini: {
|
17
|
-
url: 'https://ai.google.dev/models/gemini',
|
18
|
-
pricing_url: 'https://ai.google.dev/pricing'
|
19
|
-
},
|
20
|
-
deepseek: {
|
21
|
-
url: 'https://api-docs.deepseek.com/quick_start/pricing'
|
22
|
-
}
|
23
|
-
}.freeze
|
24
|
-
|
25
|
-
def self.parse_providers(providers_arg) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
26
|
-
provider_input = (providers_arg || '').downcase.split(',').map(&:strip).reject(&:empty?)
|
27
|
-
|
28
|
-
target_providers = if provider_input.empty? || provider_input.include?('all')
|
29
|
-
PROVIDER_DOCS.keys
|
30
|
-
else
|
31
|
-
provider_input.map(&:to_sym).select { |p| PROVIDER_DOCS.key?(p) }
|
32
|
-
end
|
33
|
-
|
34
|
-
if target_providers.empty?
|
35
|
-
puts "No valid providers. Available: all, #{PROVIDER_DOCS.keys.join(', ')}"
|
36
|
-
exit(1)
|
37
|
-
end
|
38
|
-
|
39
|
-
puts "Targeting providers: #{target_providers.join(', ')}"
|
40
|
-
target_providers
|
41
|
-
end
|
42
|
-
|
43
|
-
def initialize(providers)
|
44
|
-
@providers = providers
|
45
|
-
@browser_helper = BrowserHelper.new
|
46
|
-
end
|
47
|
-
|
48
|
-
def run # rubocop:disable Metrics/MethodLength
|
49
|
-
@providers.each do |provider_sym|
|
50
|
-
provider_name = provider_sym.to_s
|
51
|
-
puts "\n======== Processing Provider: #{provider_name.upcase} =========="
|
52
|
-
|
53
|
-
docs_info = PROVIDER_DOCS[provider_sym]
|
54
|
-
docs_html = scrape_provider(provider_sym, docs_info)
|
55
|
-
|
56
|
-
next unless docs_html.any? { |_k, v| v && !v.strip.empty? }
|
57
|
-
|
58
|
-
summarize_html_data(docs_html)
|
59
|
-
yield(provider_name, docs_html) if block_given?
|
60
|
-
end
|
61
|
-
ensure
|
62
|
-
@browser_helper&.close
|
63
|
-
end
|
64
|
-
|
65
|
-
private
|
66
|
-
|
67
|
-
def scrape_provider(provider_sym, docs_info)
|
68
|
-
if provider_sym == :openai
|
69
|
-
handle_openai_scraping(docs_info[:url])
|
70
|
-
else
|
71
|
-
handle_standard_scraping(provider_sym, docs_info)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
def handle_openai_scraping(main_url) # rubocop:disable Metrics/MethodLength
|
76
|
-
main_url_path = extract_path_from_url(main_url)
|
77
|
-
overview_key = generate_key_from_url(main_url, main_url_path)
|
78
|
-
docs_html = {}
|
79
|
-
|
80
|
-
puts "\n--- Interactive Scraping for OpenAI ---"
|
81
|
-
puts " Navigating to main OpenAI models page: #{main_url}..."
|
82
|
-
|
83
|
-
begin
|
84
|
-
@browser_helper.goto(main_url)
|
85
|
-
return docs_html unless @browser_helper.wait_for_page_load
|
86
|
-
|
87
|
-
puts ' Scraping overview page HTML...'
|
88
|
-
overview_html = @browser_helper.get_page_content('OpenAI Overview')
|
89
|
-
docs_html[overview_key] = overview_html if overview_html && !overview_html.empty?
|
90
|
-
|
91
|
-
# Interactive scraping loop
|
92
|
-
interactive_scraping_loop(docs_html, main_url, main_url_path, overview_key)
|
93
|
-
rescue StandardError => e
|
94
|
-
puts "FATAL: Navigation failed: #{e}"
|
95
|
-
end
|
96
|
-
|
97
|
-
docs_html
|
98
|
-
end
|
99
|
-
|
100
|
-
def interactive_scraping_loop(docs_html, main_url, main_url_path, overview_key) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
101
|
-
loop do # rubocop:disable Metrics/BlockLength
|
102
|
-
current_url = begin
|
103
|
-
@browser_helper.current_url
|
104
|
-
rescue StandardError
|
105
|
-
'N/A'
|
106
|
-
end
|
107
|
-
puts "\n--- OpenAI Subpage Scraping ---"
|
108
|
-
puts "Current URL: #{current_url}"
|
109
|
-
puts "Collected Keys: #{docs_html.keys.sort.join(', ')}"
|
110
|
-
puts '---------------------------------'
|
111
|
-
|
112
|
-
puts 'Ready to scrape current page. Options:'
|
113
|
-
puts 's: Scrape current page'
|
114
|
-
puts 'b: Back to overview'
|
115
|
-
puts 'q: Quit scraping'
|
116
|
-
|
117
|
-
choice = CliHelper.get_user_choice('Choose action', %w[s b q])
|
118
|
-
|
119
|
-
case choice
|
120
|
-
when 's'
|
121
|
-
scrape_current_page(docs_html, main_url_path, overview_key)
|
122
|
-
when 'b'
|
123
|
-
puts " Navigating back to overview: #{main_url}..."
|
124
|
-
begin
|
125
|
-
@browser_helper.goto(main_url)
|
126
|
-
@browser_helper.wait_for_page_load
|
127
|
-
rescue StandardError => e
|
128
|
-
puts " Error navigating back: #{e.message}. Navigate manually."
|
129
|
-
end
|
130
|
-
when 'q'
|
131
|
-
puts ' Finished OpenAI scraping.'
|
132
|
-
break
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
def scrape_current_page(docs_html, main_url_path, overview_key) # rubocop:disable Metrics/MethodLength
|
138
|
-
current_url = begin
|
139
|
-
@browser_helper.current_url
|
140
|
-
rescue StandardError
|
141
|
-
'N/A'
|
142
|
-
end
|
143
|
-
puts " Scraping content from: #{current_url}"
|
144
|
-
page_html = @browser_helper.get_page_content(current_url)
|
145
|
-
|
146
|
-
if page_html && !page_html.empty?
|
147
|
-
page_key = generate_key_from_url(current_url, main_url_path)
|
148
|
-
if page_key != overview_key || !docs_html.key?(overview_key)
|
149
|
-
docs_html[page_key] = page_html
|
150
|
-
puts " Stored HTML under key: #{page_key}"
|
151
|
-
else
|
152
|
-
puts ' Note: Back on overview page, skipping.'
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
next_choice = CliHelper.get_user_choice('Continue? (y: scrape another, n: quit)', %w[y n])
|
157
|
-
false if next_choice == 'n'
|
158
|
-
end
|
159
|
-
|
160
|
-
def handle_standard_scraping(provider, info)
|
161
|
-
provider_name = provider.to_s
|
162
|
-
docs_html = {}
|
163
|
-
|
164
|
-
puts "\n--- Standard Scraping for #{provider_name.upcase} ---"
|
165
|
-
|
166
|
-
# Main page scraping
|
167
|
-
scrape_url(provider_name, info[:url], "#{provider_name}_main_html", docs_html)
|
168
|
-
|
169
|
-
# Pricing page scraping (if available)
|
170
|
-
if info[:pricing_url] && info[:pricing_url] != info[:url]
|
171
|
-
scrape_url(provider_name, info[:pricing_url], "#{provider_name}_pricing_html", docs_html)
|
172
|
-
end
|
173
|
-
|
174
|
-
docs_html
|
175
|
-
end
|
176
|
-
|
177
|
-
def scrape_url(provider_name, url, key_name, docs_html)
|
178
|
-
puts " Scraping #{key_name}: #{url}..."
|
179
|
-
|
180
|
-
begin
|
181
|
-
@browser_helper.goto(url)
|
182
|
-
return unless @browser_helper.wait_for_page_load
|
183
|
-
|
184
|
-
html_content = @browser_helper.get_page_content("#{provider_name} #{key_name}")
|
185
|
-
docs_html[key_name] = html_content if html_content && !html_content.empty?
|
186
|
-
rescue StandardError => e
|
187
|
-
puts " Error on #{key_name}: #{e.message}"
|
188
|
-
end
|
189
|
-
end
|
190
|
-
|
191
|
-
def extract_path_from_url(url_string)
|
192
|
-
URI(url_string).path
|
193
|
-
rescue StandardError
|
194
|
-
'/docs/models'
|
195
|
-
end
|
196
|
-
|
197
|
-
def generate_key_from_url(url_string, base_url_path = '/docs/models')
|
198
|
-
uri = URI(url_string)
|
199
|
-
path = uri.path.chomp('/')
|
200
|
-
return 'models_overview_html' if path == base_url_path
|
201
|
-
|
202
|
-
key_part = path.split('/').reject(&:empty?).last || 'unknown'
|
203
|
-
"model_#{key_part.gsub(/[^a-z0-9_\-]/i, '_')}_html"
|
204
|
-
rescue StandardError => e
|
205
|
-
puts " Warning: URL parsing failed for: #{url_string} (#{e.message})"
|
206
|
-
"scrape_#{Time.now.to_i}_html"
|
207
|
-
end
|
208
|
-
|
209
|
-
def summarize_html_data(docs_html) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
210
|
-
docs_summary = docs_html.map do |k, v|
|
211
|
-
desc = if v.nil? || v.strip.empty?
|
212
|
-
'(Empty)'
|
213
|
-
elsif v.include?('challenge') || v.include?('Checking')
|
214
|
-
'(Challenge?)'
|
215
|
-
else
|
216
|
-
'(OK)'
|
217
|
-
end
|
218
|
-
"#{k}: ~#{v&.length || 0} chars #{desc}"
|
219
|
-
end.join(', ')
|
220
|
-
|
221
|
-
puts "\n HTML Summary: #{docs_summary}"
|
222
|
-
puts ' Warning: LLM results may be inaccurate.' if docs_summary.include?('(Challenge?)')
|
223
|
-
end
|
224
|
-
end
|
data/lib/tasks/cli_helper.rb
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'io/console'
|
4
|
-
|
5
|
-
class CliHelper # rubocop:disable Style/Documentation
|
6
|
-
def self.get_user_choice(prompt, valid_choices) # rubocop:disable Metrics/MethodLength
|
7
|
-
loop do
|
8
|
-
print "\n#{prompt} [#{valid_choices.join('/')}]: "
|
9
|
-
begin
|
10
|
-
choice = $stdin.getch.downcase
|
11
|
-
puts choice # Echo the character
|
12
|
-
|
13
|
-
return choice if valid_choices.include?(choice)
|
14
|
-
|
15
|
-
puts " Invalid choice '#{choice}'. Valid options: #{valid_choices.join(', ')}"
|
16
|
-
rescue Interrupt
|
17
|
-
puts "\n*** Input interrupted."
|
18
|
-
return 'q'
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
data/lib/tasks/code_validator.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'ripper'
|
4
|
-
|
5
|
-
class CodeValidator # rubocop:disable Style/Documentation
|
6
|
-
# Use Ruby's built-in Ripper to validate Ruby syntax instead of eval
|
7
|
-
def self.validate_syntax(code)
|
8
|
-
# Ripper.sexp returns nil if there's a syntax error
|
9
|
-
result = Ripper.sexp(code)
|
10
|
-
|
11
|
-
if result.nil?
|
12
|
-
puts 'Syntax error in generated code'
|
13
|
-
false
|
14
|
-
else
|
15
|
-
# Additional validation could go here
|
16
|
-
true
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def self.extract_code_from_response(content)
|
21
|
-
match = content.match(/```ruby\s*(.*?)\s*```/m)
|
22
|
-
if match
|
23
|
-
match[1].strip
|
24
|
-
else
|
25
|
-
puts 'No Ruby code block found in response'
|
26
|
-
nil
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
data/lib/tasks/model_updater.rb
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'ruby_llm'
|
4
|
-
|
5
|
-
class ModelUpdater # rubocop:disable Style/Documentation
|
6
|
-
def run
|
7
|
-
puts 'Configuring RubyLLM...'
|
8
|
-
configure_from_env
|
9
|
-
|
10
|
-
refresh_models
|
11
|
-
display_model_stats
|
12
|
-
end
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
def configure_from_env
|
17
|
-
RubyLLM.configure do |config|
|
18
|
-
config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
|
19
|
-
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
|
20
|
-
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
|
21
|
-
config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
|
22
|
-
configure_bedrock(config)
|
23
|
-
config.request_timeout = 30
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
def configure_bedrock(config)
|
28
|
-
config.bedrock_api_key = ENV.fetch('AWS_ACCESS_KEY_ID', nil)
|
29
|
-
config.bedrock_secret_key = ENV.fetch('AWS_SECRET_ACCESS_KEY', nil)
|
30
|
-
config.bedrock_region = ENV.fetch('AWS_REGION', nil)
|
31
|
-
config.bedrock_session_token = ENV.fetch('AWS_SESSION_TOKEN', nil)
|
32
|
-
end
|
33
|
-
|
34
|
-
def refresh_models # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
35
|
-
initial_count = RubyLLM.models.all.size
|
36
|
-
puts "Refreshing models (#{initial_count} cached)..."
|
37
|
-
|
38
|
-
models = RubyLLM.models.refresh!
|
39
|
-
|
40
|
-
if models.all.empty? && initial_count.zero?
|
41
|
-
puts 'Error: Failed to fetch models.'
|
42
|
-
exit(1)
|
43
|
-
elsif models.all.size == initial_count && initial_count.positive?
|
44
|
-
puts 'Warning: Model list unchanged.'
|
45
|
-
else
|
46
|
-
puts "Saving models.json (#{models.all.size} models)"
|
47
|
-
models.save_models
|
48
|
-
end
|
49
|
-
|
50
|
-
@models = models
|
51
|
-
end
|
52
|
-
|
53
|
-
def display_model_stats
|
54
|
-
puts "\nModel count:"
|
55
|
-
provider_counts = @models.all.group_by(&:provider).transform_values(&:count)
|
56
|
-
|
57
|
-
RubyLLM::Provider.providers.each_key do |sym|
|
58
|
-
name = sym.to_s.capitalize
|
59
|
-
count = provider_counts[sym.to_s] || 0
|
60
|
-
status = RubyLLM::Provider.providers[sym].configured? ? '(OK)' : '(SKIP)'
|
61
|
-
puts " #{name}: #{count} models #{status}"
|
62
|
-
end
|
63
|
-
|
64
|
-
puts 'Refresh complete.'
|
65
|
-
end
|
66
|
-
end
|
data/lib/tasks/models.rake
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require_relative 'model_updater'
|
4
|
-
require_relative 'capability_scraper'
|
5
|
-
require_relative 'capability_generator'
|
6
|
-
|
7
|
-
namespace :models do # rubocop:disable Metrics/BlockLength
|
8
|
-
desc 'Update available models from providers (API keys needed)'
|
9
|
-
task :update do
|
10
|
-
ModelUpdater.new.run
|
11
|
-
end
|
12
|
-
|
13
|
-
desc 'Update capabilities modules (GEMINI_API_KEY needed)'
|
14
|
-
task :update_capabilities, [:providers] do |_t, args|
|
15
|
-
gemini_key = ENV.fetch('GEMINI_API_KEY', nil)
|
16
|
-
unless gemini_key && !gemini_key.empty?
|
17
|
-
puts 'Error: GEMINI_API_KEY required'
|
18
|
-
exit(1)
|
19
|
-
end
|
20
|
-
|
21
|
-
RubyLLM.configure do |c|
|
22
|
-
c.gemini_api_key = gemini_key
|
23
|
-
c.request_timeout = 300
|
24
|
-
end
|
25
|
-
|
26
|
-
target_providers = CapabilityScraper.parse_providers(args[:providers])
|
27
|
-
|
28
|
-
begin
|
29
|
-
scraper = CapabilityScraper.new(target_providers)
|
30
|
-
scraper.run do |provider, docs_html|
|
31
|
-
generator = CapabilityGenerator.new(provider, docs_html)
|
32
|
-
generator.generate_and_save
|
33
|
-
end
|
34
|
-
rescue StandardError => e
|
35
|
-
puts "Error: #{e.message}"
|
36
|
-
puts e.backtrace.first(5).join("\n")
|
37
|
-
ensure
|
38
|
-
puts 'Update process complete. Review generated files.'
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
task default: ['models:update']
|