The project is in a healthy, maintained state
Drop-in Mongoid replacement for ruby_llm's ActiveRecord integration. Provides acts_as_chat, acts_as_message, acts_as_tool_call, and acts_as_model macros backed by MongoDB via Mongoid.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 8.0
>= 1.16
 Project Readme

ruby_llm-mongoid

CI Coverage Status Gem Version

Drop-in Mongoid persistence for ruby_llm. Use MongoDB as your Rails model layer instead of ActiveRecord — same acts_as_chat API, same feel.

Requirements

  • Ruby >= 3.3
  • Mongoid >= 8.0
  • ruby_llm >= 1.16

Installation

Add to your Gemfile:

gem "ruby_llm-mongoid"

Quick start

1. Generate models

bin/rails g ruby_llm:mongoid:install

This creates four model files (Chat, Message, ToolCall, LlmModel) and config/initializers/ruby_llm.rb. No migrations — Mongoid is schemaless; fields are declared in the model files.

2. Create indexes

bin/rails db:mongoid:create_indexes

3. Configure API keys

Edit config/initializers/ruby_llm.rb:

RubyLLM.configure do |config|
  config.openai_api_key    = ENV["OPENAI_API_KEY"]
  config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
end

4. Seed the model registry

bin/rails runner "LlmModel.save_to_database"

This populates MongoDB with the bundled model list so Chat.create!(model: "gpt-4o-mini") can resolve the model record.

5. Start chatting

chat = Chat.create!(model: "gpt-4o-mini")
chat.ask("What is the capital of France?")
# => saves user + assistant messages to MongoDB

Model setup

Default (generated) layout

class Chat
  include Mongoid::Document
  include Mongoid::Timestamps

  acts_as_chat model: :llm_model, model_class: "LlmModel"
end

class Message
  include Mongoid::Document
  include Mongoid::Timestamps

  field :role,                  type: String
  field :content,               type: String
  field :input_tokens,          type: Integer
  field :output_tokens,         type: Integer
  # ...additional token/thinking fields

  acts_as_message model: :llm_model, model_class: "LlmModel"
end

class ToolCall
  include Mongoid::Document
  include Mongoid::Timestamps

  field :tool_call_id,  type: String
  field :name,          type: String
  field :arguments,     type: Hash, default: {}

  acts_as_tool_call
end

class LlmModel
  include Mongoid::Document
  include Mongoid::Timestamps

  field :model_id,  type: String
  field :name,      type: String
  field :provider,  type: String
  # ...pricing/capability fields

  acts_as_model
end

Custom class / association names

class Conversation
  include Mongoid::Document

  acts_as_chat messages: :turns,
               message_class: "Turn",
               model: :ai_model,
               model_class: "AiModel"
end

API reference

Chat

chat = Chat.create!(model: "gpt-4o-mini")
chat = Chat.create!(model: "claude-opus-4-7", provider: "anthropic")

chat.ask("Hello!")
chat.say("Hello!")    # alias for ask

chat.with_instructions("You are a helpful assistant.")
chat.with_instructions("More context.", append: true)
chat.with_runtime_instructions("Only reply in French.")  # not persisted to DB

chat.with_model("claude-opus-4-7")
chat.with_tool(MyTool)
chat.with_tools(ToolA, ToolB)
chat.with_temperature(0.7)
chat.with_thinking(budget: 5000)

chat.on_new_message  { |msg| puts msg.content }
chat.on_end_message  { |msg| broadcast(msg) }

chat.cost  # => RubyLLM::Cost

Message

msg = chat.messages_association.last
msg.to_llm          # => RubyLLM::Message
msg.tokens          # => RubyLLM::Tokens
msg.cost            # => RubyLLM::Cost
msg.to_partial_path # => "messages/assistant"

Model registry

acts_as_model automatically registers a MongoidSource with RubyLLM.config.model_registry_source, so RubyLLM.models reads from MongoDB on boot instead of the bundled JSON file.

LlmModel.save_to_database  # seed MongoDB from the bundled model registry (run once after install)
LlmModel.refresh!          # fetch the latest list from all providers, then persist to MongoDB

Both methods are idempotent — refresh! is a good candidate for a periodic background job or a deploy hook.

Differences from ActiveRecord integration

Concern ActiveRecord Mongoid (this gem)
Primary key type integer BSON::ObjectId
Tool-call result FK field tool_call_id (integer) parent_tool_call_id (ObjectId)
Transactions native requires replica set; auto no-op on standalone
File attachments ActiveStorage GridFS via GridFsAttachment concern
has_many :through supported replaced by direct queries

parent_tool_call_id field name

In the ActiveRecord integration the FK column linking a tool-result message back to its ToolCall is named tool_call_id (integer). Mongoid uses parent_tool_call_id (BSON::ObjectId) to avoid a type collision with the string tool_call_id field that stores the provider-issued call ID. This is handled automatically — you don't need to think about it unless you are writing raw queries.

Transactions

Multi-document transactions require a MongoDB replica set or Atlas cluster. On a standalone mongod the transaction helper automatically falls back to running the block without a transaction — useful for development and testing.

For production, run at minimum a single-node replica set:

# mongod.conf: add replication.replSetName: "rs0"
mongosh --eval "rs.initiate()"

File attachments (GridFS)

Include RubyLLM::Mongoid::GridFsAttachment in your message model to store attachments in MongoDB's native GridFS bucket instead of ActiveStorage:

class Message
  include Mongoid::Document
  include Mongoid::Timestamps
  include RubyLLM::Mongoid::GridFsAttachment

  field :role,    type: String
  field :content, type: String
  # ...other fields

  acts_as_message model: :llm_model, model_class: "LlmModel"
end

Then pass files to ask the same way you would with the ActiveRecord integration:

chat.ask("What's in this image?", with: [params[:file]])
chat.ask("Summarise this PDF.",    with: ["/path/to/doc.pdf"])

Files are stored in a GridFS bucket named "attachments" by default. Change it per model:

class Message
  include RubyLLM::Mongoid::GridFsAttachment
  use_gridfs_bucket :llm_files
end

GridFS files are automatically deleted when the owning message or its parent chat is destroyed.

Testing

bundle exec rspec

The spec_helper checks whether MongoDB is reachable on localhost:27017. If nothing is listening it starts a mongo:8.0 Docker container automatically and stops it when the suite exits. You don't need to start MongoDB manually.

The suite uses database_cleaner-mongoid for collection-level isolation between examples. LLM HTTP calls are stubbed with WebMock — no real API keys required.

Contributing

Bug reports and pull requests welcome at github.com/SalScotto/ruby_llm-mongoid.

License

MIT — see LICENSE.txt.