ruby_llm 1.6.4 → 1.7.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -3
  3. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +115 -0
  4. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
  5. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
  6. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  7. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  8. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  10. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +10 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +30 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
  19. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +2 -2
  20. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -4
  21. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +8 -7
  22. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
  23. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +6 -5
  24. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +10 -4
  25. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -3
  26. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  27. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +2 -2
  28. data/lib/generators/ruby_llm/install_generator.rb +129 -33
  29. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +137 -0
  30. data/lib/generators/ruby_llm/upgrade_to_v1_7_generator.rb +160 -0
  31. data/lib/ruby_llm/active_record/acts_as.rb +111 -327
  32. data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
  33. data/lib/ruby_llm/active_record/chat_methods.rb +336 -0
  34. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  35. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  36. data/lib/ruby_llm/aliases.json +54 -13
  37. data/lib/ruby_llm/attachment.rb +20 -0
  38. data/lib/ruby_llm/chat.rb +5 -5
  39. data/lib/ruby_llm/configuration.rb +9 -0
  40. data/lib/ruby_llm/connection.rb +4 -4
  41. data/lib/ruby_llm/model/info.rb +12 -0
  42. data/lib/ruby_llm/models.json +3579 -2029
  43. data/lib/ruby_llm/models.rb +51 -22
  44. data/lib/ruby_llm/provider.rb +3 -3
  45. data/lib/ruby_llm/providers/anthropic/chat.rb +2 -2
  46. data/lib/ruby_llm/providers/anthropic/media.rb +1 -1
  47. data/lib/ruby_llm/providers/bedrock/chat.rb +2 -2
  48. data/lib/ruby_llm/providers/bedrock/models.rb +19 -1
  49. data/lib/ruby_llm/providers/gemini/chat.rb +1 -1
  50. data/lib/ruby_llm/providers/gemini/media.rb +1 -1
  51. data/lib/ruby_llm/providers/gpustack/chat.rb +11 -0
  52. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  53. data/lib/ruby_llm/providers/gpustack/models.rb +44 -8
  54. data/lib/ruby_llm/providers/gpustack.rb +1 -0
  55. data/lib/ruby_llm/providers/ollama/media.rb +2 -6
  56. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  57. data/lib/ruby_llm/providers/ollama.rb +1 -0
  58. data/lib/ruby_llm/providers/openai/chat.rb +1 -1
  59. data/lib/ruby_llm/providers/openai/media.rb +4 -4
  60. data/lib/ruby_llm/providers/openai/tools.rb +11 -6
  61. data/lib/ruby_llm/providers/openai.rb +2 -2
  62. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  63. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  64. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  65. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  66. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  67. data/lib/ruby_llm/railtie.rb +20 -3
  68. data/lib/ruby_llm/streaming.rb +1 -1
  69. data/lib/ruby_llm/utils.rb +5 -9
  70. data/lib/ruby_llm/version.rb +1 -1
  71. data/lib/ruby_llm.rb +4 -3
  72. data/lib/tasks/models.rake +39 -28
  73. data/lib/tasks/ruby_llm.rake +15 -0
  74. data/lib/tasks/vcr.rake +2 -2
  75. metadata +36 -2
  76. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +0 -108
@@ -22,16 +22,20 @@ module RubyLLM
22
22
  File.expand_path('models_schema.json', __dir__)
23
23
  end
24
24
 
25
- def refresh!
26
- provider_models = fetch_from_providers
25
+ def refresh!(remote_only: false)
26
+ provider_models = fetch_from_providers(remote_only: remote_only)
27
27
  parsera_models = fetch_from_parsera
28
28
  merged_models = merge_models(provider_models, parsera_models)
29
29
  @instance = new(merged_models)
30
30
  end
31
31
 
32
- def fetch_from_providers
32
+ def fetch_from_providers(remote_only: true)
33
33
  config = RubyLLM.config
