Skip to content

Multi Tenancy

adham90 edited this page Feb 14, 2026 · 6 revisions

Multi-Tenancy

RubyLLM::Agents supports multi-tenant applications with per-tenant budgets, circuit breaker isolation, and execution tracking.

Configuration

Enable multi-tenancy in your initializer:

# config/initializers/ruby_llm_agents.rb
RubyLLM::Agents.configure do |config|
  # Enable multi-tenancy support
  config.multi_tenancy_enabled = true

  # Define how to resolve the current tenant
  config.tenant_resolver = -> { Current.tenant_id }
end

Tenant Resolver

The tenant_resolver is a proc that returns the current tenant identifier. Common patterns:

# Using Rails Current attributes
config.tenant_resolver = -> { Current.tenant_id }

# Using RequestStore gem
config.tenant_resolver = -> { RequestStore.store[:tenant_id] }

# Using Apartment gem
config.tenant_resolver = -> { Apartment::Tenant.current }

# Using ActsAsTenant gem
config.tenant_resolver = -> { ActsAsTenant.current_tenant&.id }

Tenant Config Resolver

The optional tenant_config_resolver allows you to provide tenant configuration dynamically, overriding database lookups:

config.tenant_config_resolver = ->(tenant_id) {
  tenant = Tenant.find(tenant_id)
  {
    name: tenant.name,
    daily_limit: tenant.subscription.daily_budget,
    monthly_limit: tenant.subscription.monthly_budget,
    daily_token_limit: tenant.subscription.daily_tokens,
    monthly_token_limit: tenant.subscription.monthly_tokens,
    enforcement: tenant.subscription.hard_limits? ? :hard : :soft
  }
}

This is useful when tenant budgets are managed in a different system or derived from subscription plans.

Explicit Tenant Override

You can bypass the tenant resolver by passing the tenant explicitly to .call():

# Pass tenant_id explicitly (bypasses resolver, uses DB or config_resolver)
MyAgent.call(query: "Analyze this data", tenant: "acme_corp")

# Pass full config as a hash (runtime override, no DB lookup)
MyAgent.call(query: "Analyze this data", tenant: {
  id: "acme_corp",
  daily_limit: 100.0,
  monthly_limit: 1000.0,
  daily_token_limit: 1_000_000,
  monthly_token_limit: 10_000_000,
  enforcement: :hard
})

This is useful for:

  • Background jobs where Current.tenant isn't set
  • Cross-tenant operations by admin users
  • Testing with specific tenant configurations

LLMTenant DSL

The LLMTenant concern provides a declarative DSL for making ActiveRecord models function as LLM tenants with automatic budget management and usage tracking.

Including the Concern

class Organization < ApplicationRecord
  include RubyLLM::Agents::LLMTenant

  llm_tenant  # Minimal setup
end

DSL Parameters

Parameter Type Default Description
id: Symbol :id Method to call for tenant_id string
name: Symbol :to_s Method for tenant display name
budget: Boolean false Auto-create Tenant record on model creation
limits: Hash nil Default budget limits (implies budget: true)
enforcement: Symbol nil :none, :soft, or :hard
inherit_global: Boolean true Inherit from global config for unset limits
api_keys: Hash nil Provider API key mapping

Limits Hash Structure

The limits: hash supports cost, token, and execution-based limits:

limits: {
  daily_cost: 100.0,           # USD per day
  monthly_cost: 1000.0,        # USD per month
  daily_tokens: 1_000_000,     # Tokens per day
  monthly_tokens: 10_000_000,  # Tokens per month
  daily_executions: 500,       # Agent calls per day
  monthly_executions: 10_000   # Agent calls per month
}

Automatic Associations

When you include LLMTenant, the following associations are added:

has_many :llm_executions       # All agent executions for this tenant
has_one :llm_tenant_record     # The gem's Tenant model (budget + tracking)

For backward compatibility, llm_budget is available as an alias for llm_tenant_record.

Instance Methods

Tenant Identity

Method Returns Description
llm_tenant_id String Tenant identifier (from id: DSL option)
llm_api_keys Hash Resolved API keys from api_keys: config

Cost Tracking

Method Returns Description
llm_cost(period:) BigDecimal Total cost for the specified period
llm_cost_today BigDecimal Today's cost
llm_cost_this_month BigDecimal This month's cost

Token Tracking

Method Returns Description
llm_tokens(period:) Integer Total tokens for the specified period
llm_tokens_today Integer Today's tokens
llm_tokens_this_month Integer This month's tokens

