Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/configs/graph.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ production:
max_subgraphs: 10 # Support up to 10 subgraphs (1 parent + 10 children = 11 databases total)

# Resource limits
api_rate_multiplier: 2.5 # 2.5x rate limits for team usage
api_rate_multiplier: 1.5 # 1.5x rate limits (2x RAM, same CPU as standard)

# Copy operation limits
copy_operations:
Expand Down Expand Up @@ -168,7 +168,7 @@ production:
# Note: 1 parent + 25 subgraphs = 26 databases total on xlarge instance

# Resource limits
api_rate_multiplier: 5.0 # 5x rate limits for enterprise usage
api_rate_multiplier: 2.5 # 2.5x rate limits (4x RAM, 2x CPU vs standard)

# Copy operation limits
copy_operations:
Expand Down
7 changes: 7 additions & 0 deletions robosystems/adapters/sec/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"mcp_queries_per_minute": 5,
"mcp_queries_per_hour": 100,
"mcp_queries_per_day": 1000,
"searches_per_minute": 5,
"searches_per_hour": 50,
"searches_per_day": 500,
"agent_calls_per_minute": 2,
"agent_calls_per_hour": 20,
"agent_calls_per_day": 200,
Expand All @@ -39,6 +42,9 @@
"mcp_queries_per_minute": 25,
"mcp_queries_per_hour": 500,
"mcp_queries_per_day": 5000,
"searches_per_minute": 25,
"searches_per_hour": 250,
"searches_per_day": 2500,
"agent_calls_per_minute": 10,
"agent_calls_per_hour": 100,
"agent_calls_per_day": 1000,
Expand Down Expand Up @@ -81,6 +87,7 @@
"query",
"mcp",
"agent",
"search",
"schema",
"status",
"info",
Expand Down
232 changes: 103 additions & 129 deletions robosystems/config/rate_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class EndpointCategory(str, Enum):
GRAPH_SYNC = "graph_sync"
GRAPH_MCP = "graph_mcp"
GRAPH_AGENT = "graph_agent"
GRAPH_SEARCH = "graph_search" # OpenSearch full-text search (shared resource)

