ruby_llm 1.14.1 → 1.16.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -7
  3. data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
  4. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
  5. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
  6. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
  7. data/lib/ruby_llm/active_record/acts_as.rb +4 -26
  8. data/lib/ruby_llm/active_record/acts_as_legacy.rb +123 -29
  9. data/lib/ruby_llm/active_record/chat_methods.rb +41 -24
  10. data/lib/ruby_llm/active_record/message_methods.rb +87 -4
  11. data/lib/ruby_llm/active_record/model_methods.rb +7 -9
  12. data/lib/ruby_llm/active_record/payload_helpers.rb +3 -0
  13. data/lib/ruby_llm/active_record/tool_call_methods.rb +3 -0
  14. data/lib/ruby_llm/agent.rb +4 -2
  15. data/lib/ruby_llm/aliases.json +108 -75
  16. data/lib/ruby_llm/aliases.rb +3 -0
  17. data/lib/ruby_llm/attachment.rb +41 -40
  18. data/lib/ruby_llm/chat.rb +229 -59
  19. data/lib/ruby_llm/configuration.rb +14 -1
  20. data/lib/ruby_llm/connection.rb +36 -7
  21. data/lib/ruby_llm/content.rb +15 -1
  22. data/lib/ruby_llm/cost.rb +224 -0
  23. data/lib/ruby_llm/deprecator.rb +24 -0
  24. data/lib/ruby_llm/embedding.rb +31 -1
  25. data/lib/ruby_llm/error.rb +11 -75
  26. data/lib/ruby_llm/error_middleware.rb +81 -0
  27. data/lib/ruby_llm/image.rb +39 -4
  28. data/lib/ruby_llm/instrumentation.rb +36 -0
  29. data/lib/ruby_llm/message.rb +20 -0
  30. data/lib/ruby_llm/mime_type.rb +25 -0
  31. data/lib/ruby_llm/model/info.rb +53 -2
  32. data/lib/ruby_llm/model/pricing.rb +19 -9
  33. data/lib/ruby_llm/model/pricing_category.rb +13 -2
  34. data/lib/ruby_llm/model/pricing_tier.rb +20 -9
  35. data/lib/ruby_llm/model_registry.rb +39 -0
  36. data/lib/ruby_llm/models.json +17817 -13942
  37. data/lib/ruby_llm/models.rb +97 -31
  38. data/lib/ruby_llm/models_schema.json +3 -0
  39. data/lib/ruby_llm/provider.rb +20 -4
  40. data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
  41. data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
  42. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  43. data/lib/ruby_llm/providers/anthropic/tools.rb +32 -3
  44. data/lib/ruby_llm/providers/azure/media.rb +1 -1
  45. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
  46. data/lib/ruby_llm/providers/bedrock/chat.rb +26 -13
  47. data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
  48. data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
  49. data/lib/ruby_llm/providers/bedrock/streaming.rb +10 -1
  50. data/lib/ruby_llm/providers/bedrock.rb +2 -2
  51. data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
  52. data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
  53. data/lib/ruby_llm/providers/gemini/chat.rb +10 -4
  54. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  55. data/lib/ruby_llm/providers/gemini/media.rb +16 -9
  56. data/lib/ruby_llm/providers/gemini/streaming.rb +6 -1
  57. data/lib/ruby_llm/providers/gemini/tools.rb +5 -1
  58. data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
  59. data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
  60. data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -2
  61. data/lib/ruby_llm/providers/mistral/chat.rb +56 -5
  62. data/lib/ruby_llm/providers/mistral/media.rb +55 -0
  63. data/lib/ruby_llm/providers/mistral/models.rb +2 -0
  64. data/lib/ruby_llm/providers/mistral.rb +2 -2
  65. data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
  66. data/lib/ruby_llm/providers/openai/capabilities.rb +82 -12
  67. data/lib/ruby_llm/providers/openai/chat.rb +61 -7
  68. data/lib/ruby_llm/providers/openai/images.rb +58 -6
  69. data/lib/ruby_llm/providers/openai/media.rb +40 -16
  70. data/lib/ruby_llm/providers/openai/streaming.rb +7 -6
  71. data/lib/ruby_llm/providers/openai/tools.rb +2 -0
  72. data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
  73. data/lib/ruby_llm/providers/openrouter/chat.rb +36 -8
  74. data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
  75. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  76. data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
  77. data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
  78. data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
  79. data/lib/ruby_llm/providers/perplexity.rb +2 -2
  80. data/lib/ruby_llm/providers/vertexai.rb +5 -1
  81. data/lib/ruby_llm/providers/xai/chat.rb +9 -0
  82. data/lib/ruby_llm/providers/xai/models.rb +15 -27
  83. data/lib/ruby_llm/providers/xai.rb +2 -2
  84. data/lib/ruby_llm/railtie.rb +11 -1
  85. data/lib/ruby_llm/stream_accumulator.rb +45 -30
  86. data/lib/ruby_llm/streaming.rb +4 -0
  87. data/lib/ruby_llm/tokens.rb +8 -0
  88. data/lib/ruby_llm/tool.rb +24 -7
  89. data/lib/ruby_llm/tool_concurrency.rb +105 -0
  90. data/lib/ruby_llm/transcription.rb +2 -1
  91. data/lib/ruby_llm/utils.rb +39 -0
  92. data/lib/ruby_llm/version.rb +1 -1
  93. data/lib/ruby_llm.rb +11 -6
  94. data/lib/tasks/models.rake +45 -16
  95. data/lib/tasks/release.rake +50 -23
  96. metadata +35 -13