Execution Counting

Method Returns Description
llm_execution_count(period:) Integer Execution count for the period
llm_executions_today Integer Today's execution count
llm_executions_this_month Integer This month's execution count

Budget Management

Method Returns Description
llm_tenant Tenant Get or build tenant record
llm_budget Tenant Alias for llm_tenant (backward compatible)
llm_configure { } Tenant Configure and save tenant with block
llm_configure_budget { } Tenant Alias for llm_configure (backward compatible)
llm_budget_status Hash Full budget status from BudgetTracker
llm_within_budget?(type:) Boolean Check if within budget for limit type
llm_remaining_budget(type:) Numeric Remaining budget for limit type
llm_check_budget! void Raises BudgetExceededError if over hard limit

Usage Summary

Method Returns Description
llm_usage_summary(period:) Hash Combined metrics {cost:, tokens:, executions:, period:}

Period Options

All period-based methods accept these values:

Period Description
:today Current day
:yesterday Previous day
:this_week Current week
:this_month Current month
Range Custom date/time range (e.g., 2.days.ago..Time.current)

Budget Limit Types

For llm_within_budget? and llm_remaining_budget:

Type Description
:daily_cost Daily cost limit (default)
:monthly_cost Monthly cost limit
:daily_tokens Daily token limit
:monthly_tokens Monthly token limit
:daily_executions Daily execution limit
:monthly_executions Monthly execution limit

Use Case Examples

1. Minimal Tracking Only

Track executions without budgets:

class Organization < ApplicationRecord
  include RubyLLM::Agents::LLMTenant

  llm_tenant id: :slug
end

# Usage
org = Organization.find_by(slug: "acme")
org.llm_cost_this_month        # => 125.50
org.llm_executions_today       # => 42
org.llm_usage_summary(period: :today)
# => { cost: 12.50, tokens: 50000, executions: 42, period: :today }

2. Auto-Budget with Global Defaults

Create budgets automatically, inheriting global limits:

class Account < ApplicationRecord
  include RubyLLM::Agents::LLMTenant

  llm_tenant id: :uuid, name: :company_name, budget: true
end

# Budget auto-created on Account.create, inherits from global config

3. Custom Limits with Hard Enforcement

Set specific limits with strict enforcement:

class Workspace < ApplicationRecord
  include RubyLLM::Agents::LLMTenant

  llm_tenant(
    id: :external_id,
    name: :display_name,
    limits: {
      daily_cost: 50.0,
      monthly_cost: 500.0,
      daily_tokens: 500_000,
      monthly_tokens: 5_000_000,
      daily_executions: 200,
      monthly_executions: 5_000
    },
    enforcement: :hard
  )
end

# Check budget programmatically
workspace = Workspace.find(1)
workspace.llm_within_budget?(type: :daily_cost)  # => true
workspace.llm_remaining_budget(type: :daily_cost) # => 37.50

# This raises BudgetExceededError if over hard limit
workspace.llm_check_budget!

4. Full Configuration with API Keys

Complete setup with per-tenant API keys:

class Organization < ApplicationRecord
  include RubyLLM::Agents::LLMTenant

  encrypts :openai_api_key, :anthropic_api_key  # Rails 7+ encryption

  llm_tenant(
    id: :slug,
    name: :company_name,
    limits: {
      daily_cost: 100.0,
      monthly_cost: 1000.0,
      daily_tokens: 1_000_000,
      monthly_tokens: 10_000_000
    },
    enforcement: :hard,
    inherit_global: true,
    api_keys: {
      openai: :openai_api_key,
      anthropic: :anthropic_api_key,
      gemini: :fetch_gemini_key
    }
  )

  def fetch_gemini_key
    Vault.read("secret/#{slug}/gemini")
  end
end

# Tenant's API keys are automatically applied
org = Organization.find_by(slug: "acme-corp")
result = MyAgent.call(query: "Hello", tenant: org)

5. Programmatic Budget Configuration

Configure budgets dynamically after model creation:

org = Organization.find(1)

# Configure with block
org.llm_configure_budget do |budget|
  budget.daily_limit = 75.0
  budget.monthly_limit = 750.0
  budget.daily_execution_limit = 300
  budget.enforcement = "hard"
end

# Or access and modify directly
budget = org.llm_budget
budget.update(monthly_limit: 1000.0)

Tenant Model

The Tenant model is the central entity for managing multi-tenant LLM usage. It encapsulates:

  • Budget management (limits and enforcement) via the Budgetable concern
  • Usage tracking (cost, tokens, executions) via the Trackable concern
