ruby_llm_community 1.2.0 → 1.3.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -9
  3. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +127 -67
  4. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +12 -12
  5. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +7 -7
  6. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
  7. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +6 -6
  8. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +4 -4
  9. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -5
  10. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +5 -5
  11. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +4 -4
  12. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +8 -8
  13. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +5 -5
  15. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +9 -6
  16. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +7 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -5
  18. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +9 -9
  19. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +4 -6
  20. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +11 -11
  21. data/lib/generators/ruby_llm/generator_helpers.rb +152 -87
  22. data/lib/generators/ruby_llm/install/install_generator.rb +75 -79
  23. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +3 -0
  24. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +5 -0
  25. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +7 -1
  26. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
  27. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +88 -85
  28. data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  29. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  30. data/lib/ruby_llm/active_record/acts_as.rb +23 -16
  31. data/lib/ruby_llm/active_record/chat_methods.rb +41 -13
  32. data/lib/ruby_llm/active_record/message_methods.rb +11 -2
  33. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  34. data/lib/ruby_llm/aliases.json +61 -32
  35. data/lib/ruby_llm/attachment.rb +42 -11
  36. data/lib/ruby_llm/chat.rb +13 -2
  37. data/lib/ruby_llm/configuration.rb +6 -1
  38. data/lib/ruby_llm/connection.rb +4 -4
  39. data/lib/ruby_llm/content.rb +23 -0
  40. data/lib/ruby_llm/message.rb +17 -9
  41. data/lib/ruby_llm/model/info.rb +4 -0
  42. data/lib/ruby_llm/models.json +7157 -6089
  43. data/lib/ruby_llm/models.rb +14 -22
  44. data/lib/ruby_llm/provider.rb +27 -5
  45. data/lib/ruby_llm/providers/anthropic/chat.rb +18 -5
  46. data/lib/ruby_llm/providers/anthropic/content.rb +44 -0
  47. data/lib/ruby_llm/providers/anthropic/media.rb +6 -5
  48. data/lib/ruby_llm/providers/anthropic/models.rb +9 -2
  49. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -18
  50. data/lib/ruby_llm/providers/bedrock/media.rb +2 -1
  51. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +9 -2
  52. data/lib/ruby_llm/providers/gemini/chat.rb +353 -72
  53. data/lib/ruby_llm/providers/gemini/media.rb +59 -1
  54. data/lib/ruby_llm/providers/gemini/tools.rb +146 -25
  55. data/lib/ruby_llm/providers/gemini/transcription.rb +116 -0
  56. data/lib/ruby_llm/providers/gemini.rb +2 -1
  57. data/lib/ruby_llm/providers/gpustack/media.rb +1 -0
  58. data/lib/ruby_llm/providers/ollama/media.rb +1 -0
  59. data/lib/ruby_llm/providers/openai/capabilities.rb +15 -7
  60. data/lib/ruby_llm/providers/openai/chat.rb +7 -3
  61. data/lib/ruby_llm/providers/openai/media.rb +2 -1
  62. data/lib/ruby_llm/providers/openai/streaming.rb +7 -3
  63. data/lib/ruby_llm/providers/openai/tools.rb +34 -12
  64. data/lib/ruby_llm/providers/openai/transcription.rb +70 -0
  65. data/lib/ruby_llm/providers/openai_base.rb +1 -0
  66. data/lib/ruby_llm/providers/vertexai/transcription.rb +16 -0
  67. data/lib/ruby_llm/providers/vertexai.rb +11 -11
  68. data/lib/ruby_llm/railtie.rb +24 -22
  69. data/lib/ruby_llm/stream_accumulator.rb +8 -12
  70. data/lib/ruby_llm/tool.rb +126 -0
  71. data/lib/ruby_llm/transcription.rb +35 -0
  72. data/lib/ruby_llm/utils.rb +46 -0
  73. data/lib/ruby_llm/version.rb +1 -1
  74. data/lib/ruby_llm_community.rb +7 -1
  75. metadata +27 -3
