ruby_llm_community 1.0.0 → 1.1.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -1
  3. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +127 -0
  4. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
  5. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
  6. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  7. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  8. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  9. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  10. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  12. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +10 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +30 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
  19. data/lib/generators/ruby_llm/install/install_generator.rb +227 -0
  20. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +2 -2
  21. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -4
  22. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +8 -7
  23. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +12 -3
  24. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +6 -5
  25. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -8
  26. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -3
  27. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +2 -5
  28. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +2 -2
  29. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +137 -0
  30. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +170 -0
  31. data/lib/ruby_llm/active_record/acts_as.rb +108 -467
  32. data/lib/ruby_llm/active_record/acts_as_legacy.rb +403 -0
  33. data/lib/ruby_llm/active_record/chat_methods.rb +336 -0
  34. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  35. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  36. data/lib/ruby_llm/aliases.json +72 -6
  37. data/lib/ruby_llm/attachment.rb +22 -0
  38. data/lib/ruby_llm/configuration.rb +6 -0
  39. data/lib/ruby_llm/image_attachment.rb +12 -3
  40. data/lib/ruby_llm/message.rb +1 -1
  41. data/lib/ruby_llm/models.json +2640 -1756
  42. data/lib/ruby_llm/models.rb +5 -15
  43. data/lib/ruby_llm/provider.rb +6 -4
  44. data/lib/ruby_llm/providers/anthropic/media.rb +1 -1
  45. data/lib/ruby_llm/providers/bedrock/models.rb +19 -1
  46. data/lib/ruby_llm/providers/gemini/media.rb +1 -1
  47. data/lib/ruby_llm/providers/gpustack/media.rb +1 -1
  48. data/lib/ruby_llm/providers/ollama/media.rb +1 -1
  49. data/lib/ruby_llm/providers/openai/media.rb +4 -4
  50. data/lib/ruby_llm/providers/openai/response.rb +7 -6
  51. data/lib/ruby_llm/providers/openai/response_media.rb +1 -1
  52. data/lib/ruby_llm/providers/openai/streaming.rb +14 -11
  53. data/lib/ruby_llm/providers/openai/tools.rb +11 -6
  54. data/lib/ruby_llm/providers/vertexai.rb +1 -1
  55. data/lib/ruby_llm/providers/xai/capabilities.rb +166 -0
  56. data/lib/ruby_llm/providers/xai/chat.rb +15 -0
  57. data/lib/ruby_llm/providers/xai/models.rb +48 -0
  58. data/lib/ruby_llm/providers/xai.rb +46 -0
  59. data/lib/ruby_llm/railtie.rb +20 -3
  60. data/lib/ruby_llm/stream_accumulator.rb +0 -4
  61. data/lib/ruby_llm/utils.rb +5 -9
  62. data/lib/ruby_llm/version.rb +1 -1
  63. data/lib/ruby_llm_community.rb +4 -3
  64. data/lib/tasks/models.rake +29 -5
  65. data/lib/tasks/ruby_llm.rake +15 -0
  66. data/lib/tasks/vcr.rake +2 -2
  67. metadata +32 -3
  68. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +0 -108
  69. data/lib/generators/ruby_llm/install_generator.rb +0 -146
