From dbb42881c0c6dd0596de2f9367a9c7f45e3b50eb Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 7 Aug 2026 09:56:03 +0800 Subject: [PATCH 1/2] fix(openai): default OAuth identity to codex-tui --- backend/internal/pkg/antigravity/client.go | 2 +- backend/internal/pkg/openai/request.go | 6 ++++-- .../internal/service/account_test_service.go | 4 ++-- .../internal/service/account_usage_service.go | 2 +- .../internal/service/openai_alpha_search.go | 5 +++-- .../service/openai_alpha_search_test.go | 3 ++- .../service/openai_capacity_shed_test.go | 3 ++- .../internal/service/openai_codex_identity.go | 8 +++---- .../service/openai_codex_identity_test.go | 21 ++++++++++--------- .../service/openai_codex_models_service.go | 3 ++- .../openai_codex_models_service_test.go | 5 +++-- .../service/openai_codex_pat_service.go | 3 ++- .../service/openai_codex_pat_service_test.go | 3 ++- .../openai_codex_version_consistency_test.go | 3 ++- .../openai_codex_version_sync_service_test.go | 16 ++++++++++++-- .../service/openai_compat_model_test.go | 13 ++++++------ .../service/openai_gateway_grok_cache.go | 2 +- .../service/openai_gateway_passthrough.go | 3 ++- .../service/openai_gateway_scheduling.go | 3 ++- .../service/openai_gateway_service.go | 4 ++-- .../service/openai_gateway_service_test.go | 2 +- .../service/openai_oauth_passthrough_test.go | 9 ++++---- .../openai_ws_forwarder_success_test.go | 3 ++- .../service/setting_gateway_runtime.go | 8 +++---- backend/internal/service/settings_view.go | 2 +- 25 files changed, 81 insertions(+), 55 deletions(-) diff --git a/backend/internal/pkg/antigravity/client.go b/backend/internal/pkg/antigravity/client.go index 39b6d2c90..785c8af3c 100644 --- a/backend/internal/pkg/antigravity/client.go +++ b/backend/internal/pkg/antigravity/client.go @@ -585,7 +585,7 @@ func (c *Client) OnboardUser(ctx context.Context, accessToken, tierID string) (s return "", lastErr } - // done=false 时等待后重试(与 CLIProxyAPI 行为一致) + // done=false 时等待后重试,避免在上游仍未完成时提前结束轮询。 select { case <-time.After(2 * time.Second): case <-ctx.Done(): diff --git a/backend/internal/pkg/openai/request.go b/backend/internal/pkg/openai/request.go index 33e46c518..172021b8e 100644 --- a/backend/internal/pkg/openai/request.go +++ b/backend/internal/pkg/openai/request.go @@ -244,10 +244,12 @@ func canonicalizeCodexOriginator(name string) string { return name } -// CodexCLIOriginator 官方 Codex CLI 默认 originator(codex-rs DEFAULT_ORIGINATOR), -// 也是身份归一化的目标身份。 +// CodexCLIOriginator 是 codex-rs 客户端的历史默认 originator,保留用于兼容识别。 const CodexCLIOriginator = "codex_cli_rs" +// CodexDefaultOriginator 是网关默认使用的 Codex TUI originator。 +const CodexDefaultOriginator = "codex-tui" + // CodexUserAgentVersion 提取 Codex UA 的完整版本段,即 `{client}/{version} (...` 中的 version。 // 与 ParseCodexEngineVersion 的区别:后者只取三段数字用于引擎版本比较(会丢掉 -alpha.4 // 之类的预发布后缀),本函数保留原样,因为出站 version 头必须与 UA 版本段逐字一致。 diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 58a6c2f82..63d404d45 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -646,7 +646,7 @@ 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", "codex_cli_rs") + req.Header.Set("Originator", openai.CodexDefaultOriginator) if customUA := strings.TrimSpace(credentialAccount.GetOpenAIUserAgent()); customUA != "" { req.Header.Set("User-Agent", customUA) } else { @@ -1860,7 +1860,7 @@ 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", "codex_cli_rs") + req.Header.Set("originator", openai.CodexDefaultOriginator) if customUA := strings.TrimSpace(credentialAccount.GetOpenAIUserAgent()); customUA != "" { req.Header.Set("User-Agent", customUA) } else { diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 7f727b470..7033ad26c 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -759,7 +759,7 @@ 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", "codex_cli_rs") + req.Header.Set("Originator", openaipkg.CodexDefaultOriginator) req.Header.Set("Version", openAICodexProbeVersion) req.Header.Set("User-Agent", codexCLIUserAgent) if s.identityCache != nil { diff --git a/backend/internal/service/openai_alpha_search.go b/backend/internal/service/openai_alpha_search.go index 9d2a70fda..0ddc8f525 100644 --- a/backend/internal/service/openai_alpha_search.go +++ b/backend/internal/service/openai_alpha_search.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/gin-gonic/gin" "github.com/tidwall/gjson" ) @@ -246,7 +247,7 @@ func (s *OpenAIGatewayService) buildOpenAIAlphaSearchResponsesWebSearchRequest(c if originator := openAIAlphaSearchInboundHeader(c, "Originator"); originator != "" { req.Header.Set("Originator", originator) } else { - req.Header.Set("Originator", "codex_cli_rs") + req.Header.Set("Originator", openai.CodexDefaultOriginator) } if customUA := account.GetOpenAIUserAgent(); customUA != "" { req.Header.Set("User-Agent", customUA) @@ -389,7 +390,7 @@ func (s *OpenAIGatewayService) buildOpenAIAlphaSearchRequest(ctx context.Context if originator := openAIAlphaSearchInboundHeader(c, "Originator"); originator != "" { req.Header.Set("Originator", originator) } else { - req.Header.Set("Originator", "codex_cli_rs") + req.Header.Set("Originator", openai.CodexDefaultOriginator) } if customUA := account.GetOpenAIUserAgent(); customUA != "" { req.Header.Set("User-Agent", customUA) diff --git a/backend/internal/service/openai_alpha_search_test.go b/backend/internal/service/openai_alpha_search_test.go index f4ad2506c..c4eded7d5 100644 --- a/backend/internal/service/openai_alpha_search_test.go +++ b/backend/internal/service/openai_alpha_search_test.go @@ -12,6 +12,7 @@ import ( "testing" "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -155,7 +156,7 @@ func TestForwardAlphaSearchPATUsesResponsesWebSearchFallback(t *testing.T) { require.Equal(t, "responses=experimental", upstream.lastReq.Header.Get("OpenAI-Beta")) require.Equal(t, codexCLIVersion, upstream.lastReq.Header.Get("Version")) require.Equal(t, `{"turn_id":"turn-1"}`, upstream.lastReq.Header.Get("X-Codex-Turn-Metadata")) - require.Equal(t, "codex_cli_rs", upstream.lastReq.Header.Get("Originator")) + require.Equal(t, openai.CodexDefaultOriginator, upstream.lastReq.Header.Get("Originator")) require.Empty(t, upstream.lastReq.Header.Get("X-Codex-Beta-Features")) require.Empty(t, upstream.lastReq.Header.Get("X-Codex-Turn-State")) require.Empty(t, upstream.lastReq.Header.Get(responsesLiteHeaderKey)) diff --git a/backend/internal/service/openai_capacity_shed_test.go b/backend/internal/service/openai_capacity_shed_test.go index 2376149a4..83e4f8b33 100644 --- a/backend/internal/service/openai_capacity_shed_test.go +++ b/backend/internal/service/openai_capacity_shed_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/stretchr/testify/require" ) @@ -75,7 +76,7 @@ func TestStreamFailedEventCapacityShedRetriesOnSameAccount(t *testing.T) { // 各自硬编码会漂移成互相矛盾的身份,而自相矛盾或陈旧的身份会被上游优先降载。 func TestCodexOutboundVersionHasSingleSource(t *testing.T) { require.True(t, - strings.HasPrefix(codexCLIUserAgent, "codex_cli_rs/"+codexCLIVersion+" "), + strings.HasPrefix(codexCLIUserAgent, openai.CodexDefaultOriginator+"/"+codexCLIVersion+" "), "codexCLIUserAgent=%q 必须以 codexCLIVersion=%q 作为版本段", codexCLIUserAgent, codexCLIVersion, ) require.Equal(t, codexCLIVersion, openAICodexProbeVersion) diff --git a/backend/internal/service/openai_codex_identity.go b/backend/internal/service/openai_codex_identity.go index aac5301c3..510ee07cf 100644 --- a/backend/internal/service/openai_codex_identity.go +++ b/backend/internal/service/openai_codex_identity.go @@ -32,13 +32,13 @@ func NormalizeCodexClientVersion(version string) string { return version } -// buildCodexCLIUserAgent 按版本号拼出规范 Codex CLI User-Agent。 +// buildCodexCLIUserAgent 按版本号拼出规范 Codex TUI User-Agent。 // UA 形态只在 codexCLIUserAgentSuffix 一处定义,避免多处拼装漂移。 func buildCodexCLIUserAgent(version string) string { if version = NormalizeCodexClientVersion(version); version == "" { return codexCLIUserAgent } - return openai.CodexCLIOriginator + "/" + version + codexCLIUserAgentSuffix + return openai.CodexDefaultOriginator + "/" + version + codexCLIUserAgentSuffix } // codexIdentityEnforcement 控制 enforceCodexIdentityHeaders 是否强制统一出站身份, @@ -99,7 +99,7 @@ type codexOutboundIdentity struct { } // resolveCodexOutboundIdentity 由候选 User-Agent 推导自洽的出站身份。 -// candidateUA 为空时使用规范 User-Agent;推导不出官方身份时整体回退为规范 CLI 身份。 +// candidateUA 为空时使用规范 User-Agent;推导不出官方身份时整体回退为规范 TUI 身份。 // // 候选 UA(面板 / 账号级的管理员显式配置)只贡献客户端名与 OS / 架构 / 终端指纹, // 其自带的版本段一律用当前生效版本重建:一条填写于某个历史版本的 UA 否则会把出站身份 @@ -114,7 +114,7 @@ func resolveCodexOutboundIdentity(candidateUA string) codexOutboundIdentity { originator, pairedUA, ok := openai.PairCodexClientIdentity(ua) if !ok { if originator, pairedUA, ok = openai.PairCodexClientIdentity(canonical); !ok { - originator, pairedUA = openai.CodexCLIOriginator, codexCLIUserAgent + originator, pairedUA = openai.CodexDefaultOriginator, codexCLIUserAgent } } // 生效版本只有一个来源:规范身份(面板版本号 → 自动同步值 → 内置常量,见 diff --git a/backend/internal/service/openai_codex_identity_test.go b/backend/internal/service/openai_codex_identity_test.go index 70a4fbc3d..9b295241c 100644 --- a/backend/internal/service/openai_codex_identity_test.go +++ b/backend/internal/service/openai_codex_identity_test.go @@ -5,13 +5,14 @@ import ( "testing" "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/stretchr/testify/require" ) func requireOpenAICodexProbeHeaders(t *testing.T, h http.Header) { t.Helper() require.Equal(t, codexCLIUserAgent, h.Get("User-Agent")) - require.Equal(t, "codex_cli_rs", h.Get("Originator")) + require.Equal(t, openai.CodexDefaultOriginator, h.Get("Originator")) require.Equal(t, codexCLIVersion, h.Get("Version")) require.Equal(t, "responses=experimental", h.Get("OpenAI-Beta")) require.NotEmpty(t, h.Get("X-Codex-Window-ID")) @@ -27,7 +28,7 @@ func TestEnsureCodexIdentityHeaders(t *testing.T) { ensureCodexIdentityHeaders(h) enforceCodexIdentityHeaders(h) - require.Equal(t, "codex_cli_rs", h.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, h.Get("originator")) require.Equal(t, codexCLIUserAgent, h.Get("user-agent")) require.Equal(t, codexCLIVersion, h.Get("version")) require.Equal(t, "responses=experimental", h.Get("OpenAI-Beta")) @@ -42,7 +43,7 @@ func TestEnsureCodexIdentityHeaders(t *testing.T) { ensureCodexIdentityHeaders(h) enforceCodexIdentityHeaders(h) - require.Equal(t, "codex_cli_rs", h.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, h.Get("originator")) require.Equal(t, codexCLIUserAgent, h.Get("user-agent")) require.Equal(t, codexCLIVersion, h.Get("version")) require.Equal(t, "responses=experimental", h.Get("OpenAI-Beta")) @@ -112,7 +113,7 @@ func TestEnforceCodexIdentityHeaders(t *testing.T) { enforceCodexIdentityHeaders(h) - require.Equal(t, "codex_cli_rs", h.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, h.Get("originator")) require.Equal(t, codexCLIUserAgent, h.Get("user-agent")) require.Equal(t, codexCLIVersion, h.Get("version")) }) @@ -141,7 +142,7 @@ func TestEnforceCodexIdentityHeadersWithAccountOverrideUA(t *testing.T) { enforceCodexIdentityHeadersWithUA(h, "luna/1.0.0") - require.Equal(t, "codex_cli_rs", h.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, h.Get("originator")) require.Equal(t, codexCLIUserAgent, h.Get("user-agent")) require.Equal(t, codexCLIVersion, h.Get("version")) }) @@ -212,7 +213,7 @@ func TestEnforceCodexIdentityHeadersRejectsInvalidCanonicalUA(t *testing.T) { enforceCodexIdentityHeaders(h) - require.Equal(t, "codex_cli_rs", h.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, h.Get("originator")) require.Equal(t, codexCLIUserAgent, h.Get("user-agent")) require.Equal(t, codexCLIVersion, h.Get("version")) } @@ -235,7 +236,7 @@ func TestCodexIdentityEnforcementZeroValueConfigKeepsItEnabled(t *testing.T) { enforceCodexIdentityHeaders(h) - require.Equal(t, "codex_cli_rs", h.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, h.Get("originator")) require.Equal(t, codexCLIUserAgent, h.Get("user-agent")) } @@ -271,7 +272,7 @@ func TestEnforceCodexIdentityHeaders_EnforcementDisabledThirdPartyFallback(t *te enforceCodexIdentityHeaders(h) - require.Equal(t, "codex_cli_rs", h.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, h.Get("originator")) require.Equal(t, codexCLIUserAgent, h.Get("user-agent")) require.Equal(t, codexCLIVersion, h.Get("version")) } @@ -288,7 +289,7 @@ func TestEnforceCodexIdentityHeadersIsIdempotent(t *testing.T) { require.Equal(t, firstUA, h.Get("user-agent")) require.Equal(t, firstVersion, h.Get("version")) - require.Equal(t, "codex_cli_rs", h.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, h.Get("originator")) } // 缺少 originator 时必须保持 no-op:compat 桥接等非 ChatGPT 内部接口路径会显式删除 @@ -316,7 +317,7 @@ func TestNormalizeCodexClientVersion(t *testing.T) { } func TestBuildCodexCLIUserAgent(t *testing.T) { - require.Equal(t, "codex_cli_rs/0.200.1"+codexCLIUserAgentSuffix, buildCodexCLIUserAgent("0.200.1")) + require.Equal(t, openai.CodexDefaultOriginator+"/0.200.1"+codexCLIUserAgentSuffix, buildCodexCLIUserAgent("0.200.1")) // 非法版本号必须回退到内置 UA,不能拼出畸形身份。 require.Equal(t, codexCLIUserAgent, buildCodexCLIUserAgent("bogus version")) require.Equal(t, codexCLIUserAgent, buildCodexCLIUserAgent("")) diff --git a/backend/internal/service/openai_codex_models_service.go b/backend/internal/service/openai_codex_models_service.go index 8349a0075..c36c46eb1 100644 --- a/backend/internal/service/openai_codex_models_service.go +++ b/backend/internal/service/openai_codex_models_service.go @@ -18,6 +18,7 @@ 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" ) @@ -304,7 +305,7 @@ func (s *OpenAIGatewayService) FetchCodexModelsManifest(ctx context.Context, acc setOpenAIChatGPTAccountHeaders(headers, credAccount) } headers.Set("Accept", "application/json") - headers.Set("Originator", "codex_cli_rs") + headers.Set("Originator", openai.CodexDefaultOriginator) headers.Set("Version", clientVersion) headers.Set("User-Agent", codexCLIUserAgent) diff --git a/backend/internal/service/openai_codex_models_service_test.go b/backend/internal/service/openai_codex_models_service_test.go index 6333f16a7..585ad95d5 100644 --- a/backend/internal/service/openai_codex_models_service_test.go +++ b/backend/internal/service/openai_codex_models_service_test.go @@ -17,6 +17,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/stretchr/testify/require" "golang.org/x/net/http2" @@ -193,7 +194,7 @@ func TestFetchCodexModelsManifestPassthrough(t *testing.T) { if gotAccountID != "acc-123" { t.Errorf("chatgpt-account-id header: got %q", gotAccountID) } - if gotOriginator != "codex_cli_rs" { + if gotOriginator != openai.CodexDefaultOriginator { t.Errorf("originator header: got %q", gotOriginator) } if gotClientVersion != "0.137.0" { @@ -447,7 +448,7 @@ func TestFetchCodexModelsManifestAPIKeyCustomUpstream(t *testing.T) { if gotRequest.Header.Get("Authorization") != "Bearer sk-upstream" { t.Errorf("authorization header: got %q", gotRequest.Header.Get("Authorization")) } - if gotRequest.Header.Get("Originator") != "codex_cli_rs" { + if gotRequest.Header.Get("Originator") != openai.CodexDefaultOriginator { t.Errorf("originator header: got %q", gotRequest.Header.Get("Originator")) } if gotRequest.Header.Get("Version") != "0.144.0" { diff --git a/backend/internal/service/openai_codex_pat_service.go b/backend/internal/service/openai_codex_pat_service.go index e109265ab..dce678592 100644 --- a/backend/internal/service/openai_codex_pat_service.go +++ b/backend/internal/service/openai_codex_pat_service.go @@ -10,6 +10,7 @@ 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" @@ -58,7 +59,7 @@ func (s *OpenAIOAuthService) ValidateCodexPersonalAccessToken(ctx context.Contex } req.Header.Set("authorization", "Bearer "+accessToken) req.Header.Set("accept", "application/json") - req.Header.Set("originator", "codex_cli_rs") + req.Header.Set("originator", openai.CodexDefaultOriginator) req.Header.Set("user-agent", codexCLIUserAgent) resp, err := client.Do(req) diff --git a/backend/internal/service/openai_codex_pat_service_test.go b/backend/internal/service/openai_codex_pat_service_test.go index 24ac7a7c1..e7656868b 100644 --- a/backend/internal/service/openai_codex_pat_service_test.go +++ b/backend/internal/service/openai_codex_pat_service_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "testing" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/stretchr/testify/require" ) @@ -38,7 +39,7 @@ func TestOpenAIOAuthService_ValidateCodexPersonalAccessToken(t *testing.T) { info, err := svc.ValidateCodexPersonalAccessToken(context.Background(), " at-test-token ", "") require.NoError(t, err) require.Equal(t, "Bearer at-test-token", gotAuthorization) - require.Equal(t, "codex_cli_rs", gotOriginator) + require.Equal(t, openai.CodexDefaultOriginator, gotOriginator) require.Equal(t, codexCLIUserAgent, gotUserAgent) require.Equal(t, OpenAIAuthModePersonalAccessToken, info.AuthMode) require.Equal(t, "user@example.com", info.Email) diff --git a/backend/internal/service/openai_codex_version_consistency_test.go b/backend/internal/service/openai_codex_version_consistency_test.go index 48883032a..aede2ae43 100644 --- a/backend/internal/service/openai_codex_version_consistency_test.go +++ b/backend/internal/service/openai_codex_version_consistency_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/stretchr/testify/require" ) @@ -13,7 +14,7 @@ func TestCodexVersionConstants_Consistency(t *testing.T) { require.Equal(t, codexCLIVersion, openAICodexProbeVersion, "codexCLIVersion and openAICodexProbeVersion must stay in sync") - require.True(t, strings.Contains(codexCLIUserAgent, "codex_cli_rs/"+codexCLIVersion), + require.True(t, strings.Contains(codexCLIUserAgent, openai.CodexDefaultOriginator+"/"+codexCLIVersion), "codexCLIUserAgent must embed codexCLIVersion") require.True(t, strings.Contains(DefaultOpenAICodexUserAgent, codexCLIVersion), diff --git a/backend/internal/service/openai_codex_version_sync_service_test.go b/backend/internal/service/openai_codex_version_sync_service_test.go index 37a11c08d..2a50527b8 100644 --- a/backend/internal/service/openai_codex_version_sync_service_test.go +++ b/backend/internal/service/openai_codex_version_sync_service_test.go @@ -327,14 +327,14 @@ func TestGetOpenAICodexClientVersionFallsBackOnError(t *testing.T) { require.Equal(t, codexCLIVersion, svc.GetOpenAICodexClientVersion(context.Background())) } -// 规范 UA:面板未填完整 UA 时按当前生效版本号拼出标准 CLI 形态。 +// 规范 UA:面板未填完整 UA 时按当前生效版本号拼出标准 TUI 形态。 func TestGetOpenAICodexCanonicalUserAgentBuildsFromVersion(t *testing.T) { svc := NewSettingService(&codexVersionSettingRepoStub{values: map[string]string{ SettingKeyOpenAICodexClientVersionSynced: "0.200.1", }}, nil) require.Equal(t, - "codex_cli_rs/0.200.1"+codexCLIUserAgentSuffix, + "codex-tui/0.200.1"+codexCLIUserAgentSuffix, svc.GetOpenAICodexCanonicalUserAgent(context.Background()), ) } @@ -368,6 +368,18 @@ func TestGetOpenAICodexCanonicalUserAgentRebuildsPanelUAVersion(t *testing.T) { ) }) + t.Run("TUI UA 的首尾两个版本号同时更新", func(t *testing.T) { + svc := NewSettingService(&codexVersionSettingRepoStub{values: map[string]string{ + SettingKeyOpenAICodexUserAgent: "codex-tui/0.146.1 (Ubuntu 22.4.0; x86_64) WindowsTerminal (codex-tui; 0.146.1)", + SettingKeyOpenAICodexClientVersionSynced: "0.200.1", + }}, nil) + + require.Equal(t, + "codex-tui/0.200.1 (Ubuntu 22.4.0; x86_64) WindowsTerminal (codex-tui; 0.200.1)", + svc.GetOpenAICodexCanonicalUserAgent(context.Background()), + ) + }) + // 面板版本号覆写优先级仍然高于同步值:管理员固定版本的诉求不被重建绕开。 t.Run("面板版本号覆写优先", func(t *testing.T) { svc := NewSettingService(&codexVersionSettingRepoStub{values: map[string]string{ diff --git a/backend/internal/service/openai_compat_model_test.go b/backend/internal/service/openai_compat_model_test.go index ac5ed8d0d..7ccfbdead 100644 --- a/backend/internal/service/openai_compat_model_test.go +++ b/backend/internal/service/openai_compat_model_test.go @@ -18,6 +18,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -997,7 +998,7 @@ 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], codexCLIUserAgent, "codex_cli_rs") + requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], codexCLIUserAgent, 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() @@ -1011,7 +1012,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(isolateOpenAISessionID(0, "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], codexCLIUserAgent, "codex_cli_rs") + requireOpenAIMessagesCodexIdentity(t, upstream.requests[1], codexCLIUserAgent, 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()) } @@ -1062,7 +1063,7 @@ 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, codexCLIUserAgent, "codex_cli_rs") + requireOpenAIMessagesCodexIdentity(t, upstream.lastReq, codexCLIUserAgent, openai.CodexDefaultOriginator) }) } } @@ -1105,7 +1106,7 @@ 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], codexCLIUserAgent, "codex_cli_rs") + requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], codexCLIUserAgent, 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}`) @@ -1120,7 +1121,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], codexCLIUserAgent, "codex_cli_rs") + requireOpenAIMessagesCodexIdentity(t, upstream.requests[1], codexCLIUserAgent, 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()) } @@ -1275,7 +1276,7 @@ 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], codexCLIUserAgent, "codex_cli_rs") + requireOpenAIMessagesCodexIdentity(t, upstream.requests[0], codexCLIUserAgent, openai.CodexDefaultOriginator) } func TestForwardAsAnthropic_OAuthAddsClaudeCodeTodoGuardForCompatModel(t *testing.T) { diff --git a/backend/internal/service/openai_gateway_grok_cache.go b/backend/internal/service/openai_gateway_grok_cache.go index 170528ce7..4c6923e79 100644 --- a/backend/internal/service/openai_gateway_grok_cache.go +++ b/backend/internal/service/openai_gateway_grok_cache.go @@ -104,7 +104,7 @@ func resolveGrokCacheIdentity(c *gin.Context, body []byte, explicitKey, upstream func explicitGrokCacheSeed(c *gin.Context, body []byte, explicitKey string) string { // Claude Code session is the most stable multi-turn identity for // /v1/messages → Grok bridges. Prefer it over generic session headers so - // prompt cache routing matches CPA behavior. + // prompt cache routing follows the gateway's existing cache affinity rules. seed := extractClaudeCodeSessionID(c, body) if seed == "" { seed = explicitOpenAIHeaderSessionID(c) diff --git a/backend/internal/service/openai_gateway_passthrough.go b/backend/internal/service/openai_gateway_passthrough.go index c86eb4f48..b1a3d189c 100644 --- a/backend/internal/service/openai_gateway_passthrough.go +++ b/backend/internal/service/openai_gateway_passthrough.go @@ -17,6 +17,7 @@ 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" @@ -413,7 +414,7 @@ func (s *OpenAIGatewayService) buildUpstreamRequestOpenAIPassthrough( req.Header.Set("OpenAI-Beta", "responses=experimental") } if req.Header.Get("originator") == "" { - req.Header.Set("originator", "codex_cli_rs") + req.Header.Set("originator", openai.CodexDefaultOriginator) } // 用隔离后的 session 标识符覆盖客户端透传值,防止跨用户会话碰撞。 if clientSessionID == "" { diff --git a/backend/internal/service/openai_gateway_scheduling.go b/backend/internal/service/openai_gateway_scheduling.go index c70a03802..23c04daf0 100644 --- a/backend/internal/service/openai_gateway_scheduling.go +++ b/backend/internal/service/openai_gateway_scheduling.go @@ -14,6 +14,7 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/xai" "github.com/gin-gonic/gin" "github.com/tidwall/gjson" @@ -157,7 +158,7 @@ func resolveOpenAIUpstreamOriginator(c *gin.Context, isOfficialClient bool) stri } } if isOfficialClient { - return "codex_cli_rs" + return openai.CodexDefaultOriginator } return "opencode" } diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index f2e00dda4..135f63aa8 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -33,7 +33,7 @@ const ( openaiPlatformAPIURL = "https://api.openai.com/v1/responses" openaiPlatformAPIInputTokensURL = "https://api.openai.com/v1/responses/input_tokens" openaiStickySessionTTL = time.Hour // 粘性会话TTL - // 与真实 Codex CLI 的 User-Agent 结构对齐: + // 与真实 Codex TUI 的 User-Agent 结构对齐: // {originator}/{version} ({OS} {OS_version}; {arch}) {terminal} // 缺少 OS/架构/终端后缀的形态易被上游指纹识别为非官方客户端。 // 该后缀是 UA 形态的唯一定义处,buildCodexCLIUserAgent 按运行时版本号复用它。 @@ -41,7 +41,7 @@ const ( // codexCLIUserAgent 是编译期兜底 UA;运行时优先使用由后台版本号拼出的规范 UA。 // 版本段必须来自 codexCLIVersion:UA 与 version 头是同一个版本声明的两个出口, // 各自硬编码会漂移成互相矛盾的身份。 - codexCLIUserAgent = "codex_cli_rs/" + codexCLIVersion + codexCLIUserAgentSuffix + codexCLIUserAgent = openai.CodexDefaultOriginator + "/" + codexCLIVersion + codexCLIUserAgentSuffix // codex_cli_only 拒绝时单个请求头日志长度上限(字符) codexCLIOnlyHeaderValueMaxBytes = 256 diff --git a/backend/internal/service/openai_gateway_service_test.go b/backend/internal/service/openai_gateway_service_test.go index b19c7a891..4ec35804c 100644 --- a/backend/internal/service/openai_gateway_service_test.go +++ b/backend/internal/service/openai_gateway_service_test.go @@ -3030,7 +3030,7 @@ func TestOpenAIBuildUpstreamRequestOAuthOfficialClientOriginatorCompatibility(t isCodexCLI := openai.IsCodexOfficialClientByHeaders(c.GetHeader("User-Agent"), c.GetHeader("originator")) req, err := svc.buildUpstreamRequest(c.Request.Context(), c, account, []byte(`{"model":"gpt-5"}`), "token", false, "", isCodexCLI) require.NoError(t, err) - require.Equal(t, "codex_cli_rs", req.Header.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, req.Header.Get("originator")) require.Equal(t, codexCLIUserAgent, req.Header.Get("User-Agent")) require.Equal(t, codexCLIVersion, req.Header.Get("version")) }) diff --git a/backend/internal/service/openai_oauth_passthrough_test.go b/backend/internal/service/openai_oauth_passthrough_test.go index 31f30a1d9..d17f52472 100644 --- a/backend/internal/service/openai_oauth_passthrough_test.go +++ b/backend/internal/service/openai_oauth_passthrough_test.go @@ -16,6 +16,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/config" "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat" "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint" "github.com/gin-gonic/gin" @@ -989,10 +990,10 @@ func TestOpenAIGatewayService_OAuthLegacy_CompositeCodexUAUsesCodexOriginator(t _, err := svc.Forward(context.Background(), c, account, inputBody) require.NoError(t, err) require.NotNil(t, upstream.lastReq) - // 浏览器型复合 UA 被替换为默认 Codex UA(CLI 形态,避开上游降载桶), + // 浏览器型复合 UA 被替换为默认 Codex TUI UA, // originator 随最终 UA 配套(issue #3901)。 require.Equal(t, DefaultOpenAICodexUserAgent, upstream.lastReq.Header.Get("User-Agent")) - require.Equal(t, "codex_cli_rs", upstream.lastReq.Header.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, upstream.lastReq.Header.Get("originator")) require.NotEqual(t, "opencode", upstream.lastReq.Header.Get("originator")) } @@ -1831,7 +1832,7 @@ func TestOpenAIGatewayService_OAuthPassthrough_OfficialIdentityUnified(t *testin require.NoError(t, err) require.NotNil(t, upstream.lastReq) require.Equal(t, codexCLIUserAgent, upstream.lastReq.Header.Get("User-Agent")) - require.Equal(t, "codex_cli_rs", upstream.lastReq.Header.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, upstream.lastReq.Header.Get("originator")) require.Equal(t, codexCLIVersion, upstream.lastReq.Header.Get("version")) } @@ -1875,7 +1876,7 @@ func TestOpenAIGatewayService_OAuthPassthrough_CodexTuiIdentityUnified(t *testin require.NoError(t, err) require.NotNil(t, upstream.lastReq) require.Equal(t, codexCLIUserAgent, upstream.lastReq.Header.Get("User-Agent")) - require.Equal(t, "codex_cli_rs", upstream.lastReq.Header.Get("originator")) + 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_ws_forwarder_success_test.go b/backend/internal/service/openai_ws_forwarder_success_test.go index ef0a11045..04bd35649 100644 --- a/backend/internal/service/openai_ws_forwarder_success_test.go +++ b/backend/internal/service/openai_ws_forwarder_success_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/pkg/openai" coderws "github.com/coder/websocket" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" @@ -839,7 +840,7 @@ func TestOpenAIGatewayService_Forward_WSv2_OAuthOriginatorCompatibility(t *testi result, err := svc.Forward(context.Background(), c, account, body) require.NoError(t, err) require.NotNil(t, result) - require.Equal(t, "codex_cli_rs", captureDialer.lastHeaders.Get("originator")) + require.Equal(t, openai.CodexDefaultOriginator, captureDialer.lastHeaders.Get("originator")) require.Equal(t, codexCLIUserAgent, captureDialer.lastHeaders.Get("user-agent")) require.Equal(t, codexCLIVersion, captureDialer.lastHeaders.Get("version")) }) diff --git a/backend/internal/service/setting_gateway_runtime.go b/backend/internal/service/setting_gateway_runtime.go index 93dfd8546..d2d614b34 100644 --- a/backend/internal/service/setting_gateway_runtime.go +++ b/backend/internal/service/setting_gateway_runtime.go @@ -82,10 +82,8 @@ const antigravityUserAgentVersionCacheTTL = 60 * time.Second const antigravityUserAgentVersionErrorTTL = 5 * time.Second const antigravityUserAgentVersionDBTimeout = 5 * time.Second -// DefaultOpenAICodexUserAgent OpenAI Codex 默认 User-Agent(用于规避 Cloudflare 对浏览器 UA 的质询)。 -// 取官方 CLI 身份而非 TUI 身份:上游在容量紧张时按客户端身份分优先级降载,TUI 身份被观测到 -// 优先命中降载,而该默认值是浏览器 UA 兜底路径上最主要的身份来源。版本段随 codexCLIVersion -// 一起更新——陈旧版本同样会被优先降载。 +// DefaultOpenAICodexUserAgent 是 OpenAI Codex 默认 User-Agent,用于规避浏览器 UA 的质询。 +// 默认采用 codex-tui 身份,版本段随 codexCLIVersion 一起更新。 const DefaultOpenAICodexUserAgent = codexCLIUserAgent // cachedOpenAICodexUserAgent 缓存 OpenAI Codex UA(进程内缓存,60s TTL) @@ -366,7 +364,7 @@ func (s *SettingService) InvalidateOpenAICodexClientVersionCache() { } // GetOpenAICodexCanonicalUserAgent 返回出站规范 Codex User-Agent。 -// 未填面板 UA 时按当前生效的客户端版本号拼出标准 CLI UA。 +// 未填面板 UA 时按当前生效的客户端版本号拼出标准 Codex TUI UA。 // // 面板 UA 只贡献客户端名与 OS / 架构 / 终端指纹,版本段一律用生效版本重建:该输入框是 // 唯一能改 UA 后缀的地方,但它填写于某个历史版本,逐字沿用会把出站身份永久钉死在陈旧 diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 3854f4663..30024ebbe 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -228,7 +228,7 @@ type SystemSettings struct { EnableClientDatelineNormalization bool // 是否对 Anthropic OAuth/SetupToken 请求体做客户端 dateline 归一化(默认 true) RewriteMessageCacheControl bool // 是否改写 messages[*].content[*].cache_control(默认 false) AntigravityUserAgentVersion string // Antigravity 上游 User-Agent 版本号;空值使用配置/默认值 - OpenAICodexUserAgent string // OpenAI Codex 上游完整 User-Agent;空值由 Codex 客户端版本号拼出标准 CLI UA + OpenAICodexUserAgent string // OpenAI Codex 上游完整 User-Agent;空值由 Codex 客户端版本号拼出标准 TUI UA OpenAICodexClientVersion string // 出站声明的 Codex 客户端版本号(管理员覆写);空值跟随自动同步值 OpenAICodexClientVersionSynced string // 自动同步到的官方最新稳定版版本号(只读展示) OpenAICodexVersionAutoSyncEnabled bool // 是否启用 Codex 客户端版本号自动同步(默认 true) From b6e53c9320e770b14c9b3ea60406d7e817409520 Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 7 Aug 2026 10:06:28 +0800 Subject: [PATCH 2/2] fix(frontend): align Codex UA setting copy --- frontend/src/i18n/locales/en/admin/settings.ts | 4 ++-- frontend/src/i18n/locales/zh/admin/settings.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/i18n/locales/en/admin/settings.ts b/frontend/src/i18n/locales/en/admin/settings.ts index 92d562670..60630aaf3 100644 --- a/frontend/src/i18n/locales/en/admin/settings.ts +++ b/frontend/src/i18n/locales/en/admin/settings.ts @@ -475,8 +475,8 @@ export default { antigravityUserAgentVersionPlaceholder: '1.23.2', antigravityUserAgentVersionHint: 'Leave empty to use ANTIGRAVITY_USER_AGENT_VERSION or the built-in default 1.23.2; when set, the admin setting takes precedence.', openaiCodexUserAgent: 'OpenAI Codex UA', - openaiCodexUserAgentPlaceholder: 'codex_cli_rs/0.146.0 (Ubuntu 22.4.0; x86_64) xterm-256color', - openaiCodexUserAgentHint: 'The full Codex User-Agent used for all outbound requests, for customizing the OS / arch / terminal fingerprint. Leave empty to build the standard codex_cli_rs identity from the version below (recommended). If set, its version segment is still replaced by the version below, so the UA never stays pinned to whatever release you typed it on — under capacity pressure the upstream sheds load by client identity and drops stale or non-official identities first with server_is_overloaded.', + openaiCodexUserAgentPlaceholder: 'codex-tui/0.146.1 (Ubuntu 22.4.0; x86_64) WindowsTerminal (codex-tui; 0.146.1)', + openaiCodexUserAgentHint: 'The full Codex User-Agent used for all outbound requests, for customizing the OS / arch / terminal fingerprint. Leave empty to build the standard codex-tui identity from the version below (recommended). If set, both the leading and trailing version declarations are synchronized to the version below, so the UA never stays pinned to the release entered here — under capacity pressure the upstream sheds load by client identity and drops stale or non-official identities first with server_is_overloaded.', openaiCodexClientVersion: 'Codex client version', openaiCodexClientVersionPlaceholder: 'Leave empty to follow auto-sync', openaiCodexClientVersionHint: 'The Codex client version this gateway declares upstream, shared by the User-Agent and the version header. Leave empty to use the auto-synced latest stable release; setting a value pins it and stops following auto-sync.', diff --git a/frontend/src/i18n/locales/zh/admin/settings.ts b/frontend/src/i18n/locales/zh/admin/settings.ts index 65707928d..952c7b605 100644 --- a/frontend/src/i18n/locales/zh/admin/settings.ts +++ b/frontend/src/i18n/locales/zh/admin/settings.ts @@ -468,8 +468,8 @@ export default { antigravityUserAgentVersionPlaceholder: '1.23.2', antigravityUserAgentVersionHint: '留空时使用 ANTIGRAVITY_USER_AGENT_VERSION 或内置默认值 1.23.2;填写后后台设置优先。', openaiCodexUserAgent: 'OpenAI Codex UA', - openaiCodexUserAgentPlaceholder: 'codex_cli_rs/0.146.0 (Ubuntu 22.4.0; x86_64) xterm-256color', - openaiCodexUserAgentHint: '出站统一使用的完整 Codex User-Agent,用于自定义 OS / 架构 / 终端指纹。留空则按下方版本号拼出标准 codex_cli_rs 形态(推荐)。填写后其版本段仍会被下方版本号覆盖,避免这条 UA 停在填写时的旧版本——上游在容量紧张时按客户端身份分优先级降载,陈旧或非官方形态的身份会被优先丢弃并回 server_is_overloaded。', + openaiCodexUserAgentPlaceholder: 'codex-tui/0.146.1 (Ubuntu 22.4.0; x86_64) WindowsTerminal (codex-tui; 0.146.1)', + openaiCodexUserAgentHint: '出站统一使用的完整 Codex User-Agent,用于自定义 OS / 架构 / 终端指纹。留空则按下方版本号拼出标准 codex-tui 形态(推荐)。填写后首段和尾部的版本号仍会被下方版本号同步覆盖,避免这条 UA 停在填写时的旧版本——上游在容量紧张时按客户端身份分优先级降载,陈旧或非官方形态的身份会被优先丢弃并回 server_is_overloaded。', openaiCodexClientVersion: 'Codex 客户端版本号', openaiCodexClientVersionPlaceholder: '留空则跟随自动同步', openaiCodexClientVersionHint: '网关对上游声明的 Codex 客户端版本号,User-Agent 与 version 头同源使用。留空表示使用自动同步到的官方最新稳定版;填写后固定为该版本,不再跟随同步。',