Files
sub2api/backend/internal/pkg/openai/request_load_shed_test.go
T
shaw e1b76e2245 fix(codex): normalize load-shed originators to avoid upstream capacity shedding
上游 /backend-api/codex 按 Originator 头分桶调度容量:落在降载桶的请求即使返回
HTTP 200,也会立刻推 SSE `event: error`(code=server_is_overloaded)并以
response.failed 收尾。2026-07-29 起 codex-tui 落入降载桶,codex_cli_rs 正常——
判定因子是 originator 而非 User-Agent(codex_cli_rs 配 curl UA 亦可正常返回)。

网关会把该错误判定为瞬时上游故障并冷却账号,对外表现为 Codex 账号频繁过载不可用:
server_is_overloaded → isOpenAITransientProcessingError →
shouldCooldownOpenAITransientUpstreamError → 账号冷却 → 客户端 503。

本项目有三处降载身份来源:浏览器 UA 兜底的默认 UA、客户端透传的真实 TUI 身份、
以及指纹缓存注入探针的 UA。

修复收口在 enforceCodexIdentityHeaders——HTTP / 透传 / WS 握手 / compat 桥接 /
探针 / PAT / 模型列表 / alpha-search 八条出站路径共用的唯一纯函数收口点:

- 新增 NormalizeCodexClientIdentityToCLI,把降载桶身份改写为 codex_cli_rs,
  只替换身份段并裁掉尾部 (name; version) 客户端标识组,保留版本 / OS / 架构 /
  终端指纹;改写后 originator 与 UA 首段仍然配套,不破坏 #3901 的配对不变式,
  且改写幂等。
- DefaultOpenAICodexUserAgent 从 TUI 身份改为 CLI 身份(浏览器兜底路径上最大的
  降载身份来源)。
- 管理端 Codex UA 的 placeholder / hint 原本在把管理员往降载桶引导,一并修正。

新增 gateway.disable_codex_originator_normalization(默认 false,即归一化开启),
供上游调整分桶后回滚。该开关经 NewOpenAIGatewayService 发布为进程级快照,故必须
保持反义命名:正向命名的 Go 零值 false 会让未经 viper 加载而手工构造的 Config
静默关掉全局保护,viper.SetDefault 救不了这条路径。已加用例钉住该属性。

降载桶集合是上游容量策略快照而非协议常量,上游调整分桶后需同步修订。
2026-08-02 23:00:12 +08:00

107 lines
3.6 KiB
Go

package openai
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestIsCodexLoadShedOriginator(t *testing.T) {
require.True(t, IsCodexLoadShedOriginator("codex-tui"))
require.True(t, IsCodexLoadShedOriginator(" CODEX-TUI "))
require.False(t, IsCodexLoadShedOriginator("codex_cli_rs"))
require.False(t, IsCodexLoadShedOriginator("codex_vscode"))
require.False(t, IsCodexLoadShedOriginator("Codex Desktop"))
require.False(t, IsCodexLoadShedOriginator(""))
}
func TestNormalizeCodexClientIdentityToCLI(t *testing.T) {
tests := []struct {
name string
originator string
ua string
wantOriginator string
wantUA string
wantChanged bool
}{
{
name: "tui 完整 UA 改写首段并裁掉客户端标识组",
originator: "codex-tui",
ua: "codex-tui/0.144.1 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.144.1)",
wantOriginator: "codex_cli_rs",
wantUA: "codex_cli_rs/0.144.1 (Ubuntu 22.4.0; x86_64) xterm-256color",
wantChanged: true,
},
{
name: "无客户端标识组时仅改写首段",
originator: "codex-tui",
ua: "codex-tui/0.144.1 (Mac OS X 14.0; arm64)",
wantOriginator: "codex_cli_rs",
wantUA: "codex_cli_rs/0.144.1 (Mac OS X 14.0; arm64)",
wantChanged: true,
},
{
name: "OS 括号组不是客户端标识不得被裁剪",
originator: "codex-tui",
ua: "codex-tui/0.144.1 (Ubuntu 22.4.0; x86_64)",
wantOriginator: "codex_cli_rs",
wantUA: "codex_cli_rs/0.144.1 (Ubuntu 22.4.0; x86_64)",
wantChanged: true,
},
{
name: "缺少版本段时只替换 originator",
originator: "codex-tui",
ua: "codex-tui",
wantOriginator: "codex_cli_rs",
wantUA: "codex-tui",
wantChanged: true,
},
{
name: "健康身份原样返回",
originator: "codex_cli_rs",
ua: "codex_cli_rs/0.144.1 (Ubuntu 22.4.0; x86_64) xterm-256color",
wantOriginator: "codex_cli_rs",
wantUA: "codex_cli_rs/0.144.1 (Ubuntu 22.4.0; x86_64) xterm-256color",
wantChanged: false,
},
{
name: "其他官方身份不受影响",
originator: "codex_vscode",
ua: "codex_vscode/1.0.0 (Ubuntu 22.4.0; x86_64) vscode (codex_vscode; 1.0.0)",
wantOriginator: "codex_vscode",
wantUA: "codex_vscode/1.0.0 (Ubuntu 22.4.0; x86_64) vscode (codex_vscode; 1.0.0)",
wantChanged: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotOriginator, gotUA, changed := NormalizeCodexClientIdentityToCLI(tt.originator, tt.ua)
require.Equal(t, tt.wantOriginator, gotOriginator)
require.Equal(t, tt.wantUA, gotUA)
require.Equal(t, tt.wantChanged, changed)
})
}
}
// 归一化后的身份必须仍然通过上游的 originator ↔ UA 首段配对校验(issue #3901),
// 且再次归一化保持幂等。
func TestNormalizeCodexClientIdentityToCLIStaysPaired(t *testing.T) {
originator, ua, changed := NormalizeCodexClientIdentityToCLI(
"codex-tui",
"codex-tui/0.144.1 (Ubuntu 22.4.0; x86_64) xterm-256color (codex-tui; 0.144.1)",
)
require.True(t, changed)
pairedOriginator, pairedUA, ok := PairCodexClientIdentity(ua)
require.True(t, ok)
require.Equal(t, originator, pairedOriginator)
require.Equal(t, ua, pairedUA)
againOriginator, againUA, againChanged := NormalizeCodexClientIdentityToCLI(originator, ua)
require.False(t, againChanged)
require.Equal(t, originator, againOriginator)
require.Equal(t, ua, againUA)
}