mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 12:57:57 +08:00
fix(fingerprint): align credential-face identity with the real client and de-drift models version
- Replace ApplyCodexCanonicalIdentity with CodexCanonicalAuthIdentity / ApplyCodexCanonicalAuthIdentity: the credential face (auth.openai.com token exchange / refresh / PAT whoami) now sends the originator + canonical User-Agent pair and no version header, matching codex-rs default_headers(); the version gate (#3901) only exists on the /backend-api/codex inference face. whoami keeps its original header shape (originator + UA) with the canonical UA source. - Token exchange and refresh send the full pair instead of a bare UA, eliminating the half-identity (UA without originator) combination no real client ever emits. - Codex models manifest: the Version header now follows the client's own client_version when it is valid and >= the upstream floor (same source as the query param, restoring the pre-refactor consistency), falling back to the canonical version otherwise; the query param keeps its verbatim passthrough contract. - Drop the now-unreferenced openAICodexProbeVersion constant and its vacuous consistency assertions; probes resolve their version through resolveCodexOutboundIdentity at runtime.
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", service.CodexCanonicalUserAgent()).
|
||||
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", service.CodexCanonicalUserAgent()).
|
||||
SetHeader("User-Agent", authUA).
|
||||
SetHeader("originator", authOriginator).
|
||||
SetFormDataFromValues(formData).
|
||||
SetSuccessResult(&tokenResp).
|
||||
Post(s.tokenURL)
|
||||
|
||||
@@ -78,11 +78,17 @@ func (s *OpenAIOAuthServiceSuite) TestExchangeCode_DefaultRedirectURI() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if got := r.Header.Get("User-Agent"); got != service.CodexCanonicalUserAgent() {
|
||||
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}`)
|
||||
@@ -127,11 +133,17 @@ func (s *OpenAIOAuthServiceSuite) TestRefreshToken_FormFields() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if got := r.Header.Get("User-Agent"); got != service.CodexCanonicalUserAgent() {
|
||||
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}`)
|
||||
|
||||
@@ -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 // 编译期兜底;运行时探针 version 走 CodexCanonicalClientVersion
|
||||
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 封装账户使用量相关的缓存
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -83,15 +83,24 @@ func CodexCanonicalUserAgent() string {
|
||||
return resolveCodexOutboundIdentity("").userAgent
|
||||
}
|
||||
|
||||
// ApplyCodexCanonicalIdentity 为无账号句柄的出站请求写入与推理同源的身份三元组。
|
||||
func ApplyCodexCanonicalIdentity(h http.Header) {
|
||||
// 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
|
||||
}
|
||||
identity := resolveCodexOutboundIdentity("")
|
||||
h.Set("user-agent", identity.userAgent)
|
||||
h.Set("originator", identity.originator)
|
||||
h.Set("version", identity.version)
|
||||
userAgent, originator := CodexCanonicalAuthIdentity()
|
||||
h.Set("user-agent", userAgent)
|
||||
h.Set("originator", originator)
|
||||
}
|
||||
|
||||
// CodexCanonicalClientVersion 返回当前生效的 Codex 客户端版本号。
|
||||
|
||||
@@ -333,10 +333,11 @@ func TestCodexCanonicalUserAgentFollowsResolver(t *testing.T) {
|
||||
require.Equal(t, "0.200.1", CodexCanonicalClientVersion())
|
||||
|
||||
h := make(http.Header)
|
||||
ApplyCodexCanonicalIdentity(h)
|
||||
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"))
|
||||
require.Equal(t, "0.200.1", h.Get("version"))
|
||||
// 凭据面不发 version 头(真实客户端在 auth.openai.com 只带 originator + UA)。
|
||||
require.Empty(t, h.Get("version"))
|
||||
}
|
||||
|
||||
func TestCodexCanonicalUserAgentFallsBackWithoutResolver(t *testing.T) {
|
||||
|
||||
@@ -311,7 +311,15 @@ func (s *OpenAIGatewayService) FetchCodexModelsManifest(ctx context.Context, acc
|
||||
identity := resolveCodexOutboundIdentity(overrideUA)
|
||||
headers.Set("Originator", identity.originator)
|
||||
headers.Set("User-Agent", identity.userAgent)
|
||||
headers.Set("Version", identity.version)
|
||||
// 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 {
|
||||
|
||||
@@ -451,8 +451,8 @@ func TestFetchCodexModelsManifestAPIKeyCustomUpstream(t *testing.T) {
|
||||
if gotRequest.Header.Get("Originator") != openai.CodexDefaultOriginator {
|
||||
t.Errorf("originator header: got %q", gotRequest.Header.Get("Originator"))
|
||||
}
|
||||
if gotRequest.Header.Get("Version") != CodexCanonicalClientVersion() {
|
||||
t.Errorf("version header: got %q", gotRequest.Header.Get("Version"))
|
||||
if gotRequest.Header.Get("Version") != "0.144.0" {
|
||||
t.Errorf("version header must match the client_version query param: got %q", gotRequest.Header.Get("Version"))
|
||||
}
|
||||
if gotRequest.Header.Get("User-Agent") != CodexCanonicalUserAgent() {
|
||||
t.Errorf("user-agent header: got %q", gotRequest.Header.Get("User-Agent"))
|
||||
|
||||
@@ -58,7 +58,7 @@ func (s *OpenAIOAuthService) ValidateCodexPersonalAccessToken(ctx context.Contex
|
||||
}
|
||||
req.Header.Set("authorization", "Bearer "+accessToken)
|
||||
req.Header.Set("accept", "application/json")
|
||||
ApplyCodexCanonicalIdentity(req.Header)
|
||||
ApplyCodexCanonicalAuthIdentity(req.Header)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user