refactor(auth): replace dashboard sessions with stateless tokens and session control (#6329)

* refactor(auth): replace dashboard sessions with stateless tokens

* feat(auth): harden session issuance and distributed enforcement

* fix(proxy): preserve trusted proxy compatibility defaults

* refactor: address dashboard auth review feedback

* refactor: remove classic frontend and flatten web app
This commit is contained in:
Calcium-Ion
2026-07-20 16:48:43 +08:00
committed by GitHub
parent 5a6c53d496
commit 31d70fca39
1605 changed files with 17511 additions and 147913 deletions
+31
View File
@@ -0,0 +1,31 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { api } from '@/lib/api'
import type { PricingData } from './types'
// ----------------------------------------------------------------------------
// Pricing APIs
// ----------------------------------------------------------------------------
// Get model pricing data
export async function getPricing(): Promise<PricingData> {
const res = await api.get('/api/pricing')
return res.data
}
@@ -0,0 +1,454 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Tag as TagIcon } from 'lucide-react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { StaticDataTable } from '@/components/data-table'
import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'
import { useSystemConfigStore } from '@/stores/system-config-store'
import {
BILLING_PRICING_VARS,
MATCH_CONTAINS,
MATCH_EQ,
MATCH_EXISTS,
MATCH_GTE,
MATCH_LT,
MATCH_RANGE,
SOURCE_TIME,
normalizeTierLabel,
parseTiersFromExpr,
splitBillingExprAndRequestRules,
tryParseRequestRuleExpr,
type ParsedTier,
type RequestCondition,
type RequestRuleGroup,
type TierCondition,
} from '../lib/billing-expr'
type DynamicPricingBreakdownProps = {
billingExpr: string | null | undefined
/**
* Label of the tier that fired for the current request. When provided,
* the corresponding row is highlighted and tagged as "Matched". Used by
* the usage-log details dialog to show which tier the engine selected.
*/
matchedTierLabel?: string | null
/**
* Hide cache-pricing columns regardless of the per-tier values. The log
* details dialog passes this when the actual request did not consume any
* cache tokens, so users only see pricing rows that were relevant to the
* call they are inspecting. Defaults to false (show all configured prices).
*/
hideCacheColumns?: boolean
/**
* Dense rendering for the usage-log details dialog: drops the colored
* icon header and uses the dialog's small text sizes. Defaults to false.
*/
compact?: boolean
}
const VAR_LABELS: Record<string, string> = {
p: 'Input',
c: 'Output',
len: 'Length',
}
const OP_LABELS: Record<string, string> = {
'<': '<',
'<=': '≤',
'>': '>',
'>=': '≥',
}
const TIME_FUNC_LABELS: Record<string, string> = {
hour: 'Hour',
minute: 'Minute',
weekday: 'Weekday',
month: 'Month',
day: 'Day',
}
function formatTokenHint(value: string | number): string {
const n = Number(value)
if (!Number.isFinite(n) || n === 0) return ''
if (n >= 1_000_000) {
return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`
}
if (n >= 1000) {
return `${(n / 1000).toFixed(n % 1000 === 0 ? 0 : 1)}K`
}
return String(n)
}
function formatConditionSummary(
conditions: TierCondition[],
t: (key: string) => string
): string {
return conditions
.map((c) => {
const varLabel = t(VAR_LABELS[c.var] || c.var)
const hint = formatTokenHint(c.value)
return `${varLabel} ${OP_LABELS[c.op] || c.op} ${hint || c.value}`
})
.filter(Boolean)
.join(' && ')
}
function describeCondition(
cond: RequestCondition,
t: (key: string) => string
): string {
if (cond.source === SOURCE_TIME) {
const fn = t(TIME_FUNC_LABELS[cond.timeFunc] || cond.timeFunc)
const tz = cond.timezone || 'UTC'
if (cond.mode === MATCH_RANGE) {
return `${fn} ${cond.rangeStart}:00~${cond.rangeEnd}:00 (${tz})`
}
const opMap: Record<string, string> = {
[MATCH_EQ]: '=',
[MATCH_GTE]: '≥',
[MATCH_LT]: '<',
}
return `${fn} ${opMap[cond.mode] || '='} ${cond.value} (${tz})`
}
const src = cond.source === 'header' ? t('Header') : t('Body param')
const path = cond.path || ''
if (cond.mode === MATCH_EXISTS) return `${src} ${path} ${t('Exists')}`
if (cond.mode === MATCH_CONTAINS) {
return `${src} ${path} ${t('Contains')} "${cond.value}"`
}
const opMap: Record<string, string> = {
eq: '=',
gt: '>',
gte: '≥',
lt: '<',
lte: '≤',
}
return `${src} ${path} ${opMap[cond.mode] || '='} ${cond.value}`
}
function describeGroup(
group: RequestRuleGroup,
t: (key: string) => string
): string {
return (group.conditions || [])
.map((c) => describeCondition(c, t))
.join(' && ')
}
export function DynamicPricingBreakdown({
billingExpr,
matchedTierLabel,
hideCacheColumns = false,
compact = false,
}: DynamicPricingBreakdownProps) {
const { t } = useTranslation()
const expr = billingExpr || ''
const currency = useSystemConfigStore((s) => s.config.currency)
const { symbol, rate } = useMemo(() => {
if (currency.quotaDisplayType === 'CNY') {
return { symbol: '¥', rate: currency.usdExchangeRate || 7 }
}
if (currency.quotaDisplayType === 'CUSTOM') {
return {
symbol: currency.customCurrencySymbol || '¤',
rate: currency.customCurrencyExchangeRate || 1,
}
}
return { symbol: '$', rate: 1 }
}, [currency])
const { tiers, ruleGroups } = useMemo(() => {
const split = splitBillingExprAndRequestRules(expr)
const parsedTiers = parseTiersFromExpr(split.billingExpr)
const parsedRules = tryParseRequestRuleExpr(split.requestRuleExpr || '')
return {
tiers: parsedTiers,
ruleGroups: parsedRules || [],
}
}, [expr])
const hasTiers = tiers.length > 0
const hasRules = ruleGroups.length > 0
const normalizedMatchedTierLabel = normalizeTierLabel(
matchedTierLabel ?? undefined
)
if (!expr) return null
if (!hasTiers) {
return (
<section className={cn('min-w-0', !compact && 'py-4')}>
{!compact && (
<div className='mb-3 flex items-center gap-2'>
<span className='inline-flex size-6 items-center justify-center rounded-lg bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
<TagIcon className='size-3.5' />
</span>
<div>
<div className='text-foreground text-base font-medium'>
{t('Special billing expression')}
</div>
<div className='text-muted-foreground text-xs'>
{t('Unable to parse structured pricing')}
</div>
</div>
</div>
)}
<div className='text-muted-foreground mb-1 text-[10px] font-medium tracking-wider uppercase'>
{t('Raw expression')}
</div>
<code className='text-muted-foreground block text-xs break-all'>
{expr}
</code>
</section>
)
}
const visiblePriceFields = BILLING_PRICING_VARS.filter((v) => {
if (!hasTiers) return false
if (hideCacheColumns && v.group === 'cache') return false
return tiers.some(
(tier) => Number(tier[v.field as string as keyof ParsedTier] || 0) > 0
)
})
return (
<section className={cn('min-w-0', !compact && 'py-3 sm:py-4')}>
{!compact && (
<div className='mb-3 flex items-start gap-2 sm:mb-4'>
<span className='mt-0.5 inline-flex size-6 items-center justify-center rounded-lg bg-amber-100 text-amber-700 shadow-sm dark:bg-amber-500/20 dark:text-amber-300'>
<TagIcon className='size-3.5' />
</span>
<div>
<div className='text-foreground text-base font-medium'>
{t('Dynamic Pricing')}
</div>
<div className='text-muted-foreground text-xs'>
{t('Prices vary by usage tier and request conditions')}
</div>
</div>
</div>
)}
{hasTiers && (
<div className={cn(compact ? cn(hasRules && 'mb-2') : 'mb-3 sm:mb-4')}>
<div
className={
compact
? 'text-muted-foreground mb-1.5 text-xs font-medium'
: 'text-foreground mb-2 text-sm font-semibold'
}
>
{t('Tiered price table')}
</div>
<div className='space-y-1.5 sm:hidden'>
{tiers.map((tier, i) => {
const condSummary = formatConditionSummary(tier.conditions, t)
const isMatched =
matchedTierLabel != null &&
matchedTierLabel !== '' &&
tier.label === matchedTierLabel
return (
<div
key={`tier-mobile-${i}`}
className={cn(
'rounded-md border p-2',
isMatched && 'border-emerald-500/40 bg-emerald-500/10'
)}
>
<div className='mb-1.5 flex flex-wrap items-center gap-1.5'>
<Badge
variant='secondary'
className='bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300'
>
{tier.label || t('Default')}
</Badge>
{isMatched && (
<Badge
variant='secondary'
className='bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
>
{t('Matched')}
</Badge>
)}
</div>
{condSummary && (
<div className='text-muted-foreground mb-1.5 text-xs'>
{condSummary}
</div>
)}
<div className='grid grid-cols-2 gap-x-3 gap-y-1.5'>
{visiblePriceFields.map((v) => {
const value = Number(
tier[v.field as string as keyof ParsedTier] || 0
)
return (
<div key={v.field} className='min-w-0'>
<div className='text-muted-foreground truncate text-[10px] font-medium tracking-wider uppercase'>
{t(v.shortLabel)}
</div>
<div
className={cn(
'truncate font-mono',
compact ? 'text-xs' : 'text-sm font-semibold'
)}
>
{value > 0
? `${symbol}${(value * rate).toFixed(4)}`
: '-'}
</div>
</div>
)
})}
</div>
</div>
)
})}
</div>
<StaticDataTable
className='hidden rounded-none border-0 sm:block'
tableClassName={
compact
? '[&_td]:text-xs [&_td_*]:text-xs [&_th]:text-xs [&_th_*]:text-xs'
: 'text-sm'
}
headerRowClassName='hover:bg-transparent'
data={tiers}
getRowKey={(_tier, index) => `tier-${index}`}
getRowClassName={(tier) => {
const isMatched =
normalizedMatchedTierLabel !== '' &&
normalizeTierLabel(tier.label) === normalizedMatchedTierLabel
return cn(
isMatched &&
'bg-emerald-50/70 hover:bg-emerald-50/70 dark:bg-emerald-500/10 dark:hover:bg-emerald-500/10'
)
}}
columns={[
{
id: 'tier',
header: t('Tier'),
className: cn(
'text-muted-foreground py-2 font-medium',
compact && 'h-8'
),
cellClassName: cn('align-top', compact ? 'py-2' : 'py-2.5'),
cell: (tier) => {
const condSummary = formatConditionSummary(tier.conditions, t)
const isMatched =
normalizedMatchedTierLabel !== '' &&
normalizeTierLabel(tier.label) ===
normalizedMatchedTierLabel
return (
<>
<div className='flex flex-wrap items-center gap-1.5'>
<Badge
variant='secondary'
className='bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300'
>
{tier.label || t('Default')}
</Badge>
{isMatched && (
<Badge
variant='secondary'
className='bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
>
{t('Matched')}
</Badge>
)}
</div>
{condSummary && (
<div className='text-muted-foreground mt-1 text-xs'>
{condSummary}
</div>
)}
</>
)
},
},
...visiblePriceFields.map((v, index) => ({
id: v.field ?? `price-${index}`,
header: t(v.shortLabel),
className: cn(
'text-muted-foreground py-2 text-right font-medium',
compact && 'h-8'
),
cellClassName: cn(
'text-right align-top font-mono',
compact ? 'py-2' : 'py-2.5'
),
cell: (tier: ParsedTier) => {
const value = Number(
tier[v.field as string as keyof ParsedTier] || 0
)
return value > 0 ? (
<span className={cn(!compact && 'font-semibold')}>
{`${symbol}${(value * rate).toFixed(4)}`}
</span>
) : (
'-'
)
},
})),
]}
/>
</div>
)}
{hasRules && (
<div>
<div
className={
compact
? 'text-muted-foreground mb-1.5 text-xs font-medium'
: 'text-foreground mb-2 text-sm font-semibold'
}
>
{t('Conditional multipliers')}
</div>
<ul className='space-y-1.5'>
{ruleGroups.map((group, gi) => (
<li
key={`group-${gi}`}
className='bg-muted/50 flex items-center justify-between gap-3 rounded-md px-3 py-2'
>
<span
className={cn(
'text-foreground break-all',
compact ? 'text-xs' : 'text-sm'
)}
>
{describeGroup(group, t)}
</span>
<Badge
variant='secondary'
className='shrink-0 bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300'
>
{group.multiplier}x
</Badge>
</li>
))}
</ul>
</div>
)}
</section>
)
}
+58
View File
@@ -0,0 +1,58 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Search } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
export interface EmptyStateProps {
searchQuery?: string
hasActiveFilters: boolean
onClearFilters: () => void
}
export function EmptyState(props: EmptyStateProps) {
const { t } = useTranslation()
const hasSearch = Boolean(props.searchQuery?.trim())
return (
<div className='flex min-h-[320px] flex-col items-center justify-center rounded-lg border border-dashed px-6 py-12 text-center'>
<Search className='text-muted-foreground/40 mb-3 size-10' />
<h3 className='text-foreground mb-1 text-base font-semibold'>
{t('No models found')}
</h3>
<p className='text-muted-foreground mb-5 max-w-xs text-sm'>
{hasSearch
? t(
'No results for "{{query}}". Try adjusting your search or filters.',
{ query: props.searchQuery }
)
: t('No models match your current filters.')}
</p>
{(props.hasActiveFilters || hasSearch) && (
<Button variant='outline' size='sm' onClick={props.onClearFilters}>
{t('Clear all filters')}
</Button>
)}
</div>
)
}
+31
View File
@@ -0,0 +1,31 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export { PricingSidebar } from './pricing-sidebar'
export { PricingToolbar } from './pricing-toolbar'
export { ModelCard } from './model-card'
export { ModelCardGrid } from './model-card-grid'
export { LoadingSkeleton } from './loading-skeleton'
export { EmptyState } from './empty-state'
export { SearchBar } from './search-bar'
export {
ModelDetails,
ModelDetailsContent,
ModelDetailsDrawer,
} from './model-details'
export { PricingTable } from './pricing-table'
+155
View File
@@ -0,0 +1,155 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Skeleton } from '@/components/ui/skeleton'
import { VIEW_MODES, type ViewMode } from '../constants'
export interface LoadingSkeletonProps {
viewMode?: ViewMode
}
export function LoadingSkeleton(props: LoadingSkeletonProps) {
const viewMode = props.viewMode ?? VIEW_MODES.CARD
return (
<div className='space-y-5'>
<div className='space-y-1.5'>
<Skeleton className='h-8 w-40' />
<Skeleton className='h-4 w-52' />
</div>
<Skeleton className='h-10 w-full rounded-lg' />
<FilterBarSkeleton />
{viewMode === VIEW_MODES.TABLE ? (
<TableContentSkeleton />
) : (
<CardContentSkeleton />
)}
</div>
)
}
function CardContentSkeleton() {
return (
<div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3'>
{Array.from({ length: 9 }).map((_, i) => (
<div key={i} className='rounded-xl border p-5'>
<div className='flex items-start justify-between gap-3'>
<div className='flex min-w-0 items-start gap-3'>
<Skeleton className='size-10 shrink-0 rounded-xl' />
<div className='min-w-0 flex-1 space-y-2'>
<Skeleton className='h-5 w-36' />
<Skeleton className='h-3.5 w-48' />
</div>
</div>
<Skeleton className='h-8 w-16 rounded-md' />
</div>
<div className='mt-4 space-y-2'>
<Skeleton className='h-3.5 w-full' />
<Skeleton className='h-3.5 w-4/5' />
</div>
<div className='mt-4 flex items-center gap-2'>
<Skeleton className='h-4 w-24' />
<Skeleton className='h-4 w-16' />
</div>
<div className='mt-2 flex items-center gap-3'>
<Skeleton className='h-3.5 w-14' />
<Skeleton className='h-3.5 w-14' />
<Skeleton className='h-3.5 w-8' />
</div>
</div>
))}
</div>
)
}
function FilterBarSkeleton() {
return (
<div className='space-y-3'>
<div className='flex items-center gap-3'>
<div className='flex flex-1 flex-wrap items-center gap-2'>
{[80, 90, 75, 85, 70].map((width, i) => (
<Skeleton
key={i}
className='h-8 rounded-lg'
style={{ width: `${width}px` }}
/>
))}
</div>
<div className='flex items-center gap-2'>
<Skeleton className='h-8 w-24 rounded-lg' />
<Skeleton className='h-8 w-20 rounded-lg' />
<Skeleton className='h-8 w-24' />
<Skeleton className='h-8 w-20 rounded-lg' />
</div>
</div>
<Skeleton className='h-5 w-24' />
</div>
)
}
function TableContentSkeleton() {
const columns = [
{ width: 200 },
{ width: 100 },
{ width: 100 },
{ width: 100 },
{ width: 80 },
{ width: 100 },
]
return (
<div className='space-y-4'>
<div className='overflow-hidden rounded-lg border'>
<div className='bg-muted/30 border-b px-4 py-3'>
<div className='flex items-center gap-4'>
{columns.map((col, i) => (
<Skeleton
key={i}
className='h-4'
style={{ width: `${col.width}px` }}
/>
))}
</div>
</div>
{Array.from({ length: 10 }).map((_, i) => (
<div
key={i}
className='flex items-center gap-4 border-b px-4 py-3 last:border-b-0'
>
{columns.map((col, j) => (
<Skeleton
key={j}
className='h-5'
style={{ width: `${col.width}px` }}
/>
))}
</div>
))}
</div>
<div className='flex items-center justify-between'>
<Skeleton className='h-5 w-32' />
<div className='flex items-center gap-2'>
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className='size-8' />
))}
</div>
</div>
</div>
)
}
@@ -0,0 +1,54 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useTranslation } from 'react-i18next'
import { StatusBadge, type StatusVariant } from '@/components/status-badge'
import { isDynamicPricingModel } from '../lib/dynamic-price'
import { isTokenBasedModel } from '../lib/model-helpers'
import type { PricingModel } from '../types'
interface ModelBillingModeBadgeProps {
model: PricingModel
className?: string
}
export function ModelBillingModeBadge(props: ModelBillingModeBadgeProps) {
const { t } = useTranslation()
let label = t('Per Request')
let variant: StatusVariant = 'purple'
if (isDynamicPricingModel(props.model)) {
label = t('Dynamic Pricing')
variant = 'warning'
} else if (isTokenBasedModel(props.model)) {
label = t('Token-based')
variant = 'info'
}
return (
<StatusBadge
label={label}
variant={variant}
copyable={false}
size='sm'
className={props.className}
/>
)
}
+130
View File
@@ -0,0 +1,130 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useQuery } from '@tanstack/react-query'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { getPerfMetricsSummary } from '@/features/performance-metrics/api'
import { DEFAULT_PRICING_PAGE_SIZE, DEFAULT_TOKEN_UNIT } from '../constants'
import type { PricingModel, TokenUnit } from '../types'
import { ModelCard } from './model-card'
import type { ModelPerfBadgeData } from './model-perf-badge'
export interface ModelCardGridProps {
models: PricingModel[]
onModelClick: (modelName: string) => void
priceRate?: number
usdExchangeRate?: number
tokenUnit?: TokenUnit
showRechargePrice?: boolean
selectedGroup?: string
}
export function ModelCardGrid(props: ModelCardGridProps) {
const { t } = useTranslation()
const [page, setPage] = useState(1)
const pageSize = DEFAULT_PRICING_PAGE_SIZE
const tokenUnit = props.tokenUnit ?? DEFAULT_TOKEN_UNIT
const totalPages = Math.max(1, Math.ceil(props.models.length / pageSize))
const currentPage = Math.min(page, totalPages)
const perfQuery = useQuery({
queryKey: ['perf-metrics-summary', 24],
queryFn: () => getPerfMetricsSummary(24),
staleTime: 60 * 1000,
retry: false,
})
const pagedModels = useMemo(() => {
const start = (currentPage - 1) * pageSize
return props.models.slice(start, start + pageSize)
}, [currentPage, pageSize, props.models])
const perfMap = useMemo(() => {
const map = new Map<string, ModelPerfBadgeData>()
for (const model of perfQuery.data?.data?.models ?? []) {
map.set(model.model_name, model)
}
return map
}, [perfQuery.data])
if (props.models.length === 0) {
return null
}
return (
<div className='space-y-4 sm:space-y-5'>
<div className='grid grid-cols-1 gap-3 sm:gap-4 md:grid-cols-2 lg:grid-cols-3'>
{pagedModels.map((model) => (
<ModelCard
key={model.id ?? model.model_name}
model={model}
tokenUnit={tokenUnit}
priceRate={props.priceRate}
usdExchangeRate={props.usdExchangeRate}
showRechargePrice={props.showRechargePrice}
selectedGroup={props.selectedGroup}
perf={perfMap.get(model.model_name || '')}
onClick={() => props.onModelClick(model.model_name || '')}
/>
))}
</div>
{totalPages > 1 && (
<div className='text-muted-foreground flex flex-col items-center justify-between gap-3 border-t px-4 py-3 text-sm sm:flex-row'>
<p className='text-muted-foreground'>
{t('Page {{current}} of {{total}}', {
current: currentPage,
total: totalPages,
})}
</p>
<div className='flex items-center gap-2'>
<Button
type='button'
variant='outline'
size='sm'
onClick={() => setPage((current) => Math.max(1, current - 1))}
disabled={currentPage <= 1}
className='gap-1.5'
>
<ChevronLeft className='size-4' />
{t('Previous page')}
</Button>
<Button
type='button'
variant='outline'
size='sm'
onClick={() =>
setPage((current) => Math.min(totalPages, current + 1))
}
disabled={currentPage >= totalPages}
className='gap-1.5'
>
{t('Next page')}
<ChevronRight className='size-4' />
</Button>
</div>
</div>
)}
</div>
)
}
+278
View File
@@ -0,0 +1,278 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { ChevronRight, Copy } from 'lucide-react'
import { memo, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn } from '@/lib/utils'
import { DEFAULT_TOKEN_UNIT } from '../constants'
import {
getDynamicDisplayGroupRatio,
getDynamicPricingSummary,
} from '../lib/dynamic-price'
import { parseTags } from '../lib/filters'
import { isTokenBasedModel } from '../lib/model-helpers'
import { formatPrice, formatRequestPrice } from '../lib/price'
import type { PricingModel, TokenUnit } from '../types'
import { ModelBillingModeBadge } from './model-billing-mode-badge'
import { ModelPerfBadge, type ModelPerfBadgeData } from './model-perf-badge'
export interface ModelCardProps {
model: PricingModel
onClick: () => void
priceRate?: number
usdExchangeRate?: number
tokenUnit?: TokenUnit
showRechargePrice?: boolean
selectedGroup?: string
perf?: ModelPerfBadgeData
}
export const ModelCard = memo(function ModelCard(props: ModelCardProps) {
const { t } = useTranslation()
const { copyToClipboard } = useCopyToClipboard()
const tokenUnit = props.tokenUnit ?? DEFAULT_TOKEN_UNIT
const priceRate = props.priceRate ?? 1
const usdExchangeRate = props.usdExchangeRate ?? 1
const showRechargePrice = props.showRechargePrice ?? false
const isTokenBased = isTokenBasedModel(props.model)
const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M'
const tags = parseTags(props.model.tags)
const groups = props.model.enable_groups || []
const endpoints = props.model.supported_endpoint_types || []
const modelIconKey = props.model.icon || props.model.vendor_icon
const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 28) : null
const initial = props.model.model_name?.charAt(0).toUpperCase() || '?'
const isDynamicPricing =
props.model.billing_mode === 'tiered_expr' &&
Boolean(props.model.billing_expr)
const hasCachedPrice = isTokenBased && props.model.cache_ratio != null
const dynamicSummary = isDynamicPricing
? getDynamicPricingSummary(props.model, {
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
groupRatioMultiplier: getDynamicDisplayGroupRatio(
props.model,
props.selectedGroup
),
})
: null
const primaryGroup = groups[0]
const bottomTags = [...endpoints.slice(0, 2), ...tags.slice(0, 2)]
const hiddenCount =
Math.max(groups.length - 1, 0) +
Math.max(endpoints.length - 2, 0) +
Math.max(tags.length - 2, 0)
const handleCopy = (e: React.MouseEvent) => {
e.stopPropagation()
copyToClipboard(props.model.model_name || '')
}
let priceSummary: ReactNode
if (dynamicSummary) {
if (dynamicSummary.isSpecialExpression) {
priceSummary = (
<span className='min-w-0'>
<span className='text-amber-700 dark:text-amber-300'>
{t('Special billing expression')}
</span>
<code className='text-muted-foreground/70 mt-0.5 line-clamp-1 block font-mono text-[11px] break-all'>
{dynamicSummary.rawExpression}
</code>
</span>
)
} else if (dynamicSummary.primaryEntries.length > 0) {
priceSummary = (
<>
{dynamicSummary.primaryEntries.map((entry) => (
<span
key={entry.key}
className='text-muted-foreground whitespace-nowrap'
>
{t(entry.shortLabel)}{' '}
<span className='text-foreground font-mono font-semibold'>
{entry.formatted}
</span>
</span>
))}
</>
)
} else {
priceSummary = (
<span className='text-muted-foreground text-sm'>
{t('Dynamic Pricing')}
</span>
)
}
} else if (isTokenBased) {
priceSummary = (
<>
<span className='text-muted-foreground whitespace-nowrap'>
{t('Input')}{' '}
<span className='text-foreground font-mono font-semibold'>
{formatPrice(
props.model,
'input',
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
props.selectedGroup
)}
</span>
</span>
<span className='text-muted-foreground whitespace-nowrap'>
{t('Output')}{' '}
<span className='text-foreground font-mono font-semibold'>
{formatPrice(
props.model,
'output',
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
props.selectedGroup
)}
</span>
</span>
{hasCachedPrice && (
<span className='text-muted-foreground whitespace-nowrap'>
{t('Cached')}{' '}
<span className='text-foreground font-mono font-semibold'>
{formatPrice(
props.model,
'cache',
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
props.selectedGroup
)}
</span>
</span>
)}
</>
)
} else {
priceSummary = (
<span className='text-muted-foreground whitespace-nowrap'>
<span className='text-foreground font-mono font-semibold'>
{formatRequestPrice(
props.model,
showRechargePrice,
priceRate,
usdExchangeRate,
props.selectedGroup
)}
</span>{' '}
/ {t('request')}
</span>
)
}
return (
<div
className={cn(
'group relative flex flex-col rounded-xl border p-3 transition-colors sm:p-5',
'hover:bg-muted/20'
)}
>
{/* Header: icon + name + price + actions */}
<div className='flex items-start justify-between gap-2.5 sm:gap-3'>
<div className='flex min-w-0 items-start gap-2.5 sm:gap-3'>
<div className='bg-muted/40 flex size-9 shrink-0 items-center justify-center rounded-lg sm:size-10 sm:rounded-xl'>
{modelIcon || (
<span className='text-muted-foreground text-sm font-bold'>
{initial}
</span>
)}
</div>
<div className='min-w-0'>
<h3 className='text-foreground truncate font-mono text-[15px] leading-tight font-bold'>
{props.model.model_name}
</h3>
<div className='mt-0.5 flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm sm:mt-1 sm:gap-x-3'>
{priceSummary}
</div>
</div>
</div>
<div className='flex shrink-0 items-center gap-1.5'>
<button
type='button'
onClick={props.onClick}
className='text-muted-foreground hover:text-foreground hover:bg-muted inline-flex items-center gap-1 rounded-md border px-2 py-1 text-xs transition-colors sm:px-2.5 sm:py-1.5'
>
{t('Details')}
<ChevronRight className='size-3.5' />
</button>
<button
type='button'
onClick={handleCopy}
className='text-muted-foreground hover:text-foreground hover:bg-muted rounded-md border p-1.5 transition-colors'
title={t('Copy')}
>
<Copy className='size-3.5' />
</button>
</div>
</div>
{/* Description */}
<p className='text-muted-foreground mt-2 line-clamp-1 flex-1 text-[13px] leading-relaxed sm:mt-4 sm:line-clamp-2 sm:min-h-[2.5rem]'>
{props.model.description || t('No description available.')}
</p>
{/* Footer: left metadata and right performance summary share row alignment */}
<div className='mt-2 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-2 gap-y-1 sm:mt-4'>
<div className='flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1'>
{primaryGroup && (
<span className='text-muted-foreground text-sm font-medium'>
{primaryGroup}
</span>
)}
<ModelBillingModeBadge model={props.model} />
</div>
<ModelPerfBadge perf={props.perf} className='row-span-2 self-start' />
<div className='flex min-w-0 flex-wrap items-center gap-x-2.5 gap-y-0.5 sm:gap-x-3 sm:gap-y-1'>
{bottomTags.map((item) => (
<span key={item} className='text-muted-foreground/70 text-xs'>
{item}
</span>
))}
<span className='text-muted-foreground/50 text-xs'>
{tokenUnitLabel}
</span>
{hiddenCount > 0 && (
<span className='text-muted-foreground/40 text-xs'>
+{hiddenCount}
</span>
)}
</div>
</div>
</div>
)
})
@@ -0,0 +1,793 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import {
ChevronRight,
Gauge,
KeyRound,
ScrollText,
Sigma,
Zap,
} from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { BundledLanguage } from 'shiki/bundle/web'
import {
CodeBlock,
CodeBlockCopyButton,
} from '@/components/ai-elements/code-block'
import {
StaticDataTable,
staticDataTableClassNames as tableStyles,
} from '@/components/data-table'
import { Badge } from '@/components/ui/badge'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { useStatus } from '@/hooks/use-status'
import {
buildRateLimits,
buildSupportedParameters,
formatRateLimit,
type SupportedParameter,
} from '../lib/mock-stats'
import { replaceModelInPath } from '../lib/model-helpers'
import type { PricingModel } from '../types'
// ---------------------------------------------------------------------------
// Code-sample registry
// ---------------------------------------------------------------------------
//
// Each sample is keyed by language and endpoint type. The endpoint type comes
// from the model's `supported_endpoint_types`; we render samples only for the
// types the model actually supports. This keeps copy-pasted code accurate and
// provider-shaped (OpenAI vs Anthropic vs Gemini, etc.).
type Lang = 'curl' | 'python' | 'typescript' | 'javascript'
const LANG_LABELS: Record<Lang, string> = {
curl: 'cURL',
python: 'Python',
typescript: 'TypeScript',
javascript: 'JavaScript',
}
const LANG_HIGHLIGHT: Record<Lang, BundledLanguage> = {
curl: 'bash',
python: 'python',
typescript: 'typescript',
javascript: 'javascript',
}
type SampleContext = {
baseUrl: string
apiKeyEnv: string
modelName: string
endpointType: string
endpointPath: string
}
function buildChatSample(lang: Lang, ctx: SampleContext): string {
const url = `${ctx.baseUrl}${ctx.endpointPath}`
const isResponses = ctx.endpointType === 'openai-response'
const isReasoning = /^o[1-4]|reasoning|thinking|deepseek-r/i.test(
ctx.modelName
)
const userMessage = 'Explain quantum entanglement in one paragraph.'
const bodyJson = isResponses
? JSON.stringify({ model: ctx.modelName, input: userMessage }, null, 2)
: JSON.stringify(
{
model: ctx.modelName,
messages: [{ role: 'user', content: userMessage }],
...(isReasoning ? {} : { temperature: 0.7 }),
},
null,
2
)
const fnCall = isResponses ? 'responses.create' : 'chat.completions.create'
if (lang === 'curl') {
return [
`curl ${url} \\`,
` -H "Authorization: Bearer $${ctx.apiKeyEnv}" \\`,
` -H "Content-Type: application/json" \\`,
` -d '${bodyJson.replace(/\n/g, '\n ')}'`,
].join('\n')
}
if (lang === 'python') {
return [
'from openai import OpenAI',
'',
'client = OpenAI(',
` base_url="${ctx.baseUrl}/v1",`,
` api_key="<YOUR_API_KEY>",`,
')',
'',
isResponses
? `response = client.${fnCall}(\n model="${ctx.modelName}",\n input="${userMessage}",\n)\n\nprint(response.output_text)`
: `completion = client.${fnCall}(\n model="${ctx.modelName}",\n messages=[\n {"role": "user", "content": "${userMessage}"}\n ],\n)\n\nprint(completion.choices[0].message.content)`,
].join('\n')
}
if (lang === 'typescript') {
return [
`import OpenAI from 'openai'`,
'',
`const client = new OpenAI({`,
` baseURL: '${ctx.baseUrl}/v1',`,
` apiKey: process.env.${ctx.apiKeyEnv},`,
`})`,
'',
isResponses
? `const response = await client.${fnCall}({\n model: '${ctx.modelName}',\n input: '${userMessage}',\n})\n\nconsole.log(response.output_text)`
: `const completion = await client.${fnCall}({\n model: '${ctx.modelName}',\n messages: [{ role: 'user', content: '${userMessage}' }],\n})\n\nconsole.log(completion.choices[0].message.content)`,
].join('\n')
}
return [
`const response = await fetch('${url}', {`,
` method: 'POST',`,
` headers: {`,
` Authorization: \`Bearer \${process.env.${ctx.apiKeyEnv}}\`,`,
` 'Content-Type': 'application/json',`,
` },`,
` body: JSON.stringify(${bodyJson}),`,
`})`,
'',
`const data = await response.json()`,
`console.log(data)`,
].join('\n')
}
function buildAnthropicSample(lang: Lang, ctx: SampleContext): string {
const url = `${ctx.baseUrl}${ctx.endpointPath}`
const userMessage = 'Explain quantum entanglement in one paragraph.'
if (lang === 'curl') {
const body = JSON.stringify(
{
model: ctx.modelName,
max_tokens: 1024,
messages: [{ role: 'user', content: userMessage }],
},
null,
2
)
return [
`curl ${url} \\`,
` -H "x-api-key: $${ctx.apiKeyEnv}" \\`,
` -H "anthropic-version: 2023-06-01" \\`,
` -H "Content-Type: application/json" \\`,
` -d '${body.replace(/\n/g, '\n ')}'`,
].join('\n')
}
if (lang === 'python') {
return [
'import anthropic',
'',
'client = anthropic.Anthropic(',
` base_url="${ctx.baseUrl}",`,
` api_key="<YOUR_API_KEY>",`,
')',
'',
`message = client.messages.create(`,
` model="${ctx.modelName}",`,
` max_tokens=1024,`,
` messages=[{"role": "user", "content": "${userMessage}"}],`,
')',
'',
'print(message.content[0].text)',
].join('\n')
}
if (lang === 'typescript') {
return [
`import Anthropic from '@anthropic-ai/sdk'`,
'',
`const client = new Anthropic({`,
` baseURL: '${ctx.baseUrl}',`,
` apiKey: process.env.${ctx.apiKeyEnv},`,
`})`,
'',
`const message = await client.messages.create({`,
` model: '${ctx.modelName}',`,
` max_tokens: 1024,`,
` messages: [{ role: 'user', content: '${userMessage}' }],`,
`})`,
'',
`console.log(message.content[0].text)`,
].join('\n')
}
return [
`const response = await fetch('${url}', {`,
` method: 'POST',`,
` headers: {`,
` 'x-api-key': process.env.${ctx.apiKeyEnv},`,
` 'anthropic-version': '2023-06-01',`,
` 'Content-Type': 'application/json',`,
` },`,
` body: JSON.stringify({`,
` model: '${ctx.modelName}',`,
` max_tokens: 1024,`,
` messages: [{ role: 'user', content: '${userMessage}' }],`,
` }),`,
`})`,
'',
`const data = await response.json()`,
`console.log(data.content[0].text)`,
].join('\n')
}
function buildGeminiSample(lang: Lang, ctx: SampleContext): string {
const url = `${ctx.baseUrl}${ctx.endpointPath}?key=$${ctx.apiKeyEnv}`
const userMessage = 'Explain quantum entanglement in one paragraph.'
if (lang === 'curl') {
const body = JSON.stringify(
{ contents: [{ parts: [{ text: userMessage }] }] },
null,
2
)
return [
`curl '${url}' \\`,
` -H 'Content-Type: application/json' \\`,
` -d '${body.replace(/\n/g, '\n ')}'`,
].join('\n')
}
if (lang === 'python') {
return [
'import google.generativeai as genai',
'',
`genai.configure(api_key="<YOUR_API_KEY>")`,
'',
`model = genai.GenerativeModel("${ctx.modelName}")`,
`response = model.generate_content("${userMessage}")`,
'',
`print(response.text)`,
].join('\n')
}
if (lang === 'typescript') {
return [
`import { GoogleGenerativeAI } from '@google/generative-ai'`,
'',
`const genAI = new GoogleGenerativeAI(process.env.${ctx.apiKeyEnv}!)`,
`const model = genAI.getGenerativeModel({ model: '${ctx.modelName}' })`,
'',
`const result = await model.generateContent('${userMessage}')`,
`console.log(result.response.text())`,
].join('\n')
}
return [
`const response = await fetch('${url}', {`,
` method: 'POST',`,
` headers: { 'Content-Type': 'application/json' },`,
` body: JSON.stringify({`,
` contents: [{ parts: [{ text: '${userMessage}' }] }],`,
` }),`,
`})`,
'',
`const data = await response.json()`,
`console.log(data.candidates[0].content.parts[0].text)`,
].join('\n')
}
function buildEmbeddingSample(lang: Lang, ctx: SampleContext): string {
const url = `${ctx.baseUrl}${ctx.endpointPath}`
const text = 'The food was delicious and the waiter…'
if (lang === 'curl') {
const body = JSON.stringify({ model: ctx.modelName, input: text }, null, 2)
return [
`curl ${url} \\`,
` -H "Authorization: Bearer $${ctx.apiKeyEnv}" \\`,
` -H "Content-Type: application/json" \\`,
` -d '${body.replace(/\n/g, '\n ')}'`,
].join('\n')
}
if (lang === 'python') {
return [
'from openai import OpenAI',
'',
`client = OpenAI(base_url="${ctx.baseUrl}/v1", api_key="<YOUR_API_KEY>")`,
'',
'response = client.embeddings.create(',
` model="${ctx.modelName}",`,
` input="${text}",`,
')',
'',
'print(response.data[0].embedding[:8])',
].join('\n')
}
if (lang === 'typescript') {
return [
`import OpenAI from 'openai'`,
'',
`const client = new OpenAI({`,
` baseURL: '${ctx.baseUrl}/v1',`,
` apiKey: process.env.${ctx.apiKeyEnv},`,
`})`,
'',
`const response = await client.embeddings.create({`,
` model: '${ctx.modelName}',`,
` input: '${text}',`,
`})`,
'',
`console.log(response.data[0].embedding.slice(0, 8))`,
].join('\n')
}
return [
`const response = await fetch('${url}', {`,
` method: 'POST',`,
` headers: {`,
` Authorization: \`Bearer \${process.env.${ctx.apiKeyEnv}}\`,`,
` 'Content-Type': 'application/json',`,
` },`,
` body: JSON.stringify({`,
` model: '${ctx.modelName}',`,
` input: '${text}',`,
` }),`,
`})`,
'',
`const data = await response.json()`,
`console.log(data.data[0].embedding.slice(0, 8))`,
].join('\n')
}
function buildImageSample(lang: Lang, ctx: SampleContext): string {
const url = `${ctx.baseUrl}${ctx.endpointPath}`
const prompt = 'A serene koi pond at sunset, ukiyo-e style.'
if (lang === 'curl') {
const body = JSON.stringify(
{ model: ctx.modelName, prompt, size: '1024x1024', n: 1 },
null,
2
)
return [
`curl ${url} \\`,
` -H "Authorization: Bearer $${ctx.apiKeyEnv}" \\`,
` -H "Content-Type: application/json" \\`,
` -d '${body.replace(/\n/g, '\n ')}'`,
].join('\n')
}
if (lang === 'python') {
return [
'from openai import OpenAI',
'',
`client = OpenAI(base_url="${ctx.baseUrl}/v1", api_key="<YOUR_API_KEY>")`,
'',
'response = client.images.generate(',
` model="${ctx.modelName}",`,
` prompt="${prompt}",`,
` size="1024x1024",`,
` n=1,`,
')',
'',
'print(response.data[0].url)',
].join('\n')
}
if (lang === 'typescript') {
return [
`import OpenAI from 'openai'`,
'',
`const client = new OpenAI({`,
` baseURL: '${ctx.baseUrl}/v1',`,
` apiKey: process.env.${ctx.apiKeyEnv},`,
`})`,
'',
`const response = await client.images.generate({`,
` model: '${ctx.modelName}',`,
` prompt: '${prompt}',`,
` size: '1024x1024',`,
` n: 1,`,
`})`,
'',
`console.log(response.data[0].url)`,
].join('\n')
}
return [
`const response = await fetch('${url}', {`,
` method: 'POST',`,
` headers: {`,
` Authorization: \`Bearer \${process.env.${ctx.apiKeyEnv}}\`,`,
` 'Content-Type': 'application/json',`,
` },`,
` body: JSON.stringify({`,
` model: '${ctx.modelName}',`,
` prompt: '${prompt}',`,
` size: '1024x1024',`,
` n: 1,`,
` }),`,
`})`,
'',
`const data = await response.json()`,
`console.log(data.data[0].url)`,
].join('\n')
}
function buildSample(
lang: Lang,
endpointType: string,
ctx: SampleContext
): string {
if (endpointType === 'anthropic') return buildAnthropicSample(lang, ctx)
if (endpointType === 'gemini') return buildGeminiSample(lang, ctx)
if (endpointType === 'embeddings' || endpointType === 'jina-rerank')
return buildEmbeddingSample(lang, ctx)
if (endpointType === 'image-generation') return buildImageSample(lang, ctx)
return buildChatSample(lang, ctx)
}
// ---------------------------------------------------------------------------
// Code samples section
// ---------------------------------------------------------------------------
function CodeSamplesSection(props: {
model: PricingModel
endpointMap: Record<string, { path?: string; method?: string }>
}) {
const { t } = useTranslation()
const { status } = useStatus()
const baseUrl = useMemo(() => {
const candidate =
(status as Record<string, unknown> | null)?.server_address ??
(status as Record<string, unknown> | null)?.serverAddress ??
(status?.data as Record<string, unknown> | undefined)?.server_address ??
(status?.data as Record<string, unknown> | undefined)?.serverAddress
if (candidate && typeof candidate === 'string') {
return candidate.replace(/\/$/, '')
}
if (typeof window !== 'undefined') return window.location.origin
return 'https://api.example.com'
}, [status])
const endpoints = useMemo(() => {
const types = props.model.supported_endpoint_types || []
return types
.map((type) => {
const info = props.endpointMap[type] || {}
let path = info.path || ''
if (path && path.includes('{model}')) {
path = replaceModelInPath(path, props.model.model_name || '')
}
return { type, path, method: info.method || 'POST' }
})
.filter((e) => Boolean(e.path))
}, [props.model, props.endpointMap])
const [endpointType, setEndpointType] = useState<string>(
endpoints[0]?.type ?? ''
)
const [lang, setLang] = useState<Lang>('curl')
const activeEndpoint = useMemo(() => {
return endpoints.find((e) => e.type === endpointType) ?? endpoints[0]
}, [endpointType, endpoints])
if (endpoints.length === 0 || !activeEndpoint) {
return null
}
const code = buildSample(lang, activeEndpoint.type, {
baseUrl,
apiKeyEnv: 'NEW_API_KEY',
modelName: props.model.model_name || '',
endpointType: activeEndpoint.type,
endpointPath: activeEndpoint.path,
})
return (
<section>
<SectionTitle icon={ScrollText}>{t('Code samples')}</SectionTitle>
<div className='flex flex-wrap items-center gap-2'>
{endpoints.length > 1 && (
<Tabs value={endpointType} onValueChange={setEndpointType}>
<TabsList className='bg-muted/40 h-8 p-0.5'>
{endpoints.map((ep) => (
<TabsTrigger
key={ep.type}
value={ep.type}
className='h-7 px-2.5 text-xs'
>
{ep.type}
</TabsTrigger>
))}
</TabsList>
</Tabs>
)}
<Tabs
value={lang}
onValueChange={(v) => setLang(v as Lang)}
className='ml-auto'
>
<TabsList className='bg-muted/40 h-8 p-0.5'>
{(Object.keys(LANG_LABELS) as Lang[]).map((l) => (
<TabsTrigger key={l} value={l} className='h-7 px-2.5 text-xs'>
{LANG_LABELS[l]}
</TabsTrigger>
))}
</TabsList>
</Tabs>
</div>
<div className='mt-3'>
<CodeBlock code={code} language={LANG_HIGHLIGHT[lang]}>
<CodeBlockCopyButton />
</CodeBlock>
</div>
<p className='text-muted-foreground mt-2 text-xs'>
{t('Replace')}{' '}
<code className='bg-muted rounded px-1 py-0.5 font-mono text-[11px]'>
{'<YOUR_API_KEY>'}
</code>{' '}
{t('with the API key from your token settings.')}
</p>
</section>
)
}
// ---------------------------------------------------------------------------
// Supported parameters table
// ---------------------------------------------------------------------------
function SupportedParametersSection(props: { model: PricingModel }) {
const { t } = useTranslation()
const params = useMemo(
() => buildSupportedParameters(props.model),
[props.model]
)
if (params.length === 0) return null
return (
<section>
<SectionTitle icon={Sigma}>{t('Supported parameters')}</SectionTitle>
<StaticDataTable
className={tableStyles.sectionContainer}
headerRowClassName={tableStyles.mutedHeaderRow}
data={params}
getRowKey={(param) => param.name}
getRowClassName={() => 'hover:bg-muted/20'}
columns={[
{
id: 'parameter',
header: t('Parameter'),
className: 'h-9 w-44',
cellClassName: tableStyles.topCell,
cell: (p) => (
<div className='flex items-center gap-1.5'>
<code className='font-mono text-sm font-medium'>{p.name}</code>
{p.required && (
<Badge
variant='outline'
className='h-6 border-rose-500/40 px-2 text-sm text-rose-600 dark:text-rose-400'
>
{t('required')}
</Badge>
)}
</div>
),
},
{
id: 'type',
header: t('Type'),
className: 'h-9 w-24',
cellClassName: tableStyles.topCell,
cell: (p) => (
<Badge
variant='secondary'
className='h-7 rounded-full px-2.5 font-mono text-sm font-normal'
>
{p.type}
</Badge>
),
},
{
id: 'range',
header: t('Default / range'),
className: 'h-9 w-32',
cellClassName: tableStyles.topCell,
cell: (p) => <ParamRangeCell param={p} />,
},
{
id: 'description',
header: t('Description'),
className: 'h-9',
cellClassName: tableStyles.topMutedCell,
cell: (p) => t(p.descriptionKey),
},
]}
/>
</section>
)
}
function ParamRangeCell(props: { param: SupportedParameter }) {
const { defaultValue, range, enumValues } = props.param
if (defaultValue !== undefined) {
return (
<div className='flex flex-wrap items-center gap-1'>
<span className='text-muted-foreground text-sm'>=</span>
<code className='bg-muted rounded px-1.5 py-0.5 font-mono text-sm'>
{String(defaultValue)}
</code>
{range && (
<span className='text-muted-foreground text-sm'>{range}</span>
)}
</div>
)
}
if (range) {
return (
<span className='text-muted-foreground font-mono text-sm'>{range}</span>
)
}
if (enumValues && enumValues.length > 0) {
return (
<div className='flex flex-wrap gap-0.5'>
{enumValues.map((v) => (
<code
key={v}
className='bg-muted text-muted-foreground rounded px-1.5 py-0.5 font-mono text-sm'
>
{v}
</code>
))}
</div>
)
}
return <span className='text-muted-foreground/60 text-sm'></span>
}
// ---------------------------------------------------------------------------
// Rate-limits table
// ---------------------------------------------------------------------------
function RateLimitsSection(props: { model: PricingModel }) {
const { t } = useTranslation()
const limits = useMemo(() => buildRateLimits(props.model), [props.model])
if (limits.length === 0) return null
return (
<section>
<SectionTitle icon={Gauge}>{t('Rate limits')}</SectionTitle>
<StaticDataTable
className={tableStyles.sectionContainer}
headerRowClassName={tableStyles.mutedHeaderRow}
data={limits}
getRowKey={(limit) => limit.group}
getRowClassName={() => 'hover:bg-muted/20'}
columns={[
{
id: 'group',
header: t('Group'),
className: 'h-9',
cellClassName: 'py-2 font-mono',
cell: (limit) => limit.group,
},
{
id: 'rpm',
header: 'RPM',
className: 'h-9 text-right',
cellClassName: tableStyles.topNumericCell,
cell: (limit) => formatRateLimit(limit.rpm),
},
{
id: 'tpm',
header: 'TPM',
className: 'h-9 text-right',
cellClassName: tableStyles.topNumericCell,
cell: (limit) => formatRateLimit(limit.tpm),
},
{
id: 'rpd',
header: 'RPD',
className: 'h-9 text-right',
cellClassName: tableStyles.topNumericCell,
cell: (limit) => formatRateLimit(limit.rpd),
},
]}
/>
<p className='text-muted-foreground mt-2 text-[11px] leading-relaxed'>
{t(
'RPM = requests per minute, TPM = tokens per minute, RPD = requests per day. Limits apply per token group.'
)}
</p>
</section>
)
}
// ---------------------------------------------------------------------------
// Authentication preview
// ---------------------------------------------------------------------------
function AuthSection() {
const { t } = useTranslation()
return (
<section>
<SectionTitle icon={KeyRound}>{t('Authentication')}</SectionTitle>
<div className='border-border/60 bg-muted/20 flex items-start gap-2 rounded-lg border p-3'>
<ChevronRight className='text-muted-foreground mt-0.5 size-3.5 shrink-0' />
<div className='space-y-1.5 text-xs leading-relaxed'>
<p>
{t('All requests must include')}{' '}
<code className='bg-muted rounded px-1 py-0.5 font-mono text-[11px]'>
Authorization: Bearer &lt;TOKEN&gt;
</code>{' '}
{t('header. Anthropic-formatted endpoints accept the')}{' '}
<code className='bg-muted rounded px-1 py-0.5 font-mono text-[11px]'>
x-api-key
</code>{' '}
{t('header instead.')}
</p>
<p className='text-muted-foreground'>
{t(
'Generate tokens from the Tokens page; you can scope them to specific models, groups, IPs, and rate-limits.'
)}
</p>
</div>
</div>
</section>
)
}
// ---------------------------------------------------------------------------
// Composite API tab
// ---------------------------------------------------------------------------
export function ModelDetailsApi(props: {
model: PricingModel
endpointMap: Record<string, { path?: string; method?: string }>
}) {
return (
<div className='space-y-6'>
<CodeSamplesSection model={props.model} endpointMap={props.endpointMap} />
<AuthSection />
<SupportedParametersSection model={props.model} />
<RateLimitsSection model={props.model} />
</div>
)
}
// ---------------------------------------------------------------------------
// Local UI helpers
// ---------------------------------------------------------------------------
function SectionTitle(props: {
children: React.ReactNode
icon: React.ComponentType<{ className?: string }>
}) {
const Icon = props.icon
return (
<h3 className='text-foreground mb-3 flex items-center gap-1.5 text-sm font-semibold'>
<Icon className='text-muted-foreground/70 size-3.5' />
{props.children}
</h3>
)
}
// Re-export so the parent can keep its own SectionTitle if it wants:
export { Zap as ApiTabIcon }
@@ -0,0 +1,235 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import {
ArrowDownRight,
ArrowUpRight,
ExternalLink,
Trophy,
} from 'lucide-react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
StaticDataTable,
staticDataTableClassNames as tableStyles,
} from '@/components/data-table'
import { cn } from '@/lib/utils'
import {
buildAppRankings,
formatTokenVolume,
type AppRanking,
} from '../lib/mock-stats'
import type { PricingModel } from '../types'
const COMPACT_NUMBER = new Intl.NumberFormat(undefined, {
notation: 'compact',
maximumFractionDigits: 1,
})
function RankBadge(props: { rank: number }) {
const rank = props.rank
const isPodium = rank <= 3
const palette =
rank === 1
? 'bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300'
: rank === 2
? 'bg-slate-100 text-slate-700 dark:bg-slate-500/20 dark:text-slate-300'
: rank === 3
? 'bg-orange-100 text-orange-700 dark:bg-orange-500/20 dark:text-orange-300'
: 'bg-muted text-muted-foreground'
return (
<span
className={cn(
'inline-flex size-7 shrink-0 items-center justify-center rounded-md font-mono text-xs font-bold tabular-nums',
palette
)}
>
{isPodium ? <Trophy className='size-3.5' /> : rank}
</span>
)
}
function GrowthChip(props: { value: number }) {
const value = props.value
const isUp = value > 0
const isDown = value < 0
const palette = isUp
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300'
: isDown
? 'bg-rose-100 text-rose-700 dark:bg-rose-500/20 dark:text-rose-300'
: 'bg-muted text-muted-foreground'
const Icon = isUp ? ArrowUpRight : isDown ? ArrowDownRight : null
const formatted = `${value > 0 ? '+' : ''}${value.toFixed(1)}%`
return (
<span
className={cn(
'inline-flex items-center gap-0.5 rounded-md px-1.5 py-0.5 font-mono text-[11px] font-semibold tabular-nums',
palette
)}
>
{Icon && <Icon className='size-3' />}
{formatted}
</span>
)
}
function AppLink(props: { app: AppRanking }) {
if (!props.app.url) {
return <span className='text-foreground'>{props.app.name}</span>
}
return (
<a
href={props.app.url}
target='_blank'
rel='noreferrer'
className='text-foreground hover:text-primary inline-flex items-center gap-1 transition-colors'
>
{props.app.name}
<ExternalLink className='text-muted-foreground/40 size-3' />
</a>
)
}
export function ModelDetailsApps(props: { model: PricingModel }) {
const { t } = useTranslation()
const apps = useMemo(() => buildAppRankings(props.model, 12), [props.model])
if (apps.length === 0) {
return (
<div className='text-muted-foreground rounded-lg border p-6 text-center text-sm'>
{t('No app usage data available for this model.')}
</div>
)
}
const totalMonthlyTokens = apps.reduce((s, a) => s + a.monthly_tokens, 0)
const top = apps[0]
return (
<div className='flex flex-col gap-4'>
<div className='grid grid-cols-1 gap-2 sm:grid-cols-3'>
<div className='bg-muted/20 rounded-lg border p-3'>
<div className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
{t('Tracked apps')}
</div>
<div className='text-foreground mt-1 font-mono text-lg font-semibold tabular-nums'>
{apps.length}
</div>
<p className='text-muted-foreground/70 text-[11px]'>
{t('Top integrations using this model')}
</p>
</div>
<div className='bg-muted/20 rounded-lg border p-3'>
<div className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
{t('Monthly tokens')}
</div>
<div className='text-foreground mt-1 font-mono text-lg font-semibold tabular-nums'>
{COMPACT_NUMBER.format(totalMonthlyTokens)}
</div>
<p className='text-muted-foreground/70 text-[11px]'>
{t('Aggregated across the apps below')}
</p>
</div>
<div className='bg-muted/20 rounded-lg border p-3'>
<div className='text-muted-foreground text-[10px] font-medium tracking-wider uppercase'>
{t('#1 by usage')}
</div>
<div className='text-foreground mt-1 truncate text-base font-semibold'>
{top.name}
</div>
<p className='text-muted-foreground/70 truncate text-[11px]'>
{top.category} · {formatTokenVolume(top.monthly_tokens)}{' '}
{t('tokens / mo')}
</p>
</div>
</div>
<StaticDataTable
className='rounded-lg'
tableClassName='text-sm'
headerRowClassName={tableStyles.compactHeaderRow}
data={apps}
getRowKey={(app) => `${app.rank}-${app.name}`}
columns={[
{
id: 'rank',
header: '#',
className: cn(tableStyles.compactHeaderCell, 'w-12'),
cellClassName: tableStyles.compactCell,
cell: (app) => <RankBadge rank={app.rank} />,
},
{
id: 'app',
header: t('App'),
className: tableStyles.compactHeaderCell,
cellClassName: tableStyles.compactCell,
cell: (app) => (
<div className='flex items-center gap-3'>
<span className='bg-muted text-muted-foreground inline-flex size-7 shrink-0 items-center justify-center rounded-md font-bold'>
{app.initial}
</span>
<div className='min-w-0'>
<div className='text-sm font-medium'>
<AppLink app={app} />
</div>
<p className='text-muted-foreground line-clamp-1 text-sm'>
{app.description}
</p>
</div>
</div>
),
},
{
id: 'category',
header: t('Category'),
className: cn(
tableStyles.compactHeaderCell,
'hidden md:table-cell'
),
cellClassName: cn(
tableStyles.compactMutedCell,
'hidden md:table-cell'
),
cell: (app) => app.category,
},
{
id: 'monthly-tokens',
header: t('Monthly tokens'),
className: tableStyles.compactHeaderCellRight,
cellClassName: cn(tableStyles.compactNumericCell, 'tabular-nums'),
cell: (app) => formatTokenVolume(app.monthly_tokens),
},
{
id: 'growth',
header: t('30d change'),
className: tableStyles.compactHeaderCellRight,
cellClassName: cn(tableStyles.compactCell, 'text-right'),
cell: (app) => <GrowthChip value={app.growth_pct} />,
},
]}
/>
<p className='text-muted-foreground/60 text-[11px] leading-relaxed'>
{t(
'App rankings shown here are simulated for preview purposes and will be replaced with live usage data once the backend integration is complete.'
)}
</p>
</div>
)
}
@@ -0,0 +1,409 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { VChart } from '@visactor/react-vchart'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useThemeCustomization } from '@/context/theme-customization-provider'
import { getSuccessRateColor } from '@/features/performance-metrics/lib/format'
import { useThemeRadiusPx } from '@/lib/theme-radius'
import { useChartTheme } from '@/lib/use-chart-theme'
import { cn } from '@/lib/utils'
import { VCHART_OPTION } from '@/lib/vchart'
import type { LatencyTimePoint, UptimeDayPoint } from '../lib/mock-stats'
function formatHourLabel(iso: string): string {
const date = new Date(iso)
const hours = date.getHours()
return `${String(hours).padStart(2, '0')}:00`
}
function formatDayLabel(date: string): string {
const parsed = new Date(date)
if (date.includes('T')) {
return parsed.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
})
}
return parsed.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
})
}
function getChartThemeTokens(resolvedTheme: string) {
return {
textColor:
resolvedTheme === 'dark'
? 'rgba(255, 255, 255, 0.68)'
: 'rgba(15, 23, 42, 0.58)',
gridColor:
resolvedTheme === 'dark'
? 'rgba(255, 255, 255, 0.12)'
: 'rgba(15, 23, 42, 0.12)',
}
}
const UPTIME_AXIS_MAX = 100
const UPTIME_FOCUSED_AXIS_MIN = 95
const UPTIME_MINOR_OUTAGE_AXIS_MIN = 90
function toUptimeChartValue(value: number): number {
if (!Number.isFinite(value)) return 0
return Math.min(UPTIME_AXIS_MAX, Math.max(0, value))
}
function getUptimeAxisMin(values: number[]): number {
const finiteValues = values.filter((value) => Number.isFinite(value))
if (finiteValues.length === 0) return UPTIME_FOCUSED_AXIS_MIN
const minValue = Math.max(0, Math.min(...finiteValues))
if (minValue >= UPTIME_FOCUSED_AXIS_MIN) return UPTIME_FOCUSED_AXIS_MIN
if (minValue >= UPTIME_MINOR_OUTAGE_AXIS_MIN) {
return UPTIME_MINOR_OUTAGE_AXIS_MIN
}
return Math.max(0, Math.floor((minValue - 5) / 10) * 10)
}
function stripUptimePointSuffix(value: string): string {
return value.replace(/__(start|end)$/, '')
}
// ---------------------------------------------------------------------------
// Latency trend chart (24h, multi-group point-line chart)
// ---------------------------------------------------------------------------
export function LatencyTrendChart(props: {
series: LatencyTimePoint[]
className?: string
}) {
const { t } = useTranslation()
const { resolvedTheme, themeReady } = useChartTheme()
const { textColor, gridColor } = getChartThemeTokens(resolvedTheme)
const spec = useMemo(() => {
if (props.series.length === 0) return null
const data = props.series.map((point) => ({
time: formatHourLabel(point.timestamp),
group: point.group,
ttft: point.ttft_ms,
}))
return {
type: 'line' as const,
data: [{ id: 'latency', values: data }],
xField: 'time',
yField: 'ttft',
seriesField: 'group',
smooth: true,
point: {
visible: true,
style: { size: 5, stroke: '#ffffff', lineWidth: 1.5 },
},
line: {
style: { lineWidth: 2 },
},
legends: { visible: false },
tooltip: {
mark: {
title: { value: (d: { time: string }) => d.time },
content: [
{
key: t('Average TTFT'),
value: (d: { ttft: number }) => `${Math.round(d.ttft)} ms`,
},
],
},
},
axes: [
{
orient: 'bottom',
label: {
style: { fill: textColor, fontSize: 10 },
},
tick: { visible: false },
},
{
orient: 'left',
label: {
formatMethod: (val: number | string) => `${val} ms`,
style: { fill: textColor, fontSize: 10 },
},
grid: {
visible: true,
style: { lineDash: [3, 3], stroke: gridColor },
},
},
],
}
}, [gridColor, props.series, t, textColor])
if (props.series.length === 0) {
return (
<div
className={cn(
'text-muted-foreground flex h-48 items-center justify-center rounded-lg border text-xs',
props.className
)}
>
{t('No latency data available')}
</div>
)
}
return (
<div className={cn('h-64 sm:h-72', props.className)}>
{themeReady && spec && (
<VChart
key={`latency-${resolvedTheme}`}
spec={{
...spec,
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
background: 'transparent',
}}
option={VCHART_OPTION}
/>
)}
</div>
)
}
// ---------------------------------------------------------------------------
// Uptime trend chart (24h, point-line chart)
// ---------------------------------------------------------------------------
export function UptimeTrendChart(props: {
series: UptimeDayPoint[]
className?: string
}) {
const { t } = useTranslation()
const { resolvedTheme, themeReady } = useChartTheme()
const { textColor, gridColor } = getChartThemeTokens(resolvedTheme)
const spec = useMemo(() => {
if (props.series.length === 0) return null
const rawData = props.series.map((point) => ({
date: formatDayLabel(point.date),
uptime: toUptimeChartValue(point.uptime_pct),
incidents: point.incidents,
outage: point.outage_minutes,
}))
const data =
rawData.length === 1
? [
{ ...rawData[0], date: `${rawData[0].date}__start` },
{ ...rawData[0], date: `${rawData[0].date}__end` },
]
: rawData
const axisMin = getUptimeAxisMin(rawData.map((point) => point.uptime))
return {
type: 'line' as const,
data: [{ id: 'uptime', values: data }],
xField: 'date',
yField: 'uptime',
smooth: true,
line: {
style: { stroke: '#10b981', lineWidth: 2 },
},
point: {
visible: true,
style: {
size: 5,
stroke: '#ffffff',
lineWidth: 1.5,
fill: (datum: { uptime: number }) =>
getSuccessRateColor(datum.uptime),
},
},
tooltip: {
mark: {
title: {
value: (d: { date: string }) => stripUptimePointSuffix(d.date),
},
content: [
{
key: t('Uptime'),
value: (d: { uptime: number }) => `${d.uptime.toFixed(2)}%`,
},
{
key: t('Incidents'),
value: (d: { incidents: number }) => `${d.incidents}`,
},
{
key: t('Outage'),
value: (d: { outage: number }) => `${d.outage} ${t('minutes')}`,
},
],
},
},
axes: [
{
orient: 'bottom',
label: {
formatMethod: (val: number | string) =>
stripUptimePointSuffix(String(val)),
style: { fill: textColor, fontSize: 10 },
autoLimit: true,
},
tick: { visible: false },
},
{
orient: 'left',
min: axisMin,
max: UPTIME_AXIS_MAX,
label: {
formatMethod: (val: number | string) => `${val}%`,
style: { fill: textColor, fontSize: 10 },
},
grid: {
visible: true,
style: { lineDash: [3, 3], stroke: gridColor },
},
},
],
}
}, [gridColor, props.series, t, textColor])
if (props.series.length === 0) {
return (
<div
className={cn(
'text-muted-foreground flex h-48 items-center justify-center rounded-lg border text-xs',
props.className
)}
>
{t('No uptime data available')}
</div>
)
}
return (
<div className={cn('h-56 sm:h-64', props.className)}>
{themeReady && spec && (
<VChart
key={`uptime-trend-${resolvedTheme}`}
spec={{
...spec,
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
background: 'transparent',
}}
option={VCHART_OPTION}
/>
)}
</div>
)
}
// ---------------------------------------------------------------------------
// Throughput by group (horizontal bar)
// ---------------------------------------------------------------------------
export function ThroughputBarChart(props: {
rows: { group: string; throughput_tps: number }[]
className?: string
}) {
const { t } = useTranslation()
const { resolvedTheme, themeReady } = useChartTheme()
const { textColor, gridColor } = getChartThemeTokens(resolvedTheme)
const { customization } = useThemeCustomization()
const barRadius = useThemeRadiusPx(
'--radius-sm',
`${customization.preset}:${customization.radius}`
)
const filtered = useMemo(
() => props.rows.filter((r) => r.throughput_tps > 0),
[props.rows]
)
const spec = useMemo(() => {
if (filtered.length === 0) return null
return {
type: 'bar' as const,
direction: 'horizontal' as const,
data: [{ id: 'tput', values: filtered.map((r) => ({ ...r })) }],
xField: 'throughput_tps',
yField: 'group',
bar: {
style: {
fill: '#6366f1',
...(barRadius == null ? {} : { cornerRadius: barRadius }),
},
},
label: {
visible: true,
position: 'right',
style: { fontSize: 11, fill: textColor },
formatMethod: (text: string) => `${text} t/s`,
},
axes: [
{
orient: 'left',
label: { style: { fill: textColor, fontSize: 10 } },
tick: { visible: false },
},
{
orient: 'bottom',
label: { style: { fill: textColor, fontSize: 10 } },
grid: {
visible: true,
style: { lineDash: [3, 3], stroke: gridColor },
},
},
],
tooltip: {
mark: {
title: { value: (d: { group: string }) => d.group },
content: [
{
key: t('Throughput'),
value: (d: { throughput_tps: number }) =>
`${d.throughput_tps.toFixed(1)} t/s`,
},
],
},
},
}
}, [barRadius, filtered, gridColor, t, textColor])
if (filtered.length === 0) {
return null
}
return (
<div className={cn('h-48 sm:h-56', props.className)}>
{themeReady && spec && (
<VChart
key={`tput-${resolvedTheme}`}
spec={{
...spec,
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
background: 'transparent',
}}
option={VCHART_OPTION}
/>
)}
</div>
)
}
@@ -0,0 +1,375 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useQuery } from '@tanstack/react-query'
import { AlertTriangle, HeartPulse, Timer } from 'lucide-react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
StaticDataTable,
staticDataTableClassNames as tableStyles,
} from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge'
import { getPerfMetrics } from '@/features/performance-metrics/api'
import {
formatLatency,
formatThroughput,
formatUptimePct,
getSuccessRateTextClass,
} from '@/features/performance-metrics/lib/format'
import type { PerformanceGroup } from '@/features/performance-metrics/types'
import { cn } from '@/lib/utils'
import { type UptimeDayPoint } from '../lib/mock-stats'
import type { PricingModel } from '../types'
import { LatencyTrendChart, UptimeTrendChart } from './model-details-charts'
import { UptimeSparkline } from './model-details-uptime-sparkline'
function StatCard(props: {
icon: React.ComponentType<{ className?: string }>
label: string
value: React.ReactNode
hint?: string
valueClassName?: string
}) {
const Icon = props.icon
return (
<div className='bg-background flex flex-col gap-1 rounded-lg border p-3'>
<span className='text-muted-foreground inline-flex items-center gap-1.5 text-[10px] font-medium tracking-wider uppercase'>
<Icon className='size-3' />
{props.label}
</span>
<span
className={cn(
'text-foreground font-mono text-lg font-semibold tabular-nums',
props.valueClassName
)}
>
{props.value}
</span>
{props.hint && (
<span className='text-muted-foreground/70 text-[11px]'>
{props.hint}
</span>
)}
</div>
)
}
type PerformanceRow = {
group: string
avg_ttft_ms: number
avg_latency_ms: number
success_rate: number
avg_tps: number
}
function toUptimePct(value: number): number {
if (!Number.isFinite(value)) return 0
const clamped = Math.min(100, Math.max(0, value))
return Math.round(clamped * 100) / 100
}
function toLatencySeries(groups: PerformanceGroup[]) {
const byTs = new Map<number, number[]>()
for (const group of groups) {
for (const point of group.series) {
if (point.avg_ttft_ms <= 0) continue
const current = byTs.get(point.ts) ?? []
current.push(point.avg_ttft_ms)
byTs.set(point.ts, current)
}
}
return Array.from(byTs.entries())
.sort(([a], [b]) => a - b)
.map(([ts, values]) => ({
timestamp: new Date(ts * 1000).toISOString(),
group: 'latency',
ttft_ms: Math.round(
values.reduce((sum, value) => sum + value, 0) / values.length
),
}))
}
function toUptimeSeries(groups: PerformanceGroup[]): UptimeDayPoint[] {
const byTs = new Map<number, { rates: number[]; incidents: number }>()
for (const group of groups) {
for (const point of group.series) {
const current = byTs.get(point.ts) ?? { rates: [], incidents: 0 }
if (Number.isFinite(point.success_rate)) {
const successRate = toUptimePct(point.success_rate)
current.rates.push(successRate)
if (successRate < 100) current.incidents += 1
}
byTs.set(point.ts, current)
}
}
return Array.from(byTs.entries())
.sort(([a], [b]) => a - b)
.map(([ts, value]) => {
const uptime =
value.rates.length > 0
? value.rates.reduce((sum, rate) => sum + rate, 0) /
value.rates.length
: 0
return {
date: new Date(ts * 1000).toISOString(),
uptime_pct: toUptimePct(uptime),
incidents: value.incidents,
outage_minutes: 0,
}
})
}
function toGroupUptimeSeries(group: PerformanceGroup): UptimeDayPoint[] {
return group.series.map((point) => {
const successRate = toUptimePct(point.success_rate)
return {
date: new Date(point.ts * 1000).toISOString(),
uptime_pct: successRate,
incidents: successRate < 100 ? 1 : 0,
outage_minutes: 0,
}
})
}
function average(
rows: PerformanceRow[],
field: 'avg_ttft_ms' | 'avg_latency_ms'
) {
const values = rows.map((row) => row[field]).filter((value) => value > 0)
if (values.length === 0) return 0
return Math.round(
values.reduce((sum, value) => sum + value, 0) / values.length
)
}
export function ModelDetailsPerformance(props: { model: PricingModel }) {
const { t } = useTranslation()
const metricsQuery = useQuery({
queryKey: ['perf-metrics', props.model.model_name],
queryFn: () => getPerfMetrics(props.model.model_name, 24),
staleTime: 60 * 1000,
})
const groups = useMemo(
() => metricsQuery.data?.data.groups ?? [],
[metricsQuery.data]
)
const performances = useMemo<PerformanceRow[]>(
() =>
groups.map((group) => ({
group: group.group,
avg_ttft_ms: group.avg_ttft_ms,
avg_latency_ms: group.avg_latency_ms,
success_rate: group.success_rate,
avg_tps: group.avg_tps,
})),
[groups]
)
const latencySeries = useMemo(() => toLatencySeries(groups), [groups])
const uptimeSeries = useMemo(() => toUptimeSeries(groups), [groups])
const uptimeByGroup = useMemo<Record<string, UptimeDayPoint[]>>(() => {
const map: Record<string, UptimeDayPoint[]> = {}
for (const group of groups) {
map[group.group] = toGroupUptimeSeries(group)
}
return map
}, [groups])
if (metricsQuery.isLoading || performances.length === 0) {
return (
<div className='text-muted-foreground rounded-lg border p-6 text-center text-sm'>
{t('Performance data is not yet available for this model.')}
</div>
)
}
const tpsValues = performances
.map((p) => p.avg_tps)
.filter((value) => value > 0)
const avgTps =
tpsValues.length > 0
? tpsValues.reduce((sum, value) => sum + value, 0) / tpsValues.length
: 0
const avgLatency = average(performances, 'avg_latency_ms')
const successRates = performances
.map((perf) => perf.success_rate)
.filter((value) => Number.isFinite(value))
const successRate =
successRates.length > 0
? successRates.reduce((sum, value) => sum + value, 0) /
successRates.length
: 0
const incidentCount = uptimeSeries.reduce((s, p) => s + p.incidents, 0)
return (
<div className='flex flex-col gap-4'>
<div className='grid grid-cols-1 gap-2 sm:grid-cols-3'>
<StatCard
icon={Timer}
label='TPS'
value={formatThroughput(avgTps)}
hint={t('Sustained tokens per second')}
/>
<StatCard
icon={Timer}
label={t('Average latency')}
value={formatLatency(avgLatency)}
/>
<StatCard
icon={HeartPulse}
label={t('Success rate')}
value={formatUptimePct(successRate)}
hint={
incidentCount > 0
? t('{{count}} incidents in the last 24 hours', {
count: incidentCount,
})
: t('No incidents in the last 24 hours')
}
valueClassName={getSuccessRateTextClass(successRate)}
/>
</div>
<section>
<SectionHeader
icon={HeartPulse}
title={t('Per-group performance')}
description={t('Average latency, TTFT, TPS, and success rate')}
/>
<StaticDataTable
className='rounded-lg'
tableClassName='text-sm'
headerRowClassName={tableStyles.compactHeaderRow}
data={performances}
getRowKey={(perf) => perf.group}
columns={[
{
id: 'group',
header: t('Group'),
className: tableStyles.compactHeaderCell,
cellClassName: tableStyles.compactCell,
cell: (perf) => <GroupBadge group={perf.group} size='sm' />,
},
{
id: 'tps',
header: 'TPS',
className: tableStyles.compactHeaderCellRight,
cellClassName: tableStyles.compactNumericCell,
cell: (perf) => formatThroughput(perf.avg_tps),
},
{
id: 'ttft',
header: t('Average TTFT'),
className: tableStyles.compactHeaderCellRight,
cellClassName: tableStyles.compactNumericCell,
cell: (perf) => formatLatency(perf.avg_ttft_ms),
},
{
id: 'latency',
header: t('Average latency'),
className: tableStyles.compactHeaderCellRight,
cellClassName: tableStyles.compactMutedNumericCell,
cell: (perf) => formatLatency(perf.avg_latency_ms),
},
{
id: 'success',
header: t('Success rate'),
className: cn(tableStyles.compactHeaderCell, 'min-w-[180px]'),
cellClassName: tableStyles.compactCell,
cell: (perf) => (
<UptimeSparkline
size='sm'
series={uptimeByGroup[perf.group] ?? []}
/>
),
},
]}
/>
</section>
<section>
<SectionHeader
icon={Timer}
title={t('Latency trend (last 24h)')}
description={t('Average TTFT')}
/>
<LatencyTrendChart series={latencySeries} />
</section>
<section>
<SectionHeader
icon={HeartPulse}
title={t('Availability (last 24h)')}
description={
incidentCount > 0
? t(
'Request success rate; {{incidents}} incident buckets in the last 24 hours',
{
incidents: incidentCount,
}
)
: t('Request success rate sampled over the last 24 hours')
}
accent={
incidentCount > 0 ? (
<span className='inline-flex items-center gap-1 text-amber-600 dark:text-amber-400'>
<AlertTriangle className='size-3.5' />
{t('{{count}} incidents', {
count: incidentCount,
})}
</span>
) : null
}
/>
<UptimeTrendChart series={uptimeSeries} />
</section>
</div>
)
}
function SectionHeader(props: {
icon: React.ComponentType<{ className?: string }>
title: string
description?: string
accent?: React.ReactNode
}) {
const Icon = props.icon
return (
<div className='mb-2 flex flex-wrap items-center justify-between gap-2'>
<div className='flex min-w-0 items-center gap-2'>
<Icon className='text-muted-foreground/70 size-3.5 shrink-0' />
<div className='min-w-0'>
<div className='text-foreground text-sm font-semibold'>
{props.title}
</div>
{props.description && (
<p className='text-muted-foreground/80 text-xs'>
{props.description}
</p>
)}
</div>
</div>
{props.accent && (
<div className='shrink-0 text-xs font-medium'>{props.accent}</div>
)}
</div>
)
}
@@ -0,0 +1,217 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Activity, AlertCircle, CheckCircle2 } from 'lucide-react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import {
formatUptimePct,
getSuccessRateDotClass,
getSuccessRateTextClass,
} from '@/features/performance-metrics/lib/format'
import { cn } from '@/lib/utils'
import { aggregateUptime, type UptimeDayPoint } from '../lib/mock-stats'
// ---------------------------------------------------------------------------
// Uptime sparkline
// ---------------------------------------------------------------------------
//
// Compact 30-day uptime visualisation: a row of small coloured bars where:
// - Bar colour reflects per-day uptime (green / amber / red)
// - Bar height reflects severity (the worse the day, the shorter the bar)
// - Hovering a bar reveals the exact date and uptime
//
// Useful as a header strip ("at-a-glance" status) and as a per-row visual
// inside the per-group performance table.
type SparklineSize = 'sm' | 'md'
type UptimeSparklineProps = {
series: UptimeDayPoint[]
size?: SparklineSize
showOverall?: boolean
emptyLabel?: string
className?: string
}
function heightFor(uptime: number): string {
if (uptime >= 99.9) return 'h-full'
if (uptime >= 99.0) return 'h-[88%]'
if (uptime >= 95.0) return 'h-[72%]'
if (uptime >= 90.0) return 'h-[55%]'
return 'h-[40%]'
}
export function UptimeSparkline(props: UptimeSparklineProps) {
const size = props.size ?? 'md'
const showOverall = props.showOverall ?? true
if (props.series.length === 0) {
return (
<span className={cn('text-muted-foreground text-xs', props.className)}>
{props.emptyLabel ?? '—'}
</span>
)
}
const overall =
props.series.reduce((s, p) => s + p.uptime_pct, 0) / props.series.length
const containerHeight = size === 'sm' ? 'h-3.5' : 'h-5'
const barWidth = size === 'sm' ? 'w-[3px]' : 'w-1'
const gap = size === 'sm' ? 'gap-px' : 'gap-[2px]'
return (
<div className={cn('flex items-center gap-2', props.className)}>
<div
className={cn('flex items-end', containerHeight, gap)}
role='img'
aria-label={`30 day uptime ${overall.toFixed(2)}%`}
>
{props.series.map((day) => (
<Tooltip key={day.date}>
<TooltipTrigger
render={
<div
className={cn(
'rounded-sm transition-opacity hover:opacity-80',
barWidth,
containerHeight,
'flex items-end'
)}
/>
}
>
<div
className={cn(
'w-full rounded-sm',
getSuccessRateDotClass(day.uptime_pct),
heightFor(day.uptime_pct)
)}
aria-hidden
/>
</TooltipTrigger>
<TooltipContent side='top' className='font-mono text-xs'>
<div className='font-medium'>{day.date}</div>
<div>{day.uptime_pct.toFixed(2)}%</div>
{day.outage_minutes > 0 && (
<div className='text-muted-foreground'>
{day.outage_minutes} min outage
</div>
)}
</TooltipContent>
</Tooltip>
))}
</div>
{showOverall && (
<span
className={cn(
'font-mono text-sm font-semibold tabular-nums',
getSuccessRateTextClass(overall)
)}
>
{overall.toFixed(1)}%
</span>
)}
</div>
)
}
// ---------------------------------------------------------------------------
// Uptime status row — sparkline + summary text + status icon
// ---------------------------------------------------------------------------
export function UptimeStatusRow(props: {
series: UptimeDayPoint[]
className?: string
}) {
const { t } = useTranslation()
const summary = useMemo(() => aggregateUptime(props.series), [props.series])
const status = useMemo(() => {
if (summary.uptime_pct >= 99.9) return 'operational'
if (summary.uptime_pct >= 99.0) return 'minor'
if (summary.uptime_pct >= 95.0) return 'degraded'
return 'major'
}, [summary.uptime_pct])
const StatusIcon =
status === 'operational'
? CheckCircle2
: status === 'minor'
? Activity
: AlertCircle
const statusColour =
status === 'operational'
? 'text-emerald-600 dark:text-emerald-400'
: status === 'minor'
? 'text-emerald-600 dark:text-emerald-400'
: status === 'degraded'
? 'text-amber-600 dark:text-amber-400'
: 'text-rose-600 dark:text-rose-400'
const statusLabel =
status === 'operational'
? t('All systems operational')
: status === 'minor'
? t('Minor blips in the last 30 days')
: status === 'degraded'
? t('Degraded performance recently')
: t('Significant outages detected')
return (
<div
className={cn(
'border-border/60 bg-muted/30 flex flex-wrap items-center gap-3 rounded-lg border px-3 py-2 sm:gap-4 sm:px-4',
props.className
)}
>
<div className='flex items-center gap-2'>
<StatusIcon className={cn('size-4 shrink-0', statusColour)} />
<span className='text-sm font-medium'>{t('Last 30 days uptime')}</span>
</div>
<UptimeSparkline series={props.series} className='ml-auto' />
<div className='flex items-center gap-3 text-xs'>
<span className={cn('font-medium', statusColour)}>{statusLabel}</span>
{summary.incidents > 0 && (
<span className='text-muted-foreground'>
{summary.incidents}{' '}
{summary.incidents === 1 ? t('incident') : t('incidents')}
</span>
)}
{summary.outage_minutes > 0 && (
<span className='text-muted-foreground'>
{summary.outage_minutes} {t('min downtime')}
</span>
)}
<span className='text-muted-foreground hidden sm:inline'>
{formatUptimePct(summary.uptime_pct)} {t('overall')}
</span>
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+125
View File
@@ -0,0 +1,125 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { getSuccessRateDotClass } from '@/features/performance-metrics/lib/format'
import { cn } from '@/lib/utils'
export type ModelPerfBadgeData = {
avg_latency_ms: number
success_rate: number
avg_tps: number
recent_success_rates?: number[]
}
export interface ModelPerfBadgeProps extends React.HTMLAttributes<HTMLDivElement> {
perf: ModelPerfBadgeData | undefined
}
function formatCompactNumber(value: number): string {
if (!Number.isFinite(value) || value <= 0) return '—'
return value > 1 ? String(Math.round(value)) : value.toFixed(1)
}
function formatCompactLatency(ms: number): string {
if (!Number.isFinite(ms) || ms <= 0) return '—'
if (ms >= 1_000) return `${formatCompactNumber(ms / 1_000)}s`
return `${formatCompactNumber(ms)}ms`
}
function formatCompactThroughput(tps: number): string {
if (!Number.isFinite(tps) || tps <= 0) return '—'
if (tps >= 1_000) return `${formatCompactNumber(tps / 1_000)}Kt`
return `${formatCompactNumber(tps)}t`
}
export const ModelPerfBadge = memo(function ModelPerfBadge(
props: ModelPerfBadgeProps
) {
const { t } = useTranslation()
if (!props.perf) {
return null
}
const { avg_latency_ms, avg_tps, success_rate } = props.perf
const recentRates =
props.perf.recent_success_rates?.filter((rate) => Number.isFinite(rate)) ??
[]
const statusRates =
recentRates.length > 0 ? recentRates.slice(-3) : [success_rate]
const statusBars = [
...Array(Math.max(0, 3 - statusRates.length)).fill(null),
...statusRates,
].slice(-3)
return (
<div
className={cn(
'hidden w-[132px] grid-cols-[38px_48px_30px] gap-x-2 text-right tabular-nums min-[460px]:grid',
props.className
)}
>
<div title={t('Average latency')} className='min-w-0'>
<div className='text-muted-foreground/55 text-[10px] leading-4'>
{t('Latency short')}
</div>
<div className='text-muted-foreground/80 font-mono text-xs leading-4 whitespace-nowrap'>
{formatCompactLatency(avg_latency_ms)}
</div>
</div>
<div title={t('Throughput')} className='min-w-0'>
<div className='text-muted-foreground/55 truncate text-[10px] leading-4'>
{t('Throughput short')}
</div>
<div className='text-muted-foreground/80 font-mono text-xs leading-4 whitespace-nowrap'>
{formatCompactThroughput(avg_tps)}
</div>
</div>
<div
title={`${t('Success rate')}: ${success_rate.toFixed(1)}%`}
className='min-w-0'
>
<div className='text-muted-foreground/55 truncate text-[10px] leading-4'>
{t('Status short')}
</div>
<div className='flex h-4 items-center justify-end gap-0.5'>
{statusBars.map((rate, index) => (
<span
key={`${index}-${rate ?? 'empty'}`}
className={cn(
'w-1 rounded-full',
index === 0 && 'h-2',
index === 1 && 'h-2.5',
index === 2 && 'h-3',
rate == null
? index === 0
? 'bg-muted-foreground/10'
: 'bg-muted-foreground/15'
: getSuccessRateDotClass(rate)
)}
/>
))}
</div>
</div>
</div>
)
})
+413
View File
@@ -0,0 +1,413 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { ColumnDef } from '@tanstack/react-table'
import { useTranslation } from 'react-i18next'
import {
BadgeCell,
BadgeListCell,
DataTableColumnHeader,
} from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge'
import { getLobeIcon } from '@/lib/lobe-icon'
import { DEFAULT_TOKEN_UNIT } from '../constants'
import {
getDynamicDisplayGroupRatio,
getDynamicPricingSummary,
} from '../lib/dynamic-price'
import { parseTags } from '../lib/filters'
import { isTokenBasedModel } from '../lib/model-helpers'
import {
formatPrice,
formatRequestPrice,
stripTrailingZeros,
} from '../lib/price'
import type { PricingModel, TokenUnit } from '../types'
import { ModelBillingModeBadge } from './model-billing-mode-badge'
// ----------------------------------------------------------------------------
// Pricing Table Columns
// ----------------------------------------------------------------------------
export interface PricingColumnsOptions {
tokenUnit?: TokenUnit
priceRate?: number
usdExchangeRate?: number
showRechargePrice?: boolean
selectedGroup?: string
}
export function usePricingColumns(
options: PricingColumnsOptions = {}
): ColumnDef<PricingModel>[] {
const { t } = useTranslation()
const {
tokenUnit = DEFAULT_TOKEN_UNIT,
priceRate = 1,
usdExchangeRate = 1,
showRechargePrice = false,
selectedGroup,
} = options
const tokenUnitLabel = tokenUnit === 'K' ? '1K' : '1M'
return [
// Model column
{
accessorKey: 'model_name',
meta: { label: t('Model') },
header: ({ column }) => (
<DataTableColumnHeader column={column} title={t('Model')} />
),
cell: ({ row }) => {
const model = row.original
const modelIconKey = model.icon || model.vendor_icon
const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 14) : null
return (
<div className='flex max-w-full min-w-0 items-center gap-2'>
{modelIcon}
<span className='truncate font-mono text-sm font-medium'>
{model.model_name}
</span>
</div>
)
},
minSize: 200,
},
// Type column
{
accessorKey: 'quota_type',
header: t('Type'),
cell: ({ row }) => (
<ModelBillingModeBadge model={row.original} className='-ml-1.5' />
),
size: 110,
enableSorting: false,
},
// Price column
{
accessorKey: 'price',
meta: { label: t('Price') },
header: ({ column }) => (
<DataTableColumnHeader column={column} title={t('Price')} />
),
cell: ({ row }) => {
const model = row.original
const dynamicSummary = getDynamicPricingSummary(model, {
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
groupRatioMultiplier: getDynamicDisplayGroupRatio(
model,
selectedGroup
),
})
if (dynamicSummary) {
if (dynamicSummary.isSpecialExpression) {
return (
<div className='max-w-full min-w-0'>
<div className='text-xs font-medium text-amber-700 dark:text-amber-300'>
{t('Special billing expression')}
</div>
<div className='text-muted-foreground text-[11px]'>
{t('Unable to parse structured pricing')}
</div>
<code className='text-muted-foreground/70 mt-1 line-clamp-2 block font-mono text-[10px] leading-relaxed break-all'>
{dynamicSummary.rawExpression}
</code>
</div>
)
}
const primaryEntries = dynamicSummary.primaryEntries.slice(0, 2)
if (primaryEntries.length === 0) {
return (
<span className='text-muted-foreground text-xs'>
{t('Dynamic Pricing')}
</span>
)
}
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>
{primaryEntries.map((entry, index) => (
<span key={entry.key}>
{index > 0 && (
<span className='text-muted-foreground/40 mx-1'>/</span>
)}
{stripTrailingZeros(entry.formatted)}
</span>
))}
</span>
<div className='text-muted-foreground/50 text-[10px]'>
/ {tokenUnitLabel} tokens
{dynamicSummary.tierCount > 1 &&
` · ${t('{{count}} tiers', {
count: dynamicSummary.tierCount,
})}`}
</div>
</div>
)
}
const isTokenBased = isTokenBasedModel(model)
if (isTokenBased) {
const inputPrice = stripTrailingZeros(
formatPrice(
model,
'input',
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
selectedGroup
)
)
const outputPrice = stripTrailingZeros(
formatPrice(
model,
'output',
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
selectedGroup
)
)
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>
{inputPrice}
<span className='text-muted-foreground/40 mx-1'>/</span>
{outputPrice}
</span>
<div className='text-muted-foreground/50 text-[10px]'>
/ {tokenUnitLabel} tokens
</div>
</div>
)
}
const price = stripTrailingZeros(
formatRequestPrice(
model,
showRechargePrice,
priceRate,
usdExchangeRate,
selectedGroup
)
)
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>{price}</span>
<div className='text-muted-foreground/50 text-[10px]'>
/ {t('request')}
</div>
</div>
)
},
size: 180,
enableSorting: false,
},
// Cached price column (Vercel AI Gateway style)
{
id: 'cached_price',
header: t('Cached'),
cell: ({ row }) => {
const model = row.original
const dynamicSummary = getDynamicPricingSummary(model, {
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
groupRatioMultiplier: getDynamicDisplayGroupRatio(
model,
selectedGroup
),
})
if (dynamicSummary) {
if (dynamicSummary.isSpecialExpression) {
return (
<span className='text-muted-foreground/50 text-xs'>
{t('Special billing expression')}
</span>
)
}
const cacheEntry = dynamicSummary.entries.find(
(entry) => entry.field === 'cacheReadPrice'
)
if (!cacheEntry) {
return <span className='text-muted-foreground/30 text-xs'></span>
}
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>
{stripTrailingZeros(cacheEntry.formatted)}
</span>
<div className='text-muted-foreground/50 text-[10px]'>
/ {tokenUnitLabel}
</div>
</div>
)
}
const isTokenBased = isTokenBasedModel(model)
if (!isTokenBased || model.cache_ratio == null) {
return <span className='text-muted-foreground/30 text-xs'></span>
}
const cachedPrice = stripTrailingZeros(
formatPrice(
model,
'cache',
tokenUnit,
showRechargePrice,
priceRate,
usdExchangeRate,
selectedGroup
)
)
return (
<div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>
{cachedPrice}
</span>
<div className='text-muted-foreground/50 text-[10px]'>
/ {tokenUnitLabel}
</div>
</div>
)
},
size: 110,
enableSorting: false,
},
// Vendor column
{
accessorKey: 'vendor_name',
header: t('Vendor'),
cell: ({ row }) => {
const model = row.original
if (!model.vendor_name) {
return <span className='text-muted-foreground/50 text-xs'></span>
}
const vendorIcon = model.vendor_icon
? getLobeIcon(model.vendor_icon, 12)
: null
return (
<BadgeCell className='gap-1.5'>
{vendorIcon}
<StatusBadge
label={model.vendor_name}
autoColor={model.vendor_name}
size='sm'
copyable={false}
/>
</BadgeCell>
)
},
size: 130,
enableSorting: false,
},
// Tags column
{
accessorKey: 'tags',
header: t('Tags'),
cell: ({ row }) => {
const tags = parseTags(row.original.tags)
return (
<BadgeListCell
items={tags.map((tag) => (
<StatusBadge
key={tag}
label={tag}
autoColor={tag}
size='sm'
copyable={false}
/>
))}
/>
)
},
size: 140,
enableSorting: false,
},
// Endpoints column
{
accessorKey: 'supported_endpoint_types',
header: t('Endpoints'),
cell: ({ row }) => {
const endpoints = row.original.supported_endpoint_types || []
return (
<BadgeListCell
items={endpoints.map((ep) => (
<StatusBadge
key={ep}
label={ep}
autoColor={ep}
size='sm'
copyable={false}
/>
))}
/>
)
},
size: 130,
enableSorting: false,
},
// Enable Groups column
{
accessorKey: 'enable_groups',
header: t('Groups'),
cell: ({ row }) => {
const groups = row.original.enable_groups || []
return (
<BadgeListCell
items={groups.map((group) => (
<GroupBadge key={group} group={group} size='sm' />
))}
tooltipClassName='max-w-[280px] p-2'
/>
)
},
size: 130,
enableSorting: false,
},
]
}
+310
View File
@@ -0,0 +1,310 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { ChevronDown, RotateCcw } from 'lucide-react'
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import { getLobeIcon } from '@/lib/lobe-icon'
import { cn } from '@/lib/utils'
import {
ENDPOINT_TYPES,
FILTER_ALL,
QUOTA_TYPES,
getEndpointTypeLabels,
getQuotaTypeLabels,
} from '../constants'
import { parseTags } from '../lib/filters'
import type { PricingModel, PricingVendor } from '../types'
type FilterOption = {
value: string
label: string
count?: number
suffix?: string
icon?: ReactNode
}
type FilterSectionProps = {
title: string
value: string
options: FilterOption[]
onChange: (value: string) => void
}
export interface PricingSidebarProps {
quotaTypeFilter: string
endpointTypeFilter: string
vendorFilter: string
groupFilter: string
tagFilter: string
onQuotaTypeChange: (value: string) => void
onEndpointTypeChange: (value: string) => void
onVendorChange: (value: string) => void
onGroupChange: (value: string) => void
onTagChange: (value: string) => void
vendors: PricingVendor[]
groups: string[]
groupRatios?: Record<string, number>
tags: string[]
models: PricingModel[]
hasActiveFilters: boolean
onClearFilters: () => void
className?: string
}
function countBy(
models: PricingModel[],
predicate: (model: PricingModel) => boolean
): number {
return models.reduce((count, model) => count + (predicate(model) ? 1 : 0), 0)
}
function formatGroupRatio(ratio: number | undefined): string | undefined {
if (ratio == null) return undefined
const formatted = Number.isInteger(ratio)
? ratio.toString()
: ratio.toFixed(3).replace(/0+$/, '').replace(/\.$/, '')
return `x${formatted}`
}
function FilterChip(props: {
option: FilterOption
active: boolean
onClick: () => void
}) {
return (
<button
type='button'
onClick={props.onClick}
className={cn(
'group inline-flex max-w-full items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium transition-all',
props.active
? 'border-foreground/30 bg-foreground/5 text-foreground shadow-sm'
: 'border-border/70 bg-background text-muted-foreground hover:border-border hover:bg-muted/50 hover:text-foreground'
)}
title={props.option.label}
>
{props.option.icon && (
<span className='shrink-0'>{props.option.icon}</span>
)}
<span className='truncate'>{props.option.label}</span>
{(props.option.suffix || props.option.count != null) && (
<span
className={cn(
'rounded-md px-1.5 py-0.5 text-[12px]',
props.active
? 'bg-background text-foreground'
: 'bg-muted text-muted-foreground'
)}
>
{props.option.suffix ?? props.option.count}
</span>
)}
</button>
)
}
function FilterSection(props: FilterSectionProps) {
return (
<Collapsible
defaultOpen
className='border-border/70 border-b pb-3 last:border-b-0'
>
<CollapsibleTrigger className='group flex w-full items-center justify-between py-2.5 text-left'>
<span className='text-foreground text-sm font-semibold'>
{props.title}
</span>
<ChevronDown className='text-muted-foreground size-4 transition-transform group-data-[panel-open]:rotate-180' />
</CollapsibleTrigger>
<CollapsibleContent>
<div className='flex flex-wrap gap-1.5'>
{props.options.map((option) => (
<FilterChip
key={option.value}
option={option}
active={props.value === option.value}
onClick={() => props.onChange(option.value)}
/>
))}
</div>
</CollapsibleContent>
</Collapsible>
)
}
export function PricingSidebar(props: PricingSidebarProps) {
const { t } = useTranslation()
const quotaTypeLabels = getQuotaTypeLabels(t)
const endpointTypeLabels = getEndpointTypeLabels(t)
const vendorOptions: FilterOption[] = [
{
value: FILTER_ALL,
label: t('All Vendors'),
count: props.models.length,
},
...props.vendors
.map((vendor) => ({
value: vendor.name,
label: vendor.name,
count: countBy(
props.models,
(model) => model.vendor_name === vendor.name
),
icon: vendor.icon ? getLobeIcon(vendor.icon, 14) : undefined,
}))
.filter((vendor) => vendor.count > 0),
]
const groupOptions: FilterOption[] = [
{
value: FILTER_ALL,
label: t('All Groups'),
},
...props.groups.map((group) => ({
value: group,
label: group,
suffix: formatGroupRatio(props.groupRatios?.[group]),
})),
]
const quotaOptions: FilterOption[] = [
{
value: QUOTA_TYPES.ALL,
label: quotaTypeLabels[QUOTA_TYPES.ALL],
count: props.models.length,
},
{
value: QUOTA_TYPES.TOKEN,
label: quotaTypeLabels[QUOTA_TYPES.TOKEN],
count: countBy(props.models, (model) => model.quota_type === 0),
},
{
value: QUOTA_TYPES.REQUEST,
label: quotaTypeLabels[QUOTA_TYPES.REQUEST],
count: countBy(props.models, (model) => model.quota_type === 1),
},
]
const tagOptions: FilterOption[] = [
{
value: FILTER_ALL,
label: t('All Tags'),
count: props.models.length,
},
...props.tags.map((tag) => ({
value: tag,
label: tag,
count: countBy(props.models, (model) =>
parseTags(model.tags)
.map((item) => item.toLowerCase())
.includes(tag.toLowerCase())
),
})),
]
const endpointOptions: FilterOption[] = [
{
value: ENDPOINT_TYPES.ALL,
label: endpointTypeLabels[ENDPOINT_TYPES.ALL],
count: props.models.length,
},
...Object.entries(endpointTypeLabels)
.filter(([value]) => value !== ENDPOINT_TYPES.ALL)
.map(([value, label]) => ({
value,
label,
count: countBy(
props.models,
(model) => model.supported_endpoint_types?.includes(value) ?? false
),
})),
]
return (
<aside className={cn('rounded-xl border p-3', props.className)}>
<div className='mb-2.5 flex items-center justify-between gap-2'>
<div>
<h2 className='text-foreground text-sm font-bold'>{t('Filter')}</h2>
<p className='text-muted-foreground mt-1 text-xs'>
{t('Refine models by provider, group, type, and tags.')}
</p>
</div>
<Button
type='button'
variant='ghost'
size='sm'
onClick={props.onClearFilters}
disabled={!props.hasActiveFilters}
className='h-7 gap-1.5 px-2 text-xs'
>
<RotateCcw className='size-3.5' />
{t('Reset')}
</Button>
</div>
{props.hasActiveFilters && (
<Badge variant='secondary' className='mb-3'>
{t('Filters active')}
</Badge>
)}
<div className='space-y-1'>
<FilterSection
title={t('Groups')}
value={props.groupFilter}
options={groupOptions}
onChange={props.onGroupChange}
/>
<FilterSection
title={t('All Vendors')}
value={props.vendorFilter}
options={vendorOptions}
onChange={props.onVendorChange}
/>
<FilterSection
title={t('Model Tags')}
value={props.tagFilter}
options={tagOptions}
onChange={props.onTagChange}
/>
<FilterSection
title={t('Pricing Type')}
value={props.quotaTypeFilter}
options={quotaOptions}
onChange={props.onQuotaTypeChange}
/>
<FilterSection
title={t('Endpoint Type')}
value={props.endpointTypeFilter}
options={endpointOptions}
onChange={props.onEndpointTypeChange}
/>
</div>
</aside>
)
}
+115
View File
@@ -0,0 +1,115 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { Row, PaginationState } from '@tanstack/react-table'
import { useState, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
DataTablePagination,
DataTableRow,
DataTableView,
useDataTable,
} from '@/components/data-table'
import { DEFAULT_PRICING_PAGE_SIZE, DEFAULT_TOKEN_UNIT } from '../constants'
import type { PricingModel, TokenUnit } from '../types'
import { usePricingColumns } from './pricing-columns'
export interface PricingTableProps {
models: PricingModel[]
isLoading?: boolean
priceRate?: number
usdExchangeRate?: number
tokenUnit?: TokenUnit
showRechargePrice?: boolean
selectedGroup?: string
onModelClick?: (modelName: string) => void
}
export function PricingTable(props: PricingTableProps) {
const { t } = useTranslation()
const {
models,
isLoading = false,
priceRate = 1,
usdExchangeRate = 1,
tokenUnit = DEFAULT_TOKEN_UNIT,
showRechargePrice = false,
selectedGroup,
onModelClick,
} = props
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: DEFAULT_PRICING_PAGE_SIZE,
})
const columns = usePricingColumns({
tokenUnit,
priceRate,
usdExchangeRate,
showRechargePrice,
selectedGroup,
})
const { table } = useDataTable({
data: models,
columns,
pageCount: Math.ceil(models.length / pagination.pageSize),
pagination,
onPaginationChange: setPagination,
manualPagination: false,
withFilteredRowModel: false,
withSortedRowModel: false,
withFacetedRowModel: false,
})
const handleRowClick = useCallback(
(model: PricingModel) => {
onModelClick?.(model.model_name)
},
[onModelClick]
)
return (
<div className='space-y-4'>
<DataTableView
table={table}
isLoading={isLoading}
emptyTitle={t('No Models Found')}
emptyDescription={t('No models match your current filters.')}
skeletonKeyPrefix='pricing-skeleton'
applyHeaderSize
getColumnClassName={(_columnId, kind) =>
kind === 'header' ? 'text-muted-foreground font-medium' : undefined
}
renderRow={(row: Row<PricingModel>) => (
<DataTableRow
key={row.id}
row={row}
className='hover:bg-muted/30 cursor-pointer transition-colors'
onClick={() => handleRowClick(row.original)}
/>
)}
/>
{!isLoading && models.length > 0 && <DataTablePagination table={table} />}
</div>
)
}
+313
View File
@@ -0,0 +1,313 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { ArrowUpDown, Check, Filter, Grid2X2, Table2 } from 'lucide-react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
sideDrawerContentClassName,
sideDrawerFormClassName,
sideDrawerHeaderClassName,
} from '@/components/drawer-layout'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import {
VIEW_MODES,
getSortLabels,
type SortOption,
type ViewMode,
} from '../constants'
import type { PricingModel, PricingVendor, TokenUnit } from '../types'
import { PricingSidebar } from './pricing-sidebar'
type SegmentOption = {
value: string
label?: string
icon?: React.ComponentType<{ className?: string }>
tooltip?: string
}
export interface PricingToolbarProps {
filteredCount: number
totalCount?: number
sortBy: string
onSortChange: (value: string) => void
tokenUnit: TokenUnit
onTokenUnitChange: (value: TokenUnit) => void
showRechargePrice: boolean
onRechargePriceChange: (value: boolean) => void
viewMode: ViewMode
onViewModeChange: (value: ViewMode) => void
quotaTypeFilter: string
endpointTypeFilter: string
vendorFilter: string
groupFilter: string
tagFilter: string
onQuotaTypeChange: (value: string) => void
onEndpointTypeChange: (value: string) => void
onVendorChange: (value: string) => void
onGroupChange: (value: string) => void
onTagChange: (value: string) => void
vendors: PricingVendor[]
groups: string[]
groupRatios?: Record<string, number>
tags: string[]
models: PricingModel[]
hasActiveFilters: boolean
activeFilterCount: number
onClearFilters: () => void
}
function SegmentedControl(props: {
options: SegmentOption[]
value: string
onChange: (value: string) => void
ariaLabel: string
}) {
return (
<div
role='group'
aria-label={props.ariaLabel}
className='bg-muted/60 inline-flex h-8 items-center rounded-lg border p-0.5'
>
{props.options.map((option) => {
const Icon = option.icon
const isActive = option.value === props.value
const button = (
<button
key={option.value}
type='button'
onClick={() => props.onChange(option.value)}
aria-pressed={isActive}
className={cn(
'inline-flex h-full items-center justify-center rounded-md text-xs font-medium transition-all',
Icon && !option.label ? 'w-7' : 'gap-1.5 px-3',
isActive
? 'bg-primary text-primary-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
)}
>
{Icon && <Icon className='size-3.5' />}
{option.label}
</button>
)
if (!option.tooltip) {
return button
}
return (
<Tooltip key={option.value}>
<TooltipTrigger render={button}></TooltipTrigger>
<TooltipContent side='bottom' className='text-xs'>
{option.tooltip}
</TooltipContent>
</Tooltip>
)
})}
</div>
)
}
export function PricingToolbar(props: PricingToolbarProps) {
const { t } = useTranslation()
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false)
const sortLabels = getSortLabels(t)
const handleTokenUnitChange = useCallback(
(value: string) => props.onTokenUnitChange(value as TokenUnit),
[props]
)
const handleViewModeChange = useCallback(
(value: string) => props.onViewModeChange(value as ViewMode),
[props]
)
const handleRechargePriceChange = useCallback(
(value: string) => props.onRechargePriceChange(value === 'recharge'),
[props]
)
return (
<div className='rounded-xl border p-3'>
<div className='flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between'>
<div className='flex items-center gap-2'>
<Button
type='button'
variant='outline'
size='sm'
onClick={() => setMobileFiltersOpen(true)}
className='gap-1.5 xl:hidden'
>
<Filter className='size-4' />
{t('Filter')}
{props.activeFilterCount > 0 && (
<Badge className='ml-0.5 size-5 justify-center p-0 text-[10px]'>
{props.activeFilterCount}
</Badge>
)}
</Button>
<div className='text-muted-foreground flex items-baseline gap-1 text-sm'>
<span className='text-foreground font-semibold tabular-nums'>
{props.filteredCount.toLocaleString()}
</span>
<span>{props.filteredCount === 1 ? t('model') : t('models')}</span>
{props.hasActiveFilters && props.totalCount && (
<span className='text-muted-foreground/60 text-xs'>
/ {props.totalCount.toLocaleString()}
</span>
)}
</div>
</div>
<div className='flex flex-wrap items-center gap-2'>
<div className='hidden items-center gap-2 sm:flex'>
<SegmentedControl
options={[
{ value: 'standard', label: t('Standard') },
{ value: 'recharge', label: t('Recharge') },
]}
value={props.showRechargePrice ? 'recharge' : 'standard'}
onChange={handleRechargePriceChange}
ariaLabel={t('Price display mode')}
/>
<SegmentedControl
options={[
{ value: 'M', label: '/1M' },
{ value: 'K', label: '/1K' },
]}
value={props.tokenUnit}
onChange={handleTokenUnitChange}
ariaLabel={t('Token unit')}
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
type='button'
variant='outline'
size='sm'
className='h-8 gap-1.5 px-3 text-xs'
/>
}
>
<ArrowUpDown className='size-3.5' />
<span>{sortLabels[props.sortBy as SortOption] || t('Sort')}</span>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-44'>
{Object.entries(sortLabels).map(([value, label]) => (
<DropdownMenuItem
key={value}
onClick={() => props.onSortChange(value)}
className='gap-2'
>
<Check
className={cn(
'size-4 shrink-0',
props.sortBy === value ? 'opacity-100' : 'opacity-0'
)}
/>
{label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<SegmentedControl
options={[
{
value: VIEW_MODES.CARD,
icon: Grid2X2,
tooltip: t('Card view'),
},
{
value: VIEW_MODES.TABLE,
icon: Table2,
tooltip: t('Table view'),
},
]}
value={props.viewMode}
onChange={handleViewModeChange}
ariaLabel={t('View mode')}
/>
</div>
</div>
<Sheet open={mobileFiltersOpen} onOpenChange={setMobileFiltersOpen}>
<SheetContent
side='right'
className={sideDrawerContentClassName('sm:max-w-md')}
>
<SheetHeader className={sideDrawerHeaderClassName()}>
<SheetTitle>{t('Filter')}</SheetTitle>
<SheetDescription>
{t('Filter models by provider, group, type, endpoint, and tags.')}
</SheetDescription>
</SheetHeader>
<div className={sideDrawerFormClassName('gap-0')}>
<PricingSidebar
quotaTypeFilter={props.quotaTypeFilter}
endpointTypeFilter={props.endpointTypeFilter}
vendorFilter={props.vendorFilter}
groupFilter={props.groupFilter}
tagFilter={props.tagFilter}
onQuotaTypeChange={props.onQuotaTypeChange}
onEndpointTypeChange={props.onEndpointTypeChange}
onVendorChange={props.onVendorChange}
onGroupChange={props.onGroupChange}
onTagChange={props.onTagChange}
vendors={props.vendors}
groups={props.groups}
groupRatios={props.groupRatios}
tags={props.tags}
models={props.models}
hasActiveFilters={props.hasActiveFilters}
onClearFilters={props.onClearFilters}
className='border-0 bg-transparent p-0 shadow-none'
/>
</div>
</SheetContent>
</Sheet>
</div>
)
}
+88
View File
@@ -0,0 +1,88 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Search, X } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
export interface SearchBarProps {
value: string
onChange: (value: string) => void
onClear: () => void
placeholder?: string
className?: string
}
export function SearchBar(props: SearchBarProps) {
const { t } = useTranslation()
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
inputRef.current?.focus()
}
if (e.key === 'Escape' && document.activeElement === inputRef.current) {
inputRef.current?.blur()
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])
return (
<div className={cn('relative', props.className)}>
<Search className='text-muted-foreground/60 pointer-events-none absolute top-1/2 left-3.5 size-4 -translate-y-1/2' />
<input
ref={inputRef}
type='text'
placeholder={props.placeholder || t('Search models...')}
value={props.value}
onChange={(e) => props.onChange(e.target.value)}
className={cn(
'border-border/60 bg-background placeholder:text-muted-foreground/50',
'hover:border-border',
'focus:border-primary/50 focus:ring-primary/20 focus:ring-2',
'h-10 w-full rounded-lg border pr-16 pl-10 text-sm transition-all outline-none'
)}
aria-label={t('Search models')}
/>
<div className='absolute top-1/2 right-2.5 flex -translate-y-1/2 items-center gap-1'>
{props.value ? (
<Button
variant='ghost'
size='icon'
onClick={props.onClear}
className='text-muted-foreground/60 hover:text-foreground size-7'
aria-label={t('Clear search')}
>
<X className='size-4' />
</Button>
) : (
<kbd className='bg-muted text-muted-foreground pointer-events-none hidden rounded border px-1.5 py-0.5 font-mono text-[10px] sm:inline-block'>
K
</kbd>
)}
</div>
</div>
)
}
+145
View File
@@ -0,0 +1,145 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { type TFunction } from 'i18next'
import type { TokenUnit } from './types'
// ----------------------------------------------------------------------------
// Pricing Constants
// ----------------------------------------------------------------------------
/** Sort options for pricing models */
export const SORT_OPTIONS = {
NAME: 'name',
PRICE_LOW: 'price-low',
PRICE_HIGH: 'price-high',
} as const
export type SortOption = (typeof SORT_OPTIONS)[keyof typeof SORT_OPTIONS]
export function getSortLabels(t: TFunction): Record<SortOption, string> {
return {
[SORT_OPTIONS.NAME]: t('Name'),
[SORT_OPTIONS.PRICE_LOW]: t('Price: Low to High'),
[SORT_OPTIONS.PRICE_HIGH]: t('Price: High to Low'),
}
}
/** Filter values */
export const FILTER_ALL = 'all'
/** Quota type options */
export const QUOTA_TYPES = {
ALL: 'all',
TOKEN: 'token',
REQUEST: 'request',
} as const
export type QuotaTypeOption = (typeof QUOTA_TYPES)[keyof typeof QUOTA_TYPES]
/** Quota type labels */
export function getQuotaTypeLabels(
t: TFunction
): Record<QuotaTypeOption, string> {
return {
[QUOTA_TYPES.ALL]: t('All Models'),
[QUOTA_TYPES.TOKEN]: t('Token-based'),
[QUOTA_TYPES.REQUEST]: t('Per Request'),
}
}
/** Endpoint type options */
export const ENDPOINT_TYPES = {
ALL: 'all',
OPENAI: 'openai',
OPENAI_RESPONSE: 'openai-response',
ANTHROPIC: 'anthropic',
GEMINI: 'gemini',
JINA_RERANK: 'jina-rerank',
IMAGE_GENERATION: 'image-generation',
EMBEDDINGS: 'embeddings',
OPENAI_VIDEO: 'openai-video',
} as const
export type EndpointTypeOption =
(typeof ENDPOINT_TYPES)[keyof typeof ENDPOINT_TYPES]
/** Endpoint type labels */
export function getEndpointTypeLabels(
t: TFunction
): Record<EndpointTypeOption, string> {
return {
[ENDPOINT_TYPES.ALL]: t('All Types'),
[ENDPOINT_TYPES.OPENAI]: 'Chat',
[ENDPOINT_TYPES.OPENAI_RESPONSE]: 'Response',
[ENDPOINT_TYPES.ANTHROPIC]: 'Anthropic',
[ENDPOINT_TYPES.GEMINI]: 'Gemini',
[ENDPOINT_TYPES.JINA_RERANK]: 'Rerank',
[ENDPOINT_TYPES.IMAGE_GENERATION]: t('Image'),
[ENDPOINT_TYPES.EMBEDDINGS]: t('Embeddings'),
[ENDPOINT_TYPES.OPENAI_VIDEO]: t('Video'),
}
}
/** Filter section keys */
export const FILTER_SECTIONS = {
PRICING_TYPE: 'pricingType',
ENDPOINT_TYPE: 'endpointType',
VENDOR: 'vendor',
GROUP: 'group',
TAG: 'tag',
} as const
/** Maximum number of tags to display in model row */
export const MAX_TAGS_DISPLAY = 5
/** Maximum number of filter items to display before showing "More..." */
export const MAX_FILTER_ITEMS = 5
/** Sidebar width */
export const SIDEBAR_WIDTH = 'w-64'
/** Excluded groups */
export const EXCLUDED_GROUPS = ['', 'auto']
/** Quota type values */
export const QUOTA_TYPE_VALUES = {
TOKEN: 0,
REQUEST: 1,
} as const
/** Token unit divisors */
export const TOKEN_UNIT_DIVISORS = {
M: 1,
K: 1000,
} as const
/** Default token unit for pricing display */
export const DEFAULT_TOKEN_UNIT: TokenUnit = 'M'
/** View mode options */
export const VIEW_MODES = {
CARD: 'card',
TABLE: 'table',
} as const
export type ViewMode = (typeof VIEW_MODES)[keyof typeof VIEW_MODES]
/** Default page size for pricing table */
export const DEFAULT_PRICING_PAGE_SIZE = 20
+20
View File
@@ -0,0 +1,20 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
export { useFilters } from './use-filters'
export { usePricingData } from './use-pricing-data'
+231
View File
@@ -0,0 +1,231 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useSearch } from '@tanstack/react-router'
import { useMemo, useCallback, useState } from 'react'
import {
FILTER_ALL,
SORT_OPTIONS,
QUOTA_TYPES,
ENDPOINT_TYPES,
DEFAULT_TOKEN_UNIT,
VIEW_MODES,
type ViewMode,
} from '../constants'
import { filterAndSortModels, extractAllTags } from '../lib/filters'
import type { PricingModel, TokenUnit } from '../types'
type FilterState = {
search?: string
sort?: string
vendor?: string
group?: string
quotaType?: string
endpointType?: string
tag?: string
tokenUnit?: TokenUnit
view?: ViewMode
rechargePrice?: boolean
}
function normalizeViewMode(value: unknown): ViewMode {
if (value === VIEW_MODES.TABLE) {
return VIEW_MODES.TABLE
}
return VIEW_MODES.CARD
}
export function useFilters(models: PricingModel[]) {
const search = useSearch({ from: '/pricing/' })
const [filterState, setFilterState] = useState<FilterState>(() => ({
search: search.search,
sort: search.sort,
vendor: search.vendor,
group: search.group,
quotaType: search.quotaType,
endpointType: search.endpointType,
tag: search.tag,
tokenUnit: search.tokenUnit,
view: search.view,
rechargePrice: search.rechargePrice,
}))
const searchInput = filterState.search || ''
const sortBy = filterState.sort || SORT_OPTIONS.NAME
const vendorFilter = filterState.vendor || FILTER_ALL
const groupFilter = filterState.group || FILTER_ALL
const quotaTypeFilter = filterState.quotaType || QUOTA_TYPES.ALL
const endpointTypeFilter = filterState.endpointType || ENDPOINT_TYPES.ALL
const tagFilter = filterState.tag || FILTER_ALL
const tokenUnit: TokenUnit =
filterState.tokenUnit === 'K' ? 'K' : DEFAULT_TOKEN_UNIT
const viewMode = normalizeViewMode(filterState.view)
const showRechargePrice = filterState.rechargePrice === true
const updateFilters = useCallback((updates: Record<string, unknown>) => {
setFilterState((prev) => {
const next: Record<string, unknown> = { ...prev, ...updates }
for (const key of Object.keys(next)) {
if (next[key] === undefined || next[key] === null) {
delete next[key]
}
}
return next as FilterState
})
}, [])
const setSearchInput = useCallback(
(v: string) => updateFilters({ search: v || undefined }),
[updateFilters]
)
const setSortBy = useCallback(
(v: string) =>
updateFilters({ sort: v === SORT_OPTIONS.NAME ? undefined : v }),
[updateFilters]
)
const setVendorFilter = useCallback(
(v: string) => updateFilters({ vendor: v === FILTER_ALL ? undefined : v }),
[updateFilters]
)
const setGroupFilter = useCallback(
(v: string) => updateFilters({ group: v === FILTER_ALL ? undefined : v }),
[updateFilters]
)
const setQuotaTypeFilter = useCallback(
(v: string) =>
updateFilters({ quotaType: v === QUOTA_TYPES.ALL ? undefined : v }),
[updateFilters]
)
const setEndpointTypeFilter = useCallback(
(v: string) =>
updateFilters({
endpointType: v === ENDPOINT_TYPES.ALL ? undefined : v,
}),
[updateFilters]
)
const setTagFilter = useCallback(
(v: string) => updateFilters({ tag: v === FILTER_ALL ? undefined : v }),
[updateFilters]
)
const setTokenUnit = useCallback(
(v: TokenUnit) =>
updateFilters({ tokenUnit: v === DEFAULT_TOKEN_UNIT ? undefined : v }),
[updateFilters]
)
const setViewMode = useCallback(
(v: ViewMode) =>
updateFilters({ view: v === VIEW_MODES.CARD ? undefined : v }),
[updateFilters]
)
const setShowRechargePrice = useCallback(
(v: boolean) => updateFilters({ rechargePrice: v || undefined }),
[updateFilters]
)
const availableTags = useMemo(() => {
if (!models || models.length === 0) return []
return extractAllTags(models)
}, [models])
const filteredModels = useMemo(() => {
if (!models || models.length === 0) return []
return filterAndSortModels(models, {
search: searchInput,
vendor: vendorFilter,
group: groupFilter,
quotaType: quotaTypeFilter,
endpointType: endpointTypeFilter,
tag: tagFilter,
sortBy,
})
}, [
models,
searchInput,
vendorFilter,
groupFilter,
quotaTypeFilter,
endpointTypeFilter,
tagFilter,
sortBy,
])
const hasActiveFilters = useMemo(
() =>
vendorFilter !== FILTER_ALL ||
groupFilter !== FILTER_ALL ||
quotaTypeFilter !== QUOTA_TYPES.ALL ||
endpointTypeFilter !== ENDPOINT_TYPES.ALL ||
tagFilter !== FILTER_ALL,
[vendorFilter, groupFilter, quotaTypeFilter, endpointTypeFilter, tagFilter]
)
const activeFilterCount = useMemo(
() =>
(vendorFilter !== FILTER_ALL ? 1 : 0) +
(groupFilter !== FILTER_ALL ? 1 : 0) +
(quotaTypeFilter !== QUOTA_TYPES.ALL ? 1 : 0) +
(endpointTypeFilter !== ENDPOINT_TYPES.ALL ? 1 : 0) +
(tagFilter !== FILTER_ALL ? 1 : 0),
[vendorFilter, groupFilter, quotaTypeFilter, endpointTypeFilter, tagFilter]
)
const clearFilters = useCallback(() => {
updateFilters({
vendor: undefined,
group: undefined,
quotaType: undefined,
endpointType: undefined,
tag: undefined,
})
}, [updateFilters])
const clearSearch = useCallback(() => {
updateFilters({ search: undefined })
}, [updateFilters])
return {
searchInput,
sortBy,
vendorFilter,
groupFilter,
quotaTypeFilter,
endpointTypeFilter,
tagFilter,
tokenUnit,
viewMode,
showRechargePrice,
setSearchInput,
setSortBy,
setVendorFilter,
setGroupFilter,
setQuotaTypeFilter,
setEndpointTypeFilter,
setTagFilter,
setTokenUnit,
setViewMode,
setShowRechargePrice,
filteredModels,
hasActiveFilters,
activeFilterCount,
availableTags,
clearFilters,
clearSearch,
}
}
+78
View File
@@ -0,0 +1,78 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useStatus } from '@/hooks/use-status'
import { getPricing } from '../api'
export function usePricingData() {
const { status } = useStatus()
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['pricing'],
queryFn: getPricing,
staleTime: 5 * 60 * 1000,
})
// Ensure rates never reach zero to prevent division errors
const priceRate = useMemo(
() => Math.max((status?.price as number) ?? 1, 0.001),
[status?.price]
)
const usdExchangeRate = useMemo(
() => Math.max((status?.usd_exchange_rate as number) ?? priceRate, 0.001),
[status?.usd_exchange_rate, priceRate]
)
const models = useMemo(() => {
if (!data?.data || !data?.vendors) return []
const vendorMap = new Map(data.vendors.map((v) => [v.id, v]))
return data.data.map((model) => {
const vendor = model.vendor_id
? vendorMap.get(model.vendor_id)
: undefined
return {
...model,
key: model.model_name,
vendor_name: vendor?.name,
vendor_icon: vendor?.icon,
vendor_description: vendor?.description,
group_ratio: data.group_ratio,
}
})
}, [data])
return {
models,
vendors: data?.vendors ?? [],
groupRatio: data?.group_ratio ?? {},
usableGroup: data?.usable_group ?? {},
endpointMap: data?.supported_endpoint ?? {},
autoGroups: data?.auto_groups ?? [],
isLoading,
error,
refetch,
priceRate,
usdExchangeRate,
}
}
+289
View File
@@ -0,0 +1,289 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { PublicLayout } from '@/components/layout'
import { PageTransition } from '@/components/page-transition'
import {
LoadingSkeleton,
EmptyState,
SearchBar,
PricingTable,
PricingSidebar,
PricingToolbar,
ModelCardGrid,
ModelDetailsDrawer,
} from './components'
import { EXCLUDED_GROUPS, VIEW_MODES } from './constants'
import { useFilters } from './hooks/use-filters'
import { usePricingData } from './hooks/use-pricing-data'
export function Pricing() {
const { t } = useTranslation()
const [selectedModelName, setSelectedModelName] = useState<string | null>(
null
)
const {
models,
vendors,
groupRatio,
usableGroup,
endpointMap,
autoGroups,
isLoading,
priceRate,
usdExchangeRate,
} = usePricingData()
const {
searchInput,
sortBy,
vendorFilter,
groupFilter,
quotaTypeFilter,
endpointTypeFilter,
tagFilter,
tokenUnit,
viewMode,
showRechargePrice,
setSearchInput,
setSortBy,
setVendorFilter,
setGroupFilter,
setQuotaTypeFilter,
setEndpointTypeFilter,
setTagFilter,
setTokenUnit,
setViewMode,
setShowRechargePrice,
filteredModels,
hasActiveFilters,
activeFilterCount,
availableTags,
clearFilters,
clearSearch,
} = useFilters(models || [])
const handleModelClick = useCallback((modelName: string) => {
setSelectedModelName(modelName)
}, [])
const selectedModel = useMemo(
() =>
selectedModelName
? (models || []).find(
(model) => model.model_name === selectedModelName
) || null
: null,
[models, selectedModelName]
)
const availableGroups = useMemo(
() =>
Object.keys(usableGroup || {}).filter(
(g) => !EXCLUDED_GROUPS.includes(g)
),
[usableGroup]
)
const handleClearAll = useCallback(() => {
clearFilters()
clearSearch()
}, [clearFilters, clearSearch])
const renderPricingContent = () => {
if (filteredModels.length === 0) {
return (
<EmptyState
searchQuery={searchInput}
hasActiveFilters={hasActiveFilters}
onClearFilters={handleClearAll}
/>
)
}
if (viewMode === VIEW_MODES.CARD) {
return (
<ModelCardGrid
models={filteredModels}
onModelClick={handleModelClick}
priceRate={priceRate}
usdExchangeRate={usdExchangeRate}
tokenUnit={tokenUnit}
showRechargePrice={showRechargePrice}
selectedGroup={groupFilter}
/>
)
}
return (
<PricingTable
models={filteredModels}
priceRate={priceRate}
usdExchangeRate={usdExchangeRate}
tokenUnit={tokenUnit}
showRechargePrice={showRechargePrice}
selectedGroup={groupFilter}
onModelClick={handleModelClick}
/>
)
}
if (isLoading) {
return (
<PublicLayout showMainContainer={false}>
<div className='mx-auto w-full max-w-[1800px] px-3 pt-16 pb-8 sm:px-6 sm:pt-20 sm:pb-10 xl:px-8'>
<LoadingSkeleton viewMode={viewMode} />
</div>
</PublicLayout>
)
}
return (
<PublicLayout showMainContainer={false}>
<div className='relative'>
<div
aria-hidden
className='pointer-events-none absolute inset-x-0 top-0 h-[600px] opacity-20 dark:opacity-[0.10]'
style={{
background: [
'radial-gradient(ellipse 60% 50% at 20% 20%, oklch(0.72 0.18 250 / 80%) 0%, transparent 70%)',
'radial-gradient(ellipse 50% 40% at 80% 15%, oklch(0.65 0.15 200 / 60%) 0%, transparent 70%)',
'radial-gradient(ellipse 40% 35% at 50% 70%, oklch(0.70 0.12 280 / 40%) 0%, transparent 70%)',
].join(', '),
maskImage:
'linear-gradient(to bottom, black 40%, transparent 100%)',
WebkitMaskImage:
'linear-gradient(to bottom, black 40%, transparent 100%)',
}}
/>
<PageTransition className='relative mx-auto w-full max-w-[1800px] px-3 pt-16 pb-8 sm:px-6 sm:pt-20 sm:pb-10 xl:px-8'>
<header className='mx-auto mb-5 max-w-3xl pt-5 text-center sm:mb-10 sm:pt-10'>
<h1 className='text-[clamp(2rem,5.5vw,3.5rem)] leading-[1.15] font-bold tracking-tight'>
{t('Model Square')}
</h1>
<p className='text-muted-foreground/80 mt-3 text-sm sm:mt-4 sm:text-base'>
{t('This site currently has {{count}} models enabled', {
count: models?.length || 0,
})}
</p>
<p className='text-muted-foreground/60 mx-auto mt-2 max-w-2xl text-xs leading-relaxed sm:text-sm'>
{t(
'Discover curated AI models, compare pricing and capabilities, and choose the right model for every scenario.'
)}
</p>
<SearchBar
value={searchInput}
onChange={setSearchInput}
onClear={clearSearch}
placeholder={t(
'Search model name, provider, endpoint, or tag...'
)}
className='mx-auto mt-4 max-w-2xl sm:mt-6'
/>
</header>
<div className='grid gap-4 xl:grid-cols-[330px_minmax(0,1fr)]'>
<PricingSidebar
quotaTypeFilter={quotaTypeFilter}
endpointTypeFilter={endpointTypeFilter}
vendorFilter={vendorFilter}
groupFilter={groupFilter}
tagFilter={tagFilter}
onQuotaTypeChange={setQuotaTypeFilter}
onEndpointTypeChange={setEndpointTypeFilter}
onVendorChange={setVendorFilter}
onGroupChange={setGroupFilter}
onTagChange={setTagFilter}
vendors={vendors || []}
groups={availableGroups}
groupRatios={groupRatio}
tags={availableTags}
models={models || []}
hasActiveFilters={hasActiveFilters}
onClearFilters={clearFilters}
className='hover-scrollbar sticky top-4 hidden max-h-[calc(100dvh-2rem)] self-start overflow-y-auto xl:block'
/>
<main className='min-w-0 space-y-4'>
<PricingToolbar
filteredCount={filteredModels.length}
totalCount={models?.length}
sortBy={sortBy}
onSortChange={setSortBy}
tokenUnit={tokenUnit}
onTokenUnitChange={setTokenUnit}
showRechargePrice={showRechargePrice}
onRechargePriceChange={setShowRechargePrice}
viewMode={viewMode}
onViewModeChange={setViewMode}
quotaTypeFilter={quotaTypeFilter}
endpointTypeFilter={endpointTypeFilter}
vendorFilter={vendorFilter}
groupFilter={groupFilter}
tagFilter={tagFilter}
onQuotaTypeChange={setQuotaTypeFilter}
onEndpointTypeChange={setEndpointTypeFilter}
onVendorChange={setVendorFilter}
onGroupChange={setGroupFilter}
onTagChange={setTagFilter}
vendors={vendors || []}
groups={availableGroups}
groupRatios={groupRatio}
tags={availableTags}
models={models || []}
hasActiveFilters={hasActiveFilters}
activeFilterCount={activeFilterCount}
onClearFilters={clearFilters}
/>
{renderPricingContent()}
</main>
</div>
{selectedModel && (
<ModelDetailsDrawer
open={Boolean(selectedModel)}
onOpenChange={(open) => {
if (!open) setSelectedModelName(null)
}}
model={selectedModel}
groupRatio={groupRatio || {}}
usableGroup={usableGroup || {}}
endpointMap={
(endpointMap as Record<
string,
{ path?: string; method?: string }
>) || {}
}
autoGroups={autoGroups || []}
priceRate={priceRate ?? 1}
usdExchangeRate={usdExchangeRate ?? 1}
tokenUnit={tokenUnit}
showRechargePrice={showRechargePrice}
/>
)}
</PageTransition>
</div>
</PublicLayout>
)
}
+779
View File
@@ -0,0 +1,779 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
/**
* Billing expression parsing utilities.
*
* Parses the dynamic billing expression format so that the pricing breakdown
* UI can be rendered from the same backend expressions.
*
* The grammar is intentionally narrow: we only support the shapes that the
* server emits (tiered pricing + request-rule conditional multipliers), so
* the regular expressions are exact rather than tolerant of arbitrary
* expression syntax.
*/
// ---------------------------------------------------------------------------
// Variable registry
// ---------------------------------------------------------------------------
export type BillingVar = {
key: string
field: string | null
tierField: string | null
label: string
shortLabel: string
side: 'input' | 'output' | 'condition'
isBase?: boolean
isConditionOnly?: boolean
group?: string
}
export const BILLING_VARS: BillingVar[] = [
{
key: 'p',
field: 'inputPrice',
tierField: 'input_unit_cost',
label: 'Input price',
shortLabel: 'Input',
side: 'input',
isBase: true,
},
{
key: 'c',
field: 'outputPrice',
tierField: 'output_unit_cost',
label: 'Completion price',
shortLabel: 'Output',
side: 'output',
isBase: true,
},
{
key: 'len',
field: null,
tierField: null,
label: 'Input length',
shortLabel: 'Length',
side: 'condition',
isConditionOnly: true,
},
{
key: 'cr',
field: 'cacheReadPrice',
tierField: 'cache_read_unit_cost',
label: 'Cache read price',
shortLabel: 'Cache Read',
side: 'input',
group: 'cache',
},
{
key: 'cc',
field: 'cacheCreatePrice',
tierField: 'cache_create_unit_cost',
label: 'Cache create price',
shortLabel: 'Cache Write',
side: 'input',
group: 'cache',
},
{
key: 'cc1h',
field: 'cacheCreate1hPrice',
tierField: 'cache_create_1h_unit_cost',
label: 'Cache create (1h) price',
shortLabel: 'Cache Write (1h)',
side: 'input',
group: 'cache',
},
{
key: 'img',
field: 'imagePrice',
tierField: 'image_unit_cost',
label: 'Image input price',
shortLabel: 'Image In',
side: 'input',
group: 'media',
},
{
key: 'img_o',
field: 'imageOutputPrice',
tierField: 'image_output_unit_cost',
label: 'Image output price',
shortLabel: 'Image Out',
side: 'output',
group: 'media',
},
{
key: 'ai',
field: 'audioInputPrice',
tierField: 'audio_input_unit_cost',
label: 'Audio input price',
shortLabel: 'Audio In',
side: 'input',
group: 'media',
},
{
key: 'ao',
field: 'audioOutputPrice',
tierField: 'audio_output_unit_cost',
label: 'Audio output price',
shortLabel: 'Audio Out',
side: 'output',
group: 'media',
},
]
/** Vars that have real price fields (excludes condition-only vars like `len`) */
export const BILLING_PRICING_VARS: BillingVar[] = BILLING_VARS.filter(
(v) => !v.isConditionOnly
)
/** Vars valid in tier conditions (`p`, `c`, `len`) */
export const BILLING_CONDITION_VARS: string[] = BILLING_VARS.filter(
(v) => v.isBase || v.isConditionOnly
).map((v) => v.key)
const BILLING_VAR_KEY_TO_FIELD = Object.fromEntries(
BILLING_PRICING_VARS.map((v) => [v.key, v.field as string])
) as Record<string, string>
export const BILLING_EXTRA_VARS: BillingVar[] = BILLING_VARS.filter(
(v) => !v.isBase && !v.isConditionOnly
)
export const BILLING_CACHE_VAR_MAP = BILLING_EXTRA_VARS.map((v) => ({
field: v.tierField as string,
exprVar: v.key,
}))
const BILLING_VAR_REGEX = new RegExp(
`\\b(${BILLING_PRICING_VARS.map((v) => v.key).join('|')})\\s*\\*\\s*([\\d.eE+-]+)`,
'g'
)
// ---------------------------------------------------------------------------
// Request rule constants
// ---------------------------------------------------------------------------
export const SOURCE_PARAM = 'param'
export const SOURCE_HEADER = 'header'
export const SOURCE_TIME = 'time'
export const MATCH_EQ = 'eq'
export const MATCH_CONTAINS = 'contains'
export const MATCH_GT = 'gt'
export const MATCH_GTE = 'gte'
export const MATCH_LT = 'lt'
export const MATCH_LTE = 'lte'
export const MATCH_EXISTS = 'exists'
export const MATCH_RANGE = 'range'
export const TIME_FUNCS = ['hour', 'minute', 'weekday', 'month', 'day'] as const
export type TimeFunc = (typeof TIME_FUNCS)[number]
export const COMMON_TIMEZONES: { value: string; label: string }[] = [
{ value: 'Asia/Shanghai', label: 'UTC+8 Shanghai (Asia/Shanghai)' },
{ value: 'UTC', label: 'UTC' },
{ value: 'America/New_York', label: 'UTC-5 New York (America/New_York)' },
{
value: 'America/Los_Angeles',
label: 'UTC-8 Los Angeles (America/Los_Angeles)',
},
{ value: 'America/Chicago', label: 'UTC-6 Chicago (America/Chicago)' },
{ value: 'Europe/London', label: 'UTC+0 London (Europe/London)' },
{ value: 'Europe/Berlin', label: 'UTC+1 Berlin (Europe/Berlin)' },
{ value: 'Asia/Tokyo', label: 'UTC+9 Tokyo (Asia/Tokyo)' },
{ value: 'Asia/Singapore', label: 'UTC+8 Singapore (Asia/Singapore)' },
{ value: 'Asia/Seoul', label: 'UTC+9 Seoul (Asia/Seoul)' },
{ value: 'Australia/Sydney', label: 'UTC+10 Sydney (Australia/Sydney)' },
]
const NUMERIC_LITERAL_REGEX = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/
export type ParamHeaderCondition = {
source: 'param' | 'header'
path: string
mode: string
value: string
}
export type TimeCondition = {
source: 'time'
timeFunc: TimeFunc
timezone: string
mode: string
value: string
rangeStart: string
rangeEnd: string
}
export type RequestCondition = TimeCondition | ParamHeaderCondition
export type RequestRuleGroup = {
conditions: RequestCondition[]
multiplier: string
}
export type TierCondition = {
var: 'p' | 'c' | 'len'
op: '<' | '<=' | '>' | '>='
value: number
}
export type ParsedTier = {
label: string
conditions: TierCondition[]
[field: string]: unknown
}
// ---------------------------------------------------------------------------
// Tier parser
// ---------------------------------------------------------------------------
function stripExprVersion(exprStr: string): { version: number; body: string } {
if (!exprStr) return { version: 1, body: '' }
const m = exprStr.match(/^v(\d+):([\s\S]*)$/)
if (m) return { version: Number(m[1]), body: m[2] }
return { version: 1, body: exprStr }
}
function parseTierBody(bodyStr: string): Record<string, number> {
const coeffs: Record<string, number> = {}
const re = new RegExp(BILLING_VAR_REGEX.source, 'g')
let m
while ((m = re.exec(bodyStr)) !== null) {
if (!(m[1] in coeffs)) coeffs[m[1]] = Number(m[2])
}
const tier: Record<string, number> = {}
for (const [varName, field] of Object.entries(BILLING_VAR_KEY_TO_FIELD)) {
tier[field] = coeffs[varName] || 0
}
return tier
}
export function parseTiersFromExpr(exprStr: string): ParsedTier[] {
if (!exprStr) return []
try {
const { body } = stripExprVersion(exprStr)
const condGroup =
`((?:(?:p|c|len)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)` +
`(?:\\s*&&\\s*(?:p|c|len)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)*)`
const tierRe = new RegExp(
`(?:${condGroup}\\s*\\?\\s*)?tier\\("([^"]*)",\\s*([^)]+)\\)`,
'g'
)
const tiers: ParsedTier[] = []
let m
while ((m = tierRe.exec(body)) !== null) {
const condStr = m[1] || ''
const conditions: TierCondition[] = []
if (condStr) {
for (const cp of condStr.split(/\s*&&\s*/)) {
const cm = cp.trim().match(/^(p|c|len)\s*(<|<=|>|>=)\s*([\d.eE+]+)$/)
if (cm) {
conditions.push({
var: cm[1] as TierCondition['var'],
op: cm[2] as TierCondition['op'],
value: Number(cm[3]),
})
}
}
}
const tier = parseTierBody(m[3]) as ParsedTier
tier.label = m[2]
tier.conditions = conditions
tiers.push(tier)
}
return tiers
} catch {
return []
}
}
export function normalizeTierLabel(label: string | undefined): string {
if (!label) return ''
return label
.replace(/<[=]?|≤|[=]?/g, '<')
.replace(/>[=]?|≥|[=]?/g, '>')
.replace(/\s+/g, '')
.toLowerCase()
}
// ---------------------------------------------------------------------------
// Request rule parser
// ---------------------------------------------------------------------------
function splitTopLevelMultiply(expr: string): string[] {
const parts: string[] = []
let start = 0
let depth = 0
for (let index = 0; index < expr.length; index += 1) {
const char = expr[index]
if (char === '(') depth += 1
if (char === ')') depth -= 1
if (depth === 0 && expr.slice(index, index + 3) === ' * ') {
parts.push(expr.slice(start, index).trim())
start = index + 3
index += 2
}
}
parts.push(expr.slice(start).trim())
return parts.filter(Boolean)
}
function splitTopLevelAnd(expr: string): string[] {
const parts: string[] = []
let start = 0
let depth = 0
for (let i = 0; i < expr.length; i += 1) {
const c = expr[i]
if (c === '(') depth += 1
if (c === ')') depth -= 1
if (depth === 0 && expr.slice(i, i + 4) === ' && ') {
parts.push(expr.slice(start, i).trim())
start = i + 4
i += 3
}
}
parts.push(expr.slice(start).trim())
return parts.filter(Boolean)
}
function parseExprLiteral(raw: string): string | null {
const text = raw.trim()
if (text === 'true' || text === 'false') return text
if (NUMERIC_LITERAL_REGEX.test(text)) return text
try {
return JSON.parse(text) as string
} catch {
return null
}
}
function tryParseTimeCondition(expr: string): RequestCondition | null {
let m = expr.match(
/^(hour|minute|weekday|month|day)\("([^"]+)"\) >= ([\d.eE+-]+) \|\| \1\("\2"\) < ([\d.eE+-]+)$/
)
if (m) {
return {
source: 'time',
timeFunc: m[1] as TimeFunc,
timezone: m[2],
mode: MATCH_RANGE,
value: '',
rangeStart: m[3],
rangeEnd: m[4],
}
}
m = expr.match(
/^\((hour|minute|weekday|month|day)\("([^"]+)"\) >= ([\d.eE+-]+) \|\| \1\("\2"\) < ([\d.eE+-]+)\)$/
)
if (m) {
return {
source: 'time',
timeFunc: m[1] as TimeFunc,
timezone: m[2],
mode: MATCH_RANGE,
value: '',
rangeStart: m[3],
rangeEnd: m[4],
}
}
m = expr.match(
/^(hour|minute|weekday|month|day)\("([^"]+)"\) (==|>=|<) ([\d.eE+-]+)$/
)
if (m) {
const opMap: Record<string, string> = {
'==': MATCH_EQ,
'>=': MATCH_GTE,
'<': MATCH_LT,
}
return {
source: 'time',
timeFunc: m[1] as TimeFunc,
timezone: m[2],
mode: opMap[m[3]] || MATCH_EQ,
value: m[4],
rangeStart: '',
rangeEnd: '',
}
}
return null
}
function tryParseRequestCondition(expr: string): RequestCondition | null {
const tc = tryParseTimeCondition(expr)
if (tc) return tc
let m = expr.match(/^header\("([^"]+)"\) != ""$/)
if (m) return { source: 'header', path: m[1], mode: MATCH_EXISTS, value: '' }
m = expr.match(/^param\("([^"]+)"\) != nil$/)
if (m) return { source: 'param', path: m[1], mode: MATCH_EXISTS, value: '' }
m = expr.match(/^has\(header\("([^"]+)"\), ((?:"(?:[^"\\]|\\.)*"))\)$/)
if (m)
return {
source: 'header',
path: m[1],
mode: MATCH_CONTAINS,
value: JSON.parse(m[2]) as string,
}
m = expr.match(
/^param\("([^"]+)"\) != nil && has\(param\("([^"]+)"\), ((?:"(?:[^"\\]|\\.)*"))\)$/
)
if (m && m[1] === m[2])
return {
source: 'param',
path: m[1],
mode: MATCH_CONTAINS,
value: JSON.parse(m[3]) as string,
}
m = expr.match(
/^param\("([^"]+)"\) != nil && param\("([^"]+)"\) (>|>=|<|<=) ([\d.eE+-]+)$/
)
if (m && m[1] === m[2]) {
const opMap: Record<string, string> = {
'>': MATCH_GT,
'>=': MATCH_GTE,
'<': MATCH_LT,
'<=': MATCH_LTE,
}
return { source: 'param', path: m[1], mode: opMap[m[3]], value: m[4] }
}
m = expr.match(/^(param|header)\("([^"]+)"\) == (.+)$/)
if (m) {
const parsedValue = parseExprLiteral(m[3])
if (parsedValue === null) return null
return {
source: m[1] as 'param' | 'header',
path: m[2],
mode: MATCH_EQ,
value: String(parsedValue),
}
}
return null
}
function tryParseRuleGroupFactor(part: string): RequestRuleGroup | null {
const m = part.match(/^\((.+) \? ([\d.eE+-]+) : 1\)$/s)
if (!m) return null
const conditionStr = m[1]
const multiplier = m[2]
const andParts = splitTopLevelAnd(conditionStr)
const conditions: RequestCondition[] = []
for (const ap of andParts) {
const cond = tryParseRequestCondition(ap.trim())
if (!cond) return null
conditions.push(cond)
}
if (conditions.length === 0) return null
return { conditions, multiplier }
}
export function tryParseRequestRuleExpr(
expr: string
): RequestRuleGroup[] | null {
const trimmed = (expr || '').trim()
if (!trimmed) return []
const parts = splitTopLevelMultiply(trimmed)
const groups: RequestRuleGroup[] = []
for (const part of parts) {
const group = tryParseRuleGroupFactor(part)
if (!group) return null
groups.push(group)
}
return groups
}
// ---------------------------------------------------------------------------
// Combine / split billing expr and request rules
// ---------------------------------------------------------------------------
function hasFullOuterParens(expr: string): boolean {
if (!expr.startsWith('(') || !expr.endsWith(')')) return false
let depth = 0
for (let i = 0; i < expr.length; i += 1) {
if (expr[i] === '(') depth += 1
if (expr[i] === ')') depth -= 1
if (depth === 0 && i < expr.length - 1) return false
}
return depth === 0
}
function unwrapOuterParens(expr: string): string {
let current = (expr || '').trim()
while (hasFullOuterParens(current)) {
current = current.slice(1, -1).trim()
}
return current
}
export function splitBillingExprAndRequestRules(expr: string): {
billingExpr: string
requestRuleExpr: string
} {
const trimmed = (expr || '').trim()
if (!trimmed) return { billingExpr: '', requestRuleExpr: '' }
const parts = splitTopLevelMultiply(trimmed)
if (parts.length <= 1) return { billingExpr: trimmed, requestRuleExpr: '' }
const ruleParts: string[] = []
const baseParts: string[] = []
parts.forEach((part) => {
const parsed = tryParseRequestRuleExpr(part)
if (parsed && parsed.length > 0) {
ruleParts.push(part)
} else {
baseParts.push(part)
}
})
if (ruleParts.length === 0 || baseParts.length !== 1) {
return { billingExpr: trimmed, requestRuleExpr: '' }
}
return {
billingExpr: unwrapOuterParens(baseParts[0]),
requestRuleExpr: ruleParts.join(' * '),
}
}
export function combineBillingExpr(
baseExpr: string,
requestRuleExpr: string
): string {
const base = (baseExpr || '').trim()
const rules = (requestRuleExpr || '').trim()
if (!base) return ''
if (!rules) return base
return `(${base}) * ${rules}`
}
// ---------------------------------------------------------------------------
// Editor: empty constructors
// ---------------------------------------------------------------------------
export function createEmptyCondition(): ParamHeaderCondition {
return { source: 'param', path: '', mode: MATCH_EQ, value: '' }
}
export function createEmptyTimeCondition(): TimeCondition {
return {
source: 'time',
timeFunc: 'hour',
timezone: 'Asia/Shanghai',
mode: MATCH_GTE,
value: '',
rangeStart: '',
rangeEnd: '',
}
}
export function createEmptyRuleGroup(): RequestRuleGroup {
return { conditions: [createEmptyCondition()], multiplier: '' }
}
export function createEmptyTimeRuleGroup(): RequestRuleGroup {
return { conditions: [createEmptyTimeCondition()], multiplier: '' }
}
// ---------------------------------------------------------------------------
// Editor: match option helpers
// ---------------------------------------------------------------------------
export type MatchOption = { value: string; labelKey: string }
export function getRequestRuleMatchOptions(source: string): MatchOption[] {
if (source === SOURCE_TIME) {
return [
{ value: MATCH_EQ, labelKey: 'Equals' },
{ value: MATCH_GTE, labelKey: 'Greater than or equal' },
{ value: MATCH_LT, labelKey: 'Less than' },
{ value: MATCH_RANGE, labelKey: 'Overnight range' },
]
}
const base: MatchOption[] = [
{ value: MATCH_EQ, labelKey: 'Equals' },
{ value: MATCH_CONTAINS, labelKey: 'Contains' },
{ value: MATCH_EXISTS, labelKey: 'Exists' },
]
if (source === SOURCE_HEADER) return base
return [
...base,
{ value: MATCH_GT, labelKey: 'Greater than' },
{ value: MATCH_GTE, labelKey: 'Greater than or equal' },
{ value: MATCH_LT, labelKey: 'Less than' },
{ value: MATCH_LTE, labelKey: 'Less than or equal' },
]
}
// ---------------------------------------------------------------------------
// Editor: normalize a single condition
// ---------------------------------------------------------------------------
function isTimeFunc(value: unknown): value is TimeFunc {
return typeof value === 'string' && TIME_FUNCS.includes(value as TimeFunc)
}
export function normalizeCondition(
cond: Partial<RequestCondition> | null | undefined
): RequestCondition {
const source =
cond?.source === 'time'
? 'time'
: cond?.source === 'header'
? 'header'
: 'param'
if (source === 'time') {
const timeCond = cond as Partial<TimeCondition> | null | undefined
const timeFunc: TimeFunc = isTimeFunc(timeCond?.timeFunc)
? timeCond.timeFunc
: 'hour'
const options = getRequestRuleMatchOptions(SOURCE_TIME)
const mode = options.some((item) => item.value === timeCond?.mode)
? (timeCond?.mode as string)
: MATCH_GTE
return {
source: 'time',
timeFunc,
timezone: timeCond?.timezone || 'Asia/Shanghai',
mode,
value: timeCond?.value == null ? '' : String(timeCond.value),
rangeStart:
timeCond?.rangeStart == null ? '' : String(timeCond.rangeStart),
rangeEnd: timeCond?.rangeEnd == null ? '' : String(timeCond.rangeEnd),
}
}
const phCond = cond as Partial<ParamHeaderCondition> | null | undefined
const options = getRequestRuleMatchOptions(source)
const mode = options.some((item) => item.value === phCond?.mode)
? (phCond?.mode as string)
: MATCH_EQ
return {
source,
path: phCond?.path || '',
mode,
value: phCond?.value == null ? '' : String(phCond.value),
}
}
// ---------------------------------------------------------------------------
// Editor: build expression strings
// ---------------------------------------------------------------------------
function buildExprLiteral(mode: string, value: string): string {
const text = String(value || '').trim()
if (mode === MATCH_CONTAINS) return JSON.stringify(text)
if (text === 'true' || text === 'false') return text
if (NUMERIC_LITERAL_REGEX.test(text)) return text
return JSON.stringify(text)
}
function buildTimeConditionExpr(cond: TimeCondition): string {
const normalized = normalizeCondition(cond) as TimeCondition
const { timeFunc, timezone, mode } = normalized
const tz = JSON.stringify(timezone)
const fn = `${timeFunc}(${tz})`
if (mode === MATCH_RANGE) {
const s = normalized.rangeStart.trim()
const e = normalized.rangeEnd.trim()
if (!NUMERIC_LITERAL_REGEX.test(s) || !NUMERIC_LITERAL_REGEX.test(e)) {
return ''
}
return `${fn} >= ${s} || ${fn} < ${e}`
}
const v = normalized.value.trim()
if (!NUMERIC_LITERAL_REGEX.test(v)) return ''
const opMap: Record<string, string> = {
[MATCH_EQ]: '==',
[MATCH_GTE]: '>=',
[MATCH_LT]: '<',
}
return `${fn} ${opMap[mode] || '=='} ${v}`
}
function buildRequestConditionExpr(cond: RequestCondition): string {
if (cond.source === 'time') return buildTimeConditionExpr(cond)
const normalized = normalizeCondition(cond) as ParamHeaderCondition
const path = normalized.path.trim()
if (!path) return ''
const sourceExpr =
normalized.source === 'header'
? `header(${JSON.stringify(path)})`
: `param(${JSON.stringify(path)})`
switch (normalized.mode) {
case MATCH_EXISTS:
return normalized.source === 'header'
? `${sourceExpr} != ""`
: `${sourceExpr} != nil`
case MATCH_CONTAINS:
return normalized.source === 'header'
? `has(${sourceExpr}, ${buildExprLiteral(normalized.mode, normalized.value)})`
: `${sourceExpr} != nil && has(${sourceExpr}, ${buildExprLiteral(normalized.mode, normalized.value)})`
case MATCH_GT:
case MATCH_GTE:
case MATCH_LT:
case MATCH_LTE: {
const opMap: Record<string, string> = {
[MATCH_GT]: '>',
[MATCH_GTE]: '>=',
[MATCH_LT]: '<',
[MATCH_LTE]: '<=',
}
const numText = String(normalized.value).trim()
if (!NUMERIC_LITERAL_REGEX.test(numText)) return ''
return `${sourceExpr} != nil && ${sourceExpr} ${opMap[normalized.mode]} ${numText}`
}
case MATCH_EQ:
default:
return `${sourceExpr} == ${buildExprLiteral(normalized.mode, normalized.value)}`
}
}
function buildRuleGroupFactor(group: RequestRuleGroup): string {
const multiplier = (group.multiplier || '').trim()
if (!NUMERIC_LITERAL_REGEX.test(multiplier)) return ''
const condExprs = (group.conditions || [])
.map(buildRequestConditionExpr)
.filter(Boolean)
if (condExprs.length === 0) return ''
const combined =
condExprs.length === 1
? condExprs[0]
: condExprs.map((e) => (e.includes(' || ') ? `(${e})` : e)).join(' && ')
return `(${combined} ? ${multiplier} : 1)`
}
export function buildRequestRuleExpr(groups: RequestRuleGroup[]): string {
return (groups || []).map(buildRuleGroupFactor).filter(Boolean).join(' * ')
}
+182
View File
@@ -0,0 +1,182 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
import { TOKEN_UNIT_DIVISORS } from '../constants'
import type { PricingModel, TokenUnit } from '../types'
import {
BILLING_PRICING_VARS,
parseTiersFromExpr,
splitBillingExprAndRequestRules,
tryParseRequestRuleExpr,
type BillingVar,
type ParsedTier,
} from './billing-expr'
import { getDisplayGroupRatio } from './model-helpers'
type DynamicPriceOptions = {
tokenUnit: TokenUnit
showRechargePrice?: boolean
priceRate?: number
usdExchangeRate?: number
groupRatioMultiplier?: number
}
export type DynamicPriceEntry = {
key: string
field: string
label: string
shortLabel: string
value: number
formatted: string
variable: BillingVar
}
export type DynamicPricingSummary = {
tiers: ParsedTier[]
tier: ParsedTier | null
tierCount: number
hasRequestRules: boolean
isSpecialExpression: boolean
rawExpression: string
entries: DynamicPriceEntry[]
primaryEntries: DynamicPriceEntry[]
secondaryEntries: DynamicPriceEntry[]
}
const PRIMARY_DYNAMIC_FIELDS = new Set(['inputPrice', 'outputPrice'])
export function isDynamicPricingModel(model: PricingModel): boolean {
return model.billing_mode === 'tiered_expr' && Boolean(model.billing_expr)
}
export function getDynamicDisplayGroupRatio(
model: PricingModel,
selectedGroup?: string
): number {
return getDisplayGroupRatio(model, selectedGroup)
}
function applyRechargeRate(
price: number,
showWithRecharge: boolean,
priceRate: number,
usdExchangeRate: number
): number {
if (!showWithRecharge) return price
return (price * priceRate) / usdExchangeRate
}
export function formatDynamicUnitPrice(
valuePerMillionTokens: number,
options: DynamicPriceOptions
): string {
const groupRatio = options.groupRatioMultiplier ?? 1
const priceRate = options.priceRate ?? 1
const usdExchangeRate = options.usdExchangeRate ?? 1
const priceUSD =
(valuePerMillionTokens * groupRatio) /
TOKEN_UNIT_DIVISORS[options.tokenUnit]
const displayPrice = applyRechargeRate(
priceUSD,
options.showRechargePrice ?? false,
priceRate,
usdExchangeRate
)
return formatBillingCurrencyFromUSD(displayPrice, {
digitsLarge: 4,
digitsSmall: 6,
abbreviate: false,
})
}
export function getDynamicPricingTiers(model: PricingModel): ParsedTier[] {
if (!isDynamicPricingModel(model)) return []
const { billingExpr } = splitBillingExprAndRequestRules(
model.billing_expr || ''
)
return parseTiersFromExpr(billingExpr)
}
export function hasDynamicRequestRules(model: PricingModel): boolean {
if (!isDynamicPricingModel(model)) return false
const { requestRuleExpr } = splitBillingExprAndRequestRules(
model.billing_expr || ''
)
return Boolean(tryParseRequestRuleExpr(requestRuleExpr || '')?.length)
}
export function getDynamicPriceEntries(
tier: ParsedTier | null,
options: DynamicPriceOptions
): DynamicPriceEntry[] {
if (!tier) return []
return BILLING_PRICING_VARS.flatMap((variable) => {
if (!variable.field) return []
const value = Number(tier[variable.field])
if (!Number.isFinite(value) || value <= 0) return []
return [
{
key: variable.key,
field: variable.field,
label: variable.label,
shortLabel: variable.shortLabel,
value,
formatted: formatDynamicUnitPrice(value, options),
variable,
},
]
}).sort((a, b) => {
const aPrimary = PRIMARY_DYNAMIC_FIELDS.has(a.field)
const bPrimary = PRIMARY_DYNAMIC_FIELDS.has(b.field)
if (aPrimary !== bPrimary) return aPrimary ? -1 : 1
return 0
})
}
export function getDynamicPricingSummary(
model: PricingModel,
options: DynamicPriceOptions
): DynamicPricingSummary | null {
if (!isDynamicPricingModel(model)) return null
const tiers = getDynamicPricingTiers(model)
const tier = tiers[0] || null
const entries = getDynamicPriceEntries(tier, options)
const rawExpression = model.billing_expr || ''
return {
tiers,
tier,
tierCount: tiers.length,
hasRequestRules: hasDynamicRequestRules(model),
isSpecialExpression: rawExpression.trim().length > 0 && tiers.length === 0,
rawExpression,
entries,
primaryEntries: entries.filter((entry) =>
PRIMARY_DYNAMIC_FIELDS.has(entry.field)
),
secondaryEntries: entries.filter(
(entry) => !PRIMARY_DYNAMIC_FIELDS.has(entry.field)
),
}
}
+204
View File
@@ -0,0 +1,204 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import {
SORT_OPTIONS,
FILTER_ALL,
QUOTA_TYPES,
QUOTA_TYPE_VALUES,
ENDPOINT_TYPES,
} from '../constants'
import type { PricingModel } from '../types'
// ----------------------------------------------------------------------------
// Filter Utilities
// ----------------------------------------------------------------------------
/**
* Filter models by search query
*/
export function filterBySearch(
models: PricingModel[],
query: string
): PricingModel[] {
if (!query) return models
const lowerQuery = query.toLowerCase()
return models.filter(
(m) =>
m.model_name?.toLowerCase().includes(lowerQuery) ||
m.description?.toLowerCase().includes(lowerQuery) ||
m.tags?.toLowerCase().includes(lowerQuery) ||
m.vendor_name?.toLowerCase().includes(lowerQuery)
)
}
/**
* Filter models by vendor
*/
export function filterByVendor(
models: PricingModel[],
vendor: string
): PricingModel[] {
if (vendor === FILTER_ALL) return models
return models.filter((m) => m.vendor_name === vendor)
}
/**
* Filter models by group
*/
export function filterByGroup(
models: PricingModel[],
group: string
): PricingModel[] {
if (group === FILTER_ALL) return models
return models.filter((m) => m.enable_groups?.includes(group))
}
/**
* Filter models by quota type
*/
export function filterByQuotaType(
models: PricingModel[],
quotaType: string
): PricingModel[] {
if (quotaType === QUOTA_TYPES.ALL) return models
const targetType =
quotaType === QUOTA_TYPES.TOKEN
? QUOTA_TYPE_VALUES.TOKEN
: QUOTA_TYPE_VALUES.REQUEST
return models.filter((m) => m.quota_type === targetType)
}
/**
* Filter models by endpoint type
*/
export function filterByEndpointType(
models: PricingModel[],
endpointType: string
): PricingModel[] {
if (endpointType === ENDPOINT_TYPES.ALL) return models
return models.filter((m) =>
m.supported_endpoint_types?.includes(endpointType)
)
}
/**
* Get model price for sorting
*/
function getModelPrice(model: PricingModel): number {
return model.quota_type === 0 ? model.model_ratio : model.model_price || 0
}
/**
* Sort models by specified option
*/
export function sortModels(
models: PricingModel[],
sortBy: string
): PricingModel[] {
const sorted = [...models]
switch (sortBy) {
case SORT_OPTIONS.NAME:
sorted.sort((a, b) =>
(a.model_name || '').localeCompare(b.model_name || '')
)
break
case SORT_OPTIONS.PRICE_LOW:
sorted.sort((a, b) => getModelPrice(a) - getModelPrice(b))
break
case SORT_OPTIONS.PRICE_HIGH:
sorted.sort((a, b) => getModelPrice(b) - getModelPrice(a))
break
}
return sorted
}
/**
* Apply all filters and sorting to models
*/
export function filterAndSortModels(
models: PricingModel[],
filters: {
search: string
vendor: string
group: string
quotaType: string
endpointType: string
tag: string
sortBy: string
}
): PricingModel[] {
let result = filterBySearch(models, filters.search)
result = filterByVendor(result, filters.vendor)
result = filterByGroup(result, filters.group)
result = filterByQuotaType(result, filters.quotaType)
result = filterByEndpointType(result, filters.endpointType)
result = filterByTag(result, filters.tag)
result = sortModels(result, filters.sortBy)
return result
}
/**
* Parse tags from comma-separated string
*/
export function parseTags(tagsString?: string): string[] {
if (!tagsString) return []
return tagsString
.split(/[,;|\s]+/)
.map((t) => t.trim())
.filter(Boolean)
}
/**
* Extract all unique tags from models
*/
export function extractAllTags(models: PricingModel[]): string[] {
const tagSet = new Set<string>()
models.forEach((model) => {
if (model.tags) {
const tags = parseTags(model.tags)
tags.forEach((tag) => {
tagSet.add(tag.toLowerCase())
})
}
})
return Array.from(tagSet).sort((a, b) => a.localeCompare(b))
}
/**
* Filter models by tag
*/
export function filterByTag(
models: PricingModel[],
tag: string
): PricingModel[] {
if (tag === FILTER_ALL) return models
const tagLower = tag.toLowerCase()
return models.filter((m) => {
if (!m.tags) return false
const modelTags = parseTags(m.tags).map((t) => t.toLowerCase())
return modelTags.includes(tagLower)
})
}
+29
View File
@@ -0,0 +1,29 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
// ----------------------------------------------------------------------------
// Pricing Lib Exports
// ----------------------------------------------------------------------------
export * from './filters'
export * from './price'
export * from './model-helpers'
export * from './billing-expr'
export * from './tier-expr'
export * from './mock-stats'
export * from './seed'
+842
View File
@@ -0,0 +1,842 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { PricingModel } from '../types'
import {
hashStringToSeed,
randomInRange,
randomIntInRange,
seededRandom,
} from './seed'
// ----------------------------------------------------------------------------
// Mock model statistics
// ----------------------------------------------------------------------------
//
// The backend has not yet implemented latency / uptime / app-ranking data.
// These helpers generate plausible, deterministic mock values seeded from
// the model name (and optionally the group name) so that:
// - Every render of the same model shows the same numbers
// - Different models / different groups render visibly distinct values
//
// When the backend ships real metrics, callers should switch to the
// real API and these helpers can be deleted. The shape of the returned
// data is designed to mirror what we expect the real endpoints to return.
export type GroupPerformance = {
group: string
ttft_p50_ms: number
ttft_p95_ms: number
ttft_p99_ms: number
throughput_tps: number
uptime_30d_pct: number
/** Number of monitored requests in the last 24h (display only). */
request_volume_24h: number
}
export type LatencyTimePoint = {
timestamp: string
group: string
ttft_ms: number
}
export type UptimeDayPoint = {
date: string
uptime_pct: number
incidents: number
outage_minutes: number
}
export type AppRanking = {
rank: number
name: string
description: string
category: string
growth_pct: number
monthly_tokens: number
url?: string
initial: string
}
const APP_TEMPLATES: Array<
Omit<AppRanking, 'rank' | 'monthly_tokens' | 'growth_pct' | 'initial'>
> = [
{
name: 'Cline',
description: 'Autonomous coding agent inside the IDE',
category: 'Coding',
url: 'https://cline.bot',
},
{
name: 'Roo Code',
description: 'AI agent for VS Code with multi-step planning',
category: 'Coding',
url: 'https://roocode.com',
},
{
name: 'Open WebUI',
description: 'Self-hosted ChatGPT-like web interface',
category: 'Chat',
url: 'https://openwebui.com',
},
{
name: 'LibreChat',
description: 'Open-source chat platform with multi-model support',
category: 'Chat',
url: 'https://librechat.ai',
},
{
name: 'Lobe Chat',
description: 'Modern open-source chat UI with plugins',
category: 'Chat',
url: 'https://lobehub.com',
},
{
name: 'NextChat',
description: 'Cross-platform private ChatGPT client',
category: 'Chat',
url: 'https://nextchat.dev',
},
{
name: 'Continue',
description: 'Open-source AI code assistant for editors',
category: 'Coding',
url: 'https://continue.dev',
},
{
name: 'Aider',
description: 'Pair-programming agent in your terminal',
category: 'Coding',
url: 'https://aider.chat',
},
{
name: 'Dify',
description: 'LLM application development platform',
category: 'Platform',
url: 'https://dify.ai',
},
{
name: 'FastGPT',
description: 'Knowledge base orchestration and chat platform',
category: 'Platform',
url: 'https://fastgpt.in',
},
{
name: 'Flowise',
description: 'Low-code LLM workflow builder',
category: 'Platform',
url: 'https://flowiseai.com',
},
{
name: 'OpenInterpreter',
description: 'Natural-language code execution agent',
category: 'Coding',
url: 'https://openinterpreter.com',
},
{
name: 'Devika',
description: 'Open-source AI software engineer',
category: 'Coding',
url: 'https://github.com/stitionai/devika',
},
{
name: 'Cherry Studio',
description: 'Multi-model desktop chat client',
category: 'Chat',
url: 'https://cherry-ai.com',
},
{
name: 'AnythingLLM',
description: 'Workspaces around your private documents',
category: 'Platform',
url: 'https://anythingllm.com',
},
{
name: 'OpenHands',
description: 'Coding agent with browser-and-code tools',
category: 'Coding',
url: 'https://docs.all-hands.dev',
},
{
name: 'Cursor',
description: 'AI-native code editor',
category: 'Coding',
url: 'https://cursor.com',
},
{
name: 'Zed',
description: 'Multiplayer code editor with AI',
category: 'Coding',
url: 'https://zed.dev',
},
{
name: 'Notion AI',
description: 'Documents and writing assistant',
category: 'Productivity',
url: 'https://notion.so',
},
{
name: 'Raycast AI',
description: 'AI on your macOS launcher',
category: 'Productivity',
url: 'https://raycast.com',
},
{
name: 'Obsidian Smart Connections',
description: 'Connect notes with semantic search',
category: 'Productivity',
},
{
name: 'Bolt.new',
description: 'Prompt-to-app full-stack builder',
category: 'Coding',
url: 'https://bolt.new',
},
{
name: 'Pieces',
description: 'AI workflow companion for developers',
category: 'Productivity',
url: 'https://pieces.app',
},
{
name: 'AmazingAI',
description: 'Personal AI knowledge assistant',
category: 'Productivity',
},
{
name: 'TypingMind',
description: 'Better UI for ChatGPT and Claude',
category: 'Chat',
url: 'https://typingmind.com',
},
]
const PROFILE_BY_NAME = (name: string) => {
const n = name.toLowerCase()
if (/embed|rerank/.test(n)) return 'embedding'
if (/image|sora|veo|kling|pika|jimeng|dalle|imagen/.test(n)) return 'image'
if (/whisper|tts|voice|audio/.test(n)) return 'audio'
if (/o1|o3|o4|reasoning|thinking|deepseek-r/.test(n)) return 'reasoning'
if (/flash|haiku|mini|small|nano|fast/.test(n)) return 'fast'
if (/gpt-5|opus|ultra|405|70b/.test(n)) return 'large'
return 'standard'
}
type ProfileSpec = {
ttftRange: [number, number]
throughputRange: [number, number]
uptimeRange: [number, number]
}
const PROFILE_SPECS: Record<string, ProfileSpec> = {
embedding: {
ttftRange: [40, 120],
throughputRange: [0, 0],
uptimeRange: [99.9, 99.99],
},
image: {
ttftRange: [2_500, 12_000],
throughputRange: [0, 0],
uptimeRange: [98.5, 99.8],
},
audio: {
ttftRange: [180, 600],
throughputRange: [0, 0],
uptimeRange: [99.5, 99.95],
},
reasoning: {
ttftRange: [1_800, 5_500],
throughputRange: [25, 70],
uptimeRange: [99.4, 99.95],
},
fast: {
ttftRange: [180, 480],
throughputRange: [110, 240],
uptimeRange: [99.7, 99.99],
},
large: {
ttftRange: [600, 1_400],
throughputRange: [55, 95],
uptimeRange: [99.5, 99.95],
},
standard: {
ttftRange: [400, 900],
throughputRange: [70, 140],
uptimeRange: [99.6, 99.97],
},
}
function rangeFromSeed(
rand: () => number,
[min, max]: [number, number]
): number {
return randomInRange(rand, min, max)
}
function applyGroupFactor(value: number, factor: number): number {
return value * factor
}
function groupFactor(
group: string,
baseSeed: number
): { ttft: number; throughput: number; uptime: number } {
const rand = seededRandom(baseSeed ^ hashStringToSeed(group || 'default'))
return {
ttft: 0.85 + rand() * 0.55,
throughput: 0.85 + rand() * 0.4,
uptime: 0.997 + rand() * 0.003,
}
}
/**
* Build per-group performance stats for a model. Always returns at least one
* row for each enabled group, sorted alphabetically.
*/
export function buildGroupPerformance(model: PricingModel): GroupPerformance[] {
const groups = (model.enable_groups ?? []).filter((g) => g && g !== 'auto')
const targets = groups.length > 0 ? groups : ['default']
const profile = PROFILE_BY_NAME(model.model_name)
const spec = PROFILE_SPECS[profile]
const baseSeed = hashStringToSeed(model.model_name)
return targets
.slice()
.sort((a, b) => a.localeCompare(b))
.map<GroupPerformance>((group) => {
const rand = seededRandom(baseSeed ^ hashStringToSeed(group))
const factor = groupFactor(group, baseSeed)
const ttftP50 = applyGroupFactor(
rangeFromSeed(rand, spec.ttftRange),
factor.ttft
)
const throughput = applyGroupFactor(
rangeFromSeed(rand, spec.throughputRange),
factor.throughput
)
const uptimePct = Math.min(
99.99,
rangeFromSeed(rand, spec.uptimeRange) * factor.uptime
)
const requestVolume = randomIntInRange(rand, 18_000, 480_000)
return {
group,
ttft_p50_ms: Math.round(ttftP50),
ttft_p95_ms: Math.round(ttftP50 * (1.6 + rand() * 0.4)),
ttft_p99_ms: Math.round(ttftP50 * (2.4 + rand() * 0.6)),
throughput_tps: throughput === 0 ? 0 : Math.round(throughput * 10) / 10,
uptime_30d_pct: Math.round(uptimePct * 100) / 100,
request_volume_24h: requestVolume,
}
})
}
/**
* Build a 24-hour latency series for each group. Returns one point per hour
* (24 buckets), oldest first.
*/
export function buildLatencyTimeSeries(
model: PricingModel
): LatencyTimePoint[] {
const performances = buildGroupPerformance(model)
if (performances.length === 0) return []
const now = new Date()
now.setMinutes(0, 0, 0)
const baseSeed = hashStringToSeed(`${model.model_name}:lat`)
const points: LatencyTimePoint[] = []
for (const perf of performances) {
const rand = seededRandom(baseSeed ^ hashStringToSeed(perf.group))
for (let i = 23; i >= 0; i--) {
const ts = new Date(now.getTime() - i * 3_600_000)
const noise = 0.7 + rand() * 0.7
const trend = 0.85 + Math.sin(i / 3) * 0.1
const value = Math.max(50, Math.round(perf.ttft_p50_ms * noise * trend))
points.push({
timestamp: ts.toISOString(),
group: perf.group,
ttft_ms: value,
})
}
}
return points
}
/**
* Build a 30-day uptime series. Returns one point per day, oldest first.
*
* If `group` is provided the series is anchored on that group's mean uptime,
* otherwise it uses the per-model average. Either way the seed is derived
* deterministically so re-renders are stable.
*/
export function buildUptimeSeries(
model: PricingModel,
group?: string
): UptimeDayPoint[] {
const performances = buildGroupPerformance(model)
if (performances.length === 0) return []
const target = group ? performances.find((p) => p.group === group) : null
const baseUptime = target
? target.uptime_30d_pct
: performances.reduce((s, p) => s + p.uptime_30d_pct, 0) /
performances.length
const baseSeed = hashStringToSeed(`${model.model_name}:up:${group ?? '_all'}`)
const rand = seededRandom(baseSeed)
const today = new Date()
today.setHours(0, 0, 0, 0)
const points: UptimeDayPoint[] = []
for (let i = 29; i >= 0; i--) {
const date = new Date(today.getTime() - i * 86_400_000)
const isoDate = date.toISOString().slice(0, 10)
const incidentChance = rand()
const incidents = incidentChance > 0.92 ? 1 : 0
const outageMinutes = incidents > 0 ? Math.round(rand() * 30 + 5) : 0
const downtimePct = (outageMinutes / 1_440) * 100
const dayUptime = Math.max(85, Math.min(100, baseUptime - downtimePct))
points.push({
date: isoDate,
uptime_pct: Math.round(dayUptime * 100) / 100,
incidents,
outage_minutes: outageMinutes,
})
}
return points
}
/**
* Build a deterministic top-apps ranking for the model. The first three apps
* always come from the same template list; the rest is shuffled by the seed
* so different models surface different long tails.
*/
export function buildAppRankings(
model: PricingModel,
count = 12
): AppRanking[] {
const baseSeed = hashStringToSeed(`${model.model_name}:apps`)
const rand = seededRandom(baseSeed)
const candidates = [...APP_TEMPLATES]
// FisherYates shuffle.
for (let i = candidates.length - 1; i > 0; i--) {
const j = Math.floor(rand() * (i + 1))
;[candidates[i], candidates[j]] = [candidates[j], candidates[i]]
}
const top = candidates.slice(0, count)
const baseTokens = randomInRange(rand, 90_000_000, 320_000_000)
return top.map((app, idx) => {
const decay = Math.pow(0.78, idx)
const monthlyTokens = Math.round(baseTokens * decay * (0.85 + rand() * 0.3))
const growthPctRaw = randomInRange(rand, -28, 84)
const growthPct = Math.round(growthPctRaw * 10) / 10
return {
rank: idx + 1,
name: app.name,
description: app.description,
category: app.category,
url: app.url,
growth_pct: growthPct,
monthly_tokens: monthlyTokens,
initial: app.name.charAt(0).toUpperCase(),
}
})
}
/** Aggregate uptime over the most recent 30 days. */
export function aggregateUptime(points: UptimeDayPoint[]): {
uptime_pct: number
incidents: number
outage_minutes: number
} {
if (points.length === 0) {
return { uptime_pct: 0, incidents: 0, outage_minutes: 0 }
}
const incidents = points.reduce((s, p) => s + p.incidents, 0)
const outageMinutes = points.reduce((s, p) => s + p.outage_minutes, 0)
const totalMinutes = points.length * 1_440
const uptimePct = ((totalMinutes - outageMinutes) / totalMinutes) * 100
return {
incidents,
outage_minutes: outageMinutes,
uptime_pct: Math.round(uptimePct * 1000) / 1000,
}
}
/** Compact integer formatter for token counts in apps tab. */
export function formatTokenVolume(n: number): string {
if (!Number.isFinite(n) || n <= 0) return '0'
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return n.toString()
}
// ---------------------------------------------------------------------------
// Mock supported-parameters & rate-limits & misc API metadata
// ---------------------------------------------------------------------------
export type SupportedParameter = {
name: string
type:
| 'number'
| 'integer'
| 'boolean'
| 'string'
| 'object'
| 'array'
| 'enum'
defaultValue?: string | number | boolean
range?: string
enumValues?: string[]
descriptionKey: string
required?: boolean
}
const COMMON_CHAT_PARAMS: SupportedParameter[] = [
{
name: 'temperature',
type: 'number',
defaultValue: 1,
range: '0 ~ 2',
descriptionKey: 'Sampling temperature; lower is more deterministic',
},
{
name: 'top_p',
type: 'number',
defaultValue: 1,
range: '0 ~ 1',
descriptionKey: 'Nucleus sampling probability mass',
},
{
name: 'max_tokens',
type: 'integer',
range: '>= 1',
descriptionKey: 'Maximum number of tokens in the response',
},
{
name: 'frequency_penalty',
type: 'number',
defaultValue: 0,
range: '-2 ~ 2',
descriptionKey: 'Penalises repetition of frequent tokens',
},
{
name: 'presence_penalty',
type: 'number',
defaultValue: 0,
range: '-2 ~ 2',
descriptionKey: 'Encourages introducing new topics',
},
{
name: 'stop',
type: 'array',
descriptionKey: 'Up to 4 strings that stop generation',
},
{
name: 'seed',
type: 'integer',
descriptionKey: 'Deterministic sampling seed (best-effort)',
},
{
name: 'n',
type: 'integer',
defaultValue: 1,
range: '>= 1',
descriptionKey: 'Number of completions to generate',
},
{
name: 'stream',
type: 'boolean',
defaultValue: false,
descriptionKey: 'Stream tokens via Server-Sent Events',
},
{
name: 'response_format',
type: 'object',
descriptionKey: 'Force JSON object or schema-conforming output',
},
{
name: 'tools',
type: 'array',
descriptionKey: 'Tool / function declarations the model may call',
},
{
name: 'tool_choice',
type: 'string',
enumValues: ['auto', 'none', 'required'],
descriptionKey: 'Tool-choice policy or specific tool name',
},
{
name: 'logprobs',
type: 'boolean',
defaultValue: false,
descriptionKey: 'Return per-token log probabilities',
},
{
name: 'top_logprobs',
type: 'integer',
range: '0 ~ 20',
descriptionKey: 'Number of top log probabilities returned per token',
},
{
name: 'logit_bias',
type: 'object',
descriptionKey: 'Per-token logit bias map',
},
{
name: 'user',
type: 'string',
descriptionKey: 'End-user identifier for abuse monitoring',
},
]
const REASONING_PARAMS: SupportedParameter[] = [
{
name: 'reasoning_effort',
type: 'enum',
enumValues: ['low', 'medium', 'high'],
defaultValue: 'medium',
descriptionKey: 'Controls how much the model thinks before answering',
},
{
name: 'max_completion_tokens',
type: 'integer',
range: '>= 1',
descriptionKey: 'Maximum tokens including hidden reasoning tokens',
},
{
name: 'stop',
type: 'array',
descriptionKey: 'Up to 4 strings that stop generation',
},
{
name: 'seed',
type: 'integer',
descriptionKey: 'Deterministic sampling seed (best-effort)',
},
{
name: 'stream',
type: 'boolean',
defaultValue: false,
descriptionKey: 'Stream tokens via Server-Sent Events',
},
{
name: 'response_format',
type: 'object',
descriptionKey: 'Force JSON object or schema-conforming output',
},
{
name: 'tools',
type: 'array',
descriptionKey: 'Tool / function declarations the model may call',
},
{
name: 'tool_choice',
type: 'string',
enumValues: ['auto', 'none', 'required'],
descriptionKey: 'Tool-choice policy or specific tool name',
},
{
name: 'user',
type: 'string',
descriptionKey: 'End-user identifier for abuse monitoring',
},
]
const EMBEDDING_PARAMS: SupportedParameter[] = [
{
name: 'input',
type: 'string',
required: true,
descriptionKey: 'Text or array of texts to embed',
},
{
name: 'dimensions',
type: 'integer',
range: '>= 1',
descriptionKey: 'Truncate embeddings to this many dimensions',
},
{
name: 'encoding_format',
type: 'enum',
enumValues: ['float', 'base64'],
defaultValue: 'float',
descriptionKey: 'Wire encoding for the embedding vectors',
},
{
name: 'user',
type: 'string',
descriptionKey: 'End-user identifier for abuse monitoring',
},
]
const IMAGE_PARAMS: SupportedParameter[] = [
{
name: 'prompt',
type: 'string',
required: true,
descriptionKey: 'Text description of the desired image',
},
{
name: 'size',
type: 'enum',
enumValues: ['256x256', '512x512', '1024x1024', '1024x1792', '1792x1024'],
defaultValue: '1024x1024',
descriptionKey: 'Output image size',
},
{
name: 'quality',
type: 'enum',
enumValues: ['standard', 'hd'],
defaultValue: 'standard',
descriptionKey: 'Generation quality preset',
},
{
name: 'style',
type: 'enum',
enumValues: ['vivid', 'natural'],
defaultValue: 'vivid',
descriptionKey: 'Aesthetic style',
},
{
name: 'n',
type: 'integer',
defaultValue: 1,
range: '1 ~ 10',
descriptionKey: 'Number of images to generate',
},
{
name: 'response_format',
type: 'enum',
enumValues: ['url', 'b64_json'],
defaultValue: 'url',
descriptionKey: 'How to deliver the resulting image',
},
]
const VIDEO_PARAMS: SupportedParameter[] = [
{
name: 'prompt',
type: 'string',
required: true,
descriptionKey: 'Text description of the desired video',
},
{
name: 'duration',
type: 'integer',
range: '1 ~ 60',
descriptionKey: 'Video length in seconds',
},
{
name: 'aspect_ratio',
type: 'enum',
enumValues: ['16:9', '9:16', '1:1'],
defaultValue: '16:9',
descriptionKey: 'Output aspect ratio',
},
{
name: 'fps',
type: 'integer',
range: '8 ~ 60',
defaultValue: 24,
descriptionKey: 'Frames per second',
},
]
type ApiCategory = 'reasoning' | 'embedding' | 'image' | 'video' | 'chat'
/**
* Refine the broad PROFILE_BY_NAME bucket into an API-shape category. The
* `image` bucket from `PROFILE_BY_NAME` lumps still-image and video models
* together (because their performance profiles overlap); for the API tab we
* need to distinguish them so the request-parameter table is accurate.
*/
function apiCategoryOf(model: PricingModel): ApiCategory {
const profile = PROFILE_BY_NAME(model.model_name)
if (profile === 'embedding' || profile === 'reasoning') return profile
if (profile === 'image') {
return /sora|veo|kling|pika|video|wan-|hunyuanvideo/i.test(model.model_name)
? 'video'
: 'image'
}
return 'chat'
}
/**
* Build the list of request parameters that the model accepts. The list is
* shaped per-modality so reasoning, embedding, image, video and chat models
* each show their relevant parameter set.
*/
export function buildSupportedParameters(
model: PricingModel
): SupportedParameter[] {
const cat = apiCategoryOf(model)
if (cat === 'reasoning') return REASONING_PARAMS
if (cat === 'embedding') return EMBEDDING_PARAMS
if (cat === 'image') return IMAGE_PARAMS
if (cat === 'video') return VIDEO_PARAMS
return COMMON_CHAT_PARAMS
}
export type RateLimit = {
group: string
rpm: number
tpm: number
rpd: number
}
/** Build per-group RPM / TPM / RPD limits for the model. */
export function buildRateLimits(model: PricingModel): RateLimit[] {
const groups = (model.enable_groups ?? []).filter((g) => g && g !== 'auto')
const targets = groups.length > 0 ? groups : ['default']
const cat = apiCategoryOf(model)
const baseSeed = hashStringToSeed(`${model.model_name}:rl`)
const isHeavy = cat === 'image' || cat === 'video'
const isLight = cat === 'embedding'
const baseRpm = isHeavy ? 60 : isLight ? 5_000 : 500
const baseTpm = isHeavy ? 0 : isLight ? 1_000_000 : 200_000
const baseRpd = isHeavy ? 1_000 : isLight ? 100_000 : 10_000
return targets
.slice()
.sort((a, b) => a.localeCompare(b))
.map((group) => {
const rand = seededRandom(baseSeed ^ hashStringToSeed(group))
const tier = 0.6 + rand() * 1.4
return {
group,
rpm: Math.round((baseRpm * tier) / 10) * 10,
tpm: baseTpm === 0 ? 0 : Math.round((baseTpm * tier) / 1_000) * 1_000,
rpd: Math.round((baseRpd * tier) / 100) * 100,
}
})
}
/** Format an integer rate-limit value compactly. */
export function formatRateLimit(value: number): string {
if (value <= 0) return '—'
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
if (value >= 1_000)
return `${(value / 1_000).toFixed(value >= 10_000 ? 0 : 1)}K`
return value.toLocaleString()
}
+109
View File
@@ -0,0 +1,109 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { EXCLUDED_GROUPS, FILTER_ALL, QUOTA_TYPE_VALUES } from '../constants'
import type { PricingModel } from '../types'
// ----------------------------------------------------------------------------
// Model Helper Utilities
// ----------------------------------------------------------------------------
/**
* Get available groups for a model
*/
export function getAvailableGroups(
model: PricingModel,
usableGroup: Record<string, { desc: string; ratio: number }>
): string[] {
const modelEnableGroups = Array.isArray(model.enable_groups)
? model.enable_groups
: []
return Object.keys(usableGroup)
.filter((g) => !EXCLUDED_GROUPS.includes(g))
.filter((g) => modelEnableGroups.includes(g))
}
/**
* Read a configured group ratio while preserving valid zero ratios.
*/
export function getConfiguredGroupRatio(
groupRatio: Record<string, number>,
group: string
): number {
const ratio = groupRatio[group]
return typeof ratio === 'number' && Number.isFinite(ratio) ? ratio : 1
}
/**
* Resolve the group ratio used by model square summary prices.
*
* When no specific group is selected, the model square shows the best price
* available to the viewer. When a group filter is active, it shows that
* group's price instead.
*/
export function getDisplayGroupRatio(
model: PricingModel,
selectedGroup?: string
): number {
const modelEnableGroups = Array.isArray(model.enable_groups)
? model.enable_groups
: []
const groupRatio = model.group_ratio || {}
if (
selectedGroup &&
selectedGroup !== FILTER_ALL &&
modelEnableGroups.includes(selectedGroup)
) {
return getConfiguredGroupRatio(groupRatio, selectedGroup)
}
if (modelEnableGroups.length === 0) {
return 1
}
let minRatio = Number.POSITIVE_INFINITY
for (const group of modelEnableGroups) {
const ratio = groupRatio[group]
if (
typeof ratio === 'number' &&
Number.isFinite(ratio) &&
ratio < minRatio
) {
minRatio = ratio
}
}
return minRatio === Number.POSITIVE_INFINITY ? 1 : minRatio
}
/**
* Replace model placeholder in endpoint path
*/
export function replaceModelInPath(path: string, modelName: string): string {
return path.replaceAll('{model}', modelName)
}
/**
* Check if model is token-based pricing
*/
export function isTokenBasedModel(model: PricingModel): boolean {
return model.quota_type === QUOTA_TYPE_VALUES.TOKEN
}
+272
View File
@@ -0,0 +1,272 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { formatCurrencyFromUSD } from '@/lib/currency'
import { QUOTA_TYPE_VALUES, TOKEN_UNIT_DIVISORS } from '../constants'
import type { PricingModel, TokenUnit, PriceType } from '../types'
import { getConfiguredGroupRatio, getDisplayGroupRatio } from './model-helpers'
// ----------------------------------------------------------------------------
// Price Calculation Utilities
// ----------------------------------------------------------------------------
/**
* Strip trailing zeros from formatted price string while preserving currency symbols
*/
export function stripTrailingZeros(formatted: string): string {
// Match currency symbol at start, number, and potential 'k' suffix
const match = formatted.match(/^([^\d-]*)([-\d,]+\.?\d*)(k?)$/)
if (!match) return formatted
const [, symbol, number, suffix] = match
// Remove commas for processing
const cleanNumber = number.replaceAll(',', '')
// Convert to number and back to remove trailing zeros
const parsed = Number.parseFloat(cleanNumber)
if (Number.isNaN(parsed)) return formatted
// Convert to string, which automatically removes trailing zeros
let result = parsed.toString()
// If the result is in scientific notation, format it properly
if (result.includes('e')) {
result = parsed.toFixed(20).replace(/\.?0+$/, '')
}
return `${symbol}${result}${suffix}`
}
/**
* Calculate token price in USD.
*
* Returns NaN when the required ratio field is missing/null so callers can
* skip rendering that price type.
*/
function calculateTokenPrice(
model: PricingModel,
type: PriceType,
ratio: number
): number {
const base = model.model_ratio * 2 * ratio
switch (type) {
case 'input':
return base
case 'output':
return base * model.completion_ratio
case 'cache':
return hasRatio(model.cache_ratio)
? base * Number(model.cache_ratio)
: Number.NaN
case 'create_cache':
return hasRatio(model.create_cache_ratio)
? base * Number(model.create_cache_ratio)
: Number.NaN
case 'image':
return hasRatio(model.image_ratio)
? base * Number(model.image_ratio)
: Number.NaN
case 'audio_input':
return hasRatio(model.audio_ratio)
? base * Number(model.audio_ratio)
: Number.NaN
case 'audio_output':
return hasRatio(model.audio_ratio) &&
hasRatio(model.audio_completion_ratio)
? base *
Number(model.audio_ratio) *
Number(model.audio_completion_ratio)
: Number.NaN
}
}
function hasRatio(value: number | null | undefined): boolean {
return value !== undefined && value !== null && Number.isFinite(Number(value))
}
/**
* Apply recharge rate to price
*
* priceRate represents how much users need to recharge (in the display currency)
* to get 1 USD credit. usdExchangeRate is the real exchange rate.
*
* The returned value will be formatted by formatCurrencyFromUSD, which will
* multiply by the display currency's exchange rate.
*
* Examples:
*
* 1. Display currency = USD:
* - Model: 1 USD
* - priceRate = 0.5 (recharge $0.5 to get $1 credit)
* - usdExchangeRate = 1
* - Return: 1 × 0.5 / 1 = 0.5
* - formatCurrencyFromUSD(0.5) → $0.5 ✓
*
* 2. Display currency = CNY:
* - Model: 1 USD
* - priceRate = 4 (recharge ¥4 to get $1 credit)
* - usdExchangeRate = 7 (real rate: 1 USD = ¥7)
* - Return: 1 × 4 / 7 = 0.571
* - formatCurrencyFromUSD(0.571) → 0.571 × 7 = ¥4 ✓
* - Normal price: ¥7, Recharge price: ¥4 (cheaper!)
*/
function applyRechargeRate(
price: number,
showWithRecharge: boolean,
priceRate: number,
usdExchangeRate: number
): number {
if (!showWithRecharge) return price
return (price * priceRate) / usdExchangeRate
}
/**
* Format token-based price for display
*/
export function formatPrice(
model: PricingModel,
type: PriceType,
tokenUnit: TokenUnit,
showWithRecharge = false,
priceRate = 1,
usdExchangeRate = 1,
selectedGroup?: string
): string {
if (model.quota_type === QUOTA_TYPE_VALUES.REQUEST) {
return '-'
}
const displayGroupRatio = getDisplayGroupRatio(model, selectedGroup)
let priceInUSD = calculateTokenPrice(model, type, displayGroupRatio)
priceInUSD = applyRechargeRate(
priceInUSD,
showWithRecharge,
priceRate,
usdExchangeRate
)
const price = priceInUSD / TOKEN_UNIT_DIVISORS[tokenUnit]
return formatCurrencyFromUSD(price, {
digitsLarge: 4,
digitsSmall: 6,
abbreviate: false,
})
}
/**
* Format price for a specific group (token-based)
*/
export function formatGroupPrice(
model: PricingModel,
group: string,
type: PriceType,
tokenUnit: TokenUnit,
showWithRecharge = false,
priceRate = 1,
usdExchangeRate = 1,
groupRatio: Record<string, number>
): string {
if (model.quota_type === QUOTA_TYPE_VALUES.REQUEST) {
return '-'
}
const ratio = getConfiguredGroupRatio(groupRatio, group)
let priceInUSD = calculateTokenPrice(model, type, ratio)
priceInUSD = applyRechargeRate(
priceInUSD,
showWithRecharge,
priceRate,
usdExchangeRate
)
const price = priceInUSD / TOKEN_UNIT_DIVISORS[tokenUnit]
return formatCurrencyFromUSD(price, {
digitsLarge: 4,
digitsSmall: 6,
abbreviate: false,
})
}
/**
* Format fixed price for pay-per-request models (with specific group)
*/
export function formatFixedPrice(
model: PricingModel,
group: string,
showWithRecharge = false,
priceRate = 1,
usdExchangeRate = 1,
groupRatio: Record<string, number>
): string {
if (model.quota_type !== QUOTA_TYPE_VALUES.REQUEST) {
return '-'
}
const ratio = getConfiguredGroupRatio(groupRatio, group)
let priceInUSD = (model.model_price || 0) * ratio
priceInUSD = applyRechargeRate(
priceInUSD,
showWithRecharge,
priceRate,
usdExchangeRate
)
return formatCurrencyFromUSD(priceInUSD, {
digitsLarge: 4,
digitsSmall: 4,
abbreviate: false,
})
}
/**
* Format fixed price for pay-per-request models (minimum price from all groups)
*/
export function formatRequestPrice(
model: PricingModel,
showWithRecharge = false,
priceRate = 1,
usdExchangeRate = 1,
selectedGroup?: string
): string {
if (model.quota_type !== QUOTA_TYPE_VALUES.REQUEST) {
return '-'
}
const displayGroupRatio = getDisplayGroupRatio(model, selectedGroup)
let priceInUSD = (model.model_price || 0) * displayGroupRatio
priceInUSD = applyRechargeRate(
priceInUSD,
showWithRecharge,
priceRate,
usdExchangeRate
)
return formatCurrencyFromUSD(priceInUSD, {
digitsLarge: 4,
digitsSmall: 4,
abbreviate: false,
})
}
+63
View File
@@ -0,0 +1,63 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
// ----------------------------------------------------------------------------
// Deterministic seeding helpers
// ----------------------------------------------------------------------------
//
// These utilities are used to generate stable, repeatable mock metrics for
// model details (latency, throughput, uptime, app rankings) until the
// backend ships real values. Seeding the PRNG from the model name (and
// optionally the group name) ensures the same model always renders the same
// numbers, instead of jittering on every render.
/** djb2-inspired string hash → non-negative 31-bit integer. */
export function hashStringToSeed(input: string): number {
let hash = 5381
for (let i = 0; i < input.length; i++) {
hash = (hash * 33) ^ input.charCodeAt(i)
}
return Math.abs(hash | 0)
}
/** Linear-congruential generator producing pseudo-random numbers in [0, 1). */
export function seededRandom(seed: number): () => number {
let state = (seed || 1) >>> 0
return () => {
state = (state * 1664525 + 1013904223) >>> 0
return state / 0x1_0000_0000
}
}
/** Pick a number in [min, max] from a seeded PRNG. */
export function randomInRange(
rand: () => number,
min: number,
max: number
): number {
return min + rand() * (max - min)
}
/** Pick an integer in [min, max] (inclusive) from a seeded PRNG. */
export function randomIntInRange(
rand: () => number,
min: number,
max: number
): number {
return Math.floor(randomInRange(rand, min, max + 1))
}
+323
View File
@@ -0,0 +1,323 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { BILLING_CACHE_VAR_MAP } from './billing-expr'
export const CACHE_MODE_TIMED = 'timed'
export const CACHE_MODE_GENERIC = 'generic'
export type CacheMode = typeof CACHE_MODE_TIMED | typeof CACHE_MODE_GENERIC
export type TierConditionInput = {
var: 'p' | 'c' | 'len'
op: '<' | '<=' | '>' | '>='
value: number | string
}
export type VisualTier = {
label: string
conditions: TierConditionInput[]
input_unit_cost: number
output_unit_cost: number
cache_mode: CacheMode
cache_read_unit_cost?: number
cache_create_unit_cost?: number
cache_create_1h_unit_cost?: number
image_unit_cost?: number
image_output_unit_cost?: number
audio_input_unit_cost?: number
audio_output_unit_cost?: number
[field: string]: unknown
}
export type VisualConfig = {
tiers: VisualTier[]
}
export function getTierCacheMode(
tier: Partial<VisualTier> | null | undefined
): CacheMode {
if (tier?.cache_mode === CACHE_MODE_TIMED) return CACHE_MODE_TIMED
if (tier?.cache_mode === CACHE_MODE_GENERIC) return CACHE_MODE_GENERIC
return Number(tier?.cache_create_1h_unit_cost) > 0
? CACHE_MODE_TIMED
: CACHE_MODE_GENERIC
}
export function normalizeVisualTier(
tier: Partial<VisualTier> = {}
): VisualTier {
return {
label: tier.label ?? '',
input_unit_cost: Number(tier.input_unit_cost) || 0,
output_unit_cost: Number(tier.output_unit_cost) || 0,
cache_mode: getTierCacheMode(tier),
conditions: Array.isArray(tier.conditions) ? tier.conditions : [],
...tier,
cache_read_unit_cost: Number(tier.cache_read_unit_cost) || 0,
cache_create_unit_cost: Number(tier.cache_create_unit_cost) || 0,
cache_create_1h_unit_cost: Number(tier.cache_create_1h_unit_cost) || 0,
image_unit_cost: Number(tier.image_unit_cost) || 0,
image_output_unit_cost: Number(tier.image_output_unit_cost) || 0,
audio_input_unit_cost: Number(tier.audio_input_unit_cost) || 0,
audio_output_unit_cost: Number(tier.audio_output_unit_cost) || 0,
}
}
export function createDefaultVisualConfig(): VisualConfig {
return {
tiers: [
normalizeVisualTier({
conditions: [],
input_unit_cost: 0,
output_unit_cost: 0,
label: 'base',
cache_mode: CACHE_MODE_GENERIC,
}),
],
}
}
export function normalizeVisualConfig(
config: VisualConfig | null | undefined
): VisualConfig {
if (!config || !Array.isArray(config.tiers) || config.tiers.length === 0) {
return createDefaultVisualConfig()
}
return {
...config,
tiers: config.tiers.map((tier) => normalizeVisualTier(tier)),
}
}
function buildConditionStr(conditions: TierConditionInput[]): string {
if (!conditions || conditions.length === 0) return ''
return conditions
.filter((c) => c.var && c.op && c.value != null && c.value !== '')
.map((c) => `${c.var} ${c.op} ${c.value}`)
.join(' && ')
}
function buildTierBodyExpr(tier: VisualTier): string {
const parts: string[] = []
const ic = Number(tier.input_unit_cost) || 0
const oc = Number(tier.output_unit_cost) || 0
parts.push(`p * ${ic}`)
parts.push(`c * ${oc}`)
for (const cv of BILLING_CACHE_VAR_MAP) {
const v = Number((tier as Record<string, unknown>)[cv.field]) || 0
if (v !== 0) parts.push(`${cv.exprVar} * ${v}`)
}
return parts.join(' + ')
}
export function generateExprFromVisualConfig(
config: VisualConfig | null | undefined
): string {
if (!config || !config.tiers || config.tiers.length === 0) {
return 'p * 0 + c * 0'
}
const tiers = config.tiers
if (tiers.length === 1) {
const tier = tiers[0]
const label = tier.label || 'default'
const body = `tier("${label}", ${buildTierBodyExpr(tier)})`
const cond = buildConditionStr(tier.conditions)
if (cond) {
return `${cond} ? ${body} : p * 0 + c * 0`
}
return body
}
const parts: string[] = []
for (let i = 0; i < tiers.length; i++) {
const tier = tiers[i]
const label = tier.label || `tier_${i + 1}`
const body = `tier("${label}", ${buildTierBodyExpr(tier)})`
const cond = buildConditionStr(tier.conditions)
if (i < tiers.length - 1 && cond) {
parts.push(`${cond} ? ${body}`)
} else {
parts.push(body)
}
}
return parts.join(' : ')
}
export function tryParseVisualConfig(
exprStr: string | null | undefined
): VisualConfig | null {
if (!exprStr) return null
try {
let body = exprStr
const versionMatch = body.match(/^v\d+:([\s\S]*)$/)
if (versionMatch) body = versionMatch[1]
const cacheVarNames = BILLING_CACHE_VAR_MAP.map((cv) => cv.exprVar)
const optCacheStr = cacheVarNames
.map((v) => `(?:\\s*\\+\\s*${v}\\s*\\*\\s*([\\d.eE+-]+))?`)
.join('')
const bodyPat = `p\\s*\\*\\s*([\\d.eE+-]+)\\s*\\+\\s*c\\s*\\*\\s*([\\d.eE+-]+)${optCacheStr}`
const singleRe = new RegExp(`^tier\\("([^"]*)",\\s*${bodyPat}\\)$`)
const simple = body.match(singleRe)
if (simple) {
const tier: Record<string, unknown> = {
conditions: [],
input_unit_cost: Number(simple[2]),
output_unit_cost: Number(simple[3]),
label: simple[1],
}
BILLING_CACHE_VAR_MAP.forEach((cv, i) => {
const val = simple[4 + i]
if (val != null) tier[cv.field] = Number(val)
})
return normalizeVisualConfig({
tiers: [normalizeVisualTier(tier as Partial<VisualTier>)],
})
}
const condGroup =
`((?:(?:p|c|len)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)` +
`(?:\\s*&&\\s*(?:p|c|len)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)*)`
const tierRe = new RegExp(
`(?:${condGroup}\\s*\\?\\s*)?tier\\("([^"]*)",\\s*${bodyPat}\\)`,
'g'
)
const tiers: VisualTier[] = []
let match: RegExpExecArray | null
while ((match = tierRe.exec(body)) !== null) {
const condStr = match[1] || ''
const conditions: TierConditionInput[] = []
if (condStr) {
for (const cp of condStr.split(/\s*&&\s*/)) {
const cm = cp.trim().match(/^(p|c|len)\s*(<|<=|>|>=)\s*([\d.eE+]+)$/)
if (cm) {
conditions.push({
var: cm[1] as TierConditionInput['var'],
op: cm[2] as TierConditionInput['op'],
value: Number(cm[3]),
})
}
}
}
const tier: Record<string, unknown> = {
conditions,
input_unit_cost: Number(match[3]),
output_unit_cost: Number(match[4]),
label: match[2],
}
const m = match
BILLING_CACHE_VAR_MAP.forEach((cv, i) => {
const val = m[5 + i]
if (val != null) tier[cv.field] = Number(val)
})
tiers.push(normalizeVisualTier(tier as Partial<VisualTier>))
}
if (tiers.length === 0) return null
const cfg = normalizeVisualConfig({ tiers })
const regenerated = generateExprFromVisualConfig(cfg)
if (regenerated.replace(/\s+/g, '') !== body.replace(/\s+/g, '')) {
return null
}
return cfg
} catch {
return null
}
}
// ---------------------------------------------------------------------------
// Local cost evaluator (for the estimator preview)
// ---------------------------------------------------------------------------
const ESTIMATOR_VARS = [
{ var: 'cr', stateKey: 'cacheReadTokens' },
{ var: 'cc', stateKey: 'cacheCreateTokens' },
{ var: 'cc1h', stateKey: 'cacheCreate1hTokens' },
{ var: 'img', stateKey: 'imageTokens' },
{ var: 'img_o', stateKey: 'imageOutputTokens' },
{ var: 'ai', stateKey: 'audioInputTokens' },
{ var: 'ao', stateKey: 'audioOutputTokens' },
] as const
export type ExtraTokenValues = Record<
(typeof ESTIMATOR_VARS)[number]['stateKey'],
number
>
export type EvalResult = {
cost: number
matchedTier: string
error: string | null
}
export function evalExprLocally(
exprStr: string,
promptTokens: number,
completionTokens: number,
extraTokenValues: ExtraTokenValues
): EvalResult {
try {
if (!exprStr || !exprStr.trim()) {
return { cost: 0, matchedTier: '', error: null }
}
let matchedTier = ''
const tierFn = (name: string, value: number) => {
matchedTier = name
return value
}
const cacheReadTokens = extraTokenValues.cacheReadTokens || 0
const cacheCreateTokens = extraTokenValues.cacheCreateTokens || 0
const cacheCreate1hTokens = extraTokenValues.cacheCreate1hTokens || 0
const len =
promptTokens + cacheReadTokens + cacheCreateTokens + cacheCreate1hTokens
const env: Record<string, unknown> = {
p: promptTokens,
c: completionTokens,
len,
tier: tierFn,
max: Math.max,
min: Math.min,
abs: Math.abs,
ceil: Math.ceil,
floor: Math.floor,
}
for (const field of ESTIMATOR_VARS) {
env[field.var] = extraTokenValues[field.stateKey] || 0
}
const fn = new Function(
...Object.keys(env),
`"use strict"; return (${exprStr});`
)
const cost = Number(fn(...Object.values(env))) || 0
return { cost, matchedTier, error: null }
} catch (e) {
const message = e instanceof Error ? e.message : String(e)
return { cost: 0, matchedTier: '', error: message }
}
}
export function exprUsesExtraVars(exprStr: string): boolean {
if (!exprStr) return false
const varNames = ESTIMATOR_VARS.map((f) => f.var).join('|')
return new RegExp(`\\b(${varNames})\\b`).test(exprStr)
}
export const ESTIMATOR_EXTRA_FIELDS = ESTIMATOR_VARS
+111
View File
@@ -0,0 +1,111 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
// ----------------------------------------------------------------------------
// Pricing Types
// ----------------------------------------------------------------------------
export type PricingVendor = {
id: number
name: string
icon?: string
description?: string
}
export type PricingModel = {
id: number
model_name: string
description?: string
icon?: string
vendor_id?: number
vendor_name?: string
vendor_icon?: string
vendor_description?: string
quota_type: number
model_ratio: number
completion_ratio: number
model_price?: number
cache_ratio?: number | null
create_cache_ratio?: number | null
image_ratio?: number | null
audio_ratio?: number | null
audio_completion_ratio?: number | null
enable_groups: string[]
tags?: string
supported_endpoint_types?: string[]
key?: string
group_ratio?: Record<string, number>
/** Billing mode (e.g. "tiered_expr") used to flag dynamic pricing */
billing_mode?: string
/** Raw expression describing dynamic / tiered billing */
billing_expr?: string
/** Pricing version returned by backend, useful for cache busting */
pricing_version?: string
/**
* Optional model metadata fields reserved for backend-provided catalog data.
* Keep them data-driven; do not synthesize display values on the client.
*/
context_length?: number
max_output_tokens?: number
knowledge_cutoff?: string
release_date?: string
parameter_count?: string
input_modalities?: Modality[]
output_modalities?: Modality[]
capabilities?: ModelCapability[]
}
/** Input/output modalities supported by a model. */
export type Modality = 'text' | 'image' | 'audio' | 'video' | 'file'
/** Functional capabilities a model exposes. */
export type ModelCapability =
| 'function_calling'
| 'streaming'
| 'vision'
| 'json_mode'
| 'structured_output'
| 'reasoning'
| 'tools'
| 'system_prompt'
| 'web_search'
| 'code_interpreter'
| 'caching'
| 'embeddings'
export type PricingData = {
success: boolean
message?: string
data: PricingModel[]
vendors: PricingVendor[]
group_ratio: Record<string, number>
usable_group: Record<string, { desc: string; ratio: number }>
supported_endpoint: Record<string, string>
auto_groups: string[]
}
export type TokenUnit = 'M' | 'K'
export type PriceType =
| 'input'
| 'output'
| 'cache'
| 'create_cache'
| 'image'
| 'audio_input'
| 'audio_output'
export type QuotaType = 0 | 1 // 0: token-based, 1: per-request