From 8941ca723ccaeb83d1dfbd6336eeeec498f6fc56 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 4 Oct 2026 12:35:00 +0800 Subject: [PATCH] refactor(codex): one identity resolver per purpose, no unused context The Codex identity resolvers kept a context parameter and a "...Context" twin from the dispatch layer removed in #233. Fold each pair into one function: resolveCodexOutboundIdentityForAccount, enforceCodexIdentityHeadersForAccount and resolveCodexIdentitySnapshot. The one real failure is unchanged: an identity whose version cannot form a valid User-Agent still fails the send. The predictor that only reads the identity for the sandbox tag keeps ignoring that error explicitly. The five account-test messages that named a "policy" now say what failed: "Codex client identity is invalid: ...". Co-Authored-By: Claude Opus 5.5 --- .../codexruntime/profile/identity_test.go | 2 +- .../internal/service/account_test_service.go | 22 ++++++++-------- ...ccount_test_service_openai_compact_test.go | 4 ++- .../account_test_service_openai_image_test.go | 4 ++- .../internal/service/account_usage_service.go | 2 +- .../service/codex_outbound_contract.go | 2 +- .../internal/service/openai_alpha_search.go | 4 +-- .../openai_codex_account_identity_test.go | 4 ++- .../openai_codex_client_identity_test.go | 25 +++++++++++-------- .../internal/service/openai_codex_identity.go | 20 ++++----------- .../service/openai_codex_identity_snapshot.go | 7 +++--- .../service/openai_codex_models_service.go | 2 +- .../service/openai_compat_model_test.go | 20 ++++++++++----- .../service/openai_gateway_forward.go | 5 +++- .../service/openai_oauth_passthrough_test.go | 20 +++++++++++---- .../internal/service/openai_oauth_service.go | 2 +- .../service/openai_setup_token_compat_test.go | 6 +++-- backend/internal/service/upstream_models.go | 4 +-- 18 files changed, 90 insertions(+), 65 deletions(-) diff --git a/backend/internal/codexruntime/profile/identity_test.go b/backend/internal/codexruntime/profile/identity_test.go index 781cca914eff..26a845fd943b 100644 --- a/backend/internal/codexruntime/profile/identity_test.go +++ b/backend/internal/codexruntime/profile/identity_test.go @@ -8,7 +8,7 @@ import ( extensionv1 "github.com/Wei-Shaw/sub2api/internal/nativeapi" ) -func TestIdentityPolicyPreservesDeterminismAndProtocolPairing(t *testing.T) { +func TestIdentityProfilePreservesDeterminismAndProtocolPairing(t *testing.T) { first := deriveCodexClientIdentity("1c0a3d9e-58b2-4f8c-a2d1-7f3b9e6c4a55") if first != deriveCodexClientIdentity("1c0a3d9e-58b2-4f8c-a2d1-7f3b9e6c4a55") || !first.valid() { t.Fatal("stable profile changed") diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 4414d8452736..e0606f09b3c8 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -963,17 +963,17 @@ 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") - canonical, identityErr := resolveCodexOutboundIdentityForAccountContext(req.Context(), credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)) + canonical, identityErr := resolveCodexOutboundIdentityForAccount(credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)) if identityErr != nil { - return s.sendErrorAndEnd(c, "Codex identity policy unavailable: "+identityErr.Error()) + return s.sendErrorAndEnd(c, "Codex client identity is invalid: "+identityErr.Error()) } req.Header.Set("Originator", canonical.originator) req.Header.Set("User-Agent", canonical.userAgent) setOpenAIChatGPTAccountHeaders(req.Header, credentialAccount) // 与真实转发一致:使用该账号的 Codex TUI 身份,账号级自定义 UA 同样作为管理员 // 显式配置传入,否则测试用的身份与该账号真实出站的身份不是同一个。 - if err := enforceCodexIdentityHeadersForAccountContext(req.Context(), req.Header, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)); err != nil { - return s.sendErrorAndEnd(c, "Codex identity policy unavailable: "+err.Error()) + if err := enforceCodexIdentityHeadersForAccount(req.Header, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)); err != nil { + return s.sendErrorAndEnd(c, "Codex client identity is invalid: "+err.Error()) } } @@ -995,7 +995,7 @@ func (s *AccountTestService) testOpenAIAccountConnection(c *gin.Context, account } else if req, err = prepareOpenAICodexWireRequest(req, account); err != nil { return s.sendErrorAndEnd(c, fmt.Sprintf("Request failed: %s", err.Error())) } - base := resolveCodexIdentitySnapshotContext(ctx, account, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)) + base := resolveCodexIdentitySnapshot(account, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)) snapshot := base.withWire(transport, req.Header) s.sendEvent(c, TestEvent{Type: "status", Text: snapshot.summary(), Data: snapshot}) s.sendEvent(c, TestEvent{Type: "status", Text: "Codex wire headers", Data: accountTestWireHeaders(req.Header)}) @@ -2325,8 +2325,8 @@ func (s *AccountTestService) testOpenAICompactConnection(c *gin.Context, account if isOAuth { // 与真实转发一致:使用该账号的 Codex TUI 身份,账号级自定义 UA 经 ForceCodexCLI // 策略过滤后作为管理员显式配置传入(同普通 OAuth 连接测试)。 - if err := enforceCodexIdentityHeadersForAccountContext(req.Context(), req.Header, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)); err != nil { - return s.sendErrorAndEnd(c, "Codex identity policy unavailable: "+err.Error()) + if err := enforceCodexIdentityHeadersForAccount(req.Header, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)); err != nil { + return s.sendErrorAndEnd(c, "Codex client identity is invalid: "+err.Error()) } } probeSessionID := compactProbeSessionID(account.ID) @@ -3021,17 +3021,17 @@ 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") - canonical, identityErr := resolveCodexOutboundIdentityForAccountContext(req.Context(), credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)) + canonical, identityErr := resolveCodexOutboundIdentityForAccount(credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)) if identityErr != nil { - return s.sendErrorAndEnd(c, "Codex identity policy unavailable: "+identityErr.Error()) + return s.sendErrorAndEnd(c, "Codex client identity is invalid: "+identityErr.Error()) } req.Header.Set("originator", canonical.originator) req.Header.Set("User-Agent", canonical.userAgent) setOpenAIChatGPTAccountHeaders(req.Header, credentialAccount) // 与真实转发一致(同普通 OAuth 连接测试):使用该账号的 Codex TUI 身份,账号级自定义 UA // 经 ForceCodexCLI 策略过滤后作为管理员显式配置传入。 - if err := enforceCodexIdentityHeadersForAccountContext(req.Context(), req.Header, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)); err != nil { - return s.sendErrorAndEnd(c, "Codex identity policy unavailable: "+err.Error()) + if err := enforceCodexIdentityHeadersForAccount(req.Header, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)); err != nil { + return s.sendErrorAndEnd(c, "Codex client identity is invalid: "+err.Error()) } proxyURL := "" diff --git a/backend/internal/service/account_test_service_openai_compact_test.go b/backend/internal/service/account_test_service_openai_compact_test.go index a07967f923df..b5477de7a8f8 100644 --- a/backend/internal/service/account_test_service_openai_compact_test.go +++ b/backend/internal/service/account_test_service_openai_compact_test.go @@ -321,7 +321,9 @@ func TestAccountTestService_TestAccountConnection_OpenAICompactProbeIdentityMatc require.NotContains(t, upstream.lastReq.Header.Get("session-id"), "probe_compact", "探测标识不得是可被上游一眼识别的字面量") // 出站 UA = 该账号(种子派生)的 Codex TUI 身份,而不是全局规范身份。 - require.Equal(t, resolveCodexOutboundIdentityForAccount(&account, "").userAgent, upstream.lastReq.Header.Get("User-Agent")) + identity, err := resolveCodexOutboundIdentityForAccount(&account, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, upstream.lastReq.Header.Get("User-Agent")) <-updateCalls } diff --git a/backend/internal/service/account_test_service_openai_image_test.go b/backend/internal/service/account_test_service_openai_image_test.go index 8ad5c20fb058..e181ae0fbe06 100644 --- a/backend/internal/service/account_test_service_openai_image_test.go +++ b/backend/internal/service/account_test_service_openai_image_test.go @@ -60,7 +60,9 @@ func TestAccountTestService_OpenAIImageOAuthHandlesOutputItemDoneFallback(t *tes require.NotNil(t, upstream.lastReq) require.Equal(t, HTTPUpstreamProfileOpenAI, HTTPUpstreamProfileFromContext(upstream.lastReq.Context())) // Force 优先于自定义 UA;无种子账号使用规范身份。压缩后仍是 image_generation 工具调用。 - require.Equal(t, resolveCodexOutboundIdentityForAccount(account, "").userAgent, upstream.lastReq.Header.Get("User-Agent")) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, upstream.lastReq.Header.Get("User-Agent")) require.Equal(t, "codex-tui", upstream.lastReq.Header.Get("originator")) require.Equal(t, "zstd", upstream.lastReq.Header.Get("Content-Encoding")) require.Equal(t, "image_generation", gjson.GetBytes(zstdDecodeForTest(t, upstream.lastBody), "tools.0.type").String()) diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 25c397d9ff80..4c9d266accd4 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -917,7 +917,7 @@ func (s *AccountUsageService) probeOpenAICodexSnapshot(ctx context.Context, acco // 与真实转发一致:账号级自定义 UA 同样作为管理员显式配置传入。 // 上面写进 header 的指纹缓存 UA 只在强制统一被关闭时才参与配对(保持回滚后的历史语义); // 强制统一开启时客户端身份不参与构造,探针与真实转发用同一套规范身份出站。 - if err := enforceCodexIdentityHeadersForAccountContext(req.Context(), req.Header, account, codexAccountIdentityOverrideUA(account)); err != nil { + if err := enforceCodexIdentityHeadersForAccount(req.Header, account, codexAccountIdentityOverrideUA(account)); err != nil { return nil, err } setOpenAIChatGPTAccountHeaders(req.Header, account) diff --git a/backend/internal/service/codex_outbound_contract.go b/backend/internal/service/codex_outbound_contract.go index bec555669bac..6c940060ea28 100644 --- a/backend/internal/service/codex_outbound_contract.go +++ b/backend/internal/service/codex_outbound_contract.go @@ -19,5 +19,5 @@ func (s *OpenAIGatewayService) finalizeCodexOutboundHeaders(ctx context.Context, if err := resolveAndSetOpenAIChatGPTAccountHeaders(ctx, s.accountRepo, headers, account); err != nil { return err } - return enforceCodexIdentityHeadersForAccountContext(ctx, headers, codexAccountIdentitySource(c, account), s.codexIdentityOverrideUA(account)) + return enforceCodexIdentityHeadersForAccount(headers, codexAccountIdentitySource(c, account), s.codexIdentityOverrideUA(account)) } diff --git a/backend/internal/service/openai_alpha_search.go b/backend/internal/service/openai_alpha_search.go index f03557bd888e..f187ce6a439a 100644 --- a/backend/internal/service/openai_alpha_search.go +++ b/backend/internal/service/openai_alpha_search.go @@ -324,7 +324,7 @@ func (s *OpenAIGatewayService) buildOpenAIAlphaSearchResponsesWebSearchRequest(c fillCodexSessionIdentityHeaders(req.Header, generateSessionUUID(sessionID)) } applyCodexAccountIdentityHeaders(req.Header, codexAccountIdentitySource(c, account), apiKeyID) - if err := enforceCodexIdentityHeadersForAccountContext(req.Context(), req.Header, codexAccountIdentitySource(c, account), s.codexIdentityOverrideUA(account)); err != nil { + if err := enforceCodexIdentityHeadersForAccount(req.Header, codexAccountIdentitySource(c, account), s.codexIdentityOverrideUA(account)); err != nil { return nil, err } account.ApplyHeaderOverrides(req.Header) @@ -465,7 +465,7 @@ func (s *OpenAIGatewayService) buildOpenAIAlphaSearchRequest(ctx context.Context req.Header.Set("User-Agent", canonical.userAgent) } applyCodexAccountIdentityHeaders(req.Header, codexAccountIdentitySource(c, account), getAPIKeyIDFromContext(c)) - if err := enforceCodexIdentityHeadersForAccountContext(req.Context(), req.Header, codexAccountIdentitySource(c, account), s.codexIdentityOverrideUA(account)); err != nil { + if err := enforceCodexIdentityHeadersForAccount(req.Header, codexAccountIdentitySource(c, account), s.codexIdentityOverrideUA(account)); err != nil { return nil, err } } diff --git a/backend/internal/service/openai_codex_account_identity_test.go b/backend/internal/service/openai_codex_account_identity_test.go index 2233790a7c64..49312a38acdf 100644 --- a/backend/internal/service/openai_codex_account_identity_test.go +++ b/backend/internal/service/openai_codex_account_identity_test.go @@ -114,7 +114,9 @@ func TestCodexAccountIdentitySourceResolvesShadowAndOverwritesFailoverContext(t "token", true, "client-session", true, ) require.NoError(t, err) - require.Equal(t, resolveCodexOutboundIdentityForAccount(parent, "").userAgent, req.Header.Get("user-agent"), "影子账号使用凭据账号的身份") + identity, err := resolveCodexOutboundIdentityForAccount(parent, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, req.Header.Get("user-agent"), "影子账号使用凭据账号的身份") require.Empty(t, req.Header.Get("session_id")) require.Equal(t, "client-session", req.Header.Get("session-id"), "缺失连字符会话头时以最终 prompt_cache_key 补齐") diff --git a/backend/internal/service/openai_codex_client_identity_test.go b/backend/internal/service/openai_codex_client_identity_test.go index 315cdf5400b2..592b0932e125 100644 --- a/backend/internal/service/openai_codex_client_identity_test.go +++ b/backend/internal/service/openai_codex_client_identity_test.go @@ -51,7 +51,8 @@ func TestRefreshAccountTokenUsesAccountCodexIdentity(t *testing.T) { info, err := svc.RefreshAccountToken(context.Background(), account) require.NoError(t, err) require.Equal(t, "with-identity", info.AccessToken) - expected := resolveCodexOutboundIdentityForAccount(account, "") + expected, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) require.Equal(t, expected.userAgent, stub.userAgent) require.Equal(t, "codex-tui", stub.originator) } @@ -62,9 +63,11 @@ func TestCodexIdentitySnapshotIsSecretFreeAndConsistent(t *testing.T) { Credentials: map[string]any{"chatgpt_account_id": "acct-9"}, Extra: map[string]any{codexFingerprintSeedExtraKey: seed}} - snapshot := resolveCodexIdentitySnapshotContext(context.Background(), account, account, "") + snapshot := resolveCodexIdentitySnapshot(account, account, "") require.Equal(t, "account", snapshot.IdentitySource) - require.Equal(t, resolveCodexOutboundIdentityForAccount(account, "").userAgent, snapshot.UserAgent) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, snapshot.UserAgent) raw, err := json.Marshal(snapshot) require.NoError(t, err) require.NotContains(t, string(raw), seed) @@ -72,7 +75,7 @@ func TestCodexIdentitySnapshotIsSecretFreeAndConsistent(t *testing.T) { t.Cleanup(func() { SetCodexForceCLIEnabled(false) }) account.Credentials["user_agent"] = "codex_cli_rs/0.150.0 (Windows 10.0.19045; x86_64) unknown" SetCodexForceCLIEnabled(true) - forced := resolveCodexIdentitySnapshotContext(context.Background(), account, account, codexAccountIdentityOverrideUA(account)) + forced := resolveCodexIdentitySnapshot(account, account, codexAccountIdentityOverrideUA(account)) require.Equal(t, "account", forced.IdentitySource) require.Equal(t, deriveCodexClientIdentity(seed).UserAgent(forced.Version), forced.UserAgent, "ForceCodexCLI drops the custom UA while retaining the account's derived TUI profile") @@ -83,15 +86,15 @@ func TestCodexIdentitySnapshotReportsTheSelectedFingerprintSource(t *testing.T) t.Cleanup(func() { SetCodexForceCLIEnabled(false) }) account := &Account{ID: 7, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: map[string]any{codexFingerprintSeedExtraKey: "1c0a3d9e-58b2-4f8c-a2d1-7f3b9e6c4a55"}} - snapshot := resolveCodexIdentitySnapshotContext(context.Background(), account, account, "") + snapshot := resolveCodexIdentitySnapshot(account, account, "") require.Equal(t, "account", snapshot.IdentitySource) require.False(t, snapshot.UAOverridePresent) - snapshot = resolveCodexIdentitySnapshotContext(context.Background(), account, account, "codex_cli_rs/0.144.0 (Windows 10.0.19045; x86_64) unknown") + snapshot = resolveCodexIdentitySnapshot(account, account, "codex_cli_rs/0.144.0 (Windows 10.0.19045; x86_64) unknown") require.Equal(t, "override_ua", snapshot.IdentitySource) require.True(t, snapshot.UAOverridePresent) // An override that does not pair as a Codex identity leaves the canonical // identity in place; the flag is then the only sign that one is configured. - snapshot = resolveCodexIdentitySnapshotContext(context.Background(), account, account, "explicit UA override") + snapshot = resolveCodexIdentitySnapshot(account, account, "explicit UA override") require.Equal(t, "canonical", snapshot.IdentitySource) require.True(t, snapshot.UAOverridePresent) } @@ -117,10 +120,10 @@ func TestCodexIdentityInvalidVersionCannotSendCanonicalFallback(t *testing.T) { Extra: map[string]any{codexFingerprintSeedExtraKey: "1c0a3d9e-58b2-4f8c-a2d1-7f3b9e6c4a55"}} require.Equal(t, "9.9.9_bad", resolveCodexOutboundIdentity("").version) - _, err := resolveCodexOutboundIdentityForAccountContext(context.Background(), account, "") + _, err := resolveCodexOutboundIdentityForAccount(account, "") require.ErrorIs(t, err, codexprofile.ErrInvalidVersion) headers := http.Header{"Originator": []string{"fixture-original"}} - err = enforceCodexIdentityHeadersForAccountContext(context.Background(), headers, account, "") + err = enforceCodexIdentityHeadersForAccount(headers, account, "") require.ErrorIs(t, err, codexprofile.ErrInvalidVersion) require.Equal(t, "fixture-original", headers.Get("Originator")) require.Empty(t, headers.Get("User-Agent")) @@ -243,7 +246,9 @@ func TestRefreshAccountTokenSharesForceCodexCLIOverridePolicy(t *testing.T) { stub = &identityRefreshingOAuthClientStub{} _, err = NewOpenAIOAuthService(nil, stub).RefreshAccountToken(context.Background(), account) require.NoError(t, err) - require.Equal(t, resolveCodexOutboundIdentityForAccount(account, "").userAgent, stub.userAgent, "ForceCodexCLI 时与推理面一样忽略显式 UA") + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, stub.userAgent, "ForceCodexCLI 时与推理面一样忽略显式 UA") } // codexTestSeedForOS 返回一个派生出指定操作系统身份的合法种子(确定性搜索)。 diff --git a/backend/internal/service/openai_codex_identity.go b/backend/internal/service/openai_codex_identity.go index 94b49d74e70d..bd0e9d425ce2 100644 --- a/backend/internal/service/openai_codex_identity.go +++ b/backend/internal/service/openai_codex_identity.go @@ -1,7 +1,6 @@ package service import ( - "context" "net/http" "regexp" "strings" @@ -275,15 +274,10 @@ func pairCodexIdentityHeaders(h http.Header) { // 优先级:管理员显式配置的账号级 User-Agent(只贡献客户端名与 OS / 架构 / 终端指纹, // 版本段仍由生效版本重建)> 账号持久化 / 种子派生的 Codex TUI 身份 > 全局规范身份。 // 版本号三处同源:UA 首段、UA 尾部括号组与 version 头都取当前生效的官方版本。 -func resolveCodexOutboundIdentityForAccount(account *Account, overrideUA string) codexOutboundIdentity { - identity, _ := resolveCodexOutboundIdentityForAccountContext(context.Background(), account, overrideUA) - return identity -} - -// resolveCodexOutboundIdentityForAccountContext 与 resolveCodexOutboundIdentityForAccount 同语义, -// 并把唯一的失败原因交还调用方:生效版本号无法写进该账号身份的 User-Agent 时返回错误, +// +// 唯一的失败原因:生效版本号无法写进该账号身份的 User-Agent。此时返回错误, // 调用方必须让本次发送失败,而不是退回全局规范身份出站。 -func resolveCodexOutboundIdentityForAccountContext(_ context.Context, account *Account, overrideUA string) (codexOutboundIdentity, error) { +func resolveCodexOutboundIdentityForAccount(account *Account, overrideUA string) (codexOutboundIdentity, error) { canonical := resolveCodexOutboundIdentity(overrideUA) if overrideUA != "" { return canonical, nil @@ -301,12 +295,8 @@ func resolveCodexOutboundIdentityForAccountContext(_ context.Context, account *A // enforceCodexIdentityHeadersForAccount 与 enforceCodexIdentityHeadersWithUA 语义相同, // 但强制统一时使用账号级身份而不是全局规范身份,使同一账号的所有出站请求 -// 表现为同一台机器上的同一个 Codex TUI。 +// 表现为同一台机器上的同一个 Codex TUI。账号身份解析失败时返回该错误,请求头保持原样。 func enforceCodexIdentityHeadersForAccount(h http.Header, account *Account, overrideUA string) error { - return enforceCodexIdentityHeadersForAccountContext(context.Background(), h, account, overrideUA) -} - -func enforceCodexIdentityHeadersForAccountContext(ctx context.Context, h http.Header, account *Account, overrideUA string) error { if h == nil || h.Get("originator") == "" { return nil } @@ -314,7 +304,7 @@ func enforceCodexIdentityHeadersForAccountContext(ctx context.Context, h http.He pairCodexIdentityHeaders(h) return nil } - identity, err := resolveCodexOutboundIdentityForAccountContext(ctx, account, overrideUA) + identity, err := resolveCodexOutboundIdentityForAccount(account, overrideUA) if err != nil { return err } diff --git a/backend/internal/service/openai_codex_identity_snapshot.go b/backend/internal/service/openai_codex_identity_snapshot.go index 3e143b7907d1..8ee24445a35a 100644 --- a/backend/internal/service/openai_codex_identity_snapshot.go +++ b/backend/internal/service/openai_codex_identity_snapshot.go @@ -1,7 +1,6 @@ package service import ( - "context" "fmt" "net/http" "strconv" @@ -44,7 +43,7 @@ type codexIdentitySnapshot struct { // resolveCodexIdentitySnapshot computes the identity and fingerprint state // from the routed account and the credential source used for this attempt. // It intentionally contains no credential, seed, cookie, or proxy URL data. -func resolveCodexIdentitySnapshotContext(ctx context.Context, routed, source *Account, overrideUA string) codexIdentitySnapshot { +func resolveCodexIdentitySnapshot(routed, source *Account, overrideUA string) codexIdentitySnapshot { if source == nil { source = routed } @@ -91,7 +90,9 @@ func resolveCodexIdentitySnapshotContext(ctx context.Context, routed, source *Ac }() } - identity, _ := resolveCodexOutboundIdentityForAccountContext(ctx, source, overrideUA) + // The snapshot only reports. The send path resolves the same identity for + // the request and is where a failure to build it stops the send. + identity, _ := resolveCodexOutboundIdentityForAccount(source, overrideUA) snapshot.UserAgent = identity.userAgent snapshot.Originator = identity.originator snapshot.Version = identity.version diff --git a/backend/internal/service/openai_codex_models_service.go b/backend/internal/service/openai_codex_models_service.go index 4d2c5c96e87d..caf02da8b8dd 100644 --- a/backend/internal/service/openai_codex_models_service.go +++ b/backend/internal/service/openai_codex_models_service.go @@ -1792,7 +1792,7 @@ func (s *OpenAIGatewayService) FetchCodexModelsManifest(ctx context.Context, acc identity := resolveCodexOutboundIdentity(overrideUA) if !useAPIKeyUpstream { overrideUA = codexAccountIdentityOverrideUA(credAccount) - identity, err = resolveCodexOutboundIdentityForAccountContext(ctx, credAccount, overrideUA) + identity, err = resolveCodexOutboundIdentityForAccount(credAccount, overrideUA) if err != nil { return nil, err } diff --git a/backend/internal/service/openai_compat_model_test.go b/backend/internal/service/openai_compat_model_test.go index 604be7530758..94ed8e17aefd 100644 --- a/backend/internal/service/openai_compat_model_test.go +++ b/backend/internal/service/openai_compat_model_test.go @@ -1171,7 +1171,9 @@ func TestForwardAsAnthropic_ReusesOAuthCodexTurnState(t *testing.T) { require.NoError(t, err) require.NotNil(t, firstResult) require.Empty(t, upstream.requests[0].Header.Get("x-codex-turn-state")) - requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], resolveCodexOutboundIdentityForAccount(account, "").userAgent, openai.CodexDefaultOriginator) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], identity.userAgent, openai.CodexDefaultOriginator) secondBody := []byte(`{"model":"claude-sonnet-4-5","max_tokens":16,"messages":[{"role":"user","content":"first"},{"role":"assistant","content":"ok"},{"role":"user","content":"second"}],"stream":false}`) secondRec := httptest.NewRecorder() @@ -1185,7 +1187,7 @@ func TestForwardAsAnthropic_ReusesOAuthCodexTurnState(t *testing.T) { require.Equal(t, "turn_state_first", upstream.requests[1].Header.Get("x-codex-turn-state")) require.Equal(t, generateSessionUUID(isolateOpenAIUpstreamSessionID(0, account, "stable-cache-key")), upstream.requests[1].Header.Get("session-id")) require.Empty(t, upstream.requests[1].Header.Get("conversation_id")) - requireOpenAIMessagesCodexIdentity(t, upstream.requests[1], resolveCodexOutboundIdentityForAccount(account, "").userAgent, openai.CodexDefaultOriginator) + requireOpenAIMessagesCodexIdentity(t, upstream.requests[1], identity.userAgent, openai.CodexDefaultOriginator) require.False(t, gjson.GetBytes(upstream.bodies[1], "prompt_cache_key").Exists()) require.False(t, gjson.GetBytes(upstream.bodies[1], "previous_response_id").Exists()) } @@ -1236,7 +1238,9 @@ func TestForwardAsAnthropic_OAuthRestoresCodexIdentityHeaders(t *testing.T) { result, err := svc.ForwardAsAnthropic(context.Background(), c, account, body, "", "gpt-5.4") require.NoError(t, err) require.NotNil(t, result) - requireOpenAIMessagesCodexIdentity(t, upstream.lastReq, resolveCodexOutboundIdentityForAccount(account, "").userAgent, openai.CodexDefaultOriginator) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + requireOpenAIMessagesCodexIdentity(t, upstream.lastReq, identity.userAgent, openai.CodexDefaultOriginator) }) } } @@ -1279,7 +1283,9 @@ func TestForwardAsAnthropic_OAuthDigestFallbackReusesTurnStateWithoutExplicitKey firstSessionID := upstream.requests[0].Header.Get("session-id") require.NotEmpty(t, firstSessionID) require.Empty(t, upstream.requests[0].Header.Get("x-codex-turn-state")) - requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], resolveCodexOutboundIdentityForAccount(account, "").userAgent, openai.CodexDefaultOriginator) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], identity.userAgent, openai.CodexDefaultOriginator) require.False(t, gjson.GetBytes(upstream.bodies[0], "prompt_cache_key").Exists()) secondBody := []byte(`{"model":"claude-sonnet-4-5","max_tokens":16,"messages":[{"role":"user","content":"first"},{"role":"assistant","content":"ok"},{"role":"user","content":"second"}],"stream":false}`) @@ -1294,7 +1300,7 @@ func TestForwardAsAnthropic_OAuthDigestFallbackReusesTurnStateWithoutExplicitKey require.Equal(t, firstSessionID, upstream.requests[1].Header.Get("session-id")) require.Equal(t, "turn_state_digest_first", upstream.requests[1].Header.Get("x-codex-turn-state")) require.Empty(t, upstream.requests[1].Header.Get("conversation_id")) - requireOpenAIMessagesCodexIdentity(t, upstream.requests[1], resolveCodexOutboundIdentityForAccount(account, "").userAgent, openai.CodexDefaultOriginator) + requireOpenAIMessagesCodexIdentity(t, upstream.requests[1], identity.userAgent, openai.CodexDefaultOriginator) require.False(t, gjson.GetBytes(upstream.bodies[1], "prompt_cache_key").Exists()) require.False(t, gjson.GetBytes(upstream.bodies[1], "previous_response_id").Exists()) } @@ -1449,7 +1455,9 @@ func TestForwardAsAnthropic_OAuthKeepsSystemAsDeveloperInput(t *testing.T) { instructions := gjson.GetBytes(upstream.lastBody, "instructions") require.True(t, instructions.Exists()) require.Empty(t, instructions.String()) - requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], resolveCodexOutboundIdentityForAccount(account, "").userAgent, openai.CodexDefaultOriginator) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], identity.userAgent, openai.CodexDefaultOriginator) } func TestForwardAsAnthropic_OAuthAddsClaudeCodeTodoGuardForCompatModel(t *testing.T) { diff --git a/backend/internal/service/openai_gateway_forward.go b/backend/internal/service/openai_gateway_forward.go index ac42dac6f4c0..4e7d38020f1e 100644 --- a/backend/internal/service/openai_gateway_forward.go +++ b/backend/internal/service/openai_gateway_forward.go @@ -1588,7 +1588,10 @@ func (s *OpenAIGatewayService) effectiveCodexOutboundUserAgent(c *gin.Context, a } overrideUA := s.codexIdentityOverrideUA(account) if codexIdentityEnforcement.Load() { - return resolveCodexOutboundIdentityForAccount(codexAccountIdentitySource(c, account), overrideUA).userAgent + // 这里只预测终态 UA 供 sandbox 对齐,不决定请求能否发出:身份解析失败时预测为空 + // (sandbox 不改写),是否拒绝发送由终态收口 enforceCodexIdentityHeadersForAccount 决定。 + identity, _ := resolveCodexOutboundIdentityForAccount(codexAccountIdentitySource(c, account), overrideUA) + return identity.userAgent } candidate := overrideUA if candidate == "" { diff --git a/backend/internal/service/openai_oauth_passthrough_test.go b/backend/internal/service/openai_oauth_passthrough_test.go index 52f326aeb0ae..c263f98823b7 100644 --- a/backend/internal/service/openai_oauth_passthrough_test.go +++ b/backend/internal/service/openai_oauth_passthrough_test.go @@ -579,7 +579,9 @@ func TestOpenAIGatewayService_OAuthPassthrough_StreamKeepsToolNameAndBodyNormali // 2) only auth is replaced; inbound auth/cookie are not forwarded require.Equal(t, "Bearer oauth-token", upstream.lastReq.Header.Get("Authorization")) // 强制统一出口:客户端自报的 codex_cli_rs/0.1.0 不会到达上游,出站是该账号的 Codex TUI 身份。 - require.Equal(t, resolveCodexOutboundIdentityForAccount(account, "").userAgent, upstream.lastReq.Header.Get("User-Agent")) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, upstream.lastReq.Header.Get("User-Agent")) require.Empty(t, upstream.lastReq.Header.Get("Cookie")) require.Empty(t, upstream.lastReq.Header.Get("X-Api-Key")) require.Empty(t, upstream.lastReq.Header.Get("X-Goog-Api-Key")) @@ -1301,7 +1303,9 @@ func TestOpenAIGatewayService_OAuthLegacy_CompositeCodexUAUsesCodexOriginator(t require.NotNil(t, upstream.lastReq) // 浏览器型复合 UA 被替换为该账号的 Codex TUI 身份 UA, // originator 随最终 UA 配套(issue #3901)。 - require.Equal(t, resolveCodexOutboundIdentityForAccount(account, "").userAgent, upstream.lastReq.Header.Get("User-Agent")) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, upstream.lastReq.Header.Get("User-Agent")) require.Equal(t, openai.CodexDefaultOriginator, upstream.lastReq.Header.Get("originator")) require.NotEqual(t, "opencode", upstream.lastReq.Header.Get("originator")) } @@ -2522,7 +2526,9 @@ func TestOpenAIGatewayService_OAuthPassthrough_NonCodexUAFallbackToCodexUA(t *te require.NoError(t, err) require.Equal(t, false, gjson.GetBytes(upstream.lastBody, "store").Bool()) require.Equal(t, true, gjson.GetBytes(upstream.lastBody, "stream").Bool()) - require.Equal(t, resolveCodexOutboundIdentityForAccount(account, "").userAgent, upstream.lastReq.Header.Get("User-Agent")) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, upstream.lastReq.Header.Get("User-Agent")) } // 透传模式的 OAuth 与非透传一致:官方客户端身份同样被强制统一为网关规范身份, @@ -2570,7 +2576,9 @@ func TestOpenAIGatewayService_OAuthPassthrough_OfficialIdentityUnified(t *testin _, err := svc.Forward(context.Background(), c, account, inputBody) require.NoError(t, err) require.NotNil(t, upstream.lastReq) - require.Equal(t, resolveCodexOutboundIdentityForAccount(account, "").userAgent, upstream.lastReq.Header.Get("User-Agent")) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, upstream.lastReq.Header.Get("User-Agent")) require.Equal(t, openai.CodexDefaultOriginator, upstream.lastReq.Header.Get("originator")) require.Equal(t, codexCLIVersion, upstream.lastReq.Header.Get("version")) } @@ -2614,7 +2622,9 @@ func TestOpenAIGatewayService_OAuthPassthrough_CodexTuiIdentityUnified(t *testin _, err := svc.Forward(context.Background(), c, account, inputBody) require.NoError(t, err) require.NotNil(t, upstream.lastReq) - require.Equal(t, resolveCodexOutboundIdentityForAccount(account, "").userAgent, upstream.lastReq.Header.Get("User-Agent")) + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + require.Equal(t, identity.userAgent, upstream.lastReq.Header.Get("User-Agent")) require.Equal(t, openai.CodexDefaultOriginator, upstream.lastReq.Header.Get("originator")) require.Equal(t, codexCLIVersion, upstream.lastReq.Header.Get("version")) } diff --git a/backend/internal/service/openai_oauth_service.go b/backend/internal/service/openai_oauth_service.go index 221414665856..8749f36b6bf6 100644 --- a/backend/internal/service/openai_oauth_service.go +++ b/backend/internal/service/openai_oauth_service.go @@ -402,7 +402,7 @@ func (s *OpenAIOAuthService) RefreshAccountToken(ctx context.Context, account *A } clientID := account.GetCredential("client_id") - identity, err := resolveCodexOutboundIdentityForAccountContext(ctx, account, codexAccountIdentityOverrideUA(account)) + identity, err := resolveCodexOutboundIdentityForAccount(account, codexAccountIdentityOverrideUA(account)) if err != nil { return nil, err } diff --git a/backend/internal/service/openai_setup_token_compat_test.go b/backend/internal/service/openai_setup_token_compat_test.go index 7aa52223b3e0..5eac0de75d0b 100644 --- a/backend/internal/service/openai_setup_token_compat_test.go +++ b/backend/internal/service/openai_setup_token_compat_test.go @@ -212,7 +212,9 @@ func TestOpenAISetupTokenMessagesUsesCodexBridgeAndTurnState(t *testing.T) { require.Equal(t, chatgptCodexURL, upstream.requests[0].URL.String()) require.Equal(t, "Bearer setup-token-value", upstream.requests[0].Header.Get("Authorization")) require.Equal(t, "chatgpt-setup", upstream.requests[0].Header.Get("chatgpt-account-id")) - requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], resolveCodexOutboundIdentityForAccount(account, "").userAgent, "codex-tui") + identity, err := resolveCodexOutboundIdentityForAccount(account, "") + require.NoError(t, err) + requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], identity.userAgent, "codex-tui") require.Empty(t, upstream.requests[0].Header.Get("x-codex-turn-state")) secondBody := []byte(`{"model":"claude-sonnet-4-5","max_tokens":16,"messages":[{"role":"user","content":"next"}],"stream":false}`) @@ -229,7 +231,7 @@ func TestOpenAISetupTokenMessagesUsesCodexBridgeAndTurnState(t *testing.T) { require.Equal(t, "turn_state_setup", upstream.requests[1].Header.Get("x-codex-turn-state")) require.Equal(t, generateSessionUUID(isolateOpenAIUpstreamSessionID(0, account, "stable-cache-key")), upstream.requests[1].Header.Get("session-id")) require.Empty(t, upstream.requests[1].Header.Get("conversation_id")) - requireOpenAIMessagesCodexIdentity(t, upstream.requests[1], resolveCodexOutboundIdentityForAccount(account, "").userAgent, "codex-tui") + requireOpenAIMessagesCodexIdentity(t, upstream.requests[1], identity.userAgent, "codex-tui") } func openAISetupTokenCompatAccount(id int64) *Account { diff --git a/backend/internal/service/upstream_models.go b/backend/internal/service/upstream_models.go index 788b381aa7f2..2722de345c5e 100644 --- a/backend/internal/service/upstream_models.go +++ b/backend/internal/service/upstream_models.go @@ -1400,7 +1400,7 @@ func (s *AccountTestService) buildOpenAIOAuthUpstreamModelsRequest(ctx context.C req.Header.Set("Authorization", "Bearer "+accessToken) } - identity, identityErr := resolveCodexOutboundIdentityForAccountContext(req.Context(), credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)) + identity, identityErr := resolveCodexOutboundIdentityForAccount(credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)) if identityErr != nil { return nil, identityErr } @@ -1410,7 +1410,7 @@ func (s *AccountTestService) buildOpenAIOAuthUpstreamModelsRequest(ctx context.C req.Header.Set("Version", identity.version) setOpenAIChatGPTAccountHeaders(req.Header, credentialAccount) credentialAccount.ApplyHeaderOverrides(req.Header) - if err := enforceCodexIdentityHeadersForAccountContext(req.Context(), req.Header, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)); err != nil { + if err := enforceCodexIdentityHeadersForAccount(req.Header, credentialAccount, codexAccountIdentityOverrideUA(credentialAccount)); err != nil { return nil, err } return req, nil