ruby_llm 1.15.0 → 1.16.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
  4. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
  5. data/lib/ruby_llm/active_record/acts_as.rb +1 -26
  6. data/lib/ruby_llm/active_record/acts_as_legacy.rb +71 -4
  7. data/lib/ruby_llm/active_record/chat_methods.rb +2 -2
  8. data/lib/ruby_llm/active_record/message_methods.rb +70 -3
  9. data/lib/ruby_llm/agent.rb +1 -0
  10. data/lib/ruby_llm/aliases.json +78 -75
  11. data/lib/ruby_llm/aliases.rb +3 -0
  12. data/lib/ruby_llm/attachment.rb +34 -17
  13. data/lib/ruby_llm/chat.rb +176 -47
  14. data/lib/ruby_llm/configuration.rb +14 -1
  15. data/lib/ruby_llm/connection.rb +36 -7
  16. data/lib/ruby_llm/content.rb +15 -1
  17. data/lib/ruby_llm/deprecator.rb +24 -0
  18. data/lib/ruby_llm/embedding.rb +31 -1
  19. data/lib/ruby_llm/error.rb +11 -75
  20. data/lib/ruby_llm/error_middleware.rb +81 -0
  21. data/lib/ruby_llm/image.rb +2 -0
  22. data/lib/ruby_llm/instrumentation.rb +36 -0
  23. data/lib/ruby_llm/mime_type.rb +25 -0
  24. data/lib/ruby_llm/model/info.rb +36 -2
  25. data/lib/ruby_llm/model/pricing.rb +19 -9
  26. data/lib/ruby_llm/model/pricing_tier.rb +20 -9
  27. data/lib/ruby_llm/model_registry.rb +39 -0
  28. data/lib/ruby_llm/models.json +18225 -19144
  29. data/lib/ruby_llm/models.rb +95 -30
  30. data/lib/ruby_llm/provider.rb +11 -2
  31. data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
  32. data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
  33. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  34. data/lib/ruby_llm/providers/anthropic/tools.rb +28 -2
  35. data/lib/ruby_llm/providers/azure/media.rb +1 -1
  36. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
  37. data/lib/ruby_llm/providers/bedrock/chat.rb +2 -0
  38. data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
  39. data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
  40. data/lib/ruby_llm/providers/bedrock/streaming.rb +6 -0
  41. data/lib/ruby_llm/providers/bedrock.rb +2 -2
  42. data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
  43. data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
  44. data/lib/ruby_llm/providers/gemini/chat.rb +2 -3
  45. data/lib/ruby_llm/providers/gemini/media.rb +16 -9
  46. data/lib/ruby_llm/providers/gemini/streaming.rb +2 -0
  47. data/lib/ruby_llm/providers/gemini/tools.rb +2 -0
  48. data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
  49. data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
  50. data/lib/ruby_llm/providers/mistral/capabilities.rb +1 -1
  51. data/lib/ruby_llm/providers/mistral/chat.rb +1 -1
  52. data/lib/ruby_llm/providers/mistral/media.rb +55 -0
  53. data/lib/ruby_llm/providers/mistral/models.rb +2 -0
  54. data/lib/ruby_llm/providers/mistral.rb +2 -2
  55. data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
  56. data/lib/ruby_llm/providers/openai/chat.rb +16 -1
  57. data/lib/ruby_llm/providers/openai/images.rb +9 -9
  58. data/lib/ruby_llm/providers/openai/media.rb +40 -16
  59. data/lib/ruby_llm/providers/openai/streaming.rb +2 -0
  60. data/lib/ruby_llm/providers/openai/tools.rb +2 -0
  61. data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
  62. data/lib/ruby_llm/providers/openrouter/chat.rb +6 -2
  63. data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
  64. data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
  65. data/lib/ruby_llm/providers/perplexity.rb +2 -2
  66. data/lib/ruby_llm/providers/vertexai.rb +5 -1
  67. data/lib/ruby_llm/providers/xai/chat.rb +9 -0
  68. data/lib/ruby_llm/providers/xai/models.rb +15 -27
  69. data/lib/ruby_llm/providers/xai.rb +2 -2
  70. data/lib/ruby_llm/railtie.rb +5 -1
  71. data/lib/ruby_llm/stream_accumulator.rb +45 -30
  72. data/lib/ruby_llm/streaming.rb +4 -0
  73. data/lib/ruby_llm/tool_concurrency.rb +105 -0
  74. data/lib/ruby_llm/transcription.rb +2 -1
  75. data/lib/ruby_llm/utils.rb +39 -0
  76. data/lib/ruby_llm/version.rb +1 -1
  77. data/lib/ruby_llm.rb +9 -2
  78. data/lib/tasks/models.rake +32 -4
  79. data/lib/tasks/release.rake +50 -23
  80. metadata +17 -10
