mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
web: Roo Code Cloud Provider pricing page and changes elsewhere (#9195)
Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
parent
744f4bd4c8
commit
7a06902dd8
5 changed files with 574 additions and 42 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Users, Building2, ArrowRight, Star, LucideIcon, Check, Cloud } from "lucide-react"
|
||||
import { Users, Building2, ArrowRight, Star, LucideIcon, Check, Cloud, PlugZap } from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
|
|
@ -64,7 +64,6 @@ interface PricingTier {
|
|||
period?: string
|
||||
creditPrice?: string
|
||||
trial?: string
|
||||
cancellation?: string
|
||||
description: string
|
||||
featuresIntro?: string
|
||||
features: string[]
|
||||
|
|
@ -80,13 +79,12 @@ const pricingTiers: PricingTier[] = [
|
|||
name: "Cloud Free",
|
||||
icon: Cloud,
|
||||
price: "$0",
|
||||
cancellation: "Cancel anytime",
|
||||
description: "For folks just getting started",
|
||||
features: [
|
||||
"Token usage analytics",
|
||||
"Access to the Roo Code Cloud Provider, including early access to free stealth models",
|
||||
"Follow your tasks from anywhere",
|
||||
"Share tasks with friends and co-workers",
|
||||
"Early access to free AI Models",
|
||||
"Community support",
|
||||
],
|
||||
cta: {
|
||||
|
|
@ -95,18 +93,18 @@ const pricingTiers: PricingTier[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
name: "Cloud Pro",
|
||||
icon: Star,
|
||||
price: "$20",
|
||||
period: "/mo",
|
||||
trial: "Free 14-day trial · ",
|
||||
trial: "Free for 14 days, then",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
cancellation: "Cancel anytime",
|
||||
description: "For pro Roo coders",
|
||||
featuresIntro: "Everything in Free +",
|
||||
features: [
|
||||
"Cloud Agents: PR Reviewer and more",
|
||||
"Roomote Control: Start, stop and control tasks from anywhere",
|
||||
"Cloud Agents: Coder, Explainer, Planner, Reviewer, Fixer and more",
|
||||
"Start tasks from Slack",
|
||||
"Roomote Control: Start, stop and control extension tasks from anywhere",
|
||||
"Paid support",
|
||||
],
|
||||
cta: {
|
||||
|
|
@ -115,13 +113,12 @@ const pricingTiers: PricingTier[] = [
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "Team",
|
||||
name: "Cloud Team",
|
||||
icon: Users,
|
||||
price: "$99",
|
||||
period: "/mo",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
trial: "Free 14-day trial · ",
|
||||
cancellation: "Cancel anytime",
|
||||
trial: "Free for 14 days, then",
|
||||
description: "For AI-forward teams",
|
||||
featuresIntro: "Everything in Pro +",
|
||||
features: ["Unlimited users (no per-seat cost)", "Shared configuration & policies", "Centralized billing"],
|
||||
|
|
@ -151,12 +148,30 @@ export default function PricingPage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* Free Extension Notice */}
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="rounded-xl p-4 mb-8 text-center bg-gradient-to-r from-blue-500/10 via-cyan-500/10 to-purple-500/10 border border-blue-500/20 dark:border-white/20">
|
||||
<div className="mx-6 md:mx-auto max-w-6xl">
|
||||
<div className="rounded-xl p-4 mb-8 text-center bg-gradient-to-r from-blue-500/10 via-cyan-500/10 to-purple-500/10 border border-blue-500/20 dark:border-white/20 ">
|
||||
<p className="text-center">
|
||||
<strong className="font-semibold">The Roo Code extension is free! </strong>
|
||||
Roo Code Cloud is an optional service which takes it to the next level.
|
||||
<strong className="font-semibold">The Roo Code extension is totally free! </strong>
|
||||
But Cloud takes you so much further.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-6 md:mx-auto max-w-6xl p-7 mb-4 relative flex flex-col justify-start bg-background border rounded-2xl transition-all shadow-none hover:shadow-lg">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-2 justify-between">
|
||||
Roo Code Provider
|
||||
<PlugZap className="size-6" />
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground space-y-1 mt-2">
|
||||
<p className="">
|
||||
On any plan, you can bring your own provider key or use the built-in Roo Code Cloud provider.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
We offer a select mix of tested state of the art closed and open weight LLMs for you to choose,
|
||||
with no markup.
|
||||
<Link href="/provider/pricing" className="underline hover:no-underline ml-1">
|
||||
See detailed pricing
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -183,7 +198,7 @@ export default function PricingPage() {
|
|||
<p className="text-sm text-muted-foreground font-light mb-2">
|
||||
{tier.featuresIntro}
|
||||
</p>
|
||||
<ul className="space-y-3 my-0 h-[148px]">
|
||||
<ul className="space-y-3 my-0 h-[168px]">
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-4 w-4 text-muted-foreground shrink-0" />
|
||||
|
|
@ -193,20 +208,28 @@ export default function PricingPage() {
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<p className="text-2xl mt-0 mb-1 tracking-tight">
|
||||
<strong>{tier.price}</strong>
|
||||
{tier.period}
|
||||
<p className="text-base font-light">{tier.trial}</p>
|
||||
|
||||
<p className="text-xl my-1 tracking-tight font-light">
|
||||
<strong className="font-bold">{tier.price}</strong>
|
||||
{tier.period} + prepaid credits
|
||||
</p>
|
||||
|
||||
{tier.creditPrice && (
|
||||
<p className="text-sm text-muted-foreground mb-1">
|
||||
+ {tier.creditPrice}/hour for Cloud tasks
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
{tier.trial}
|
||||
{tier.cancellation}
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
{tier.creditPrice && (
|
||||
<>
|
||||
Cloud Agents: {tier.creditPrice}/hour if used
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
Inference:{" "}
|
||||
<Link href="/provider/pricing" className="underline hover:no-underline">
|
||||
Roo Provider pricing
|
||||
</Link>{" "}
|
||||
or{" "}
|
||||
<abbr title="Bring Your Own Key" className="cursor-help">
|
||||
BYOK
|
||||
</abbr>
|
||||
</p>
|
||||
|
||||
{tier.cta.isContactForm ? (
|
||||
|
|
@ -249,7 +272,7 @@ export default function PricingPage() {
|
|||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Frequently Asked Questions</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 grid max-w-5xl gap-8 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Wait, is Roo Code free or not?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes! The Roo Code VS Code extension is open source and free forever. The extension acts
|
||||
|
|
@ -257,7 +280,7 @@ export default function PricingPage() {
|
|||
Code Cloud.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Is there a free trial?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, all paid plans come with a 14-day free trial to try out functionality.
|
||||
|
|
@ -266,12 +289,25 @@ export default function PricingPage() {
|
|||
To use Cloud Agents, you can buy credits.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">How do Cloud Agent credits work?</h3>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">How do credits work?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Cloud Agents are a version of Roo running in the cloud without depending on your IDE.
|
||||
You can run as many as you want, and bring your own inference provider key.
|
||||
Roo Code Cloud credits can be used in two ways:
|
||||
</p>
|
||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
||||
<li>To pay for Cloud Agents running time (${PRICE_CREDITS}/hour)</li>
|
||||
<li>
|
||||
To pay for AI model inference costs (
|
||||
<a
|
||||
href="https://app.roocode.com/provider/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline">
|
||||
varies by model
|
||||
</a>
|
||||
)
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
To cover our infrastructure costs, we charge ${PRICE_CREDITS}/hour while the agent is
|
||||
running (independent of inference costs).
|
||||
|
|
@ -280,25 +316,25 @@ export default function PricingPage() {
|
|||
There are no markups, no tiers, no dumbing-down of models to increase our profit.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Do I need a credit card for the free trial?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, but you won't be charged until your trial ends, except for credit purchases.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">You can cancel anytime with one click.</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">What payment methods do you accept?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
We accept all major credit cards, debit cards, and can arrange invoice billing for
|
||||
Enterprise customers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Can I change plans anytime?</h3>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Can I cancel or change plans?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, you can upgrade or downgrade your plan at any time. Changes will be reflected in
|
||||
your next billing cycle.
|
||||
Yes, you can upgrade, downgrade or cancel your plan at any time. Changes will be
|
||||
reflected in your next billing cycle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,190 @@
|
|||
import { ModelWithTotalPrice } from "@/lib/types/models"
|
||||
import { formatCurrency, formatTokens } from "@/lib/formatters"
|
||||
import {
|
||||
ArrowLeftToLine,
|
||||
ArrowRightToLine,
|
||||
Building2,
|
||||
Check,
|
||||
Expand,
|
||||
Gift,
|
||||
HardDriveDownload,
|
||||
HardDriveUpload,
|
||||
RulerDimensionLine,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
interface ModelCardProps {
|
||||
model: ModelWithTotalPrice
|
||||
}
|
||||
|
||||
export function ModelCard({ model }: ModelCardProps) {
|
||||
// Prices are per token, multiply by 1M to get price per million tokens
|
||||
const inputPrice = parseFloat(model.pricing.input) * 1_000_000
|
||||
const outputPrice = parseFloat(model.pricing.output) * 1_000_000
|
||||
const cacheReadPrice = parseFloat(model.pricing.input_cache_read || "0") * 1_000_000
|
||||
const cacheWritePrice = parseFloat(model.pricing.input_cache_write || "0") * 1_000_000
|
||||
|
||||
const free = model.tags.includes("free")
|
||||
// Filter tags to only show vision and reasoning
|
||||
const displayTags = model.tags.filter((tag) => tag === "vision" || tag === "reasoning")
|
||||
|
||||
// Mobile collapsed/expanded state
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"relative cursor-default px-8 pt-7 pb-5 flex flex-col justify-start bg-background border rounded-3xl transition-all hover:shadow-xl",
|
||||
// On mobile, visually hint at expandability
|
||||
"sm:cursor-default",
|
||||
].join(" ")}>
|
||||
{/* Header: always visible */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-semibold tracking-tight mb-2 flex items-center gap-2 justify-between">
|
||||
{model.name}
|
||||
{free && (
|
||||
<span className="inline-flex items-center text-sm font-medium text-green-500">
|
||||
<Gift className="size-4 mr-1" />
|
||||
Free!
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<p
|
||||
className={[
|
||||
"text-sm text-muted-foreground",
|
||||
// On mobile + collapsed: clamp description
|
||||
"sm:line-clamp-none",
|
||||
!expanded ? "line-clamp-2" : "",
|
||||
]
|
||||
.join(" ")
|
||||
.trim()}>
|
||||
{model.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content - pinned to bottom */}
|
||||
<div className="overflow-x-auto mt-auto">
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
{/* Provider: always visible if present */}
|
||||
{model.owned_by && (
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<Building2 className="size-4 inline-block mr-1.5" />
|
||||
Provider
|
||||
</td>
|
||||
<td className="py-1.5 text-right">{model.owned_by}</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Context Window: always visible */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<RulerDimensionLine className="size-4 inline-block mr-1.5" />
|
||||
Context Window
|
||||
</td>
|
||||
<td className="py-1.5 text-right font-mono">{formatTokens(model.context_window)}</td>
|
||||
</tr>
|
||||
|
||||
{/* Max Output Tokens: always visible on >=sm, expandable on mobile */}
|
||||
<tr
|
||||
className={["border-b border-border", expanded ? "table-row" : "hidden sm:table-row"].join(
|
||||
" ",
|
||||
)}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<Expand className="size-4 inline-block mr-1.5" />
|
||||
Max Output Tokens
|
||||
</td>
|
||||
<td className="py-1.5 text-right font-mono">{formatTokens(model.max_tokens)}</td>
|
||||
</tr>
|
||||
|
||||
{/* Input Price: always visible */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<ArrowRightToLine className="size-4 inline-block mr-1.5" />
|
||||
Input Price
|
||||
</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{inputPrice === 0 ? "Free" : `${formatCurrency(inputPrice)}/1M tokens`}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Output Price: always visible */}
|
||||
<tr
|
||||
className={[
|
||||
"border-b border-border",
|
||||
// Add subtle separation from toggle on mobile
|
||||
].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<ArrowLeftToLine className="size-4 inline-block mr-1.5" />
|
||||
Output Price
|
||||
</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{outputPrice === 0 ? "Free" : `${formatCurrency(outputPrice)}/1M tokens`}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Cache pricing: only visible on mobile when expanded, always visible on >=sm */}
|
||||
{cacheReadPrice > 0 && (
|
||||
<tr
|
||||
className={[
|
||||
"border-b border-border",
|
||||
expanded ? "table-row" : "hidden sm:table-row",
|
||||
].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<HardDriveUpload className="size-4 inline-block mr-1.5" />
|
||||
Cache Read
|
||||
</td>
|
||||
<td className="py-1.5 text-right">{formatCurrency(cacheReadPrice)}/1M tokens</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{cacheWritePrice > 0 && (
|
||||
<tr
|
||||
className={[
|
||||
"border-b border-border",
|
||||
expanded ? "table-row" : "hidden sm:table-row",
|
||||
].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<HardDriveDownload className="size-4 inline-block mr-1.5" />
|
||||
Cache Write
|
||||
</td>
|
||||
<td className="py-1.5 text-right">{formatCurrency(cacheWritePrice)}/1M tokens</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Tags row: only show if there are vision or reasoning tags */}
|
||||
{displayTags.length > 0 && (
|
||||
<tr className={[expanded ? "table-row" : "hidden sm:table-row"].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground align-top">Features</td>
|
||||
<td className="py-1.5">
|
||||
{displayTags.map((tag) => (
|
||||
<span key={tag} className="flex justify-end items-center text-xs capitalize">
|
||||
<Check className="size-3 m-1" />
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Mobile-only toggle row */}
|
||||
<tr className="sm:hidden">
|
||||
<td colSpan={2} className="pt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="w-full inline-flex items-center justify-center gap-1.5 text-xs font-medium text-primary">
|
||||
{expanded ? "Less" : "More"}
|
||||
{expanded ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
253
apps/web-roo-code/src/app/provider/pricing/page.tsx
Normal file
253
apps/web-roo-code/src/app/provider/pricing/page.tsx
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { ModelCard } from "./components/model-card"
|
||||
import { Model, ModelWithTotalPrice, ModelsResponse, SortOption } from "@/lib/types/models"
|
||||
import Link from "next/link"
|
||||
import { ChevronDown, CircleX, Loader, LoaderCircle, Search } from "lucide-react"
|
||||
|
||||
const API_URL = "https://api.roocode.com/proxy/v1/models?include_paid=true"
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What are AI model providers?",
|
||||
answer: "AI model providers offer various language models with different capabilities and pricing.",
|
||||
},
|
||||
{
|
||||
question: "How is pricing calculated?",
|
||||
answer: "Pricing is based on token usage for input and output, measured per million tokens, like pretty much any other provider out there.",
|
||||
},
|
||||
{
|
||||
question: "What is the Roo Code Cloud Provider?",
|
||||
answer: (
|
||||
<>
|
||||
<p>This is our very own model provider, optimized to work seamlessly with Roo Code Cloud.</p>
|
||||
<p>
|
||||
It offers a selection of state-of-the-art LLMs (both closed and open weight) we know work well with
|
||||
Roo for you to choose, with no markup.
|
||||
</p>
|
||||
<p>
|
||||
We also often feature 100% free models which labs share with us for the community to use and provide
|
||||
feedback.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "But how much does the Roo Code Cloud service cost?",
|
||||
answer: (
|
||||
<>
|
||||
Our{" "}
|
||||
<Link href="/pricing" className="underline hover:no-underline">
|
||||
service pricing is here.
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
function calculateTotalPrice(model: Model): number {
|
||||
return parseFloat(model.pricing.input) + parseFloat(model.pricing.output)
|
||||
}
|
||||
|
||||
function enrichModelWithTotalPrice(model: Model): ModelWithTotalPrice {
|
||||
return {
|
||||
...model,
|
||||
totalPrice: calculateTotalPrice(model),
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProviderPricingPage() {
|
||||
const [models, setModels] = useState<ModelWithTotalPrice[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [sortOption, setSortOption] = useState<SortOption>("alphabetical")
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchModels() {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const response = await fetch(API_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.statusText}`)
|
||||
}
|
||||
const data: ModelsResponse = await response.json()
|
||||
const enrichedModels = data.data.map(enrichModelWithTotalPrice)
|
||||
setModels(enrichedModels)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "An error occurred while fetching models")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchModels()
|
||||
}, [])
|
||||
|
||||
const filteredAndSortedModels = useMemo(() => {
|
||||
// Filter out deprecated models
|
||||
let filtered = models.filter((model) => !model.deprecated)
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase()
|
||||
filtered = filtered.filter((model) => {
|
||||
return (
|
||||
model.name.toLowerCase().includes(query) ||
|
||||
model.owned_by?.toLowerCase().includes(query) ||
|
||||
model.description.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Sort filtered results
|
||||
const sorted = [...filtered]
|
||||
switch (sortOption) {
|
||||
case "alphabetical":
|
||||
sorted.sort((a, b) => a.name.localeCompare(b.name))
|
||||
break
|
||||
case "price-asc":
|
||||
sorted.sort((a, b) => a.totalPrice - b.totalPrice)
|
||||
break
|
||||
case "price-desc":
|
||||
sorted.sort((a, b) => b.totalPrice - a.totalPrice)
|
||||
break
|
||||
case "context-window-asc":
|
||||
sorted.sort((a, b) => a.context_window - b.context_window)
|
||||
break
|
||||
case "context-window-desc":
|
||||
sorted.sort((a, b) => b.context_window - a.context_window)
|
||||
break
|
||||
}
|
||||
|
||||
return sorted
|
||||
}, [models, searchQuery, sortOption])
|
||||
|
||||
// Count non-deprecated models for the display
|
||||
const nonDeprecatedCount = useMemo(() => models.filter((model) => !model.deprecated).length, [models])
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="relative overflow-hidden py-16">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">
|
||||
Roo Code Cloud Provider Pricing
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl md:text-lg text-muted-foreground">
|
||||
See pricing and features for all models we offer in our selection.
|
||||
<br />
|
||||
You can always bring your own key (
|
||||
<Link href="#faq" className="underline hover:no-underline">
|
||||
FAQ
|
||||
</Link>
|
||||
).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-10 relative border-t border-b">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/0 via-violet-500/10 to-violet-500/0 dark:from-blue-500/10 dark:via-cyan-500/10 dark:to-purple-500/10" />
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search models..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full rounded-full border border-input bg-background px-10 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
/>
|
||||
|
||||
<div className="text-sm cursor-default text-muted-foreground absolute bg-background right-0 top-0 m-0.5 px-3 py-2 rounded-full">
|
||||
{filteredAndSortedModels.length} of {nonDeprecatedCount} models
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<select
|
||||
id="sort"
|
||||
value={sortOption}
|
||||
onChange={(e) => setSortOption(e.target.value as SortOption)}
|
||||
className="rounded-full cursor-pointer border border-input bg-background hover:bg-muted pl-4 w-full md:w-auto pr-9 py-2.5 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative appearance-none">
|
||||
<option value="alphabetical">Alphabetical</option>
|
||||
<option value="price-asc">Price: Low to High</option>
|
||||
<option value="price-desc">Price: High to Low</option>
|
||||
<option value="context-window-asc">Context Window: Small to Large</option>
|
||||
<option value="context-window-desc">Context Window: Large to Small</option>
|
||||
</select>
|
||||
<ChevronDown className="size-4 absolute right-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 ">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
{loading && (
|
||||
<div className="text-center pt-12 space-y-2 mb-4">
|
||||
<LoaderCircle className="size-8 text-muted-foreground mx-auto animate-spin" />
|
||||
<p className="text-lg">Loading model list...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-center pt-12 space-y-2">
|
||||
<CircleX className="size-8 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-lg">Oops, couldn't load the model list.</p>
|
||||
<p className="text-muted-foreground">Try again in a bit please.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && filteredAndSortedModels.length === 0 && (
|
||||
<div className="text-center pt-12 space-y-2">
|
||||
<Loader className="size-8 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-lg">No models match your search.</p>
|
||||
<p className="text-muted-foreground">
|
||||
Keep in mind we don't have every model under the sun – only the ones we think
|
||||
are worth using.
|
||||
<br />
|
||||
You can always use a third-party provider to access a wider selection.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && filteredAndSortedModels.length > 0 && (
|
||||
<div className="grid gap-4 pt-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredAndSortedModels.map((model) => (
|
||||
<ModelCard key={model.id} model={model} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="bg-background my-16 relative z-50">
|
||||
<a id="faq" />
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Frequently Asked Questions</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 grid max-w-5xl gap-8 md:grid-cols-2">
|
||||
{faqs.map((faq, index) => (
|
||||
<div key={index} className="rounded-lg border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">{faq.question}</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{faq.answer}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
22
apps/web-roo-code/src/lib/formatters.ts
Normal file
22
apps/web-roo-code/src/lib/formatters.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})
|
||||
|
||||
export const formatCurrency = (amount: number) => formatter.format(amount)
|
||||
|
||||
export const formatTokens = (tokens: number) => {
|
||||
if (tokens < 1000) {
|
||||
return tokens.toString()
|
||||
}
|
||||
|
||||
if (tokens < 1000000) {
|
||||
return `${(tokens / 1000).toFixed(1)}K`
|
||||
}
|
||||
|
||||
if (tokens < 1000000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`
|
||||
}
|
||||
|
||||
return `${(tokens / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
31
apps/web-roo-code/src/lib/types/models.ts
Normal file
31
apps/web-roo-code/src/lib/types/models.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
export interface ModelPricing {
|
||||
input: string
|
||||
output: string
|
||||
input_cache_read: string
|
||||
input_cache_write: string
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
id: string
|
||||
object: string
|
||||
created: number
|
||||
owned_by: string
|
||||
name: string
|
||||
description: string
|
||||
context_window: number
|
||||
max_tokens: number
|
||||
type: string
|
||||
tags: string[]
|
||||
pricing: ModelPricing
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
export interface ModelsResponse {
|
||||
data: Model[]
|
||||
}
|
||||
|
||||
export interface ModelWithTotalPrice extends Model {
|
||||
totalPrice: number
|
||||
}
|
||||
|
||||
export type SortOption = "alphabetical" | "price-asc" | "price-desc" | "context-window-asc" | "context-window-desc"
|
||||
Loading…
Add table
Reference in a new issue