Files
new-api-hunter/web/src/pages/Setting/Model/SettingGlobalModel.jsx
Seefs 71460cba15 feat: /v1/chat/completion -> /v1/response (#2629)
* feat: /v1/chat/completion -> /v1/response
2026-01-11 21:38:07 +08:00

393 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState, useRef } from 'react';
import { Button, Col, Form, Row, Spin, Banner, Tag } from '@douyinfe/semi-ui';
import {
compareObjects,
API,
showError,
showSuccess,
showWarning,
verifyJSON,
} from '../../../helpers';
import { useTranslation } from 'react-i18next';
const thinkingExample = JSON.stringify(
['moonshotai/kimi-k2-thinking', 'kimi-k2-thinking'],
null,
2,
);
const chatCompletionsToResponsesPolicyExample = JSON.stringify(
{
enabled: true,
all_channels: false,
channel_ids: [1, 2],
model_patterns: ['^gpt-4o.*$', '^gpt-5.*$'],
},
null,
2,
);
const chatCompletionsToResponsesPolicyAllChannelsExample = JSON.stringify(
{
enabled: true,
all_channels: true,
model_patterns: ['^gpt-4o.*$', '^gpt-5.*$'],
},
null,
2,
);
const defaultGlobalSettingInputs = {
'global.pass_through_request_enabled': false,
'global.thinking_model_blacklist': '[]',
'global.chat_completions_to_responses_policy': '{}',
'general_setting.ping_interval_enabled': false,
'general_setting.ping_interval_seconds': 60,
};
export default function SettingGlobalModel(props) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [inputs, setInputs] = useState(defaultGlobalSettingInputs);
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(defaultGlobalSettingInputs);
const normalizeValueBeforeSave = (key, value) => {
if (key === 'global.thinking_model_blacklist') {
const text = typeof value === 'string' ? value.trim() : '';
return text === '' ? '[]' : value;
}
if (key === 'global.chat_completions_to_responses_policy') {
const text = typeof value === 'string' ? value.trim() : '';
return text === '' ? '{}' : value;
}
return value;
};
function onSubmit() {
const updateArray = compareObjects(inputs, inputsRow);
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
const requestQueue = updateArray.map((item) => {
const normalizedValue = normalizeValueBeforeSave(
item.key,
inputs[item.key],
);
let value = String(normalizedValue);
return API.put('/api/option/', {
key: item.key,
value,
});
});
setLoading(true);
Promise.all(requestQueue)
.then((res) => {
if (requestQueue.length === 1) {
if (res.includes(undefined)) return;
} else if (requestQueue.length > 1) {
if (res.includes(undefined))
return showError(t('部分保存失败,请重试'));
}
showSuccess(t('保存成功'));
props.refresh();
})
.catch(() => {
showError(t('保存失败,请重试'));
})
.finally(() => {
setLoading(false);
});
}
useEffect(() => {
const currentInputs = {};
for (const key of Object.keys(defaultGlobalSettingInputs)) {
if (props.options[key] !== undefined) {
let value = props.options[key];
if (key === 'global.thinking_model_blacklist') {
try {
value =
value && String(value).trim() !== ''
? JSON.stringify(JSON.parse(value), null, 2)
: defaultGlobalSettingInputs[key];
} catch (error) {
value = defaultGlobalSettingInputs[key];
}
}
if (key === 'global.chat_completions_to_responses_policy') {
try {
value =
value && String(value).trim() !== ''
? JSON.stringify(JSON.parse(value), null, 2)
: defaultGlobalSettingInputs[key];
} catch (error) {
value = defaultGlobalSettingInputs[key];
}
}
currentInputs[key] = value;
} else {
currentInputs[key] = defaultGlobalSettingInputs[key];
}
}
setInputs(currentInputs);
setInputsRow(structuredClone(currentInputs));
if (refForm.current) {
refForm.current.setValues(currentInputs);
}
}, [props.options]);
return (
<>
<Spin spinning={loading}>
<Form
values={inputs}
getFormApi={(formAPI) => (refForm.current = formAPI)}
style={{ marginBottom: 15 }}
>
<Form.Section text={t('全局设置')}>
<Row>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
label={t('启用请求透传')}
field={'global.pass_through_request_enabled'}
onChange={(value) =>
setInputs({
...inputs,
'global.pass_through_request_enabled': value,
})
}
extraText={
t('开启后,所有请求将直接透传给上游,不会进行任何处理(重定向和渠道适配也将失效),请谨慎开启')
}
/>
</Col>
</Row>
<Row>
<Col span={24}>
<Form.TextArea
label={t('禁用思考处理的模型列表')}
field={'global.thinking_model_blacklist'}
placeholder={
t('例如:') +
'\n' +
thinkingExample
}
rows={4}
rules={[
{
validator: (rule, value) => {
if (!value || value.trim() === '') return true;
return verifyJSON(value);
},
message: t('不是合法的 JSON 字符串'),
},
]}
extraText={t(
'列出的模型将不会自动添加或移除-thinking/-nothinking 后缀',
)}
onChange={(value) =>
setInputs({
...inputs,
'global.thinking_model_blacklist': value,
})
}
/>
</Col>
</Row>
<Form.Section text={t('ChatCompletions→Responses 兼容配置')}>
<Row style={{ marginTop: 10 }}>
<Col span={24}>
<Banner
type='warning'
title={
<span>
{t('ChatCompletions→Responses 兼容配置')}{' '}
<Tag color='red' size='small'>
Alpha
</Tag>
</span>
}
description={t(
'提示:该功能为测试版,未来配置结构与功能行为可能发生变更,请勿在生产环境使用。',
)}
/>
</Col>
</Row>
<Row style={{ marginTop: 10 }}>
<Col span={24}>
<div
style={{
display: 'flex',
gap: 8,
flexWrap: 'wrap',
alignItems: 'center',
}}
>
<Button
type='secondary'
size='small'
onClick={() =>
setInputs({
...inputs,
'global.chat_completions_to_responses_policy':
chatCompletionsToResponsesPolicyExample,
})
}
>
{t('填充模板(指定渠道)')}
</Button>
<Button
type='secondary'
size='small'
onClick={() =>
setInputs({
...inputs,
'global.chat_completions_to_responses_policy':
chatCompletionsToResponsesPolicyAllChannelsExample,
})
}
>
{t('填充模板(全渠道)')}
</Button>
<Button
type='secondary'
size='small'
onClick={() => {
const raw =
inputs['global.chat_completions_to_responses_policy'];
if (!raw || String(raw).trim() === '') return;
try {
const formatted = JSON.stringify(
JSON.parse(raw),
null,
2,
);
setInputs({
...inputs,
'global.chat_completions_to_responses_policy':
formatted,
});
} catch (error) {
showError(t('不是合法的 JSON 字符串'));
}
}}
>
{t('格式化 JSON')}
</Button>
</div>
</Col>
</Row>
<Row style={{ marginTop: 10 }}>
<Col span={24}>
<Form.TextArea
label={t('配置 JSON')}
field={'global.chat_completions_to_responses_policy'}
placeholder={
t('例如(指定渠道):') +
'\n' +
chatCompletionsToResponsesPolicyExample +
'\n\n' +
t('例如(全渠道):') +
'\n' +
chatCompletionsToResponsesPolicyAllChannelsExample
}
rows={8}
rules={[
{
validator: (rule, value) => {
if (!value || value.trim() === '') return true;
return verifyJSON(value);
},
message: t('不是合法的 JSON 字符串'),
},
]}
extraText={t(
'当客户端调用 /v1/chat/completions 且 model 命中 model_patterns 时,自动改走上游 /v1/responses并把响应转换回 /v1/chat/completions 结构',
)}
onChange={(value) =>
setInputs({
...inputs,
'global.chat_completions_to_responses_policy': value,
})
}
/>
</Col>
</Row>
</Form.Section>
<Form.Section text={t('连接保活设置')}>
<Row style={{ marginTop: 10 }}>
<Col span={24}>
<Banner
type='warning'
description={t('警告启用保活后如果已经写入保活数据后渠道出错系统无法重试如果必须开启推荐设置尽可能大的Ping间隔')}
/>
</Col>
</Row>
<Row>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.Switch
label={t('启用Ping间隔')}
field={'general_setting.ping_interval_enabled'}
onChange={(value) =>
setInputs({
...inputs,
'general_setting.ping_interval_enabled': value,
})
}
extraText={t('开启后将定期发送ping数据保持连接活跃')}
/>
</Col>
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
<Form.InputNumber
label={t('Ping间隔')}
field={'general_setting.ping_interval_seconds'}
onChange={(value) =>
setInputs({
...inputs,
'general_setting.ping_interval_seconds': value,
})
}
min={1}
disabled={!inputs['general_setting.ping_interval_enabled']}
/>
</Col>
</Row>
</Form.Section>
<Row>
<Button size='default' onClick={onSubmit}>
{t('保存')}
</Button>
</Row>
</Form.Section>
</Form>
</Spin>
</>
);
}