feat(openai): 支持自动透传开关并透传 User-Agent
- OpenAI OAuth/API Key 统一支持自动透传开关,编辑页可开关\n- 透传模式仅替换认证并保留计费/并发/审计,修复 API Key responses 端点拼接\n- Usage 页面显示原始 User-Agent 且不截断,补充回归测试与清单
This commit is contained in:
@@ -747,11 +747,11 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
||||
originalModel := reqModel
|
||||
|
||||
isCodexCLI := openai.IsCodexCLIRequest(c.GetHeader("User-Agent")) || (s.cfg != nil && s.cfg.Gateway.ForceCodexCLI)
|
||||
passthroughEnabled := account.Type == AccountTypeOAuth && account.IsOpenAIOAuthPassthroughEnabled() && isCodexCLI
|
||||
passthroughEnabled := account.IsOpenAIPassthroughEnabled()
|
||||
if passthroughEnabled {
|
||||
// 透传分支只需要轻量提取字段,避免热路径全量 Unmarshal。
|
||||
reasoningEffort := extractOpenAIReasoningEffortFromBody(body, reqModel)
|
||||
return s.forwardOAuthPassthrough(ctx, c, account, originalBody, reqModel, reasoningEffort, reqStream, startTime)
|
||||
return s.forwardOpenAIPassthrough(ctx, c, account, originalBody, reqModel, reasoningEffort, reqStream, startTime)
|
||||
}
|
||||
|
||||
reqBody, err := getOpenAIRequestBodyMap(c, body)
|
||||
@@ -775,6 +775,14 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
||||
// Track if body needs re-serialization
|
||||
bodyModified := false
|
||||
|
||||
// 非透传模式下,保持历史行为:非 Codex CLI 请求在 instructions 为空时注入默认指令。
|
||||
if !isCodexCLI && isInstructionsEmpty(reqBody) {
|
||||
if instructions := strings.TrimSpace(GetOpenCodeInstructions()); instructions != "" {
|
||||
reqBody["instructions"] = instructions
|
||||
bodyModified = true
|
||||
}
|
||||
}
|
||||
|
||||
// 对所有请求执行模型映射(包含 Codex CLI)。
|
||||
mappedModel := account.GetMappedModel(reqModel)
|
||||
if mappedModel != reqModel {
|
||||
@@ -994,7 +1002,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) forwardOAuthPassthrough(
|
||||
func (s *OpenAIGatewayService) forwardOpenAIPassthrough(
|
||||
ctx context.Context,
|
||||
c *gin.Context,
|
||||
account *Account,
|
||||
@@ -1012,7 +1020,7 @@ func (s *OpenAIGatewayService) forwardOAuthPassthrough(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upstreamReq, err := s.buildUpstreamRequestOAuthPassthrough(ctx, c, account, body, token)
|
||||
upstreamReq, err := s.buildUpstreamRequestOpenAIPassthrough(ctx, c, account, body, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1092,14 +1100,29 @@ func (s *OpenAIGatewayService) forwardOAuthPassthrough(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) buildUpstreamRequestOAuthPassthrough(
|
||||
func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
|
||||
ctx context.Context,
|
||||
c *gin.Context,
|
||||
account *Account,
|
||||
body []byte,
|
||||
token string,
|
||||
) (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, chatgptCodexURL, bytes.NewReader(body))
|
||||
targetURL := openaiPlatformAPIURL
|
||||
switch account.Type {
|
||||
case AccountTypeOAuth:
|
||||
targetURL = chatgptCodexURL
|
||||
case AccountTypeAPIKey:
|
||||
baseURL := account.GetOpenAIBaseURL()
|
||||
if baseURL != "" {
|
||||
validatedURL, err := s.validateUpstreamBaseURL(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetURL = buildOpenAIResponsesURL(validatedURL)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, targetURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1123,16 +1146,18 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOAuthPassthrough(
|
||||
req.Header.Del("x-goog-api-key")
|
||||
req.Header.Set("authorization", "Bearer "+token)
|
||||
|
||||
// ChatGPT internal Codex API 必要头
|
||||
req.Host = "chatgpt.com"
|
||||
if chatgptAccountID := account.GetChatGPTAccountID(); chatgptAccountID != "" {
|
||||
req.Header.Set("chatgpt-account-id", chatgptAccountID)
|
||||
}
|
||||
if req.Header.Get("OpenAI-Beta") == "" {
|
||||
req.Header.Set("OpenAI-Beta", "responses=experimental")
|
||||
}
|
||||
if req.Header.Get("originator") == "" {
|
||||
req.Header.Set("originator", "codex_cli_rs")
|
||||
// OAuth 透传到 ChatGPT internal API 时补齐必要头。
|
||||
if account.Type == AccountTypeOAuth {
|
||||
req.Host = "chatgpt.com"
|
||||
if chatgptAccountID := account.GetChatGPTAccountID(); chatgptAccountID != "" {
|
||||
req.Header.Set("chatgpt-account-id", chatgptAccountID)
|
||||
}
|
||||
if req.Header.Get("OpenAI-Beta") == "" {
|
||||
req.Header.Set("OpenAI-Beta", "responses=experimental")
|
||||
}
|
||||
if req.Header.Get("originator") == "" {
|
||||
req.Header.Set("originator", "codex_cli_rs")
|
||||
}
|
||||
}
|
||||
|
||||
if req.Header.Get("content-type") == "" {
|
||||
@@ -1389,7 +1414,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
targetURL = validatedURL + "/responses"
|
||||
targetURL = buildOpenAIResponsesURL(validatedURL)
|
||||
}
|
||||
default:
|
||||
targetURL = openaiPlatformAPIURL
|
||||
@@ -2084,6 +2109,21 @@ func (s *OpenAIGatewayService) validateUpstreamBaseURL(raw string) (string, erro
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
// buildOpenAIResponsesURL 组装 OpenAI Responses 端点。
|
||||
// - base 以 /v1 结尾:追加 /responses
|
||||
// - base 已是 /responses:原样返回
|
||||
// - 其他情况:追加 /v1/responses
|
||||
func buildOpenAIResponsesURL(base string) string {
|
||||
normalized := strings.TrimRight(strings.TrimSpace(base), "/")
|
||||
if strings.HasSuffix(normalized, "/responses") {
|
||||
return normalized
|
||||
}
|
||||
if strings.HasSuffix(normalized, "/v1") {
|
||||
return normalized + "/responses"
|
||||
}
|
||||
return normalized + "/v1/responses"
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) replaceModelInResponseBody(body []byte, fromModel, toModel string) []byte {
|
||||
// 使用 gjson/sjson 精确替换 model 字段,避免全量 JSON 反序列化
|
||||
if m := gjson.GetBytes(body, "model"); m.Exists() && m.Str == fromModel {
|
||||
|
||||
Reference in New Issue
Block a user