ruby_llm 1.14.0 → 1.15.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 +7 -5
- data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +3 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +52 -25
- data/lib/ruby_llm/active_record/chat_methods.rb +47 -23
- data/lib/ruby_llm/active_record/message_methods.rb +19 -14
- data/lib/ruby_llm/active_record/model_methods.rb +7 -9
- data/lib/ruby_llm/active_record/payload_helpers.rb +29 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +5 -15
- data/lib/ruby_llm/agent.rb +3 -2
- data/lib/ruby_llm/aliases.json +53 -14
- data/lib/ruby_llm/attachment.rb +11 -27
- data/lib/ruby_llm/chat.rb +62 -21
- data/lib/ruby_llm/cost.rb +224 -0
- data/lib/ruby_llm/image.rb +37 -4
- data/lib/ruby_llm/message.rb +20 -0
- data/lib/ruby_llm/model/info.rb +17 -0
- data/lib/ruby_llm/model/pricing_category.rb +13 -2
- data/lib/ruby_llm/models.json +26511 -24930
- data/lib/ruby_llm/models.rb +2 -1
- data/lib/ruby_llm/models_schema.json +3 -0
- data/lib/ruby_llm/provider.rb +10 -3
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -133
- data/lib/ruby_llm/providers/anthropic/models.rb +2 -8
- data/lib/ruby_llm/providers/anthropic/tools.rb +4 -1
- data/lib/ruby_llm/providers/bedrock/chat.rb +24 -13
- data/lib/ruby_llm/providers/bedrock/streaming.rb +4 -1
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -119
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -215
- data/lib/ruby_llm/providers/gemini/chat.rb +8 -1
- data/lib/ruby_llm/providers/gemini/images.rb +2 -2
- data/lib/ruby_llm/providers/gemini/models.rb +2 -4
- data/lib/ruby_llm/providers/gemini/streaming.rb +4 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +3 -1
- data/lib/ruby_llm/providers/mistral/capabilities.rb +6 -1
- data/lib/ruby_llm/providers/mistral/chat.rb +55 -4
- data/lib/ruby_llm/providers/openai/capabilities.rb +157 -195
- data/lib/ruby_llm/providers/openai/chat.rb +45 -6
- data/lib/ruby_llm/providers/openai/images.rb +58 -6
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/streaming.rb +5 -6
- data/lib/ruby_llm/providers/openrouter/chat.rb +30 -6
- 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/capabilities.rb +34 -99
- data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
- data/lib/ruby_llm/railtie.rb +6 -0
- data/lib/ruby_llm/tokens.rb +8 -0
- data/lib/ruby_llm/tool.rb +24 -7
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +2 -4
- data/lib/tasks/models.rake +13 -12
- metadata +21 -5
|
@@ -3,13 +3,35 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Providers
|
|
5
5
|
class Gemini
|
|
6
|
-
#
|
|
6
|
+
# Provider-level capability checks and narrow registry fallbacks.
|
|
7
7
|
module Capabilities
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
PRICES = {
|
|
11
|
+
flash_2: { input: 0.10, output: 0.40 }, # rubocop:disable Naming/VariableNumber
|
|
12
|
+
flash_lite_2: { input: 0.075, output: 0.30 }, # rubocop:disable Naming/VariableNumber
|
|
13
|
+
flash: { input: 0.075, output: 0.30 },
|
|
14
|
+
flash_8b: { input: 0.0375, output: 0.15 },
|
|
15
|
+
pro: { input: 1.25, output: 5.0 },
|
|
16
|
+
pro_2_5: { input: 0.12, output: 0.50 }, # rubocop:disable Naming/VariableNumber
|
|
17
|
+
gemini_embedding: { input: 0.002, output: 0.004 },
|
|
18
|
+
embedding: { input: 0.00, output: 0.00 },
|
|
19
|
+
imagen: { price: 0.03 },
|
|
20
|
+
aqa: { input: 0.00, output: 0.00 }
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def supports_tool_choice?(_model_id)
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def supports_tool_parallel_control?(_model_id)
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
10
31
|
def context_window_for(model_id)
|
|
11
32
|
case model_id
|
|
12
|
-
when /gemini-2\.5-pro-exp-03-25/, /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/,
|
|
33
|
+
when /gemini-2\.5-pro-exp-03-25/, /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/,
|
|
34
|
+
/gemini-1\.5-flash-8b/
|
|
13
35
|
1_048_576
|
|
14
36
|
when /gemini-1\.5-pro/ then 2_097_152
|
|
15
37
|
when /gemini-embedding-exp/ then 8_192
|
|
@@ -23,7 +45,8 @@ module RubyLLM
|
|
|
23
45
|
def max_tokens_for(model_id)
|
|
24
46
|
case model_id
|
|
25
47
|
when /gemini-2\.5-pro-exp-03-25/ then 64_000
|
|
26
|
-
when /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/,
|
|
48
|
+
when /gemini-2\.0-flash/, /gemini-2\.0-flash-lite/, /gemini-1\.5-flash/, /gemini-1\.5-flash-8b/,
|
|
49
|
+
/gemini-1\.5-pro/
|
|
27
50
|
8_192
|
|
28
51
|
when /gemini-embedding-exp/ then nil
|
|
29
52
|
when /text-embedding-004/, /embedding-001/ then 768
|
|
@@ -32,18 +55,24 @@ module RubyLLM
|
|
|
32
55
|
end
|
|
33
56
|
end
|
|
34
57
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
def critical_capabilities_for(model_id)
|
|
59
|
+
capabilities = []
|
|
60
|
+
capabilities << 'function_calling' if supports_functions?(model_id)
|
|
61
|
+
capabilities << 'structured_output' if supports_structured_output?(model_id)
|
|
62
|
+
capabilities << 'vision' if supports_vision?(model_id)
|
|
63
|
+
capabilities
|
|
40
64
|
end
|
|
41
65
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
def pricing_for(model_id)
|
|
67
|
+
prices = PRICES.fetch(pricing_family(model_id), { input: 0.075, output: 0.30 })
|
|
68
|
+
{
|
|
69
|
+
text_tokens: {
|
|
70
|
+
standard: {
|
|
71
|
+
input_per_million: prices[:input] || prices[:price] || 0.075,
|
|
72
|
+
output_per_million: prices[:output] || prices[:price] || 0.30
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
47
76
|
end
|
|
48
77
|
|
|
49
78
|
def supports_vision?(model_id)
|
|
@@ -52,25 +81,13 @@ module RubyLLM
|
|
|
52
81
|
model_id.match?(/gemini|flash|pro|imagen/)
|
|
53
82
|
end
|
|
54
83
|
|
|
55
|
-
def supports_video?(model_id)
|
|
56
|
-
model_id.match?(/gemini/)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
84
|
def supports_functions?(model_id)
|
|
60
85
|
return false if model_id.match?(/text-embedding|embedding-001|aqa|flash-lite|imagen|gemini-2\.0-flash-lite/)
|
|
61
86
|
|
|
62
87
|
model_id.match?(/gemini|pro|flash/)
|
|
63
88
|
end
|
|
64
89
|
|
|
65
|
-
def
|
|
66
|
-
true
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def supports_tool_parallel_control?(_model_id)
|
|
70
|
-
false
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def supports_json_mode?(model_id)
|
|
90
|
+
def supports_structured_output?(model_id)
|
|
74
91
|
if model_id.match?(/text-embedding|embedding-001|aqa|imagen|gemini-2\.0-flash-lite|gemini-2\.5-pro-exp-03-25/)
|
|
75
92
|
return false
|
|
76
93
|
end
|
|
@@ -78,59 +95,6 @@ module RubyLLM
|
|
|
78
95
|
model_id.match?(/gemini|pro|flash/)
|
|
79
96
|
end
|
|
80
97
|
|
|
81
|
-
def format_display_name(model_id)
|
|
82
|
-
model_id
|
|
83
|
-
.delete_prefix('models/')
|
|
84
|
-
.split('-')
|
|
85
|
-
.map(&:capitalize)
|
|
86
|
-
.join(' ')
|
|
87
|
-
.gsub(/(\d+\.\d+)/, ' \1')
|
|
88
|
-
.gsub(/\s+/, ' ')
|
|
89
|
-
.gsub('Aqa', 'AQA')
|
|
90
|
-
.strip
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def supports_caching?(model_id)
|
|
94
|
-
if model_id.match?(/flash-lite|gemini-2\.5-pro-exp-03-25|aqa|imagen|text-embedding|embedding-001/)
|
|
95
|
-
return false
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
model_id.match?(/gemini|pro|flash/)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def supports_tuning?(model_id)
|
|
102
|
-
model_id.match?(/gemini-1\.5-flash|gemini-1\.5-flash-8b/)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def supports_audio?(model_id)
|
|
106
|
-
model_id.match?(/gemini|pro|flash/)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def model_type(model_id)
|
|
110
|
-
case model_id
|
|
111
|
-
when /text-embedding|embedding|gemini-embedding/ then 'embedding'
|
|
112
|
-
when /imagen/ then 'image'
|
|
113
|
-
else 'chat'
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def model_family(model_id)
|
|
118
|
-
case model_id
|
|
119
|
-
when /gemini-2\.5-pro-exp-03-25/ then 'gemini25_pro_exp'
|
|
120
|
-
when /gemini-2\.0-flash-lite/ then 'gemini20_flash_lite'
|
|
121
|
-
when /gemini-2\.0-flash/ then 'gemini20_flash'
|
|
122
|
-
when /gemini-1\.5-flash-8b/ then 'gemini15_flash_8b'
|
|
123
|
-
when /gemini-1\.5-flash/ then 'gemini15_flash'
|
|
124
|
-
when /gemini-1\.5-pro/ then 'gemini15_pro'
|
|
125
|
-
when /gemini-embedding-exp/ then 'gemini_embedding_exp'
|
|
126
|
-
when /text-embedding-004/ then 'embedding4'
|
|
127
|
-
when /embedding-001/ then 'embedding1'
|
|
128
|
-
when /aqa/ then 'aqa'
|
|
129
|
-
when /imagen-3/ then 'imagen3'
|
|
130
|
-
else 'other'
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
98
|
def pricing_family(model_id)
|
|
135
99
|
case model_id
|
|
136
100
|
when /gemini-2\.5-pro-exp-03-25/ then :pro_2_5 # rubocop:disable Naming/VariableNumber
|
|
@@ -147,142 +111,8 @@ module RubyLLM
|
|
|
147
111
|
end
|
|
148
112
|
end
|
|
149
113
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def context_length(model_id)
|
|
155
|
-
context_window_for(model_id)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
PRICES = {
|
|
159
|
-
flash_2: { # rubocop:disable Naming/VariableNumber
|
|
160
|
-
input: 0.10,
|
|
161
|
-
output: 0.40,
|
|
162
|
-
audio_input: 0.70,
|
|
163
|
-
cache: 0.025,
|
|
164
|
-
cache_storage: 1.00,
|
|
165
|
-
grounding_search: 35.00
|
|
166
|
-
},
|
|
167
|
-
flash_lite_2: { # rubocop:disable Naming/VariableNumber
|
|
168
|
-
input: 0.075,
|
|
169
|
-
output: 0.30
|
|
170
|
-
},
|
|
171
|
-
flash: {
|
|
172
|
-
input: 0.075,
|
|
173
|
-
output: 0.30,
|
|
174
|
-
cache: 0.01875,
|
|
175
|
-
cache_storage: 1.00,
|
|
176
|
-
grounding_search: 35.00
|
|
177
|
-
},
|
|
178
|
-
flash_8b: {
|
|
179
|
-
input: 0.0375,
|
|
180
|
-
output: 0.15,
|
|
181
|
-
cache: 0.01,
|
|
182
|
-
cache_storage: 0.25,
|
|
183
|
-
grounding_search: 35.00
|
|
184
|
-
},
|
|
185
|
-
pro: {
|
|
186
|
-
input: 1.25,
|
|
187
|
-
output: 5.0,
|
|
188
|
-
cache: 0.3125,
|
|
189
|
-
cache_storage: 4.50,
|
|
190
|
-
grounding_search: 35.00
|
|
191
|
-
},
|
|
192
|
-
pro_2_5: { # rubocop:disable Naming/VariableNumber
|
|
193
|
-
input: 0.12,
|
|
194
|
-
output: 0.50
|
|
195
|
-
},
|
|
196
|
-
gemini_embedding: {
|
|
197
|
-
input: 0.002,
|
|
198
|
-
output: 0.004
|
|
199
|
-
},
|
|
200
|
-
embedding: {
|
|
201
|
-
input: 0.00,
|
|
202
|
-
output: 0.00
|
|
203
|
-
},
|
|
204
|
-
imagen: {
|
|
205
|
-
price: 0.03
|
|
206
|
-
},
|
|
207
|
-
aqa: {
|
|
208
|
-
input: 0.00,
|
|
209
|
-
output: 0.00
|
|
210
|
-
}
|
|
211
|
-
}.freeze
|
|
212
|
-
|
|
213
|
-
def default_input_price
|
|
214
|
-
0.075
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def default_output_price
|
|
218
|
-
0.30
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def modalities_for(model_id)
|
|
222
|
-
modalities = {
|
|
223
|
-
input: ['text'],
|
|
224
|
-
output: ['text']
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
if supports_vision?(model_id)
|
|
228
|
-
modalities[:input] << 'image'
|
|
229
|
-
modalities[:input] << 'pdf'
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
modalities[:input] << 'video' if supports_video?(model_id)
|
|
233
|
-
modalities[:input] << 'audio' if model_id.match?(/audio/)
|
|
234
|
-
modalities[:output] << 'embeddings' if model_id.match?(/embedding|gemini-embedding/)
|
|
235
|
-
modalities[:output] = ['image'] if model_id.match?(/imagen/)
|
|
236
|
-
|
|
237
|
-
modalities
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def capabilities_for(model_id)
|
|
241
|
-
capabilities = ['streaming']
|
|
242
|
-
|
|
243
|
-
capabilities << 'function_calling' if supports_functions?(model_id)
|
|
244
|
-
capabilities << 'structured_output' if supports_json_mode?(model_id)
|
|
245
|
-
capabilities << 'batch' if model_id.match?(/embedding|flash/)
|
|
246
|
-
capabilities << 'caching' if supports_caching?(model_id)
|
|
247
|
-
capabilities << 'fine_tuning' if supports_tuning?(model_id)
|
|
248
|
-
capabilities
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def pricing_for(model_id)
|
|
252
|
-
family = pricing_family(model_id)
|
|
253
|
-
prices = PRICES.fetch(family, { input: default_input_price, output: default_output_price })
|
|
254
|
-
|
|
255
|
-
standard_pricing = {
|
|
256
|
-
input_per_million: prices[:input],
|
|
257
|
-
output_per_million: prices[:output]
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
|
|
261
|
-
|
|
262
|
-
batch_pricing = {
|
|
263
|
-
input_per_million: (standard_pricing[:input_per_million] || 0) * 0.5,
|
|
264
|
-
output_per_million: (standard_pricing[:output_per_million] || 0) * 0.5
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if standard_pricing[:cached_input_per_million]
|
|
268
|
-
batch_pricing[:cached_input_per_million] = standard_pricing[:cached_input_per_million] * 0.5
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
pricing = {
|
|
272
|
-
text_tokens: {
|
|
273
|
-
standard: standard_pricing,
|
|
274
|
-
batch: batch_pricing
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if model_id.match?(/embedding|gemini-embedding/)
|
|
279
|
-
pricing[:embeddings] = {
|
|
280
|
-
standard: { input_per_million: prices[:price] || 0.002 }
|
|
281
|
-
}
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
pricing
|
|
285
|
-
end
|
|
114
|
+
module_function :context_window_for, :max_tokens_for, :critical_capabilities_for, :pricing_for,
|
|
115
|
+
:supports_vision?, :supports_functions?, :supports_structured_output?, :pricing_family
|
|
286
116
|
end
|
|
287
117
|
end
|
|
288
118
|
end
|
|
@@ -118,7 +118,7 @@ module RubyLLM
|
|
|
118
118
|
signature: extract_thought_signature(parts)
|
|
119
119
|
),
|
|
120
120
|
tool_calls: tool_calls,
|
|
121
|
-
input_tokens: data
|
|
121
|
+
input_tokens: input_tokens(data),
|
|
122
122
|
output_tokens: calculate_output_tokens(data),
|
|
123
123
|
cached_tokens: data.dig('usageMetadata', 'cachedContentTokenCount'),
|
|
124
124
|
thinking_tokens: data.dig('usageMetadata', 'thoughtsTokenCount'),
|
|
@@ -127,6 +127,13 @@ module RubyLLM
|
|
|
127
127
|
)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
def input_tokens(data)
|
|
131
|
+
prompt_tokens = data.dig('usageMetadata', 'promptTokenCount')
|
|
132
|
+
return unless prompt_tokens
|
|
133
|
+
|
|
134
|
+
[prompt_tokens.to_i - data.dig('usageMetadata', 'cachedContentTokenCount').to_i, 0].max
|
|
135
|
+
end
|
|
136
|
+
|
|
130
137
|
def convert_schema_to_gemini(schema)
|
|
131
138
|
return nil unless schema
|
|
132
139
|
|
|
@@ -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
|
{
|
|
@@ -17,14 +17,12 @@ module RubyLLM
|
|
|
17
17
|
|
|
18
18
|
Model::Info.new(
|
|
19
19
|
id: model_id,
|
|
20
|
-
name: model_data['displayName'],
|
|
20
|
+
name: model_data['displayName'] || model_id,
|
|
21
21
|
provider: slug,
|
|
22
|
-
family: capabilities.model_family(model_id),
|
|
23
22
|
created_at: nil,
|
|
24
23
|
context_window: model_data['inputTokenLimit'] || capabilities.context_window_for(model_id),
|
|
25
24
|
max_output_tokens: model_data['outputTokenLimit'] || capabilities.max_tokens_for(model_id),
|
|
26
|
-
|
|
27
|
-
capabilities: capabilities.capabilities_for(model_id),
|
|
25
|
+
capabilities: capabilities.critical_capabilities_for(model_id),
|
|
28
26
|
pricing: capabilities.pricing_for(model_id),
|
|
29
27
|
metadata: {
|
|
30
28
|
version: model_data['version'],
|
|
@@ -70,7 +70,10 @@ module RubyLLM
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def extract_input_tokens(data)
|
|
73
|
-
data.dig('usageMetadata', 'promptTokenCount')
|
|
73
|
+
prompt_tokens = data.dig('usageMetadata', 'promptTokenCount')
|
|
74
|
+
return unless prompt_tokens
|
|
75
|
+
|
|
76
|
+
[prompt_tokens.to_i - data.dig('usageMetadata', 'cachedContentTokenCount').to_i, 0].max
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
def extract_output_tokens(data)
|
|
@@ -46,13 +46,15 @@ module RubyLLM
|
|
|
46
46
|
|
|
47
47
|
def format_tool_result(msg, function_name = nil)
|
|
48
48
|
function_name ||= msg.tool_call_id
|
|
49
|
+
content = msg.content
|
|
50
|
+
content = '(no output)' if content.nil? || (content.respond_to?(:empty?) && content.empty?)
|
|
49
51
|
|
|
50
52
|
[{
|
|
51
53
|
functionResponse: {
|
|
52
54
|
name: function_name,
|
|
53
55
|
response: {
|
|
54
56
|
name: function_name,
|
|
55
|
-
content: Media.format_content(
|
|
57
|
+
content: Media.format_content(content)
|
|
56
58
|
}
|
|
57
59
|
}
|
|
58
60
|
}]
|
|
@@ -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/)
|
|
@@ -27,12 +27,30 @@ 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
55
|
formatted_content = OpenAI::Media.format_content(msg.content)
|
|
38
56
|
return formatted_content unless msg.role == :assistant && 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
|
|