ruby_llm 1.13.2 → 1.14.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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
  4. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
  5. data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
  10. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +1 -1
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
  37. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
  49. data/lib/generators/ruby_llm/generator_helpers.rb +33 -17
  50. data/lib/generators/ruby_llm/install/install_generator.rb +21 -18
  51. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +3 -4
  52. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +1 -1
  53. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
  54. data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
  55. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
  56. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
  57. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
  58. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
  59. data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
  60. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +1 -1
  61. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  62. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  63. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
  64. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
  65. data/lib/ruby_llm/active_record/acts_as.rb +2 -0
  66. data/lib/ruby_llm/active_record/acts_as_legacy.rb +2 -0
  67. data/lib/ruby_llm/active_record/chat_methods.rb +1 -1
  68. data/lib/ruby_llm/active_record/message_methods.rb +28 -0
  69. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  70. data/lib/ruby_llm/active_record/tool_call_methods.rb +28 -0
  71. data/lib/ruby_llm/agent.rb +11 -0
  72. data/lib/ruby_llm/aliases.json +15 -5
  73. data/lib/ruby_llm/attachment.rb +3 -0
  74. data/lib/ruby_llm/configuration.rb +54 -73
  75. data/lib/ruby_llm/connection.rb +1 -3
  76. data/lib/ruby_llm/error.rb +5 -0
  77. data/lib/ruby_llm/model/info.rb +14 -12
  78. data/lib/ruby_llm/models.json +2693 -2160
  79. data/lib/ruby_llm/models.rb +10 -3
  80. data/lib/ruby_llm/provider.rb +5 -0
  81. data/lib/ruby_llm/providers/anthropic.rb +4 -0
  82. data/lib/ruby_llm/providers/azure.rb +4 -0
  83. data/lib/ruby_llm/providers/bedrock.rb +4 -0
  84. data/lib/ruby_llm/providers/deepseek.rb +4 -0
  85. data/lib/ruby_llm/providers/gemini.rb +4 -0
  86. data/lib/ruby_llm/providers/gpustack.rb +4 -0
  87. data/lib/ruby_llm/providers/mistral.rb +4 -0
  88. data/lib/ruby_llm/providers/ollama.rb +4 -0
  89. data/lib/ruby_llm/providers/openai.rb +10 -0
  90. data/lib/ruby_llm/providers/openrouter/images.rb +1 -1
  91. data/lib/ruby_llm/providers/openrouter.rb +4 -0
  92. data/lib/ruby_llm/providers/perplexity.rb +4 -0
  93. data/lib/ruby_llm/providers/vertexai.rb +4 -0
  94. data/lib/ruby_llm/providers/xai.rb +4 -0
  95. data/lib/ruby_llm/version.rb +1 -1
  96. data/lib/tasks/release.rake +1 -1
  97. data/lib/tasks/ruby_llm.rake +6 -5
  98. data/lib/tasks/vcr.rake +1 -1
  99. metadata +47 -10
  100. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
@@ -21,7 +21,7 @@ module RubyLLM
21
21
  desc: 'Skip ActiveStorage installation and attachment setup'
22
22
 
23
23
  desc 'Creates models and migrations for RubyLLM Rails integration\n' \
24
- 'Usage: rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
24
+ 'Usage: bin/rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
25
25
 
26
26
  def self.next_migration_number(dirname)
27
27
  ::ActiveRecord::Generators::Base.next_migration_number(dirname)
@@ -30,20 +30,12 @@ module RubyLLM
30
30
  def create_migration_files
31
31
  migration_template 'create_chats_migration.rb.tt',
32
32
  "db/migrate/create_#{chat_table_name}.rb"
33
-
34
- sleep 1 # Ensure different timestamp
35
33
  migration_template 'create_messages_migration.rb.tt',
36
34
  "db/migrate/create_#{message_table_name}.rb"
37
-
38
- sleep 1 # Ensure different timestamp
39
35
  migration_template 'create_tool_calls_migration.rb.tt',
40
36
  "db/migrate/create_#{tool_call_table_name}.rb"
41
-
42
- sleep 1 # Ensure different timestamp
43
37
  migration_template 'create_models_migration.rb.tt',
44
38
  "db/migrate/create_#{model_table_name}.rb"
45
-
46
- sleep 1 # Ensure different timestamp
47
39
  migration_template 'add_references_to_chats_tool_calls_and_messages_migration.rb.tt',
