mirror of https://github.com/kortix-ai/suna.git
Merge pull request #850 from tnfssc/chore/model-pricing
This commit is contained in:
commit
720e31ad78
|
@ -18,9 +18,75 @@ import {
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { useAvailableModels } from '@/hooks/react-query/subscriptions/use-billing';
|
import { useAvailableModels } from '@/hooks/react-query/subscriptions/use-billing';
|
||||||
import type { Model } from '@/lib/api';
|
import type { Model } from '@/lib/api';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
// Example task data with token usage
|
||||||
|
const exampleTasks = [
|
||||||
|
{
|
||||||
|
name: 'Social Automation System',
|
||||||
|
complexity: 'Complex',
|
||||||
|
complexityVariant: 'destructive' as const,
|
||||||
|
inputTokens: 3410337,
|
||||||
|
outputTokens: 93616,
|
||||||
|
duration: '35 minutes',
|
||||||
|
originalModel: 'claude-sonnet-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Content Marketing Strategy',
|
||||||
|
complexity: 'Standard Complexity',
|
||||||
|
complexityVariant: 'secondary' as const,
|
||||||
|
inputTokens: 212312,
|
||||||
|
outputTokens: 3378,
|
||||||
|
duration: '11 minutes',
|
||||||
|
originalModel: 'claude-sonnet-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Go-to-Market Strategy',
|
||||||
|
complexity: 'Standard Complexity',
|
||||||
|
complexityVariant: 'secondary' as const,
|
||||||
|
inputTokens: 307719,
|
||||||
|
outputTokens: 24033,
|
||||||
|
duration: '16 minutes',
|
||||||
|
originalModel: 'claude-sonnet-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Learning Path Generator',
|
||||||
|
complexity: 'Standard Complexity',
|
||||||
|
complexityVariant: 'secondary' as const,
|
||||||
|
inputTokens: 90953,
|
||||||
|
outputTokens: 17472,
|
||||||
|
duration: '5 minutes',
|
||||||
|
originalModel: 'claude-sonnet-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Customer Journey Mapping',
|
||||||
|
complexity: 'Complex',
|
||||||
|
complexityVariant: 'destructive' as const,
|
||||||
|
inputTokens: 360013,
|
||||||
|
outputTokens: 17287,
|
||||||
|
duration: '20 minutes',
|
||||||
|
originalModel: 'claude-sonnet-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sales Funnel Optimization',
|
||||||
|
complexity: 'Complex',
|
||||||
|
complexityVariant: 'destructive' as const,
|
||||||
|
inputTokens: 559918,
|
||||||
|
outputTokens: 33392,
|
||||||
|
duration: '14 minutes',
|
||||||
|
originalModel: 'claude-sonnet-4',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function PricingPage() {
|
export default function PricingPage() {
|
||||||
const {
|
const {
|
||||||
|
@ -30,6 +96,10 @@ export default function PricingPage() {
|
||||||
refetch,
|
refetch,
|
||||||
} = useAvailableModels();
|
} = useAvailableModels();
|
||||||
|
|
||||||
|
const [selectedModelId, setSelectedModelId] = useState<string>(
|
||||||
|
'anthropic/claude-sonnet-4-20250514',
|
||||||
|
);
|
||||||
|
|
||||||
// Filter to only show models that have pricing information available
|
// Filter to only show models that have pricing information available
|
||||||
const models =
|
const models =
|
||||||
modelsResponse?.models?.filter((model: Model) => {
|
modelsResponse?.models?.filter((model: Model) => {
|
||||||
|
@ -41,6 +111,30 @@ export default function PricingPage() {
|
||||||
);
|
);
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|
||||||
|
// Find the selected model
|
||||||
|
const selectedModel = models.find((model) => model.id === selectedModelId);
|
||||||
|
|
||||||
|
// Function to calculate cost based on tokens and model pricing
|
||||||
|
const calculateCost = (
|
||||||
|
inputTokens: number,
|
||||||
|
outputTokens: number,
|
||||||
|
model: Model,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!model.input_cost_per_million_tokens ||
|
||||||
|
!model.output_cost_per_million_tokens
|
||||||
|
) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputCost =
|
||||||
|
(inputTokens / 1000000) * model.input_cost_per_million_tokens;
|
||||||
|
const outputCost =
|
||||||
|
(outputTokens / 1000000) * model.output_cost_per_million_tokens;
|
||||||
|
|
||||||
|
return inputCost + outputCost;
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-[400px]">
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
@ -139,106 +233,90 @@ export default function PricingPage() {
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="space-y-6">
|
||||||
{/* Example 4 */}
|
{/* Model Selection */}
|
||||||
<div className="p-4 border border-border rounded-lg space-y-3">
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
<label className="text-sm font-medium text-foreground">
|
||||||
<h4 className="font-semibold text-foreground">
|
Select a model to see pricing:
|
||||||
Social Automation System
|
</label>
|
||||||
</h4>
|
<Select
|
||||||
<Badge variant="destructive">Complex</Badge>
|
value={selectedModelId}
|
||||||
</div>
|
onValueChange={setSelectedModelId}
|
||||||
<div className="space-y-2 text-sm mt-6">
|
>
|
||||||
<div className="flex justify-between">
|
<SelectTrigger className="w-full max-w-md">
|
||||||
<span className="text-muted-foreground">Model:</span>
|
<SelectValue placeholder="Choose a model to calculate costs" />
|
||||||
<span>claude-sonnet-4</span>
|
</SelectTrigger>
|
||||||
</div>
|
<SelectContent>
|
||||||
<div className="flex justify-between">
|
{models.map((model) => (
|
||||||
<span className="text-muted-foreground">Duration:</span>
|
<SelectItem key={model.id} value={model.id}>
|
||||||
<span>35 minutes</span>
|
{model.display_name}
|
||||||
</div>
|
</SelectItem>
|
||||||
<div className="flex justify-between font-semibold">
|
))}
|
||||||
<span className="text-muted-foreground">Cost:</span>
|
</SelectContent>
|
||||||
<span className="text-blue-600">$17.45</span>
|
</Select>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Example 6 */}
|
{/* Example Tasks Grid */}
|
||||||
<div className="p-4 border border-border rounded-lg space-y-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<div className="space-y-2">
|
{exampleTasks.map((task, index) => {
|
||||||
<h4 className="font-semibold text-foreground">
|
const calculatedCost = selectedModel
|
||||||
Content Marketing Strategy
|
? calculateCost(
|
||||||
</h4>
|
task.inputTokens,
|
||||||
<Badge variant="secondary">Standard Complexity</Badge>
|
task.outputTokens,
|
||||||
</div>
|
selectedModel,
|
||||||
<div className="space-y-2 text-sm mt-6">
|
)
|
||||||
<div className="flex justify-between">
|
: null;
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Example 5 */}
|
return (
|
||||||
<div className="p-4 border border-border rounded-lg space-y-3">
|
<div
|
||||||
<div className="space-y-2">
|
key={index}
|
||||||
<h4 className="font-semibold text-foreground">
|
className="p-4 border border-border rounded-lg space-y-3"
|
||||||
Go-to-Market Strategy
|
>
|
||||||
</h4>
|
<div className="space-y-2">
|
||||||
<Badge variant="secondary">Standard Complexity</Badge>
|
<h4 className="font-semibold text-foreground">
|
||||||
</div>
|
{task.name}
|
||||||
<div className="space-y-2 text-sm mt-6">
|
</h4>
|
||||||
<div className="flex justify-between">
|
</div>
|
||||||
<span className="text-muted-foreground">Model:</span>
|
<div className="space-y-2 text-sm mt-6">
|
||||||
<span>deepseek-chat</span>
|
<div className="flex justify-between">
|
||||||
</div>
|
<span className="text-muted-foreground">Model:</span>
|
||||||
<div className="flex justify-between">
|
<span>
|
||||||
<span className="text-muted-foreground">Duration:</span>
|
{selectedModel?.display_name || task.originalModel}
|
||||||
<span>3 minutes</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between font-semibold">
|
<div className="flex justify-between">
|
||||||
<span className="text-muted-foreground">Cost:</span>
|
<span className="text-muted-foreground">
|
||||||
<span className="text-blue-600">$0.13</span>
|
Input Tokens:
|
||||||
</div>
|
</span>
|
||||||
</div>
|
<span>{task.inputTokens.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Output Tokens:
|
||||||
|
</span>
|
||||||
|
<span>{task.outputTokens.toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
{/* <div className="flex justify-between">
|
||||||
|
<span className="text-muted-foreground">Duration:</span>
|
||||||
|
<span>{task.duration}</span>
|
||||||
|
</div> */}
|
||||||
|
<div className="flex justify-between font-semibold">
|
||||||
|
<span className="text-muted-foreground">Cost:</span>
|
||||||
|
{calculatedCost !== null ? (
|
||||||
|
<span className="text-blue-600">
|
||||||
|
${calculatedCost.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
Select model above
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -267,7 +345,11 @@ export default function PricingPage() {
|
||||||
{models.map((model, index) => (
|
{models.map((model, index) => (
|
||||||
<div
|
<div
|
||||||
key={model.id}
|
key={model.id}
|
||||||
className="px-6 py-4 hover:bg-muted/50 transition-colors duration-150"
|
className={`px-6 py-4 hover:bg-muted/50 transition-colors duration-150 ${
|
||||||
|
selectedModelId === model.id
|
||||||
|
? 'bg-blue-50 dark:bg-blue-950/20 border-l-4 border-l-blue-500'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-3 gap-4 items-center">
|
<div className="grid grid-cols-3 gap-4 items-center">
|
||||||
{/* Model Name */}
|
{/* Model Name */}
|
||||||
|
|
Loading…
Reference in New Issue