| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import i18next from 'i18next'; |
| import { Modal, Tag, Typography, Avatar } from '@douyinfe/semi-ui'; |
| import { copy, showSuccess } from './utils'; |
| import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile'; |
| import { visit } from 'unist-util-visit'; |
| import * as LobeIcons from '@lobehub/icons'; |
| import { |
| OpenAI, |
| Claude, |
| Gemini, |
| Moonshot, |
| Zhipu, |
| Qwen, |
| DeepSeek, |
| Minimax, |
| Wenxin, |
| Spark, |
| Midjourney, |
| Hunyuan, |
| Cohere, |
| Cloudflare, |
| Ai360, |
| Yi, |
| Jina, |
| Mistral, |
| XAI, |
| Ollama, |
| Doubao, |
| Suno, |
| Xinference, |
| OpenRouter, |
| Dify, |
| Coze, |
| SiliconCloud, |
| FastGPT, |
| Kling, |
| Jimeng, |
| Perplexity, |
| Replicate, |
| } from '@lobehub/icons'; |
|
|
| import { |
| LayoutDashboard, |
| TerminalSquare, |
| MessageSquare, |
| Key, |
| BarChart3, |
| Image as ImageIcon, |
| CheckSquare, |
| CreditCard, |
| Layers, |
| Gift, |
| User, |
| Settings, |
| CircleUser, |
| Package, |
| Server, |
| CalendarClock, |
| } from 'lucide-react'; |
| import { |
| SiAtlassian, |
| SiAuth0, |
| SiAuthentik, |
| SiBitbucket, |
| SiDiscord, |
| SiDropbox, |
| SiFacebook, |
| SiGitea, |
| SiGithub, |
| SiGitlab, |
| SiGoogle, |
| SiKeycloak, |
| SiLinkedin, |
| SiNextcloud, |
| SiNotion, |
| SiOkta, |
| SiOpenid, |
| SiReddit, |
| SiSlack, |
| SiTelegram, |
| SiTwitch, |
| SiWechat, |
| SiX, |
| } from 'react-icons/si'; |
|
|
| |
| export function getLucideIcon(key, selected = false) { |
| const size = 16; |
| const strokeWidth = 2; |
| const SELECTED_COLOR = 'var(--semi-color-primary)'; |
| const iconColor = selected ? SELECTED_COLOR : 'currentColor'; |
| const commonProps = { |
| size, |
| strokeWidth, |
| className: `transition-colors duration-200 ${selected ? 'transition-transform duration-200 scale-105' : ''}`, |
| }; |
|
|
| |
| switch (key) { |
| case 'detail': |
| return <LayoutDashboard {...commonProps} color={iconColor} />; |
| case 'playground': |
| return <TerminalSquare {...commonProps} color={iconColor} />; |
| case 'chat': |
| return <MessageSquare {...commonProps} color={iconColor} />; |
| case 'token': |
| return <Key {...commonProps} color={iconColor} />; |
| case 'log': |
| return <BarChart3 {...commonProps} color={iconColor} />; |
| case 'midjourney': |
| return <ImageIcon {...commonProps} color={iconColor} />; |
| case 'task': |
| return <CheckSquare {...commonProps} color={iconColor} />; |
| case 'topup': |
| return <CreditCard {...commonProps} color={iconColor} />; |
| case 'channel': |
| return <Layers {...commonProps} color={iconColor} />; |
| case 'redemption': |
| return <Gift {...commonProps} color={iconColor} />; |
| case 'user': |
| case 'personal': |
| return <User {...commonProps} color={iconColor} />; |
| case 'models': |
| return <Package {...commonProps} color={iconColor} />; |
| case 'deployment': |
| return <Server {...commonProps} color={iconColor} />; |
| case 'subscription': |
| return <CalendarClock {...commonProps} color={iconColor} />; |
| case 'setting': |
| return <Settings {...commonProps} color={iconColor} />; |
| default: |
| return <CircleUser {...commonProps} color={iconColor} />; |
| } |
| } |
|
|
| |
| export const getModelCategories = (() => { |
| let categoriesCache = null; |
| let lastLocale = null; |
|
|
| return (t) => { |
| const currentLocale = i18next.language; |
| if (categoriesCache && lastLocale === currentLocale) { |
| return categoriesCache; |
| } |
|
|
| categoriesCache = { |
| all: { |
| label: t('全部模型'), |
| icon: null, |
| filter: () => true, |
| }, |
| openai: { |
| label: 'OpenAI', |
| icon: <OpenAI />, |
| filter: (model) => |
| model.model_name.toLowerCase().includes('gpt') || |
| model.model_name.toLowerCase().includes('dall-e') || |
| model.model_name.toLowerCase().includes('whisper') || |
| model.model_name.toLowerCase().includes('tts-1') || |
| model.model_name.toLowerCase().includes('text-embedding-3') || |
| model.model_name.toLowerCase().includes('text-moderation') || |
| model.model_name.toLowerCase().includes('babbage') || |
| model.model_name.toLowerCase().includes('davinci') || |
| model.model_name.toLowerCase().includes('curie') || |
| model.model_name.toLowerCase().includes('ada') || |
| model.model_name.toLowerCase().includes('o1') || |
| model.model_name.toLowerCase().includes('o3') || |
| model.model_name.toLowerCase().includes('o4'), |
| }, |
| anthropic: { |
| label: 'Anthropic', |
| icon: <Claude.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('claude'), |
| }, |
| gemini: { |
| label: 'Gemini', |
| icon: <Gemini.Color />, |
| filter: (model) => |
| model.model_name.toLowerCase().includes('gemini') || |
| model.model_name.toLowerCase().includes('gemma') || |
| model.model_name.toLowerCase().includes('learnlm') || |
| model.model_name.toLowerCase().startsWith('embedding-') || |
| model.model_name.toLowerCase().includes('text-embedding-004') || |
| model.model_name.toLowerCase().includes('imagen-4') || |
| model.model_name.toLowerCase().includes('veo-') || |
| model.model_name.toLowerCase().includes('aqa'), |
| }, |
| moonshot: { |
| label: 'Moonshot', |
| icon: <Moonshot />, |
| filter: (model) => |
| model.model_name.toLowerCase().includes('moonshot') || |
| model.model_name.toLowerCase().includes('kimi'), |
| }, |
| zhipu: { |
| label: t('智谱'), |
| icon: <Zhipu.Color />, |
| filter: (model) => |
| model.model_name.toLowerCase().includes('chatglm') || |
| model.model_name.toLowerCase().includes('glm-') || |
| model.model_name.toLowerCase().includes('cogview') || |
| model.model_name.toLowerCase().includes('cogvideo'), |
| }, |
| qwen: { |
| label: t('通义千问'), |
| icon: <Qwen.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('qwen'), |
| }, |
| deepseek: { |
| label: 'DeepSeek', |
| icon: <DeepSeek.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('deepseek'), |
| }, |
| minimax: { |
| label: 'MiniMax', |
| icon: <Minimax.Color />, |
| filter: (model) => |
| model.model_name.toLowerCase().includes('abab') || |
| model.model_name.toLowerCase().includes('minimax'), |
| }, |
| baidu: { |
| label: t('文心一言'), |
| icon: <Wenxin.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('ernie'), |
| }, |
| xunfei: { |
| label: t('讯飞星火'), |
| icon: <Spark.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('spark'), |
| }, |
| midjourney: { |
| label: 'Midjourney', |
| icon: <Midjourney />, |
| filter: (model) => model.model_name.toLowerCase().includes('mj_'), |
| }, |
| tencent: { |
| label: t('腾讯混元'), |
| icon: <Hunyuan.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('hunyuan'), |
| }, |
| cohere: { |
| label: 'Cohere', |
| icon: <Cohere.Color />, |
| filter: (model) => |
| model.model_name.toLowerCase().includes('command') || |
| model.model_name.toLowerCase().includes('c4ai-') || |
| model.model_name.toLowerCase().includes('embed-'), |
| }, |
| cloudflare: { |
| label: 'Cloudflare', |
| icon: <Cloudflare.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('@cf/'), |
| }, |
| ai360: { |
| label: t('360智脑'), |
| icon: <Ai360.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('360'), |
| }, |
| jina: { |
| label: 'Jina', |
| icon: <Jina />, |
| filter: (model) => model.model_name.toLowerCase().includes('jina'), |
| }, |
| mistral: { |
| label: 'Mistral AI', |
| icon: <Mistral.Color />, |
| filter: (model) => |
| model.model_name.toLowerCase().includes('mistral') || |
| model.model_name.toLowerCase().includes('codestral') || |
| model.model_name.toLowerCase().includes('pixtral') || |
| model.model_name.toLowerCase().includes('voxtral') || |
| model.model_name.toLowerCase().includes('magistral'), |
| }, |
| xai: { |
| label: 'xAI', |
| icon: <XAI />, |
| filter: (model) => model.model_name.toLowerCase().includes('grok'), |
| }, |
| llama: { |
| label: 'Llama', |
| icon: <Ollama />, |
| filter: (model) => model.model_name.toLowerCase().includes('llama'), |
| }, |
| doubao: { |
| label: t('豆包'), |
| icon: <Doubao.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('doubao'), |
| }, |
| yi: { |
| label: t('零一万物'), |
| icon: <Yi.Color />, |
| filter: (model) => model.model_name.toLowerCase().includes('yi'), |
| }, |
| }; |
|
|
| lastLocale = currentLocale; |
| return categoriesCache; |
| }; |
| })(); |
|
|
| |
| |
| |
| |
| |
| export function getChannelIcon(channelType) { |
| const iconSize = 14; |
|
|
| switch (channelType) { |
| case 1: |
| case 3: |
| case 57: |
| return <OpenAI size={iconSize} />; |
| case 2: |
| case 5: |
| return <Midjourney size={iconSize} />; |
| case 36: |
| return <Suno size={iconSize} />; |
| case 4: |
| return <Ollama size={iconSize} />; |
| case 14: |
| case 33: |
| return <Claude.Color size={iconSize} />; |
| case 41: |
| return <Gemini.Color size={iconSize} />; |
| case 34: |
| return <Cohere.Color size={iconSize} />; |
| case 39: |
| return <Cloudflare.Color size={iconSize} />; |
| case 43: |
| return <DeepSeek.Color size={iconSize} />; |
| case 15: |
| case 46: |
| return <Wenxin.Color size={iconSize} />; |
| case 17: |
| return <Qwen.Color size={iconSize} />; |
| case 18: |
| return <Spark.Color size={iconSize} />; |
| case 16: |
| case 26: |
| return <Zhipu.Color size={iconSize} />; |
| case 24: |
| case 11: |
| return <Gemini.Color size={iconSize} />; |
| case 47: |
| return <Xinference.Color size={iconSize} />; |
| case 25: |
| return <Moonshot size={iconSize} />; |
| case 27: |
| return <Perplexity.Color size={iconSize} />; |
| case 20: |
| return <OpenRouter size={iconSize} />; |
| case 19: |
| return <Ai360.Color size={iconSize} />; |
| case 23: |
| return <Hunyuan.Color size={iconSize} />; |
| case 31: |
| return <Yi.Color size={iconSize} />; |
| case 35: |
| return <Minimax.Color size={iconSize} />; |
| case 37: |
| return <Dify.Color size={iconSize} />; |
| case 38: |
| return <Jina size={iconSize} />; |
| case 40: |
| return <SiliconCloud.Color size={iconSize} />; |
| case 42: |
| return <Mistral.Color size={iconSize} />; |
| case 45: |
| return <Doubao.Color size={iconSize} />; |
| case 48: |
| return <XAI size={iconSize} />; |
| case 49: |
| return <Coze size={iconSize} />; |
| case 50: |
| return <Kling.Color size={iconSize} />; |
| case 51: |
| return <Jimeng.Color size={iconSize} />; |
| case 54: |
| return <Doubao.Color size={iconSize} />; |
| case 56: |
| return <Replicate size={iconSize} />; |
| case 8: |
| case 22: |
| return <FastGPT.Color size={iconSize} />; |
| case 21: |
| case 44: |
| default: |
| return null; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function getLobeHubIcon(iconName, size = 14) { |
| if (typeof iconName === 'string') iconName = iconName.trim(); |
| |
| if (!iconName) { |
| return <Avatar size='extra-extra-small'>?</Avatar>; |
| } |
|
|
| |
| const segments = String(iconName).split('.'); |
| const baseKey = segments[0]; |
| const BaseIcon = LobeIcons[baseKey]; |
|
|
| let IconComponent = undefined; |
| let propStartIndex = 1; |
|
|
| if (BaseIcon && segments.length > 1 && BaseIcon[segments[1]]) { |
| IconComponent = BaseIcon[segments[1]]; |
| propStartIndex = 2; |
| } else { |
| IconComponent = LobeIcons[baseKey]; |
| propStartIndex = 1; |
| } |
|
|
| |
| if ( |
| !IconComponent || |
| (typeof IconComponent !== 'function' && typeof IconComponent !== 'object') |
| ) { |
| const firstLetter = String(iconName).charAt(0).toUpperCase(); |
| return <Avatar size='extra-extra-small'>{firstLetter}</Avatar>; |
| } |
|
|
| |
| const props = {}; |
|
|
| const parseValue = (raw) => { |
| if (raw == null) return true; |
| let v = String(raw).trim(); |
| |
| if (v.startsWith('{') && v.endsWith('}')) { |
| v = v.slice(1, -1).trim(); |
| } |
| |
| if ( |
| (v.startsWith('"') && v.endsWith('"')) || |
| (v.startsWith("'") && v.endsWith("'")) |
| ) { |
| return v.slice(1, -1); |
| } |
| |
| if (v === 'true') return true; |
| if (v === 'false') return false; |
| |
| if (/^-?\d+(?:\.\d+)?$/.test(v)) return Number(v); |
| |
| return v; |
| }; |
|
|
| for (let i = propStartIndex; i < segments.length; i++) { |
| const seg = segments[i]; |
| if (!seg) continue; |
| const eqIdx = seg.indexOf('='); |
| if (eqIdx === -1) { |
| props[seg.trim()] = true; |
| continue; |
| } |
| const key = seg.slice(0, eqIdx).trim(); |
| const valRaw = seg.slice(eqIdx + 1).trim(); |
| props[key] = parseValue(valRaw); |
| } |
|
|
| |
| if (props.size == null && size != null) props.size = size; |
|
|
| return <IconComponent {...props} />; |
| } |
|
|
| const oauthProviderIconMap = { |
| github: SiGithub, |
| gitlab: SiGitlab, |
| gitea: SiGitea, |
| google: SiGoogle, |
| discord: SiDiscord, |
| facebook: SiFacebook, |
| linkedin: SiLinkedin, |
| x: SiX, |
| twitter: SiX, |
| slack: SiSlack, |
| telegram: SiTelegram, |
| wechat: SiWechat, |
| keycloak: SiKeycloak, |
| nextcloud: SiNextcloud, |
| authentik: SiAuthentik, |
| openid: SiOpenid, |
| okta: SiOkta, |
| auth0: SiAuth0, |
| atlassian: SiAtlassian, |
| bitbucket: SiBitbucket, |
| notion: SiNotion, |
| twitch: SiTwitch, |
| reddit: SiReddit, |
| dropbox: SiDropbox, |
| }; |
|
|
| function isHttpUrl(value) { |
| return /^https?:\/\//i.test(value || ''); |
| } |
|
|
| function isSimpleEmoji(value) { |
| if (!value) return false; |
| const trimmed = String(value).trim(); |
| return trimmed.length > 0 && trimmed.length <= 4 && !isHttpUrl(trimmed); |
| } |
|
|
| function normalizeOAuthIconKey(raw) { |
| return raw |
| .trim() |
| .toLowerCase() |
| .replace(/^ri:/, '') |
| .replace(/^react-icons:/, '') |
| .replace(/^si:/, ''); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function getOAuthProviderIcon(iconName, size = 20) { |
| const raw = String(iconName || '').trim(); |
| const iconSize = Number(size) > 0 ? Number(size) : 20; |
|
|
| if (!raw) { |
| return <Layers size={iconSize} color='var(--semi-color-text-2)' />; |
| } |
|
|
| if (isHttpUrl(raw)) { |
| return ( |
| <img |
| src={raw} |
| alt='provider icon' |
| width={iconSize} |
| height={iconSize} |
| style={{ borderRadius: 4, objectFit: 'cover' }} |
| /> |
| ); |
| } |
|
|
| if (isSimpleEmoji(raw)) { |
| return ( |
| <span |
| style={{ |
| width: iconSize, |
| height: iconSize, |
| lineHeight: `${iconSize}px`, |
| textAlign: 'center', |
| display: 'inline-block', |
| fontSize: Math.max(Math.floor(iconSize * 0.8), 14), |
| }} |
| > |
| {raw} |
| </span> |
| ); |
| } |
|
|
| const key = normalizeOAuthIconKey(raw); |
| const IconComp = oauthProviderIconMap[key]; |
| if (IconComp) { |
| return <IconComp size={iconSize} />; |
| } |
|
|
| return <Avatar size='extra-extra-small'>{raw.charAt(0).toUpperCase()}</Avatar>; |
| } |
|
|
| |
| const colors = [ |
| 'amber', |
| 'blue', |
| 'cyan', |
| 'green', |
| 'grey', |
| 'indigo', |
| 'light-blue', |
| 'lime', |
| 'orange', |
| 'pink', |
| 'purple', |
| 'red', |
| 'teal', |
| 'violet', |
| 'yellow', |
| ]; |
|
|
| |
| const baseColors = [ |
| '#1664FF', |
| '#1AC6FF', |
| '#FF8A00', |
| '#3CC780', |
| '#7442D4', |
| '#FFC400', |
| '#304D77', |
| '#B48DEB', |
| '#009488', |
| '#FF7DDA', |
| ]; |
|
|
| |
| const extendedColors = [ |
| '#1664FF', |
| '#B2CFFF', |
| '#1AC6FF', |
| '#94EFFF', |
| '#FF8A00', |
| '#FFCE7A', |
| '#3CC780', |
| '#B9EDCD', |
| '#7442D4', |
| '#DDC5FA', |
| '#FFC400', |
| '#FAE878', |
| '#304D77', |
| '#8B959E', |
| '#B48DEB', |
| '#EFE3FF', |
| '#009488', |
| '#59BAA8', |
| '#FF7DDA', |
| '#FFCFEE', |
| ]; |
|
|
| |
| export const modelColorMap = { |
| 'dall-e': 'rgb(147,112,219)', |
| |
| 'dall-e-3': 'rgb(153,50,204)', |
| 'gpt-3.5-turbo': 'rgb(184,227,167)', |
| |
| 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', |
| 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', |
| 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', |
| 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', |
| 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', |
| 'gpt-4': 'rgb(135,206,235)', |
| |
| 'gpt-4-0613': 'rgb(100,149,237)', |
| 'gpt-4-1106-preview': 'rgb(30,144,255)', |
| 'gpt-4-0125-preview': 'rgb(2,177,236)', |
| 'gpt-4-turbo-preview': 'rgb(2,177,255)', |
| 'gpt-4-32k': 'rgb(104,111,238)', |
| |
| 'gpt-4-32k-0613': 'rgb(61,71,139)', |
| 'gpt-4-all': 'rgb(65,105,225)', |
| 'gpt-4-gizmo-*': 'rgb(0,0,255)', |
| 'gpt-4-vision-preview': 'rgb(25,25,112)', |
| 'text-ada-001': 'rgb(255,192,203)', |
| 'text-babbage-001': 'rgb(255,160,122)', |
| 'text-curie-001': 'rgb(219,112,147)', |
| |
| 'text-davinci-003': 'rgb(219,112,147)', |
| 'text-davinci-edit-001': 'rgb(255,105,180)', |
| 'text-embedding-ada-002': 'rgb(255,182,193)', |
| 'text-embedding-v1': 'rgb(255,174,185)', |
| 'text-moderation-latest': 'rgb(255,130,171)', |
| 'text-moderation-stable': 'rgb(255,160,122)', |
| 'tts-1': 'rgb(255,140,0)', |
| 'tts-1-1106': 'rgb(255,165,0)', |
| 'tts-1-hd': 'rgb(255,215,0)', |
| 'tts-1-hd-1106': 'rgb(255,223,0)', |
| 'whisper-1': 'rgb(245,245,220)', |
| 'claude-3-opus-20240229': 'rgb(255,132,31)', |
| 'claude-3-sonnet-20240229': 'rgb(253,135,93)', |
| 'claude-3-haiku-20240307': 'rgb(255,175,146)', |
| }; |
|
|
| export function modelToColor(modelName) { |
| |
| if (modelColorMap[modelName]) { |
| return modelColorMap[modelName]; |
| } |
|
|
| |
| let hash = 0; |
| for (let i = 0; i < modelName.length; i++) { |
| hash = (hash << 5) - hash + modelName.charCodeAt(i); |
| hash = hash & hash; |
| } |
| hash = Math.abs(hash); |
|
|
| |
| const colorPalette = modelName.length > 10 ? extendedColors : baseColors; |
|
|
| |
| const index = hash % colorPalette.length; |
| return colorPalette[index]; |
| } |
|
|
| export function stringToColor(str) { |
| let sum = 0; |
| for (let i = 0; i < str.length; i++) { |
| sum += str.charCodeAt(i); |
| } |
| let i = sum % colors.length; |
| return colors[i]; |
| } |
|
|
| |
| export function renderModelTag(modelName, options = {}) { |
| const { |
| color, |
| size = 'default', |
| shape = 'circle', |
| onClick, |
| suffixIcon, |
| } = options; |
|
|
| const categories = getModelCategories(i18next.t); |
| let icon = null; |
|
|
| for (const [key, category] of Object.entries(categories)) { |
| if (key !== 'all' && category.filter({ model_name: modelName })) { |
| icon = category.icon; |
| break; |
| } |
| } |
|
|
| return ( |
| <Tag |
| color={color || stringToColor(modelName)} |
| prefixIcon={icon} |
| suffixIcon={suffixIcon} |
| size={size} |
| shape={shape} |
| onClick={onClick} |
| > |
| {modelName} |
| </Tag> |
| ); |
| } |
|
|
| export function renderText(text, limit) { |
| if (text.length > limit) { |
| return text.slice(0, limit - 3) + '...'; |
| } |
| return text; |
| } |
|
|
| |
| |
| |
| |
| |
| export function renderGroup(group) { |
| if (group === '') { |
| return ( |
| <Tag key='default' color='white' shape='circle'> |
| {i18next.t('用户分组')} |
| </Tag> |
| ); |
| } |
|
|
| const tagColors = { |
| vip: 'yellow', |
| pro: 'yellow', |
| svip: 'red', |
| premium: 'red', |
| }; |
|
|
| const groups = group.split(',').sort(); |
|
|
| return ( |
| <span key={group}> |
| {groups.map((group) => ( |
| <Tag |
| color={tagColors[group] || stringToColor(group)} |
| key={group} |
| shape='circle' |
| onClick={async (event) => { |
| event.stopPropagation(); |
| if (await copy(group)) { |
| showSuccess(i18next.t('已复制:') + group); |
| } else { |
| Modal.error({ |
| title: i18next.t('无法复制到剪贴板,请手动复制'), |
| content: group, |
| }); |
| } |
| }} |
| > |
| {group} |
| </Tag> |
| ))} |
| </span> |
| ); |
| } |
|
|
| export function renderRatio(ratio) { |
| let color = 'green'; |
| if (ratio > 5) { |
| color = 'red'; |
| } else if (ratio > 3) { |
| color = 'orange'; |
| } else if (ratio > 1) { |
| color = 'blue'; |
| } |
| return ( |
| <Tag color={color}> |
| {ratio}x {i18next.t('倍率')} |
| </Tag> |
| ); |
| } |
|
|
| const measureTextWidth = ( |
| text, |
| style = { |
| fontSize: '14px', |
| fontFamily: |
| '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', |
| }, |
| containerWidth, |
| ) => { |
| const span = document.createElement('span'); |
|
|
| span.style.visibility = 'hidden'; |
| span.style.position = 'absolute'; |
| span.style.whiteSpace = 'nowrap'; |
| span.style.fontSize = style.fontSize; |
| span.style.fontFamily = style.fontFamily; |
|
|
| span.textContent = text; |
|
|
| document.body.appendChild(span); |
| const width = span.offsetWidth; |
|
|
| document.body.removeChild(span); |
|
|
| return width; |
| }; |
|
|
| export function truncateText(text, maxWidth = 200) { |
| const isMobileScreen = window.matchMedia( |
| `(max-width: ${MOBILE_BREAKPOINT - 1}px)`, |
| ).matches; |
| if (!isMobileScreen) { |
| return text; |
| } |
| if (!text) return text; |
|
|
| try { |
| |
| let actualMaxWidth = maxWidth; |
| if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) { |
| const percentage = parseFloat(maxWidth) / 100; |
| |
| actualMaxWidth = window.innerWidth * percentage; |
| } |
|
|
| const width = measureTextWidth(text); |
| if (width <= actualMaxWidth) return text; |
|
|
| let left = 0; |
| let right = text.length; |
| let result = text; |
|
|
| while (left <= right) { |
| const mid = Math.floor((left + right) / 2); |
| const truncated = text.slice(0, mid) + '...'; |
| const currentWidth = measureTextWidth(truncated); |
|
|
| if (currentWidth <= actualMaxWidth) { |
| result = truncated; |
| left = mid + 1; |
| } else { |
| right = mid - 1; |
| } |
| } |
|
|
| return result; |
| } catch (error) { |
| console.warn( |
| 'Text measurement failed, falling back to character count', |
| error, |
| ); |
| if (text.length > 20) { |
| return text.slice(0, 17) + '...'; |
| } |
| return text; |
| } |
| } |
|
|
| export const renderGroupOption = (item) => { |
| const { |
| disabled, |
| selected, |
| label, |
| value, |
| focused, |
| className, |
| style, |
| onMouseEnter, |
| onClick, |
| empty, |
| emptyContent, |
| ...rest |
| } = item; |
|
|
| const baseStyle = { |
| display: 'flex', |
| justifyContent: 'space-between', |
| alignItems: 'center', |
| padding: '8px 16px', |
| cursor: disabled ? 'not-allowed' : 'pointer', |
| backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent', |
| opacity: disabled ? 0.5 : 1, |
| ...(selected && { |
| backgroundColor: 'var(--semi-color-primary-light-default)', |
| }), |
| '&:hover': { |
| backgroundColor: !disabled && 'var(--semi-color-fill-1)', |
| }, |
| }; |
|
|
| const handleClick = () => { |
| if (!disabled && onClick) { |
| onClick(); |
| } |
| }; |
|
|
| const handleMouseEnter = (e) => { |
| if (!disabled && onMouseEnter) { |
| onMouseEnter(e); |
| } |
| }; |
|
|
| return ( |
| <div |
| style={baseStyle} |
| onClick={handleClick} |
| onMouseEnter={handleMouseEnter} |
| > |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}> |
| <Typography.Text strong type={disabled ? 'tertiary' : undefined}> |
| {value} |
| </Typography.Text> |
| <Typography.Text type='secondary' size='small'> |
| {label} |
| </Typography.Text> |
| </div> |
| {item.ratio && renderRatio(item.ratio)} |
| </div> |
| ); |
| }; |
|
|
| export function renderNumber(num) { |
| if (num >= 1000000000) { |
| return (num / 1000000000).toFixed(1) + 'B'; |
| } else if (num >= 1000000) { |
| return (num / 1000000).toFixed(1) + 'M'; |
| } else if (num >= 10000) { |
| return (num / 1000).toFixed(1) + 'k'; |
| } else { |
| return num; |
| } |
| } |
|
|
| export function renderQuotaNumberWithDigit(num, digits = 2) { |
| if (typeof num !== 'number' || isNaN(num)) { |
| return 0; |
| } |
| const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| num = num.toFixed(digits); |
| if (quotaDisplayType === 'CNY') { |
| return '¥' + num; |
| } else if (quotaDisplayType === 'USD') { |
| return '$' + num; |
| } else if (quotaDisplayType === 'CUSTOM') { |
| const statusStr = localStorage.getItem('status'); |
| let symbol = '¤'; |
| try { |
| if (statusStr) { |
| const s = JSON.parse(statusStr); |
| symbol = s?.custom_currency_symbol || symbol; |
| } |
| } catch (e) {} |
| return symbol + num; |
| } else { |
| return num; |
| } |
| } |
|
|
| export function renderNumberWithPoint(num) { |
| if (num === undefined) return ''; |
| num = num.toFixed(2); |
| if (num >= 100000) { |
| |
| let numStr = num.toString(); |
| |
| let decimalPointIndex = numStr.indexOf('.'); |
|
|
| let wholePart = numStr; |
| let decimalPart = ''; |
|
|
| |
| if (decimalPointIndex !== -1) { |
| wholePart = numStr.slice(0, decimalPointIndex); |
| decimalPart = numStr.slice(decimalPointIndex); |
| } |
|
|
| |
| let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2); |
|
|
| |
| return shortenedWholePart + decimalPart; |
| } |
|
|
| |
| return num; |
| } |
|
|
| export function getQuotaPerUnit() { |
| let quotaPerUnit = localStorage.getItem('quota_per_unit'); |
| quotaPerUnit = parseFloat(quotaPerUnit); |
| return quotaPerUnit; |
| } |
|
|
| export function renderUnitWithQuota(quota) { |
| let quotaPerUnit = localStorage.getItem('quota_per_unit'); |
| quotaPerUnit = parseFloat(quotaPerUnit); |
| quota = parseFloat(quota); |
| return quotaPerUnit * quota; |
| } |
|
|
| export function getQuotaWithUnit(quota, digits = 6) { |
| let quotaPerUnit = localStorage.getItem('quota_per_unit'); |
| quotaPerUnit = parseFloat(quotaPerUnit); |
| return (quota / quotaPerUnit).toFixed(digits); |
| } |
|
|
| export function renderQuotaWithAmount(amount) { |
| const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| if (quotaDisplayType === 'TOKENS') { |
| return renderNumber(renderUnitWithQuota(amount)); |
| } |
| if (quotaDisplayType === 'CNY') { |
| return '¥' + amount; |
| } else if (quotaDisplayType === 'CUSTOM') { |
| const statusStr = localStorage.getItem('status'); |
| let symbol = '¤'; |
| try { |
| if (statusStr) { |
| const s = JSON.parse(statusStr); |
| symbol = s?.custom_currency_symbol || symbol; |
| } |
| } catch (e) {} |
| return symbol + amount; |
| } |
| return '$' + amount; |
| } |
|
|
| |
| |
| |
| |
| export function getCurrencyConfig() { |
| const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| const statusStr = localStorage.getItem('status'); |
|
|
| let symbol = '$'; |
| let rate = 1; |
|
|
| if (quotaDisplayType === 'CNY') { |
| symbol = '¥'; |
| try { |
| if (statusStr) { |
| const s = JSON.parse(statusStr); |
| rate = s?.usd_exchange_rate || 7; |
| } |
| } catch (e) {} |
| } else if (quotaDisplayType === 'CUSTOM') { |
| try { |
| if (statusStr) { |
| const s = JSON.parse(statusStr); |
| symbol = s?.custom_currency_symbol || '¤'; |
| rate = s?.custom_currency_exchange_rate || 1; |
| } |
| } catch (e) {} |
| } |
|
|
| return { symbol, rate, type: quotaDisplayType }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function convertUSDToCurrency(usdAmount, digits = 2) { |
| const { symbol, rate } = getCurrencyConfig(); |
| const convertedAmount = usdAmount * rate; |
| return symbol + convertedAmount.toFixed(digits); |
| } |
|
|
| export function renderQuota(quota, digits = 2) { |
| let quotaPerUnit = localStorage.getItem('quota_per_unit'); |
| const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| quotaPerUnit = parseFloat(quotaPerUnit); |
| if (quotaDisplayType === 'TOKENS') { |
| return renderNumber(quota); |
| } |
| const resultUSD = quota / quotaPerUnit; |
| let symbol = '$'; |
| let value = resultUSD; |
| if (quotaDisplayType === 'CNY') { |
| const statusStr = localStorage.getItem('status'); |
| let usdRate = 1; |
| try { |
| if (statusStr) { |
| const s = JSON.parse(statusStr); |
| usdRate = s?.usd_exchange_rate || 1; |
| } |
| } catch (e) {} |
| value = resultUSD * usdRate; |
| symbol = '¥'; |
| } else if (quotaDisplayType === 'CUSTOM') { |
| const statusStr = localStorage.getItem('status'); |
| let symbolCustom = '¤'; |
| let rate = 1; |
| try { |
| if (statusStr) { |
| const s = JSON.parse(statusStr); |
| symbolCustom = s?.custom_currency_symbol || symbolCustom; |
| rate = s?.custom_currency_exchange_rate || rate; |
| } |
| } catch (e) {} |
| value = resultUSD * rate; |
| symbol = symbolCustom; |
| } |
| const fixedResult = value.toFixed(digits); |
| if (parseFloat(fixedResult) === 0 && quota > 0 && value > 0) { |
| const minValue = Math.pow(10, -digits); |
| return symbol + minValue.toFixed(digits); |
| } |
| return symbol + fixedResult; |
| } |
|
|
| function isValidGroupRatio(ratio) { |
| return Number.isFinite(ratio) && ratio !== -1; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| function getEffectiveRatio(groupRatio, user_group_ratio) { |
| const useUserGroupRatio = isValidGroupRatio(user_group_ratio); |
| const ratioLabel = useUserGroupRatio |
| ? i18next.t('专属倍率') |
| : i18next.t('分组倍率'); |
| const effectiveRatio = useUserGroupRatio ? user_group_ratio : groupRatio; |
|
|
| return { |
| ratio: effectiveRatio, |
| label: ratioLabel, |
| useUserGroupRatio: useUserGroupRatio, |
| }; |
| } |
|
|
| function getQuotaDisplayType() { |
| return localStorage.getItem('quota_display_type') || 'USD'; |
| } |
|
|
| function resolveBillingDisplayMode(displayMode, modelPrice = -1) { |
| if (modelPrice !== -1) { |
| return 'price'; |
| } |
| if (getQuotaDisplayType() === 'TOKENS') { |
| return 'ratio'; |
| } |
| return displayMode === 'ratio' ? 'ratio' : 'price'; |
| } |
|
|
| function isPriceDisplayMode(displayMode, modelPrice = -1) { |
| return resolveBillingDisplayMode(displayMode, modelPrice) === 'price'; |
| } |
|
|
| function shouldUseRatioBillingProcess(modelPrice = -1) { |
| return modelPrice === -1 && getQuotaDisplayType() === 'TOKENS'; |
| } |
|
|
| function formatCompactDisplayPrice(usdAmount, digits = 6) { |
| const { symbol, rate } = getCurrencyConfig(); |
| const amount = Number((usdAmount * rate).toFixed(digits)); |
| return `${symbol}${amount}`; |
| } |
|
|
| function appendPricePart(parts, condition, key, vars) { |
| if (!condition) { |
| return; |
| } |
| parts.push(i18next.t(key, vars)); |
| } |
|
|
| function joinBillingSummary(parts) { |
| return parts.filter(Boolean).join(','); |
| } |
|
|
| function getGroupRatioText(groupRatio, user_group_ratio) { |
| const { ratio, label } = getEffectiveRatio(groupRatio, user_group_ratio); |
| return i18next.t('{{ratioType}} {{ratio}}', { |
| ratioType: label, |
| ratio, |
| }); |
| } |
|
|
| function formatRatioValue(value, digits = 6) { |
| const num = Number(value); |
| if (!Number.isFinite(num)) { |
| return 0; |
| } |
| return Number(num.toFixed(digits)); |
| } |
|
|
| function renderDisplayAmountFromUsd(usdAmount, digits = 6) { |
| return renderQuotaWithAmount(Number(Number(usdAmount || 0).toFixed(digits))); |
| } |
|
|
| |
| function renderPriceSimpleCore({ |
| modelRatio, |
| modelPrice = -1, |
| groupRatio, |
| user_group_ratio, |
| cacheTokens = 0, |
| cacheRatio = 1.0, |
| cacheCreationTokens = 0, |
| cacheCreationRatio = 1.0, |
| cacheCreationTokens5m = 0, |
| cacheCreationRatio5m = 1.0, |
| cacheCreationTokens1h = 0, |
| cacheCreationRatio1h = 1.0, |
| image = false, |
| imageRatio = 1.0, |
| isSystemPromptOverride = false, |
| displayMode = 'price', |
| }) { |
| const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| groupRatio, |
| user_group_ratio, |
| ); |
| const finalGroupRatio = effectiveGroupRatio; |
|
|
| const { symbol, rate } = getCurrencyConfig(); |
| if (modelPrice !== -1) { |
| if (isPriceDisplayMode(displayMode, modelPrice)) { |
| return joinBillingSummary([ |
| i18next.t('模型价格:{{symbol}}{{price}} / 次', { |
| symbol: symbol, |
| price: (modelPrice * rate).toFixed(6), |
| }), |
| getGroupRatioText(groupRatio, user_group_ratio), |
| ]); |
| } |
| const displayPrice = (modelPrice * rate).toFixed(6); |
| return i18next.t('价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}', { |
| symbol: symbol, |
| price: displayPrice, |
| ratioType: ratioLabel, |
| ratio: finalGroupRatio, |
| }); |
| } |
|
|
| const hasSplitCacheCreation = |
| cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; |
|
|
| const shouldShowLegacyCacheCreation = |
| !hasSplitCacheCreation && cacheCreationTokens !== 0; |
|
|
| const shouldShowCache = cacheTokens !== 0; |
| const shouldShowCacheCreation5m = |
| hasSplitCacheCreation && cacheCreationTokens5m > 0; |
| const shouldShowCacheCreation1h = |
| hasSplitCacheCreation && cacheCreationTokens1h > 0; |
|
|
| if (isPriceDisplayMode(displayMode, modelPrice)) { |
| const parts = []; |
| if (modelPrice !== -1) { |
| parts.push( |
| i18next.t('按次 {{price}} / 次', { |
| price: formatCompactDisplayPrice(modelPrice), |
| }), |
| ); |
| parts.push(getGroupRatioText(groupRatio, user_group_ratio)); |
| return joinBillingSummary(parts); |
| } |
|
|
| parts.push( |
| i18next.t('输入 {{price}} / 1M tokens', { |
| price: formatCompactDisplayPrice(modelRatio * 2.0), |
| }), |
| ); |
|
|
| if (shouldShowCache) { |
| parts.push( |
| i18next.t('缓存读取 {{price}}', { |
| price: formatCompactDisplayPrice(modelRatio * 2.0 * cacheRatio), |
| }), |
| ); |
| } |
|
|
| if (hasSplitCacheCreation && shouldShowCacheCreation5m) { |
| parts.push( |
| i18next.t('5m缓存创建 {{price}}', { |
| price: formatCompactDisplayPrice(modelRatio * 2.0 * cacheCreationRatio5m), |
| }), |
| ); |
| } |
| if (hasSplitCacheCreation && shouldShowCacheCreation1h) { |
| parts.push( |
| i18next.t('1h缓存创建 {{price}}', { |
| price: formatCompactDisplayPrice(modelRatio * 2.0 * cacheCreationRatio1h), |
| }), |
| ); |
| } |
| if (!hasSplitCacheCreation && shouldShowLegacyCacheCreation) { |
| parts.push( |
| i18next.t('缓存创建 {{price}}', { |
| price: formatCompactDisplayPrice(modelRatio * 2.0 * cacheCreationRatio), |
| }), |
| ); |
| } |
|
|
| if (image) { |
| parts.push( |
| i18next.t('图片输入 {{price}}', { |
| price: formatCompactDisplayPrice(modelRatio * 2.0 * imageRatio), |
| }), |
| ); |
| } |
|
|
| parts.push(getGroupRatioText(groupRatio, user_group_ratio)); |
|
|
| let result = joinBillingSummary(parts); |
| if (isSystemPromptOverride) { |
| result += '\n\r' + i18next.t('系统提示覆盖'); |
| } |
| return result; |
| } |
|
|
| const parts = []; |
| |
| parts.push(i18next.t('模型: {{ratio}}')); |
|
|
| |
| if (shouldShowCache) { |
| parts.push(i18next.t('缓存: {{cacheRatio}}')); |
| } |
|
|
| if (hasSplitCacheCreation) { |
| if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) { |
| parts.push( |
| i18next.t( |
| '缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}', |
| ), |
| ); |
| } else if (shouldShowCacheCreation5m) { |
| parts.push(i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}')); |
| } else if (shouldShowCacheCreation1h) { |
| parts.push(i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}')); |
| } |
| } else if (shouldShowLegacyCacheCreation) { |
| parts.push(i18next.t('缓存创建: {{cacheCreationRatio}}')); |
| } |
|
|
| |
| if (image) { |
| parts.push(i18next.t('图片输入: {{imageRatio}}')); |
| } |
|
|
| parts.push(`{{ratioType}}: {{groupRatio}}`); |
|
|
| let result = i18next.t(parts.join(' * '), { |
| ratio: modelRatio, |
| ratioType: ratioLabel, |
| groupRatio: finalGroupRatio, |
| cacheRatio: cacheRatio, |
| cacheCreationRatio: cacheCreationRatio, |
| cacheCreationRatio5m: cacheCreationRatio5m, |
| cacheCreationRatio1h: cacheCreationRatio1h, |
| imageRatio: imageRatio, |
| }); |
|
|
| if (isSystemPromptOverride) { |
| result += '\n\r' + i18next.t('系统提示覆盖'); |
| } |
|
|
| return result; |
| } |
|
|
| export function renderModelPrice( |
| inputTokens, |
| completionTokens, |
| modelRatio, |
| modelPrice = -1, |
| completionRatio, |
| groupRatio, |
| user_group_ratio, |
| cacheTokens = 0, |
| cacheRatio = 1.0, |
| image = false, |
| imageRatio = 1.0, |
| imageOutputTokens = 0, |
| webSearch = false, |
| webSearchCallCount = 0, |
| webSearchPrice = 0, |
| fileSearch = false, |
| fileSearchCallCount = 0, |
| fileSearchPrice = 0, |
| audioInputSeperatePrice = false, |
| audioInputTokens = 0, |
| audioInputPrice = 0, |
| imageGenerationCall = false, |
| imageGenerationCallPrice = 0, |
| displayMode = 'price', |
| ) { |
| const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| groupRatio, |
| user_group_ratio, |
| ); |
| groupRatio = effectiveGroupRatio; |
|
|
| const { symbol, rate } = getCurrencyConfig(); |
|
|
| if (!shouldUseRatioBillingProcess(modelPrice)) { |
| if (modelPrice !== -1) { |
| return ( |
| <> |
| <article> |
| <p> |
| {i18next.t('模型价格:{{symbol}}{{price}} / 次', { |
| symbol, |
| price: (modelPrice * rate).toFixed(6), |
| })} |
| </p> |
| <p> |
| {i18next.t( |
| '模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', |
| { |
| symbol, |
| price: (modelPrice * rate).toFixed(6), |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| total: (modelPrice * groupRatio * rate).toFixed(6), |
| }, |
| )} |
| </p> |
| <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| </article> |
| </> |
| ); |
| } |
|
|
| if (completionRatio === undefined) { |
| completionRatio = 0; |
| } |
| const inputRatioPrice = modelRatio * 2.0; |
| const completionRatioPrice = modelRatio * 2.0 * completionRatio; |
| const cacheRatioPrice = modelRatio * 2.0 * cacheRatio; |
| const imageRatioPrice = modelRatio * 2.0 * imageRatio; |
| let effectiveInputTokens = |
| inputTokens - cacheTokens + cacheTokens * cacheRatio; |
| if (image && imageOutputTokens > 0) { |
| effectiveInputTokens = |
| inputTokens - imageOutputTokens + imageOutputTokens * imageRatio; |
| } |
| if (audioInputTokens > 0) { |
| effectiveInputTokens -= audioInputTokens; |
| } |
| const price = |
| (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + |
| (audioInputTokens / 1000000) * audioInputPrice * groupRatio + |
| (completionTokens / 1000000) * completionRatioPrice * groupRatio + |
| (webSearchCallCount / 1000) * webSearchPrice * groupRatio + |
| (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio + |
| imageGenerationCallPrice * groupRatio; |
|
|
| return ( |
| <> |
| <article> |
| <p> |
| {i18next.t('输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}', { |
| symbol, |
| price: (inputRatioPrice * rate).toFixed(6), |
| audioPrice: audioInputSeperatePrice |
| ? `,${i18next.t('音频输入价格')} ${symbol}${(audioInputPrice * rate).toFixed(6)} / 1M tokens` |
| : '', |
| })} |
| </p> |
| <p> |
| {i18next.t('补全价格:{{symbol}}{{total}} / 1M tokens', { |
| symbol, |
| total: (completionRatioPrice * rate).toFixed(6), |
| })} |
| </p> |
| {cacheTokens > 0 && ( |
| <p> |
| {i18next.t('缓存读取价格:{{symbol}}{{total}} / 1M tokens', { |
| symbol, |
| total: (inputRatioPrice * cacheRatio * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| {image && imageOutputTokens > 0 && ( |
| <p> |
| {i18next.t('图片输入价格:{{symbol}}{{total}} / 1M tokens', { |
| symbol, |
| total: (imageRatioPrice * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| {webSearch && webSearchCallCount > 0 && ( |
| <p> |
| {i18next.t('Web搜索价格:{{symbol}}{{price}} / 1K 次', { |
| symbol, |
| price: (webSearchPrice * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| {fileSearch && fileSearchCallCount > 0 && ( |
| <p> |
| {i18next.t('文件搜索价格:{{symbol}}{{price}} / 1K 次', { |
| symbol, |
| price: (fileSearchPrice * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| {imageGenerationCall && imageGenerationCallPrice > 0 && ( |
| <p> |
| {i18next.t('图片生成调用:{{symbol}}{{price}} / 1次', { |
| symbol, |
| price: (imageGenerationCallPrice * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| <p> |
| {(() => { |
| let inputDesc = ''; |
| if (image && imageOutputTokens > 0) { |
| inputDesc = i18next.t( |
| '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}', |
| { |
| nonImageInput: inputTokens - imageOutputTokens, |
| imageInput: imageOutputTokens, |
| symbol: symbol, |
| price: (inputRatioPrice * rate).toFixed(6), |
| }, |
| ); |
| } else if (cacheTokens > 0) { |
| inputDesc = i18next.t( |
| '(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}', |
| { |
| nonCacheInput: inputTokens - cacheTokens, |
| cacheInput: cacheTokens, |
| symbol: symbol, |
| price: (inputRatioPrice * rate).toFixed(6), |
| cachePrice: (cacheRatioPrice * rate).toFixed(6), |
| }, |
| ); |
| } else if (audioInputSeperatePrice && audioInputTokens > 0) { |
| inputDesc = i18next.t( |
| '(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}', |
| { |
| nonAudioInput: inputTokens - audioInputTokens, |
| audioInput: audioInputTokens, |
| symbol: symbol, |
| price: (inputRatioPrice * rate).toFixed(6), |
| audioPrice: (audioInputPrice * rate).toFixed(6), |
| }, |
| ); |
| } else { |
| inputDesc = i18next.t( |
| '(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', |
| { |
| input: inputTokens, |
| symbol: symbol, |
| price: (inputRatioPrice * rate).toFixed(6), |
| }, |
| ); |
| } |
| |
| const outputDesc = i18next.t( |
| '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}', |
| { |
| completion: completionTokens, |
| symbol: symbol, |
| compPrice: (completionRatioPrice * rate).toFixed(6), |
| ratio: groupRatio, |
| ratioType: ratioLabel, |
| }, |
| ); |
| |
| const extraServices = [ |
| webSearch && webSearchCallCount > 0 |
| ? i18next.t( |
| ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', |
| { |
| count: webSearchCallCount, |
| symbol: symbol, |
| price: (webSearchPrice * rate).toFixed(6), |
| ratio: groupRatio, |
| ratioType: ratioLabel, |
| }, |
| ) |
| : '', |
| fileSearch && fileSearchCallCount > 0 |
| ? i18next.t( |
| ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}', |
| { |
| count: fileSearchCallCount, |
| symbol: symbol, |
| price: (fileSearchPrice * rate).toFixed(6), |
| ratio: groupRatio, |
| ratioType: ratioLabel, |
| }, |
| ) |
| : '', |
| imageGenerationCall && imageGenerationCallPrice > 0 |
| ? i18next.t( |
| ' + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}', |
| { |
| symbol: symbol, |
| price: (imageGenerationCallPrice * rate).toFixed(6), |
| ratio: groupRatio, |
| ratioType: ratioLabel, |
| }, |
| ) |
| : '', |
| ].join(''); |
| |
| return i18next.t( |
| '{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}', |
| { |
| inputDesc, |
| outputDesc, |
| extraServices, |
| symbol, |
| total: (price * rate).toFixed(6), |
| }, |
| ); |
| })()} |
| </p> |
| <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| </article> |
| </> |
| ); |
| } |
|
|
| if (modelPrice !== -1) { |
| const displayPrice = (modelPrice * rate).toFixed(6); |
| const displayTotal = (modelPrice * groupRatio * rate).toFixed(6); |
| return i18next.t( |
| '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', |
| { |
| symbol: symbol, |
| price: displayPrice, |
| ratio: groupRatio, |
| total: displayTotal, |
| ratioType: ratioLabel, |
| }, |
| ); |
| } |
|
|
| if (completionRatio === undefined) { |
| completionRatio = 0; |
| } |
|
|
| const modelRatioValue = formatRatioValue(modelRatio); |
| const completionRatioValue = formatRatioValue(completionRatio); |
| const cacheRatioValue = formatRatioValue(cacheRatio); |
| const imageRatioValue = formatRatioValue(imageRatio); |
| const inputRatioPrice = modelRatio * 2.0; |
| const completionRatioPrice = modelRatio * 2.0 * completionRatioValue; |
| const audioRatioValue = |
| audioInputSeperatePrice && audioInputPrice > 0 |
| ? formatRatioValue(audioInputPrice / inputRatioPrice) |
| : null; |
|
|
| const textInputTokens = Math.max(inputTokens - cacheTokens - audioInputTokens, 0); |
| const imageInputTokens = image && imageOutputTokens > 0 ? imageOutputTokens : 0; |
| const cacheInputTokens = cacheTokens; |
|
|
| const textInputAmount = |
| (textInputTokens / 1000000) * inputRatioPrice * groupRatio; |
| const cacheInputAmount = |
| (cacheInputTokens / 1000000) * |
| inputRatioPrice * |
| cacheRatioValue * |
| groupRatio; |
| const imageInputAmount = |
| (imageInputTokens / 1000000) * |
| inputRatioPrice * |
| imageRatioValue * |
| groupRatio; |
| const audioInputAmount = |
| (audioInputTokens / 1000000) * audioInputPrice * groupRatio; |
| const completionAmount = |
| (completionTokens / 1000000) * completionRatioPrice * groupRatio; |
| const webSearchAmount = (webSearchCallCount / 1000) * webSearchPrice * groupRatio; |
| const fileSearchAmount = |
| (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio; |
| const imageGenerationAmount = imageGenerationCallPrice * groupRatio; |
|
|
| const totalAmount = |
| textInputAmount + |
| cacheInputAmount + |
| imageInputAmount + |
| audioInputAmount + |
| completionAmount + |
| webSearchAmount + |
| fileSearchAmount + |
| imageGenerationAmount; |
|
|
| return ( |
| <> |
| <article> |
| <p> |
| {[ |
| i18next.t('模型倍率 {{modelRatio}}', { |
| modelRatio: modelRatioValue, |
| }), |
| i18next.t('补全倍率 {{completionRatio}}', { |
| completionRatio: completionRatioValue, |
| }), |
| cacheInputTokens > 0 |
| ? i18next.t('缓存倍率 {{cacheRatio}}', { |
| cacheRatio: cacheRatioValue, |
| }) |
| : null, |
| imageInputTokens > 0 |
| ? i18next.t('图片倍率 {{imageRatio}}', { |
| imageRatio: imageRatioValue, |
| }) |
| : null, |
| audioRatioValue !== null |
| ? i18next.t('音频倍率 {{audioRatio}}', { |
| audioRatio: audioRatioValue, |
| }) |
| : null, |
| i18next.t('{{ratioType}} {{ratio}}', { |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| }), |
| ] |
| .filter(Boolean) |
| .join(',')} |
| </p> |
| {textInputTokens > 0 && ( |
| <p> |
| {i18next.t( |
| '普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: textInputTokens, |
| modelRatio: modelRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd(textInputAmount), |
| }, |
| )} |
| </p> |
| )} |
| {cacheInputTokens > 0 && ( |
| <p> |
| {i18next.t( |
| '缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: cacheInputTokens, |
| modelRatio: modelRatioValue, |
| cacheRatio: cacheRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd(cacheInputAmount), |
| }, |
| )} |
| </p> |
| )} |
| {imageInputTokens > 0 && ( |
| <p> |
| {i18next.t( |
| '图片输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: imageInputTokens, |
| modelRatio: modelRatioValue, |
| imageRatio: imageRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd(imageInputAmount), |
| }, |
| )} |
| </p> |
| )} |
| {audioInputTokens > 0 && audioRatioValue !== null && ( |
| <p> |
| {i18next.t( |
| '音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: audioInputTokens, |
| modelRatio: modelRatioValue, |
| audioRatio: audioRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd(audioInputAmount), |
| }, |
| )} |
| </p> |
| )} |
| <p> |
| {i18next.t( |
| '输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: completionTokens, |
| modelRatio: modelRatioValue, |
| completionRatio: completionRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd(completionAmount), |
| }, |
| )} |
| </p> |
| {webSearch && webSearchCallCount > 0 && ( |
| <p> |
| {i18next.t( |
| 'Web 搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| count: webSearchCallCount, |
| price: renderDisplayAmountFromUsd(webSearchPrice), |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd(webSearchAmount), |
| }, |
| )} |
| </p> |
| )} |
| {fileSearch && fileSearchCallCount > 0 && ( |
| <p> |
| {i18next.t( |
| '文件搜索:{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| count: fileSearchCallCount, |
| price: renderDisplayAmountFromUsd(fileSearchPrice), |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd(fileSearchAmount), |
| }, |
| )} |
| </p> |
| )} |
| {imageGenerationCall && imageGenerationCallPrice > 0 && ( |
| <p> |
| {i18next.t( |
| '图片生成:1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| price: renderDisplayAmountFromUsd(imageGenerationCallPrice), |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd(imageGenerationAmount), |
| }, |
| )} |
| </p> |
| )} |
| <p> |
| {i18next.t('合计:{{total}}', { |
| total: renderDisplayAmountFromUsd(totalAmount), |
| })} |
| </p> |
| <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| </article> |
| </> |
| ); |
| } |
|
|
| export function renderLogContent( |
| modelRatio, |
| completionRatio, |
| modelPrice = -1, |
| groupRatio, |
| user_group_ratio, |
| cacheRatio = 1.0, |
| image = false, |
| imageRatio = 1.0, |
| webSearch = false, |
| webSearchCallCount = 0, |
| fileSearch = false, |
| fileSearchCallCount = 0, |
| displayMode = 'price', |
| ) { |
| const { |
| ratio, |
| label: ratioLabel, |
| useUserGroupRatio: useUserGroupRatio, |
| } = getEffectiveRatio(groupRatio, user_group_ratio); |
|
|
| |
| const { symbol, rate } = getCurrencyConfig(); |
|
|
| if (isPriceDisplayMode(displayMode, modelPrice)) { |
| if (modelPrice !== -1) { |
| return joinBillingSummary([ |
| i18next.t('模型价格 {{symbol}}{{price}} / 次', { |
| symbol, |
| price: (modelPrice * rate).toFixed(6), |
| }), |
| getGroupRatioText(groupRatio, user_group_ratio), |
| ]); |
| } |
|
|
| const parts = [ |
| i18next.t('输入价格 {{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (modelRatio * 2.0 * rate).toFixed(6), |
| }), |
| i18next.t('补全价格 {{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (modelRatio * 2.0 * completionRatio * rate).toFixed(6), |
| }), |
| ]; |
| appendPricePart(parts, cacheRatio !== 1.0, '缓存读取价格 {{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (modelRatio * 2.0 * cacheRatio * rate).toFixed(6), |
| }); |
| appendPricePart(parts, image, '图片输入价格 {{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (modelRatio * 2.0 * imageRatio * rate).toFixed(6), |
| }); |
| appendPricePart(parts, webSearch, 'Web 搜索调用 {{webSearchCallCount}} 次', { |
| webSearchCallCount, |
| }); |
| appendPricePart(parts, fileSearch, '文件搜索调用 {{fileSearchCallCount}} 次', { |
| fileSearchCallCount, |
| }); |
| parts.push(getGroupRatioText(groupRatio, user_group_ratio)); |
| return joinBillingSummary(parts); |
| } |
|
|
| if (modelPrice !== -1) { |
| return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { |
| symbol: symbol, |
| price: (modelPrice * rate).toFixed(6), |
| ratioType: ratioLabel, |
| ratio, |
| }); |
| } else { |
| if (image) { |
| return i18next.t( |
| '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}', |
| { |
| modelRatio: modelRatio, |
| cacheRatio: cacheRatio, |
| completionRatio: completionRatio, |
| imageRatio: imageRatio, |
| ratioType: ratioLabel, |
| ratio, |
| }, |
| ); |
| } else if (webSearch) { |
| return i18next.t( |
| '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次', |
| { |
| modelRatio: modelRatio, |
| cacheRatio: cacheRatio, |
| completionRatio: completionRatio, |
| ratioType: ratioLabel, |
| ratio, |
| webSearchCallCount, |
| }, |
| ); |
| } else { |
| return i18next.t( |
| '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}', |
| { |
| modelRatio: modelRatio, |
| cacheRatio: cacheRatio, |
| completionRatio: completionRatio, |
| ratioType: ratioLabel, |
| ratio, |
| }, |
| ); |
| } |
| } |
| } |
|
|
| export function renderModelPriceSimple( |
| modelRatio, |
| modelPrice = -1, |
| groupRatio, |
| user_group_ratio, |
| cacheTokens = 0, |
| cacheRatio = 1.0, |
| cacheCreationTokens = 0, |
| cacheCreationRatio = 1.0, |
| cacheCreationTokens5m = 0, |
| cacheCreationRatio5m = 1.0, |
| cacheCreationTokens1h = 0, |
| cacheCreationRatio1h = 1.0, |
| image = false, |
| imageRatio = 1.0, |
| isSystemPromptOverride = false, |
| provider = 'openai', |
| displayMode = 'price', |
| ) { |
| return renderPriceSimpleCore({ |
| modelRatio, |
| modelPrice, |
| groupRatio, |
| user_group_ratio, |
| cacheTokens, |
| cacheRatio, |
| cacheCreationTokens, |
| cacheCreationRatio, |
| cacheCreationTokens5m, |
| cacheCreationRatio5m, |
| cacheCreationTokens1h, |
| cacheCreationRatio1h, |
| image, |
| imageRatio, |
| isSystemPromptOverride, |
| displayMode, |
| }); |
| } |
|
|
| export function renderAudioModelPrice( |
| inputTokens, |
| completionTokens, |
| modelRatio, |
| modelPrice = -1, |
| completionRatio, |
| audioInputTokens, |
| audioCompletionTokens, |
| audioRatio, |
| audioCompletionRatio, |
| groupRatio, |
| user_group_ratio, |
| cacheTokens = 0, |
| cacheRatio = 1.0, |
| displayMode = 'price', |
| ) { |
| const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| groupRatio, |
| user_group_ratio, |
| ); |
| groupRatio = effectiveGroupRatio; |
|
|
| |
| const { symbol, rate } = getCurrencyConfig(); |
|
|
| if (!shouldUseRatioBillingProcess(modelPrice)) { |
| if (modelPrice !== -1) { |
| return ( |
| <> |
| <article> |
| <p> |
| {i18next.t('模型价格:{{symbol}}{{price}} / 次', { |
| symbol, |
| price: (modelPrice * rate).toFixed(6), |
| })} |
| </p> |
| <p> |
| {i18next.t( |
| '模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', |
| { |
| symbol, |
| price: (modelPrice * rate).toFixed(6), |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| total: (modelPrice * groupRatio * rate).toFixed(6), |
| }, |
| )} |
| </p> |
| <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| </article> |
| </> |
| ); |
| } |
|
|
| if (completionRatio === undefined) { |
| completionRatio = 0; |
| } |
| audioRatio = parseFloat(audioRatio).toFixed(6); |
| const inputRatioPrice = modelRatio * 2.0; |
| const completionRatioPrice = modelRatio * 2.0 * completionRatio; |
| const textPrice = |
| ((inputTokens - cacheTokens + cacheTokens * cacheRatio) / 1000000) * |
| inputRatioPrice * |
| groupRatio + |
| (completionTokens / 1000000) * completionRatioPrice * groupRatio; |
| const audioPrice = |
| (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio + |
| (audioCompletionTokens / 1000000) * |
| inputRatioPrice * |
| audioRatio * |
| audioCompletionRatio * |
| groupRatio; |
| const totalPrice = textPrice + audioPrice; |
|
|
| return ( |
| <> |
| <article> |
| <p> |
| {i18next.t('输入价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (inputRatioPrice * rate).toFixed(6), |
| })} |
| </p> |
| <p> |
| {i18next.t('补全价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (completionRatioPrice * rate).toFixed(6), |
| })} |
| </p> |
| {cacheTokens > 0 && ( |
| <p> |
| {i18next.t('缓存读取价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (inputRatioPrice * cacheRatio * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| <p> |
| {i18next.t('音频输入价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (inputRatioPrice * audioRatio * rate).toFixed(6), |
| })} |
| </p> |
| <p> |
| {i18next.t('音频补全价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: ( |
| inputRatioPrice * |
| audioRatio * |
| audioCompletionRatio * |
| rate |
| ).toFixed(6), |
| })} |
| </p> |
| <p> |
| {i18next.t( |
| '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', |
| { |
| input: inputTokens, |
| completion: completionTokens, |
| audioInput: audioInputTokens, |
| audioCompletion: audioCompletionTokens, |
| textInputPrice: (inputRatioPrice * rate).toFixed(6), |
| textCompPrice: (completionRatioPrice * rate).toFixed(6), |
| audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed(6), |
| audioCompPrice: ( |
| audioRatio * |
| audioCompletionRatio * |
| inputRatioPrice * |
| rate |
| ).toFixed(6), |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| symbol, |
| total: (totalPrice * rate).toFixed(6), |
| }, |
| )} |
| </p> |
| <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| </article> |
| </> |
| ); |
| } |
|
|
| |
| if (modelPrice !== -1) { |
| return i18next.t( |
| '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', |
| { |
| symbol: symbol, |
| price: (modelPrice * rate).toFixed(6), |
| ratio: groupRatio, |
| total: (modelPrice * groupRatio * rate).toFixed(6), |
| ratioType: ratioLabel, |
| }, |
| ); |
| } |
|
|
| if (completionRatio === undefined) { |
| completionRatio = 0; |
| } |
|
|
| const modelRatioValue = formatRatioValue(modelRatio); |
| const completionRatioValue = formatRatioValue(completionRatio); |
| const cacheRatioValue = formatRatioValue(cacheRatio); |
| const audioRatioValue = formatRatioValue(audioRatio); |
| const audioCompletionRatioValue = formatRatioValue(audioCompletionRatio); |
|
|
| const inputRatioPrice = modelRatio * 2.0; |
| const completionRatioPrice = modelRatio * 2.0 * completionRatioValue; |
|
|
| const effectiveInputTokens = |
| inputTokens - cacheTokens + cacheTokens * cacheRatioValue; |
|
|
| const textPrice = |
| (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + |
| (completionTokens / 1000000) * completionRatioPrice * groupRatio; |
| const audioPrice = |
| (audioInputTokens / 1000000) * inputRatioPrice * audioRatioValue * groupRatio + |
| (audioCompletionTokens / 1000000) * |
| inputRatioPrice * |
| audioRatioValue * |
| audioCompletionRatioValue * |
| groupRatio; |
| const totalPrice = textPrice + audioPrice; |
|
|
| return ( |
| <> |
| <article> |
| <p> |
| {i18next.t( |
| '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},音频倍率 {{audioRatio}},音频补全倍率 {{audioCompletionRatio}},{{cachePart}}{{ratioType}} {{ratio}}', |
| { |
| modelRatio: modelRatioValue, |
| completionRatio: completionRatioValue, |
| audioRatio: audioRatioValue, |
| audioCompletionRatio: audioCompletionRatioValue, |
| cachePart: |
| cacheTokens > 0 |
| ? `${i18next.t('缓存倍率')} ${cacheRatioValue},` |
| : '', |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| }, |
| )} |
| </p> |
| <p> |
| {i18next.t( |
| '普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: Math.max(inputTokens - cacheTokens, 0), |
| modelRatio: modelRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((Math.max(inputTokens - cacheTokens, 0) / 1000000) * |
| inputRatioPrice * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| {cacheTokens > 0 && ( |
| <p> |
| {i18next.t( |
| '缓存输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: cacheTokens, |
| modelRatio: modelRatioValue, |
| cacheRatio: cacheRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((cacheTokens / 1000000) * |
| inputRatioPrice * |
| cacheRatioValue * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| )} |
| <p> |
| {i18next.t( |
| '文字输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 补全倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: completionTokens, |
| modelRatio: modelRatioValue, |
| completionRatio: completionRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((completionTokens / 1000000) * |
| inputRatioPrice * |
| completionRatioValue * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| <p> |
| {i18next.t( |
| '音频输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: audioInputTokens, |
| modelRatio: modelRatioValue, |
| audioRatio: audioRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((audioInputTokens / 1000000) * |
| inputRatioPrice * |
| audioRatioValue * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| <p> |
| {i18next.t( |
| '音频输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频补全倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: audioCompletionTokens, |
| modelRatio: modelRatioValue, |
| audioRatio: audioRatioValue, |
| audioCompletionRatio: audioCompletionRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((audioCompletionTokens / 1000000) * |
| inputRatioPrice * |
| audioRatioValue * |
| audioCompletionRatioValue * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| <p> |
| {i18next.t( |
| '合计:文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}', |
| { |
| textTotal: renderDisplayAmountFromUsd(textPrice), |
| audioTotal: renderDisplayAmountFromUsd(audioPrice), |
| total: renderDisplayAmountFromUsd(totalPrice), |
| }, |
| )} |
| </p> |
| <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| </article> |
| </> |
| ); |
| } |
|
|
| export function renderQuotaWithPrompt(quota, digits) { |
| const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; |
| if (quotaDisplayType !== 'TOKENS') { |
| return i18next.t('等价金额:') + renderQuota(quota, digits); |
| } |
| return ''; |
| } |
|
|
| export function renderClaudeModelPrice( |
| inputTokens, |
| completionTokens, |
| modelRatio, |
| modelPrice = -1, |
| completionRatio, |
| groupRatio, |
| user_group_ratio, |
| cacheTokens = 0, |
| cacheRatio = 1.0, |
| cacheCreationTokens = 0, |
| cacheCreationRatio = 1.0, |
| cacheCreationTokens5m = 0, |
| cacheCreationRatio5m = 1.0, |
| cacheCreationTokens1h = 0, |
| cacheCreationRatio1h = 1.0, |
| displayMode = 'price', |
| ) { |
| const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| groupRatio, |
| user_group_ratio, |
| ); |
| groupRatio = effectiveGroupRatio; |
|
|
| |
| const { symbol, rate } = getCurrencyConfig(); |
|
|
| if (!shouldUseRatioBillingProcess(modelPrice)) { |
| if (modelPrice !== -1) { |
| return ( |
| <> |
| <article> |
| <p> |
| {i18next.t('模型价格:{{symbol}}{{price}} / 次', { |
| symbol, |
| price: (modelPrice * rate).toFixed(6), |
| })} |
| </p> |
| <p> |
| {i18next.t( |
| '模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', |
| { |
| symbol, |
| price: (modelPrice * rate).toFixed(6), |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| total: (modelPrice * groupRatio * rate).toFixed(6), |
| }, |
| )} |
| </p> |
| <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| </article> |
| </> |
| ); |
| } |
|
|
| if (completionRatio === undefined) { |
| completionRatio = 0; |
| } |
|
|
| const inputRatioPrice = modelRatio * 2.0; |
| const completionRatioPrice = modelRatio * 2.0 * completionRatio; |
| const cacheRatioPrice = modelRatio * 2.0 * cacheRatio; |
| const cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio; |
| const cacheCreationRatioPrice5m = modelRatio * 2.0 * cacheCreationRatio5m; |
| const cacheCreationRatioPrice1h = modelRatio * 2.0 * cacheCreationRatio1h; |
| const hasSplitCacheCreation = |
| cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; |
| const legacyCacheCreationTokens = hasSplitCacheCreation |
| ? 0 |
| : cacheCreationTokens; |
| const effectiveInputTokens = |
| inputTokens + |
| cacheTokens * cacheRatio + |
| legacyCacheCreationTokens * cacheCreationRatio + |
| cacheCreationTokens5m * cacheCreationRatio5m + |
| cacheCreationTokens1h * cacheCreationRatio1h; |
| const price = |
| (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + |
| (completionTokens / 1000000) * completionRatioPrice * groupRatio; |
| const inputUnitPrice = inputRatioPrice * rate; |
| const completionUnitPrice = completionRatioPrice * rate; |
| const cacheUnitPrice = cacheRatioPrice * rate; |
| const cacheCreationUnitPrice = cacheCreationRatioPrice * rate; |
| const cacheCreationUnitPrice5m = cacheCreationRatioPrice5m * rate; |
| const cacheCreationUnitPrice1h = cacheCreationRatioPrice1h * rate; |
| const cacheCreationUnitPriceTotal = |
| cacheCreationUnitPrice5m + cacheCreationUnitPrice1h; |
| const shouldShowCache = cacheTokens > 0; |
| const shouldShowLegacyCacheCreation = |
| !hasSplitCacheCreation && cacheCreationTokens > 0; |
| const shouldShowCacheCreation5m = |
| hasSplitCacheCreation && cacheCreationTokens5m > 0; |
| const shouldShowCacheCreation1h = |
| hasSplitCacheCreation && cacheCreationTokens1h > 0; |
|
|
| const breakdownSegments = [ |
| i18next.t('提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', { |
| input: inputTokens, |
| symbol, |
| price: inputUnitPrice.toFixed(6), |
| }), |
| ]; |
|
|
| if (shouldShowCache) { |
| breakdownSegments.push( |
| i18next.t('缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}', { |
| tokens: cacheTokens, |
| symbol, |
| price: cacheUnitPrice.toFixed(6), |
| }), |
| ); |
| } |
|
|
| if (shouldShowLegacyCacheCreation) { |
| breakdownSegments.push( |
| i18next.t('缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}', { |
| tokens: cacheCreationTokens, |
| symbol, |
| price: cacheCreationUnitPrice.toFixed(6), |
| }), |
| ); |
| } |
|
|
| if (shouldShowCacheCreation5m) { |
| breakdownSegments.push( |
| i18next.t('5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}', { |
| tokens: cacheCreationTokens5m, |
| symbol, |
| price: cacheCreationUnitPrice5m.toFixed(6), |
| }), |
| ); |
| } |
|
|
| if (shouldShowCacheCreation1h) { |
| breakdownSegments.push( |
| i18next.t('1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}', { |
| tokens: cacheCreationTokens1h, |
| symbol, |
| price: cacheCreationUnitPrice1h.toFixed(6), |
| }), |
| ); |
| } |
|
|
| breakdownSegments.push( |
| i18next.t( |
| '补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}', |
| { |
| completion: completionTokens, |
| symbol, |
| price: completionUnitPrice.toFixed(6), |
| }, |
| ), |
| ); |
|
|
| const breakdownText = breakdownSegments.join(' + '); |
|
|
| return ( |
| <> |
| <article> |
| <p> |
| {i18next.t('输入价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (inputRatioPrice * rate).toFixed(6), |
| })} |
| </p> |
| <p> |
| {i18next.t('补全价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (completionRatioPrice * rate).toFixed(6), |
| })} |
| </p> |
| {cacheTokens > 0 && ( |
| <p> |
| {i18next.t('缓存读取价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (cacheRatioPrice * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| {!hasSplitCacheCreation && cacheCreationTokens > 0 && ( |
| <p> |
| {i18next.t('缓存创建价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (cacheCreationRatioPrice * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| {hasSplitCacheCreation && cacheCreationTokens5m > 0 && ( |
| <p> |
| {i18next.t('5m缓存创建价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (cacheCreationRatioPrice5m * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| {hasSplitCacheCreation && cacheCreationTokens1h > 0 && ( |
| <p> |
| {i18next.t('1h缓存创建价格:{{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (cacheCreationRatioPrice1h * rate).toFixed(6), |
| })} |
| </p> |
| )} |
| <p> |
| {i18next.t( |
| '{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}', |
| { |
| breakdown: breakdownText, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| symbol, |
| total: (price * rate).toFixed(6), |
| }, |
| )} |
| </p> |
| <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| </article> |
| </> |
| ); |
| } |
|
|
| if (modelPrice !== -1) { |
| return i18next.t( |
| '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}', |
| { |
| symbol: symbol, |
| price: (modelPrice * rate).toFixed(6), |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| total: (modelPrice * groupRatio * rate).toFixed(6), |
| }, |
| ); |
| } |
|
|
| if (completionRatio === undefined) { |
| completionRatio = 0; |
| } |
|
|
| const modelRatioValue = formatRatioValue(modelRatio); |
| const completionRatioValue = formatRatioValue(completionRatio); |
| const cacheRatioValue = formatRatioValue(cacheRatio); |
| const cacheCreationRatioValue = formatRatioValue(cacheCreationRatio); |
| const cacheCreationRatio5mValue = formatRatioValue(cacheCreationRatio5m); |
| const cacheCreationRatio1hValue = formatRatioValue(cacheCreationRatio1h); |
|
|
| const inputRatioPrice = modelRatio * 2.0; |
| const completionRatioPrice = modelRatio * 2.0 * completionRatioValue; |
|
|
| const hasSplitCacheCreation = |
| cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; |
| const shouldShowCache = cacheTokens > 0; |
| const shouldShowLegacyCacheCreation = |
| !hasSplitCacheCreation && cacheCreationTokens > 0; |
| const shouldShowCacheCreation5m = |
| hasSplitCacheCreation && cacheCreationTokens5m > 0; |
| const shouldShowCacheCreation1h = |
| hasSplitCacheCreation && cacheCreationTokens1h > 0; |
|
|
| const legacyCacheCreationTokens = hasSplitCacheCreation ? 0 : cacheCreationTokens; |
| const effectiveInputTokens = |
| inputTokens + |
| cacheTokens * cacheRatioValue + |
| legacyCacheCreationTokens * cacheCreationRatioValue + |
| cacheCreationTokens5m * cacheCreationRatio5mValue + |
| cacheCreationTokens1h * cacheCreationRatio1hValue; |
|
|
| const totalAmount = |
| (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + |
| (completionTokens / 1000000) * completionRatioPrice * groupRatio; |
|
|
| return ( |
| <> |
| <article> |
| <p> |
| {i18next.t( |
| '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},{{ratioType}} {{ratio}}', |
| { |
| modelRatio: modelRatioValue, |
| completionRatio: completionRatioValue, |
| cacheRatio: cacheRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| }, |
| )} |
| </p> |
| <p> |
| {hasSplitCacheCreation |
| ? i18next.t( |
| '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}', |
| { |
| cacheCreationRatio5m: cacheCreationRatio5mValue, |
| cacheCreationRatio1h: cacheCreationRatio1hValue, |
| }, |
| ) |
| : i18next.t('缓存创建倍率 {{cacheCreationRatio}}', { |
| cacheCreationRatio: cacheCreationRatioValue, |
| })} |
| </p> |
| <p> |
| {i18next.t( |
| '普通输入:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: inputTokens, |
| modelRatio: modelRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((inputTokens / 1000000) * inputRatioPrice * groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| {shouldShowCache && ( |
| <p> |
| {i18next.t( |
| '缓存读取:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: cacheTokens, |
| modelRatio: modelRatioValue, |
| cacheRatio: cacheRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((cacheTokens / 1000000) * |
| inputRatioPrice * |
| cacheRatioValue * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| )} |
| {shouldShowLegacyCacheCreation && ( |
| <p> |
| {i18next.t( |
| '缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: cacheCreationTokens, |
| modelRatio: modelRatioValue, |
| cacheCreationRatio: cacheCreationRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((cacheCreationTokens / 1000000) * |
| inputRatioPrice * |
| cacheCreationRatioValue * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| )} |
| {shouldShowCacheCreation5m && ( |
| <p> |
| {i18next.t( |
| '5m缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: cacheCreationTokens5m, |
| modelRatio: modelRatioValue, |
| cacheCreationRatio5m: cacheCreationRatio5mValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((cacheCreationTokens5m / 1000000) * |
| inputRatioPrice * |
| cacheCreationRatio5mValue * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| )} |
| {shouldShowCacheCreation1h && ( |
| <p> |
| {i18next.t( |
| '1h缓存创建:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: cacheCreationTokens1h, |
| modelRatio: modelRatioValue, |
| cacheCreationRatio1h: cacheCreationRatio1hValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((cacheCreationTokens1h / 1000000) * |
| inputRatioPrice * |
| cacheCreationRatio1hValue * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| )} |
| <p> |
| {i18next.t('补全 {{completion}} tokens * 输出倍率 {{completionRatio}}', { |
| completion: completionTokens, |
| completionRatio: completionRatioValue, |
| })} |
| </p> |
| <p> |
| {i18next.t( |
| '输出:{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}', |
| { |
| tokens: completionTokens, |
| modelRatio: modelRatioValue, |
| completionRatio: completionRatioValue, |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| amount: renderDisplayAmountFromUsd( |
| ((completionTokens / 1000000) * |
| inputRatioPrice * |
| completionRatioValue * |
| groupRatio), |
| ), |
| }, |
| )} |
| </p> |
| <p> |
| {i18next.t('合计:{{total}}', { |
| total: renderDisplayAmountFromUsd(totalAmount), |
| }, |
| )} |
| </p> |
| <p>{i18next.t('仅供参考,以实际扣费为准')}</p> |
| </article> |
| </> |
| ); |
| } |
|
|
| export function renderClaudeLogContent( |
| modelRatio, |
| completionRatio, |
| modelPrice = -1, |
| groupRatio, |
| user_group_ratio, |
| cacheRatio = 1.0, |
| cacheCreationRatio = 1.0, |
| cacheCreationTokens5m = 0, |
| cacheCreationRatio5m = 1.0, |
| cacheCreationTokens1h = 0, |
| cacheCreationRatio1h = 1.0, |
| displayMode = 'price', |
| ) { |
| const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio( |
| groupRatio, |
| user_group_ratio, |
| ); |
| groupRatio = effectiveGroupRatio; |
|
|
| |
| const { symbol, rate } = getCurrencyConfig(); |
|
|
| if (isPriceDisplayMode(displayMode, modelPrice)) { |
| if (modelPrice !== -1) { |
| return joinBillingSummary([ |
| i18next.t('模型价格 {{symbol}}{{price}} / 次', { |
| symbol, |
| price: (modelPrice * rate).toFixed(6), |
| }), |
| getGroupRatioText(groupRatio, user_group_ratio), |
| ]); |
| } |
|
|
| const parts = [ |
| i18next.t('输入价格 {{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (modelRatio * 2.0 * rate).toFixed(6), |
| }), |
| i18next.t('补全价格 {{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (modelRatio * 2.0 * completionRatio * rate).toFixed(6), |
| }), |
| i18next.t('缓存读取价格 {{symbol}}{{price}} / 1M tokens', { |
| symbol, |
| price: (modelRatio * 2.0 * cacheRatio * rate).toFixed(6), |
| }), |
| ]; |
| const hasSplitCacheCreation = |
| cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; |
| appendPricePart( |
| parts, |
| hasSplitCacheCreation && cacheCreationTokens5m > 0, |
| '5m缓存创建价格 {{symbol}}{{price}} / 1M tokens', |
| { |
| symbol, |
| price: (modelRatio * 2.0 * cacheCreationRatio5m * rate).toFixed(6), |
| }, |
| ); |
| appendPricePart( |
| parts, |
| hasSplitCacheCreation && cacheCreationTokens1h > 0, |
| '1h缓存创建价格 {{symbol}}{{price}} / 1M tokens', |
| { |
| symbol, |
| price: (modelRatio * 2.0 * cacheCreationRatio1h * rate).toFixed(6), |
| }, |
| ); |
| appendPricePart( |
| parts, |
| !hasSplitCacheCreation, |
| '缓存创建价格 {{symbol}}{{price}} / 1M tokens', |
| { |
| symbol, |
| price: (modelRatio * 2.0 * cacheCreationRatio * rate).toFixed(6), |
| }, |
| ); |
| parts.push(getGroupRatioText(groupRatio, user_group_ratio)); |
| return joinBillingSummary(parts); |
| } |
|
|
| if (modelPrice !== -1) { |
| return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', { |
| symbol: symbol, |
| price: (modelPrice * rate).toFixed(6), |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| }); |
| } else { |
| const hasSplitCacheCreation = |
| cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0; |
| const shouldShowCacheCreation5m = |
| hasSplitCacheCreation && cacheCreationTokens5m > 0; |
| const shouldShowCacheCreation1h = |
| hasSplitCacheCreation && cacheCreationTokens1h > 0; |
|
|
| let cacheCreationPart = null; |
| if (hasSplitCacheCreation) { |
| if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) { |
| cacheCreationPart = i18next.t( |
| '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}', |
| { |
| cacheCreationRatio5m, |
| cacheCreationRatio1h, |
| }, |
| ); |
| } else if (shouldShowCacheCreation5m) { |
| cacheCreationPart = i18next.t( |
| '缓存创建倍率 5m {{cacheCreationRatio5m}}', |
| { |
| cacheCreationRatio5m, |
| }, |
| ); |
| } else if (shouldShowCacheCreation1h) { |
| cacheCreationPart = i18next.t( |
| '缓存创建倍率 1h {{cacheCreationRatio1h}}', |
| { |
| cacheCreationRatio1h, |
| }, |
| ); |
| } |
| } |
|
|
| if (!cacheCreationPart) { |
| cacheCreationPart = i18next.t('缓存创建倍率 {{cacheCreationRatio}}', { |
| cacheCreationRatio, |
| }); |
| } |
|
|
| const parts = [ |
| i18next.t('模型倍率 {{modelRatio}}', { modelRatio }), |
| i18next.t('输出倍率 {{completionRatio}}', { completionRatio }), |
| i18next.t('缓存倍率 {{cacheRatio}}', { cacheRatio }), |
| cacheCreationPart, |
| i18next.t('{{ratioType}} {{ratio}}', { |
| ratioType: ratioLabel, |
| ratio: groupRatio, |
| }), |
| ]; |
|
|
| return parts.join(','); |
| } |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| export function rehypeSplitWordsIntoSpans(options = {}) { |
| const { previousContentLength = 0 } = options; |
|
|
| return (tree) => { |
| let currentCharCount = 0; |
|
|
| visit(tree, 'element', (node) => { |
| if ( |
| ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes( |
| node.tagName, |
| ) && |
| node.children |
| ) { |
| const newChildren = []; |
| node.children.forEach((child) => { |
| if (child.type === 'text') { |
| try { |
| |
| const segmenter = new Intl.Segmenter('zh', { |
| granularity: 'word', |
| }); |
| const segments = segmenter.segment(child.value); |
|
|
| Array.from(segments) |
| .map((seg) => seg.segment) |
| .filter(Boolean) |
| .forEach((word) => { |
| const wordStartPos = currentCharCount; |
| const wordEndPos = currentCharCount + word.length; |
|
|
| |
| const isNewContent = wordStartPos >= previousContentLength; |
|
|
| newChildren.push({ |
| type: 'element', |
| tagName: 'span', |
| properties: { |
| className: isNewContent ? ['animate-fade-in'] : [], |
| }, |
| children: [{ type: 'text', value: word }], |
| }); |
|
|
| currentCharCount = wordEndPos; |
| }); |
| } catch (_) { |
| |
| const textStartPos = currentCharCount; |
| const isNewContent = textStartPos >= previousContentLength; |
|
|
| if (isNewContent) { |
| |
| newChildren.push({ |
| type: 'element', |
| tagName: 'span', |
| properties: { |
| className: ['animate-fade-in'], |
| }, |
| children: [{ type: 'text', value: child.value }], |
| }); |
| } else { |
| |
| newChildren.push(child); |
| } |
|
|
| currentCharCount += child.value.length; |
| } |
| } else { |
| newChildren.push(child); |
| } |
| }); |
| node.children = newChildren; |
| } |
| }); |
| }; |
| } |
|
|