48
40
  'db/migrate/add_references_to_' \
49
41
  "#{chat_table_name}_#{tool_call_table_name}_and_#{message_table_name}.rb"
@@ -63,6 +55,13 @@ module RubyLLM
63
55
  template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
64
56
  end
65
57
 
58
+ def create_convention_directories
59
+ %w[agents tools schemas prompts].each do |name|
60
+ empty_directory "app/#{name}"
61
+ create_file "app/#{name}/.gitkeep" unless File.exist?(Rails.root.join("app/#{name}/.gitkeep"))
62
+ end
63
+ end
64
+
66
65
  def install_active_storage
67
66
  return if options[:skip_active_storage]
68
67
 
@@ -76,22 +75,18 @@ module RubyLLM
76
75
  say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
77
76
 
78
77
  say "\n Next steps:", :yellow
79
- say ' 1. Run: rails db:migrate'
80
- say ' 2. Run: rails ruby_llm:load_models'
78
+ say ' 1. Run: bin/rails db:migrate'
79
+ say ' 2. Run: bin/rails ruby_llm:load_models'
81
80
  say ' 3. Set your API keys in config/initializers/ruby_llm.rb'
82
81
 
83
- say " 4. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
84
-
85
- say "\n 🚀 Model registry supports database + JSON fallback!", :cyan
86
- say ' Models load from database when present, otherwise from models.json'
87
- say ' Pass model names as strings - RubyLLM handles the rest!'
88
- say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
82
+ say " 4. Start chatting: #{chat_model_name}.create!(model: 'gpt-5-nano').ask('Hello!')"
83
+ say " 5. Optional UI: #{chat_ui_generator_command}"
89
84
 
90
85
  if options[:skip_active_storage]
91
86
  say "\n 📎 Note: ActiveStorage was skipped", :yellow
92
87
  say ' File attachments won\'t work without ActiveStorage.'
93
88
  say ' To enable later:'
94
- say ' 1. Run: rails active_storage:install && rails db:migrate'
89
+ say ' 1. Run: bin/rails active_storage:install && bin/rails db:migrate'
95
90
  say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
96
91
  end
97
92
 
@@ -102,6 +97,14 @@ module RubyLLM
102
97
  say ' • 🐦 Follow for updates: https://x.com/paolino'
103
98
  say "\n"
104
99
  end
100
+
101
+ private
102
+
103
+ def chat_ui_generator_command
104
+ mappings = model_mappings.join(' ')
105
+ mappings = " #{mappings}" unless mappings.empty?
106
+ "bin/rails generate ruby_llm:chat_ui#{mappings}"
107
+ end
105
108
  end
106
109
  end
107
110
  end
@@ -27,14 +27,13 @@ class <%= create_migration_class_name(model_table_name) %> < ActiveRecord::Migra
27
27
  <% end %>
28
28
  t.timestamps
29
29
 
30
- t.index [:provider, :model_id], unique: true
30
+ t.index [ :provider, :model_id ], unique: true
31
31
  t.index :provider
32
32
  t.index :family
33
- <% if postgresql? %>
33
+ <% if postgresql? -%>
34
34
  t.index :capabilities, using: :gin
35
35
  t.index :modalities, using: :gin
36
- <% end %>
36
+ <% end -%>
37
37
  end
38
-
39
38
  end
40
39
  end
@@ -4,7 +4,7 @@ class <%= create_migration_class_name(tool_call_table_name) %> < ActiveRecord::M
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.string :thought_signature
7
+ t.text :thought_signature
8
8
  <% if postgresql? %>
9
9
  t.jsonb :arguments, default: {}
10
10
  <% elsif mysql? %>
@@ -1,6 +1,6 @@
1
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"
2
+ config.openai_api_key = ENV.fetch("OPENAI_API_KEY", Rails.application.credentials.dig(:openai_api_key))
3
+ # config.default_model = "gpt-5-nano"
4
4
 
5
5
  # Use the new association-based acts_as API (recommended)