@@ -0,0 +1,7 @@
1
+ <div style="display: flex; flex-direction: column; gap: 3px; align-items: flex-start; font-family: monospace;">
2
+ <%% <%= message_model_name.demodulize.underscore %>.<%= tool_call_variable_name.pluralize %>.each do |tool_call| %>
3
+ <div style="background: #eee; padding: 5px; border-radius: 4px;">
4
+ <%%= tool_call.name %>(<%%= tool_call.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %>)
5
+ </div>
6
+ <%% end %>
7
+ </div>
@@ -1,9 +1,9 @@
1
- <%%= turbo_stream.append "<%= message_model_name.tableize %>" do %>
2
- <%% @<%= chat_model_name.underscore %>.messages_association.last(2).each do |<%= message_model_name.underscore %>| %>
3
- <%%= render "<%= message_model_name.tableize %>/<%= message_model_name.underscore %>", <%= message_model_name.underscore %>: <%= message_model_name.underscore %> %>
1
+ <%%= turbo_stream.append "<%= message_table_name %>" do %>
2
+ <%% @<%= chat_variable_name %>.<%= message_table_name %>.last(2).each do |<%= message_variable_name %>| %>
3
+ <%%= render <%= message_variable_name %> %>
4
4
  <%% end %>
5
5
  <%% end %>
6
6
 
7
- <%%= turbo_stream.replace "new_<%= message_model_name.underscore %>" do %>
8
- <%%= render "<%= message_model_name.tableize %>/form", <%= chat_model_name.underscore %>: @<%= chat_model_name.underscore %>, <%= message_model_name.underscore %>: @<%= chat_model_name.underscore %>.messages_association.build %>
7
+ <%%= turbo_stream.replace "new_<%= message_variable_name %>" do %>
8
+ <%%= render "<%= message_model_name.underscore.pluralize %>/form", <%= chat_variable_name %>: @<%= chat_variable_name %>, <%= message_variable_name %>: @<%= chat_variable_name %>.<%= message_table_name %>.build %>
9
9
  <%% end %>
@@ -1,16 +1,16 @@
1
- <tr id="<%%= dom_id <%= model_model_name.underscore %> %>">
2
- <td><%%= <%= model_model_name.underscore %>.provider %></td>
3
- <td><%%= <%= model_model_name.underscore %>.name %></td>
4
- <td><%%= number_with_delimiter(<%= model_model_name.underscore %>.context_window) if <%= model_model_name.underscore %>.context_window %></td>
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
5
  <td>
6
- <%% if <%= model_model_name.underscore %>.pricing && <%= model_model_name.underscore %>.pricing['text_tokens'] && <%= model_model_name.underscore %>.pricing['text_tokens']['standard'] %>
7
- <%% input = <%= model_model_name.underscore %>.pricing['text_tokens']['standard']['input_per_million'] %>
8
- <%% output = <%= model_model_name.underscore %>.pricing['text_tokens']['standard']['output_per_million'] %>
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
9
  <%% if input && output %>
10
10
  $<%%= "%.2f" % input %> / $<%%= "%.2f" % output %>
11
11
  <%% end %>
12
12
  <%% end %>
13
13
  </td>
14
- <td><%%= link_to "Show", <%= model_model_name.underscore %> %></td>
15
- <td><%%= link_to "Start <%= chat_model_name.underscore.humanize.downcase %>", new_<%= chat_model_name.underscore %>_path(model: <%= model_model_name.underscore %>.model_id) %></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
16
  </tr>
@@ -5,10 +5,10 @@
5
5
  <h1><%= model_model_name.pluralize %></h1>
6
6
 
7
7
  <p>
8
- <%%= button_to "Refresh <%= model_model_name.pluralize %>", refresh_<%= model_model_name.tableize %>_path, method: :post %>
8
+ <%%= button_to "Refresh <%= model_model_name.pluralize %>", refresh_<%= model_table_name %>_path, method: :post %>
9
9
  </p>
10
10
 
