From 3e86f523db31fc9b80413faf4fae9b8b55bbc162 Mon Sep 17 00:00:00 2001 From: fengsilin Date: Fri, 8 May 2026 14:58:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(playground):=20temperature/top=5Fp=20?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E4=BA=92=E6=96=A5=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 部分上游模型不允许同时设置 temperature 和 top_p,新增管理员可配置的 模型前缀列表,匹配的模型在 Playground 中自动互斥切换两个参数。 - 后端新增 PlaygroundSetting 配置 + GET /api/playground/config 公开端点 - 运营设置页面新增 Playground 互斥模型前缀编辑 textarea - Playground 自动检测模型名匹配,启用一个参数自动禁用另一个 - 默认包含 deepseek-v4-flash/pro、deepseek-reasoner、o1-/o3-/o4-、gpt-5 Co-Authored-By: Claude Opus 4.7 --- controller/playground.go | 10 ++ router/api-router.go | 1 + .../operation_setting/playground_setting.go | 22 ++++ .../playground/OptimizedComponents.js | 3 +- .../playground/ParameterControl.jsx | 8 ++ .../components/playground/SettingsPanel.jsx | 2 + .../components/settings/OperationSetting.jsx | 8 ++ .../hooks/playground/usePlaygroundState.js | 83 +++++++++++-- web/src/pages/Playground/index.jsx | 3 + .../Setting/Operation/SettingsPlayground.jsx | 110 ++++++++++++++++++ 10 files changed, 241 insertions(+), 9 deletions(-) create mode 100644 setting/operation_setting/playground_setting.go create mode 100644 web/src/pages/Setting/Operation/SettingsPlayground.jsx 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 ( + <> + +
(refForm.current = formAPI)} + style={{ marginBottom: 15 }} + > + + + {t( + '配置 temperature 和 top_p 互斥的模型列表。每行一个模型前缀,匹配的模型在 Playground 中只能启用 temperature 或 top_p 其中一个。', + )} + + + + + + + + + + +
+
+ + ); +}