# High-cost operations
GRAPH_QUERY = "graph_query" # Direct Cypher queries
Expand Down Expand Up @@ -87,150 +88,119 @@ class RateLimitConfig:
SUBSCRIPTION_RATE_LIMITS: dict[
str, dict[EndpointCategory, tuple[int, RateLimitPeriod]]
] = {
"free": {
# Non-graph endpoints - keep some restrictions for free tier
# -----------------------------------------------------------------------
# MANAGED SERVICE RATE LIMITS
# All tiers share managed infrastructure. Limits are conservative to
# protect shared resources (OpenSearch t3.medium, LadybugDB on m7g/r7g).
# For self-hosted scale, customers deploy their own infrastructure.
# Loosen these as infra scales up.
# -----------------------------------------------------------------------
"base": {
# Anonymous / unrecognized tier — tightest limits
EndpointCategory.AUTH: (10, RateLimitPeriod.MINUTE),
EndpointCategory.USER_MANAGEMENT: (60, RateLimitPeriod.MINUTE),
EndpointCategory.TASKS: (60, RateLimitPeriod.MINUTE),
EndpointCategory.STATUS: (120, RateLimitPeriod.MINUTE),
EndpointCategory.SSE: (
5,
RateLimitPeriod.MINUTE,
), # Limited SSE connections for free
EndpointCategory.USER_MANAGEMENT: (30, RateLimitPeriod.MINUTE),
EndpointCategory.TASKS: (30, RateLimitPeriod.MINUTE),
EndpointCategory.STATUS: (60, RateLimitPeriod.MINUTE),
EndpointCategory.SSE: (3, RateLimitPeriod.MINUTE),
EndpointCategory.BILLING: (60, RateLimitPeriod.MINUTE), # Never block payments
# Graph-scoped endpoints - burst protection only
EndpointCategory.GRAPH_READ: (100, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_WRITE: (20, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_ANALYTICS: (10, RateLimitPeriod.MINUTE),
# Graph-scoped
EndpointCategory.GRAPH_READ: (30, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_WRITE: (10, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_ANALYTICS: (5, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_BACKUP: (2, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_SYNC: (5, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_MCP: (10, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_AGENT: (5, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_QUERY: (50, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_SYNC: (3, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_MCP: (5, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_AGENT: (3, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_SEARCH: (5, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_QUERY: (20, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_IMPORT: (2, RateLimitPeriod.MINUTE),
# Table operations - free tier
EndpointCategory.TABLE_QUERY: (30, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_UPLOAD: (10, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_MANAGEMENT: (10, RateLimitPeriod.MINUTE),
# Table operations
EndpointCategory.TABLE_QUERY: (15, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_UPLOAD: (5, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_MANAGEMENT: (5, RateLimitPeriod.MINUTE),
},
# Technical tier names (primary)
# ladybug-standard: m7g.large (8GB, 2 vCPU) — anchor tier
"ladybug-standard": {
# Non-graph endpoints - generous burst limits
EndpointCategory.AUTH: (20, RateLimitPeriod.MINUTE),
EndpointCategory.USER_MANAGEMENT: (600, RateLimitPeriod.MINUTE),
EndpointCategory.TASKS: (200, RateLimitPeriod.MINUTE),
EndpointCategory.STATUS: (600, RateLimitPeriod.MINUTE),
EndpointCategory.SSE: (
10,
RateLimitPeriod.MINUTE,
), # Standard SSE connection rate
EndpointCategory.USER_MANAGEMENT: (60, RateLimitPeriod.MINUTE),
EndpointCategory.TASKS: (60, RateLimitPeriod.MINUTE),
EndpointCategory.STATUS: (120, RateLimitPeriod.MINUTE),
EndpointCategory.SSE: (5, RateLimitPeriod.MINUTE),
EndpointCategory.BILLING: (60, RateLimitPeriod.MINUTE), # Never block payments
# Graph-scoped endpoints - HIGH BURST LIMITS
EndpointCategory.GRAPH_READ: (500, RateLimitPeriod.MINUTE), # 30k/hour possible
EndpointCategory.GRAPH_WRITE: (100, RateLimitPeriod.MINUTE), # 6k/hour possible
EndpointCategory.GRAPH_ANALYTICS: (
50,
RateLimitPeriod.MINUTE,
), # 3k/hour possible
EndpointCategory.GRAPH_BACKUP: (10, RateLimitPeriod.MINUTE), # 600/hour possible
EndpointCategory.GRAPH_SYNC: (100, RateLimitPeriod.MINUTE), # 6k/hour possible
EndpointCategory.GRAPH_MCP: (100, RateLimitPeriod.MINUTE), # 6k/hour possible
EndpointCategory.GRAPH_AGENT: (50, RateLimitPeriod.MINUTE), # 3k/hour possible
EndpointCategory.GRAPH_QUERY: (200, RateLimitPeriod.MINUTE), # 12k/hour possible
EndpointCategory.GRAPH_IMPORT: (50, RateLimitPeriod.MINUTE), # 3k/hour possible
# Table operations (generous burst limits)
EndpointCategory.TABLE_QUERY: (60, RateLimitPeriod.MINUTE), # 3.6k/hour possible
EndpointCategory.TABLE_UPLOAD: (20, RateLimitPeriod.MINUTE), # 1.2k/hour possible
EndpointCategory.TABLE_MANAGEMENT: (
30,
# Graph-scoped — sized for m7g.large
EndpointCategory.GRAPH_READ: (120, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_WRITE: (30, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_ANALYTICS: (15, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_BACKUP: (5, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_SYNC: (10, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_MCP: (30, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_AGENT: (15, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_SEARCH: (
10,
RateLimitPeriod.MINUTE,
), # 1.8k/hour possible
), # Shared OpenSearch t3.medium
EndpointCategory.GRAPH_QUERY: (60, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_IMPORT: (10, RateLimitPeriod.MINUTE),
# Table operations
EndpointCategory.TABLE_QUERY: (30, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_UPLOAD: (10, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_MANAGEMENT: (15, RateLimitPeriod.MINUTE),
},
# ladybug-large: r7g.large (16GB, 2 vCPU)
# Same base values as standard — graph.yml api_rate_multiplier (1.5x) handles scaling
"ladybug-large": {
# Non-graph endpoints - very high burst limits
EndpointCategory.AUTH: (50, RateLimitPeriod.MINUTE),
EndpointCategory.USER_MANAGEMENT: (1000, RateLimitPeriod.MINUTE),
EndpointCategory.TASKS: (1000, RateLimitPeriod.MINUTE),
EndpointCategory.STATUS: (3000, RateLimitPeriod.MINUTE),
EndpointCategory.SSE: (
30,
RateLimitPeriod.MINUTE,
), # More SSE connections for large tier
EndpointCategory.AUTH: (20, RateLimitPeriod.MINUTE),
EndpointCategory.USER_MANAGEMENT: (60, RateLimitPeriod.MINUTE),
EndpointCategory.TASKS: (60, RateLimitPeriod.MINUTE),
EndpointCategory.STATUS: (120, RateLimitPeriod.MINUTE),
EndpointCategory.SSE: (5, RateLimitPeriod.MINUTE),
EndpointCategory.BILLING: (60, RateLimitPeriod.MINUTE), # Never block payments
# Graph-scoped endpoints - VERY HIGH BURST LIMITS
EndpointCategory.GRAPH_READ: (2000, RateLimitPeriod.MINUTE), # 120k/hour possible
EndpointCategory.GRAPH_WRITE: (500, RateLimitPeriod.MINUTE), # 30k/hour possible
EndpointCategory.GRAPH_ANALYTICS: (
200,
RateLimitPeriod.MINUTE,
), # 12k/hour possible
EndpointCategory.GRAPH_BACKUP: (50, RateLimitPeriod.MINUTE), # 3k/hour possible
EndpointCategory.GRAPH_SYNC: (500, RateLimitPeriod.MINUTE), # 30k/hour possible
EndpointCategory.GRAPH_MCP: (500, RateLimitPeriod.MINUTE), # 30k/hour possible
EndpointCategory.GRAPH_AGENT: (200, RateLimitPeriod.MINUTE), # 12k/hour possible
EndpointCategory.GRAPH_QUERY: (1000, RateLimitPeriod.MINUTE), # 60k/hour possible
EndpointCategory.GRAPH_IMPORT: (200, RateLimitPeriod.MINUTE), # 12k/hour possible
# Table operations - large tier (very high burst limits)
EndpointCategory.TABLE_QUERY: (300, RateLimitPeriod.MINUTE), # 18k/hour possible
EndpointCategory.TABLE_UPLOAD: (100, RateLimitPeriod.MINUTE), # 6k/hour possible
EndpointCategory.TABLE_MANAGEMENT: (
150,
# Graph-scoped — same base, multiplied by 1.5x from graph.yml
EndpointCategory.GRAPH_READ: (120, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_WRITE: (30, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_ANALYTICS: (15, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_BACKUP: (5, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_SYNC: (10, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_MCP: (30, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_AGENT: (15, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_SEARCH: (
10,
RateLimitPeriod.MINUTE,
), # 9k/hour possible
), # Shared OpenSearch t3.medium
EndpointCategory.GRAPH_QUERY: (60, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_IMPORT: (10, RateLimitPeriod.MINUTE),
# Table operations
EndpointCategory.TABLE_QUERY: (30, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_UPLOAD: (10, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_MANAGEMENT: (15, RateLimitPeriod.MINUTE),
},
# ladybug-xlarge: r7g.xlarge (32GB, 4 vCPU)
# Same base values as standard — graph.yml api_rate_multiplier (2.5x) handles scaling
"ladybug-xlarge": {
# XLarge tier gets extreme burst limits - essentially unlimited
# Only safety limits to prevent complete system abuse
EndpointCategory.AUTH: (100, RateLimitPeriod.MINUTE),
EndpointCategory.USER_MANAGEMENT: (3000, RateLimitPeriod.MINUTE),
EndpointCategory.TASKS: (5000, RateLimitPeriod.MINUTE),
EndpointCategory.STATUS: (10000, RateLimitPeriod.MINUTE),
EndpointCategory.SSE: (
100,
RateLimitPeriod.MINUTE,
), # Generous SSE connections for xlarge tier
EndpointCategory.AUTH: (20, RateLimitPeriod.MINUTE),
EndpointCategory.USER_MANAGEMENT: (60, RateLimitPeriod.MINUTE),
EndpointCategory.TASKS: (60, RateLimitPeriod.MINUTE),
EndpointCategory.STATUS: (120, RateLimitPeriod.MINUTE),
EndpointCategory.SSE: (5, RateLimitPeriod.MINUTE),
EndpointCategory.BILLING: (60, RateLimitPeriod.MINUTE), # Never block payments
# Graph-scoped endpoints - EXTREME BURST LIMITS
EndpointCategory.GRAPH_READ: (
10000,
RateLimitPeriod.MINUTE,
), # 600k/hour possible
EndpointCategory.GRAPH_WRITE: (
5000,
RateLimitPeriod.MINUTE,
), # 300k/hour possible
EndpointCategory.GRAPH_ANALYTICS: (
2000,
RateLimitPeriod.MINUTE,
), # 120k/hour possible
EndpointCategory.GRAPH_BACKUP: (200, RateLimitPeriod.MINUTE), # 12k/hour possible
EndpointCategory.GRAPH_SYNC: (2000, RateLimitPeriod.MINUTE), # 120k/hour possible
EndpointCategory.GRAPH_MCP: (5000, RateLimitPeriod.MINUTE), # 300k/hour possible
EndpointCategory.GRAPH_AGENT: (
2000,
RateLimitPeriod.MINUTE,
), # 120k/hour possible
EndpointCategory.GRAPH_QUERY: (
10000,
RateLimitPeriod.MINUTE,
), # 600k/hour possible
EndpointCategory.GRAPH_IMPORT: (
1000,
RateLimitPeriod.MINUTE,
), # 60k/hour possible
# Table operations - xlarge tier (extreme burst limits)
EndpointCategory.TABLE_QUERY: (
1000,
RateLimitPeriod.MINUTE,
), # 60k/hour possible
EndpointCategory.TABLE_UPLOAD: (
500,
RateLimitPeriod.MINUTE,
), # 30k/hour possible
EndpointCategory.TABLE_MANAGEMENT: (
500,
# Graph-scoped — same base, multiplied by 2.5x from graph.yml
EndpointCategory.GRAPH_READ: (120, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_WRITE: (30, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_ANALYTICS: (15, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_BACKUP: (5, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_SYNC: (10, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_MCP: (30, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_AGENT: (15, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_SEARCH: (
10,
RateLimitPeriod.MINUTE,
), # 30k/hour possible
), # Shared OpenSearch t3.medium
EndpointCategory.GRAPH_QUERY: (60, RateLimitPeriod.MINUTE),
EndpointCategory.GRAPH_IMPORT: (10, RateLimitPeriod.MINUTE),
# Table operations
EndpointCategory.TABLE_QUERY: (30, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_UPLOAD: (10, RateLimitPeriod.MINUTE),
EndpointCategory.TABLE_MANAGEMENT: (15, RateLimitPeriod.MINUTE),
},
}

Expand All @@ -246,8 +216,8 @@ def get_rate_limit(
"""
tier_limits = cls.SUBSCRIPTION_RATE_LIMITS.get(tier)
if not tier_limits:
# Default to free tier if unknown
tier_limits = cls.SUBSCRIPTION_RATE_LIMITS["free"]
# Default to base tier if unknown
tier_limits = cls.SUBSCRIPTION_RATE_LIMITS["base"]

limit_config = tier_limits.get(category)
if not limit_config:
Expand Down Expand Up @@ -353,6 +323,10 @@ def get_endpoint_category(
elif endpoint_type == "agent":
return EndpointCategory.GRAPH_AGENT

# Search operations (OpenSearch - shared resource)
elif endpoint_type == "search":
return EndpointCategory.GRAPH_SEARCH

# Backup operations
elif endpoint_type == "graph" and "backup" in path:
return EndpointCategory.GRAPH_BACKUP
Expand Down
8 changes: 4 additions & 4 deletions robosystems/middleware/rate_limits/rate_limiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,8 +472,8 @@ def subscription_aware_rate_limit_dependency(request: Request):
# Get user identification
user_id = get_user_from_request(request)
if not user_id:
# Anonymous users get free tier limits
subscription_tier = "free"
# Anonymous users get base tier limits
subscription_tier = "base"
identifier = f"anon_sub:{request.client.host if request.client else 'unknown'}"
else:
# All authenticated users get ladybug-standard tier rate limits
Expand Down Expand Up @@ -537,7 +537,7 @@ def subscription_aware_rate_limit_dependency(request: Request):

# Provide helpful error message
upgrade_msg = ""
if subscription_tier in ["free", "starter"]:
if subscription_tier == "base":
upgrade_msg = " Upgrade your subscription for higher limits."

raise HTTPException(
Expand Down Expand Up @@ -583,7 +583,7 @@ def sse_connection_rate_limit_dependency(request: Request):
# Determine subscription tier
# For now, all authenticated users get ladybug-standard tier
# In the future, this could check actual subscription status
subscription_tier = "ladybug-standard" if user_id else "free"
subscription_tier = "ladybug-standard" if user_id else "base"

# Get rate limit for SSE based on subscription tier
rate_limit = RateLimitConfig.get_rate_limit(subscription_tier, EndpointCategory.SSE)
Expand Down
11 changes: 9 additions & 2 deletions robosystems/middleware/rate_limits/repository_rate_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class AllowedSharedEndpoints(str, Enum):
QUERY = "query" # Direct Cypher queries
MCP = "mcp" # MCP tool access
AGENT = "agent" # AI agent operations
SEARCH = "search" # Full-text search (OpenSearch)
SCHEMA = "schema" # Schema inspection
STATUS = "status" # Status checks

Expand Down Expand Up @@ -195,7 +196,12 @@ async def _check_repository_limit(
return {"allowed": False, "message": "No access to repository"}

# Map operation to limit keys
operation_keys = {"query": "queries", "mcp": "mcp_queries", "agent": "agent_calls"}
operation_keys = {
"query": "queries",
"mcp": "mcp_queries",
"agent": "agent_calls",
"search": "searches",
}

base_key = operation_keys.get(operation, "queries")

Expand Down Expand Up @@ -273,6 +279,7 @@ def _operation_to_category(self, operation: str) -> EndpointCategory:
"query": EndpointCategory.GRAPH_QUERY,
"mcp": EndpointCategory.GRAPH_MCP,
"agent": EndpointCategory.GRAPH_AGENT,
"search": EndpointCategory.GRAPH_SEARCH,
}
return mapping.get(operation, EndpointCategory.GRAPH_READ)

Expand All @@ -286,7 +293,7 @@ async def get_usage_stats(self, user_id: str, repository: str, plan: str) -> dic
stats = {}

# Get current usage for each operation type
for operation in ["query", "mcp", "agent"]:
for operation in ["query", "mcp", "agent", "search"]:
operation_stats = {}

# Check each time window
Expand Down
Loading
Loading