11
- <div id="<%= model_model_name.tableize %>">
11
+ <div id="<%= model_table_name %>">
12
12
  <table>
13
13
  <thead>
14
14
  <tr>
@@ -20,11 +20,9 @@
20
20
  </tr>
21
21
  </thead>
22
22
  <tbody>
23
- <%% @<%= model_model_name.tableize %>.values.flatten.each do |<%= model_model_name.underscore %>| %>
24
- <%%= render <%= model_model_name.underscore %> %>
25
- <%% end %>
23
+ <%%= render @<%= model_variable_name.pluralize %> %>
26
24
  </tbody>
27
25
  </table>
28
26
  </div>
29
27
 
30
- <%%= link_to "Back to <%= chat_model_name.tableize.humanize.downcase %>", <%= chat_model_name.tableize %>_path %>
28
+ <%%= link_to "Back to <%= chat_table_name.humanize.downcase %>", <%= chat_table_name %>_path %>
@@ -1,18 +1,18 @@
1
- <%% content_for :title, @model.name %>
1
+ <%% content_for :title, @<%= model_variable_name %>.name %>
2
2
 
3
- <h1><%%= @model.name %></h1>
3
+ <h1><%%= @<%= model_variable_name %>.name %></h1>
4
4
 
5
- <p><strong>ID:</strong> <%%= @model.model_id %></p>
6
- <p><strong>Provider:</strong> <%%= @model.provider %></p>
7
- <p><strong>Context Window:</strong> <%%= number_with_delimiter(@model.context_window) %> tokens</p>
8
- <p><strong>Max Output:</strong> <%%= number_with_delimiter(@model.max_output_tokens) %> tokens</p>
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
9
 
10
- <%% if @model.capabilities.any? %>
11
- <p><strong>Capabilities:</strong> <%%= @model.capabilities.join(", ") %></p>
10
+ <%% if @<%= model_variable_name %>.capabilities.any? %>
11
+ <p><strong>Capabilities:</strong> <%%= @<%= model_variable_name %>.capabilities.join(", ") %></p>
12
12
  <%% end %>
13
13
 
14
14
  <p>
15
- <%%= link_to "Start chat with this model", new_chat_path(model: @model.model_id) %> |
16
- <%%= link_to "All models", models_path %> |
17
- <%%= link_to "Back to chats", chats_path %>
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
18
  </p>
@@ -1,129 +1,194 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- # Shared helpers for RubyLLM generators
5
- module GeneratorHelpers
6
- def parse_model_mappings
7
- @model_names = {
8
- chat: 'Chat',
9
- message: 'Message',
10
- tool_call: 'ToolCall',
11
- model: 'Model'
12
- }
13
-
14
- model_mappings.each do |mapping|
15
- if mapping.include?(':')
16
- key, value = mapping.split(':', 2)
17
- @model_names[key.to_sym] = value.classify
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
18
20
  end
21
+
22
+ @model_names
19
23
  end
20
24
 
21
- @model_names
22
- end
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
23
30
 
24
- %i[chat message tool_call model].each do |type|
25
- define_method("#{type}_model_name") do
26
- @model_names ||= parse_model_mappings
27
- @model_names[type]
28
- end
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
29
46
 
30
- define_method("#{type}_table_name") do
31
- table_name_for(send("#{type}_model_name"))
47
+ define_method("#{type}_partial") do
48
+ partial_path_for(send("#{type}_model_name"))
49
+ end
32
50
  end
33
- end
34
51
 
35
- def acts_as_chat_declaration
36
- params = []
52
+ def acts_as_chat_declaration
53
+ params = []
37
54
 
38
- add_association_params(params, :messages, message_table_name, message_model_name, plural: true)
39
- add_association_params(params, :model, model_table_name, model_model_name)
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)
40
59
 
41
- "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
42
- end
60
+ "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
61
+ end
43
62
 
44
- def acts_as_message_declaration
45
- params = []
63
+ def acts_as_message_declaration
64
+ params = []
46
65
 
