ruby_llm 1.12.1 → 1.13.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 +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +1 -1
- data/lib/generators/ruby_llm/generator_helpers.rb +4 -0
- data/lib/generators/ruby_llm/install/install_generator.rb +5 -4
- 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 +1 -6
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +8 -2
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +29 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +35 -4
- data/lib/ruby_llm/agent.rb +33 -5
- data/lib/ruby_llm/aliases.json +19 -9
- data/lib/ruby_llm/chat.rb +107 -11
- data/lib/ruby_llm/configuration.rb +18 -0
- data/lib/ruby_llm/connection.rb +10 -4
- data/lib/ruby_llm/content.rb +0 -2
- data/lib/ruby_llm/error.rb +32 -1
- data/lib/ruby_llm/message.rb +5 -3
- data/lib/ruby_llm/model/info.rb +1 -1
- data/lib/ruby_llm/models.json +3535 -2894
- data/lib/ruby_llm/models.rb +5 -3
- data/lib/ruby_llm/provider.rb +5 -1
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +22 -4
- data/lib/ruby_llm/providers/anthropic/chat.rb +22 -5
- data/lib/ruby_llm/providers/anthropic/models.rb +1 -1
- data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
- data/lib/ruby_llm/providers/anthropic.rb +1 -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 +88 -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 +5 -1
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +8 -0
- data/lib/ruby_llm/providers/deepseek.rb +1 -1
- data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +19 -4
- data/lib/ruby_llm/providers/gemini/images.rb +1 -1
- data/lib/ruby_llm/providers/gemini/streaming.rb +1 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +19 -0
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/gpustack.rb +4 -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/ollama/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/ollama.rb +7 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +10 -2
- 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/temperature.rb +2 -2
- data/lib/ruby_llm/providers/openai/tools.rb +27 -2
- 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 +31 -1
- data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +14 -6
- data/lib/ruby_llm/stream_accumulator.rb +10 -5
- data/lib/ruby_llm/streaming.rb +6 -6
- 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 +7 -0
- data/lib/tasks/vcr.rake +1 -1
- metadata +4 -1
data/lib/ruby_llm/models.rb
CHANGED
|
@@ -266,8 +266,8 @@ module RubyLLM
|
|
|
266
266
|
end
|
|
267
267
|
|
|
268
268
|
def index_by_key(models)
|
|
269
|
-
models.
|
|
270
|
-
|
|
269
|
+
models.to_h do |model|
|
|
270
|
+
["#{model.provider}:#{model.id}", model]
|
|
271
271
|
end
|
|
272
272
|
end
|
|
273
273
|
|
|
@@ -312,12 +312,14 @@ module RubyLLM
|
|
|
312
312
|
modalities = normalize_models_dev_modalities(model_data[:modalities])
|
|
313
313
|
capabilities = models_dev_capabilities(model_data, modalities)
|
|
314
314
|
|
|
315
|
+
created_date = model_data[:release_date] || model_data[:last_updated]
|
|
316
|
+
|
|
315
317
|
data = {
|
|
316
318
|
id: model_data[:id],
|
|
317
319
|
name: model_data[:name] || model_data[:id],
|
|
318
320
|
provider: provider_slug,
|
|
319
321
|
family: model_data[:family],
|
|
320
|
-
created_at:
|
|
322
|
+
created_at: "#{created_date} 00:00:00 UTC",
|
|
321
323
|
context_window: model_data.dig(:limit, :context),
|
|
322
324
|
max_output_tokens: model_data.dig(:limit, :output),
|
|
323
325
|
knowledge_cutoff: normalize_models_dev_knowledge(model_data[:knowledge]),
|
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
|
|
@@ -31,11 +31,28 @@ module RubyLLM
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def supports_functions?(model_id)
|
|
34
|
-
model_id.match?(/claude-
|
|
34
|
+
!model_id.match?(/claude-[12]/)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def supports_tool_choice?(_model_id)
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def supports_tool_parallel_control?(_model_id)
|
|
42
|
+
true
|
|
35
43
|
end
|
|
36
44
|
|
|
37
45
|
def supports_json_mode?(model_id)
|
|
38
|
-
model_id.match?(/claude-
|
|
46
|
+
!model_id.match?(/claude-[12]/)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def supports_structured_output?(model_id)
|
|
50
|
+
match = model_id.match(/claude-(?:sonnet|opus|haiku)-(\d+)-(\d+)/)
|
|
51
|
+
return false unless match
|
|
52
|
+
|
|
53
|
+
major = match[1].to_i
|
|
54
|
+
minor = match[2].to_i
|
|
55
|
+
major > 4 || (major == 4 && minor >= 5)
|
|
39
56
|
end
|
|
40
57
|
|
|
41
58
|
def supports_extended_thinking?(model_id)
|
|
@@ -92,12 +109,13 @@ module RubyLLM
|
|
|
92
109
|
def capabilities_for(model_id)
|
|
93
110
|
capabilities = ['streaming']
|
|
94
111
|
|
|
95
|
-
|
|
112
|
+
unless model_id.match?(/claude-[12]/)
|
|
96
113
|
capabilities << 'function_calling'
|
|
97
114
|
capabilities << 'batch'
|
|
98
115
|
end
|
|
99
116
|
|
|
100
|
-
capabilities << '
|
|
117
|
+
capabilities << 'structured_output' if supports_structured_output?(model_id)
|
|
118
|
+
capabilities << 'reasoning' if model_id.match?(/claude-3-7-sonnet|claude-(?:sonnet|opus|haiku)-4/)
|
|
101
119
|
capabilities << 'citations' if model_id.match?(/claude-3\.5|claude-3-7/)
|
|
102
120
|
capabilities
|
|
103
121
|
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)
|
|
@@ -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
|
|
@@ -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,6 +28,21 @@ 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
|
|
29
47
|
def configuration_requirements
|
|
30
48
|
%i[azure_api_base]
|
|
@@ -51,6 +69,76 @@ module RubyLLM
|
|
|
51
69
|
raise ConfigurationError,
|
|
52
70
|
"Missing configuration for Azure: #{missing.join(', ')}"
|
|
53
71
|
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def azure_base_parts
|
|
76
|
+
@azure_base_parts ||= begin
|
|
77
|
+
raw_base = api_base.to_s.sub(%r{/+\z}, '')
|
|
78
|
+
version = raw_base[/[?&]api-version=([^&]+)/i, 1]
|
|
79
|
+
path_base = raw_base.sub(/\?.*\z/, '')
|
|
80
|
+
|
|
81
|
+
mode = if path_base.include?('/chat/completions')
|
|
82
|
+
:chat_endpoint
|
|
83
|
+
elsif path_base.include?('/openai/deployments/')
|
|
84
|
+
:deployment_base
|
|
85
|
+
elsif path_base.include?('/openai/v1')
|
|
86
|
+
:openai_v1_base
|
|
87
|
+
else
|
|
88
|
+
:resource_base
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
raw_base: raw_base,
|
|
93
|
+
path_base: path_base,
|
|
94
|
+
root: azure_host_root(path_base),
|
|
95
|
+
mode: mode,
|
|
96
|
+
version: version
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def chat_endpoint(parts)
|
|
102
|
+
case parts[:mode]
|
|
103
|
+
when :chat_endpoint
|
|
104
|
+
''
|
|
105
|
+
when :deployment_base
|
|
106
|
+
with_api_version('chat/completions', parts[:version] || AZURE_DEFAULT_CHAT_API_VERSION)
|
|
107
|
+
when :openai_v1_base
|
|
108
|
+
with_api_version('chat/completions', parts[:version])
|
|
109
|
+
else
|
|
110
|
+
with_api_version('models/chat/completions', parts[:version] || AZURE_DEFAULT_CHAT_API_VERSION)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def embeddings_endpoint(parts)
|
|
115
|
+
case parts[:mode]
|
|
116
|
+
when :deployment_base, :openai_v1_base
|
|
117
|
+
with_api_version('embeddings', parts[:version])
|
|
118
|
+
else
|
|
119
|
+
"#{parts[:root]}/openai/v1/embeddings"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def models_endpoint(parts)
|
|
124
|
+
case parts[:mode]
|
|
125
|
+
when :openai_v1_base
|
|
126
|
+
with_api_version('models', parts[:version] || AZURE_DEFAULT_MODELS_API_VERSION)
|
|
127
|
+
else
|
|
128
|
+
"#{parts[:root]}/openai/v1/models?api-version=#{parts[:version] || AZURE_DEFAULT_MODELS_API_VERSION}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def with_api_version(path, version)
|
|
133
|
+
return path unless version
|
|
134
|
+
|
|
135
|
+
separator = path.include?('?') ? '&' : '?'
|
|
136
|
+
"#{path}#{separator}api-version=#{version}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def azure_host_root(base_without_query)
|
|
140
|
+
base_without_query.sub(%r{/(models|openai)/.*\z}, '').sub(%r{/+\z}, '')
|
|
141
|
+
end
|
|
54
142
|
end
|
|
55
143
|
end
|
|
56
144
|
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
|
|
|
@@ -18,12 +18,15 @@ module RubyLLM
|
|
|
18
18
|
{}
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
# rubocop:disable Metrics/ParameterLists
|
|
22
|
+
def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
|
|
23
|
+
tool_prefs: nil, &)
|
|
22
24
|
normalized_params = normalize_params(params, model:)
|
|
23
25
|
|
|
24
26
|
super(
|
|
25
27
|
messages,
|
|
26
28
|
tools: tools,
|
|
29
|
+
tool_prefs: tool_prefs,
|
|
27
30
|
temperature: temperature,
|
|
28
31
|
model: model,
|
|
29
32
|
params: normalized_params,
|
|
@@ -33,6 +36,7 @@ module RubyLLM
|
|
|
33
36
|
&
|
|
34
37
|
)
|
|
35
38
|
end
|
|
39
|
+
# rubocop:enable Metrics/ParameterLists
|
|
36
40
|
|
|
37
41
|
def parse_error(response)
|
|
38
42
|
return if response.body.nil? || response.body.empty?
|
|
@@ -62,6 +62,14 @@ module RubyLLM
|
|
|
62
62
|
model_id.match?(/gemini|pro|flash/)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
def supports_tool_choice?(_model_id)
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def supports_tool_parallel_control?(_model_id)
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
|
|
65
73
|
def supports_json_mode?(model_id)
|
|
66
74
|
if model_id.match?(/text-embedding|embedding-001|aqa|imagen|gemini-2\.0-flash-lite|gemini-2\.5-pro-exp-03-25/)
|
|
67
75
|
return false
|
|
@@ -14,7 +14,10 @@ module RubyLLM
|
|
|
14
14
|
"models/#{@model}:generateContent"
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
# rubocop:disable Metrics/ParameterLists,Lint/UnusedMethodArgument
|
|
18
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil,
|
|
19
|
+
thinking: nil, tool_prefs: nil)
|
|
20
|
+
tool_prefs ||= {}
|
|
18
21
|
@model = model.id
|
|
19
22
|
payload = {
|
|
20
23
|
contents: format_messages(messages),
|
|
@@ -26,9 +29,15 @@ module RubyLLM
|
|
|
26
29
|
payload[:generationConfig].merge!(structured_output_config(schema, model)) if schema
|
|
27
30
|
payload[:generationConfig][:thinkingConfig] = build_thinking_config(model, thinking) if thinking&.enabled?
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
if tools.any?
|
|
33
|
+
payload[:tools] = format_tools(tools)
|
|
34
|
+
# Gemini doesn't support controlling parallel tool calls
|
|
35
|
+
payload[:toolConfig] = build_tool_config(tool_prefs[:choice]) unless tool_prefs[:choice].nil?
|
|
36
|
+
end
|
|
37
|
+
|
|
30
38
|
payload
|
|
31
39
|
end
|
|
40
|
+
# rubocop:enable Metrics/ParameterLists,Lint/UnusedMethodArgument
|
|
32
41
|
|
|
33
42
|
def build_thinking_config(_model, thinking)
|
|
34
43
|
config = { includeThoughts: true }
|
|
@@ -120,6 +129,9 @@ module RubyLLM
|
|
|
120
129
|
def convert_schema_to_gemini(schema)
|
|
121
130
|
return nil unless schema
|
|
122
131
|
|
|
132
|
+
# Extract inner schema if wrapper format (e.g., from RubyLLM::Schema.to_json_schema)
|
|
133
|
+
schema = schema[:schema] || schema
|
|
134
|
+
|
|
123
135
|
GeminiSchema.new(schema).to_h
|
|
124
136
|
end
|
|
125
137
|
|
|
@@ -132,7 +144,10 @@ module RubyLLM
|
|
|
132
144
|
parts = candidate.dig('content', 'parts')
|
|
133
145
|
return '' unless parts&.any?
|
|
134
146
|
|
|
135
|
-
|
|
147
|
+
non_thought_parts = parts.reject { |part| part['thought'] }
|
|
148
|
+
return '' unless non_thought_parts.any?
|
|
149
|
+
|
|
150
|
+
build_response_content(non_thought_parts)
|
|
136
151
|
end
|
|
137
152
|
|
|
138
153
|
def extract_text_parts(parts)
|
|
@@ -176,7 +191,7 @@ module RubyLLM
|
|
|
176
191
|
end
|
|
177
192
|
|
|
178
193
|
def build_json_schema(schema)
|
|
179
|
-
normalized = RubyLLM::Utils.deep_dup(schema)
|
|
194
|
+
normalized = RubyLLM::Utils.deep_dup(schema[:schema])
|
|
180
195
|
normalized.delete(:strict)
|
|
181
196
|
normalized.delete('strict')
|
|
182
197
|
RubyLLM::Utils.deep_stringify_keys(normalized)
|
|
@@ -10,7 +10,7 @@ module RubyLLM
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def render_image_payload(prompt, model:, size:)
|
|
13
|
-
RubyLLM.logger.debug "Ignoring size #{size}. Gemini does not support image size customization."
|
|
13
|
+
RubyLLM.logger.debug { "Ignoring size #{size}. Gemini does not support image size customization." }
|
|
14
14
|
@model = model
|
|
15
15
|
{
|
|
16
16
|
instances: [
|
|
@@ -83,7 +83,7 @@ module RubyLLM
|
|
|
83
83
|
error_data = JSON.parse(data)
|
|
84
84
|
[error_data['error']['code'], error_data['error']['message']]
|
|
85
85
|
rescue JSON::ParserError => e
|
|
86
|
-
RubyLLM.logger.debug "Failed to parse streaming error: #{e.message}"
|
|
86
|
+
RubyLLM.logger.debug { "Failed to parse streaming error: #{e.message}" }
|
|
87
87
|
[500, "Failed to parse error: #{data}"]
|
|
88
88
|
end
|
|
89
89
|
end
|