lex-llm 0.1.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 (135) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/dependabot.yml +18 -0
  4. data/.github/workflows/ci.yml +16 -0
  5. data/.gitignore +19 -0
  6. data/.rubocop.yml +42 -0
  7. data/CHANGELOG.md +15 -0
  8. data/Gemfile +50 -0
  9. data/LICENSE +21 -0
  10. data/README.md +279 -0
  11. data/lex-llm.gemspec +43 -0
  12. data/lib/generators/lex_llm/agent/agent_generator.rb +36 -0
  13. data/lib/generators/lex_llm/agent/templates/agent.rb.tt +6 -0
  14. data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
  15. data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +256 -0
  16. data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +38 -0
  17. data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +21 -0
  18. data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  19. data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  20. data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  21. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  22. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  23. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  24. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  25. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  26. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  27. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  28. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  29. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  30. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  31. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  32. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  33. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  34. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  35. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  36. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  37. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  38. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  39. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  40. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  41. data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +28 -0
  42. data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  43. data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +25 -0
  44. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  45. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
  46. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  47. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  48. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  49. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  50. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -0
  51. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  52. data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +7 -0
  53. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  54. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  55. data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +15 -0
  56. data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +38 -0
  57. data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +17 -0
  58. data/lib/generators/lex_llm/generator_helpers.rb +214 -0
  59. data/lib/generators/lex_llm/install/install_generator.rb +109 -0
  60. data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
  61. data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +3 -0
  62. data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +7 -0
  63. data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +19 -0
  64. data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +39 -0
  65. data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +21 -0
  66. data/lib/generators/lex_llm/install/templates/initializer.rb.tt +20 -0
  67. data/lib/generators/lex_llm/install/templates/message_model.rb.tt +4 -0
  68. data/lib/generators/lex_llm/install/templates/model_model.rb.tt +3 -0
  69. data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +3 -0
  70. data/lib/generators/lex_llm/schema/schema_generator.rb +26 -0
  71. data/lib/generators/lex_llm/schema/templates/schema.rb.tt +2 -0
  72. data/lib/generators/lex_llm/tool/templates/tool.rb.tt +9 -0
  73. data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +13 -0
  74. data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +13 -0
  75. data/lib/generators/lex_llm/tool/tool_generator.rb +96 -0
  76. data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
  77. data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
  78. data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  79. data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  80. data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
  81. data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +122 -0
  82. data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  83. data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  84. data/lib/legion/extensions/llm/provider_settings.rb +49 -0
  85. data/lib/legion/extensions/llm/transport/fleet_lane.rb +70 -0
  86. data/lib/legion/extensions/llm.rb +50 -0
  87. data/lib/lex_llm/active_record/acts_as.rb +180 -0
  88. data/lib/lex_llm/active_record/acts_as_legacy.rb +503 -0
  89. data/lib/lex_llm/active_record/chat_methods.rb +468 -0
  90. data/lib/lex_llm/active_record/message_methods.rb +131 -0
  91. data/lib/lex_llm/active_record/model_methods.rb +76 -0
  92. data/lib/lex_llm/active_record/payload_helpers.rb +26 -0
  93. data/lib/lex_llm/active_record/tool_call_methods.rb +15 -0
  94. data/lib/lex_llm/agent.rb +365 -0
  95. data/lib/lex_llm/aliases.json +436 -0
  96. data/lib/lex_llm/aliases.rb +38 -0
  97. data/lib/lex_llm/attachment.rb +223 -0
  98. data/lib/lex_llm/chat.rb +351 -0
  99. data/lib/lex_llm/chunk.rb +6 -0
  100. data/lib/lex_llm/configuration.rb +81 -0
  101. data/lib/lex_llm/connection.rb +130 -0
  102. data/lib/lex_llm/content.rb +77 -0
  103. data/lib/lex_llm/context.rb +29 -0
  104. data/lib/lex_llm/embedding.rb +29 -0
  105. data/lib/lex_llm/error.rb +112 -0
  106. data/lib/lex_llm/image.rb +105 -0
  107. data/lib/lex_llm/message.rb +107 -0
  108. data/lib/lex_llm/mime_type.rb +71 -0
  109. data/lib/lex_llm/model/info.rb +113 -0
  110. data/lib/lex_llm/model/modalities.rb +22 -0
  111. data/lib/lex_llm/model/pricing.rb +48 -0
  112. data/lib/lex_llm/model/pricing_category.rb +46 -0
  113. data/lib/lex_llm/model/pricing_tier.rb +33 -0
  114. data/lib/lex_llm/model.rb +7 -0
  115. data/lib/lex_llm/models.json +57241 -0
  116. data/lib/lex_llm/models.rb +506 -0
  117. data/lib/lex_llm/models_schema.json +168 -0
  118. data/lib/lex_llm/moderation.rb +56 -0
  119. data/lib/lex_llm/provider.rb +278 -0
  120. data/lib/lex_llm/railtie.rb +35 -0
  121. data/lib/lex_llm/routing/lane_key.rb +51 -0
  122. data/lib/lex_llm/routing/model_offering.rb +169 -0
  123. data/lib/lex_llm/routing.rb +7 -0
  124. data/lib/lex_llm/stream_accumulator.rb +203 -0
  125. data/lib/lex_llm/streaming.rb +175 -0
  126. data/lib/lex_llm/thinking.rb +49 -0
  127. data/lib/lex_llm/tokens.rb +47 -0
  128. data/lib/lex_llm/tool.rb +254 -0
  129. data/lib/lex_llm/tool_call.rb +25 -0
  130. data/lib/lex_llm/transcription.rb +35 -0
  131. data/lib/lex_llm/utils.rb +91 -0
  132. data/lib/lex_llm/version.rb +5 -0
  133. data/lib/lex_llm.rb +95 -0
  134. data/lib/tasks/lex_llm.rake +23 -0
  135. metadata +349 -0
