mirror of https://github.com/kortix-ai/suna.git
limit agent creation based on tiers
This commit is contained in:
parent
f1f8f82b07
commit
c786e190b6
|
@ -1767,7 +1767,6 @@ async def import_agent_from_json(
|
||||||
request: JsonImportRequestModel,
|
request: JsonImportRequestModel,
|
||||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||||
):
|
):
|
||||||
"""Import an agent from JSON with credential mappings"""
|
|
||||||
logger.info(f"Importing agent from JSON - user: {user_id}")
|
logger.info(f"Importing agent from JSON - user: {user_id}")
|
||||||
|
|
||||||
if not await is_enabled("custom_agents"):
|
if not await is_enabled("custom_agents"):
|
||||||
|
@ -1776,6 +1775,21 @@ async def import_agent_from_json(
|
||||||
detail="Custom agents currently disabled. This feature is not available at the moment."
|
detail="Custom agents currently disabled. This feature is not available at the moment."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
client = await db.client
|
||||||
|
from .utils import check_agent_count_limit
|
||||||
|
limit_check = await check_agent_count_limit(client, user_id)
|
||||||
|
|
||||||
|
if not limit_check['can_create']:
|
||||||
|
error_detail = {
|
||||||
|
"message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.",
|
||||||
|
"current_count": limit_check['current_count'],
|
||||||
|
"limit": limit_check['limit'],
|
||||||
|
"tier_name": limit_check['tier_name'],
|
||||||
|
"error_code": "AGENT_LIMIT_EXCEEDED"
|
||||||
|
}
|
||||||
|
logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents")
|
||||||
|
raise HTTPException(status_code=402, detail=error_detail)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from agent.json_import_service import JsonImportService, JsonImportRequest
|
from agent.json_import_service import JsonImportService, JsonImportRequest
|
||||||
import_service = JsonImportService(db)
|
import_service = JsonImportService(db)
|
||||||
|
@ -1817,6 +1831,20 @@ async def create_agent(
|
||||||
)
|
)
|
||||||
client = await db.client
|
client = await db.client
|
||||||
|
|
||||||
|
from .utils import check_agent_count_limit
|
||||||
|
limit_check = await check_agent_count_limit(client, user_id)
|
||||||
|
|
||||||
|
if not limit_check['can_create']:
|
||||||
|
error_detail = {
|
||||||
|
"message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.",
|
||||||
|
"current_count": limit_check['current_count'],
|
||||||
|
"limit": limit_check['limit'],
|
||||||
|
"tier_name": limit_check['tier_name'],
|
||||||
|
"error_code": "AGENT_LIMIT_EXCEEDED"
|
||||||
|
}
|
||||||
|
logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents")
|
||||||
|
raise HTTPException(status_code=402, detail=error_detail)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if agent_data.is_default:
|
if agent_data.is_default:
|
||||||
await client.table('agents').update({"is_default": False}).eq("account_id", user_id).eq("is_default", True).execute()
|
await client.table('agents').update({"is_default": False}).eq("account_id", user_id).eq("is_default", True).execute()
|
||||||
|
@ -1874,6 +1902,10 @@ async def create_agent(
|
||||||
await client.table('agents').delete().eq('agent_id', agent['agent_id']).execute()
|
await client.table('agents').delete().eq('agent_id', agent['agent_id']).execute()
|
||||||
raise HTTPException(status_code=500, detail="Failed to create initial version")
|
raise HTTPException(status_code=500, detail="Failed to create initial version")
|
||||||
|
|
||||||
|
# Invalidate agent count cache after successful creation
|
||||||
|
from utils.cache import Cache
|
||||||
|
await Cache.invalidate(f"agent_count_limit:{user_id}")
|
||||||
|
|
||||||
logger.info(f"Created agent {agent['agent_id']} with v1 for user: {user_id}")
|
logger.info(f"Created agent {agent['agent_id']} with v1 for user: {user_id}")
|
||||||
return AgentResponse(
|
return AgentResponse(
|
||||||
agent_id=agent['agent_id'],
|
agent_id=agent['agent_id'],
|
||||||
|
@ -2275,7 +2307,20 @@ async def delete_agent(agent_id: str, user_id: str = Depends(get_current_user_id
|
||||||
if agent['is_default']:
|
if agent['is_default']:
|
||||||
raise HTTPException(status_code=400, detail="Cannot delete default agent")
|
raise HTTPException(status_code=400, detail="Cannot delete default agent")
|
||||||
|
|
||||||
await client.table('agents').delete().eq('agent_id', agent_id).execute()
|
if agent.get('metadata', {}).get('is_suna_default', False):
|
||||||
|
raise HTTPException(status_code=400, detail="Cannot delete Suna default agent")
|
||||||
|
|
||||||
|
delete_result = await client.table('agents').delete().eq('agent_id', agent_id).execute()
|
||||||
|
|
||||||
|
if not delete_result.data:
|
||||||
|
logger.warning(f"No agent was deleted for agent_id: {agent_id}, user_id: {user_id}")
|
||||||
|
raise HTTPException(status_code=403, detail="Unable to delete agent - permission denied or agent not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from utils.cache import Cache
|
||||||
|
await Cache.invalidate(f"agent_count_limit:{user_id}")
|
||||||
|
except Exception as cache_error:
|
||||||
|
logger.warning(f"Cache invalidation failed for user {user_id}: {str(cache_error)}")
|
||||||
|
|
||||||
logger.info(f"Successfully deleted agent: {agent_id}")
|
logger.info(f"Successfully deleted agent: {agent_id}")
|
||||||
return {"message": "Agent deleted successfully"}
|
return {"message": "Agent deleted successfully"}
|
||||||
|
|
|
@ -114,6 +114,9 @@ class JsonImportService:
|
||||||
request.custom_system_prompt or json_data.get('system_prompt', '')
|
request.custom_system_prompt or json_data.get('system_prompt', '')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from utils.cache import Cache
|
||||||
|
await Cache.invalidate(f"agent_count_limit:{request.account_id}")
|
||||||
|
|
||||||
logger.info(f"Successfully imported agent {agent_id} from JSON")
|
logger.info(f"Successfully imported agent {agent_id} from JSON")
|
||||||
|
|
||||||
return JsonImportResult(
|
return JsonImportResult(
|
||||||
|
|
|
@ -138,3 +138,63 @@ async def check_agent_run_limit(client, account_id: str) -> Dict[str, Any]:
|
||||||
'running_count': 0,
|
'running_count': 0,
|
||||||
'running_thread_ids': []
|
'running_thread_ids': []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def check_agent_count_limit(client, account_id: str) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
result = await Cache.get(f"agent_count_limit:{account_id}")
|
||||||
|
if result:
|
||||||
|
logger.debug(f"Cache hit for agent count limit: {account_id}")
|
||||||
|
return result
|
||||||
|
except Exception as cache_error:
|
||||||
|
logger.warning(f"Cache read failed for agent count limit {account_id}: {str(cache_error)}")
|
||||||
|
|
||||||
|
agents_result = await client.table('agents').select('agent_id, metadata').eq('account_id', account_id).execute()
|
||||||
|
|
||||||
|
non_suna_agents = []
|
||||||
|
for agent in agents_result.data or []:
|
||||||
|
metadata = agent.get('metadata', {}) or {}
|
||||||
|
is_suna_default = metadata.get('is_suna_default', False)
|
||||||
|
if not is_suna_default:
|
||||||
|
non_suna_agents.append(agent)
|
||||||
|
|
||||||
|
current_count = len(non_suna_agents)
|
||||||
|
logger.debug(f"Account {account_id} has {current_count} custom agents (excluding Suna defaults)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from services.billing import get_subscription_tier
|
||||||
|
tier_name = await get_subscription_tier(client, account_id)
|
||||||
|
logger.debug(f"Account {account_id} subscription tier: {tier_name}")
|
||||||
|
except Exception as billing_error:
|
||||||
|
logger.warning(f"Could not get subscription tier for {account_id}: {str(billing_error)}, defaulting to free")
|
||||||
|
tier_name = 'free'
|
||||||
|
|
||||||
|
agent_limit = config.AGENT_LIMITS.get(tier_name, config.AGENT_LIMITS['free'])
|
||||||
|
|
||||||
|
can_create = current_count < agent_limit
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'can_create': can_create,
|
||||||
|
'current_count': current_count,
|
||||||
|
'limit': agent_limit,
|
||||||
|
'tier_name': tier_name
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
await Cache.set(f"agent_count_limit:{account_id}", result, ttl=300)
|
||||||
|
except Exception as cache_error:
|
||||||
|
logger.warning(f"Cache write failed for agent count limit {account_id}: {str(cache_error)}")
|
||||||
|
|
||||||
|
logger.info(f"Account {account_id} has {current_count}/{agent_limit} agents (tier: {tier_name}) - can_create: {can_create}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking agent count limit for account {account_id}: {str(e)}", exc_info=True)
|
||||||
|
return {
|
||||||
|
'can_create': True,
|
||||||
|
'current_count': 0,
|
||||||
|
'limit': config.AGENT_LIMITS['free'],
|
||||||
|
'tier_name': 'free'
|
||||||
|
}
|
||||||
|
|
|
@ -592,6 +592,30 @@ async def can_use_model(client, user_id: str, model_name: str):
|
||||||
|
|
||||||
return False, f"Your current subscription plan does not include access to {model_name}. Please upgrade your subscription or choose from your available models: {', '.join(allowed_models)}", allowed_models
|
return False, f"Your current subscription plan does not include access to {model_name}. Please upgrade your subscription or choose from your available models: {', '.join(allowed_models)}", allowed_models
|
||||||
|
|
||||||
|
async def get_subscription_tier(client, user_id: str) -> str:
|
||||||
|
try:
|
||||||
|
subscription = await get_user_subscription(user_id)
|
||||||
|
|
||||||
|
if not subscription:
|
||||||
|
return 'free'
|
||||||
|
|
||||||
|
price_id = None
|
||||||
|
if subscription.get('items') and subscription['items'].get('data') and len(subscription['items']['data']) > 0:
|
||||||
|
price_id = subscription['items']['data'][0]['price']['id']
|
||||||
|
else:
|
||||||
|
price_id = subscription.get('price_id', config.STRIPE_FREE_TIER_ID)
|
||||||
|
|
||||||
|
tier_info = SUBSCRIPTION_TIERS.get(price_id)
|
||||||
|
if tier_info:
|
||||||
|
return tier_info['name']
|
||||||
|
|
||||||
|
logger.warning(f"Unknown price_id {price_id} for user {user_id}, defaulting to free tier")
|
||||||
|
return 'free'
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting subscription tier for user {user_id}: {str(e)}")
|
||||||
|
return 'free'
|
||||||
|
|
||||||
async def check_billing_status(client, user_id: str) -> Tuple[bool, str, Optional[Dict]]:
|
async def check_billing_status(client, user_id: str) -> Tuple[bool, str, Optional[Dict]]:
|
||||||
"""
|
"""
|
||||||
Check if a user can run agents based on their subscription and usage.
|
Check if a user can run agents based on their subscription and usage.
|
||||||
|
@ -634,8 +658,6 @@ async def check_billing_status(client, user_id: str) -> Tuple[bool, str, Optiona
|
||||||
# Calculate current month's usage
|
# Calculate current month's usage
|
||||||
current_usage = await calculate_monthly_usage(client, user_id)
|
current_usage = await calculate_monthly_usage(client, user_id)
|
||||||
|
|
||||||
# TODO: also do user's AAL check
|
|
||||||
# Check if within limits
|
|
||||||
if current_usage >= tier_info['cost']:
|
if current_usage >= tier_info['cost']:
|
||||||
return False, f"Monthly limit of {tier_info['cost']} dollars reached. Please upgrade your plan or wait until next month.", subscription
|
return False, f"Monthly limit of {tier_info['cost']} dollars reached. Please upgrade your plan or wait until next month.", subscription
|
||||||
|
|
||||||
|
|
|
@ -325,15 +325,22 @@ async def install_template(
|
||||||
request: InstallTemplateRequest,
|
request: InstallTemplateRequest,
|
||||||
user_id: str = Depends(get_current_user_id_from_jwt)
|
user_id: str = Depends(get_current_user_id_from_jwt)
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
Install a template as a new agent instance.
|
|
||||||
|
|
||||||
Requires:
|
|
||||||
- User must have access to the template (own it or it's public)
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# Validate template access first
|
await validate_template_access_and_get(request.template_id, user_id)
|
||||||
template = await validate_template_access_and_get(request.template_id, user_id)
|
client = await db.client
|
||||||
|
from agent.utils import check_agent_count_limit
|
||||||
|
limit_check = await check_agent_count_limit(client, user_id)
|
||||||
|
|
||||||
|
if not limit_check['can_create']:
|
||||||
|
error_detail = {
|
||||||
|
"message": f"Maximum of {limit_check['limit']} agents allowed for your current plan. You have {limit_check['current_count']} agents.",
|
||||||
|
"current_count": limit_check['current_count'],
|
||||||
|
"limit": limit_check['limit'],
|
||||||
|
"tier_name": limit_check['tier_name'],
|
||||||
|
"error_code": "AGENT_LIMIT_EXCEEDED"
|
||||||
|
}
|
||||||
|
logger.warning(f"Agent limit exceeded for account {user_id}: {limit_check['current_count']}/{limit_check['limit']} agents")
|
||||||
|
raise HTTPException(status_code=402, detail=error_detail)
|
||||||
|
|
||||||
logger.info(f"User {user_id} installing template {request.template_id}")
|
logger.info(f"User {user_id} installing template {request.template_id}")
|
||||||
|
|
||||||
|
@ -362,7 +369,6 @@ async def install_template(
|
||||||
)
|
)
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
# Re-raise HTTP exceptions from our validation functions
|
|
||||||
raise
|
raise
|
||||||
except TemplateInstallationError as e:
|
except TemplateInstallationError as e:
|
||||||
logger.warning(f"Template installation failed: {e}")
|
logger.warning(f"Template installation failed: {e}")
|
||||||
|
|
|
@ -107,6 +107,9 @@ class InstallationService:
|
||||||
|
|
||||||
await self._increment_download_count(template.template_id)
|
await self._increment_download_count(template.template_id)
|
||||||
|
|
||||||
|
from utils.cache import Cache
|
||||||
|
await Cache.invalidate(f"agent_count_limit:{request.account_id}")
|
||||||
|
|
||||||
agent_name = request.instance_name or f"{template.name} (from marketplace)"
|
agent_name = request.instance_name or f"{template.name} (from marketplace)"
|
||||||
logger.info(f"Successfully installed template {template.template_id} as agent {agent_id}")
|
logger.info(f"Successfully installed template {template.template_id} as agent {agent_id}")
|
||||||
|
|
||||||
|
|
|
@ -270,6 +270,30 @@ class Configuration:
|
||||||
|
|
||||||
# Agent execution limits (can be overridden via environment variable)
|
# Agent execution limits (can be overridden via environment variable)
|
||||||
_MAX_PARALLEL_AGENT_RUNS_ENV: Optional[str] = None
|
_MAX_PARALLEL_AGENT_RUNS_ENV: Optional[str] = None
|
||||||
|
|
||||||
|
# Agent limits per billing tier
|
||||||
|
AGENT_LIMITS = {
|
||||||
|
'free': 2,
|
||||||
|
'tier_2_20': 5,
|
||||||
|
'tier_6_50': 20,
|
||||||
|
'tier_12_100': 20,
|
||||||
|
'tier_25_200': 100,
|
||||||
|
'tier_50_400': 100,
|
||||||
|
'tier_125_800': 100,
|
||||||
|
'tier_200_1000': 100,
|
||||||
|
# Yearly plans have same limits as monthly
|
||||||
|
'tier_2_20_yearly': 5,
|
||||||
|
'tier_6_50_yearly': 20,
|
||||||
|
'tier_12_100_yearly': 20,
|
||||||
|
'tier_25_200_yearly': 100,
|
||||||
|
'tier_50_400_yearly': 100,
|
||||||
|
'tier_125_800_yearly': 100,
|
||||||
|
'tier_200_1000_yearly': 100,
|
||||||
|
# Yearly commitment plans
|
||||||
|
'tier_2_17_yearly_commitment': 5,
|
||||||
|
'tier_6_42_yearly_commitment': 20,
|
||||||
|
'tier_25_170_yearly_commitment': 100,
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def MAX_PARALLEL_AGENT_RUNS(self) -> int:
|
def MAX_PARALLEL_AGENT_RUNS(self) -> int:
|
||||||
|
|
|
@ -23,6 +23,8 @@ import { PublishDialog } from '@/components/agents/custom-agents-page/publish-di
|
||||||
import { LoadingSkeleton } from '@/components/agents/custom-agents-page/loading-skeleton';
|
import { LoadingSkeleton } from '@/components/agents/custom-agents-page/loading-skeleton';
|
||||||
import { NewAgentDialog } from '@/components/agents/new-agent-dialog';
|
import { NewAgentDialog } from '@/components/agents/new-agent-dialog';
|
||||||
import { MarketplaceAgentPreviewDialog } from '@/components/agents/marketplace-agent-preview-dialog';
|
import { MarketplaceAgentPreviewDialog } from '@/components/agents/marketplace-agent-preview-dialog';
|
||||||
|
import { AgentCountLimitDialog } from '@/components/agents/agent-count-limit-dialog';
|
||||||
|
import { AgentCountLimitError } from '@/lib/api';
|
||||||
|
|
||||||
type ViewMode = 'grid' | 'list';
|
type ViewMode = 'grid' | 'list';
|
||||||
type AgentSortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count';
|
type AgentSortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count';
|
||||||
|
@ -86,6 +88,8 @@ export default function AgentsPage() {
|
||||||
|
|
||||||
const [publishingAgentId, setPublishingAgentId] = useState<string | null>(null);
|
const [publishingAgentId, setPublishingAgentId] = useState<string | null>(null);
|
||||||
const [showNewAgentDialog, setShowNewAgentDialog] = useState(false);
|
const [showNewAgentDialog, setShowNewAgentDialog] = useState(false);
|
||||||
|
const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false);
|
||||||
|
const [agentLimitError, setAgentLimitError] = useState<AgentCountLimitError | null>(null);
|
||||||
|
|
||||||
const activeTab = useMemo(() => {
|
const activeTab = useMemo(() => {
|
||||||
return searchParams.get('tab') || 'my-agents';
|
return searchParams.get('tab') || 'my-agents';
|
||||||
|
@ -146,7 +150,6 @@ export default function AgentsPage() {
|
||||||
|
|
||||||
if (marketplaceTemplates) {
|
if (marketplaceTemplates) {
|
||||||
marketplaceTemplates.forEach(template => {
|
marketplaceTemplates.forEach(template => {
|
||||||
|
|
||||||
const item: MarketplaceTemplate = {
|
const item: MarketplaceTemplate = {
|
||||||
id: template.template_id,
|
id: template.template_id,
|
||||||
creator_id: template.creator_id,
|
creator_id: template.creator_id,
|
||||||
|
@ -174,14 +177,12 @@ export default function AgentsPage() {
|
||||||
item.creator_name?.toLowerCase().includes(searchLower);
|
item.creator_name?.toLowerCase().includes(searchLower);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (!matchesSearch) return; // Skip items that don't match search
|
if (!matchesSearch) return;
|
||||||
|
|
||||||
// Always add user's own templates to mineItems for the "mine" filter
|
|
||||||
if (user?.id === template.creator_id) {
|
if (user?.id === template.creator_id) {
|
||||||
mineItems.push(item);
|
mineItems.push(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categorize all templates (including user's own) for the "all" view
|
|
||||||
if (template.is_kortix_team) {
|
if (template.is_kortix_team) {
|
||||||
kortixItems.push(item);
|
kortixItems.push(item);
|
||||||
} else {
|
} else {
|
||||||
|
@ -392,6 +393,12 @@ export default function AgentsPage() {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Installation error:', error);
|
console.error('Installation error:', error);
|
||||||
|
|
||||||
|
if (error instanceof AgentCountLimitError) {
|
||||||
|
setAgentLimitError(error);
|
||||||
|
setShowAgentLimitDialog(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (error.message?.includes('already in your library')) {
|
if (error.message?.includes('already in your library')) {
|
||||||
toast.error('This agent is already in your library');
|
toast.error('This agent is already in your library');
|
||||||
} else if (error.message?.includes('Credential profile not found')) {
|
} else if (error.message?.includes('Credential profile not found')) {
|
||||||
|
@ -628,6 +635,15 @@ export default function AgentsPage() {
|
||||||
onInstall={handlePreviewInstall}
|
onInstall={handlePreviewInstall}
|
||||||
isInstalling={installingItemId === selectedItem?.id}
|
isInstalling={installingItemId === selectedItem?.id}
|
||||||
/>
|
/>
|
||||||
|
{agentLimitError && (
|
||||||
|
<AgentCountLimitDialog
|
||||||
|
open={showAgentLimitDialog}
|
||||||
|
onOpenChange={setShowAgentLimitDialog}
|
||||||
|
currentCount={agentLimitError.detail.current_count}
|
||||||
|
limit={agentLimitError.detail.limit}
|
||||||
|
tierName={agentLimitError.detail.tier_name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { PricingSection } from '@/components/home/sections/pricing-section';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
|
||||||
|
interface AgentCountLimitDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
currentCount: number;
|
||||||
|
limit: number;
|
||||||
|
tierName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AgentCountLimitDialog: React.FC<AgentCountLimitDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
currentCount,
|
||||||
|
limit,
|
||||||
|
tierName,
|
||||||
|
}) => {
|
||||||
|
const returnUrl = typeof window !== 'undefined' ? window.location.href : '/';
|
||||||
|
|
||||||
|
const getNextTierRecommendation = () => {
|
||||||
|
if (tierName === 'free') {
|
||||||
|
return {
|
||||||
|
name: 'Plus',
|
||||||
|
price: '$20/month',
|
||||||
|
agentLimit: 5,
|
||||||
|
};
|
||||||
|
} else if (tierName.includes('tier_2_20')) {
|
||||||
|
return {
|
||||||
|
name: 'Pro',
|
||||||
|
price: '$50/month',
|
||||||
|
agentLimit: 20,
|
||||||
|
};
|
||||||
|
} else if (tierName.includes('tier_6_50')) {
|
||||||
|
return {
|
||||||
|
name: 'Business',
|
||||||
|
price: '$200/month',
|
||||||
|
agentLimit: 100,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextTier = getNextTierRecommendation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="sm:max-w-5xl h-auto overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="border border-amber-300 dark:border-amber-900 flex h-8 w-8 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="text-lg font-semibold">
|
||||||
|
Agent Limit Reached
|
||||||
|
</DialogTitle>
|
||||||
|
</div>
|
||||||
|
<DialogDescription className="text-sm text-muted-foreground">
|
||||||
|
You've reached the maximum number of agents allowed on your current plan.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="[&_.grid]:!grid-cols-4 [&_.grid]:gap-3 mt-8">
|
||||||
|
<PricingSection
|
||||||
|
returnUrl={returnUrl}
|
||||||
|
showTitleAndTabs={false}
|
||||||
|
insideDialog={true}
|
||||||
|
showInfo={false}
|
||||||
|
noPadding={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
|
@ -11,6 +11,8 @@ import { ProfileConnector } from './installation/streamlined-profile-connector';
|
||||||
import { CustomServerStep } from './installation/custom-server-step';
|
import { CustomServerStep } from './installation/custom-server-step';
|
||||||
import type { SetupStep } from './installation/types';
|
import type { SetupStep } from './installation/types';
|
||||||
import { useAnalyzeJsonForImport, useImportAgentFromJson, type JsonAnalysisResult, type JsonImportResult } from '@/hooks/react-query/agents/use-json-import';
|
import { useAnalyzeJsonForImport, useImportAgentFromJson, type JsonAnalysisResult, type JsonImportResult } from '@/hooks/react-query/agents/use-json-import';
|
||||||
|
import { AgentCountLimitDialog } from './agent-count-limit-dialog';
|
||||||
|
import { AgentCountLimitError } from '@/lib/api';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface JsonImportDialogProps {
|
interface JsonImportDialogProps {
|
||||||
|
@ -34,6 +36,8 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
||||||
const [currentStep, setCurrentStep] = useState(0);
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
const [profileMappings, setProfileMappings] = useState<Record<string, string>>({});
|
const [profileMappings, setProfileMappings] = useState<Record<string, string>>({});
|
||||||
const [customMcpConfigs, setCustomMcpConfigs] = useState<Record<string, Record<string, any>>>({});
|
const [customMcpConfigs, setCustomMcpConfigs] = useState<Record<string, Record<string, any>>>({});
|
||||||
|
const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false);
|
||||||
|
const [agentLimitError, setAgentLimitError] = useState<AgentCountLimitError | null>(null);
|
||||||
|
|
||||||
const analyzeJsonMutation = useAnalyzeJsonForImport();
|
const analyzeJsonMutation = useAnalyzeJsonForImport();
|
||||||
const importJsonMutation = useImportAgentFromJson();
|
const importJsonMutation = useImportAgentFromJson();
|
||||||
|
@ -47,6 +51,8 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
||||||
setCurrentStep(0);
|
setCurrentStep(0);
|
||||||
setProfileMappings({});
|
setProfileMappings({});
|
||||||
setCustomMcpConfigs({});
|
setCustomMcpConfigs({});
|
||||||
|
setShowAgentLimitDialog(false);
|
||||||
|
setAgentLimitError(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -215,6 +221,13 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
||||||
}
|
}
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (error instanceof AgentCountLimitError) {
|
||||||
|
setAgentLimitError(error);
|
||||||
|
setShowAgentLimitDialog(true);
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -380,7 +393,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
||||||
Import Agent from JSON
|
Import Agent from JSON
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{analysis && !analysis.requires_setup && step === 'paste' && (
|
{analysis && !analysis.requires_setup && step === 'paste' && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<CheckCircle2 className="h-4 w-4" />
|
<CheckCircle2 className="h-4 w-4" />
|
||||||
|
@ -389,7 +401,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{analysis && analysis.requires_setup && step === 'paste' && (
|
{analysis && analysis.requires_setup && step === 'paste' && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
@ -400,7 +411,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{analyzeJsonMutation.isError && step === 'paste' && (
|
{analyzeJsonMutation.isError && step === 'paste' && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
@ -409,7 +419,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{importJsonMutation.isError && (step === 'setup' || step === 'importing') && (
|
{importJsonMutation.isError && (step === 'setup' || step === 'importing') && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
|
@ -418,11 +427,19 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{step === 'paste' && renderPasteStep()}
|
{step === 'paste' && renderPasteStep()}
|
||||||
{step === 'setup' && renderSetupStep()}
|
{step === 'setup' && renderSetupStep()}
|
||||||
{step === 'importing' && renderImportingStep()}
|
{step === 'importing' && renderImportingStep()}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
{agentLimitError && (
|
||||||
|
<AgentCountLimitDialog
|
||||||
|
open={showAgentLimitDialog}
|
||||||
|
onOpenChange={setShowAgentLimitDialog}
|
||||||
|
currentCount={agentLimitError.detail.current_count}
|
||||||
|
limit={agentLimitError.detail.limit}
|
||||||
|
tierName={agentLimitError.detail.tier_name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { Loader2, Plus, FileJson, Code } from 'lucide-react';
|
import { Loader2, Plus, FileJson, Code } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
|
@ -14,6 +14,8 @@ import {
|
||||||
} from '@/components/ui/alert-dialog';
|
} from '@/components/ui/alert-dialog';
|
||||||
import { useCreateNewAgent } from '@/hooks/react-query/agents/use-agents';
|
import { useCreateNewAgent } from '@/hooks/react-query/agents/use-agents';
|
||||||
import { JsonImportDialog } from './json-import-dialog';
|
import { JsonImportDialog } from './json-import-dialog';
|
||||||
|
import { AgentCountLimitDialog } from './agent-count-limit-dialog';
|
||||||
|
import { AgentCountLimitError } from '@/lib/api';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface NewAgentDialogProps {
|
interface NewAgentDialogProps {
|
||||||
|
@ -25,19 +27,43 @@ interface NewAgentDialogProps {
|
||||||
export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialogProps) {
|
export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialogProps) {
|
||||||
const [showJsonImport, setShowJsonImport] = useState(false);
|
const [showJsonImport, setShowJsonImport] = useState(false);
|
||||||
const [jsonImportText, setJsonImportText] = useState('');
|
const [jsonImportText, setJsonImportText] = useState('');
|
||||||
|
const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false);
|
||||||
|
const [agentLimitError, setAgentLimitError] = useState<AgentCountLimitError | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const createNewAgentMutation = useCreateNewAgent();
|
const createNewAgentMutation = useCreateNewAgent();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[DEBUG] NewAgentDialog state:', {
|
||||||
|
agentLimitError: !!agentLimitError,
|
||||||
|
showAgentLimitDialog,
|
||||||
|
errorDetail: agentLimitError?.detail
|
||||||
|
});
|
||||||
|
}, [agentLimitError, showAgentLimitDialog]);
|
||||||
|
|
||||||
const handleCreateNewAgent = () => {
|
const handleCreateNewAgent = () => {
|
||||||
|
console.log('[DEBUG] Creating new agent...');
|
||||||
createNewAgentMutation.mutate(undefined, {
|
createNewAgentMutation.mutate(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
console.log('[DEBUG] Agent created successfully');
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error) => {
|
||||||
// Keep dialog open on error so user can see the error and try again
|
console.log('[DEBUG] Error creating agent:', error);
|
||||||
// The useCreateNewAgent hook already shows error toasts
|
console.log('[DEBUG] Error type:', typeof error);
|
||||||
|
console.log('[DEBUG] Error constructor:', error.constructor.name);
|
||||||
|
console.log('[DEBUG] Is AgentCountLimitError?', error instanceof AgentCountLimitError);
|
||||||
|
|
||||||
|
if (error instanceof AgentCountLimitError) {
|
||||||
|
console.log('[DEBUG] Setting agent limit error state');
|
||||||
|
setAgentLimitError(error);
|
||||||
|
setShowAgentLimitDialog(true);
|
||||||
|
onOpenChange(false);
|
||||||
|
} else {
|
||||||
|
console.log('[DEBUG] Not an agent limit error, keeping dialog open');
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Failed to create agent');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -147,6 +173,16 @@ export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialog
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{agentLimitError && (
|
||||||
|
<AgentCountLimitDialog
|
||||||
|
open={showAgentLimitDialog}
|
||||||
|
onOpenChange={setShowAgentLimitDialog}
|
||||||
|
currentCount={agentLimitError.detail.current_count}
|
||||||
|
limit={agentLimitError.detail.limit}
|
||||||
|
tierName={agentLimitError.detail.tier_name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -566,7 +566,7 @@ function PricingTier({
|
||||||
variant={buttonVariant || 'default'}
|
variant={buttonVariant || 'default'}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full font-medium transition-all duration-200',
|
'w-full font-medium transition-all duration-200',
|
||||||
isCompact || insideDialog ? 'h-8 rounded-md text-xs' : 'h-10 rounded-full text-sm',
|
isCompact || insideDialog ? 'h-8 text-xs' : 'h-10 rounded-full text-sm',
|
||||||
buttonClassName,
|
buttonClassName,
|
||||||
isPlanLoading && 'animate-pulse',
|
isPlanLoading && 'animate-pulse',
|
||||||
)}
|
)}
|
||||||
|
@ -584,13 +584,17 @@ interface PricingSectionProps {
|
||||||
showTitleAndTabs?: boolean;
|
showTitleAndTabs?: boolean;
|
||||||
hideFree?: boolean;
|
hideFree?: boolean;
|
||||||
insideDialog?: boolean;
|
insideDialog?: boolean;
|
||||||
|
showInfo?: boolean;
|
||||||
|
noPadding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PricingSection({
|
export function PricingSection({
|
||||||
returnUrl = typeof window !== 'undefined' ? window.location.href : '/',
|
returnUrl = typeof window !== 'undefined' ? window.location.href : '/',
|
||||||
showTitleAndTabs = true,
|
showTitleAndTabs = true,
|
||||||
hideFree = false,
|
hideFree = false,
|
||||||
insideDialog = false
|
insideDialog = false,
|
||||||
|
showInfo = true,
|
||||||
|
noPadding = false,
|
||||||
}: PricingSectionProps) {
|
}: PricingSectionProps) {
|
||||||
const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>(
|
const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>(
|
||||||
'cloud',
|
'cloud',
|
||||||
|
@ -598,18 +602,14 @@ export function PricingSection({
|
||||||
const { data: subscriptionData, isLoading: isFetchingPlan, error: subscriptionQueryError, refetch: refetchSubscription } = useSubscription();
|
const { data: subscriptionData, isLoading: isFetchingPlan, error: subscriptionQueryError, refetch: refetchSubscription } = useSubscription();
|
||||||
const subCommitmentQuery = useSubscriptionCommitment(subscriptionData?.subscription_id);
|
const subCommitmentQuery = useSubscriptionCommitment(subscriptionData?.subscription_id);
|
||||||
|
|
||||||
// Derive authentication and subscription status from the hook data
|
|
||||||
const isAuthenticated = !!subscriptionData && subscriptionQueryError === null;
|
const isAuthenticated = !!subscriptionData && subscriptionQueryError === null;
|
||||||
const currentSubscription = subscriptionData || null;
|
const currentSubscription = subscriptionData || null;
|
||||||
|
|
||||||
// Determine default billing period based on user's current subscription
|
|
||||||
const getDefaultBillingPeriod = useCallback((): 'monthly' | 'yearly' | 'yearly_commitment' => {
|
const getDefaultBillingPeriod = useCallback((): 'monthly' | 'yearly' | 'yearly_commitment' => {
|
||||||
if (!isAuthenticated || !currentSubscription) {
|
if (!isAuthenticated || !currentSubscription) {
|
||||||
// Default to yearly_commitment for non-authenticated users (the new yearly plans)
|
|
||||||
return 'yearly_commitment';
|
return 'yearly_commitment';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find current tier to determine if user is on monthly, yearly, or yearly commitment plan
|
|
||||||
const currentTier = siteConfig.cloudPricingItems.find(
|
const currentTier = siteConfig.cloudPricingItems.find(
|
||||||
(p) => p.stripePriceId === currentSubscription.price_id ||
|
(p) => p.stripePriceId === currentSubscription.price_id ||
|
||||||
p.yearlyStripePriceId === currentSubscription.price_id ||
|
p.yearlyStripePriceId === currentSubscription.price_id ||
|
||||||
|
@ -685,7 +685,7 @@ export function PricingSection({
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="pricing"
|
id="pricing"
|
||||||
className={cn("flex flex-col items-center justify-center gap-10 w-full relative pb-12")}
|
className={cn("flex flex-col items-center justify-center gap-10 w-full relative", noPadding ? "pb-0" : "pb-12")}
|
||||||
>
|
>
|
||||||
{showTitleAndTabs && (
|
{showTitleAndTabs && (
|
||||||
<>
|
<>
|
||||||
|
@ -747,14 +747,15 @@ export function PricingSection({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg max-w-2xl mx-auto">
|
{showInfo && (
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-200 text-center">
|
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg max-w-2xl mx-auto">
|
||||||
<strong>What are AI tokens?</strong> Tokens are units of text that AI models process.
|
<p className="text-sm text-blue-800 dark:text-blue-200 text-center">
|
||||||
Your plan includes credits to spend on various AI models - the more complex the task,
|
<strong>What are AI tokens?</strong> Tokens are units of text that AI models process.
|
||||||
the more tokens used.
|
Your plan includes credits to spend on various AI models - the more complex the task,
|
||||||
</p>
|
the more tokens used.
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
|
@ -42,6 +42,14 @@ export const useCreateAgent = () => {
|
||||||
queryClient.setQueryData(agentKeys.detail(data.agent_id), data);
|
queryClient.setQueryData(agentKeys.detail(data.agent_id), data);
|
||||||
toast.success('Agent created successfully');
|
toast.success('Agent created successfully');
|
||||||
},
|
},
|
||||||
|
onError: async (error) => {
|
||||||
|
const { AgentCountLimitError } = await import('@/lib/api');
|
||||||
|
if (error instanceof AgentCountLimitError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error('Error creating agent:', error);
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Failed to create agent');
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)();
|
)();
|
||||||
};
|
};
|
||||||
|
|
|
@ -73,7 +73,18 @@ export const useImportAgentFromJson = () => {
|
||||||
const response = await backendApi.post('/agents/json/import', request);
|
const response = await backendApi.post('/agents/json/import', request);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Extract error message from different error formats
|
const errorData = error.response?.data;
|
||||||
|
const isAgentLimitError = (error.response?.status === 402) && (
|
||||||
|
errorData?.error_code === 'AGENT_LIMIT_EXCEEDED' ||
|
||||||
|
errorData?.detail?.error_code === 'AGENT_LIMIT_EXCEEDED'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAgentLimitError) {
|
||||||
|
const { AgentCountLimitError } = await import('@/lib/api');
|
||||||
|
const errorDetail = errorData?.detail || errorData;
|
||||||
|
throw new AgentCountLimitError(error.response.status, errorDetail);
|
||||||
|
}
|
||||||
|
|
||||||
const message = error.response?.data?.detail || error.message || 'Failed to import agent';
|
const message = error.response?.data?.detail || error.message || 'Failed to import agent';
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -256,6 +256,25 @@ export const createAgent = async (agentData: AgentCreateRequest): Promise<Agent>
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
|
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
|
||||||
|
console.log('[DEBUG] Error response data:', errorData);
|
||||||
|
console.log('[DEBUG] Response status:', response.status);
|
||||||
|
console.log('[DEBUG] Error code check:', errorData.error_code);
|
||||||
|
|
||||||
|
// Check for agent limit error - handle both direct error_code and nested in detail
|
||||||
|
const isAgentLimitError = (response.status === 402) && (
|
||||||
|
errorData.error_code === 'AGENT_LIMIT_EXCEEDED' ||
|
||||||
|
errorData.detail?.error_code === 'AGENT_LIMIT_EXCEEDED'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAgentLimitError) {
|
||||||
|
console.log('[DEBUG] Converting to AgentCountLimitError');
|
||||||
|
const { AgentCountLimitError } = await import('@/lib/api');
|
||||||
|
// Use the nested detail if it exists, otherwise use the errorData directly
|
||||||
|
const errorDetail = errorData.detail || errorData;
|
||||||
|
throw new AgentCountLimitError(response.status, errorDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DEBUG] Throwing generic error');
|
||||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -447,6 +447,22 @@ export function useInstallTemplate() {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
|
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
|
||||||
|
console.log('[DEBUG] Template install error data:', errorData);
|
||||||
|
|
||||||
|
// Check for agent limit error - handle both direct error_code and nested in detail
|
||||||
|
const isAgentLimitError = (response.status === 402) && (
|
||||||
|
errorData.error_code === 'AGENT_LIMIT_EXCEEDED' ||
|
||||||
|
errorData.detail?.error_code === 'AGENT_LIMIT_EXCEEDED'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAgentLimitError) {
|
||||||
|
console.log('[DEBUG] Converting template install to AgentCountLimitError');
|
||||||
|
const { AgentCountLimitError } = await import('@/lib/api');
|
||||||
|
// Use the nested detail if it exists, otherwise use the errorData directly
|
||||||
|
const errorDetail = errorData.detail || errorData;
|
||||||
|
throw new AgentCountLimitError(response.status, errorDetail);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,36 @@ export class AgentRunLimitError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AgentCountLimitError extends Error {
|
||||||
|
status: number;
|
||||||
|
detail: {
|
||||||
|
message: string;
|
||||||
|
current_count: number;
|
||||||
|
limit: number;
|
||||||
|
tier_name: string;
|
||||||
|
error_code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
status: number,
|
||||||
|
detail: {
|
||||||
|
message: string;
|
||||||
|
current_count: number;
|
||||||
|
limit: number;
|
||||||
|
tier_name: string;
|
||||||
|
error_code: string;
|
||||||
|
[key: string]: any;
|
||||||
|
},
|
||||||
|
message?: string,
|
||||||
|
) {
|
||||||
|
super(message || detail.message || `Agent Count Limit Exceeded: ${status}`);
|
||||||
|
this.name = 'AgentCountLimitError';
|
||||||
|
this.status = status;
|
||||||
|
this.detail = detail;
|
||||||
|
Object.setPrototypeOf(this, AgentCountLimitError.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class NoAccessTokenAvailableError extends Error {
|
export class NoAccessTokenAvailableError extends Error {
|
||||||
constructor(message?: string, options?: { cause?: Error }) {
|
constructor(message?: string, options?: { cause?: Error }) {
|
||||||
super(message || 'No access token available', options);
|
super(message || 'No access token available', options);
|
||||||
|
|
|
@ -124,6 +124,7 @@ export const siteConfig = {
|
||||||
hours: '60 min',
|
hours: '60 min',
|
||||||
features: [
|
features: [
|
||||||
'$5 free AI tokens included',
|
'$5 free AI tokens included',
|
||||||
|
'2 custom agents',
|
||||||
'Public projects',
|
'Public projects',
|
||||||
'Basic Models',
|
'Basic Models',
|
||||||
'Community support',
|
'Community support',
|
||||||
|
@ -145,6 +146,7 @@ export const siteConfig = {
|
||||||
hours: '2 hours',
|
hours: '2 hours',
|
||||||
features: [
|
features: [
|
||||||
'$20 AI token credits/month',
|
'$20 AI token credits/month',
|
||||||
|
'5 custom agents',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Premium AI Models',
|
'Premium AI Models',
|
||||||
'Community support',
|
'Community support',
|
||||||
|
@ -168,6 +170,7 @@ export const siteConfig = {
|
||||||
hours: '6 hours',
|
hours: '6 hours',
|
||||||
features: [
|
features: [
|
||||||
'$50 AI token credits/month',
|
'$50 AI token credits/month',
|
||||||
|
'20 custom agents',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Premium AI Models',
|
'Premium AI Models',
|
||||||
'Community support',
|
'Community support',
|
||||||
|
@ -190,6 +193,7 @@ export const siteConfig = {
|
||||||
hours: '12 hours',
|
hours: '12 hours',
|
||||||
features: [
|
features: [
|
||||||
'$100 AI token credits/month',
|
'$100 AI token credits/month',
|
||||||
|
'20 custom agents',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Premium AI Models',
|
'Premium AI Models',
|
||||||
'Community support',
|
'Community support',
|
||||||
|
@ -212,6 +216,7 @@ export const siteConfig = {
|
||||||
hours: '25 hours',
|
hours: '25 hours',
|
||||||
features: [
|
features: [
|
||||||
'$200 AI token credits/month',
|
'$200 AI token credits/month',
|
||||||
|
'100 custom agents',
|
||||||
'Private projects',
|
'Private projects',
|
||||||
'Premium AI Models',
|
'Premium AI Models',
|
||||||
'Priority support',
|
'Priority support',
|
||||||
|
|
Loading…
Reference in New Issue