feat(billing): introduce token price multiplier and update cost calculations; enhance billing UI with new pricing buttons

This commit is contained in:
sharath 2025-06-26 17:33:52 +00:00
parent 79b71db250
commit e7f02f31bc
No known key found for this signature in database
7 changed files with 194 additions and 227 deletions

View File

@ -20,6 +20,9 @@ import time
# Initialize Stripe
stripe.api_key = config.STRIPE_SECRET_KEY
# Token price multiplier
TOKEN_PRICE_MULTIPLIER = 2
# Initialize router
router = APIRouter(prefix="/billing", tags=["billing"])
@ -278,8 +281,8 @@ async def calculate_monthly_usage(client, user_id: str) -> float:
total_cost += message_cost
# Return total cost * 2 (as per original logic)
total_cost = total_cost * 2
# Return total cost * TOKEN_PRICE_MULTIPLIER (as per original logic)
total_cost = total_cost * TOKEN_PRICE_MULTIPLIER
logger.info(f"Total cost for user {user_id}: {total_cost}")
return total_cost
@ -1038,8 +1041,8 @@ async def get_available_models(
if hardcoded_pricing:
input_cost_per_million, output_cost_per_million = hardcoded_pricing
pricing_info = {
"input_cost_per_million_tokens": input_cost_per_million,
"output_cost_per_million_tokens": output_cost_per_million,
"input_cost_per_million_tokens": input_cost_per_million * TOKEN_PRICE_MULTIPLIER,
"output_cost_per_million_tokens": output_cost_per_million * TOKEN_PRICE_MULTIPLIER,
"max_tokens": None
}
else:
@ -1090,8 +1093,8 @@ async def get_available_models(
if input_cost_per_token is not None and output_cost_per_token is not None:
pricing_info = {
"input_cost_per_million_tokens": round(input_cost_per_token * 2, 2),
"output_cost_per_million_tokens": round(output_cost_per_token * 2, 2),
"input_cost_per_million_tokens": input_cost_per_token * TOKEN_PRICE_MULTIPLIER,
"output_cost_per_million_tokens": output_cost_per_token * TOKEN_PRICE_MULTIPLIER,
"max_tokens": None # cost_per_token doesn't provide max_tokens info
}
else:

View File

@ -1,226 +1,183 @@
'use client';
import { useEffect, useState } from 'react';
import { SectionHeader } from '@/components/home/section-header';
import { getAvailableModels, Model } from '@/lib/api';
import { Loader2, AlertCircle } from 'lucide-react';
import { AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
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 PricingTableRowProps {
interface ModelCardProps {
model: Model;
index: number;
}
function PricingTableRow({ model, index }: PricingTableRowProps) {
function ModelCard({ model, index }: ModelCardProps) {
return (
<tr
className="border-b border-border/50 transition-all duration-200 hover:bg-muted/30"
<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` }}
>
<td className="py-6 px-6">
<div>
<div className="font-semibold text-foreground capitalize">
<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>
<div className="text-sm text-muted-foreground font-mono truncate max-w-[200px]">
{model.id}
{/* 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>
</div>
</td>
<td className="py-6 px-6 text-right">
{model.input_cost_per_million_tokens !== null &&
model.input_cost_per_million_tokens !== undefined ? (
<>
<div className="font-semibold text-foreground">
<div className="text-2xl font-bold text-foreground">
${model.input_cost_per_million_tokens.toFixed(2)}
</div>
<div className="text-sm text-muted-foreground">per 1M tokens</div>
<div className="text-xs text-muted-foreground">
per 1M tokens
</div>
</>
) : (
<div className="text-muted-foreground"></div>
<div className="text-2xl font-bold text-muted-foreground"></div>
)}
</td>
<td className="py-6 px-6 text-right">
</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="font-semibold text-foreground">
<div className="text-2xl font-bold text-foreground">
${model.output_cost_per_million_tokens.toFixed(2)}
</div>
<div className="text-sm text-muted-foreground">per 1M tokens</div>
<div className="text-xs text-muted-foreground">
per 1M tokens
</div>
</>
) : (
<div className="text-muted-foreground"></div>
<div className="text-2xl font-bold text-muted-foreground"></div>
)}
</td>
</tr>
</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 [models, setModels] = useState<Model[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const {
data: modelsResponse,
isLoading: loading,
error,
refetch,
} = useAvailableModels();
useEffect(() => {
const fetchPricing = async () => {
try {
setLoading(true);
setError(null);
const response = await getAvailableModels();
// Filter to only show models that have pricing information available
const filteredModels = response.models.filter((model) => {
const models =
modelsResponse?.models?.filter((model: Model) => {
return (
model.input_cost_per_million_tokens !== null &&
model.input_cost_per_million_tokens !== undefined &&
model.output_cost_per_million_tokens !== null &&
model.output_cost_per_million_tokens !== undefined
);
});
setModels(filteredModels);
} catch (err) {
console.error('Error fetching model pricing:', err);
// If there's an authentication error, show fallback data for the specific models requested
if (
err instanceof Error &&
(err.message.includes('token') || err.message.includes('auth'))
) {
const fallbackModels: Model[] = [
{
id: 'anthropic/claude-sonnet-4-20250514',
display_name: 'Claude Sonnet 4',
requires_subscription: true,
is_available: false,
input_cost_per_million_tokens: 3.0,
output_cost_per_million_tokens: 15.0,
max_tokens: 200000,
},
{
id: 'openrouter/qwen/qwen3-235b-a22b',
display_name: 'Qwen3 A22B',
requires_subscription: false,
is_available: false,
input_cost_per_million_tokens: 0.4,
output_cost_per_million_tokens: 0.4,
max_tokens: 32768,
},
{
id: 'openrouter/google/gemini-2.5-flash-preview-05-20',
display_name: 'Gemini Flash 2.5',
requires_subscription: false,
is_available: false,
input_cost_per_million_tokens: 0.075,
output_cost_per_million_tokens: 0.3,
max_tokens: 1000000,
},
{
id: 'openrouter/deepseek/deepseek-chat',
display_name: 'DeepSeek Chat',
requires_subscription: false,
is_available: false,
input_cost_per_million_tokens: 0.14,
output_cost_per_million_tokens: 0.28,
max_tokens: 65536,
},
];
setModels(fallbackModels);
} else {
setError(
err instanceof Error
? err.message
: 'Failed to fetch model pricing',
);
}
} finally {
setLoading(false);
}
};
fetchPricing();
}, []);
}) || [];
return (
<div className="relative min-h-screen w-full">
<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-4">
<div className="inline-flex items-center px-3 py-1.5 rounded-full border border-border bg-background/50 backdrop-blur-sm">
<span className="text-sm font-medium text-muted-foreground">
💰 MODEL PRICING
</span>
</div>
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-br from-foreground to-foreground/70 bg-clip-text text-transparent">
AI Model Pricing
<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>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
Compare pricing across our supported AI models. All prices are shown
per million tokens.
</p>
</div>
</div>
</SectionHeader>
{/* Content */}
<div className="relative z-10 max-w-7xl mx-auto px-6 py-12">
<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-20">
<div className="flex flex-col items-center gap-4">
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
<p className="text-muted-foreground">Loading model pricing...</p>
<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>
</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>
</div>
</div>
) : error ? (
<div className="flex items-center justify-center py-20">
<div className="flex flex-col items-center gap-4 text-center">
<div className="w-16 h-16 rounded-full bg-red-500/10 border border-red-500/20 flex items-center justify-center">
<AlertCircle className="w-8 h-8 text-red-500" />
<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" />
</div>
<div>
<h3 className="text-xl font-semibold text-foreground mb-2">
Failed to load pricing
<div className="space-y-3">
<h3 className="text-2xl font-bold text-foreground">
Pricing Unavailable
</h3>
<p className="text-muted-foreground mb-4">{error}</p>
<p className="text-muted-foreground leading-relaxed">
{error instanceof Error
? error.message
: 'Failed to fetch model pricing'}
</p>
</div>
<Button
variant="outline"
onClick={() => window.location.reload()}
className="hover:bg-muted/50"
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>
</div>
</div>
) : (
<div className="space-y-8">
{/* Pricing Table */}
<div className="border border-border rounded-xl bg-background/50 backdrop-blur-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border bg-muted/30">
<th className="text-left py-4 px-6 font-semibold text-foreground">
Model
</th>
<th className="text-right py-4 px-6 font-semibold text-foreground">
Input Cost
</th>
<th className="text-right py-4 px-6 font-semibold text-foreground">
Output Cost
</th>
</tr>
</thead>
<tbody>
<div className="space-y-12">
{/* Model Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{models.map((model, index) => (
<PricingTableRow
key={model.id}
model={model}
index={index}
/>
<ModelCard key={model.id} model={model} index={index} />
))}
</tbody>
</table>
</div>
</div>
</div>
)}

View File

@ -122,6 +122,13 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
<div className="mt-20"></div>
{/* Manage Subscription Button */}
<Button
onClick={() => window.open('/model-pricing', '_blank')}
variant="outline"
className="w-full border-border hover:bg-muted/50 shadow-sm hover:shadow-md transition-all mb-3"
>
View Compute Pricing
</Button>
<Button
onClick={handleManageSubscription}
disabled={isManaging}
@ -161,11 +168,11 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
{/* Action Buttons */}
<div className="space-y-3">
<Button
onClick={() => window.open('/models', '_blank')}
onClick={() => window.open('/model-pricing', '_blank')}
variant="outline"
className="w-full border-border hover:bg-muted/50 shadow-sm hover:shadow-md transition-all"
>
View Model Pricing
View Compute Pricing
</Button>
<Button
onClick={handleManageSubscription}

View File

@ -88,7 +88,7 @@ export function BillingModal({ open, onOpenChange, returnUrl = window?.location?
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogContent className="max-w-5xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Upgrade Your Plan</DialogTitle>
</DialogHeader>
@ -127,7 +127,7 @@ export function BillingModal({ open, onOpenChange, returnUrl = window?.location?
<Button
onClick={handleManageSubscription}
disabled={isManaging}
className="w-full bg-primary hover:bg-primary/90 shadow-md hover:shadow-lg transition-all mt-4"
className="max-w-xs mx-auto w-full bg-primary hover:bg-primary/90 shadow-md hover:shadow-lg transition-all mt-4"
>
{isManaging ? 'Loading...' : 'Manage Subscription'}
</Button>

View File

@ -491,7 +491,7 @@ export default function UsageLogs({ accountId }: Props) {
<TableHead>Time</TableHead>
<TableHead>Model</TableHead>
<TableHead className="text-right">
Tokens
Compute
</TableHead>
<TableHead className="text-right">Cost</TableHead>
<TableHead className="text-center">

View File

@ -278,7 +278,7 @@ function PricingTier({
isScheduled && currentSubscription?.scheduled_price_id === tierPriceId;
const isPlanLoading = isLoading[tierPriceId];
let buttonText = isAuthenticated ? 'Select Plan' : 'Try Free';
let buttonText = isAuthenticated ? 'Select Plan' : 'Start Free';
let buttonDisabled = isPlanLoading;
let buttonVariant: ButtonVariant = null;
let ringClass = '';
@ -480,7 +480,7 @@ function PricingTier({
{billingPeriod === 'yearly' && tier.yearlyPrice && tier.discountPercentage ? (
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-green-50 border-green-200 text-green-700 w-fit">
Save ${Math.round((parseFloat(tier.originalYearlyPrice?.slice(1) || '0') - parseFloat(tier.yearlyPrice.slice(1))) / 12)} per month
Save ${Math.round(parseFloat(tier.originalYearlyPrice?.slice(1) || '0') - parseFloat(tier.yearlyPrice.slice(1)))} per year
</div>
) : (
<div className="inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-primary/10 border-primary/20 text-primary w-fit">
@ -631,7 +631,7 @@ export function PricingSection({
return (
<section
id="pricing"
className={cn("flex flex-col items-center justify-center gap-10 w-full relative", { "pb-20": !insideDialog })}
className={cn("flex flex-col items-center justify-center gap-10 w-full relative")}
>
{showTitleAndTabs && (
<>

View File

@ -116,7 +116,7 @@ export const siteConfig = {
name: 'Free',
price: '$0',
description: 'Get started with',
buttonText: 'Try Free',
buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
/** @deprecated */
@ -136,7 +136,7 @@ export const siteConfig = {
originalYearlyPrice: '$240',
discountPercentage: 15,
description: 'Everything in Free, plus:',
buttonText: 'Try Free',
buttonText: 'Start Free',
buttonColor: 'bg-primary text-white dark:text-black',
isPopular: true,
/** @deprecated */
@ -157,7 +157,7 @@ export const siteConfig = {
originalYearlyPrice: '$600',
discountPercentage: 15,
description: 'Everything in Free, plus:',
buttonText: 'Try Free',
buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
/** @deprecated */
@ -178,7 +178,7 @@ export const siteConfig = {
originalYearlyPrice: '$1200',
discountPercentage: 15,
description: 'Everything in Pro, plus:',
buttonText: 'Try Free',
buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '12 hours',
@ -200,7 +200,7 @@ export const siteConfig = {
originalYearlyPrice: '$2400',
discountPercentage: 15,
description: 'Everything in Free, plus:',
buttonText: 'Try Free',
buttonText: 'Start Free',
buttonColor: 'bg-primary text-white dark:text-black',
isPopular: false,
hours: '25 hours',
@ -220,7 +220,7 @@ export const siteConfig = {
originalYearlyPrice: '$4800',
discountPercentage: 15,
description: 'Everything in Ultra, plus:',
buttonText: 'Try Free',
buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '50 hours',
@ -243,7 +243,7 @@ export const siteConfig = {
originalYearlyPrice: '$9600',
discountPercentage: 15,
description: 'Everything in Enterprise, plus:',
buttonText: 'Try Free',
buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '125 hours',
@ -267,7 +267,7 @@ export const siteConfig = {
originalYearlyPrice: '$12000',
discountPercentage: 15,
description: 'Everything in Scale, plus:',
buttonText: 'Try Free',
buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white',
isPopular: false,
hours: '200 hours',