ModelPricing.js 11 KB

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