data/lib/ruby_llm/chat.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module RubyLLM
4
6
  # Represents a conversation with an AI model
5
7
  class Chat
6
8
  include Enumerable
7
9
 
8
- attr_reader :model, :messages, :tools, :tool_prefs, :params, :headers, :schema
10
+ attr_reader :model, :messages, :tools, :tool_prefs, :params, :headers, :schema, :concurrency
9
11
 
10
12
  def initialize(model: nil, provider: nil, assume_model_exists: false, context: nil)
11
13
  if assume_model_exists && !provider
@@ -20,6 +22,7 @@ module RubyLLM
20
22
  @messages = []
21
23
  @tools = {}
22
24
  @tool_prefs = { choice: nil, calls: nil }
25
+ @concurrency = normalize_tool_concurrency(@config.tool_concurrency)
23
26
  @params = {}
24
27
  @headers = {}
25
28
  @schema = nil
@@ -30,6 +33,7 @@ module RubyLLM
30
33
  tool_call: nil,
31
34
  tool_result: nil
32
35
  }
36
+ @callbacks = Hash.new { |callbacks, name| callbacks[name] = [] }
33
37
  end
34
38
 
35
39
  def ask(message = nil, with: nil, &)
@@ -51,19 +55,21 @@ module RubyLLM
51
55
  self
52
56
  end
53
57
 
54
- def with_tool(tool, choice: nil, calls: nil)
58
+ def with_tool(tool, choice: nil, calls: nil, concurrency: @concurrency)
55
59
  unless tool.nil?
56
60
  tool_instance = tool.is_a?(Class) ? tool.new : tool
57
61
  @tools[tool_instance.name.to_sym] = tool_instance
58
62
  end
59
63
  update_tool_options(choice:, calls:)
64
+ update_tool_concurrency(concurrency)
60
65
  self
61
66
  end
62
67
 
63
- def with_tools(*tools, replace: false, choice: nil, calls: nil)
68
+ def with_tools(*tools, replace: false, choice: nil, calls: nil, concurrency: @concurrency)
64
69
  @tools.clear if replace
65
70
  tools.compact.each { |tool| with_tool tool }
66
71
  update_tool_options(choice:, calls:)
72
+ update_tool_concurrency(concurrency)
67
73
  self
68
74
  end
69
75
 
@@ -112,62 +118,48 @@ module RubyLLM
112
118
  self
113
119
  end
114
120
 
115
- def on_new_message(&block)
116
- @on[:new_message] = block
117
- self
121
+ def on_new_message(&)
122
+ set_legacy_callback(:new_message, :on_new_message, :before_message, &)
118
123
  end
119
124
 
120
- def on_end_message(&block)
121
- @on[:end_message] = block
122
- self
125
+ def on_end_message(&)
126
+ set_legacy_callback(:end_message, :on_end_message, :after_message, &)
123
127
  end
