diff --git a/.dockerignore b/.dockerignore index cf14beb..7c16b58 100644 --- a/.dockerignore +++ b/.dockerignore @@ -15,4 +15,16 @@ models-page* pricing-page.png login-page scripts -relay/helper/price_test.go \ No newline at end of file +relay/helper/price_test.go +.superpowers +*.png +*.bak +.worktrees +**/node_modules +**/.gocache +**/.gocache-temp +logs +*.db +*.db-journal +*.zip +web/dist diff --git a/.gitattributes b/.gitattributes index 63328aa..b70c922 100644 --- a/.gitattributes +++ b/.gitattributes @@ -36,3 +36,4 @@ # ============================================ # Mark web frontend as vendored so GitHub recognizes this as a Go project electron/** linguist-vendored +.dockerignore text eol=lf diff --git a/dto/openai_response.go b/dto/openai_response.go index a405b97..37ea17b 100644 --- a/dto/openai_response.go +++ b/dto/openai_response.go @@ -375,6 +375,7 @@ const ( type ResponsesStreamResponse struct { Type string `json:"type"` Response *OpenAIResponsesResponse `json:"response,omitempty"` + Error any `json:"error,omitempty"` Delta string `json:"delta,omitempty"` Item *ResponsesOutput `json:"item,omitempty"` // - response.function_call_arguments.delta diff --git a/relay/channel/codex/adaptor.go b/relay/channel/codex/adaptor.go index 42f3b8e..f01da8d 100644 --- a/relay/channel/codex/adaptor.go +++ b/relay/channel/codex/adaptor.go @@ -138,9 +138,21 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { if info.RelayMode != relayconstant.RelayModeResponses && info.RelayMode != relayconstant.RelayModeResponsesCompact { return "", errors.New("codex channel: only /v1/responses and /v1/responses/compact are supported") } - path := "/backend-api/codex/responses" + + key := strings.TrimSpace(info.ApiKey) + if strings.HasPrefix(key, "{") { + // OAuth mode: route to ChatGPT backend API + path := "/backend-api/codex/responses" + if info.RelayMode == relayconstant.RelayModeResponsesCompact { + path = "/backend-api/codex/responses/compact" + } + return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, path, info.ChannelType), nil + } + + // API key mode: route to standard /v1/responses + path := "/v1/responses" if info.RelayMode == relayconstant.RelayModeResponsesCompact { - path = "/backend-api/codex/responses/compact" + path = "/v1/responses/compact" } return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, path, info.ChannelType), nil } @@ -149,11 +161,20 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel channel.SetupApiRequestHeader(info, c, req) key := strings.TrimSpace(info.ApiKey) - if !strings.HasPrefix(key, "{") { - return errors.New("codex channel: key must be a JSON object") + if strings.HasPrefix(key, "{") { + return setupOAuthHeader(req, key) } - oauthKey, err := ParseOAuthKey(key) + // Simple API key mode + req.Set("Authorization", "Bearer "+key) + if info.IsStream { + req.Set("Accept", "text/event-stream") + } + return nil +} + +func setupOAuthHeader(req *http.Header, rawKey string) error { + oauthKey, err := ParseOAuthKey(rawKey) if err != nil { return err } @@ -178,13 +199,8 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel req.Set("originator", "codex_cli_rs") } - // chatgpt.com/backend-api/codex/responses is strict about Content-Type. - // Clients may omit it or include parameters like `application/json; charset=utf-8`, - // which can be rejected by the upstream. Force the exact media type. req.Set("Content-Type", "application/json") - if info.IsStream { - req.Set("Accept", "text/event-stream") - } else if req.Get("Accept") == "" { + if req.Get("Accept") == "" { req.Set("Accept", "application/json") } diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go index eb52d6a..6bf5f3b 100644 --- a/relay/compatible_handler.go +++ b/relay/compatible_handler.go @@ -27,6 +27,22 @@ import ( "github.com/gin-gonic/gin" ) +func shouldUseChatCompletionsViaResponses(info *relaycommon.RelayInfo, passThroughGlobal bool) bool { + if info == nil { + return false + } + if info.RelayMode != relayconstant.RelayModeChatCompletions { + return false + } + if info.ChannelType == constant.ChannelTypeCodex { + return true + } + if passThroughGlobal || info.ChannelSetting.PassThroughBodyEnabled { + return false + } + return service.ShouldChatCompletionsUseResponsesGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) +} + func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types.NewAPIError) { info.InitChannelMeta(c) @@ -76,10 +92,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types adaptor.Init(info) passThroughGlobal := model_setting.GetGlobalSettings().PassThroughRequestEnabled - if info.RelayMode == relayconstant.RelayModeChatCompletions && - !passThroughGlobal && - !info.ChannelSetting.PassThroughBodyEnabled && - service.ShouldChatCompletionsUseResponsesGlobal(info.ChannelId, info.ChannelType, info.OriginModelName) { + if shouldUseChatCompletionsViaResponses(info, passThroughGlobal) { applySystemPromptIfNeeded(c, info, request) usage, newApiErr := chatCompletionsViaResponses(c, info, adaptor, request) if newApiErr != nil { diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index de16b09..ec649f7 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -548,6 +548,9 @@ const EditChannelModal = (props) => { if (value === 57) { setCodexCredentialMode(CODEX_CREDENTIAL_MODE.API_KEY); + if (formApiRef.current) { + formApiRef.current.setValue('codex_credential_mode', CODEX_CREDENTIAL_MODE.API_KEY); + } setBatch(false); setMultiToSingle(false); setMultiKeyMode('random'); @@ -703,7 +706,9 @@ const EditChannelModal = (props) => { } if (data.type === 57) { - setCodexCredentialMode(detectCodexCredentialMode(data.key)); + const mode = detectCodexCredentialMode(data.key); + setCodexCredentialMode(mode); + data.codex_credential_mode = mode; } else { setCodexCredentialMode(CODEX_CREDENTIAL_MODE.API_KEY); } @@ -942,6 +947,9 @@ const EditChannelModal = (props) => { const handleCodexOAuthGenerated = (key) => { setCodexCredentialMode(CODEX_CREDENTIAL_MODE.OAUTH); + if (formApiRef.current) { + formApiRef.current.setValue('codex_credential_mode', CODEX_CREDENTIAL_MODE.OAUTH); + } handleInputChange('key', key); formatJsonField('key'); }; @@ -1098,6 +1106,7 @@ const EditChannelModal = (props) => { // 清空表单中的key_mode字段 if (formApiRef.current) { formApiRef.current.setValue('key_mode', undefined); + formApiRef.current.setValue('codex_credential_mode', CODEX_CREDENTIAL_MODE.API_KEY); } // 重置本地输入,避免下次打开残留上一次的 JSON 字段值 setInputs(getInitValues()); @@ -2175,8 +2184,12 @@ const EditChannelModal = (props) => { value: CODEX_CREDENTIAL_MODE.OAUTH, }, ]} - value={codexCredentialMode} - onChange={(value) => setCodexCredentialMode(value)} + onChange={(value) => { + setCodexCredentialMode(value); + if (formApiRef.current) { + formApiRef.current.setValue('codex_credential_mode', value); + } + }} style={{ width: '100%' }} extraText={ isCodexOAuthMode