ruby_llm_community 0.0.6 → 1.0.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +34 -0
  4. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +5 -0
  5. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +6 -0
  6. data/lib/generators/ruby_llm/install_generator.rb +27 -2
  7. data/lib/ruby_llm/active_record/acts_as.rb +163 -24
  8. data/lib/ruby_llm/aliases.json +58 -5
  9. data/lib/ruby_llm/aliases.rb +7 -25
  10. data/lib/ruby_llm/chat.rb +10 -17
  11. data/lib/ruby_llm/configuration.rb +5 -12
  12. data/lib/ruby_llm/connection.rb +4 -4
  13. data/lib/ruby_llm/connection_multipart.rb +19 -0
  14. data/lib/ruby_llm/content.rb +5 -2
  15. data/lib/ruby_llm/embedding.rb +1 -2
  16. data/lib/ruby_llm/error.rb +0 -8
  17. data/lib/ruby_llm/image.rb +23 -8
  18. data/lib/ruby_llm/image_attachment.rb +21 -0
  19. data/lib/ruby_llm/message.rb +6 -6
  20. data/lib/ruby_llm/model/info.rb +12 -10
  21. data/lib/ruby_llm/model/pricing.rb +0 -3
  22. data/lib/ruby_llm/model/pricing_category.rb +0 -2
  23. data/lib/ruby_llm/model/pricing_tier.rb +0 -1
  24. data/lib/ruby_llm/models.json +2147 -470
  25. data/lib/ruby_llm/models.rb +65 -34
  26. data/lib/ruby_llm/provider.rb +8 -8
  27. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -46
  28. data/lib/ruby_llm/providers/anthropic/chat.rb +2 -2
  29. data/lib/ruby_llm/providers/anthropic/media.rb +0 -1
  30. data/lib/ruby_llm/providers/anthropic/tools.rb +1 -2
  31. data/lib/ruby_llm/providers/anthropic.rb +1 -2
  32. data/lib/ruby_llm/providers/bedrock/chat.rb +2 -4
  33. data/lib/ruby_llm/providers/bedrock/media.rb +0 -1
  34. data/lib/ruby_llm/providers/bedrock/models.rb +0 -2
  35. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -12
  36. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -7
  37. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -12
  38. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -12
  39. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -13
  40. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -18
  41. data/lib/ruby_llm/providers/bedrock.rb +1 -2
  42. data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -2
  43. data/lib/ruby_llm/providers/deepseek/chat.rb +0 -1
  44. data/lib/ruby_llm/providers/gemini/capabilities.rb +28 -100
  45. data/lib/ruby_llm/providers/gemini/chat.rb +57 -29
  46. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -2
  47. data/lib/ruby_llm/providers/gemini/images.rb +1 -2
  48. data/lib/ruby_llm/providers/gemini/media.rb +0 -1
  49. data/lib/ruby_llm/providers/gemini/models.rb +1 -2
  50. data/lib/ruby_llm/providers/gemini/streaming.rb +15 -1
  51. data/lib/ruby_llm/providers/gemini/tools.rb +0 -5
  52. data/lib/ruby_llm/providers/gpustack/chat.rb +11 -1
  53. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  54. data/lib/ruby_llm/providers/gpustack/models.rb +44 -9
  55. data/lib/ruby_llm/providers/gpustack.rb +1 -0
  56. data/lib/ruby_llm/providers/mistral/capabilities.rb +2 -10
  57. data/lib/ruby_llm/providers/mistral/chat.rb +0 -2
  58. data/lib/ruby_llm/providers/mistral/embeddings.rb +0 -3
  59. data/lib/ruby_llm/providers/mistral/models.rb +0 -1
  60. data/lib/ruby_llm/providers/ollama/chat.rb +0 -1
  61. data/lib/ruby_llm/providers/ollama/media.rb +1 -6
  62. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  63. data/lib/ruby_llm/providers/ollama.rb +1 -0
  64. data/lib/ruby_llm/providers/openai/capabilities.rb +3 -16
  65. data/lib/ruby_llm/providers/openai/chat.rb +1 -3
  66. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -3
  67. data/lib/ruby_llm/providers/openai/images.rb +73 -3
  68. data/lib/ruby_llm/providers/openai/media.rb +0 -1
  69. data/lib/ruby_llm/providers/openai/response.rb +120 -29
  70. data/lib/ruby_llm/providers/openai/response_media.rb +2 -2
  71. data/lib/ruby_llm/providers/openai/streaming.rb +107 -47
  72. data/lib/ruby_llm/providers/openai/tools.rb +1 -1
  73. data/lib/ruby_llm/providers/openai.rb +1 -3
  74. data/lib/ruby_llm/providers/openai_base.rb +2 -2
  75. data/lib/ruby_llm/providers/openrouter/models.rb +1 -16
  76. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -1
  77. data/lib/ruby_llm/providers/perplexity/chat.rb +0 -1
  78. data/lib/ruby_llm/providers/perplexity.rb +1 -5
  79. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  80. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  81. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  82. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  83. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  84. data/lib/ruby_llm/railtie.rb +0 -1
  85. data/lib/ruby_llm/stream_accumulator.rb +72 -10
  86. data/lib/ruby_llm/streaming.rb +16 -25
  87. data/lib/ruby_llm/tool.rb +2 -19
  88. data/lib/ruby_llm/tool_call.rb +0 -9
  89. data/lib/ruby_llm/version.rb +1 -1
  90. data/lib/ruby_llm_community.rb +5 -3
  91. data/lib/tasks/models.rake +525 -0
  92. data/lib/tasks/release.rake +37 -2
  93. data/lib/tasks/vcr.rake +0 -7
  94. metadata +13 -4
  95. data/lib/tasks/aliases.rake +0 -235
  96. data/lib/tasks/models_docs.rake +0 -224
  97. data/lib/tasks/models_update.rake +0 -108
