ruby_llm_swarm 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +175 -0
  4. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +187 -0
  5. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
  6. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  8. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  10. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +13 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +7 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +28 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
  22. data/lib/generators/ruby_llm/generator_helpers.rb +194 -0
  23. data/lib/generators/ruby_llm/install/install_generator.rb +106 -0
  24. data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
  25. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  26. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +7 -0
  27. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  28. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +45 -0
  29. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +20 -0
  30. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +12 -0
  31. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  32. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  33. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  34. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
  35. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +124 -0
  36. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  37. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  38. data/lib/ruby_llm/active_record/acts_as.rb +174 -0
  39. data/lib/ruby_llm/active_record/acts_as_legacy.rb +384 -0
  40. data/lib/ruby_llm/active_record/chat_methods.rb +350 -0
  41. data/lib/ruby_llm/active_record/message_methods.rb +81 -0
  42. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  43. data/lib/ruby_llm/aliases.json +295 -0
  44. data/lib/ruby_llm/aliases.rb +38 -0
  45. data/lib/ruby_llm/attachment.rb +220 -0
  46. data/lib/ruby_llm/chat.rb +816 -0
  47. data/lib/ruby_llm/chunk.rb +6 -0
  48. data/lib/ruby_llm/configuration.rb +78 -0
  49. data/lib/ruby_llm/connection.rb +126 -0
  50. data/lib/ruby_llm/content.rb +73 -0
  51. data/lib/ruby_llm/context.rb +29 -0
  52. data/lib/ruby_llm/embedding.rb +29 -0
  53. data/lib/ruby_llm/error.rb +84 -0
  54. data/lib/ruby_llm/image.rb +49 -0
  55. data/lib/ruby_llm/message.rb +86 -0
  56. data/lib/ruby_llm/mime_type.rb +71 -0
  57. data/lib/ruby_llm/model/info.rb +111 -0
  58. data/lib/ruby_llm/model/modalities.rb +22 -0
  59. data/lib/ruby_llm/model/pricing.rb +48 -0
  60. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  61. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  62. data/lib/ruby_llm/model.rb +7 -0
  63. data/lib/ruby_llm/models.json +33198 -0
  64. data/lib/ruby_llm/models.rb +231 -0
  65. data/lib/ruby_llm/models_schema.json +168 -0
  66. data/lib/ruby_llm/moderation.rb +56 -0
  67. data/lib/ruby_llm/provider.rb +243 -0
  68. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  69. data/lib/ruby_llm/providers/anthropic/chat.rb +125 -0
  70. data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
  71. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  72. data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
  73. data/lib/ruby_llm/providers/anthropic/models.rb +63 -0
  74. data/lib/ruby_llm/providers/anthropic/streaming.rb +45 -0
  75. data/lib/ruby_llm/providers/anthropic/tools.rb +109 -0
  76. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  77. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  78. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  79. data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
  80. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  81. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  82. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  83. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +71 -0
  84. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  85. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +80 -0
  86. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  87. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  88. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  89. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  90. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  91. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  92. data/lib/ruby_llm/providers/gemini/capabilities.rb +281 -0
  93. data/lib/ruby_llm/providers/gemini/chat.rb +454 -0
  94. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  95. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  96. data/lib/ruby_llm/providers/gemini/media.rb +112 -0
  97. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  98. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  99. data/lib/ruby_llm/providers/gemini/tools.rb +198 -0
  100. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  101. data/lib/ruby_llm/providers/gemini.rb +37 -0
  102. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  103. data/lib/ruby_llm/providers/gpustack/media.rb +46 -0
  104. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  105. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  106. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  107. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  108. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  109. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  110. data/lib/ruby_llm/providers/mistral.rb +32 -0
  111. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  112. data/lib/ruby_llm/providers/ollama/media.rb +46 -0
  113. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  114. data/lib/ruby_llm/providers/ollama.rb +30 -0
  115. data/lib/ruby_llm/providers/openai/capabilities.rb +299 -0
  116. data/lib/ruby_llm/providers/openai/chat.rb +88 -0
  117. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  118. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  119. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  120. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  121. data/lib/ruby_llm/providers/openai/moderation.rb +34 -0
  122. data/lib/ruby_llm/providers/openai/streaming.rb +46 -0
  123. data/lib/ruby_llm/providers/openai/tools.rb +98 -0
  124. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  125. data/lib/ruby_llm/providers/openai.rb +44 -0
  126. data/lib/ruby_llm/providers/openai_responses.rb +395 -0
  127. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  128. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  129. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  130. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  131. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  132. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  133. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  134. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  135. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  136. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  137. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  138. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  139. data/lib/ruby_llm/railtie.rb +35 -0
  140. data/lib/ruby_llm/responses_session.rb +77 -0
  141. data/lib/ruby_llm/stream_accumulator.rb +101 -0
  142. data/lib/ruby_llm/streaming.rb +153 -0
  143. data/lib/ruby_llm/tool.rb +209 -0
  144. data/lib/ruby_llm/tool_call.rb +22 -0
  145. data/lib/ruby_llm/tool_executors.rb +125 -0
  146. data/lib/ruby_llm/transcription.rb +35 -0
  147. data/lib/ruby_llm/utils.rb +91 -0
  148. data/lib/ruby_llm/version.rb +5 -0
  149. data/lib/ruby_llm.rb +140 -0
  150. data/lib/tasks/models.rake +525 -0
  151. data/lib/tasks/release.rake +67 -0
  152. data/lib/tasks/ruby_llm.rake +15 -0
  153. data/lib/tasks/vcr.rake +92 -0
  154. metadata +346 -0