@@ -72,8 +72,7 @@ module RubyLLM
72
72
  def format_role(role)
73
73
  case role
74
74
  when :assistant then 'model'
75
- when :system then 'user'
76
- when :tool then 'function'
75
+ when :system, :tool then 'user'
77
76
  else role.to_s
78
77
  end
79
78
  end
@@ -314,7 +313,7 @@ module RubyLLM
314
313
  end
315
314
 
316
315
  def build_tool_response(parts)
317
- { role: 'function', parts: parts }
316
+ { role: 'user', parts: parts }
318
317
  end
319
318
 
320
319
  def remember_tool_calls
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'base64'
4
+ require 'stringio'
5
+
3
6
  module RubyLLM
4
7
  module Providers
5
8
  class Gemini # rubocop:disable Style/Documentation
@@ -16,19 +19,23 @@ module RubyLLM
16
19
  parts << format_text(content.text) if content.text
17
20
 
18
21
  content.attachments.each do |attachment|
19
- case attachment.type
20
- when :text
21
- parts << format_text_file(attachment)
22
- when :unknown
23
- raise UnsupportedAttachmentError, attachment.mime_type
24
- else
25
- parts << format_attachment(attachment)
26
- end
22
+ parts << format_content_attachment(attachment)
27
23
  end
28
24
 
29
25
  parts
30
26
  end
31
27
 
28
+ def format_content_attachment(attachment)
29
+ case attachment.type
30
+ when :text
31
+ format_text_file(attachment)
32
+ when :document, :unknown
33
+ raise UnsupportedAttachmentError, attachment.mime_type
34
+ else
35
+ format_attachment(attachment)
36
+ end
37
+ end
38
+
32
39
  def format_attachment(attachment)
