ruby_llm_community 0.0.5 → 1.0.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -91
  3. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +34 -0
  4. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +5 -0
  5. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +6 -0
  6. data/lib/generators/ruby_llm/install_generator.rb +27 -2
  7. data/lib/ruby_llm/active_record/acts_as.rb +168 -24
  8. data/lib/ruby_llm/aliases.json +62 -5
  9. data/lib/ruby_llm/aliases.rb +7 -25
  10. data/lib/ruby_llm/chat.rb +10 -17
  11. data/lib/ruby_llm/configuration.rb +5 -12
  12. data/lib/ruby_llm/connection.rb +4 -4
  13. data/lib/ruby_llm/connection_multipart.rb +19 -0
  14. data/lib/ruby_llm/content.rb +5 -2
  15. data/lib/ruby_llm/embedding.rb +1 -2
  16. data/lib/ruby_llm/error.rb +0 -8
  17. data/lib/ruby_llm/image.rb +23 -8
  18. data/lib/ruby_llm/image_attachment.rb +21 -0
  19. data/lib/ruby_llm/message.rb +6 -6
  20. data/lib/ruby_llm/model/info.rb +12 -10
  21. data/lib/ruby_llm/model/pricing.rb +0 -3
  22. data/lib/ruby_llm/model/pricing_category.rb +0 -2
  23. data/lib/ruby_llm/model/pricing_tier.rb +0 -1
  24. data/lib/ruby_llm/models.json +2485 -676
  25. data/lib/ruby_llm/models.rb +65 -34
  26. data/lib/ruby_llm/provider.rb +8 -8
  27. data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -46
  28. data/lib/ruby_llm/providers/anthropic/chat.rb +2 -2
  29. data/lib/ruby_llm/providers/anthropic/media.rb +0 -1
  30. data/lib/ruby_llm/providers/anthropic/tools.rb +1 -2
  31. data/lib/ruby_llm/providers/anthropic.rb +1 -2
  32. data/lib/ruby_llm/providers/bedrock/chat.rb +2 -4
  33. data/lib/ruby_llm/providers/bedrock/media.rb +0 -1
  34. data/lib/ruby_llm/providers/bedrock/models.rb +0 -2
  35. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -12
  36. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -7
  37. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -12
  38. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -12
  39. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -13
  40. data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -18
  41. data/lib/ruby_llm/providers/bedrock.rb +1 -2
  42. data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -2
  43. data/lib/ruby_llm/providers/deepseek/chat.rb +0 -1
  44. data/lib/ruby_llm/providers/gemini/capabilities.rb +28 -100
  45. data/lib/ruby_llm/providers/gemini/chat.rb +57 -29
  46. data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -2
  47. data/lib/ruby_llm/providers/gemini/images.rb +1 -2
  48. data/lib/ruby_llm/providers/gemini/media.rb +0 -1
  49. data/lib/ruby_llm/providers/gemini/models.rb +1 -2
  50. data/lib/ruby_llm/providers/gemini/streaming.rb +15 -1
  51. data/lib/ruby_llm/providers/gemini/tools.rb +0 -5
  52. data/lib/ruby_llm/providers/gpustack/chat.rb +11 -1
  53. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  54. data/lib/ruby_llm/providers/gpustack/models.rb +44 -9
  55. data/lib/ruby_llm/providers/gpustack.rb +1 -0
  56. data/lib/ruby_llm/providers/mistral/capabilities.rb +2 -10
  57. data/lib/ruby_llm/providers/mistral/chat.rb +0 -2
  58. data/lib/ruby_llm/providers/mistral/embeddings.rb +0 -3
  59. data/lib/ruby_llm/providers/mistral/models.rb +0 -1
  60. data/lib/ruby_llm/providers/ollama/chat.rb +0 -1
  61. data/lib/ruby_llm/providers/ollama/media.rb +1 -6
  62. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  63. data/lib/ruby_llm/providers/ollama.rb +1 -0
  64. data/lib/ruby_llm/providers/openai/capabilities.rb +3 -16
  65. data/lib/ruby_llm/providers/openai/chat.rb +1 -3
  66. data/lib/ruby_llm/providers/openai/embeddings.rb +0 -3
  67. data/lib/ruby_llm/providers/openai/images.rb +73 -3
  68. data/lib/ruby_llm/providers/openai/media.rb +0 -1
  69. data/lib/ruby_llm/providers/openai/response.rb +120 -29
  70. data/lib/ruby_llm/providers/openai/response_media.rb +2 -2
  71. data/lib/ruby_llm/providers/openai/streaming.rb +107 -47
  72. data/lib/ruby_llm/providers/openai/tools.rb +1 -1
  73. data/lib/ruby_llm/providers/openai.rb +1 -3
  74. data/lib/ruby_llm/providers/openai_base.rb +2 -2
  75. data/lib/ruby_llm/providers/openrouter/models.rb +1 -16
  76. data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -1
  77. data/lib/ruby_llm/providers/perplexity/chat.rb +0 -1
  78. data/lib/ruby_llm/providers/perplexity.rb +1 -5
  79. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  80. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  81. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  82. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  83. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  84. data/lib/ruby_llm/railtie.rb +0 -1
  85. data/lib/ruby_llm/stream_accumulator.rb +72 -10
  86. data/lib/ruby_llm/streaming.rb +16 -25
  87. data/lib/ruby_llm/tool.rb +2 -19
  88. data/lib/ruby_llm/tool_call.rb +0 -9
  89. data/lib/ruby_llm/version.rb +1 -1
  90. data/lib/ruby_llm_community.rb +5 -3
  91. data/lib/tasks/models.rake +525 -0
  92. data/lib/tasks/release.rake +37 -2
  93. data/lib/tasks/vcr.rake +0 -7
  94. metadata +13 -4
  95. data/lib/tasks/aliases.rake +0 -235
  96. data/lib/tasks/models_docs.rake +0 -224
  97. data/lib/tasks/models_update.rake +0 -108
