fix(openai): default OAuth identity to codex-tui
This commit is contained in:
1 parent
e4b0e1b66b
commit
dbb42881c0
25 files changed
+81
-55
No files matched your search
@@ -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():
|
||||
|
||||
@@ -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 版本段逐字一致。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
// 生效版本只有一个来源:规范身份(面板版本号 → 自动同步值 → 内置常量,见
|
||||
|
||||
@@ -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(""))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"))
|
||||
})
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))
|
||||
})
|
||||
|
||||
@@ -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 后缀的地方,但它填写于某个历史版本,逐字沿用会把出站身份永久钉死在陈旧
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user