dify_llm 1.9.1 → 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 +27 -8
- 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 +4 -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 -1
- 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/templates/add_v1_10_message_columns.rb.tt +19 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
- 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/templates/migration.rb.tt +1 -1
- 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 +132 -27
- data/lib/ruby_llm/active_record/chat_methods.rb +132 -28
- data/lib/ruby_llm/active_record/message_methods.rb +58 -8
- 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 +365 -0
- data/lib/ruby_llm/aliases.json +199 -62
- data/lib/ruby_llm/attachment.rb +15 -4
- data/lib/ruby_llm/chat.rb +150 -22
- data/lib/ruby_llm/configuration.rb +65 -65
- 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 +43 -15
- data/lib/ruby_llm/model/info.rb +15 -13
- data/lib/ruby_llm/models.json +37560 -14094
- data/lib/ruby_llm/models.rb +321 -38
- data/lib/ruby_llm/models_schema.json +2 -2
- data/lib/ruby_llm/provider.rb +26 -4
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
- data/lib/ruby_llm/providers/anthropic/chat.rb +149 -17
- data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
- data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
- data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
- 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 +29 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
- data/lib/ruby_llm/providers/azure/media.rb +45 -0
- data/lib/ruby_llm/providers/azure/models.rb +14 -0
- data/lib/ruby_llm/providers/azure.rb +148 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +357 -28
- data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
- data/lib/ruby_llm/providers/bedrock/models.rb +107 -62
- data/lib/ruby_llm/providers/bedrock/streaming.rb +309 -8
- data/lib/ruby_llm/providers/bedrock.rb +69 -52
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/dify/chat.rb +82 -7
- data/lib/ruby_llm/providers/dify/media.rb +2 -2
- data/lib/ruby_llm/providers/dify/streaming.rb +26 -4
- data/lib/ruby_llm/providers/dify.rb +4 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
- data/lib/ruby_llm/providers/gemini/chat.rb +88 -6
- 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 +34 -2
- data/lib/ruby_llm/providers/gemini/tools.rb +35 -3
- 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/chat.rb +1 -1
- 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 +59 -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/chat.rb +1 -1
- data/lib/ruby_llm/providers/ollama.rb +11 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +96 -192
- data/lib/ruby_llm/providers/openai/chat.rb +101 -7
- data/lib/ruby_llm/providers/openai/media.rb +5 -2
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
- data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
- data/lib/ruby_llm/providers/openai/tools.rb +27 -2
- data/lib/ruby_llm/providers/openai.rb +11 -1
- data/lib/ruby_llm/providers/openrouter/chat.rb +168 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
- data/lib/ruby_llm/providers/openrouter.rb +37 -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 +23 -7
- data/lib/ruby_llm/providers/xai/chat.rb +15 -0
- data/lib/ruby_llm/providers/xai/models.rb +75 -0
- data/lib/ruby_llm/providers/xai.rb +32 -0
- data/lib/ruby_llm/stream_accumulator.rb +120 -18
- data/lib/ruby_llm/streaming.rb +82 -60
- data/lib/ruby_llm/thinking.rb +49 -0
- data/lib/ruby_llm/tokens.rb +47 -0
- data/lib/ruby_llm/tool.rb +49 -4
- data/lib/ruby_llm/tool_call.rb +6 -3
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +14 -8
- data/lib/tasks/models.rake +62 -23
- data/lib/tasks/release.rake +1 -1
- data/lib/tasks/ruby_llm.rake +9 -1
- data/lib/tasks/vcr.rake +33 -1
- metadata +67 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
- data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -80
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
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, :provider
|
|
8
|
+
attr_reader :model, :messages, :tools, :tool_prefs, :params, :headers, :schema, :provider
|
|
9
9
|
|
|
10
10
|
def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
|
|
11
11
|
if assume_model_exists && !provider
|
|
@@ -19,9 +19,11 @@ 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
|
|
26
|
+
@thinking = nil
|
|
25
27
|
@on = {
|
|
26
28
|
new_message: nil,
|
|
27
29
|
end_message: nil,
|
|
@@ -37,22 +39,31 @@ module RubyLLM
|
|
|
37
39
|
|
|
38
40
|
alias say ask
|
|
39
41
|
|
|
40
|
-
def with_instructions(instructions, replace:
|
|
41
|
-
|
|
42
|
+
def with_instructions(instructions, append: false, replace: nil)
|
|
43
|
+
append ||= (replace == false) unless replace.nil?
|
|
44
|
+
|
|
45
|
+
if append
|
|
46
|
+
append_system_instruction(instructions)
|
|
47
|
+
else
|
|
48
|
+
replace_system_instruction(instructions)
|
|
49
|
+
end
|
|
42
50
|
|
|
43
|
-
add_message role: :system, content: instructions
|
|
44
51
|
self
|
|
45
52
|
end
|
|
46
53
|
|
|
47
|
-
def with_tool(tool)
|
|
48
|
-
|
|
49
|
-
|
|
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:)
|
|
50
60
|
self
|
|
51
61
|
end
|
|
52
62
|
|
|
53
|
-
def with_tools(*tools, replace: false)
|
|
63
|
+
def with_tools(*tools, replace: false, choice: nil, calls: nil)
|
|
54
64
|
@tools.clear if replace
|
|
55
65
|
tools.compact.each { |tool| with_tool tool }
|
|
66
|
+
update_tool_options(choice:, calls:)
|
|
56
67
|
self
|
|
57
68
|
end
|
|
58
69
|
|
|
@@ -67,6 +78,13 @@ module RubyLLM
|
|
|
67
78
|
self
|
|
68
79
|
end
|
|
69
80
|
|
|
81
|
+
def with_thinking(effort: nil, budget: nil)
|
|
82
|
+
raise ArgumentError, 'with_thinking requires :effort or :budget' if effort.nil? && budget.nil?
|
|
83
|
+
|
|
84
|
+
@thinking = Thinking::Config.new(effort: effort, budget: budget)
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
70
88
|
def with_context(context)
|
|
71
89
|
@context = context
|
|
72
90
|
@config = context.config
|
|
@@ -87,12 +105,9 @@ module RubyLLM
|
|
|
87
105
|
def with_schema(schema)
|
|
88
106
|
schema_instance = schema.is_a?(Class) ? schema.new : schema
|
|
89
107
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
else
|
|
94
|
-
schema_instance
|
|
95
|
-
end
|
|
108
|
+
@schema = normalize_schema_payload(
|
|
109
|
+
schema_instance.respond_to?(:to_json_schema) ? schema_instance.to_json_schema : schema_instance
|
|
110
|
+
)
|
|
96
111
|
|
|
97
112
|
self
|
|
98
113
|
end
|
|
@@ -125,17 +140,19 @@ module RubyLLM
|
|
|
125
140
|
response = @provider.complete(
|
|
126
141
|
messages,
|
|
127
142
|
tools: @tools,
|
|
143
|
+
tool_prefs: @tool_prefs,
|
|
128
144
|
temperature: @temperature,
|
|
129
145
|
model: @model,
|
|
130
146
|
params: @params,
|
|
131
147
|
headers: @headers,
|
|
132
148
|
schema: @schema,
|
|
149
|
+
thinking: @thinking,
|
|
133
150
|
&wrap_streaming_block(&)
|
|
134
151
|
)
|
|
135
152
|
|
|
136
153
|
@on[:new_message]&.call unless block_given?
|
|
137
154
|
|
|
138
|
-
if @schema && response.content.is_a?(String)
|
|
155
|
+
if @schema && response.content.is_a?(String) && !response.tool_call?
|
|
139
156
|
begin
|
|
140
157
|
response.content = JSON.parse(response.content)
|
|
141
158
|
rescue JSON::ParserError
|
|
@@ -169,18 +186,47 @@ module RubyLLM
|
|
|
169
186
|
|
|
170
187
|
private
|
|
171
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
|
+
|
|
172
224
|
def wrap_streaming_block(&block)
|
|
173
225
|
return nil unless block_given?
|
|
174
226
|
|
|
175
|
-
|
|
227
|
+
@on[:new_message]&.call
|
|
176
228
|
|
|
177
229
|
proc do |chunk|
|
|
178
|
-
# Create message on first content chunk
|
|
179
|
-
unless first_chunk_received
|
|
180
|
-
first_chunk_received = true
|
|
181
|
-
@on[:new_message]&.call
|
|
182
|
-
end
|
|
183
|
-
|
|
184
230
|
block.call chunk
|
|
185
231
|
end
|
|
186
232
|
end
|
|
@@ -201,15 +247,78 @@ module RubyLLM
|
|
|
201
247
|
halt_result = result if result.is_a?(Tool::Halt)
|
|
202
248
|
end
|
|
203
249
|
|
|
250
|
+
reset_tool_choice if forced_tool_choice?
|
|
204
251
|
halt_result || complete(&)
|
|
205
252
|
end
|
|
206
253
|
|
|
207
254
|
def execute_tool(tool_call)
|
|
208
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
|
+
|
|
209
263
|
args = tool_call.arguments
|
|
210
264
|
tool.call(args)
|
|
211
265
|
end
|
|
212
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
|
+
|
|
213
322
|
def build_content(message, attachments)
|
|
214
323
|
return message if content_like?(message)
|
|
215
324
|
|
|
@@ -219,5 +328,24 @@ module RubyLLM
|
|
|
219
328
|
def content_like?(object)
|
|
220
329
|
object.is_a?(Content) || object.is_a?(Content::Raw)
|
|
221
330
|
end
|
|
331
|
+
|
|
332
|
+
def append_system_instruction(instructions)
|
|
333
|
+
system_messages, non_system_messages = @messages.partition { |msg| msg.role == :system }
|
|
334
|
+
system_messages << Message.new(role: :system, content: instructions)
|
|
335
|
+
@messages = system_messages + non_system_messages
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def replace_system_instruction(instructions)
|
|
339
|
+
system_messages, non_system_messages = @messages.partition { |msg| msg.role == :system }
|
|
340
|
+
|
|
341
|
+
if system_messages.empty?
|
|
342
|
+
system_messages = [Message.new(role: :system, content: instructions)]
|
|
343
|
+
else
|
|
344
|
+
system_messages.first.content = instructions
|
|
345
|
+
system_messages = [system_messages.first]
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
@messages = system_messages + non_system_messages
|
|
349
|
+
end
|
|
222
350
|
end
|
|
223
351
|
end
|
|
@@ -3,79 +3,79 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
# Global configuration for RubyLLM
|
|
5
5
|
class Configuration
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
:anthropic_api_key,
|
|
12
|
-
:gemini_api_key,
|
|
13
|
-
:gemini_api_base,
|
|
14
|
-
:vertexai_project_id,
|
|
15
|
-
:vertexai_location,
|
|
16
|
-
:deepseek_api_key,
|
|
17
|
-
:dify_api_base,
|
|
18
|
-
:dify_api_key,
|
|
19
|
-
:dify_user,
|
|
20
|
-
:perplexity_api_key,
|
|
21
|
-
:bedrock_api_key,
|
|
22
|
-
:bedrock_secret_key,
|
|
23
|
-
:bedrock_region,
|
|
24
|
-
:bedrock_session_token,
|
|
25
|
-
:openrouter_api_key,
|
|
26
|
-
:ollama_api_base,
|
|
27
|
-
:gpustack_api_base,
|
|
28
|
-
:gpustack_api_key,
|
|
29
|
-
:mistral_api_key,
|
|
30
|
-
# Default models
|
|
31
|
-
:default_model,
|
|
32
|
-
:default_embedding_model,
|
|
33
|
-
:default_moderation_model,
|
|
34
|
-
:default_image_model,
|
|
35
|
-
:default_transcription_model,
|
|
36
|
-
# Model registry
|
|
37
|
-
:model_registry_file,
|
|
38
|
-
:model_registry_class,
|
|
39
|
-
# Rails integration
|
|
40
|
-
:use_new_acts_as,
|
|
41
|
-
# Connection configuration
|
|
42
|
-
:request_timeout,
|
|
43
|
-
:max_retries,
|
|
44
|
-
:retry_interval,
|
|
45
|
-
:retry_backoff_factor,
|
|
46
|
-
:retry_interval_randomness,
|
|
47
|
-
:http_proxy,
|
|
48
|
-
# Logging configuration
|
|
49
|
-
:logger,
|
|
50
|
-
:log_file,
|
|
51
|
-
:log_level,
|
|
52
|
-
: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)
|
|
53
11
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
61
31
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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'
|
|
67
40
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@use_new_acts_as = false
|
|
41
|
+
option :model_registry_file, -> { File.expand_path('models.json', __dir__) }
|
|
42
|
+
option :model_registry_class, 'Model'
|
|
71
43
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
75
64
|
end
|
|
76
65
|
|
|
77
66
|
def instance_variables
|
|
78
67
|
super.reject { |ivar| ivar.to_s.match?(/_id|_key|_secret|_token$/) }
|
|
79
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
|
|
80
80
|
end
|
|
81
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
|
|
@@ -71,24 +70,29 @@ module RubyLLM
|
|
|
71
70
|
def setup_logging(faraday)
|
|
72
71
|
faraday.response :logger,
|
|
73
72
|
RubyLLM.logger,
|
|
74
|
-
bodies:
|
|
75
|
-
response: true,
|
|
73
|
+
bodies: RubyLLM.logger.debug?,
|
|
76
74
|
errors: true,
|
|
77
75
|
headers: false,
|
|
78
76
|
log_level: :debug do |logger|
|
|
79
|
-
logger.filter(
|
|
80
|
-
logger.filter(
|
|
77
|
+
logger.filter(logging_regexp('[A-Za-z0-9+/=]{100,}'), '[BASE64 DATA]')
|
|
78
|
+
logger.filter(logging_regexp('[-\\d.e,\\s]{100,}'), '[EMBEDDINGS ARRAY]')
|
|
81
79
|
end
|
|
82
80
|
end
|
|
83
81
|
|
|
82
|
+
def logging_regexp(pattern)
|
|
83
|
+
return Regexp.new(pattern) if @config.log_regexp_timeout.nil? || !Regexp.respond_to?(:timeout)
|
|
84
|
+
|
|
85
|
+
Regexp.new(pattern, timeout: @config.log_regexp_timeout)
|
|
86
|
+
end
|
|
87
|
+
|
|
84
88
|
def setup_retry(faraday)
|
|
85
89
|
faraday.request :retry, {
|
|
86
90
|
max: @config.max_retries,
|
|
87
91
|
interval: @config.retry_interval,
|
|
88
92
|
interval_randomness: @config.retry_interval_randomness,
|
|
89
93
|
backoff_factor: @config.retry_backoff_factor,
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
methods: Faraday::Retry::Middleware::IDEMPOTENT_METHODS + [:post],
|
|
95
|
+
exceptions: retry_exceptions
|
|
92
96
|
}
|
|
93
97
|
end
|
|
94
98
|
|
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
|
@@ -5,22 +5,26 @@ module RubyLLM
|
|
|
5
5
|
class Message
|
|
6
6
|
ROLES = %i[system user assistant tool].freeze
|
|
7
7
|
|
|
8
|
-
attr_reader :role, :model_id, :tool_calls, :tool_call_id, :
|
|
9
|
-
:cached_tokens, :cache_creation_tokens, :raw, :conversation_id
|
|
8
|
+
attr_reader :role, :model_id, :tool_calls, :tool_call_id, :raw, :conversation_id, :thinking, :tokens
|
|
10
9
|
attr_writer :content
|
|
11
10
|
|
|
12
11
|
def initialize(options = {})
|
|
13
12
|
@role = options.fetch(:role).to_sym
|
|
14
|
-
@content = normalize_content(options.fetch(:content))
|
|
15
|
-
@model_id = options[:model_id]
|
|
16
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]
|
|
17
16
|
@tool_call_id = options[:tool_call_id]
|
|
18
|
-
@
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
@tokens = options[:tokens] || Tokens.build(
|
|
18
|
+
input: options[:input_tokens],
|
|
19
|
+
output: options[:output_tokens],
|
|
20
|
+
cached: options[:cached_tokens],
|
|
21
|
+
cache_creation: options[:cache_creation_tokens],
|
|
22
|
+
thinking: options[:thinking_tokens],
|
|
23
|
+
reasoning: options[:reasoning_tokens]
|
|
24
|
+
)
|
|
23
25
|
@raw = options[:raw]
|
|
26
|
+
@conversation_id = options[:conversation_id]
|
|
27
|
+
@thinking = options[:thinking]
|
|
24
28
|
|
|
25
29
|
ensure_valid_role
|
|
26
30
|
end
|
|
@@ -45,6 +49,30 @@ module RubyLLM
|
|
|
45
49
|
content if tool_result?
|
|
46
50
|
end
|
|
47
51
|
|
|
52
|
+
def input_tokens
|
|
53
|
+
tokens&.input
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def output_tokens
|
|
57
|
+
tokens&.output
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cached_tokens
|
|
61
|
+
tokens&.cached
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def cache_creation_tokens
|
|
65
|
+
tokens&.cache_creation
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def thinking_tokens
|
|
69
|
+
tokens&.thinking
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def reasoning_tokens
|
|
73
|
+
tokens&.thinking
|
|
74
|
+
end
|
|
75
|
+
|
|
48
76
|
def to_h
|
|
49
77
|
{
|
|
50
78
|
role: role,
|
|
@@ -53,11 +81,9 @@ module RubyLLM
|
|
|
53
81
|
tool_calls: tool_calls,
|
|
54
82
|
tool_call_id: tool_call_id,
|
|
55
83
|
conversation_id: conversation_id,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
cache_creation_tokens: cache_creation_tokens
|
|
60
|
-
}.compact
|
|
84
|
+
thinking: thinking&.text,
|
|
85
|
+
thinking_signature: thinking&.signature
|
|
86
|
+
}.merge(tokens ? tokens.to_h : {}).compact
|
|
61
87
|
end
|
|
62
88
|
|
|
63
89
|
def instance_variables
|
|
@@ -66,7 +92,9 @@ module RubyLLM
|
|
|
66
92
|
|
|
67
93
|
private
|
|
68
94
|
|
|
69
|
-
def normalize_content(content)
|
|
95
|
+
def normalize_content(content, role:, tool_calls:)
|
|
96
|
+
return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
|
|
97
|
+
|
|
70
98
|
case content
|
|
71
99
|
when String then Content.new(content)
|
|
72
100
|
when Hash then Content.new(content[:text], content)
|