Skip to content

Commit cf61b71

Browse files
committed
Add DB-backed model registry with ActiveRecord associations
Implements the new Model registry feature that replaces simple string fields with proper ActiveRecord associations, enabling rich model metadata, provider routing, and usage tracking. Key features: - Model associations with foreign keys for better data integrity - Rich model metadata (context_window, capabilities, pricing, etc.) - Provider-specific model routing - Automatic model population from models.json during migration - Backward compatibility through model_id= setter - Support for assume_model_exists with dynamic model creation - Context attribute for multi-tenant applications Migration improvements: - Fresh installs automatically get model registry - Existing apps can migrate with rails generate ruby_llm:migrate_model_fields - Legacy mode continues to work with deprecation warning - Models table auto-populated from models.json Rails integration updates: - Context must be passed at Chat.create! for DB-backed registry - Legacy mode retains with_context method - Added documentation for context persistence considerations Testing enhancements: - Added tests for assume_model_exists with fictional models - Database reload after test model creation - Proper error handling for non-existent models - Fixed generator specs for new migration structure
1 parent 9d5c277 commit cf61b71

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1399
-342
lines changed

docs/_advanced/rails.md

Lines changed: 103 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ After running the generator:
7777

7878
```bash
7979
rails db:migrate
80+
rails ruby_llm:load_models # Populates the models table from models.json
8081
```
8182

82-
You're ready to go! The generator handles all the setup complexity for you.
83+
You're ready to go! The generator handles all the setup complexity for you, including configuring the DB-backed model registry.
8384

8485
#### Generator Options
8586

@@ -89,8 +90,8 @@ The generator supports custom model names if needed:
8990
# Use custom model names
9091
rails generate ruby_llm:install --chat-model-name=Conversation --message-model-name=ChatMessage --tool-call-model-name=FunctionCall
9192

92-
# Skip the Model registry (opt-out)
93-
rails generate ruby_llm:install --skip-model
93+
# Skip the Model registry (uses string fields instead)
94+
rails generate ruby_llm:install --skip-model-registry
9495
```
9596

9697
This is useful if you already have models with these names or prefer different naming conventions.
@@ -236,7 +237,6 @@ class Chat < ApplicationRecord
236237

237238
# --- Add your standard Rails model logic below ---
238239
belongs_to :user, optional: true # Example
239-
validates :model_id, presence: true # Example
240240
end
241241

242242
# app/models/message.rb
@@ -270,28 +270,105 @@ class Model < ApplicationRecord
270270
end
271271
```
272272

273-
### Setup RubyLLM.chat yourself
273+
### Using Provider Overrides
274274

275-
In some scenarios, you need to tap into the power and arguments of `RubyLLM.chat`. For example, if want to use model aliases with alternate providers. Here is a working example:
275+
With the DB-backed model registry (v1.7.0+), you can specify alternate providers:
276276

277277
```ruby
278-
class Chat < ApplicationRecord
279-
acts_as_chat
278+
# Use a model through a different provider
279+
chat = Chat.create!(
280+
model: 'claude-3-5-sonnet',
281+
provider: 'bedrock' # Use AWS Bedrock instead of Anthropic
282+
)
283+
284+
# The model registry handles the routing automatically
285+
chat.ask("Hello!")
286+
```
287+
288+
### Custom Contexts and Dynamic Models
289+
{: .d-inline-block }
290+
291+
Available in v1.7.0+
292+
{: .label .label-green }
293+
294+
#### Using Custom Contexts
295+
296+
For multi-tenant applications or when you need different API keys per chat:
297+
298+
**With DB-backed model registry (default in v1.7.0+):**
299+
300+
```ruby
301+
# Create a custom context
302+
custom_context = RubyLLM.context do |config|
303+
config.openai_api_key = 'sk-customer-specific-key'
304+
end
305+
306+
# Pass context when creating the chat
307+
chat = Chat.create!(
308+
model: 'gpt-4',
309+
context: custom_context
310+
)
311+
```
312+
313+
**Legacy mode (when using `--skip-model-registry`):**
314+
315+
```ruby
316+
# In legacy mode, you can set context after creation
317+
chat = Chat.create!(model_id: 'gpt-4')
318+
chat.with_context(custom_context) # This method only exists in legacy mode
319+
```
320+
321+
> The `context` is NOT persisted to the database. You must set it again when reloading chats:
322+
{: .warning }
280323

