ruby_llm 1.12.0 → 1.14.1
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 +11 -5
- data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
- data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
- data/lib/generators/ruby_llm/generator_helpers.rb +37 -17
- data/lib/generators/ruby_llm/install/install_generator.rb +22 -18
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +4 -10
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +2 -2
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
- data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +10 -4
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +87 -20
- data/lib/ruby_llm/active_record/chat_methods.rb +80 -22
- data/lib/ruby_llm/active_record/message_methods.rb +17 -0
- data/lib/ruby_llm/active_record/model_methods.rb +1 -1
- data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
- data/lib/ruby_llm/agent.rb +50 -8
- data/lib/ruby_llm/aliases.json +60 -21
- data/lib/ruby_llm/attachment.rb +4 -1
- data/lib/ruby_llm/chat.rb +113 -12
- data/lib/ruby_llm/configuration.rb +65 -66
- data/lib/ruby_llm/connection.rb +11 -7
- data/lib/ruby_llm/content.rb +6 -2
- data/lib/ruby_llm/error.rb +37 -1
- data/lib/ruby_llm/message.rb +5 -3
- data/lib/ruby_llm/model/info.rb +15 -13
- data/lib/ruby_llm/models.json +12279 -13517
- data/lib/ruby_llm/models.rb +16 -6
- data/lib/ruby_llm/provider.rb +10 -1
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
- data/lib/ruby_llm/providers/anthropic/chat.rb +22 -5
- data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
- data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +1 -1
- data/lib/ruby_llm/providers/azure/embeddings.rb +1 -1
- data/lib/ruby_llm/providers/azure/models.rb +1 -1
- data/lib/ruby_llm/providers/azure.rb +92 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +50 -5
- data/lib/ruby_llm/providers/bedrock/models.rb +17 -1
- data/lib/ruby_llm/providers/bedrock/streaming.rb +8 -4
- data/lib/ruby_llm/providers/bedrock.rb +9 -1
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
- data/lib/ruby_llm/providers/gemini/chat.rb +20 -4
- data/lib/ruby_llm/providers/gemini/images.rb +1 -1
- data/lib/ruby_llm/providers/gemini/models.rb +2 -4
- data/lib/ruby_llm/providers/gemini/streaming.rb +2 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +19 -0
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/gpustack.rb +8 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +2 -1
- data/lib/ruby_llm/providers/mistral.rb +4 -0
- data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/ollama.rb +11 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +95 -195
- data/lib/ruby_llm/providers/openai/chat.rb +15 -5
- data/lib/ruby_llm/providers/openai/media.rb +4 -1
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/temperature.rb +2 -2
- data/lib/ruby_llm/providers/openai/tools.rb +27 -2
- data/lib/ruby_llm/providers/openai.rb +10 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +19 -5
- data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
- data/lib/ruby_llm/providers/openrouter.rb +35 -1
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
- data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
- data/lib/ruby_llm/providers/perplexity.rb +4 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +18 -6
- data/lib/ruby_llm/providers/xai.rb +4 -0
- data/lib/ruby_llm/stream_accumulator.rb +10 -5
- data/lib/ruby_llm/streaming.rb +7 -7
- data/lib/ruby_llm/tool.rb +48 -3
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/tasks/models.rake +33 -7
- data/lib/tasks/release.rake +1 -1
- data/lib/tasks/ruby_llm.rake +9 -1
- data/lib/tasks/vcr.rake +1 -1
- metadata +56 -15
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
data/lib/ruby_llm/models.rb
CHANGED
|
@@ -47,7 +47,8 @@ module RubyLLM
|
|
|
47
47
|
|
|
48
48
|
def read_from_json(file = RubyLLM.config.model_registry_file)
|
|
49
49
|
data = File.exist?(file) ? File.read(file) : '[]'
|
|
50
|
-
JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
|
|
50
|
+
models = JSON.parse(data, symbolize_names: true).map { |model| Model::Info.new(model) }
|
|
51
|
+
filter_models(models)
|
|
51
52
|
rescue JSON::ParserError
|
|
52
53
|
[]
|
|
53
54
|
end
|
|
@@ -232,7 +233,13 @@ module RubyLLM
|
|
|
232
233
|
end
|
|
233
234
|
end
|
|
234
235
|
|
|
235
|
-
models.sort_by { |m| [m.provider, m.id] }
|
|
236
|
+
filter_models(models).sort_by { |m| [m.provider, m.id] }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def filter_models(models)
|
|
240
|
+
models.reject do |model|
|
|
241
|
+
model.provider.to_s == 'vertexai' && model.id.to_s.include?('/')
|
|
242
|
+
end
|
|
236
243
|
end
|
|
237
244
|
|
|
238
245
|
def find_models_dev_model(key, models_dev_by_key)
|
|
@@ -266,8 +273,8 @@ module RubyLLM
|
|
|
266
273
|
end
|
|
267
274
|
|
|
268
275
|
def index_by_key(models)
|
|
269
|
-
models.
|
|
270
|
-
|
|
276
|
+
models.to_h do |model|
|
|
277
|
+
["#{model.provider}:#{model.id}", model]
|
|
271
278
|
end
|
|
272
279
|
end
|
|
273
280
|
|
|
@@ -312,12 +319,15 @@ module RubyLLM
|
|
|
312
319
|
modalities = normalize_models_dev_modalities(model_data[:modalities])
|
|
313
320
|
capabilities = models_dev_capabilities(model_data, modalities)
|
|
314
321
|
|
|
322
|
+
created_date = [model_data[:release_date], model_data[:last_updated]]
|
|
323
|
+
.find { |value| !value.to_s.strip.empty? }
|
|
324
|
+
|
|
315
325
|
data = {
|
|
316
326
|
id: model_data[:id],
|
|
317
327
|
name: model_data[:name] || model_data[:id],
|
|
318
328
|
provider: provider_slug,
|
|
319
329
|
family: model_data[:family],
|
|
320
|
-
created_at:
|
|
330
|
+
created_at: created_date ? "#{created_date} 00:00:00 UTC" : nil,
|
|
321
331
|
context_window: model_data.dig(:limit, :context),
|
|
322
332
|
max_output_tokens: model_data.dig(:limit, :output),
|
|
323
333
|
knowledge_cutoff: normalize_models_dev_knowledge(model_data[:knowledge]),
|
|
@@ -398,7 +408,7 @@ module RubyLLM
|
|
|
398
408
|
end
|
|
399
409
|
|
|
400
410
|
def initialize(models = nil)
|
|
401
|
-
@models = models || self.class.load_models
|
|
411
|
+
@models = self.class.filter_models(models || self.class.load_models)
|
|
402
412
|
end
|
|
403
413
|
|
|
404
414
|
def load_from_json!(file = RubyLLM.config.model_registry_file)
|
data/lib/ruby_llm/provider.rb
CHANGED
|
@@ -37,13 +37,16 @@ module RubyLLM
|
|
|
37
37
|
self.class.configuration_requirements
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
# rubocop:disable Metrics/ParameterLists
|
|
41
|
+
def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
|
|
42
|
+
tool_prefs: nil, &)
|
|
41
43
|
normalized_temperature = maybe_normalize_temperature(temperature, model)
|
|
42
44
|
|
|
43
45
|
payload = Utils.deep_merge(
|
|
44
46
|
render_payload(
|
|
45
47
|
messages,
|
|
46
48
|
tools: tools,
|
|
49
|
+
tool_prefs: tool_prefs,
|
|
47
50
|
temperature: normalized_temperature,
|
|
48
51
|
model: model,
|
|
49
52
|
stream: block_given?,
|
|
@@ -59,6 +62,7 @@ module RubyLLM
|
|
|
59
62
|
sync_response @connection, payload, headers
|
|
60
63
|
end
|
|
61
64
|
end
|
|
65
|
+
# rubocop:enable Metrics/ParameterLists
|
|
62
66
|
|
|
63
67
|
def list_models
|
|
64
68
|
response = @connection.get models_url
|
|
@@ -160,6 +164,10 @@ module RubyLLM
|
|
|
160
164
|
[]
|
|
161
165
|
end
|
|
162
166
|
|
|
167
|
+
def configuration_options
|
|
168
|
+
[]
|
|
169
|
+
end
|
|
170
|
+
|
|
163
171
|
def local?
|
|
164
172
|
false
|
|
165
173
|
end
|
|
@@ -178,6 +186,7 @@ module RubyLLM
|
|
|
178
186
|
|
|
179
187
|
def register(name, provider_class)
|
|
180
188
|
providers[name.to_sym] = provider_class
|
|
189
|
+
RubyLLM::Configuration.register_provider_options(provider_class.configuration_options)
|
|
181
190
|
end
|
|
182
191
|
|
|
183
192
|
def resolve(name)
|
|
@@ -3,130 +3,16 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Providers
|
|
5
5
|
class Anthropic
|
|
6
|
-
#
|
|
6
|
+
# Provider-level capability checks used outside the model registry.
|
|
7
7
|
module Capabilities
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def
|
|
11
|
-
|
|
10
|
+
def supports_tool_choice?(_model_id)
|
|
11
|
+
true
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
when /claude-3-7-sonnet/, /claude-3-5/ then 8_192
|
|
17
|
-
else 4_096
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def get_input_price(model_id)
|
|
22
|
-
PRICES.dig(model_family(model_id), :input) || default_input_price
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def get_output_price(model_id)
|
|
26
|
-
PRICES.dig(model_family(model_id), :output) || default_output_price
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def supports_vision?(model_id)
|
|
30
|
-
!model_id.match?(/claude-[12]/)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def supports_functions?(model_id)
|
|
34
|
-
model_id.match?(/claude-3/)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def supports_json_mode?(model_id)
|
|
38
|
-
model_id.match?(/claude-3/)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def supports_extended_thinking?(model_id)
|
|
42
|
-
model_id.match?(/claude-3-7-sonnet/)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def model_family(model_id)
|
|
46
|
-
case model_id
|
|
47
|
-
when /claude-3-7-sonnet/ then 'claude-3-7-sonnet'
|
|
48
|
-
when /claude-3-5-sonnet/ then 'claude-3-5-sonnet'
|
|
49
|
-
when /claude-3-5-haiku/ then 'claude-3-5-haiku'
|
|
50
|
-
when /claude-3-opus/ then 'claude-3-opus'
|
|
51
|
-
when /claude-3-sonnet/ then 'claude-3-sonnet'
|
|
52
|
-
when /claude-3-haiku/ then 'claude-3-haiku'
|
|
53
|
-
else 'claude-2'
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def model_type(_)
|
|
58
|
-
'chat'
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
PRICES = {
|
|
62
|
-
'claude-3-7-sonnet': { input: 3.0, output: 15.0 },
|
|
63
|
-
'claude-3-5-sonnet': { input: 3.0, output: 15.0 },
|
|
64
|
-
'claude-3-5-haiku': { input: 0.80, output: 4.0 },
|
|
65
|
-
'claude-3-opus': { input: 15.0, output: 75.0 },
|
|
66
|
-
'claude-3-haiku': { input: 0.25, output: 1.25 },
|
|
67
|
-
'claude-2': { input: 3.0, output: 15.0 }
|
|
68
|
-
}.freeze
|
|
69
|
-
|
|
70
|
-
def default_input_price
|
|
71
|
-
3.0
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def default_output_price
|
|
75
|
-
15.0
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def modalities_for(model_id)
|
|
79
|
-
modalities = {
|
|
80
|
-
input: ['text'],
|
|
81
|
-
output: ['text']
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
unless model_id.match?(/claude-[12]/)
|
|
85
|
-
modalities[:input] << 'image'
|
|
86
|
-
modalities[:input] << 'pdf'
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
modalities
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def capabilities_for(model_id)
|
|
93
|
-
capabilities = ['streaming']
|
|
94
|
-
|
|
95
|
-
if model_id.match?(/claude-3/)
|
|
96
|
-
capabilities << 'function_calling'
|
|
97
|
-
capabilities << 'batch'
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
capabilities << 'reasoning' if model_id.match?(/claude-3-7|-4/)
|
|
101
|
-
capabilities << 'citations' if model_id.match?(/claude-3\.5|claude-3-7/)
|
|
102
|
-
capabilities
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def pricing_for(model_id)
|
|
106
|
-
family = model_family(model_id)
|
|
107
|
-
prices = PRICES.fetch(family.to_sym, { input: default_input_price, output: default_output_price })
|
|
108
|
-
|
|
109
|
-
standard_pricing = {
|
|
110
|
-
input_per_million: prices[:input],
|
|
111
|
-
output_per_million: prices[:output]
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
batch_pricing = {
|
|
115
|
-
input_per_million: prices[:input] * 0.5,
|
|
116
|
-
output_per_million: prices[:output] * 0.5
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if model_id.match?(/claude-3-7/)
|
|
120
|
-
standard_pricing[:reasoning_output_per_million] = prices[:output] * 2.5
|
|
121
|
-
batch_pricing[:reasoning_output_per_million] = prices[:output] * 1.25
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
text_tokens: {
|
|
126
|
-
standard: standard_pricing,
|
|
127
|
-
batch: batch_pricing
|
|
128
|
-
}
|
|
129
|
-
}
|
|
14
|
+
def supports_tool_parallel_control?(_model_id)
|
|
15
|
+
true
|
|
130
16
|
end
|
|
131
17
|
end
|
|
132
18
|
end
|
|
@@ -8,17 +8,21 @@ module RubyLLM
|
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
def completion_url
|
|
11
|
-
'
|
|
11
|
+
'v1/messages'
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# rubocop:disable Metrics/ParameterLists
|
|
15
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false,
|
|
16
|
+
schema: nil, thinking: nil, tool_prefs: nil)
|
|
17
|
+
tool_prefs ||= {}
|
|
15
18
|
system_messages, chat_messages = separate_messages(messages)
|
|
16
19
|
system_content = build_system_content(system_messages)
|
|
17
20
|
|
|
18
21
|
build_base_payload(chat_messages, model, stream, thinking).tap do |payload|
|
|
19
|
-
add_optional_fields(payload, system_content:, tools:, temperature:)
|
|
22
|
+
add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema:)
|
|
20
23
|
end
|
|
21
24
|
end
|
|
25
|
+
# rubocop:enable Metrics/ParameterLists
|
|
22
26
|
|
|
23
27
|
def separate_messages(messages)
|
|
24
28
|
messages.partition { |msg| msg.role == :system }
|
|
@@ -59,10 +63,23 @@ module RubyLLM
|
|
|
59
63
|
payload
|
|
60
64
|
end
|
|
61
65
|
|
|
62
|
-
def add_optional_fields(payload, system_content:, tools:, temperature:)
|
|
63
|
-
|
|
66
|
+
def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil) # rubocop:disable Metrics/ParameterLists
|
|
67
|
+
if tools.any?
|
|
68
|
+
payload[:tools] = tools.values.map { |t| Tools.function_for(t) }
|
|
69
|
+
unless tool_prefs[:choice].nil? && tool_prefs[:calls].nil?
|
|
70
|
+
payload[:tool_choice] = Tools.build_tool_choice(tool_prefs)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
64
73
|
payload[:system] = system_content unless system_content.empty?
|
|
65
74
|
payload[:temperature] = temperature unless temperature.nil?
|
|
75
|
+
payload[:output_config] = build_output_config(schema) if schema
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_output_config(schema)
|
|
79
|
+
normalized = RubyLLM::Utils.deep_dup(schema[:schema])
|
|
80
|
+
normalized.delete(:strict)
|
|
81
|
+
normalized.delete('strict')
|
|
82
|
+
{ format: { type: 'json_schema', schema: normalized } }
|
|
66
83
|
end
|
|
67
84
|
|
|
68
85
|
def parse_completion_response(response)
|
|
@@ -8,24 +8,18 @@ module RubyLLM
|
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
def models_url
|
|
11
|
-
'
|
|
11
|
+
'v1/models'
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def parse_list_models_response(response, slug,
|
|
14
|
+
def parse_list_models_response(response, slug, _capabilities)
|
|
15
15
|
Array(response.body['data']).map do |model_data|
|
|
16
16
|
model_id = model_data['id']
|
|
17
17
|
|
|
18
18
|
Model::Info.new(
|
|
19
19
|
id: model_id,
|
|
20
|
-
name: model_data['display_name'],
|
|
20
|
+
name: model_data['display_name'] || model_id,
|
|
21
21
|
provider: slug,
|
|
22
|
-
family: capabilities.model_family(model_id),
|
|
23
22
|
created_at: Time.parse(model_data['created_at']),
|
|
24
|
-
context_window: capabilities.determine_context_window(model_id),
|
|
25
|
-
max_output_tokens: capabilities.determine_max_tokens(model_id),
|
|
26
|
-
modalities: capabilities.modalities_for(model_id),
|
|
27
|
-
capabilities: capabilities.capabilities_for(model_id),
|
|
28
|
-
pricing: capabilities.pricing_for(model_id),
|
|
29
23
|
metadata: {}
|
|
30
24
|
)
|
|
31
25
|
end
|
|
@@ -103,6 +103,26 @@ module RubyLLM
|
|
|
103
103
|
'strict' => true
|
|
104
104
|
}
|
|
105
105
|
end
|
|
106
|
+
|
|
107
|
+
def build_tool_choice(tool_prefs)
|
|
108
|
+
tool_choice = tool_prefs[:choice]
|
|
109
|
+
calls_in_response = tool_prefs[:calls]
|
|
110
|
+
tool_choice = :auto if tool_choice.nil?
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
type: case tool_choice
|
|
114
|
+
when :auto, :none
|
|
115
|
+
tool_choice
|
|
116
|
+
when :required
|
|
117
|
+
:any
|
|
118
|
+
else
|
|
119
|
+
:tool
|
|
120
|
+
end
|
|
121
|
+
}.tap do |tc|
|
|
122
|
+
tc[:name] = tool_choice if tc[:type] == :tool
|
|
123
|
+
tc[:disable_parallel_tool_use] = calls_in_response == :one if tc[:type] != :none && !calls_in_response.nil?
|
|
124
|
+
end
|
|
125
|
+
end
|
|
106
126
|
end
|
|
107
127
|
end
|
|
108
128
|
end
|
|
@@ -12,7 +12,7 @@ module RubyLLM
|
|
|
12
12
|
include Anthropic::Tools
|
|
13
13
|
|
|
14
14
|
def api_base
|
|
15
|
-
'https://api.anthropic.com'
|
|
15
|
+
@config.anthropic_api_base || 'https://api.anthropic.com'
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def headers
|
|
@@ -27,6 +27,10 @@ module RubyLLM
|
|
|
27
27
|
Anthropic::Capabilities
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
def configuration_options
|
|
31
|
+
%i[anthropic_api_key anthropic_api_base]
|
|
32
|
+
end
|
|
33
|
+
|
|
30
34
|
def configuration_requirements
|
|
31
35
|
%i[anthropic_api_key]
|
|
32
36
|
end
|
|
@@ -4,6 +4,9 @@ module RubyLLM
|
|
|
4
4
|
module Providers
|
|
5
5
|
# Azure AI Foundry / OpenAI-compatible API integration.
|
|
6
6
|
class Azure < OpenAI
|
|
7
|
+
AZURE_DEFAULT_CHAT_API_VERSION = '2024-05-01-preview'
|
|
8
|
+
AZURE_DEFAULT_MODELS_API_VERSION = 'preview'
|
|
9
|
+
|
|
7
10
|
include Azure::Chat
|
|
8
11
|
include Azure::Embeddings
|
|
9
12
|
include Azure::Media
|
|
@@ -25,7 +28,26 @@ module RubyLLM
|
|
|
25
28
|
self.class.configured?(@config)
|
|
26
29
|
end
|
|
27
30
|
|
|
31
|
+
def azure_endpoint(kind)
|
|
32
|
+
parts = azure_base_parts
|
|
33
|
+
|
|
34
|
+
case kind
|
|
35
|
+
when :chat
|
|
36
|
+
chat_endpoint(parts)
|
|
37
|
+
when :embeddings
|
|
38
|
+
embeddings_endpoint(parts)
|
|
39
|
+
when :models
|
|
40
|
+
models_endpoint(parts)
|
|
41
|
+
else
|
|
42
|
+
raise ArgumentError, "Unknown Azure endpoint kind: #{kind.inspect}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
28
46
|
class << self
|
|
47
|
+
def configuration_options
|
|
48
|
+
%i[azure_api_base azure_api_key azure_ai_auth_token]
|
|
49
|
+
end
|
|
50
|
+
|
|
29
51
|
def configuration_requirements
|
|
30
52
|
%i[azure_api_base]
|
|
31
53
|
end
|
|
@@ -51,6 +73,76 @@ module RubyLLM
|
|
|
51
73
|
raise ConfigurationError,
|
|
52
74
|
"Missing configuration for Azure: #{missing.join(', ')}"
|
|
53
75
|
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def azure_base_parts
|
|
80
|
+
@azure_base_parts ||= begin
|
|
81
|
+
raw_base = api_base.to_s.sub(%r{/+\z}, '')
|
|
82
|
+
version = raw_base[/[?&]api-version=([^&]+)/i, 1]
|
|
83
|
+
path_base = raw_base.sub(/\?.*\z/, '')
|
|
84
|
+
|
|
85
|
+
mode = if path_base.include?('/chat/completions')
|
|
86
|
+
:chat_endpoint
|
|
87
|
+
elsif path_base.include?('/openai/deployments/')
|
|
88
|
+
:deployment_base
|
|
89
|
+
elsif path_base.include?('/openai/v1')
|
|
90
|
+
:openai_v1_base
|
|
91
|
+
else
|
|
92
|
+
:resource_base
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
raw_base: raw_base,
|
|
97
|
+
path_base: path_base,
|
|
98
|
+
root: azure_host_root(path_base),
|
|
99
|
+
mode: mode,
|
|
100
|
+
version: version
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def chat_endpoint(parts)
|
|
106
|
+
case parts[:mode]
|
|
107
|
+
when :chat_endpoint
|
|
108
|
+
''
|
|
109
|
+
when :deployment_base
|
|
110
|
+
with_api_version('chat/completions', parts[:version] || AZURE_DEFAULT_CHAT_API_VERSION)
|
|
111
|
+
when :openai_v1_base
|
|
112
|
+
with_api_version('chat/completions', parts[:version])
|
|
113
|
+
else
|
|
114
|
+
with_api_version('models/chat/completions', parts[:version] || AZURE_DEFAULT_CHAT_API_VERSION)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def embeddings_endpoint(parts)
|
|
119
|
+
case parts[:mode]
|
|
120
|
+
when :deployment_base, :openai_v1_base
|
|
121
|
+
with_api_version('embeddings', parts[:version])
|
|
122
|
+
else
|
|
123
|
+
"#{parts[:root]}/openai/v1/embeddings"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def models_endpoint(parts)
|
|
128
|
+
case parts[:mode]
|
|
129
|
+
when :openai_v1_base
|
|
130
|
+
with_api_version('models', parts[:version] || AZURE_DEFAULT_MODELS_API_VERSION)
|
|
131
|
+
else
|
|
132
|
+
"#{parts[:root]}/openai/v1/models?api-version=#{parts[:version] || AZURE_DEFAULT_MODELS_API_VERSION}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def with_api_version(path, version)
|
|
137
|
+
return path unless version
|
|
138
|
+
|
|
139
|
+
separator = path.include?('?') ? '&' : '?'
|
|
140
|
+
"#{path}#{separator}api-version=#{version}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def azure_host_root(base_without_query)
|
|
144
|
+
base_without_query.sub(%r{/(models|openai)/.*\z}, '').sub(%r{/+\z}, '')
|
|
145
|
+
end
|
|
54
146
|
end
|
|
55
147
|
end
|
|
56
148
|
end
|
|
@@ -11,11 +11,13 @@ module RubyLLM
|
|
|
11
11
|
"/model/#{@model.id}/converse"
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
|
|
15
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false,
|
|
16
|
+
schema: nil, thinking: nil, tool_prefs: nil)
|
|
17
|
+
tool_prefs ||= {}
|
|
15
18
|
@model = model
|
|
16
19
|
@used_document_names = {}
|
|
17
20
|
system_messages, chat_messages = messages.partition { |msg| msg.role == :system }
|
|
18
|
-
|
|
19
21
|
payload = {
|
|
20
22
|
messages: render_messages(chat_messages)
|
|
21
23
|
}
|
|
@@ -25,7 +27,7 @@ module RubyLLM
|
|
|
25
27
|
|
|
26
28
|
payload[:inferenceConfig] = render_inference_config(model, temperature)
|
|
27
29
|
|
|
28
|
-
tool_config = render_tool_config(tools)
|
|
30
|
+
tool_config = render_tool_config(tools, tool_prefs)
|
|
29
31
|
if tool_config
|
|
30
32
|
payload[:toolConfig] = tool_config
|
|
31
33
|
payload[:tools] = tool_config[:tools] # Internal mirror for shared payload inspections in specs.
|
|
@@ -34,8 +36,12 @@ module RubyLLM
|
|
|
34
36
|
additional_fields = render_additional_model_request_fields(thinking)
|
|
35
37
|
payload[:additionalModelRequestFields] = additional_fields if additional_fields
|
|
36
38
|
|
|
39
|
+
output_config = build_output_config(schema)
|
|
40
|
+
payload[:outputConfig] = output_config if output_config
|
|
41
|
+
|
|
37
42
|
payload
|
|
38
43
|
end
|
|
44
|
+
# rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument
|
|
39
45
|
|
|
40
46
|
def parse_completion_response(response)
|
|
41
47
|
data = response.body
|
|
@@ -203,12 +209,31 @@ module RubyLLM
|
|
|
203
209
|
config
|
|
204
210
|
end
|
|
205
211
|
|
|
206
|
-
def render_tool_config(tools)
|
|
212
|
+
def render_tool_config(tools, tool_prefs)
|
|
207
213
|
return nil if tools.empty?
|
|
208
214
|
|
|
209
|
-
{
|
|
215
|
+
config = {
|
|
210
216
|
tools: tools.values.map { |tool| render_tool(tool) }
|
|
211
217
|
}
|
|
218
|
+
|
|
219
|
+
return config if tool_prefs.nil? || tool_prefs[:choice].nil?
|
|
220
|
+
|
|
221
|
+
tool_choice = render_tool_choice(tool_prefs[:choice])
|
|
222
|
+
config[:toolChoice] = tool_choice if tool_choice
|
|
223
|
+
config
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def render_tool_choice(choice)
|
|
227
|
+
case choice
|
|
228
|
+
when :auto
|
|
229
|
+
{ auto: {} }
|
|
230
|
+
when :none
|
|
231
|
+
nil
|
|
232
|
+
when :required
|
|
233
|
+
{ any: {} }
|
|
234
|
+
else
|
|
235
|
+
{ tool: { name: choice.to_s } }
|
|
236
|
+
end
|
|
212
237
|
end
|
|
213
238
|
|
|
214
239
|
def render_tool(tool)
|
|
@@ -238,6 +263,26 @@ module RubyLLM
|
|
|
238
263
|
fields.empty? ? nil : fields
|
|
239
264
|
end
|
|
240
265
|
|
|
266
|
+
def build_output_config(schema)
|
|
267
|
+
return nil unless schema
|
|
268
|
+
|
|
269
|
+
cleaned = RubyLLM::Utils.deep_dup(schema[:schema])
|
|
270
|
+
cleaned.delete(:strict)
|
|
271
|
+
cleaned.delete('strict')
|
|
272
|
+
|
|
273
|
+
{
|
|
274
|
+
textFormat: {
|
|
275
|
+
type: 'json_schema',
|
|
276
|
+
structure: {
|
|
277
|
+
jsonSchema: {
|
|
278
|
+
schema: JSON.generate(cleaned),
|
|
279
|
+
name: schema[:name]
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
end
|
|
285
|
+
|
|
241
286
|
def render_reasoning_fields(thinking)
|
|
242
287
|
return nil unless thinking&.enabled?
|
|
243
288
|
|
|
@@ -7,7 +7,7 @@ module RubyLLM
|
|
|
7
7
|
module Models
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
REGION_PREFIXES = %w[us eu ap sa ca me af il].freeze
|
|
10
|
+
REGION_PREFIXES = %w[global us eu ap sa ca me af il].freeze
|
|
11
11
|
|
|
12
12
|
def models_api_base
|
|
13
13
|
"https://bedrock.#{bedrock_region}.amazonaws.com"
|
|
@@ -100,10 +100,26 @@ module RubyLLM
|
|
|
100
100
|
converse = model_data['converse'] || {}
|
|
101
101
|
capabilities << 'function_calling' if converse.is_a?(Hash)
|
|
102
102
|
capabilities << 'reasoning' if converse.dig('reasoningSupported', 'embedded')
|
|
103
|
+
capabilities << 'structured_output' if supports_structured_output?(model_data['modelId'])
|
|
103
104
|
|
|
104
105
|
capabilities
|
|
105
106
|
end
|
|
106
107
|
|
|
108
|
+
# Structured output supported on Claude 4.5+ and assumed for future major versions.
|
|
109
|
+
# Bedrock IDs look like: us.anthropic.claude-haiku-4-5-20251001-v1:0
|
|
110
|
+
# Must handle optional region prefix (us./eu./global.) and anthropic. prefix.
|
|
111
|
+
def supports_structured_output?(model_id)
|
|
112
|
+
return false unless model_id
|
|
113
|
+
|
|
114
|
+
normalized = model_id.sub(/\A(?:#{REGION_PREFIXES.join('|')})\./, '').delete_prefix('anthropic.')
|
|
115
|
+
match = normalized.match(/claude-(?:opus|sonnet|haiku)-(\d+)-(\d{1,2})(?:\b|-)/)
|
|
116
|
+
return false unless match
|
|
117
|
+
|
|
118
|
+
major = match[1].to_i
|
|
119
|
+
minor = match[2].to_i
|
|
120
|
+
major > 4 || (major == 4 && minor >= 5)
|
|
121
|
+
end
|
|
122
|
+
|
|
107
123
|
def reasoning_embedded?(model)
|
|
108
124
|
metadata = RubyLLM::Utils.deep_symbolize_keys(model.metadata || {})
|
|
109
125
|
converse = metadata[:converse] || {}
|
|
@@ -38,7 +38,7 @@ module RubyLLM
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
message = accumulator.to_message(response)
|
|
41
|
-
RubyLLM.logger.debug "Stream completed: #{message.content}"
|
|
41
|
+
RubyLLM.logger.debug { "Stream completed: #{message.content}" }
|
|
42
42
|
message
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -56,7 +56,7 @@ module RubyLLM
|
|
|
56
56
|
error_response = env.merge(body: data)
|
|
57
57
|
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
|
58
58
|
rescue JSON::ParserError
|
|
59
|
-
RubyLLM.logger.debug "Failed Bedrock stream error chunk: #{chunk}"
|
|
59
|
+
RubyLLM.logger.debug { "Failed Bedrock stream error chunk: #{chunk}" }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def parse_stream_chunk(decoder, raw_chunk, accumulator)
|
|
@@ -100,7 +100,11 @@ module RubyLLM
|
|
|
100
100
|
|
|
101
101
|
while message
|
|
102
102
|
event = decode_event_payload(message.payload.read)
|
|
103
|
-
|
|
103
|
+
if event && RubyLLM.config.log_stream_debug
|
|
104
|
+
RubyLLM.logger.debug do
|
|
105
|
+
"Bedrock stream event keys: #{event.keys}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
104
108
|
events << event if event
|
|
105
109
|
break if eof
|
|
106
110
|
|
|
@@ -119,7 +123,7 @@ module RubyLLM
|
|
|
119
123
|
outer
|
|
120
124
|
end
|
|
121
125
|
rescue JSON::ParserError => e
|
|
122
|
-
RubyLLM.logger.debug "Failed to decode Bedrock stream event payload: #{e.message}"
|
|
126
|
+
RubyLLM.logger.debug { "Failed to decode Bedrock stream event payload: #{e.message}" }
|
|
123
127
|
nil
|
|
124
128
|
end
|
|
125
129
|
|