@@ -3,17 +3,19 @@
3
3
  module RubyLLM
4
4
  module ActiveRecord
5
5
  # Adds chat and message persistence capabilities to ActiveRecord models.
6
- # Provides a clean interface for storing chat history, message metadata,
7
- # and attachments in your database.
8
6
  module ActsAs
9
7
  extend ActiveSupport::Concern
10
8
 
11
9
  class_methods do # rubocop:disable Metrics/BlockLength
12
- def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall')
10
+ def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall',
11
+ model_class: 'Model', model_foreign_key: nil, model_primary_key: nil)
13
12
  include ChatMethods
14
13
 
15
14
  @message_class = message_class.to_s
16
15
  @tool_call_class = tool_call_class.to_s
16
+ @model_class = model_class.to_s
17
+ @model_foreign_key = model_foreign_key || ActiveSupport::Inflector.foreign_key(@model_class)
18
+ @model_primary_key = model_primary_key || 'model_id'
17
19
 
18
20
  has_many :messages,
19
21
  -> { order(created_at: :asc) },
@@ -21,13 +23,43 @@ module RubyLLM
21
23
  inverse_of: :chat,
22
24
  dependent: :destroy
23
25
 
26
+ # Set up model association if model registry is configured
27
+ if RubyLLM.config.model_registry_class
28
+ belongs_to :model,
29
+ class_name: @model_class,
30
+ foreign_key: @model_foreign_key,
31
+ primary_key: @model_primary_key,
32
+ optional: true
33
+ end
34
+
24
35
  delegate :add_message, to: :to_llm
25
36
  end
26
37
 