# Create a tenant
RubyLLM::Agents::Tenant.create!(
  tenant_id: "tenant_123",
  name: "Acme Corporation",
  daily_limit: 50.0,
  monthly_limit: 500.0,
  daily_token_limit: 500_000,
  monthly_token_limit: 5_000_000,
  daily_execution_limit: 500,
  monthly_execution_limit: 10_000,
  enforcement: "hard"
)

Schema

Field Type Description
tenant_id string Unique tenant identifier
name string Human-readable display name
daily_limit decimal Daily spending limit in USD
monthly_limit decimal Monthly spending limit in USD
daily_token_limit integer Daily token usage limit
monthly_token_limit integer Monthly token usage limit
daily_execution_limit integer Daily agent call limit
monthly_execution_limit integer Monthly agent call limit
enforcement string "none", "soft" (warn), or "hard" (block)
inherit_global_defaults boolean Fall back to global config for unset limits
active boolean Whether tenant is active (default: true)
metadata json Extensible metadata storage

Managing Tenants

# Find or create with defaults
tenant = RubyLLM::Agents::Tenant.for!("tenant_123", name: "Acme Corp")

# Update limits
tenant.update(daily_limit: 50.0)

# Query effective limits (includes inheritance from global config)
tenant.effective_daily_limit           # => 50.0 (cost)
tenant.effective_monthly_limit         # => 250.0 (cost)
tenant.effective_daily_token_limit     # => 500_000 (tokens)
tenant.effective_monthly_token_limit   # => 5_000_000 (tokens)
tenant.effective_daily_execution_limit # => 200 (executions)
tenant.effective_monthly_execution_limit # => nil (not set)

# Check enforcement mode
tenant.effective_enforcement  # => :hard
tenant.budgets_enabled?       # => true

# Get display name
tenant.display_name  # => "Acme Corp" (or tenant_id if name not set)

# Check status
tenant.active?     # => true
tenant.linked?     # => false (no polymorphic association)

# Deactivate/activate tenant
tenant.deactivate!
tenant.activate!

# Convert to budget config hash (used by BudgetTracker)
tenant.to_budget_config
# => { enabled: true, enforcement: :hard, global_daily: 50.0, ... }

Finding Tenants

# By tenant_id string
tenant = RubyLLM::Agents::Tenant.for("tenant_123")

# By tenant object (uses polymorphic association or llm_tenant_id)
tenant = RubyLLM::Agents::Tenant.for(organization)

# Find or create with name
tenant = RubyLLM::Agents::Tenant.for!("tenant_123", name: "Acme")

Usage Tracking (Trackable Concern)

The Tenant model provides rich usage tracking methods:

tenant = RubyLLM::Agents::Tenant.for("tenant_123")

# Cost tracking
tenant.cost                        # => Total cost
tenant.cost_today                  # => Today's cost
tenant.cost_yesterday              # => Yesterday's cost
tenant.cost_this_week              # => This week's cost
tenant.cost_this_month             # => This month's cost
tenant.cost(period: 7.days.ago..Time.current)  # Custom range

# Token tracking
tenant.tokens                      # => Total tokens
tenant.tokens_today                # => Today's tokens
tenant.tokens_this_month           # => This month's tokens

# Execution tracking
tenant.execution_count             # => Total executions
tenant.executions_today            # => Today's executions
tenant.executions_this_month       # => This month's executions

# Usage summary
tenant.usage_summary(period: :this_month)
# => { tenant_id: "tenant_123", name: "Acme", period: :this_month,
#      cost: 150.50, tokens: 1_500_000, executions: 500 }

# Usage by agent type
tenant.usage_by_agent(period: :this_month)
# => { "ChatAgent" => { cost: 100.0, tokens: 1_000_000, count: 300 },
#      "SummaryAgent" => { cost: 50.50, tokens: 500_000, count: 200 } }

# Usage by model
tenant.usage_by_model(period: :this_month)
# => { "gpt-4o" => { cost: 120.0, tokens: 800_000, count: 400 },
#      "claude-3-5-sonnet" => { cost: 30.50, tokens: 700_000, count: 100 } }

# Usage by day
tenant.usage_by_day(period: :this_month)
# => { Date.current => { cost: 10.0, tokens: 100_000, count: 50 }, ... }

# Recent and failed executions
tenant.recent_executions(limit: 10)
tenant.failed_executions(limit: 5, period: :today)

Scopes