@@ -0,0 +1,350 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # Methods mixed into chat models.
6
+ module ChatMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ before_save :resolve_model_from_strings
11
+ end
12
+
13
+ attr_accessor :assume_model_exists, :context
14
+
15
+ def model=(value)
16
+ @model_string = value if value.is_a?(String)
17
+ return if value.is_a?(String)
18
+
19
+ if self.class.model_association_name == :model
20
+ super
21
+ else
22
+ self.model_association = value
23
+ end
24
+ end
25
+
26
+ def model_id=(value)
27
+ @model_string = value
28
+ end
29
+
30
+ def model_id
31
+ model_association&.model_id
32
+ end
33
+
34
+ def provider=(value)
35
+ @provider_string = value
36
+ end
37
+
38
+ def provider
39
+ model_association&.provider
40
+ end
41
+
42
+ private
43
+
44
+ def resolve_model_from_strings # rubocop:disable Metrics/PerceivedComplexity
45
+ config = context&.config || RubyLLM.config
46
+ @model_string ||= config.default_model unless model_association
47
+ return unless @model_string
48
+
49
+ model_info, _provider = Models.resolve(
50
+ @model_string,
51
+ provider: @provider_string,
52
+ assume_exists: assume_model_exists || false,
53
+ config: config
54
+ )
55
+
56
+ model_class = self.class.model_class.constantize
57
+ model_record = model_class.find_or_create_by!(
58
+ model_id: model_info.id,
59
+ provider: model_info.provider
60
+ ) do |m|
61
+ m.name = model_info.name || model_info.id
62
+ m.family = model_info.family
63
+ m.context_window = model_info.context_window
64
+ m.max_output_tokens = model_info.max_output_tokens
65
+ m.capabilities = model_info.capabilities || []
66
+ m.modalities = model_info.modalities || {}
67
+ m.pricing = model_info.pricing || {}
68
+ m.metadata = model_info.metadata || {}
69
+ end
70
+
71
+ self.model_association = model_record
72
+ @model_string = nil
73
+ @provider_string = nil
74
+ end
75
+
76
+ public
77
+
78
+ def to_llm
79
+ model_record = model_association
80
+ @chat ||= (context || RubyLLM).chat(
81
+ model: model_record.model_id,
82
+ provider: model_record.provider.to_sym
83
+ )
84
+ @chat.reset_messages!
85
+
86
+ messages_association.each do |msg|
87
+ @chat.add_message(msg.to_llm)
88
+ end
89
+
90
+ setup_persistence_callbacks
91
+ end
92
+
93
+ def with_instructions(instructions, replace: false)
94
+ transaction do
95
+ messages_association.where(role: :system).destroy_all if replace
96
+ messages_association.create!(role: :system, content: instructions)
97
+ end
98
+ to_llm.with_instructions(instructions)
99
+ self
100
+ end
101
+
102
+ def with_tool(...)
103
+ to_llm.with_tool(...)
104
+ self
105
+ end
106
+
107
+ def with_tools(...)
108
+ to_llm.with_tools(...)
109
+ self
110
+ end
111
+
112
+ def with_model(model_name, provider: nil, assume_exists: false)
113
+ self.model = model_name
114
+ self.provider = provider if provider
115
+ self.assume_model_exists = assume_exists
116
+ resolve_model_from_strings
117
+ save!
118
+ to_llm.with_model(model.model_id, provider: model.provider.to_sym, assume_exists:)
119
+ self
120
+ end
121
+
122
+ def with_temperature(...)
123
+ to_llm.with_temperature(...)
124
+ self
125
+ end
126
+
127
+ def with_params(...)
128
+ to_llm.with_params(...)
129
+ self
130
+ end
131
+
132
+ def with_headers(...)
133
+ to_llm.with_headers(...)
134
+ self
135
+ end
136
+
137
+ def with_schema(...)
138
+ to_llm.with_schema(...)
139
+ self
140
+ end
141
+
142
+ def on_new_message(&)
143
+ to_llm.on_new_message(&)
144
+ self
145
+ end
146
+
147
+ def on_end_message(&)
148
+ to_llm.on_end_message(&)
149
+ self
150
+ end
151
+
152
+ def on_tool_call(...)
153
+ to_llm.on_tool_call(...)
154
+ self
155
+ end
156
+
157
+ def on_tool_result(...)
158
+ to_llm.on_tool_result(...)
159
+ self
160
+ end
161
+
162
+ def create_user_message(content, with: nil)
163
+ content_text, attachments, content_raw = prepare_content_for_storage(content)
164
+
165
+ message_record = messages_association.build(role: :user)
166
+ message_record.content = content_text
167
+ message_record.content_raw = content_raw if message_record.respond_to?(:content_raw=)
168
+ message_record.save!
169
+
170
+ persist_content(message_record, with) if with.present?
171
+ persist_content(message_record, attachments) if attachments.present?
172
+
173
+ message_record
174
+ end
175
+
176
+ def ask(message, with: nil, &)
177
+ create_user_message(message, with:)
178
+ complete(&)
179
+ end
180
+
181
+ alias say ask
182
+
183
+ def complete(...)
184
+ to_llm.complete(...)
185
+ rescue RubyLLM::Error => e
186
+ cleanup_failed_messages if @message&.persisted? && @message.content.blank?
187
+ cleanup_orphaned_tool_results
188
+ raise e
189
+ end
190
+
191
+ private
192
+
193
+ def cleanup_failed_messages
194
+ RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
195
+ @message.destroy
196
+ end
197
+
198
+ def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
199
+ messages_association.reload
200
+ last = messages_association.order(:id).last
201
+
202
+ return unless last&.tool_call? || last&.tool_result?
203
+
204
+ if last.tool_call?
205
+ last.destroy
206
+ elsif last.tool_result?
207
+ tool_call_message = last.parent_tool_call.message
208
+ expected_results = tool_call_message.tool_calls.pluck(:id)
209
+ actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
210
+
211
+ if expected_results.sort != actual_results.sort
212
+ tool_call_message.tool_results.each(&:destroy)
213
+ tool_call_message.destroy
214
+ end
215
+ end
216
+ end
217
+
218
+ def setup_persistence_callbacks
219
+ return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
220
+
221
+ @chat.on_new_message { persist_new_message }
222
+ @chat.on_end_message { |msg| persist_message_completion(msg) }
223
+
224
+ @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
225
+ @chat
226
+ end
227
+
228
+ def persist_new_message
229
+ @message = messages_association.create!(role: :assistant, content: '')
230
+ end
231
+
232
+ # rubocop:disable Metrics/PerceivedComplexity
233
+ def persist_message_completion(message)
234
+ return unless message
235
+
236
+ tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
237
+
238
+ transaction do
239
+ content_text, attachments_to_persist, content_raw = prepare_content_for_storage(message.content)
240
+
241
+ attrs = {
242
+ role: message.role,
243
+ content: content_text,
244
+ input_tokens: message.input_tokens,
245
+ output_tokens: message.output_tokens
246
+ }
247
+ attrs[:cached_tokens] = message.cached_tokens if @message.has_attribute?(:cached_tokens)
248
+ if @message.has_attribute?(:cache_creation_tokens)
249
+ attrs[:cache_creation_tokens] = message.cache_creation_tokens
250
+ end
251
+
252
+ # Add model association dynamically
253
+ attrs[self.class.model_association_name] = model_association
254
+
255
+ if tool_call_id
256
+ parent_tool_call_assoc = @message.class.reflect_on_association(:parent_tool_call)
257
+ attrs[parent_tool_call_assoc.foreign_key] = tool_call_id
258
+ end
259
+
260
+ @message.assign_attributes(attrs)
261
+ @message.content_raw = content_raw if @message.respond_to?(:content_raw=)
262
+ @message.save!
263
+
264
+ persist_content(@message, attachments_to_persist) if attachments_to_persist
265
+ persist_tool_calls(message.tool_calls) if message.tool_calls.present?
266
+ end
267
+ end
268
+ # rubocop:enable Metrics/PerceivedComplexity
269
+
270
+ def persist_tool_calls(tool_calls)
271
+ tool_calls.each_value do |tool_call|
272
+ attributes = tool_call.to_h
273
+ attributes[:tool_call_id] = attributes.delete(:id)
274
+ @message.tool_calls_association.create!(**attributes)
275
+ end
276
+ end
277
+
278
+ def find_tool_call_id(tool_call_id)
279
+ messages = messages_association
280
+ message_class = messages.klass
281
+ tool_calls_assoc = message_class.tool_calls_association_name
282
+ tool_call_table_name = message_class.reflect_on_association(tool_calls_assoc).table_name
283
+
284
+ message_with_tool_call = messages.joins(tool_calls_assoc)
285
+ .find_by(tool_call_table_name => { tool_call_id: tool_call_id })
286
+ return nil unless message_with_tool_call
287
+
288
+ tool_call = message_with_tool_call.tool_calls_association.find_by(tool_call_id: tool_call_id)
289
+ tool_call&.id
290
+ end
291
+
292
+ def persist_content(message_record, attachments)
293
+ return unless message_record.respond_to?(:attachments)
294
+
295
+ attachables = prepare_for_active_storage(attachments)
296
+ message_record.attachments.attach(attachables) if attachables.any?
297
+ end
298
+
299
+ def prepare_for_active_storage(attachments)
300
+ Utils.to_safe_array(attachments).filter_map do |attachment|
301
+ case attachment
302
+ when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
303
+ attachment
304
+ when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
305
+ attachment.blobs
306
+ when Hash
307
+ attachment.values.map { |v| prepare_for_active_storage(v) }
308
+ else
309
+ convert_to_active_storage_format(attachment)
310
+ end
311
+ end.flatten.compact
312
+ end
313
+
314
+ def convert_to_active_storage_format(source)
315
+ return if source.blank?
316
+
317
+ attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
318
+
319
+ {
320
+ io: StringIO.new(attachment.content),
321
+ filename: attachment.filename,
322
+ content_type: attachment.mime_type
323
+ }
324
+ rescue StandardError => e
325
+ RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
326
+ nil
327
+ end
328
+
329
+ def prepare_content_for_storage(content)
330
+ attachments = nil
331
+ content_raw = nil
332
+ content_text = content
333
+
334
+ case content
335
+ when RubyLLM::Content::Raw
336
+ content_raw = content.value
337
+ content_text = nil
338
+ when RubyLLM::Content
339
+ attachments = content.attachments if content.attachments.any?
340
+ content_text = content.text
341
+ when Hash, Array
342
+ content_raw = content
343
+ content_text = nil
344
+ end
345
+
346
+ [content_text, attachments, content_raw]
347
+ end
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # Methods mixed into message models.
6
+ module MessageMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
11
+ end
12
+
13
+ def to_llm
14
+ cached = has_attribute?(:cached_tokens) ? self[:cached_tokens] : nil
15
+ cache_creation = has_attribute?(:cache_creation_tokens) ? self[:cache_creation_tokens] : nil
16
+
17
+ RubyLLM::Message.new(
18
+ role: role.to_sym,
19
+ content: extract_content,
20
+ tool_calls: extract_tool_calls,
21
+ tool_call_id: extract_tool_call_id,
22
+ input_tokens: input_tokens,
23
+ output_tokens: output_tokens,
24
+ cached_tokens: cached,
25
+ cache_creation_tokens: cache_creation,
26
+ model_id: model_association&.model_id
27
+ )
28
+ end
29
+
30
+ private
31
+
32
+ def extract_tool_calls
33
+ tool_calls_association.to_h do |tool_call|
34
+ [
35
+ tool_call.tool_call_id,
36
+ RubyLLM::ToolCall.new(
37
+ id: tool_call.tool_call_id,
38
+ name: tool_call.name,
39
+ arguments: tool_call.arguments
40
+ )
41
+ ]
42
+ end
43
+ end
44
+
45
+ def extract_tool_call_id
46
+ parent_tool_call&.tool_call_id
47
+ end
48
+
49
+ def extract_content
50
+ return RubyLLM::Content::Raw.new(content_raw) if has_attribute?(:content_raw) && content_raw.present?
51
+
52
+ content_value = self[:content]
53
+
54
+ return content_value unless respond_to?(:attachments) && attachments.attached?
55
+
56
+ RubyLLM::Content.new(content_value).tap do |content_obj|
57
+ @_tempfiles = []
58
+
59
+ attachments.each do |attachment|
60
+ tempfile = download_attachment(attachment)
61
+ content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
62
+ end
63
+ end
64
+ end
65
+
66
+ def download_attachment(attachment)
67
+ ext = File.extname(attachment.filename.to_s)
68
+ basename = File.basename(attachment.filename.to_s, ext)
69
+ tempfile = Tempfile.new([basename, ext])
70
+ tempfile.binmode
71
+
72
+ attachment.download { |chunk| tempfile.write(chunk) }
73
+
74
+ tempfile.flush
75
+ tempfile.rewind
76
+ @_tempfiles << tempfile
77
+ tempfile
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # Methods mixed into model registry models.
6
+ module ModelMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do # rubocop:disable Metrics/BlockLength
10
+ def refresh!
11
+ RubyLLM.models.refresh!
12
+
13
+ transaction do
14
+ RubyLLM.models.all.each do |model_info|
15
+ model = find_or_initialize_by(
16
+ model_id: model_info.id,
17
+ provider: model_info.provider
18
+ )
19
+ model.update!(from_llm_attributes(model_info))
20
+ end
21
+ end
22
+ end
23
+
24
+ def save_to_database
25
+ transaction do
26
+ RubyLLM.models.all.each do |model_info|
27
+ model = find_or_initialize_by(
28
+ model_id: model_info.id,
29
+ provider: model_info.provider
30
+ )
31
+ model.update!(from_llm_attributes(model_info))
32
+ end
33
+ end
34
+ end
35
+
36
+ def from_llm(model_info)
37
+ new(from_llm_attributes(model_info))
38
+ end
39
+
40
+ private
41
+
42
+ def from_llm_attributes(model_info)
43
+ {
44
+ model_id: model_info.id,
45
+ name: model_info.name,
46
+ provider: model_info.provider,
47
+ family: model_info.family,
48
+ model_created_at: model_info.created_at,
49
+ context_window: model_info.context_window,
50
+ max_output_tokens: model_info.max_output_tokens,
51
+ knowledge_cutoff: model_info.knowledge_cutoff,
52
+ modalities: model_info.modalities.to_h,
53
+ capabilities: model_info.capabilities,
54
+ pricing: model_info.pricing.to_h,
55
+ metadata: model_info.metadata
56
+ }
57
+ end
58
+ end
59
+
60
+ def to_llm
61
+ RubyLLM::Model::Info.new(
62
+ id: model_id,
63
+ name: name,
64
+ provider: provider,
65
+ family: family,
66
+ created_at: model_created_at,
67
+ context_window: context_window,
68
+ max_output_tokens: max_output_tokens,
69
+ knowledge_cutoff: knowledge_cutoff,
70
+ modalities: modalities&.deep_symbolize_keys || {},
71
+ capabilities: capabilities,
72
+ pricing: pricing&.deep_symbolize_keys || {},
73
+ metadata: metadata&.deep_symbolize_keys || {}
74
+ )
75
+ end
76
+
77
+ delegate :supports?, :supports_vision?, :supports_functions?, :type,
78
+ :input_price_per_million, :output_price_per_million,
79
+ :function_calling?, :structured_output?, :batch?,
80
+ :reasoning?, :citations?, :streaming?, :provider_class,
81
+ to: :to_llm
82
+ end
83
+ end
84
+ end