@@ -0,0 +1,336 @@
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(&block)
143
+ to_llm
144
+
145
+ existing_callback = @chat.instance_variable_get(:@on)[:new_message]
146
+
147
+ @chat.on_new_message do
148
+ existing_callback&.call
149
+ block&.call
150
+ end
151
+ self
152
+ end
153
+
154
+ def on_end_message(&block)
155
+ to_llm
156
+
157
+ existing_callback = @chat.instance_variable_get(:@on)[:end_message]
158
+
159
+ @chat.on_end_message do |msg|
160
+ existing_callback&.call(msg)
161
+ block&.call(msg)
162
+ end
163
+ self
164
+ end
165
+
166
+ def on_tool_call(...)
167
+ to_llm.on_tool_call(...)
168
+ self
169
+ end
170
+
171
+ def on_tool_result(...)
172
+ to_llm.on_tool_result(...)
173
+ self
174
+ end
175
+
176
+ def create_user_message(content, with: nil)
177
+ message_record = messages_association.create!(role: :user, content: content)
178
+ persist_content(message_record, with) if with.present?
179
+ message_record
180
+ end
181
+
182
+ def ask(message, with: nil, &)
183
+ create_user_message(message, with:)
184
+ complete(&)
185
+ end
186
+
187
+ alias say ask
188
+
189
+ def complete(...)
190
+ to_llm.complete(...)
191
+ rescue RubyLLM::Error => e
192
+ cleanup_failed_messages if @message&.persisted? && @message.content.blank?
193
+ cleanup_orphaned_tool_results
194
+ raise e
195
+ end
196
+
197
+ private
198
+
199
+ def cleanup_failed_messages
200
+ RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
201
+ @message.destroy
202
+ end
203
+
204
+ def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
205
+ messages_association.reload
206
+ last = messages_association.order(:id).last
207
+
208
+ return unless last&.tool_call? || last&.tool_result?
209
+
210
+ if last.tool_call?
211
+ last.destroy
212
+ elsif last.tool_result?
213
+ tool_call_message = last.parent_tool_call.message
214
+ expected_results = tool_call_message.tool_calls.pluck(:id)
215
+ actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
216
+
217
+ if expected_results.sort != actual_results.sort
218
+ tool_call_message.tool_results.each(&:destroy)
219
+ tool_call_message.destroy
220
+ end
221
+ end
222
+ end
223
+
224
+ def setup_persistence_callbacks
225
+ return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
226
+
227
+ @chat.on_new_message { persist_new_message }
228
+ @chat.on_end_message { |msg| persist_message_completion(msg) }
229
+
230
+ @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
231
+ @chat
232
+ end
233
+
234
+ def persist_new_message
235
+ @message = messages_association.create!(role: :assistant, content: '')
236
+ end
237
+
238
+ def persist_message_completion(message) # rubocop:disable Metrics/PerceivedComplexity
239
+ return unless message
240
+
241
+ tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
242
+
243
+ transaction do
244
+ content = message.content
245
+ attachments_to_persist = nil
246
+
247
+ if content.is_a?(RubyLLM::Content)
248
+ attachments_to_persist = content.attachments if content.attachments.any?
249
+ content = content.text
250
+ elsif content.is_a?(Hash) || content.is_a?(Array)
251
+ content = content.to_json
252
+ end
253
+
254
+ attrs = {
255
+ role: message.role,
256
+ content: content,
257
+ input_tokens: message.input_tokens,
258
+ output_tokens: message.output_tokens
259
+ }
260
+
261
+ # Add model association dynamically
262
+ attrs[self.class.model_association_name] = model_association
263
+
264
+ if tool_call_id
265
+ parent_tool_call_assoc = @message.class.reflect_on_association(:parent_tool_call)
266
+ attrs[parent_tool_call_assoc.foreign_key] = tool_call_id
267
+ end
268
+
269
+ @message.update!(attrs)
270
+
271
+ persist_content(@message, attachments_to_persist) if attachments_to_persist
272
+ persist_tool_calls(message.tool_calls) if message.tool_calls.present?
273
+ end
274
+ end
275
+
276
+ def persist_tool_calls(tool_calls)
277
+ tool_calls.each_value do |tool_call|
278
+ attributes = tool_call.to_h
279
+ attributes[:tool_call_id] = attributes.delete(:id)
280
+ @message.tool_calls_association.create!(**attributes)
281
+ end
282
+ end
283
+
284
+ def find_tool_call_id(tool_call_id)
285
+ messages = messages_association
286
+ message_class = messages.klass
287
+ tool_calls_assoc = message_class.tool_calls_association_name
288
+ tool_call_table_name = message_class.reflect_on_association(tool_calls_assoc).table_name
289
+
290
+ message_with_tool_call = messages.joins(tool_calls_assoc)
291
+ .find_by(tool_call_table_name => { tool_call_id: tool_call_id })
292
+ return nil unless message_with_tool_call
293
+
294
+ tool_call = message_with_tool_call.tool_calls_association.find_by(tool_call_id: tool_call_id)
295
+ tool_call&.id
296
+ end
297
+
298
+ def persist_content(message_record, attachments)
299
+ return unless message_record.respond_to?(:attachments)
300
+
301
+ attachables = prepare_for_active_storage(attachments)
302
+ message_record.attachments.attach(attachables) if attachables.any?
303
+ end
304
+
305
+ def prepare_for_active_storage(attachments)
306
+ Utils.to_safe_array(attachments).filter_map do |attachment|
307
+ case attachment
308
+ when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
309
+ attachment
310
+ when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
311
+ attachment.blobs
312
+ when Hash
313
+ attachment.values.map { |v| prepare_for_active_storage(v) }
314
+ else
315
+ convert_to_active_storage_format(attachment)
316
+ end
317
+ end.flatten.compact
318
+ end
319
+
320
+ def convert_to_active_storage_format(source)
321
+ return if source.blank?
322
+
323
+ attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
324
+
325
+ {
326
+ io: StringIO.new(attachment.content),
327
+ filename: attachment.filename,
328
+ content_type: attachment.mime_type
329
+ }
330
+ rescue StandardError => e
331
+ RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
332
+ nil
333
+ end
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,72 @@
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
+ RubyLLM::Message.new(
15
+ role: role.to_sym,
16
+ content: extract_content,
17
+ tool_calls: extract_tool_calls,
18
+ tool_call_id: extract_tool_call_id,
19
+ input_tokens: input_tokens,
20
+ output_tokens: output_tokens,
21
+ model_id: model_association&.model_id
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ def extract_tool_calls
28
+ tool_calls_association.to_h do |tool_call|
29
+ [
30
+ tool_call.tool_call_id,
31
+ RubyLLM::ToolCall.new(
32
+ id: tool_call.tool_call_id,
33
+ name: tool_call.name,
34
+ arguments: tool_call.arguments
35
+ )
36
+ ]
37
+ end
38
+ end
39
+
40
+ def extract_tool_call_id
41
+ parent_tool_call&.tool_call_id
42
+ end
43
+
44
+ def extract_content
45
+ return content unless respond_to?(:attachments) && attachments.attached?
46
+
47
+ RubyLLM::Content.new(content).tap do |content_obj|
48
+ @_tempfiles = []
49
+
50
+ attachments.each do |attachment|
51
+ tempfile = download_attachment(attachment)
52
+ content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
53
+ end
54
+ end
55
+ end
56
+
57
+ def download_attachment(attachment)
58
+ ext = File.extname(attachment.filename.to_s)
59
+ basename = File.basename(attachment.filename.to_s, ext)
60
+ tempfile = Tempfile.new([basename, ext])
61
+ tempfile.binmode
62
+
63
+ attachment.download { |chunk| tempfile.write(chunk) }
64
+
65
+ tempfile.flush
66
+ tempfile.rewind
67
+ @_tempfiles << tempfile
68
+ tempfile
69
+ end
70
+ end
71
+ end
72
+ 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?,
81
+ to: :to_llm
82
+ end
83
+ end
84
+ end
@@ -227,13 +227,79 @@
227
227
  "openai": "gpt-5-nano",
228
228
  "openrouter": "openai/gpt-5-nano"
229
229
  },