47
- add_association_params(params, :chat, chat_table_name, chat_model_name)
48
- add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name, plural: true)
49
- add_association_params(params, :model, model_table_name, model_model_name)
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)
50
72
 
51
- "acts_as_message#{" #{params.join(', ')}" if params.any?}"
52
- end
73
+ "acts_as_message#{" #{params.join(', ')}" if params.any?}"
74
+ end
53
75
 
54
- def acts_as_model_declaration
55
- params = []
76
+ def acts_as_model_declaration
77
+ params = []
56
78
 
57
- add_association_params(params, :chats, chat_table_name, chat_model_name, plural: true)
79
+ add_association_params(params, :chats, chat_table_name, chat_model_name,
80
+ owner_table: model_table_name, plural: true)
58
81
 
59
- "acts_as_model#{" #{params.join(', ')}" if params.any?}"
60
- end
82
+ "acts_as_model#{" #{params.join(', ')}" if params.any?}"
83
+ end
61
84
 
62
- def acts_as_tool_call_declaration
63
- params = []
85
+ def acts_as_tool_call_declaration
86
+ params = []
64
87
 
65
- add_association_params(params, :message, message_table_name, message_model_name)
88
+ add_association_params(params, :message, message_table_name, message_model_name,
89
+ owner_table: tool_call_table_name)
66
90
 
67
- "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
68
- end
91
+ "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
92
+ end
69
93
 
70
- def create_namespace_modules
71
- namespaces = []
94
+ def create_namespace_modules
95
+ namespaces = []
72
96
 
73
- [chat_model_name, message_model_name, tool_call_model_name, model_model_name].each do |model_name|
74
- if model_name.include?('::')
75
- namespace = model_name.split('::').first
76
- namespaces << namespace unless namespaces.include?(namespace)
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
77
102
  end
78
- end
79
103
 
80
- namespaces.each do |namespace|
81
- module_path = "app/models/#{namespace.underscore}.rb"
82
- next if File.exist?(Rails.root.join(module_path))
104
+ namespaces.each do |namespace|
105
+ module_path = "app/models/#{namespace.underscore}.rb"
106
+ next if File.exist?(Rails.root.join(module_path))
83
107
 
84
- create_file module_path do
85
- <<~RUBY
86
- module #{namespace}
87
- def self.table_name_prefix
88
- "#{namespace.underscore}_"
108
+ create_file module_path do
109
+ <<~RUBY
110
+ module #{namespace}
111
+ def self.table_name_prefix
112
+ "#{namespace.underscore}_"
113
+ end
89
114
  end
90
- end
91
- RUBY
115
+ RUBY
116
+ end
92
117
  end
93
118
  end
94
- end
95
119
 
96
- def migration_version
97
- "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
98
- end
120
+ def migration_version
121
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
122
+ end
99
123
 
100
- def postgresql?
101
- ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
102
- rescue StandardError
103
- false
104
- end
124
+ def postgresql?
125
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
126
+ rescue StandardError
127
+ false
128
+ end
105
129
 
106
- def table_exists?(table_name)
107
- ::ActiveRecord::Base.connection.table_exists?(table_name)
108
- rescue StandardError
109
- false
110
- end
130
+ def mysql?
131
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('mysql')
132
+ rescue StandardError
133
+ false
134
+ end
111
135
 
112
- private
136
+ def table_exists?(table_name)
137
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
138
+ rescue StandardError
139
+ false
140
+ end
113
141
 
114
- def add_association_params(params, default_assoc, table_name, model_name, plural: false)
115
- assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
142
+ private
116
143
 
117
- return if assoc == default_assoc
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
118
146
 
119
- params << "#{default_assoc}: :#{assoc}"
120
- params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
121
- end
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
122
160
 
123
- def table_name_for(model_name)
124
161
  # Convert namespaced model names to proper table names
125
162
  # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
126
- model_name.underscore.pluralize.tr('/', '_')
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
127
192
  end
128
193
  end
129
194
  end
