mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3760 from DaydreamCoding/fix/codex-detect-version-gate-message
fix(codex-detect): codex 版本门拒绝返回带版本号的差异化文案
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
@@ -8,6 +9,11 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CodexOfficialClientsOnlyMessage 是 codex_cli_only 拒绝时面向客户端的通用兜底文案。
|
||||
// 仅当拒绝原因不是「可解析版本但越界」(VersionTooLow/VersionTooHigh)时使用:
|
||||
// 未命中官方/黑名单/缺指纹/版本无法识别都沿用这句(避免向伪装客户端泄露门控细节)。
|
||||
const CodexOfficialClientsOnlyMessage = "This account only allows Codex official clients"
|
||||
|
||||
const (
|
||||
// CodexClientRestrictionReasonDisabled 表示账号未开启 codex_cli_only。
|
||||
CodexClientRestrictionReasonDisabled = "codex_cli_only_disabled"
|
||||
@@ -51,6 +57,13 @@ type CodexClientRestrictionDetectionResult struct {
|
||||
Enabled bool
|
||||
Matched bool
|
||||
Reason string
|
||||
// DetectedVersion 是从官方 UA 解析出的 Codex 引擎版本;仅在版本门拒绝
|
||||
// (VersionTooLow / VersionTooHigh) 时填充,供面向客户端的差异化文案使用。
|
||||
DetectedVersion string
|
||||
// MinCodexVersion 是触发 VersionTooLow 时的最低要求版本(来自策略快照)。
|
||||
MinCodexVersion string
|
||||
// MaxCodexVersion 是触发 VersionTooHigh 时的最高允许版本(来自策略快照)。
|
||||
MaxCodexVersion string
|
||||
}
|
||||
|
||||
// CodexClientRestrictionDetector 定义 codex_cli_only 统一检测入口。
|
||||
@@ -127,10 +140,22 @@ func (d *OpenAICodexClientRestrictionDetector) Detect(c *gin.Context, account *A
|
||||
return CodexClientRestrictionDetectionResult{Enabled: true, Matched: false, Reason: CodexClientRestrictionReasonVersionUndetectable}
|
||||
}
|
||||
if policy.MinCodexVersion != "" && CompareVersions(ver, policy.MinCodexVersion) < 0 {
|
||||
return CodexClientRestrictionDetectionResult{Enabled: true, Matched: false, Reason: CodexClientRestrictionReasonVersionTooLow}
|
||||
return CodexClientRestrictionDetectionResult{
|
||||
Enabled: true,
|
||||
Matched: false,
|
||||
Reason: CodexClientRestrictionReasonVersionTooLow,
|
||||
DetectedVersion: ver,
|
||||
MinCodexVersion: policy.MinCodexVersion,
|
||||
}
|
||||
}
|
||||
if policy.MaxCodexVersion != "" && CompareVersions(ver, policy.MaxCodexVersion) > 0 {
|
||||
return CodexClientRestrictionDetectionResult{Enabled: true, Matched: false, Reason: CodexClientRestrictionReasonVersionTooHigh}
|
||||
return CodexClientRestrictionDetectionResult{
|
||||
Enabled: true,
|
||||
Matched: false,
|
||||
Reason: CodexClientRestrictionReasonVersionTooHigh,
|
||||
DetectedVersion: ver,
|
||||
MaxCodexVersion: policy.MaxCodexVersion,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,3 +170,22 @@ func (d *OpenAICodexClientRestrictionDetector) Detect(c *gin.Context, account *A
|
||||
|
||||
return CodexClientRestrictionDetectionResult{Enabled: true, Matched: true, Reason: reason}
|
||||
}
|
||||
|
||||
// CodexClientRestrictionMessage 把检测结果映射为面向客户端的 403 文案。
|
||||
// 仅版本越界(VersionTooLow/VersionTooHigh)给出带实际版本号与边界的差异化提示——
|
||||
// 这类请求其实已被识别为官方 Codex(命中官方 UA/originator),再回「只允许官方客户端」会误导;
|
||||
// 其余拒绝原因统一沿用通用兜底句,不暴露门控细节。
|
||||
func CodexClientRestrictionMessage(r CodexClientRestrictionDetectionResult) string {
|
||||
switch r.Reason {
|
||||
case CodexClientRestrictionReasonVersionTooLow:
|
||||
return fmt.Sprintf(
|
||||
"Your Codex version (%s) is below the minimum required version (%s). Please update Codex.",
|
||||
r.DetectedVersion, r.MinCodexVersion)
|
||||
case CodexClientRestrictionReasonVersionTooHigh:
|
||||
return fmt.Sprintf(
|
||||
"Your Codex version (%s) exceeds the maximum allowed version (%s). Please downgrade Codex to %s or lower.",
|
||||
r.DetectedVersion, r.MaxCodexVersion, r.MaxCodexVersion)
|
||||
default:
|
||||
return CodexOfficialClientsOnlyMessage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +284,66 @@ func TestDetect_V3_AppServerAndSkipAndVersionScope(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetect_VersionGateCarriesVersionFields(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
d := NewOpenAICodexClientRestrictionDetector(nil)
|
||||
acc := func() *Account {
|
||||
return &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: map[string]any{"codex_cli_only": true}}
|
||||
}
|
||||
|
||||
t.Run("版本太低:携带 DetectedVersion + MinCodexVersion", func(t *testing.T) {
|
||||
c := newCodexDetectorTestContext("codex_cli_rs/0.39.0 (x)", "")
|
||||
r := d.Detect(c, acc(), CodexRestrictionPolicy{MinCodexVersion: "0.42.0"}, nil)
|
||||
require.False(t, r.Matched)
|
||||
require.Equal(t, CodexClientRestrictionReasonVersionTooLow, r.Reason)
|
||||
require.Equal(t, "0.39.0", r.DetectedVersion)
|
||||
require.Equal(t, "0.42.0", r.MinCodexVersion)
|
||||
})
|
||||
|
||||
t.Run("版本太高:携带 DetectedVersion + MaxCodexVersion", func(t *testing.T) {
|
||||
c := newCodexDetectorTestContext("codex_cli_rs/0.45.0 (x)", "")
|
||||
r := d.Detect(c, acc(), CodexRestrictionPolicy{MaxCodexVersion: "0.42.0"}, nil)
|
||||
require.False(t, r.Matched)
|
||||
require.Equal(t, CodexClientRestrictionReasonVersionTooHigh, r.Reason)
|
||||
require.Equal(t, "0.45.0", r.DetectedVersion)
|
||||
require.Equal(t, "0.42.0", r.MaxCodexVersion)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCodexClientRestrictionMessage(t *testing.T) {
|
||||
t.Run("版本太低:带实际版本与最低要求", func(t *testing.T) {
|
||||
msg := CodexClientRestrictionMessage(CodexClientRestrictionDetectionResult{
|
||||
Reason: CodexClientRestrictionReasonVersionTooLow,
|
||||
DetectedVersion: "0.39.0",
|
||||
MinCodexVersion: "0.42.0",
|
||||
})
|
||||
require.Equal(t, "Your Codex version (0.39.0) is below the minimum required version (0.42.0). Please update Codex.", msg)
|
||||
})
|
||||
|
||||
t.Run("版本太高:带实际版本与最高允许", func(t *testing.T) {
|
||||
msg := CodexClientRestrictionMessage(CodexClientRestrictionDetectionResult{
|
||||
Reason: CodexClientRestrictionReasonVersionTooHigh,
|
||||
DetectedVersion: "0.45.0",
|
||||
MaxCodexVersion: "0.42.0",
|
||||
})
|
||||
require.Equal(t, "Your Codex version (0.45.0) exceeds the maximum allowed version (0.42.0). Please downgrade Codex to 0.42.0 or lower.", msg)
|
||||
})
|
||||
|
||||
t.Run("无法识别版本:保持原通用句", func(t *testing.T) {
|
||||
msg := CodexClientRestrictionMessage(CodexClientRestrictionDetectionResult{
|
||||
Reason: CodexClientRestrictionReasonVersionUndetectable,
|
||||
})
|
||||
require.Equal(t, "This account only allows Codex official clients", msg)
|
||||
})
|
||||
|
||||
t.Run("未命中官方:保持原通用句", func(t *testing.T) {
|
||||
msg := CodexClientRestrictionMessage(CodexClientRestrictionDetectionResult{
|
||||
Reason: CodexClientRestrictionReasonNotMatchedUA,
|
||||
})
|
||||
require.Equal(t, "This account only allows Codex official clients", msg)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDetect_EngineFingerprintSignals(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
det := NewOpenAICodexClientRestrictionDetector(&config.Config{})
|
||||
|
||||
@@ -2617,7 +2617,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": gin.H{
|
||||
"type": "forbidden_error",
|
||||
"message": "This account only allows Codex official clients",
|
||||
"message": CodexClientRestrictionMessage(restrictionResult),
|
||||
},
|
||||
})
|
||||
return nil, errors.New("codex_cli_only restriction: only codex official clients are allowed")
|
||||
|
||||
@@ -59,6 +59,52 @@ func TestOpenAIGatewayService_GetCodexClientRestrictionDetector(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenAIGatewayService_Forward_VersionGateMessage(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
newCtx := func() (*httptest.ResponseRecorder, *gin.Context) {
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(nil))
|
||||
return rec, c
|
||||
}
|
||||
account := func() *Account {
|
||||
return &Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: map[string]any{"codex_cli_only": true}}
|
||||
}
|
||||
body := []byte(`{"model":"gpt-5.1-codex"}`)
|
||||
|
||||
t.Run("版本太低:返回带版本号的差异化文案", func(t *testing.T) {
|
||||
rec, c := newCtx()
|
||||
svc := &OpenAIGatewayService{codexDetector: &stubCodexRestrictionDetector{result: CodexClientRestrictionDetectionResult{
|
||||
Enabled: true,
|
||||
Matched: false,
|
||||
Reason: CodexClientRestrictionReasonVersionTooLow,
|
||||
DetectedVersion: "0.39.0",
|
||||
MinCodexVersion: "0.42.0",
|
||||
}}}
|
||||
|
||||
_, err := svc.Forward(context.Background(), c, account(), body)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "Your Codex version (0.39.0) is below the minimum required version (0.42.0)")
|
||||
require.NotContains(t, rec.Body.String(), "This account only allows Codex official clients")
|
||||
})
|
||||
|
||||
t.Run("未命中官方:仍返回通用兜底文案", func(t *testing.T) {
|
||||
rec, c := newCtx()
|
||||
svc := &OpenAIGatewayService{codexDetector: &stubCodexRestrictionDetector{result: CodexClientRestrictionDetectionResult{
|
||||
Enabled: true,
|
||||
Matched: false,
|
||||
Reason: CodexClientRestrictionReasonNotMatchedUA,
|
||||
}}}
|
||||
|
||||
_, err := svc.Forward(context.Background(), c, account(), body)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, rec.Code)
|
||||
require.Contains(t, rec.Body.String(), "This account only allows Codex official clients")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAPIKeyIDFromContext(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user