ruby_llm 1.15.0 → 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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
  4. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
  5. data/lib/ruby_llm/active_record/acts_as.rb +1 -26
  6. data/lib/ruby_llm/active_record/acts_as_legacy.rb +71 -4
  7. data/lib/ruby_llm/active_record/chat_methods.rb +2 -2
  8. data/lib/ruby_llm/active_record/message_methods.rb +70 -3
  9. data/lib/ruby_llm/agent.rb +1 -0
  10. data/lib/ruby_llm/aliases.json +78 -75
  11. data/lib/ruby_llm/aliases.rb +3 -0
  12. data/lib/ruby_llm/attachment.rb +34 -17
  13. data/lib/ruby_llm/chat.rb +176 -47
  14. data/lib/ruby_llm/configuration.rb +14 -1
  15. data/lib/ruby_llm/connection.rb +36 -7
  16. data/lib/ruby_llm/content.rb +15 -1
  17. data/lib/ruby_llm/deprecator.rb +24 -0
  18. data/lib/ruby_llm/embedding.rb +31 -1
  19. data/lib/ruby_llm/error.rb +11 -75
  20. data/lib/ruby_llm/error_middleware.rb +81 -0
  21. data/lib/ruby_llm/image.rb +2 -0
  22. data/lib/ruby_llm/instrumentation.rb +36 -0
  23. data/lib/ruby_llm/mime_type.rb +25 -0
  24. data/lib/ruby_llm/model/info.rb +36 -2
  25. data/lib/ruby_llm/model/pricing.rb +19 -9
  26. data/lib/ruby_llm/model/pricing_tier.rb +20 -9
  27. data/lib/ruby_llm/model_registry.rb +39 -0
  28. data/lib/ruby_llm/models.json +18225 -19144
  29. data/lib/ruby_llm/models.rb +95 -30
  30. data/lib/ruby_llm/provider.rb +11 -2
  31. data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
  32. data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
  33. data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
  34. data/lib/ruby_llm/providers/anthropic/tools.rb +28 -2
  35. data/lib/ruby_llm/providers/azure/media.rb +1 -1
  36. data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
  37. data/lib/ruby_llm/providers/bedrock/chat.rb +2 -0
  38. data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
  39. data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
  40. data/lib/ruby_llm/providers/bedrock/streaming.rb +6 -0
  41. data/lib/ruby_llm/providers/bedrock.rb +2 -2
  42. data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
  43. data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
  44. data/lib/ruby_llm/providers/gemini/chat.rb +2 -3
  45. data/lib/ruby_llm/providers/gemini/media.rb +16 -9
  46. data/lib/ruby_llm/providers/gemini/streaming.rb +2 -0
  47. data/lib/ruby_llm/providers/gemini/tools.rb +2 -0
  48. data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
  49. data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
  50. data/lib/ruby_llm/providers/mistral/capabilities.rb +1 -1
  51. data/lib/ruby_llm/providers/mistral/chat.rb +1 -1
  52. data/lib/ruby_llm/providers/mistral/media.rb +55 -0
  53. data/lib/ruby_llm/providers/mistral/models.rb +2 -0
  54. data/lib/ruby_llm/providers/mistral.rb +2 -2
  55. data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
  56. data/lib/ruby_llm/providers/openai/chat.rb +16 -1
  57. data/lib/ruby_llm/providers/openai/images.rb +9 -9
  58. data/lib/ruby_llm/providers/openai/media.rb +40 -16
  59. data/lib/ruby_llm/providers/openai/streaming.rb +2 -0
  60. data/lib/ruby_llm/providers/openai/tools.rb +2 -0
  61. data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
  62. data/lib/ruby_llm/providers/openrouter/chat.rb +6 -2
  63. data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
  64. data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
  65. data/lib/ruby_llm/providers/perplexity.rb +2 -2
  66. data/lib/ruby_llm/providers/vertexai.rb +5 -1
  67. data/lib/ruby_llm/providers/xai/chat.rb +9 -0
  68. data/lib/ruby_llm/providers/xai/models.rb +15 -27
  69. data/lib/ruby_llm/providers/xai.rb +2 -2
  70. data/lib/ruby_llm/railtie.rb +5 -1
  71. data/lib/ruby_llm/stream_accumulator.rb +45 -30
  72. data/lib/ruby_llm/streaming.rb +4 -0
  73. data/lib/ruby_llm/tool_concurrency.rb +105 -0
  74. data/lib/ruby_llm/transcription.rb +2 -1
  75. data/lib/ruby_llm/utils.rb +39 -0
  76. data/lib/ruby_llm/version.rb +1 -1
  77. data/lib/ruby_llm.rb +9 -2
  78. data/lib/tasks/models.rake +32 -4
  79. data/lib/tasks/release.rake +50 -23
  80. metadata +17 -10
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'base64'
3
4
  require 'pathname'
