/*
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 .
For commercial licensing, please contact support@quantumnous.com
*/
import type { PricingModel } from '../types'
import {
hashStringToSeed,
randomInRange,
randomIntInRange,
seededRandom,
} from './seed'
// ----------------------------------------------------------------------------
// Mock model statistics
// ----------------------------------------------------------------------------
//
// The backend has not yet implemented latency / uptime / app-ranking data.
// These helpers generate plausible, deterministic mock values seeded from
// the model name (and optionally the group name) so that:
// - Every render of the same model shows the same numbers
// - Different models / different groups render visibly distinct values
//
// When the backend ships real metrics, callers should switch to the
// real API and these helpers can be deleted. The shape of the returned
// data is designed to mirror what we expect the real endpoints to return.
export type GroupPerformance = {
group: string
ttft_p50_ms: number
ttft_p95_ms: number
ttft_p99_ms: number
throughput_tps: number
uptime_30d_pct: number
/** Number of monitored requests in the last 24h (display only). */
request_volume_24h: number
}
export type LatencyTimePoint = {
timestamp: string
group: string
ttft_ms: number
}
export type UptimeDayPoint = {
date: string
uptime_pct: number
incidents: number
outage_minutes: number
}
export type AppRanking = {
rank: number
name: string
description: string
category: string
growth_pct: number
monthly_tokens: number
url?: string
initial: string
}
const APP_TEMPLATES: Array<
Omit
> = [
{
name: 'Cline',
description: 'Autonomous coding agent inside the IDE',
category: 'Coding',
url: 'https://cline.bot',
},
{
name: 'Roo Code',
description: 'AI agent for VS Code with multi-step planning',
category: 'Coding',
url: 'https://roocode.com',
},
{
name: 'Open WebUI',
description: 'Self-hosted ChatGPT-like web interface',
category: 'Chat',
url: 'https://openwebui.com',
},
{
name: 'LibreChat',
description: 'Open-source chat platform with multi-model support',
category: 'Chat',
url: 'https://librechat.ai',
},
{
name: 'Lobe Chat',
description: 'Modern open-source chat UI with plugins',
category: 'Chat',
url: 'https://lobehub.com',
},
{
name: 'NextChat',
description: 'Cross-platform private ChatGPT client',
category: 'Chat',
url: 'https://nextchat.dev',
},
{
name: 'Continue',
description: 'Open-source AI code assistant for editors',
category: 'Coding',
url: 'https://continue.dev',
},
{
name: 'Aider',
description: 'Pair-programming agent in your terminal',
category: 'Coding',
url: 'https://aider.chat',
},
{
name: 'Dify',
description: 'LLM application development platform',
category: 'Platform',
url: 'https://dify.ai',
},
{
name: 'FastGPT',
description: 'Knowledge base orchestration and chat platform',
category: 'Platform',
url: 'https://fastgpt.in',
},
{
name: 'Flowise',
description: 'Low-code LLM workflow builder',
category: 'Platform',
url: 'https://flowiseai.com',
},
{
name: 'OpenInterpreter',
description: 'Natural-language code execution agent',
category: 'Coding',
url: 'https://openinterpreter.com',
},
{
name: 'Devika',
description: 'Open-source AI software engineer',
category: 'Coding',
url: 'https://github.com/stitionai/devika',
},
{
name: 'Cherry Studio',
description: 'Multi-model desktop chat client',
category: 'Chat',
url: 'https://cherry-ai.com',
},
{
name: 'AnythingLLM',
description: 'Workspaces around your private documents',
category: 'Platform',
url: 'https://anythingllm.com',
},
{
name: 'OpenHands',
description: 'Coding agent with browser-and-code tools',
category: 'Coding',
url: 'https://docs.all-hands.dev',
},
{
name: 'Cursor',
description: 'AI-native code editor',
category: 'Coding',
url: 'https://cursor.com',
},
{
name: 'Zed',
description: 'Multiplayer code editor with AI',
category: 'Coding',
url: 'https://zed.dev',
},
{
name: 'Notion AI',
description: 'Documents and writing assistant',
category: 'Productivity',
url: 'https://notion.so',
},
{
name: 'Raycast AI',
description: 'AI on your macOS launcher',
category: 'Productivity',
url: 'https://raycast.com',
},
{
name: 'Obsidian Smart Connections',
description: 'Connect notes with semantic search',
category: 'Productivity',
},
{
name: 'Bolt.new',
description: 'Prompt-to-app full-stack builder',
category: 'Coding',
url: 'https://bolt.new',
},
{
name: 'Pieces',
description: 'AI workflow companion for developers',
category: 'Productivity',
url: 'https://pieces.app',
},
{
name: 'AmazingAI',
description: 'Personal AI knowledge assistant',
category: 'Productivity',
},
{
name: 'TypingMind',
description: 'Better UI for ChatGPT and Claude',
category: 'Chat',
url: 'https://typingmind.com',
},
]
const PROFILE_BY_NAME = (name: string) => {
const n = name.toLowerCase()
if (/embed|rerank/.test(n)) return 'embedding'
if (/image|sora|veo|kling|pika|jimeng|dalle|imagen/.test(n)) return 'image'
if (/whisper|tts|voice|audio/.test(n)) return 'audio'
if (/o1|o3|o4|reasoning|thinking|deepseek-r/.test(n)) return 'reasoning'
if (/flash|haiku|mini|small|nano|fast/.test(n)) return 'fast'
if (/gpt-5|opus|ultra|405|70b/.test(n)) return 'large'
return 'standard'
}
type ProfileSpec = {
ttftRange: [number, number]
throughputRange: [number, number]
uptimeRange: [number, number]
}
const PROFILE_SPECS: Record = {
embedding: {
ttftRange: [40, 120],
throughputRange: [0, 0],
uptimeRange: [99.9, 99.99],
},
image: {
ttftRange: [2_500, 12_000],
throughputRange: [0, 0],
uptimeRange: [98.5, 99.8],
},
audio: {
ttftRange: [180, 600],
throughputRange: [0, 0],
uptimeRange: [99.5, 99.95],
},
reasoning: {
ttftRange: [1_800, 5_500],
throughputRange: [25, 70],
uptimeRange: [99.4, 99.95],
},
fast: {
ttftRange: [180, 480],
throughputRange: [110, 240],
uptimeRange: [99.7, 99.99],
},
large: {
ttftRange: [600, 1_400],
throughputRange: [55, 95],
uptimeRange: [99.5, 99.95],
},
standard: {
ttftRange: [400, 900],
throughputRange: [70, 140],
uptimeRange: [99.6, 99.97],
},
}
function rangeFromSeed(
rand: () => number,
[min, max]: [number, number]
): number {
return randomInRange(rand, min, max)
}
function applyGroupFactor(value: number, factor: number): number {
return value * factor
}
function groupFactor(
group: string,
baseSeed: number
): { ttft: number; throughput: number; uptime: number } {
const rand = seededRandom(baseSeed ^ hashStringToSeed(group || 'default'))
return {
ttft: 0.85 + rand() * 0.55,
throughput: 0.85 + rand() * 0.4,
uptime: 0.997 + rand() * 0.003,
}
}
/**
* Build per-group performance stats for a model. Always returns at least one
* row for each enabled group, sorted alphabetically.
*/
export function buildGroupPerformance(model: PricingModel): GroupPerformance[] {
const groups = (model.enable_groups ?? []).filter((g) => g && g !== 'auto')
const targets = groups.length > 0 ? groups : ['default']
const profile = PROFILE_BY_NAME(model.model_name)
const spec = PROFILE_SPECS[profile]
const baseSeed = hashStringToSeed(model.model_name)
return targets
.slice()
.sort((a, b) => a.localeCompare(b))
.map((group) => {
const rand = seededRandom(baseSeed ^ hashStringToSeed(group))
const factor = groupFactor(group, baseSeed)
const ttftP50 = applyGroupFactor(
rangeFromSeed(rand, spec.ttftRange),
factor.ttft
)
const throughput = applyGroupFactor(
rangeFromSeed(rand, spec.throughputRange),
factor.throughput
)
const uptimePct = Math.min(
99.99,
rangeFromSeed(rand, spec.uptimeRange) * factor.uptime
)
const requestVolume = randomIntInRange(rand, 18_000, 480_000)
return {
group,
ttft_p50_ms: Math.round(ttftP50),
ttft_p95_ms: Math.round(ttftP50 * (1.6 + rand() * 0.4)),
ttft_p99_ms: Math.round(ttftP50 * (2.4 + rand() * 0.6)),
throughput_tps: throughput === 0 ? 0 : Math.round(throughput * 10) / 10,
uptime_30d_pct: Math.round(uptimePct * 100) / 100,
request_volume_24h: requestVolume,
}
})
}
/**
* Build a 24-hour latency series for each group. Returns one point per hour
* (24 buckets), oldest first.
*/
export function buildLatencyTimeSeries(
model: PricingModel
): LatencyTimePoint[] {
const performances = buildGroupPerformance(model)
if (performances.length === 0) return []
const now = new Date()
now.setMinutes(0, 0, 0)
const baseSeed = hashStringToSeed(`${model.model_name}:lat`)
const points: LatencyTimePoint[] = []
for (const perf of performances) {
const rand = seededRandom(baseSeed ^ hashStringToSeed(perf.group))
for (let i = 23; i >= 0; i--) {
const ts = new Date(now.getTime() - i * 3_600_000)
const noise = 0.7 + rand() * 0.7
const trend = 0.85 + Math.sin(i / 3) * 0.1
const value = Math.max(50, Math.round(perf.ttft_p50_ms * noise * trend))
points.push({
timestamp: ts.toISOString(),
group: perf.group,
ttft_ms: value,
})
}
}
return points
}
/**
* Build a 30-day uptime series. Returns one point per day, oldest first.
*
* If `group` is provided the series is anchored on that group's mean uptime,
* otherwise it uses the per-model average. Either way the seed is derived
* deterministically so re-renders are stable.
*/
export function buildUptimeSeries(
model: PricingModel,
group?: string
): UptimeDayPoint[] {
const performances = buildGroupPerformance(model)
if (performances.length === 0) return []
const target = group ? performances.find((p) => p.group === group) : null
const baseUptime = target
? target.uptime_30d_pct
: performances.reduce((s, p) => s + p.uptime_30d_pct, 0) /
performances.length
const baseSeed = hashStringToSeed(`${model.model_name}:up:${group ?? '_all'}`)
const rand = seededRandom(baseSeed)
const today = new Date()
today.setHours(0, 0, 0, 0)
const points: UptimeDayPoint[] = []
for (let i = 29; i >= 0; i--) {
const date = new Date(today.getTime() - i * 86_400_000)
const isoDate = date.toISOString().slice(0, 10)
const incidentChance = rand()
const incidents = incidentChance > 0.92 ? 1 : 0
const outageMinutes = incidents > 0 ? Math.round(rand() * 30 + 5) : 0
const downtimePct = (outageMinutes / 1_440) * 100
const dayUptime = Math.max(85, Math.min(100, baseUptime - downtimePct))
points.push({
date: isoDate,
uptime_pct: Math.round(dayUptime * 100) / 100,
incidents,
outage_minutes: outageMinutes,
})
}
return points
}
/**
* Build a deterministic top-apps ranking for the model. The first three apps
* always come from the same template list; the rest is shuffled by the seed
* so different models surface different long tails.
*/
export function buildAppRankings(
model: PricingModel,
count = 12
): AppRanking[] {
const baseSeed = hashStringToSeed(`${model.model_name}:apps`)
const rand = seededRandom(baseSeed)
const candidates = [...APP_TEMPLATES]
// Fisher–Yates shuffle.
for (let i = candidates.length - 1; i > 0; i--) {
const j = Math.floor(rand() * (i + 1))
;[candidates[i], candidates[j]] = [candidates[j], candidates[i]]
}
const top = candidates.slice(0, count)
const baseTokens = randomInRange(rand, 90_000_000, 320_000_000)
return top.map((app, idx) => {
const decay = Math.pow(0.78, idx)
const monthlyTokens = Math.round(baseTokens * decay * (0.85 + rand() * 0.3))
const growthPctRaw = randomInRange(rand, -28, 84)
const growthPct = Math.round(growthPctRaw * 10) / 10
return {
rank: idx + 1,
name: app.name,
description: app.description,
category: app.category,
url: app.url,
growth_pct: growthPct,
monthly_tokens: monthlyTokens,
initial: app.name.charAt(0).toUpperCase(),
}
})
}
/** Aggregate uptime over the most recent 30 days. */
export function aggregateUptime(points: UptimeDayPoint[]): {
uptime_pct: number
incidents: number
outage_minutes: number
} {
if (points.length === 0) {
return { uptime_pct: 0, incidents: 0, outage_minutes: 0 }
}
const incidents = points.reduce((s, p) => s + p.incidents, 0)
const outageMinutes = points.reduce((s, p) => s + p.outage_minutes, 0)
const totalMinutes = points.length * 1_440
const uptimePct = ((totalMinutes - outageMinutes) / totalMinutes) * 100
return {
incidents,
outage_minutes: outageMinutes,
uptime_pct: Math.round(uptimePct * 1000) / 1000,
}
}
/** Compact integer formatter for token counts in apps tab. */
export function formatTokenVolume(n: number): string {
if (!Number.isFinite(n) || n <= 0) return '0'
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
return n.toString()
}
// ---------------------------------------------------------------------------
// Mock supported-parameters & rate-limits & misc API metadata
// ---------------------------------------------------------------------------
export type SupportedParameter = {
name: string
type:
| 'number'
| 'integer'
| 'boolean'
| 'string'
| 'object'
| 'array'
| 'enum'
defaultValue?: string | number | boolean
range?: string
enumValues?: string[]
descriptionKey: string
required?: boolean
}
const COMMON_CHAT_PARAMS: SupportedParameter[] = [
{
name: 'temperature',
type: 'number',
defaultValue: 1,
range: '0 ~ 2',
descriptionKey: 'Sampling temperature; lower is more deterministic',
},
{
name: 'top_p',
type: 'number',
defaultValue: 1,
range: '0 ~ 1',
descriptionKey: 'Nucleus sampling probability mass',
},
{
name: 'max_tokens',
type: 'integer',
range: '>= 1',
descriptionKey: 'Maximum number of tokens in the response',
},
{
name: 'frequency_penalty',
type: 'number',
defaultValue: 0,
range: '-2 ~ 2',
descriptionKey: 'Penalises repetition of frequent tokens',
},
{
name: 'presence_penalty',
type: 'number',
defaultValue: 0,
range: '-2 ~ 2',
descriptionKey: 'Encourages introducing new topics',
},
{
name: 'stop',
type: 'array',
descriptionKey: 'Up to 4 strings that stop generation',
},
{
name: 'seed',
type: 'integer',
descriptionKey: 'Deterministic sampling seed (best-effort)',
},
{
name: 'n',
type: 'integer',
defaultValue: 1,
range: '>= 1',
descriptionKey: 'Number of completions to generate',
},
{
name: 'stream',
type: 'boolean',
defaultValue: false,
descriptionKey: 'Stream tokens via Server-Sent Events',
},
{
name: 'response_format',
type: 'object',
descriptionKey: 'Force JSON object or schema-conforming output',
},
{
name: 'tools',
type: 'array',
descriptionKey: 'Tool / function declarations the model may call',
},
{
name: 'tool_choice',
type: 'string',
enumValues: ['auto', 'none', 'required'],
descriptionKey: 'Tool-choice policy or specific tool name',
},
{
name: 'logprobs',
type: 'boolean',
defaultValue: false,
descriptionKey: 'Return per-token log probabilities',
},
{
name: 'top_logprobs',
type: 'integer',
range: '0 ~ 20',
descriptionKey: 'Number of top log probabilities returned per token',
},
{
name: 'logit_bias',
type: 'object',
descriptionKey: 'Per-token logit bias map',
},
{
name: 'user',
type: 'string',
descriptionKey: 'End-user identifier for abuse monitoring',
},
]
const REASONING_PARAMS: SupportedParameter[] = [
{
name: 'reasoning_effort',
type: 'enum',
enumValues: ['low', 'medium', 'high'],
defaultValue: 'medium',
descriptionKey: 'Controls how much the model thinks before answering',
},
{
name: 'max_completion_tokens',
type: 'integer',
range: '>= 1',
descriptionKey: 'Maximum tokens including hidden reasoning tokens',
},
{
name: 'stop',
type: 'array',
descriptionKey: 'Up to 4 strings that stop generation',
},
{
name: 'seed',
type: 'integer',
descriptionKey: 'Deterministic sampling seed (best-effort)',
},
{
name: 'stream',
type: 'boolean',
defaultValue: false,
descriptionKey: 'Stream tokens via Server-Sent Events',
},
{
name: 'response_format',
type: 'object',
descriptionKey: 'Force JSON object or schema-conforming output',
},
{
name: 'tools',
type: 'array',
descriptionKey: 'Tool / function declarations the model may call',
},
{
name: 'tool_choice',
type: 'string',
enumValues: ['auto', 'none', 'required'],
descriptionKey: 'Tool-choice policy or specific tool name',
},
{
name: 'user',
type: 'string',
descriptionKey: 'End-user identifier for abuse monitoring',
},
]
const EMBEDDING_PARAMS: SupportedParameter[] = [
{
name: 'input',
type: 'string',
required: true,
descriptionKey: 'Text or array of texts to embed',
},
{
name: 'dimensions',
type: 'integer',
range: '>= 1',
descriptionKey: 'Truncate embeddings to this many dimensions',
},
{
name: 'encoding_format',
type: 'enum',
enumValues: ['float', 'base64'],
defaultValue: 'float',
descriptionKey: 'Wire encoding for the embedding vectors',
},
{
name: 'user',
type: 'string',
descriptionKey: 'End-user identifier for abuse monitoring',
},
]
const IMAGE_PARAMS: SupportedParameter[] = [
{
name: 'prompt',
type: 'string',
required: true,
descriptionKey: 'Text description of the desired image',
},
{
name: 'size',
type: 'enum',
enumValues: ['256x256', '512x512', '1024x1024', '1024x1792', '1792x1024'],
defaultValue: '1024x1024',
descriptionKey: 'Output image size',
},
{
name: 'quality',
type: 'enum',
enumValues: ['standard', 'hd'],
defaultValue: 'standard',
descriptionKey: 'Generation quality preset',
},
{
name: 'style',
type: 'enum',
enumValues: ['vivid', 'natural'],
defaultValue: 'vivid',
descriptionKey: 'Aesthetic style',
},
{
name: 'n',
type: 'integer',
defaultValue: 1,
range: '1 ~ 10',
descriptionKey: 'Number of images to generate',
},
{
name: 'response_format',
type: 'enum',
enumValues: ['url', 'b64_json'],
defaultValue: 'url',
descriptionKey: 'How to deliver the resulting image',
},
]
const VIDEO_PARAMS: SupportedParameter[] = [
{
name: 'prompt',
type: 'string',
required: true,
descriptionKey: 'Text description of the desired video',
},
{
name: 'duration',
type: 'integer',
range: '1 ~ 60',
descriptionKey: 'Video length in seconds',
},
{
name: 'aspect_ratio',
type: 'enum',
enumValues: ['16:9', '9:16', '1:1'],
defaultValue: '16:9',
descriptionKey: 'Output aspect ratio',
},
{
name: 'fps',
type: 'integer',
range: '8 ~ 60',
defaultValue: 24,
descriptionKey: 'Frames per second',
},
]
type ApiCategory = 'reasoning' | 'embedding' | 'image' | 'video' | 'chat'
/**
* Refine the broad PROFILE_BY_NAME bucket into an API-shape category. The
* `image` bucket from `PROFILE_BY_NAME` lumps still-image and video models
* together (because their performance profiles overlap); for the API tab we
* need to distinguish them so the request-parameter table is accurate.
*/
function apiCategoryOf(model: PricingModel): ApiCategory {
const profile = PROFILE_BY_NAME(model.model_name)
if (profile === 'embedding' || profile === 'reasoning') return profile
if (profile === 'image') {
return /sora|veo|kling|pika|video|wan-|hunyuanvideo/i.test(model.model_name)
? 'video'
: 'image'
}
return 'chat'
}
/**
* Build the list of request parameters that the model accepts. The list is
* shaped per-modality so reasoning, embedding, image, video and chat models
* each show their relevant parameter set.
*/
export function buildSupportedParameters(
model: PricingModel
): SupportedParameter[] {
const cat = apiCategoryOf(model)
if (cat === 'reasoning') return REASONING_PARAMS
if (cat === 'embedding') return EMBEDDING_PARAMS
if (cat === 'image') return IMAGE_PARAMS
if (cat === 'video') return VIDEO_PARAMS
return COMMON_CHAT_PARAMS
}
export type RateLimit = {
group: string
rpm: number
tpm: number
rpd: number
}
/** Build per-group RPM / TPM / RPD limits for the model. */
export function buildRateLimits(model: PricingModel): RateLimit[] {
const groups = (model.enable_groups ?? []).filter((g) => g && g !== 'auto')
const targets = groups.length > 0 ? groups : ['default']
const cat = apiCategoryOf(model)
const baseSeed = hashStringToSeed(`${model.model_name}:rl`)
const isHeavy = cat === 'image' || cat === 'video'
const isLight = cat === 'embedding'
const baseRpm = isHeavy ? 60 : isLight ? 5_000 : 500
const baseTpm = isHeavy ? 0 : isLight ? 1_000_000 : 200_000
const baseRpd = isHeavy ? 1_000 : isLight ? 100_000 : 10_000
return targets
.slice()
.sort((a, b) => a.localeCompare(b))
.map((group) => {
const rand = seededRandom(baseSeed ^ hashStringToSeed(group))
const tier = 0.6 + rand() * 1.4
return {
group,
rpm: Math.round((baseRpm * tier) / 10) * 10,
tpm: baseTpm === 0 ? 0 : Math.round((baseTpm * tier) / 1_000) * 1_000,
rpd: Math.round((baseRpd * tier) / 100) * 100,
}
})
}
/** Format an integer rate-limit value compactly. */
export function formatRateLimit(value: number): string {
if (value <= 0) return '—'
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
if (value >= 1_000)
return `${(value / 1_000).toFixed(value >= 10_000 ? 0 : 1)}K`
return value.toLocaleString()
}