From dbb42881c0c6dd0596de2f9367a9c7f45e3b50eb Mon Sep 17 00:00:00 2001 From: shaw Date: Fri, 7 Aug 2026 09:56:03 +0800 Subject: [PATCH] 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)