34
- configured_classes = Provider.configured_remote_providers(config)
34
+ configured_classes = if remote_only
35
+ Provider.configured_remote_providers(config)
36
+ else
37
+ Provider.configured_providers(config)
38
+ end
35
39
  configured = configured_classes.map { |klass| klass.new(config) }
36
40
 
37
41
  RubyLLM.logger.info "Fetching models from providers: #{configured.map(&:name).join(', ')}"
@@ -54,14 +58,15 @@ module RubyLLM
54
58
  provider_class ||= raise(Error, "Unknown provider: #{provider.to_sym}")
55
59
  provider_instance = provider_class.new(config)
56
60
 
57
- model = Model::Info.new(
58
- id: model_id,
59
- name: model_id.tr('-', ' ').capitalize,
60
- provider: provider_instance.slug,
61
- capabilities: %w[function_calling streaming],
62
- modalities: { input: %w[text image], output: %w[text] },
63
- metadata: { warning: 'Assuming model exists, capabilities may not be accurate' }
64
- )
61
+ model = if provider_instance.local?
62
+ begin
63
+ Models.find(model_id, provider)
64
+ rescue ModelNotFoundError
65
+ nil
66
+ end
67
+ end
68
+
69
+ model ||= Model::Info.default(model_id, provider_instance.slug)
65
70
  else
66
71
  model = Models.find model_id, provider
67
72
  provider_class = Provider.providers[model.provider.to_sym] || raise(Error,
@@ -102,20 +107,36 @@ module RubyLLM
102
107
  all_keys = parsera_by_key.keys | provider_by_key.keys
103
108
 
104
109
  models = all_keys.map do |key|
105
- if (parsera_model = parsera_by_key[key])
106
- if (provider_model = provider_by_key[key])
107
- add_provider_metadata(parsera_model, provider_model)
108
- else
109
- parsera_model
110
- end
110
+ parsera_model = find_parsera_model(key, parsera_by_key)
111
+ provider_model = provider_by_key[key]
112
+
113
+ if parsera_model && provider_model
114
+ add_provider_metadata(parsera_model, provider_model)
115
+ elsif parsera_model
116
+ parsera_model
111
117
  else
112
- provider_by_key[key]
118
+ provider_model
113
119
  end
114
120
  end
115
121
 
116
122
  models.sort_by { |m| [m.provider, m.id] }
117
123
  end
118
124
 
125
+ def find_parsera_model(key, parsera_by_key)
126
+ # Direct match
127
+ return parsera_by_key[key] if parsera_by_key[key]
128
+
129
+ # VertexAI uses same models as Gemini
130
+ provider, model_id = key.split(':', 2)
131
+ return unless provider == 'vertexai'
132
+
133
+ gemini_model = parsera_by_key["gemini:#{model_id}"]
134
+ return unless gemini_model
135
+
136
+ # Return Gemini's Parsera data but with VertexAI as provider
137
+ Model::Info.new(gemini_model.to_h.merge(provider: 'vertexai'))
138
+ end
139
+
119
140
  def index_by_key(models)
120
141
  models.each_with_object({}) do |model, hash|
121
142
  hash["#{model.provider}:#{model.id}"] = model
@@ -134,13 +155,21 @@ module RubyLLM
134
155
  end
135
156
 
136
157
  def load_models
158
+ read_from_json
159
+ end
160
+
161
+ def load_from_json!
162
+ @models = read_from_json
163
+ end
164
+
165
+ def read_from_json
137
166
  data = File.exist?(self.class.models_file) ? File.read(self.class.models_file) : '[]'
138
167
  JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
139
168
  rescue JSON::ParserError
140
169
  []
141
170
  end
142
171
 
143
- def save_models
172
+ def save_to_json
144
173
  File.write(self.class.models_file, JSON.pretty_generate(all.map(&:to_h)))
145
174
  end
146
175
 
@@ -184,8 +213,8 @@ module RubyLLM
184
213
  self.class.new(all.select { |m| m.provider == provider.to_s })
185
214
  end
186
215
 
187
- def refresh!
188
- self.class.refresh!
216
+ def refresh!(remote_only: false)
217
+ self.class.refresh!(remote_only: remote_only)
189
218
  end
190
219
 
191
220
  private
@@ -41,7 +41,6 @@ module RubyLLM
41
41
  normalized_temperature = maybe_normalize_temperature(temperature, model)
42
42
 
43
43
  payload = Utils.deep_merge(
44
- params,
45
44
  render_payload(
46
45
  messages,
47
46
  tools: tools,
@@ -49,7 +48,8 @@ module RubyLLM
49
48
  model: model,
50
49
  stream: block_given?,
51
50
  schema: schema
52
- )
51
+ ),
52
+ params
53
53
  )