124
128
 
125
- def on_tool_call(&block)
126
- @on[:tool_call] = block
127
- self
129
+ def on_tool_call(&)
130
+ set_legacy_callback(:tool_call, :on_tool_call, :before_tool_call, &)
128
131
  end
129
132
 
130
- def on_tool_result(&block)
131
- @on[:tool_result] = block
132
- self
133
+ def on_tool_result(&)
134
+ set_legacy_callback(:tool_result, :on_tool_result, :after_tool_result, &)
133
135
  end
134
136
 
135
- def each(&)
136
- messages.each(&)
137
+ def before_message(&)
138
+ add_callback(:before_message, &)
137
139
  end
138
140
 
139
- def complete(&) # rubocop:disable Metrics/PerceivedComplexity
140
- response = @provider.complete(
141
- messages,
142
- tools: @tools,
143
- tool_prefs: @tool_prefs,
144
- temperature: @temperature,
145
- model: @model,
146
- params: @params,
147
- headers: @headers,
148
- schema: @schema,
149
- thinking: @thinking,
150
- &wrap_streaming_block(&)
151
- )
141
+ def after_message(&)
142
+ add_callback(:after_message, &)
143
+ end
152
144
 
153
- @on[:new_message]&.call unless block_given?
145
+ def before_tool_call(&)
146
+ add_callback(:before_tool_call, &)
147
+ end
154
148
 
155
- if @schema && response.content.is_a?(String) && !response.tool_call?
156
- begin
157
- response.content = JSON.parse(response.content)
158
- rescue JSON::ParserError
159
- # If parsing fails, keep content as string
160
- end
161
- end
149
+ def after_tool_result(&)
150
+ add_callback(:after_tool_result, &)
151
+ end
162
152
 
163
- add_message response
164
- @on[:end_message]&.call(response)
153
+ def each(&)
154
+ messages.each(&)
155
+ end
165
156
 
166
- if response.tool_call?
167
- handle_tool_calls(response, &)
168
- else
169
- response
170
- end
157
+ def cost
158
+ Cost.aggregate(messages.map { |message| message.cost(model: message.model_info || model) })
159
+ end
160
+
161
+ def complete(&)
162
+ instrument_completion(&)
171
163
  end
172
164
 
173
165
  def add_message(message_or_attributes)
@@ -176,6 +168,7 @@ module RubyLLM
176
168
  message
177
169
  end
178
170
 
171
+ # Mutates this chat by removing all in-memory messages.
179
172
  def reset_messages!
180
173
  @messages.clear
181
174
  end
@@ -221,34 +214,176 @@ module RubyLLM
221
214
  sanitized.empty? ? 'response' : sanitized
222
215
  end
223
216
 
