/*
Copyright (c) 2025 Tethys Plex
This file is part of Veloera.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
import React, { useEffect, useState, useRef } from 'react';
import {
Button,
Col,
Form,
Row,
Spin,
Modal,
Select,
Space,
Typography,
} from '@douyinfe/semi-ui';
import {
API,
showError,
showSuccess,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
const { Text } = Typography;
export default function ModelCommonRatioSettings(props) {
const [loading, setLoading] = useState(false);
const [presetModalVisible, setPresetModalVisible] = useState(false);
const [resetModalVisible, setResetModalVisible] = useState(false);
const [presetLoading, setPresetLoading] = useState(false);
const [resetLoading, setResetLoading] = useState(false);
const [selectedPresetSource, setSelectedPresetSource] = useState('flexible');
const [selectedResetSource, setSelectedResetSource] = useState('flexible');
const [inputs, setInputs] = useState({
fallback_pricing_enabled: false,
fallback_single_price: '',
fallback_input_ratio: '',
fallback_completion_ratio: '',
redirect_billing_enabled: false,
});
const { t } = useTranslation();
const refForm = useRef();
function handleFieldChange(fieldName) {
return (value) => {
if (fieldName === 'fallback_single_price') {
setInputs((inputs) => ({
...inputs,
fallback_single_price: typeof value === 'number' ? String(value) : value,
fallback_input_ratio: value ? '' : inputs.fallback_input_ratio,
fallback_completion_ratio: value ? '' : inputs.fallback_completion_ratio,
}));
} else if (fieldName === 'fallback_input_ratio' || fieldName === 'fallback_completion_ratio') {
setInputs((inputs) => ({
...inputs,
[fieldName]: typeof value === 'number' ? String(value) : value,
fallback_single_price: value ? '' : inputs.fallback_single_price,
}));
} else if (fieldName === 'redirect_billing_enabled') {
const newInputs = {
...inputs,
[fieldName]: value
};
setInputs(newInputs);
// Auto-save redirect billing setting
saveRedirectBillingSetting(value);
} else if (fieldName === 'fallback_pricing_enabled') {
const newInputs = {
...inputs,
[fieldName]: value
};
setInputs(newInputs);
// Auto-save fallback pricing enabled setting
saveFallbackPricingEnabled(value);
} else {
setInputs((inputs) => ({
...inputs,
[fieldName]: typeof value === 'number' ? String(value) : value
}));
}
};
}
const presetSources = [
{ value: 'flexible', label: '通用(默认,推荐)' },
{ value: 'openrouter', label: 'OpenRouter' },
{ value: 'mixed', label: '混合' },
{ value: 'legacy', label: '传统(不推荐)' },
];
const resetPresetRatios = async () => {
setResetLoading(true);
try {
let promptUrl, completionUrl;
if (selectedResetSource === 'legacy') {
completionUrl = 'https://public-assets.veloera.org/defaults/model-ratios/completion.json';
promptUrl = null;
} else {
promptUrl = `https://public-assets.veloera.org/defaults/model-ratios/${selectedResetSource}/prompt.json`;
completionUrl = `https://public-assets.veloera.org/defaults/model-ratios/${selectedResetSource}/completion.json`;
}
const requests = [];
if (promptUrl) {
requests.push(fetch(promptUrl).then(res => res.json()).catch(() => ({})));
}
requests.push(fetch(completionUrl).then(res => res.json()).catch(() => ({})));
const [promptRatios, completionRatios] = promptUrl ? await Promise.all(requests) : [null, await requests[0]];
const currentModelRatio = JSON.parse(props.options.ModelRatio || '{}');
const currentCompletionRatio = JSON.parse(props.options.CompletionRatio || '{}');
const filteredModelRatio = { ...currentModelRatio };
const filteredCompletionRatio = { ...currentCompletionRatio };
if (promptRatios) {
Object.keys(promptRatios).forEach(model => {
delete filteredModelRatio[model];
});
}
if (completionRatios) {
Object.keys(completionRatios).forEach(model => {
delete filteredCompletionRatio[model];
});
}
const requestQueue = [
API.put('/api/option/', {
key: 'ModelRatio',
value: JSON.stringify(filteredModelRatio, null, 2)
}),
API.put('/api/option/', {
key: 'CompletionRatio',
value: JSON.stringify(filteredCompletionRatio, null, 2)
})
];
const results = await Promise.all(requestQueue);
if (results.includes(undefined)) {
return showError(t('重置预设倍率失败,请重试'));
}
for (const res of results) {
if (!res.data.success) {
return showError(res.data.message);
}
}
showSuccess(t('预设倍率重置成功'));
setResetModalVisible(false);
props.refresh();
} catch (error) {
console.error('重置预设倍率失败:', error);
showError(t('重置预设倍率失败,请重试'));
} finally {
setResetLoading(false);
}
};
const fetchPresetRatios = async () => {
setPresetLoading(true);
try {
let promptUrl, completionUrl;
if (selectedPresetSource === 'legacy') {
completionUrl = 'https://public-assets.veloera.org/defaults/model-ratios/completion.json';
promptUrl = null;
} else {
promptUrl = `https://public-assets.veloera.org/defaults/model-ratios/${selectedPresetSource}/prompt.json`;
completionUrl = `https://public-assets.veloera.org/defaults/model-ratios/${selectedPresetSource}/completion.json`;
}
const requests = [];
if (promptUrl) {
requests.push(fetch(promptUrl).then(res => res.json()).catch(() => ({})));
}
requests.push(fetch(completionUrl).then(res => res.json()).catch(() => ({})));
const [promptRatios, completionRatios] = promptUrl ? await Promise.all(requests) : [null, await requests[0]];
const currentModelRatio = JSON.parse(props.options.ModelRatio || '{}');
const currentCompletionRatio = JSON.parse(props.options.CompletionRatio || '{}');
const mergedModelRatio = { ...currentModelRatio };
const mergedCompletionRatio = { ...currentCompletionRatio };
if (promptRatios) {
Object.keys(promptRatios).forEach(model => {
mergedModelRatio[model] = promptRatios[model];
});
}
if (completionRatios) {
Object.keys(completionRatios).forEach(model => {
mergedCompletionRatio[model] = completionRatios[model];
});
}
const requestQueue = [
API.put('/api/option/', {
key: 'ModelRatio',
value: JSON.stringify(mergedModelRatio, null, 2)
}),
API.put('/api/option/', {
key: 'CompletionRatio',
value: JSON.stringify(mergedCompletionRatio, null, 2)
})
];
const results = await Promise.all(requestQueue);
if (results.includes(undefined)) {
return showError(t('获取预设倍率失败,请重试'));
}
for (const res of results) {
if (!res.data.success) {
return showError(res.data.message);
}
}
showSuccess(t('预设倍率获取成功'));
setPresetModalVisible(false);
props.refresh();
await saveFallbackPricing();
} catch (error) {
console.error('获取预设倍率失败:', error);
showError(t('获取预设倍率失败,请重试'));
} finally {
setPresetLoading(false);
}
};
const handleFallbackPriceChange = (type, value) => {
const newInputs = {...inputs};
if (type === 'single') {
newInputs.fallback_single_price = value;
if (value) {
newInputs.fallback_input_ratio = '';
newInputs.fallback_completion_ratio = '';
}
} else {
newInputs[`fallback_${type}_ratio`] = value;
if (value) {
newInputs.fallback_single_price = '';
}
}
setInputs(newInputs);
};
const validateFallbackPricing = () => {
const { fallback_single_price, fallback_input_ratio, fallback_completion_ratio } = inputs;
if (fallback_single_price) {
return !fallback_input_ratio && !fallback_completion_ratio;
}
if (fallback_input_ratio || fallback_completion_ratio) {
return fallback_input_ratio && fallback_completion_ratio && !fallback_single_price;
}
return true;
};
const saveFallbackPricing = async () => {
if (!validateFallbackPricing()) {
showError(t('请检查兜底倍率配置:使用单次价格时不能设置倍率,使用倍率时需要同时设置输入和补全倍率'));
return;
}
const fallbackOptions = [
{ key: 'fallback_pricing_enabled', value: String(inputs.fallback_pricing_enabled) },
{ key: 'fallback_single_price', value: String(inputs.fallback_single_price || '') },
{ key: 'fallback_input_ratio', value: String(inputs.fallback_input_ratio || '') },
{ key: 'fallback_completion_ratio', value: String(inputs.fallback_completion_ratio || '') }
];
try {
setLoading(true);
const requestQueue = fallbackOptions.map((option) =>
API.put('/api/option/', option)
);
const res = await Promise.all(requestQueue);
if (res.includes(undefined)) {
return showError(t('保存失败,请重试'));
}
for (let i = 0; i < res.length; i++) {
if (!res[i].data.success) {
return showError(res[i].data.message);
}
}
showSuccess(t('兜底倍率保存成功'));
props.refresh();
} catch (error) {
console.error('Unexpected error:', error);
showError(t('保存失败,请重试'));
} finally {
setLoading(false);
}
};
const saveRedirectBillingSetting = async (enabled) => {
try {
setLoading(true);
const res = await API.put('/api/option/', {
key: 'redirect_billing_enabled',
value: String(enabled)
});
if (!res.data.success) {
showError(res.data.message);
return;
}
showSuccess(t('重定向计费设置保存成功'));
props.refresh();
} catch (error) {
console.error('保存重定向计费设置失败:', error);
showError(t('保存失败,请重试'));
} finally {
setLoading(false);
}
};
const saveFallbackPricingEnabled = async (enabled) => {
try {
setLoading(true);
const res = await API.put('/api/option/', {
key: 'fallback_pricing_enabled',
value: String(enabled)
});
if (!res.data.success) {
showError(res.data.message);
return;
}
showSuccess(t('启用兜底倍率设置保存成功'));
props.refresh();
} catch (error) {
console.error('保存启用兜底倍率设置失败:', error);
showError(t('保存失败,请重试'));
} finally {
setLoading(false);
}
};
useEffect(() => {
const currentInputs = {
fallback_pricing_enabled: false,
fallback_single_price: '',
fallback_input_ratio: '',
fallback_completion_ratio: '',
redirect_billing_enabled: false,
};
for (let key in props.options) {
if (Object.keys(currentInputs).includes(key)) {
if (key === 'fallback_pricing_enabled' || key === 'redirect_billing_enabled') {
currentInputs[key] = props.options[key] === 'true';
} else {
currentInputs[key] = props.options[key] || '';
}
}
}
setInputs(currentInputs);
if (refForm.current) {
refForm.current.setValues(currentInputs);
}
}, [props.options]);
return (
{inputs.fallback_pricing_enabled && (
)}
setPresetModalVisible(false)}
onOk={fetchPresetRatios}
okText={t('保存')}
confirmLoading={presetLoading}
width={500}
>
{t('选择预设源:')}
{t('注意事项:')}
- {t('将覆盖已有模型倍率(如果本地有同名模型则覆盖,否则保留)')}
- {t('请确认网络连接正常')}
- {t('建议在操作前备份当前配置')}
{t('由 Veloera Public Assets 提供支持,')}
{t('条款和条件')}
{t('适用。')}
setResetModalVisible(false)}
onOk={resetPresetRatios}
okText={t('重置')}
confirmLoading={resetLoading}
width={500}
>
{t('选择要重置的预设源:')}
{t('警告:')}
- {t('将从本地配置中移除选定预设源中包含的所有模型')}
- {t('此操作不可撤销,请谨慎操作')}
- {t('建议在操作前备份当前配置')}
{t('由 Veloera Public Assets 提供支持,')}
{t('条款和条件')}
{t('适用。')}
);
}