6
6
  config.use_new_acts_as = true
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RubyLLM
6
+ module Generators
7
+ # Generator for RubyLLM schema classes.
8
+ class SchemaGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ namespace 'ruby_llm:schema'
12
+
13
+ desc 'Creates a RubyLLM schema class'
14
+
15
+ def create_schema_file
16
+ template 'schema.rb.tt', File.join('app/schemas', class_path, "#{file_name}.rb")
17
+ end
18
+
19
+ private
20
+
21
+ def schema_class_name
22
+ class_name.end_with?('Schema') ? class_name : "#{class_name}Schema"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,2 @@
1
+ class <%= schema_class_name %> < RubyLLM::Schema
2
+ end
@@ -0,0 +1,9 @@
1
+ class <%= class_name %>Tool < RubyLLM::Tool
2
+ description "TODO: describe what this tool does"
3
+
4
+ def execute
5
+ # TODO: return something to the LLM.
6
+ # Anything you return here gets converted to a string.
7
+ "TODO: implement <%= class_name %>Tool"
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ <%% tool_call_error = tool_call.tool_error_message %>
2
+ <%% if tool_call_error.present? %>
3
+ <%%= render "messages/error", message: tool_calls, title: "Tool Call Error", error_message: tool_call_error %>
4
+ <%% else %>
5
+ <div id="message_<%%= tool_calls.id %>" class="message"
6
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
7
+ <div style="font-weight: bold; margin-bottom: 5px;"><%= tool_display_name %> Call</div>
8
+ <pre style="white-space: pre-wrap; margin: 0;"><%%= tool_call.name %>(<%%= tool_call.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %>)</pre>
9
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
10
+ <%%= tool_calls.created_at&.strftime("%I:%M %p") %>
11
+ </div>
12
+ </div>
13
+ <%% end %>
@@ -0,0 +1,13 @@
1
+ <%% error_message = tool.tool_error_message %>
2
+ <%% if error_message.present? %>
3
+ <%%= render "messages/error", message: tool, title: "Tool Result Error", error_message: error_message %>
4
+ <%% else %>
5
+ <div id="message_<%%= tool.id %>" class="message"
6
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
7
+ <div style="font-weight: bold; margin-bottom: 5px;"><%= tool_display_name %> Result</div>
8
+ <pre style="white-space: pre-wrap; margin: 0;"><%%= tool.content.presence || "(no output)" %></pre>
9
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
10
+ <%%= tool.created_at&.strftime("%I:%M %p") %>
11
+ </div>
12
+ </div>
13
+ <%% end %>
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RubyLLM
6
+ module Generators
7
+ # Generator for RubyLLM tool classes and related message partials.
8
+ class ToolGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ namespace 'ruby_llm:tool'
12
+
13
+ check_class_collision suffix: 'Tool'
14
+
15
+ desc 'Creates a RubyLLM tool class and matching tool call/result view partials'
16
+
17
+ def create_tool_file
18
+ template 'tool.rb.tt', File.join('app/tools', class_path, "#{file_name}_tool.rb")
19
+ end
20
+
21
+ def create_tool_view_partials
22
+ empty_directory 'app/views/messages/tool_calls'
23
+ empty_directory 'app/views/messages/tool_results'
24
+
25
+ create_tool_call_partial
26
+ create_tool_result_partial
27
+ end
28
+
29
+ private
30
+
31
+ def create_tool_call_partial
32
+ destination_path = File.join('app/views/messages/tool_calls', "_#{tool_partial_name}.html.erb")
33
+ default_partial_path = File.join(destination_root, 'app/views/messages/tool_calls/_default.html.erb')
34
+
35
+ if File.exist?(default_partial_path)
36
+ default_markup = tool_named_call_markup(File.read(default_partial_path))
37
+ indented_markup = indent_non_empty_lines(default_markup, 2)
38
+ create_file destination_path, <<~ERB
39
+ <% tool_call_error = tool_call.tool_error_message %>
40
+ <% if tool_call_error.present? %>
41
+ <%= render "messages/error", message: tool_calls, title: "Tool Call Error", error_message: tool_call_error %>
42
+ <% else %>
43
+ #{indented_markup}<% end %>
44
+ ERB
45
+ else
46
+ template 'tool_call.html.erb.tt', destination_path
47
+ end
48
+
49
+ strip_trailing_whitespace(destination_path)
50
+ end
51
+
52
+ def create_tool_result_partial
53
+ destination_path = File.join('app/views/messages/tool_results', "_#{tool_partial_name}.html.erb")
54
+ default_partial_path = File.join(destination_root, 'app/views/messages/tool_results/_default.html.erb')
55
+
56
+ if File.exist?(default_partial_path)
57
+ create_file destination_path, tool_named_result_markup(File.read(default_partial_path))
58
+ else
59
+ template 'tool_result.html.erb.tt', destination_path
60
+ end
61
+
62
+ strip_trailing_whitespace(destination_path)
63
+ end
64
+
65
+ def tool_named_call_markup(markup)
66
+ markup.sub('Tool Call', "#{tool_display_name} Call")
67
+ end
68
+
69
+ def tool_named_result_markup(markup)
70
+ markup.sub(/\bTool\b(?!\s*Result)/, "#{tool_display_name} Result")
71
+ end
72
+
73
+ def tool_display_name
74
+ class_name.demodulize
75
+ end
76
+
77
+ def tool_partial_name
78
+ file_name.delete_suffix('_tool')
79
+ end
80
+
81
+ def indent_non_empty_lines(markup, spaces)
82
+ indentation = ' ' * spaces
83
+ markup.lines.map { |line| line.strip.empty? ? line : "#{indentation}#{line}" }.join
84
+ end
85
+
86
+ def strip_trailing_whitespace(path)
87
+ content = File.read(path)
88
+ stripped_content = content.lines.map(&:rstrip).join("\n")
89
+ stripped_content = "#{stripped_content}\n" unless stripped_content.end_with?("\n")
90
+ return if content == stripped_content
91
+
92
+ File.write(path, stripped_content)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -38,7 +38,7 @@ module RubyLLM
38
38
 
