ruby_llm 1.10.0 → 1.12.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -2
  3. data/lib/ruby_llm/active_record/acts_as_legacy.rb +41 -7
  4. data/lib/ruby_llm/active_record/chat_methods.rb +41 -7
  5. data/lib/ruby_llm/agent.rb +323 -0
  6. data/lib/ruby_llm/aliases.json +50 -32
  7. data/lib/ruby_llm/chat.rb +27 -3
  8. data/lib/ruby_llm/configuration.rb +4 -0
  9. data/lib/ruby_llm/models.json +19806 -5991
  10. data/lib/ruby_llm/models.rb +35 -6
  11. data/lib/ruby_llm/provider.rb +13 -1
  12. data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
  13. data/lib/ruby_llm/providers/azure/chat.rb +29 -0
  14. data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
  15. data/lib/ruby_llm/providers/azure/media.rb +45 -0
  16. data/lib/ruby_llm/providers/azure/models.rb +14 -0
  17. data/lib/ruby_llm/providers/azure.rb +56 -0
  18. data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
  19. data/lib/ruby_llm/providers/bedrock/chat.rb +297 -56
  20. data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
  21. data/lib/ruby_llm/providers/bedrock/models.rb +88 -65
  22. data/lib/ruby_llm/providers/bedrock/streaming.rb +305 -8
  23. data/lib/ruby_llm/providers/bedrock.rb +61 -52
  24. data/lib/ruby_llm/providers/openai/media.rb +1 -1
  25. data/lib/ruby_llm/providers/xai/chat.rb +15 -0
  26. data/lib/ruby_llm/providers/xai/models.rb +75 -0
  27. data/lib/ruby_llm/providers/xai.rb +28 -0
  28. data/lib/ruby_llm/version.rb +1 -1
  29. data/lib/ruby_llm.rb +14 -8
  30. data/lib/tasks/models.rake +10 -4
  31. data/lib/tasks/vcr.rake +32 -0
  32. metadata +16 -13
  33. data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
  34. data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
  35. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
  36. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -128
  37. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
  38. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -85
  39. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class XAI
6
+ # Models metadata for xAI list models.
7
+ module Models
8
+ module_function
9
+
10
+ IMAGE_MODELS = %w[grok-2-image-1212].freeze
11
+ VISION_MODELS = %w[
12
+ grok-2-vision-1212
13
+ grok-4-0709
14
+ grok-4-fast-non-reasoning
15
+ grok-4-fast-reasoning
16
+ grok-4-1-fast-non-reasoning
17
+ grok-4-1-fast-reasoning
18
+ ].freeze
19
+ REASONING_MODELS = %w[
20
+ grok-3-mini
21
+ grok-4-0709
22
+ grok-4-fast-reasoning
23
+ grok-4-1-fast-reasoning
24
+ grok-code-fast-1
25
+ ].freeze
26
+
27
+ def parse_list_models_response(response, slug, _capabilities)
28
+ Array(response.body['data']).map do |model_data|
29
+ model_id = model_data['id']
30
+
31
+ Model::Info.new(
32
+ id: model_id,
33
+ name: format_display_name(model_id),
34
+ provider: slug,
35
+ family: 'grok',
36
+ created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
37
+ context_window: nil,
38
+ max_output_tokens: nil,
39
+ modalities: modalities_for(model_id),
40
+ capabilities: capabilities_for(model_id),
41
+ pricing: {},
42
+ metadata: {
43
+ object: model_data['object'],
44
+ owned_by: model_data['owned_by']
45
+ }.compact
46
+ )
47
+ end
48
+ end
49
+
50
+ def modalities_for(model_id)
51
+ if IMAGE_MODELS.include?(model_id)
52
+ { input: ['text'], output: ['image'] }
53
+ else
54
+ input = ['text']
55
+ input << 'image' if VISION_MODELS.include?(model_id)
56
+ { input: input, output: ['text'] }
57
+ end
58
+ end
59
+
60
+ def capabilities_for(model_id)
61
+ return [] if IMAGE_MODELS.include?(model_id)
62
+
63
+ capabilities = %w[streaming function_calling structured_output]
64
+ capabilities << 'reasoning' if REASONING_MODELS.include?(model_id)
65
+ capabilities << 'vision' if VISION_MODELS.include?(model_id)
66
+ capabilities
67
+ end
68
+
69
+ def format_display_name(model_id)
70
+ model_id.tr('-', ' ').split.map(&:capitalize).join(' ')
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # xAI API integration
6
+ class XAI < OpenAI
7
+ include XAI::Chat
8
+ include XAI::Models
9
+
10
+ def api_base
11
+ 'https://api.x.ai/v1'
12
+ end
13
+
14
+ def headers
15
+ {
16
+ 'Authorization' => "Bearer #{@config.xai_api_key}",
17
+ 'Content-Type' => 'application/json'
18
+ }
19
+ end
20
+
21
+ class << self
22
+ def configuration_requirements
23
+ %i[xai_api_key]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '1.10.0'
4
+ VERSION = '1.12.0'
5
5
  end