54
54
 
55
55
  if block_given?
@@ -201,7 +201,7 @@ module RubyLLM
201
201
  raise ConfigurationError, "Missing configuration for #{name}: #{missing.join(', ')}"
202
202
  end
203
203
 
204
- def maybe_normalize_temperature(temperature, _model_id)
204
+ def maybe_normalize_temperature(temperature, _model)
205
205
  temperature
206
206
  end
207
207
 
@@ -37,10 +37,10 @@ module RubyLLM
37
37
 
38
38
  def build_base_payload(chat_messages, model, stream)
39
39
  {
40
- model: model,
40
+ model: model.id,
41
41
  messages: chat_messages.map { |msg| format_message(msg) },
42
42
  stream: stream,
43
- max_tokens: RubyLLM.models.find(model)&.max_tokens || 4096
43
+ max_tokens: model.max_tokens || 4096
44
44
  }
45
45
  end
46
46
 
@@ -82,7 +82,7 @@ module RubyLLM
82
82
  def format_text_file(text_file)
83
83
  {
84
84
  type: 'text',
85
- text: Utils.format_text_file_for_llm(text_file)
85
+ text: text_file.for_llm
86
86
  }
87
87
  end
88
88
  end
@@ -40,7 +40,7 @@ module RubyLLM
40
40
  end
41
41
 
42
42
  def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Lint/UnusedMethodArgument,Metrics/ParameterLists
43
- @model_id = model
43
+ @model_id = model.id
44
44
 
45
45
  system_messages, chat_messages = Anthropic::Chat.separate_messages(messages)
46
46
  system_content = Anthropic::Chat.build_system_content(system_messages)
@@ -54,7 +54,7 @@ module RubyLLM
54
54
  {
55
55
  anthropic_version: 'bedrock-2023-05-31',
56
56
  messages: chat_messages.map { |msg| format_message(msg) },
57
- max_tokens: RubyLLM.models.find(model)&.max_tokens || 4096
57
+ max_tokens: model.max_tokens || 4096
58
58
  }
59
59
  end
60
60
  end
@@ -72,7 +72,25 @@ module RubyLLM
72
72
  return model_id unless model_data['inferenceTypesSupported']&.include?('INFERENCE_PROFILE')
73
73
  return model_id if model_data['inferenceTypesSupported']&.include?('ON_DEMAND')
74
74
 
75
- "us.#{model_id}"
75
+ desired_region_prefix = inference_profile_region_prefix
76
+
77
+ # Return unchanged if model already has the correct region prefix
78
+ return model_id if model_id.start_with?("#{desired_region_prefix}.")
79
+
80
+ # Remove any existing region prefix (e.g., "us.", "eu.", "ap.")
81
+ clean_model_id = model_id.sub(/^[a-z]{2}\./, '')
82
+
83
+ # Apply the desired region prefix
84
+ "#{desired_region_prefix}.#{clean_model_id}"
85
+ end
86
+
87
+ def inference_profile_region_prefix
88
+ # Extract region prefix from bedrock_region (e.g., "eu-west-3" -> "eu")
89
+ region = @config.bedrock_region.to_s
90
+ return 'us' if region.empty? # Default fallback
91
+
92
+ # Take first two characters as the region prefix
93
+ region[0, 2]
76
94
  end
