ModelPricingColumnDefs.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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 React from 'react';
  16. import { Tag, Space, Tooltip, Switch } from '@douyinfe/semi-ui';
  17. import { IconVerify, IconHelpCircle } from '@douyinfe/semi-icons';
  18. import { Popover } from '@douyinfe/semi-ui';
  19. import { renderModelTag, stringToColor } from '../../../helpers';
  20. function renderQuotaType(type, t) {
  21. switch (type) {
  22. case 1:
  23. return (
  24. <Tag color='teal' shape='circle'>
  25. {t('按次计费')}
  26. </Tag>
  27. );
  28. case 0:
  29. return (
  30. <Tag color='violet' shape='circle'>
  31. {t('按量计费')}
  32. </Tag>
  33. );
  34. default:
  35. return t('未知');
  36. }
  37. }
  38. function renderAvailable(available, t) {
  39. return available ? (
  40. <Popover
  41. content={
  42. <div style={{ padding: 8 }}>{t('您的分组可以使用该模型')}</div>
  43. }
  44. position='top'
  45. key={available}
  46. className="bg-green-50"
  47. >
  48. <IconVerify style={{ color: 'rgb(22 163 74)' }} size='large' />
  49. </Popover>
  50. ) : null;
  51. }
  52. function renderSupportedEndpoints(endpoints) {
  53. if (!endpoints || endpoints.length === 0) {
  54. return null;
  55. }
  56. return (
  57. <Space wrap>
  58. {endpoints.map((endpoint, idx) => (
  59. <Tag
  60. key={endpoint}
  61. color={stringToColor(endpoint)}
  62. shape='circle'
  63. >
  64. {endpoint}
  65. </Tag>
  66. ))}
  67. </Space>
  68. );
  69. }
  70. export const getModelPricingColumns = ({
  71. t,
  72. selectedGroup,
  73. usableGroup,
  74. groupRatio,
  75. copyText,
  76. setModalImageUrl,
  77. setIsModalOpenurl,
  78. currency,
  79. showWithRecharge,
  80. tokenUnit,
  81. setTokenUnit,
  82. displayPrice,
  83. handleGroupClick,
  84. }) => {
  85. return [
  86. {
  87. title: t('可用性'),
  88. dataIndex: 'available',
  89. render: (text, record, index) => {
  90. return renderAvailable(record.enable_groups.includes(selectedGroup), t);
  91. },
  92. sorter: (a, b) => {
  93. const aAvailable = a.enable_groups.includes(selectedGroup);
  94. const bAvailable = b.enable_groups.includes(selectedGroup);
  95. return Number(aAvailable) - Number(bAvailable);
  96. },
  97. defaultSortOrder: 'descend',
  98. },
  99. {
  100. title: t('可用端点类型'),
  101. dataIndex: 'supported_endpoint_types',
  102. render: (text, record, index) => {
  103. return renderSupportedEndpoints(text);
  104. },
  105. },
  106. {
  107. title: t('模型名称'),
  108. dataIndex: 'model_name',
  109. render: (text, record, index) => {
  110. return renderModelTag(text, {
  111. onClick: () => {
  112. copyText(text);
  113. }
  114. });
  115. },
  116. onFilter: (value, record) =>
  117. record.model_name.toLowerCase().includes(value.toLowerCase()),
  118. },
  119. {
  120. title: t('计费类型'),
  121. dataIndex: 'quota_type',
  122. render: (text, record, index) => {
  123. return renderQuotaType(parseInt(text), t);
  124. },
  125. sorter: (a, b) => a.quota_type - b.quota_type,
  126. },
  127. {
  128. title: t('可用分组'),
  129. dataIndex: 'enable_groups',
  130. render: (text, record, index) => {
  131. return (
  132. <Space wrap>
  133. {text.map((group) => {
  134. if (usableGroup[group]) {
  135. if (group === selectedGroup) {
  136. return (
  137. <Tag key={group} color='blue' shape='circle' prefixIcon={<IconVerify />}>
  138. {group}
  139. </Tag>
  140. );
  141. } else {
  142. return (
  143. <Tag
  144. key={group}
  145. color='blue'
  146. shape='circle'
  147. onClick={() => handleGroupClick(group)}
  148. className="cursor-pointer hover:opacity-80 transition-opacity"
  149. >
  150. {group}
  151. </Tag>
  152. );
  153. }
  154. }
  155. })}
  156. </Space>
  157. );
  158. },
  159. },
  160. {
  161. title: () => (
  162. <div className="flex items-center space-x-1">
  163. <span>{t('倍率')}</span>
  164. <Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
  165. <IconHelpCircle
  166. className="text-blue-500 cursor-pointer"
  167. onClick={() => {
  168. setModalImageUrl('/ratio.png');
  169. setIsModalOpenurl(true);
  170. }}
  171. />
  172. </Tooltip>
  173. </div>
  174. ),
  175. dataIndex: 'model_ratio',
  176. render: (text, record, index) => {
  177. let content = text;
  178. let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
  179. content = (
  180. <div className="space-y-1">
  181. <div className="text-gray-700">
  182. {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
  183. </div>
  184. <div className="text-gray-700">
  185. {t('补全倍率')}:
  186. {record.quota_type === 0 ? completionRatio : t('无')}
  187. </div>
  188. <div className="text-gray-700">
  189. {t('分组倍率')}:{groupRatio[selectedGroup]}
  190. </div>
  191. </div>
  192. );
  193. return content;
  194. },
  195. },
  196. {
  197. title: (
  198. <div className="flex items-center space-x-2">
  199. <span>{t('模型价格')}</span>
  200. {/* 计费单位切换 */}
  201. <Switch
  202. checked={tokenUnit === 'K'}
  203. onChange={(checked) => setTokenUnit(checked ? 'K' : 'M')}
  204. checkedText="K"
  205. uncheckedText="M"
  206. />
  207. </div>
  208. ),
  209. dataIndex: 'model_price',
  210. render: (text, record, index) => {
  211. let content = text;
  212. if (record.quota_type === 0) {
  213. let inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup];
  214. let completionRatioPriceUSD =
  215. record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup];
  216. const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
  217. const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
  218. let displayInput = displayPrice(inputRatioPriceUSD);
  219. let displayCompletion = displayPrice(completionRatioPriceUSD);
  220. const divisor = unitDivisor;
  221. const numInput = parseFloat(displayInput.replace(/[^0-9.]/g, '')) / divisor;
  222. const numCompletion = parseFloat(displayCompletion.replace(/[^0-9.]/g, '')) / divisor;
  223. displayInput = `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(3)}`;
  224. displayCompletion = `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(3)}`;
  225. content = (
  226. <div className="space-y-1">
  227. <div className="text-gray-700">
  228. {t('提示')} {displayInput} / 1{unitLabel} tokens
  229. </div>
  230. <div className="text-gray-700">
  231. {t('补全')} {displayCompletion} / 1{unitLabel} tokens
  232. </div>
  233. </div>
  234. );
  235. } else {
  236. let priceUSD = parseFloat(text) * groupRatio[selectedGroup];
  237. let displayVal = displayPrice(priceUSD);
  238. content = (
  239. <div className="text-gray-700">
  240. {t('模型价格')}:{displayVal}
  241. </div>
  242. );
  243. }
  244. return content;
  245. },
  246. },
  247. ];
  248. };