@@ -5,106 +5,102 @@ require 'rails/generators/active_record'
5
5
  require_relative '../generator_helpers'
6
6
 
7
7
  module RubyLLM
8
- # Generator for RubyLLM Rails models and migrations
9
- class InstallGenerator < Rails::Generators::Base
10
- include Rails::Generators::Migration
11
- include RubyLLM::GeneratorHelpers
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
12
13
 
13
- namespace 'ruby_llm:install'
14
+ namespace 'ruby_llm:install'
14
15
 
15
- source_root File.expand_path('templates', __dir__)
16
+ source_root File.expand_path('templates', __dir__)
16
17
 
17
- argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
18
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
18
19
 
19
- class_option :skip_active_storage, type: :boolean, default: false,
20
- desc: 'Skip ActiveStorage installation and attachment setup'
20
+ class_option :skip_active_storage, type: :boolean, default: false,
21
+ desc: 'Skip ActiveStorage installation and attachment setup'
21
22
 
22
- desc 'Creates models and migrations for RubyLLM Rails integration\n' \
23
- 'Usage: rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
23
+ desc 'Creates models and migrations for RubyLLM Rails integration\n' \
24
+ 'Usage: rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
24
25
 
25
- def self.next_migration_number(dirname)
26
- ::ActiveRecord::Generators::Base.next_migration_number(dirname)
27
- end
26
+ def self.next_migration_number(dirname)
27
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
28
+ end
28
29
 
29
- def create_migration_files
30
- # Create migrations with timestamps to ensure proper order
31
- # First create chats table
32
- migration_template 'create_chats_migration.rb.tt',
33
- "db/migrate/create_#{chat_table_name}.rb"
34
-
35
- # Then create messages table
36
- sleep 1 # Ensure different timestamp
37
- migration_template 'create_messages_migration.rb.tt',
38
- "db/migrate/create_#{message_table_name}.rb"
39
-
40
- # Then create tool_calls table
41
- sleep 1 # Ensure different timestamp
42
- migration_template 'create_tool_calls_migration.rb.tt',
43
- "db/migrate/create_#{tool_call_table_name}.rb"
44
-
45
- # Create models table
46
- sleep 1 # Ensure different timestamp
47
- migration_template 'create_models_migration.rb.tt',
48
- "db/migrate/create_#{model_table_name}.rb"
49
-
50
- # Add references to chats, tool_calls and messages.
51
- sleep 1 # Ensure different timestamp
52
- migration_template 'add_references_to_chats_tool_calls_and_messages_migration.rb.tt',
53
- 'db/migrate/add_references_to_' \
54
- "#{chat_table_name}_#{tool_call_table_name}_and_#{message_table_name}.rb"
55
- end
30
+ def create_migration_files
31
+ migration_template 'create_chats_migration.rb.tt',
32
+ "db/migrate/create_#{chat_table_name}.rb"
56
33
 
57
- def create_model_files
58
- create_namespace_modules
34
+ sleep 1 # Ensure different timestamp
35
+ migration_template 'create_messages_migration.rb.tt',
36
+ "db/migrate/create_#{message_table_name}.rb"
59
37
 
60
- template 'chat_model.rb.tt', "app/models/#{chat_model_name.underscore}.rb"
61
- template 'message_model.rb.tt', "app/models/#{message_model_name.underscore}.rb"
62
- template 'tool_call_model.rb.tt', "app/models/#{tool_call_model_name.underscore}.rb"
38
+ sleep 1 # Ensure different timestamp
39
+ migration_template 'create_tool_calls_migration.rb.tt',
40
+ "db/migrate/create_#{tool_call_table_name}.rb"
63
41
 
64
- template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
65
- end
42
+ sleep 1 # Ensure different timestamp
43
+ migration_template 'create_models_migration.rb.tt',
44
+ "db/migrate/create_#{model_table_name}.rb"
66
45
 
67
- def create_initializer
68
- template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
69
- end
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
70
51
 
