ruby_llm 1.12.0 → 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 -4
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +85 -20
- data/lib/ruby_llm/active_record/chat_methods.rb +67 -16
- data/lib/ruby_llm/agent.rb +39 -8
- 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 +6 -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 +8 -5
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
|
|
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
|
@@ -14,13 +14,16 @@ module RubyLLM
|
|
|
14
14
|
|
|
15
15
|
# Error classes for non-HTTP errors
|
|
16
16
|
class ConfigurationError < StandardError; end
|
|
17
|
+
class PromptNotFoundError < StandardError; end
|
|
17
18
|
class InvalidRoleError < StandardError; end
|
|
19
|
+
class InvalidToolChoiceError < StandardError; end
|
|
18
20
|
class ModelNotFoundError < StandardError; end
|
|
19
21
|
class UnsupportedAttachmentError < StandardError; end
|
|
20
22
|
|
|
21
23
|
# Error classes for different HTTP status codes
|
|
22
24
|
class BadRequestError < Error; end
|
|
23
25
|
class ForbiddenError < Error; end
|
|
26
|
+
class ContextLengthExceededError < Error; end
|
|
24
27
|
class OverloadedError < Error; end
|
|
25
28
|
class PaymentRequiredError < Error; end
|
|
26
29
|
class RateLimitError < Error; end
|
|
@@ -42,6 +45,18 @@ module RubyLLM
|
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
class << self
|
|
48
|
+
CONTEXT_LENGTH_PATTERNS = [
|
|
49
|
+
/context length/i,
|
|
50
|
+
/context window/i,
|
|
51
|
+
/maximum context/i,
|
|
52
|
+
/request too large/i,
|
|
53
|
+
/too many tokens/i,
|
|
54
|
+
/token count exceeds/i,
|
|
55
|
+
/input[_\s-]?token/i,
|
|
56
|
+
/input or output tokens? must be reduced/i,
|
|
57
|
+
/reduce the length of messages/i
|
|
58
|
+
].freeze
|
|
59
|
+
|
|
45
60
|
def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
|
|
46
61
|
message = provider&.parse_error(response)
|
|
47
62
|
|
|
@@ -49,6 +64,10 @@ module RubyLLM
|
|
|
49
64
|
when 200..399
|
|
50
65
|
message
|
|
51
66
|
when 400
|
|
67
|
+
if context_length_exceeded?(message)
|
|
68
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
69
|
+
end
|
|
70
|
+
|
|
52
71
|
raise BadRequestError.new(response, message || 'Invalid request - please check your input')
|
|
53
72
|
when 401
|
|
54
73
|
raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
|
|
@@ -58,10 +77,14 @@ module RubyLLM
|
|
|
58
77
|
raise ForbiddenError.new(response,
|
|
59
78
|
message || 'Forbidden - you do not have permission to access this resource')
|
|
60
79
|
when 429
|
|
80
|
+
if context_length_exceeded?(message)
|
|
81
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
82
|
+
end
|
|
83
|
+
|
|
61
84
|
raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
|
|
62
85
|
when 500
|
|
63
86
|
raise ServerError.new(response, message || 'API server error - please try again')
|
|
64
|
-
when 502..
|
|
87
|
+
when 502..504
|
|
65
88
|
raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
|
|
66
89
|
when 529
|
|
67
90
|
raise OverloadedError.new(response, message || 'Service overloaded - please try again later')
|
|
@@ -69,6 +92,14 @@ module RubyLLM
|
|
|
69
92
|
raise Error.new(response, message || 'An unknown error occurred')
|
|
70
93
|
end
|
|
71
94
|
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def context_length_exceeded?(message)
|
|
99
|
+
return false if message.to_s.empty?
|
|
100
|
+
|
|
101
|
+
CONTEXT_LENGTH_PATTERNS.any? { |pattern| message.match?(pattern) }
|
|
102
|
+
end
|
|
72
103
|
end
|
|
73
104
|
end
|
|
74
105
|
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)
|
data/lib/ruby_llm/model/info.rb
CHANGED
|
@@ -24,7 +24,7 @@ module RubyLLM
|
|
|
24
24
|
@name = data[:name]
|
|
25
25
|
@provider = data[:provider]
|
|
26
26
|
@family = data[:family]
|
|
27
|
-
@created_at = Utils.to_time(data[:created_at])
|
|
27
|
+
@created_at = Utils.to_time(data[:created_at])&.utc
|
|
28
28
|
@context_window = data[:context_window]
|
|
29
29
|
@max_output_tokens = data[:max_output_tokens]
|
|
30
30
|
@knowledge_cutoff = Utils.to_date(data[:knowledge_cutoff])
|