39
39
  Next steps:
40
40
  1. Review the generated migration
41
- 2. Run: rails db:migrate
41
+ 2. Run: bin/rails db:migrate
42
42
  3. Restart your application server
43
43
 
44
44
  📚 See the v1.10.0 release notes for details on extended thinking support.
@@ -0,0 +1,7 @@
1
+ class AddRubyLlmV114Columns < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ if column_exists?(:<%= tool_call_table_name %>, :thought_signature, :string)
4
+ change_column :<%= tool_call_table_name %>, :thought_signature, :text
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,49 @@
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 to fix tool call thought signature column type for MySQL safety.
10
+ class UpgradeToV114Generator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+ include RubyLLM::Generators::GeneratorHelpers
13
+
14
+ namespace 'ruby_llm:upgrade_to_v1_14'
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ argument :model_mappings, type: :array, default: [], banner: 'tool_call:ToolCallName'
18
+
19
+ desc 'Updates tool call thought_signature column to text introduced in v1.14.0'
20
+
21
+ def self.next_migration_number(dirname)
22
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
23
+ end
24
+
25
+ def create_migration_file
26
+ parse_model_mappings
27
+
28
+ migration_template 'add_v1_14_tool_call_columns.rb.tt',
29
+ 'db/migrate/add_ruby_llm_v1_14_columns.rb',
30
+ migration_version: migration_version,
31
+ tool_call_table_name: tool_call_table_name
32
+ end
33
+
34
+ def show_next_steps
35
+ say_status :success, 'Upgrade prepared!', :green
36
+ say <<~INSTRUCTIONS
37
+
38
+ Next steps:
39
+ 1. Review the generated migration
40
+ 2. Run: bin/rails db:migrate
41
+ 3. Restart your application server
42
+
43
+ 📚 See the v1.14.0 release notes for details on thought signature persistence.
44
+
45
+ INSTRUCTIONS
46
+ end
47
+ end
48
+ end
49
+ end
@@ -25,7 +25,7 @@ module RubyLLM
25
25
  argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
26
26
 
27
27
  desc 'Upgrades existing RubyLLM apps to v1.7 with new Rails-like API\n' \
28
- 'Usage: rails g ruby_llm:upgrade_to_v1_7 [chat:ChatName] [message:MessageName] ...'
28
+ 'Usage: bin/rails g ruby_llm:upgrade_to_v1_7 [chat:ChatName] [message:MessageName] ...'
29
29
 
30
30
  def self.next_migration_number(dirname)
31
31
  ::ActiveRecord::Generators::Base.next_migration_number(dirname)
@@ -40,8 +40,6 @@ module RubyLLM
40
40
  "db/migrate/create_#{table_name_for(model_model_name)}.rb",
41
41
  migration_version: migration_version,
42
42
  model_model_name: model_model_name
43
-
44
- sleep 1 # Ensure different timestamp
45
43
  end
46
44
 