4
5
  require 'uri'
5
6
 
@@ -8,6 +9,16 @@ module RubyLLM
8
9
  class Attachment
9
10
  attr_reader :source, :filename, :mime_type
10
11
 
12
+ DOCUMENT_EXTENSIONS = %w[
13
+ doc docx dot key numbers odp ods odt pages pot pps ppt pptx rtf xls xlsx
14
+ ].freeze
15
+ ACTIVE_STORAGE_CLASS_NAMES = %w[
16
+ ActiveStorage::Blob
17
+ ActiveStorage::Attachment
18
+ ActiveStorage::Attached::One
19
+ ActiveStorage::Attached::Many
20
+ ].freeze
21
+
11
22
  def initialize(source, filename: nil)
12
23
  @source = source
13
24
  @source = source_type_cast
@@ -29,12 +40,7 @@ module RubyLLM
29
40
  end
30
41
 
31
42
  def active_storage?
32
- return false unless defined?(ActiveStorage)
33
-
34
- @source.is_a?(ActiveStorage::Blob) ||
35
- @source.is_a?(ActiveStorage::Attachment) ||
36
- @source.is_a?(ActiveStorage::Attached::One) ||
37
- @source.is_a?(ActiveStorage::Attached::Many)
43
+ ACTIVE_STORAGE_CLASS_NAMES.any? { |class_name| source_is_a?(class_name) }
38
44
  end
39
45
 
40
46
  def content
@@ -83,6 +89,7 @@ module RubyLLM
83
89
  return :audio if audio?
84
90
  return :pdf if pdf?
85
91
  return :text if text?
92
+ return :document if document?
86
93
 
87
94
  :unknown
88
95
  end
@@ -114,6 +121,17 @@ module RubyLLM
114
121
  RubyLLM::MimeType.pdf? mime_type
115
122
  end
116
123
 
124
+ def document?
125
+ return false if pdf? || text?
126
+
127
+ RubyLLM::MimeType.document?(mime_type) || DOCUMENT_EXTENSIONS.include?(extension)
128
+ end
129
+
130
+ def extension
131
+ extension = File.extname(filename.to_s).delete_prefix('.').downcase
132
+ extension.empty? ? nil : extension
133
+ end
134
+
117
135
  def text?
118
136
  RubyLLM::MimeType.text? mime_type
119
137
  end
@@ -147,8 +165,6 @@ module RubyLLM
147
165
  end
148
166
 
149
167
  def load_content_from_active_storage
150
- return unless defined?(ActiveStorage)
151
-
152
168
  @content = active_storage_blob&.download
153
169
  end
154
170
 
@@ -185,23 +201,24 @@ module RubyLLM
185
201
  end
186
202
 
187
203
  def extract_filename_from_active_storage
188
- return 'attachment' unless defined?(ActiveStorage)
189
-
190
204
  active_storage_blob&.filename&.to_s || 'attachment'
191
205
  end
192
206
 
193
207
  def active_storage_content_type
194
- return unless defined?(ActiveStorage)
195
-
196
208
  active_storage_blob&.content_type
197
209
  end
198
210
 
199
211
  def active_storage_blob
