From ddd89fad1bf59d57a8b75e4958c30090fe511797 Mon Sep 17 00:00:00 2001 From: Trevor Turk Date: Sat, 1 Nov 2025 21:09:50 -0500 Subject: [PATCH] Fix Zeitwerk eager loading crash with railtie.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Applications using ruby_llm crash during eager loading (production boot): ``` uninitialized constant RubyLLM::Rails (NameError) class Railtie < Rails::Railtie ^^^^^ ``` This occurs when: - `config.eager_load = true` (standard in production) - Zeitwerk eager loads all files including railtie.rb - railtie.rb references `Rails::Railtie` unconditionally - Rails isn't loaded yet or app doesn't use Rails ##Root Cause PR #59 added conditional require (`require 'ruby_llm/railtie' if defined?(Rails::Railtie)`), but this is insufficient because Zeitwerk's eager loading happens BEFORE that conditional check runs. When Zeitwerk eager loads, it loads railtie.rb and expects `RubyLLM::Railtie` to be defined, but the class definition fails because `Rails::Railtie` doesn't exist. ## Solution Two complementary fixes (both required): **1. Wrap class definition** in `if defined?(Rails::Railtie)` (railtie.rb) - Prevents error when file is loaded without Rails - Pattern used by github/secure_headers gem **2. Ignore from Zeitwerk** with `loader.ignore()` (ruby_llm.rb) - Prevents Zeitwerk from expecting a constant from this file - Per Zeitwerk docs: "files not following conventions" should be ignored ## Evidence **Zeitwerk maintainer guidance** (issue #143): > "have the strategy defined in `lib`, perform a `require` in the initializer" **Zeitwerk docs**: > Use `loader.ignore()` for "files not following conventions" **Real-world precedent** (github/secure_headers): ```ruby if defined?(Rails::Railtie) module SecureHeaders class Railtie < Rails::Railtie # ... end end end ``` ## Testing Before (ruby_llm 1.8.2): ```bash $ bundle exec ruby -e "require 'swarm_sdk'; Zeitwerk::Loader.eager_load_all" NameError: uninitialized constant RubyLLM::Rails ``` After (with both fixes): ```bash $ bundle exec ruby -e "require 'swarm_sdk'; Zeitwerk::Loader.eager_load_all" ✓ SUCCESS ``` ## Related - PR #59 - Partial fix (conditional require only) - Zeitwerk issue #143 - Guidance on conditional loading - github/secure_headers - Real-world example of pattern --- lib/ruby_llm.rb | 1 + lib/ruby_llm/railtie.rb | 46 +++++++++++++++++++++-------------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/lib/ruby_llm.rb b/lib/ruby_llm.rb index dfe1c5cd3..3e5c17a3c 100644 --- a/lib/ruby_llm.rb +++ b/lib/ruby_llm.rb @@ -29,6 +29,7 @@ ) loader.ignore("#{__dir__}/tasks") loader.ignore("#{__dir__}/generators") +loader.ignore("#{__dir__}/ruby_llm/railtie.rb") loader.setup # A delightful Ruby interface to modern AI language models. diff --git a/lib/ruby_llm/railtie.rb b/lib/ruby_llm/railtie.rb index 14db08bf4..97a368b06 100644 --- a/lib/ruby_llm/railtie.rb +++ b/lib/ruby_llm/railtie.rb @@ -1,33 +1,35 @@ # frozen_string_literal: true -module RubyLLM - # Rails integration for RubyLLM - class Railtie < Rails::Railtie - initializer 'ruby_llm.inflections' do - ActiveSupport::Inflector.inflections(:en) do |inflect| - inflect.acronym 'RubyLLM' +if defined?(Rails::Railtie) + module RubyLLM + # Rails integration for RubyLLM + class Railtie < Rails::Railtie + initializer 'ruby_llm.inflections' do + ActiveSupport::Inflector.inflections(:en) do |inflect| + inflect.acronym 'RubyLLM' + end end - end - initializer 'ruby_llm.active_record' do - ActiveSupport.on_load :active_record do - if RubyLLM.config.use_new_acts_as - require 'ruby_llm/active_record/acts_as' - ::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs - else - require 'ruby_llm/active_record/acts_as_legacy' - ::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAsLegacy + initializer 'ruby_llm.active_record' do + ActiveSupport.on_load :active_record do + if RubyLLM.config.use_new_acts_as + require 'ruby_llm/active_record/acts_as' + ::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs + else + require 'ruby_llm/active_record/acts_as_legacy' + ::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAsLegacy - Rails.logger.warn( - "\n!!! RubyLLM's legacy acts_as API is deprecated and will be removed in RubyLLM 2.0.0. " \ - "Please consult the migration guide at https://rubyllm.com/upgrading-to-1-7/\n" - ) + Rails.logger.warn( + "\n!!! RubyLLM's legacy acts_as API is deprecated and will be removed in RubyLLM 2.0.0. " \ + "Please consult the migration guide at https://rubyllm.com/upgrading-to-1-7/\n" + ) + end end end - end - rake_tasks do - load 'tasks/ruby_llm.rake' + rake_tasks do + load 'tasks/ruby_llm.rake' + end end end end