mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-08 00:54:17 +00:00
上游 /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 救不了这条路径。已加用例钉住该属性。 降载桶集合是上游容量策略快照而非协议常量,上游调整分桶后需同步修订。
89 lines
3.7 KiB
Go
89 lines
3.7 KiB
Go
package service
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
"sync/atomic"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// codexUpstreamMinVersion 上游 /backend-api/codex 接受的最低 version 头:
|
||
// 若请求携带 version 且低于该值,上游直接 404(issue #3901,2026-07 实测)。
|
||
const codexUpstreamMinVersion = "0.144.0"
|
||
|
||
// codexOriginatorNormalization 控制 enforceCodexIdentityHeaders 是否把落在上游降载桶的
|
||
// Codex 身份改写为 CLI 身份,由 gateway.disable_codex_originator_normalization 在服务构造时取反发布。
|
||
// 默认开启:降载桶命中会让上游回 server_is_overloaded,网关据此判定瞬时故障并冷却账号。
|
||
var codexOriginatorNormalization = func() *atomic.Bool {
|
||
v := &atomic.Bool{}
|
||
v.Store(true)
|
||
return v
|
||
}()
|
||
|
||
// SetCodexOriginatorNormalizationEnabled 发布 Codex 降载身份归一化开关。
|
||
// enforceCodexIdentityHeaders 是所有出站路径共用的纯函数收口点,无法在热路径注入配置,
|
||
// 故由持有配置的服务在构造时发布进程级快照。
|
||
func SetCodexOriginatorNormalizationEnabled(enabled bool) {
|
||
codexOriginatorNormalization.Store(enabled)
|
||
}
|
||
|
||
// ensureCodexIdentityHeaders 补齐 OAuth(ChatGPT 内部接口)出站请求所需的 Codex 身份头。
|
||
// 已有 User-Agent 与 version 保持不变,交给紧随其后的 enforceCodexIdentityHeaders
|
||
// 做官方身份配对与最低版本校正。
|
||
func ensureCodexIdentityHeaders(h http.Header) {
|
||
if h == nil {
|
||
return
|
||
}
|
||
if strings.TrimSpace(h.Get("user-agent")) == "" {
|
||
h.Set("user-agent", codexCLIUserAgent)
|
||
}
|
||
if strings.TrimSpace(h.Get("originator")) == "" {
|
||
h.Set("originator", openai.CodexCLIOriginator)
|
||
}
|
||
if strings.TrimSpace(h.Get("version")) == "" {
|
||
h.Set("version", codexCLIVersion)
|
||
}
|
||
h.Set("OpenAI-Beta", "responses=experimental")
|
||
}
|
||
|
||
// applyOpenAICodexProbeHeaders 为合成探测请求补齐 Codex 身份和引擎指纹。
|
||
func applyOpenAICodexProbeHeaders(h http.Header) {
|
||
if h == nil {
|
||
return
|
||
}
|
||
ensureCodexIdentityHeaders(h)
|
||
h.Set("X-Codex-Window-ID", uuid.NewString())
|
||
}
|
||
|
||
// enforceCodexIdentityHeaders 收口 OAuth(ChatGPT 内部接口)出站请求的客户端身份头。
|
||
// 上游要求 originator 与 User-Agent 首段配套且为官方客户端标识,version 头(若携带)
|
||
// 不低于 0.144.0,任一不满足即 404(issue #3901)。以最终 User-Agent 为准推导配套
|
||
// originator;推导不出官方身份(第三方 UA / UA 缺失)时整体回退为默认 Codex CLI 身份。
|
||
//
|
||
// 配对之后再做降载身份归一化:上游按 originator 分桶调度容量,命中降载桶的请求会被回
|
||
// server_is_overloaded,网关据此判定瞬时上游故障并冷却账号(对外表现为账号过载不可用),
|
||
// 故这类身份统一改写为 CLI 身份——只替换身份段,保留版本 / OS / 架构 / 终端指纹。
|
||
//
|
||
// 仅对携带 originator 的请求生效;需要从缺失身份头恢复的调用方应先调用
|
||
// ensureCodexIdentityHeaders。
|
||
// 必须在所有 User-Agent 改写(自定义 UA / ForceCodexCLI / 浏览器 UA 兜底)之后调用。
|
||
func enforceCodexIdentityHeaders(h http.Header) {
|
||
if h == nil || h.Get("originator") == "" {
|
||
return
|
||
}
|
||
originator, pairedUA, ok := openai.PairCodexClientIdentity(h.Get("user-agent"))
|
||
if !ok {
|
||
originator, pairedUA = openai.CodexCLIOriginator, codexCLIUserAgent
|
||
}
|
||
if codexOriginatorNormalization.Load() {
|
||
originator, pairedUA, _ = openai.NormalizeCodexClientIdentityToCLI(originator, pairedUA)
|
||
}
|
||
h.Set("user-agent", pairedUA)
|
||
h.Set("originator", originator)
|
||
if v := strings.TrimSpace(h.Get("version")); v != "" && CompareVersions(v, codexUpstreamMinVersion) < 0 {
|
||
h.Set("version", codexCLIVersion)
|
||
}
|
||
}
|