- Notifications
You must be signed in to change notification settings - Fork 4
Multi Tenancy
RubyLLM::Agents supports multi-tenant applications with per-tenant budgets, circuit breaker isolation, and execution tracking.
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 } endThe 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 }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.
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.tenantisn't set - Cross-tenant operations by admin users
- Testing with specific tenant configurations
The LLMTenant concern provides a declarative DSL for making ActiveRecord models function as LLM tenants with automatic budget management and usage tracking.
class Organization < ApplicationRecord include RubyLLM::Agents::LLMTenant llm_tenant # Minimal setup end| 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 |
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 }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.
| Method | Returns | Description |
|---|---|---|
llm_tenant_id | String | Tenant identifier (from id: DSL option) |
llm_api_keys | Hash | Resolved API keys from api_keys: config |
| 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 |
| 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 |
| 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 |
| 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 |
| Method | Returns | Description |
|---|---|---|
llm_usage_summary(period:) | Hash | Combined metrics {cost:, tokens:, executions:, period:} |
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) |
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 |
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 }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 configSet 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!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)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)The Tenant model is the central entity for managing multi-tenant LLM usage. It encapsulates:
- Budget management (limits and enforcement) via the
Budgetableconcern - Usage tracking (cost, tokens, executions) via the
Trackableconcern
# 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" )| 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 |
# 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, ... }# 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")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)# 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 tenantsDeprecation Notice:
TenantBudgetis now an alias forTenant. All functionality has been moved to theTenantmodel.TenantBudgetwill 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")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# 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)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 endWith 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
# 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" )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 endWith 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.
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 endThe 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 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.
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 endWhen an agent executes, API keys are resolved in this order:
- Tenant object
api_keys:→ DSL-defined methods/columns (highest priority) - Runtime hash
api_keys:→ Passed viatenant: { id: ..., api_keys: {...} } - RubyLLM.configure → Config file/environment (lowest priority)
# 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" } } )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= |
- Always encrypt API keys - Use
encrypts(Rails 7+) orattr_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
# 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- Budget Controls - Spending limits
- Execution Tracking - Filtering and analytics
- Circuit Breakers - Failure handling
- Configuration - Full setup guide