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,16 @@
1
+ <tr id="<%%= dom_id <%= model_model_name.demodulize.underscore %> %>">
2
+ <td><%%= <%= model_model_name.demodulize.underscore %>.provider %></td>
3
+ <td><%%= <%= model_model_name.demodulize.underscore %>.name %></td>
4
+ <td><%%= number_with_delimiter(<%= model_model_name.demodulize.underscore %>.context_window) if <%= model_model_name.demodulize.underscore %>.context_window %></td>
5
+ <td>
6
+ <%% if <%= model_model_name.demodulize.underscore %>.pricing && <%= model_model_name.demodulize.underscore %>.pricing['text_tokens'] && <%= model_model_name.demodulize.underscore %>.pricing['text_tokens']['standard'] %>
7
+ <%% input = <%= model_model_name.demodulize.underscore %>.pricing['text_tokens']['standard']['input_per_million'] %>
8
+ <%% output = <%= model_model_name.demodulize.underscore %>.pricing['text_tokens']['standard']['output_per_million'] %>
9
+ <%% if input && output %>
10
+ $<%%= "%.2f" % input %> / $<%%= "%.2f" % output %>
11
+ <%% end %>
12
+ <%% end %>
13
+ </td>
14
+ <td><%%= link_to "Show", <%= model_model_name.demodulize.underscore %> %></td>
15
+ <td><%%= link_to "Start <%= chat_table_name.singularize.humanize.downcase %>", new_<%= chat_variable_name %>_path(model: <%= model_model_name.demodulize.underscore %>.model_id) %></td>
16
+ </tr>
@@ -0,0 +1,28 @@
1
+ <p style="color: green"><%%= notice %></p>
2
+
3
+ <%% content_for :title, "<%= model_model_name.pluralize %>" %>
4
+
5
+ <h1><%= model_model_name.pluralize %></h1>
6
+
7
+ <p>
8
+ <%%= button_to "Refresh <%= model_model_name.pluralize %>", refresh_<%= model_table_name %>_path, method: :post %>
9
+ </p>
10
+
11
+ <div id="<%= model_table_name %>">
12
+ <table>
13
+ <thead>
14
+ <tr>
15
+ <th>Provider</th>
16
+ <th>Model</th>
17
+ <th>Context Window</th>
18
+ <th>$/1M tokens (In/Out)</th>
19
+ <th colspan="2"></th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <%%= render @<%= model_variable_name.pluralize %> %>
24
+ </tbody>
25
+ </table>
26
+ </div>
27
+
28
+ <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path %>
@@ -0,0 +1,18 @@
1
+ <%% content_for :title, @<%= model_variable_name %>.name %>
2
+
3
+ <h1><%%= @<%= model_variable_name %>.name %></h1>
4
+
5
+ <p><strong>ID:</strong> <%%= @<%= model_variable_name %>.model_id %></p>
6
+ <p><strong>Provider:</strong> <%%= @<%= model_variable_name %>.provider %></p>
7
+ <p><strong>Context Window:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.context_window) %> tokens</p>
8
+ <p><strong>Max Output:</strong> <%%= number_with_delimiter(@<%= model_variable_name %>.max_output_tokens) %> tokens</p>
9
+
10
+ <%% if @<%= model_variable_name %>.capabilities.any? %>
11
+ <p><strong>Capabilities:</strong> <%%= @<%= model_variable_name %>.capabilities.join(", ") %></p>
12
+ <%% end %>
13
+
14
+ <p>
15
+ <%%= link_to "Start chat with this model", new_<%= chat_variable_name %>_path(model: @<%= model_variable_name %>.model_id) %> |
16
+ <%%= link_to "All models", <%= model_table_name %>_path %> |
17
+ <%%= link_to "Back to chats", <%= chat_table_name %>_path %>
18
+ </p>
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Generators
5
+ # Shared helpers for RubyLLM generators
6
+ module GeneratorHelpers
7
+ def parse_model_mappings
8
+ @model_names = {
9
+ chat: 'Chat',
10
+ message: 'Message',
11
+ tool_call: 'ToolCall',
12
+ model: 'Model'
13
+ }
14
+
15
+ model_mappings.each do |mapping|
16
+ if mapping.include?(':')
17
+ key, value = mapping.split(':', 2)
18
+ @model_names[key.to_sym] = value.classify
19
+ end
20
+ end
21
+
22
+ @model_names
23
+ end
24
+
25
+ %i[chat message tool_call model].each do |type|
26
+ define_method("#{type}_model_name") do
27
+ @model_names ||= parse_model_mappings
28
+ @model_names[type]
29
+ end
30
+
31
+ define_method("#{type}_table_name") do
32
+ table_name_for(send("#{type}_model_name"))
33
+ end
34
+
35
+ define_method("#{type}_variable_name") do
36
+ variable_name_for(send("#{type}_model_name"))
37
+ end
38
+
39
+ define_method("#{type}_controller_class_name") do
40
+ controller_class_name_for(send("#{type}_model_name"))
41
+ end
42
+
43
+ define_method("#{type}_job_class_name") do
44
+ "#{variable_name_for(send("#{type}_model_name")).camelize}ResponseJob"
45
+ end
46
+
47
+ define_method("#{type}_partial") do
48
+ partial_path_for(send("#{type}_model_name"))
49
+ end
50
+ end
51
+
52
+ def acts_as_chat_declaration
53
+ params = []
54
+
55
+ add_association_params(params, :messages, message_table_name, message_model_name,
56
+ owner_table: chat_table_name, plural: true)
57
+ add_association_params(params, :model, model_table_name, model_model_name,
58
+ owner_table: chat_table_name)
59
+
60
+ "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
61
+ end
62
+
63
+ def acts_as_message_declaration
64
+ params = []
65
+
66
+ add_association_params(params, :chat, chat_table_name, chat_model_name,
67
+ owner_table: message_table_name)
68
+ add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name,
69
+ owner_table: message_table_name, plural: true)
70
+ add_association_params(params, :model, model_table_name, model_model_name,
71
+ owner_table: message_table_name)
72
+
73
+ "acts_as_message#{" #{params.join(', ')}" if params.any?}"
74
+ end
75
+
76
+ def acts_as_model_declaration
77
+ params = []
78
+
79
+ add_association_params(params, :chats, chat_table_name, chat_model_name,
80
+ owner_table: model_table_name, plural: true)
81
+
82
+ "acts_as_model#{" #{params.join(', ')}" if params.any?}"
83
+ end
84
+
85
+ def acts_as_tool_call_declaration
86
+ params = []
87
+
88
+ add_association_params(params, :message, message_table_name, message_model_name,
89
+ owner_table: tool_call_table_name)
90
+
91
+ "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
92
+ end
93
+
94
+ def create_namespace_modules
95
+ namespaces = []
96
+
97
+ [chat_model_name, message_model_name, tool_call_model_name, model_model_name].each do |model_name|
98
+ if model_name.include?('::')
99
+ namespace = model_name.split('::').first
100
+ namespaces << namespace unless namespaces.include?(namespace)
101
+ end
102
+ end
103
+
104
+ namespaces.each do |namespace|
105
+ module_path = "app/models/#{namespace.underscore}.rb"
106
+ next if File.exist?(Rails.root.join(module_path))
107
+
108
+ create_file module_path do
109
+ <<~RUBY
110
+ module #{namespace}
111
+ def self.table_name_prefix
112
+ "#{namespace.underscore}_"
113
+ end
114
+ end
115
+ RUBY
116
+ end
117
+ end
118
+ end
119
+
120
+ def migration_version
121
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
122
+ end
123
+
124
+ def postgresql?
125
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
126
+ rescue StandardError
127
+ false
128
+ end
129
+
130
+ def mysql?
131
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
132
+ rescue StandardError
133
+ false
134
+ end
135
+
136
+ def table_exists?(table_name)
137
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
138
+ rescue StandardError
139
+ false
140
+ end
141
+
142
+ private
143
+
144
+ def add_association_params(params, default_assoc, table_name, model_name, owner_table:, plural: false) # rubocop:disable Metrics/ParameterLists
145
+ assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
146
+
147
+ default_foreign_key = "#{default_assoc}_id"
148
+ # has_many/has_one: foreign key is on the associated table pointing back to owner
149
+ # belongs_to: foreign key is on the owner table pointing to associated table
150
+ foreign_key = if plural || default_assoc.to_s.pluralize == default_assoc.to_s # has_many or has_one
151
+ "#{owner_table.singularize}_id"
152
+ else # belongs_to
153
+ "#{table_name.singularize}_id"
154
+ end
155
+
156
+ params << "#{default_assoc}: :#{assoc}" if assoc != default_assoc
157
+ params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
158
+ params << "#{default_assoc}_foreign_key: :#{foreign_key}" if foreign_key != default_foreign_key
159
+ end
160
+
161
+ # Convert namespaced model names to proper table names
162
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
163
+ def table_name_for(model_name)
164
+ model_name.underscore.pluralize.tr('/', '_')
165
+ end
166
+
167
+ # Convert model name to instance variable name
168
+ # e.g., "LLM::Chat" -> "llm_chat" (not "llm/chat")
169
+ def variable_name_for(model_name)
170
+ model_name.underscore.tr('/', '_')
171
+ end
172
+
173
+ # Convert model name to controller class name
174
+ # For namespaced models, use Rails convention: "Llm::Chat" -> "Llm::ChatsController"
175
+ # For regular models: "Chat" -> "ChatsController"
176
+ def controller_class_name_for(model_name)
177
+ if model_name.include?('::')
178
+ parts = model_name.split('::')
179
+ namespace = parts[0..-2].join('::')
180
+ resource = parts.last.pluralize
181
+ "#{namespace}::#{resource}Controller"
182
+ else
183
+ "#{model_name.pluralize}Controller"
184
+ end
185
+ end
186
+
187
+ # Convert model name to partial path
188
+ # e.g., "LLM::Message" -> "llm/message" (not "llm_message")
189
+ def partial_path_for(model_name)
190
+ "#{model_name.underscore.pluralize}/#{model_name.demodulize.underscore}"
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require_relative '../generator_helpers'
6
+
7
+ module RubyLLM
8
+ module Generators
9
+ # Generator for RubyLLM Rails models and migrations
10
+ class InstallGenerator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+ include RubyLLM::Generators::GeneratorHelpers
13
+
14
+ namespace 'ruby_llm:install'
15
+
16
+ source_root File.expand_path('templates', __dir__)
17
+
18
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
19
+
20
+ class_option :skip_active_storage, type: :boolean, default: false,
21
+ desc: 'Skip ActiveStorage installation and attachment setup'
22
+
23
+ desc 'Creates models and migrations for RubyLLM Rails integration\n' \
24
+ 'Usage: rails g ruby_llm:install [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 create_migration_files
31
+ migration_template 'create_chats_migration.rb.tt',
32
+ "db/migrate/create_#{chat_table_name}.rb"
33
+
34
+ sleep 1 # Ensure different timestamp
35
+ migration_template 'create_messages_migration.rb.tt',
36
+ "db/migrate/create_#{message_table_name}.rb"
37
+
38
+ sleep 1 # Ensure different timestamp
39
+ migration_template 'create_tool_calls_migration.rb.tt',
40
+ "db/migrate/create_#{tool_call_table_name}.rb"
41
+
42
+ sleep 1 # Ensure different timestamp
43
+ migration_template 'create_models_migration.rb.tt',
44
+ "db/migrate/create_#{model_table_name}.rb"
45
+
46
+ sleep 1 # Ensure different timestamp
47
+ migration_template 'add_references_to_chats_tool_calls_and_messages_migration.rb.tt',
48
+ 'db/migrate/add_references_to_' \
49
+ "#{chat_table_name}_#{tool_call_table_name}_and_#{message_table_name}.rb"
50
+ end
51
+
52
+ def create_model_files
53
+ create_namespace_modules
54
+
55
+ template 'chat_model.rb.tt', "app/models/#{chat_model_name.underscore}.rb"
56
+ template 'message_model.rb.tt', "app/models/#{message_model_name.underscore}.rb"
57
+ template 'tool_call_model.rb.tt', "app/models/#{tool_call_model_name.underscore}.rb"
58
+
59
+ template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
60
+ end
61
+
62
+ def create_initializer
63
+ template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
64
+ end
65
+
66
+ def install_active_storage
67
+ return if options[:skip_active_storage]
68
+
69
+ say ' Installing ActiveStorage for file attachments...', :cyan
70
+ rails_command 'active_storage:install'
71
+ end
72
+
73
+ def show_install_info
74
+ say "\n ✅ RubyLLM installed!", :green
75
+
76
+ say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
77
+
78
+ say "\n Next steps:", :yellow
79
+ say ' 1. Run: rails db:migrate'
80
+ say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
81
+
82
+ say " 3. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
83
+
84
+ say "\n 🚀 Model registry is database-backed!", :cyan
85
+ say ' Models automatically load from the database'
86
+ say ' Pass model names as strings - RubyLLM handles the rest!'
87
+ say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
88
+
89
+ if options[:skip_active_storage]
90
+ say "\n 📎 Note: ActiveStorage was skipped", :yellow
91
+ say ' File attachments won\'t work without ActiveStorage.'
92
+ say ' To enable later:'
93
+ say ' 1. Run: rails active_storage:install && rails db:migrate'
94
+ say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
95
+ end
96
+
97
+ say "\n 📚 Documentation: https://rubyllm.com", :cyan
98
+
99
+ say "\n ❤️ Love RubyLLM?", :magenta
100
+ say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
101
+ say ' • 🐦 Follow for updates: https://x.com/paolino'
102
+ say "\n"
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,9 @@
1
+ class AddReferencesTo<%= "#{chat_model_name.gsub('::', '').pluralize}#{tool_call_model_name.gsub('::', '').pluralize}And#{message_model_name.gsub('::', '').pluralize}" %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ add_reference :<%= chat_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
4
+ add_reference :<%= tool_call_table_name %>, :<%= message_table_name.singularize %>, null: false, foreign_key: true
5
+ add_reference :<%= message_table_name %>, :<%= chat_table_name.singularize %>, null: false, foreign_key: true
6
+ add_reference :<%= message_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
7
+ add_reference :<%= message_table_name %>, :<%= tool_call_table_name.singularize %>, foreign_key: true
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ class <%= chat_model_name %> < ApplicationRecord
2
+ <%= acts_as_chat_declaration %>
3
+ end
@@ -0,0 +1,7 @@
1
+ class Create<%= chat_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= chat_table_name %> do |t|
4
+ t.timestamps
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= message_table_name %> do |t|
4
+ t.string :role, null: false
5
+ t.text :content
6
+ t.json :content_raw
7
+ t.integer :input_tokens
8
+ t.integer :output_tokens
9
+ t.integer :cached_tokens
10
+ t.integer :cache_creation_tokens
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :<%= message_table_name %>, :role
15
+ end
16
+ end
@@ -0,0 +1,45 @@
1
+ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= model_table_name %> do |t|
4
+ t.string :model_id, null: false
5
+ t.string :name, null: false
6
+ t.string :provider, null: false
7
+ t.string :family
8
+ t.datetime :model_created_at
9
+ t.integer :context_window
10
+ t.integer :max_output_tokens
11
+ t.date :knowledge_cutoff
12
+ <% if postgresql? %>
13
+ t.jsonb :modalities, default: {}
14
+ t.jsonb :capabilities, default: []
15
+ t.jsonb :pricing, default: {}
16
+ t.jsonb :metadata, default: {}
17
+ <% elsif mysql? %>
18
+ t.json :modalities
19
+ t.json :capabilities
20
+ t.json :pricing
21
+ t.json :metadata
22
+ <% else %>
23
+ t.json :modalities, default: {}
24
+ t.json :capabilities, default: []
25
+ t.json :pricing, default: {}
26
+ t.json :metadata, default: {}
27
+ <% end %>
28
+ t.timestamps
29
+
30
+ t.index [:provider, :model_id], unique: true
31
+ t.index :provider
32
+ t.index :family
33
+ <% if postgresql? %>
34
+ t.index :capabilities, using: :gin
35
+ t.index :modalities, using: :gin
36
+ <% end %>
37
+ end
38
+
39
+ # Load models from JSON
40
+ say_with_time "Loading models from models.json" do
41
+ RubyLLM.models.load_from_json!
42
+ <%= model_model_name %>.save_to_database
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ <%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
2
+ class Create<%= tool_call_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
3
+ def change
4
+ create_table :<%= tool_call_table_name %> do |t|
5
+ t.string :tool_call_id, null: false
6
+ t.string :name, null: false
7
+ <% if postgresql? %>
8
+ t.jsonb :arguments, default: {}
9
+ <% elsif mysql? %>
10
+ t.json :arguments
11
+ <% else %>
12
+ t.json :arguments, default: {}
13
+ <% end %>
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :<%= tool_call_table_name %>, :tool_call_id, unique: true
18
+ add_index :<%= tool_call_table_name %>, :name
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ RubyLLM.configure do |config|
2
+ config.openai_api_key = ENV['OPENAI_API_KEY'] || Rails.application.credentials.dig(:openai_api_key)
3
+ # config.default_model = "gpt-4.1-nano"
4
+
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
@@ -0,0 +1,4 @@
1
+ class <%= message_model_name %> < ApplicationRecord
2
+ <%= acts_as_message_declaration %><% unless options[:skip_active_storage] %>
3
+ has_many_attached :attachments<% end %>
4
+ end
@@ -0,0 +1,3 @@
1
+ class <%= model_model_name %> < ApplicationRecord
2
+ <%= acts_as_model_declaration %>
3
+ end
@@ -0,0 +1,3 @@
1
+ class <%= tool_call_model_name %> < ApplicationRecord
2
+ <%= acts_as_tool_call_declaration %>
3
+ end
@@ -0,0 +1,145 @@
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
+ <% if @model_table_already_existed %>
7
+ # Load models from models.json if Model table already existed
8
+ say_with_time "Loading models from models.json" do
9
+ RubyLLM.models.load_from_json!
10
+ model_class.save_to_database
11
+ "Loaded #{model_class.count} models"
12
+ end
13
+ <% end %>
14
+
15
+ # Then check for any models in existing data that aren't in models.json
16
+ say_with_time "Checking for additional models in existing data" do
17
+ collect_and_create_models(chat_class, :<%= chat_table_name %>, model_class)
18
+ collect_and_create_models(message_class, :<%= message_table_name %>, model_class)
19
+ model_class.count
20
+ end
21
+
22
+ # Migrate foreign keys
23
+ migrate_foreign_key(:<%= chat_table_name %>, chat_class, model_class, :<%= model_table_name.singularize %>)
24
+ migrate_foreign_key(:<%= message_table_name %>, message_class, model_class, :<%= model_table_name.singularize %>)
25
+ end
26
+
27
+ def down
28
+ # Remove foreign key references
29
+ if column_exists?(:<%= message_table_name %>, :<%= model_table_name.singularize %>_id)
30
+ remove_reference :<%= message_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
31
+ end
32
+
33
+ if column_exists?(:<%= chat_table_name %>, :<%= model_table_name.singularize %>_id)
34
+ remove_reference :<%= chat_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
35
+ end
36
+
37
+ # Restore original model_id string columns
38
+ if column_exists?(:<%= message_table_name %>, :model_id_string)
39
+ rename_column :<%= message_table_name %>, :model_id_string, :model_id
40
+ end
41
+
42
+ if column_exists?(:<%= chat_table_name %>, :model_id_string)
43
+ rename_column :<%= chat_table_name %>, :model_id_string, :model_id
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def collect_and_create_models(record_class, table_name, model_class)
50
+ return unless column_exists?(table_name, :model_id)
51
+
52
+ has_provider = column_exists?(table_name, :provider)
53
+
54
+ # Collect unique model/provider combinations using read_attribute to bypass overrides
55
+ models_set = Set.new
56
+
57
+ record_class.find_each do |record|
58
+ model_id = record.read_attribute(:model_id)
59
+ next if model_id.blank?
60
+
61
+ provider = has_provider ? record.read_attribute(:provider) : nil
62
+ models_set.add([ model_id, provider ])
63
+ end
64
+
65
+ models_set.each do |model_id, provider|
66
+ find_or_create_model(model_id, provider, model_class)
67
+ end
68
+ end
69
+
70
+ def find_or_create_model(model_id, provider, model_class)
71
+ return if model_id.blank?
72
+
73
+ begin
74
+ model_info, _provider = RubyLLM.models.resolve(model_id, provider: provider)
75
+
76
+ model_class.find_or_create_by!(
77
+ model_id: model_info.id,
78
+ provider: model_info.provider
79
+ ) do |m|
80
+ m.name = model_info.name || model_info.id
81
+ m.family = model_info.family
82
+ m.model_created_at = model_info.created_at
83
+ m.context_window = model_info.context_window
84
+ m.max_output_tokens = model_info.max_output_tokens
85
+ m.knowledge_cutoff = model_info.knowledge_cutoff
86
+ m.modalities = model_info.modalities.to_h
87
+ m.capabilities = model_info.capabilities
88
+ m.pricing = model_info.pricing.to_h
89
+ m.metadata = model_info.metadata
90
+ end
91
+ rescue => e
92
+ # Skip models that can't be resolved - they'll need manual fixing
93
+ Rails.logger.warn "Skipping unresolvable model: #{model_id} - will need manual update"
94
+ nil
95
+ end
96
+ end
97
+
98
+
99
+ def migrate_foreign_key(table_name, record_class, model_class, foreign_key_name)
100
+ return unless column_exists?(table_name, :model_id)
101
+
102
+ # Check if we need to rename the string column to avoid collision
103
+ if column_exists?(table_name, :model_id) && !foreign_key_exists?(table_name, :models)
104
+ # Temporarily rename the string column
105
+ rename_column table_name, :model_id, :model_id_string
106
+ end
107
+
108
+ # Add the foreign key reference
109
+ unless column_exists?(table_name, "#{foreign_key_name}_id")
110
+ add_reference table_name, foreign_key_name, foreign_key: true
111
+ end
112
+
113
+ say_with_time "Migrating #{table_name} model references" do
114
+ record_class.reset_column_information
115
+ has_provider = column_exists?(table_name, :provider)
116
+
117
+ # Determine which column to read from (renamed or original)
118
+ model_id_column = column_exists?(table_name, :model_id_string) ? :model_id_string : :model_id
119
+
120
+ record_class.find_each do |record|
121
+ model_id = record.read_attribute(model_id_column)
122
+ next if model_id.blank?
123
+
124
+ provider = has_provider ? record.read_attribute(:provider) : nil
125
+
126
+ model = if has_provider && provider.present?
127
+ model_class.find_by(model_id: model_id, provider: provider)
128
+ else
129
+ find_model_for_record(model_id, model_class)
130
+ end
131
+
132
+ record.update_column("#{foreign_key_name}_id", model.id) if model
133
+ end
134
+ end
135
+ end
136
+
137
+ def find_model_for_record(model_id, model_class)
138
+ begin
139
+ model_info, _provider = RubyLLM.models.resolve(model_id)
140
+ model_class.find_by(model_id: model_info.id, provider: model_info.provider)
141
+ rescue => e
142
+ model_class.find_by(model_id: model_id)
143
+ end
144
+ end
145
+ end