mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 13:18:18 +08:00
Merge pull request #5760 from spongehah/feature/unify-codex-outbound-identity
fix(Fingerprint): 将 Codex 非推理出站身份统一到推理解析链
This commit is contained in:
@@ -46,9 +46,11 @@ func (s *openaiOAuthService) ExchangeCode(ctx context.Context, code, codeVerifie
|
||||
|
||||
var tokenResp openai.TokenResponse
|
||||
|
||||
authUA, authOriginator := service.CodexCanonicalAuthIdentity()
|
||||
resp, err := client.R().
|
||||
SetContext(ctx).
|
||||
SetHeader("User-Agent", "codex-cli/0.91.0").
|
||||
SetHeader("User-Agent", authUA).
|
||||
SetHeader("originator", authOriginator).
|
||||
SetFormDataFromValues(formData).
|
||||
SetSuccessResult(&tokenResp).
|
||||
Post(s.tokenURL)
|
||||
@@ -94,9 +96,11 @@ func (s *openaiOAuthService) refreshTokenWithClientID(ctx context.Context, refre
|
||||
|
||||
var tokenResp openai.TokenResponse
|
||||
|
||||
authUA, authOriginator := service.CodexCanonicalAuthIdentity()
|
||||
resp, err := client.R().
|
||||
SetContext(ctx).
|
||||
SetHeader("User-Agent", "codex-cli/0.91.0").
|
||||
SetHeader("User-Agent", authUA).
|
||||
SetHeader("originator", authOriginator).
|
||||
SetFormDataFromValues(formData).
|
||||
SetSuccessResult(&tokenResp).
|
||||
Post(s.tokenURL)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@@ -77,6 +78,17 @@ func (s *OpenAIOAuthServiceSuite) TestExchangeCode_DefaultRedirectURI() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
wantUA, wantOriginator := service.CodexCanonicalAuthIdentity()
|
||||
if got := r.Header.Get("User-Agent"); got != wantUA {
|
||||
errCh <- "user-agent mismatch"
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if got := r.Header.Get("originator"); got != wantOriginator {
|
||||
errCh <- "originator mismatch"
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, `{"access_token":"at","refresh_token":"rt","token_type":"bearer","expires_in":3600}`)
|
||||
@@ -121,6 +133,17 @@ func (s *OpenAIOAuthServiceSuite) TestRefreshToken_FormFields() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
wantUA, wantOriginator := service.CodexCanonicalAuthIdentity()
|
||||
if got := r.Header.Get("User-Agent"); got != wantUA {
|
||||
errCh <- "user-agent mismatch"
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if got := r.Header.Get("originator"); got != wantOriginator {
|
||||
errCh <- "originator mismatch"
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, `{"access_token":"at2","refresh_token":"rt2","token_type":"bearer","expires_in":3600}`)
|
||||
|
||||
@@ -731,11 +731,12 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account
|
||||
req.Host = "chatgpt.com"
|
||||
req.Header.Set("accept", "text/event-stream")
|
||||
req.Header.Set("OpenAI-Beta", "responses=experimental")
|
||||
req.Header.Set("Originator", openai.CodexDefaultOriginator)
|
||||
canonical := resolveCodexOutboundIdentity("")
|
||||
req.Header.Set("Originator", canonical.originator)
|
||||
if customUA := strings.TrimSpace(credentialAccount.GetOpenAIUserAgent()); customUA != "" {
|
||||
req.Header.Set("User-Agent", customUA)
|
||||
} else {
|
||||
req.Header.Set("User-Agent", codexCLIUserAgent)
|
||||
req.Header.Set("User-Agent", canonical.userAgent)
|
||||
}
|
||||
setOpenAIChatGPTAccountHeaders(req.Header, credentialAccount)
|
||||
// 与真实转发一致:账号级自定义 UA 同样作为管理员显式配置传入,否则测试用的身份
|
||||
@@ -2063,6 +2064,9 @@ func (s *AccountTestService) testOpenAICompactConnection(c *gin.Context, account
|
||||
req.Header.Set("Authorization", "Bearer "+authToken)
|
||||
}
|
||||
applyOpenAICodexProbeHeaders(req.Header)
|
||||
if isOAuth {
|
||||
enforceCodexIdentityHeadersWithUA(req.Header, credentialAccount.GetOpenAIUserAgent())
|
||||
}
|
||||
probeSessionID := compactProbeSessionID(account.ID)
|
||||
req.Header.Set("Session_ID", probeSessionID)
|
||||
req.Header.Set("Conversation_ID", probeSessionID)
|
||||
@@ -2968,11 +2972,12 @@ func (s *AccountTestService) testOpenAIImageOAuth(c *gin.Context, ctx context.Co
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "text/event-stream")
|
||||
req.Header.Set("OpenAI-Beta", "responses=experimental")
|
||||
req.Header.Set("originator", openai.CodexDefaultOriginator)
|
||||
canonical := resolveCodexOutboundIdentity("")
|
||||
req.Header.Set("originator", canonical.originator)
|
||||
if customUA := strings.TrimSpace(credentialAccount.GetOpenAIUserAgent()); customUA != "" {
|
||||
req.Header.Set("User-Agent", customUA)
|
||||
} else {
|
||||
req.Header.Set("User-Agent", codexCLIUserAgent)
|
||||
req.Header.Set("User-Agent", canonical.userAgent)
|
||||
}
|
||||
setOpenAIChatGPTAccountHeaders(req.Header, credentialAccount)
|
||||
// 与真实转发一致:账号级自定义 UA 同样作为管理员显式配置传入,否则测试用的身份
|
||||
|
||||
@@ -105,15 +105,14 @@ type antigravityUsageCache struct {
|
||||
}
|
||||
|
||||
const (
|
||||
apiCacheTTL = 3 * time.Minute
|
||||
apiErrorCacheTTL = 1 * time.Minute // 负缓存 TTL:429 等错误缓存 1 分钟
|
||||
antigravityErrorTTL = 1 * time.Minute // Antigravity 错误缓存 TTL(可恢复错误)
|
||||
apiQueryMaxJitter = 800 * time.Millisecond // 用量查询最大随机延迟
|
||||
windowStatsCacheTTL = 1 * time.Minute
|
||||
openAIProbeCacheTTL = 10 * time.Minute
|
||||
grokProbeRetryTTL = 1 * time.Minute
|
||||
grokFreeQuotaWindow = 24 * time.Hour
|
||||
openAICodexProbeVersion = codexCLIVersion // 与网关出站身份同源,避免两处硬编码版本各自漂移
|
||||
apiCacheTTL = 3 * time.Minute
|
||||
apiErrorCacheTTL = 1 * time.Minute // 负缓存 TTL:429 等错误缓存 1 分钟
|
||||
antigravityErrorTTL = 1 * time.Minute // Antigravity 错误缓存 TTL(可恢复错误)
|
||||
apiQueryMaxJitter = 800 * time.Millisecond // 用量查询最大随机延迟
|
||||
windowStatsCacheTTL = 1 * time.Minute
|
||||
openAIProbeCacheTTL = 10 * time.Minute
|
||||
grokProbeRetryTTL = 1 * time.Minute
|
||||
grokFreeQuotaWindow = 24 * time.Hour
|
||||
)
|
||||
|
||||
// UsageCache 封装账户使用量相关的缓存
|
||||
@@ -860,9 +859,10 @@ func (s *AccountUsageService) probeOpenAICodexSnapshot(ctx context.Context, acco
|
||||
}
|
||||
req.Header.Set("Accept", "text/event-stream")
|
||||
req.Header.Set("OpenAI-Beta", "responses=experimental")
|
||||
req.Header.Set("Originator", openaipkg.CodexDefaultOriginator)
|
||||
req.Header.Set("Version", openAICodexProbeVersion)
|
||||
req.Header.Set("User-Agent", codexCLIUserAgent)
|
||||
canonical := resolveCodexOutboundIdentity("")
|
||||
req.Header.Set("Originator", canonical.originator)
|
||||
req.Header.Set("Version", canonical.version)
|
||||
req.Header.Set("User-Agent", canonical.userAgent)
|
||||
if s.identityCache != nil {
|
||||
if fp, fpErr := s.identityCache.GetFingerprint(reqCtx, account.ID); fpErr == nil && fp != nil && strings.TrimSpace(fp.UserAgent) != "" {
|
||||
req.Header.Set("User-Agent", strings.TrimSpace(fp.UserAgent))
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
@@ -239,25 +238,26 @@ func (s *OpenAIGatewayService) buildOpenAIAlphaSearchResponsesWebSearchRequest(c
|
||||
if turnMetadata := openAIAlphaSearchInboundHeader(c, "X-Codex-Turn-Metadata"); turnMetadata != "" {
|
||||
req.Header.Set("X-Codex-Turn-Metadata", turnMetadata)
|
||||
}
|
||||
canonical := resolveCodexOutboundIdentity("")
|
||||
if version := openAIAlphaSearchInboundHeader(c, "Version"); version != "" {
|
||||
req.Header.Set("Version", version)
|
||||
} else {
|
||||
req.Header.Set("Version", codexCLIVersion)
|
||||
req.Header.Set("Version", canonical.version)
|
||||
}
|
||||
if originator := openAIAlphaSearchInboundHeader(c, "Originator"); originator != "" {
|
||||
req.Header.Set("Originator", originator)
|
||||
} else {
|
||||
req.Header.Set("Originator", openai.CodexDefaultOriginator)
|
||||
req.Header.Set("Originator", canonical.originator)
|
||||
}
|
||||
if customUA := account.GetOpenAIUserAgent(); customUA != "" {
|
||||
req.Header.Set("User-Agent", customUA)
|
||||
} else if userAgent := openAIAlphaSearchInboundHeader(c, "User-Agent"); userAgent != "" {
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
} else {
|
||||
req.Header.Set("User-Agent", codexCLIUserAgent)
|
||||
req.Header.Set("User-Agent", canonical.userAgent)
|
||||
}
|
||||
if s.cfg != nil && s.cfg.Gateway.ForceCodexCLI {
|
||||
req.Header.Set("User-Agent", codexCLIUserAgent)
|
||||
req.Header.Set("User-Agent", canonical.userAgent)
|
||||
}
|
||||
apiKeyID := getAPIKeyIDFromContext(c)
|
||||
if sessionID := strings.TrimSpace(gjson.GetBytes(alphaBody, "id").String()); sessionID != "" {
|
||||
@@ -382,25 +382,26 @@ func (s *OpenAIGatewayService) buildOpenAIAlphaSearchRequest(ctx context.Context
|
||||
if turnMetadata := openAIAlphaSearchInboundHeader(c, "X-Codex-Turn-Metadata"); turnMetadata != "" {
|
||||
req.Header.Set("X-Codex-Turn-Metadata", turnMetadata)
|
||||
}
|
||||
canonical := resolveCodexOutboundIdentity("")
|
||||
if version := openAIAlphaSearchInboundHeader(c, "Version"); version != "" {
|
||||
req.Header.Set("Version", version)
|
||||
} else {
|
||||
req.Header.Set("Version", codexCLIVersion)
|
||||
req.Header.Set("Version", canonical.version)
|
||||
}
|
||||
if originator := openAIAlphaSearchInboundHeader(c, "Originator"); originator != "" {
|
||||
req.Header.Set("Originator", originator)
|
||||
} else {
|
||||
req.Header.Set("Originator", openai.CodexDefaultOriginator)
|
||||
req.Header.Set("Originator", canonical.originator)
|
||||
}
|
||||
if customUA := account.GetOpenAIUserAgent(); customUA != "" {
|
||||
req.Header.Set("User-Agent", customUA)
|
||||
} else if userAgent := openAIAlphaSearchInboundHeader(c, "User-Agent"); userAgent != "" {
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
} else {
|
||||
req.Header.Set("User-Agent", codexCLIUserAgent)
|
||||
req.Header.Set("User-Agent", canonical.userAgent)
|
||||
}
|
||||
if s.cfg != nil && s.cfg.Gateway.ForceCodexCLI {
|
||||
req.Header.Set("User-Agent", codexCLIUserAgent)
|
||||
req.Header.Set("User-Agent", canonical.userAgent)
|
||||
}
|
||||
enforceCodexIdentityHeadersWithUA(req.Header, s.codexIdentityOverrideUA(account))
|
||||
}
|
||||
|
||||
@@ -250,7 +250,6 @@ func TestCodexOutboundVersionHasSingleSource(t *testing.T) {
|
||||
strings.HasPrefix(codexCLIUserAgent, openai.CodexDefaultOriginator+"/"+codexCLIVersion+" "),
|
||||
"codexCLIUserAgent=%q 必须以 codexCLIVersion=%q 作为版本段", codexCLIUserAgent, codexCLIVersion,
|
||||
)
|
||||
require.Equal(t, codexCLIVersion, openAICodexProbeVersion)
|
||||
require.GreaterOrEqual(t, CompareVersions(codexCLIVersion, codexUpstreamMinVersion), 0,
|
||||
"codexCLIVersion=%q 不得低于上游最低门槛 %q", codexCLIVersion, codexUpstreamMinVersion,
|
||||
)
|
||||
|
||||
@@ -76,6 +76,38 @@ func SetCodexCanonicalUserAgentResolver(resolver func() string) {
|
||||
codexCanonicalUAResolver = resolver
|
||||
}
|
||||
|
||||
// CodexCanonicalUserAgent 返回当前生效的规范 Codex User-Agent。
|
||||
// 取值走与推理相同的解析链:面板 UA 指纹 + 面板/自动同步版本号 + 编译期兜底。
|
||||
// 供无账号句柄的出站路径(OAuth 换 Token / 刷新)使用。
|
||||
func CodexCanonicalUserAgent() string {
|
||||
return resolveCodexOutboundIdentity("").userAgent
|
||||
}
|
||||
|
||||
// CodexCanonicalAuthIdentity 返回凭据面(auth.openai.com:换 Token / 刷新 / whoami)
|
||||
// 出站请求的身份对:规范 User-Agent 与配套 originator,与推理解析链同源。
|
||||
// 凭据面不发 version 头——真实 Codex 客户端在该面只携带 originator 与 User-Agent
|
||||
// (codex-rs login/default_client.rs 的 default_headers()),version 门槛
|
||||
// (issue #3901)只存在于 /backend-api/codex 推理面。
|
||||
func CodexCanonicalAuthIdentity() (userAgent, originator string) {
|
||||
identity := resolveCodexOutboundIdentity("")
|
||||
return identity.userAgent, identity.originator
|
||||
}
|
||||
|
||||
// ApplyCodexCanonicalAuthIdentity 为凭据面出站请求写入身份对(不含 version)。
|
||||
func ApplyCodexCanonicalAuthIdentity(h http.Header) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
userAgent, originator := CodexCanonicalAuthIdentity()
|
||||
h.Set("user-agent", userAgent)
|
||||
h.Set("originator", originator)
|
||||
}
|
||||
|
||||
// CodexCanonicalClientVersion 返回当前生效的 Codex 客户端版本号。
|
||||
func CodexCanonicalClientVersion() string {
|
||||
return resolveCodexOutboundIdentity("").version
|
||||
}
|
||||
|
||||
// codexCanonicalUserAgent 返回出站规范 User-Agent。
|
||||
func codexCanonicalUserAgent() string {
|
||||
codexCanonicalUAMu.RLock()
|
||||
@@ -210,6 +242,6 @@ func pairCodexIdentityHeaders(h http.Header) {
|
||||
h.Set("user-agent", pairedUA)
|
||||
h.Set("originator", originator)
|
||||
if v := strings.TrimSpace(h.Get("version")); v != "" && CompareVersions(v, codexUpstreamMinVersion) < 0 {
|
||||
h.Set("version", codexCLIVersion)
|
||||
h.Set("version", resolveCodexOutboundIdentity("").version)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,3 +322,27 @@ func TestBuildCodexCLIUserAgent(t *testing.T) {
|
||||
require.Equal(t, codexCLIUserAgent, buildCodexCLIUserAgent("bogus version"))
|
||||
require.Equal(t, codexCLIUserAgent, buildCodexCLIUserAgent(""))
|
||||
}
|
||||
|
||||
func TestCodexCanonicalUserAgentFollowsResolver(t *testing.T) {
|
||||
SetCodexCanonicalUserAgentResolver(func() string {
|
||||
return "codex_cli_rs/0.200.1" + codexCLIUserAgentSuffix
|
||||
})
|
||||
t.Cleanup(func() { SetCodexCanonicalUserAgentResolver(nil) })
|
||||
|
||||
require.Equal(t, "codex_cli_rs/0.200.1"+codexCLIUserAgentSuffix, CodexCanonicalUserAgent())
|
||||
require.Equal(t, "0.200.1", CodexCanonicalClientVersion())
|
||||
|
||||
h := make(http.Header)
|
||||
ApplyCodexCanonicalAuthIdentity(h)
|
||||
require.Equal(t, "codex_cli_rs", h.Get("originator"))
|
||||
require.Equal(t, "codex_cli_rs/0.200.1"+codexCLIUserAgentSuffix, h.Get("user-agent"))
|
||||
// 凭据面不发 version 头(真实客户端在 auth.openai.com 只带 originator + UA)。
|
||||
require.Empty(t, h.Get("version"))
|
||||
}
|
||||
|
||||
func TestCodexCanonicalUserAgentFallsBackWithoutResolver(t *testing.T) {
|
||||
SetCodexCanonicalUserAgentResolver(nil)
|
||||
|
||||
require.Equal(t, codexCLIUserAgent, CodexCanonicalUserAgent())
|
||||
require.Equal(t, codexCLIVersion, CodexCanonicalClientVersion())
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
@@ -243,7 +242,7 @@ func (s *OpenAIGatewayService) FetchCodexModelsManifest(ctx context.Context, acc
|
||||
|
||||
clientVersion = strings.TrimSpace(clientVersion)
|
||||
if clientVersion == "" {
|
||||
clientVersion = openAICodexProbeVersion
|
||||
clientVersion = CodexCanonicalClientVersion()
|
||||
}
|
||||
|
||||
requestEndpoint := chatgptCodexModelsURL
|
||||
@@ -305,9 +304,22 @@ func (s *OpenAIGatewayService) FetchCodexModelsManifest(ctx context.Context, acc
|
||||
setOpenAIChatGPTAccountHeaders(headers, credAccount)
|
||||
}
|
||||
headers.Set("Accept", "application/json")
|
||||
headers.Set("Originator", openai.CodexDefaultOriginator)
|
||||
headers.Set("Version", clientVersion)
|
||||
headers.Set("User-Agent", codexCLIUserAgent)
|
||||
overrideUA := ""
|
||||
if !useAPIKeyUpstream {
|
||||
overrideUA = credAccount.GetOpenAIUserAgent()
|
||||
}
|
||||
identity := resolveCodexOutboundIdentity(overrideUA)
|
||||
headers.Set("Originator", identity.originator)
|
||||
headers.Set("User-Agent", identity.userAgent)
|
||||
// Version 头优先与 client_version 查询参数同源:客户端自报版本合法且不低于上游
|
||||
// 门槛时原样使用;否则回退规范版本,避免陈旧 version 触发上游 404(issue #3901)。
|
||||
// client_version 查询参数本身始终按客户端原值透传(内容协商语义,契约见
|
||||
// TestFetchCodexModelsManifestPassthrough)。
|
||||
headerVersion := NormalizeCodexClientVersion(clientVersion)
|
||||
if headerVersion == "" || CompareVersions(headerVersion, codexUpstreamMinVersion) < 0 {
|
||||
headerVersion = identity.version
|
||||
}
|
||||
headers.Set("Version", headerVersion)
|
||||
|
||||
proxyURL := ""
|
||||
if account.ProxyID != nil && account.Proxy != nil {
|
||||
|
||||
@@ -348,8 +348,8 @@ func TestFetchCodexModelsManifestDefaultClientVersion(t *testing.T) {
|
||||
if _, err := s.FetchCodexModelsManifest(context.Background(), newCodexModelsTestAccount(), "", ""); err != nil {
|
||||
t.Fatalf("FetchCodexModelsManifest returned error: %v", err)
|
||||
}
|
||||
if gotClientVersion != openAICodexProbeVersion {
|
||||
t.Errorf("default client_version: got %q, want %q", gotClientVersion, openAICodexProbeVersion)
|
||||
if gotClientVersion != CodexCanonicalClientVersion() {
|
||||
t.Errorf("default client_version: got %q, want %q", gotClientVersion, CodexCanonicalClientVersion())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,9 +452,9 @@ func TestFetchCodexModelsManifestAPIKeyCustomUpstream(t *testing.T) {
|
||||
t.Errorf("originator header: got %q", gotRequest.Header.Get("Originator"))
|
||||
}
|
||||
if gotRequest.Header.Get("Version") != "0.144.0" {
|
||||
t.Errorf("version header: got %q", gotRequest.Header.Get("Version"))
|
||||
t.Errorf("version header must match the client_version query param: got %q", gotRequest.Header.Get("Version"))
|
||||
}
|
||||
if gotRequest.Header.Get("User-Agent") != codexCLIUserAgent {
|
||||
if gotRequest.Header.Get("User-Agent") != CodexCanonicalUserAgent() {
|
||||
t.Errorf("user-agent header: got %q", gotRequest.Header.Get("User-Agent"))
|
||||
}
|
||||
if gotRequest.Header.Get("chatgpt-account-id") != "" {
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
)
|
||||
|
||||
const openAICodexPATWhoamiURLDefault = "https://auth.openai.com/api/accounts/v1/user-auth-credential/whoami"
|
||||
@@ -59,8 +58,7 @@ func (s *OpenAIOAuthService) ValidateCodexPersonalAccessToken(ctx context.Contex
|
||||
}
|
||||
req.Header.Set("authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("accept", "application/json")
|
||||
req.Header.Set("originator", openai.CodexDefaultOriginator)
|
||||
req.Header.Set("user-agent", codexCLIUserAgent)
|
||||
ApplyCodexCanonicalAuthIdentity(req.Header)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -40,7 +40,7 @@ func TestOpenAIOAuthService_ValidateCodexPersonalAccessToken(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Bearer at-test-token", gotAuthorization)
|
||||
require.Equal(t, openai.CodexDefaultOriginator, gotOriginator)
|
||||
require.Equal(t, codexCLIUserAgent, gotUserAgent)
|
||||
require.Equal(t, CodexCanonicalUserAgent(), gotUserAgent)
|
||||
require.Equal(t, OpenAIAuthModePersonalAccessToken, info.AuthMode)
|
||||
require.Equal(t, "user@example.com", info.Email)
|
||||
require.Equal(t, "user-123", info.ChatGPTUserID)
|
||||
|
||||
@@ -11,9 +11,6 @@ import (
|
||||
)
|
||||
|
||||
func TestCodexVersionConstants_Consistency(t *testing.T) {
|
||||
require.Equal(t, codexCLIVersion, openAICodexProbeVersion,
|
||||
"codexCLIVersion and openAICodexProbeVersion must stay in sync")
|
||||
|
||||
require.True(t, strings.Contains(codexCLIUserAgent, openai.CodexDefaultOriginator+"/"+codexCLIVersion),
|
||||
"codexCLIUserAgent must embed codexCLIVersion")
|
||||
|
||||
|
||||
@@ -1128,7 +1128,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
|
||||
if isOpenAIResponsesCompactPath(c) {
|
||||
req.Header.Set("accept", "application/json")
|
||||
if req.Header.Get("version") == "" {
|
||||
req.Header.Set("version", codexCLIVersion)
|
||||
req.Header.Set("version", CodexCanonicalClientVersion())
|
||||
}
|
||||
compactSession := resolveOpenAICompactSessionID(c)
|
||||
req.Header.Set("session_id", isolateOpenAISessionID(apiKeyID, compactSession))
|
||||
@@ -1154,10 +1154,10 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
|
||||
req.Header.Set("user-agent", customUA)
|
||||
}
|
||||
|
||||
// 若开启 ForceCodexCLI,则强制将上游 User-Agent 伪装为 Codex CLI。
|
||||
// 若开启 ForceCodexCLI,则强制将上游 User-Agent 伪装为规范 Codex 身份。
|
||||
// 用于网关未透传/改写 User-Agent 时,仍能命中 Codex 侧识别逻辑。
|
||||
if s.cfg != nil && s.cfg.Gateway.ForceCodexCLI {
|
||||
req.Header.Set("user-agent", codexCLIUserAgent)
|
||||
req.Header.Set("user-agent", CodexCanonicalUserAgent())
|
||||
}
|
||||
|
||||
// 指纹收敛:使用 Forward() 中预计算的收敛 ID 改写出站头,与请求体使用同一份 IDs。
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
||||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||||
"github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tidwall/gjson"
|
||||
@@ -445,7 +444,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
|
||||
if isOpenAIResponsesCompactPath(c) {
|
||||
req.Header.Set("accept", "application/json")
|
||||
if req.Header.Get("version") == "" {
|
||||
req.Header.Set("version", codexCLIVersion)
|
||||
req.Header.Set("version", CodexCanonicalClientVersion())
|
||||
}
|
||||
if clientSessionID == "" {
|
||||
clientSessionID = resolveOpenAICompactSessionID(c)
|
||||
@@ -454,7 +453,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
|
||||
req.Header.Set("accept", "text/event-stream")
|
||||
}
|
||||
if req.Header.Get("originator") == "" {
|
||||
req.Header.Set("originator", openai.CodexDefaultOriginator)
|
||||
req.Header.Set("originator", resolveCodexOutboundIdentity("").originator)
|
||||
}
|
||||
// 用隔离后的 session 标识符覆盖客户端透传值,防止跨用户会话碰撞。
|
||||
if clientSessionID == "" {
|
||||
@@ -482,7 +481,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough(
|
||||
req.Header.Set("user-agent", customUA)
|
||||
}
|
||||
if s.cfg != nil && s.cfg.Gateway.ForceCodexCLI {
|
||||
req.Header.Set("user-agent", codexCLIUserAgent)
|
||||
req.Header.Set("user-agent", CodexCanonicalUserAgent())
|
||||
}
|
||||
// 指纹收敛:使用 forwardOpenAIPassthrough 中预计算的收敛 ID 改写出站头,
|
||||
// 与请求体 client_metadata 共享同一份 IDs(与非透传路径相同的相对位置:
|
||||
|
||||
@@ -161,7 +161,7 @@ func (s *OpenAIGatewayService) buildOpenAIWSHeaders(
|
||||
}
|
||||
}
|
||||
if s != nil && s.cfg != nil && s.cfg.Gateway.ForceCodexCLI {
|
||||
headers.Set("user-agent", codexCLIUserAgent)
|
||||
headers.Set("user-agent", CodexCanonicalUserAgent())
|
||||
}
|
||||
// 终态收口:WS 握手与 HTTP 出站共用同一套身份语义,账号级自定义 UA 同样作为
|
||||
// 管理员显式配置传入(上面写进 headers 的值只在强制统一被关闭时才参与配对)。
|
||||
|
||||
Reference in New Issue
Block a user