217
+ def add_callback(name, &block)
218
+ @callbacks[name] << block if block
219
+ self
220
+ end
221
+
222
+ def complete_once(&)
223
+ response = provider_completion(&)
224
+
225
+ run_callbacks(:before_message, :new_message) unless block_given?
226
+
227
+ normalize_schema_response(response)
228
+
229
+ add_message response
230
+ run_callbacks(:after_message, :end_message, response)
231
+
232
+ if response.tool_call?
233
+ handle_tool_calls(response, &)
234
+ else
235
+ response
236
+ end
237
+ end
238
+
239
+ def instrument_completion(&block)
240
+ result = nil
241
+ streaming = block_given?
242
+ payload = {
243
+ chat: self,
244
+ provider: @provider.slug,
245
+ provider_class: @provider.class.name,
246
+ model: @model.id,
247
+ model_info: @model,
248
+ input_messages: messages.dup,
249
+ message_count: messages.size,
250
+ tools: tools.keys,
251
+ tool_choice: tool_prefs[:choice],
252
+ tool_call_limit: tool_prefs[:calls],
253
+ temperature: @temperature,
254
+ params: params,
255
+ schema: schema,
256
+ thinking: @thinking,
257
+ streaming: streaming
258
+ }
259
+
260
+ RubyLLM.instrument('chat.ruby_llm', payload, config: @config) do |event|
261
+ result = complete_once(&block)
262
+ event[:response] = result
263
+ event[:messages_after] = messages.dup
264
+ event[:response_role] = result.role if result.respond_to?(:role)
265
+
266
+ if result.respond_to?(:tool_call?)
267
+ event[:response_model] = result.model_id
268
+ event[:tool_call] = result.tool_call?
269
+ event[:tool_calls] = result.tool_calls
270
+ event[:input_tokens] = result.input_tokens
271
+ event[:output_tokens] = result.output_tokens
272
+ event[:cached_tokens] = result.cached_tokens
273
+ event[:cache_creation_tokens] = result.cache_creation_tokens
274
+ event[:thinking_tokens] = result.thinking_tokens
275
+ end
276
+ end
277
+ result
278
+ end
279
+
280
+ def provider_completion(&)
281
+ @provider.complete(
282
+ messages,
283
+ tools: @tools,
284
+ tool_prefs: @tool_prefs,
285
+ temperature: @temperature,
286
+ model: @model,
287
+ params: @params,
288
+ headers: @headers,
289
+ schema: @schema,
290
+ thinking: @thinking,
291
+ &wrap_streaming_block(&)
292
+ )
293
+ end
294
+
295
+ def normalize_schema_response(response)
296
+ return unless @schema && response.content.is_a?(String) && !response.tool_call?
297
+
298
+ response.content = JSON.parse(response.content)
299
+ rescue JSON::ParserError
300
+ # If parsing fails, keep content as string.
301
+ end
302
+
303
+ def set_legacy_callback(name, legacy_name, additive_name, &block)
304
+ warn_legacy_callback_deprecation(legacy_name, additive_name) if block
305
+
306
+ @on[name] = block
307
+ self
308
+ end
309
+
310
+ def warn_legacy_callback_deprecation(legacy_name, additive_name)
311
+ RubyLLM.deprecator.warn(
312
+ "`#{legacy_name}` is deprecated and will be removed in RubyLLM 2.0. " \
313
+ "Use `#{additive_name}` instead."
314
+ )
315
+ end
316
+
317
+ def run_callbacks(name, legacy_name, *args)
318
+ @callbacks[name].each { |callback| callback.call(*args) }
319
+ @on[legacy_name]&.call(*args)
320
+ end
321
+
224
322
  def wrap_streaming_block(&block)
225
323
  return nil unless block_given?
226
324
 
227
- @on[:new_message]&.call
325
+ run_callbacks(:before_message, :new_message)
228
326
 
229
327
  proc do |chunk|
230
328
  block.call chunk
231
329
  end
232
330
  end
233
331
 
234
- def handle_tool_calls(response, &) # rubocop:disable Metrics/PerceivedComplexity
332
+ def handle_tool_calls(response, &)
333
+ halt_result = if concurrency
334
+ handle_concurrent_tool_calls(response.tool_calls)
335
+ else
336
+ handle_sequential_tool_calls(response.tool_calls)
337
+ end
338
+
339
+ reset_tool_choice if forced_tool_choice?
340
+ halt_result || complete(&)
341
+ end
342
+
343
+ def handle_sequential_tool_calls(tool_calls)
235
344
  halt_result = nil
236
345
 
237
- response.tool_calls.each_value do |tool_call|
238
- @on[:new_message]&.call
239
- @on[:tool_call]&.call(tool_call)
240
- result = execute_tool tool_call
241
- @on[:tool_result]&.call(result)
242
- tool_payload = result.is_a?(Tool::Halt) ? result.content : result
243
- content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
244
- message = add_message role: :tool, content:, tool_call_id: tool_call.id
245
- @on[:end_message]&.call(message)
346
+ tool_calls.each_value do |tool_call|
347
+ run_callbacks(:before_message, :new_message)
348
+ result = execute_tool_with_callbacks(tool_call)
349
+ add_tool_result_message(tool_call, result)
350
+ halt_result = result if result.is_a?(Tool::Halt)
351
+ end
352
+
353
+ halt_result
354
+ end
355
+
356
+ def handle_concurrent_tool_calls(tool_calls)
357
+ halt_result = nil
246
358
 
359
+ execute_tools_concurrently(tool_calls) do |tool_call, result|
360
+ run_callbacks(:before_message, :new_message)
361
+ add_tool_result_message(tool_call, result)
247
362
  halt_result = result if result.is_a?(Tool::Halt)