data/lib/ruby_llm.rb CHANGED
@@ -9,23 +9,27 @@ require 'json'
9
9
  require 'logger'
10
10
  require 'marcel'
11
11
  require 'securerandom'
12
+ require 'date'
13
+ require 'time'
12
14
  require 'zeitwerk'
13
15
 
14
16
  loader = Zeitwerk::Loader.for_gem
15
17
  loader.inflector.inflect(
16
- 'ruby_llm' => 'RubyLLM',
17
- 'llm' => 'LLM',
18
- 'openai' => 'OpenAI',
18
+ 'azure' => 'Azure',
19
+ 'UI' => 'UI',
19
20
  'api' => 'API',
20
- 'deepseek' => 'DeepSeek',
21
- 'perplexity' => 'Perplexity',
22
21
  'bedrock' => 'Bedrock',
23
- 'openrouter' => 'OpenRouter',
22
+ 'deepseek' => 'DeepSeek',
24
23
  'gpustack' => 'GPUStack',
24
+ 'llm' => 'LLM',
25
25
  'mistral' => 'Mistral',
26
- 'vertexai' => 'VertexAI',
26
+ 'openai' => 'OpenAI',
27
+ 'openrouter' => 'OpenRouter',
27
28
  'pdf' => 'PDF',
28
- 'UI' => 'UI'
29
+ 'perplexity' => 'Perplexity',
30
+ 'ruby_llm' => 'RubyLLM',
31
+ 'vertexai' => 'VertexAI',
32
+ 'xai' => 'XAI'
29
33
  )
30
34
  loader.ignore("#{__dir__}/tasks")
31
35
  loader.ignore("#{__dir__}/generators")
@@ -90,6 +94,7 @@ module RubyLLM
90
94
  end
91
95
 
92
96
  RubyLLM::Provider.register :anthropic, RubyLLM::Providers::Anthropic
97
+ RubyLLM::Provider.register :azure, RubyLLM::Providers::Azure
93
98
  RubyLLM::Provider.register :bedrock, RubyLLM::Providers::Bedrock
94
99
  RubyLLM::Provider.register :deepseek, RubyLLM::Providers::DeepSeek
95
100
  RubyLLM::Provider.register :gemini, RubyLLM::Providers::Gemini
@@ -100,6 +105,7 @@ RubyLLM::Provider.register :openai, RubyLLM::Providers::OpenAI
100
105
  RubyLLM::Provider.register :openrouter, RubyLLM::Providers::OpenRouter
101
106
  RubyLLM::Provider.register :perplexity, RubyLLM::Providers::Perplexity
102
107
  RubyLLM::Provider.register :vertexai, RubyLLM::Providers::VertexAI
108
+ RubyLLM::Provider.register :xai, RubyLLM::Providers::XAI
103
109
 
104
110
  if defined?(Rails::Railtie)
105
111
  require 'ruby_llm/railtie'
@@ -39,15 +39,18 @@ end
39
39
 
40
40
  def configure_from_env
41
41
  RubyLLM.configure do |config|
42
- config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
43
42
  config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
44
- config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
43
+ config.azure_api_base = ENV.fetch('AZURE_API_BASE', nil)
44
+ config.azure_api_key = ENV.fetch('AZURE_API_KEY', nil)
45
45
  config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