230
- "gpt-oss-120b": {
231
- "openai": "gpt-oss-120b",
232
- "openrouter": "openai/gpt-oss-120b"
230
+ "grok-2": {
231
+ "xai": "grok-2-1212"
233
232
  },
234
- "gpt-oss-20b": {
235
- "openai": "gpt-oss-20b",
236
- "openrouter": "openai/gpt-oss-20b"
233
+ "grok-2-4": {
234
+ "xai": "grok-2-4",
235
+ "openrouter": "x-ai/grok-2-1212"
236
+ },
237
+ "grok-2-image": {
238
+ "xai": "grok-2-image-1212"
239
+ },
240
+ "grok-2-image-latest": {
241
+ "xai": "grok-2-image-1212"
242
+ },
243
+ "grok-2-latest": {
244
+ "xai": "grok-2-1212"
245
+ },
246
+ "grok-2-vision": {
247
+ "xai": "grok-2-vision-1212"
248
+ },
249
+ "grok-2-vision-4": {
250
+ "xai": "grok-2-vision-4",
251
+ "openrouter": "x-ai/grok-2-vision-1212"
252
+ },
253
+ "grok-2-vision-latest": {
254
+ "xai": "grok-2-vision-1212"
255
+ },
256
+ "grok-3": {
257
+ "xai": "grok-3",
258
+ "openrouter": "x-ai/grok-3"
259
+ },
260
+ "grok-3-beta": {
261
+ "xai": "grok-3"
262
+ },
263
+ "grok-3-fast-beta": {
264
+ "xai": "grok-3-fast"
265
+ },
266
+ "grok-3-fast-latest": {
267
+ "xai": "grok-3-fast"
268
+ },
269
+ "grok-3-latest": {
270
+ "xai": "grok-3"
271
+ },
272
+ "grok-3-mini": {
273
+ "xai": "grok-3-mini",
274
+ "openrouter": "x-ai/grok-3-mini"
275
+ },
276
+ "grok-3-mini-beta": {
277
+ "xai": "grok-3-mini"
278
+ },
279
+ "grok-3-mini-fast-beta": {
280
+ "xai": "grok-3-mini-fast"
281
+ },
282
+ "grok-3-mini-fast-latest": {
283
+ "xai": "grok-3-mini-fast"
284
+ },
285
+ "grok-3-mini-latest": {
286
+ "xai": "grok-3-mini"
287
+ },
288
+ "grok-4": {
289
+ "xai": "grok-4-0709"
290
+ },
291
+ "grok-4-latest": {
292
+ "xai": "grok-4-0709"
293
+ },
294
+ "grok-code-fast": {
295
+ "xai": "grok-code-fast-1"
296
+ },
297
+ "grok-code-fast-1": {
298
+ "xai": "grok-code-fast-1",
299
+ "openrouter": "x-ai/grok-code-fast-1"
300
+ },
301
+ "grok-code-fast-1-0825": {
302
+ "xai": "grok-code-fast-1"
237
303
  },
238
304
  "o1": {
239
305
  "openai": "o1",
@@ -62,9 +62,20 @@ module RubyLLM
62
62
  end
63
63
 
64
64
  def encoded
65
+ return nil if content.nil?
66
+
65
67
  Base64.strict_encode64(content)
66
68
  end
67
69
 
70
+ def for_llm
71
+ case type
72
+ when :text
73
+ "<file name='#{filename}' mime_type='#{mime_type}'>#{content}</file>"
74
+ else
75
+ "data:#{mime_type};base64,#{encoded}"
76
+ end
77
+ end
78
+
68
79
  def type
69
80
  return :image if image?
70
81
  return :audio if audio?
@@ -82,6 +93,17 @@ module RubyLLM
82
93
  RubyLLM::MimeType.audio? mime_type
83
94
  end
84
95
 
96
+ def format
97
+ case mime_type
98
+ when 'audio/mpeg'
99
+ 'mp3'
100
+ when 'audio/wav', 'audio/wave', 'audio/x-wav'
101
+ 'wav'
102
+ else
103
+ mime_type.split('/').last
104
+ end
105
+ end
106
+
85
107
  def pdf?
86
108
  RubyLLM::MimeType.pdf? mime_type
87
109
  end
@@ -14,6 +14,7 @@ module RubyLLM
14
14
  :vertexai_location,
15
15
  :deepseek_api_key,
16
16
  :perplexity_api_key,
17
+ :xai_api_key,
17
18
  :bedrock_api_key,
18
19
  :bedrock_secret_key,
19
20
  :bedrock_region,
@@ -29,6 +30,8 @@ module RubyLLM
29
30
  :default_image_model,
30
31
  # Model registry
31
32
  :model_registry_class,
33
+ # Rails integration
34
+ :use_new_acts_as,
32
35
  # Connection configuration
33
36
  :request_timeout,
34
37
  :max_retries,
@@ -54,6 +57,9 @@ module RubyLLM
54
57
  @default_embedding_model = 'text-embedding-3-small'
55
58
  @default_image_model = 'gpt-image-1'
56
59
 
60
+ @model_registry_class = 'Model'
61
+ @use_new_acts_as = false
62
+
57
63
  @log_file = $stdout
58
64
  @log_level = ENV['RUBYLLM_DEBUG'] ? Logger::DEBUG : Logger::INFO
59
65
  @log_stream_debug = ENV['RUBYLLM_STREAM_DEBUG'] == 'true'
@@ -3,19 +3,28 @@
3
3
  module RubyLLM
4
4
  # A class representing a file attachment that is an image generated by an LLM.
5
5
  class ImageAttachment < Attachment
6
- attr_reader :image, :content, :id, :reasoning_id
6
+ attr_reader :image, :content, :id, :reasoning_id, :revised_prompt, :partial
7
7
 
8
- def initialize(data:, mime_type:, model_id:, id: nil, reasoning_id: nil)
8
+ def initialize(data:, mime_type:, model_id:, id: nil, reasoning_id: nil, revised_prompt: nil, partial: false) # rubocop:disable Metrics/ParameterLists
9
9
  super(nil, filename: nil)
10
10
  @image = Image.new(data:, mime_type:, model_id:)
11
- @content = Base64.strict_decode64(data)
12
11
  @mime_type = mime_type
13
12
  @id = id
14
13
  @reasoning_id = reasoning_id
14
+ @revised_prompt = revised_prompt
15
+ @partial = partial || false
15
16
  end
16
17
 
17
18
  def image?
18
19
  true
19
20
  end
21
+
22
+ def encoded
23
+ image.data
24
+ end
25
+
26
+ def to_h
27
+ { type: :image, source: "data:#{image.mime_type};base64,#{encoded}" }
28
+ end
20
29
  end
21
30
  end
@@ -48,7 +48,7 @@ module RubyLLM
48
48
  def to_h
49
49
  {
50
50
  role: role,
51
- content: content,
51
+ content: content.is_a?(Content) ? content.to_h : content,
52
52
  tool_calls: tool_calls,
53
53
  tool_call_id: tool_call_id,
54
54
  input_tokens: input_tokens,