@@ -13,7 +13,7 @@ module RubyLLM
13
13
 
14
14
  def render_response_payload(messages, tools:, temperature:, model:, cache_prompts:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
15
15
  payload = {
16
- model: model,
16
+ model: model.id,
17
17
  input: format_input(messages),
18
18
  stream: stream
19
19
  }
@@ -40,39 +40,72 @@ module RubyLLM
40
40
  payload
41
41
  end
42
42
 
43
- def format_input(messages) # rubocop:disable Metrics/PerceivedComplexity
43
+ def format_input(messages)
44
44
  all_tool_calls = messages.flat_map do |m|
45
45
  m.tool_calls&.values || []
46
46
  end
47
- messages.flat_map do |msg|
48
- if msg.tool_call?
49
- msg.tool_calls.map do |_, tc|
50
- {
51
- type: 'function_call',
52
- call_id: tc.id,
53
- name: tc.name,
54
- arguments: JSON.generate(tc.arguments),
55
- status: 'completed'
56
- }
57
- end
58
- elsif msg.role == :tool
59
- {
60
- type: 'function_call_output',
61
- call_id: all_tool_calls.detect { |tc| tc.id == msg.tool_call_id }&.id,
62
- output: msg.content,
63
- status: 'completed'
64
- }
65
- else
66
- {
67
- type: 'message',
68
- role: format_role(msg.role),
69
- content: ResponseMedia.format_content(msg.content),
70
- status: 'completed'
71
- }.compact
72
- end
47
+ messages.flat_map { |msg| format_message_input(msg, all_tool_calls) }.flatten
48
+ end
49
+
50
+ def format_message_input(msg, all_tool_calls)
51
+ if msg.tool_call?
52
+ format_tool_call_message(msg)
53
+ elsif msg.role == :tool
54
+ format_tool_response_message(msg, all_tool_calls)
55
+ elsif assistant_message_with_image_attachment?(msg)
56
+ format_image_generation_message(msg)
57
+ else
58
+ format_regular_message(msg)
73
59
  end
74
60
  end
75
61
 
62
+ def format_tool_call_message(msg)
63
+ msg.tool_calls.map do |_, tc|
64
+ {
65
+ type: 'function_call',
66
+ call_id: tc.id,
67
+ name: tc.name,
68
+ arguments: JSON.generate(tc.arguments),
69
+ status: 'completed'
70
+ }
71
+ end
72
+ end
73
+
74
+ def format_tool_response_message(msg, all_tool_calls)
75
+ {
76
+ type: 'function_call_output',
77
+ call_id: all_tool_calls.detect { |tc| tc.id == msg.tool_call_id }&.id,
78
+ output: msg.content,
79
+ status: 'completed'
80
+ }
81
+ end
82
+
83
+ def format_image_generation_message(msg)
84
+ items = []
85
+ image_attachment = msg.content.attachments.first
86
+ if image_attachment.reasoning_id
87
+ items << {
88
+ type: 'reasoning',
89
+ id: image_attachment.reasoning_id,
90
+ summary: []
91
+ }
92
+ end
93
+ items << {
94
+ type: 'image_generation_call',
95
+ id: image_attachment.id
96
+ }
97
+ items
98
+ end
99
+
100
+ def format_regular_message(msg)
101
+ {
102
+ type: 'message',
103
+ role: format_role(msg.role),
104
+ content: ResponseMedia.format_content(msg.content),
105
+ status: 'completed'
106
+ }.compact
107
+ end
108
+
76
109
  def format_role(role)
77
110
  case role
78
111
  when :system
@@ -93,16 +126,62 @@ module RubyLLM
93
126
 
94
127
  Message.new(
95
128
  role: :assistant,
96
- content: all_output_text(outputs),
129
+ content: all_output_content(outputs),
97
130
  tool_calls: parse_response_tool_calls(outputs),
98
131
  input_tokens: data['usage']['input_tokens'],
99
132
  output_tokens: data['usage']['output_tokens'],
100
133
  cached_tokens: data.dig('usage', 'input_tokens_details', 'cached_tokens'),
101
134
  model_id: data['model'],
135
+ reasoning_id: extract_reasoning_id(outputs),
102
136
  raw: response
103
137
  )
104
138
  end
105
139
 
140
+ def all_output_content(outputs)
141
+ @current_outputs = outputs
142
+ text_content = extract_text_content(outputs)
143
+ image_outputs = outputs.select { |o| o['type'] == 'image_generation_call' }
144
+
145
+ return text_content unless image_outputs.any?
146
+
147
+ build_content_with_images(text_content, image_outputs)
148
+ end
149
+
150
+ private
151
+
152
+ def extract_text_content(outputs)
153
+ outputs.select { |o| o['type'] == 'message' }.flat_map do |o|
154
+ o['content'].filter_map do |c|
155
+ c['type'] == 'output_text' && c['text']
156
+ end
157
+ end.join("\n")
158
+ end
159
+
160
+ def build_content_with_images(text_content, image_outputs)
161
+ content = RubyLLM::Content.new(text_content)
162
+ reasoning_id = extract_reasoning_id(@current_outputs)
163
+ image_outputs.each do |output|
164
+ attach_image_to_content(content, output, reasoning_id)
165
+ end
166
+ content
167
+ end
168
+
169
+ def attach_image_to_content(content, output, reasoning_id)
170
+ image_data = output['result']
171
+ output_format = output['output_format'] || 'png'
172
+ mime_type = "image/#{output_format}"
173
+
174
+ content.attach(
175
+ RubyLLM::ImageAttachment.new(
176
+ data: image_data,
177
+ mime_type: mime_type,
178
+ model_id: nil,
179
+ id: output['id'],
180
+ reasoning_id: reasoning_id
181
+ )
182
+ )
183
+ end
184
+
106
185
  def all_output_text(outputs)
107
186
  outputs.select { |o| o['type'] == 'message' }.flat_map do |o|
108
187
  o['content'].filter_map do |c|
@@ -110,6 +189,18 @@ module RubyLLM
110
189
  end
111
190
  end.join("\n")
112
191
  end
192
+
193
+ def assistant_message_with_image_attachment?(msg)
194
+ msg.role == :assistant &&
195
+ msg.content.is_a?(RubyLLM::Content) &&
196
+ msg.content.attachments.any? &&
197
+ msg.content.attachments.first.is_a?(RubyLLM::ImageAttachment)
198
+ end
199
+
200
+ def extract_reasoning_id(outputs)
201
+ reasoning_item = outputs.find { |o| o['type'] == 'reasoning' }
202
+ reasoning_item&.dig('id')
203
+ end
113
204
  end
114
205
  end
115
206
  end
@@ -7,12 +7,12 @@ module RubyLLM
7
7
  module ResponseMedia
8
8
  module_function
9
9
 
10
- def format_content(content)
10
+ def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
11
11
  return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
12
12
  return content unless content.is_a?(Content)
13
13
 
14
14
  parts = []
15
- parts << format_text(content.text) if content.text
15
+ parts << format_text(content.text) if content.text && !content.text.empty?
16
16
 
17
17
  content.attachments.each do |attachment|
18
18
  case attachment.type
@@ -26,60 +26,65 @@ module RubyLLM
26
26
 
27
27
  def build_responses_chunk(data)
28
28
  case data['type']
29
- when 'response.text.delta'
30
- # Text content delta - deprecated format
31
- Chunk.new(
32
- role: :assistant,
33
- model_id: data.dig('response', 'model'),
34
- content: data['delta'],
35
- tool_calls: nil,
36
- input_tokens: nil,
37
- output_tokens: nil
38
- )
39
29
  when 'response.output_text.delta'
40
- # Text content delta - new format
41
- Chunk.new(
42
- role: :assistant,
43
- model_id: nil, # Model is in the completion event
44
- content: data['delta'],
45
- tool_calls: nil,
46
- input_tokens: nil,
47
- output_tokens: nil
48
- )
30
+ build_text_delta_chunk(data)
49
31
  when 'response.function_call_arguments.delta'
50
- # Tool call arguments delta - handled by accumulator
51
- # We need to track these deltas to build up the complete tool call
52
32
  build_tool_call_delta_chunk(data)
33
+ when 'response.image_generation_call.partial_image'
34
+ build_partial_image_chunk(data)
53
35
  when 'response.output_item.added'
54
- # New tool call or message starting
55
- if data.dig('item', 'type') == 'function_call'
56
- build_tool_call_start_chunk(data)
57
- else
58
- build_empty_chunk(data)
59
- end
36
+ handle_output_item_added(data)
60
37
  when 'response.output_item.done'
61
- # Tool call or message completed
62
- if data.dig('item', 'type') == 'function_call'
63
- build_tool_call_complete_chunk(data)
64
- else
65
- build_empty_chunk(data)
66
- end
38
+ handle_output_item_done(data)
67
39
  when 'response.completed'
68
- # Final response with usage stats
69
- Chunk.new(
70
- role: :assistant,
71
- model_id: data.dig('response', 'model'),
72
- content: nil,
73
- tool_calls: nil,
74
- input_tokens: data.dig('response', 'usage', 'input_tokens'),
75
- output_tokens: data.dig('response', 'usage', 'output_tokens')
76
- )
40
+ build_completion_chunk(data)
41
+ else
42
+ build_empty_chunk(data)
43
+ end
44
+ end
45
+
46
+ def build_text_delta_chunk(data)
47
+ Chunk.new(
48
+ role: :assistant,
49
+ model_id: nil,
50
+ content: data['delta'],
51
+ tool_calls: nil,
52
+ input_tokens: nil,
53
+ output_tokens: nil
54
+ )
55
+ end
56
+
57
+ def handle_output_item_added(data)
58
+ if data.dig('item', 'type') == 'function_call'
59
+ build_tool_call_start_chunk(data)
60
+ elsif data.dig('item', 'type') == 'reasoning'
61
+ build_reasoning_chunk(data)
77
62
  else
78
- # Other event types (response.created, response.in_progress, etc.)
79
63
  build_empty_chunk(data)
80
64
  end
81
65
  end
82
66
 
67
+ def handle_output_item_done(data)
68
+ if data.dig('item', 'type') == 'function_call'
69
+ build_tool_call_complete_chunk(data)
70
+ elsif data.dig('item', 'type') == 'image_generation_call'
71
+ build_completed_image_chunk(data)
72
+ else
73
+ build_empty_chunk(data)
74
+ end
75
+ end
76
+
77
+ def build_completion_chunk(data)
78
+ Chunk.new(
79
+ role: :assistant,
80
+ model_id: data.dig('response', 'model'),
81
+ content: nil,
82
+ tool_calls: nil,
83
+ input_tokens: data.dig('response', 'usage', 'input_tokens'),
84
+ output_tokens: data.dig('response', 'usage', 'output_tokens')
85
+ )
86
+ end
87
+
83
88
  def build_chat_completions_chunk(data)
84
89
  Chunk.new(
85
90
  role: :assistant,
@@ -93,8 +98,6 @@ module RubyLLM
93
98
  end
94
99
 
95
100
  def build_tool_call_delta_chunk(data)
96
- # For tool call argument deltas, we need to create a partial tool call
97
- # The accumulator will handle building up the complete arguments
98
101
  tool_call_data = {
99
102
  'id' => data['item_id'],
100
103
  'function' => {
@@ -153,10 +156,10 @@ module RubyLLM
153
156
  )
154
157
  end
155
158
 
156
- def build_empty_chunk(data)
159
+ def build_empty_chunk(_data)
157
160
  Chunk.new(
158
161
  role: :assistant,
159
- model_id: data.dig('response', 'model'),
162
+ model_id: nil,
160
163
  content: nil,
161
164
  tool_calls: nil,
162
165
  input_tokens: nil,
@@ -164,6 +167,63 @@ module RubyLLM
164
167
  )
165
168
  end
166
169
 
170
+ def build_partial_image_chunk(data)
171
+ content = build_image_content(data['partial_image_b64'], 'image/png', nil, nil)
172
+
173
+ Chunk.new(
174
+ role: :assistant,
175
+ model_id: nil,
176
+ content: content,
177
+ tool_calls: nil,
178
+ input_tokens: nil,
179
+ output_tokens: nil
180
+ )
181
+ end
182
+
183
+ def build_completed_image_chunk(data)
184
+ item = data['item']
185
+ image_data = item['result']
186
+ output_format = item['output_format'] || 'png'
187
+ mime_type = "image/#{output_format}"
188
+ revised_prompt = item['revised_prompt']
189
+
190
+ content = build_image_content(image_data, mime_type, nil, revised_prompt)
191
+
192
+ Chunk.new(
193
+ role: :assistant,
194
+ model_id: nil,
195
+ content: content,
196
+ tool_calls: nil,
197
+ input_tokens: nil,
198
+ output_tokens: nil
199
+ )
200
+ end
201
+
202
+ def build_reasoning_chunk(data)
203
+ Chunk.new(
204
+ role: :assistant,
205
+ model_id: nil,
206
+ content: nil,
207
+ tool_calls: nil,
208
+ input_tokens: nil,
209
+ output_tokens: nil,
210
+ reasoning_id: data.dig('item', 'id')
211
+ )
212
+ end
213
+
214
+ def build_image_content(base64_data, mime_type, model_id, revised_prompt = nil)
215
+ text_content = revised_prompt || ''
216
+ content = RubyLLM::Content.new(text_content)
217
+ content.attach(
218
+ RubyLLM::ImageAttachment.new(
219
+ data: base64_data,
220
+ mime_type: mime_type,
221
+ model_id: model_id
222
+ )
223
+ )
224
+ content
225
+ end
226
+
167
227
  def create_streaming_tool_call(tool_call_data)
168
228
  ToolCall.new(
169
229
  id: tool_call_data['id'],
@@ -83,7 +83,7 @@ module RubyLLM
83
83
 
84
84
  def parse_response_tool_calls(outputs)
85
85
  # TODO: implement the other & built-in tools
86
- # 'web_search_call', 'file_search_call', 'image_generation_call',
86
+ # 'web_search_call', 'file_search_call',
87
87
  # 'code_interpreter_call', 'local_shell_call', 'mcp_call',
88
88
  # 'mcp_list_tools', 'mcp_approval_request'
89
89
  outputs.select { |o| o['type'] == 'function_call' }.to_h do |o|
@@ -2,9 +2,7 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Providers
5
- # OpenAI API integration using the new Responses API. Handles response generation,
6
- # function calling, and OpenAI's unique streaming format. Supports GPT-4, GPT-3.5,
7
- # and other OpenAI models.
5
+ # OpenAI API integration.
8
6
  class OpenAI < OpenAIBase
9
7
  include OpenAI::Response
10
8
  include OpenAI::ResponseMedia
@@ -26,8 +26,8 @@ module RubyLLM
26
26
  }.compact
27
27
  end
28
28
 
29
- def maybe_normalize_temperature(temperature, model_id)
30
- OpenAI::Capabilities.normalize_temperature(temperature, model_id)
29
+ def maybe_normalize_temperature(temperature, model)
30
+ OpenAI::Capabilities.normalize_temperature(temperature, model.id)
31
31
  end
32
32
 
33
33
  class << self
@@ -13,13 +13,11 @@ module RubyLLM
13
13
 
14
14
  def parse_list_models_response(response, slug, _capabilities)
15
15
  Array(response.body['data']).map do |model_data| # rubocop:disable Metrics/BlockLength
16
- # Extract modalities directly from architecture
17
16
  modalities = {
18
17
  input: Array(model_data.dig('architecture', 'input_modalities')),
19
18
  output: Array(model_data.dig('architecture', 'output_modalities'))
20
19
  }
21
20
 
22
- # Construct pricing from API data, only adding non-zero values
23
21
  pricing = { text_tokens: { standard: {} } }
24
22
 
25
23
  pricing_types = {
@@ -34,7 +32,6 @@ module RubyLLM
34
32
  pricing[:text_tokens][:standard][target_key] = value * 1_000_000 if value.positive?
35
33
  end
36
34
 
37
- # Convert OpenRouter's supported parameters to our capability format
38
35
  capabilities = supported_parameters_to_capabilities(model_data['supported_parameters'])
39
36
 
40
37
  Model::Info.new(
@@ -63,23 +60,11 @@ module RubyLLM
63
60
  return [] unless params
64
61
 
65
62
  capabilities = []
66
-
67
- # Standard capabilities mapping
68
- capabilities << 'streaming' # Assume all OpenRouter models support streaming
69
-
70
- # Function calling capability
63
+ capabilities << 'streaming'
71
64
  capabilities << 'function_calling' if params.include?('tools') || params.include?('tool_choice')
72
-
73
- # Structured output capability
74
65
  capabilities << 'structured_output' if params.include?('response_format')
75
-
76
- # Batch capability
77
66
  capabilities << 'batch' if params.include?('batch')
78
-
79
- # Additional mappings based on params
80
- # Handles advanced model capabilities that might be inferred from supported params
81
67
  capabilities << 'predicted_outputs' if params.include?('logit_bias') && params.include?('top_k')
82
-
83
68
  capabilities
84
69
  end
85
70
  end
@@ -106,7 +106,6 @@ module RubyLLM
106
106
  }
107
107
  end
108
108
 
109
- # Pricing information for Perplexity models (USD per 1M tokens)
110
109
  PRICES = {
111
110
  sonar: {
112
111
  input: 1.0,
@@ -8,7 +8,6 @@ module RubyLLM
8
8
  module_function
9
9
 
10
10
  def format_role(role)
11
- # Perplexity doesn't use the new OpenAI convention for system prompts
12
11
  role.to_s
13
12
  end
14
13
  end
@@ -34,17 +34,13 @@ module RubyLLM
34
34
 
35
35
  # If response is HTML (Perplexity returns HTML for auth errors)
36
36
  if body.include?('<html>') && body.include?('<title>')
37
- # Extract title content
38
37
  title_match = body.match(%r{<title>(.+?)</title>})
39
38
  if title_match
40
- # Clean up the title - remove status code if present
41
39
  message = title_match[1]
42
- message = message.sub(/^\d+\s+/, '') # Remove leading digits and space
40
+ message = message.sub(/^\d+\s+/, '')
43
41
  return message
44
42
  end
45
43
  end
46
-
47
- # Fall back to parent's implementation
48
44
  super
49
45
  end
50
46
  end
@@ -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