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/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",
|
|
@@ -14,7 +10,7 @@
|
|
|
14
10
|
"claude-3-5-sonnet": {
|
|
15
11
|
"anthropic": "claude-3-5-sonnet-20241022",
|
|
16
12
|
"openrouter": "anthropic/claude-3.5-sonnet",
|
|
17
|
-
"bedrock": "anthropic.claude-3-5-sonnet-
|
|
13
|
+
"bedrock": "anthropic.claude-3-5-sonnet-20241022-v2:0"
|
|
18
14
|
},
|
|
19
15
|
"claude-3-7-sonnet": {
|
|
20
16
|
"anthropic": "claude-3-7-sonnet-20250219",
|
|
@@ -30,8 +26,7 @@
|
|
|
30
26
|
"bedrock": "anthropic.claude-3-haiku-20240307-v1:0:200k"
|
|
31
27
|
},
|
|
32
28
|
"claude-3-opus": {
|
|
33
|
-
"anthropic": "claude-3-opus-20240229"
|
|
34
|
-
"bedrock": "anthropic.claude-3-opus-20240229-v1:0:200k"
|
|
29
|
+
"anthropic": "claude-3-opus-20240229"
|
|
35
30
|
},
|
|
36
31
|
"claude-3-sonnet": {
|
|
37
32
|
"anthropic": "claude-3-sonnet-20240229",
|
|
@@ -66,7 +61,8 @@
|
|
|
66
61
|
"claude-opus-4-6": {
|
|
67
62
|
"anthropic": "claude-opus-4-6",
|
|
68
63
|
"openrouter": "anthropic/claude-opus-4.6",
|
|
69
|
-
"bedrock": "anthropic.claude-opus-4-6-v1"
|
|
64
|
+
"bedrock": "anthropic.claude-opus-4-6-v1",
|
|
65
|
+
"azure": "claude-opus-4-6"
|
|
70
66
|
},
|
|
71
67
|
"claude-sonnet-4": {
|
|
72
68
|
"anthropic": "claude-sonnet-4-20250514",
|
|
@@ -82,6 +78,12 @@
|
|
|
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
|
+
"azure": "claude-sonnet-4-6"
|
|
86
|
+
},
|
|
85
87
|
"deepseek-chat": {
|
|
86
88
|
"deepseek": "deepseek-chat",
|
|
87
89
|
"openrouter": "deepseek/deepseek-chat"
|
|
@@ -181,14 +183,30 @@
|
|
|
181
183
|
"openrouter": "google/gemini-3-pro-preview",
|
|
182
184
|
"vertexai": "gemini-3-pro-preview"
|
|
183
185
|
},
|
|
186
|
+
"gemini-3.1-flash-image-preview": {
|
|
187
|
+
"gemini": "gemini-3.1-flash-image-preview",
|
|
188
|
+
"openrouter": "google/gemini-3.1-flash-image-preview",
|
|
189
|
+
"vertexai": "gemini-3.1-flash-image-preview"
|
|
190
|
+
},
|
|
191
|
+
"gemini-3.1-flash-lite-preview": {
|
|
192
|
+
"gemini": "gemini-3.1-flash-lite-preview",
|
|
193
|
+
"openrouter": "google/gemini-3.1-flash-lite-preview",
|
|
194
|
+
"vertexai": "gemini-3.1-flash-lite-preview"
|
|
195
|
+
},
|
|
196
|
+
"gemini-3.1-pro-preview": {
|
|
197
|
+
"gemini": "gemini-3.1-pro-preview",
|
|
198
|
+
"openrouter": "google/gemini-3.1-pro-preview",
|
|
199
|
+
"vertexai": "gemini-3.1-pro-preview"
|
|
200
|
+
},
|
|
201
|
+
"gemini-3.1-pro-preview-customtools": {
|
|
202
|
+
"gemini": "gemini-3.1-pro-preview-customtools",
|
|
203
|
+
"openrouter": "google/gemini-3.1-pro-preview-customtools",
|
|
204
|
+
"vertexai": "gemini-3.1-pro-preview-customtools"
|
|
205
|
+
},
|
|
184
206
|
"gemini-embedding-001": {
|
|
185
207
|
"gemini": "gemini-embedding-001",
|
|
186
208
|
"vertexai": "gemini-embedding-001"
|
|
187
209
|
},
|
|
188
|
-
"gemini-exp-1206": {
|
|
189
|
-
"gemini": "gemini-exp-1206",
|
|
190
|
-
"vertexai": "gemini-exp-1206"
|
|
191
|
-
},
|
|
192
210
|
"gemini-flash": {
|
|
193
211
|
"gemini": "gemini-flash-latest",
|
|
194
212
|
"vertexai": "gemini-flash-latest"
|
|
@@ -230,18 +248,10 @@
|
|
|
230
248
|
"openrouter": "openai/gpt-4",
|
|
231
249
|
"azure": "gpt-4"
|
|
232
250
|
},
|
|
233
|
-
"gpt-4-1106-preview": {
|
|
234
|
-
"openai": "gpt-4-1106-preview",
|
|
235
|
-
"openrouter": "openai/gpt-4-1106-preview"
|
|
236
|
-
},
|
|
237
251
|
"gpt-4-turbo": {
|
|
238
252
|
"openai": "gpt-4-turbo",
|
|
239
253
|
"openrouter": "openai/gpt-4-turbo"
|
|
240
254
|
},
|
|
241
|
-
"gpt-4-turbo-preview": {
|
|
242
|
-
"openai": "gpt-4-turbo-preview",
|
|
243
|
-
"openrouter": "openai/gpt-4-turbo-preview"
|
|
244
|
-
},
|
|
245
255
|
"gpt-4.1": {
|
|
246
256
|
"openai": "gpt-4.1",
|
|
247
257
|
"openrouter": "openai/gpt-4.1",
|
|
@@ -321,7 +331,8 @@
|
|
|
321
331
|
},
|
|
322
332
|
"gpt-5.1": {
|
|
323
333
|
"openai": "gpt-5.1",
|
|
324
|
-
"openrouter": "openai/gpt-5.1"
|
|
334
|
+
"openrouter": "openai/gpt-5.1",
|
|
335
|
+
"azure": "gpt-5.1"
|
|
325
336
|
},
|
|
326
337
|
"gpt-5.1-codex": {
|
|
327
338
|
"openai": "gpt-5.1-codex",
|
|
@@ -347,6 +358,26 @@
|
|
|
347
358
|
"openai": "gpt-5.2-pro",
|
|
348
359
|
"openrouter": "openai/gpt-5.2-pro"
|
|
349
360
|
},
|
|
361
|
+
"gpt-5.3-codex": {
|
|
362
|
+
"openai": "gpt-5.3-codex",
|
|
363
|
+
"openrouter": "openai/gpt-5.3-codex"
|
|
364
|
+
},
|
|
365
|
+
"gpt-5.4": {
|
|
366
|
+
"openai": "gpt-5.4",
|
|
367
|
+
"openrouter": "openai/gpt-5.4"
|
|
368
|
+
},
|
|
369
|
+
"gpt-5.4-mini": {
|
|
370
|
+
"openai": "gpt-5.4-mini",
|
|
371
|
+
"openrouter": "openai/gpt-5.4-mini"
|
|
372
|
+
},
|
|
373
|
+
"gpt-5.4-nano": {
|
|
374
|
+
"openai": "gpt-5.4-nano",
|
|
375
|
+
"openrouter": "openai/gpt-5.4-nano"
|
|
376
|
+
},
|
|
377
|
+
"gpt-5.4-pro": {
|
|
378
|
+
"openai": "gpt-5.4-pro",
|
|
379
|
+
"openrouter": "openai/gpt-5.4-pro"
|
|
380
|
+
},
|
|
350
381
|
"gpt-audio": {
|
|
351
382
|
"openai": "gpt-audio",
|
|
352
383
|
"openrouter": "openai/gpt-audio"
|
|
@@ -355,6 +386,14 @@
|
|
|
355
386
|
"openai": "gpt-audio-mini",
|
|
356
387
|
"openrouter": "openai/gpt-audio-mini"
|
|
357
388
|
},
|
|
389
|
+
"lyria-3-clip-preview": {
|
|
390
|
+
"gemini": "lyria-3-clip-preview",
|
|
391
|
+
"openrouter": "google/lyria-3-clip-preview"
|
|
392
|
+
},
|
|
393
|
+
"lyria-3-pro-preview": {
|
|
394
|
+
"gemini": "lyria-3-pro-preview",
|
|
395
|
+
"openrouter": "google/lyria-3-pro-preview"
|
|
396
|
+
},
|
|
358
397
|
"o1": {
|
|
359
398
|
"openai": "o1",
|
|
360
399
|
"openrouter": "openai/o1"
|
data/lib/ruby_llm/attachment.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
3
6
|
module RubyLLM
|
|
4
7
|
# A class representing a file attachment.
|
|
5
8
|
class Attachment
|
|
@@ -134,7 +137,7 @@ module RubyLLM
|
|
|
134
137
|
end
|
|
135
138
|
|
|
136
139
|
def load_content_from_path
|
|
137
|
-
@content = File.
|
|
140
|
+
@content = File.binread(@source)
|
|
138
141
|
end
|
|
139
142
|
|
|
140
143
|
def load_content_from_io
|
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,
|
|
@@ -149,7 +152,7 @@ module RubyLLM
|
|
|
149
152
|
|
|
150
153
|
@on[:new_message]&.call unless block_given?
|
|
151
154
|
|
|
152
|
-
if @schema && response.content.is_a?(String)
|
|
155
|
+
if @schema && response.content.is_a?(String) && !response.tool_call?
|
|
153
156
|
begin
|
|
154
157
|
response.content = JSON.parse(response.content)
|
|
155
158
|
rescue JSON::ParserError
|
|
@@ -183,6 +186,41 @@ 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: sanitize_schema_name(schema[:name] || 'response'),
|
|
213
|
+
schema: schema_def,
|
|
214
|
+
strict: strict.nil? || strict,
|
|
215
|
+
description: schema[:description]
|
|
216
|
+
}.compact
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def sanitize_schema_name(name)
|
|
220
|
+
sanitized = name.to_s.gsub(/[^a-zA-Z0-9_-]/, '_')
|
|
221
|
+
sanitized.empty? ? 'response' : sanitized
|
|
222
|
+
end
|
|
223
|
+
|
|
186
224
|
def wrap_streaming_block(&block)
|
|
187
225
|
return nil unless block_given?
|
|
188
226
|
|
|
@@ -209,15 +247,78 @@ module RubyLLM
|
|
|
209
247
|
halt_result = result if result.is_a?(Tool::Halt)
|
|
210
248
|
end
|
|
211
249
|
|
|
250
|
+
reset_tool_choice if forced_tool_choice?
|
|
212
251
|
halt_result || complete(&)
|
|
213
252
|
end
|
|
214
253
|
|
|
215
254
|
def execute_tool(tool_call)
|
|
216
255
|
tool = tools[tool_call.name.to_sym]
|
|
256
|
+
if tool.nil?
|
|
257
|
+
return {
|
|
258
|
+
error: "Model tried to call unavailable tool `#{tool_call.name}`. " \
|
|
259
|
+
"Available tools: #{tools.keys.to_json}."
|
|
260
|
+
}
|
|
261
|
+
end
|
|
262
|
+
|
|
217
263
|
args = tool_call.arguments
|
|
218
264
|
tool.call(args)
|
|
219
265
|
end
|
|
220
266
|
|
|
267
|
+
def update_tool_options(choice:, calls:)
|
|
268
|
+
unless choice.nil?
|
|
269
|
+
normalized_choice = normalize_tool_choice(choice)
|
|
270
|
+
valid_tool_choices = %i[auto none required] + tools.keys
|
|
271
|
+
unless valid_tool_choices.include?(normalized_choice)
|
|
272
|
+
raise InvalidToolChoiceError,
|
|
273
|
+
"Invalid tool choice: #{choice}. Valid choices are: #{valid_tool_choices.join(', ')}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
@tool_prefs[:choice] = normalized_choice
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
@tool_prefs[:calls] = normalize_calls(calls) unless calls.nil?
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def normalize_calls(calls)
|
|
283
|
+
case calls
|
|
284
|
+
when :many, 'many'
|
|
285
|
+
:many
|
|
286
|
+
when :one, 'one', 1
|
|
287
|
+
:one
|
|
288
|
+
else
|
|
289
|
+
raise ArgumentError, "Invalid calls value: #{calls.inspect}. Valid values are: :many, :one, or 1"
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def normalize_tool_choice(choice)
|
|
294
|
+
return choice.to_sym if choice.is_a?(String) || choice.is_a?(Symbol)
|
|
295
|
+
return tool_name_for_choice_class(choice) if choice.is_a?(Class)
|
|
296
|
+
|
|
297
|
+
choice.respond_to?(:name) ? choice.name.to_sym : choice.to_sym
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def tool_name_for_choice_class(tool_class)
|
|
301
|
+
matched_tool_name = tools.find { |_name, tool| tool.is_a?(tool_class) }&.first
|
|
302
|
+
return matched_tool_name if matched_tool_name
|
|
303
|
+
|
|
304
|
+
classify_tool_name(tool_class.name)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def classify_tool_name(class_name)
|
|
308
|
+
class_name.split('::').last
|
|
309
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
310
|
+
.downcase
|
|
311
|
+
.to_sym
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def forced_tool_choice?
|
|
315
|
+
@tool_prefs[:choice] && !%i[auto none].include?(@tool_prefs[:choice])
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def reset_tool_choice
|
|
319
|
+
@tool_prefs[:choice] = nil
|
|
320
|
+
end
|
|
321
|
+
|
|
221
322
|
def build_content(message, attachments)
|
|
222
323
|
return message if content_like?(message)
|
|
223
324
|
|
|
@@ -3,80 +3,79 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
# Global configuration for RubyLLM
|
|
5
5
|
class Configuration
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
:azure_api_base,
|
|
12
|
-
:azure_api_key,
|
|
13
|
-
:azure_ai_auth_token,
|
|
14
|
-
:anthropic_api_key,
|
|
15
|
-
:gemini_api_key,
|
|
16
|
-
:gemini_api_base,
|
|
17
|
-
:vertexai_project_id,
|
|
18
|
-
:vertexai_location,
|
|
19
|
-
:deepseek_api_key,
|
|
20
|
-
:perplexity_api_key,
|
|
21
|
-
:bedrock_api_key,
|
|
22
|
-
:bedrock_secret_key,
|
|
23
|
-
:bedrock_region,
|
|
24
|
-
:bedrock_session_token,
|
|
25
|
-
:openrouter_api_key,
|
|
26
|
-
:xai_api_key,
|
|
27
|
-
:ollama_api_base,
|
|
28
|
-
:gpustack_api_base,
|
|
29
|
-
:gpustack_api_key,
|
|
30
|
-
:mistral_api_key,
|
|
31
|
-
# Default models
|
|
32
|
-
:default_model,
|
|
33
|
-
:default_embedding_model,
|
|
34
|
-
:default_moderation_model,
|
|
35
|
-
:default_image_model,
|
|
36
|
-
:default_transcription_model,
|
|
37
|
-
# Model registry
|
|
38
|
-
:model_registry_file,
|
|
39
|
-
:model_registry_class,
|
|
40
|
-
# Rails integration
|
|
41
|
-
:use_new_acts_as,
|
|
42
|
-
# Connection configuration
|
|
43
|
-
:request_timeout,
|
|
44
|
-
:max_retries,
|
|
45
|
-
:retry_interval,
|
|
46
|
-
:retry_backoff_factor,
|
|
47
|
-
:retry_interval_randomness,
|
|
48
|
-
:http_proxy,
|
|
49
|
-
# Logging configuration
|
|
50
|
-
:logger,
|
|
51
|
-
:log_file,
|
|
52
|
-
:log_level,
|
|
53
|
-
:log_stream_debug
|
|
6
|
+
class << self
|
|
7
|
+
# Declare a single configuration option.
|
|
8
|
+
def option(key, default = nil)
|
|
9
|
+
key = key.to_sym
|
|
10
|
+
return if options.include?(key)
|
|
54
11
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
12
|
+
send(:attr_accessor, key)
|
|
13
|
+
option_keys << key
|
|
14
|
+
defaults[key] = default
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def register_provider_options(options)
|
|
18
|
+
Array(options).each { |key| option(key, nil) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def options
|
|
22
|
+
option_keys.dup
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def option_keys = @option_keys ||= []
|
|
28
|
+
def defaults = @defaults ||= {}
|
|
29
|
+
private :option
|
|
30
|
+
end
|
|
62
31
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
32
|
+
# System-level options are declared here.
|
|
33
|
+
# Provider-specific options are declared in each provider class via
|
|
34
|
+
# `self.configuration_options` and registered through Provider.register.
|
|
35
|
+
option :default_model, 'gpt-5.4'
|
|
36
|
+
option :default_embedding_model, 'text-embedding-3-small'
|
|
37
|
+
option :default_moderation_model, 'omni-moderation-latest'
|
|
38
|
+
option :default_image_model, 'gpt-image-1.5'
|
|
39
|
+
option :default_transcription_model, 'whisper-1'
|
|
68
40
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@use_new_acts_as = false
|
|
41
|
+
option :model_registry_file, -> { File.expand_path('models.json', __dir__) }
|
|
42
|
+
option :model_registry_class, 'Model'
|
|
72
43
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
44
|
+
option :use_new_acts_as, false
|
|
45
|
+
|
|
46
|
+
option :request_timeout, 300
|
|
47
|
+
option :max_retries, 3
|
|
48
|
+
option :retry_interval, 0.1
|
|
49
|
+
option :retry_backoff_factor, 2
|
|
50
|
+
option :retry_interval_randomness, 0.5
|
|
51
|
+
option :http_proxy, nil
|
|
52
|
+
|
|
53
|
+
option :logger, nil
|
|
54
|
+
option :log_file, -> { $stdout }
|
|
55
|
+
option :log_level, -> { ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO }
|
|
56
|
+
option :log_stream_debug, -> { ENV['RUBYLLM_STREAM_DEBUG'] == 'true' }
|
|
57
|
+
option :log_regexp_timeout, -> { Regexp.respond_to?(:timeout) ? (Regexp.timeout || 1.0) : nil }
|
|
58
|
+
|
|
59
|
+
def initialize
|
|
60
|
+
self.class.send(:defaults).each do |key, default|
|
|
61
|
+
value = default.respond_to?(:call) ? instance_exec(&default) : default
|
|
62
|
+
public_send("#{key}=", value)
|
|
63
|
+
end
|
|
76
64
|
end
|
|
77
65
|
|
|
78
66
|
def instance_variables
|
|
79
67
|
super.reject { |ivar| ivar.to_s.match?(/_id|_key|_secret|_token$/) }
|
|
80
68
|
end
|
|
69
|
+
|
|
70
|
+
def log_regexp_timeout=(value)
|
|
71
|
+
if value.nil?
|
|
72
|
+
@log_regexp_timeout = nil
|
|
73
|
+
elsif Regexp.respond_to?(:timeout)
|
|
74
|
+
@log_regexp_timeout = value
|
|
75
|
+
else
|
|
76
|
+
RubyLLM.logger.warn("log_regexp_timeout is not supported on Ruby #{RUBY_VERSION}")
|
|
77
|
+
@log_regexp_timeout = value
|
|
78
|
+
end
|
|
79
|
+
end
|
|
81
80
|
end
|
|
82
81
|
end
|
data/lib/ruby_llm/connection.rb
CHANGED
|
@@ -10,7 +10,6 @@ module RubyLLM
|
|
|
10
10
|
f.response :logger,
|
|
11
11
|
RubyLLM.logger,
|
|
12
12
|
bodies: false,
|
|
13
|
-
response: false,
|
|
14
13
|
errors: true,
|
|
15
14
|
headers: false,
|
|
16
15
|
log_level: :debug
|
|
@@ -60,24 +59,29 @@ module RubyLLM
|
|
|
60
59
|
def setup_logging(faraday)
|
|
61
60
|
faraday.response :logger,
|
|
62
61
|
RubyLLM.logger,
|
|
63
|
-
bodies:
|
|
64
|
-
response: true,
|
|
62
|
+
bodies: RubyLLM.logger.debug?,
|
|
65
63
|
errors: true,
|
|
66
64
|
headers: false,
|
|
67
65
|
log_level: :debug do |logger|
|
|
68
|
-
logger.filter(
|
|
69
|
-
logger.filter(
|
|
66
|
+
logger.filter(logging_regexp('[A-Za-z0-9+/=]{100,}'), '[BASE64 DATA]')
|
|
67
|
+
logger.filter(logging_regexp('[-\\d.e,\\s]{100,}'), '[EMBEDDINGS ARRAY]')
|
|
70
68
|
end
|
|
71
69
|
end
|
|
72
70
|
|
|
71
|
+
def logging_regexp(pattern)
|
|
72
|
+
return Regexp.new(pattern) if @config.log_regexp_timeout.nil? || !Regexp.respond_to?(:timeout)
|
|
73
|
+
|
|
74
|
+
Regexp.new(pattern, timeout: @config.log_regexp_timeout)
|
|
75
|
+
end
|
|
76
|
+
|
|
73
77
|
def setup_retry(faraday)
|
|
74
78
|
faraday.request :retry, {
|
|
75
79
|
max: @config.max_retries,
|
|
76
80
|
interval: @config.retry_interval,
|
|
77
81
|
interval_randomness: @config.retry_interval_randomness,
|
|
78
82
|
backoff_factor: @config.retry_backoff_factor,
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
methods: Faraday::Retry::Middleware::IDEMPOTENT_METHODS + [:post],
|
|
84
|
+
exceptions: retry_exceptions
|
|
81
85
|
}
|
|
82
86
|
end
|
|
83
87
|
|
data/lib/ruby_llm/content.rb
CHANGED
|
@@ -35,10 +35,16 @@ module RubyLLM
|
|
|
35
35
|
|
|
36
36
|
def process_attachments_array_or_string(attachments)
|
|
37
37
|
Utils.to_safe_array(attachments).each do |file|
|
|
38
|
+
next if blank_attachment_entry?(file)
|
|
39
|
+
|
|
38
40
|
add_attachment(file)
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
def blank_attachment_entry?(file)
|
|
45
|
+
file.nil? || (file.is_a?(String) && file.strip.empty?)
|
|
46
|
+
end
|
|
47
|
+
|
|
42
48
|
def process_attachments(attachments)
|
|
43
49
|
if attachments.is_a?(Hash)
|
|
44
50
|
attachments.each_value { |attachment| process_attachments_array_or_string(attachment) }
|
|
@@ -47,9 +53,7 @@ module RubyLLM
|
|
|
47
53
|
end
|
|
48
54
|
end
|
|
49
55
|
end
|
|
50
|
-
end
|
|
51
56
|
|
|
52
|
-
module RubyLLM
|
|
53
57
|
class Content
|
|
54
58
|
# Represents provider-specific payloads that should bypass RubyLLM formatting.
|
|
55
59
|
class Raw
|
data/lib/ruby_llm/error.rb
CHANGED
|
@@ -7,6 +7,11 @@ module RubyLLM
|
|
|
7
7
|
attr_reader :response
|
|
8
8
|
|
|
9
9
|
def initialize(response = nil, message = nil)
|
|
10
|
+
if response.is_a?(String)
|
|
11
|
+
message = response
|
|
12
|
+
response = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
10
15
|
@response = response
|
|
11
16
|
super(message || response&.body)
|
|
12
17
|
end
|
|
@@ -14,13 +19,16 @@ module RubyLLM
|
|
|
14
19
|
|
|
15
20
|
# Error classes for non-HTTP errors
|
|
16
21
|
class ConfigurationError < StandardError; end
|
|
22
|
+
class PromptNotFoundError < StandardError; end
|
|
17
23
|
class InvalidRoleError < StandardError; end
|
|
24
|
+
class InvalidToolChoiceError < StandardError; end
|
|
18
25
|
class ModelNotFoundError < StandardError; end
|
|
19
26
|
class UnsupportedAttachmentError < StandardError; end
|
|
20
27
|
|
|
21
28
|
# Error classes for different HTTP status codes
|
|
22
29
|
class BadRequestError < Error; end
|
|
23
30
|
class ForbiddenError < Error; end
|
|
31
|
+
class ContextLengthExceededError < Error; end
|
|
24
32
|
class OverloadedError < Error; end
|
|
25
33
|
class PaymentRequiredError < Error; end
|
|
26
34
|
class RateLimitError < Error; end
|
|
@@ -42,6 +50,18 @@ module RubyLLM
|
|
|
42
50
|
end
|
|
43
51
|
|
|
44
52
|
class << self
|
|
53
|
+
CONTEXT_LENGTH_PATTERNS = [
|
|
54
|
+
/context length/i,
|
|
55
|
+
/context window/i,
|
|
56
|
+
/maximum context/i,
|
|
57
|
+
/request too large/i,
|
|
58
|
+
/too many tokens/i,
|
|
59
|
+
/token count exceeds/i,
|
|
60
|
+
/input[_\s-]?token/i,
|
|
61
|
+
/input or output tokens? must be reduced/i,
|
|
62
|
+
/reduce the length of messages/i
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
45
65
|
def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
|
|
46
66
|
message = provider&.parse_error(response)
|
|
47
67
|
|
|
@@ -49,6 +69,10 @@ module RubyLLM
|
|
|
49
69
|
when 200..399
|
|
50
70
|
message
|
|
51
71
|
when 400
|
|
72
|
+
if context_length_exceeded?(message)
|
|
73
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
74
|
+
end
|
|
75
|
+
|
|
52
76
|
raise BadRequestError.new(response, message || 'Invalid request - please check your input')
|
|
53
77
|
when 401
|
|
54
78
|
raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
|
|
@@ -58,10 +82,14 @@ module RubyLLM
|
|
|
58
82
|
raise ForbiddenError.new(response,
|
|
59
83
|
message || 'Forbidden - you do not have permission to access this resource')
|
|
60
84
|
when 429
|
|
85
|
+
if context_length_exceeded?(message)
|
|
86
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
87
|
+
end
|
|
88
|
+
|
|
61
89
|
raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
|
|
62
90
|
when 500
|
|
63
91
|
raise ServerError.new(response, message || 'API server error - please try again')
|
|
64
|
-
when 502..
|
|
92
|
+
when 502..504
|
|
65
93
|
raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
|
|
66
94
|
when 529
|
|
67
95
|
raise OverloadedError.new(response, message || 'Service overloaded - please try again later')
|
|
@@ -69,6 +97,14 @@ module RubyLLM
|
|
|
69
97
|
raise Error.new(response, message || 'An unknown error occurred')
|
|
70
98
|
end
|
|
71
99
|
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def context_length_exceeded?(message)
|
|
104
|
+
return false if message.to_s.empty?
|
|
105
|
+
|
|
106
|
+
CONTEXT_LENGTH_PATTERNS.any? { |pattern| message.match?(pattern) }
|
|
107
|
+
end
|
|
72
108
|
end
|
|
73
109
|
end
|
|
74
110
|
end
|
data/lib/ruby_llm/message.rb
CHANGED
|
@@ -10,9 +10,9 @@ module RubyLLM
|
|
|
10
10
|
|
|
11
11
|
def initialize(options = {})
|
|
12
12
|
@role = options.fetch(:role).to_sym
|
|
13
|
-
@content = normalize_content(options.fetch(:content))
|
|
14
|
-
@model_id = options[:model_id]
|
|
15
13
|
@tool_calls = options[:tool_calls]
|
|
14
|
+
@content = normalize_content(options.fetch(:content), role: @role, tool_calls: @tool_calls)
|
|
15
|
+
@model_id = options[:model_id]
|
|
16
16
|
@tool_call_id = options[:tool_call_id]
|
|
17
17
|
@tokens = options[:tokens] || Tokens.build(
|
|
18
18
|
input: options[:input_tokens],
|
|
@@ -90,7 +90,9 @@ module RubyLLM
|
|
|
90
90
|
|
|
91
91
|
private
|
|
92
92
|
|
|
93
|
-
def normalize_content(content)
|
|
93
|
+
def normalize_content(content, role:, tool_calls:)
|
|
94
|
+
return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
|
|
95
|
+
|
|
94
96
|
case content
|
|
95
97
|
when String then Content.new(content)
|
|
96
98
|
when Hash then Content.new(content[:text], content)
|