248
363
  end
249
364
 
250
- reset_tool_choice if forced_tool_choice?
251
- halt_result || complete(&)
365
+ halt_result
366
+ end
367
+
368
+ def execute_tools_concurrently(tool_calls, &on_result)
369
+ ToolConcurrency.run(concurrency, tool_calls, on_result:) do |tool_call|
370
+ execute_tool_with_callbacks(tool_call)
371
+ end
372
+ end
373
+
374
+ def execute_tool_with_callbacks(tool_call)
375
+ run_callbacks(:before_tool_call, :tool_call, tool_call)
376
+ result = execute_tool tool_call
377
+ run_callbacks(:after_tool_result, :tool_result, result)
378
+ result
379
+ end
380
+
381
+ def add_tool_result_message(tool_call, result)
382
+ tool_payload = result.is_a?(Tool::Halt) ? result.content : result
383
+ content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
384
+ message = add_message role: :tool, content:, tool_call_id: tool_call.id
385
+ run_callbacks(:after_message, :end_message, message)
386
+ message
252
387
  end
253
388
 
254
389
  def execute_tool(tool_call)
@@ -261,7 +396,26 @@ module RubyLLM
261
396
  end
262
397
 
263
398
  args = tool_call.arguments
264
- tool.call(args)
399
+ payload = {
400
+ chat: self,
401
+ provider: @provider.slug,
402
+ provider_class: @provider.class.name,
403
+ model: @model.id,
404
+ model_info: @model,
405
+ tool: tool,
406
+ tool_call: tool_call,
407
+ tool_name: tool.name,
408
+ tool_arguments: args,
409
+ tool_call_id: tool_call.id
410
+ }
411
+
412
+ RubyLLM.instrument('tool_call.ruby_llm', payload, config: @config) do |event|
413
+ result = tool.call(args)
414
+ event[:result] = result
415
+ event[:result_content] = result.is_a?(Tool::Halt) ? result.content : result
416
+ event[:result_class] = result.class.name
417
+ result
418
+ end
265
419
  end
266
420
 
267
421
  def update_tool_options(choice:, calls:)
@@ -279,6 +433,22 @@ module RubyLLM
279
433
  @tool_prefs[:calls] = normalize_calls(calls) unless calls.nil?
280
434
  end
281
435
 
436
+ def update_tool_concurrency(concurrency)
437
+ @concurrency = normalize_tool_concurrency(concurrency)
438
+ end
439
+
440
+ def normalize_tool_concurrency(concurrency)
441
+ return nil if concurrency.nil? || concurrency == false
442
+ return :threads if concurrency == true
443
+
444
+ normalized = concurrency.to_sym
445
+ return normalized if ToolConcurrency.supported?(normalized)
446
+
447
+ raise ArgumentError,
448
+ "Unknown tool concurrency: #{concurrency.inspect}. " \
449
+ "Available modes: #{ToolConcurrency.modes.join(', ')}"
450
+ end
451
+
282
452
  def normalize_calls(calls)
283
453
  case calls
284
454
  when :many, 'many'
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logger'
4
+
3
5
  module RubyLLM
4
6
  # Global configuration for RubyLLM
5
7
  class Configuration
@@ -9,7 +11,13 @@ module RubyLLM
9
11
  key = key.to_sym
10
12
  return if options.include?(key)
11
13
 
12
- send(:attr_accessor, key)
14
+ attr_reader key
15
+
16
+ define_method("#{key}=") do |value|
17
+ value = nil if value.is_a?(String) && value.strip.empty?
18
+ instance_variable_set(:"@#{key}", value)
19
+ end
20
+
13
21
  option_keys << key
14
22
  defaults[key] = default
15
23
  end
@@ -42,6 +50,7 @@ module RubyLLM
42
50
  option :model_registry_class, 'Model'
43
51
 
44
52
  option :use_new_acts_as, false
53
+ option :model_registry_source, nil
45
54
 
46
55
  option :request_timeout, 300
47
56
  option :max_retries, 3
@@ -49,8 +58,12 @@ module RubyLLM
49
58
  option :retry_backoff_factor, 2
