Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
a95655a2
authored
Jun 17, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(performance): implement success rate grading and enhance UI representation
parent
3cc2b1be
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
447 additions
and
1340 deletions
+447
-1340
web/default/src/features/dashboard/components/models/performance-overview.tsx
+5
-17
web/default/src/features/dashboard/components/overview/performance-health-panel.tsx
+5
-17
web/default/src/features/performance-metrics/lib/format.ts
+64
-0
web/default/src/features/pricing/components/model-details-api.tsx
+0
-104
web/default/src/features/pricing/components/model-details-capabilities.tsx
+0
-181
web/default/src/features/pricing/components/model-details-charts.tsx
+2
-5
web/default/src/features/pricing/components/model-details-modalities.tsx
+0
-160
web/default/src/features/pricing/components/model-details-performance.tsx
+4
-11
web/default/src/features/pricing/components/model-details-quick-stats.tsx
+0
-147
web/default/src/features/pricing/components/model-details-uptime-sparkline.tsx
+7
-18
web/default/src/features/pricing/components/model-details.tsx
+341
-98
web/default/src/features/pricing/components/model-perf-badge.tsx
+2
-6
web/default/src/features/pricing/lib/index.ts
+0
-1
web/default/src/features/pricing/lib/model-metadata.ts
+0
-569
web/default/src/features/pricing/types.ts
+2
-4
web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx
+15
-2
No files found.
web/default/src/features/dashboard/components/models/performance-overview.tsx
View file @
a95655a2
...
...
@@ -27,6 +27,8 @@ import {
formatLatency
,
formatThroughput
,
formatUptimePct
,
getSuccessRateDotClass
,
getSuccessRateTextClass
,
}
from
'@/features/performance-metrics/lib/format'
import
type
{
PerfModelSummary
}
from
'@/features/performance-metrics/types'
...
...
@@ -79,20 +81,6 @@ function buildPerformanceSummary(rows: PerfModelSummary[]): PerformanceSummary {
}
}
function
successRateClassName
(
successRate
:
number
):
string
{
if
(
!
Number
.
isFinite
(
successRate
))
return
'text-muted-foreground'
if
(
successRate
>=
99.9
)
return
'text-success'
if
(
successRate
>=
99
)
return
'text-warning'
return
'text-destructive'
}
function
successDotClassName
(
successRate
:
number
):
string
{
if
(
!
Number
.
isFinite
(
successRate
))
return
'bg-muted-foreground'
if
(
successRate
>=
99.9
)
return
'bg-success'
if
(
successRate
>=
99
)
return
'bg-warning'
return
'bg-destructive'
}
export
function
PerformanceOverview
()
{
const
{
t
}
=
useTranslation
()
const
metricsQuery
=
useQuery
({
...
...
@@ -152,7 +140,7 @@ export function PerformanceOverview() {
icon=
{
HeartPulse
}
label=
{
t
(
'Success rate'
)
}
value=
{
formatUptimePct
(
summary
.
successRate
)
}
valueClassName=
{
successRateClassName
(
summary
.
successRate
)
}
valueClassName=
{
getSuccessRateTextClass
(
summary
.
successRate
)
}
/>
<
InlineMetric
icon=
{
Timer
}
...
...
@@ -221,14 +209,14 @@ function ModelBadge(props: { model: PerfModelSummary }) {
<
span
className=
{
cn
(
'size-1.5 rounded-full'
,
successDotClassName
(
model
.
success_rate
)
getSuccessRateDotClass
(
model
.
success_rate
)
)
}
aria
-
hidden=
'true'
/>
<
span
className=
{
cn
(
'font-mono text-[11px] font-semibold tabular-nums'
,
successRateClassName
(
model
.
success_rate
)
getSuccessRateTextClass
(
model
.
success_rate
)
)
}
>
{
formatUptimePct
(
model
.
success_rate
)
}
...
...
web/default/src/features/dashboard/components/overview/performance-health-panel.tsx
View file @
a95655a2
...
...
@@ -27,6 +27,8 @@ import {
formatLatency
,
formatThroughput
,
formatUptimePct
,
getSuccessRateDotClass
,
getSuccessRateTextClass
,
}
from
'@/features/performance-metrics/lib/format'
import
type
{
PerfModelSummary
}
from
'@/features/performance-metrics/types'
...
...
@@ -51,20 +53,6 @@ function simpleAverage(
return
count
>
0
?
total
/
count
:
NaN
}
function
rateTextClass
(
rate
:
number
):
string
{
if
(
!
Number
.
isFinite
(
rate
))
return
'text-muted-foreground'
if
(
rate
>=
99.9
)
return
'text-success'
if
(
rate
>=
99
)
return
'text-warning'
return
'text-destructive'
}
function
rateDotClass
(
rate
:
number
):
string
{
if
(
!
Number
.
isFinite
(
rate
))
return
'bg-muted-foreground'
if
(
rate
>=
99.9
)
return
'bg-success'
if
(
rate
>=
99
)
return
'bg-warning'
return
'bg-destructive'
}
export
function
PerformanceHealthPanel
()
{
const
{
t
}
=
useTranslation
()
const
metricsQuery
=
useQuery
({
...
...
@@ -121,7 +109,7 @@ export function PerformanceHealthPanel() {
label=
{
t
(
'Success rate'
)
}
value=
{
formatUptimePct
(
summary
.
successRate
)
}
loading=
{
loading
}
valueClassName=
{
r
ateTextClass
(
summary
.
successRate
)
}
valueClassName=
{
getSuccessR
ateTextClass
(
summary
.
successRate
)
}
/>
<
MetricCell
icon=
{
Timer
}
...
...
@@ -162,14 +150,14 @@ export function PerformanceHealthPanel() {
<
span
className=
{
cn
(
'size-1.5 rounded-full'
,
r
ateDotClass
(
model
.
success_rate
)
getSuccessR
ateDotClass
(
model
.
success_rate
)
)
}
aria
-
hidden=
'true'
/>
<
span
className=
{
cn
(
'font-mono text-[11px] font-semibold tabular-nums'
,
r
ateTextClass
(
model
.
success_rate
)
getSuccessR
ateTextClass
(
model
.
success_rate
)
)
}
>
{
formatUptimePct
(
model
.
success_rate
)
}
...
...
web/default/src/features/performance-metrics/lib/format.ts
View file @
a95655a2
...
...
@@ -32,3 +32,67 @@ export function formatUptimePct(pct: number): string {
if
(
!
Number
.
isFinite
(
pct
))
return
'—'
return
`
${
pct
.
toFixed
(
2
)}
%`
}
export
type
SuccessRateLevel
=
|
'excellent'
|
'good'
|
'warning'
|
'critical'
|
'unknown'
const
SUCCESS_RATE_EXCELLENT_MIN
=
100
const
SUCCESS_RATE_GOOD_MIN
=
90
const
SUCCESS_RATE_WARNING_MIN
=
70
/**
* Single source of truth for grading a success rate (0-100).
* - excellent: 100% (full green)
* - good: >= 90% (slightly lighter green)
* - warning: >= 70%
* - critical: below 70%
* - unknown: non-finite values
*/
export
function
getSuccessRateLevel
(
rate
:
number
):
SuccessRateLevel
{
if
(
!
Number
.
isFinite
(
rate
))
return
'unknown'
if
(
rate
>=
SUCCESS_RATE_EXCELLENT_MIN
)
return
'excellent'
if
(
rate
>=
SUCCESS_RATE_GOOD_MIN
)
return
'good'
if
(
rate
>=
SUCCESS_RATE_WARNING_MIN
)
return
'warning'
return
'critical'
}
const
SUCCESS_RATE_TEXT_CLASS
:
Record
<
SuccessRateLevel
,
string
>
=
{
excellent
:
'text-success'
,
good
:
'text-success/70'
,
warning
:
'text-warning'
,
critical
:
'text-destructive'
,
unknown
:
'text-muted-foreground'
,
}
const
SUCCESS_RATE_DOT_CLASS
:
Record
<
SuccessRateLevel
,
string
>
=
{
excellent
:
'bg-success'
,
good
:
'bg-success/60'
,
warning
:
'bg-warning'
,
critical
:
'bg-destructive'
,
unknown
:
'bg-muted-foreground'
,
}
// Hex colors for non-CSS contexts (e.g. chart libraries that need raw values).
const
SUCCESS_RATE_HEX_COLOR
:
Record
<
SuccessRateLevel
,
string
>
=
{
excellent
:
'#10b981'
,
// emerald-500 (full green)
good
:
'#34d399'
,
// emerald-400 (slightly lighter green)
warning
:
'#f59e0b'
,
// amber-500
critical
:
'#ef4444'
,
// red-500
unknown
:
'#9ca3af'
,
// gray-400
}
export
function
getSuccessRateTextClass
(
rate
:
number
):
string
{
return
SUCCESS_RATE_TEXT_CLASS
[
getSuccessRateLevel
(
rate
)]
}
export
function
getSuccessRateDotClass
(
rate
:
number
):
string
{
return
SUCCESS_RATE_DOT_CLASS
[
getSuccessRateLevel
(
rate
)]
}
export
function
getSuccessRateColor
(
rate
:
number
):
string
{
return
SUCCESS_RATE_HEX_COLOR
[
getSuccessRateLevel
(
rate
)]
}
web/default/src/features/pricing/components/model-details-api.tsx
View file @
a95655a2
...
...
@@ -19,17 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
import
{
useMemo
,
useState
}
from
'react'
import
{
ChevronRight
,
ExternalLink
,
Gauge
,
KeyRound
,
ScrollText
,
ShieldCheck
,
Sigma
,
Zap
,
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
type
{
BundledLanguage
}
from
'shiki/bundle/web'
import
{
cn
}
from
'@/lib/utils'
import
{
useStatus
}
from
'@/hooks/use-status'
import
{
Badge
}
from
'@/components/ui/badge'
import
{
Tabs
,
TabsList
,
TabsTrigger
}
from
'@/components/ui/tabs'
...
...
@@ -48,7 +45,6 @@ import {
type
SupportedParameter
,
}
from
'../lib/mock-stats'
import
{
replaceModelInPath
}
from
'../lib/model-helpers'
import
{
inferApiInfo
}
from
'../lib/model-metadata'
import
type
{
PricingModel
}
from
'../types'
// ---------------------------------------------------------------------------
...
...
@@ -723,106 +719,6 @@ function RateLimitsSection(props: { model: PricingModel }) {
}
// ---------------------------------------------------------------------------
// Provider info card (vendor / tokenizer / license / privacy)
// ---------------------------------------------------------------------------
//
// Exported separately so the Overview tab can render it alongside capabilities
// and modalities (i.e. "what is this model?" rather than "how do I call it?").
export
function
ModelDetailsProviderInfo
(
props
:
{
model
:
PricingModel
})
{
const
{
t
}
=
useTranslation
()
const
info
=
useMemo
(()
=>
inferApiInfo
(
props
.
model
),
[
props
.
model
])
return
(
<
section
>
<
SectionTitle
icon=
{
ShieldCheck
}
>
{
t
(
'Provider & data privacy'
)
}
</
SectionTitle
>
<
div
className=
'border-border/60 bg-border/60 grid grid-cols-1 gap-px overflow-hidden rounded-lg border sm:grid-cols-2'
>
<
InfoCell
label=
{
t
(
'Provider'
)
}
>
<
div
className=
'flex items-center gap-1.5'
>
<
span
className=
'text-sm font-medium'
>
{
info
.
vendor_label
}
</
span
>
{
info
.
homepage
&&
(
<
a
href=
{
info
.
homepage
}
target=
'_blank'
rel=
'noopener noreferrer'
className=
'text-muted-foreground hover:text-foreground inline-flex items-center gap-0.5 text-[11px]'
>
{
t
(
'Docs'
)
}
<
ExternalLink
className=
'size-3'
/>
</
a
>
)
}
</
div
>
</
InfoCell
>
<
InfoCell
label=
{
t
(
'Tokenizer'
)
}
>
<
div
className=
'flex flex-col gap-0.5'
>
<
code
className=
'font-mono text-xs'
>
{
info
.
tokenizer
}
</
code
>
{
info
.
tokenizer_note
&&
(
<
span
className=
'text-muted-foreground text-[10px]'
>
{
info
.
tokenizer_note
}
</
span
>
)
}
</
div
>
</
InfoCell
>
<
InfoCell
label=
{
t
(
'License'
)
}
>
<
div
className=
'flex flex-col gap-1'
>
<
span
className=
'text-sm'
>
{
info
.
license
}
</
span
>
<
Badge
variant=
'outline'
className=
{
cn
(
'h-4 w-fit px-1.5 text-[9px] font-medium'
,
info
.
license_kind
===
'open'
&&
'border-emerald-500/40 text-emerald-600 dark:text-emerald-400'
,
info
.
license_kind
===
'open-weight'
&&
'border-sky-500/40 text-sky-600 dark:text-sky-400'
,
info
.
license_kind
===
'proprietary'
&&
'border-amber-500/40 text-amber-600 dark:text-amber-400'
)
}
>
{
info
.
license_kind
===
'open'
?
t
(
'Open source'
)
:
info
.
license_kind
===
'open-weight'
?
t
(
'Open weights'
)
:
info
.
license_kind
===
'proprietary'
?
t
(
'Proprietary'
)
:
t
(
'Unknown'
)
}
</
Badge
>
</
div
>
</
InfoCell
>
<
InfoCell
label=
{
t
(
'Data retention'
)
}
>
<
span
className=
'text-sm'
>
{
info
.
data_retention_days
===
0
?
t
(
'Zero retention'
)
:
`${info.data_retention_days} ${t('days')}`
}
</
span
>
<
span
className=
'text-muted-foreground text-[10px]'
>
{
info
.
training_opt_out
?
t
(
'Not used for upstream training by default'
)
:
t
(
'May be used for training by upstream provider'
)
}
</
span
>
</
InfoCell
>
</
div
>
</
section
>
)
}
function
InfoCell
(
props
:
{
label
:
string
;
children
:
React
.
ReactNode
})
{
return
(
<
div
className=
'bg-card flex flex-col gap-1 px-3 py-2.5'
>
<
span
className=
'text-muted-foreground text-[10px] font-medium tracking-wider uppercase'
>
{
props
.
label
}
</
span
>
{
props
.
children
}
</
div
>
)
}
// ---------------------------------------------------------------------------
// Authentication preview
// ---------------------------------------------------------------------------
...
...
web/default/src/features/pricing/components/model-details-capabilities.tsx
deleted
100644 → 0
View file @
3cc2b1be
/*
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
{
BookOpenCheck
,
Braces
,
Code2
,
Database
,
FileCode
,
Globe
,
type
LucideIcon
,
PanelTopOpen
,
ScanEye
,
Settings2
,
Sparkles
,
Workflow
,
Zap
,
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
type
{
ModelCapability
}
from
'../types'
type
CapabilityMeta
=
{
icon
:
LucideIcon
labelKey
:
string
descriptionKey
:
string
}
const
CAPABILITY_META
:
Record
<
ModelCapability
,
CapabilityMeta
>
=
{
function_calling
:
{
icon
:
Workflow
,
labelKey
:
'Function calling'
,
descriptionKey
:
'Invoke developer-defined functions with structured arguments'
,
},
streaming
:
{
icon
:
Zap
,
labelKey
:
'Streaming'
,
descriptionKey
:
'Stream tokens incrementally as they are generated'
,
},
vision
:
{
icon
:
ScanEye
,
labelKey
:
'Vision'
,
descriptionKey
:
'Understand image inputs alongside text'
,
},
json_mode
:
{
icon
:
Braces
,
labelKey
:
'JSON mode'
,
descriptionKey
:
'Force a syntactically valid JSON response'
,
},
structured_output
:
{
icon
:
FileCode
,
labelKey
:
'Structured output'
,
descriptionKey
:
'Return data conforming to a JSON schema'
,
},
reasoning
:
{
icon
:
Sparkles
,
labelKey
:
'Reasoning'
,
descriptionKey
:
'Multi-step thinking before final answer'
,
},
tools
:
{
icon
:
Settings2
,
labelKey
:
'Tools'
,
descriptionKey
:
'Use external tools to extend capabilities'
,
},
system_prompt
:
{
icon
:
PanelTopOpen
,
labelKey
:
'System prompt'
,
descriptionKey
:
'Steer behaviour with a system instruction'
,
},
web_search
:
{
icon
:
Globe
,
labelKey
:
'Web search'
,
descriptionKey
:
'Search the public web at inference time'
,
},
code_interpreter
:
{
icon
:
Code2
,
labelKey
:
'Code interpreter'
,
descriptionKey
:
'Execute code in a sandbox during the response'
,
},
caching
:
{
icon
:
Database
,
labelKey
:
'Prompt caching'
,
descriptionKey
:
'Cache repeated prompt prefixes for cheaper, faster reuse'
,
},
embeddings
:
{
icon
:
BookOpenCheck
,
labelKey
:
'Embeddings'
,
descriptionKey
:
'Return vector embeddings for inputs'
,
},
}
/**
* Order capabilities for display. We put the most user-facing capabilities
* first, then the rest. Anything not listed sinks to the bottom in a stable
* order so the layout looks tidy across models.
*/
const
CAPABILITY_ORDER
:
ModelCapability
[]
=
[
'streaming'
,
'function_calling'
,
'tools'
,
'json_mode'
,
'structured_output'
,
'vision'
,
'reasoning'
,
'caching'
,
'system_prompt'
,
'web_search'
,
'code_interpreter'
,
'embeddings'
,
]
function
orderCapabilities
(
capabilities
:
ModelCapability
[]):
ModelCapability
[]
{
const
set
=
new
Set
(
capabilities
)
const
ordered
=
CAPABILITY_ORDER
.
filter
((
c
)
=>
set
.
has
(
c
))
for
(
const
c
of
capabilities
)
{
if
(
!
ordered
.
includes
(
c
))
ordered
.
push
(
c
)
}
return
ordered
}
export
function
ModelDetailsCapabilities
(
props
:
{
capabilities
:
ModelCapability
[]
})
{
const
{
t
}
=
useTranslation
()
const
ordered
=
orderCapabilities
(
props
.
capabilities
)
if
(
ordered
.
length
===
0
)
{
return
(
<
p
className=
'text-muted-foreground text-sm'
>
{
t
(
'No capabilities reported for this model.'
)
}
</
p
>
)
}
return
(
<
div
className=
'grid grid-cols-2 gap-2 @md/details:grid-cols-3 @2xl/details:grid-cols-4'
>
{
ordered
.
map
((
capability
)
=>
{
const
meta
=
CAPABILITY_META
[
capability
]
if
(
!
meta
)
return
null
const
Icon
=
meta
.
icon
return
(
<
div
key=
{
capability
}
className=
{
cn
(
'group flex items-start gap-2 rounded-lg border p-3 transition-colors'
,
'hover:bg-muted/30'
)
}
>
<
span
className=
'bg-muted text-foreground inline-flex size-7 shrink-0 items-center justify-center rounded-md transition-colors group-hover:bg-emerald-100 group-hover:text-emerald-700 dark:group-hover:bg-emerald-500/20 dark:group-hover:text-emerald-300'
>
<
Icon
className=
'size-3.5'
/>
</
span
>
<
div
className=
'min-w-0 flex-1'
>
<
div
className=
'text-foreground truncate text-xs font-semibold'
>
{
t
(
meta
.
labelKey
)
}
</
div
>
<
p
className=
'text-muted-foreground mt-0.5 line-clamp-2 text-[11px] leading-snug'
>
{
t
(
meta
.
descriptionKey
)
}
</
p
>
</
div
>
</
div
>
)
})
}
</
div
>
)
}
web/default/src/features/pricing/components/model-details-charts.tsx
View file @
a95655a2
...
...
@@ -24,6 +24,7 @@ import { useChartTheme } from '@/lib/use-chart-theme'
import
{
cn
}
from
'@/lib/utils'
import
{
VCHART_OPTION
}
from
'@/lib/vchart'
import
{
useThemeCustomization
}
from
'@/context/theme-customization-provider'
import
{
getSuccessRateColor
}
from
'@/features/performance-metrics/lib/format'
import
type
{
LatencyTimePoint
,
UptimeDayPoint
}
from
'../lib/mock-stats'
function
formatHourLabel
(
iso
:
string
):
string
{
...
...
@@ -229,11 +230,7 @@ export function UptimeTrendChart(props: {
size
:
5
,
stroke
:
'#ffffff'
,
lineWidth
:
1.5
,
fill
:
(
datum
:
{
uptime
:
number
})
=>
{
if
(
datum
.
uptime
>=
99.9
)
return
'#10b981'
if
(
datum
.
uptime
>=
99.0
)
return
'#f59e0b'
return
'#ef4444'
},
fill
:
(
datum
:
{
uptime
:
number
})
=>
getSuccessRateColor
(
datum
.
uptime
),
},
},
tooltip
:
{
...
...
web/default/src/features/pricing/components/model-details-modalities.tsx
deleted
100644 → 0
View file @
3cc2b1be
/*
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
{
FileText
,
Image
as
ImageIcon
,
Mic2
,
Type
as
TypeIcon
,
Video
,
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
{
Tooltip
,
TooltipContent
,
TooltipTrigger
,
}
from
'@/components/ui/tooltip'
import
{
StaticDataTable
}
from
'@/components/data-table'
import
type
{
Modality
}
from
'../types'
type
IconComponent
=
React
.
ComponentType
<
{
className
?:
string
}
>
const
MODALITY_META
:
Record
<
Modality
,
{
icon
:
IconComponent
;
labelKey
:
string
}
>
=
{
text
:
{
icon
:
TypeIcon
,
labelKey
:
'Text'
},
image
:
{
icon
:
ImageIcon
,
labelKey
:
'Image'
},
audio
:
{
icon
:
Mic2
,
labelKey
:
'Audio'
},
video
:
{
icon
:
Video
,
labelKey
:
'Video'
},
file
:
{
icon
:
FileText
,
labelKey
:
'File'
},
}
const
ALL_MODALITIES
:
Modality
[]
=
[
'text'
,
'image'
,
'audio'
,
'video'
,
'file'
]
/** Inline modality icons (used by the quick-stats flow). */
export
function
ModalityIcons
(
props
:
{
modalities
:
Modality
[]
className
?:
string
})
{
const
{
t
}
=
useTranslation
()
if
(
props
.
modalities
.
length
===
0
)
{
return
<
span
className=
'text-muted-foreground text-xs'
>
—
</
span
>
}
return
(
<
span
className=
'inline-flex items-center gap-1'
>
{
props
.
modalities
.
map
((
modality
)
=>
{
const
meta
=
MODALITY_META
[
modality
]
const
Icon
=
meta
.
icon
return
(
<
Tooltip
key=
{
modality
}
>
<
TooltipTrigger
render=
{
<
span
aria
-
label=
{
t
(
meta
.
labelKey
)
}
className=
'text-foreground/80 inline-flex'
/>
}
>
<
Icon
className=
{
cn
(
'size-3.5'
,
props
.
className
)
}
/>
</
TooltipTrigger
>
<
TooltipContent
side=
'top'
className=
'text-xs'
>
{
t
(
meta
.
labelKey
)
}
</
TooltipContent
>
</
Tooltip
>
)
})
}
</
span
>
)
}
/**
* 2 × N matrix showing which modalities are supported as input vs output.
* Cells with a checkmark indicate support; empty cells show a dash.
*/
export
function
ModalitiesMatrix
(
props
:
{
input
:
Modality
[]
output
:
Modality
[]
})
{
const
{
t
}
=
useTranslation
()
const
inputSet
=
new
Set
(
props
.
input
)
const
outputSet
=
new
Set
(
props
.
output
)
return
(
<
StaticDataTable
className=
'rounded-lg'
tableClassName=
'text-sm'
headerRowClassName=
'bg-muted/40'
data=
{
[
{
label
:
t
(
'Input'
),
set
:
inputSet
},
{
label
:
t
(
'Output'
),
set
:
outputSet
},
]
}
getRowKey=
{
(
row
)
=>
row
.
label
}
columns=
{
[
{
id
:
'modality'
,
header
:
t
(
'Modality'
),
className
:
'text-muted-foreground px-3 py-2 text-left text-[11px] font-medium tracking-wider uppercase'
,
cellClassName
:
'text-muted-foreground bg-muted/30 px-3 py-2 text-left text-[11px] font-medium tracking-wider uppercase'
,
cell
:
(
row
)
=>
row
.
label
,
},
...
ALL_MODALITIES
.
map
((
modality
)
=>
({
id
:
modality
,
header
:
t
(
MODALITY_META
[
modality
].
labelKey
),
className
:
'text-muted-foreground border-l px-3 py-2 text-center text-[11px] font-medium tracking-wider uppercase'
,
cellClassName
:
(
row
:
{
label
:
string
;
set
:
Set
<
Modality
>
})
=>
cn
(
'border-l px-3 py-2 text-center'
,
row
.
set
.
has
(
modality
)
?
'bg-emerald-50/40 dark:bg-emerald-500/10'
:
'bg-background'
),
cell
:
(
row
:
{
label
:
string
;
set
:
Set
<
Modality
>
})
=>
{
const
enabled
=
row
.
set
.
has
(
modality
)
const
Icon
=
MODALITY_META
[
modality
].
icon
return
(
<
span
className=
{
cn
(
'inline-flex items-center justify-center'
,
enabled
?
'text-emerald-700 dark:text-emerald-300'
:
'text-muted-foreground/40'
)
}
aria
-
label=
{
enabled
?
t
(
'
{{
modality
}}
supported
'
,
{
modality
:
t
(
MODALITY_META
[
modality
].
labelKey
),
})
:
t
(
'
{{
modality
}}
not
supported
'
,
{
modality
:
t
(
MODALITY_META
[
modality
].
labelKey
),
})
}
>
<
Icon
className=
'size-4'
/>
</
span
>
)
},
})),
]
}
/>
)
}
web/default/src/features/pricing/components/model-details-performance.tsx
View file @
a95655a2
...
...
@@ -31,6 +31,7 @@ import {
formatLatency
,
formatThroughput
,
formatUptimePct
,
getSuccessRateTextClass
,
}
from
'@/features/performance-metrics/lib/format'
import
type
{
PerformanceGroup
}
from
'@/features/performance-metrics/types'
import
{
type
UptimeDayPoint
}
from
'../lib/mock-stats'
...
...
@@ -43,10 +44,9 @@ function StatCard(props: {
label
:
string
value
:
React
.
ReactNode
hint
?:
string
intent
?:
'default'
|
'warning'
|
'success'
valueClassName
?:
string
})
{
const
Icon
=
props
.
icon
const
intent
=
props
.
intent
??
'default'
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'
>
...
...
@@ -56,8 +56,7 @@ function StatCard(props: {
<
span
className=
{
cn
(
'text-foreground font-mono text-lg font-semibold tabular-nums'
,
intent
===
'warning'
&&
'text-amber-600 dark:text-amber-400'
,
intent
===
'success'
&&
'text-emerald-600 dark:text-emerald-400'
props
.
valueClassName
)
}
>
{
props
.
value
}
...
...
@@ -217,12 +216,6 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
successRates
.
length
:
0
const
incidentCount
=
uptimeSeries
.
reduce
((
s
,
p
)
=>
s
+
p
.
incidents
,
0
)
let
intent
:
'default'
|
'warning'
|
'success'
=
'warning'
if
(
successRate
>=
99.9
)
{
intent
=
'success'
}
else
if
(
successRate
>=
99
)
{
intent
=
'default'
}
return
(
<
div
className=
'flex flex-col gap-4'
>
...
...
@@ -249,7 +242,7 @@ export function ModelDetailsPerformance(props: { model: PricingModel }) {
})
:
t
(
'No incidents in the last 24 hours'
)
}
intent=
{
intent
}
valueClassName=
{
getSuccessRateTextClass
(
successRate
)
}
/>
</
div
>
...
...
web/default/src/features/pricing/components/model-details-quick-stats.tsx
deleted
100644 → 0
View file @
3cc2b1be
/*
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
{
CalendarClock
,
FileText
,
Layers
,
Maximize2
,
Sparkles
,
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
cn
}
from
'@/lib/utils'
import
{
formatTokenCount
,
formatYearMonth
,
type
ModelMetadata
,
}
from
'../lib/model-metadata'
import
type
{
Modality
}
from
'../types'
import
{
ModalityIcons
}
from
'./model-details-modalities'
type
QuickStatsProps
=
{
metadata
:
ModelMetadata
}
type
Stat
=
{
key
:
string
icon
:
React
.
ComponentType
<
{
className
?:
string
}
>
label
:
string
value
:
React
.
ReactNode
hint
?:
string
}
function
buildStats
(
metadata
:
ModelMetadata
,
t
:
(
key
:
string
)
=>
string
):
Stat
[]
{
const
stats
:
Stat
[]
=
[
{
key
:
'context'
,
icon
:
Layers
,
label
:
t
(
'Context'
),
value
:
formatTokenCount
(
metadata
.
context_length
),
hint
:
t
(
'Maximum input window'
),
},
]
if
(
metadata
.
max_output_tokens
>
0
)
{
stats
.
push
({
key
:
'max-output'
,
icon
:
Maximize2
,
label
:
t
(
'Max output'
),
value
:
formatTokenCount
(
metadata
.
max_output_tokens
),
hint
:
t
(
'Maximum tokens per response'
),
})
}
stats
.
push
({
key
:
'modalities'
,
icon
:
FileText
,
label
:
t
(
'Modalities'
),
value
:
(
<
ModalityFlow
input=
{
metadata
.
input_modalities
}
output=
{
metadata
.
output_modalities
}
/>
),
})
if
(
metadata
.
knowledge_cutoff
)
{
stats
.
push
({
key
:
'knowledge'
,
icon
:
Sparkles
,
label
:
t
(
'Knowledge cutoff'
),
value
:
formatYearMonth
(
metadata
.
knowledge_cutoff
),
})
}
if
(
metadata
.
release_date
)
{
stats
.
push
({
key
:
'release'
,
icon
:
CalendarClock
,
label
:
t
(
'Released'
),
value
:
formatYearMonth
(
metadata
.
release_date
),
})
}
return
stats
}
function
ModalityFlow
(
props
:
{
input
:
Modality
[];
output
:
Modality
[]
})
{
return
(
<
span
className=
'inline-flex items-center gap-1 align-middle'
>
<
ModalityIcons
modalities=
{
props
.
input
}
className=
'size-3.5'
/>
<
span
className=
'text-muted-foreground/40'
>
→
</
span
>
<
ModalityIcons
modalities=
{
props
.
output
}
className=
'size-3.5'
/>
</
span
>
)
}
export
function
ModelDetailsQuickStats
(
props
:
QuickStatsProps
)
{
const
{
t
}
=
useTranslation
()
const
stats
=
buildStats
(
props
.
metadata
,
t
)
return
(
<
div
className=
'bg-muted/20 grid grid-cols-2 gap-px overflow-hidden rounded-lg border @md/details:grid-cols-3 @2xl/details:grid-cols-5'
>
{
stats
.
map
((
stat
)
=>
{
const
Icon
=
stat
.
icon
return
(
<
div
key=
{
stat
.
key
}
className=
{
cn
(
'bg-background flex min-w-0 flex-col gap-0.5 px-3 py-2.5'
)
}
>
<
span
className=
'text-muted-foreground inline-flex min-w-0 items-center gap-1 text-[10px] font-medium tracking-wider uppercase'
>
<
Icon
className=
'size-3 shrink-0'
/>
<
span
className=
'truncate'
>
{
stat
.
label
}
</
span
>
</
span
>
<
span
className=
'text-foreground truncate text-sm font-semibold tabular-nums'
>
{
stat
.
value
}
</
span
>
{
stat
.
hint
&&
(
<
span
className=
'text-muted-foreground/60 truncate text-[10px]'
>
{
stat
.
hint
}
</
span
>
)
}
</
div
>
)
})
}
</
div
>
)
}
web/default/src/features/pricing/components/model-details-uptime-sparkline.tsx
View file @
a95655a2
...
...
@@ -25,7 +25,11 @@ import {
TooltipContent
,
TooltipTrigger
,
}
from
'@/components/ui/tooltip'
import
{
formatUptimePct
}
from
'@/features/performance-metrics/lib/format'
import
{
formatUptimePct
,
getSuccessRateDotClass
,
getSuccessRateTextClass
,
}
from
'@/features/performance-metrics/lib/format'
import
{
aggregateUptime
,
type
UptimeDayPoint
}
from
'../lib/mock-stats'
// ---------------------------------------------------------------------------
...
...
@@ -50,14 +54,6 @@ type UptimeSparklineProps = {
className
?:
string
}
function
colourFor
(
uptime
:
number
):
string
{
if
(
uptime
>=
99.9
)
return
'bg-emerald-500'
if
(
uptime
>=
99.0
)
return
'bg-emerald-400'
if
(
uptime
>=
95.0
)
return
'bg-amber-500'
if
(
uptime
>=
90.0
)
return
'bg-amber-600'
return
'bg-rose-500'
}
function
heightFor
(
uptime
:
number
):
string
{
if
(
uptime
>=
99.9
)
return
'h-full'
if
(
uptime
>=
99.0
)
return
'h-[88%]'
...
...
@@ -66,13 +62,6 @@ function heightFor(uptime: number): string {
return
'h-[40%]'
}
function
overallTextColour
(
pct
:
number
):
string
{
if
(
pct
>=
99.9
)
return
'text-emerald-600 dark:text-emerald-400'
if
(
pct
>=
99.0
)
return
'text-emerald-600 dark:text-emerald-400'
if
(
pct
>=
95.0
)
return
'text-amber-600 dark:text-amber-400'
return
'text-rose-600 dark:text-rose-400'
}
export
function
UptimeSparkline
(
props
:
UptimeSparklineProps
)
{
const
size
=
props
.
size
??
'md'
const
showOverall
=
props
.
showOverall
??
true
...
...
@@ -116,7 +105,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
<
div
className=
{
cn
(
'w-full rounded-sm'
,
colourFor
(
day
.
uptime_pct
),
getSuccessRateDotClass
(
day
.
uptime_pct
),
heightFor
(
day
.
uptime_pct
)
)
}
aria
-
hidden
...
...
@@ -138,7 +127,7 @@ export function UptimeSparkline(props: UptimeSparklineProps) {
<
span
className=
{
cn
(
'font-mono text-sm font-semibold tabular-nums'
,
overallTextColour
(
overall
)
getSuccessRateTextClass
(
overall
)
)
}
>
{
overall
.
toFixed
(
1
)
}
%
...
...
web/default/src/features/pricing/components/model-details.tsx
View file @
a95655a2
...
...
@@ -19,7 +19,18 @@ For commercial licensing, please contact support@quantumnous.com
import
{
useMemo
}
from
'react'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
useNavigate
,
useParams
,
useSearch
}
from
'@tanstack/react-router'
import
{
ArrowLeft
,
Code2
,
HeartPulse
,
Info
,
Timer
}
from
'lucide-react'
import
{
ArrowLeft
,
CalendarClock
,
Code2
,
FileText
,
HeartPulse
,
Info
,
Layers
,
Maximize2
,
Sparkles
,
Timer
,
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
getLobeIcon
}
from
'@/lib/lobe-icon'
import
{
cn
}
from
'@/lib/utils'
...
...
@@ -43,6 +54,7 @@ import {
formatLatency
,
formatThroughput
,
formatUptimePct
,
getSuccessRateTextClass
,
}
from
'@/features/performance-metrics/lib/format'
import
{
DEFAULT_TOKEN_UNIT
,
QUOTA_TYPE_VALUES
}
from
'../constants'
import
{
usePricingData
}
from
'../hooks/use-pricing-data'
...
...
@@ -54,20 +66,16 @@ import {
}
from
'../lib/dynamic-price'
import
{
parseTags
}
from
'../lib/filters'
import
{
getAvailableGroups
,
isTokenBasedModel
}
from
'../lib/model-helpers'
import
{
inferModelMetadata
}
from
'../lib/model-metadata'
import
{
formatFixedPrice
,
formatGroupPrice
}
from
'../lib/price'
import
type
{
Modality
,
ModelCapability
,
PriceType
,
PricingModel
,
TokenUnit
,
}
from
'../types'
import
{
DynamicPricingBreakdown
}
from
'./dynamic-pricing-breakdown'
import
{
ModelDetailsApi
,
ModelDetailsProviderInfo
}
from
'./model-details-api'
import
{
ModalityIcons
}
from
'./model-details-modalities'
import
{
ModelDetailsApi
}
from
'./model-details-api'
import
{
ModelDetailsPerformance
}
from
'./model-details-performance'
import
{
ModelDetailsQuickStats
}
from
'./model-details-quick-stats'
// ----------------------------------------------------------------------------
// Local UI helpers
...
...
@@ -96,80 +104,51 @@ const CAPABILITY_LABEL_KEYS: Record<ModelCapability, string> = {
embeddings
:
'Embeddings'
,
}
function
CompactCapabilityList
(
props
:
{
capabilities
:
ModelCapability
[]
})
{
const
{
t
}
=
useTranslation
()
if
(
props
.
capabilities
.
length
===
0
)
{
return
(
<
span
className=
'text-muted-foreground text-xs'
>
{
t
(
'No capabilities reported for this model.'
)
}
</
span
>
)
}
return
(
<
div
className=
'flex flex-wrap gap-1.5'
>
{
props
.
capabilities
.
map
((
capability
)
=>
(
<
span
key=
{
capability
}
className=
'bg-muted text-muted-foreground rounded-md px-2 py-1 text-xs font-medium'
>
{
t
(
CAPABILITY_LABEL_KEYS
[
capability
]
??
capability
)
}
</
span
>
))
}
</
div
>
)
const
MODALITY_LABEL_KEYS
:
Record
<
string
,
string
>
=
{
text
:
'Text'
,
image
:
'Image'
,
audio
:
'Audio'
,
video
:
'Video'
,
file
:
'File'
,
}
function
CompactModalities
(
props
:
{
input
:
Modality
[];
output
:
Modality
[]
})
{
const
{
t
}
=
useTranslation
()
const
TOKEN_FORMAT
=
new
Intl
.
NumberFormat
(
undefined
,
{
maximumFractionDigits
:
1
,
})
return
(
<
div
className=
'grid gap-2 sm:grid-cols-2'
>
<
div
className=
'flex items-center justify-between gap-3 rounded-lg border px-3 py-2'
>
<
span
className=
'text-muted-foreground text-xs font-medium'
>
{
t
(
'Input'
)
}
</
span
>
<
ModalityIcons
modalities=
{
props
.
input
}
/>
</
div
>
<
div
className=
'flex items-center justify-between gap-3 rounded-lg border px-3 py-2'
>
<
span
className=
'text-muted-foreground text-xs font-medium'
>
{
t
(
'Output'
)
}
</
span
>
<
ModalityIcons
modalities=
{
props
.
output
}
/>
</
div
>
</
div
>
)
function
formatCatalogTokenCount
(
tokens
:
number
):
string
{
if
(
!
Number
.
isFinite
(
tokens
)
||
tokens
<=
0
)
return
''
if
(
tokens
>=
1
_000_000
)
{
return
`
${
TOKEN_FORMAT
.
format
(
tokens
/
1
_000_000
)}
M`
}
if
(
tokens
>=
1
_000
)
{
return
`
${
TOKEN_FORMAT
.
format
(
tokens
/
1
_000
)}
K`
}
return
TOKEN_FORMAT
.
format
(
tokens
)
}
function
ModelSignalsSection
(
props
:
{
capabilities
:
ModelCapability
[]
input
:
Modality
[]
output
:
Modality
[]
})
{
const
{
t
}
=
useTranslation
()
function
formatCatalogYearMonth
(
value
?:
string
):
string
{
if
(
!
value
)
return
''
const
[
yearStr
,
monthStr
]
=
value
.
split
(
'-'
)
const
year
=
Number
(
yearStr
)
const
month
=
Number
(
monthStr
)
if
(
!
Number
.
isFinite
(
year
)
||
!
Number
.
isFinite
(
month
))
return
value
const
date
=
new
Date
(
Date
.
UTC
(
year
,
month
-
1
,
1
))
return
date
.
toLocaleString
(
undefined
,
{
year
:
'numeric'
,
month
:
'short'
})
}
return
(
<
section
>
<
SectionTitle
>
{
t
(
'Capabilities'
)
}
/
{
t
(
'Supported modalities'
)
}
</
SectionTitle
>
<
div
className=
'grid gap-3 rounded-xl border p-3 @2xl/details:grid-cols-[minmax(0,1.5fr)_minmax(260px,1fr)]'
>
<
CompactCapabilityList
capabilities=
{
props
.
capabilities
}
/>
<
CompactModalities
input=
{
props
.
input
}
output=
{
props
.
output
}
/>
</
div
>
</
section
>
)
function
normalizeCatalogItems
(
items
?:
readonly
string
[]):
string
[]
{
if
(
!
items
)
return
[]
return
items
.
filter
((
item
)
=>
item
.
trim
().
length
>
0
)
}
function
OverviewMetric
(
props
:
{
icon
:
React
.
ComponentType
<
{
className
?:
string
}
>
label
:
string
value
:
React
.
ReactNode
intent
?:
'default'
|
'warning'
|
'success'
valueClassName
?:
string
})
{
const
Icon
=
props
.
icon
const
intent
=
props
.
intent
??
'default'
return
(
<
div
className=
'flex min-w-0 items-center gap-2 px-3 py-2'
>
...
...
@@ -181,8 +160,7 @@ function OverviewMetric(props: {
<
div
className=
{
cn
(
'text-foreground truncate font-mono text-sm font-semibold tabular-nums'
,
intent
===
'warning'
&&
'text-amber-600 dark:text-amber-400'
,
intent
===
'success'
&&
'text-emerald-600 dark:text-emerald-400'
props
.
valueClassName
)
}
>
{
props
.
value
}
...
...
@@ -208,12 +186,6 @@ function OverviewSummaryGrid(props: { model: PricingModel }) {
successRates
.
length
>
0
?
successRates
.
reduce
((
sum
,
rate
)
=>
sum
+
rate
,
0
)
/
successRates
.
length
:
Number
.
NaN
let
successIntent
:
'default'
|
'warning'
|
'success'
=
'warning'
if
(
successRate
>=
99.9
)
{
successIntent
=
'success'
}
else
if
(
successRate
>=
99
)
{
successIntent
=
'default'
}
const
tpsValues
=
groups
.
map
((
group
)
=>
group
.
avg_tps
)
.
filter
((
value
)
=>
value
>
0
)
...
...
@@ -248,12 +220,305 @@ function OverviewSummaryGrid(props: { model: PricingModel }) {
icon=
{
HeartPulse
}
label=
{
t
(
'Success rate'
)
}
value=
{
formatUptimePct
(
successRate
)
}
intent=
{
successIntent
}
valueClassName=
{
getSuccessRateTextClass
(
successRate
)
}
/>
</
div
>
)
}
function
CatalogPillList
(
props
:
{
items
:
string
[]
})
{
return
(
<
div
className=
'flex min-w-0 flex-wrap gap-1.5'
>
{
props
.
items
.
map
((
item
)
=>
(
<
span
key=
{
item
}
className=
'bg-muted text-muted-foreground rounded-md px-2 py-1 text-xs font-medium'
>
{
item
}
</
span
>
))
}
</
div
>
)
}
function
CatalogTextValue
(
props
:
{
children
:
React
.
ReactNode
})
{
return
(
<
span
className=
'text-foreground min-w-0 truncate text-sm font-semibold'
>
{
props
.
children
}
</
span
>
)
}
function
CatalogInfoCell
(
props
:
{
label
:
string
;
children
:
React
.
ReactNode
})
{
return
(
<
div
className=
'bg-card flex min-w-0 flex-col gap-1 px-3 py-2.5'
>
<
span
className=
'text-muted-foreground text-[10px] font-medium tracking-wider uppercase'
>
{
props
.
label
}
</
span
>
{
props
.
children
}
</
div
>
)
}
function
ModalityLabels
(
props
:
{
items
:
string
[]
})
{
const
{
t
}
=
useTranslation
()
if
(
props
.
items
.
length
===
0
)
return
null
return
(
<
span
className=
'inline-flex items-center gap-1 align-middle'
>
{
props
.
items
.
map
((
item
)
=>
(
<
span
key=
{
item
}
className=
'font-medium'
>
{
t
(
MODALITY_LABEL_KEYS
[
item
]
??
item
)
}
</
span
>
))
}
</
span
>
)
}
function
ModelBackendQuickStats
(
props
:
{
model
:
PricingModel
})
{
const
{
t
}
=
useTranslation
()
const
model
=
props
.
model
const
inputModalities
=
normalizeCatalogItems
(
model
.
input_modalities
)
const
outputModalities
=
normalizeCatalogItems
(
model
.
output_modalities
)
const
contextLength
=
model
.
context_length
??
0
const
maxOutput
=
model
.
max_output_tokens
??
0
const
knowledgeCutoff
=
formatCatalogYearMonth
(
model
.
knowledge_cutoff
)
const
releaseDate
=
formatCatalogYearMonth
(
model
.
release_date
)
const
stats
:
{
key
:
string
icon
:
React
.
ComponentType
<
{
className
?:
string
}
>
label
:
string
value
:
React
.
ReactNode
hint
?:
string
}[]
=
[]
if
(
contextLength
>
0
)
{
stats
.
push
({
key
:
'context'
,
icon
:
Layers
,
label
:
t
(
'Context'
),
value
:
formatCatalogTokenCount
(
contextLength
),
hint
:
t
(
'Maximum input window'
),
})
}
if
(
maxOutput
>
0
)
{
stats
.
push
({
key
:
'max-output'
,
icon
:
Maximize2
,
label
:
t
(
'Max output'
),
value
:
formatCatalogTokenCount
(
maxOutput
),
hint
:
t
(
'Maximum tokens per response'
),
})
}
if
(
inputModalities
.
length
>
0
||
outputModalities
.
length
>
0
)
{
stats
.
push
({
key
:
'modalities'
,
icon
:
FileText
,
label
:
t
(
'Modalities'
),
value
:
(
<
span
className=
'inline-flex items-center gap-1'
>
<
ModalityLabels
items=
{
inputModalities
}
/>
{
inputModalities
.
length
>
0
&&
outputModalities
.
length
>
0
&&
(
<
span
className=
'text-muted-foreground/40'
>
→
</
span
>
)
}
<
ModalityLabels
items=
{
outputModalities
}
/>
</
span
>
),
})
}
if
(
knowledgeCutoff
)
{
stats
.
push
({
key
:
'knowledge'
,
icon
:
Sparkles
,
label
:
t
(
'Knowledge cutoff'
),
value
:
knowledgeCutoff
,
})
}
if
(
releaseDate
)
{
stats
.
push
({
key
:
'release'
,
icon
:
CalendarClock
,
label
:
t
(
'Released'
),
value
:
releaseDate
,
})
}
if
(
stats
.
length
===
0
)
return
null
return
(
<
div
className=
'bg-muted/20 grid grid-cols-2 gap-px overflow-hidden rounded-lg border @md/details:grid-cols-3 @2xl/details:grid-cols-5'
>
{
stats
.
map
((
stat
)
=>
{
const
Icon
=
stat
.
icon
return
(
<
div
key=
{
stat
.
key
}
className=
'bg-background flex min-w-0 flex-col gap-0.5 px-3 py-2.5'
>
<
span
className=
'text-muted-foreground inline-flex min-w-0 items-center gap-1 text-[10px] font-medium tracking-wider uppercase'
>
<
Icon
className=
'size-3 shrink-0'
/>
<
span
className=
'truncate'
>
{
stat
.
label
}
</
span
>
</
span
>
<
span
className=
'text-foreground truncate text-sm font-semibold tabular-nums'
>
{
stat
.
value
}
</
span
>
{
stat
.
hint
&&
(
<
span
className=
'text-muted-foreground/60 truncate text-[10px]'
>
{
stat
.
hint
}
</
span
>
)
}
</
div
>
)
})
}
</
div
>
)
}
function
ModelBackendSignalsSection
(
props
:
{
model
:
PricingModel
})
{
const
{
t
}
=
useTranslation
()
const
capabilities
=
normalizeCatalogItems
(
props
.
model
.
capabilities
)
const
inputModalities
=
normalizeCatalogItems
(
props
.
model
.
input_modalities
)
const
outputModalities
=
normalizeCatalogItems
(
props
.
model
.
output_modalities
)
if
(
capabilities
.
length
===
0
&&
inputModalities
.
length
===
0
&&
outputModalities
.
length
===
0
)
{
return
null
}
return
(
<
section
>
<
SectionTitle
>
{
t
(
'Capabilities'
)
}
/
{
t
(
'Supported modalities'
)
}
</
SectionTitle
>
<
div
className=
'grid gap-3 rounded-xl border p-3 @2xl/details:grid-cols-[minmax(0,1.5fr)_minmax(260px,1fr)]'
>
{
capabilities
.
length
>
0
?
(
<
CatalogPillList
items=
{
capabilities
.
map
((
capability
)
=>
t
(
CAPABILITY_LABEL_KEYS
[
capability
as
ModelCapability
]
??
capability
)
)
}
/>
)
:
(
<
div
/>
)
}
{
(
inputModalities
.
length
>
0
||
outputModalities
.
length
>
0
)
&&
(
<
div
className=
'grid gap-2 sm:grid-cols-2'
>
{
inputModalities
.
length
>
0
&&
(
<
div
className=
'flex items-center justify-between gap-3 rounded-lg border px-3 py-2'
>
<
span
className=
'text-muted-foreground text-xs font-medium'
>
{
t
(
'Input'
)
}
</
span
>
<
CatalogTextValue
>
<
ModalityLabels
items=
{
inputModalities
}
/>
</
CatalogTextValue
>
</
div
>
)
}
{
outputModalities
.
length
>
0
&&
(
<
div
className=
'flex items-center justify-between gap-3 rounded-lg border px-3 py-2'
>
<
span
className=
'text-muted-foreground text-xs font-medium'
>
{
t
(
'Output'
)
}
</
span
>
<
CatalogTextValue
>
<
ModalityLabels
items=
{
outputModalities
}
/>
</
CatalogTextValue
>
</
div
>
)
}
</
div
>
)
}
</
div
>
</
section
>
)
}
function
ModelBackendProviderSection
(
props
:
{
model
:
PricingModel
})
{
const
{
t
}
=
useTranslation
()
const
model
=
props
.
model
const
groups
=
normalizeCatalogItems
(
model
.
enable_groups
)
const
endpoints
=
normalizeCatalogItems
(
model
.
supported_endpoint_types
)
const
tags
=
parseTags
(
model
.
tags
)
const
cells
:
React
.
ReactNode
[]
=
[]
if
(
model
.
vendor_name
)
{
cells
.
push
(
<
CatalogInfoCell
key=
'provider'
label=
{
t
(
'Provider'
)
}
>
<
CatalogTextValue
>
{
model
.
vendor_name
}
</
CatalogTextValue
>
</
CatalogInfoCell
>
)
}
cells
.
push
(
<
CatalogInfoCell
key=
'type'
label=
{
t
(
'Type'
)
}
>
<
CatalogTextValue
>
{
model
.
quota_type
===
QUOTA_TYPE_VALUES
.
TOKEN
?
t
(
'Token-based'
)
:
t
(
'Per Request'
)
}
</
CatalogTextValue
>
</
CatalogInfoCell
>
)
if
(
groups
.
length
>
0
)
{
cells
.
push
(
<
CatalogInfoCell
key=
'groups'
label=
{
t
(
'Groups'
)
}
>
<
CatalogPillList
items=
{
groups
}
/>
</
CatalogInfoCell
>
)
}
if
(
endpoints
.
length
>
0
)
{
cells
.
push
(
<
CatalogInfoCell
key=
'endpoints'
label=
{
t
(
'Endpoints'
)
}
>
<
CatalogPillList
items=
{
endpoints
}
/>
</
CatalogInfoCell
>
)
}
if
(
tags
.
length
>
0
)
{
cells
.
push
(
<
CatalogInfoCell
key=
'tags'
label=
{
t
(
'Tags'
)
}
>
<
CatalogPillList
items=
{
tags
}
/>
</
CatalogInfoCell
>
)
}
if
(
model
.
parameter_count
)
{
cells
.
push
(
<
CatalogInfoCell
key=
'parameters'
label=
{
t
(
'Parameters'
)
}
>
<
CatalogTextValue
>
{
model
.
parameter_count
}
</
CatalogTextValue
>
</
CatalogInfoCell
>
)
}
if
(
cells
.
length
===
0
)
return
null
return
(
<
section
>
<
SectionTitle
>
{
t
(
'Model'
)
}
</
SectionTitle
>
<
div
className=
'border-border/60 bg-border/60 grid grid-cols-1 gap-px overflow-hidden rounded-lg border sm:grid-cols-2'
>
{
cells
}
</
div
>
</
section
>
)
}
function
ModelBackendDetailsSection
(
props
:
{
model
:
PricingModel
})
{
return
(
<>
<
ModelBackendQuickStats
model=
{
props
.
model
}
/>
<
ModelBackendSignalsSection
model=
{
props
.
model
}
/>
<
ModelBackendProviderSection
model=
{
props
.
model
}
/>
</>
)
}
// ----------------------------------------------------------------------------
// Model header (always visible above the detail sections)
// ----------------------------------------------------------------------------
...
...
@@ -264,7 +529,6 @@ function ModelHeader(props: { model: PricingModel }) {
const
modelIconKey
=
model
.
icon
||
model
.
vendor_icon
const
modelIcon
=
modelIconKey
?
getLobeIcon
(
modelIconKey
,
20
)
:
null
const
description
=
model
.
description
||
model
.
vendor_description
||
null
const
tags
=
parseTags
(
model
.
tags
)
const
isSpecialExpression
=
model
.
billing_mode
===
'tiered_expr'
&&
Boolean
(
model
.
billing_expr
)
&&
...
...
@@ -312,18 +576,6 @@ function ModelHeader(props: { model: PricingModel }) {
{
description
}
</
p
>
)
}
{
tags
.
length
>
0
&&
(
<
div
className=
'mt-2.5 flex flex-wrap gap-1'
>
{
tags
.
map
((
tag
)
=>
(
<
span
key=
{
tag
}
className=
'bg-muted text-muted-foreground rounded px-2 py-0.5 text-[11px] font-medium'
>
{
tag
}
</
span
>
))
}
</
div
>
)
}
</
header
>
)
}
...
...
@@ -901,7 +1153,6 @@ export interface ModelDetailsContentProps {
export
function
ModelDetailsContent
(
props
:
ModelDetailsContentProps
)
{
const
{
t
}
=
useTranslation
()
const
showRechargePrice
=
props
.
showRechargePrice
??
false
const
metadata
=
useMemo
(()
=>
inferModelMetadata
(
props
.
model
),
[
props
.
model
])
const
isDynamic
=
props
.
model
.
billing_mode
===
'tiered_expr'
&&
...
...
@@ -955,15 +1206,7 @@ export function ModelDetailsContent(props: ModelDetailsContentProps) {
/>
</
section
>
<
ModelDetailsQuickStats
metadata=
{
metadata
}
/>
<
ModelSignalsSection
capabilities=
{
metadata
.
capabilities
}
input=
{
metadata
.
input_modalities
}
output=
{
metadata
.
output_modalities
}
/>
<
ModelDetailsProviderInfo
model=
{
props
.
model
}
/>
<
ModelBackendDetailsSection
model=
{
props
.
model
}
/>
</
TabsContent
>
<
TabsContent
value=
'performance'
className=
'outline-none'
>
...
...
web/default/src/features/pricing/components/model-perf-badge.tsx
View file @
a95655a2
...
...
@@ -22,6 +22,7 @@ import { cn } from '@/lib/utils'
import
{
formatLatency
,
formatThroughput
,
getSuccessRateDotClass
,
}
from
'@/features/performance-metrics/lib/format'
export
type
ModelPerfBadgeData
=
{
...
...
@@ -49,12 +50,7 @@ export const ModelPerfBadge = memo(function ModelPerfBadge(
const
{
avg_latency_ms
,
avg_tps
,
success_rate
}
=
props
.
perf
let
statusColor
=
'bg-emerald-500'
if
(
success_rate
<
99
)
{
statusColor
=
'bg-red-500'
}
else
if
(
success_rate
<
99.9
)
{
statusColor
=
'bg-amber-500'
}
const
statusColor
=
getSuccessRateDotClass
(
success_rate
)
return
(
<
div
...
...
web/default/src/features/pricing/lib/index.ts
View file @
a95655a2
...
...
@@ -25,6 +25,5 @@ export * from './price'
export
*
from
'./model-helpers'
export
*
from
'./billing-expr'
export
*
from
'./tier-expr'
export
*
from
'./model-metadata'
export
*
from
'./mock-stats'
export
*
from
'./seed'
web/default/src/features/pricing/lib/model-metadata.ts
deleted
100644 → 0
View file @
3cc2b1be
/*
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
{
Modality
,
ModelCapability
,
PricingModel
}
from
'../types'
import
{
hashStringToSeed
,
seededRandom
}
from
'./seed'
// ----------------------------------------------------------------------------
// Model metadata inference
// ----------------------------------------------------------------------------
//
// The backend does not currently return `context_length`, `max_output_tokens`,
// `knowledge_cutoff`, `release_date`, `parameter_count`, or modality/capability
// flags for a model. Until it does, we infer reasonable values client-side
// from the data we already have (endpoint types, ratios, tags, model name)
// and fall back to a deterministic mock seeded from the model name so that
// every render of the same model shows the same numbers.
//
// When the backend starts returning these fields, callers should prefer the
// explicit values on `model.*` and only fall back to the inferred ones.
const
TEXT_INPUT_ENDPOINTS
=
new
Set
([
'openai'
,
'openai-response'
,
'anthropic'
,
'gemini'
,
'embeddings'
,
'jina-rerank'
,
])
const
IMAGE_OUTPUT_ENDPOINTS
=
new
Set
([
'image-generation'
])
const
VIDEO_OUTPUT_ENDPOINTS
=
new
Set
([
'openai-video'
])
const
EMBEDDING_ENDPOINTS
=
new
Set
([
'embeddings'
,
'jina-rerank'
])
const
REASONING_NAME_PATTERNS
=
[
/^o
[
1-4
](?:[
-:_
]
.+
)?
$/i
,
/reasoning/i
,
/thinking/i
,
/qwq/i
,
/deepseek-r
\d
/i
,
/grok.*-
(?:
thinking|reasoning
)
/i
,
]
const
VISION_NAME_PATTERNS
=
[
/vision/i
,
/vl
(?:[
-_
]
|$
)
/i
,
/multimodal/i
,
/-omni/i
,
]
const
AUDIO_NAME_PATTERNS
=
[
/audio/i
,
/whisper/i
,
/tts/i
,
/voice/i
,
/-realtime/i
,
]
const
VIDEO_NAME_PATTERNS
=
[
/video/i
,
/sora/i
,
/veo/i
,
/kling/i
,
/pika/i
]
const
CODE_NAME_PATTERNS
=
[
/code/i
,
/-coder/i
]
const
WEB_SEARCH_PATTERNS
=
[
/web
[
-_
]?
search/i
,
/-online/i
,
/perplexity/i
]
const
KNOWLEDGE_CUTOFFS
=
[
'2023-04'
,
'2023-10'
,
'2023-12'
,
'2024-04'
,
'2024-06'
,
'2024-08'
,
'2024-10'
,
'2024-12'
,
'2025-02'
,
'2025-04'
,
'2025-08'
,
]
const
PARAM_BUCKETS
=
[
'1.5B'
,
'3B'
,
'7B'
,
'8B'
,
'14B'
,
'32B'
,
'70B'
,
'120B'
,
'405B'
,
]
const
CONTEXT_BUCKETS
=
[
8
_192
,
16
_384
,
32
_768
,
65
_536
,
128
_000
,
200
_000
,
1
_000_000
,
]
const
MAX_OUTPUT_BUCKETS
=
[
2
_048
,
4
_096
,
8
_192
,
16
_384
,
32
_768
,
65
_536
]
const
TAG_TO_CAPABILITY
:
Record
<
string
,
ModelCapability
>
=
{
vision
:
'vision'
,
multimodal
:
'vision'
,
reasoning
:
'reasoning'
,
thinking
:
'reasoning'
,
tools
:
'tools'
,
function
:
'function_calling'
,
'function-calling'
:
'function_calling'
,
streaming
:
'streaming'
,
json
:
'json_mode'
,
structured
:
'structured_output'
,
search
:
'web_search'
,
code
:
'code_interpreter'
,
embedding
:
'embeddings'
,
}
const
TAG_TO_MODALITY
:
Record
<
string
,
Modality
>
=
{
text
:
'text'
,
image
:
'image'
,
audio
:
'audio'
,
video
:
'video'
,
file
:
'file'
,
document
:
'file'
,
pdf
:
'file'
,
}
function
pickFromBuckets
<
T
>
(
buckets
:
T
[],
rand
:
()
=>
number
):
T
{
return
buckets
[
Math
.
floor
(
rand
()
*
buckets
.
length
)]
}
function
parseModelTags
(
tagsString
?:
string
):
string
[]
{
if
(
!
tagsString
)
return
[]
return
tagsString
.
split
(
/
[
,;|
\s]
+/
)
.
map
((
t
)
=>
t
.
trim
().
toLowerCase
())
.
filter
(
Boolean
)
}
function
nameMatches
(
name
:
string
,
patterns
:
RegExp
[]):
boolean
{
return
patterns
.
some
((
re
)
=>
re
.
test
(
name
))
}
function
inferInputModalities
(
model
:
PricingModel
,
tags
:
string
[],
endpoints
:
string
[],
name
:
string
):
Modality
[]
{
const
set
=
new
Set
<
Modality
>
()
if
(
endpoints
.
length
===
0
||
endpoints
.
some
((
e
)
=>
TEXT_INPUT_ENDPOINTS
.
has
(
e
))
)
{
set
.
add
(
'text'
)
}
if
(
model
.
image_ratio
!=
null
||
nameMatches
(
name
,
VISION_NAME_PATTERNS
))
{
set
.
add
(
'image'
)
}
if
(
model
.
audio_ratio
!=
null
||
nameMatches
(
name
,
AUDIO_NAME_PATTERNS
))
{
set
.
add
(
'audio'
)
}
if
(
nameMatches
(
name
,
VIDEO_NAME_PATTERNS
))
{
set
.
add
(
'video'
)
}
for
(
const
tag
of
tags
)
{
const
m
=
TAG_TO_MODALITY
[
tag
]
if
(
m
)
set
.
add
(
m
)
}
if
(
set
.
size
===
0
)
set
.
add
(
'text'
)
return
ordered
(
set
)
}
function
inferOutputModalities
(
model
:
PricingModel
,
endpoints
:
string
[],
name
:
string
):
Modality
[]
{
const
set
=
new
Set
<
Modality
>
()
if
(
endpoints
.
some
((
e
)
=>
IMAGE_OUTPUT_ENDPOINTS
.
has
(
e
)))
set
.
add
(
'image'
)
if
(
endpoints
.
some
((
e
)
=>
VIDEO_OUTPUT_ENDPOINTS
.
has
(
e
)))
set
.
add
(
'video'
)
if
(
endpoints
.
some
((
e
)
=>
EMBEDDING_ENDPOINTS
.
has
(
e
)))
set
.
add
(
'text'
)
if
(
model
.
audio_completion_ratio
!=
null
||
/tts|voice|audio-out/i
.
test
(
name
)
)
{
set
.
add
(
'audio'
)
}
if
(
set
.
size
===
0
)
set
.
add
(
'text'
)
return
ordered
(
set
)
}
function
inferCapabilities
(
model
:
PricingModel
,
tags
:
string
[],
endpoints
:
string
[],
name
:
string
,
outputs
:
Modality
[],
inputs
:
Modality
[]
):
ModelCapability
[]
{
const
set
=
new
Set
<
ModelCapability
>
()
if
(
outputs
.
includes
(
'text'
)
&&
!
endpoints
.
includes
(
'image-generation'
))
{
set
.
add
(
'streaming'
)
set
.
add
(
'system_prompt'
)
}
if
(
!
endpoints
.
includes
(
'image-generation'
)
&&
!
endpoints
.
includes
(
'embeddings'
)
&&
!
endpoints
.
includes
(
'jina-rerank'
)
)
{
set
.
add
(
'function_calling'
)
set
.
add
(
'tools'
)
set
.
add
(
'json_mode'
)
set
.
add
(
'structured_output'
)
}
if
(
inputs
.
includes
(
'image'
))
set
.
add
(
'vision'
)
if
(
model
.
cache_ratio
!=
null
)
set
.
add
(
'caching'
)
if
(
endpoints
.
some
((
e
)
=>
EMBEDDING_ENDPOINTS
.
has
(
e
)))
set
.
add
(
'embeddings'
)
if
(
nameMatches
(
name
,
REASONING_NAME_PATTERNS
))
set
.
add
(
'reasoning'
)
if
(
nameMatches
(
name
,
CODE_NAME_PATTERNS
))
set
.
add
(
'code_interpreter'
)
if
(
nameMatches
(
name
,
WEB_SEARCH_PATTERNS
))
set
.
add
(
'web_search'
)
for
(
const
tag
of
tags
)
{
const
cap
=
TAG_TO_CAPABILITY
[
tag
]
if
(
cap
)
set
.
add
(
cap
)
}
return
Array
.
from
(
set
)
}
function
ordered
(
modalities
:
Set
<
Modality
>
):
Modality
[]
{
const
order
:
Modality
[]
=
[
'text'
,
'image'
,
'audio'
,
'video'
,
'file'
]
return
order
.
filter
((
m
)
=>
modalities
.
has
(
m
))
}
function
inferContextAndOutputs
(
name
:
string
,
rand
:
()
=>
number
,
endpoints
:
string
[]
):
{
context
:
number
;
maxOutput
:
number
}
{
if
(
endpoints
.
includes
(
'embeddings'
)
||
endpoints
.
includes
(
'jina-rerank'
))
{
return
{
context
:
8
_192
,
maxOutput
:
0
}
}
if
(
endpoints
.
includes
(
'image-generation'
)
||
endpoints
.
includes
(
'openai-video'
)
)
{
return
{
context
:
4
_096
,
maxOutput
:
0
}
}
const
lower
=
name
.
toLowerCase
()
if
(
lower
.
includes
(
'1m'
)
||
lower
.
includes
(
'-long'
))
{
return
{
context
:
1
_000_000
,
maxOutput
:
65
_536
}
}
if
(
/claude.*
(?:
4|opus|sonnet
)
/
.
test
(
lower
))
{
return
{
context
:
1
_000_000
,
maxOutput
:
65
_536
}
}
if
(
lower
.
includes
(
'200k'
)
||
lower
.
includes
(
'claude-3'
)
||
lower
.
includes
(
'claude-4'
)
)
{
return
{
context
:
200
_000
,
maxOutput
:
16
_384
}
}
if
(
lower
.
includes
(
'128k'
)
||
/gpt-4o|gpt-4
\.
1|gpt-5|o1|o3|o4/
.
test
(
lower
))
{
return
{
context
:
128
_000
,
maxOutput
:
16
_384
}
}
if
(
/gemini.*-2|gemini.*pro|gemini.*flash/
.
test
(
lower
))
{
return
{
context
:
1
_000_000
,
maxOutput
:
8
_192
}
}
if
(
/gpt-3
\.
5|claude-2/
.
test
(
lower
))
{
return
{
context
:
16
_384
,
maxOutput
:
4
_096
}
}
const
context
=
pickFromBuckets
(
CONTEXT_BUCKETS
,
rand
)
const
maxOutput
=
Math
.
min
(
context
,
pickFromBuckets
(
MAX_OUTPUT_BUCKETS
,
rand
))
return
{
context
,
maxOutput
}
}
function
inferReleaseAndCutoff
(
rand
:
()
=>
number
):
{
release
:
string
cutoff
:
string
}
{
const
cutoff
=
pickFromBuckets
(
KNOWLEDGE_CUTOFFS
,
rand
)
const
[
year
,
month
]
=
cutoff
.
split
(
'-'
).
map
(
Number
)
const
offsetMonths
=
4
+
Math
.
floor
(
rand
()
*
6
)
const
releaseMonth
=
month
+
offsetMonths
const
releaseYear
=
year
+
Math
.
floor
((
releaseMonth
-
1
)
/
12
)
const
finalMonth
=
((
releaseMonth
-
1
)
%
12
)
+
1
const
release
=
`
${
releaseYear
}
-
${
String
(
finalMonth
).
padStart
(
2
,
'0'
)}
-15`
return
{
release
,
cutoff
}
}
export
type
ModelMetadata
=
{
context_length
:
number
max_output_tokens
:
number
knowledge_cutoff
:
string
release_date
:
string
parameter_count
:
string
input_modalities
:
Modality
[]
output_modalities
:
Modality
[]
capabilities
:
ModelCapability
[]
}
/**
* Infer / mock model metadata. Prefers explicit fields on `model.*` and
* falls back to inference + a deterministic seed otherwise.
*/
export
function
inferModelMetadata
(
model
:
PricingModel
):
ModelMetadata
{
const
name
=
model
.
model_name
||
''
const
rand
=
seededRandom
(
hashStringToSeed
(
name
))
const
tags
=
parseModelTags
(
model
.
tags
)
const
endpoints
=
model
.
supported_endpoint_types
||
[]
const
inputs
=
model
.
input_modalities
??
inferInputModalities
(
model
,
tags
,
endpoints
,
name
)
const
outputs
=
model
.
output_modalities
??
inferOutputModalities
(
model
,
endpoints
,
name
)
const
capabilities
=
model
.
capabilities
??
inferCapabilities
(
model
,
tags
,
endpoints
,
name
,
outputs
,
inputs
)
const
fallback
=
inferContextAndOutputs
(
name
,
rand
,
endpoints
)
const
cutoffAndRelease
=
inferReleaseAndCutoff
(
rand
)
return
{
context_length
:
model
.
context_length
??
fallback
.
context
,
max_output_tokens
:
model
.
max_output_tokens
??
fallback
.
maxOutput
,
knowledge_cutoff
:
model
.
knowledge_cutoff
??
cutoffAndRelease
.
cutoff
,
release_date
:
model
.
release_date
??
cutoffAndRelease
.
release
,
parameter_count
:
model
.
parameter_count
??
pickFromBuckets
(
PARAM_BUCKETS
,
rand
),
input_modalities
:
inputs
,
output_modalities
:
outputs
,
capabilities
,
}
}
const
TOKEN_FORMAT
=
new
Intl
.
NumberFormat
(
undefined
,
{
maximumFractionDigits
:
1
,
})
/** Format a token count compactly: 128_000 → "128K", 1_000_000 → "1M". */
export
function
formatTokenCount
(
tokens
:
number
):
string
{
if
(
!
Number
.
isFinite
(
tokens
)
||
tokens
<=
0
)
return
'—'
if
(
tokens
>=
1
_000_000
)
{
const
value
=
tokens
/
1
_000_000
return
`
${
TOKEN_FORMAT
.
format
(
value
)}
M`
}
if
(
tokens
>=
1
_000
)
{
const
value
=
tokens
/
1
_000
return
`
${
TOKEN_FORMAT
.
format
(
value
)}
K`
}
return
TOKEN_FORMAT
.
format
(
tokens
)
}
/** Format a YYYY-MM (or YYYY-MM-DD) date as `Mon YYYY` for display. */
export
function
formatYearMonth
(
value
:
string
):
string
{
if
(
!
value
)
return
'—'
const
[
yearStr
,
monthStr
]
=
value
.
split
(
'-'
)
const
year
=
Number
(
yearStr
)
const
month
=
Number
(
monthStr
)
if
(
!
Number
.
isFinite
(
year
)
||
!
Number
.
isFinite
(
month
))
return
value
const
date
=
new
Date
(
Date
.
UTC
(
year
,
month
-
1
,
1
))
return
date
.
toLocaleString
(
undefined
,
{
year
:
'numeric'
,
month
:
'short'
})
}
// ---------------------------------------------------------------------------
// Provider / vendor / tokenizer / license inference
// ---------------------------------------------------------------------------
//
// These helpers derive vendor-style metadata from the model name. They are
// purely heuristic and serve only the API-info display until the backend
// returns explicit fields.
export
type
ModelVendor
=
|
'openai'
|
'anthropic'
|
'google'
|
'meta'
|
'mistral'
|
'qwen'
|
'deepseek'
|
'xai'
|
'cohere'
|
'baidu'
|
'zhipu'
|
'moonshot'
|
'minimax'
|
'tencent'
|
'bytedance'
|
'midjourney'
|
'stability'
|
'unknown'
export
type
ApiInfo
=
{
vendor
:
ModelVendor
vendor_label
:
string
tokenizer
:
string
tokenizer_note
?:
string
license
:
string
license_kind
:
'proprietary'
|
'open'
|
'open-weight'
|
'unknown'
data_retention_days
:
number
training_opt_out
:
boolean
homepage
?:
string
}
const
VENDOR_LABELS
:
Record
<
ModelVendor
,
string
>
=
{
openai
:
'OpenAI'
,
anthropic
:
'Anthropic'
,
google
:
'Google'
,
meta
:
'Meta'
,
mistral
:
'Mistral AI'
,
qwen
:
'Alibaba (Qwen)'
,
deepseek
:
'DeepSeek'
,
xai
:
'xAI'
,
cohere
:
'Cohere'
,
baidu
:
'Baidu'
,
zhipu
:
'Zhipu AI'
,
moonshot
:
'Moonshot AI'
,
minimax
:
'MiniMax'
,
tencent
:
'Tencent'
,
bytedance
:
'ByteDance'
,
midjourney
:
'Midjourney'
,
stability
:
'Stability AI'
,
unknown
:
'Unknown'
,
}
function
detectVendor
(
name
:
string
):
ModelVendor
{
const
n
=
name
.
toLowerCase
()
if
(
/^gpt|^o
[
1-4
]
|davinci|babbage|whisper|tts|dall.
?
e|sora|^omni/
.
test
(
n
))
return
'openai'
if
(
/claude/
.
test
(
n
))
return
'anthropic'
if
(
/gemini|gemma|imagen|veo|palm/
.
test
(
n
))
return
'google'
if
(
/llama|^codellama/
.
test
(
n
))
return
'meta'
if
(
/mistral|mixtral|codestral|magistral|pixtral/
.
test
(
n
))
return
'mistral'
if
(
/qwen|qwq|qvq/
.
test
(
n
))
return
'qwen'
if
(
/deepseek/
.
test
(
n
))
return
'deepseek'
if
(
/grok/
.
test
(
n
))
return
'xai'
if
(
/command|cohere|aya/
.
test
(
n
))
return
'cohere'
if
(
/ernie|wenxin/
.
test
(
n
))
return
'baidu'
if
(
/glm|chatglm|cogview|cogvideo/
.
test
(
n
))
return
'zhipu'
if
(
/kimi|moonshot/
.
test
(
n
))
return
'moonshot'
if
(
/abab|minimax|hailuo/
.
test
(
n
))
return
'minimax'
if
(
/hunyuan/
.
test
(
n
))
return
'tencent'
if
(
/doubao|seed|jimeng/
.
test
(
n
))
return
'bytedance'
if
(
/midjourney|niji/
.
test
(
n
))
return
'midjourney'
if
(
/^sd-|stable
[
-_
]?
diffusion|sdxl/
.
test
(
n
))
return
'stability'
return
'unknown'
}
const
TOKENIZER_BY_VENDOR
:
Partial
<
Record
<
ModelVendor
,
string
>>
=
{
openai
:
'o200k_base'
,
anthropic
:
'Anthropic Claude tokenizer'
,
google
:
'SentencePiece (Gemini)'
,
meta
:
'Llama 3 tokenizer'
,
mistral
:
'Mistral tokenizer (BPE)'
,
qwen
:
'Qwen tokenizer (tiktoken-compat)'
,
deepseek
:
'DeepSeek tokenizer (BPE)'
,
xai
:
'Grok tokenizer (BPE)'
,
cohere
:
'Cohere tokenizer'
,
baidu
:
'Ernie tokenizer'
,
zhipu
:
'GLM tokenizer'
,
moonshot
:
'Kimi tokenizer'
,
minimax
:
'ABAB tokenizer'
,
tencent
:
'Hunyuan tokenizer'
,
bytedance
:
'Doubao tokenizer'
,
}
function
inferTokenizer
(
model
:
PricingModel
,
vendor
:
ModelVendor
):
{
tokenizer
:
string
note
?:
string
}
{
const
name
=
model
.
model_name
.
toLowerCase
()
if
(
vendor
===
'openai'
)
{
if
(
/gpt-3|davinci|babbage|whisper|tts/
.
test
(
name
))
{
return
{
tokenizer
:
'cl100k_base'
,
note
:
'Older GPT-3.5 family'
}
}
return
{
tokenizer
:
'o200k_base'
}
}
return
{
tokenizer
:
TOKENIZER_BY_VENDOR
[
vendor
]
??
'BPE (vendor-specific)'
}
}
const
LICENSE_BY_VENDOR
:
Record
<
ModelVendor
,
{
license
:
string
;
kind
:
ApiInfo
[
'license_kind'
]
}
>
=
{
openai
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
anthropic
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
google
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
meta
:
{
license
:
'Llama Community License'
,
kind
:
'open-weight'
},
mistral
:
{
license
:
'Apache 2.0 / Commercial'
,
kind
:
'open-weight'
},
qwen
:
{
license
:
'Tongyi Qianwen License'
,
kind
:
'open-weight'
},
deepseek
:
{
license
:
'DeepSeek License'
,
kind
:
'open-weight'
},
xai
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
cohere
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
baidu
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
zhipu
:
{
license
:
'GLM-4 License'
,
kind
:
'open-weight'
},
moonshot
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
minimax
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
tencent
:
{
license
:
'Hunyuan License'
,
kind
:
'open-weight'
},
bytedance
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
midjourney
:
{
license
:
'Proprietary (commercial)'
,
kind
:
'proprietary'
},
stability
:
{
license
:
'Stability AI Community License'
,
kind
:
'open-weight'
},
unknown
:
{
license
:
'Provider-specific'
,
kind
:
'unknown'
},
}
const
HOMEPAGE_BY_VENDOR
:
Partial
<
Record
<
ModelVendor
,
string
>>
=
{
openai
:
'https://platform.openai.com/docs/models'
,
anthropic
:
'https://docs.anthropic.com/claude/docs/models-overview'
,
google
:
'https://ai.google.dev/models'
,
meta
:
'https://llama.meta.com/'
,
mistral
:
'https://docs.mistral.ai/getting-started/models/'
,
qwen
:
'https://qwenlm.github.io/'
,
deepseek
:
'https://api-docs.deepseek.com/'
,
xai
:
'https://x.ai/api'
,
cohere
:
'https://docs.cohere.com/docs/models'
,
baidu
:
'https://cloud.baidu.com/product/wenxinworkshop'
,
zhipu
:
'https://open.bigmodel.cn/dev/api'
,
moonshot
:
'https://platform.moonshot.cn/docs'
,
minimax
:
'https://platform.minimaxi.com/document/notice'
,
tencent
:
'https://cloud.tencent.com/document/product/1729'
,
bytedance
:
'https://www.volcengine.com/docs/82379'
,
midjourney
:
'https://www.midjourney.com/'
,
stability
:
'https://platform.stability.ai/'
,
}
/**
* Build vendor / tokenizer / license / privacy metadata for the model.
* Returns deterministic values keyed off the model name so each render is
* stable.
*/
export
function
inferApiInfo
(
model
:
PricingModel
):
ApiInfo
{
const
vendor
=
detectVendor
(
model
.
model_name
||
''
)
const
tk
=
inferTokenizer
(
model
,
vendor
)
const
license
=
LICENSE_BY_VENDOR
[
vendor
]
const
rand
=
seededRandom
(
hashStringToSeed
(
`
${
model
.
model_name
}
:api`
))
const
retention
=
vendor
===
'openai'
?
30
:
Math
.
round
(
rand
()
*
90
)
return
{
vendor
,
vendor_label
:
VENDOR_LABELS
[
vendor
],
tokenizer
:
tk
.
tokenizer
,
tokenizer_note
:
tk
.
note
,
license
:
license
.
license
,
license_kind
:
license
.
kind
,
data_retention_days
:
retention
,
training_opt_out
:
true
,
homepage
:
HOMEPAGE_BY_VENDOR
[
vendor
],
}
}
web/default/src/features/pricing/types.ts
View file @
a95655a2
...
...
@@ -57,10 +57,8 @@ export type PricingModel = {
/** Pricing version returned by backend, useful for cache busting */
pricing_version
?:
string
/**
* Optional model metadata fields. These are not yet returned by the backend
* and are populated client-side from {@link inferModelMetadata}.
* When the backend ships these fields, the inference layer becomes a
* fallback rather than the source of truth.
* 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
...
...
web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx
View file @
a95655a2
...
...
@@ -16,12 +16,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
{
useEffect
,
useState
}
from
'react'
import
{
type
FormEvent
,
useEffect
,
useState
}
from
'react'
import
{
useForm
}
from
'react-hook-form'
import
{
zodResolver
}
from
'@hookform/resolvers/zod'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
getCurrencyDisplay
,
getCurrencyLabel
}
from
'@/lib/currency'
import
{
formatQuota
,
parseQuotaFromDollars
}
from
'@/lib/format'
import
{
addTimeToDate
}
from
'@/lib/time'
import
{
Button
}
from
'@/components/ui/button'
import
{
...
...
@@ -135,6 +136,18 @@ export function RedemptionsMutateDrawer({
}
}
const
handleSubmit
=
(
event
:
FormEvent
<
HTMLFormElement
>
)
=>
{
if
(
!
isUpdate
)
{
const
name
=
form
.
getValues
(
'name'
)
if
(
!
name
?.
trim
())
{
const
quota
=
parseQuotaFromDollars
(
form
.
getValues
(
'quota_dollars'
))
form
.
setValue
(
'name'
,
formatQuota
(
quota
),
{
shouldValidate
:
true
})
}
}
void
form
.
handleSubmit
(
onSubmit
)(
event
)
}
const
handleSetExpiry
=
(
months
:
number
,
days
:
number
,
hours
:
number
)
=>
{
const
newDate
=
addTimeToDate
(
months
,
days
,
hours
)
form
.
setValue
(
'expired_time'
,
newDate
)
...
...
@@ -177,7 +190,7 @@ export function RedemptionsMutateDrawer({
<
Form
{
...
form
}
>
<
form
id=
'redemption-form'
onSubmit=
{
form
.
handleSubmit
(
onSubmit
)
}
onSubmit=
{
handleSubmit
}
className=
{
sideDrawerFormClassName
()
}
>
<
SideDrawerSection
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment