mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-11 06:31:39 +00:00
上游 /backend-api/codex 在容量紧张时按客户端身份分优先级降载,被降载的请求 HTTP 200 后立刻推流内 server_is_overloaded。此前网关对配不出官方身份的客户端 整体回退到硬编码的 codex_cli_rs/0.144.1(落后官方 4 个发布),这些请求稳定 落在被优先丢弃的一侧。 - 强制统一出口:所有 OAuth 出站的 User-Agent / originator / version 一律改写 为网关规范身份,客户端自报身份不参与构造;HTTP / 透传 / WS / alpha-search / 探针全覆盖。compat 桥接故意删除 originator 的路径保持 no-op。 - 版本号收敛为单一来源,运行时优先级为面板覆写 → 自动同步值 → 内置常量; UA 与 version 头同源派生,不再各自硬编码。 - 新增 3 小时自动同步官方客户端最新稳定版,面板可关闭,无需为跟版本而发版。 - 流内 server_is_overloaded / slow_down 改为先在同账号有界重试再切号,并标记为 请求级瞬时故障,不再据此临时封禁账号。 - 移除被取代的降载身份黑名单、浏览器 UA 兜底及其辅助函数。
304 lines
11 KiB
Go
304 lines
11 KiB
Go
package service
|
||
|
||
import (
|
||
"net/http"
|
||
"testing"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||
"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, codexCLIVersion, h.Get("Version"))
|
||
require.Equal(t, "responses=experimental", h.Get("OpenAI-Beta"))
|
||
require.NotEmpty(t, h.Get("X-Codex-Window-ID"))
|
||
}
|
||
|
||
// 强制统一出口:无论客户端自报什么身份,OAuth 出站的 User-Agent / originator / version
|
||
// 一律是网关规范身份。上游在容量紧张时按客户端身份分优先级降载,统一出口确保没有请求
|
||
// 带着第三方或陈旧身份出站。
|
||
func TestEnsureCodexIdentityHeaders(t *testing.T) {
|
||
t.Run("补齐缺失身份头", func(t *testing.T) {
|
||
h := make(http.Header)
|
||
|
||
ensureCodexIdentityHeaders(h)
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Equal(t, "codex_cli_rs", 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"))
|
||
})
|
||
|
||
t.Run("官方非 CLI 客户端身份同样被统一", func(t *testing.T) {
|
||
h := make(http.Header)
|
||
h.Set("user-agent", "codex_vscode/9.9.9 (Mac OS X 14.0; arm64) vscode (codex_vscode; 9.9.9)")
|
||
h.Set("version", "9.9.9")
|
||
h.Set("OpenAI-Beta", "assistants=v2")
|
||
|
||
ensureCodexIdentityHeaders(h)
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Equal(t, "codex_cli_rs", 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"))
|
||
})
|
||
}
|
||
|
||
func TestEnforceCodexIdentityHeaders(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
originator string
|
||
userAgent string
|
||
version string
|
||
}{
|
||
{
|
||
name: "TUI 身份",
|
||
originator: "codex-tui",
|
||
userAgent: "codex-tui/0.140.2 (Mac OS X 14.0; arm64) iTerm (codex-tui; 0.140.2)",
|
||
},
|
||
{
|
||
name: "错配 originator",
|
||
originator: "codex_cli_rs",
|
||
userAgent: "codex-tui/0.140.2 (Mac OS X 14.0; arm64) iTerm (codex-tui; 0.140.2)",
|
||
},
|
||
{
|
||
name: "官方 vscode 身份",
|
||
originator: "codex_vscode",
|
||
userAgent: "codex_vscode/1.2.3 (Ubuntu 22.4.0; x86_64) vscode (codex_vscode; 1.2.3)",
|
||
},
|
||
{
|
||
name: "第三方客户端身份",
|
||
originator: "opencode",
|
||
userAgent: "luna/1.0.0",
|
||
version: "2.1.0",
|
||
},
|
||
{
|
||
name: "浏览器型 UA(原浏览器兜底已被统一出口吸收)",
|
||
originator: "codex_cli_rs",
|
||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||
},
|
||
{
|
||
name: "UA 缺失",
|
||
originator: "codex_vscode",
|
||
},
|
||
{
|
||
name: "originator override 的真实 TUI 客户端",
|
||
originator: "cccc",
|
||
userAgent: "cccc/0.142.0 (Ubuntu 22.4.0; x86_64) screen (codex-tui; 0.142.0)",
|
||
},
|
||
{
|
||
name: "陈旧客户端版本",
|
||
originator: "codex_cli_rs",
|
||
userAgent: "codex_cli_rs/0.125.0",
|
||
version: "0.125.0",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
h := make(http.Header)
|
||
h.Set("originator", tt.originator)
|
||
if tt.userAgent != "" {
|
||
h.Set("user-agent", tt.userAgent)
|
||
}
|
||
if tt.version != "" {
|
||
h.Set("version", tt.version)
|
||
}
|
||
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Equal(t, "codex_cli_rs", h.Get("originator"))
|
||
require.Equal(t, codexCLIUserAgent, h.Get("user-agent"))
|
||
require.Equal(t, codexCLIVersion, h.Get("version"))
|
||
})
|
||
}
|
||
}
|
||
|
||
// 账号级自定义 UA 是管理员的显式配置,仍然生效;但必须被重新配对出配套的 originator
|
||
// 与同源 version,不允许出现自相矛盾的身份。
|
||
func TestEnforceCodexIdentityHeadersWithAccountOverrideUA(t *testing.T) {
|
||
t.Run("官方形态覆写 UA 被配对并对齐 version", func(t *testing.T) {
|
||
h := make(http.Header)
|
||
h.Set("originator", "codex-tui")
|
||
h.Set("user-agent", "luna/1.0.0")
|
||
h.Set("version", "2.1.0")
|
||
|
||
enforceCodexIdentityHeadersWithUA(h, "codex_vscode/0.150.0 (Ubuntu 22.4.0; x86_64) vscode")
|
||
|
||
require.Equal(t, "codex_vscode", h.Get("originator"))
|
||
require.Equal(t, "codex_vscode/0.150.0 (Ubuntu 22.4.0; x86_64) vscode", h.Get("user-agent"))
|
||
require.Equal(t, "0.150.0", h.Get("version"))
|
||
})
|
||
|
||
t.Run("非官方形态覆写 UA 回退规范身份", func(t *testing.T) {
|
||
h := make(http.Header)
|
||
h.Set("originator", "codex_cli_rs")
|
||
|
||
enforceCodexIdentityHeadersWithUA(h, "luna/1.0.0")
|
||
|
||
require.Equal(t, "codex_cli_rs", h.Get("originator"))
|
||
require.Equal(t, codexCLIUserAgent, h.Get("user-agent"))
|
||
require.Equal(t, codexCLIVersion, h.Get("version"))
|
||
})
|
||
|
||
// 覆写 UA 自报版本低于上游门槛时 version 头必须抬升,否则上游 404(issue #3901)。
|
||
t.Run("覆写 UA 版本低于门槛时抬升 version", func(t *testing.T) {
|
||
h := make(http.Header)
|
||
h.Set("originator", "codex_cli_rs")
|
||
|
||
enforceCodexIdentityHeadersWithUA(h, "codex_cli_rs/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color")
|
||
|
||
require.Equal(t, "codex_cli_rs", h.Get("originator"))
|
||
require.Equal(t, "codex_cli_rs/0.125.0 (Ubuntu 22.4.0; x86_64) xterm-256color", h.Get("user-agent"))
|
||
require.Equal(t, codexCLIVersion, h.Get("version"))
|
||
})
|
||
}
|
||
|
||
// 规范身份跟随注入的解析器(后台面板 UA / 自动同步版本号),无需重启或发版。
|
||
//
|
||
// 不得给本用例加 t.Parallel():它改写进程级解析器。
|
||
func TestEnforceCodexIdentityHeadersFollowsCanonicalResolver(t *testing.T) {
|
||
SetCodexCanonicalUserAgentResolver(func() string {
|
||
return "codex_cli_rs/0.200.1 (Ubuntu 22.4.0; x86_64) xterm-256color"
|
||
})
|
||
t.Cleanup(func() { SetCodexCanonicalUserAgentResolver(nil) })
|
||
|
||
h := make(http.Header)
|
||
h.Set("originator", "codex-tui")
|
||
h.Set("user-agent", "codex-tui/0.140.2 (Mac OS X 14.0; arm64) iTerm (codex-tui; 0.140.2)")
|
||
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Equal(t, "codex_cli_rs", h.Get("originator"))
|
||
require.Equal(t, "codex_cli_rs/0.200.1 (Ubuntu 22.4.0; x86_64) xterm-256color", h.Get("user-agent"))
|
||
require.Equal(t, "0.200.1", h.Get("version"))
|
||
}
|
||
|
||
// 解析器返回非法值(配置被写坏、同步到异常内容)时必须回退到内置身份,
|
||
// 绝不能把不可控内容拼进出站身份。
|
||
//
|
||
// 不得给本用例加 t.Parallel():它改写进程级解析器。
|
||
func TestEnforceCodexIdentityHeadersRejectsInvalidCanonicalUA(t *testing.T) {
|
||
SetCodexCanonicalUserAgentResolver(func() string { return "not-a-codex-client" })
|
||
t.Cleanup(func() { SetCodexCanonicalUserAgentResolver(nil) })
|
||
|
||
h := make(http.Header)
|
||
h.Set("originator", "codex_cli_rs")
|
||
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Equal(t, "codex_cli_rs", h.Get("originator"))
|
||
require.Equal(t, codexCLIUserAgent, h.Get("user-agent"))
|
||
require.Equal(t, codexCLIVersion, h.Get("version"))
|
||
}
|
||
|
||
// 开关是进程级快照,零值 Config(测试 / 工具手工构造,不经 viper)必须落在「强制统一开启」
|
||
// 一侧,否则任意一处零值构造都会静默关掉全局保护。
|
||
//
|
||
// 不得给本文件的开关类用例加 t.Parallel():它们改写进程级状态。
|
||
func TestCodexIdentityEnforcementZeroValueConfigKeepsItEnabled(t *testing.T) {
|
||
var cfg config.Config
|
||
require.False(t, cfg.Gateway.DisableCodexIdentityEnforcement,
|
||
"零值必须表示强制统一开启;若改为正向命名,零值会静默关闭保护")
|
||
|
||
SetCodexIdentityEnforcementEnabled(!cfg.Gateway.DisableCodexIdentityEnforcement)
|
||
t.Cleanup(func() { SetCodexIdentityEnforcementEnabled(true) })
|
||
|
||
h := make(http.Header)
|
||
h.Set("originator", "codex-tui")
|
||
h.Set("user-agent", "codex-tui/0.140.2 (Mac OS X 14.0; arm64) iTerm (codex-tui; 0.140.2)")
|
||
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Equal(t, "codex_cli_rs", h.Get("originator"))
|
||
require.Equal(t, codexCLIUserAgent, h.Get("user-agent"))
|
||
}
|
||
|
||
// 关闭强制统一后退回配对语义:客户端真实身份逐字保留,仅保证 originator 与 UA 首段配套
|
||
// (issue #3901),供上游策略变动时回滚。
|
||
func TestEnforceCodexIdentityHeaders_EnforcementDisabled(t *testing.T) {
|
||
const tuiUA = "codex-tui/0.145.2 (Mac OS X 14.0; arm64) iTerm (codex-tui; 0.145.2)"
|
||
|
||
SetCodexIdentityEnforcementEnabled(false)
|
||
t.Cleanup(func() { SetCodexIdentityEnforcementEnabled(true) })
|
||
|
||
h := make(http.Header)
|
||
h.Set("originator", "codex-tui")
|
||
h.Set("user-agent", tuiUA)
|
||
h.Set("version", "0.145.2")
|
||
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Equal(t, "codex-tui", h.Get("originator"))
|
||
require.Equal(t, tuiUA, h.Get("user-agent"))
|
||
require.Equal(t, "0.145.2", h.Get("version"))
|
||
}
|
||
|
||
// 关闭强制统一后,第三方 UA 仍整体回退为规范身份并对齐 version。
|
||
func TestEnforceCodexIdentityHeaders_EnforcementDisabledThirdPartyFallback(t *testing.T) {
|
||
SetCodexIdentityEnforcementEnabled(false)
|
||
t.Cleanup(func() { SetCodexIdentityEnforcementEnabled(true) })
|
||
|
||
h := make(http.Header)
|
||
h.Set("originator", "opencode")
|
||
h.Set("user-agent", "luna/1.0.0")
|
||
h.Set("version", "2.1.0")
|
||
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Equal(t, "codex_cli_rs", h.Get("originator"))
|
||
require.Equal(t, codexCLIUserAgent, h.Get("user-agent"))
|
||
require.Equal(t, codexCLIVersion, h.Get("version"))
|
||
}
|
||
|
||
// 收口必须幂等:透传等路径可能先后多次经过收口。
|
||
func TestEnforceCodexIdentityHeadersIsIdempotent(t *testing.T) {
|
||
h := make(http.Header)
|
||
h.Set("originator", "codex-tui")
|
||
h.Set("user-agent", "codex-tui/0.140.2 (Mac OS X 14.0; arm64) iTerm (codex-tui; 0.140.2)")
|
||
|
||
enforceCodexIdentityHeaders(h)
|
||
firstUA, firstVersion := h.Get("user-agent"), h.Get("version")
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Equal(t, firstUA, h.Get("user-agent"))
|
||
require.Equal(t, firstVersion, h.Get("version"))
|
||
require.Equal(t, "codex_cli_rs", h.Get("originator"))
|
||
}
|
||
|
||
// 缺少 originator 时必须保持 no-op:compat 桥接等非 ChatGPT 内部接口路径会显式删除
|
||
// originator,不应被补回身份头。
|
||
func TestEnforceCodexIdentityHeaders_NoOriginatorIsNoop(t *testing.T) {
|
||
h := make(http.Header)
|
||
h.Set("user-agent", "third-party-client/1.0.0")
|
||
|
||
enforceCodexIdentityHeaders(h)
|
||
|
||
require.Empty(t, h.Get("originator"))
|
||
require.Empty(t, h.Get("version"))
|
||
require.Equal(t, "third-party-client/1.0.0", h.Get("user-agent"))
|
||
}
|
||
|
||
func TestNormalizeCodexClientVersion(t *testing.T) {
|
||
require.Equal(t, "0.146.0", NormalizeCodexClientVersion(" 0.146.0 "))
|
||
require.Equal(t, "0.147.0-alpha.4", NormalizeCodexClientVersion("0.147.0-alpha.4"))
|
||
require.Equal(t, "1.2", NormalizeCodexClientVersion("1.2"))
|
||
require.Empty(t, NormalizeCodexClientVersion(""))
|
||
require.Empty(t, NormalizeCodexClientVersion("v0.146.0"))
|
||
require.Empty(t, NormalizeCodexClientVersion("0.146.0 (Ubuntu)"))
|
||
require.Empty(t, NormalizeCodexClientVersion("0.146.0\r\nX-Injected: 1"))
|
||
require.Empty(t, NormalizeCodexClientVersion("latest"))
|
||
}
|
||
|
||
func TestBuildCodexCLIUserAgent(t *testing.T) {
|
||
require.Equal(t, "codex_cli_rs/0.200.1"+codexCLIUserAgentSuffix, buildCodexCLIUserAgent("0.200.1"))
|
||
// 非法版本号必须回退到内置 UA,不能拼出畸形身份。
|
||
require.Equal(t, codexCLIUserAgent, buildCodexCLIUserAgent("bogus version"))
|
||
require.Equal(t, codexCLIUserAgent, buildCodexCLIUserAgent(""))
|
||
}
|