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,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module RubyLLM
7
+ # Generator for RubyLLM Rails models and migrations
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ namespace 'ruby_llm:install'
12
+
13
+ source_root File.expand_path('templates', __dir__)
14
+
15
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
16
+
17
+ class_option :skip_active_storage, type: :boolean, default: false,
18
+ desc: 'Skip ActiveStorage installation and attachment setup'
19
+
20
+ desc 'Creates models and migrations for RubyLLM Rails integration\n' \
21
+ 'Usage: rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
22
+
23
+ def self.next_migration_number(dirname)
24
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
25
+ end
26
+
27
+ def migration_version
28
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
29
+ end
30
+
31
+ def postgresql?
32
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
33
+ rescue StandardError
34
+ false
35
+ end
36
+
37
+ def parse_model_mappings
38
+ @model_names = {
39
+ chat: 'Chat',
40
+ message: 'Message',
41
+ tool_call: 'ToolCall',
42
+ model: 'Model'
43
+ }
44
+
45
+ model_mappings.each do |mapping|
46
+ if mapping.include?(':')
47
+ key, value = mapping.split(':', 2)
48
+ @model_names[key.to_sym] = value.classify
49
+ end
50
+ end
51
+
52
+ @model_names
53
+ end
54
+
55
+ %i[chat message tool_call model].each do |type|
56
+ define_method("#{type}_model_name") do
57
+ @model_names ||= parse_model_mappings
58
+ @model_names[type]
59
+ end
60
+
61
+ define_method("#{type}_table_name") do
62
+ table_name_for(send("#{type}_model_name"))
63
+ end
64
+ end
65
+
66
+ def acts_as_chat_declaration
67
+ acts_as_chat_params = []
68
+ messages_assoc = message_model_name.tableize.to_sym
69
+ model_assoc = model_model_name.underscore.to_sym
70
+
71
+ if messages_assoc != :messages
72
+ acts_as_chat_params << "messages: :#{messages_assoc}"
73
+ if message_model_name != messages_assoc.to_s.classify
74
+ acts_as_chat_params << "message_class: '#{message_model_name}'"
75
+ end
76
+ end
77
+
78
+ if model_assoc != :model
79
+ acts_as_chat_params << "model: :#{model_assoc}"
80
+ acts_as_chat_params << "model_class: '#{model_model_name}'" if model_model_name != model_assoc.to_s.classify
81
+ end
82
+
83
+ if acts_as_chat_params.any?
84
+ "acts_as_chat #{acts_as_chat_params.join(', ')}"
85
+ else
86
+ 'acts_as_chat'
87
+ end
88
+ end
89
+
90
+ def acts_as_message_declaration
91
+ params = []
92
+
93
+ add_message_association_params(params, :chat, chat_model_name)
94
+ add_message_association_params(params, :tool_calls, tool_call_model_name, tableize: true)
95
+ add_message_association_params(params, :model, model_model_name)
96
+
97
+ params.any? ? "acts_as_message #{params.join(', ')}" : 'acts_as_message'
98
+ end
99
+
100
+ private
101
+
102
+ def add_message_association_params(params, default_assoc, model_name, tableize: false)
103
+ assoc = tableize ? model_name.tableize.to_sym : model_name.underscore.to_sym
104
+
105
+ return if assoc == default_assoc
106
+
107
+ params << "#{default_assoc}: :#{assoc}"
108
+ expected_class = assoc.to_s.classify
109
+ params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != expected_class
110
+ end
111
+
112
+ public
113
+
114
+ def acts_as_tool_call_declaration
115
+ acts_as_tool_call_params = []
116
+ message_assoc = message_model_name.underscore.to_sym
117
+
118
+ if message_assoc != :message
119
+ acts_as_tool_call_params << "message: :#{message_assoc}"
120
+ if message_model_name != message_assoc.to_s.classify
121
+ acts_as_tool_call_params << "message_class: '#{message_model_name}'"
122
+ end
123
+ end
124
+
125
+ if acts_as_tool_call_params.any?
126
+ "acts_as_tool_call #{acts_as_tool_call_params.join(', ')}"
127
+ else
128
+ 'acts_as_tool_call'
129
+ end
130
+ end
131
+
132
+ def acts_as_model_declaration
133
+ acts_as_model_params = []
134
+ chats_assoc = chat_model_name.tableize.to_sym
135
+
136
+ if chats_assoc != :chats
137
+ acts_as_model_params << "chats: :#{chats_assoc}"
138
+ acts_as_model_params << "chat_class: '#{chat_model_name}'" if chat_model_name != chats_assoc.to_s.classify
139
+ end
140
+
141
+ if acts_as_model_params.any?
142
+ "acts_as_model #{acts_as_model_params.join(', ')}"
143
+ else
144
+ 'acts_as_model'
145
+ end
146
+ end
147
+
148
+ def create_migration_files
149
+ # Create migrations with timestamps to ensure proper order
150
+ # First create chats table
151
+ migration_template 'create_chats_migration.rb.tt',
152
+ "db/migrate/create_#{chat_table_name}.rb"
153
+
154
+ # Then create messages table (must come before tool_calls due to foreign key)
155
+ sleep 1 # Ensure different timestamp
156
+ migration_template 'create_messages_migration.rb.tt',
157
+ "db/migrate/create_#{message_table_name}.rb"
158
+
159
+ # Then create tool_calls table (references messages)
160
+ sleep 1 # Ensure different timestamp
161
+ migration_template 'create_tool_calls_migration.rb.tt',
162
+ "db/migrate/create_#{tool_call_table_name}.rb"
163
+
164
+ # Create models table
165
+ sleep 1 # Ensure different timestamp
166
+ migration_template 'create_models_migration.rb.tt',
167
+ "db/migrate/create_#{model_table_name}.rb"
168
+ end
169
+
170
+ def create_model_files
171
+ template 'chat_model.rb.tt', "app/models/#{chat_model_name.underscore}.rb"
172
+ template 'message_model.rb.tt', "app/models/#{message_model_name.underscore}.rb"
173
+ template 'tool_call_model.rb.tt', "app/models/#{tool_call_model_name.underscore}.rb"
174
+
175
+ template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
176
+ end
177
+
178
+ def create_initializer
179
+ template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
180
+ end
181
+
182
+ def install_active_storage
183
+ return if options[:skip_active_storage]
184
+
185
+ say ' Installing ActiveStorage for file attachments...', :cyan
186
+ rails_command 'active_storage:install'
187
+ end
188
+
189
+ def table_name_for(model_name)
190
+ # Convert namespaced model names to proper table names
191
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
192
+ model_name.underscore.pluralize.tr('/', '_')
193
+ end
194
+
195
+ def show_install_info
196
+ say "\n ✅ RubyLLM installed!", :green
197
+
198
+ say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
199
+
200
+ say "\n Next steps:", :yellow
201
+ say ' 1. Run: rails db:migrate'
202
+ say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
203
+
204
+ say " 3. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
205
+
206
+ say "\n 🚀 Model registry is database-backed!", :cyan
207
+ say ' Models automatically load from the database'
208
+ say ' Pass model names as strings - RubyLLM handles the rest!'
209
+ say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
210
+
211
+ if options[:skip_active_storage]
212
+ say "\n 📎 Note: ActiveStorage was skipped", :yellow
213
+ say ' File attachments won\'t work without ActiveStorage.'
214
+ say ' To enable later:'
215
+ say ' 1. Run: rails active_storage:install && rails db:migrate'
216
+ say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
217
+ end
218
+
219
+ say "\n 📚 Documentation: https://rubyllm.com", :cyan
220
+
221
+ say "\n ❤️ Love RubyLLM?", :magenta
222
+ say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
223
+ say ' • 🐦 Follow for updates: https://x.com/paolino'
224
+ say "\n"
225
+ end
226
+ end
227
+ end
@@ -1,3 +1,3 @@
1
- class <%= options[:chat_model_name] %> < ApplicationRecord
1
+ class <%= chat_model_name %> < ApplicationRecord
2
2
  <%= acts_as_chat_declaration %>