# Filter by status
RubyLLM::Agents::Tenant.active
RubyLLM::Agents::Tenant.inactive

# Filter by linkage
RubyLLM::Agents::Tenant.linked    # Has polymorphic tenant_record
RubyLLM::Agents::Tenant.unlinked  # Standalone tenants

TenantBudget (Deprecated)

Deprecation Notice: TenantBudget is now an alias for Tenant. All functionality has been moved to the Tenant model. TenantBudget will be removed in a future major version.

For backward compatibility, you can still use TenantBudget:

# Old usage (still works, deprecated)
RubyLLM::Agents::TenantBudget.for_tenant("tenant_123")
RubyLLM::Agents::TenantBudget.for_tenant!("tenant_123", name: "Acme")

# New usage (preferred)
RubyLLM::Agents::Tenant.for("tenant_123")
RubyLLM::Agents::Tenant.for!("tenant_123", name: "Acme")

Execution Filtering by Tenant

Filter executions by tenant:

# All executions for a specific tenant
RubyLLM::Agents::Execution.by_tenant("tenant_123")

# Executions for the current tenant (uses tenant_resolver)
RubyLLM::Agents::Execution.for_current_tenant

# Executions with tenant_id set
RubyLLM::Agents::Execution.with_tenant

# Executions without tenant_id (global/system executions)
RubyLLM::Agents::Execution.without_tenant

Tenant Analytics

# Cost by tenant this month
RubyLLM::Agents::Execution
  .this_month
  .with_tenant
  .group(:tenant_id)
  .sum(:total_cost)
# => { "tenant_a" => 150.00, "tenant_b" => 75.00, ... }

# Execution count by tenant
RubyLLM::Agents::Execution
  .this_week
  .with_tenant
  .group(:tenant_id)
  .count
# => { "tenant_a" => 1250, "tenant_b" => 890, ... }

# Top spending tenants
RubyLLM::Agents::Execution
  .this_month
  .with_tenant
  .group(:tenant_id)
  .sum(:total_cost)
  .sort_by { |_, cost| -cost }
  .first(10)

Circuit Breaker Isolation

When multi-tenancy is enabled, circuit breakers are isolated per tenant. This prevents one tenant's failures from affecting other tenants.

class MyAgent < ApplicationAgent
  model "gpt-4o"
  circuit_breaker errors: 10, within: 60, cooldown: 300
end

With multi-tenancy enabled:

  • Tenant A's errors only affect Tenant A's circuit breaker
  • Tenant B can continue operating even if Tenant A's circuit is open
  • Each tenant has their own error count and cooldown state

Checking Circuit Breaker State

# Check if circuit is open for current tenant
RubyLLM::Agents::CircuitBreaker.open_for?(
  agent: MyAgent,
  tenant_id: Current.tenant_id
)

# Check for specific tenant
RubyLLM::Agents::CircuitBreaker.open_for?(
  agent: MyAgent,
  tenant_id: "tenant_123"
)

Adding Custom Tenant Metadata

Include tenant information in execution metadata:

class TenantAwareAgent < ApplicationAgent
  model "gpt-4o"

  def metadata
    {
      tenant_id: Current.tenant_id,
      tenant_name: Current.tenant&.name,
      tenant_plan: Current.tenant&.plan
    }
  end
end

Budget Enforcement

With multi-tenancy enabled, budget checks happen at both global and tenant levels. Limits can be set for costs, tokens, and executions:

# Global limits (all tenants combined)
config.budgets = {
  global_daily: 1000.0,
  global_monthly: 20000.0,
  global_daily_tokens: 10_000_000,
  global_monthly_tokens: 100_000_000,
  global_daily_executions: 5000,
  global_monthly_executions: 100_000
}

# Per-tenant limits (via Tenant model)
RubyLLM::Agents::Tenant.create!(
  tenant_id: "tenant_123",
  name: "Acme Corp",
  daily_limit: 50.0,
  monthly_limit: 500.0,
  daily_token_limit: 500_000,
  monthly_token_limit: 5_000_000,
  daily_execution_limit: 200,
  monthly_execution_limit: 5000,
  enforcement: "hard"
)

Execution is blocked if any limit is exceeded (when using "hard" enforcement). With "soft" enforcement, warnings are logged but execution continues.

Handling Tenant Budget Errors

begin
  result = MyAgent.call(query: params[:query])
rescue RubyLLM::Agents::BudgetExceededError => e
  if e.tenant_budget?
    # Tenant-specific budget exceeded
    render json: { error: "Your organization has exceeded its daily limit" }
  else
    # Global budget exceeded
    render json: { error: "Service temporarily unavailable" }
  end