200
- case @source
201
- when ActiveStorage::Blob then @source
202
- when ActiveStorage::Attachment, ActiveStorage::Attached::One then @source.blob
203
- when ActiveStorage::Attached::Many then @source.blobs.first
204
- end
212
+ return @source if source_is_a?('ActiveStorage::Blob')
213
+ return @source.blob if source_is_a?('ActiveStorage::Attachment')
214
+ return @source.blob if source_is_a?('ActiveStorage::Attached::One')
215
+
216
+ @source.blobs.first if source_is_a?('ActiveStorage::Attached::Many')
217
+ end
218
+
219
+ def source_is_a?(class_name)
220
+ klass = RubyLLM::Utils.safe_constantize(class_name)
221
+ klass ? @source.is_a?(klass) : false
205
222
  end
206
223
  end
207
224
  end
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
@@ -52,19 +55,21 @@ module RubyLLM
52
55
  self
53
56
  end
54
57
 
55
- def with_tool(tool, choice: nil, calls: nil)
58
+ def with_tool(tool, choice: nil, calls: nil, concurrency: @concurrency)
56
59
  unless tool.nil?
57
60
  tool_instance = tool.is_a?(Class) ? tool.new : tool
58
61
  @tools[tool_instance.name.to_sym] = tool_instance
59
62
  end
60
63
  update_tool_options(choice:, calls:)
64
+ update_tool_concurrency(concurrency)
61
65
  self
62
66
  end
63
67
 
64
- def with_tools(*tools, replace: false, choice: nil, calls: nil)
68
+ def with_tools(*tools, replace: false, choice: nil, calls: nil, concurrency: @concurrency)
65
69
  @tools.clear if replace
66
70
  tools.compact.each { |tool| with_tool tool }
67
71
  update_tool_options(choice:, calls:)
72
+ update_tool_concurrency(concurrency)
68
73
  self
69
74
  end
70
75
 
@@ -150,41 +155,11 @@ module RubyLLM
150
155
  end
151
156
 
152
157
  def cost
153
- Cost.aggregate(messages.map(&:cost))
158
+ Cost.aggregate(messages.map { |message| message.cost(model: message.model_info || model) })
154
159
  end
155
160
 
156
161
  def complete(&)
157
- response = @provider.complete(
158
- messages,
159
- tools: @tools,
160
- tool_prefs: @tool_prefs,
161
- temperature: @temperature,
162
- model: @model,
163
- params: @params,
164
- headers: @headers,
165
- schema: @schema,
166
- thinking: @thinking,
167
- &wrap_streaming_block(&)
168
- )
169
-
170
- run_callbacks(:before_message, :new_message) unless block_given?
171
-
172
- if @schema && response.content.is_a?(String) && !response.tool_call?
173
- begin
174
- response.content = JSON.parse(response.content)
175
- rescue JSON::ParserError
176
- # If parsing fails, keep content as string
177
- end
178
- end
179
-
180
- add_message response
181
- run_callbacks(:after_message, :end_message, response)
182
-
183
- if response.tool_call?
184
- handle_tool_calls(response, &)
185
- else
186
- response
187
- end
162
+ instrument_completion(&)
188
163
  end
189
164
 
190
165
  def add_message(message_or_attributes)
@@ -193,6 +168,7 @@ module RubyLLM
193
168
  message
194
169
  end
195
170
 
171
+ # Mutates this chat by removing all in-memory messages.
196
172
  def reset_messages!
197
173
  @messages.clear
198
174
  end
@@ -243,6 +219,87 @@ module RubyLLM
243
219
  self
244
220
  end
245
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
+
246
303
  def set_legacy_callback(name, legacy_name, additive_name, &block)
247
304
  warn_legacy_callback_deprecation(legacy_name, additive_name) if block
248
305
 
@@ -251,7 +308,7 @@ module RubyLLM
251
308
  end
252
309
 
253
310
  def warn_legacy_callback_deprecation(legacy_name, additive_name)
