dify_llm 1.6.4

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 (129) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +157 -0
  4. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  5. data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  7. data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +16 -0
  8. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  9. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
  10. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
  12. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  13. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  14. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  15. data/lib/generators/ruby_llm/install_generator.rb +184 -0
  16. data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
  17. data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
  18. data/lib/ruby_llm/active_record/acts_as.rb +137 -0
  19. data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
  20. data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
  21. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  22. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  23. data/lib/ruby_llm/aliases.json +274 -0
  24. data/lib/ruby_llm/aliases.rb +38 -0
  25. data/lib/ruby_llm/attachment.rb +191 -0
  26. data/lib/ruby_llm/chat.rb +212 -0
  27. data/lib/ruby_llm/chunk.rb +6 -0
  28. data/lib/ruby_llm/configuration.rb +69 -0
  29. data/lib/ruby_llm/connection.rb +137 -0
  30. data/lib/ruby_llm/content.rb +50 -0
  31. data/lib/ruby_llm/context.rb +29 -0
  32. data/lib/ruby_llm/embedding.rb +29 -0
  33. data/lib/ruby_llm/error.rb +76 -0
  34. data/lib/ruby_llm/image.rb +49 -0
  35. data/lib/ruby_llm/message.rb +76 -0
  36. data/lib/ruby_llm/mime_type.rb +67 -0
  37. data/lib/ruby_llm/model/info.rb +103 -0
  38. data/lib/ruby_llm/model/modalities.rb +22 -0
  39. data/lib/ruby_llm/model/pricing.rb +48 -0
  40. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  41. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  42. data/lib/ruby_llm/model.rb +7 -0
  43. data/lib/ruby_llm/models.json +31418 -0
  44. data/lib/ruby_llm/models.rb +235 -0
  45. data/lib/ruby_llm/models_schema.json +168 -0
  46. data/lib/ruby_llm/provider.rb +215 -0
  47. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  48. data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
  49. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  50. data/lib/ruby_llm/providers/anthropic/media.rb +91 -0
  51. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  52. data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
  53. data/lib/ruby_llm/providers/anthropic/tools.rb +107 -0
  54. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  55. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  56. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  57. data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
  58. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  59. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  60. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  61. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
  62. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  63. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
  64. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  65. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  66. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  67. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  68. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  69. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  70. data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
  71. data/lib/ruby_llm/providers/dify/chat.rb +59 -0
  72. data/lib/ruby_llm/providers/dify/media.rb +37 -0
  73. data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
  74. data/lib/ruby_llm/providers/dify.rb +48 -0
  75. data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
  76. data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
  77. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  78. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  79. data/lib/ruby_llm/providers/gemini/media.rb +54 -0
  80. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  81. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  82. data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
  83. data/lib/ruby_llm/providers/gemini.rb +36 -0
  84. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  85. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  86. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  87. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  88. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  89. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  90. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  91. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  92. data/lib/ruby_llm/providers/mistral.rb +32 -0
  93. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  94. data/lib/ruby_llm/providers/ollama/media.rb +45 -0
  95. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  96. data/lib/ruby_llm/providers/ollama.rb +30 -0
  97. data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
  98. data/lib/ruby_llm/providers/openai/chat.rb +83 -0
  99. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  100. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  101. data/lib/ruby_llm/providers/openai/media.rb +80 -0
  102. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  103. data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
  104. data/lib/ruby_llm/providers/openai/tools.rb +78 -0
  105. data/lib/ruby_llm/providers/openai.rb +42 -0
  106. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  107. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  108. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  109. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  110. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  111. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  112. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  113. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  114. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  115. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  116. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  117. data/lib/ruby_llm/railtie.rb +41 -0
  118. data/lib/ruby_llm/stream_accumulator.rb +97 -0
  119. data/lib/ruby_llm/streaming.rb +153 -0
  120. data/lib/ruby_llm/tool.rb +83 -0
  121. data/lib/ruby_llm/tool_call.rb +22 -0
  122. data/lib/ruby_llm/utils.rb +45 -0
  123. data/lib/ruby_llm/version.rb +5 -0
  124. data/lib/ruby_llm.rb +97 -0
  125. data/lib/tasks/models.rake +525 -0
  126. data/lib/tasks/release.rake +67 -0
  127. data/lib/tasks/ruby_llm.rake +15 -0
  128. data/lib/tasks/vcr.rake +92 -0
  129. metadata +291 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cf640d22ec6e4390de10874e92f3d5386df4a7fb16249fdc3b9e3c685aef24d8
