ModelPricing.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
  2. import { API, copy, showError, showInfo, showSuccess } from '../helpers';
  3. import { useTranslation } from 'react-i18next';
  4. import {
  5. Banner,
  6. Input,
  7. Layout,
  8. Modal,
  9. Space,
  10. Table,
  11. Tag,
  12. Tooltip,
  13. Popover,
  14. ImagePreview,
  15. Button,
  16. } from '@douyinfe/semi-ui';
  17. import {
  18. IconMore,
  19. IconVerify,
  20. IconUploadError,
  21. IconHelpCircle,
  22. } from '@douyinfe/semi-icons';
  23. import { UserContext } from '../context/User/index.js';
  24. import Text from '@douyinfe/semi-ui/lib/es/typography/text';
  25. const ModelPricing = () => {
  26. const { t } = useTranslation();
  27. const [filteredValue, setFilteredValue] = useState([]);
  28. const compositionRef = useRef({ isComposition: false });
  29. const [selectedRowKeys, setSelectedRowKeys] = useState([]);
  30. const [modalImageUrl, setModalImageUrl] = useState('');
  31. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  32. const [selectedGroup, setSelectedGroup] = useState('default');
  33. const rowSelection = useMemo(
  34. () => ({
  35. onChange: (selectedRowKeys, selectedRows) => {
  36. setSelectedRowKeys(selectedRowKeys);
  37. },
  38. }),
  39. []
  40. );
  41. const handleChange = (value) => {
  42. if (compositionRef.current.isComposition) {
  43. return;
  44. }
  45. const newFilteredValue = value ? [value] : [];
  46. setFilteredValue(newFilteredValue);
  47. };
  48. const handleCompositionStart = () => {
  49. compositionRef.current.isComposition = true;
  50. };
  51. const handleCompositionEnd = (event) => {
  52. compositionRef.current.isComposition = false;
  53. const value = event.target.value;
  54. const newFilteredValue = value ? [value] : [];
  55. setFilteredValue(newFilteredValue);
  56. };
  57. function renderQuotaType(type) {
  58. // Ensure all cases are string literals by adding quotes.
  59. switch (type) {
  60. case 1:
  61. return (
  62. <Tag color='teal' size='large'>
  63. {t('按次计费')}
  64. </Tag>
  65. );
  66. case 0:
  67. return (
  68. <Tag color='violet' size='large'>
  69. {t('按量计费')}
  70. </Tag>
  71. );
  72. default:
  73. return t('未知');
  74. }
  75. }
  76. function renderAvailable(available) {
  77. return available ? (
  78. <Popover
  79. content={
  80. <div style={{ padding: 8 }}>{t('您的分组可以使用该模型')}</div>
  81. }
  82. position='top'
  83. key={available}
  84. style={{
  85. backgroundColor: 'rgba(var(--semi-blue-4),1)',
  86. borderColor: 'rgba(var(--semi-blue-4),1)',
  87. color: 'var(--semi-color-white)',
  88. borderWidth: 1,
  89. borderStyle: 'solid',
  90. }}
  91. >
  92. <IconVerify style={{ color: 'green' }} size="large" />
  93. </Popover>
  94. ) : (
  95. <Popover
  96. content={
  97. <div style={{ padding: 8 }}>{t('您的分组无权使用该模型')}</div>
  98. }
  99. position='top'
  100. key={available}
  101. style={{
  102. backgroundColor: 'rgba(var(--semi-blue-4),1)',
  103. borderColor: 'rgba(var(--semi-blue-4),1)',
  104. color: 'var(--semi-color-white)',
  105. borderWidth: 1,
  106. borderStyle: 'solid',
  107. }}
  108. >
  109. <IconUploadError style={{ color: '#FFA54F' }} size="large" />
  110. </Popover>
  111. );
  112. }
  113. const columns = [
  114. {
  115. title: t('可用性'),
  116. dataIndex: 'available',
  117. render: (text, record, index) => {
  118. // if record.enable_groups contains selectedGroup, then available is true
  119. return renderAvailable(record.enable_groups.includes(selectedGroup));
  120. },
  121. sorter: (a, b) => a.available - b.available,
  122. },
  123. {
  124. title: t('模型名称'),
  125. dataIndex: 'model_name',
  126. render: (text, record, index) => {
  127. return (
  128. <>
  129. <Tag
  130. color='green'
  131. size='large'
  132. onClick={() => {
  133. copyText(text);
  134. }}
  135. >
  136. {text}
  137. </Tag>
  138. </>
  139. );
  140. },
  141. onFilter: (value, record) =>
  142. record.model_name.toLowerCase().includes(value.toLowerCase()),
  143. filteredValue,
  144. },
  145. {
  146. title: t('计费类型'),
  147. dataIndex: 'quota_type',
  148. render: (text, record, index) => {
  149. return renderQuotaType(parseInt(text));
  150. },
  151. sorter: (a, b) => a.quota_type - b.quota_type,
  152. },
  153. {
  154. title: t('可用分组'),
  155. dataIndex: 'enable_groups',
  156. render: (text, record, index) => {
  157. // enable_groups is a string array
  158. return (
  159. <Space>
  160. {text.map((group) => {
  161. if (usableGroup[group]) {
  162. if (group === selectedGroup) {
  163. return (
  164. <Tag
  165. color='blue'
  166. size='large'
  167. prefixIcon={<IconVerify />}
  168. >
  169. {group}
  170. </Tag>
  171. );
  172. } else {
  173. return (
  174. <Tag
  175. color='blue'
  176. size='large'
  177. onClick={() => {
  178. setSelectedGroup(group);
  179. showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
  180. group: group,
  181. ratio: groupRatio[group]
  182. }));
  183. }}
  184. >
  185. {group}
  186. </Tag>
  187. );
  188. }
  189. }
  190. })}
  191. </Space>
  192. );
  193. },
  194. },
  195. {
  196. title: () => (
  197. <span style={{'display':'flex','alignItems':'center'}}>
  198. {t('倍率')}
  199. <Popover
  200. content={
  201. <div style={{ padding: 8 }}>
  202. {t('倍率是为了方便换算不同价格的模型')}<br/>
  203. {t('点击查看倍率说明')}
  204. </div>
  205. }
  206. position='top'
  207. style={{
  208. backgroundColor: 'rgba(var(--semi-blue-4),1)',
  209. borderColor: 'rgba(var(--semi-blue-4),1)',
  210. color: 'var(--semi-color-white)',
  211. borderWidth: 1,
  212. borderStyle: 'solid',
  213. }}
  214. >
  215. <IconHelpCircle
  216. onClick={() => {
  217. setModalImageUrl('/ratio.png');
  218. setIsModalOpenurl(true);
  219. }}
  220. />
  221. </Popover>
  222. </span>
  223. ),
  224. dataIndex: 'model_ratio',
  225. render: (text, record, index) => {
  226. let content = text;
  227. let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
  228. content = (
  229. <>
  230. <Text>{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}</Text>
  231. <br />
  232. <Text>{t('补全倍率')}:{record.quota_type === 0 ? completionRatio : t('无')}</Text>
  233. <br />
  234. <Text>{t('分组倍率')}:{groupRatio[selectedGroup]}</Text>
  235. </>
  236. );
  237. return <div>{content}</div>;
  238. },
  239. },
  240. {
  241. title: t('模型价格'),
  242. dataIndex: 'model_price',
  243. render: (text, record, index) => {
  244. let content = text;
  245. if (record.quota_type === 0) {
  246. // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
  247. let inputRatioPrice = record.model_ratio * 2 * groupRatio[selectedGroup];
  248. let completionRatioPrice =
  249. record.model_ratio *
  250. record.completion_ratio * 2 *
  251. groupRatio[selectedGroup];
  252. content = (
  253. <>
  254. <Text>{t('提示')} ${inputRatioPrice} / 1M tokens</Text>
  255. <br />
  256. <Text>{t('补全')} ${completionRatioPrice} / 1M tokens</Text>
  257. </>
  258. );
  259. } else {
  260. let price = parseFloat(text) * groupRatio[selectedGroup];
  261. content = <>${t('模型价格')}:${price}</>;
  262. }
  263. return <div>{content}</div>;
  264. },
  265. },
  266. ];
  267. const [models, setModels] = useState([]);
  268. const [loading, setLoading] = useState(true);
  269. const [userState, userDispatch] = useContext(UserContext);
  270. const [groupRatio, setGroupRatio] = useState({});
  271. const [usableGroup, setUsableGroup] = useState({});
  272. const setModelsFormat = (models, groupRatio) => {
  273. for (let i = 0; i < models.length; i++) {
  274. models[i].key = models[i].model_name;
  275. models[i].group_ratio = groupRatio[models[i].model_name];
  276. }
  277. // sort by quota_type
  278. models.sort((a, b) => {
  279. return a.quota_type - b.quota_type;
  280. });
  281. // sort by model_name, start with gpt is max, other use localeCompare
  282. models.sort((a, b) => {
  283. if (a.model_name.startsWith('gpt') && !b.model_name.startsWith('gpt')) {
  284. return -1;
  285. } else if (
  286. !a.model_name.startsWith('gpt') &&
  287. b.model_name.startsWith('gpt')
  288. ) {
  289. return 1;
  290. } else {
  291. return a.model_name.localeCompare(b.model_name);
  292. }
  293. });
  294. setModels(models);
  295. };
  296. const loadPricing = async () => {
  297. setLoading(true);
  298. let url = '';
  299. url = `/api/pricing`;
  300. const res = await API.get(url);
  301. const { success, message, data, group_ratio, usable_group } = res.data;
  302. if (success) {
  303. setGroupRatio(group_ratio);
  304. setUsableGroup(usable_group);
  305. setSelectedGroup(userState.user ? userState.user.group : 'default')
  306. setModelsFormat(data, group_ratio);
  307. } else {
  308. showError(message);
  309. }
  310. setLoading(false);
  311. };
  312. const refresh = async () => {
  313. await loadPricing();
  314. };
  315. const copyText = async (text) => {
  316. if (await copy(text)) {
  317. showSuccess('已复制:' + text);
  318. } else {
  319. // setSearchKeyword(text);
  320. Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
  321. }
  322. };
  323. useEffect(() => {
  324. refresh().then();
  325. }, []);
  326. return (
  327. <>
  328. <Layout>
  329. {userState.user ? (
  330. <Banner
  331. type="success"
  332. fullMode={false}
  333. closeIcon="null"
  334. description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', {
  335. group: userState.user.group,
  336. ratio: groupRatio[userState.user.group]
  337. })}
  338. />
  339. ) : (
  340. <Banner
  341. type='warning'
  342. fullMode={false}
  343. closeIcon="null"
  344. description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', {
  345. ratio: groupRatio['default']
  346. })}
  347. />
  348. )}
  349. <br/>
  350. <Banner
  351. type="info"
  352. fullMode={false}
  353. description={<div>{t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}</div>}
  354. closeIcon="null"
  355. />
  356. <br/>
  357. <Space style={{ marginBottom: 16 }}>
  358. <Input
  359. placeholder={t('模糊搜索模型名称')}
  360. style={{ width: 200 }}
  361. onCompositionStart={handleCompositionStart}
  362. onCompositionEnd={handleCompositionEnd}
  363. onChange={handleChange}
  364. showClear
  365. />
  366. <Button
  367. theme='light'
  368. type='tertiary'
  369. style={{width: 150}}
  370. onClick={() => {
  371. copyText(selectedRowKeys);
  372. }}
  373. disabled={selectedRowKeys == ""}
  374. >
  375. {t('复制选中模型')}
  376. </Button>
  377. </Space>
  378. <Table
  379. style={{ marginTop: 5 }}
  380. columns={columns}
  381. dataSource={models}
  382. loading={loading}
  383. pagination={{
  384. formatPageText: (page) =>
  385. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  386. start: page.currentStart,
  387. end: page.currentEnd,
  388. total: models.length
  389. }),
  390. pageSize: models.length,
  391. showSizeChanger: false,
  392. }}
  393. rowSelection={rowSelection}
  394. />
  395. <ImagePreview
  396. src={modalImageUrl}
  397. visible={isModalOpenurl}
  398. onVisibleChange={(visible) => setIsModalOpenurl(visible)}
  399. />
  400. </Layout>
  401. </>
  402. );
  403. };
  404. export default ModelPricing;