@@ -181,8 +160,7 @@ function OverviewMetric(props: {
{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)}
/>
)
}
+function CatalogPillList(props: { items: string[] }) {
+ return (
+
+ {props.items.map((item) => (
+
+ {item}
+
+ ))}
+
+ )
+}
+
+function CatalogTextValue(props: { children: React.ReactNode }) {
+ return (
+
+ {props.children}
+
+ )
+}
+
+function CatalogInfoCell(props: { label: string; children: React.ReactNode }) {
+ return (
+
+
+ {props.label}
+
+ {props.children}
+
+ )
+}
+
+function ModalityLabels(props: { items: string[] }) {
+ const { t } = useTranslation()
+ if (props.items.length === 0) return null
+
+ return (
+
+ {props.items.map((item) => (
+
+ {t(MODALITY_LABEL_KEYS[item] ?? item)}
+
+ ))}
+
+ )
+}
+
+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: (
+
+
+ {inputModalities.length > 0 && outputModalities.length > 0 && (
+ →
+ )}
+
+
+ ),
+ })
+ }
+
+ 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 (
+
+ {stats.map((stat) => {
+ const Icon = stat.icon
+ return (
+
+
+
+ {stat.label}
+
+
+ {stat.value}
+
+ {stat.hint && (
+
+ {stat.hint}
+
+ )}
+
+ )
+ })}
+
+ )
+}
+
+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 (
+
+
+ {t('Capabilities')} / {t('Supported modalities')}
+
+
+ {capabilities.length > 0 ? (
+
+ t(
+ CAPABILITY_LABEL_KEYS[capability as ModelCapability] ??
+ capability
+ )
+ )}
+ />
+ ) : (
+
+ )}
+ {(inputModalities.length > 0 || outputModalities.length > 0) && (
+
+ {inputModalities.length > 0 && (
+
+
+ {t('Input')}
+
+
+
+
+
+ )}
+ {outputModalities.length > 0 && (
+
+
+ {t('Output')}
+
+
+
+
+
+ )}
+
+ )}
+
+
+ )
+}
+
+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(
+
+ {model.vendor_name}
+
+ )
+ }
+
+ cells.push(
+
+
+ {model.quota_type === QUOTA_TYPE_VALUES.TOKEN
+ ? t('Token-based')
+ : t('Per Request')}
+
+
+ )
+
+ if (groups.length > 0) {
+ cells.push(
+
+
+
+ )
+ }
+
+ if (endpoints.length > 0) {
+ cells.push(
+
+
+
+ )
+ }
+
+ if (tags.length > 0) {
+ cells.push(
+
+
+
+ )
+ }
+
+ if (model.parameter_count) {
+ cells.push(
+
+ {model.parameter_count}
+
+ )
+ }
+
+ if (cells.length === 0) return null
+
+ return (
+
+ {t('Model')}
+
+ {cells}
+
+
+ )
+}
+
+function ModelBackendDetailsSection(props: { model: PricingModel }) {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
// ----------------------------------------------------------------------------
// 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}
)}
- {tags.length > 0 && (
-
- {tags.map((tag) => (
-
- {tag}
-
- ))}
-
- )}
)
}
@@ -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) {
/>
-
-
-
-
-
+
diff --git a/web/default/src/features/pricing/components/model-perf-badge.tsx b/web/default/src/features/pricing/components/model-perf-badge.tsx
index 25c5c5ca..75fa46bf 100644
--- a/web/default/src/features/pricing/components/model-perf-badge.tsx
+++ b/web/default/src/features/pricing/components/model-perf-badge.tsx
@@ -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 (
.
-
-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 = {
- 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 = {
- text: 'text',
- image: 'image',
- audio: 'audio',
- video: 'video',
- file: 'file',
- document: 'file',
- pdf: 'file',
-}
-
-function pickFromBuckets(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()
-
- 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()
-
- 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()
-
- 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[] {
- 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 = {
- 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> = {
- 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> = {
- 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],
- }
-}
diff --git a/web/default/src/features/pricing/types.ts b/web/default/src/features/pricing/types.ts
index 9ee69cd7..8a0e244d 100644
--- a/web/default/src/features/pricing/types.ts
+++ b/web/default/src/features/pricing/types.ts
@@ -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
diff --git a/web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx b/web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx
index 0c6d7ac2..d26b28ae 100644
--- a/web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx
+++ b/web/default/src/features/redemption-codes/components/redemptions-mutate-drawer.tsx
@@ -16,12 +16,13 @@ along with this program. If not, see .
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) => {
+ 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({