4
+ data.tar.gz: d3a832819f2300fdbe92fc8123344c862244af1e066a0d3ed4ab99c8e37146be
5
+ SHA512:
6
+ metadata.gz: 051e1512fb39e9c775fda04aef83e4749cf1eb00140e96d5c7d8e40944300532162971bc75da9c1b93084682fe77fbb81bc631c8a5a8369cee8392cf488fc06f
7
+ data.tar.gz: 0e63f109548b3a41ec18a64b00c243a27eb793a85f303a9d35fa9516c98d8c029654e7c7b4d9f34bf6e248424435d39f20954c51b26faee48caf033a1e627a96
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Carmine Paolino
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ <div align="center">
2
+
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="/docs/assets/images/logotype_dark.svg">
5
+ <img src="/docs/assets/images/logotype.svg" alt="RubyLLM" height="120" width="250">
6
+ </picture>
7
+
8
+ <strong>One *beautiful* Ruby API for GPT, Claude, Gemini, and more.</strong>
9
+
10
+ Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="https://chatwithwork.com/logotype-dark.svg"><img src="https://chatwithwork.com/logotype.svg" alt="Chat with Work" height="30" align="absmiddle"></picture>](https://chatwithwork.com) — *Claude Code for your documents*
11
+
12
+ [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=7)](https://badge.fury.io/rb/ruby_llm)
13
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
14
+ [![Gem Downloads](https://img.shields.io/gem/dt/ruby_llm)](https://rubygems.org/gems/ruby_llm)
15
+ [![codecov](https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg?a=2)](https://codecov.io/gh/crmne/ruby_llm)
16
+
17
+ <a href="https://trendshift.io/repositories/13640" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13640" alt="crmne%2Fruby_llm | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
18
+ </div>
19
+
20
+ > [!NOTE]
21
+ > Using RubyLLM in production? [Share your story](https://tally.so/r/3Na02p)! Takes 5 minutes.
22
+
23
+ ---
24
+
25
+ Build chatbots, AI agents, RAG applications. Works with OpenAI, Anthropic, Google, AWS, local models, and any OpenAI-compatible API.
26
+
27
+ ## Why RubyLLM?
28
+
29
+ Every AI provider ships their own bloated client. Different APIs. Different response formats. Different conventions. It's exhausting.
30
+
31
+ RubyLLM gives you one beautiful API for all of them. Same interface whether you're using GPT, Claude, or your local Ollama. Just three dependencies: Faraday, Zeitwerk, and Marcel. That's it.
32
+
33
+ ## Show me the code
34
+
35
+ ```ruby
36
+ # Just ask questions
37
+ chat = RubyLLM.chat
38
+ chat.ask "What's the best way to learn Ruby?"
39
+ ```
40
+
41
+ ```ruby
42
+ # Analyze any file type
43
+ chat.ask "What's in this image?", with: "ruby_conf.jpg"
44
+ chat.ask "Describe this meeting", with: "meeting.wav"
45
+ chat.ask "Summarize this document", with: "contract.pdf"
46
+ chat.ask "Explain this code", with: "app.rb"
47
+ ```
48
+
49
+ ```ruby
50
+ # Multiple files at once
51
+ chat.ask "Analyze these files", with: ["diagram.png", "report.pdf", "notes.txt"]
52
+ ```
53
+
54
+ ```ruby
55
+ # Stream responses
56
+ chat.ask "Tell me a story about Ruby" do |chunk|
57
+ print chunk.content
58
+ end
59
+ ```
60
+
61
+ ```ruby
62
+ # Generate images
63
+ RubyLLM.paint "a sunset over mountains in watercolor style"
64
+ ```
65
+
66
+ ```ruby
67
+ # Create embeddings
68
+ RubyLLM.embed "Ruby is elegant and expressive"
69
+ ```
70
+
71
+ ```ruby
72
+ # Let AI use your code
73
+ class Weather < RubyLLM::Tool
74
+ description "Get current weather"
75
+ param :latitude
76
+ param :longitude
77
+
78
+ def execute(latitude:, longitude:)
79
+ url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
80
+ JSON.parse(Faraday.get(url).body)
81
+ end
82
+ end
83
+
84
+ chat.with_tool(Weather).ask "What's the weather in Berlin?"
85
+ ```
86
+
87
+ ```ruby
88
+ # Get structured output
89
+ class ProductSchema < RubyLLM::Schema
90
+ string :name
91
+ number :price
92
+ array :features do
93
+ string
94
+ end
95
+ end
96
+
97
+ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "product.txt"
98
+ ```
99
+
100
+ ## Features
101
+
102
+ * **Chat:** Conversational AI with `RubyLLM.chat`
103
+ * **Vision:** Analyze images and screenshots
104
+ * **Audio:** Transcribe and understand speech
105
+ * **Documents:** Extract from PDFs, CSVs, JSON, any file type
106
+ * **Image generation:** Create images with `RubyLLM.paint`
107
+ * **Embeddings:** Vector search with `RubyLLM.embed`
108
+ * **Tools:** Let AI call your Ruby methods
109
+ * **Structured output:** JSON schemas that just work
110
+ * **Streaming:** Real-time responses with blocks
111
+ * **Rails:** ActiveRecord integration with `acts_as_chat`
112
+ * **Async:** Fiber-based concurrency
113
+ * **Model registry:** 500+ models with capability detection and pricing
114
+ * **Providers:** OpenAI, Anthropic, Gemini, VertexAI, Bedrock, DeepSeek, Mistral, Ollama, OpenRouter, Perplexity, GPUStack, and any OpenAI-compatible API
115
+
116
+ ## Installation
117
+
118
+ Add to your Gemfile:
119
+ ```ruby
120
+ gem 'ruby_llm'
121
+ ```
122
+ Then `bundle install`.
123
+
124
+ Configure your API keys:
125
+ ```ruby
126
+ # config/initializers/ruby_llm.rb
127
+ RubyLLM.configure do |config|
128
+ config.openai_api_key = ENV['OPENAI_API_KEY']
129
+ end
130
+ ```
131
+
132
+ ## Rails
133
+
134
+ ```bash
135
+ rails generate ruby_llm:install
136
+ ```
137
+
138
+ ```ruby
139
+ class Chat < ApplicationRecord
140
+ acts_as_chat
141
+ end
142
+
143
+ chat = Chat.create! model_id: "claude-sonnet-4"
144
+ chat.ask "What's in this file?", with: "report.pdf"
145
+ ```
146
+
147
+ ## Documentation
148
+
149
+ [rubyllm.com](https://rubyllm.com)
150
+
151
+ ## Contributing
152
+
153
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
154
+
155
+ ## License
156
+
157
+ Released under the MIT License.
@@ -0,0 +1,3 @@
1
+ class <%= options[:chat_model_name] %> < ApplicationRecord
2
+ <%= acts_as_chat_declaration %>
3
+ end
@@ -0,0 +1,8 @@
1
+ class Create<%= options[:chat_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= options[:chat_model_name].tableize %> do |t|
4
+ t.string :model_id
5
+ t.timestamps
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class Create<%= options[:chat_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= options[:chat_model_name].tableize %> do |t|
4
+ t.references :<%= options[:model_model_name].tableize.singularize %>, foreign_key: true
5
+ t.timestamps
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ class Create<%= options[:message_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= options[:message_model_name].tableize %> do |t|
4
+ t.references :<%= options[:chat_model_name].tableize.singularize %>, null: false, foreign_key: true
5
+ t.string :role, null: false
6
+ t.text :content
7
+ t.string :model_id
8
+ t.integer :input_tokens
9
+ t.integer :output_tokens
10
+ t.references :<%= options[:tool_call_model_name].tableize.singularize %>, foreign_key: true
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :<%= options[:message_model_name].tableize %>, :role
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ class Create<%= options[:message_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= options[:message_model_name].tableize %> do |t|
4
+ t.references :<%= options[:chat_model_name].tableize.singularize %>, null: false, foreign_key: true
5
+ t.string :role, null: false
6
+ t.text :content
7
+ t.references :<%= options[:model_model_name].tableize.singularize %>, foreign_key: true
8
+ t.integer :input_tokens
9
+ t.integer :output_tokens
10
+ t.references :<%= options[:tool_call_model_name].tableize.singularize %>, foreign_key: true
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :<%= options[:message_model_name].tableize %>, :role
15
+ end
16
+ end
@@ -0,0 +1,43 @@
1
+ class Create<%= options[:model_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= options[:model_model_name].tableize %> do |t|
4
+ t.string :model_id, null: false
5
+ t.string :name, null: false
6
+ t.string :provider, null: false
7
+ t.string :family
8
+ t.datetime :model_created_at
9
+ t.integer :context_window
10
+ t.integer :max_output_tokens
11
+ t.date :knowledge_cutoff
12
+ <% if postgresql? %>
13
+ t.jsonb :modalities, default: {}
14
+ t.jsonb :capabilities, default: []
15
+ t.jsonb :pricing, default: {}
16
+ t.jsonb :metadata, default: {}
17
+ <% else %>
18
+ t.json :modalities, default: {}
19
+ t.json :capabilities, default: []
20
+ t.json :pricing, default: {}
21
+ t.json :metadata, default: {}
22
+ <% end %>
23
+ t.timestamps
24
+
25
+ t.index [:provider, :model_id], unique: true
26
+ t.index :provider
27
+ t.index :family
28
+ <% if postgresql? %>
29
+ t.index :capabilities, using: :gin
30
+ t.index :modalities, using: :gin
31
+ <% end %>
32
+ end
33
+
34
+ # Load models from JSON
35
+ say_with_time "Loading models from models.json" do
36
+ RubyLLM.models.load_from_json!
37
+ model_class = '<%= options[:model_model_name] %>'.constantize
38
+ model_class.save_to_database
39
+
40
+ "Loaded #{model_class.count} models"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ <%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
2
+ class Create<%= options[:tool_call_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
3
+ def change
4
+ create_table :<%= options[:tool_call_model_name].tableize %> do |t|
5
+ t.references :<%= options[:message_model_name].tableize.singularize %>, null: false, foreign_key: true
6
+ t.string :tool_call_id, null: false
7
+ t.string :name, null: false
8
+ t.<%= postgresql? ? 'jsonb' : 'json' %> :arguments, default: {}
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :<%= options[:tool_call_model_name].tableize %>, :tool_call_id, unique: true
13
+ add_index :<%= options[:tool_call_model_name].tableize %>, :name
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ RubyLLM.configure do |config|
2
+ config.openai_api_key = Rails.application.credentials.dig(:openai_api_key)
3
+ # config.default_model = "gpt-4.1-nano"
4
+
5
+ <% unless skip_model_registry? -%>
6
+ # DB-backed model registry for rich model metadata
7
+ config.model_registry_class = "<%= options[:model_model_name] %>"
8
+ <% end -%>
9
+ end
@@ -0,0 +1,4 @@
1
+ class <%= options[:message_model_name] %> < ApplicationRecord
2
+ <%= acts_as_message_declaration %><% unless options[:skip_active_storage] %>
3
+ has_many_attached :attachments<% end %>
4
+ end
@@ -0,0 +1,3 @@
1
+ class <%= options[:model_model_name] %> < ApplicationRecord
2
+ <%= acts_as_model_declaration %>
3
+ end
@@ -0,0 +1,3 @@
1
+ class <%= options[:tool_call_model_name] %> < ApplicationRecord
2
+ <%= acts_as_tool_call_declaration %>
3
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module RubyLLM
7
+ # Generator for RubyLLM Rails models and migrations
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ namespace 'ruby_llm:install'
12
+
13
+ source_root File.expand_path('install/templates', __dir__)
14
+
15
+ class_option :chat_model_name, type: :string, default: 'Chat',
16
+ desc: 'Name of the Chat model class'
17
+ class_option :message_model_name, type: :string, default: 'Message',
18
+ desc: 'Name of the Message model class'
19
+ class_option :tool_call_model_name, type: :string, default: 'ToolCall',
20
+ desc: 'Name of the ToolCall model class'
21
+ class_option :model_model_name, type: :string, default: 'Model',
22
+ desc: 'Name of the Model model class (for model registry)'
23
+ class_option :skip_model_registry, type: :boolean, default: false,
24
+ desc: 'Skip creating Model registry (uses string fields instead)'
25
+ class_option :skip_active_storage, type: :boolean, default: false,
26
+ desc: 'Skip ActiveStorage installation and attachment setup'
27
+
28
+ desc 'Creates models and migrations for RubyLLM Rails integration with ActiveStorage for file attachments'
29
+
30
+ def self.next_migration_number(dirname)
31
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
32
+ end
33
+
34
+ def migration_version
35
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
36
+ end
37
+
38
+ def postgresql?
39
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
40
+ rescue StandardError
41
+ false
42
+ end
43
+
44
+ def acts_as_chat_declaration
45
+ acts_as_chat_params = []
46
+ if options[:message_model_name] != 'Message'
47
+ acts_as_chat_params << "message_class: \"#{options[:message_model_name]}\""
48
+ end
49
+ if options[:tool_call_model_name] != 'ToolCall'
50
+ acts_as_chat_params << "tool_call_class: \"#{options[:tool_call_model_name]}\""
51
+ end
52
+ if acts_as_chat_params.any?
53
+ "acts_as_chat #{acts_as_chat_params.join(', ')}"
54
+ else
55
+ 'acts_as_chat'
56
+ end
57
+ end
58
+
59
+ def acts_as_message_declaration
60
+ acts_as_message_params = []
61
+ acts_as_message_params << "chat_class: \"#{options[:chat_model_name]}\"" if options[:chat_model_name] != 'Chat'
62
+ if options[:tool_call_model_name] != 'ToolCall'
63
+ acts_as_message_params << "tool_call_class: \"#{options[:tool_call_model_name]}\""
64
+ end
65
+ if acts_as_message_params.any?
66
+ "acts_as_message #{acts_as_message_params.join(', ')}"
67
+ else
68
+ 'acts_as_message'
69
+ end
70
+ end
71
+
72
+ def acts_as_tool_call_declaration
73
+ acts_as_tool_call_params = []
74
+ if options[:message_model_name] != 'Message'
75
+ acts_as_tool_call_params << "message_class: \"#{options[:message_model_name]}\""
76
+ end
77
+ if acts_as_tool_call_params.any?
78
+ "acts_as_tool_call #{acts_as_tool_call_params.join(', ')}"
79
+ else
80
+ 'acts_as_tool_call'
81
+ end
82
+ end
83
+
84
+ def acts_as_model_declaration
85
+ 'acts_as_model'
86
+ end
87
+
88
+ def skip_model_registry?
89
+ options[:skip_model_registry]
90
+ end
91
+
92
+ def create_migration_files
93
+ # Create migrations with timestamps to ensure proper order
94
+ # First create chats table
95
+ template_file = skip_model_registry? ? 'create_chats_legacy_migration.rb.tt' : 'create_chats_migration.rb.tt'
96
+ migration_template template_file,
97
+ "db/migrate/create_#{options[:chat_model_name].tableize}.rb"
98
+
99
+ # Then create messages table (must come before tool_calls due to foreign key)
100
+ sleep 1 # Ensure different timestamp
101
+ template_file = if skip_model_registry?
102
+ 'create_messages_legacy_migration.rb.tt'
103
+ else
104
+ 'create_messages_migration.rb.tt'
105
+ end
106
+ migration_template template_file,
107
+ "db/migrate/create_#{options[:message_model_name].tableize}.rb"
108
+
109
+ # Then create tool_calls table (references messages)
110
+ sleep 1 # Ensure different timestamp
111
+ migration_template 'create_tool_calls_migration.rb.tt',
112
+ "db/migrate/create_#{options[:tool_call_model_name].tableize}.rb"
113
+
114
+ # Create models table unless using legacy or skipping
115
+ return if skip_model_registry?
116
+
117
+ sleep 1 # Ensure different timestamp
118
+ migration_template 'create_models_migration.rb.tt',
119
+ "db/migrate/create_#{options[:model_model_name].tableize}.rb"
120
+ end
121
+
122
+ def create_model_files
123
+ template 'chat_model.rb.tt', "app/models/#{options[:chat_model_name].underscore}.rb"
124
+ template 'message_model.rb.tt', "app/models/#{options[:message_model_name].underscore}.rb"
125
+ template 'tool_call_model.rb.tt', "app/models/#{options[:tool_call_model_name].underscore}.rb"
126
+
127
+ # Only create Model class if not using legacy
128
+ return if skip_model_registry?
129
+
130
+ template 'model_model.rb.tt', "app/models/#{options[:model_model_name].underscore}.rb"
131
+ end
132
+
133
+ def create_initializer
134
+ template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
135
+ end
136
+
137
+ def install_active_storage
138
+ return if options[:skip_active_storage]
139
+
140
+ say ' Installing ActiveStorage for file attachments...', :cyan
141
+ rails_command 'active_storage:install'
142
+ end
143
+
144
+ def show_install_info
145
+ say "\n ✅ RubyLLM installed!", :green
146
+
147
+ say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
148
+
149
+ say "\n Next steps:", :yellow
150
+ say ' 1. Run: rails db:migrate'
151
+ say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
152
+
153
+ if skip_model_registry?
154
+ say " 3. Start chatting: #{options[:chat_model_name]}.create!(model_id: 'gpt-4.1-nano').ask('Hello!')"
155
+
156
+ say "\n Note: Using string-based model fields", :yellow
157
+ say ' For rich model metadata, consider adding the model registry:'
158
+ say ' rails generate ruby_llm:migrate_model_fields'
159
+ else
160
+ say " 3. Start chatting: #{options[:chat_model_name]}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
161
+
162
+ say "\n 🚀 Model registry is database-backed!", :cyan
163
+ say ' Models automatically load from the database'
164
+ say ' Pass model names as strings - RubyLLM handles the rest!'
165
+ end
166
+ say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
167
+
168
+ if options[:skip_active_storage]
169
+ say "\n 📎 Note: ActiveStorage was skipped", :yellow
170
+ say ' File attachments won\'t work without ActiveStorage.'
171
+ say ' To enable later:'
172
+ say ' 1. Run: rails active_storage:install && rails db:migrate'
173
+ say " 2. Add to your #{options[:message_model_name]} model: has_many_attached :attachments"
174
+ end
175
+
176
+ say "\n 📚 Documentation: https://rubyllm.com", :cyan
177
+
178
+ say "\n ❤️ Love RubyLLM?", :magenta
179
+ say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
180
+ say ' • 🐦 Follow for updates: https://x.com/paolino'
181
+ say "\n"
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,142 @@
1
+ class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ # Ensure Models table exists
4
+ unless table_exists?(:<%= options[:model_model_name].tableize %>)
5
+ raise "Models table doesn't exist. Please run 'rails generate ruby_llm:install' with model registry first."
6
+ end
7
+
8
+ model_class = <%= options[:model_model_name] %>
9
+ chat_class = <%= options[:chat_model_name] %>
10
+ message_class = <%= options[:message_model_name] %>
11
+
12
+ # Then check for any models in existing data that aren't in models.json
13
+ say_with_time "Checking for additional models in existing data" do
14
+ collect_and_create_models(chat_class, :<%= options[:chat_model_name].tableize %>, model_class)
15
+ collect_and_create_models(message_class, :<%= options[:message_model_name].tableize %>, model_class)
16
+ model_class.count
17
+ end
18
+
19
+ # Migrate foreign keys
20
+ migrate_foreign_key(:<%= options[:chat_model_name].tableize %>, chat_class, model_class, :<%= options[:model_model_name].underscore %>)
21
+ migrate_foreign_key(:<%= options[:message_model_name].tableize %>, message_class, model_class, :<%= options[:model_model_name].underscore %>)
22
+ end
23
+
24
+ def down
25
+ # Remove foreign key references
26
+ if column_exists?(:<%= options[:message_model_name].tableize %>, :<%= options[:model_model_name].underscore %>_id)
27
+ remove_reference :<%= options[:message_model_name].tableize %>, :<%= options[:model_model_name].underscore %>, foreign_key: true
28
+ end
29
+
30
+ if column_exists?(:<%= options[:chat_model_name].tableize %>, :<%= options[:model_model_name].underscore %>_id)
31
+ remove_reference :<%= options[:chat_model_name].tableize %>, :<%= options[:model_model_name].underscore %>, foreign_key: true
32
+ end
33
+
34
+ # Restore original model_id string columns
35
+ if column_exists?(:<%= options[:message_model_name].tableize %>, :model_id_string)
36
+ rename_column :<%= options[:message_model_name].tableize %>, :model_id_string, :model_id
37
+ end
38
+
39
+ if column_exists?(:<%= options[:chat_model_name].tableize %>, :model_id_string)
40
+ rename_column :<%= options[:chat_model_name].tableize %>, :model_id_string, :model_id
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def collect_and_create_models(record_class, table_name, model_class)
47
+ return unless column_exists?(table_name, :model_id)
48
+
49
+ has_provider = column_exists?(table_name, :provider)
50
+
51
+ # Collect unique model/provider combinations using read_attribute to bypass overrides
52
+ models_set = Set.new
53
+
54
+ record_class.find_each do |record|
55
+ model_id = record.read_attribute(:model_id)
56
+ next if model_id.blank?
57
+
58
+ provider = has_provider ? record.read_attribute(:provider) : nil
59
+ models_set.add([ model_id, provider ])
60
+ end
61
+
62
+ models_set.each do |model_id, provider|
63
+ find_or_create_model(model_id, provider, model_class)
64
+ end
65
+ end
66
+
67
+ def find_or_create_model(model_id, provider, model_class)
68
+ return if model_id.blank?
69
+
70
+ begin
71
+ model_info, _provider = RubyLLM.models.resolve(model_id, provider: provider)
72
+
73
+ model_class.find_or_create_by!(
74
+ model_id: model_info.id,
75
+ provider: model_info.provider
76
+ ) do |m|
77
+ m.name = model_info.name || model_info.id
78
+ m.family = model_info.family
79
+ m.model_created_at = model_info.created_at
80
+ m.context_window = model_info.context_window
81
+ m.max_output_tokens = model_info.max_output_tokens
82
+ m.knowledge_cutoff = model_info.knowledge_cutoff
83
+ m.modalities = model_info.modalities.to_h
84
+ m.capabilities = model_info.capabilities
85
+ m.pricing = model_info.pricing.to_h
86
+ m.metadata = model_info.metadata
87
+ end
88
+ rescue => e
89
+ # Skip models that can't be resolved - they'll need manual fixing
90
+ Rails.logger.warn "Skipping unresolvable model: #{model_id} - will need manual update"
91
+ nil
92
+ end
93
+ end
94
+
95
+
96
+ def migrate_foreign_key(table_name, record_class, model_class, foreign_key_name)
97
+ return unless column_exists?(table_name, :model_id)
98
+
99
+ # Check if we need to rename the string column to avoid collision
100
+ if column_exists?(table_name, :model_id) && !foreign_key_exists?(table_name, :models)
101
+ # Temporarily rename the string column
102
+ rename_column table_name, :model_id, :model_id_string
103
+ end
104
+
105
+ # Add the foreign key reference
106
+ unless column_exists?(table_name, "#{foreign_key_name}_id")
107
+ add_reference table_name, foreign_key_name, foreign_key: true
108
+ end
109
+
110
+ say_with_time "Migrating #{table_name} model references" do
111
+ record_class.reset_column_information
112
+ has_provider = column_exists?(table_name, :provider)
113
+
114
+ # Determine which column to read from (renamed or original)
115
+ model_id_column = column_exists?(table_name, :model_id_string) ? :model_id_string : :model_id
116
+
117
+ record_class.find_each do |record|
118
+ model_id = record.read_attribute(model_id_column)
119
+ next if model_id.blank?
120
+
121
+ provider = has_provider ? record.read_attribute(:provider) : nil
122
+
123
+ model = if has_provider && provider.present?
124
+ model_class.find_by(model_id: model_id, provider: provider)
125
+ else
126
+ find_model_for_record(model_id, model_class)
127
+ end
128
+
129
+ record.update_column("#{foreign_key_name}_id", model.id) if model
130
+ end
131
+ end
132
+ end
133
+
134
+ def find_model_for_record(model_id, model_class)
135
+ begin
136
+ model_info, _provider = RubyLLM.models.resolve(model_id)
137
+ model_class.find_by(model_id: model_info.id, provider: model_info.provider)
138
+ rescue => e
139
+ model_class.find_by(model_id: model_id)
140
+ end
141
+ end
142
+ end