Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

1048 строки
35 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 React, { useEffect, useState, useRef } from 'react';
  16. import {
  17. Table,
  18. Button,
  19. Input,
  20. Modal,
  21. Form,
  22. Space,
  23. Typography,
  24. RadioGroup,
  25. Radio,
  26. Notification,
  27. } from '@douyinfe/semi-ui';
  28. import {
  29. IconDelete,
  30. IconPlus,
  31. IconSearch,
  32. IconSave,
  33. IconEdit,
  34. IconBolt,
  35. } from '@douyinfe/semi-icons';
  36. import { API, showError, showSuccess } from '../../../helpers';
  37. import { useTranslation } from 'react-i18next';
  38. export default function ModelRatioNotSetEditor(props) {
  39. const { t } = useTranslation();
  40. const [models, setModels] = useState([]);
  41. const [visible, setVisible] = useState(false);
  42. const [batchVisible, setBatchVisible] = useState(false);
  43. const [currentModel, setCurrentModel] = useState(null);
  44. const [searchText, setSearchText] = useState('');
  45. const [currentPage, setCurrentPage] = useState(1);
  46. const [pageSize, setPageSize] = useState(10);
  47. const [loading, setLoading] = useState(false);
  48. const [enabledModels, setEnabledModels] = useState([]);
  49. const [selectedRowKeys, setSelectedRowKeys] = useState([]);
  50. const [batchFillType, setBatchFillType] = useState('ratio');
  51. const [batchFillValue, setBatchFillValue] = useState('');
  52. const [batchRatioValue, setBatchRatioValue] = useState('');
  53. const [batchCompletionRatioValue, setBatchCompletionRatioValue] =
  54. useState('');
  55. const addFormRef = useRef(null);
  56. const editFormRef = useRef(null);
  57. const [editVisible, setEditVisible] = useState(false);
  58. const [editRecord, setEditRecord] = useState(null);
  59. const [pricingMode, setPricingMode] = useState('per-token');
  60. const [pricingSubMode, setPricingSubMode] = useState('ratio');
  61. const { Text } = Typography;
  62. // 定义可选的每页显示条数
  63. const pageSizeOptions = [10, 20, 50, 100];
  64. const getAllEnabledModels = async () => {
  65. try {
  66. const res = await API.get('/api/channel/models_enabled');
  67. const { success, message, data } = res.data;
  68. if (success) {
  69. setEnabledModels(data);
  70. } else {
  71. showError(message);
  72. }
  73. } catch (error) {
  74. console.error(t('获取启用模型失败:'), error);
  75. showError(t('获取启用模型失败'));
  76. }
  77. };
  78. useEffect(() => {
  79. // 获取所有启用的模型
  80. getAllEnabledModels();
  81. }, []);
  82. useEffect(() => {
  83. try {
  84. const modelPrice = JSON.parse(props.options.ModelPrice || '{}');
  85. const modelRatio = JSON.parse(props.options.ModelRatio || '{}');
  86. const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
  87. const cacheRatio = JSON.parse(props.options.CacheRatio || '{}');
  88. const createCacheRatio = JSON.parse(props.options.CreateCacheRatio || '{}');
  89. const imageRatio = JSON.parse(props.options.ImageRatio || '{}');
  90. const audioRatio = JSON.parse(props.options.AudioRatio || '{}');
  91. const audioCompletionRatio = JSON.parse(props.options.AudioCompletionRatio || '{}');
  92. // 找出所有未设置价格和倍率的模型
  93. const unsetModels = enabledModels.filter((modelName) => {
  94. const hasPrice = modelPrice[modelName] !== undefined;
  95. const hasRatio = modelRatio[modelName] !== undefined;
  96. // 如果模型没有价格或者没有倍率设置,则显示
  97. return !hasPrice && !hasRatio;
  98. });
  99. // 创建模型数据
  100. const modelData = unsetModels.map((name) => ({
  101. name,
  102. price: modelPrice[name] || '',
  103. ratio: modelRatio[name] || '',
  104. completionRatio: completionRatio[name] || '',
  105. cacheRatio: cacheRatio[name] ?? '',
  106. cacheCreationRatio: createCacheRatio[name] ?? '',
  107. imageRatio: imageRatio[name] ?? '',
  108. audioRatio: audioRatio[name] ?? '',
  109. audioCompletionRatio: audioCompletionRatio[name] ?? '',
  110. }));
  111. setModels(modelData);
  112. // 清空选择
  113. setSelectedRowKeys([]);
  114. } catch (error) {
  115. console.error(t('JSON解析错误:'), error);
  116. }
  117. }, [props.options, enabledModels]);
  118. // 首先声明分页相关的工具函数
  119. const getPagedData = (data, currentPage, pageSize) => {
  120. const start = (currentPage - 1) * pageSize;
  121. const end = start + pageSize;
  122. return data.slice(start, end);
  123. };
  124. // 处理页面大小变化
  125. const handlePageSizeChange = (size) => {
  126. setPageSize(size);
  127. // 重新计算当前页,避免数据丢失
  128. const totalPages = Math.ceil(filteredModels.length / size);
  129. if (currentPage > totalPages) {
  130. setCurrentPage(totalPages || 1);
  131. }
  132. };
  133. // 在 return 语句之前,先处理过滤和分页逻辑
  134. const filteredModels = models.filter((model) =>
  135. searchText ? model.name.includes(searchText) : true,
  136. );
  137. // 然后基于过滤后的数据计算分页数据
  138. const pagedData = getPagedData(filteredModels, currentPage, pageSize);
  139. const SubmitData = async () => {
  140. setLoading(true);
  141. const output = {
  142. ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
  143. ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
  144. CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
  145. CacheRatio: JSON.parse(props.options.CacheRatio || '{}'),
  146. CreateCacheRatio: JSON.parse(props.options.CreateCacheRatio || '{}'),
  147. ImageRatio: JSON.parse(props.options.ImageRatio || '{}'),
  148. AudioRatio: JSON.parse(props.options.AudioRatio || '{}'),
  149. AudioCompletionRatio: JSON.parse(props.options.AudioCompletionRatio || '{}'),
  150. };
  151. try {
  152. // 数据转换 - 只处理已修改的模型
  153. models.forEach((model) => {
  154. // 只有当用户设置了值时才更新
  155. if (model.price !== '') {
  156. // 如果价格不为空,则转换为浮点数,忽略倍率参数
  157. output.ModelPrice[model.name] = parseFloat(model.price);
  158. } else {
  159. if (model.ratio !== '')
  160. output.ModelRatio[model.name] = parseFloat(model.ratio);
  161. if (model.completionRatio !== '')
  162. output.CompletionRatio[model.name] = parseFloat(
  163. model.completionRatio,
  164. );
  165. }
  166. // 高级倍率字段(0 是有效值)
  167. if (model.cacheRatio !== '' && model.cacheRatio != null)
  168. output.CacheRatio[model.name] = parseFloat(model.cacheRatio);
  169. if (model.cacheCreationRatio !== '' && model.cacheCreationRatio != null)
  170. output.CreateCacheRatio[model.name] = parseFloat(model.cacheCreationRatio);
  171. if (model.imageRatio !== '' && model.imageRatio != null)
  172. output.ImageRatio[model.name] = parseFloat(model.imageRatio);
  173. if (model.audioRatio !== '' && model.audioRatio != null)
  174. output.AudioRatio[model.name] = parseFloat(model.audioRatio);
  175. if (model.audioCompletionRatio !== '' && model.audioCompletionRatio != null)
  176. output.AudioCompletionRatio[model.name] = parseFloat(model.audioCompletionRatio);
  177. });
  178. // 准备API请求数组
  179. const finalOutput = {
  180. ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
  181. ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
  182. CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
  183. CacheRatio: JSON.stringify(output.CacheRatio, null, 2),
  184. CreateCacheRatio: JSON.stringify(output.CreateCacheRatio, null, 2),
  185. ImageRatio: JSON.stringify(output.ImageRatio, null, 2),
  186. AudioRatio: JSON.stringify(output.AudioRatio, null, 2),
  187. AudioCompletionRatio: JSON.stringify(output.AudioCompletionRatio, null, 2),
  188. };
  189. const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
  190. return API.put('/api/option/', {
  191. key,
  192. value,
  193. });
  194. });
  195. // 批量处理请求
  196. const results = await Promise.all(requestQueue);
  197. // 验证结果
  198. if (requestQueue.length === 1) {
  199. if (results.includes(undefined)) return;
  200. } else if (requestQueue.length > 1) {
  201. if (results.includes(undefined)) {
  202. return showError(t('部分保存失败,请重试'));
  203. }
  204. }
  205. // 检查每个请求的结果
  206. for (const res of results) {
  207. if (!res.data.success) {
  208. return showError(res.data.message);
  209. }
  210. }
  211. showSuccess(t('保存成功'));
  212. props.refresh();
  213. // 重新获取未设置的模型
  214. getAllEnabledModels();
  215. } catch (error) {
  216. console.error(t('保存失败:'), error);
  217. showError(t('保存失败,请重试'));
  218. } finally {
  219. setLoading(false);
  220. }
  221. };
  222. const columns = [
  223. {
  224. title: t('模型名称'),
  225. dataIndex: 'name',
  226. key: 'name',
  227. },
  228. {
  229. title: t('模型固定价格'),
  230. dataIndex: 'price',
  231. key: 'price',
  232. render: (text, record) => (
  233. <Input
  234. value={text}
  235. placeholder={t('按量计费')}
  236. onChange={(value) => updateModel(record.name, 'price', value)}
  237. />
  238. ),
  239. },
  240. {
  241. title: t('模型倍率'),
  242. dataIndex: 'ratio',
  243. key: 'ratio',
  244. render: (text, record) => (
  245. <Input
  246. value={text}
  247. placeholder={record.price !== '' ? t('模型倍率') : t('输入模型倍率')}
  248. disabled={record.price !== ''}
  249. onChange={(value) => updateModel(record.name, 'ratio', value)}
  250. />
  251. ),
  252. },
  253. {
  254. title: t('补全倍率'),
  255. dataIndex: 'completionRatio',
  256. key: 'completionRatio',
  257. render: (text, record) => (
  258. <Input
  259. value={text}
  260. placeholder={record.price !== '' ? t('补全倍率') : t('输入补全倍率')}
  261. disabled={record.price !== ''}
  262. onChange={(value) =>
  263. updateModel(record.name, 'completionRatio', value)
  264. }
  265. />
  266. ),
  267. },
  268. {
  269. title: t('操作'),
  270. key: 'action',
  271. render: (_, record) => (
  272. <Button
  273. type='primary'
  274. icon={<IconEdit />}
  275. onClick={() => openEditModal(record)}
  276. />
  277. ),
  278. },
  279. ];
  280. const updateModel = (name, field, value) => {
  281. if (value !== '' && isNaN(value)) {
  282. showError(t('请输入数字'));
  283. return;
  284. }
  285. setModels((prev) =>
  286. prev.map((model) =>
  287. model.name === name ? { ...model, [field]: value } : model,
  288. ),
  289. );
  290. };
  291. const addModel = (values) => {
  292. // 检查模型名称是否存在, 如果存在则拒绝添加
  293. if (models.some((model) => model.name === values.name)) {
  294. showError(t('模型名称已存在'));
  295. return;
  296. }
  297. // 从表单读取高级倍率字段
  298. let advancedValues = {};
  299. if (addFormRef.current) {
  300. const formValues = addFormRef.current.getValues();
  301. advancedValues = {
  302. cacheRatio: formValues.cacheRatio != null ? formValues.cacheRatio : '',
  303. cacheCreationRatio: formValues.cacheCreationRatio != null ? formValues.cacheCreationRatio : '',
  304. imageRatio: formValues.imageRatio != null ? formValues.imageRatio : '',
  305. audioRatio: formValues.audioRatio != null ? formValues.audioRatio : '',
  306. audioCompletionRatio: formValues.audioCompletionRatio != null ? formValues.audioCompletionRatio : '',
  307. };
  308. }
  309. setModels((prev) => [
  310. {
  311. name: values.name,
  312. price: values.price || '',
  313. ratio: values.ratio || '',
  314. completionRatio: values.completionRatio || '',
  315. cacheRatio: advancedValues.cacheRatio ?? '',
  316. cacheCreationRatio: advancedValues.cacheCreationRatio ?? '',
  317. imageRatio: advancedValues.imageRatio ?? '',
  318. audioRatio: advancedValues.audioRatio ?? '',
  319. audioCompletionRatio: advancedValues.audioCompletionRatio ?? '',
  320. },
  321. ...prev,
  322. ]);
  323. setVisible(false);
  324. showSuccess(t('添加成功'));
  325. };
  326. // 批量填充功能
  327. const handleBatchFill = () => {
  328. if (selectedRowKeys.length === 0) {
  329. showError(t('请先选择需要批量设置的模型'));
  330. return;
  331. }
  332. if (batchFillType === 'bothRatio') {
  333. if (batchRatioValue === '' || batchCompletionRatioValue === '') {
  334. showError(t('请输入模型倍率和补全倍率'));
  335. return;
  336. }
  337. if (isNaN(batchRatioValue) || isNaN(batchCompletionRatioValue)) {
  338. showError(t('请输入有效的数字'));
  339. return;
  340. }
  341. } else {
  342. if (batchFillValue === '') {
  343. showError(t('请输入填充值'));
  344. return;
  345. }
  346. if (isNaN(batchFillValue)) {
  347. showError(t('请输入有效的数字'));
  348. return;
  349. }
  350. }
  351. // 根据选择的类型批量更新模型
  352. setModels((prev) =>
  353. prev.map((model) => {
  354. if (selectedRowKeys.includes(model.name)) {
  355. if (batchFillType === 'price') {
  356. return {
  357. ...model,
  358. price: batchFillValue,
  359. ratio: '',
  360. completionRatio: '',
  361. };
  362. } else if (batchFillType === 'ratio') {
  363. return {
  364. ...model,
  365. price: '',
  366. ratio: batchFillValue,
  367. };
  368. } else if (batchFillType === 'completionRatio') {
  369. return {
  370. ...model,
  371. price: '',
  372. completionRatio: batchFillValue,
  373. };
  374. } else if (batchFillType === 'bothRatio') {
  375. return {
  376. ...model,
  377. price: '',
  378. ratio: batchRatioValue,
  379. completionRatio: batchCompletionRatioValue,
  380. };
  381. }
  382. }
  383. return model;
  384. }),
  385. );
  386. setBatchVisible(false);
  387. Notification.success({
  388. title: t('批量设置成功'),
  389. content: t('已为 {{count}} 个模型设置{{type}}', {
  390. count: selectedRowKeys.length,
  391. type:
  392. batchFillType === 'price'
  393. ? t('固定价格')
  394. : batchFillType === 'ratio'
  395. ? t('模型倍率')
  396. : batchFillType === 'completionRatio'
  397. ? t('补全倍率')
  398. : t('模型倍率和补全倍率'),
  399. }),
  400. duration: 3,
  401. });
  402. };
  403. const handleBatchTypeChange = (value) => {
  404. setBatchFillType(value);
  405. // 切换类型时清空对应的值
  406. if (value !== 'bothRatio') {
  407. setBatchFillValue('');
  408. } else {
  409. setBatchRatioValue('');
  410. setBatchCompletionRatioValue('');
  411. }
  412. };
  413. const calculateTokenPriceFromRatio = (ratio) => {
  414. return ratio * 2;
  415. };
  416. const openEditModal = (record) => {
  417. const initialPricingMode = record.price !== '' ? 'per-request' : 'per-token';
  418. const initialPricingSubMode = 'ratio';
  419. setPricingMode(initialPricingMode);
  420. setPricingSubMode(initialPricingSubMode);
  421. const modelCopy = { ...record };
  422. // 计算tokenPrice(从ratio反推)
  423. if (record.ratio) {
  424. modelCopy.tokenPrice = calculateTokenPriceFromRatio(
  425. parseFloat(record.ratio),
  426. ).toString();
  427. if (record.completionRatio) {
  428. modelCopy.completionTokenPrice = (
  429. parseFloat(modelCopy.tokenPrice) *
  430. parseFloat(record.completionRatio)
  431. ).toString();
  432. }
  433. }
  434. setEditRecord(modelCopy);
  435. setEditVisible(true);
  436. // 使用 setTimeout 确保表单渲染后再设置值
  437. setTimeout(() => {
  438. if (editFormRef.current) {
  439. const formValues = {};
  440. if (initialPricingMode === 'per-request') {
  441. formValues.priceInput = modelCopy.price;
  442. } else if (initialPricingMode === 'per-token') {
  443. formValues.ratioInput = modelCopy.ratio;
  444. formValues.completionRatioInput = modelCopy.completionRatio;
  445. formValues.modelTokenPrice = modelCopy.tokenPrice;
  446. formValues.completionTokenPrice = modelCopy.completionTokenPrice;
  447. }
  448. formValues.cacheRatio = modelCopy.cacheRatio;
  449. formValues.cacheCreationRatio = modelCopy.cacheCreationRatio;
  450. formValues.imageRatio = modelCopy.imageRatio;
  451. formValues.audioRatio = modelCopy.audioRatio;
  452. formValues.audioCompletionRatio = modelCopy.audioCompletionRatio;
  453. editFormRef.current.setValues(formValues);
  454. }
  455. }, 0);
  456. };
  457. const rowSelection = {
  458. selectedRowKeys,
  459. onChange: (selectedKeys) => {
  460. setSelectedRowKeys(selectedKeys);
  461. },
  462. };
  463. return (
  464. <>
  465. <Space vertical align='start' style={{ width: '100%' }}>
  466. <Space className='mt-2'>
  467. <Button icon={<IconPlus />} onClick={() => setVisible(true)}>
  468. {t('添加模型')}
  469. </Button>
  470. <Button
  471. icon={<IconBolt />}
  472. type='secondary'
  473. onClick={() => setBatchVisible(true)}
  474. disabled={selectedRowKeys.length === 0}
  475. >
  476. {t('批量设置')} ({selectedRowKeys.length})
  477. </Button>
  478. <Button
  479. type='primary'
  480. icon={<IconSave />}
  481. onClick={SubmitData}
  482. loading={loading}
  483. >
  484. {t('应用更改')}
  485. </Button>
  486. <Input
  487. prefix={<IconSearch />}
  488. placeholder={t('搜索模型名称')}
  489. value={searchText}
  490. onChange={(value) => {
  491. setSearchText(value);
  492. setCurrentPage(1);
  493. }}
  494. style={{ width: 200 }}
  495. />
  496. </Space>
  497. <Text>
  498. {t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}
  499. </Text>
  500. <Table
  501. columns={columns}
  502. dataSource={pagedData}
  503. rowSelection={rowSelection}
  504. rowKey='name'
  505. pagination={{
  506. currentPage: currentPage,
  507. pageSize: pageSize,
  508. total: filteredModels.length,
  509. onPageChange: (page) => setCurrentPage(page),
  510. onPageSizeChange: handlePageSizeChange,
  511. pageSizeOptions: pageSizeOptions,
  512. showTotal: true,
  513. showSizeChanger: true,
  514. }}
  515. empty={
  516. <div style={{ textAlign: 'center', padding: '20px' }}>
  517. {t('没有未设置的模型')}
  518. </div>
  519. }
  520. />
  521. </Space>
  522. {/* 添加模型弹窗 */}
  523. <Modal
  524. title={t('添加模型')}
  525. visible={visible}
  526. onCancel={() => setVisible(false)}
  527. onOk={() => {
  528. currentModel && addModel(currentModel);
  529. }}
  530. >
  531. <Form getFormApi={(api) => (addFormRef.current = api)}>
  532. <Form.Input
  533. field='name'
  534. label={t('模型名称')}
  535. placeholder='strawberry'
  536. required
  537. onChange={(value) =>
  538. setCurrentModel((prev) => ({ ...prev, name: value }))
  539. }
  540. />
  541. <Form.Switch
  542. field='priceMode'
  543. label={
  544. <>
  545. {t('定价模式')}:
  546. {currentModel?.priceMode ? t('固定价格') : t('倍率模式')}
  547. </>
  548. }
  549. onChange={(checked) => {
  550. setCurrentModel((prev) => ({
  551. ...prev,
  552. price: '',
  553. ratio: '',
  554. completionRatio: '',
  555. priceMode: checked,
  556. }));
  557. }}
  558. />
  559. {currentModel?.priceMode ? (
  560. <Form.Input
  561. field='price'
  562. label={t('固定价格(每次)')}
  563. placeholder={t('输入每次价格')}
  564. onChange={(value) =>
  565. setCurrentModel((prev) => ({ ...prev, price: value }))
  566. }
  567. />
  568. ) : (
  569. <>
  570. <Form.Input
  571. field='ratio'
  572. label={t('模型倍率')}
  573. placeholder={t('输入模型倍率')}
  574. onChange={(value) =>
  575. setCurrentModel((prev) => ({ ...prev, ratio: value }))
  576. }
  577. />
  578. <Form.Input
  579. field='completionRatio'
  580. label={t('补全倍率')}
  581. placeholder={t('输入补全价格')}
  582. onChange={(value) =>
  583. setCurrentModel((prev) => ({
  584. ...prev,
  585. completionRatio: value,
  586. }))
  587. }
  588. />
  589. <Form.InputNumber
  590. field='cacheRatio'
  591. label={t('缓存读取倍率')}
  592. min={0}
  593. step={0.01}
  594. placeholder={t('全局默认值(1.0)')}
  595. extraText={t('缓存命中时输入价格的倍率。0.5 = 缓存命中的 token 按输入价格的一半计费,留空使用全局默认值 1.0')}
  596. />
  597. <Form.InputNumber
  598. field='cacheCreationRatio'
  599. label={t('缓存创建倍率(5分钟)')}
  600. min={0}
  601. step={0.01}
  602. placeholder={t('全局默认值(1.25)')}
  603. extraText={t('写入缓存的 token 按此倍率 × 输入价格计费。默认 1.25(5分钟 TTL);超过 1 小时自动按 1.6x 计费,留空使用全局默认值')}
  604. />
  605. <Form.InputNumber
  606. field='imageRatio'
  607. label={t('图片倍率')}
  608. min={0}
  609. step={0.01}
  610. placeholder={t('全局默认值')}
  611. />
  612. <Form.InputNumber
  613. field='audioRatio'
  614. label={t('音频输入倍率')}
  615. min={0}
  616. step={0.01}
  617. placeholder={t('全局默认值')}
  618. />
  619. <Form.InputNumber
  620. field='audioCompletionRatio'
  621. label={t('音频输出倍率')}
  622. min={0}
  623. step={0.01}
  624. placeholder={t('全局默认值')}
  625. />
  626. </>
  627. )}
  628. </Form>
  629. </Modal>
  630. {/* 批量设置弹窗 */}
  631. <Modal
  632. title={t('批量设置模型参数')}
  633. visible={batchVisible}
  634. onCancel={() => setBatchVisible(false)}
  635. onOk={handleBatchFill}
  636. width={500}
  637. >
  638. <Form>
  639. <Form.Section text={t('设置类型')}>
  640. <div style={{ marginBottom: '16px' }}>
  641. <Space>
  642. <Radio
  643. checked={batchFillType === 'price'}
  644. onChange={() => handleBatchTypeChange('price')}
  645. >
  646. {t('固定价格')}
  647. </Radio>
  648. <Radio
  649. checked={batchFillType === 'ratio'}
  650. onChange={() => handleBatchTypeChange('ratio')}
  651. >
  652. {t('模型倍率')}
  653. </Radio>
  654. <Radio
  655. checked={batchFillType === 'completionRatio'}
  656. onChange={() => handleBatchTypeChange('completionRatio')}
  657. >
  658. {t('补全倍率')}
  659. </Radio>
  660. <Radio
  661. checked={batchFillType === 'bothRatio'}
  662. onChange={() => handleBatchTypeChange('bothRatio')}
  663. >
  664. {t('模型倍率和补全倍率同时设置')}
  665. </Radio>
  666. </Space>
  667. </div>
  668. </Form.Section>
  669. {batchFillType === 'bothRatio' ? (
  670. <>
  671. <Form.Input
  672. field='batchRatioValue'
  673. label={t('模型倍率值')}
  674. placeholder={t('请输入模型倍率')}
  675. value={batchRatioValue}
  676. onChange={(value) => setBatchRatioValue(value)}
  677. />
  678. <Form.Input
  679. field='batchCompletionRatioValue'
  680. label={t('补全倍率值')}
  681. placeholder={t('请输入补全倍率')}
  682. value={batchCompletionRatioValue}
  683. onChange={(value) => setBatchCompletionRatioValue(value)}
  684. />
  685. </>
  686. ) : (
  687. <Form.Input
  688. field='batchFillValue'
  689. label={
  690. batchFillType === 'price'
  691. ? t('固定价格值')
  692. : batchFillType === 'ratio'
  693. ? t('模型倍率值')
  694. : t('补全倍率值')
  695. }
  696. placeholder={t('请输入数值')}
  697. value={batchFillValue}
  698. onChange={(value) => setBatchFillValue(value)}
  699. />
  700. )}
  701. <Text type='tertiary'>
  702. {t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text>{' '}
  703. {t(' 个模型设置相同的值')}
  704. </Text>
  705. <div style={{ marginTop: '8px' }}>
  706. <Text type='tertiary'>
  707. {t('当前设置类型: ')}{' '}
  708. <Text strong>
  709. {batchFillType === 'price'
  710. ? t('固定价格')
  711. : batchFillType === 'ratio'
  712. ? t('模型倍率')
  713. : batchFillType === 'completionRatio'
  714. ? t('补全倍率')
  715. : t('模型倍率和补全倍率')}
  716. </Text>
  717. </Text>
  718. </div>
  719. </Form>
  720. </Modal>
  721. {/* 编辑模型弹窗 */}
  722. <Modal
  723. title={t('编辑模型') + ' - ' + (editRecord?.name || '')}
  724. visible={editVisible}
  725. onCancel={() => {
  726. setEditVisible(false);
  727. setEditRecord(null);
  728. setPricingMode('per-token');
  729. setPricingSubMode('ratio');
  730. }}
  731. onOk={() => {
  732. if (editRecord && editFormRef.current) {
  733. const valuesToSave = { ...editRecord };
  734. // 处理按价格设置模式
  735. if (
  736. pricingMode === 'per-token' &&
  737. pricingSubMode === 'token-price' &&
  738. editRecord.tokenPrice
  739. ) {
  740. const tokenPrice = parseFloat(editRecord.tokenPrice);
  741. valuesToSave.ratio = (tokenPrice / 2).toString();
  742. if (editRecord.completionTokenPrice) {
  743. const completionPrice = parseFloat(
  744. editRecord.completionTokenPrice,
  745. );
  746. const modelPrice = parseFloat(editRecord.tokenPrice);
  747. if (modelPrice > 0) {
  748. valuesToSave.completionRatio = (
  749. completionPrice / modelPrice
  750. ).toString();
  751. }
  752. }
  753. }
  754. // 根据定价模式清理字段
  755. if (pricingMode === 'per-token') {
  756. valuesToSave.price = '';
  757. } else {
  758. valuesToSave.ratio = '';
  759. valuesToSave.completionRatio = '';
  760. }
  761. // 从表单读取高级倍率字段
  762. const formValues = editFormRef.current.getValues();
  763. valuesToSave.cacheRatio =
  764. formValues.cacheRatio != null
  765. ? formValues.cacheRatio
  766. : '';
  767. valuesToSave.cacheCreationRatio =
  768. formValues.cacheCreationRatio != null
  769. ? formValues.cacheCreationRatio
  770. : '';
  771. valuesToSave.imageRatio =
  772. formValues.imageRatio != null
  773. ? formValues.imageRatio
  774. : '';
  775. valuesToSave.audioRatio =
  776. formValues.audioRatio != null
  777. ? formValues.audioRatio
  778. : '';
  779. valuesToSave.audioCompletionRatio =
  780. formValues.audioCompletionRatio != null
  781. ? formValues.audioCompletionRatio
  782. : '';
  783. setModels((prev) =>
  784. prev.map((model) => {
  785. if (model.name !== editRecord.name) return model;
  786. return {
  787. ...model,
  788. price: valuesToSave.price,
  789. ratio: valuesToSave.ratio,
  790. completionRatio: valuesToSave.completionRatio,
  791. cacheRatio: valuesToSave.cacheRatio,
  792. cacheCreationRatio: valuesToSave.cacheCreationRatio,
  793. imageRatio: valuesToSave.imageRatio,
  794. audioRatio: valuesToSave.audioRatio,
  795. audioCompletionRatio:
  796. valuesToSave.audioCompletionRatio,
  797. };
  798. }),
  799. );
  800. setEditVisible(false);
  801. setEditRecord(null);
  802. showSuccess(t('更新成功'));
  803. }
  804. }}
  805. >
  806. <Form
  807. key={editRecord?.name || 'empty'}
  808. getFormApi={(api) => (editFormRef.current = api)}
  809. >
  810. <Form.Section text={t('定价模式')}>
  811. <div style={{ marginBottom: '16px' }}>
  812. <RadioGroup
  813. type='button'
  814. value={pricingMode}
  815. onChange={(e) => {
  816. setPricingMode(e.target.value);
  817. }}
  818. >
  819. <Radio value='per-token'>{t('按量计费')}</Radio>
  820. <Radio value='per-request'>{t('按次计费')}</Radio>
  821. </RadioGroup>
  822. </div>
  823. </Form.Section>
  824. {pricingMode === 'per-token' && (
  825. <>
  826. <Form.Section text={t('价格设置方式')}>
  827. <div style={{ marginBottom: '16px' }}>
  828. <RadioGroup
  829. type='button'
  830. value={pricingSubMode}
  831. onChange={(e) => {
  832. setPricingSubMode(e.target.value);
  833. }}
  834. >
  835. <Radio value='ratio'>{t('按倍率设置')}</Radio>
  836. <Radio value='token-price'>
  837. {t('按价格设置')}
  838. </Radio>
  839. </RadioGroup>
  840. </div>
  841. </Form.Section>
  842. {pricingSubMode === 'ratio' && (
  843. <>
  844. <Form.Input
  845. field='ratioInput'
  846. label={t('模型倍率')}
  847. placeholder={t('输入模型倍率')}
  848. onChange={(value) =>
  849. setEditRecord((prev) =>
  850. prev ? { ...prev, ratio: value } : prev,
  851. )
  852. }
  853. initValue={editRecord?.ratio || ''}
  854. />
  855. <Form.Input
  856. field='completionRatioInput'
  857. label={t('补全倍率')}
  858. placeholder={t('输入补全倍率')}
  859. onChange={(value) =>
  860. setEditRecord((prev) =>
  861. prev
  862. ? { ...prev, completionRatio: value }
  863. : prev,
  864. )
  865. }
  866. initValue={editRecord?.completionRatio || ''}
  867. />
  868. </>
  869. )}
  870. {pricingSubMode === 'token-price' && (
  871. <>
  872. <Form.Input
  873. field='modelTokenPrice'
  874. label={t('输入价格')}
  875. suffix={t('$/1M tokens')}
  876. onChange={(value) => {
  877. let updated = {
  878. ...(editRecord || {}),
  879. tokenPrice: value,
  880. ratio: 0,
  881. };
  882. if (!isNaN(value) && value !== '') {
  883. const tokenPrice = parseFloat(value);
  884. updated.ratio = (tokenPrice / 2).toString();
  885. }
  886. setEditRecord(updated);
  887. }}
  888. initValue={editRecord?.tokenPrice || ''}
  889. />
  890. <Form.Input
  891. field='completionTokenPrice'
  892. label={t('输出价格')}
  893. suffix={t('$/1M tokens')}
  894. onChange={(value) => {
  895. let updated = {
  896. ...(editRecord || {}),
  897. completionTokenPrice: value,
  898. completionRatio: 0,
  899. };
  900. if (
  901. !isNaN(value) &&
  902. value !== '' &&
  903. editRecord?.tokenPrice
  904. ) {
  905. const completionPrice = parseFloat(value);
  906. const modelPrice = parseFloat(
  907. editRecord.tokenPrice,
  908. );
  909. if (modelPrice > 0) {
  910. updated.completionRatio = (
  911. completionPrice / modelPrice
  912. ).toString();
  913. }
  914. }
  915. setEditRecord(updated);
  916. }}
  917. initValue={editRecord?.completionTokenPrice || ''}
  918. />
  919. </>
  920. )}
  921. <Form.Section text={t('高级比例(留空使用默认值)')}>
  922. <Form.InputNumber
  923. field='cacheRatio'
  924. label={t('缓存读取倍率')}
  925. min={0}
  926. step={0.01}
  927. placeholder={t('全局默认值(1.0)')}
  928. extraText={t(
  929. '缓存命中时输入价格的倍率。0.5 = 缓存命中的 token 按输入价格的一半计费,留空使用全局默认值 1.0',
  930. )}
  931. />
  932. <Form.InputNumber
  933. field='cacheCreationRatio'
  934. label={t('缓存创建倍率(5分钟)')}
  935. min={0}
  936. step={0.01}
  937. placeholder={t('全局默认值(1.25)')}
  938. extraText={t(
  939. '写入缓存的 token 按此倍率 × 输入价格计费。默认 1.25(5分钟 TTL);超过 1 小时自动按 1.6x 计费,留空使用全局默认值',
  940. )}
  941. />
  942. <Form.InputNumber
  943. field='imageRatio'
  944. label={t('图片倍率')}
  945. min={0}
  946. step={0.01}
  947. placeholder={t('全局默认值')}
  948. />
  949. <Form.InputNumber
  950. field='audioRatio'
  951. label={t('音频输入倍率')}
  952. min={0}
  953. step={0.01}
  954. placeholder={t('全局默认值')}
  955. />
  956. <Form.InputNumber
  957. field='audioCompletionRatio'
  958. label={t('音频输出倍率')}
  959. min={0}
  960. step={0.01}
  961. placeholder={t('全局默认值')}
  962. />
  963. </Form.Section>
  964. </>
  965. )}
  966. {pricingMode === 'per-request' && (
  967. <Form.Input
  968. field='priceInput'
  969. label={t('固定价格(每次)')}
  970. placeholder={t('输入每次价格')}
  971. onChange={(value) =>
  972. setEditRecord((prev) =>
  973. prev ? { ...prev, price: value } : prev,
  974. )
  975. }
  976. initValue={editRecord?.price || ''}
  977. />
  978. )}
  979. </Form>
  980. </Modal>
  981. </>
  982. );
  983. }