46
- config.perplexity_api_key = ENV.fetch('PERPLEXITY_API_KEY', nil)
47
- config.openrouter_api_key = ENV.fetch('OPENROUTER_API_KEY', nil)
46
+ config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
48
47
  config.mistral_api_key = ENV.fetch('MISTRAL_API_KEY', nil)
48
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
49
+ config.openrouter_api_key = ENV.fetch('OPENROUTER_API_KEY', nil)
50
+ config.perplexity_api_key = ENV.fetch('PERPLEXITY_API_KEY', nil)
49
51
  config.vertexai_location = ENV.fetch('GOOGLE_CLOUD_LOCATION', nil)
50
52
  config.vertexai_project_id = ENV.fetch('GOOGLE_CLOUD_PROJECT', nil)
53
+ config.xai_api_key = ENV.fetch('XAI_API_KEY', nil)
51
54
  configure_bedrock(config)
52
55
  config.request_timeout = 30
53
56
  end
@@ -330,6 +333,7 @@ def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
330
333
  # OpenAI models
331
334
  models['openai'].each do |model|
332
335
  openrouter_model = "openai/#{model}"
336
+ azure_model = models['azure'].include?(model) ? model : nil
333
337
  next unless models['openrouter'].include?(openrouter_model)
334
338
 
335
339
  alias_key = model.gsub('-latest', '')
@@ -337,6 +341,7 @@ def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
337
341
  'openai' => model,
338
342
  'openrouter' => openrouter_model
339
343
  }
344
+ aliases[alias_key]['azure'] = azure_model if azure_model
340
345
  end
341
346
 
342
347
  anthropic_latest = group_anthropic_models_by_base_name(models['anthropic'])
@@ -357,6 +362,7 @@ def generate_aliases # rubocop:disable Metrics/PerceivedComplexity
357
362
  aliases[base_name] = { 'anthropic' => latest_model }
358
363
  aliases[base_name]['openrouter'] = openrouter_model if openrouter_model
359
364
  aliases[base_name]['bedrock'] = bedrock_model if bedrock_model
365
+ aliases[base_name]['azure'] = latest_model if models['azure'].include?(latest_model)
360
366
  end
361
367
 
362
368
  models['bedrock'].each do |bedrock_model|
data/lib/tasks/vcr.rake CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'dotenv/load'
4
+ require 'time'
4
5
 
5
6
  def record_all_cassettes(cassette_dir)
6
7
  FileUtils.rm_rf(cassette_dir)
@@ -73,6 +74,31 @@ def run_tests
73
74
  system('bundle exec rspec') || abort('Tests failed')
74
75
  end
75
76
 
77
+ def retimestamp_cassettes(cassette_dir)
78
+ timestamp = Time.now.utc.httpdate
79
+ updated_files = 0
80
+ updated_entries = 0
81
+
82
+ Dir.glob("#{cassette_dir}/**/*.yml").each do |file|
83
+ content = File.read(file)
84
+ replacements = 0
85
+
86
+ updated = content.gsub(/^(\s*recorded_at:\s*).+$/) do
87
+ replacements += 1
88
+ "#{Regexp.last_match(1)}#{timestamp}"
89
+ end
90
+
91
+ next if replacements.zero?
92
+
93
+ File.write(file, updated)
94
+ updated_files += 1
95
+ updated_entries += replacements
96
+ end
97
+
98
+ puts "Updated #{updated_entries} recorded_at entries in #{updated_files} cassette files."
99
+ puts "New recorded_at value: #{timestamp}"
100
+ end
101
+
76
102
  namespace :vcr do
77
103
  desc 'Record VCR cassettes (rake vcr:record[all] or vcr:record[openai,anthropic])'
78
104
  task :record, :providers do |_, args|
@@ -89,4 +115,10 @@ namespace :vcr do
89
115
  record_for_providers(providers, cassette_dir)
90
116
  end
91
117
  end
118
+
119
+ desc 'Update recorded_at timestamps for all cassette entries'
120
+ task :retimestamp do
121
+ cassette_dir = 'spec/fixtures/vcr_cassettes'
122
+ retimestamp_cassettes(cassette_dir)
123
+ end
92
124
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.0
4
+ version: 1.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -43,28 +43,28 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 1.10.0
46
+ version: '2.0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 1.10.0
53
+ version: '2.0'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: faraday-retry
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '1'
60
+ version: '2.0'
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: '1'
67
+ version: '2.0'
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: faraday-multipart
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -192,6 +192,7 @@ files:
192
192
  - lib/ruby_llm/active_record/chat_methods.rb