77
95
  end
78
96
  end
@@ -12,7 +12,7 @@ module RubyLLM
12
12
  end
13
13
 
14
14
  def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
15
- @model = model
15
+ @model = model.id
16
16
  payload = {
17
17
  contents: format_messages(messages),
18
18
  generationConfig: {}
@@ -39,7 +39,7 @@ module RubyLLM
39
39
 
40
40
  def format_text_file(text_file)
41
41
  {
42
- text: Utils.format_text_file_for_llm(text_file)
42
+ text: text_file.for_llm
43
43
  }
44
44
  end
45
45
 
@@ -7,6 +7,17 @@ module RubyLLM
7
7
  module Chat
8
8
  module_function
9
9
 
10
+ def format_messages(messages)
11
+ messages.map do |msg|
12
+ {
13
+ role: format_role(msg.role),
14
+ content: GPUStack::Media.format_content(msg.content),
15
+ tool_calls: format_tool_calls(msg.tool_calls),
16
+ tool_call_id: msg.tool_call_id
17
+ }.compact
18
+ end
19
+ end
20
+
10
21
  def format_role(role)
11
22
  role.to_s
12
23
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class GPUStack
6
+ # Handles formatting of media content (images, audio) for GPUStack APIs
7
+ module Media
8
+ extend OpenAI::Media
9
+
10
+ module_function
11
+
12
+ def format_content(content)
13
+ return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
14
+ return content unless content.is_a?(Content)
15
+
16
+ parts = []
17
+ parts << format_text(content.text) if content.text
18
+
19
+ content.attachments.each do |attachment|
20
+ case attachment.type
21
+ when :image
22
+ parts << GPUStack::Media.format_image(attachment)
23
+ when :text
24
+ parts << format_text_file(attachment)
25
+ else
26
+ raise UnsupportedAttachmentError, attachment.mime_type
27
+ end
28
+ end
29
+
30
+ parts
31
+ end
32
+
33
+ def format_image(image)
34
+ {
35
+ type: 'image_url',
36
+ image_url: {
37
+ url: image.for_llm,
38
+ detail: 'auto'
39
+ }
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -16,10 +16,10 @@ module RubyLLM
16
16
  items.map do |model|
17
17
  Model::Info.new(
18
18
  id: model['name'],
19
+ name: model['name'],
19
20
  created_at: model['created_at'] ? Time.parse(model['created_at']) : nil,
20
- display_name: "#{model['source']}/#{model['name']}",
21
21
  provider: slug,
22
- type: determine_model_type(model),
22
+ family: 'gpustack',
23
23
  metadata: {
24
24
  description: model['description'],
25
25
  source: model['source'],
@@ -30,12 +30,10 @@ module RubyLLM
30
30
  categories: model['categories']
31
31
  },
32
32
  context_window: model.dig('meta', 'n_ctx'),
33
- max_tokens: model.dig('meta', 'n_ctx'),
34
- supports_vision: model.dig('meta', 'support_vision') || false,
35
- supports_functions: model.dig('meta', 'support_tool_calls') || false,
36
- supports_json_mode: true,
37
- input_price_per_million: 0.0,
38
- output_price_per_million: 0.0
33
+ max_output_tokens: model.dig('meta', 'n_ctx'),
34
+ capabilities: build_capabilities(model),
35
+ modalities: build_modalities(model),
36
+ pricing: {}
39
37
  )
40
38
  end
41
39
  end
@@ -48,6 +46,44 @@ module RubyLLM
48
46
 
49
47
  'other'
50
48
  end
49
+
50
+ def build_capabilities(model) # rubocop:disable Metrics/PerceivedComplexity
51
+ capabilities = []
52
+
53
+ # Add streaming by default for LLM models
54
+ capabilities << 'streaming' if model['categories']&.include?('llm')
55
+
56
+ # Map GPUStack metadata to standard capabilities
57
+ capabilities << 'function_calling' if model.dig('meta', 'support_tool_calls')
58
+ capabilities << 'vision' if model.dig('meta', 'support_vision')
59
+ capabilities << 'reasoning' if model.dig('meta', 'support_reasoning')
60
+
61
+ # GPUStack models generally support structured output and json mode
62
+ capabilities << 'structured_output' if model['categories']&.include?('llm')
63
+ capabilities << 'json_mode' if model['categories']&.include?('llm')
64
+
65
+ capabilities
66
+ end
67
+
68
+ def build_modalities(model)
69
+ input_modalities = []
70
+ output_modalities = []
71
+
72
+ if model['categories']&.include?('llm')
73
+ input_modalities << 'text'
74
+ input_modalities << 'image' if model.dig('meta', 'support_vision')
75
+ input_modalities << 'audio' if model.dig('meta', 'support_audio')
76
+ output_modalities << 'text'
77
+ elsif model['categories']&.include?('embedding')
78
+ input_modalities << 'text'
79
+ output_modalities << 'embeddings'
80
+ end
81
+
82
+ {
83
+ input: input_modalities,
84
+ output: output_modalities
85
+ }
86
+ end
51
87
  end
52
88
  end
53
89
  end
@@ -6,6 +6,7 @@ module RubyLLM
6
6
  class GPUStack < OpenAI
7
7
  include GPUStack::Chat
8
8
  include GPUStack::Models
9
+ include GPUStack::Media
9
10
 
10
11
  def api_base
11
12
  @config.gpustack_api_base
@@ -3,7 +3,7 @@
3
3
  module RubyLLM
4
4
  module Providers
5
5
  class Ollama
6
- # Handles formatting of media content (images, audio) for OpenAI APIs
6
+ # Handles formatting of media content (images, audio) for Ollama APIs
7
7
  module Media
8
8
  extend OpenAI::Media
9
9
 
@@ -20,10 +20,6 @@ module RubyLLM
20
20
  case attachment.type
21
21
  when :image
22
22
  parts << Ollama::Media.format_image(attachment)
23
- when :pdf
24
- parts << format_pdf(attachment)
25
- when :audio
26
- parts << format_audio(attachment)
27
23
  when :text
28
24
  parts << format_text_file(attachment)
29
25
  else
@@ -38,7 +34,7 @@ module RubyLLM
38
34
  {
39
35
  type: 'image_url',
40
36
  image_url: {
41
- url: "data:#{image.mime_type};base64,#{image.encoded}",
37
+ url: image.for_llm,
42
38
  detail: 'auto'
43
39
  }
44
40
  }
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Ollama
6
+ # Models methods for the Ollama API integration
7
+ module Models
8
+ def models_url
9
+ 'models'
10
+ end
11
+
12
+ def parse_list_models_response(response, slug, _capabilities)
13
+ data = response.body['data'] || []
14
+ data.map do |model|
15
+ Model::Info.new(
16
+ id: model['id'],
17
+ name: model['id'],
18
+ provider: slug,
19
+ family: 'ollama',
20
+ created_at: model['created'] ? Time.at(model['created']) : nil,
21
+ modalities: {
22
+ input: %w[text image],
23
+ output: %w[text]
24
+ },
25
+ capabilities: %w[streaming function_calling structured_output vision],
26
+ pricing: {},
27
+ metadata: {
28
+ owned_by: model['owned_by']
29
+ }
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -6,6 +6,7 @@ module RubyLLM
6
6
  class Ollama < OpenAI
7
7
  include Ollama::Chat
8
8
  include Ollama::Media
9
+ include Ollama::Models
9
10
 
10
11
  def api_base
11
12
  @config.ollama_api_base
@@ -13,7 +13,7 @@ module RubyLLM
13
13
 
14
14
  def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists
15
15
  payload = {
16
- model: model,
16
+ model: model.id,
17
17
  messages: format_messages(messages),
18
18
  stream: stream
19
19
  }
@@ -36,7 +36,7 @@ module RubyLLM
36
36
  {
37
37
  type: 'image_url',
38
38
  image_url: {
39
- url: image.url? ? image.source : "data:#{image.mime_type};base64,#{image.encoded}"
39
+ url: image.url? ? image.source : image.for_llm
40
40
  }
41
41
  }
42
42
  end
@@ -46,7 +46,7 @@ module RubyLLM
46
46
  type: 'file',
47
47
  file: {
48
48
  filename: pdf.filename,
49
- file_data: "data:#{pdf.mime_type};base64,#{pdf.encoded}"
49
+ file_data: pdf.for_llm
50
50
  }
51
51
  }
52
52
  end
@@ -54,7 +54,7 @@ module RubyLLM
54
54
  def format_text_file(text_file)
55
55
  {
56
56
  type: 'text',
57
- text: Utils.format_text_file_for_llm(text_file)
57
+ text: text_file.for_llm
58
58
  }
59
59
  end
60
60
 
@@ -63,7 +63,7 @@ module RubyLLM
63
63
  type: 'input_audio',
64
64
  input_audio: {
65
65
  data: audio.encoded,
66
- format: audio.mime_type.split('/').last
66
+ format: audio.format
67
67
  }
68
68
  }
69
69
  end
@@ -44,6 +44,16 @@ module RubyLLM
44
44
  end
45
45
  end
46
46
 
47
+ def parse_tool_call_arguments(tool_call)
48
+ arguments = tool_call.dig('function', 'arguments')
49
+
50
+ if arguments.nil? || arguments.empty?
51
+ {}
52
+ else
53
+ JSON.parse(arguments)
54
+ end
55
+ end
56
+
47
57
  def parse_tool_calls(tool_calls, parse_arguments: true)
48
58
  return nil unless tool_calls&.any?
49
59
 
@@ -54,12 +64,7 @@ module RubyLLM
54
64
  id: tc['id'],
55
65
  name: tc.dig('function', 'name'),
56
66
  arguments: if parse_arguments
57
- if tc.dig('function', 'arguments').empty?
58
- {}
59
- else
60
- JSON.parse(tc.dig('function',
61
- 'arguments'))
62
- end
67
+ parse_tool_call_arguments(tc)
63
68
  else
64
69
  tc.dig('function', 'arguments')
65
70
  end
@@ -24,8 +24,8 @@ module RubyLLM
24
24
  }.compact