33
40
  {
34
41
  inline_data: {
@@ -71,7 +78,7 @@ module RubyLLM
71
78
  text = nil if text.empty?
72
79
  return text if attachments.empty?
73
80
 
74
- Content.new(text:, attachments:)
81
+ Content.new(text, attachments)
75
82
  end
76
83
 
77
84
  def build_inline_attachment(inline_data, index)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module RubyLLM
4
6
  module Providers
5
7
  class Gemini
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
4
+
3
5
  module RubyLLM
4
6
  module Providers
5
7
  class Gemini
@@ -11,13 +11,20 @@ module RubyLLM
11
11
  messages.map do |msg|
12
12
  {
13
13
  role: format_role(msg.role),
14
- content: GPUStack::Media.format_content(msg.content),
14
+ content: format_message_content(msg),
15
15
  tool_calls: format_tool_calls(msg.tool_calls),
16
16
  tool_call_id: msg.tool_call_id
17
17
  }.compact.merge(OpenAI::Chat.format_thinking(msg))
18
18
  end
19
19
  end
20
20
 
21
+ def format_message_content(msg)
22
+ content = GPUStack::Media.format_content(msg.content)
23
+ return '' if content.nil? && OpenAI::Chat.thinking_only_assistant_message?(msg)
24
+
25
+ content
26
+ end
27
+
21
28
  def format_role(role)
22
29
  role.to_s
23
30
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module RubyLLM
4
6
  module Providers
5
7
  class GPUStack
@@ -123,7 +123,7 @@ module RubyLLM
123
123
  }
124
124
  end
125
125
 
126
- def release_date_for(model_id)
126
+ def release_date_for(model_id) # rubocop:disable Metrics/CyclomaticComplexity
127
127
  case model_id
128
128
  when 'open-mistral-7b', 'mistral-tiny' then '2023-09-27'
129
129
  when 'mistral-medium-2312', 'mistral-small-2312', 'mistral-small',
@@ -52,7 +52,7 @@ module RubyLLM
52
52
  end
53
53
 
54
54
  def format_content_with_thinking(msg)
55
- formatted_content = OpenAI::Media.format_content(msg.content)
55
+ formatted_content = Mistral::Media.format_content(msg.content)
56
56
  return formatted_content unless msg.role == :assistant && msg.thinking
57
57
 
58
58
  content_blocks = build_thinking_blocks(msg.thinking)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Mistral
6
+ # Handles media content for Mistral Chat Completions.
7
+ module Media
8
+ module_function
9
+
10
+ def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
11
+ if content.is_a?(RubyLLM::Content::Raw)
12
+ value = content.value
13
+ return value.is_a?(Hash) ? value.to_json : value
14
+ end
15
+ return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
16
+ return content unless content.is_a?(Content)
17
+
18
+ parts = []
19
+ parts << OpenAI::Media.format_text(content.text) if content.text
20
+
21
+ content.attachments.each do |attachment|
22
+ case attachment.type
23
+ when :image
24
+ parts << format_image(attachment)
25
+ when :audio
26
+ parts << OpenAI::Media.format_audio(attachment)
27
+ when :pdf, :document
28
+ parts << format_document(attachment)
29
+ when :text
30
+ parts << OpenAI::Media.format_text_file(attachment)
31
+ else
32
+ raise UnsupportedAttachmentError, attachment.mime_type
33
+ end
34
+ end
35
+
36
+ parts
37
+ end
38
+
39
+ def format_image(image)
40
+ {
41
+ type: 'image_url',
42
+ image_url: image.url? ? image.source.to_s : image.for_llm
43
+ }
44
+ end
45
+
46
+ def format_document(document)
47
+ {
48
+ type: 'document_url',
49
+ document_url: document.url? ? document.source.to_s : document.for_llm
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module RubyLLM
4
6
  module Providers
5
7
  class Mistral
@@ -9,7 +9,7 @@ module RubyLLM
9
9
  include Mistral::Embeddings
10
10
 
11
11
  def api_base
12
- 'https://api.mistral.ai/v1'
12
+ @config.mistral_api_base || 'https://api.mistral.ai/v1'
13
13
  end
14
14
 
15
15
  def headers
@@ -24,7 +24,7 @@ module RubyLLM
24
24
  end
25
25
 
26
26
  def configuration_options
27
- %i[mistral_api_key]
27
+ %i[mistral_api_key mistral_api_base]
28
28
  end
29
29
 
30
30
  def configuration_requirements
@@ -11,13 +11,20 @@ module RubyLLM
11
11
  messages.map do |msg|
12
12
  {
13
13
  role: format_role(msg.role),
14
- content: Ollama::Media.format_content(msg.content),
14
+ content: format_message_content(msg),
15
15
  tool_calls: format_tool_calls(msg.tool_calls),
16
16
  tool_call_id: msg.tool_call_id
17
17
  }.compact.merge(OpenAI::Chat.format_thinking(msg))
18
18
  end
19
19
  end
20
20
 
21
+ def format_message_content(msg)
22
+ content = Ollama::Media.format_content(msg.content)
23
+ return '' if content.nil? && OpenAI::Chat.thinking_only_assistant_message?(msg)
24
+
25
+ content
26
+ end
27
+
21
28
  def format_role(role)
22
29
  role.to_s
23
30
  end
@@ -125,13 +125,28 @@ module RubyLLM
125
125
  messages.map do |msg|
126
126
  {
127
127
  role: format_role(msg.role),
128
- content: Media.format_content(msg.content),
128
+ content: format_message_content(msg),
129
129
  tool_calls: format_tool_calls(msg.tool_calls),
130
130
  tool_call_id: msg.tool_call_id
131
131
  }.compact.merge(format_thinking(msg))
132
132
  end
133
133
  end
134
134
 
135
+ def format_message_content(msg)
136
+ content = format_content(msg.content)
137
+ return '' if content.nil? && thinking_only_assistant_message?(msg)
138
+
139
+ content
140
+ end
141
+
142
+ def thinking_only_assistant_message?(msg)
143
+ msg.role == :assistant && msg.thinking && !msg.tool_call?
144
+ end
145
+
146
+ def format_content(content)
147
+ Media.format_content(content)
148
+ end
149
+
135
150
  def format_role(role)
136
151
  case role
137
152
  when :system
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'faraday'
4
+ require 'stringio'
5
+
3
6
  module RubyLLM
4
7
  module Providers
5
8
  class OpenAI
@@ -48,27 +51,24 @@ module RubyLLM
48
51
  payload = params.merge(
49
52
  model: model,
50
53
  prompt: prompt,
51
- image: build_upload_parts(with, label: 'images'),
54
+ image: build_upload_parts(with),
52
55
  n: 1
53
56
  )
54
- payload[:mask] = build_upload_part(mask, label: 'mask') if mask
57
+ payload[:mask] = build_upload_part(mask) if mask
55
58
  payload
56
59
  end
57
60
 
58
- def build_upload_parts(sources, label:)
61
+ def build_upload_parts(sources)
59
62
  Array(sources).filter_map do |source|
60
63
  next if blank_attachment?(source)
61
64
 
62
- build_upload_part(source, label:)
65
+ build_upload_part(source)
63
66
  end
64
67
  end
65
68
 
66
- def build_upload_part(source, label:)
69
+ def build_upload_part(source)
67
70
  attachment = Attachment.new(source)
68
- unless attachment.image?
69
- raise UnsupportedAttachmentError,
70
- "OpenAI image editing only supports image attachments for #{label}"
71
- end
71
+ raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
72
72
 
73
73
  Faraday::UploadIO.new(StringIO.new(attachment.content), attachment.mime_type, attachment.filename)
74
74
  end
@@ -7,7 +7,7 @@ module RubyLLM
7
7
  module Media
8
8
  module_function
9
9
 
10
- def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
10
+ def format_content(content, document_attachments: :pdf, image_attachments: true, audio_attachments: true)
11
11
  if content.is_a?(RubyLLM::Content::Raw)
12
12
  value = content.value
13
13
  return value.is_a?(Hash) ? value.to_json : value
@@ -19,23 +19,36 @@ module RubyLLM
19
19
  parts << format_text(content.text) if content.text
20
20
 
21
21
  content.attachments.each do |attachment|
22
- case attachment.type
23
- when :image
24
- parts << format_image(attachment)
25
- when :pdf
26
- parts << format_pdf(attachment)
27
- when :audio
28
- parts << format_audio(attachment)
29
- when :text
30
- parts << format_text_file(attachment)
31
- else
32
- raise UnsupportedAttachmentError, attachment.type
33
- end
22
+ parts << format_attachment(
23
+ attachment,
24
+ document_attachments:,
25
+ image_attachments:,
26
+ audio_attachments:
27
+ )
34
28
  end
35
29
 
36
30
  parts
37
31
  end
38
32
 
33
+ def format_attachment(attachment, document_attachments:, image_attachments:, audio_attachments:)
34
+ case attachment.type
35
+ when :image
36
+ raise UnsupportedAttachmentError, attachment.mime_type unless image_attachments
37
+
38
+ format_image(attachment)
39
+ when :audio
40
+ raise UnsupportedAttachmentError, attachment.mime_type unless audio_attachments
41
+
42
+ format_audio(attachment)
43
+ when :pdf, :document
44
+ format_document_attachment(attachment, document_attachments)
45
+ when :text
46
+ format_text_file(attachment)
47
+ else
48
+ raise UnsupportedAttachmentError, attachment.mime_type
49
+ end
50
+ end
51
+
39
52
  def format_image(image)
40
53
  {
41
54
  type: 'image_url',
@@ -45,16 +58,20 @@ module RubyLLM
45
58
  }
46
59
  end
47
60
 
48
- def format_pdf(pdf)
61
+ def format_document(document)
49
62
  {
50
63
  type: 'file',
51
64
  file: {
52
- filename: pdf.filename,
53
- file_data: pdf.for_llm
65
+ filename: document.filename,
66
+ file_data: document.for_llm
54
67
  }
55
68
  }
56
69
  end
57
70
 
71
+ def format_pdf(pdf)
72
+ format_document(pdf)
73
+ end
74
+
58
75
  def format_text_file(text_file)
59
76
  {
60
77
  type: 'text',
@@ -78,6 +95,13 @@ module RubyLLM
78
95
  text: text
79
96
  }
80
97
  end
98
+
99
+ def format_document_attachment(attachment, strategy)
100
+ return format_document(attachment) if strategy == :all
101
+ return format_document(attachment) if strategy == :pdf && attachment.pdf?
102
+
103
+ raise UnsupportedAttachmentError, attachment.mime_type
104
+ end
81
105
  end
82
106
  end
83
107
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module RubyLLM
4
6
  module Providers
5
7
  class OpenAI
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module RubyLLM
4
6
  module Providers
5
7
  class OpenAI
@@ -60,6 +60,7 @@ module RubyLLM
60
60
  language: data['language'],
61
61
  duration: data['duration'],
62
62
  segments: data['segments'],
63
+ words: data['words'],
63
64
  input_tokens: usage['input_tokens'] || usage['prompt_tokens'],
64
65
  output_tokens: usage['output_tokens'] || usage['completion_tokens']
65
66
  )
@@ -52,7 +52,7 @@ module RubyLLM
52
52
 
53
53
  def parse_completion_response(response)
54
54
  data = response.body
55
- return if data.empty?
55
+ return if data.nil? || data.empty?
56
56
 
57
57
  raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message')
58
58
 
@@ -108,13 +108,17 @@ module RubyLLM
108
108
  messages.map do |msg|
109
109
  {
110
110
  role: format_role(msg.role),
111
- content: OpenAI::Media.format_content(msg.content),
111
+ content: format_content(msg.content),
112
112
  tool_calls: OpenAI::Tools.format_tool_calls(msg.tool_calls),
113
113
  tool_call_id: msg.tool_call_id
114
114
  }.compact.merge(format_thinking(msg))
115
115
  end
116
116
  end
117
117
 
118
+ def format_content(content)
119
+ OpenAI::Media.format_content(content)
120
+ end
121
+
118
122
  def format_role(role)
119
123
  case role
120
124
  when :system
@@ -10,6 +10,17 @@ module RubyLLM
10
10
  def format_role(role)
11
11
  role.to_s
12
12
  end
13
+
14
+ def format_messages(messages)
15
+ messages.map do |msg|
16
+ {
17
+ role: format_role(msg.role),
18
+ content: Perplexity::Media.format_content(msg.content),
19
+ tool_calls: OpenAI::Tools.format_tool_calls(msg.tool_calls),
20
+ tool_call_id: msg.tool_call_id
21
+ }.compact.merge(OpenAI::Chat.format_thinking(msg))
22
+ end
23
+ end
13
24
  end
14
25
  end
15
26
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Perplexity
6
+ # Handles Perplexity Sonar media content.
7
+ module Media
8
+ module_function
9
+
10
+ SUPPORTED_DOCUMENT_EXTENSIONS = %w[pdf doc docx txt rtf].freeze
11
+
12
+ def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
13
+ if content.is_a?(RubyLLM::Content::Raw)
14
+ value = content.value
15
+ return value.is_a?(Hash) ? value.to_json : value
16
+ end
17
+ return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
18
+ return content unless content.is_a?(Content)
19
+
20
+ parts = []
21
+ parts << OpenAI::Media.format_text(content.text) if content.text
22
+
23
+ content.attachments.each do |attachment|
24
+ case attachment.type
25
+ when :image
26
+ parts << OpenAI::Media.format_image(attachment)
27
+ when :pdf, :document
28
+ parts << format_document(attachment)
29
+ when :text
30
+ parts << format_text_attachment(attachment)
31
+ else
32
+ raise UnsupportedAttachmentError, attachment.mime_type
33
+ end
34
+ end
35
+
36
+ parts
37
+ end
38
+
39
+ def format_document(attachment)
40
+ raise UnsupportedAttachmentError, attachment.mime_type unless supported_file?(attachment)
41
+
42
+ {
43
+ type: 'file_url',
44
+ file_url: {
45
+ url: attachment.url? ? attachment.source.to_s : attachment.encoded
46
+ }
47
+ }
48
+ end
49
+
50
+ def format_text_attachment(attachment)
51
+ OpenAI::Media.format_text_file(attachment)
52
+ end
53
+
54
+ def supported_file?(attachment)
55
+ return true if attachment.pdf?
56
+
57
+ SUPPORTED_DOCUMENT_EXTENSIONS.include?(attachment.extension)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -8,7 +8,7 @@ module RubyLLM
8
8
  include Perplexity::Models
9
9
 
10
10
  def api_base
11
- 'https://api.perplexity.ai'
11
+ @config.perplexity_api_base || 'https://api.perplexity.ai'
12
12
  end
13
13
 
14
14
  def headers
@@ -24,7 +24,7 @@ module RubyLLM
24
24
  end
25
25
 
26
26
  def configuration_options
27
- %i[perplexity_api_key]
27
+ %i[perplexity_api_key perplexity_api_base]
28
28
  end
29
29
 
30
30
  def configuration_requirements
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'stringio'
4
+
3
5
  module RubyLLM
4
6
  module Providers
5
7
  # Google Vertex AI implementation
@@ -21,6 +23,8 @@ module RubyLLM
21
23
  end
22
24
 
23
25
  def api_base
26
+ return @config.vertexai_api_base if @config.vertexai_api_base
27
+
24
28
  if @config.vertexai_location.to_s == 'global'
25
29
  'https://aiplatform.googleapis.com/v1beta1'
26
30
  else
@@ -41,7 +45,7 @@ module RubyLLM
41
45
 
42
46
  class << self
43
47
  def configuration_options
44
- %i[vertexai_project_id vertexai_location vertexai_service_account_key]
48
+ %i[vertexai_project_id vertexai_location vertexai_service_account_key vertexai_api_base]
45
49
  end
46
50
 
47
51
  def configuration_requirements
@@ -9,6 +9,15 @@ module RubyLLM
9
9
  def format_role(role)
10
10
  role.to_s
11
11
  end
12
+
13
+ def format_content(content)
14
+ OpenAI::Media.format_content(
15
+ content,
16
+ document_attachments: :none,
17
+ image_attachments: true,
18
+ audio_attachments: false
19
+ )
20
+ end
12
21
  end
13
22
  end
14
23
  end
@@ -7,23 +7,6 @@ module RubyLLM
7
7
  module Models
8
8
  module_function
9
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
10
  def parse_list_models_response(response, slug, _capabilities)
28
11
  Array(response.body['data']).map do |model_data|
29
12
  model_id = model_data['id']
@@ -48,27 +31,32 @@ module RubyLLM
48
31
  end
49
32
 
50
33
  def modalities_for(model_id)
51
- if IMAGE_MODELS.include?(model_id)
52
- { input: ['text'], output: ['image'] }
34
+ if image_model?(model_id)
35
+ { input: %w[text image], output: ['image'] }
36
+ elsif video_model?(model_id)
37
+ { input: %w[text image video], output: ['video'] }
53
38
  else
54
- input = ['text']
55
- input << 'image' if VISION_MODELS.include?(model_id)
56
- { input: input, output: ['text'] }
39
+ { input: ['text'], output: ['text'] }
57
40
  end
58
41
  end
59
42
 
60
43
  def capabilities_for(model_id)
61
- return [] if IMAGE_MODELS.include?(model_id)
44
+ return ['vision'] if image_model?(model_id) || video_model?(model_id)
62
45
 
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
46
+ ['streaming']
67
47
  end
68
48
 
69
49
  def format_display_name(model_id)
70
50
  model_id.tr('-', ' ').split.map(&:capitalize).join(' ')
71
51
  end
52
+
53
+ def image_model?(model_id)
54
+ model_id.match?(/\Agrok-(?:2-)?imagine-image/) || model_id == 'grok-2-image-1212'
55
+ end
56
+
57
+ def video_model?(model_id)
58
+ model_id.match?(/\Agrok-(?:2-)?imagine-video/)
59
+ end
72
60
  end
73
61
  end
74
62
  end