You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

2318 lines
70 KiB

  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import i18next from 'i18next';
  16. import { Modal, Tag, Typography, Avatar } from '@douyinfe/semi-ui';
  17. import { copy, showSuccess } from './utils';
  18. import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile';
  19. import { visit } from 'unist-util-visit';
  20. import * as LobeIcons from '@lobehub/icons';
  21. import {
  22. OpenAI,
  23. Claude,
  24. Gemini,
  25. Moonshot,
  26. Zhipu,
  27. Qwen,
  28. DeepSeek,
  29. Minimax,
  30. Wenxin,
  31. Spark,
  32. Midjourney,
  33. Hunyuan,
  34. Cohere,
  35. Cloudflare,
  36. Ai360,
  37. Yi,
  38. Jina,
  39. Mistral,
  40. XAI,
  41. Ollama,
  42. Doubao,
  43. Suno,
  44. Xinference,
  45. OpenRouter,
  46. Dify,
  47. Coze,
  48. SiliconCloud,
  49. FastGPT,
  50. Kling,
  51. Jimeng,
  52. Perplexity,
  53. Replicate,
  54. } from '@lobehub/icons';
  55. import {
  56. LayoutDashboard,
  57. TerminalSquare,
  58. MessageSquare,
  59. Key,
  60. BarChart3,
  61. Image as ImageIcon,
  62. CheckSquare,
  63. CreditCard,
  64. Layers,
  65. Gift,
  66. User,
  67. Settings,
  68. CircleUser,
  69. Package,
  70. Server,
  71. CalendarClock,
  72. Activity,
  73. } from 'lucide-react';
  74. import {
  75. SiAtlassian,
  76. SiAuth0,
  77. SiAuthentik,
  78. SiBitbucket,
  79. SiDiscord,
  80. SiDropbox,
  81. SiFacebook,
  82. SiGitea,
  83. SiGithub,
  84. SiGitlab,
  85. SiGoogle,
  86. SiKeycloak,
  87. SiNextcloud,
  88. SiNotion,
  89. SiOkta,
  90. SiOpenid,
  91. SiReddit,
  92. SiSlack,
  93. SiTelegram,
  94. SiTwitch,
  95. SiWechat,
  96. SiX,
  97. } from 'react-icons/si';
  98. // 获取侧边栏Lucide图标组件
  99. export function getLucideIcon(key, selected = false) {
  100. const size = 16;
  101. const strokeWidth = 2;
  102. const SELECTED_COLOR = 'var(--semi-color-primary)';
  103. const iconColor = selected ? SELECTED_COLOR : 'currentColor';
  104. const commonProps = {
  105. size,
  106. strokeWidth,
  107. className: `transition-colors duration-200 ${selected ? 'transition-transform duration-200 scale-105' : ''}`,
  108. };
  109. // 根据不同的key返回不同的图标
  110. switch (key) {
  111. case 'detail':
  112. return <LayoutDashboard {...commonProps} color={iconColor} />;
  113. case 'playground':
  114. return <TerminalSquare {...commonProps} color={iconColor} />;
  115. case 'chat':
  116. return <MessageSquare {...commonProps} color={iconColor} />;
  117. case 'token':
  118. return <Key {...commonProps} color={iconColor} />;
  119. case 'log':
  120. return <BarChart3 {...commonProps} color={iconColor} />;
  121. case 'midjourney':
  122. return <ImageIcon {...commonProps} color={iconColor} />;
  123. case 'task':
  124. return <CheckSquare {...commonProps} color={iconColor} />;
  125. case 'topup':
  126. return <CreditCard {...commonProps} color={iconColor} />;
  127. case 'channel':
  128. return <Layers {...commonProps} color={iconColor} />;
  129. case 'redemption':
  130. return <Gift {...commonProps} color={iconColor} />;
  131. case 'user':
  132. case 'personal':
  133. return <User {...commonProps} color={iconColor} />;
  134. case 'models':
  135. return <Package {...commonProps} color={iconColor} />;
  136. case 'deployment':
  137. return <Server {...commonProps} color={iconColor} />;
  138. case 'subscription':
  139. return <CalendarClock {...commonProps} color={iconColor} />;
  140. case 'setting':
  141. return <Settings {...commonProps} color={iconColor} />;
  142. case 'monitoring':
  143. return <Activity {...commonProps} color={iconColor} />;
  144. default:
  145. return <CircleUser {...commonProps} color={iconColor} />;
  146. }
  147. }
  148. // 获取模型分类
  149. export const getModelCategories = (() => {
  150. let categoriesCache = null;
  151. let lastLocale = null;
  152. return (t) => {
  153. const currentLocale = i18next.language;
  154. if (categoriesCache && lastLocale === currentLocale) {
  155. return categoriesCache;
  156. }
  157. categoriesCache = {
  158. all: {
  159. label: t('全部模型'),
  160. icon: null,
  161. filter: () => true,
  162. },
  163. openai: {
  164. label: 'OpenAI',
  165. icon: <OpenAI />,
  166. filter: (model) =>
  167. model.model_name.toLowerCase().includes('gpt') ||
  168. model.model_name.toLowerCase().includes('dall-e') ||
  169. model.model_name.toLowerCase().includes('whisper') ||
  170. model.model_name.toLowerCase().includes('tts-1') ||
  171. model.model_name.toLowerCase().includes('text-embedding-3') ||
  172. model.model_name.toLowerCase().includes('text-moderation') ||
  173. model.model_name.toLowerCase().includes('babbage') ||
  174. model.model_name.toLowerCase().includes('davinci') ||
  175. model.model_name.toLowerCase().includes('curie') ||
  176. model.model_name.toLowerCase().includes('ada') ||
  177. model.model_name.toLowerCase().includes('o1') ||
  178. model.model_name.toLowerCase().includes('o3') ||
  179. model.model_name.toLowerCase().includes('o4'),
  180. },
  181. anthropic: {
  182. label: 'Anthropic',
  183. icon: <Claude.Color />,
  184. filter: (model) => model.model_name.toLowerCase().includes('claude'),
  185. },
  186. gemini: {
  187. label: 'Gemini',
  188. icon: <Gemini.Color />,
  189. filter: (model) =>
  190. model.model_name.toLowerCase().includes('gemini') ||
  191. model.model_name.toLowerCase().includes('gemma') ||
  192. model.model_name.toLowerCase().includes('learnlm') ||
  193. model.model_name.toLowerCase().startsWith('embedding-') ||
  194. model.model_name.toLowerCase().includes('text-embedding-004') ||
  195. model.model_name.toLowerCase().includes('imagen-4') ||
  196. model.model_name.toLowerCase().includes('veo-') ||
  197. model.model_name.toLowerCase().includes('aqa'),
  198. },
  199. moonshot: {
  200. label: 'Moonshot',
  201. icon: <Moonshot />,
  202. filter: (model) =>
  203. model.model_name.toLowerCase().includes('moonshot') ||
  204. model.model_name.toLowerCase().includes('kimi'),
  205. },
  206. zhipu: {
  207. label: t('智谱'),
  208. icon: <Zhipu.Color />,
  209. filter: (model) =>
  210. model.model_name.toLowerCase().includes('chatglm') ||
  211. model.model_name.toLowerCase().includes('glm-') ||
  212. model.model_name.toLowerCase().includes('cogview') ||
  213. model.model_name.toLowerCase().includes('cogvideo'),
  214. },
  215. qwen: {
  216. label: t('通义千问'),
  217. icon: <Qwen.Color />,
  218. filter: (model) => model.model_name.toLowerCase().includes('qwen'),
  219. },
  220. deepseek: {
  221. label: 'DeepSeek',
  222. icon: <DeepSeek.Color />,
  223. filter: (model) => model.model_name.toLowerCase().includes('deepseek'),
  224. },
  225. minimax: {
  226. label: 'MiniMax',
  227. icon: <Minimax.Color />,
  228. filter: (model) =>
  229. model.model_name.toLowerCase().includes('abab') ||
  230. model.model_name.toLowerCase().includes('minimax'),
  231. },
  232. baidu: {
  233. label: t('文心一言'),
  234. icon: <Wenxin.Color />,
  235. filter: (model) => model.model_name.toLowerCase().includes('ernie'),
  236. },
  237. xunfei: {
  238. label: t('讯飞星火'),
  239. icon: <Spark.Color />,
  240. filter: (model) => model.model_name.toLowerCase().includes('spark'),
  241. },
  242. midjourney: {
  243. label: 'Midjourney',
  244. icon: <Midjourney />,
  245. filter: (model) => model.model_name.toLowerCase().includes('mj_'),
  246. },
  247. tencent: {
  248. label: t('腾讯混元'),
  249. icon: <Hunyuan.Color />,
  250. filter: (model) => model.model_name.toLowerCase().includes('hunyuan'),
  251. },
  252. cohere: {
  253. label: 'Cohere',
  254. icon: <Cohere.Color />,
  255. filter: (model) =>
  256. model.model_name.toLowerCase().includes('command') ||
  257. model.model_name.toLowerCase().includes('c4ai-') ||
  258. model.model_name.toLowerCase().includes('embed-'),
  259. },
  260. cloudflare: {
  261. label: 'Cloudflare',
  262. icon: <Cloudflare.Color />,
  263. filter: (model) => model.model_name.toLowerCase().includes('@cf/'),
  264. },
  265. ai360: {
  266. label: t('360智脑'),
  267. icon: <Ai360.Color />,
  268. filter: (model) => model.model_name.toLowerCase().includes('360'),
  269. },
  270. jina: {
  271. label: 'Jina',
  272. icon: <Jina />,
  273. filter: (model) => model.model_name.toLowerCase().includes('jina'),
  274. },
  275. mistral: {
  276. label: 'Mistral AI',
  277. icon: <Mistral.Color />,
  278. filter: (model) =>
  279. model.model_name.toLowerCase().includes('mistral') ||
  280. model.model_name.toLowerCase().includes('codestral') ||
  281. model.model_name.toLowerCase().includes('pixtral') ||
  282. model.model_name.toLowerCase().includes('voxtral') ||
  283. model.model_name.toLowerCase().includes('magistral'),
  284. },
  285. xai: {
  286. label: 'xAI',
  287. icon: <XAI />,
  288. filter: (model) => model.model_name.toLowerCase().includes('grok'),
  289. },
  290. llama: {
  291. label: 'Llama',
  292. icon: <Ollama />,
  293. filter: (model) => model.model_name.toLowerCase().includes('llama'),
  294. },
  295. doubao: {
  296. label: t('豆包'),
  297. icon: <Doubao.Color />,
  298. filter: (model) => model.model_name.toLowerCase().includes('doubao'),
  299. },
  300. yi: {
  301. label: t('零一万物'),
  302. icon: <Yi.Color />,
  303. filter: (model) => model.model_name.toLowerCase().includes('yi'),
  304. },
  305. };
  306. lastLocale = currentLocale;
  307. return categoriesCache;
  308. };
  309. })();
  310. /**
  311. * 根据渠道类型返回对应的厂商图标
  312. * @param {number} channelType - 渠道类型值
  313. * @returns {JSX.Element|null} - 对应的厂商图标组件
  314. */
  315. export function getChannelIcon(channelType) {
  316. const iconSize = 14;
  317. switch (channelType) {
  318. case 1: // OpenAI
  319. case 3: // Azure OpenAI
  320. case 57: // Codex
  321. return <OpenAI size={iconSize} />;
  322. case 2: // Midjourney Proxy
  323. case 5: // Midjourney Proxy Plus
  324. return <Midjourney size={iconSize} />;
  325. case 36: // Suno API
  326. return <Suno size={iconSize} />;
  327. case 4: // Ollama
  328. return <Ollama size={iconSize} />;
  329. case 14: // Anthropic Claude
  330. case 33: // AWS Claude
  331. return <Claude.Color size={iconSize} />;
  332. case 41: // Vertex AI
  333. return <Gemini.Color size={iconSize} />;
  334. case 34: // Cohere
  335. return <Cohere.Color size={iconSize} />;
  336. case 39: // Cloudflare
  337. return <Cloudflare.Color size={iconSize} />;
  338. case 43: // DeepSeek
  339. return <DeepSeek.Color size={iconSize} />;
  340. case 15: // 百度文心千帆
  341. case 46: // 百度文心千帆V2
  342. return <Wenxin.Color size={iconSize} />;
  343. case 17: // 阿里通义千问
  344. return <Qwen.Color size={iconSize} />;
  345. case 18: // 讯飞星火认知
  346. return <Spark.Color size={iconSize} />;
  347. case 16: // 智谱 ChatGLM
  348. case 26: // 智谱 GLM-4V
  349. return <Zhipu.Color size={iconSize} />;
  350. case 24: // Google Gemini
  351. case 11: // Google PaLM2
  352. return <Gemini.Color size={iconSize} />;
  353. case 47: // Xinference
  354. return <Xinference.Color size={iconSize} />;
  355. case 25: // Moonshot
  356. return <Moonshot size={iconSize} />;
  357. case 27: // Perplexity
  358. return <Perplexity.Color size={iconSize} />;
  359. case 20: // OpenRouter
  360. return <OpenRouter size={iconSize} />;
  361. case 19: // 360 智脑
  362. return <Ai360.Color size={iconSize} />;
  363. case 23: // 腾讯混元
  364. return <Hunyuan.Color size={iconSize} />;
  365. case 31: // 零一万物
  366. return <Yi.Color size={iconSize} />;
  367. case 35: // MiniMax
  368. return <Minimax.Color size={iconSize} />;
  369. case 37: // Dify
  370. return <Dify.Color size={iconSize} />;
  371. case 38: // Jina
  372. return <Jina size={iconSize} />;
  373. case 40: // SiliconCloud
  374. return <SiliconCloud.Color size={iconSize} />;
  375. case 42: // Mistral AI
  376. return <Mistral.Color size={iconSize} />;
  377. case 45: // 字节火山方舟、豆包通用
  378. return <Doubao.Color size={iconSize} />;
  379. case 48: // xAI
  380. return <XAI size={iconSize} />;
  381. case 49: // Coze
  382. return <Coze size={iconSize} />;
  383. case 50: // 可灵 Kling
  384. case 59: // KlingAiping
  385. return <Kling.Color size={iconSize} />;
  386. case 51: // 即梦 Jimeng
  387. return <Jimeng.Color size={iconSize} />;
  388. case 54: // 豆包视频 Doubao Video
  389. return <Doubao.Color size={iconSize} />;
  390. case 56: // Replicate
  391. return <Replicate size={iconSize} />;
  392. case 8: // 自定义渠道
  393. case 22: // 知识库:FastGPT
  394. return <FastGPT.Color size={iconSize} />;
  395. case 21: // 知识库:AI Proxy
  396. case 44: // 嵌入模型:MokaAI M3E
  397. default:
  398. return null; // 未知类型或自定义渠道不显示图标
  399. }
  400. }
  401. /**
  402. * 根据图标名称动态获取 LobeHub 图标组件
  403. * 支持:
  404. * - 基础:"OpenAI"、"OpenAI.Color" 等
  405. * - 额外属性(点号链式):"OpenAI.Avatar.type={'platform'}"、"OpenRouter.Avatar.shape={'square'}"
  406. * - 继续兼容第二参数 size;若字符串里有 size=,以字符串为准
  407. * @param {string} iconName - 图标名称/描述
  408. * @param {number} size - 图标大小,默认为 14
  409. * @returns {JSX.Element} - 对应的图标组件或 Avatar
  410. */
  411. export function getLobeHubIcon(iconName, size = 14) {
  412. if (typeof iconName === 'string') iconName = iconName.trim();
  413. // 如果没有图标名称,返回 Avatar
  414. if (!iconName) {
  415. return <Avatar size='extra-extra-small'>?</Avatar>;
  416. }
  417. // 解析组件路径与点号链式属性
  418. const segments = String(iconName).split('.');
  419. const baseKey = segments[0];
  420. const BaseIcon = LobeIcons[baseKey];
  421. let IconComponent = undefined;
  422. let propStartIndex = 1;
  423. if (BaseIcon && segments.length > 1 && BaseIcon[segments[1]]) {
  424. IconComponent = BaseIcon[segments[1]];
  425. propStartIndex = 2;
  426. } else {
  427. IconComponent = LobeIcons[baseKey];
  428. propStartIndex = 1;
  429. }
  430. // 失败兜底
  431. if (
  432. !IconComponent ||
  433. (typeof IconComponent !== 'function' && typeof IconComponent !== 'object')
  434. ) {
  435. const firstLetter = String(iconName).charAt(0).toUpperCase();
  436. return <Avatar size='extra-extra-small'>{firstLetter}</Avatar>;
  437. }
  438. // 解析点号链式属性,形如:key={...}、key='...'、key="..."、key=123、key、key=true/false
  439. const props = {};
  440. const parseValue = (raw) => {
  441. if (raw == null) return true;
  442. let v = String(raw).trim();
  443. // 去除一层花括号包裹
  444. if (v.startsWith('{') && v.endsWith('}')) {
  445. v = v.slice(1, -1).trim();
  446. }
  447. // 去除引号
  448. if (
  449. (v.startsWith('"') && v.endsWith('"')) ||
  450. (v.startsWith("'") && v.endsWith("'"))
  451. ) {
  452. return v.slice(1, -1);
  453. }
  454. // 布尔
  455. if (v === 'true') return true;
  456. if (v === 'false') return false;
  457. // 数字
  458. if (/^-?\d+(?:\.\d+)?$/.test(v)) return Number(v);
  459. // 其他原样返回字符串
  460. return v;
  461. };
  462. for (let i = propStartIndex; i < segments.length; i++) {
  463. const seg = segments[i];
  464. if (!seg) continue;
  465. const eqIdx = seg.indexOf('=');
  466. if (eqIdx === -1) {
  467. props[seg.trim()] = true;
  468. continue;
  469. }
  470. const key = seg.slice(0, eqIdx).trim();
  471. const valRaw = seg.slice(eqIdx + 1).trim();
  472. props[key] = parseValue(valRaw);
  473. }
  474. // 兼容第二参数 size,若字符串中未显式指定 size,则使用函数入参
  475. if (props.size == null && size != null) props.size = size;
  476. return <IconComponent {...props} />;
  477. }
  478. const oauthProviderIconMap = {
  479. github: SiGithub,
  480. gitlab: SiGitlab,
  481. gitea: SiGitea,
  482. google: SiGoogle,
  483. discord: SiDiscord,
  484. facebook: SiFacebook,
  485. x: SiX,
  486. twitter: SiX,
  487. slack: SiSlack,
  488. telegram: SiTelegram,
  489. wechat: SiWechat,
  490. keycloak: SiKeycloak,
  491. nextcloud: SiNextcloud,
  492. authentik: SiAuthentik,
  493. openid: SiOpenid,
  494. okta: SiOkta,
  495. auth0: SiAuth0,
  496. atlassian: SiAtlassian,
  497. bitbucket: SiBitbucket,
  498. notion: SiNotion,
  499. twitch: SiTwitch,
  500. reddit: SiReddit,
  501. dropbox: SiDropbox,
  502. };
  503. function isHttpUrl(value) {
  504. return /^https?:\/\//i.test(value || '');
  505. }
  506. function isSimpleEmoji(value) {
  507. if (!value) return false;
  508. const trimmed = String(value).trim();
  509. return trimmed.length > 0 && trimmed.length <= 4 && !isHttpUrl(trimmed);
  510. }
  511. function normalizeOAuthIconKey(raw) {
  512. return raw
  513. .trim()
  514. .toLowerCase()
  515. .replace(/^ri:/, '')
  516. .replace(/^react-icons:/, '')
  517. .replace(/^si:/, '');
  518. }
  519. /**
  520. * Render custom OAuth provider icon with react-icons or URL/emoji fallback.
  521. * Supported formats:
  522. * - react-icons simple key: github / gitlab / google / keycloak
  523. * - prefixed key: ri:github / si:github
  524. * - full URL image: https://example.com/logo.png
  525. * - emoji: 🐱
  526. */
  527. export function getOAuthProviderIcon(iconName, size = 20) {
  528. const raw = String(iconName || '').trim();
  529. const iconSize = Number(size) > 0 ? Number(size) : 20;
  530. if (!raw) {
  531. return <Layers size={iconSize} color='var(--semi-color-text-2)' />;
  532. }
  533. if (isHttpUrl(raw)) {
  534. return (
  535. <img
  536. src={raw}
  537. alt='provider icon'
  538. width={iconSize}
  539. height={iconSize}
  540. style={{ borderRadius: 4, objectFit: 'cover' }}
  541. />
  542. );
  543. }
  544. if (isSimpleEmoji(raw)) {
  545. return (
  546. <span
  547. style={{
  548. width: iconSize,
  549. height: iconSize,
  550. lineHeight: `${iconSize}px`,
  551. textAlign: 'center',
  552. display: 'inline-block',
  553. fontSize: Math.max(Math.floor(iconSize * 0.8), 14),
  554. }}
  555. >
  556. {raw}
  557. </span>
  558. );
  559. }
  560. const key = normalizeOAuthIconKey(raw);
  561. const IconComp = oauthProviderIconMap[key];
  562. if (IconComp) {
  563. return <IconComp size={iconSize} />;
  564. }
  565. return <Avatar size='extra-extra-small'>{raw.charAt(0).toUpperCase()}</Avatar>;
  566. }
  567. // 颜色列表
  568. const colors = [
  569. 'amber',
  570. 'blue',
  571. 'cyan',
  572. 'green',
  573. 'grey',
  574. 'indigo',
  575. 'light-blue',
  576. 'lime',
  577. 'orange',
  578. 'pink',
  579. 'purple',
  580. 'red',
  581. 'teal',
  582. 'violet',
  583. 'yellow',
  584. ];
  585. // 基础10色色板 (N ≤ 10)
  586. const baseColors = [
  587. '#1664FF', // 主色
  588. '#1AC6FF',
  589. '#FF8A00',
  590. '#3CC780',
  591. '#7442D4',
  592. '#FFC400',
  593. '#304D77',
  594. '#B48DEB',
  595. '#009488',
  596. '#FF7DDA',
  597. ];
  598. // 扩展20色色板 (10 < N ≤ 20)
  599. const extendedColors = [
  600. '#1664FF',
  601. '#B2CFFF',
  602. '#1AC6FF',
  603. '#94EFFF',
  604. '#FF8A00',
  605. '#FFCE7A',
  606. '#3CC780',
  607. '#B9EDCD',
  608. '#7442D4',
  609. '#DDC5FA',
  610. '#FFC400',
  611. '#FAE878',
  612. '#304D77',
  613. '#8B959E',
  614. '#B48DEB',
  615. '#EFE3FF',
  616. '#009488',
  617. '#59BAA8',
  618. '#FF7DDA',
  619. '#FFCFEE',
  620. ];
  621. // 模型颜色映射
  622. export const modelColorMap = {
  623. 'dall-e': 'rgb(147,112,219)', // 深紫色
  624. // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
  625. 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
  626. 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
  627. // 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
  628. 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
  629. 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
  630. 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
  631. 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
  632. 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
  633. 'gpt-4': 'rgb(135,206,235)', // 天蓝色
  634. // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
  635. 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
  636. 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
  637. 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
  638. 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
  639. 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
  640. // 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
  641. 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
  642. 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
  643. 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
  644. 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
  645. 'text-ada-001': 'rgb(255,192,203)', // 粉红色
  646. 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
  647. 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
  648. // 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
  649. 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
  650. 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
  651. 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
  652. 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
  653. 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
  654. 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
  655. 'tts-1': 'rgb(255,140,0)', // 深橙色
  656. 'tts-1-1106': 'rgb(255,165,0)', // 橙色
  657. 'tts-1-hd': 'rgb(255,215,0)', // 金色
  658. 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
  659. 'whisper-1': 'rgb(245,245,220)', // 米色
  660. 'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
  661. 'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
  662. 'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
  663. };
  664. export function modelToColor(modelName) {
  665. // 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
  666. if (modelColorMap[modelName]) {
  667. return modelColorMap[modelName];
  668. }
  669. // 2. 生成一个稳定的数字作为索引
  670. let hash = 0;
  671. for (let i = 0; i < modelName.length; i++) {
  672. hash = (hash << 5) - hash + modelName.charCodeAt(i);
  673. hash = hash & hash; // Convert to 32-bit integer
  674. }
  675. hash = Math.abs(hash);
  676. // 3. 根据模型名称长度选择不同的色板
  677. const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
  678. // 4. 使用hash值选择颜色
  679. const index = hash % colorPalette.length;
  680. return colorPalette[index];
  681. }
  682. export function stringToColor(str) {
  683. let sum = 0;
  684. for (let i = 0; i < str.length; i++) {
  685. sum += str.charCodeAt(i);
  686. }
  687. let i = sum % colors.length;
  688. return colors[i];
  689. }
  690. // 渲染带有模型图标的标签
  691. export function renderModelTag(modelName, options = {}) {
  692. const {
  693. color,
  694. size = 'default',
  695. shape = 'circle',
  696. onClick,
  697. suffixIcon,
  698. } = options;
  699. const categories = getModelCategories(i18next.t);
  700. let icon = null;
  701. for (const [key, category] of Object.entries(categories)) {
  702. if (key !== 'all' && category.filter({ model_name: modelName })) {
  703. icon = category.icon;
  704. break;
  705. }
  706. }
  707. return (
  708. <Tag
  709. color={color || stringToColor(modelName)}
  710. prefixIcon={icon}
  711. suffixIcon={suffixIcon}
  712. size={size}
  713. shape={shape}
  714. onClick={onClick}
  715. >
  716. {modelName}
  717. </Tag>
  718. );
  719. }
  720. export function renderText(text, limit) {
  721. if (text.length > limit) {
  722. return text.slice(0, limit - 3) + '...';
  723. }
  724. return text;
  725. }
  726. /**
  727. * Render group tags based on the input group string
  728. * @param {string} group - The input group string
  729. * @returns {JSX.Element} - The rendered group tags
  730. */
  731. export function renderGroup(group) {
  732. if (group === '') {
  733. return (
  734. <Tag key='default' color='white' shape='circle'>
  735. {i18next.t('用户分组')}
  736. </Tag>
  737. );
  738. }
  739. const tagColors = {
  740. vip: 'yellow',
  741. pro: 'yellow',
  742. svip: 'red',
  743. premium: 'red',
  744. };
  745. const groups = group.split(',').sort();
  746. return (
  747. <span key={group}>
  748. {groups.map((group) => (
  749. <Tag
  750. color={tagColors[group] || stringToColor(group)}
  751. key={group}
  752. shape='circle'
  753. onClick={async (event) => {
  754. event.stopPropagation();
  755. if (await copy(group)) {
  756. showSuccess(i18next.t('已复制:') + group);
  757. } else {
  758. Modal.error({
  759. title: i18next.t('无法复制到剪贴板,请手动复制'),
  760. content: group,
  761. });
  762. }
  763. }}
  764. >
  765. {group}
  766. </Tag>
  767. ))}
  768. </span>
  769. );
  770. }
  771. export function renderRatio(ratio) {
  772. let color = 'green';
  773. if (ratio > 5) {
  774. color = 'red';
  775. } else if (ratio > 3) {
  776. color = 'orange';
  777. } else if (ratio > 1) {
  778. color = 'blue';
  779. }
  780. return (
  781. <Tag color={color}>
  782. {ratio}x {i18next.t('倍率')}
  783. </Tag>
  784. );
  785. }
  786. const measureTextWidth = (
  787. text,
  788. style = {
  789. fontSize: '14px',
  790. fontFamily:
  791. '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
  792. },
  793. containerWidth,
  794. ) => {
  795. const span = document.createElement('span');
  796. span.style.visibility = 'hidden';
  797. span.style.position = 'absolute';
  798. span.style.whiteSpace = 'nowrap';
  799. span.style.fontSize = style.fontSize;
  800. span.style.fontFamily = style.fontFamily;
  801. span.textContent = text;
  802. document.body.appendChild(span);
  803. const width = span.offsetWidth;
  804. document.body.removeChild(span);
  805. return width;
  806. };
  807. export function truncateText(text, maxWidth = 200) {
  808. const isMobileScreen = window.matchMedia(
  809. `(max-width: ${MOBILE_BREAKPOINT - 1}px)`,
  810. ).matches;
  811. if (!isMobileScreen) {
  812. return text;
  813. }
  814. if (!text) return text;
  815. try {
  816. // Handle percentage-based maxWidth
  817. let actualMaxWidth = maxWidth;
  818. if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) {
  819. const percentage = parseFloat(maxWidth) / 100;
  820. // Use window width as fallback container width
  821. actualMaxWidth = window.innerWidth * percentage;
  822. }
  823. const width = measureTextWidth(text);
  824. if (width <= actualMaxWidth) return text;
  825. let left = 0;
  826. let right = text.length;
  827. let result = text;
  828. while (left <= right) {
  829. const mid = Math.floor((left + right) / 2);
  830. const truncated = text.slice(0, mid) + '...';
  831. const currentWidth = measureTextWidth(truncated);
  832. if (currentWidth <= actualMaxWidth) {
  833. result = truncated;
  834. left = mid + 1;
  835. } else {
  836. right = mid - 1;
  837. }
  838. }
  839. return result;
  840. } catch (error) {
  841. console.warn(
  842. 'Text measurement failed, falling back to character count',
  843. error,
  844. );
  845. if (text.length > 20) {
  846. return text.slice(0, 17) + '...';
  847. }
  848. return text;
  849. }
  850. }
  851. export const renderGroupOption = (item) => {
  852. const {
  853. disabled,
  854. selected,
  855. label,
  856. value,
  857. focused,
  858. className,
  859. style,
  860. onMouseEnter,
  861. onClick,
  862. empty,
  863. emptyContent,
  864. ...rest
  865. } = item;
  866. const baseStyle = {
  867. display: 'flex',
  868. justifyContent: 'space-between',
  869. alignItems: 'center',
  870. padding: '8px 16px',
  871. cursor: disabled ? 'not-allowed' : 'pointer',
  872. backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
  873. opacity: disabled ? 0.5 : 1,
  874. ...(selected && {
  875. backgroundColor: 'var(--semi-color-primary-light-default)',
  876. }),
  877. '&:hover': {
  878. backgroundColor: !disabled && 'var(--semi-color-fill-1)',
  879. },
  880. };
  881. const handleClick = () => {
  882. if (!disabled && onClick) {
  883. onClick();
  884. }
  885. };
  886. const handleMouseEnter = (e) => {
  887. if (!disabled && onMouseEnter) {
  888. onMouseEnter(e);
  889. }
  890. };
  891. return (
  892. <div
  893. style={baseStyle}
  894. onClick={handleClick}
  895. onMouseEnter={handleMouseEnter}
  896. >
  897. <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
  898. <Typography.Text strong type={disabled ? 'tertiary' : undefined}>
  899. {value}
  900. </Typography.Text>
  901. <Typography.Text type='secondary' size='small'>
  902. {label}
  903. </Typography.Text>
  904. </div>
  905. {item.ratio && renderRatio(item.ratio)}
  906. </div>
  907. );
  908. };
  909. export function renderNumber(num) {
  910. if (num >= 1000000000) {
  911. return (num / 1000000000).toFixed(1) + 'B';
  912. } else if (num >= 1000000) {
  913. return (num / 1000000).toFixed(1) + 'M';
  914. } else if (num >= 10000) {
  915. return (num / 1000).toFixed(1) + 'k';
  916. } else {
  917. return num;
  918. }
  919. }
  920. export function renderQuotaNumberWithDigit(num, digits = 2) {
  921. if (typeof num !== 'number' || isNaN(num)) {
  922. return 0;
  923. }
  924. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  925. num = num.toFixed(digits);
  926. if (quotaDisplayType === 'CNY') {
  927. return '¥' + num;
  928. } else if (quotaDisplayType === 'USD') {
  929. return '$' + num;
  930. } else if (quotaDisplayType === 'CUSTOM') {
  931. const statusStr = localStorage.getItem('status');
  932. let symbol = '¤';
  933. try {
  934. if (statusStr) {
  935. const s = JSON.parse(statusStr);
  936. symbol = s?.custom_currency_symbol || symbol;
  937. }
  938. } catch (e) {}
  939. return symbol + num;
  940. } else {
  941. return num;
  942. }
  943. }
  944. export function renderNumberWithPoint(num) {
  945. if (num === undefined) return '';
  946. num = num.toFixed(2);
  947. if (num >= 100000) {
  948. // Convert number to string to manipulate it
  949. let numStr = num.toString();
  950. // Find the position of the decimal point
  951. let decimalPointIndex = numStr.indexOf('.');
  952. let wholePart = numStr;
  953. let decimalPart = '';
  954. // If there is a decimal point, split the number into whole and decimal parts
  955. if (decimalPointIndex !== -1) {
  956. wholePart = numStr.slice(0, decimalPointIndex);
  957. decimalPart = numStr.slice(decimalPointIndex);
  958. }
  959. // Take the first two and last two digits of the whole number part
  960. let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
  961. // Return the formatted number
  962. return shortenedWholePart + decimalPart;
  963. }
  964. // If the number is less than 100,000, return it unmodified
  965. return num;
  966. }
  967. export function getQuotaPerUnit() {
  968. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  969. quotaPerUnit = parseFloat(quotaPerUnit);
  970. return quotaPerUnit;
  971. }
  972. export function renderUnitWithQuota(quota) {
  973. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  974. quotaPerUnit = parseFloat(quotaPerUnit);
  975. quota = parseFloat(quota);
  976. return quotaPerUnit * quota;
  977. }
  978. export function getQuotaWithUnit(quota, digits = 6) {
  979. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  980. quotaPerUnit = parseFloat(quotaPerUnit);
  981. return (quota / quotaPerUnit).toFixed(digits);
  982. }
  983. export function renderQuotaWithAmount(amount) {
  984. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  985. if (quotaDisplayType === 'TOKENS') {
  986. return renderNumber(renderUnitWithQuota(amount));
  987. }
  988. if (quotaDisplayType === 'CNY') {
  989. return '¥' + amount;
  990. } else if (quotaDisplayType === 'CUSTOM') {
  991. const statusStr = localStorage.getItem('status');
  992. let symbol = '¤';
  993. try {
  994. if (statusStr) {
  995. const s = JSON.parse(statusStr);
  996. symbol = s?.custom_currency_symbol || symbol;
  997. }
  998. } catch (e) {}
  999. return symbol + amount;
  1000. }
  1001. return '$' + amount;
  1002. }
  1003. /**
  1004. * 获取当前货币配置信息
  1005. * @returns {Object} - { symbol, rate, type }
  1006. */
  1007. export function getCurrencyConfig() {
  1008. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  1009. const statusStr = localStorage.getItem('status');
  1010. let symbol = '$';
  1011. let rate = 1;
  1012. if (quotaDisplayType === 'CNY') {
  1013. symbol = '¥';
  1014. try {
  1015. if (statusStr) {
  1016. const s = JSON.parse(statusStr);
  1017. rate = s?.usd_exchange_rate || 7;
  1018. }
  1019. } catch (e) {}
  1020. } else if (quotaDisplayType === 'CUSTOM') {
  1021. try {
  1022. if (statusStr) {
  1023. const s = JSON.parse(statusStr);
  1024. symbol = s?.custom_currency_symbol || '¤';
  1025. rate = s?.custom_currency_exchange_rate || 1;
  1026. }
  1027. } catch (e) {}
  1028. }
  1029. return { symbol, rate, type: quotaDisplayType };
  1030. }
  1031. /**
  1032. * 将美元金额转换为当前选择的货币
  1033. * @param {number} usdAmount - 美元金额
  1034. * @param {number} digits - 小数位数
  1035. * @returns {string} - 格式化后的货币字符串
  1036. */
  1037. export function convertUSDToCurrency(usdAmount, digits = 2) {
  1038. const { symbol, rate } = getCurrencyConfig();
  1039. const convertedAmount = usdAmount * rate;
  1040. return symbol + convertedAmount.toFixed(digits);
  1041. }
  1042. export function renderQuota(quota, digits = 2) {
  1043. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  1044. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  1045. quotaPerUnit = parseFloat(quotaPerUnit);
  1046. if (quotaDisplayType === 'TOKENS') {
  1047. return renderNumber(quota);
  1048. }
  1049. const resultUSD = quota / quotaPerUnit;
  1050. let symbol = '$';
  1051. let value = resultUSD;
  1052. if (quotaDisplayType === 'CNY') {
  1053. const statusStr = localStorage.getItem('status');
  1054. let usdRate = 1;
  1055. try {
  1056. if (statusStr) {
  1057. const s = JSON.parse(statusStr);
  1058. usdRate = s?.usd_exchange_rate || 1;
  1059. }
  1060. } catch (e) {}
  1061. value = resultUSD * usdRate;
  1062. symbol = '¥';
  1063. } else if (quotaDisplayType === 'CUSTOM') {
  1064. const statusStr = localStorage.getItem('status');
  1065. let symbolCustom = '¤';
  1066. let rate = 1;
  1067. try {
  1068. if (statusStr) {
  1069. const s = JSON.parse(statusStr);
  1070. symbolCustom = s?.custom_currency_symbol || symbolCustom;
  1071. rate = s?.custom_currency_exchange_rate || rate;
  1072. }
  1073. } catch (e) {}
  1074. value = resultUSD * rate;
  1075. symbol = symbolCustom;
  1076. }
  1077. const fixedResult = value.toFixed(digits);
  1078. if (parseFloat(fixedResult) === 0 && quota > 0 && value > 0) {
  1079. const minValue = Math.pow(10, -digits);
  1080. return symbol + minValue.toFixed(digits);
  1081. }
  1082. return symbol + fixedResult;
  1083. }
  1084. function isValidGroupRatio(ratio) {
  1085. return Number.isFinite(ratio) && ratio !== -1;
  1086. }
  1087. /**
  1088. * Helper function to get effective ratio and label
  1089. * @param {number} groupRatio - The default group ratio
  1090. * @param {number} user_group_ratio - The user-specific group ratio
  1091. * @returns {Object} - Object containing { ratio, label, useUserGroupRatio }
  1092. */
  1093. function getEffectiveRatio(groupRatio, user_group_ratio) {
  1094. const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
  1095. const ratioLabel = useUserGroupRatio
  1096. ? i18next.t('专属倍率')
  1097. : i18next.t('分组倍率');
  1098. const effectiveRatio = useUserGroupRatio ? user_group_ratio : groupRatio;
  1099. return {
  1100. ratio: effectiveRatio,
  1101. label: ratioLabel,
  1102. useUserGroupRatio: useUserGroupRatio,
  1103. };
  1104. }
  1105. // Shared core for simple price rendering (used by OpenAI-like and Claude-like variants)
  1106. function renderPriceSimpleCore({
  1107. modelRatio,
  1108. modelPrice = -1,
  1109. groupRatio,
  1110. user_group_ratio,
  1111. cacheTokens = 0,
  1112. cacheRatio = 1.0,
  1113. cacheCreationTokens = 0,
  1114. cacheCreationRatio = 1.0,
  1115. cacheCreationTokens5m = 0,
  1116. cacheCreationRatio5m = 1.0,
  1117. cacheCreationTokens1h = 0,
  1118. cacheCreationRatio1h = 1.0,
  1119. image = false,
  1120. imageRatio = 1.0,
  1121. isSystemPromptOverride = false,
  1122. }) {
  1123. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  1124. groupRatio,
  1125. user_group_ratio,
  1126. );
  1127. const finalGroupRatio = effectiveGroupRatio;
  1128. const { symbol, rate } = getCurrencyConfig();
  1129. if (modelPrice !== -1) {
  1130. const displayPrice = (modelPrice * rate).toFixed(6);
  1131. return i18next.t('价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}}', {
  1132. symbol: symbol,
  1133. price: displayPrice,
  1134. ratioType: ratioLabel,
  1135. ratio: finalGroupRatio,
  1136. });
  1137. }
  1138. const hasSplitCacheCreation =
  1139. cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
  1140. const shouldShowLegacyCacheCreation =
  1141. !hasSplitCacheCreation && cacheCreationTokens !== 0;
  1142. const shouldShowCache = cacheTokens !== 0;
  1143. const shouldShowCacheCreation5m =
  1144. hasSplitCacheCreation && cacheCreationTokens5m > 0;
  1145. const shouldShowCacheCreation1h =
  1146. hasSplitCacheCreation && cacheCreationTokens1h > 0;
  1147. const parts = [];
  1148. // base: model ratio
  1149. parts.push(i18next.t('模型: {{ratio}}'));
  1150. // cache part (label differs when with image)
  1151. if (shouldShowCache) {
  1152. parts.push(i18next.t('缓存: {{cacheRatio}}'));
  1153. }
  1154. if (hasSplitCacheCreation) {
  1155. if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
  1156. parts.push(
  1157. i18next.t(
  1158. '缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
  1159. ),
  1160. );
  1161. } else if (shouldShowCacheCreation5m) {
  1162. parts.push(i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}'));
  1163. } else if (shouldShowCacheCreation1h) {
  1164. parts.push(i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}'));
  1165. }
  1166. } else if (shouldShowLegacyCacheCreation) {
  1167. parts.push(i18next.t('缓存创建: {{cacheCreationRatio}}'));
  1168. }
  1169. // image part
  1170. if (image) {
  1171. parts.push(i18next.t('图片输入: {{imageRatio}}'));
  1172. }
  1173. parts.push(`{{ratioType}}: {{groupRatio}}`);
  1174. let result = i18next.t(parts.join(' * '), {
  1175. ratio: modelRatio,
  1176. ratioType: ratioLabel,
  1177. groupRatio: finalGroupRatio,
  1178. cacheRatio: cacheRatio,
  1179. cacheCreationRatio: cacheCreationRatio,
  1180. cacheCreationRatio5m: cacheCreationRatio5m,
  1181. cacheCreationRatio1h: cacheCreationRatio1h,
  1182. imageRatio: imageRatio,
  1183. });
  1184. if (isSystemPromptOverride) {
  1185. result += '\n\r' + i18next.t('系统提示覆盖');
  1186. }
  1187. return result;
  1188. }
  1189. export function renderModelPrice(
  1190. inputTokens,
  1191. completionTokens,
  1192. modelRatio,
  1193. modelPrice = -1,
  1194. completionRatio,
  1195. groupRatio,
  1196. user_group_ratio,
  1197. cacheTokens = 0,
  1198. cacheRatio = 1.0,
  1199. image = false,
  1200. imageRatio = 1.0,
  1201. imageOutputTokens = 0,
  1202. webSearch = false,
  1203. webSearchCallCount = 0,
  1204. webSearchPrice = 0,
  1205. fileSearch = false,
  1206. fileSearchCallCount = 0,
  1207. fileSearchPrice = 0,
  1208. audioInputSeperatePrice = false,
  1209. audioInputTokens = 0,
  1210. audioInputPrice = 0,
  1211. imageGenerationCall = false,
  1212. imageGenerationCallPrice = 0,
  1213. ) {
  1214. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  1215. groupRatio,
  1216. user_group_ratio,
  1217. );
  1218. groupRatio = effectiveGroupRatio;
  1219. // 获取货币配置
  1220. const { symbol, rate } = getCurrencyConfig();
  1221. if (modelPrice !== -1) {
  1222. const displayPrice = (modelPrice * rate).toFixed(6);
  1223. const displayTotal = (modelPrice * groupRatio * rate).toFixed(6);
  1224. const ratioParts = `${ratioLabel}:${groupRatio}` ;
  1225. return i18next.t(
  1226. '模型价格:{{symbol}}{{price}} * {{ratioParts}} = {{symbol}}{{total}}',
  1227. {
  1228. symbol: symbol,
  1229. price: displayPrice,
  1230. ratioParts,
  1231. total: displayTotal,
  1232. },
  1233. );
  1234. } else {
  1235. if (completionRatio === undefined) {
  1236. completionRatio = 0;
  1237. }
  1238. let inputRatioPrice = modelRatio * 2.0;
  1239. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  1240. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  1241. let imageRatioPrice = modelRatio * 2.0 * imageRatio;
  1242. // Calculate effective input tokens (non-cached + cached with ratio applied)
  1243. let effectiveInputTokens =
  1244. inputTokens - cacheTokens + cacheTokens * cacheRatio;
  1245. // Handle image tokens if present
  1246. if (image && imageOutputTokens > 0) {
  1247. effectiveInputTokens =
  1248. inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
  1249. }
  1250. if (audioInputTokens > 0) {
  1251. effectiveInputTokens -= audioInputTokens;
  1252. }
  1253. let price =
  1254. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  1255. (audioInputTokens / 1000000) * audioInputPrice * groupRatio +
  1256. (completionTokens / 1000000) * completionRatioPrice * groupRatio +
  1257. (webSearchCallCount / 1000) * webSearchPrice * groupRatio +
  1258. (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio +
  1259. imageGenerationCallPrice * groupRatio;
  1260. return (
  1261. <>
  1262. <article>
  1263. <p>
  1264. {i18next.t(
  1265. '输入价格:{{symbol}}{{price}} / 1M tokens{{audioPrice}}',
  1266. {
  1267. symbol: symbol,
  1268. price: (inputRatioPrice * rate).toFixed(6),
  1269. audioPrice: audioInputSeperatePrice
  1270. ? i18next.t(',音频 {{symbol}}{{price}} / 1M tokens', { symbol, price: (audioInputPrice * rate).toFixed(6) })
  1271. : '',
  1272. },
  1273. )}
  1274. </p>
  1275. <p>
  1276. {i18next.t(
  1277. '输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})',
  1278. {
  1279. symbol: symbol,
  1280. price: (inputRatioPrice * rate).toFixed(6),
  1281. total: (completionRatioPrice * rate).toFixed(6),
  1282. completionRatio: completionRatio,
  1283. },
  1284. )}
  1285. </p>
  1286. {cacheTokens > 0 && (
  1287. <p>
  1288. {i18next.t(
  1289. '缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  1290. {
  1291. symbol: symbol,
  1292. price: (inputRatioPrice * rate).toFixed(6),
  1293. total: (inputRatioPrice * cacheRatio * rate).toFixed(6),
  1294. cacheRatio: cacheRatio,
  1295. },
  1296. )}
  1297. </p>
  1298. )}
  1299. {image && imageOutputTokens > 0 && (
  1300. <p>
  1301. {i18next.t(
  1302. '图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})',
  1303. {
  1304. symbol: symbol,
  1305. price: (imageRatioPrice * rate).toFixed(6),
  1306. ratio: groupRatio,
  1307. total: (imageRatioPrice * groupRatio * rate).toFixed(6),
  1308. imageRatio: imageRatio,
  1309. },
  1310. )}
  1311. </p>
  1312. )}
  1313. {webSearch && webSearchCallCount > 0 && (
  1314. <p>
  1315. {i18next.t('Web搜索价格:{{symbol}}{{price}} / 1K 次', {
  1316. symbol: symbol,
  1317. price: (webSearchPrice * rate).toFixed(6),
  1318. })}
  1319. </p>
  1320. )}
  1321. {fileSearch && fileSearchCallCount > 0 && (
  1322. <p>
  1323. {i18next.t('文件搜索价格:{{symbol}}{{price}} / 1K 次', {
  1324. symbol: symbol,
  1325. price: (fileSearchPrice * rate).toFixed(6),
  1326. })}
  1327. </p>
  1328. )}
  1329. {imageGenerationCall && imageGenerationCallPrice > 0 && (
  1330. <p>
  1331. {i18next.t('图片生成调用:{{symbol}}{{price}} / 1次', {
  1332. symbol: symbol,
  1333. price: (imageGenerationCallPrice * rate).toFixed(6),
  1334. })}
  1335. </p>
  1336. )}
  1337. <p>
  1338. {(() => {
  1339. // 构建输入部分描述
  1340. let inputDesc = '';
  1341. if (image && imageOutputTokens > 0) {
  1342. inputDesc = i18next.t(
  1343. '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}',
  1344. {
  1345. nonImageInput: inputTokens - imageOutputTokens,
  1346. imageInput: imageOutputTokens,
  1347. imageRatio: imageRatio,
  1348. symbol: symbol,
  1349. price: (inputRatioPrice * rate).toFixed(6),
  1350. },
  1351. );
  1352. } else if (cacheTokens > 0) {
  1353. inputDesc = i18next.t(
  1354. '(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}',
  1355. {
  1356. nonCacheInput: inputTokens - cacheTokens,
  1357. cacheInput: cacheTokens,
  1358. symbol: symbol,
  1359. price: (inputRatioPrice * rate).toFixed(6),
  1360. cachePrice: (cacheRatioPrice * rate).toFixed(6),
  1361. },
  1362. );
  1363. } else if (audioInputSeperatePrice && audioInputTokens > 0) {
  1364. inputDesc = i18next.t(
  1365. '(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}',
  1366. {
  1367. nonAudioInput: inputTokens - audioInputTokens,
  1368. audioInput: audioInputTokens,
  1369. symbol: symbol,
  1370. price: (inputRatioPrice * rate).toFixed(6),
  1371. audioPrice: (audioInputPrice * rate).toFixed(6),
  1372. },
  1373. );
  1374. } else {
  1375. inputDesc = i18next.t(
  1376. '(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}',
  1377. {
  1378. input: inputTokens,
  1379. symbol: symbol,
  1380. price: (inputRatioPrice * rate).toFixed(6),
  1381. },
  1382. );
  1383. }
  1384. // 构建输出部分描述
  1385. const outputDesc = i18next.t(
  1386. '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}',
  1387. {
  1388. completion: completionTokens,
  1389. symbol: symbol,
  1390. compPrice: (completionRatioPrice * rate).toFixed(6),
  1391. ratio: groupRatio,
  1392. ratioType: ratioLabel,
  1393. },
  1394. );
  1395. // 构建额外服务描述
  1396. const extraServices = [
  1397. webSearch && webSearchCallCount > 0
  1398. ? i18next.t(
  1399. ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
  1400. {
  1401. count: webSearchCallCount,
  1402. symbol: symbol,
  1403. price: (webSearchPrice * rate).toFixed(6),
  1404. ratio: groupRatio,
  1405. ratioType: ratioLabel,
  1406. },
  1407. )
  1408. : '',
  1409. fileSearch && fileSearchCallCount > 0
  1410. ? i18next.t(
  1411. ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
  1412. {
  1413. count: fileSearchCallCount,
  1414. symbol: symbol,
  1415. price: (fileSearchPrice * rate).toFixed(6),
  1416. ratio: groupRatio,
  1417. ratioType: ratioLabel,
  1418. },
  1419. )
  1420. : '',
  1421. imageGenerationCall && imageGenerationCallPrice > 0
  1422. ? i18next.t(
  1423. ' + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}',
  1424. {
  1425. symbol: symbol,
  1426. price: (imageGenerationCallPrice * rate).toFixed(6),
  1427. ratio: groupRatio,
  1428. ratioType: ratioLabel,
  1429. },
  1430. )
  1431. : '',
  1432. ].join('');
  1433. return i18next.t(
  1434. '{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}',
  1435. {
  1436. inputDesc,
  1437. outputDesc,
  1438. extraServices,
  1439. symbol: symbol,
  1440. total: (price * rate).toFixed(6),
  1441. },
  1442. );
  1443. })()}
  1444. </p>
  1445. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  1446. </article>
  1447. </>
  1448. );
  1449. }
  1450. }
  1451. export function renderLogContent(
  1452. modelRatio,
  1453. completionRatio,
  1454. modelPrice = -1,
  1455. groupRatio,
  1456. user_group_ratio,
  1457. cacheRatio = 1.0,
  1458. image = false,
  1459. imageRatio = 1.0,
  1460. webSearch = false,
  1461. webSearchCallCount = 0,
  1462. fileSearch = false,
  1463. fileSearchCallCount = 0,
  1464. ) {
  1465. const {
  1466. ratio,
  1467. label: ratioLabel,
  1468. useUserGroupRatio: useUserGroupRatio,
  1469. } = getEffectiveRatio(groupRatio, user_group_ratio);
  1470. // 获取货币配置
  1471. const { symbol, rate } = getCurrencyConfig();
  1472. const userRatioSuffix = '';
  1473. let result;
  1474. if (modelPrice !== -1) {
  1475. result = i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', {
  1476. symbol: symbol,
  1477. price: (modelPrice * rate).toFixed(6),
  1478. ratioType: ratioLabel,
  1479. ratio,
  1480. });
  1481. } else if (image) {
  1482. result = i18next.t(
  1483. '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}',
  1484. {
  1485. modelRatio: modelRatio,
  1486. cacheRatio: cacheRatio,
  1487. completionRatio: completionRatio,
  1488. imageRatio: imageRatio,
  1489. ratioType: ratioLabel,
  1490. ratio,
  1491. },
  1492. );
  1493. } else if (webSearch) {
  1494. result = i18next.t(
  1495. '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次',
  1496. {
  1497. modelRatio: modelRatio,
  1498. cacheRatio: cacheRatio,
  1499. completionRatio: completionRatio,
  1500. ratioType: ratioLabel,
  1501. ratio,
  1502. webSearchCallCount,
  1503. },
  1504. );
  1505. } else {
  1506. result = i18next.t(
  1507. '模型倍率 {{modelRatio}},缓存倍率 {{cacheRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}',
  1508. {
  1509. modelRatio: modelRatio,
  1510. cacheRatio: cacheRatio,
  1511. completionRatio: completionRatio,
  1512. ratioType: ratioLabel,
  1513. ratio,
  1514. },
  1515. );
  1516. }
  1517. return result + userRatioSuffix;
  1518. }
  1519. export function renderModelPriceSimple(
  1520. modelRatio,
  1521. modelPrice = -1,
  1522. groupRatio,
  1523. user_group_ratio,
  1524. cacheTokens = 0,
  1525. cacheRatio = 1.0,
  1526. cacheCreationTokens = 0,
  1527. cacheCreationRatio = 1.0,
  1528. cacheCreationTokens5m = 0,
  1529. cacheCreationRatio5m = 1.0,
  1530. cacheCreationTokens1h = 0,
  1531. cacheCreationRatio1h = 1.0,
  1532. image = false,
  1533. imageRatio = 1.0,
  1534. isSystemPromptOverride = false,
  1535. provider = 'openai',
  1536. ) {
  1537. return renderPriceSimpleCore({
  1538. modelRatio,
  1539. modelPrice,
  1540. groupRatio,
  1541. user_group_ratio,
  1542. cacheTokens,
  1543. cacheRatio,
  1544. cacheCreationTokens,
  1545. cacheCreationRatio,
  1546. cacheCreationTokens5m,
  1547. cacheCreationRatio5m,
  1548. cacheCreationTokens1h,
  1549. cacheCreationRatio1h,
  1550. image,
  1551. imageRatio,
  1552. isSystemPromptOverride,
  1553. });
  1554. }
  1555. export function renderAudioModelPrice(
  1556. inputTokens,
  1557. completionTokens,
  1558. modelRatio,
  1559. modelPrice = -1,
  1560. completionRatio,
  1561. audioInputTokens,
  1562. audioCompletionTokens,
  1563. audioRatio,
  1564. audioCompletionRatio,
  1565. groupRatio,
  1566. user_group_ratio,
  1567. cacheTokens = 0,
  1568. cacheRatio = 1.0,
  1569. ) {
  1570. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  1571. groupRatio,
  1572. user_group_ratio,
  1573. );
  1574. groupRatio = effectiveGroupRatio;
  1575. // 获取货币配置
  1576. const { symbol, rate } = getCurrencyConfig();
  1577. // 1 ratio = $0.002 / 1K tokens
  1578. if (modelPrice !== -1) {
  1579. return i18next.t(
  1580. '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}',
  1581. {
  1582. symbol: symbol,
  1583. price: (modelPrice * rate).toFixed(6),
  1584. ratio: groupRatio,
  1585. total: (modelPrice * groupRatio * rate).toFixed(6),
  1586. ratioType: ratioLabel,
  1587. },
  1588. );
  1589. } else {
  1590. if (completionRatio === undefined) {
  1591. completionRatio = 0;
  1592. }
  1593. // try toFixed audioRatio
  1594. audioRatio = parseFloat(audioRatio).toFixed(6);
  1595. // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
  1596. let inputRatioPrice = modelRatio * 2.0;
  1597. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  1598. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  1599. // Calculate effective input tokens (non-cached + cached with ratio applied)
  1600. const effectiveInputTokens =
  1601. inputTokens - cacheTokens + cacheTokens * cacheRatio;
  1602. let textPrice =
  1603. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  1604. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  1605. let audioPrice =
  1606. (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
  1607. (audioCompletionTokens / 1000000) *
  1608. inputRatioPrice *
  1609. audioRatio *
  1610. audioCompletionRatio *
  1611. groupRatio;
  1612. let price = textPrice + audioPrice;
  1613. return (
  1614. <>
  1615. <article>
  1616. <p>
  1617. {i18next.t('提示价格:{{symbol}}{{price}} / 1M tokens', {
  1618. symbol: symbol,
  1619. price: (inputRatioPrice * rate).toFixed(6),
  1620. })}
  1621. </p>
  1622. <p>
  1623. {i18next.t(
  1624. '补全价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})',
  1625. {
  1626. symbol: symbol,
  1627. price: (inputRatioPrice * rate).toFixed(6),
  1628. total: (completionRatioPrice * rate).toFixed(6),
  1629. completionRatio: completionRatio,
  1630. },
  1631. )}
  1632. </p>
  1633. {cacheTokens > 0 && (
  1634. <p>
  1635. {i18next.t(
  1636. '缓存价格:{{symbol}}{{price}} * {{cacheRatio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  1637. {
  1638. symbol: symbol,
  1639. price: (inputRatioPrice * rate).toFixed(6),
  1640. total: (inputRatioPrice * cacheRatio * rate).toFixed(6),
  1641. cacheRatio: cacheRatio,
  1642. },
  1643. )}
  1644. </p>
  1645. )}
  1646. <p>
  1647. {i18next.t(
  1648. '音频提示价格:{{symbol}}{{price}} * {{audioRatio}} = {{symbol}}{{total}} / 1M tokens (音频倍率: {{audioRatio}})',
  1649. {
  1650. symbol: symbol,
  1651. price: (inputRatioPrice * rate).toFixed(6),
  1652. total: (inputRatioPrice * audioRatio * rate).toFixed(6),
  1653. audioRatio: audioRatio,
  1654. },
  1655. )}
  1656. </p>
  1657. <p>
  1658. {i18next.t(
  1659. '音频补全价格:{{symbol}}{{price}} * {{audioRatio}} * {{audioCompRatio}} = {{symbol}}{{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})',
  1660. {
  1661. symbol: symbol,
  1662. price: (inputRatioPrice * rate).toFixed(6),
  1663. total: (
  1664. inputRatioPrice *
  1665. audioRatio *
  1666. audioCompletionRatio *
  1667. rate
  1668. ).toFixed(6),
  1669. audioRatio: audioRatio,
  1670. audioCompRatio: audioCompletionRatio,
  1671. },
  1672. )}
  1673. </p>
  1674. <p>
  1675. {cacheTokens > 0
  1676. ? i18next.t(
  1677. '文字提示 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}',
  1678. {
  1679. nonCacheInput: inputTokens - cacheTokens,
  1680. cacheInput: cacheTokens,
  1681. symbol: symbol,
  1682. cachePrice: (inputRatioPrice * cacheRatio * rate).toFixed(
  1683. 6,
  1684. ),
  1685. price: (inputRatioPrice * rate).toFixed(6),
  1686. completion: completionTokens,
  1687. compPrice: (completionRatioPrice * rate).toFixed(6),
  1688. total: (textPrice * rate).toFixed(6),
  1689. },
  1690. )
  1691. : i18next.t(
  1692. '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}} + 文字补全 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}} = {{symbol}}{{total}}',
  1693. {
  1694. input: inputTokens,
  1695. symbol: symbol,
  1696. price: (inputRatioPrice * rate).toFixed(6),
  1697. completion: completionTokens,
  1698. compPrice: (completionRatioPrice * rate).toFixed(6),
  1699. total: (textPrice * rate).toFixed(6),
  1700. },
  1701. )}
  1702. </p>
  1703. <p>
  1704. {i18next.t(
  1705. '音频提示 {{input}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} = {{symbol}}{{total}}',
  1706. {
  1707. input: audioInputTokens,
  1708. completion: audioCompletionTokens,
  1709. symbol: symbol,
  1710. audioInputPrice: (audioRatio * inputRatioPrice * rate).toFixed(
  1711. 6,
  1712. ),
  1713. audioCompPrice: (
  1714. audioRatio *
  1715. audioCompletionRatio *
  1716. inputRatioPrice *
  1717. rate
  1718. ).toFixed(6),
  1719. total: (audioPrice * rate).toFixed(6),
  1720. },
  1721. )}
  1722. </p>
  1723. <p>
  1724. {i18next.t(
  1725. '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = {{symbol}}{{total}}',
  1726. {
  1727. symbol: symbol,
  1728. total: (price * rate).toFixed(6),
  1729. textPrice: (textPrice * rate).toFixed(6),
  1730. audioPrice: (audioPrice * rate).toFixed(6),
  1731. },
  1732. )}
  1733. </p>
  1734. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  1735. </article>
  1736. </>
  1737. );
  1738. }
  1739. }
  1740. export function renderQuotaWithPrompt(quota, digits) {
  1741. const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  1742. if (quotaDisplayType !== 'TOKENS') {
  1743. return i18next.t('等价金额:') + renderQuota(quota, digits);
  1744. }
  1745. return '';
  1746. }
  1747. export function renderClaudeModelPrice(
  1748. inputTokens,
  1749. completionTokens,
  1750. modelRatio,
  1751. modelPrice = -1,
  1752. completionRatio,
  1753. groupRatio,
  1754. user_group_ratio,
  1755. cacheTokens = 0,
  1756. cacheRatio = 1.0,
  1757. cacheCreationTokens = 0,
  1758. cacheCreationRatio = 1.0,
  1759. cacheCreationTokens5m = 0,
  1760. cacheCreationRatio5m = 1.0,
  1761. cacheCreationTokens1h = 0,
  1762. cacheCreationRatio1h = 1.0,
  1763. ) {
  1764. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  1765. groupRatio,
  1766. user_group_ratio,
  1767. );
  1768. groupRatio = effectiveGroupRatio;
  1769. // 获取货币配置
  1770. const { symbol, rate } = getCurrencyConfig();
  1771. if (modelPrice !== -1) {
  1772. return i18next.t(
  1773. '模型价格:{{symbol}}{{price}} * {{ratioType}}:{{ratio}} = {{symbol}}{{total}}',
  1774. {
  1775. symbol: symbol,
  1776. price: (modelPrice * rate).toFixed(6),
  1777. ratioType: ratioLabel,
  1778. ratio: groupRatio,
  1779. total: (modelPrice * groupRatio * rate).toFixed(6),
  1780. },
  1781. );
  1782. } else {
  1783. if (completionRatio === undefined) {
  1784. completionRatio = 0;
  1785. }
  1786. const completionRatioValue = completionRatio || 0;
  1787. const inputRatioPrice = modelRatio * 2.0;
  1788. const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
  1789. const cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  1790. const cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
  1791. const cacheCreationRatioPrice5m = modelRatio * 2.0 * cacheCreationRatio5m;
  1792. const cacheCreationRatioPrice1h = modelRatio * 2.0 * cacheCreationRatio1h;
  1793. const hasSplitCacheCreation =
  1794. cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
  1795. const shouldShowCache = cacheTokens > 0;
  1796. const shouldShowLegacyCacheCreation =
  1797. !hasSplitCacheCreation && cacheCreationTokens > 0;
  1798. const shouldShowCacheCreation5m =
  1799. hasSplitCacheCreation && cacheCreationTokens5m > 0;
  1800. const shouldShowCacheCreation1h =
  1801. hasSplitCacheCreation && cacheCreationTokens1h > 0;
  1802. // Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
  1803. const nonCachedTokens = inputTokens;
  1804. const legacyCacheCreationTokens = hasSplitCacheCreation
  1805. ? 0
  1806. : cacheCreationTokens;
  1807. const effectiveInputTokens =
  1808. nonCachedTokens +
  1809. cacheTokens * cacheRatio +
  1810. legacyCacheCreationTokens * cacheCreationRatio +
  1811. cacheCreationTokens5m * cacheCreationRatio5m +
  1812. cacheCreationTokens1h * cacheCreationRatio1h;
  1813. const userRatio = 1.0;
  1814. let price =
  1815. ((effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  1816. (completionTokens / 1000000) * completionRatioPrice * groupRatio) *
  1817. userRatio;
  1818. const inputUnitPrice = inputRatioPrice * rate;
  1819. const completionUnitPrice = completionRatioPrice * rate;
  1820. const cacheUnitPrice = cacheRatioPrice * rate;
  1821. const cacheCreationUnitPrice = cacheCreationRatioPrice * rate;
  1822. const cacheCreationUnitPrice5m = cacheCreationRatioPrice5m * rate;
  1823. const cacheCreationUnitPrice1h = cacheCreationRatioPrice1h * rate;
  1824. const cacheCreationUnitPriceTotal =
  1825. cacheCreationUnitPrice5m + cacheCreationUnitPrice1h;
  1826. const breakdownSegments = [
  1827. i18next.t('提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', {
  1828. input: inputTokens,
  1829. symbol,
  1830. price: inputUnitPrice.toFixed(6),
  1831. }),
  1832. ];
  1833. if (shouldShowCache) {
  1834. breakdownSegments.push(
  1835. i18next.t(
  1836. '缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})',
  1837. {
  1838. tokens: cacheTokens,
  1839. symbol,
  1840. price: cacheUnitPrice.toFixed(6),
  1841. ratio: cacheRatio,
  1842. },
  1843. ),
  1844. );
  1845. }
  1846. if (shouldShowLegacyCacheCreation) {
  1847. breakdownSegments.push(
  1848. i18next.t(
  1849. '缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})',
  1850. {
  1851. tokens: cacheCreationTokens,
  1852. symbol,
  1853. price: cacheCreationUnitPrice.toFixed(6),
  1854. ratio: cacheCreationRatio,
  1855. },
  1856. ),
  1857. );
  1858. }
  1859. if (shouldShowCacheCreation5m) {
  1860. breakdownSegments.push(
  1861. i18next.t(
  1862. '5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})',
  1863. {
  1864. tokens: cacheCreationTokens5m,
  1865. symbol,
  1866. price: cacheCreationUnitPrice5m.toFixed(6),
  1867. ratio: cacheCreationRatio5m,
  1868. },
  1869. ),
  1870. );
  1871. }
  1872. if (shouldShowCacheCreation1h) {
  1873. breakdownSegments.push(
  1874. i18next.t(
  1875. '1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}} (倍率: {{ratio}})',
  1876. {
  1877. tokens: cacheCreationTokens1h,
  1878. symbol,
  1879. price: cacheCreationUnitPrice1h.toFixed(6),
  1880. ratio: cacheCreationRatio1h,
  1881. },
  1882. ),
  1883. );
  1884. }
  1885. breakdownSegments.push(
  1886. i18next.t(
  1887. '补全 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}',
  1888. {
  1889. completion: completionTokens,
  1890. symbol,
  1891. price: completionUnitPrice.toFixed(6),
  1892. },
  1893. ),
  1894. );
  1895. const breakdownText = breakdownSegments.join(' + ');
  1896. return (
  1897. <>
  1898. <article>
  1899. <p>
  1900. {i18next.t('提示价格:{{symbol}}{{price}} / 1M tokens', {
  1901. symbol: symbol,
  1902. price: (inputRatioPrice * rate).toFixed(6),
  1903. })}
  1904. </p>
  1905. <p>
  1906. {i18next.t(
  1907. '补全价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens',
  1908. {
  1909. symbol: symbol,
  1910. price: (inputRatioPrice * rate).toFixed(6),
  1911. ratio: completionRatio,
  1912. total: (completionRatioPrice * rate).toFixed(6),
  1913. },
  1914. )}
  1915. </p>
  1916. {shouldShowCache && (
  1917. <p>
  1918. {i18next.t(
  1919. '缓存价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  1920. {
  1921. symbol: symbol,
  1922. price: (inputRatioPrice * rate).toFixed(6),
  1923. ratio: cacheRatio,
  1924. total: cacheUnitPrice.toFixed(6),
  1925. cacheRatio: cacheRatio,
  1926. },
  1927. )}
  1928. </p>
  1929. )}
  1930. {shouldShowLegacyCacheCreation && (
  1931. <p>
  1932. {i18next.t(
  1933. '缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
  1934. {
  1935. symbol: symbol,
  1936. price: (inputRatioPrice * rate).toFixed(6),
  1937. ratio: cacheCreationRatio,
  1938. total: cacheCreationUnitPrice.toFixed(6),
  1939. cacheCreationRatio: cacheCreationRatio,
  1940. },
  1941. )}
  1942. </p>
  1943. )}
  1944. {shouldShowCacheCreation5m && (
  1945. <p>
  1946. {i18next.t(
  1947. '5m缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (5m缓存创建倍率: {{cacheCreationRatio5m}})',
  1948. {
  1949. symbol: symbol,
  1950. price: (inputRatioPrice * rate).toFixed(6),
  1951. ratio: cacheCreationRatio5m,
  1952. total: cacheCreationUnitPrice5m.toFixed(6),
  1953. cacheCreationRatio5m: cacheCreationRatio5m,
  1954. },
  1955. )}
  1956. </p>
  1957. )}
  1958. {shouldShowCacheCreation1h && (
  1959. <p>
  1960. {i18next.t(
  1961. '1h缓存创建价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (1h缓存创建倍率: {{cacheCreationRatio1h}})',
  1962. {
  1963. symbol: symbol,
  1964. price: (inputRatioPrice * rate).toFixed(6),
  1965. ratio: cacheCreationRatio1h,
  1966. total: cacheCreationUnitPrice1h.toFixed(6),
  1967. cacheCreationRatio1h: cacheCreationRatio1h,
  1968. },
  1969. )}
  1970. </p>
  1971. )}
  1972. {shouldShowCacheCreation5m && shouldShowCacheCreation1h && (
  1973. <p>
  1974. {i18next.t(
  1975. '缓存创建价格合计:5m {{symbol}}{{five}} + 1h {{symbol}}{{one}} = {{symbol}}{{total}} / 1M tokens',
  1976. {
  1977. symbol: symbol,
  1978. five: cacheCreationUnitPrice5m.toFixed(6),
  1979. one: cacheCreationUnitPrice1h.toFixed(6),
  1980. total: cacheCreationUnitPriceTotal.toFixed(6),
  1981. },
  1982. )}
  1983. </p>
  1984. )}
  1985. <p></p>
  1986. <p>
  1987. {i18next.t(
  1988. '{{breakdown}} * {{ratioType}} {{ratio}}{{userRatioText}} = {{symbol}}{{total}}',
  1989. {
  1990. breakdown: breakdownText,
  1991. ratioType: ratioLabel,
  1992. ratio: groupRatio,
  1993. userRatioText: userRatio !== 1.0 ? i18next.t(' * 用户倍率 {{userRatio}}', { userRatio }) : '',
  1994. symbol: symbol,
  1995. total: (price * rate).toFixed(6),
  1996. },
  1997. )}
  1998. </p>
  1999. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  2000. </article>
  2001. </>
  2002. );
  2003. }
  2004. }
  2005. export function renderClaudeLogContent(
  2006. modelRatio,
  2007. completionRatio,
  2008. modelPrice = -1,
  2009. groupRatio,
  2010. user_group_ratio,
  2011. cacheRatio = 1.0,
  2012. cacheCreationRatio = 1.0,
  2013. cacheCreationTokens5m = 0,
  2014. cacheCreationRatio5m = 1.0,
  2015. cacheCreationTokens1h = 0,
  2016. cacheCreationRatio1h = 1.0,
  2017. ) {
  2018. const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
  2019. groupRatio,
  2020. user_group_ratio,
  2021. );
  2022. groupRatio = effectiveGroupRatio;
  2023. // 获取货币配置
  2024. const { symbol, rate } = getCurrencyConfig();
  2025. const userRatioSuffix = '';
  2026. if (modelPrice !== -1) {
  2027. return i18next.t('模型价格 {{symbol}}{{price}},{{ratioType}} {{ratio}}', {
  2028. symbol: symbol,
  2029. price: (modelPrice * rate).toFixed(6),
  2030. ratioType: ratioLabel,
  2031. ratio: groupRatio,
  2032. }) + userRatioSuffix;
  2033. } else {
  2034. const hasSplitCacheCreation =
  2035. cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
  2036. const shouldShowCacheCreation5m =
  2037. hasSplitCacheCreation && cacheCreationTokens5m > 0;
  2038. const shouldShowCacheCreation1h =
  2039. hasSplitCacheCreation && cacheCreationTokens1h > 0;
  2040. let cacheCreationPart = null;
  2041. if (hasSplitCacheCreation) {
  2042. if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
  2043. cacheCreationPart = i18next.t(
  2044. '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
  2045. {
  2046. cacheCreationRatio5m,
  2047. cacheCreationRatio1h,
  2048. },
  2049. );
  2050. } else if (shouldShowCacheCreation5m) {
  2051. cacheCreationPart = i18next.t(
  2052. '缓存创建倍率 5m {{cacheCreationRatio5m}}',
  2053. {
  2054. cacheCreationRatio5m,
  2055. },
  2056. );
  2057. } else if (shouldShowCacheCreation1h) {
  2058. cacheCreationPart = i18next.t(
  2059. '缓存创建倍率 1h {{cacheCreationRatio1h}}',
  2060. {
  2061. cacheCreationRatio1h,
  2062. },
  2063. );
  2064. }
  2065. }
  2066. if (!cacheCreationPart) {
  2067. cacheCreationPart = i18next.t('缓存创建倍率 {{cacheCreationRatio}}', {
  2068. cacheCreationRatio,
  2069. });
  2070. }
  2071. const parts = [
  2072. i18next.t('模型倍率 {{modelRatio}}', { modelRatio }),
  2073. i18next.t('输出倍率 {{completionRatio}}', { completionRatio }),
  2074. i18next.t('缓存倍率 {{cacheRatio}}', { cacheRatio }),
  2075. cacheCreationPart,
  2076. i18next.t('{{ratioType}} {{ratio}}', {
  2077. ratioType: ratioLabel,
  2078. ratio: groupRatio,
  2079. }),
  2080. ];
  2081. return parts.join(',') + userRatioSuffix;
  2082. }
  2083. }
  2084. // 已统一至 renderModelPriceSimple,若仍有遗留引用,请改为传入 provider='claude'
  2085. /**
  2086. * rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
  2087. * 仅在流式渲染阶段使用,避免已渲染文字重复动画。
  2088. */
  2089. export function rehypeSplitWordsIntoSpans(options = {}) {
  2090. const { previousContentLength = 0 } = options;
  2091. return (tree) => {
  2092. let currentCharCount = 0; // 当前已处理的字符数
  2093. visit(tree, 'element', (node) => {
  2094. if (
  2095. ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(
  2096. node.tagName,
  2097. ) &&
  2098. node.children
  2099. ) {
  2100. const newChildren = [];
  2101. node.children.forEach((child) => {
  2102. if (child.type === 'text') {
  2103. try {
  2104. // 使用 Intl.Segmenter 精准拆分中英文及标点
  2105. const segmenter = new Intl.Segmenter('zh', {
  2106. granularity: 'word',
  2107. });
  2108. const segments = segmenter.segment(child.value);
  2109. Array.from(segments)
  2110. .map((seg) => seg.segment)
  2111. .filter(Boolean)
  2112. .forEach((word) => {
  2113. const wordStartPos = currentCharCount;
  2114. const wordEndPos = currentCharCount + word.length;
  2115. // 判断这个词是否是新增的(在 previousContentLength 之后)
  2116. const isNewContent = wordStartPos >= previousContentLength;
  2117. newChildren.push({
  2118. type: 'element',
  2119. tagName: 'span',
  2120. properties: {
  2121. className: isNewContent ? ['animate-fade-in'] : [],
  2122. },
  2123. children: [{ type: 'text', value: word }],
  2124. });
  2125. currentCharCount = wordEndPos;
  2126. });
  2127. } catch (_) {
  2128. // Fallback:如果浏览器不支持 Segmenter
  2129. const textStartPos = currentCharCount;
  2130. const isNewContent = textStartPos >= previousContentLength;
  2131. if (isNewContent) {
  2132. // 新内容,添加动画
  2133. newChildren.push({
  2134. type: 'element',
  2135. tagName: 'span',
  2136. properties: {
  2137. className: ['animate-fade-in'],
  2138. },
  2139. children: [{ type: 'text', value: child.value }],
  2140. });
  2141. } else {
  2142. // 旧内容,不添加动画
  2143. newChildren.push(child);
  2144. }
  2145. currentCharCount += child.value.length;
  2146. }
  2147. } else {
  2148. newChildren.push(child);
  2149. }
  2150. });
  2151. node.children = newChildren;
  2152. }
  2153. });
  2154. };
  2155. }