25
25
  end
26
26
 
27
- def maybe_normalize_temperature(temperature, model_id)
28
- OpenAI::Capabilities.normalize_temperature(temperature, model_id)
27
+ def maybe_normalize_temperature(temperature, model)
28
+ OpenAI::Capabilities.normalize_temperature(temperature, model.id)
29
29
  end
30
30
 
31
31
  class << self
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class VertexAI
6
+ # Chat methods for the Vertex AI implementation
7
+ module Chat
8
+ def completion_url
9
+ "projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{@model}:generateContent" # rubocop:disable Layout/LineLength
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class VertexAI
6
+ # Embeddings methods for the Vertex AI implementation
7
+ module Embeddings
8
+ module_function
9
+
10
+ def embedding_url(model:)
11
+ "projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{model}:predict" # rubocop:disable Layout/LineLength
12
+ end
13
+
14
+ def render_embedding_payload(text, model:, dimensions:) # rubocop:disable Lint/UnusedMethodArgument
15
+ {
16
+ instances: [text].flatten.map { |t| { content: t.to_s } }
17
+ }.tap do |payload|
18
+ payload[:parameters] = { outputDimensionality: dimensions } if dimensions
19
+ end
20
+ end
21
+
22
+ def parse_embedding_response(response, model:, text:)
23
+ predictions = response.body['predictions']
24
+ vectors = predictions&.map { |p| p.dig('embeddings', 'values') }
25
+ vectors = vectors.first if vectors&.length == 1 && !text.is_a?(Array)
26
+
27
+ Embedding.new(vectors:, model:, input_tokens: 0)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end