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.
- checksums.yaml +4 -4
- data/README.md +5 -4
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
- data/lib/ruby_llm/active_record/acts_as.rb +1 -26
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +71 -4
- data/lib/ruby_llm/active_record/chat_methods.rb +2 -2
- data/lib/ruby_llm/active_record/message_methods.rb +70 -3
- data/lib/ruby_llm/agent.rb +1 -0
- data/lib/ruby_llm/aliases.json +78 -75
- data/lib/ruby_llm/aliases.rb +3 -0
- data/lib/ruby_llm/attachment.rb +34 -17
- data/lib/ruby_llm/chat.rb +176 -47
- data/lib/ruby_llm/configuration.rb +14 -1
- data/lib/ruby_llm/connection.rb +36 -7
- data/lib/ruby_llm/content.rb +15 -1
- data/lib/ruby_llm/deprecator.rb +24 -0
- data/lib/ruby_llm/embedding.rb +31 -1
- data/lib/ruby_llm/error.rb +11 -75
- data/lib/ruby_llm/error_middleware.rb +81 -0
- data/lib/ruby_llm/image.rb +2 -0
- data/lib/ruby_llm/instrumentation.rb +36 -0
- data/lib/ruby_llm/mime_type.rb +25 -0
- data/lib/ruby_llm/model/info.rb +36 -2
- data/lib/ruby_llm/model/pricing.rb +19 -9
- data/lib/ruby_llm/model/pricing_tier.rb +20 -9
- data/lib/ruby_llm/model_registry.rb +39 -0
- data/lib/ruby_llm/models.json +18225 -19144
- data/lib/ruby_llm/models.rb +95 -30
- data/lib/ruby_llm/provider.rb +11 -2
- data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
- data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +28 -2
- data/lib/ruby_llm/providers/azure/media.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +2 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
- data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/streaming.rb +6 -0
- data/lib/ruby_llm/providers/bedrock.rb +2 -2
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +2 -3
- data/lib/ruby_llm/providers/gemini/media.rb +16 -9
- data/lib/ruby_llm/providers/gemini/streaming.rb +2 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +2 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
- data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +1 -1
- data/lib/ruby_llm/providers/mistral/chat.rb +1 -1
- data/lib/ruby_llm/providers/mistral/media.rb +55 -0
- data/lib/ruby_llm/providers/mistral/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral.rb +2 -2
- data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
- data/lib/ruby_llm/providers/openai/chat.rb +16 -1
- data/lib/ruby_llm/providers/openai/images.rb +9 -9
- data/lib/ruby_llm/providers/openai/media.rb +40 -16
- data/lib/ruby_llm/providers/openai/streaming.rb +2 -0
- data/lib/ruby_llm/providers/openai/tools.rb +2 -0
- data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +6 -2
- data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
- data/lib/ruby_llm/providers/perplexity.rb +2 -2
- data/lib/ruby_llm/providers/vertexai.rb +5 -1
- data/lib/ruby_llm/providers/xai/chat.rb +9 -0
- data/lib/ruby_llm/providers/xai/models.rb +15 -27
- data/lib/ruby_llm/providers/xai.rb +2 -2
- data/lib/ruby_llm/railtie.rb +5 -1
- data/lib/ruby_llm/stream_accumulator.rb +45 -30
- data/lib/ruby_llm/streaming.rb +4 -0
- data/lib/ruby_llm/tool_concurrency.rb +105 -0
- data/lib/ruby_llm/transcription.rb +2 -1
- data/lib/ruby_llm/utils.rb +39 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +9 -2
- data/lib/tasks/models.rake +32 -4
- data/lib/tasks/release.rake +50 -23
- 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: '
|
|
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
|
-
|
|
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
|
|
81
|
+
Content.new(text, attachments)
|
|
75
82
|
end
|
|
76
83
|
|
|
77
84
|
def build_inline_attachment(inline_data, index)
|
|
@@ -11,13 +11,20 @@ module RubyLLM
|
|
|
11
11
|
messages.map do |msg|
|
|
12
12
|
{
|
|
13
13
|
role: format_role(msg.role),
|
|
14
|
-
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
|
|
@@ -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 =
|
|
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
|
|
@@ -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:
|
|
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:
|
|
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
|
|
54
|
+
image: build_upload_parts(with),
|
|
52
55
|
n: 1
|
|
53
56
|
)
|
|
54
|
-
payload[:mask] = build_upload_part(mask
|
|
57
|
+
payload[:mask] = build_upload_part(mask) if mask
|
|
55
58
|
payload
|
|
56
59
|
end
|
|
57
60
|
|
|
58
|
-
def build_upload_parts(sources
|
|
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
|
|
65
|
+
build_upload_part(source)
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
|
|
66
|
-
def build_upload_part(source
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
61
|
+
def format_document(document)
|
|
49
62
|
{
|
|
50
63
|
type: 'file',
|
|
51
64
|
file: {
|
|
52
|
-
filename:
|
|
53
|
-
file_data:
|
|
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
|
|
@@ -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:
|
|
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
|
|
52
|
-
{ input: [
|
|
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
|
|
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
|
|
44
|
+
return ['vision'] if image_model?(model_id) || video_model?(model_id)
|
|
62
45
|
|
|
63
|
-
|
|
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
|