ruby_llm 1.14.1 → 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 +6 -7
- data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool.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 +4 -26
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +123 -29
- data/lib/ruby_llm/active_record/chat_methods.rb +41 -24
- data/lib/ruby_llm/active_record/message_methods.rb +87 -4
- data/lib/ruby_llm/active_record/model_methods.rb +7 -9
- data/lib/ruby_llm/active_record/payload_helpers.rb +3 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +3 -0
- data/lib/ruby_llm/agent.rb +4 -2
- data/lib/ruby_llm/aliases.json +108 -75
- data/lib/ruby_llm/aliases.rb +3 -0
- data/lib/ruby_llm/attachment.rb +41 -40
- data/lib/ruby_llm/chat.rb +229 -59
- 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/cost.rb +224 -0
- 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 +39 -4
- data/lib/ruby_llm/instrumentation.rb +36 -0
- data/lib/ruby_llm/message.rb +20 -0
- data/lib/ruby_llm/mime_type.rb +25 -0
- data/lib/ruby_llm/model/info.rb +53 -2
- data/lib/ruby_llm/model/pricing.rb +19 -9
- data/lib/ruby_llm/model/pricing_category.rb +13 -2
- 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 +17817 -13942
- data/lib/ruby_llm/models.rb +97 -31
- data/lib/ruby_llm/models_schema.json +3 -0
- data/lib/ruby_llm/provider.rb +20 -4
- 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 +32 -3
- 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 +26 -13
- 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 +10 -1
- 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 +10 -4
- data/lib/ruby_llm/providers/gemini/images.rb +2 -2
- data/lib/ruby_llm/providers/gemini/media.rb +16 -9
- data/lib/ruby_llm/providers/gemini/streaming.rb +6 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +5 -1
- 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 +7 -2
- data/lib/ruby_llm/providers/mistral/chat.rb +56 -5
- 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/capabilities.rb +82 -12
- data/lib/ruby_llm/providers/openai/chat.rb +61 -7
- data/lib/ruby_llm/providers/openai/images.rb +58 -6
- data/lib/ruby_llm/providers/openai/media.rb +40 -16
- data/lib/ruby_llm/providers/openai/streaming.rb +7 -6
- 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 +36 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
- 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 +11 -1
- data/lib/ruby_llm/stream_accumulator.rb +45 -30
- data/lib/ruby_llm/streaming.rb +4 -0
- data/lib/ruby_llm/tokens.rb +8 -0
- data/lib/ruby_llm/tool.rb +24 -7
- 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 +11 -6
- data/lib/tasks/models.rake +45 -16
- data/lib/tasks/release.rake +50 -23
- metadata +35 -13
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
3
7
|
module RubyLLM
|
|
4
8
|
module Providers
|
|
5
9
|
class Bedrock
|
|
@@ -158,7 +162,10 @@ module RubyLLM
|
|
|
158
162
|
end
|
|
159
163
|
|
|
160
164
|
def extract_input_tokens(metadata_usage, usage, message_usage)
|
|
161
|
-
metadata_usage['inputTokens']
|
|
165
|
+
bedrock_usage = metadata_usage['inputTokens'] ? metadata_usage : usage
|
|
166
|
+
return Bedrock::Chat.input_tokens(bedrock_usage) if bedrock_usage['inputTokens']
|
|
167
|
+
|
|
168
|
+
message_usage['input_tokens']
|
|
162
169
|
end
|
|
163
170
|
|
|
164
171
|
def extract_output_tokens(metadata_usage, usage)
|
|
@@ -221,6 +228,7 @@ module RubyLLM
|
|
|
221
228
|
|
|
222
229
|
reasoning_text = reasoning_content['reasoningText'] || {}
|
|
223
230
|
return reasoning_text['text'] if reasoning_text['text']
|
|
231
|
+
return reasoning_content['text'] if reasoning_content['text']
|
|
224
232
|
return event.dig('delta', 'thinking') if event.dig('delta', 'type') == 'thinking_delta'
|
|
225
233
|
|
|
226
234
|
nil
|
|
@@ -241,6 +249,7 @@ module RubyLLM
|
|
|
241
249
|
reasoning_content = delta['reasoningContent'] || {}
|
|
242
250
|
reasoning_text = reasoning_content['reasoningText'] || {}
|
|
243
251
|
return reasoning_text['signature'] if reasoning_text['signature']
|
|
252
|
+
return reasoning_content['signature'] if reasoning_content['signature']
|
|
244
253
|
return event.dig('delta', 'signature') if event.dig('delta', 'type') == 'signature_delta'
|
|
245
254
|
|
|
246
255
|
nil
|
|
@@ -11,7 +11,7 @@ module RubyLLM
|
|
|
11
11
|
include Bedrock::Streaming
|
|
12
12
|
|
|
13
13
|
def api_base
|
|
14
|
-
"https://bedrock-runtime.#{bedrock_region}.amazonaws.com"
|
|
14
|
+
@config.bedrock_api_base || "https://bedrock-runtime.#{bedrock_region}.amazonaws.com"
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def headers
|
|
@@ -54,7 +54,7 @@ module RubyLLM
|
|
|
54
54
|
|
|
55
55
|
class << self
|
|
56
56
|
def configuration_options
|
|
57
|
-
%i[bedrock_api_key bedrock_secret_key bedrock_region bedrock_session_token]
|
|
57
|
+
%i[bedrock_api_key bedrock_secret_key bedrock_region bedrock_session_token bedrock_api_base]
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def configuration_requirements
|
|
@@ -7,6 +7,19 @@ module RubyLLM
|
|
|
7
7
|
module Capabilities
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
DEFAULT_CONTEXT_WINDOW = 1_000_000
|
|
11
|
+
DEFAULT_MAX_OUTPUT_TOKENS = 384_000
|
|
12
|
+
DEFAULT_PRICES = {
|
|
13
|
+
input: 0.14,
|
|
14
|
+
output: 0.28,
|
|
15
|
+
cache_read: 0.0028
|
|
16
|
+
}.freeze
|
|
17
|
+
PRO_PRICES = {
|
|
18
|
+
input: 0.435,
|
|
19
|
+
output: 0.87,
|
|
20
|
+
cache_read: 0.003625
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
10
23
|
def supports_tool_choice?(_model_id)
|
|
11
24
|
true
|
|
12
25
|
end
|
|
@@ -14,6 +27,36 @@ module RubyLLM
|
|
|
14
27
|
def supports_tool_parallel_control?(_model_id)
|
|
15
28
|
false
|
|
16
29
|
end
|
|
30
|
+
|
|
31
|
+
def context_window_for(_model_id)
|
|
32
|
+
DEFAULT_CONTEXT_WINDOW
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def max_tokens_for(_model_id)
|
|
36
|
+
DEFAULT_MAX_OUTPUT_TOKENS
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def critical_capabilities_for(model_id)
|
|
40
|
+
v4_model = model_id.start_with?('deepseek-v4-')
|
|
41
|
+
capabilities = ['function_calling']
|
|
42
|
+
capabilities << 'structured_output' if v4_model
|
|
43
|
+
capabilities << 'reasoning' if model_id == 'deepseek-reasoner' || v4_model
|
|
44
|
+
capabilities
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def pricing_for(model_id)
|
|
48
|
+
prices = model_id == 'deepseek-v4-pro' ? PRO_PRICES : DEFAULT_PRICES
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
text_tokens: {
|
|
52
|
+
standard: {
|
|
53
|
+
input_per_million: prices[:input],
|
|
54
|
+
output_per_million: prices[:output],
|
|
55
|
+
cache_read_input_per_million: prices[:cache_read]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
end
|
|
17
60
|
end
|
|
18
61
|
end
|
|
19
62
|
end
|
|
@@ -10,6 +10,15 @@ module RubyLLM
|
|
|
10
10
|
def format_role(role)
|
|
11
11
|
role.to_s
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
def format_content(content)
|
|
15
|
+
OpenAI::Media.format_content(
|
|
16
|
+
content,
|
|
17
|
+
document_attachments: :none,
|
|
18
|
+
image_attachments: false,
|
|
19
|
+
audio_attachments: false
|
|
20
|
+
)
|
|
21
|
+
end
|
|
13
22
|
end
|
|
14
23
|
end
|
|
15
24
|
end
|
|
@@ -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
|
|
@@ -118,7 +117,7 @@ module RubyLLM
|
|
|
118
117
|
signature: extract_thought_signature(parts)
|
|
119
118
|
),
|
|
120
119
|
tool_calls: tool_calls,
|
|
121
|
-
input_tokens: data
|
|
120
|
+
input_tokens: input_tokens(data),
|
|
122
121
|
output_tokens: calculate_output_tokens(data),
|
|
123
122
|
cached_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'),
|
|
124
123
|
thinking_tokens: data.dig('usageMetadata', 'thoughtsTokenCount'),
|
|
@@ -127,6 +126,13 @@ module RubyLLM
|
|
|
127
126
|
)
|
|
128
127
|
end
|
|
129
128
|
|
|
129
|
+
def input_tokens(data)
|
|
130
|
+
prompt_tokens = data.dig('usageMetadata', 'promptTokenCount')
|
|
131
|
+
return unless prompt_tokens
|
|
132
|
+
|
|
133
|
+
[prompt_tokens.to_i - data.dig('usageMetadata', 'cachedContentTokenCount').to_i, 0].max
|
|
134
|
+
end
|
|
135
|
+
|
|
130
136
|
def convert_schema_to_gemini(schema)
|
|
131
137
|
return nil unless schema
|
|
132
138
|
|
|
@@ -307,7 +313,7 @@ module RubyLLM
|
|
|
307
313
|
end
|
|
308
314
|
|
|
309
315
|
def build_tool_response(parts)
|
|
310
|
-
{ role: '
|
|
316
|
+
{ role: 'user', parts: parts }
|
|
311
317
|
end
|
|
312
318
|
|
|
313
319
|
def remember_tool_calls
|
|
@@ -5,11 +5,11 @@ module RubyLLM
|
|
|
5
5
|
class Gemini
|
|
6
6
|
# Image generation methods for the Gemini API implementation
|
|
7
7
|
module Images
|
|
8
|
-
def images_url
|
|
8
|
+
def images_url(with: nil, mask: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
9
9
|
"models/#{@model}:predict"
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def render_image_payload(prompt, model:, size:)
|
|
12
|
+
def render_image_payload(prompt, model:, size:, with: nil, mask: nil, params: {}) # rubocop:disable Lint/UnusedMethodArgument,Metrics/ParameterLists
|
|
13
13
|
RubyLLM.logger.debug { "Ignoring size #{size}. Gemini does not support image size customization." }
|
|
14
14
|
@model = model
|
|
15
15
|
{
|
|
@@ -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)
|
|
@@ -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
|
|
@@ -70,7 +72,10 @@ module RubyLLM
|
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
def extract_input_tokens(data)
|
|
73
|
-
data.dig('usageMetadata', 'promptTokenCount')
|
|
75
|
+
prompt_tokens = data.dig('usageMetadata', 'promptTokenCount')
|
|
76
|
+
return unless prompt_tokens
|
|
77
|
+
|
|
78
|
+
[prompt_tokens.to_i - data.dig('usageMetadata', 'cachedContentTokenCount').to_i, 0].max
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
def extract_output_tokens(data)
|
|
@@ -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
|
|
@@ -46,13 +48,15 @@ module RubyLLM
|
|
|
46
48
|
|
|
47
49
|
def format_tool_result(msg, function_name = nil)
|
|
48
50
|
function_name ||= msg.tool_call_id
|
|
51
|
+
content = msg.content
|
|
52
|
+
content = '(no output)' if content.nil? || (content.respond_to?(:empty?) && content.empty?)
|
|
49
53
|
|
|
50
54
|
[{
|
|
51
55
|
functionResponse: {
|
|
52
56
|
name: function_name,
|
|
53
57
|
response: {
|
|
54
58
|
name: function_name,
|
|
55
|
-
content: Media.format_content(
|
|
59
|
+
content: Media.format_content(content)
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
}]
|
|
@@ -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
|
|
@@ -31,6 +31,11 @@ module RubyLLM
|
|
|
31
31
|
!model_id.match?(/embed|moderation|ocr|voxtral|transcriptions/) && supports_tools?(model_id)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def supports_reasoning?(model_id)
|
|
35
|
+
model_id.match?(/magistral/) ||
|
|
36
|
+
model_id.match?(/\Amistral-(?:small-latest|medium-(?:3(?:[.-]5)?|latest))\z/)
|
|
37
|
+
end
|
|
38
|
+
|
|
34
39
|
def format_display_name(model_id)
|
|
35
40
|
case model_id
|
|
36
41
|
when /mistral-large/ then 'Mistral Large'
|
|
@@ -101,7 +106,7 @@ module RubyLLM
|
|
|
101
106
|
capabilities << 'structured_output' if supports_json_mode?(model_id)
|
|
102
107
|
capabilities << 'vision' if supports_vision?(model_id)
|
|
103
108
|
|
|
104
|
-
capabilities << 'reasoning' if
|
|
109
|
+
capabilities << 'reasoning' if supports_reasoning?(model_id)
|
|
105
110
|
capabilities << 'batch' unless model_id.match?(/voxtral|ocr|embed|moderation/)
|
|
106
111
|
capabilities << 'fine_tuning' if model_id.match?(/mistral-(small|medium|large)|devstral/)
|
|
107
112
|
capabilities << 'distillation' if model_id.match?(/ministral/)
|
|
@@ -118,7 +123,7 @@ module RubyLLM
|
|
|
118
123
|
}
|
|
119
124
|
end
|
|
120
125
|
|
|
121
|
-
def release_date_for(model_id)
|
|
126
|
+
def release_date_for(model_id) # rubocop:disable Metrics/CyclomaticComplexity
|
|
122
127
|
case model_id
|
|
123
128
|
when 'open-mistral-7b', 'mistral-tiny' then '2023-09-27'
|
|
124
129
|
when 'mistral-medium-2312', 'mistral-small-2312', 'mistral-small',
|
|
@@ -27,14 +27,32 @@ module RubyLLM
|
|
|
27
27
|
schema: nil, thinking: nil, tool_prefs: nil)
|
|
28
28
|
payload = super
|
|
29
29
|
payload.delete(:stream_options)
|
|
30
|
-
payload
|
|
31
|
-
|
|
30
|
+
configure_thinking_payload(payload, model, thinking)
|
|
31
|
+
normalize_required_tool_choice(payload)
|
|
32
32
|
payload
|
|
33
33
|
end
|
|
34
34
|
# rubocop:enable Metrics/ParameterLists
|
|
35
35
|
|
|
36
|
+
def build_tool_choice(tool_choice)
|
|
37
|
+
return 'any' if tool_choice == :required
|
|
38
|
+
|
|
39
|
+
OpenAI::Tools.build_tool_choice(tool_choice)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize_required_tool_choice(payload)
|
|
43
|
+
return unless payload[:tool_choice] == 'any' && Array(payload[:tools]).one?
|
|
44
|
+
|
|
45
|
+
function_name = payload.dig(:tools, 0, :function, :name)
|
|
46
|
+
return unless function_name
|
|
47
|
+
|
|
48
|
+
payload[:tool_choice] = {
|
|
49
|
+
type: 'function',
|
|
50
|
+
function: { name: function_name }
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
36
54
|
def format_content_with_thinking(msg)
|
|
37
|
-
formatted_content =
|
|
55
|
+
formatted_content = Mistral::Media.format_content(msg.content)
|
|
38
56
|
return formatted_content unless msg.role == :assistant && msg.thinking
|
|
39
57
|
|
|
40
58
|
content_blocks = build_thinking_blocks(msg.thinking)
|
|
@@ -45,14 +63,47 @@ module RubyLLM
|
|
|
45
63
|
|
|
46
64
|
def warn_on_unsupported_thinking(model, thinking)
|
|
47
65
|
return unless thinking&.enabled?
|
|
48
|
-
return if model.id
|
|
66
|
+
return if native_reasoning_model?(model.id) || adjustable_reasoning_model?(model.id)
|
|
49
67
|
|
|
50
68
|
RubyLLM.logger.warn(
|
|
51
|
-
'Mistral thinking is only supported on Magistral models. ' \
|
|
69
|
+
'Mistral thinking is only supported on Magistral and adjustable-reasoning models. ' \
|
|
52
70
|
"Ignoring thinking settings for #{model.id}."
|
|
53
71
|
)
|
|
54
72
|
end
|
|
55
73
|
|
|
74
|
+
def configure_thinking_payload(payload, model, thinking)
|
|
75
|
+
return unless thinking&.enabled?
|
|
76
|
+
|
|
77
|
+
if native_reasoning_model?(model.id)
|
|
78
|
+
configure_native_reasoning_payload(payload, thinking)
|
|
79
|
+
elsif adjustable_reasoning_model?(model.id)
|
|
80
|
+
payload[:reasoning_effort] = reasoning_effort_for(thinking)
|
|
81
|
+
else
|
|
82
|
+
payload.delete(:reasoning_effort)
|
|
83
|
+
warn_on_unsupported_thinking(model, thinking)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def configure_native_reasoning_payload(payload, thinking)
|
|
88
|
+
payload.delete(:reasoning_effort)
|
|
89
|
+
payload[:prompt_mode] = thinking.effort == 'none' ? nil : 'reasoning'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def reasoning_effort_for(thinking)
|
|
93
|
+
effort = thinking.respond_to?(:effort) ? thinking.effort : nil
|
|
94
|
+
return effort if %w[high none].include?(effort)
|
|
95
|
+
|
|
96
|
+
'high'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def native_reasoning_model?(model_id)
|
|
100
|
+
model_id.to_s.include?('magistral')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def adjustable_reasoning_model?(model_id)
|
|
104
|
+
model_id.to_s.match?(/\Amistral-(?:small-latest|medium-(?:3(?:[.-]5)?|latest))\z/)
|
|
105
|
+
end
|
|
106
|
+
|
|
56
107
|
def build_thinking_blocks(thinking)
|
|
57
108
|
return [] unless thinking
|
|
58
109
|
|
|
@@ -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
|