@@ -0,0 +1,145 @@
1
+ class MigrateToLexLlmModelReferences < 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
+ LexLLM.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 = LexLLM.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 = LexLLM.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
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require_relative '../generator_helpers'
6
+
7
+ module LexLLM
8
+ module Generators
9
+ # Generator to upgrade existing LexLLM apps to v1.7 with new Rails-like API
10
+ class UpgradeToV17Generator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+ include LexLLM::Generators::GeneratorHelpers
13
+
14
+ namespace 'lex_llm:upgrade_to_v1_7'
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ # Override source_paths to include install templates
18
+ def self.source_paths
19
+ [
20
+ File.expand_path('templates', __dir__),
21
+ File.expand_path('../install/templates', __dir__)
22
+ ]
23
+ end
24
+
25
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
26
+
27
+ desc 'Upgrades existing LexLLM apps to v1.7 with new Rails-like API\n' \
28
+ 'Usage: bin/rails g lex_llm:upgrade_to_v1_7 [chat:ChatName] [message:MessageName] ...'
29
+
30
+ def self.next_migration_number(dirname)
31
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
32
+ end
33
+
34
+ def create_migration_file
35
+ @model_table_already_existed = table_exists?(table_name_for(model_model_name))
36
+
37
+ # First check if models table exists, if not create it
38
+ unless @model_table_already_existed
39
+ migration_template 'create_models_migration.rb.tt',
40
+ "db/migrate/create_#{table_name_for(model_model_name)}.rb",
41
+ migration_version: migration_version,
42
+ model_model_name: model_model_name
43
+ end
44
+
45
+ migration_template 'migration.rb.tt',
46
+ 'db/migrate/migrate_to_lex_llm_model_references.rb',
47
+ migration_version: migration_version,
48
+ chat_model_name: chat_model_name,
49
+ message_model_name: message_model_name,
50
+ tool_call_model_name: tool_call_model_name,
51
+ model_model_name: model_model_name,
52
+ model_table_already_existed: @model_table_already_existed
53
+ end
54
+
55
+ def create_model_file
56
+ create_namespace_modules
57
+
58
+ template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
59
+ end
60
+
61
+ def update_existing_models
62
+ update_model_acts_as(chat_model_name, 'acts_as_chat', acts_as_chat_declaration)
63
+ update_model_acts_as(message_model_name, 'acts_as_message', acts_as_message_declaration)
64
+ update_model_acts_as(tool_call_model_name, 'acts_as_tool_call', acts_as_tool_call_declaration)
65
+ end
66
+
67
+ def update_initializer
68
+ initializer_path = 'config/initializers/lex_llm.rb'
69
+
70
+ unless File.exist?(initializer_path)
71
+ say_status :warning, 'No initializer found. Creating one...', :yellow
72
+ template 'initializer.rb.tt', initializer_path
73
+ return
74
+ end
75
+
76
+ initializer_content = File.read(initializer_path)
77
+
78
+ return if initializer_content.include?('config.use_new_acts_as')
79
+
80
+ inject_into_file initializer_path, after: /^LexLLM\.configure do \|config\|\n/ do
81
+ lines = [' # Enable the new Rails-like API', ' config.use_new_acts_as = true']
82
+ lines << " config.model_registry_class = \"#{model_model_name}\"" if model_model_name != 'Model'
83
+ lines << ''
84
+ lines.join("\n")
85
+ end
86
+ end
87
+
88
+ def show_next_steps
89
+ say_status :success, 'Upgrade prepared!', :green
90
+ say <<~INSTRUCTIONS
91
+
92
+ Next steps:
93
+ 1. Review the generated migrations
94
+ 2. Run: bin/rails db:migrate
95
+ 3. Update your code to use the new API: #{chat_model_name}.create! now has the same signature as LexLLM.chat
96
+
97
+ ⚠️ If you get "undefined method 'acts_as_model'" during migration:
98
+ Add this to config/application.rb BEFORE your Application class:
99
+
100
+ LexLLM.configure do |config|
101
+ config.use_new_acts_as = true
102
+ end
103
+
104
+ 📚 See the full migration guide: https://github.com/LegionIO/lex-llm
105
+
106
+ INSTRUCTIONS
107
+ end
108
+
109
+ private
110
+
111
+ def update_model_acts_as(model_name, old_acts_as, new_acts_as)
112
+ model_path = "app/models/#{model_name.underscore}.rb"
113
+ return unless File.exist?(Rails.root.join(model_path))
114
+
115
+ content = File.read(Rails.root.join(model_path))
116
+ return unless content.match?(/^\s*#{old_acts_as}/)
117
+
118
+ gsub_file model_path, /^\s*#{old_acts_as}.*$/, " #{new_acts_as}"
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,15 @@
1
+ class AddLexLlmV19Columns < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ unless column_exists?(:<%= message_table_name %>, :cached_tokens)
4
+ add_column :<%= message_table_name %>, :cached_tokens, :integer
5
+ end
6
+
7
+ unless column_exists?(:<%= message_table_name %>, :cache_creation_tokens)
8
+ add_column :<%= message_table_name %>, :cache_creation_tokens, :integer
9
+ end
10
+
11
+ unless column_exists?(:<%= message_table_name %>, :content_raw)
12
+ add_column :<%= message_table_name %>, :content_raw, :json
13
+ end
14
+ end
15
+ 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 LexLLM
8
+ module Generators
9
+ # Generator to add v1.9 columns (cached tokens + raw content support) to existing apps.
10
+ class UpgradeToV19Generator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+ include LexLLM::Generators::GeneratorHelpers
13
+
14
+ namespace 'lex_llm:upgrade_to_v1_9'
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ argument :model_mappings, type: :array, default: [], banner: 'message:MessageName'
18
+
19
+ desc 'Adds cached token columns and raw content storage fields introduced in v1.9.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_9_message_columns.rb.tt',
29
+ 'db/migrate/add_lex_llm_v1_9_columns.rb',
30
+ migration_version: migration_version,
31
+ message_table_name: message_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.9.0 release notes for details on cached token tracking and raw content support.
44
+
45
+ INSTRUCTIONS
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Llm
6
+ # Builds shared provider defaults for lex-llm-* extension gems.
7
+ module ProviderSettings
8
+ module_function
9
+
10
+ def build(family:, instance: {}, enabled: true, discovery: {}, instances: {})
11
+ deep_merge(
12
+ Legion::Extensions::Llm.default_settings,
13
+ {
14
+ enabled: enabled,
15
+ provider_family: family,
16
+ discovery: deep_merge({ enabled: true, interval_seconds: 300 }, discovery || {}),
17
+ instances: deep_merge(
18
+ {
19
+ default: deep_merge(
20
+ { enabled: true, credentials: nil, fleet: { enabled: false, consumer_priority: 0, prefetch: 1 } },
21
+ instance || {}
22
+ )
23
+ },
24
+ instances || {}
25
+ )
26
+ }
27
+ )
28
+ end
29
+
30
+ def deep_dup(value)
31
+ case value
32
+ when Hash
33
+ value.to_h { |key, inner_value| [key, deep_dup(inner_value)] }
34
+ when Array
35
+ value.map { |inner_value| deep_dup(inner_value) }
36
+ else
37
+ value
38
+ end
39
+ end
40
+
41
+ def deep_merge(left, right)
42
+ deep_dup(left || {}).merge(deep_dup(right || {})) do |_key, left_value, right_value|
43
+ left_value.is_a?(Hash) && right_value.is_a?(Hash) ? deep_merge(left_value, right_value) : right_value
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Llm
6
+ module Transport
7
+ # Shared RabbitMQ live-work lane defaults for provider fleet workers.
8
+ module FleetLane
9
+ DEFAULTS = {
10
+ queue_expires_ms: 60_000,
11
+ message_ttl_ms: 120_000,
12
+ queue_max_length: 100,
13
+ delivery_limit: 3,
14
+ consumer_ack_timeout_ms: 300_000
15
+ }.freeze
16
+
17
+ module_function
18
+
19
+ def queue_options(settings = {})
20
+ config = DEFAULTS.merge((settings || {}).compact.transform_keys(&:to_sym))
21
+ {
22
+ durable: true,
23
+ auto_delete: false,
24
+ arguments: queue_arguments(config)
25
+ }
26
+ end
27
+
28
+ def queue_arguments(config)
29
+ {
30
+ 'x-queue-type' => 'quorum',
31
+ 'x-queue-leader-locator' => 'balanced',
32
+ 'x-expires' => config.fetch(:queue_expires_ms),
33
+ 'x-message-ttl' => config.fetch(:message_ttl_ms),
34
+ 'x-overflow' => 'reject-publish',
35
+ 'x-max-length' => config.fetch(:queue_max_length),
36
+ 'x-delivery-limit' => config.fetch(:delivery_limit),
37
+ 'x-consumer-timeout' => config.fetch(:consumer_ack_timeout_ms)
38
+ }
39
+ end
40
+
41
+ def build_queue_class(queue_name:, exchange_class:, routing_key: queue_name, base_queue_class: nil,
42
+ settings: {})
43
+ parent = base_queue_class || legion_queue_class
44
+ unless parent
45
+ raise ArgumentError,
46
+ 'base_queue_class is required when Legion::Transport::Queue is not loaded'
47
+ end
48
+
49
+ options = queue_options(settings)
50
+ Class.new(parent) do
51
+ define_method(:queue_name) { queue_name }
52
+ define_method(:queue_options) { options }
53
+ define_method(:dlx_enabled) { false }
54
+ define_method(:initialize) do
55
+ super()
56
+ bind(exchange_class.new, routing_key: routing_key)
57
+ end
58
+ end
59
+ end
60
+
61
+ def legion_queue_class
62
+ return nil unless defined?(::Legion::Transport::Queue)
63
+
64
+ ::Legion::Transport::Queue
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lex_llm'
4
+ require 'legion/extensions/llm/provider_settings'
5
+ require 'legion/extensions/llm/transport/fleet_lane'
6
+
7
+ module Legion
8
+ module Extensions
9
+ # Legion-native namespace for the shared LLM provider framework.
10
+ module Llm
11
+ VERSION = LexLLM::VERSION unless const_defined?(:VERSION, false)
12
+
13
+ # Provider-neutral value objects exposed under the Legion extension namespace.
14
+ module Types
15
+ ModelOffering = LexLLM::Routing::ModelOffering unless const_defined?(:ModelOffering, false)
16
+ end
17
+
18
+ # Shared routing helpers exposed under the Legion extension namespace.
19
+ module Routing
20
+ LaneKey = LexLLM::Routing::LaneKey unless const_defined?(:LaneKey, false)
21
+ end
22
+
23
+ def self.default_settings
24
+ {
25
+ fleet: {
26
+ enabled: false,
27
+ scheduler: :basic_get,
28
+ consumer_priority: 0,
29
+ queue_expires_ms: 60_000,
30
+ message_ttl_ms: 120_000,
31
+ queue_max_length: 100,
32
+ delivery_limit: 3,
33
+ consumer_ack_timeout_ms: 300_000,
34
+ endpoint: {
35
+ enabled: false,
36
+ empty_lane_backoff_ms: 250,
37
+ idle_backoff_ms: 1_000,
38
+ max_consecutive_pulls_per_lane: 0,
39
+ accept_when: []
40
+ }
41
+ }
42
+ }
43
+ end
44
+
45
+ def self.provider_settings(...)
46
+ ProviderSettings.build(...)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LexLLM
4
+ module ActiveRecord
5
+ # Adds chat and message persistence capabilities to ActiveRecord models.
6
+ module ActsAs
7
+ extend ActiveSupport::Concern
8
+
9
+ # When ActsAs is included, ensure models are loaded from database
10
+ def self.included(base)
11
+ super
12
+ # Monkey-patch Models to use database when ActsAs is active
13
+ LexLLM::Models.class_eval do
14
+ def self.load_models
15
+ database_models = read_from_database
16
+ return database_models if database_models.any?
17
+
18
+ LexLLM.logger.debug { 'Model registry is empty in database, falling back to JSON registry' }
19
+ read_from_json
20
+ rescue StandardError => e
21
+ LexLLM.logger.debug { "Failed to load models from database: #{e.message}, falling back to JSON" }
22
+ read_from_json
23
+ end
24
+
25
+ def self.read_from_database
26
+ model_class = LexLLM.config.model_registry_class
27
+ model_class = model_class.constantize if model_class.is_a?(String)
28
+ return [] unless model_class.table_exists?
29
+
30
+ model_class.all.map(&:to_llm)
31
+ end
32
+
33
+ def load_from_database!
34
+ @models = self.class.read_from_database
35
+ end
36
+ end
37
+ end
38
+
39
+ class_methods do # rubocop:disable Metrics/BlockLength
40
+ def acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
41
+ model: :model, model_class: nil, model_foreign_key: nil)
42
+ include LexLLM::ActiveRecord::ChatMethods
43
+
44
+ class_attribute :messages_association_name, :model_association_name, :message_class, :model_class
45
+
46
+ self.messages_association_name = messages
47
+ self.model_association_name = model
48
+ self.message_class = (message_class || messages.to_s.classify).to_s
49
+ self.model_class = (model_class || model.to_s.classify).to_s
50
+
51
+ has_many messages,
52
+ -> { order(created_at: :asc) },
53
+ class_name: self.message_class,
54
+ foreign_key: messages_foreign_key,
55
+ dependent: :destroy
56
+
57
+ belongs_to model,
58
+ class_name: self.model_class,
59
+ foreign_key: model_foreign_key,
60
+ optional: true
61
+
62
+ define_method :messages_association do
63
+ send(messages_association_name)
64
+ end
65
+
66
+ define_method :model_association do
67
+ send(model_association_name)
68
+ end
69
+
70
+ define_method :'model_association=' do |value|
71
+ send("#{model_association_name}=", value)
72
+ end
73
+ end
74
+
75
+ def acts_as_model(chats: :chats, chat_class: nil, chats_foreign_key: nil)
76
+ include LexLLM::ActiveRecord::ModelMethods
77
+
78
+ class_attribute :chats_association_name, :chat_class
79
+
80
+ self.chats_association_name = chats
81
+ self.chat_class = (chat_class || chats.to_s.classify).to_s
82
+
83
+ validates :model_id, presence: true, uniqueness: { scope: :provider }
84
+ validates :provider, presence: true
85
+ validates :name, presence: true
86
+
87
+ has_many chats, class_name: self.chat_class, foreign_key: chats_foreign_key
88
+
89
+ define_method :chats_association do
90
+ send(chats_association_name)
91
+ end
92
+ end
93
+
94
+ def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false, # rubocop:disable Metrics/ParameterLists
95
+ tool_calls: :tool_calls, tool_call_class: nil, tool_calls_foreign_key: nil,
96
+ model: :model, model_class: nil, model_foreign_key: nil)
97
+ include LexLLM::ActiveRecord::MessageMethods
98
+
99
+ class_attribute :chat_association_name, :tool_calls_association_name, :model_association_name,
100
+ :chat_class, :tool_call_class, :model_class
101
+
102
+ self.chat_association_name = chat
103
+ self.tool_calls_association_name = tool_calls
104
+ self.model_association_name = model
105
+ self.chat_class = (chat_class || chat.to_s.classify).to_s
106
+ self.tool_call_class = (tool_call_class || tool_calls.to_s.classify).to_s
107
+ self.model_class = (model_class || model.to_s.classify).to_s
108
+
109
+ belongs_to chat,
110
+ class_name: self.chat_class,
111
+ foreign_key: chat_foreign_key,
112
+ touch: touch_chat
113
+
114
+ has_many tool_calls,
115
+ class_name: self.tool_call_class,
116
+ foreign_key: tool_calls_foreign_key,
117
+ dependent: :destroy
118
+
119
+ belongs_to :parent_tool_call,
120
+ class_name: self.tool_call_class,
121
+ foreign_key: ActiveSupport::Inflector.foreign_key(tool_calls.to_s.singularize),
122
+ optional: true
123
+
124
+ has_many :tool_results,
125
+ through: tool_calls,
126
+ source: :result,
127
+ class_name: name
128
+
129
+ belongs_to model,
130
+ class_name: self.model_class,
131
+ foreign_key: model_foreign_key,
132
+ optional: true
133
+
134
+ delegate :tool_call?, :tool_result?, to: :to_llm
135
+
136
+ define_method :chat_association do
137
+ send(chat_association_name)
138
+ end
139
+
140
+ define_method :tool_calls_association do
141
+ send(tool_calls_association_name)
142
+ end
143
+
144
+ define_method :model_association do
145
+ send(model_association_name)
146
+ end
147
+ end
148
+
149
+ def acts_as_tool_call(message: :message, message_class: nil, message_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
150
+ result: :result, result_class: nil, result_foreign_key: nil)
151
+ include LexLLM::ActiveRecord::ToolCallMethods
152
+
153
+ class_attribute :message_association_name, :result_association_name, :message_class, :result_class
154
+
155
+ self.message_association_name = message
156
+ self.result_association_name = result
157
+ self.message_class = (message_class || message.to_s.classify).to_s
158
+ self.result_class = (result_class || self.message_class).to_s
159
+
160
+ belongs_to message,
161
+ class_name: self.message_class,
162
+ foreign_key: message_foreign_key
163
+
164
+ has_one result,
165
+ class_name: self.result_class,
166
+ foreign_key: result_foreign_key,
167
+ dependent: :nullify
168
+
169
+ define_method :message_association do
170
+ send(message_association_name)
171
+ end
172
+
173
+ define_method :result_association do
174
+ send(result_association_name)
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end