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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: efa046d43a24a45287ca68b9ec54c12374dd7a716a41bda2226974925067f547
|
|
4
|
+
data.tar.gz: e7d5187b9cb8d543e8b3b6b2590569cdcedec745f7ff35b6944d68768f803a6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 61a499df157a4276b21aaaebab88518ad8a57a497dc032307bce6a02522ae45eccad01e7989ae1dfafb7149502eb5878d1604cba99ab3f4e2c0a34ef33e43c56
|
|
7
|
+
data.tar.gz: e2a03066ca8fdc265316bff01a9afed4c16a8e9bb32d7ed7f05bb594a00b3897c621039800b7fa8a75eb729b4511af133e240fdd054065d9aa82617899664595
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@ class <%= chat_job_class_name %> < ApplicationJob
|
|
|
3
3
|
<%= chat_variable_name %> = <%= chat_model_name %>.find(<%= chat_variable_name %>_id)
|
|
4
4
|
|
|
5
5
|
<%= chat_variable_name %>.ask(content) do |chunk|
|
|
6
|
-
if chunk.content && !chunk.content.
|
|
6
|
+
if chunk.content && !chunk.content.empty?
|
|
7
7
|
<%= message_variable_name %> = <%= chat_variable_name %>.<%= message_table_name %>.last
|
|
8
8
|
<%= message_variable_name %>.broadcast_append_chunk(chunk.content)
|
|
9
9
|
end
|
|
@@ -121,6 +121,10 @@ module RubyLLM
|
|
|
121
121
|
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
+
def create_migration_class_name(table_name)
|
|
125
|
+
"create_#{table_name}".camelize
|
|
126
|
+
end
|
|
127
|
+
|
|
124
128
|
def postgresql?
|
|
125
129
|
::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
|
|
126
130
|
rescue StandardError
|
|
@@ -77,12 +77,13 @@ module RubyLLM
|
|
|
77
77
|
|
|
78
78
|
say "\n Next steps:", :yellow
|
|
79
79
|
say ' 1. Run: rails db:migrate'
|
|
80
|
-
say ' 2.
|
|
80
|
+
say ' 2. Run: rails ruby_llm:load_models'
|
|
81
|
+
say ' 3. Set your API keys in config/initializers/ruby_llm.rb'
|
|
81
82
|
|
|
82
|
-
say "
|
|
83
|
+
say " 4. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
|
|
83
84
|
|
|
84
|
-
say "\n 🚀 Model registry
|
|
85
|
-
say ' Models
|
|
85
|
+
say "\n 🚀 Model registry supports database + JSON fallback!", :cyan
|
|
86
|
+
say ' Models load from database when present, otherwise from models.json'
|
|
86
87
|
say ' Pass model names as strings - RubyLLM handles the rest!'
|
|
87
88
|
say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
|
|
88
89
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class
|
|
1
|
+
class <%= create_migration_class_name(chat_table_name) %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
2
|
def change
|
|
3
3
|
create_table :<%= chat_table_name %> do |t|
|
|
4
4
|
t.timestamps
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class
|
|
1
|
+
class <%= create_migration_class_name(message_table_name) %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
2
|
def change
|
|
3
3
|
create_table :<%= message_table_name %> do |t|
|
|
4
4
|
t.string :role, null: false
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class
|
|
1
|
+
class <%= create_migration_class_name(model_table_name) %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
2
|
def change
|
|
3
3
|
create_table :<%= model_table_name %> do |t|
|
|
4
4
|
t.string :model_id, null: false
|
|
@@ -36,10 +36,5 @@ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Mig
|
|
|
36
36
|
<% end %>
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# Load models from JSON
|
|
40
|
-
say_with_time "Loading models from models.json" do
|
|
41
|
-
RubyLLM.models.load_from_json!
|
|
42
|
-
<%= model_model_name %>.save_to_database
|
|
43
|
-
end
|
|
44
39
|
end
|
|
45
40
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
|
|
2
|
-
class
|
|
2
|
+
class <%= create_migration_class_name(tool_call_table_name) %> < ActiveRecord::Migration<%= migration_version %>
|
|
3
3
|
def change
|
|
4
4
|
create_table :<%= tool_call_table_name %> do |t|
|
|
5
5
|
t.string :tool_call_id, null: false
|
|
@@ -12,15 +12,21 @@ module RubyLLM
|
|
|
12
12
|
# Monkey-patch Models to use database when ActsAs is active
|
|
13
13
|
RubyLLM::Models.class_eval do
|
|
14
14
|
def self.load_models
|
|
15
|
-
read_from_database
|
|
15
|
+
database_models = read_from_database
|
|
16
|
+
return database_models if database_models.any?
|
|
17
|
+
|
|
18
|
+
RubyLLM.logger.debug { 'Model registry is empty in database, falling back to JSON registry' }
|
|
19
|
+
read_from_json
|
|
16
20
|
rescue StandardError => e
|
|
17
|
-
RubyLLM.logger.debug "Failed to load models from database: #{e.message}, falling back to JSON"
|
|
21
|
+
RubyLLM.logger.debug { "Failed to load models from database: #{e.message}, falling back to JSON" }
|
|
18
22
|
read_from_json
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
def self.read_from_database
|
|
22
26
|
model_class = RubyLLM.config.model_registry_class
|
|
23
27
|
model_class = model_class.constantize if model_class.is_a?(String)
|
|
28
|
+
return [] unless model_class.table_exists?
|
|
29
|
+
|
|
24
30
|
model_class.all.map(&:to_llm)
|
|
25
31
|
end
|
|
26
32
|
|
|
@@ -97,6 +97,7 @@ module RubyLLM
|
|
|
97
97
|
ordered_messages.each do |msg|
|
|
98
98
|
@chat.add_message(msg.to_llm)
|
|
99
99
|
end
|
|
100
|
+
reapply_runtime_instructions(@chat)
|
|
100
101
|
|
|
101
102
|
setup_persistence_callbacks
|
|
102
103
|
end
|
|
@@ -109,6 +110,14 @@ module RubyLLM
|
|
|
109
110
|
self
|
|
110
111
|
end
|
|
111
112
|
|
|
113
|
+
def with_runtime_instructions(instructions, append: false, replace: nil)
|
|
114
|
+
append = append_instructions?(append:, replace:)
|
|
115
|
+
store_runtime_instruction(instructions, append:)
|
|
116
|
+
|
|
117
|
+
to_llm.with_instructions(instructions, append:, replace:)
|
|
118
|
+
self
|
|
119
|
+
end
|
|
120
|
+
|
|
112
121
|
def with_tool(...)
|
|
113
122
|
to_llm.with_tool(...)
|
|
114
123
|
self
|
|
@@ -285,6 +294,26 @@ module RubyLLM
|
|
|
285
294
|
system_messages + non_system_messages
|
|
286
295
|
end
|
|
287
296
|
|
|
297
|
+
def runtime_instructions
|
|
298
|
+
@runtime_instructions ||= []
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def store_runtime_instruction(instructions, append:)
|
|
302
|
+
if append
|
|
303
|
+
runtime_instructions << instructions
|
|
304
|
+
else
|
|
305
|
+
@runtime_instructions = [instructions]
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def reapply_runtime_instructions(chat)
|
|
310
|
+
return if runtime_instructions.empty?
|
|
311
|
+
|
|
312
|
+
first, *rest = runtime_instructions
|
|
313
|
+
chat.with_instructions(first)
|
|
314
|
+
rest.each { |instruction| chat.with_instructions(instruction, append: true) }
|
|
315
|
+
end
|
|
316
|
+
|
|
288
317
|
def setup_persistence_callbacks
|
|
289
318
|
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
|
290
319
|
|
|
@@ -79,7 +79,8 @@ module RubyLLM
|
|
|
79
79
|
model_record = model_association
|
|
80
80
|
@chat ||= (context || RubyLLM).chat(
|
|
81
81
|
model: model_record.model_id,
|
|
82
|
-
provider: model_record.provider.to_sym
|
|
82
|
+
provider: model_record.provider.to_sym,
|
|
83
|
+
assume_model_exists: assume_model_exists || false
|
|
83
84
|
)
|
|
84
85
|
@chat.reset_messages!
|
|
85
86
|
|
|
@@ -87,6 +88,7 @@ module RubyLLM
|
|
|
87
88
|
ordered_messages.each do |msg|
|
|
88
89
|
@chat.add_message(msg.to_llm)
|
|
89
90
|
end
|
|
91
|
+
reapply_runtime_instructions(@chat)
|
|
90
92
|
|
|
91
93
|
setup_persistence_callbacks
|
|
92
94
|
end
|
|
@@ -99,6 +101,14 @@ module RubyLLM
|
|
|
99
101
|
self
|
|
100
102
|
end
|
|
101
103
|
|
|
104
|
+
def with_runtime_instructions(instructions, append: false, replace: nil)
|
|
105
|
+
append = append_instructions?(append:, replace:)
|
|
106
|
+
store_runtime_instruction(instructions, append:)
|
|
107
|
+
|
|
108
|
+
to_llm.with_instructions(instructions, append:, replace:)
|
|
109
|
+
self
|
|
110
|
+
end
|
|
111
|
+
|
|
102
112
|
def with_tool(...)
|
|
103
113
|
to_llm.with_tool(...)
|
|
104
114
|
self
|
|
@@ -233,9 +243,10 @@ module RubyLLM
|
|
|
233
243
|
if last.tool_call?
|
|
234
244
|
last.destroy
|
|
235
245
|
elsif last.tool_result?
|
|
236
|
-
tool_call_message = last.parent_tool_call.
|
|
237
|
-
expected_results = tool_call_message.
|
|
238
|
-
|
|
246
|
+
tool_call_message = last.parent_tool_call.message_association
|
|
247
|
+
expected_results = tool_call_message.tool_calls_association.pluck(:id)
|
|
248
|
+
fk_column = tool_call_message.class.reflections['tool_results'].foreign_key
|
|
249
|
+
actual_results = tool_call_message.tool_results.pluck(fk_column)
|
|
239
250
|
|
|
240
251
|
if expected_results.sort != actual_results.sort
|
|
241
252
|
tool_call_message.tool_results.each(&:destroy)
|
|
@@ -288,6 +299,26 @@ module RubyLLM
|
|
|
288
299
|
system_messages + non_system_messages
|
|
289
300
|
end
|
|
290
301
|
|
|
302
|
+
def runtime_instructions
|
|
303
|
+
@runtime_instructions ||= []
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def store_runtime_instruction(instructions, append:)
|
|
307
|
+
if append
|
|
308
|
+
runtime_instructions << instructions
|
|
309
|
+
else
|
|
310
|
+
@runtime_instructions = [instructions]
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def reapply_runtime_instructions(chat)
|
|
315
|
+
return if runtime_instructions.empty?
|
|
316
|
+
|
|
317
|
+
first, *rest = runtime_instructions
|
|
318
|
+
chat.with_instructions(first)
|
|
319
|
+
rest.each { |instruction| chat.with_instructions(instruction, append: true) }
|
|
320
|
+
end
|
|
321
|
+
|
|
291
322
|
def persist_new_message
|
|
292
323
|
@message = messages_association.create!(role: :assistant, content: '')
|
|
293
324
|
end
|
data/lib/ruby_llm/agent.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'erb'
|
|
4
4
|
require 'forwardable'
|
|
5
5
|
require 'pathname'
|
|
6
|
+
require 'ruby_llm/schema'
|
|
6
7
|
|
|
7
8
|
module RubyLLM
|
|
8
9
|
# Base class for simple, class-configured agents.
|
|
@@ -138,7 +139,10 @@ module RubyLLM
|
|
|
138
139
|
|
|
139
140
|
def render_prompt(name, chat:, inputs:, locals:)
|
|
140
141
|
path = prompt_path_for(name)
|
|
141
|
-
|
|
142
|
+
unless File.exist?(path)
|
|
143
|
+
raise RubyLLM::PromptNotFoundError,
|
|
144
|
+
"Prompt file not found for #{self}: #{path}. Create the file or use inline instructions."
|
|
145
|
+
end
|
|
142
146
|
|
|
143
147
|
resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
|
|
144
148
|
ERB.new(File.read(path)).result_with_hash(resolved_locals)
|
|
@@ -177,7 +181,10 @@ module RubyLLM
|
|
|
177
181
|
value = resolved_instructions_value(chat_object, runtime, inputs:)
|
|
178
182
|
return if value.nil?
|
|
179
183
|
|
|
180
|
-
instruction_target(chat_object, persist:)
|
|
184
|
+
target = instruction_target(chat_object, persist:)
|
|
185
|
+
return target.with_runtime_instructions(value) if use_runtime_instructions?(target, persist:)
|
|
186
|
+
|
|
187
|
+
target.with_instructions(value)
|
|
181
188
|
end
|
|
182
189
|
|
|
183
190
|
def apply_tools(llm_chat, runtime)
|
|
@@ -204,10 +211,21 @@ module RubyLLM
|
|
|
204
211
|
end
|
|
205
212
|
|
|
206
213
|
def apply_schema(llm_chat, runtime)
|
|
207
|
-
value =
|
|
214
|
+
value = resolved_schema_value(runtime)
|
|
208
215
|
llm_chat.with_schema(value) if value
|
|
209
216
|
end
|
|
210
217
|
|
|
218
|
+
def resolved_schema_value(runtime)
|
|
219
|
+
value = schema
|
|
220
|
+
return value unless value.is_a?(Proc)
|
|
221
|
+
|
|
222
|
+
evaluate(value, runtime)
|
|
223
|
+
rescue NoMethodError => e
|
|
224
|
+
raise unless e.receiver.equal?(runtime)
|
|
225
|
+
|
|
226
|
+
RubyLLM::Schema.create(&value)
|
|
227
|
+
end
|
|
228
|
+
|
|
211
229
|
def llm_chat_for(chat_object)
|
|
212
230
|
chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
|
|
213
231
|
end
|
|
@@ -217,7 +235,7 @@ module RubyLLM
|
|
|
217
235
|
end
|
|
218
236
|
|
|
219
237
|
def resolved_instructions_value(chat_object, runtime, inputs:)
|
|
220
|
-
value = evaluate(instructions, runtime)
|
|
238
|
+
value = evaluate(@instructions, runtime)
|
|
221
239
|
return value unless prompt_instruction?(value)
|
|
222
240
|
|
|
223
241
|
runtime.prompt(
|
|
@@ -234,10 +252,20 @@ module RubyLLM
|
|
|
234
252
|
if persist || !chat_object.respond_to?(:to_llm)
|
|
235
253
|
chat_object
|
|
236
254
|
else
|
|
237
|
-
chat_object
|
|
255
|
+
runtime_instruction_target(chat_object)
|
|
238
256
|
end
|
|
239
257
|
end
|
|
240
258
|
|
|
259
|
+
def runtime_instruction_target(chat_object)
|
|
260
|
+
return chat_object if chat_object.respond_to?(:with_runtime_instructions)
|
|
261
|
+
|
|
262
|
+
chat_object.to_llm
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def use_runtime_instructions?(target, persist:)
|
|
266
|
+
!persist && target.respond_to?(:with_runtime_instructions)
|
|
267
|
+
end
|
|
268
|
+
|
|
241
269
|
def resolve_prompt_locals(locals, runtime:, chat:, inputs:)
|
|
242
270
|
base = { chat: chat }.merge(inputs)
|
|
243
271
|
evaluated = locals.each_with_object({}) do |(key, value), acc|
|
data/lib/ruby_llm/aliases.json
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"chatgpt-4o": {
|
|
3
|
-
"openai": "chatgpt-4o-latest",
|
|
4
|
-
"openrouter": "openai/chatgpt-4o-latest"
|
|
5
|
-
},
|
|
6
2
|
"claude-3-5-haiku": {
|
|
7
3
|
"anthropic": "claude-3-5-haiku-20241022",
|
|
8
4
|
"openrouter": "anthropic/claude-3.5-haiku",
|
|
@@ -31,7 +27,7 @@
|
|
|
31
27
|
},
|
|
32
28
|
"claude-3-opus": {
|
|
33
29
|
"anthropic": "claude-3-opus-20240229",
|
|
34
|
-
"bedrock": "anthropic.claude-3-opus-20240229-v1:0
|
|
30
|
+
"bedrock": "anthropic.claude-3-opus-20240229-v1:0"
|
|
35
31
|
},
|
|
36
32
|
"claude-3-sonnet": {
|
|
37
33
|
"anthropic": "claude-3-sonnet-20240229",
|
|
@@ -82,6 +78,11 @@
|
|
|
82
78
|
"bedrock": "anthropic.claude-sonnet-4-5-20250929-v1:0",
|
|
83
79
|
"azure": "claude-sonnet-4-5-20250929"
|
|
84
80
|
},
|
|
81
|
+
"claude-sonnet-4-6": {
|
|
82
|
+
"anthropic": "claude-sonnet-4-6",
|
|
83
|
+
"openrouter": "anthropic/claude-sonnet-4.6",
|
|
84
|
+
"bedrock": "anthropic.claude-sonnet-4-6"
|
|
85
|
+
},
|
|
85
86
|
"deepseek-chat": {
|
|
86
87
|
"deepseek": "deepseek-chat",
|
|
87
88
|
"openrouter": "deepseek/deepseek-chat"
|
|
@@ -181,14 +182,19 @@
|
|
|
181
182
|
"openrouter": "google/gemini-3-pro-preview",
|
|
182
183
|
"vertexai": "gemini-3-pro-preview"
|
|
183
184
|
},
|
|
185
|
+
"gemini-3.1-pro-preview": {
|
|
186
|
+
"gemini": "gemini-3.1-pro-preview",
|
|
187
|
+
"openrouter": "google/gemini-3.1-pro-preview",
|
|
188
|
+
"vertexai": "gemini-3.1-pro-preview"
|
|
189
|
+
},
|
|
190
|
+
"gemini-3.1-pro-preview-customtools": {
|
|
191
|
+
"gemini": "gemini-3.1-pro-preview-customtools",
|
|
192
|
+
"vertexai": "gemini-3.1-pro-preview-customtools"
|
|
193
|
+
},
|
|
184
194
|
"gemini-embedding-001": {
|
|
185
195
|
"gemini": "gemini-embedding-001",
|
|
186
196
|
"vertexai": "gemini-embedding-001"
|
|
187
197
|
},
|
|
188
|
-
"gemini-exp-1206": {
|
|
189
|
-
"gemini": "gemini-exp-1206",
|
|
190
|
-
"vertexai": "gemini-exp-1206"
|
|
191
|
-
},
|
|
192
198
|
"gemini-flash": {
|
|
193
199
|
"gemini": "gemini-flash-latest",
|
|
194
200
|
"vertexai": "gemini-flash-latest"
|
|
@@ -347,6 +353,10 @@
|
|
|
347
353
|
"openai": "gpt-5.2-pro",
|
|
348
354
|
"openrouter": "openai/gpt-5.2-pro"
|
|
349
355
|
},
|
|
356
|
+
"gpt-5.3-codex": {
|
|
357
|
+
"openai": "gpt-5.3-codex",
|
|
358
|
+
"openrouter": "openai/gpt-5.3-codex"
|
|
359
|
+
},
|
|
350
360
|
"gpt-audio": {
|
|
351
361
|
"openai": "gpt-audio",
|
|
352
362
|
"openrouter": "openai/gpt-audio"
|
data/lib/ruby_llm/chat.rb
CHANGED
|
@@ -5,7 +5,7 @@ module RubyLLM
|
|
|
5
5
|
class Chat
|
|
6
6
|
include Enumerable
|
|
7
7
|
|
|
8
|
-
attr_reader :model, :messages, :tools, :params, :headers, :schema
|
|
8
|
+
attr_reader :model, :messages, :tools, :tool_prefs, :params, :headers, :schema
|
|
9
9
|
|
|
10
10
|
def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
|
|
11
11
|
if assume_model_exists && !provider
|
|
@@ -19,6 +19,7 @@ module RubyLLM
|
|
|
19
19
|
@temperature = nil
|
|
20
20
|
@messages = []
|
|
21
21
|
@tools = {}
|
|
22
|
+
@tool_prefs = { choice: nil, calls: nil }
|
|
22
23
|
@params = {}
|
|
23
24
|
@headers = {}
|
|
24
25
|
@schema = nil
|
|
@@ -50,15 +51,19 @@ module RubyLLM
|
|
|
50
51
|
self
|
|
51
52
|
end
|
|
52
53
|
|
|
53
|
-
def with_tool(tool)
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
def with_tool(tool, choice: nil, calls: nil)
|
|
55
|
+
unless tool.nil?
|
|
56
|
+
tool_instance = tool.is_a?(Class) ? tool.new : tool
|
|
57
|
+
@tools[tool_instance.name.to_sym] = tool_instance
|
|
58
|
+
end
|
|
59
|
+
update_tool_options(choice:, calls:)
|
|
56
60
|
self
|
|
57
61
|
end
|
|
58
62
|
|
|
59
|
-
def with_tools(*tools, replace: false)
|
|
63
|
+
def with_tools(*tools, replace: false, choice: nil, calls: nil)
|
|
60
64
|
@tools.clear if replace
|
|
61
65
|
tools.compact.each { |tool| with_tool tool }
|
|
66
|
+
update_tool_options(choice:, calls:)
|
|
62
67
|
self
|
|
63
68
|
end
|
|
64
69
|
|
|
@@ -100,12 +105,9 @@ module RubyLLM
|
|
|
100
105
|
def with_schema(schema)
|
|
101
106
|
schema_instance = schema.is_a?(Class) ? schema.new : schema
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
else
|
|
107
|
-
schema_instance
|
|
108
|
-
end
|
|
108
|
+
@schema = normalize_schema_payload(
|
|
109
|
+
schema_instance.respond_to?(:to_json_schema) ? schema_instance.to_json_schema : schema_instance
|
|
110
|
+
)
|
|
109
111
|
|
|
110
112
|
self
|
|
111
113
|
end
|
|
@@ -138,6 +140,7 @@ module RubyLLM
|
|
|
138
140
|
response = @provider.complete(
|
|
139
141
|
messages,
|
|
140
142
|
tools: @tools,
|
|
143
|
+
tool_prefs: @tool_prefs,
|
|
141
144
|
temperature: @temperature,
|
|
142
145
|
model: @model,
|
|
143
146
|
params: @params,
|
|
@@ -183,6 +186,36 @@ module RubyLLM
|
|
|
183
186
|
|
|
184
187
|
private
|
|
185
188
|
|
|
189
|
+
def normalize_schema_payload(raw_schema)
|
|
190
|
+
return nil if raw_schema.nil?
|
|
191
|
+
return raw_schema unless raw_schema.is_a?(Hash)
|
|
192
|
+
|
|
193
|
+
schema = RubyLLM::Utils.deep_symbolize_keys(raw_schema)
|
|
194
|
+
schema_def = extract_schema_definition(schema)
|
|
195
|
+
strict = extract_schema_strict(schema, schema_def)
|
|
196
|
+
build_schema_payload(schema, schema_def, strict)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def extract_schema_definition(schema)
|
|
200
|
+
RubyLLM::Utils.deep_dup(schema[:schema] || schema)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def extract_schema_strict(schema, schema_def)
|
|
204
|
+
return schema[:strict] if schema.key?(:strict)
|
|
205
|
+
return schema_def.delete(:strict) if schema_def.is_a?(Hash)
|
|
206
|
+
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def build_schema_payload(schema, schema_def, strict)
|
|
211
|
+
{
|
|
212
|
+
name: schema[:name] || 'response',
|
|
213
|
+
schema: schema_def,
|
|
214
|
+
strict: strict.nil? || strict,
|
|
215
|
+
description: schema[:description]
|
|
216
|
+
}.compact
|
|
217
|
+
end
|
|
218
|
+
|
|
186
219
|
def wrap_streaming_block(&block)
|
|
187
220
|
return nil unless block_given?
|
|
188
221
|
|
|
@@ -209,15 +242,78 @@ module RubyLLM
|
|
|
209
242
|
halt_result = result if result.is_a?(Tool::Halt)
|
|
210
243
|
end
|
|
211
244
|
|
|
245
|
+
reset_tool_choice if forced_tool_choice?
|
|
212
246
|
halt_result || complete(&)
|
|
213
247
|
end
|
|
214
248
|
|
|
215
249
|
def execute_tool(tool_call)
|
|
216
250
|
tool = tools[tool_call.name.to_sym]
|
|
251
|
+
if tool.nil?
|
|
252
|
+
return {
|
|
253
|
+
error: "Model tried to call unavailable tool `#{tool_call.name}`. " \
|
|
254
|
+
"Available tools: #{tools.keys.to_json}."
|
|
255
|
+
}
|
|
256
|
+
end
|
|
257
|
+
|
|
217
258
|
args = tool_call.arguments
|
|
218
259
|
tool.call(args)
|
|
219
260
|
end
|
|
220
261
|
|
|
262
|
+
def update_tool_options(choice:, calls:)
|
|
263
|
+
unless choice.nil?
|
|
264
|
+
normalized_choice = normalize_tool_choice(choice)
|
|
265
|
+
valid_tool_choices = %i[auto none required] + tools.keys
|
|
266
|
+
unless valid_tool_choices.include?(normalized_choice)
|
|
267
|
+
raise InvalidToolChoiceError,
|
|
268
|
+
"Invalid tool choice: #{choice}. Valid choices are: #{valid_tool_choices.join(', ')}"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
@tool_prefs[:choice] = normalized_choice
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
@tool_prefs[:calls] = normalize_calls(calls) unless calls.nil?
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def normalize_calls(calls)
|
|
278
|
+
case calls
|
|
279
|
+
when :many, 'many'
|
|
280
|
+
:many
|
|
281
|
+
when :one, 'one', 1
|
|
282
|
+
:one
|
|
283
|
+
else
|
|
284
|
+
raise ArgumentError, "Invalid calls value: #{calls.inspect}. Valid values are: :many, :one, or 1"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def normalize_tool_choice(choice)
|
|
289
|
+
return choice.to_sym if choice.is_a?(String) || choice.is_a?(Symbol)
|
|
290
|
+
return tool_name_for_choice_class(choice) if choice.is_a?(Class)
|
|
291
|
+
|
|
292
|
+
choice.respond_to?(:name) ? choice.name.to_sym : choice.to_sym
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def tool_name_for_choice_class(tool_class)
|
|
296
|
+
matched_tool_name = tools.find { |_name, tool| tool.is_a?(tool_class) }&.first
|
|
297
|
+
return matched_tool_name if matched_tool_name
|
|
298
|
+
|
|
299
|
+
classify_tool_name(tool_class.name)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def classify_tool_name(class_name)
|
|
303
|
+
class_name.split('::').last
|
|
304
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
305
|
+
.downcase
|
|
306
|
+
.to_sym
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def forced_tool_choice?
|
|
310
|
+
@tool_prefs[:choice] && !%i[auto none].include?(@tool_prefs[:choice])
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def reset_tool_choice
|
|
314
|
+
@tool_prefs[:choice] = nil
|
|
315
|
+
end
|
|
316
|
+
|
|
221
317
|
def build_content(message, attachments)
|
|
222
318
|
return message if content_like?(message)
|
|
223
319
|
|
|
@@ -11,20 +11,25 @@ module RubyLLM
|
|
|
11
11
|
:azure_api_base,
|
|
12
12
|
:azure_api_key,
|
|
13
13
|
:azure_ai_auth_token,
|
|
14
|
+
:anthropic_api_base,
|
|
14
15
|
:anthropic_api_key,
|
|
15
16
|
:gemini_api_key,
|
|
16
17
|
:gemini_api_base,
|
|
17
18
|
:vertexai_project_id,
|
|
18
19
|
:vertexai_location,
|
|
20
|
+
:vertexai_service_account_key,
|
|
19
21
|
:deepseek_api_key,
|
|
22
|
+
:deepseek_api_base,
|
|
20
23
|
:perplexity_api_key,
|
|
21
24
|
:bedrock_api_key,
|
|
22
25
|
:bedrock_secret_key,
|
|
23
26
|
:bedrock_region,
|
|
24
27
|
:bedrock_session_token,
|
|
28
|
+
:openrouter_api_base,
|
|
25
29
|
:openrouter_api_key,
|
|
26
30
|
:xai_api_key,
|
|
27
31
|
:ollama_api_base,
|
|
32
|
+
:ollama_api_key,
|
|
28
33
|
:gpustack_api_base,
|
|
29
34
|
:gpustack_api_key,
|
|
30
35
|
:mistral_api_key,
|
|
@@ -51,6 +56,7 @@ module RubyLLM
|
|
|
51
56
|
:log_file,
|
|
52
57
|
:log_level,
|
|
53
58
|
:log_stream_debug
|
|
59
|
+
attr_reader :log_regexp_timeout
|
|
54
60
|
|
|
55
61
|
def initialize
|
|
56
62
|
@request_timeout = 300
|
|
@@ -73,10 +79,22 @@ module RubyLLM
|
|
|
73
79
|
@log_file = $stdout
|
|
74
80
|
@log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
|
|
75
81
|
@log_stream_debug = ENV['RUBYLLM_STREAM_DEBUG'] == 'true'
|
|
82
|
+
self.log_regexp_timeout = Regexp.respond_to?(:timeout) ? (Regexp.timeout || 1.0) : nil
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
def instance_variables
|
|
79
86
|
super.reject { |ivar| ivar.to_s.match?(/_id|_key|_secret|_token$/) }
|
|
80
87
|
end
|
|
88
|
+
|
|
89
|
+
def log_regexp_timeout=(value)
|
|
90
|
+
if value.nil?
|
|
91
|
+
@log_regexp_timeout = nil
|
|
92
|
+
elsif Regexp.respond_to?(:timeout)
|
|
93
|
+
@log_regexp_timeout = value
|
|
94
|
+
else
|
|
95
|
+
RubyLLM.logger.warn("log_regexp_timeout is not supported on Ruby #{RUBY_VERSION}")
|
|
96
|
+
@log_regexp_timeout = value
|
|
97
|
+
end
|
|
98
|
+
end
|
|
81
99
|
end
|
|
82
100
|
end
|
data/lib/ruby_llm/connection.rb
CHANGED
|
@@ -65,19 +65,25 @@ module RubyLLM
|
|
|
65
65
|
errors: true,
|
|
66
66
|
headers: false,
|
|
67
67
|
log_level: :debug do |logger|
|
|
68
|
-
logger.filter(
|
|
69
|
-
logger.filter(
|
|
68
|
+
logger.filter(logging_regexp('[A-Za-z0-9+/=]{100,}'), '[BASE64 DATA]')
|
|
69
|
+
logger.filter(logging_regexp('[-\\d.e,\\s]{100,}'), '[EMBEDDINGS ARRAY]')
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
def logging_regexp(pattern)
|
|
74
|
+
return Regexp.new(pattern) if @config.log_regexp_timeout.nil? || !Regexp.respond_to?(:timeout)
|
|
75
|
+
|
|
76
|
+
Regexp.new(pattern, timeout: @config.log_regexp_timeout)
|
|
77
|
+
end
|
|
78
|
+
|
|
73
79
|
def setup_retry(faraday)
|
|
74
80
|
faraday.request :retry, {
|
|
75
81
|
max: @config.max_retries,
|
|
76
82
|
interval: @config.retry_interval,
|
|
77
83
|
interval_randomness: @config.retry_interval_randomness,
|
|
78
84
|
backoff_factor: @config.retry_backoff_factor,
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
methods: Faraday::Retry::Middleware::IDEMPOTENT_METHODS + [:post],
|
|
86
|
+
exceptions: retry_exceptions
|
|
81
87
|
}
|
|
82
88
|
end
|
|
83
89
|
|