47
45
  migration_template 'migration.rb.tt',
@@ -93,7 +91,7 @@ module RubyLLM
93
91
 
94
92
  Next steps:
95
93
  1. Review the generated migrations
96
- 2. Run: rails db:migrate
94
+ 2. Run: bin/rails db:migrate
97
95
  3. Update your code to use the new API: #{chat_model_name}.create! now has the same signature as RubyLLM.chat
98
96
 
99
97
  ⚠️ If you get "undefined method 'acts_as_model'" during migration:
@@ -37,7 +37,7 @@ module RubyLLM
37
37
 
38
38
  Next steps:
39
39
  1. Review the generated migration
40
- 2. Run: rails db:migrate
40
+ 2. Run: bin/rails db:migrate
41
41
  3. Restart your application server
42
42
 
43
43
  📚 See the v1.9.0 release notes for details on cached token tracking and raw content support.
@@ -148,6 +148,8 @@ module RubyLLM
148
148
 
149
149
  def acts_as_tool_call(message: :message, message_class: nil, message_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
150
150
  result: :result, result_class: nil, result_foreign_key: nil)
151
+ include RubyLLM::ActiveRecord::ToolCallMethods
152
+
151
153
  class_attribute :message_association_name, :result_association_name, :message_class, :result_class
152
154
 
153
155
  self.message_association_name = message
@@ -58,6 +58,8 @@ module RubyLLM
58
58
  end
59
59
 
60
60
  def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
61
+ include RubyLLM::ActiveRecord::ToolCallMethods
62
+
61
63
  @message_class = message_class.to_s
62
64
  @message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
63
65
  @result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
@@ -125,7 +125,7 @@ module RubyLLM
125
125
  self.assume_model_exists = assume_exists
126
126
  resolve_model_from_strings
127
127
  save!
128
- to_llm.with_model(model.model_id, provider: model.provider.to_sym, assume_exists:)
128
+ to_llm.with_model(model_association.model_id, provider: model_association.provider.to_sym, assume_exists:)
129
129
  self
130
130
  end
131
131
 
@@ -39,6 +39,25 @@ module RubyLLM
39
39
  )
40
40
  end
41
41
 
42
+ def to_partial_path
43
+ partial_prefix = self.class.name.underscore.pluralize
44
+ role_partial = if to_llm.tool_call?
45
+ 'tool_calls'
46
+ elsif role.to_s == 'tool'
47
+ 'tool'
48
+ else
49
+ role.to_s.presence || 'assistant'
50
+ end
51
+ "#{partial_prefix}/#{role_partial}"
52
+ end
53
+
54
+ def tool_error_message
55
+ payload = parse_payload(content)
56
+ return unless payload.is_a?(Hash)
57
+
58
+ payload['error'] || payload[:error]
59
+ end
60
+
42
61
  private
43
62
 
44
63
  def thinking_text_value
@@ -109,6 +128,15 @@ module RubyLLM
109
128
  @_tempfiles << tempfile
110
129
  tempfile
111
130
  end
131
+
132
+ def parse_payload(value)
133
+ return value if value.is_a?(Hash) || value.is_a?(Array)
134
+ return if value.blank?
135
+
136
+ JSON.parse(value)
137
+ rescue JSON::ParserError
138
+ nil
139
+ end
112
140
  end
113
141
  end
114
142
  end
@@ -77,7 +77,7 @@ module RubyLLM
77
77
  delegate :supports?, :supports_vision?, :supports_functions?, :type,
78
78
  :input_price_per_million, :output_price_per_million,
79
79
  :function_calling?, :structured_output?, :batch?,
80
- :reasoning?, :citations?, :streaming?, :provider_class,
80
+ :reasoning?, :citations?, :streaming?, :provider_class, :label,
81
81
  to: :to_llm
82
82
  end
83
83
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ActiveRecord
5
+ # Methods mixed into tool call models.
6
+ module ToolCallMethods
7
+ extend ActiveSupport::Concern
8
+
9
+ def tool_error_message
10
+ payload = parse_payload(arguments)
11
+ return unless payload.is_a?(Hash)
12
+
13
+ payload['error'] || payload[:error]
14
+ end
15
+
16
+ private
17
+
18
+ def parse_payload(value)
19
+ return value if value.is_a?(Hash) || value.is_a?(Array)
20
+ return if value.blank?
21
+
22
+ JSON.parse(value)
23
+ rescue JSON::ParserError
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -121,6 +121,7 @@ module RubyLLM
121
121
  input_values, = partition_inputs(kwargs)
