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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -133
  3. data/lib/ruby_llm/active_record/acts_as.rb +144 -47
  4. data/lib/ruby_llm/aliases.json +187 -17
  5. data/lib/ruby_llm/attachment.rb +164 -0
  6. data/lib/ruby_llm/chat.rb +31 -20
  7. data/lib/ruby_llm/configuration.rb +34 -1
  8. data/lib/ruby_llm/connection.rb +121 -0
  9. data/lib/ruby_llm/content.rb +27 -79
  10. data/lib/ruby_llm/context.rb +30 -0
  11. data/lib/ruby_llm/embedding.rb +13 -5
  12. data/lib/ruby_llm/error.rb +2 -1
  13. data/lib/ruby_llm/image.rb +15 -8
  14. data/lib/ruby_llm/message.rb +14 -6
  15. data/lib/ruby_llm/mime_type.rb +67 -0
  16. data/lib/ruby_llm/model/info.rb +101 -0
  17. data/lib/ruby_llm/model/modalities.rb +22 -0
  18. data/lib/ruby_llm/model/pricing.rb +51 -0
  19. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  20. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  21. data/lib/ruby_llm/model.rb +7 -0
  22. data/lib/ruby_llm/models.json +26279 -2362
  23. data/lib/ruby_llm/models.rb +95 -14
  24. data/lib/ruby_llm/provider.rb +48 -90
  25. data/lib/ruby_llm/providers/anthropic/capabilities.rb +76 -13
  26. data/lib/ruby_llm/providers/anthropic/chat.rb +7 -14
  27. data/lib/ruby_llm/providers/anthropic/media.rb +49 -28
  28. data/lib/ruby_llm/providers/anthropic/models.rb +16 -16
  29. data/lib/ruby_llm/providers/anthropic/tools.rb +2 -2
  30. data/lib/ruby_llm/providers/anthropic.rb +3 -3
  31. data/lib/ruby_llm/providers/bedrock/capabilities.rb +61 -2
  32. data/lib/ruby_llm/providers/bedrock/chat.rb +30 -73
  33. data/lib/ruby_llm/providers/bedrock/media.rb +59 -0
  34. data/lib/ruby_llm/providers/bedrock/models.rb +50 -58
  35. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +16 -0
  36. data/lib/ruby_llm/providers/bedrock.rb +14 -25
  37. data/lib/ruby_llm/providers/deepseek/capabilities.rb +35 -2
  38. data/lib/ruby_llm/providers/deepseek.rb +3 -3
  39. data/lib/ruby_llm/providers/gemini/capabilities.rb +84 -3
  40. data/lib/ruby_llm/providers/gemini/chat.rb +8 -37
  41. data/lib/ruby_llm/providers/gemini/embeddings.rb +18 -34
  42. data/lib/ruby_llm/providers/gemini/images.rb +4 -3
  43. data/lib/ruby_llm/providers/gemini/media.rb +28 -111
  44. data/lib/ruby_llm/providers/gemini/models.rb +17 -23
  45. data/lib/ruby_llm/providers/gemini/tools.rb +1 -1
  46. data/lib/ruby_llm/providers/gemini.rb +3 -3
  47. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  48. data/lib/ruby_llm/providers/ollama/media.rb +48 -0
  49. data/lib/ruby_llm/providers/ollama.rb +34 -0
  50. data/lib/ruby_llm/providers/openai/capabilities.rb +78 -3
  51. data/lib/ruby_llm/providers/openai/chat.rb +6 -4
  52. data/lib/ruby_llm/providers/openai/embeddings.rb +8 -12
  53. data/lib/ruby_llm/providers/openai/images.rb +3 -2
  54. data/lib/ruby_llm/providers/openai/media.rb +48 -21
  55. data/lib/ruby_llm/providers/openai/models.rb +17 -18
  56. data/lib/ruby_llm/providers/openai/tools.rb +9 -5
  57. data/lib/ruby_llm/providers/openai.rb +7 -5
  58. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  59. data/lib/ruby_llm/providers/openrouter.rb +31 -0
  60. data/lib/ruby_llm/stream_accumulator.rb +4 -4
  61. data/lib/ruby_llm/streaming.rb +48 -13
  62. data/lib/ruby_llm/utils.rb +27 -0
  63. data/lib/ruby_llm/version.rb +1 -1
  64. data/lib/ruby_llm.rb +15 -5
  65. data/lib/tasks/aliases.rake +235 -0
  66. data/lib/tasks/models_docs.rake +164 -121
  67. data/lib/tasks/models_update.rake +79 -0
  68. data/lib/tasks/release.rake +32 -0
  69. data/lib/tasks/vcr.rake +4 -2
  70. metadata +56 -32
  71. data/lib/ruby_llm/model_info.rb +0 -56
  72. data/lib/tasks/browser_helper.rb +0 -97
  73. data/lib/tasks/capability_generator.rb +0 -123
  74. data/lib/tasks/capability_scraper.rb +0 -224
  75. data/lib/tasks/cli_helper.rb +0 -22
  76. data/lib/tasks/code_validator.rb +0 -29
  77. data/lib/tasks/model_updater.rb +0 -66
  78. 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
@@ -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
@@ -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
@@ -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
@@ -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']