71
- def install_active_storage
72
- return if options[:skip_active_storage]
52
+ def create_model_files
53
+ create_namespace_modules
73
54
 
74
- say ' Installing ActiveStorage for file attachments...', :cyan
75
- rails_command 'active_storage:install'
76
- end
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
77
61
 
78
- def show_install_info
79
- say "\n ✅ RubyLLM installed!", :green
62
+ def create_initializer
63
+ template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
64
+ end
80
65
 
81
- say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
66
+ def install_active_storage
67
+ return if options[:skip_active_storage]
82
68
 
83
- say "\n Next steps:", :yellow
84
- say ' 1. Run: rails db:migrate'
85
- say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
69
+ say ' Installing ActiveStorage for file attachments...', :cyan
70
+ rails_command 'active_storage:install'
71
+ end
86
72
 
87
- say " 3. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
73
+ def show_install_info
74
+ say "\n ✅ RubyLLM installed!", :green
88
75
 
89
- say "\n 🚀 Model registry is database-backed!", :cyan
90
- say ' Models automatically load from the database'
91
- say ' Pass model names as strings - RubyLLM handles the rest!'
92
- say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
76
+ say ' ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
93
77
 
94
- if options[:skip_active_storage]
95
- say "\n 📎 Note: ActiveStorage was skipped", :yellow
96
- say ' File attachments won\'t work without ActiveStorage.'
97
- say ' To enable later:'
98
- say ' 1. Run: rails active_storage:install && rails db:migrate'
99
- say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
100
- end
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'
101
81
 
102
- say "\n 📚 Documentation: https://rubyllm.com", :cyan
82
+ say " 3. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
103
83
 
104
- say "\n ❤️ Love RubyLLM?", :magenta
105
- say ' Star on GitHub: https://github.com/crmne/ruby_llm'
106
- say ' 🐦 Follow for updates: https://x.com/paolino'
107
- say "\n"
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
108
104
  end
109
105
  end
110
106
  end
@@ -3,8 +3,11 @@ class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::M
3
3
  create_table :<%= message_table_name %> do |t|
4
4
  t.string :role, null: false
5
5
  t.text :content
6
+ t.json :content_raw
6
7
  t.integer :input_tokens
7
8
  t.integer :output_tokens
9
+ t.integer :cached_tokens
10
+ t.integer :cache_creation_tokens
8
11
  t.timestamps
9
12
  end
10
13
 
@@ -14,6 +14,11 @@ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Mig
14
14
  t.jsonb :capabilities, default: []
15
15
  t.jsonb :pricing, default: {}
16
16
  t.jsonb :metadata, default: {}
17
+ <% elsif mysql? %>
18
+ t.json :modalities
19
+ t.json :capabilities
20
+ t.json :pricing
21
+ t.json :metadata
17
22
  <% else %>
18
23
  t.json :modalities, default: {}
19
24
  t.json :capabilities, default: []
@@ -4,7 +4,13 @@ class Create<%= tool_call_model_name.gsub('::', '').pluralize %> < ActiveRecord:
4
4
  create_table :<%= tool_call_table_name %> do |t|
5
5
  t.string :tool_call_id, null: false
6
6
  t.string :name, null: false
7
- t.<%= postgresql? ? 'jsonb' : 'json' %> :arguments, default: {}
7
+ <% if postgresql? %>
8
+ t.jsonb :arguments, default: {}
9
+ <% elsif mysql? %>
10
+ t.json :arguments
11
+ <% else %>
12
+ t.json :arguments, default: {}
13
+ <% end %>
8
14
  t.timestamps
9
15
  end
10
16
 
@@ -1,5 +1,5 @@
1
1
  RubyLLM.configure do |config|
2
- config.openai_api_key = Rails.application.credentials.dig(:openai_api_key)
2
+ config.openai_api_key = ENV['OPENAI_API_KEY'] || Rails.application.credentials.dig(:openai_api_key)
3
3
  # config.default_model = "gpt-4.1-nano"
4
4
 
5
5
  # Use the new association-based acts_as API (recommended)