281-
validates :model_id, presence: true
282-
validates :provider, presence: true
324+
```ruby
325+
# Later, in a different request or after restart
326+
chat = Chat.find(chat_id)
327+
chat.context = custom_context # Must set this!
328+
chat.ask("Continue our conversation")
329+
```
330+
331+
For multi-tenant apps, consider using an `after_find` callback:
332+
333+
```ruby
334+
class Chat < ApplicationRecord
335+
acts_as_chat
336+
belongs_to :tenant
283337

284-
after_initialize :set_chat
338+
after_find :set_tenant_context
285339

286-
def set_chat
287-
@chat = RubyLLM.chat(model: model_id, provider:)
340+
private
341+
342+
def set_tenant_context
343+
self.context = RubyLLM.context do |config|
344+
config.openai_api_key = tenant.openai_api_key
288345
end
289346
end
347+
end
348+
```
349+
350+
#### Dynamic Model Creation
351+
352+
When using models not in the registry (e.g., new OpenRouter models):
290353

291-
# Then in your controller or background job:
292-
Chat.new(model_id: 'alias', provider: 'provider_name')
354+
```ruby
355+
# Create chat with a dynamic model
356+
chat = Chat.create!(
357+
model: 'experimental-llm-v2',
358+
provider: 'openrouter',
359+
assume_model_exists: true # Creates Model record automatically
360+
)
293361
```
294362

363+
> Like `context`, `assume_model_exists` is NOT persisted. Set it when needed for model changes:
364+
{: .note }
365+
366+
```ruby
367+
# When switching to another dynamic model later
368+
chat = Chat.find(chat_id)
369+
chat.assume_model_exists = true
370+
chat.with_model('another-experimental-model', provider: 'openrouter')
371+
```
295372

296373
## Basic Usage
297374

@@ -333,24 +410,25 @@ Available in v1.7.0+
333410
When using the Model registry (created by default by the generator), your chats and messages get associations to model records:
334411

335412
```ruby
336-
chat = Chat.create! model_id: 'gpt-4.1-nano'
337-
chat.model # => #<Model model_id: "gpt-4.1-nano", provider: "openai">
338-
chat.model.context_window # => 1047576
339-
chat.model.supports? 'structured_output' # => true
413+
# String automatically resolves to Model record
414+
chat = Chat.create!(model: 'gpt-4')
415+
chat.model # => #<Model model_id: "gpt-4o", provider: "openai">
416+
chat.model.name # => "GPT-4"
417+
chat.model.context_window # => 128000
418+
chat.model.supports_vision # => true
340419

341-
# Populate your database with all available models
342-
Model.refresh!
420+
# Populate/refresh models from models.json
421+
rails ruby_llm:load_models
343422

344423
# Query based on model attributes
345424
Chat.joins(:model).where(models: { provider: 'anthropic' })
346425
Model.left_joins(:chats).group(:id).order('COUNT(chats.id) DESC')
347426

348-
# Extend with custom attributes via migration
349-
Model.where('monthly_limit > ?', 1000)
427+
# Find models with specific capabilities
428+
Model.where(supports_functions: true)
429+
Model.where(supports_vision: true)
350430
```
351431

352-
The registry automatically falls back to JSON if the database is unavailable, ensuring resilience.
353-
354432
### System Instructions
355433

356434
Instructions (system prompts) set via `with_instructions` are also automatically persisted as `Message` records with the `system` role:

docs/_advanced/upgrading-to-1.7.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
---
2+
layout: default
3+
title: Upgrading to 1.7
4+
nav_order: 6
5+
description: Upgrade to the DB-backed model registry for better data integrity and rich model metadata.
6+
---
7+
8+
# {{ page.title }}
9+
{: .no_toc }
10+
11+
{{ page.description }}
12+
{: .fs-6 .fw-300 }
13+
14+
## Table of contents
15+
{: .no_toc .text-delta }
16+
17+
1. TOC
18+
{:toc}
19+
20+
---
21+
22+
## What's New in 1.7
23+
24+
Among other features, the DB-backed model registry replaces simple string fields with proper ActiveRecord associations.
25+
26+
### Available with DB-backed Model Registry (1.7+)
27+
28+
**Rich model metadata**
29+
```ruby
30+
chat.model.name # => "GPT-4"
31+
chat.model.context_window # => 128000
32+
chat.model.supports_vision # => true
33+
chat.model.input_token_cost # => 2.50
34+
```
35+
36+
**Provider routing**
37+
```ruby
38+
Chat.create!(model: "claude-3-5-sonnet", provider: "bedrock")
39+
```
40+
41+
**Model associations and queries**
42+
```ruby
43+
Chat.joins(:model).where(models: { provider: 'anthropic' })
44+
Model.select { |m| m.supports_functions? } # Use delegated methods
45+
```
46+
47+
**Model alias resolution**
48+
```ruby
49+
Chat.create!(model: "gpt-4.1-nano", provider: "openrouter") # Resolves to openai/gpt-4.1-nano automatically
50+
```
51+
52+
**Usage tracking**
53+
```ruby
54+
Model.joins(:chats).group(:id).order('COUNT(chats.id) DESC')
55+
```
56+
57+
### Available without Model Registry (<1.7 or legacy mode)
58+
59+
**Basic functionality** - All core RubyLLM features work
60+
```ruby
61+
chat.ask("Hello!") # Works fine
62+
chat.model_id # => "gpt-4" (string only, no metadata)
63+
```
64+
65+
**Limited to:**
66+
- String-based model IDs only
67+
- Default provider routing
68+
69+
## Upgrading from 1.6
70+
71+
### Your App Continues Working
72+
73+
Without any changes, your 1.6 app continues to work with string fields. You'll see a deprecation warning on Rails boot.
74+
75+
### Migrate to Model Registry (Recommended)
76+
77+
```bash
78+
rails generate ruby_llm:install
79+
rails generate ruby_llm:migrate_model_fields
80+
rails db:migrate
81+
```
82+
83+
Then enable it in your initializer:
84+
85+
```ruby
86+
# config/initializers/ruby_llm.rb
87+
RubyLLM.configure do |config|
88+
config.model_registry_class = "Model"
89+
end
90+
```
91+
92+
That's it! The migration:
93+
- Creates the models table
94+
- Loads all models from models.json automatically
95+
- Migrates your existing data to use foreign keys
96+
- Preserves everything (renames old columns to `model_id_string`)
97+
98+
## New Applications
99+
100+
Fresh installs get the model registry automatically:
101+
102+
```bash
103+
rails generate ruby_llm:install
104+
rails db:migrate
105+
```

docs/_getting_started/getting-started.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,15 @@ After reading this guide, you will know:
3535
Add RubyLLM to your Gemfile:
3636

3737
```ruby
38-
gem 'ruby_llm'
38+
bundle add ruby_llm
3939
```
4040

41-
Then run `bundle install`.
42-
4341
### Rails Quick Setup
4442

4543
For Rails applications, you can use the generator to set up database-backed conversations:
4644

4745
```bash
48-
$ rails generate ruby_llm:install
46+
rails generate ruby_llm:install
4947
```
5048

5149
This creates Chat and Message models with ActiveRecord persistence. Your conversations will be automatically saved to the database. See the [Rails Integration Guide]({% link _advanced/rails.md %}) for full details.

gemfiles/rails_7.1.gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ GEM
214214
mutex_m (0.3.0)
215215
net-http (0.6.0)
216216
uri
217-
net-imap (0.5.9)
217+
net-imap (0.5.10)
218218
date
219219
net-protocol
220220
net-pop (0.1.2)

gemfiles/rails_7.2.gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ GEM
207207
multipart-post (2.4.1)
208208
net-http (0.6.0)
209209
uri
210-
net-imap (0.5.9)
210+
net-imap (0.5.10)
211211
date
212212
net-protocol
213213
net-pop (0.1.2)

gemfiles/rails_8.0.gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ GEM
207207
multipart-post (2.4.1)
208208
net-http (0.6.0)
209209
uri
210-
net-imap (0.5.9)
210+
net-imap (0.5.10)
211211
date
212212
net-protocol
213213
net-pop (0.1.2)

0 commit comments

Comments
 (0)