122
122
  record = resolved_chat_model.find(id)
123
123
  apply_configuration(record, input_values:, persist_instructions: false)
124
+
124
125
  record
125
126
  end
126
127
 
@@ -129,6 +130,7 @@ module RubyLLM
129
130
 
130
131
  input_values, = partition_inputs(kwargs)
131
132
  record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
133
+ apply_assume_model_exists(record)
132
134
  runtime = runtime_context(chat: record, inputs: input_values)
133
135
  instructions_value = resolved_instructions_value(record, runtime, inputs: input_values)
134
136
  return record if instructions_value.nil?
@@ -227,9 +229,18 @@ module RubyLLM
227
229
  end
228
230
 
229
231
  def llm_chat_for(chat_object)
232
+ apply_assume_model_exists(chat_object)
230
233
  chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
231
234
  end
232
235
 
236
+ def apply_assume_model_exists(chat_object)
237
+ return unless chat_kwargs.key?(:assume_model_exists) &&
238
+ resolved_chat_model &&
239
+ chat_object.is_a?(resolved_chat_model)
240
+
241
+ chat_object.assume_model_exists = chat_kwargs[:assume_model_exists]
242
+ end
243
+
233
244
  def evaluate(value, runtime)
234
245
  value.is_a?(Proc) ? runtime.instance_exec(&value) : value
235
246
  end
@@ -26,8 +26,7 @@
26
26
  "bedrock": "anthropic.claude-3-haiku-20240307-v1:0:200k"
27
27
  },
28
28
  "claude-3-opus": {
29
- "anthropic": "claude-3-opus-20240229",
30
- "bedrock": "anthropic.claude-3-opus-20240229-v1:0"
29
+ "anthropic": "claude-3-opus-20240229"
31
30
  },
32
31
  "claude-3-sonnet": {
33
32
  "anthropic": "claude-3-sonnet-20240229",
@@ -62,7 +61,8 @@
62
61
  "claude-opus-4-6": {
63
62
  "anthropic": "claude-opus-4-6",
64
63
  "openrouter": "anthropic/claude-opus-4.6",
65
- "bedrock": "anthropic.claude-opus-4-6-v1"
64
+ "bedrock": "anthropic.claude-opus-4-6-v1",
65
+ "azure": "claude-opus-4-6"
66
66
  },
67
67
  "claude-sonnet-4": {
68
68
  "anthropic": "claude-sonnet-4-20250514",
@@ -81,7 +81,8 @@
81
81
  "claude-sonnet-4-6": {
82
82
  "anthropic": "claude-sonnet-4-6",
83
83
  "openrouter": "anthropic/claude-sonnet-4.6",
84
- "bedrock": "anthropic.claude-sonnet-4-6"
84
+ "bedrock": "anthropic.claude-sonnet-4-6",
85
+ "azure": "claude-sonnet-4-6"
85
86
  },
86
87
  "deepseek-chat": {
87
88
  "deepseek": "deepseek-chat",
@@ -337,7 +338,8 @@
337
338
  },
338
339
  "gpt-5.1": {
339
340
  "openai": "gpt-5.1",
340
- "openrouter": "openai/gpt-5.1"
341
+ "openrouter": "openai/gpt-5.1",
342
+ "azure": "gpt-5.1"
341
343
  },
342
344
  "gpt-5.1-codex": {
343
345
  "openai": "gpt-5.1-codex",
@@ -367,6 +369,14 @@
367
369
  "openai": "gpt-5.3-codex",
368
370
  "openrouter": "openai/gpt-5.3-codex"
369
371
  },
372
+ "gpt-5.4": {
373
+ "openai": "gpt-5.4",
374
+ "openrouter": "openai/gpt-5.4"
375
+ },
376
+ "gpt-5.4-pro": {
377
+ "openai": "gpt-5.4-pro",
378
+ "openrouter": "openai/gpt-5.4-pro"
379
+ },
370
380
  "gpt-audio": {
371
381
  "openai": "gpt-audio",
372
382
  "openrouter": "openai/gpt-audio"
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
4
+ require 'uri'
5
+
3
6
  module RubyLLM
4
7
  # A class representing a file attachment.
5
8
  class Attachment