193
193
  - lib/ruby_llm/active_record/message_methods.rb
194
194
  - lib/ruby_llm/active_record/model_methods.rb
195
+ - lib/ruby_llm/agent.rb
195
196
  - lib/ruby_llm/aliases.json
196
197
  - lib/ruby_llm/aliases.rb
197
198
  - lib/ruby_llm/attachment.rb
@@ -226,18 +227,17 @@ files:
226
227
  - lib/ruby_llm/providers/anthropic/models.rb
227
228
  - lib/ruby_llm/providers/anthropic/streaming.rb
228
229
  - lib/ruby_llm/providers/anthropic/tools.rb
230
+ - lib/ruby_llm/providers/azure.rb
231
+ - lib/ruby_llm/providers/azure/chat.rb
232
+ - lib/ruby_llm/providers/azure/embeddings.rb
233
+ - lib/ruby_llm/providers/azure/media.rb
234
+ - lib/ruby_llm/providers/azure/models.rb
229
235
  - lib/ruby_llm/providers/bedrock.rb
230
- - lib/ruby_llm/providers/bedrock/capabilities.rb
236
+ - lib/ruby_llm/providers/bedrock/auth.rb
231
237
  - lib/ruby_llm/providers/bedrock/chat.rb
232
238
  - lib/ruby_llm/providers/bedrock/media.rb
233
239
  - lib/ruby_llm/providers/bedrock/models.rb
234
- - lib/ruby_llm/providers/bedrock/signing.rb
235
240
  - lib/ruby_llm/providers/bedrock/streaming.rb
236
- - lib/ruby_llm/providers/bedrock/streaming/base.rb
237
- - lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb
238
- - lib/ruby_llm/providers/bedrock/streaming/message_processing.rb
239
- - lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb
240
- - lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb
241
241
  - lib/ruby_llm/providers/deepseek.rb
242
242
  - lib/ruby_llm/providers/deepseek/capabilities.rb
243
243
  - lib/ruby_llm/providers/deepseek/chat.rb
@@ -290,6 +290,9 @@ files:
290
290
  - lib/ruby_llm/providers/vertexai/models.rb
291
291
  - lib/ruby_llm/providers/vertexai/streaming.rb
292
292
  - lib/ruby_llm/providers/vertexai/transcription.rb
293
+ - lib/ruby_llm/providers/xai.rb
294
+ - lib/ruby_llm/providers/xai/chat.rb
295
+ - lib/ruby_llm/providers/xai/models.rb
293
296
  - lib/ruby_llm/railtie.rb
294
297
  - lib/ruby_llm/stream_accumulator.rb
295
298
  - lib/ruby_llm/streaming.rb
@@ -332,7 +335,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
332
335
  - !ruby/object:Gem::Version
333
336
  version: '0'
334
337
  requirements: []
335
- rubygems_version: 3.6.9
338
+ rubygems_version: 4.0.3
336
339
  specification_version: 4
337
340
  summary: One beautiful Ruby API for GPT, Claude, Gemini, and more.
338
341
  test_files: []