end

Dashboard Integration

The dashboard automatically shows:

  • Spending breakdown by tenant (when multi-tenancy enabled)
  • Tenant budget status and utilization
  • Per-tenant execution filtering

Filter executions by tenant in the dashboard URL:

/agents/executions?tenant_id=tenant_123

Tenant API Keys

Each tenant can have their own API keys stored on the model and resolved at runtime via the api_keys: option in the llm_tenant DSL.

Configuration

class Organization < ApplicationRecord
  include RubyLLM::Agents::LLMTenant

  # Encrypt API keys at rest (Rails 7+)
  encrypts :openai_api_key, :anthropic_api_key

  llm_tenant(
    id: :slug,
    name: :company_name,
    api_keys: {
      openai: :openai_api_key,        # Column name
      anthropic: :anthropic_api_key,  # Column name
      gemini: :fetch_gemini_key       # Custom method
    }
  )

  # Custom method to fetch from external source
  def fetch_gemini_key
    Vault.read("secret/#{slug}/gemini")
  end
end

API Key Resolution Priority

When an agent executes, API keys are resolved in this order:

  1. Tenant object api_keys: → DSL-defined methods/columns (highest priority)
  2. Runtime hash api_keys: → Passed via tenant: { id: ..., api_keys: {...} }
  3. RubyLLM.configure → Config file/environment (lowest priority)

Usage

# Tenant's API keys are automatically applied when agent executes
org = Organization.find_by(slug: "acme-corp")
result = MyAgent.call(query: "Hello", tenant: org)
# Uses org.openai_api_key for OpenAI requests

# Runtime hash also supports api_keys
result = MyAgent.call(
  query: "Hello",
  tenant: {
    id: "acme-corp",
    api_keys: {
      openai: "sk-runtime-key-123"
    }
  }
)

Supported Providers

The api_keys: hash maps provider names to RubyLLM config setters:

Key RubyLLM Setter
openai: openai_api_key=
anthropic: anthropic_api_key=
gemini: gemini_api_key=
deepseek: deepseek_api_key=
mistral: mistral_api_key=

Security Considerations

  • Always encrypt API keys - Use encrypts (Rails 7+) or attr_encrypted
  • Avoid logging - Ensure API keys aren't exposed in logs
  • Rotate regularly - Allow tenants to rotate their keys through your UI
  • Validate keys - Consider validating keys before storing them

Example: Full Multi-Tenant Setup

# config/initializers/ruby_llm_agents.rb
RubyLLM::Agents.configure do |config|
  config.multi_tenancy_enabled = true
  config.tenant_resolver = -> { Current.tenant_id }

  # Global limits as a safety net
  config.budgets = {
    global_daily: 1000.0,
    global_monthly: 20000.0,
    global_daily_tokens: 10_000_000,
    global_monthly_executions: 50_000,
    enforcement: :hard
  }
end

# app/models/organization.rb
class Organization < ApplicationRecord
  include RubyLLM::Agents::LLMTenant

  encrypts :openai_api_key, :anthropic_api_key

  llm_tenant(
    id: :slug,
    name: :display_name,
    limits: {
      daily_cost: 100.0,
      monthly_cost: 1000.0,
      daily_tokens: 1_000_000,
      monthly_tokens: 10_000_000,
      daily_executions: 500,
      monthly_executions: 10_000
    },
    enforcement: :hard,
    api_keys: {
      openai: :openai_api_key,
      anthropic: :anthropic_api_key
    }
  )
end

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :tenant_id, :organization
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :set_current_tenant

  private

  def set_current_tenant
    Current.organization = current_user&.organization
    Current.tenant_id = Current.organization&.slug
  end
end

# Usage in controllers
class AiController < ApplicationController
  def analyze
    # Pass the organization as tenant - API keys and budget are automatic
    result = AnalysisAgent.call(
      query: params[:query],
      tenant: Current.organization
    )
    render json: result.response
  rescue RubyLLM::Agents::BudgetExceededError => e
    render json: { error: "Usage limit exceeded" }, status: 429
  end
end

# Usage in views/dashboards
org = Organization.find(1)
org.llm_usage_summary(period: :this_month)
# => { cost: 450.50, tokens: 4_500_000, executions: 3200, period: :this_month }

org.llm_within_budget?(type: :monthly_cost)  # => true
org.llm_remaining_budget(type: :monthly_cost) # => 549.50

Related Pages

Clone this wiki locally