diff --git a/backend/services/billing.py b/backend/services/billing.py index dcbfa923..4593bb30 100644 --- a/backend/services/billing.py +++ b/backend/services/billing.py @@ -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: diff --git a/frontend/src/app/(dashboard)/model-pricing/page.tsx b/frontend/src/app/(dashboard)/model-pricing/page.tsx index fc7161ac..4df75abe 100644 --- a/frontend/src/app/(dashboard)/model-pricing/page.tsx +++ b/frontend/src/app/(dashboard)/model-pricing/page.tsx @@ -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 ( - - -
-
+
+ {/* Model Header */} +
+

{model.display_name} +

+
+ + {/* Pricing Grid */} +
+ {/* Input Cost */} +
+
+
+ + Input + +
+ {model.input_cost_per_million_tokens !== null && + model.input_cost_per_million_tokens !== undefined ? ( + <> +
+ ${model.input_cost_per_million_tokens.toFixed(2)} +
+
+ per 1M tokens +
+ + ) : ( +
+ )}
-
- {model.id} + + {/* Output Cost */} +
+
+
+ + Output + +
+ {model.output_cost_per_million_tokens !== null && + model.output_cost_per_million_tokens !== undefined ? ( + <> +
+ ${model.output_cost_per_million_tokens.toFixed(2)} +
+
+ per 1M tokens +
+ + ) : ( +
+ )}
- - - {model.input_cost_per_million_tokens !== null && - model.input_cost_per_million_tokens !== undefined ? ( - <> -
- ${model.input_cost_per_million_tokens.toFixed(2)} -
-
per 1M tokens
- - ) : ( -
- )} - - - {model.output_cost_per_million_tokens !== null && - model.output_cost_per_million_tokens !== undefined ? ( - <> -
- ${model.output_cost_per_million_tokens.toFixed(2)} -
-
per 1M tokens
- - ) : ( -
- )} - - - ); -} -export default function PricingPage() { - const [models, setModels] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - 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) => { - 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 ( -
- {/* Header */} - -
-
- - 💰 MODEL PRICING - -
-

- AI Model Pricing -

-

- Compare pricing across our supported AI models. All prices are shown - per million tokens. -

-
-
- - {/* Content */} -
- {loading ? ( -
-
- -

Loading model pricing...

-
-
- ) : error ? ( -
-
-
- -
-
-

- Failed to load pricing -

-

{error}

- -
-
-
- ) : ( -
- {/* Pricing Table */} -
-
- - - - - - - - - - {models.map((model, index) => ( - - ))} - -
- Model - - Input Cost - - Output Cost -
-
+ {/* Model Stats */} + {model.max_tokens && ( +
+
+ Max Tokens + + {model.max_tokens.toLocaleString()} + +
+
+ )} +
+
+ ); +} + +export default function PricingPage() { + const { + data: modelsResponse, + isLoading: loading, + error, + refetch, + } = useAvailableModels(); + + // Filter to only show models that have pricing information available + 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 + ); + }) || []; + + return ( +
+ {/* Animated Background */} +
+ + {/* Header */} + +
+
+

+ Compute Pricing +

+
+
+
+ + {/* Content */} +
+ {loading ? ( +
+
+
+
+
+
+
+

+ Loading compute pricing +

+

+ Fetching the latest pricing data... +

+
+
+
+ ) : error ? ( +
+
+
+ +
+
+

+ Pricing Unavailable +

+

+ {error instanceof Error + ? error.message + : 'Failed to fetch model pricing'} +

+
+ +
+
+ ) : ( +
+ {/* Model Cards Grid */} +
+ {models.map((model, index) => ( + + ))}
)} diff --git a/frontend/src/components/billing/account-billing-status.tsx b/frontend/src/components/billing/account-billing-status.tsx index c2ddcea0..c3cb18f5 100644 --- a/frontend/src/components/billing/account-billing-status.tsx +++ b/frontend/src/components/billing/account-billing-status.tsx @@ -122,6 +122,13 @@ export default function AccountBillingStatus({ accountId, returnUrl }: Props) {
{/* Manage Subscription Button */} + diff --git a/frontend/src/components/billing/usage-logs.tsx b/frontend/src/components/billing/usage-logs.tsx index 032ebd1b..4a948601 100644 --- a/frontend/src/components/billing/usage-logs.tsx +++ b/frontend/src/components/billing/usage-logs.tsx @@ -491,7 +491,7 @@ export default function UsageLogs({ accountId }: Props) { Time Model - Tokens + Compute Cost diff --git a/frontend/src/components/home/sections/pricing-section.tsx b/frontend/src/components/home/sections/pricing-section.tsx index 8c2d43c9..0ad8f0f8 100644 --- a/frontend/src/components/home/sections/pricing-section.tsx +++ b/frontend/src/components/home/sections/pricing-section.tsx @@ -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 ? (
- 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
) : (
@@ -631,7 +631,7 @@ export function PricingSection({ return (
{showTitleAndTabs && ( <> diff --git a/frontend/src/lib/home.tsx b/frontend/src/lib/home.tsx index c717148e..44a8fb43 100644 --- a/frontend/src/lib/home.tsx +++ b/frontend/src/lib/home.tsx @@ -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',