From b25841e50da6b99158a32303471a354f012a23fb Mon Sep 17 00:00:00 2001 From: CaIon Date: Fri, 25 Jul 2025 18:48:59 +0800 Subject: [PATCH 1/7] feat: add upstream error type and default handling for OpenAI and Claude errors --- types/error.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/types/error.go b/types/error.go index 4ffae2d7..fa07c231 100644 --- a/types/error.go +++ b/types/error.go @@ -28,6 +28,7 @@ const ( ErrorTypeMidjourneyError ErrorType = "midjourney_error" ErrorTypeGeminiError ErrorType = "gemini_error" ErrorTypeRerankError ErrorType = "rerank_error" + ErrorTypeUpstreamError ErrorType = "upstream_error" ) type ErrorCode string @@ -194,6 +195,9 @@ func WithOpenAIError(openAIError OpenAIError, statusCode int) *NewAPIError { if !ok { code = fmt.Sprintf("%v", openAIError.Code) } + if openAIError.Type == "" { + openAIError.Type = "upstream_error" + } return &NewAPIError{ RelayError: openAIError, errorType: ErrorTypeOpenAIError, @@ -204,6 +208,9 @@ func WithOpenAIError(openAIError OpenAIError, statusCode int) *NewAPIError { } func WithClaudeError(claudeError ClaudeError, statusCode int) *NewAPIError { + if claudeError.Type == "" { + claudeError.Type = "upstream_error" + } return &NewAPIError{ RelayError: claudeError, errorType: ErrorTypeClaudeError, From df647e7b422d83b78a62af69513704d1842ba924 Mon Sep 17 00:00:00 2001 From: Raymond <240029725@qq.com> Date: Fri, 25 Jul 2025 22:40:12 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E5=88=A4=E6=96=AD=E5=85=91=E6=8D=A2?= =?UTF-8?q?=E7=A0=81=E5=90=8D=E7=A7=B0=E9=95=BF=E5=BA=A6=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=8C=89=E5=AD=97=E7=AC=A6=E9=95=BF=E5=BA=A6=E8=AE=A1?= =?UTF-8?q?=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/redemption.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controller/redemption.go b/controller/redemption.go index 83ec19ad..1e305e3d 100644 --- a/controller/redemption.go +++ b/controller/redemption.go @@ -6,6 +6,7 @@ import ( "one-api/common" "one-api/model" "strconv" + "unicode/utf8" "github.com/gin-gonic/gin" ) @@ -63,7 +64,7 @@ func AddRedemption(c *gin.Context) { common.ApiError(c, err) return } - if len(redemption.Name) == 0 || len(redemption.Name) > 20 { + if utf8.RuneCountInString(redemption.Name) == 0 || utf8.RuneCountInString(redemption.Name) > 20 { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "兑换码名称长度必须在1-20之间", From 1297addfb1e5d109419e6b226099cda2b6a69305 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 26 Jul 2025 11:39:09 +0800 Subject: [PATCH 3/7] feat: enhance request handling with pass-through options and system prompt support --- dto/channel_settings.go | 8 +- dto/openai_request.go | 9 + i18n/zh-cn.json | 14 + relay/claude_handler.go | 46 +++- relay/gemini_handler.go | 40 ++- relay/image_handler.go | 44 +++- relay/relay-text.go | 26 +- relay/rerank_handler.go | 48 +++- .../channels/modals/EditChannelModal.jsx | 239 ++++++++++++++++-- web/src/i18n/locales/en.json | 13 + 10 files changed, 417 insertions(+), 70 deletions(-) diff --git a/dto/channel_settings.go b/dto/channel_settings.go index 871d6716..47f8bf95 100644 --- a/dto/channel_settings.go +++ b/dto/channel_settings.go @@ -1,7 +1,9 @@ package dto type ChannelSettings struct { - ForceFormat bool `json:"force_format,omitempty"` - ThinkingToContent bool `json:"thinking_to_content,omitempty"` - Proxy string `json:"proxy"` + ForceFormat bool `json:"force_format,omitempty"` + ThinkingToContent bool `json:"thinking_to_content,omitempty"` + Proxy string `json:"proxy"` + PassThroughBodyEnabled bool `json:"pass_through_body_enabled,omitempty"` + SystemPrompt string `json:"system_prompt,omitempty"` } diff --git a/dto/openai_request.go b/dto/openai_request.go index a35ee6b6..b410dffe 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -73,6 +73,15 @@ func (r *GeneralOpenAIRequest) ToMap() map[string]any { return result } +func (r *GeneralOpenAIRequest) GetSystemRoleName() string { + if strings.HasPrefix(r.Model, "o") { + if !strings.HasPrefix(r.Model, "o1-mini") && !strings.HasPrefix(r.Model, "o1-preview") { + return "developer" + } + } + return "system" +} + type ToolCallRequest struct { ID string `json:"id,omitempty"` Type string `json:"type"` diff --git a/i18n/zh-cn.json b/i18n/zh-cn.json index 160fc0a4..0c838c5c 100644 --- a/i18n/zh-cn.json +++ b/i18n/zh-cn.json @@ -585,6 +585,20 @@ "渠道权重": "渠道权重", "渠道额外设置": "渠道额外设置", "此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:": "此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:", + "强制格式化": "强制格式化", + "强制格式化(只适用于OpenAI渠道类型)": "强制格式化(只适用于OpenAI渠道类型)", + "强制将响应格式化为 OpenAI 标准格式": "强制将响应格式化为 OpenAI 标准格式", + "思考内容转换": "思考内容转换", + "将 reasoning_content 转换为 标签拼接到内容中": "将 reasoning_content 转换为 标签拼接到内容中", + "透传请求体": "透传请求体", + "启用请求体透传功能": "启用请求体透传功能", + "代理地址": "代理地址", + "例如: socks5://user:pass@host:port": "例如: socks5://user:pass@host:port", + "用于配置网络代理": "用于配置网络代理", + "用于配置网络代理,支持 socks5 协议": "用于配置网络代理,支持 socks5 协议", + "系统提示词": "系统提示词", + "输入系统提示词,用户的系统提示词将优先于此设置": "输入系统提示词,用户的系统提示词将优先于此设置", + "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置", "参数覆盖": "参数覆盖", "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:": "此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:", "请输入组织org-xxx": "请输入组织org-xxx", diff --git a/relay/claude_handler.go b/relay/claude_handler.go index 5f38960e..2c60a91e 100644 --- a/relay/claude_handler.go +++ b/relay/claude_handler.go @@ -80,7 +80,6 @@ func ClaudeHelper(c *gin.Context) (newAPIError *types.NewAPIError) { return types.NewError(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), types.ErrorCodeInvalidApiType) } adaptor.Init(relayInfo) - var requestBody io.Reader if textRequest.MaxTokens == 0 { textRequest.MaxTokens = uint(model_setting.GetClaudeSettings().GetDefaultMaxTokens(textRequest.Model)) @@ -108,18 +107,41 @@ func ClaudeHelper(c *gin.Context) (newAPIError *types.NewAPIError) { relayInfo.UpstreamModelName = textRequest.Model } - convertedRequest, err := adaptor.ConvertClaudeRequest(c, relayInfo, textRequest) - if err != nil { - return types.NewError(err, types.ErrorCodeConvertRequestFailed) + var requestBody io.Reader + if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled { + body, err := common.GetRequestBody(c) + if err != nil { + return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest) + } + requestBody = bytes.NewBuffer(body) + } else { + convertedRequest, err := adaptor.ConvertClaudeRequest(c, relayInfo, textRequest) + if err != nil { + return types.NewError(err, types.ErrorCodeConvertRequestFailed) + } + jsonData, err := common.Marshal(convertedRequest) + if err != nil { + return types.NewError(err, types.ErrorCodeConvertRequestFailed) + } + + // apply param override + if len(relayInfo.ParamOverride) > 0 { + reqMap := make(map[string]interface{}) + _ = common.Unmarshal(jsonData, &reqMap) + for key, value := range relayInfo.ParamOverride { + reqMap[key] = value + } + jsonData, err = common.Marshal(reqMap) + if err != nil { + return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid) + } + } + + if common.DebugEnabled { + println("requestBody: ", string(jsonData)) + } + requestBody = bytes.NewBuffer(jsonData) } - jsonData, err := common.Marshal(convertedRequest) - if common.DebugEnabled { - println("requestBody: ", string(jsonData)) - } - if err != nil { - return types.NewError(err, types.ErrorCodeConvertRequestFailed) - } - requestBody = bytes.NewBuffer(jsonData) statusCodeMappingStr := c.GetString("status_code_mapping") var httpResp *http.Response diff --git a/relay/gemini_handler.go b/relay/gemini_handler.go index e448b491..0f1aa5bf 100644 --- a/relay/gemini_handler.go +++ b/relay/gemini_handler.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "one-api/common" "one-api/dto" @@ -194,16 +195,39 @@ func GeminiHelper(c *gin.Context) (newAPIError *types.NewAPIError) { } } - requestBody, err := json.Marshal(req) - if err != nil { - return types.NewError(err, types.ErrorCodeConvertRequestFailed) + var requestBody io.Reader + if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled { + body, err := common.GetRequestBody(c) + if err != nil { + return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest) + } + requestBody = bytes.NewReader(body) + } else { + jsonData, err := json.Marshal(req) + if err != nil { + return types.NewError(err, types.ErrorCodeConvertRequestFailed) + } + + // apply param override + if len(relayInfo.ParamOverride) > 0 { + reqMap := make(map[string]interface{}) + _ = common.Unmarshal(jsonData, &reqMap) + for key, value := range relayInfo.ParamOverride { + reqMap[key] = value + } + jsonData, err = common.Marshal(reqMap) + if err != nil { + return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid) + } + } + + if common.DebugEnabled { + println("Gemini request body: %s", string(jsonData)) + } + requestBody = bytes.NewReader(jsonData) } - if common.DebugEnabled { - println("Gemini request body: %s", string(requestBody)) - } - - resp, err := adaptor.DoRequest(c, relayInfo, bytes.NewReader(requestBody)) + resp, err := adaptor.DoRequest(c, relayInfo, requestBody) if err != nil { common.LogError(c, "Do gemini request failed: "+err.Error()) return types.NewError(err, types.ErrorCodeDoRequestFailed) diff --git a/relay/image_handler.go b/relay/image_handler.go index 8e059863..c97eb48e 100644 --- a/relay/image_handler.go +++ b/relay/image_handler.go @@ -16,6 +16,7 @@ import ( "one-api/relay/helper" "one-api/service" "one-api/setting" + "one-api/setting/model_setting" "one-api/types" "strings" @@ -187,22 +188,43 @@ func ImageHelper(c *gin.Context) (newAPIError *types.NewAPIError) { var requestBody io.Reader - convertedRequest, err := adaptor.ConvertImageRequest(c, relayInfo, *imageRequest) - if err != nil { - return types.NewError(err, types.ErrorCodeConvertRequestFailed) - } - if relayInfo.RelayMode == relayconstant.RelayModeImagesEdits { - requestBody = convertedRequest.(io.Reader) + if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled { + body, err := common.GetRequestBody(c) + if err != nil { + return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest) + } + requestBody = bytes.NewBuffer(body) } else { - jsonData, err := json.Marshal(convertedRequest) + convertedRequest, err := adaptor.ConvertImageRequest(c, relayInfo, *imageRequest) if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed) } - requestBody = bytes.NewBuffer(jsonData) - } + if relayInfo.RelayMode == relayconstant.RelayModeImagesEdits { + requestBody = convertedRequest.(io.Reader) + } else { + jsonData, err := json.Marshal(convertedRequest) + if err != nil { + return types.NewError(err, types.ErrorCodeConvertRequestFailed) + } - if common.DebugEnabled { - println(fmt.Sprintf("image request body: %s", requestBody)) + // apply param override + if len(relayInfo.ParamOverride) > 0 { + reqMap := make(map[string]interface{}) + _ = common.Unmarshal(jsonData, &reqMap) + for key, value := range relayInfo.ParamOverride { + reqMap[key] = value + } + jsonData, err = common.Marshal(reqMap) + if err != nil { + return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid) + } + } + + if common.DebugEnabled { + println(fmt.Sprintf("image request body: %s", string(jsonData))) + } + requestBody = bytes.NewBuffer(jsonData) + } } statusCodeMappingStr := c.GetString("status_code_mapping") diff --git a/relay/relay-text.go b/relay/relay-text.go index 60327074..bcb93a65 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -2,7 +2,6 @@ package relay import ( "bytes" - "encoding/json" "errors" "fmt" "io" @@ -171,7 +170,7 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) { adaptor.Init(relayInfo) var requestBody io.Reader - if model_setting.GetGlobalSettings().PassThroughRequestEnabled { + if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled { body, err := common.GetRequestBody(c) if err != nil { return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest) @@ -182,7 +181,28 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) { if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed) } - jsonData, err := json.Marshal(convertedRequest) + + if relayInfo.ChannelSetting.SystemPrompt != "" { + // 如果有系统提示,则将其添加到请求中 + request := convertedRequest.(*dto.GeneralOpenAIRequest) + containSystemPrompt := false + for _, message := range request.Messages { + if message.Role == request.GetSystemRoleName() { + containSystemPrompt = true + break + } + } + if !containSystemPrompt { + // 如果没有系统提示,则添加系统提示 + systemMessage := dto.Message{ + Role: request.GetSystemRoleName(), + Content: relayInfo.ChannelSetting.SystemPrompt, + } + request.Messages = append([]dto.Message{systemMessage}, request.Messages...) + } + } + + jsonData, err := common.Marshal(convertedRequest) if err != nil { return types.NewError(err, types.ErrorCodeConvertRequestFailed) } diff --git a/relay/rerank_handler.go b/relay/rerank_handler.go index a092de4b..0190cf08 100644 --- a/relay/rerank_handler.go +++ b/relay/rerank_handler.go @@ -3,12 +3,14 @@ package relay import ( "bytes" "fmt" + "io" "net/http" "one-api/common" "one-api/dto" relaycommon "one-api/relay/common" "one-api/relay/helper" "one-api/service" + "one-api/setting/model_setting" "one-api/types" "github.com/gin-gonic/gin" @@ -70,18 +72,42 @@ func RerankHelper(c *gin.Context, relayMode int) (newAPIError *types.NewAPIError } adaptor.Init(relayInfo) - convertedRequest, err := adaptor.ConvertRerankRequest(c, relayInfo.RelayMode, *rerankRequest) - if err != nil { - return types.NewError(err, types.ErrorCodeConvertRequestFailed) - } - jsonData, err := common.Marshal(convertedRequest) - if err != nil { - return types.NewError(err, types.ErrorCodeConvertRequestFailed) - } - requestBody := bytes.NewBuffer(jsonData) - if common.DebugEnabled { - println(fmt.Sprintf("Rerank request body: %s", requestBody.String())) + var requestBody io.Reader + if model_setting.GetGlobalSettings().PassThroughRequestEnabled || relayInfo.ChannelSetting.PassThroughBodyEnabled { + body, err := common.GetRequestBody(c) + if err != nil { + return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest) + } + requestBody = bytes.NewBuffer(body) + } else { + convertedRequest, err := adaptor.ConvertRerankRequest(c, relayInfo.RelayMode, *rerankRequest) + if err != nil { + return types.NewError(err, types.ErrorCodeConvertRequestFailed) + } + jsonData, err := common.Marshal(convertedRequest) + if err != nil { + return types.NewError(err, types.ErrorCodeConvertRequestFailed) + } + + // apply param override + if len(relayInfo.ParamOverride) > 0 { + reqMap := make(map[string]interface{}) + _ = common.Unmarshal(jsonData, &reqMap) + for key, value := range relayInfo.ParamOverride { + reqMap[key] = value + } + jsonData, err = common.Marshal(reqMap) + if err != nil { + return types.NewError(err, types.ErrorCodeChannelParamOverrideInvalid) + } + } + + if common.DebugEnabled { + println(fmt.Sprintf("Rerank request body: %s", string(jsonData))) + } + requestBody = bytes.NewBuffer(jsonData) } + resp, err := adaptor.DoRequest(c, relayInfo, requestBody) if err != nil { return types.NewOpenAIError(err, types.ErrorCodeDoRequestFailed, http.StatusInternalServerError) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index d2fd6758..f20c86d9 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -121,6 +121,12 @@ const EditChannelModal = (props) => { weight: 0, tag: '', multi_key_mode: 'random', + // 渠道额外设置的默认值 + force_format: false, + thinking_to_content: false, + proxy: '', + pass_through_body_enabled: false, + system_prompt: '', }; const [batch, setBatch] = useState(false); const [multiToSingle, setMultiToSingle] = useState(false); @@ -142,8 +148,69 @@ const EditChannelModal = (props) => { const [isMultiKeyChannel, setIsMultiKeyChannel] = useState(false); const [channelSearchValue, setChannelSearchValue] = useState(''); const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式 + // 渠道额外设置状态 + const [channelSettings, setChannelSettings] = useState({ + force_format: false, + thinking_to_content: false, + proxy: '', + pass_through_body_enabled: false, + system_prompt: '', + }); const showApiConfigCard = inputs.type !== 45; // 控制是否显示 API 配置卡片(仅当渠道类型不是 豆包 时显示) const getInitValues = () => ({ ...originInputs }); + + // 处理渠道额外设置的更新 + const handleChannelSettingsChange = (key, value) => { + // 更新内部状态 + setChannelSettings(prev => ({ ...prev, [key]: value })); + + // 同步更新到表单字段 + if (formApiRef.current) { + formApiRef.current.setValue(key, value); + } + + // 同步更新inputs状态 + setInputs(prev => ({ ...prev, [key]: value })); + + // 生成setting JSON并更新 + const newSettings = { ...channelSettings, [key]: value }; + const settingsJson = JSON.stringify(newSettings); + handleInputChange('setting', settingsJson); + }; + + // 解析渠道设置JSON为单独的状态 + const parseChannelSettings = (settingJson) => { + try { + if (settingJson && settingJson.trim()) { + const parsed = JSON.parse(settingJson); + setChannelSettings({ + force_format: parsed.force_format || false, + thinking_to_content: parsed.thinking_to_content || false, + proxy: parsed.proxy || '', + pass_through_body_enabled: parsed.pass_through_body_enabled || false, + system_prompt: parsed.system_prompt || '', + }); + } else { + setChannelSettings({ + force_format: false, + thinking_to_content: false, + proxy: '', + pass_through_body_enabled: false, + system_prompt: '', + }); + } + } catch (error) { + console.error('解析渠道设置失败:', error); + setChannelSettings({ + force_format: false, + thinking_to_content: false, + proxy: '', + pass_through_body_enabled: false, + system_prompt: '', + }); + } + }; + const handleInputChange = (name, value) => { if (formApiRef.current) { formApiRef.current.setValue(name, value); @@ -256,6 +323,30 @@ const EditChannelModal = (props) => { setBatch(false); setMultiToSingle(false); } + // 解析渠道额外设置并合并到data中 + if (data.setting) { + try { + const parsedSettings = JSON.parse(data.setting); + data.force_format = parsedSettings.force_format || false; + data.thinking_to_content = parsedSettings.thinking_to_content || false; + data.proxy = parsedSettings.proxy || ''; + data.pass_through_body_enabled = parsedSettings.pass_through_body_enabled || false; + data.system_prompt = parsedSettings.system_prompt || ''; + } catch (error) { + console.error('解析渠道设置失败:', error); + data.force_format = false; + data.thinking_to_content = false; + data.proxy = ''; + data.pass_through_body_enabled = false; + } + } else { + data.force_format = false; + data.thinking_to_content = false; + data.proxy = ''; + data.pass_through_body_enabled = false; + data.system_prompt = ''; + } + setInputs(data); if (formApiRef.current) { formApiRef.current.setValues(data); @@ -266,6 +357,14 @@ const EditChannelModal = (props) => { setAutoBan(true); } setBasicModels(getChannelModels(data.type)); + // 同步更新channelSettings状态显示 + setChannelSettings({ + force_format: data.force_format, + thinking_to_content: data.thinking_to_content, + proxy: data.proxy, + pass_through_body_enabled: data.pass_through_body_enabled, + system_prompt: data.system_prompt, + }); // console.log(data); } else { showError(message); @@ -446,6 +545,14 @@ const EditChannelModal = (props) => { setUseManualInput(false); } else { formApiRef.current?.reset(); + // 重置渠道设置状态 + setChannelSettings({ + force_format: false, + thinking_to_content: false, + proxy: '', + pass_through_body_enabled: false, + system_prompt: '', + }); } }, [props.visible, channelId]); @@ -579,6 +686,24 @@ const EditChannelModal = (props) => { if (localInputs.type === 18 && localInputs.other === '') { localInputs.other = 'v2.1'; } + + // 生成渠道额外设置JSON + const channelExtraSettings = { + force_format: localInputs.force_format || false, + thinking_to_content: localInputs.thinking_to_content || false, + proxy: localInputs.proxy || '', + pass_through_body_enabled: localInputs.pass_through_body_enabled || false, + system_prompt: localInputs.system_prompt || '', + }; + localInputs.setting = JSON.stringify(channelExtraSettings); + + // 清理不需要发送到后端的字段 + delete localInputs.force_format; + delete localInputs.thinking_to_content; + delete localInputs.proxy; + delete localInputs.pass_through_body_enabled; + delete localInputs.system_prompt; + let res; localInputs.auto_ban = localInputs.auto_ban ? 1 : 0; localInputs.models = localInputs.models.join(','); @@ -1446,33 +1571,103 @@ const EditChannelModal = (props) => { showClear /> - handleInputChange('setting', value)} - extraText={( - +
+ + {t('渠道额外设置')} + +
+ + +
+ {t('强制格式化(只适用于OpenAI渠道类型)')} + + {t('强制将响应格式化为 OpenAI 标准格式')} + +
+ + + handleChannelSettingsChange('force_format', val)} + /> + +
+ + + +
+ {t('思考内容转换')} + + {t('将 reasoning_content 转换为 标签拼接到内容中')} + +
+ + + handleChannelSettingsChange('thinking_to_content', val)} + /> + +
+ + + +
+ {t('透传请求体')} + + {t('启用请求体透传功能')} + +
+ + + handleChannelSettingsChange('pass_through_body_enabled', val)} + /> + +
+ +
+ handleChannelSettingsChange('proxy', val)} + showClear + helpText={t('用于配置网络代理')} + /> +
+ +
+ handleChannelSettingsChange('system_prompt', val)} + autosize + showClear + helpText={t('用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置')} + /> +
+ +
handleInputChange('setting', JSON.stringify({ force_format: true }, null, 2))} - > - {t('填入模板')} - - window.open('https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md')} > {t('设置说明')} - - )} - showClear - /> +
+
+
+ + diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 5762533f..d340d825 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1330,6 +1330,19 @@ "API地址": "Base URL", "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in", "渠道额外设置": "Channel extra settings", + "强制格式化": "Force format", + "强制格式化(只适用于OpenAI渠道类型)": "Force format (Only for OpenAI channel types)", + "强制将响应格式化为 OpenAI 标准格式": "Force format responses to OpenAI standard format", + "思考内容转换": "Thinking content conversion", + "将 reasoning_content 转换为 标签拼接到内容中": "Convert reasoning_content to tags and append to content", + "透传请求体": "Pass through body", + "启用请求体透传功能": "Enable request body pass-through functionality", + "代理地址": "Proxy address", + "例如: socks5://user:pass@host:port": "e.g.: socks5://user:pass@host:port", + "用于配置网络代理,支持 socks5 协议": "Used to configure network proxy, supports socks5 protocol", + "系统提示词": "System Prompt", + "输入系统提示词,用户的系统提示词将优先于此设置": "Enter system prompt, user's system prompt will take priority over this setting", + "用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置": "User priority: If the user specifies a system prompt in the request, the user's setting will be used first", "参数覆盖": "Parameters override", "模型请求速率限制": "Model request rate limit", "启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)", From 2469c439b1d97a9ce945a8b380cc6b1cc8f9312e Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 26 Jul 2025 12:11:20 +0800 Subject: [PATCH 4/7] fix: improve error messaging and JSON schema handling in distributor and relay components --- dto/openai_request.go | 12 ++++++------ middleware/distributor.go | 13 ++++++------- relay/channel/gemini/relay-gemini.go | 10 +++++++--- relay/relay-text.go | 3 +++ 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/dto/openai_request.go b/dto/openai_request.go index b410dffe..29076ef6 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -7,15 +7,15 @@ import ( ) type ResponseFormat struct { - Type string `json:"type,omitempty"` - JsonSchema *FormatJsonSchema `json:"json_schema,omitempty"` + Type string `json:"type,omitempty"` + JsonSchema json.RawMessage `json:"json_schema,omitempty"` } type FormatJsonSchema struct { - Description string `json:"description,omitempty"` - Name string `json:"name"` - Schema any `json:"schema,omitempty"` - Strict any `json:"strict,omitempty"` + Description string `json:"description,omitempty"` + Name string `json:"name"` + Schema any `json:"schema,omitempty"` + Strict json.RawMessage `json:"strict,omitempty"` } type GeneralOpenAIRequest struct { diff --git a/middleware/distributor.go b/middleware/distributor.go index 3c529a41..cba9b521 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -111,18 +111,17 @@ func Distribute() func(c *gin.Context) { if userGroup == "auto" { showGroup = fmt.Sprintf("auto(%s)", selectGroup) } - message := fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败(distributor): %s", showGroup, modelRequest.Model, err.Error()) + message := fmt.Sprintf("获取分组 %s 下模型 %s 的可用渠道失败(数据库一致性已被破坏,distributor): %s", showGroup, modelRequest.Model, err.Error()) // 如果错误,但是渠道不为空,说明是数据库一致性问题 - if channel != nil { - common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id)) - message = "数据库一致性已被破坏,请联系管理员" - } - // 如果错误,而且渠道为空,说明是没有可用渠道 + //if channel != nil { + // common.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id)) + // message = "数据库一致性已被破坏,请联系管理员" + //} abortWithOpenAiMessage(c, http.StatusServiceUnavailable, message) return } if channel == nil { - abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 的可用渠道不存在(数据库一致性已被破坏,distributor)", userGroup, modelRequest.Model)) + abortWithOpenAiMessage(c, http.StatusServiceUnavailable, fmt.Sprintf("分组 %s 下模型 %s 无可用渠道(distributor)", userGroup, modelRequest.Model)) return } } diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index 6f3babeb..d19ee1ae 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -219,9 +219,13 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") { geminiRequest.GenerationConfig.ResponseMimeType = "application/json" - if textRequest.ResponseFormat.JsonSchema != nil && textRequest.ResponseFormat.JsonSchema.Schema != nil { - cleanedSchema := removeAdditionalPropertiesWithDepth(textRequest.ResponseFormat.JsonSchema.Schema, 0) - geminiRequest.GenerationConfig.ResponseSchema = cleanedSchema + if len(textRequest.ResponseFormat.JsonSchema) > 0 { + // 先将json.RawMessage解析 + var jsonSchema dto.FormatJsonSchema + if err := common.Unmarshal(textRequest.ResponseFormat.JsonSchema, &jsonSchema); err == nil { + cleanedSchema := removeAdditionalPropertiesWithDepth(jsonSchema.Schema, 0) + geminiRequest.GenerationConfig.ResponseSchema = cleanedSchema + } } } tool_call_ids := make(map[string]string) diff --git a/relay/relay-text.go b/relay/relay-text.go index bcb93a65..84d4e38b 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -175,6 +175,9 @@ func TextHelper(c *gin.Context) (newAPIError *types.NewAPIError) { if err != nil { return types.NewErrorWithStatusCode(err, types.ErrorCodeReadRequestBodyFailed, http.StatusBadRequest) } + if common.DebugEnabled { + println("requestBody: ", string(body)) + } requestBody = bytes.NewBuffer(body) } else { convertedRequest, err := adaptor.ConvertOpenAIRequest(c, relayInfo, textRequest) From 8e3cf2eaabe2f831fb03bbad9971333abf76b5f4 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sat, 26 Jul 2025 13:31:33 +0800 Subject: [PATCH 5/7] feat: support claude convert to gemini --- model/ability.go | 2 +- model/channel_cache.go | 2 +- relay/channel/gemini/adaptor.go | 12 ++++-- relay/channel/gemini/relay-gemini.go | 61 ++++++++++++++++++++++------ relay/channel/openai/helper.go | 4 +- relay/channel/openai/relay-openai.go | 21 ++-------- relay/helper/common.go | 18 ++++++++ types/error.go | 1 + 8 files changed, 84 insertions(+), 37 deletions(-) diff --git a/model/ability.go b/model/ability.go index f36ff764..6dd8d8a6 100644 --- a/model/ability.go +++ b/model/ability.go @@ -136,7 +136,7 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel, } } } else { - return nil, errors.New("channel not found") + return nil, nil } err = DB.First(&channel, "id = ?", channel.Id).Error return &channel, err diff --git a/model/channel_cache.go b/model/channel_cache.go index d18e9c89..45069ba0 100644 --- a/model/channel_cache.go +++ b/model/channel_cache.go @@ -130,7 +130,7 @@ func getRandomSatisfiedChannel(group string, model string, retry int) (*Channel, channels := group2model2channels[group][model] if len(channels) == 0 { - return nil, errors.New("channel not found") + return nil, nil } if len(channels) == 1 { diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index 71eb9ba4..2e31ec55 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -9,6 +9,7 @@ import ( "one-api/common" "one-api/dto" "one-api/relay/channel" + "one-api/relay/channel/openai" relaycommon "one-api/relay/common" "one-api/relay/constant" "one-api/setting/model_setting" @@ -21,10 +22,13 @@ import ( type Adaptor struct { } -func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { - //TODO implement me - panic("implement me") - return nil, nil +func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, req *dto.ClaudeRequest) (any, error) { + adaptor := openai.Adaptor{} + oaiReq, err := adaptor.ConvertClaudeRequest(c, info, req) + if err != nil { + return nil, err + } + return a.ConvertOpenAIRequest(c, info, oaiReq.(*dto.GeneralOpenAIRequest)) } func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index d19ee1ae..7e57bdac 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -9,6 +9,7 @@ import ( "one-api/common" "one-api/constant" "one-api/dto" + "one-api/relay/channel/openai" relaycommon "one-api/relay/common" "one-api/relay/helper" "one-api/service" @@ -736,7 +737,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C choice := dto.ChatCompletionsStreamResponseChoice{ Index: int(candidate.Index), Delta: dto.ChatCompletionsStreamResponseChoiceDelta{ - Role: "assistant", + //Role: "assistant", }, } var texts []string @@ -798,6 +799,27 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C return &response, isStop, hasImage } +func handleStream(c *gin.Context, info *relaycommon.RelayInfo, resp *dto.ChatCompletionsStreamResponse) error { + streamData, err := common.Marshal(resp) + if err != nil { + return fmt.Errorf("failed to marshal stream response: %w", err) + } + err = openai.HandleStreamFormat(c, info, string(streamData), info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent) + if err != nil { + return fmt.Errorf("failed to handle stream format: %w", err) + } + return nil +} + +func handleFinalStream(c *gin.Context, info *relaycommon.RelayInfo, resp *dto.ChatCompletionsStreamResponse) error { + streamData, err := common.Marshal(resp) + if err != nil { + return fmt.Errorf("failed to marshal stream response: %w", err) + } + openai.HandleFinalResponse(c, info, string(streamData), resp.Id, resp.Created, resp.Model, resp.GetSystemFingerprint(), resp.Usage, info.ShouldIncludeUsage) + return nil +} + func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.Usage, *types.NewAPIError) { // responseText := "" id := helper.GetResponseID(c) @@ -805,6 +827,8 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * var usage = &dto.Usage{} var imageCount int + respCount := 0 + helper.StreamScannerHandler(c, resp, info, func(data string) bool { var geminiResponse GeminiChatResponse err := common.UnmarshalJsonStr(data, &geminiResponse) @@ -833,18 +857,31 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * } } } - err = helper.ObjectData(c, response) + + if respCount == 0 { + // send first response + err = handleStream(c, info, helper.GenerateStartEmptyResponse(id, createAt, info.UpstreamModelName, nil)) + if err != nil { + common.LogError(c, err.Error()) + } + } + + err = handleStream(c, info, response) if err != nil { common.LogError(c, err.Error()) } if isStop { - response := helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop) - helper.ObjectData(c, response) + _ = handleStream(c, info, helper.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)) } + respCount++ return true }) - var response *dto.ChatCompletionsStreamResponse + if respCount == 0 { + // 空补全,报错不计费 + // empty response, throw an error + return nil, types.NewOpenAIError(errors.New("no response received from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError) + } if imageCount != 0 { if usage.CompletionTokens == 0 { @@ -855,14 +892,14 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * usage.PromptTokensDetails.TextTokens = usage.PromptTokens usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens - if info.ShouldIncludeUsage { - response = helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage) - err := helper.ObjectData(c, response) - if err != nil { - common.SysError("send final response failed: " + err.Error()) - } + response := helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage) + err := handleFinalStream(c, info, response) + if err != nil { + common.SysError("send final response failed: " + err.Error()) } - helper.Done(c) + //if info.RelayFormat == relaycommon.RelayFormatOpenAI { + // helper.Done(c) + //} //resp.Body.Close() return usage, nil } diff --git a/relay/channel/openai/helper.go b/relay/channel/openai/helper.go index 7fee505a..1681c9ff 100644 --- a/relay/channel/openai/helper.go +++ b/relay/channel/openai/helper.go @@ -14,7 +14,7 @@ import ( ) // 辅助函数 -func handleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error { +func HandleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error { info.SendResponseCount++ switch info.RelayFormat { case relaycommon.RelayFormatOpenAI: @@ -158,7 +158,7 @@ func handleLastResponse(lastStreamData string, responseId *string, createAt *int return nil } -func handleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStreamData string, +func HandleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStreamData string, responseId string, createAt int64, model string, systemFingerprint string, usage *dto.Usage, containStreamUsage bool) { diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index d739ea19..82bd2d26 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -123,24 +123,11 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re var toolCount int var usage = &dto.Usage{} var streamItems []string // store stream items - var forceFormat bool - var thinkToContent bool - - if info.ChannelSetting.ForceFormat { - forceFormat = true - } - - if info.ChannelSetting.ThinkingToContent { - thinkToContent = true - } - - var ( - lastStreamData string - ) + var lastStreamData string helper.StreamScannerHandler(c, resp, info, func(data string) bool { if lastStreamData != "" { - err := handleStreamFormat(c, info, lastStreamData, forceFormat, thinkToContent) + err := HandleStreamFormat(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent) if err != nil { common.SysError("error handling stream format: " + err.Error()) } @@ -161,7 +148,7 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re if info.RelayFormat == relaycommon.RelayFormatOpenAI { if shouldSendLastResp { - _ = sendStreamData(c, info, lastStreamData, forceFormat, thinkToContent) + _ = sendStreamData(c, info, lastStreamData, info.ChannelSetting.ForceFormat, info.ChannelSetting.ThinkingToContent) } } @@ -180,7 +167,7 @@ func OaiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Re } } } - handleFinalResponse(c, info, lastStreamData, responseId, createAt, model, systemFingerprint, usage, containStreamUsage) + HandleFinalResponse(c, info, lastStreamData, responseId, createAt, model, systemFingerprint, usage, containStreamUsage) return usage, nil } diff --git a/relay/helper/common.go b/relay/helper/common.go index 5d23b512..c8edb798 100644 --- a/relay/helper/common.go +++ b/relay/helper/common.go @@ -139,6 +139,24 @@ func GetLocalRealtimeID(c *gin.Context) string { return fmt.Sprintf("evt_%s", logID) } +func GenerateStartEmptyResponse(id string, createAt int64, model string, systemFingerprint *string) *dto.ChatCompletionsStreamResponse { + return &dto.ChatCompletionsStreamResponse{ + Id: id, + Object: "chat.completion.chunk", + Created: createAt, + Model: model, + SystemFingerprint: systemFingerprint, + Choices: []dto.ChatCompletionsStreamResponseChoice{ + { + Delta: dto.ChatCompletionsStreamResponseChoiceDelta{ + Role: "assistant", + Content: common.GetPointer(""), + }, + }, + }, + } +} + func GenerateStopResponse(id string, createAt int64, model string, finishReason string) *dto.ChatCompletionsStreamResponse { return &dto.ChatCompletionsStreamResponse{ Id: id, diff --git a/types/error.go b/types/error.go index fa07c231..c94bd001 100644 --- a/types/error.go +++ b/types/error.go @@ -63,6 +63,7 @@ const ( ErrorCodeBadResponseStatusCode ErrorCode = "bad_response_status_code" ErrorCodeBadResponse ErrorCode = "bad_response" ErrorCodeBadResponseBody ErrorCode = "bad_response_body" + ErrorCodeEmptyResponse ErrorCode = "empty_response" // sql error ErrorCodeQueryDataError ErrorCode = "query_data_error" From f15a53fae4aac2d649c5ca6ffae54165648a4815 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sat, 26 Jul 2025 13:33:10 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=8E=A8=20refactor(ui):=20redesign=20c?= =?UTF-8?q?hannel=20extra=20settings=20section=20in=20EditChannelModal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract channel extra settings into a dedicated Card component for better visual hierarchy - Replace custom gray background container with consistent Form component styling - Simplify layout structure by removing complex Row/Col grid layout in favor of native Form component layout - Unify help text styling by using extraText prop consistently across all form fields - Move "Settings Documentation" link to card header subtitle for better accessibility - Improve visual consistency with other setting cards by using matching design patterns The channel extra settings (force format, thinking content conversion, pass-through body, proxy address, and system prompt) now follow the same design language as other configuration sections, providing a more cohesive user experience. Affected settings: - Force Format (OpenAI channels only) - Thinking Content Conversion - Pass-through Body - Proxy Address - System Prompt --- i18n/zh-cn.json | 3 +- .../channels/modals/EditChannelModal.jsx | 159 +++++++----------- web/src/i18n/locales/en.json | 3 +- 3 files changed, 66 insertions(+), 99 deletions(-) diff --git a/i18n/zh-cn.json b/i18n/zh-cn.json index 0c838c5c..dc7a1e4c 100644 --- a/i18n/zh-cn.json +++ b/i18n/zh-cn.json @@ -586,8 +586,7 @@ "渠道额外设置": "渠道额外设置", "此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:": "此项可选,用于配置渠道特定设置,为一个 JSON 字符串,例如:", "强制格式化": "强制格式化", - "强制格式化(只适用于OpenAI渠道类型)": "强制格式化(只适用于OpenAI渠道类型)", - "强制将响应格式化为 OpenAI 标准格式": "强制将响应格式化为 OpenAI 标准格式", + "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)", "思考内容转换": "思考内容转换", "将 reasoning_content 转换为 标签拼接到内容中": "将 reasoning_content 转换为 标签拼接到内容中", "透传请求体": "透传请求体", diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index f20c86d9..a4c8ea76 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -158,20 +158,20 @@ const EditChannelModal = (props) => { }); const showApiConfigCard = inputs.type !== 45; // 控制是否显示 API 配置卡片(仅当渠道类型不是 豆包 时显示) const getInitValues = () => ({ ...originInputs }); - + // 处理渠道额外设置的更新 const handleChannelSettingsChange = (key, value) => { // 更新内部状态 setChannelSettings(prev => ({ ...prev, [key]: value })); - + // 同步更新到表单字段 if (formApiRef.current) { formApiRef.current.setValue(key, value); } - + // 同步更新inputs状态 setInputs(prev => ({ ...prev, [key]: value })); - + // 生成setting JSON并更新 const newSettings = { ...channelSettings, [key]: value }; const settingsJson = JSON.stringify(newSettings); @@ -686,7 +686,7 @@ const EditChannelModal = (props) => { if (localInputs.type === 18 && localInputs.other === '') { localInputs.other = 'v2.1'; } - + // 生成渠道额外设置JSON const channelExtraSettings = { force_format: localInputs.force_format || false, @@ -696,14 +696,14 @@ const EditChannelModal = (props) => { system_prompt: localInputs.system_prompt || '', }; localInputs.setting = JSON.stringify(channelExtraSettings); - + // 清理不需要发送到后端的字段 delete localInputs.force_format; delete localInputs.thinking_to_content; delete localInputs.proxy; delete localInputs.pass_through_body_enabled; delete localInputs.system_prompt; - + let res; localInputs.auto_ban = localInputs.auto_ban ? 1 : 0; localInputs.models = localInputs.models.join(','); @@ -1525,7 +1525,7 @@ const EditChannelModal = (props) => { label={t('是否自动禁用')} checkedText={t('开')} uncheckedText={t('关')} - onChange={(val) => setAutoBan(val)} + onChange={(value) => setAutoBan(value)} extraText={t('仅当自动禁用开启时有效,关闭后不会自动禁用该渠道')} initValue={autoBan} /> @@ -1570,95 +1570,20 @@ const EditChannelModal = (props) => { } showClear /> + -
- - {t('渠道额外设置')} - -
- - -
- {t('强制格式化(只适用于OpenAI渠道类型)')} - - {t('强制将响应格式化为 OpenAI 标准格式')} - -
- - - handleChannelSettingsChange('force_format', val)} - /> - -
- - - -
- {t('思考内容转换')} - - {t('将 reasoning_content 转换为 标签拼接到内容中')} - -
- - - handleChannelSettingsChange('thinking_to_content', val)} - /> - -
- - - -
- {t('透传请求体')} - - {t('启用请求体透传功能')} - -
- - - handleChannelSettingsChange('pass_through_body_enabled', val)} - /> - -
- -
- handleChannelSettingsChange('proxy', val)} - showClear - helpText={t('用于配置网络代理')} - /> -
- -
- handleChannelSettingsChange('system_prompt', val)} - autosize - showClear - helpText={t('用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置')} - /> -
- -
+ {/* Channel Extra Settings Card */} + + {/* Header: Channel Extra Settings */} +
+ + + +
+ {t('渠道额外设置')} +
window.open('https://github.com/QuantumNous/new-api/blob/main/docs/channel/other_setting.md')} > {t('设置说明')} @@ -1667,7 +1592,51 @@ const EditChannelModal = (props) => {
+ handleChannelSettingsChange('force_format', value)} + extraText={t('强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)')} + /> + handleChannelSettingsChange('thinking_to_content', value)} + extraText={t('将 reasoning_content 转换为 标签拼接到内容中')} + /> + + handleChannelSettingsChange('pass_through_body_enabled', value)} + extraText={t('启用请求体透传功能')} + /> + + handleChannelSettingsChange('proxy', value)} + showClear + extraText={t('用于配置网络代理,支持 socks5 协议')} + /> + + handleChannelSettingsChange('system_prompt', value)} + autosize + showClear + extraText={t('用户优先:如果用户在请求中指定了系统提示词,将优先使用用户的设置')} + />
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index d340d825..a1bf619d 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1331,8 +1331,7 @@ "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in", "渠道额外设置": "Channel extra settings", "强制格式化": "Force format", - "强制格式化(只适用于OpenAI渠道类型)": "Force format (Only for OpenAI channel types)", - "强制将响应格式化为 OpenAI 标准格式": "Force format responses to OpenAI standard format", + "强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)": "Force format responses to OpenAI standard format (Only for OpenAI channel types)", "思考内容转换": "Thinking content conversion", "将 reasoning_content 转换为 标签拼接到内容中": "Convert reasoning_content to tags and append to content", "透传请求体": "Pass through body", From a8a42cbfa8eb51ef9ecf9b02b53369e65291c8dd Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sat, 26 Jul 2025 17:18:47 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=92=84=20style(ui):=20show=20"Force?= =?UTF-8?q?=20Format"=20toggle=20only=20for=20OpenAI=20channels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the "Force Format" switch was displayed for every channel type although it only applies to OpenAI (type === 1). This change wraps the switch in a conditional so it renders exclusively when the selected channel type is OpenAI. Why: - Prevents user confusion when configuring non-OpenAI channels - Keeps the UI clean and context-relevant Scope: - web/src/components/table/channels/modals/EditChannelModal.jsx No backend logic affected. --- .../table/channels/modals/EditChannelModal.jsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index a4c8ea76..248307c4 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -1592,14 +1592,16 @@ const EditChannelModal = (props) => {
- handleChannelSettingsChange('force_format', value)} - extraText={t('强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)')} - /> + {inputs.type === 1 && ( + handleChannelSettingsChange('force_format', value)} + extraText={t('强制将响应格式化为 OpenAI 标准格式(只适用于OpenAI渠道类型)')} + /> + )}