27
- def acts_as_message(chat_class: 'Chat',
38
+ def acts_as_model(chat_class: 'Chat')
39
+ include ModelMethods
40
+
41
+ @chat_class = chat_class.to_s
42
+
43
+ validates :model_id, presence: true, uniqueness: { scope: :provider }
44
+ validates :name, presence: true
45
+ validates :provider, presence: true
46
+
47
+ # Set up chat association if configured
48
+ return unless RubyLLM.config.model_registry_class
49
+
50
+ has_many :chats,
51
+ class_name: @chat_class,
52
+ foreign_key: 'model_id',
53
+ primary_key: 'model_id'
54
+ end
55
+
56
+ def acts_as_message(chat_class: 'Chat', # rubocop:disable Metrics/ParameterLists
28
57
  chat_foreign_key: nil,
29
58
  tool_call_class: 'ToolCall',
30
59
  tool_call_foreign_key: nil,
60
+ model_class: 'Model',
61
+ model_foreign_key: nil,
62
+ model_primary_key: nil,
31
63
  touch_chat: false)
32
64
  include MessageMethods
33
65
 
@@ -37,6 +69,10 @@ module RubyLLM
37
69
  @tool_call_class = tool_call_class.to_s
38
70
  @tool_call_foreign_key = tool_call_foreign_key || ActiveSupport::Inflector.foreign_key(@tool_call_class)
39
71
 
72
+ @model_class = model_class.to_s
73
+ @model_foreign_key = model_foreign_key || ActiveSupport::Inflector.foreign_key(@model_class)
74
+ @model_primary_key = model_primary_key || 'model_id'
75
+
40
76
  belongs_to :chat,
41
77
  class_name: @chat_class,
42
78
  foreign_key: @chat_foreign_key,
@@ -53,7 +89,21 @@ module RubyLLM
53
89
  optional: true,
54
90
  inverse_of: :result
55
91
 
56
- delegate :tool_call?, :tool_result?, :tool_results, to: :to_llm
92
+ has_many :tool_results,
93
+ through: :tool_calls,
94
+ source: :result,
95
+ class_name: @message_class
96
+
97
+ # Set up model association if model registry is configured
98
+ if RubyLLM.config.model_registry_class
99
+ belongs_to :model,
100
+ class_name: @model_class,
101
+ foreign_key: @model_foreign_key,
102
+ primary_key: @model_primary_key,
103
+ optional: true
104
+ end
105
+
106
+ delegate :tool_call?, :tool_result?, to: :to_llm
57
107
  end
58
108
 
59
109
  def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
@@ -75,8 +125,7 @@ module RubyLLM
75
125
  end
76
126
  end
77
127
 
78
- # Methods mixed into chat models to handle message persistence and
79
- # provide a conversation interface.
128
+ # Methods mixed into chat models.
80
129
  module ChatMethods
81
130
  extend ActiveSupport::Concern
82
131
 
@@ -85,10 +134,20 @@ module RubyLLM
85
134
  end
86
135
 
87
136
  def to_llm(context: nil)
137
+ # If we have a model association, use both model_id and provider
138
+ # Otherwise, model_id is a string that RubyLLM can resolve
139
+ if respond_to?(:model) && model
140
+ model_to_use = model.model_id
141
+ provider_to_use = model.provider.to_sym
142
+ else
143
+ model_to_use = model_id
144
+ provider_to_use = nil
145
+ end
146
+
88
147
  @chat ||= if context
89
- context.chat(model: model_id)
148
+ context.chat(model: model_to_use, provider: provider_to_use)
90
149
  else
91
- RubyLLM.chat(model: model_id)
150
+ RubyLLM.chat(model: model_to_use, provider: provider_to_use)
92
151
  end
93
152
  @chat.reset_messages!
94
153
 
@@ -148,6 +207,11 @@ module RubyLLM
148
207
  self
149
208
  end
150
209
 
210
+ def cache_prompts(...)
211
+ to_llm.cache_prompts(...)
212
+ self
213
+ end
214
+
151
215
  def on_new_message(&block)
152
216
  to_llm
153
217
 
@@ -206,26 +270,33 @@ module RubyLLM
206
270
  private
207
271
 
208
272
  def cleanup_failed_messages
209
- RubyLLM.logger.debug "RubyLLM: API call failed, destroying message: #{@message.id}"
273
+ RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
210
274
  @message.destroy
211
275
  end
212
276
 
213
- def cleanup_orphaned_tool_results
214
- loop do
215
- messages.reload
216
- last = messages.order(:id).last
277
+ def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
278
+ messages.reload
279
+ last = messages.order(:id).last
217
280
 
218
- break unless last&.tool_call? || last&.tool_result?
281
+ return unless last&.tool_call? || last&.tool_result?
219
282
 
283
+ if last.tool_call?
220
284
  last.destroy
285
+ elsif last.tool_result?
286
+ tool_call_message = last.parent_tool_call.message
287
+ expected_results = tool_call_message.tool_calls.pluck(:id)
288
+ actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
289
+
290
+ if expected_results.sort != actual_results.sort
291
+ tool_call_message.tool_results.each(&:destroy)
292
+ tool_call_message.destroy
293
+ end
221
294
  end
222
295
  end
223
296
 
224
297
  def setup_persistence_callbacks
225
- # Only set up once per chat instance
226
298
  return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
227
299
 
228
- # Set up persistence callbacks (user callbacks will be chained via on_new_message/on_end_message methods)
229
300
  @chat.on_new_message { persist_new_message }
230
301
  @chat.on_end_message { |msg| persist_message_completion(msg) }
231
302
 
@@ -237,15 +308,21 @@ module RubyLLM
237
308
  @message = messages.create!(role: :assistant, content: '')
238
309
  end
239
310
 
240
- def persist_message_completion(message)
311
+ def persist_message_completion(message) # rubocop:disable Metrics/PerceivedComplexity
241
312
  return unless message
242
313
 
243
314
  tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
244
315
 
245
316
  transaction do
246
- # Convert parsed JSON back to JSON string for storage
247
317
  content = message.content
248
- content = content.to_json if content.is_a?(Hash) || content.is_a?(Array)
318
+ attachments_to_persist = nil
319
+
320
+ if content.is_a?(RubyLLM::Content)
321
+ attachments_to_persist = content.attachments if content.attachments.any?
322
+ content = content.text
323
+ elsif content.is_a?(Hash) || content.is_a?(Array)
324
+ content = content.to_json
325
+ end
249
326
 
250
327
  @message.update!(
251
328
  role: message.role,
@@ -256,6 +333,8 @@ module RubyLLM
256
333
  )
257
334
  @message.write_attribute(@message.class.tool_call_foreign_key, tool_call_id) if tool_call_id
258
335
  @message.save!
336
+
337
+ persist_content(@message, attachments_to_persist) if attachments_to_persist
259
338
  persist_tool_calls(message.tool_calls) if message.tool_calls.present?
260
339
  end
261
340
  end
@@ -297,8 +376,7 @@ module RubyLLM
297
376
  def convert_to_active_storage_format(source)
298
377
  return if source.blank?
299
378
 
300
- # Let RubyLLM::Attachment handle the heavy lifting
301
- attachment = RubyLLM::Attachment.new(source)
379
+ attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
302
380
 
303
381
  {
304
382
  io: StringIO.new(attachment.content),
@@ -311,8 +389,7 @@ module RubyLLM
311
389
  end
312
390
  end
313
391
 
314
- # Methods mixed into message models to handle serialization and
315
- # provide a clean interface to the underlying message data.
392
+ # Methods mixed into message models.
316
393
  module MessageMethods
317
394
  extend ActiveSupport::Concern
318
395
 
@@ -378,5 +455,72 @@ module RubyLLM
378
455
  tempfile
379
456
  end
380
457
  end
458
+
459
+ # Methods mixed into model registry models.
460
+ module ModelMethods
461
+ extend ActiveSupport::Concern
462
+
463
+ class_methods do # rubocop:disable Metrics/BlockLength
464
+ def refresh!
465
+ RubyLLM.models.refresh!
466
+
467
+ transaction do
468
+ RubyLLM.models.all.each do |model_info|
469
+ model = find_or_initialize_by(
470
+ model_id: model_info.id,
471
+ provider: model_info.provider
472
+ )
473
+ model.update!(from_llm_attributes(model_info))
474
+ end
475
+ end
476
+ end
477
+
478
+ def from_llm(model_info)
479
+ new(from_llm_attributes(model_info))
480
+ end
481
+
482
+ private
483
+
484
+ def from_llm_attributes(model_info)
485
+ {
486
+ model_id: model_info.id,
487
+ name: model_info.name,
488
+ provider: model_info.provider,
489
+ family: model_info.family,
490
+ model_created_at: model_info.created_at,
491
+ context_window: model_info.context_window,
492
+ max_output_tokens: model_info.max_output_tokens,
493
+ knowledge_cutoff: model_info.knowledge_cutoff,
494
+ modalities: model_info.modalities.to_h,
495
+ capabilities: model_info.capabilities,
496
+ pricing: model_info.pricing.to_h,
497
+ metadata: model_info.metadata
498
+ }
499
+ end
500
+ end
501
+
502
+ def to_llm
503
+ RubyLLM::Model::Info.new(
504
+ id: model_id,
505
+ name: name,
506
+ provider: provider,
507
+ family: family,
508
+ created_at: model_created_at,
509
+ context_window: context_window,
510
+ max_output_tokens: max_output_tokens,
511
+ knowledge_cutoff: knowledge_cutoff,
512
+ modalities: modalities&.deep_symbolize_keys || {},
513
+ capabilities: capabilities,
514
+ pricing: pricing&.deep_symbolize_keys || {},
515
+ metadata: metadata&.deep_symbolize_keys || {}
516
+ )
517
+ end
518
+
519
+ delegate :supports?, :supports_vision?, :supports_functions?, :type,
520
+ :input_price_per_million, :output_price_per_million,
521
+ :function_calling?, :structured_output?, :batch?,
522
+ :reasoning?, :citations?, :streaming?,
523
+ to: :to_llm
524
+ end
381
525
  end
382
526
  end
@@ -50,21 +50,57 @@
50
50
  "deepseek": "deepseek-chat",
51
51
  "openrouter": "deepseek/deepseek-chat"
52
52
  },
53
+ "gemini-1.5-flash": {
54
+ "gemini": "gemini-1.5-flash",
55
+ "vertexai": "gemini-1.5-flash"
56
+ },
57
+ "gemini-1.5-flash-002": {
58
+ "gemini": "gemini-1.5-flash-002",
59
+ "vertexai": "gemini-1.5-flash-002"
60
+ },
61
+ "gemini-1.5-flash-8b": {
62
+ "gemini": "gemini-1.5-flash-8b",
63
+ "vertexai": "gemini-1.5-flash-8b"
64
+ },
65
+ "gemini-1.5-pro": {
66
+ "gemini": "gemini-1.5-pro",
67
+ "vertexai": "gemini-1.5-pro"
68
+ },
69
+ "gemini-1.5-pro-002": {
70
+ "gemini": "gemini-1.5-pro-002",
71
+ "vertexai": "gemini-1.5-pro-002"
72
+ },
73
+ "gemini-2.0-flash": {
74
+ "gemini": "gemini-2.0-flash",
75
+ "vertexai": "gemini-2.0-flash"
76
+ },
53
77
  "gemini-2.0-flash-001": {
54
78
  "gemini": "gemini-2.0-flash-001",
55
- "openrouter": "google/gemini-2.0-flash-001"
79
+ "openrouter": "google/gemini-2.0-flash-001",
80
+ "vertexai": "gemini-2.0-flash-001"
81
+ },
82
+ "gemini-2.0-flash-exp": {
83
+ "gemini": "gemini-2.0-flash-exp",
84
+ "vertexai": "gemini-2.0-flash-exp"
56
85
  },
57
86
  "gemini-2.0-flash-lite-001": {
58
87
  "gemini": "gemini-2.0-flash-lite-001",
59
- "openrouter": "google/gemini-2.0-flash-lite-001"
88
+ "openrouter": "google/gemini-2.0-flash-lite-001",
89
+ "vertexai": "gemini-2.0-flash-lite-001"
60
90
  },
61
91
  "gemini-2.5-flash": {
62
92
  "gemini": "gemini-2.5-flash",
63
- "openrouter": "google/gemini-2.5-flash"
93
+ "openrouter": "google/gemini-2.5-flash",
94
+ "vertexai": "gemini-2.5-flash"
95
+ },
96
+ "gemini-2.5-flash-image-preview": {
97
+ "gemini": "gemini-2.5-flash-image-preview",
98
+ "openrouter": "google/gemini-2.5-flash-image-preview"
64
99
  },
65
100
  "gemini-2.5-flash-lite": {
66
101
  "gemini": "gemini-2.5-flash-lite",
67
- "openrouter": "google/gemini-2.5-flash-lite"
102
+ "openrouter": "google/gemini-2.5-flash-lite",
103
+ "vertexai": "gemini-2.5-flash-lite"
68
104
  },
69
105
  "gemini-2.5-flash-lite-preview-06-17": {
70
106
  "gemini": "gemini-2.5-flash-lite-preview-06-17",
@@ -72,12 +108,21 @@
72
108
  },
73
109
  "gemini-2.5-pro": {
74
110
  "gemini": "gemini-2.5-pro",
75
- "openrouter": "google/gemini-2.5-pro"
111
+ "openrouter": "google/gemini-2.5-pro",
112
+ "vertexai": "gemini-2.5-pro"
76
113
  },
77
114
  "gemini-2.5-pro-preview-05-06": {
78
115
  "gemini": "gemini-2.5-pro-preview-05-06",
79
116
  "openrouter": "google/gemini-2.5-pro-preview-05-06"
80
117
  },
118
+ "gemini-embedding-001": {
119
+ "gemini": "gemini-embedding-001",
120
+ "vertexai": "gemini-embedding-001"
121
+ },
122
+ "gemini-exp-1206": {
123
+ "gemini": "gemini-exp-1206",
124
+ "vertexai": "gemini-exp-1206"
125
+ },
81
126
  "gemma-3-12b-it": {
82
127
  "gemini": "gemma-3-12b-it",
83
128
  "openrouter": "google/gemma-3-12b-it"
@@ -150,6 +195,10 @@
150
195
  "openai": "gpt-4o-2024-11-20",
151
196
  "openrouter": "openai/gpt-4o-2024-11-20"
152
197
  },
198
+ "gpt-4o-audio-preview": {
199
+ "openai": "gpt-4o-audio-preview",
200
+ "openrouter": "openai/gpt-4o-audio-preview"
201
+ },
153
202
  "gpt-4o-mini": {
154
203
  "openai": "gpt-4o-mini",
155
204
  "openrouter": "openai/gpt-4o-mini"
@@ -182,6 +231,10 @@
182
231
  "openai": "gpt-oss-120b",
183
232
  "openrouter": "openai/gpt-oss-120b"
184
233
  },
234
+ "gpt-oss-20b": {
235
+ "openai": "gpt-oss-20b",
236
+ "openrouter": "openai/gpt-oss-20b"
237
+ },
185
238
  "o1": {
186
239
  "openai": "o1",
187
240
  "openrouter": "openai/o1"
@@ -213,5 +266,9 @@
213
266
  "o4-mini": {
214
267
  "openai": "o4-mini",
215
268
  "openrouter": "openai/o4-mini"
269
+ },
270
+ "text-embedding-004": {
271
+ "gemini": "text-embedding-004",
272
+ "vertexai": "text-embedding-004"
216
273
  }
217
274
  }
@@ -1,53 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Manages model aliases, allowing users to reference models by simpler names
5
- # that map to specific model versions across different providers.
6
- #
7
- # Aliases are defined in aliases.json and follow the format:
8
- # {
9
- # "simple-name": {
10
- # "provider1": "specific-version-for-provider1",
11
- # "provider2": "specific-version-for-provider2"
12
- # }
13
- # }
4
+ # Manages model aliases for provider-specific versions
14
5
  class Aliases
15
6
  class << self
16
- # Resolves a model ID to its provider-specific version
17
- #
18
- # @param model_id [String] the model identifier or alias
19
- # @param provider_slug [String, Symbol, nil] optional provider to resolve for
20
- # @return [String] the resolved model ID or the original if no alias exists
21
7
  def resolve(model_id, provider = nil)
22
8
  return model_id unless aliases[model_id]
23
9
 
24
10
  if provider
25
11
  aliases[model_id][provider.to_s] || model_id
26
12
  else
27
- # Get native provider's version
28
13
  aliases[model_id].values.first || model_id
29
14
  end
30
15
  end
31
16
 
32
- # Returns the loaded aliases mapping
33
- # @return [Hash] the aliases mapping
34
17
  def aliases
35
18
  @aliases ||= load_aliases
36
19
  end
37
20
 
38
- # Loads aliases from the JSON file
39
- # @return [Hash] the loaded aliases
21
+ def aliases_file
22
+ File.expand_path('aliases.json', __dir__)
23
+ end
24
+
40
25
  def load_aliases
41
- file_path = File.expand_path('aliases.json', __dir__)
42
- if File.exist?(file_path)
43
- JSON.parse(File.read(file_path))
26
+ if File.exist?(aliases_file)
27
+ JSON.parse(File.read(aliases_file))
44
28
  else
45
29
  {}
46
30
  end
47
31
  end
48
32
 
49
- # Reloads aliases from disk
50
- # @return [Hash] the reloaded aliases
51
33
  def reload!
52
34
  @aliases = load_aliases
53
35
  end
data/lib/ruby_llm/chat.rb CHANGED
@@ -1,13 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Represents a conversation with an AI model. Handles message history,
5
- # streaming responses, and tool integration with a simple, conversational API.
6
- #
7
- # Example:
8
- # chat = RubyLLM.chat
9
- # chat.ask "What's the best way to learn Ruby?"
10
- # chat.ask "Can you elaborate on that?"
4
+ # Represents a conversation with an AI model
11
5
  class Chat
12
6
  include Enumerable
13
7
 
@@ -22,7 +16,7 @@ module RubyLLM
22
16
  @config = context&.config || RubyLLM.config
23
17
  model_id = model || @config.default_model
24
18
  with_model(model_id, provider: provider, assume_exists: assume_model_exists)
25
- @temperature = 0.7
19
+ @temperature = nil
26
20
  @messages = []
27
21
  @tools = {}
28
22
  @cache_prompts = { system: false, user: false, tools: false }
@@ -138,8 +132,8 @@ module RubyLLM
138
132
  messages,
139
133
  tools: @tools,
140
134
  temperature: @temperature,
141
- model: @model.id,
142
135
  cache_prompts: @cache_prompts.dup,
136
+ model: @model,
143
137
  params: @params,
144
138
  headers: @headers,
145
139
  schema: @schema,
@@ -148,7 +142,6 @@ module RubyLLM
148
142
 
149
143
  @on[:new_message]&.call unless block_given?
150
144
 
151
- # Parse JSON if schema was set
152
145
  if @schema && response.content.is_a?(String)
153
146
  begin
154
147
  response.content = JSON.parse(response.content)
@@ -177,6 +170,10 @@ module RubyLLM
177
170
  @messages.clear
178
171
  end
179
172
 
173
+ def instance_variables
174
+ super - %i[@connection @config]
175
+ end
176
+
180
177
  private
181
178
 
182
179
  def wrap_streaming_block(&block)
@@ -191,12 +188,11 @@ module RubyLLM
191
188
  @on[:new_message]&.call
192
189
  end
193
190
 
194
- # Pass chunk to user's block
195
191
  block.call chunk
196
192
  end
197
193
  end
198
194
 
199
- def handle_tool_calls(response, &)
195
+ def handle_tool_calls(response, &) # rubocop:disable Metrics/PerceivedComplexity
200
196
  halt_result = nil
201
197
 
202
198
  response.tool_calls.each_value do |tool_call|
@@ -204,7 +200,8 @@ module RubyLLM
204
200
  @on[:tool_call]&.call(tool_call)
205
201
  result = execute_tool tool_call
206
202
  @on[:tool_result]&.call(result)
207
- message = add_message role: :tool, content: result.to_s, tool_call_id: tool_call.id
203
+ content = result.is_a?(Content) ? result : result.to_s
204
+ message = add_message role: :tool, content:, tool_call_id: tool_call.id
208
205
  @on[:end_message]&.call(message)
209
206
 
210
207
  halt_result = result if result.is_a?(Tool::Halt)
@@ -218,9 +215,5 @@ module RubyLLM
218
215
  args = tool_call.arguments
219
216
  tool.call(args)
220
217
  end
221
-
222
- def instance_variables
223
- super - %i[@connection @config]
224
- end
225
218
  end
226
219
  end
@@ -1,16 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Global configuration for RubyLLM. Manages API keys, default models,
5
- # and provider-specific settings.
6
- #
7
- # Configure via:
8
- # RubyLLM.configure do |config|
9
- # config.openai_api_key = ENV['OPENAI_API_KEY']
10
- # config.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
11
- # end
4
+ # Global configuration for RubyLLM
12
5
  class Configuration
13
- # Provider-specific configuration
14
6
  attr_accessor :openai_api_key,
15
7
  :openai_api_base,
16
8
  :openai_organization_id,
@@ -18,6 +10,8 @@ module RubyLLM
18
10
  :openai_use_system_role,
19
11
  :anthropic_api_key,
20
12
  :gemini_api_key,
13
+ :vertexai_project_id,
14
+ :vertexai_location,
21
15
  :deepseek_api_key,
22
16
  :perplexity_api_key,
23
17
  :bedrock_api_key,
@@ -33,6 +27,8 @@ module RubyLLM
33
27
  :default_model,
34
28
  :default_embedding_model,
35
29
  :default_image_model,
30
+ # Model registry
31
+ :model_registry_class,
36
32
  # Connection configuration
37
33
  :request_timeout,
38
34
  :max_retries,
@@ -47,7 +43,6 @@ module RubyLLM
47
43
  :log_stream_debug
48
44
 
49
45
  def initialize
50
- # Connection configuration
51
46
  @request_timeout = 120
52
47
  @max_retries = 3
53
48
  @retry_interval = 0.1
@@ -55,12 +50,10 @@ module RubyLLM
55
50
  @retry_interval_randomness = 0.5
56
51
  @http_proxy = nil
57
52
 
58
- # Default models
59
53
  @default_model = 'gpt-4.1-nano'
60
54
  @default_embedding_model = 'text-embedding-3-small'
61
55
  @default_image_model = 'gpt-image-1'
62
56
 
63
- # Logging configuration
64
57
  @log_file = $stdout
65
58
  @log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
66
59
  @log_stream_debug = ENV['RUBYLLM_STREAM_DEBUG'] == 'true'
@@ -48,6 +48,10 @@ module RubyLLM
48
48
  end
49
49
  end
50
50
 
51
+ def instance_variables
52
+ super - %i[@config @connection]
53
+ end
54
+
51
55
  private
52
56
 
53
57
  def setup_timeout(faraday)
@@ -118,9 +122,5 @@ module RubyLLM
118
122
  raise ConfigurationError,
119
123
  "#{@provider.name} provider is not configured. Add this to your initialization:\n\n#{config_block}"
120
124
  end
121
-
122
- def instance_variables
123
- super - %i[@config @connection]
124
- end
125
125
  end
126
126
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # A connection that uses multipart/form-data for file uploads
5
+ class ConnectionMultipart < Connection
6
+ def post(url, payload, &)
7
+ @connection.post url, payload do |req|
8
+ req.headers.merge! @provider.headers if @provider.respond_to?(:headers)
9
+ req.headers['Content-Type'] = 'multipart/form-data'
10
+ yield req if block_given?
11
+ end
12
+ end
13
+
14
+ def setup_middleware(faraday)
15
+ super
16
+ faraday.request :multipart, content_type: 'multipart/form-data'
17
+ end
18
+ end
19
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  # Represents the content sent to or received from an LLM.
5
- # Selects the appropriate attachment class based on the content type.
6
5
  class Content
7
6
  attr_reader :text, :attachments
8
7
 
@@ -19,6 +18,11 @@ module RubyLLM
19
18
  self
20
19
  end
21
20
 
21
+ def attach(attachment)
22
+ @attachments << attachment
23
+ self
24
+ end
25
+
22
26
  def format
23
27
  if @text && @attachments.empty?
24
28
  @text
@@ -42,7 +46,6 @@ module RubyLLM
42
46
 
43
47
  def process_attachments(attachments)
44
48
  if attachments.is_a?(Hash)
45
- # Ignores types (like :image, :audio, :text, :pdf) since we have robust MIME type detection
46
49
  attachments.each_value { |attachment| process_attachments_array_or_string(attachment) }
47
50
  else
48
51
  process_attachments_array_or_string attachments