mirror of https://github.com/kortix-ai/suna.git
feat(billing): update token price multiplier and add new model pricing details
This commit is contained in:
parent
a7305ed199
commit
d74d8d3d6a
|
@ -21,7 +21,7 @@ import time
|
|||
stripe.api_key = config.STRIPE_SECRET_KEY
|
||||
|
||||
# Token price multiplier
|
||||
TOKEN_PRICE_MULTIPLIER = 2
|
||||
TOKEN_PRICE_MULTIPLIER = 1.5
|
||||
|
||||
# Initialize router
|
||||
router = APIRouter(prefix="/billing", tags=["billing"])
|
||||
|
@ -51,7 +51,11 @@ HARDCODED_MODEL_PRICES = {
|
|||
"openrouter/google/gemini-2.5-flash-preview-05-20": {
|
||||
"input_cost_per_million_tokens": 0.15,
|
||||
"output_cost_per_million_tokens": 0.60
|
||||
}
|
||||
},
|
||||
"anthropic/claude-sonnet-4": {
|
||||
"input_cost_per_million_tokens": 3.00,
|
||||
"output_cost_per_million_tokens": 15.00,
|
||||
},
|
||||
}
|
||||
|
||||
def get_model_pricing(model: str) -> tuple[float, float] | None:
|
||||
|
|
|
@ -1,96 +1,26 @@
|
|||
'use client';
|
||||
|
||||
import { SectionHeader } from '@/components/home/section-header';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import {
|
||||
AlertCircle,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Zap,
|
||||
Server,
|
||||
Globe,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { useAvailableModels } from '@/hooks/react-query/subscriptions/use-billing';
|
||||
import type { Model } from '@/lib/api';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface ModelCardProps {
|
||||
model: Model;
|
||||
index: number;
|
||||
}
|
||||
|
||||
function ModelCard({ model, index }: ModelCardProps) {
|
||||
return (
|
||||
<div
|
||||
className="group relative bg-gradient-to-br from-background to-background/80 border border-border/50 rounded-2xl p-6 hover:border-border transition-all duration-300 hover:shadow-xl hover:shadow-black/5 hover:-translate-y-1"
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Model Header */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold text-foreground group-hover:text-blue-600 transition-colors duration-300">
|
||||
{model.display_name}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Pricing Grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Input Cost */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
Input
|
||||
</span>
|
||||
</div>
|
||||
{model.input_cost_per_million_tokens !== null &&
|
||||
model.input_cost_per_million_tokens !== undefined ? (
|
||||
<>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
${model.input_cost_per_million_tokens.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
per 1M tokens
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-2xl font-bold text-muted-foreground">—</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Output Cost */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
Output
|
||||
</span>
|
||||
</div>
|
||||
{model.output_cost_per_million_tokens !== null &&
|
||||
model.output_cost_per_million_tokens !== undefined ? (
|
||||
<>
|
||||
<div className="text-2xl font-bold text-foreground">
|
||||
${model.output_cost_per_million_tokens.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
per 1M tokens
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-2xl font-bold text-muted-foreground">—</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Stats */}
|
||||
{model.max_tokens && (
|
||||
<div className="pt-4 border-t border-border/50">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Max Tokens</span>
|
||||
<span className="font-medium text-foreground">
|
||||
{model.max_tokens.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PricingPage() {
|
||||
const {
|
||||
|
@ -111,77 +41,295 @@ export default function PricingPage() {
|
|||
);
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen w-full bg-gradient-to-br from-background via-background to-background/90">
|
||||
{/* Animated Background */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-50/20 via-transparent to-purple-50/20 dark:from-blue-950/20 dark:to-purple-950/20"></div>
|
||||
|
||||
{/* Header */}
|
||||
<SectionHeader>
|
||||
<div className="text-center space-y-6">
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-5xl md:text-6xl font-bold bg-gradient-to-br from-foreground via-foreground/90 to-foreground/70 bg-clip-text text-transparent">
|
||||
Compute Pricing
|
||||
</h1>
|
||||
</div>
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Loading pricing data...
|
||||
</p>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-6 pb-20 pt-4">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-32">
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 border-4 border-blue-200 dark:border-blue-800 rounded-full animate-spin border-t-blue-500"></div>
|
||||
<div className="absolute inset-0 w-16 h-16 border-4 border-transparent rounded-full animate-ping border-t-blue-500/20"></div>
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="max-w-md text-center space-y-4">
|
||||
<AlertCircle className="w-12 h-12 text-red-500 mx-auto" />
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Pricing Unavailable
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to fetch model pricing'}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => refetch()} size="sm">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 p-8 max-w-4xl mx-auto">
|
||||
{/* Header Section */}
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl font-bold text-foreground">
|
||||
Credits & Pricing
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground max-w-3xl">
|
||||
Understand how credits work, explore pricing for AI models, and find
|
||||
the right plan for your needs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* What are Credits Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Zap className="w-5 h-5 text-blue-500" />
|
||||
What are credits?
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground">
|
||||
Credits are our standard unit of measurement for platform usage -
|
||||
the more complex or lengthy the task, the more credits it requires.
|
||||
Credits provide a unified way to measure consumption across
|
||||
different types of AI operations and computational resources.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* How Credits Work Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Server className="w-5 h-5 text-green-500" />
|
||||
How do credits work?
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground">
|
||||
Credits are consumed based on AI model usage. We apply a 50% markup
|
||||
over the direct model provider costs. The specific credits
|
||||
consumption is determined by the model used and the number of tokens
|
||||
processed (both input and output tokens).
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Usage Examples Section */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Globe className="w-5 h-5 text-orange-500" />
|
||||
Usage Examples
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Here are some examples demonstrating credits consumption across
|
||||
different task types and complexity levels.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Example 4 */}
|
||||
<div className="p-4 border border-border rounded-lg space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-foreground">
|
||||
Social Automation System
|
||||
</h4>
|
||||
<Badge variant="destructive">Complex</Badge>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-medium text-foreground">
|
||||
Loading compute pricing
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Fetching the latest pricing data...
|
||||
</p>
|
||||
<div className="space-y-2 text-sm mt-6">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Model:</span>
|
||||
<span>claude-sonnet-4</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Duration:</span>
|
||||
<span>35 minutes</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span className="text-muted-foreground">Cost:</span>
|
||||
<span className="text-blue-600">$17.45</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="flex items-center justify-center py-32">
|
||||
<div className="max-w-md text-center space-y-6">
|
||||
<div className="w-20 h-20 mx-auto rounded-full bg-gradient-to-br from-red-50 to-red-100 dark:from-red-950/50 dark:to-red-900/50 border border-red-200 dark:border-red-800 flex items-center justify-center">
|
||||
<AlertCircle className="w-10 h-10 text-red-500" />
|
||||
|
||||
{/* Example 6 */}
|
||||
<div className="p-4 border border-border rounded-lg space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-foreground">
|
||||
Content Marketing Strategy
|
||||
</h4>
|
||||
<Badge variant="secondary">Standard Complexity</Badge>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-2xl font-bold text-foreground">
|
||||
Pricing Unavailable
|
||||
</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">
|
||||
{error instanceof Error
|
||||
? error.message
|
||||
: 'Failed to fetch model pricing'}
|
||||
</p>
|
||||
<div className="space-y-2 text-sm mt-6">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Model:</span>
|
||||
<span>claude-sonnet-4</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Duration:</span>
|
||||
<span>11 minutes</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span className="text-muted-foreground">Cost:</span>
|
||||
<span className="text-blue-600">$1.93</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
className="bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white border-0 shadow-lg hover:shadow-xl transition-all duration-300"
|
||||
>
|
||||
<Loader2 className="w-4 h-4 mr-2" />
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Example 5 */}
|
||||
<div className="p-4 border border-border rounded-lg space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-foreground">
|
||||
Go-to-Market Strategy
|
||||
</h4>
|
||||
<Badge variant="secondary">Standard Complexity</Badge>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm mt-6">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Model:</span>
|
||||
<span>deepseek-chat</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Duration:</span>
|
||||
<span>3 minutes</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span className="text-muted-foreground">Cost:</span>
|
||||
<span className="text-blue-600">$0.13</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Example 7 */}
|
||||
{/* <div className="p-4 border border-border rounded-lg space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-semibold text-foreground">
|
||||
6-Month Content Marketing Strategy
|
||||
</h4>
|
||||
<Badge variant="secondary">Standard Complexity</Badge>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Task type:</span>
|
||||
<span>Marketing • SEO • Content</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Model:</span>
|
||||
<span>deepseek-chat</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Duration:</span>
|
||||
<span>4 minutes</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span className="text-muted-foreground">Cost:</span>
|
||||
<span className="text-blue-600">$0.20</span>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-12">
|
||||
{/* Model Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Model Pricing Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Compute Pricing by Model</CardTitle>
|
||||
<CardDescription>
|
||||
Detailed pricing information for available AI models. We apply a 50%
|
||||
markup on direct LLM provider costs to maintain our service and
|
||||
generate profit.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-card border border-border rounded-lg">
|
||||
<div className="px-6 py-4 border-b border-border">
|
||||
<div className="grid grid-cols-3 gap-4 text-sm font-medium text-muted-foreground">
|
||||
<div className="col-span-1">Model</div>
|
||||
<div className="col-span-1 text-center">Input Cost</div>
|
||||
<div className="col-span-1 text-center">Output Cost</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
{models.map((model, index) => (
|
||||
<ModelCard key={model.id} model={model} index={index} />
|
||||
<div
|
||||
key={model.id}
|
||||
className="px-6 py-4 hover:bg-muted/50 transition-colors duration-150"
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-4 items-center">
|
||||
{/* Model Name */}
|
||||
<div className="col-span-1">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full flex-shrink-0"></div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium text-foreground truncate">
|
||||
{model.display_name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Input Cost */}
|
||||
<div className="col-span-1 text-center">
|
||||
<div className="space-y-1">
|
||||
{model.input_cost_per_million_tokens !== null &&
|
||||
model.input_cost_per_million_tokens !== undefined ? (
|
||||
<>
|
||||
<div className="font-semibold text-foreground">
|
||||
${model.input_cost_per_million_tokens.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
per 1M tokens
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="font-semibold text-muted-foreground">
|
||||
—
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Output Cost */}
|
||||
<div className="col-span-1 text-center">
|
||||
<div className="space-y-1">
|
||||
{model.output_cost_per_million_tokens !== null &&
|
||||
model.output_cost_per_million_tokens !== undefined ? (
|
||||
<>
|
||||
<div className="font-semibold text-foreground">
|
||||
${model.output_cost_per_million_tokens.toFixed(2)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
per 1M tokens
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="font-semibold text-muted-foreground">
|
||||
—
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -131,11 +131,12 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
|
|||
{/* Manage Subscription Button */}
|
||||
<div className='flex justify-center items-center gap-4'>
|
||||
<Button
|
||||
onClick={() => window.open('/model-pricing', '_blank')}
|
||||
variant="outline"
|
||||
className="border-border hover:bg-muted/50 shadow-sm hover:shadow-md transition-all"
|
||||
>
|
||||
View Compute Pricing <OpenInNewWindowIcon className='w-4 h-4' />
|
||||
<Link href="/model-pricing">
|
||||
View Compute Pricing <OpenInNewWindowIcon className='w-4 h-4' />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleManageSubscription}
|
||||
|
|
Loading…
Reference in New Issue