50
59
  option :retry_interval_randomness, 0.5
51
60
  option :http_proxy, nil
61
+ option :tool_concurrency, false
52
62
 
53
63
  option :logger, nil
64
+ option :instrumenter, nil
65
+ option :deprecation_behavior, :warn
66
+ option :faraday_adapter, :net_http
54
67
  option :log_file, -> { $stdout }
55
68
  option :log_level, -> { ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO }
56
69
  option :log_stream_debug, -> { ENV['RUBYLLM_STREAM_DEBUG'] == 'true' }
@@ -1,5 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'faraday'
4
+ require 'faraday/multipart'
5
+ require 'faraday/retry'
6
+ require 'ruby_llm/error_middleware'
7
+ require 'timeout'
8
+
3
9
  module RubyLLM
4
10
  # Connection class for managing API connections to various providers.
5
11
  class Connection
@@ -33,16 +39,20 @@ module RubyLLM
33
39
  end
34
40
 
35
41
  def post(url, payload, &)
36
- @connection.post url, payload do |req|
37
- req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
38
- yield req if block_given?
42
+ instrument_request(:post, url) do
43
+ @connection.post url, payload do |req|
44
+ req.headers.merge! provider_headers
45
+ yield req if block_given?
46
+ end
39
47
  end
40
48
  end
41
49
 
42
50
  def get(url, &)
43
- @connection.get url do |req|
44
- req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
45
- yield req if block_given?
51
+ instrument_request(:get, url) do
52
+ @connection.get url do |req|
53
+ req.headers.merge! provider_headers
54
+ yield req if block_given?
55
+ end
46
56
  end
47
57
  end
48
58
 
@@ -52,6 +62,24 @@ module RubyLLM
52
62
 
53
63
  private
54
64
 
65
+ def provider_headers
66
+ @provider.respond_to?(:headers) ? @provider.headers : {}
67
+ end
68
+
69
+ def instrument_request(method, url)
70
+ payload = {
71
+ provider: @provider.respond_to?(:slug) ? @provider.slug : @provider.class.name,
72
+ method: method,
73
+ url: url
74
+ }
75
+
76
+ RubyLLM.instrument('request.ruby_llm', payload, config: @config) do
77
+ response = yield
78
+ payload[:status] = response.status if response.respond_to?(:status)
79
+ response
80
+ end
81
+ end
82
+
55
83
  def setup_timeout(faraday)
56
84
  faraday.options.timeout = @config.request_timeout
57
85
  end
@@ -89,7 +117,8 @@ module RubyLLM
89
117
  faraday.request :multipart
90
118
  faraday.request :json
91
119
  faraday.response :json
92
- faraday.adapter :net_http
120
+ adapter = @config.respond_to?(:faraday_adapter) ? @config.faraday_adapter : :net_http
121
+ faraday.adapter(adapter || :net_http)
93
122
  faraday.use :llm_errors, provider: @provider
94
123
  end
95
124
 
@@ -14,7 +14,7 @@ module RubyLLM
14
14
  end
15
15
 
16
16
  def add_attachment(source, filename: nil)
17
- @attachments << Attachment.new(source, filename:)
17
+ @attachments << build_attachment(source, filename:)
18
18
  self
19
19
  end
20
20
 
@@ -26,6 +26,10 @@ module RubyLLM
26
26
  end
27
27
  end
28
28
 
29
+ def empty?
30
+ attachments.empty? && (@text.nil? || (@text.respond_to?(:empty?) && @text.empty?))
31
+ end
32
+
29
33
  # For Rails serialization
30
34
  def to_h
31
35
  { text: @text, attachments: @attachments.map(&:to_h) }
@@ -33,6 +37,16 @@ module RubyLLM
33
37
 
34
38
  private
35
39
 
40
+ def build_attachment(source, filename: nil)
41
+ if source.is_a?(Attachment)
42
+ return source unless filename
43
+
44
+ return Attachment.new(source.source, filename:)
45
+ end
46
+
47
+ Attachment.new(source, filename:)
48
+ end
49
+
36
50
  def process_attachments_array_or_string(attachments)
37
51
  Utils.to_safe_array(attachments).each do |file|
38
52
  next if blank_attachment_entry?(file)