limit agent creation based on tiers

This commit is contained in:
Saumya 2025-08-10 01:15:29 +05:30
parent f1f8f82b07
commit c786e190b6
18 changed files with 450 additions and 42 deletions

View File

@ -1767,7 +1767,6 @@ async def import_agent_from_json(
request: JsonImportRequestModel,
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}")
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."
)
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:
from agent.json_import_service import JsonImportService, JsonImportRequest
import_service = JsonImportService(db)
@ -1817,6 +1831,20 @@ async def create_agent(
)
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:
if agent_data.is_default:
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()
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}")
return AgentResponse(
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']:
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}")
return {"message": "Agent deleted successfully"}

View File

@ -114,6 +114,9 @@ class JsonImportService:
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")
return JsonImportResult(

View File

@ -138,3 +138,63 @@ async def check_agent_run_limit(client, account_id: str) -> Dict[str, Any]:
'running_count': 0,
'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'
}

View File

@ -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
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]]:
"""
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
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']:
return False, f"Monthly limit of {tier_info['cost']} dollars reached. Please upgrade your plan or wait until next month.", subscription

View File

@ -325,15 +325,22 @@ async def install_template(
request: InstallTemplateRequest,
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:
# Validate template access first
template = await validate_template_access_and_get(request.template_id, user_id)
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}")
@ -362,7 +369,6 @@ async def install_template(
)
except HTTPException:
# Re-raise HTTP exceptions from our validation functions
raise
except TemplateInstallationError as e:
logger.warning(f"Template installation failed: {e}")

View File

@ -107,6 +107,9 @@ class InstallationService:
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)"
logger.info(f"Successfully installed template {template.template_id} as agent {agent_id}")

View File

@ -270,6 +270,30 @@ class Configuration:
# Agent execution limits (can be overridden via environment variable)
_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
def MAX_PARALLEL_AGENT_RUNS(self) -> int:

View File

@ -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 { NewAgentDialog } from '@/components/agents/new-agent-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 AgentSortOption = 'name' | 'created_at' | 'updated_at' | 'tools_count';
@ -86,6 +88,8 @@ export default function AgentsPage() {
const [publishingAgentId, setPublishingAgentId] = useState<string | null>(null);
const [showNewAgentDialog, setShowNewAgentDialog] = useState(false);
const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false);
const [agentLimitError, setAgentLimitError] = useState<AgentCountLimitError | null>(null);
const activeTab = useMemo(() => {
return searchParams.get('tab') || 'my-agents';
@ -146,7 +150,6 @@ export default function AgentsPage() {
if (marketplaceTemplates) {
marketplaceTemplates.forEach(template => {
const item: MarketplaceTemplate = {
id: template.template_id,
creator_id: template.creator_id,
@ -174,14 +177,12 @@ export default function AgentsPage() {
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) {
mineItems.push(item);
}
// Categorize all templates (including user's own) for the "all" view
if (template.is_kortix_team) {
kortixItems.push(item);
} else {
@ -392,6 +393,12 @@ export default function AgentsPage() {
} catch (error: any) {
console.error('Installation error:', error);
if (error instanceof AgentCountLimitError) {
setAgentLimitError(error);
setShowAgentLimitDialog(true);
return;
}
if (error.message?.includes('already in your library')) {
toast.error('This agent is already in your library');
} else if (error.message?.includes('Credential profile not found')) {
@ -628,6 +635,15 @@ export default function AgentsPage() {
onInstall={handlePreviewInstall}
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>
);

View File

@ -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>
);
};

View File

@ -11,6 +11,8 @@ import { ProfileConnector } from './installation/streamlined-profile-connector';
import { CustomServerStep } from './installation/custom-server-step';
import type { SetupStep } from './installation/types';
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';
interface JsonImportDialogProps {
@ -34,6 +36,8 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
const [currentStep, setCurrentStep] = useState(0);
const [profileMappings, setProfileMappings] = useState<Record<string, string>>({});
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 importJsonMutation = useImportAgentFromJson();
@ -47,6 +51,8 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
setCurrentStep(0);
setProfileMappings({});
setCustomMcpConfigs({});
setShowAgentLimitDialog(false);
setAgentLimitError(null);
}, []);
useEffect(() => {
@ -215,6 +221,13 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
}
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
</DialogTitle>
</DialogHeader>
{analysis && !analysis.requires_setup && step === 'paste' && (
<Alert>
<CheckCircle2 className="h-4 w-4" />
@ -389,7 +401,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
</AlertDescription>
</Alert>
)}
{analysis && analysis.requires_setup && step === 'paste' && (
<Alert>
<AlertCircle className="h-4 w-4" />
@ -400,7 +411,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
</AlertDescription>
</Alert>
)}
{analyzeJsonMutation.isError && step === 'paste' && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
@ -409,7 +419,6 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
</AlertDescription>
</Alert>
)}
{importJsonMutation.isError && (step === 'setup' || step === 'importing') && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
@ -418,11 +427,19 @@ export const JsonImportDialog: React.FC<JsonImportDialogProps> = ({
</AlertDescription>
</Alert>
)}
{step === 'paste' && renderPasteStep()}
{step === 'setup' && renderSetupStep()}
{step === 'importing' && renderImportingStep()}
</DialogContent>
{agentLimitError && (
<AgentCountLimitDialog
open={showAgentLimitDialog}
onOpenChange={setShowAgentLimitDialog}
currentCount={agentLimitError.detail.current_count}
limit={agentLimitError.detail.limit}
tierName={agentLimitError.detail.tier_name}
/>
)}
</Dialog>
);
};

View File

@ -1,6 +1,6 @@
'use client';
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import { Loader2, Plus, FileJson, Code } from 'lucide-react';
import {
AlertDialog,
@ -14,6 +14,8 @@ import {
} from '@/components/ui/alert-dialog';
import { useCreateNewAgent } from '@/hooks/react-query/agents/use-agents';
import { JsonImportDialog } from './json-import-dialog';
import { AgentCountLimitDialog } from './agent-count-limit-dialog';
import { AgentCountLimitError } from '@/lib/api';
import { toast } from 'sonner';
interface NewAgentDialogProps {
@ -25,19 +27,43 @@ interface NewAgentDialogProps {
export function NewAgentDialog({ open, onOpenChange, onSuccess }: NewAgentDialogProps) {
const [showJsonImport, setShowJsonImport] = useState(false);
const [jsonImportText, setJsonImportText] = useState('');
const [showAgentLimitDialog, setShowAgentLimitDialog] = useState(false);
const [agentLimitError, setAgentLimitError] = useState<AgentCountLimitError | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const createNewAgentMutation = useCreateNewAgent();
useEffect(() => {
console.log('[DEBUG] NewAgentDialog state:', {
agentLimitError: !!agentLimitError,
showAgentLimitDialog,
errorDetail: agentLimitError?.detail
});
}, [agentLimitError, showAgentLimitDialog]);
const handleCreateNewAgent = () => {
console.log('[DEBUG] Creating new agent...');
createNewAgentMutation.mutate(undefined, {
onSuccess: () => {
console.log('[DEBUG] Agent created successfully');
onOpenChange(false);
onSuccess?.();
},
onError: () => {
// Keep dialog open on error so user can see the error and try again
// The useCreateNewAgent hook already shows error toasts
onError: (error) => {
console.log('[DEBUG] Error creating agent:', error);
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?.();
}}
/>
{agentLimitError && (
<AgentCountLimitDialog
open={showAgentLimitDialog}
onOpenChange={setShowAgentLimitDialog}
currentCount={agentLimitError.detail.current_count}
limit={agentLimitError.detail.limit}
tierName={agentLimitError.detail.tier_name}
/>
)}
</AlertDialog>
);
}

View File

@ -566,7 +566,7 @@ function PricingTier({
variant={buttonVariant || 'default'}
className={cn(
'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,
isPlanLoading && 'animate-pulse',
)}
@ -584,13 +584,17 @@ interface PricingSectionProps {
showTitleAndTabs?: boolean;
hideFree?: boolean;
insideDialog?: boolean;
showInfo?: boolean;
noPadding?: boolean;
}
export function PricingSection({
returnUrl = typeof window !== 'undefined' ? window.location.href : '/',
showTitleAndTabs = true,
hideFree = false,
insideDialog = false
insideDialog = false,
showInfo = true,
noPadding = false,
}: PricingSectionProps) {
const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>(
'cloud',
@ -598,18 +602,14 @@ export function PricingSection({
const { data: subscriptionData, isLoading: isFetchingPlan, error: subscriptionQueryError, refetch: refetchSubscription } = useSubscription();
const subCommitmentQuery = useSubscriptionCommitment(subscriptionData?.subscription_id);
// Derive authentication and subscription status from the hook data
const isAuthenticated = !!subscriptionData && subscriptionQueryError === null;
const currentSubscription = subscriptionData || null;
// Determine default billing period based on user's current subscription
const getDefaultBillingPeriod = useCallback((): 'monthly' | 'yearly' | 'yearly_commitment' => {
if (!isAuthenticated || !currentSubscription) {
// Default to yearly_commitment for non-authenticated users (the new yearly plans)
return 'yearly_commitment';
}
// Find current tier to determine if user is on monthly, yearly, or yearly commitment plan
const currentTier = siteConfig.cloudPricingItems.find(
(p) => p.stripePriceId === currentSubscription.price_id ||
p.yearlyStripePriceId === currentSubscription.price_id ||
@ -685,7 +685,7 @@ export function PricingSection({
return (
<section
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 && (
<>
@ -747,14 +747,15 @@ export function PricingSection({
))}
</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">
<p className="text-sm text-blue-800 dark:text-blue-200 text-center">
<strong>What are AI tokens?</strong> Tokens are units of text that AI models process.
Your plan includes credits to spend on various AI models - the more complex the task,
the more tokens used.
</p>
</div>
{showInfo && (
<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">
<p className="text-sm text-blue-800 dark:text-blue-200 text-center">
<strong>What are AI tokens?</strong> Tokens are units of text that AI models process.
Your plan includes credits to spend on various AI models - the more complex the task,
the more tokens used.
</p>
</div>
)}
</section>
);

View File

@ -42,6 +42,14 @@ export const useCreateAgent = () => {
queryClient.setQueryData(agentKeys.detail(data.agent_id), data);
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');
},
}
)();
};

View File

@ -73,7 +73,18 @@ export const useImportAgentFromJson = () => {
const response = await backendApi.post('/agents/json/import', request);
return response.data;
} 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';
throw new Error(message);
}

View File

@ -256,6 +256,25 @@ export const createAgent = async (agentData: AgentCreateRequest): Promise<Agent>
if (!response.ok) {
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}`);
}

View File

@ -447,6 +447,22 @@ export function useInstallTemplate() {
if (!response.ok) {
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}`);
}

View File

@ -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 {
constructor(message?: string, options?: { cause?: Error }) {
super(message || 'No access token available', options);

View File

@ -124,6 +124,7 @@ export const siteConfig = {
hours: '60 min',
features: [
'$5 free AI tokens included',
'2 custom agents',
'Public projects',
'Basic Models',
'Community support',
@ -145,6 +146,7 @@ export const siteConfig = {
hours: '2 hours',
features: [
'$20 AI token credits/month',
'5 custom agents',
'Private projects',
'Premium AI Models',
'Community support',
@ -168,6 +170,7 @@ export const siteConfig = {
hours: '6 hours',
features: [
'$50 AI token credits/month',
'20 custom agents',
'Private projects',
'Premium AI Models',
'Community support',
@ -190,6 +193,7 @@ export const siteConfig = {
hours: '12 hours',
features: [
'$100 AI token credits/month',
'20 custom agents',
'Private projects',
'Premium AI Models',
'Community support',
@ -212,6 +216,7 @@ export const siteConfig = {
hours: '25 hours',
features: [
'$200 AI token credits/month',
'100 custom agents',
'Private projects',
'Premium AI Models',
'Priority support',