diff --git a/controller/playground.go b/controller/playground.go
index 501c4e1..9249b7f 100644
--- a/controller/playground.go
+++ b/controller/playground.go
@@ -4,9 +4,11 @@ import (
"errors"
"fmt"
+ "github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common"
+ "github.com/QuantumNous/new-api/setting/operation_setting"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
@@ -54,3 +56,11 @@ func Playground(c *gin.Context) {
Relay(c, types.RelayFormatOpenAI)
}
+
+// GetPlaygroundConfig 获取 Playground 公开配置(无需认证)
+func GetPlaygroundConfig(c *gin.Context) {
+ setting := operation_setting.GetPlaygroundSetting()
+ common.ApiSuccess(c, gin.H{
+ "mutual_exclusive_params": setting.MutualExclusiveParams,
+ })
+}
diff --git a/router/api-router.go b/router/api-router.go
index 40ba0f9..43b618c 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -48,6 +48,7 @@ func SetApiRouter(router *gin.Engine) {
// Standard OAuth providers (GitHub, Discord, OIDC, LinuxDO) - unified route
apiRouter.GET("/oauth/:provider", middleware.CriticalRateLimit(), controller.HandleOAuth)
apiRouter.GET("/ratio_config", middleware.CriticalRateLimit(), controller.GetRatioConfig)
+ apiRouter.GET("/playground/config", controller.GetPlaygroundConfig)
apiRouter.POST("/stripe/webhook", controller.StripeWebhook)
apiRouter.POST("/creem/webhook", controller.CreemWebhook)
diff --git a/setting/operation_setting/playground_setting.go b/setting/operation_setting/playground_setting.go
new file mode 100644
index 0000000..252ae89
--- /dev/null
+++ b/setting/operation_setting/playground_setting.go
@@ -0,0 +1,22 @@
+package operation_setting
+
+import "github.com/QuantumNous/new-api/setting/config"
+
+// PlaygroundSetting Playground 配置
+type PlaygroundSetting struct {
+ MutualExclusiveParams string `json:"mutual_exclusive_params"` // temperature/top_p 互斥的模型前缀列表,换行分隔
+}
+
+// 默认配置
+var playgroundSetting = PlaygroundSetting{
+ MutualExclusiveParams: "deepseek-v4-flash\ndeepseek-v4-pro\ndeepseek-reasoner\no1-\no3-\no4-\ngpt-5",
+}
+
+func init() {
+ config.GlobalConfig.Register("playground_setting", &playgroundSetting)
+}
+
+// GetPlaygroundSetting 获取 Playground 配置
+func GetPlaygroundSetting() *PlaygroundSetting {
+ return &playgroundSetting
+}
diff --git a/web/src/components/playground/OptimizedComponents.js b/web/src/components/playground/OptimizedComponents.js
index 1fe4268..37ff65b 100644
--- a/web/src/components/playground/OptimizedComponents.js
+++ b/web/src/components/playground/OptimizedComponents.js
@@ -75,7 +75,8 @@ export const OptimizedSettingsPanel = React.memo(
JSON.stringify(prevProps.previewPayload) ===
JSON.stringify(nextProps.previewPayload) &&
JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages) &&
- JSON.stringify(prevProps.channels) === JSON.stringify(nextProps.channels)
+ JSON.stringify(prevProps.channels) === JSON.stringify(nextProps.channels) &&
+ prevProps.mutualExclusive === nextProps.mutualExclusive
);
},
);
diff --git a/web/src/components/playground/ParameterControl.jsx b/web/src/components/playground/ParameterControl.jsx
index e06c397..9da8ca2 100644
--- a/web/src/components/playground/ParameterControl.jsx
+++ b/web/src/components/playground/ParameterControl.jsx
@@ -37,12 +37,20 @@ const ParameterControl = ({
onInputChange,
onParameterToggle,
disabled = false,
+ mutualExclusive = false,
}) => {
const { t } = useTranslation();
return (
<>
{/* Temperature */}
+ {mutualExclusive && (
+
+
+ {t('此模型的 Temperature 和 Top P 互斥,只能启用一个')}
+
+
+ )}
diff --git a/web/src/components/playground/SettingsPanel.jsx b/web/src/components/playground/SettingsPanel.jsx
index df4a099..7b6260d 100644
--- a/web/src/components/playground/SettingsPanel.jsx
+++ b/web/src/components/playground/SettingsPanel.jsx
@@ -36,6 +36,7 @@ const SettingsPanel = ({
showDebugPanel,
customRequestMode,
customRequestBody,
+ mutualExclusive = false,
onInputChange,
onParameterToggle,
onCloseSettings,
@@ -231,6 +232,7 @@ const SettingsPanel = ({
onInputChange={onInputChange}
onParameterToggle={onParameterToggle}
disabled={customRequestMode}
+ mutualExclusive={mutualExclusive}
/>
diff --git a/web/src/components/settings/OperationSetting.jsx b/web/src/components/settings/OperationSetting.jsx
index aa94842..ef0e427 100644
--- a/web/src/components/settings/OperationSetting.jsx
+++ b/web/src/components/settings/OperationSetting.jsx
@@ -29,6 +29,7 @@ import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit';
import SettingsCheckin from '../../pages/Setting/Operation/SettingsCheckin';
import SettingsRegionSync from '../../pages/Setting/Operation/SettingsRegionSync';
+import SettingsPlayground from '../../pages/Setting/Operation/SettingsPlayground';
import { API, showError, toBoolean } from '../../helpers';
const OperationSetting = () => {
@@ -98,6 +99,9 @@ const OperationSetting = () => {
/* 令牌设置 */
'token_setting.max_user_tokens': 1000,
+
+ /* Playground 设置 */
+ 'playground_setting.mutual_exclusive_params': '',
});
let [loading, setLoading] = useState(false);
@@ -181,6 +185,10 @@ const OperationSetting = () => {
+ {/* Playground 参数互斥设置 */}
+
+
+
>
);
diff --git a/web/src/hooks/playground/usePlaygroundState.js b/web/src/hooks/playground/usePlaygroundState.js
index b549bcd..6855d68 100644
--- a/web/src/hooks/playground/usePlaygroundState.js
+++ b/web/src/hooks/playground/usePlaygroundState.js
@@ -17,7 +17,7 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import { useState, useCallback, useRef, useEffect } from 'react';
+import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
DEFAULT_MESSAGES,
@@ -32,7 +32,7 @@ import {
loadMessages,
saveMessages,
} from '../../components/playground/configStorage';
-import { processIncompleteThinkTags } from '../../helpers';
+import { processIncompleteThinkTags, API } from '../../helpers';
export const usePlaygroundState = () => {
const { t } = useTranslation();
@@ -120,17 +120,70 @@ export const usePlaygroundState = () => {
const saveConfigTimeoutRef = useRef(null);
const saveMessagesTimeoutRef = useRef(null);
+ // 互斥模型前缀配置
+ const [mutualExclusivePrefixes, setMutualExclusivePrefixes] = useState([]);
+
+ // 加载互斥配置
+ useEffect(() => {
+ const fetchConfig = async () => {
+ try {
+ const res = await API.get('/api/playground/config');
+ const { success, data } = res.data;
+ if (success && data?.mutual_exclusive_params) {
+ const prefixes = data.mutual_exclusive_params
+ .split('\n')
+ .map((s) => s.trim())
+ .filter(Boolean);
+ setMutualExclusivePrefixes(prefixes);
+ }
+ } catch (e) {
+ // 静默失败,不影响 Playground 使用
+ }
+ };
+ fetchConfig();
+ }, []);
+
+ // 判断当前模型是否需要互斥
+ const isMutualExclusiveModel = useCallback(
+ (modelName) => {
+ if (!modelName || mutualExclusivePrefixes.length === 0) return false;
+ return mutualExclusivePrefixes.some((prefix) =>
+ modelName.toLowerCase().startsWith(prefix.toLowerCase()),
+ );
+ },
+ [mutualExclusivePrefixes],
+ );
+
+ const isMutualExclusive = useMemo(
+ () => isMutualExclusiveModel(inputs.model),
+ [inputs.model, isMutualExclusiveModel],
+ );
+
// 配置更新函数
const handleInputChange = useCallback((name, value) => {
setInputs((prev) => ({ ...prev, [name]: value }));
}, []);
- const handleParameterToggle = useCallback((paramName) => {
- setParameterEnabled((prev) => ({
- ...prev,
- [paramName]: !prev[paramName],
- }));
- }, []);
+ const handleParameterToggle = useCallback(
+ (paramName) => {
+ setParameterEnabled((prev) => {
+ const newVal = !prev[paramName];
+ const updated = { ...prev, [paramName]: newVal };
+
+ // temperature/top_p 互斥逻辑
+ if (isMutualExclusiveModel(inputs.model)) {
+ if (paramName === 'temperature' && newVal) {
+ updated.top_p = false;
+ } else if (paramName === 'top_p' && newVal) {
+ updated.temperature = false;
+ }
+ }
+
+ return updated;
+ });
+ },
+ [inputs.model, isMutualExclusiveModel],
+ );
// 消息保存函数 - 改为立即保存,可以接受参数
const saveMessagesImmediately = useCallback(
@@ -218,6 +271,18 @@ export const usePlaygroundState = () => {
};
}, []);
+ // 模型切换时,若互斥模型且两个参数都启用,自动禁用 top_p
+ useEffect(() => {
+ if (isMutualExclusiveModel(inputs.model)) {
+ setParameterEnabled((prev) => {
+ if (prev.temperature && prev.top_p) {
+ return { ...prev, top_p: false };
+ }
+ return prev;
+ });
+ }
+ }, [inputs.model, isMutualExclusiveModel]);
+
// 页面首次加载时,若最后一条消息仍处于 LOADING/INCOMPLETE 状态,自动修复
useEffect(() => {
if (!Array.isArray(message) || message.length === 0) return;
@@ -301,6 +366,8 @@ export const usePlaygroundState = () => {
// 处理函数
handleInputChange,
handleParameterToggle,
+ isMutualExclusiveModel,
+ isMutualExclusive,
debouncedSaveConfig,
saveMessagesImmediately,
handleConfigImport,
diff --git a/web/src/pages/Playground/index.jsx b/web/src/pages/Playground/index.jsx
index 75f90ef..aeaa928 100644
--- a/web/src/pages/Playground/index.jsx
+++ b/web/src/pages/Playground/index.jsx
@@ -104,6 +104,8 @@ const Playground = () => {
chatRef,
handleInputChange,
handleParameterToggle,
+ isMutualExclusiveModel,
+ isMutualExclusive,
debouncedSaveConfig,
saveMessagesImmediately,
handleConfigImport,
@@ -537,6 +539,7 @@ const Playground = () => {
showDebugPanel={showDebugPanel}
customRequestMode={customRequestMode}
customRequestBody={customRequestBody}
+ mutualExclusive={isMutualExclusive}
onInputChange={handleInputChange}
onParameterToggle={handleParameterToggle}
onCloseSettings={() => setShowSettings(false)}
diff --git a/web/src/pages/Setting/Operation/SettingsPlayground.jsx b/web/src/pages/Setting/Operation/SettingsPlayground.jsx
new file mode 100644
index 0000000..6c89451
--- /dev/null
+++ b/web/src/pages/Setting/Operation/SettingsPlayground.jsx
@@ -0,0 +1,110 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin, Typography } from '@douyinfe/semi-ui';
+import {
+ compareObjects,
+ API,
+ showError,
+ showSuccess,
+ showWarning,
+} from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+export default function SettingsPlayground(props) {
+ const { t } = useTranslation();
+ const [loading, setLoading] = useState(false);
+ const [inputs, setInputs] = useState({
+ 'playground_setting.mutual_exclusive_params': '',
+ });
+ const refForm = useRef();
+ const [inputsRow, setInputsRow] = useState(inputs);
+
+ function handleFieldChange(fieldName) {
+ return (value) => {
+ setInputs((inputs) => ({ ...inputs, [fieldName]: value }));
+ };
+ }
+
+ function onSubmit() {
+ const updateArray = compareObjects(inputs, inputsRow);
+ if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
+ const requestQueue = updateArray.map((item) => {
+ let value = String(inputs[item.key]);
+ 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 (let key in props.options) {
+ if (Object.keys(inputs).includes(key)) {
+ currentInputs[key] = props.options[key];
+ }
+ }
+ setInputs(currentInputs);
+ setInputsRow(structuredClone(currentInputs));
+ refForm.current.setValues(currentInputs);
+ }, [props.options]);
+
+ return (
+ <>
+
+
+
+ {t(
+ '配置 temperature 和 top_p 互斥的模型列表。每行一个模型前缀,匹配的模型在 Playground 中只能启用 temperature 或 top_p 其中一个。',
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}