254
- RubyLLM.logger.warn(
311
+ RubyLLM.deprecator.warn(
255
312
  "`#{legacy_name}` is deprecated and will be removed in RubyLLM 2.0. " \
256
313
  "Use `#{additive_name}` instead."
257
314
  )
@@ -273,23 +330,60 @@ module RubyLLM
273
330
  end
274
331
 
275
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)
276
344
  halt_result = nil
277
345
 
278
- response.tool_calls.each_value do |tool_call|
346
+ tool_calls.each_value do |tool_call|
279
347
  run_callbacks(:before_message, :new_message)
280
- run_callbacks(:before_tool_call, :tool_call, tool_call)
281
- result = execute_tool tool_call
282
- run_callbacks(:after_tool_result, :tool_result, result)
283
- tool_payload = result.is_a?(Tool::Halt) ? result.content : result
284
- content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
285
- message = add_message role: :tool, content:, tool_call_id: tool_call.id
286
- run_callbacks(:after_message, :end_message, 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
287
352
 
353
+ halt_result
354
+ end
355
+
356
+ def handle_concurrent_tool_calls(tool_calls)
357
+ halt_result = nil
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)
288
362
  halt_result = result if result.is_a?(Tool::Halt)
289
363
  end
290
364
 
291
- reset_tool_choice if forced_tool_choice?
292
- 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
293
387
  end
294
388
 
295
389
  def execute_tool(tool_call)
@@ -302,7 +396,26 @@ module RubyLLM
302
396
  end
303
397
 
304
398
  args = tool_call.arguments
305
- 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
306
419
  end
307
420
 
308
421
  def update_tool_options(choice:, calls:)
@@ -320,6 +433,22 @@ module RubyLLM
320
433
  @tool_prefs[:calls] = normalize_calls(calls) unless calls.nil?
321
434
  end
322
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
+
323
452
  def normalize_calls(calls)
324
453
  case calls
325
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)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Owns RubyLLM deprecation warnings so applications and tests can decide how
5
+ # aggressively to handle compatibility paths.
6
+ class Deprecator
7
+ def warn(message)
8
+ case RubyLLM.config.deprecation_behavior
9
+ when :silence
10
+ nil
11
+ when :raise
12
+ raise DeprecationError, message
13
+ else
14
+ RubyLLM.logger.warn(message)
15
+ end
16
+ end
17
+
18
+ def deprecate(name, replacement:, removal:)
19
+ warn("#{name} is deprecated and will be removed in RubyLLM #{removal}. Use #{replacement} instead.")
20
+ end
21
+ end
22
+
23
+ class DeprecationError < StandardError; end
24
+ end
@@ -23,7 +23,37 @@ module RubyLLM
23
23
  config: config)
24
24
  model_id = model.id
25
25
 
26
- provider_instance.embed(text, model: model_id, dimensions:)
26
+ payload = {
27
+ provider: provider_instance.slug,
28
+ provider_class: provider_instance.class.name,
29
+ model: model_id,
30
+ model_info: model,
31
+ input: text,
32
+ dimensions: dimensions
33
+ }
34
+
35
+ RubyLLM.instrument('embedding.ruby_llm', payload, config: config) do |event|
36
+ result = provider_instance.embed(text, model: model_id, dimensions:)
37
+ event[:result] = result
38
+ event[:response_model] = result.model
39
+ event[:input_tokens] = result.input_tokens
40
+ event[:embedding_dimensions] = vector_dimensions(result.vectors)
41
+ event[:embedding_count] = embedding_count(result.vectors)
42
+ result
43
+ end
44
+ end
45
+
46
+ def self.vector_dimensions(vectors)
47
+ return unless vectors.is_a?(Array)
48
+
49
+ vector = vectors.first.is_a?(Array) ? vectors.first : vectors
50
+ vector.length if vector.respond_to?(:length)
51
+ end
52
+
53
+ def self.embedding_count(vectors)
54
+ return unless vectors.is_a?(Array)
55
+
56
+ vectors.first.is_a?(Array) ? vectors.size : 1
27
57
  end
28
58
  end
29
59
  end