@@ -1,167 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RubyLLM
4
- module Providers
5
- class Bedrock
6
- # Determines capabilities and pricing for AWS Bedrock models
7
- module Capabilities
8
- module_function
9
-
10
- def context_window_for(model_id)
11
- case model_id
12
- when /anthropic\.claude-2/ then 100_000
13
- else 200_000
14
- end
15
- end
16
-
17
- def max_tokens_for(_model_id)
18
- 4_096
19
- end
20
-
21
- def input_price_for(model_id)
22
- PRICES.dig(model_family(model_id), :input) || default_input_price
23
- end
24
-
25
- def output_price_for(model_id)
26
- PRICES.dig(model_family(model_id), :output) || default_output_price
27
- end
28
-
29
- def supports_chat?(model_id)
30
- model_id.match?(/anthropic\.claude/)
31
- end
32
-
33
- def supports_streaming?(model_id)
34
- model_id.match?(/anthropic\.claude/)
35
- end
36
-
37
- def supports_images?(model_id)
38
- model_id.match?(/anthropic\.claude/)
39
- end
40
-
41
- def supports_vision?(model_id)
42
- model_id.match?(/anthropic\.claude/)
43
- end
44
-
45
- def supports_functions?(model_id)
46
- model_id.match?(/anthropic\.claude/)
47
- end
48
-
49
- def supports_audio?(_model_id)
50
- false
51
- end
52
-
53
- def supports_json_mode?(model_id)
54
- model_id.match?(/anthropic\.claude/)
55
- end
56
-
57
- def format_display_name(model_id)
58
- model_id.then { |id| humanize(id) }
59
- end
60
-
61
- def model_type(_model_id)
62
- 'chat'
63
- end
64
-
65
- def supports_structured_output?(_model_id)
66
- false
67
- end
68
-
69
- # Model family patterns for capability lookup
70
- MODEL_FAMILIES = {
71
- /anthropic\.claude-3-opus/ => :claude3_opus,
72
- /anthropic\.claude-3-sonnet/ => :claude3_sonnet,
73
- /anthropic\.claude-3-5-sonnet/ => :claude3_sonnet,
74
- /anthropic\.claude-3-7-sonnet/ => :claude3_sonnet,
75
- /anthropic\.claude-3-haiku/ => :claude3_haiku,
76
- /anthropic\.claude-3-5-haiku/ => :claude3_5_haiku,
77
- /anthropic\.claude-v2/ => :claude2,
78
- /anthropic\.claude-instant/ => :claude_instant
79
- }.freeze
80
-
81
- def model_family(model_id)
82
- MODEL_FAMILIES.find { |pattern, _family| model_id.match?(pattern) }&.last || :other
83
- end
84
-
85
- # Pricing information for Bedrock models (per million tokens)
86
- PRICES = {
87
- claude3_opus: { input: 15.0, output: 75.0 },
88
- claude3_sonnet: { input: 3.0, output: 15.0 },
89
- claude3_haiku: { input: 0.25, output: 1.25 },
90
- claude3_5_haiku: { input: 0.8, output: 4.0 },
91
- claude2: { input: 8.0, output: 24.0 },
92
- claude_instant: { input: 0.8, output: 2.4 }
93
- }.freeze
94
-
95
- def default_input_price
96
- 0.1
97
- end
98
-
99
- def default_output_price
100
- 0.2
101
- end
102
-
103
- def humanize(id)
104
- id.tr('-', ' ')
105
- .split('.')
106
- .last
107
- .split
108
- .map(&:capitalize)
109
- .join(' ')
110
- end
111
-
112
- def modalities_for(model_id)
113
- modalities = {
114
- input: ['text'],
115
- output: ['text']
116
- }
117
-
118
- if model_id.match?(/anthropic\.claude/) && supports_vision?(model_id)
119
- modalities[:input] << 'image'
120
- modalities[:input] << 'pdf'
121
- end
122
-
123
- modalities
124
- end
125
-
126
- def capabilities_for(model_id)
127
- capabilities = []
128
-
129
- capabilities << 'streaming' if model_id.match?(/anthropic\.claude/)
130
-
131
- capabilities << 'function_calling' if supports_functions?(model_id)
132
-
133
- capabilities << 'reasoning' if model_id.match?(/claude-3-7/)
134
-
135
- if model_id.match?(/claude-3\.5|claude-3-7/)
136
- capabilities << 'batch'
137
- capabilities << 'citations'
138
- end
139
-
140
- capabilities
141
- end
142
-
143
- def pricing_for(model_id)
144
- family = model_family(model_id)
145
- prices = PRICES.fetch(family, { input: default_input_price, output: default_output_price })
146
-
147
- standard_pricing = {
148
- input_per_million: prices[:input],
149
- output_per_million: prices[:output]
150
- }
151
-
152
- batch_pricing = {
153
- input_per_million: prices[:input] * 0.5,
154
- output_per_million: prices[:output] * 0.5
155
- }
156
-
157
- {
158
- text_tokens: {
159
- standard: standard_pricing,
160
- batch: batch_pricing
161
- }
162
- }
163
- end
164
- end
165
- end
166
- end
167
- end