diff --git a/dto/channel_settings.go b/dto/channel_settings.go index 8791f516..d6d6e084 100644 --- a/dto/channel_settings.go +++ b/dto/channel_settings.go @@ -19,4 +19,12 @@ const ( type ChannelOtherSettings struct { AzureResponsesVersion string `json:"azure_responses_version,omitempty"` VertexKeyType VertexKeyType `json:"vertex_key_type,omitempty"` // "json" or "api_key" + OpenRouterEnterprise *bool `json:"openrouter_enterprise,omitempty"` +} + +func (s *ChannelOtherSettings) IsOpenRouterEnterprise() bool { + if s == nil || s.OpenRouterEnterprise == nil { + return false + } + return *s.OpenRouterEnterprise } diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go index a065caff..79a0f706 100644 --- a/relay/channel/api_request.go +++ b/relay/channel/api_request.go @@ -265,6 +265,7 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http resp, err := client.Do(req) if err != nil { + logger.LogError(c, "do request failed: "+err.Error()) return nil, types.NewError(err, types.ErrorCodeDoRequestFailed, types.ErrOptionWithHideErrMsg("upstream error: do request failed")) } if resp == nil { diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index 4b13a7df..26a7f40c 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -12,6 +12,7 @@ import ( "one-api/constant" "one-api/dto" "one-api/logger" + "one-api/relay/channel/openrouter" relaycommon "one-api/relay/common" "one-api/relay/helper" "one-api/service" @@ -185,9 +186,25 @@ func OpenaiHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Respo if common.DebugEnabled { println("upstream response body:", string(responseBody)) } - err = common.Unmarshal(responseBody, &simpleResponse) - if err != nil { - return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + // Unmarshal to simpleResponse + if info.ChannelType == constant.ChannelTypeOpenRouter && info.ChannelOtherSettings.IsOpenRouterEnterprise() { + // 尝试解析为 openrouter enterprise + var enterpriseResponse openrouter.OpenRouterEnterpriseResponse + err = common.Unmarshal(responseBody, &enterpriseResponse) + if err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + if enterpriseResponse.Success { + responseBody = enterpriseResponse.Data + } else { + logger.LogError(c, fmt.Sprintf("openrouter enterprise response success=false, data: %s", enterpriseResponse.Data)) + return nil, types.NewOpenAIError(fmt.Errorf("openrouter response success=false"), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } + } else { + err = common.Unmarshal(responseBody, &simpleResponse) + if err != nil { + return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) + } } if oaiError := simpleResponse.GetOpenAIError(); oaiError != nil && oaiError.Type != "" { return nil, types.WithOpenAIError(*oaiError, resp.StatusCode) diff --git a/relay/channel/openrouter/dto.go b/relay/channel/openrouter/dto.go index 607f495b..a3249985 100644 --- a/relay/channel/openrouter/dto.go +++ b/relay/channel/openrouter/dto.go @@ -1,5 +1,7 @@ package openrouter +import "encoding/json" + type RequestReasoning struct { // One of the following (not both): Effort string `json:"effort,omitempty"` // Can be "high", "medium", or "low" (OpenAI-style) @@ -7,3 +9,8 @@ type RequestReasoning struct { // Optional: Default is false. All models support this. Exclude bool `json:"exclude,omitempty"` // Set to true to exclude reasoning tokens from response } + +type OpenRouterEnterpriseResponse struct { + Data json.RawMessage `json:"data"` + Success bool `json:"success"` +} diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index dd620fe0..25ef68c6 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -164,6 +164,8 @@ const EditChannelModal = (props) => { settings: '', // 仅 Vertex: 密钥格式(存入 settings.vertex_key_type) vertex_key_type: 'json', + // 企业账户设置 + is_enterprise_account: false, }; const [batch, setBatch] = useState(false); const [multiToSingle, setMultiToSingle] = useState(false); @@ -189,6 +191,7 @@ const EditChannelModal = (props) => { const [channelSearchValue, setChannelSearchValue] = useState(''); const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式 const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加) + const [isEnterpriseAccount, setIsEnterpriseAccount] = useState(false); // 是否为企业账户 // 2FA验证查看密钥相关状态 const [twoFAState, setTwoFAState] = useState({ @@ -437,15 +440,19 @@ const EditChannelModal = (props) => { parsedSettings.azure_responses_version || ''; // 读取 Vertex 密钥格式 data.vertex_key_type = parsedSettings.vertex_key_type || 'json'; + // 读取企业账户设置 + data.is_enterprise_account = parsedSettings.openrouter_enterprise === true; } catch (error) { console.error('解析其他设置失败:', error); data.azure_responses_version = ''; data.region = ''; data.vertex_key_type = 'json'; + data.is_enterprise_account = false; } } else { // 兼容历史数据:老渠道没有 settings 时,默认按 json 展示 data.vertex_key_type = 'json'; + data.is_enterprise_account = false; } setInputs(data); @@ -457,6 +464,8 @@ const EditChannelModal = (props) => { } else { setAutoBan(true); } + // 同步企业账户状态 + setIsEnterpriseAccount(data.is_enterprise_account || false); setBasicModels(getChannelModels(data.type)); // 同步更新channelSettings状态显示 setChannelSettings({ @@ -716,6 +725,8 @@ const EditChannelModal = (props) => { }); // 重置密钥模式状态 setKeyMode('append'); + // 重置企业账户状态 + setIsEnterpriseAccount(false); // 清空表单中的key_mode字段 if (formApiRef.current) { formApiRef.current.setValue('key_mode', undefined); @@ -879,6 +890,21 @@ const EditChannelModal = (props) => { }; localInputs.setting = JSON.stringify(channelExtraSettings); + // 处理type === 20的企业账户设置 + if (localInputs.type === 20) { + let settings = {}; + if (localInputs.settings) { + try { + settings = JSON.parse(localInputs.settings); + } catch (error) { + console.error('解析settings失败:', error); + } + } + // 设置企业账户标识,无论是true还是false都要传到后端 + settings.openrouter_enterprise = localInputs.is_enterprise_account === true; + localInputs.settings = JSON.stringify(settings); + } + // 清理不需要发送到后端的字段 delete localInputs.force_format; delete localInputs.thinking_to_content; @@ -886,6 +912,7 @@ const EditChannelModal = (props) => { delete localInputs.pass_through_body_enabled; delete localInputs.system_prompt; delete localInputs.system_prompt_override; + delete localInputs.is_enterprise_account; // 顶层的 vertex_key_type 不应发送给后端 delete localInputs.vertex_key_type; @@ -1203,6 +1230,21 @@ const EditChannelModal = (props) => { onChange={(value) => handleInputChange('type', value)} /> + {inputs.type === 20 && ( +