3
- end
3
+ end
@@ -1,8 +1,8 @@
1
- class Create<%= options[:chat_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class Create<%= chat_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :<%= options[:chat_model_name].tableize %> do |t|
4
- t.string :model_id
3
+ create_table :<%= chat_table_name %> do |t|
4
+ t.references :<%= model_table_name.singularize %>, foreign_key: true
5
5
  t.timestamps
6
6
  end
7
7
  end
8
- end
8
+ end
@@ -1,15 +1,16 @@
1
- # Migration for creating messages table with references to chats and tool_calls
2
- class Create<%= options[:message_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
3
2
  def change
4
- create_table :<%= options[:message_model_name].tableize %> do |t|
5
- t.references :<%= options[:chat_model_name].tableize.singularize %>, null: false, foreign_key: true
6
- t.string :role
3
+ create_table :<%= message_table_name %> do |t|
4
+ t.references :<%= chat_table_name.singularize %>, null: false, foreign_key: true
5
+ t.string :role, null: false
7
6
  t.text :content
8
- t.string :model_id
7
+ t.references :<%= model_table_name.singularize %>, foreign_key: true
9
8
  t.integer :input_tokens
10
9
  t.integer :output_tokens
11
- t.references :<%= options[:tool_call_model_name].tableize.singularize %>
10
+ t.references :<%= tool_call_table_name.singularize %>, foreign_key: true
12
11
  t.timestamps
13
12
  end
13
+
14
+ add_index :<%= message_table_name %>, :role
14
15
  end
15
16
  end
@@ -1,6 +1,6 @@
1
- class Create<%= options[:model_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :<%= options[:model_model_name].tableize %> do |t|
3
+ create_table :<%= model_table_name %> do |t|
4
4
  t.string :model_id, null: false
5
5
  t.string :name, null: false
6
6
  t.string :provider, null: false
@@ -30,5 +30,14 @@ class Create<%= options[:model_model_name].pluralize %> < ActiveRecord::Migratio
30
30
  t.index :modalities, using: :gin
31
31
  <% end %>
32
32
  end
33
+
34
+ # Load models from JSON
35
+ say_with_time "Loading models from models.json" do
36
+ RubyLLM.models.load_from_json!
37
+ model_class = '<%= model_model_name %>'.constantize
38
+ model_class.save_to_database
39
+
40
+ "Loaded #{model_class.count} models"
41
+ end
33
42
  end
34
- end
43
+ end
@@ -1,14 +1,15 @@
1
1
  <%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
2
- class Create<%= options[:tool_call_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ class Create<%= tool_call_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
3
3
  def change
4
- create_table :<%= options[:tool_call_model_name].tableize %> do |t|
5
- t.references :<%= options[:message_model_name].tableize.singularize %>, null: false, foreign_key: true
4
+ create_table :<%= tool_call_table_name %> do |t|
5
+ t.references :<%= message_table_name.singularize %>, null: false, foreign_key: true
6
6
  t.string :tool_call_id, null: false
7
7
  t.string :name, null: false
8
8
  t.<%= postgresql? ? 'jsonb' : 'json' %> :arguments, default: {}
9
9
  t.timestamps
10
10
  end
11
11
 
12
- add_index :<%= options[:tool_call_model_name].tableize %>, :tool_call_id
12
+ add_index :<%= tool_call_table_name %>, :tool_call_id, unique: true
13
+ add_index :<%= tool_call_table_name %>, :name
13
14
  end
14
- end
15
+ end
@@ -1,11 +1,12 @@
1
1
  RubyLLM.configure do |config|
2
- config.openai_api_key = ENV["OPENAI_API_KEY"]
3
- config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
4
-
2
+ config.openai_api_key = Rails.application.credentials.dig(:openai_api_key)
5
3
  # config.default_model = "gpt-4.1-nano"
6
- <% unless options[:skip_model] %>
7
4
 
8
- # Model registry persistence
9
- config.model_registry_class = "<%= options[:model_model_name] %>"
10
- <% end %>
11
- end
5
+ # Use the new association-based acts_as API (recommended)
6
+ config.use_new_acts_as = true
7
+ <% if model_model_name != 'Model' -%>
8
+
9
+ # Custom model registry class name
10
+ config.model_registry_class = "<%= model_model_name %>"
11
+ <% end -%>
12
+ end
@@ -1,3 +1,4 @@
1
- class <%= options[:message_model_name] %> < ApplicationRecord
2
- <%= acts_as_message_declaration %>
3
- end
1
+ class <%= message_model_name %> < ApplicationRecord
2
+ <%= acts_as_message_declaration %><% unless options[:skip_active_storage] %>
3
+ has_many_attached :attachments<% end %>
4
+ end
@@ -1,6 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
- # Model registry persistence
4
- class <%= options[:model_model_name] %> < ApplicationRecord
1
+ class <%= model_model_name %> < ApplicationRecord
5
2
  <%= acts_as_model_declaration %>
6
- end
3
+ end
@@ -1,3 +1,3 @@
1
- class <%= options[:tool_call_model_name] %> < ApplicationRecord
1
+ class <%= tool_call_model_name %> < ApplicationRecord
2
2
  <%= acts_as_tool_call_declaration %>
3
- end
3
+ end
@@ -0,0 +1,137 @@
1
+ class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ model_class = <%= model_model_name %>
4
+ chat_class = <%= chat_model_name %>
5
+ message_class = <%= message_model_name %>
6
+
7
+ # Then check for any models in existing data that aren't in models.json
8
+ say_with_time "Checking for additional models in existing data" do
9
+ collect_and_create_models(chat_class, :<%= chat_table_name %>, model_class)
10
+ collect_and_create_models(message_class, :<%= message_table_name %>, model_class)
11
+ model_class.count
12
+ end
13
+
14
+ # Migrate foreign keys
15
+ migrate_foreign_key(:<%= chat_table_name %>, chat_class, model_class, :<%= model_table_name.singularize %>)
16
+ migrate_foreign_key(:<%= message_table_name %>, message_class, model_class, :<%= model_table_name.singularize %>)
17
+ end
18
+
19
+ def down
20
+ # Remove foreign key references
21
+ if column_exists?(:<%= message_table_name %>, :<%= model_table_name.singularize %>_id)
22
+ remove_reference :<%= message_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
23
+ end
24
+
25
+ if column_exists?(:<%= chat_table_name %>, :<%= model_table_name.singularize %>_id)
26
+ remove_reference :<%= chat_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
27
+ end
28
+
29
+ # Restore original model_id string columns
30
+ if column_exists?(:<%= message_table_name %>, :model_id_string)
31
+ rename_column :<%= message_table_name %>, :model_id_string, :model_id
32
+ end
33
+
34
+ if column_exists?(:<%= chat_table_name %>, :model_id_string)
35
+ rename_column :<%= chat_table_name %>, :model_id_string, :model_id
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def collect_and_create_models(record_class, table_name, model_class)
42
+ return unless column_exists?(table_name, :model_id)
43
+
44
+ has_provider = column_exists?(table_name, :provider)
45
+
46
+ # Collect unique model/provider combinations using read_attribute to bypass overrides
47
+ models_set = Set.new
48
+
49
+ record_class.find_each do |record|
50
+ model_id = record.read_attribute(:model_id)
51
+ next if model_id.blank?
52
+
53
+ provider = has_provider ? record.read_attribute(:provider) : nil
54
+ models_set.add([ model_id, provider ])
55
+ end
56
+
57
+ models_set.each do |model_id, provider|
58
+ find_or_create_model(model_id, provider, model_class)
59
+ end
60
+ end
61
+
62
+ def find_or_create_model(model_id, provider, model_class)
63
+ return if model_id.blank?
64
+
65
+ begin
66
+ model_info, _provider = RubyLLM.models.resolve(model_id, provider: provider)
67
+
68
+ model_class.find_or_create_by!(
69
+ model_id: model_info.id,
70
+ provider: model_info.provider
71
+ ) do |m|
72
+ m.name = model_info.name || model_info.id
73
+ m.family = model_info.family
74
+ m.model_created_at = model_info.created_at
75
+ m.context_window = model_info.context_window
76
+ m.max_output_tokens = model_info.max_output_tokens
77
+ m.knowledge_cutoff = model_info.knowledge_cutoff
78
+ m.modalities = model_info.modalities.to_h
79
+ m.capabilities = model_info.capabilities
80
+ m.pricing = model_info.pricing.to_h
81
+ m.metadata = model_info.metadata
82
+ end
83
+ rescue => e
84
+ # Skip models that can't be resolved - they'll need manual fixing
85
+ Rails.logger.warn "Skipping unresolvable model: #{model_id} - will need manual update"
86
+ nil
87
+ end
88
+ end
89
+
90
+
91
+ def migrate_foreign_key(table_name, record_class, model_class, foreign_key_name)
92
+ return unless column_exists?(table_name, :model_id)
93
+
94
+ # Check if we need to rename the string column to avoid collision
95
+ if column_exists?(table_name, :model_id) && !foreign_key_exists?(table_name, :models)
96
+ # Temporarily rename the string column
97
+ rename_column table_name, :model_id, :model_id_string
98
+ end
99
+
100
+ # Add the foreign key reference
101
+ unless column_exists?(table_name, "#{foreign_key_name}_id")
102
+ add_reference table_name, foreign_key_name, foreign_key: true
103
+ end
104
+
105
+ say_with_time "Migrating #{table_name} model references" do
106
+ record_class.reset_column_information
107
+ has_provider = column_exists?(table_name, :provider)
108
+
109
+ # Determine which column to read from (renamed or original)
110
+ model_id_column = column_exists?(table_name, :model_id_string) ? :model_id_string : :model_id
111
+
112
+ record_class.find_each do |record|
113
+ model_id = record.read_attribute(model_id_column)
114
+ next if model_id.blank?
115
+
116
+ provider = has_provider ? record.read_attribute(:provider) : nil
117
+
118
+ model = if has_provider && provider.present?
119
+ model_class.find_by(model_id: model_id, provider: provider)
120
+ else
121
+ find_model_for_record(model_id, model_class)
122
+ end
123
+
124
+ record.update_column("#{foreign_key_name}_id", model.id) if model
125
+ end
126
+ end
127
+ end
128
+
129
+ def find_model_for_record(model_id, model_class)
130
+ begin
131
+ model_info, _provider = RubyLLM.models.resolve(model_id)
132
+ model_class.find_by(model_id: model_info.id, provider: model_info.provider)
133
+ rescue => e
134
+ model_class.find_by(model_id: model_id)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module RubyLLM
7
+ class UpgradeToV17Generator < Rails::Generators::Base # rubocop:disable Style/Documentation
8
+ include Rails::Generators::Migration
9
+
10
+ namespace 'ruby_llm:upgrade_to_v1_7'
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ # Override source_paths to include install templates
14
+ def self.source_paths
15
+ [
16
+ File.expand_path('templates', __dir__),
17
+ File.expand_path('../install/templates', __dir__)
18
+ ]
19
+ end
20
+
21
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
22
+
23
+ desc 'Upgrades existing RubyLLM apps to v1.7 with new Rails-like API\n' \
24
+ 'Usage: rails g ruby_llm:upgrade_to_v1_7 [chat:ChatName] [message:MessageName] ...'
25
+
26
+ def self.next_migration_number(dirname)
27
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
28
+ end
29
+
30
+ def parse_model_mappings
31
+ @model_names = {
32
+ chat: 'Chat',
33
+ message: 'Message',
34
+ tool_call: 'ToolCall',
35
+ model: 'Model'
36
+ }
37
+
38
+ model_mappings.each do |mapping|
39
+ if mapping.include?(':')
40
+ key, value = mapping.split(':', 2)
41
+ @model_names[key.to_sym] = value.classify
42
+ end
43
+ end
44
+
45
+ @model_names
46
+ end
47
+
48
+ %i[chat message tool_call model].each do |type|
49
+ define_method("#{type}_model_name") do
50
+ @model_names ||= parse_model_mappings
51
+ @model_names[type]
52
+ end
53
+
54
+ define_method("#{type}_table_name") do
55
+ table_name_for(send("#{type}_model_name"))
56
+ end
57
+ end
58
+
59
+ def create_migration_file
60
+ # First check if models table exists, if not create it
61
+ unless table_exists?(table_name_for(model_model_name))
62
+ migration_template 'create_models_migration.rb.tt',
63
+ "db/migrate/create_#{table_name_for(model_model_name)}.rb",
64
+ migration_version: migration_version,
65
+ model_model_name: model_model_name
66
+
67
+ sleep 1 # Ensure different timestamp
68
+ end
69
+
70
+ migration_template 'migration.rb.tt',
71
+ 'db/migrate/migrate_to_ruby_llm_model_references.rb',
72
+ migration_version: migration_version,
73
+ chat_model_name: chat_model_name,
74
+ message_model_name: message_model_name,
75
+ tool_call_model_name: tool_call_model_name,
76
+ model_model_name: model_model_name
77
+ end
78
+
79
+ def create_model_file
80
+ # Check if Model file already exists
81
+ model_path = "app/models/#{model_model_name.underscore}.rb"
82
+
83
+ if File.exist?(Rails.root.join(model_path))
84
+ say_status :skip, model_path, :yellow
85
+ else
86
+ create_file model_path do
87
+ <<~RUBY
88
+ class #{model_model_name} < ApplicationRecord
89
+ #{acts_as_model_declaration}
90
+ end
91
+ RUBY
92
+ end
93
+ end
94
+ end
95
+
96
+ def acts_as_model_declaration
97
+ acts_as_model_params = []
98
+ chats_assoc = chat_model_name.tableize.to_sym
99
+
100
+ if chats_assoc != :chats
101
+ acts_as_model_params << "chats: :#{chats_assoc}"
102
+ acts_as_model_params << "chat_class: '#{chat_model_name}'" if chat_model_name != chats_assoc.to_s.classify
103
+ end
104
+
105
+ if acts_as_model_params.any?
106
+ "acts_as_model #{acts_as_model_params.join(', ')}"
107
+ else
108
+ 'acts_as_model'
109
+ end
110
+ end
111
+
112
+ def update_initializer
113
+ initializer_content = File.read('config/initializers/ruby_llm.rb')
114
+
115
+ unless initializer_content.include?('config.use_new_acts_as')
116
+ inject_into_file 'config/initializers/ruby_llm.rb', before: /^end/ do
117
+ lines = ["\n # Enable the new Rails-like API", ' config.use_new_acts_as = true']
118
+ lines << " config.model_registry_class = \"#{model_model_name}\"" if model_model_name != 'Model'
119
+ lines << "\n"
120
+ lines.join("\n")
121
+ end
122
+ end
123
+ rescue Errno::ENOENT
124
+ say_status :error, 'config/initializers/ruby_llm.rb not found', :red
125
+ end
126
+
127
+ def show_next_steps
128
+ say_status :success, 'Migration created!', :green
129
+ say <<~INSTRUCTIONS
130
+
131
+ Next steps:
132
+ 1. Review the migration: db/migrate/*_migrate_to_ruby_llm_model_references.rb
133
+ 2. Run: rails db:migrate
134
+ 3. Update config/initializers/ruby_llm.rb as shown above
135
+ 4. Test your application thoroughly
136
+
137
+ The migration will:
138
+ - Create the Models table if it doesn't exist
139
+ - Load all models from models.json
140
+ - Migrate your existing data to use foreign keys
141
+ - Preserve all existing data (string columns renamed to model_id_string)
142
+
143
+ INSTRUCTIONS
144
+ end
145
+
146
+ private
147
+
148
+ def migration_version
149
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
150
+ end
151
+
152
+ def table_name_for(model_name)
153
+ # Convert namespaced model names to proper table names
154
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
155
+ model_name.underscore.pluralize.tr('/', '_')
156
+ end
157
+
158
+ def table_exists?(table_name)
159
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
160
+ rescue StandardError
161
+ false
162
+ end
163
+
164
+ def postgresql?
165
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
166
+ rescue StandardError
167
+ false
168
+ end
169
+ end
170
+ end