Просмотр исходного кода

feat: codex API key 模式 + 前端凭证回显修复

- codex adaptor 支持 API key 和 OAuth 两种认证模式
- 提取 setupOAuthHeader 和 shouldUseChatCompletionsViaResponses
- 修复编辑页 codex_credential_mode 切换/回显不同步问题
- ResponsesStreamResponse 添加 Error 字段支持独立错误事件

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
master
fengsilin 1 неделю назад
Родитель
Сommit
575f84064e
6 измененных файлов: 75 добавлений и 19 удалений
  1. +13
    -1
      .dockerignore
  2. +1
    -0
      .gitattributes
  3. +1
    -0
      dto/openai_response.go
  4. +27
    -11
      relay/channel/codex/adaptor.go
  5. +17
    -4
      relay/compatible_handler.go
  6. +16
    -3
      web/src/components/table/channels/modals/EditChannelModal.jsx

+ 13
- 1
.dockerignore Просмотреть файл

@@ -15,4 +15,16 @@ models-page*
pricing-page.png pricing-page.png
login-page login-page
scripts scripts
relay/helper/price_test.go
relay/helper/price_test.go
.superpowers
*.png
*.bak
.worktrees
**/node_modules
**/.gocache
**/.gocache-temp
logs
*.db
*.db-journal
*.zip
web/dist

+ 1
- 0
.gitattributes Просмотреть файл

@@ -36,3 +36,4 @@
# ============================================ # ============================================
# Mark web frontend as vendored so GitHub recognizes this as a Go project # Mark web frontend as vendored so GitHub recognizes this as a Go project
electron/** linguist-vendored electron/** linguist-vendored
.dockerignore text eol=lf

+ 1
- 0
dto/openai_response.go Просмотреть файл

@@ -375,6 +375,7 @@ const (
type ResponsesStreamResponse struct { type ResponsesStreamResponse struct {
Type string `json:"type"` Type string `json:"type"`
Response *OpenAIResponsesResponse `json:"response,omitempty"` Response *OpenAIResponsesResponse `json:"response,omitempty"`
Error any `json:"error,omitempty"`
Delta string `json:"delta,omitempty"` Delta string `json:"delta,omitempty"`
Item *ResponsesOutput `json:"item,omitempty"` Item *ResponsesOutput `json:"item,omitempty"`
// - response.function_call_arguments.delta // - response.function_call_arguments.delta


+ 27
- 11
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 { if info.RelayMode != relayconstant.RelayModeResponses && info.RelayMode != relayconstant.RelayModeResponsesCompact {
return "", errors.New("codex channel: only /v1/responses and /v1/responses/compact are supported") 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 { if info.RelayMode == relayconstant.RelayModeResponsesCompact {
path = "/backend-api/codex/responses/compact"
path = "/v1/responses/compact"
} }
return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, path, info.ChannelType), nil 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) channel.SetupApiRequestHeader(info, c, req)


key := strings.TrimSpace(info.ApiKey) 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 { if err != nil {
return err return err
} }
@@ -178,13 +199,8 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel
req.Set("originator", "codex_cli_rs") 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") 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") req.Set("Accept", "application/json")
} }




+ 17
- 4
relay/compatible_handler.go Просмотреть файл

@@ -27,6 +27,22 @@ import (
"github.com/gin-gonic/gin" "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) { func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types.NewAPIError) {
info.InitChannelMeta(c) info.InitChannelMeta(c)


@@ -76,10 +92,7 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
adaptor.Init(info) adaptor.Init(info)


passThroughGlobal := model_setting.GetGlobalSettings().PassThroughRequestEnabled 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) applySystemPromptIfNeeded(c, info, request)
usage, newApiErr := chatCompletionsViaResponses(c, info, adaptor, request) usage, newApiErr := chatCompletionsViaResponses(c, info, adaptor, request)
if newApiErr != nil { if newApiErr != nil {


+ 16
- 3
web/src/components/table/channels/modals/EditChannelModal.jsx Просмотреть файл

@@ -548,6 +548,9 @@ const EditChannelModal = (props) => {


if (value === 57) { if (value === 57) {
setCodexCredentialMode(CODEX_CREDENTIAL_MODE.API_KEY); setCodexCredentialMode(CODEX_CREDENTIAL_MODE.API_KEY);
if (formApiRef.current) {
formApiRef.current.setValue('codex_credential_mode', CODEX_CREDENTIAL_MODE.API_KEY);
}
setBatch(false); setBatch(false);
setMultiToSingle(false); setMultiToSingle(false);
setMultiKeyMode('random'); setMultiKeyMode('random');
@@ -703,7 +706,9 @@ const EditChannelModal = (props) => {
} }


if (data.type === 57) { if (data.type === 57) {
setCodexCredentialMode(detectCodexCredentialMode(data.key));
const mode = detectCodexCredentialMode(data.key);
setCodexCredentialMode(mode);
data.codex_credential_mode = mode;
} else { } else {
setCodexCredentialMode(CODEX_CREDENTIAL_MODE.API_KEY); setCodexCredentialMode(CODEX_CREDENTIAL_MODE.API_KEY);
} }
@@ -942,6 +947,9 @@ const EditChannelModal = (props) => {


const handleCodexOAuthGenerated = (key) => { const handleCodexOAuthGenerated = (key) => {
setCodexCredentialMode(CODEX_CREDENTIAL_MODE.OAUTH); setCodexCredentialMode(CODEX_CREDENTIAL_MODE.OAUTH);
if (formApiRef.current) {
formApiRef.current.setValue('codex_credential_mode', CODEX_CREDENTIAL_MODE.OAUTH);
}
handleInputChange('key', key); handleInputChange('key', key);
formatJsonField('key'); formatJsonField('key');
}; };
@@ -1098,6 +1106,7 @@ const EditChannelModal = (props) => {
// 清空表单中的key_mode字段 // 清空表单中的key_mode字段
if (formApiRef.current) { if (formApiRef.current) {
formApiRef.current.setValue('key_mode', undefined); formApiRef.current.setValue('key_mode', undefined);
formApiRef.current.setValue('codex_credential_mode', CODEX_CREDENTIAL_MODE.API_KEY);
} }
// 重置本地输入,避免下次打开残留上一次的 JSON 字段值 // 重置本地输入,避免下次打开残留上一次的 JSON 字段值
setInputs(getInitValues()); setInputs(getInitValues());
@@ -2175,8 +2184,12 @@ const EditChannelModal = (props) => {
value: CODEX_CREDENTIAL_MODE.OAUTH, 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%' }} style={{ width: '100%' }}
extraText={ extraText={
isCodexOAuthMode isCodexOAuthMode


Загрузка…
Отмена
Сохранить