feat(gateway): 支持强制 Codex CLI 模式并伪装 UA
- Codex CLI 请求仅使用内置 instructions,不再读取 opencode 缓存/回源\n- 新增 gateway.force_codex_cli(环境变量 GATEWAY_FORCE_CODEX_CLI)\n- ForceCodexCLI=true 时转发上游强制 User-Agent=codex_cli_rs/0.0.0\n- 更新 deploy 示例配置
This commit is contained in:
@@ -226,6 +226,9 @@ type GatewayConfig struct {
|
|||||||
MaxBodySize int64 `mapstructure:"max_body_size"`
|
MaxBodySize int64 `mapstructure:"max_body_size"`
|
||||||
// ConnectionPoolIsolation: 上游连接池隔离策略(proxy/account/account_proxy)
|
// ConnectionPoolIsolation: 上游连接池隔离策略(proxy/account/account_proxy)
|
||||||
ConnectionPoolIsolation string `mapstructure:"connection_pool_isolation"`
|
ConnectionPoolIsolation string `mapstructure:"connection_pool_isolation"`
|
||||||
|
// ForceCodexCLI: 强制将 OpenAI `/v1/responses` 请求按 Codex CLI 处理。
|
||||||
|
// 用于网关未透传/改写 User-Agent 时的兼容兜底(默认关闭,避免影响其他客户端)。
|
||||||
|
ForceCodexCLI bool `mapstructure:"force_codex_cli"`
|
||||||
|
|
||||||
// HTTP 上游连接池配置(性能优化:支持高并发场景调优)
|
// HTTP 上游连接池配置(性能优化:支持高并发场景调优)
|
||||||
// MaxIdleConns: 所有主机的最大空闲连接总数
|
// MaxIdleConns: 所有主机的最大空闲连接总数
|
||||||
@@ -882,6 +885,7 @@ func setDefaults() {
|
|||||||
viper.SetDefault("gateway.failover_on_400", false)
|
viper.SetDefault("gateway.failover_on_400", false)
|
||||||
viper.SetDefault("gateway.max_account_switches", 10)
|
viper.SetDefault("gateway.max_account_switches", 10)
|
||||||
viper.SetDefault("gateway.max_account_switches_gemini", 3)
|
viper.SetDefault("gateway.max_account_switches_gemini", 3)
|
||||||
|
viper.SetDefault("gateway.force_codex_cli", false)
|
||||||
viper.SetDefault("gateway.antigravity_fallback_cooldown_minutes", 1)
|
viper.SetDefault("gateway.antigravity_fallback_cooldown_minutes", 1)
|
||||||
viper.SetDefault("gateway.max_body_size", int64(100*1024*1024))
|
viper.SetDefault("gateway.max_body_size", int64(100*1024*1024))
|
||||||
viper.SetDefault("gateway.connection_pool_isolation", ConnectionPoolIsolationAccountProxy)
|
viper.SetDefault("gateway.connection_pool_isolation", ConnectionPoolIsolationAccountProxy)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type OpenAIGatewayHandler struct {
|
|||||||
errorPassthroughService *service.ErrorPassthroughService
|
errorPassthroughService *service.ErrorPassthroughService
|
||||||
concurrencyHelper *ConcurrencyHelper
|
concurrencyHelper *ConcurrencyHelper
|
||||||
maxAccountSwitches int
|
maxAccountSwitches int
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOpenAIGatewayHandler creates a new OpenAIGatewayHandler
|
// NewOpenAIGatewayHandler creates a new OpenAIGatewayHandler
|
||||||
@@ -54,6 +55,7 @@ func NewOpenAIGatewayHandler(
|
|||||||
errorPassthroughService: errorPassthroughService,
|
errorPassthroughService: errorPassthroughService,
|
||||||
concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatComment, pingInterval),
|
concurrencyHelper: NewConcurrencyHelper(concurrencyService, SSEPingFormatComment, pingInterval),
|
||||||
maxAccountSwitches: maxAccountSwitches,
|
maxAccountSwitches: maxAccountSwitches,
|
||||||
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +111,8 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
userAgent := c.GetHeader("User-Agent")
|
userAgent := c.GetHeader("User-Agent")
|
||||||
if !openai.IsCodexCLIRequest(userAgent) {
|
isCodexCLI := openai.IsCodexCLIRequest(userAgent) || (h.cfg != nil && h.cfg.Gateway.ForceCodexCLI)
|
||||||
|
if !isCodexCLI {
|
||||||
existingInstructions, _ := reqBody["instructions"].(string)
|
existingInstructions, _ := reqBody["instructions"].(string)
|
||||||
if strings.TrimSpace(existingInstructions) == "" {
|
if strings.TrimSpace(existingInstructions) == "" {
|
||||||
if instructions := strings.TrimSpace(service.GetOpenCodeInstructions()); instructions != "" {
|
if instructions := strings.TrimSpace(service.GetOpenCodeInstructions()); instructions != "" {
|
||||||
|
|||||||
@@ -342,8 +342,8 @@ func GetCodexCLIInstructions() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// applyInstructions 处理 instructions 字段
|
// applyInstructions 处理 instructions 字段
|
||||||
// isCodexCLI=true: 仅补充缺失的 instructions(使用 opencode 指令)
|
// isCodexCLI=true: 仅补充缺失的 instructions(使用内置 Codex CLI 指令)
|
||||||
// isCodexCLI=false: 优先使用 opencode 指令覆盖
|
// isCodexCLI=false: 优先使用 opencode 指令覆盖(不可用时回退到内置 Codex CLI 指令)
|
||||||
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
||||||
if isCodexCLI {
|
if isCodexCLI {
|
||||||
return applyCodexCLIInstructions(reqBody)
|
return applyCodexCLIInstructions(reqBody)
|
||||||
@@ -352,13 +352,13 @@ func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// applyCodexCLIInstructions 为 Codex CLI 请求补充缺失的 instructions
|
// applyCodexCLIInstructions 为 Codex CLI 请求补充缺失的 instructions
|
||||||
// 仅在 instructions 为空时添加 opencode 指令
|
// 仅在 instructions 为空时添加内置 Codex CLI 指令(不依赖 opencode 缓存/回源)
|
||||||
func applyCodexCLIInstructions(reqBody map[string]any) bool {
|
func applyCodexCLIInstructions(reqBody map[string]any) bool {
|
||||||
if !isInstructionsEmpty(reqBody) {
|
if !isInstructionsEmpty(reqBody) {
|
||||||
return false // 已有有效 instructions,不修改
|
return false // 已有有效 instructions,不修改
|
||||||
}
|
}
|
||||||
|
|
||||||
instructions := strings.TrimSpace(getOpenCodeCodexHeader())
|
instructions := strings.TrimSpace(getCodexCLIInstructions())
|
||||||
if instructions != "" {
|
if instructions != "" {
|
||||||
reqBody["instructions"] = instructions
|
reqBody["instructions"] = instructions
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -765,7 +765,7 @@ func (s *OpenAIGatewayService) Forward(ctx context.Context, c *gin.Context, acco
|
|||||||
bodyModified := false
|
bodyModified := false
|
||||||
originalModel := reqModel
|
originalModel := reqModel
|
||||||
|
|
||||||
isCodexCLI := openai.IsCodexCLIRequest(c.GetHeader("User-Agent"))
|
isCodexCLI := openai.IsCodexCLIRequest(c.GetHeader("User-Agent")) || (s.cfg != nil && s.cfg.Gateway.ForceCodexCLI)
|
||||||
|
|
||||||
// 对所有请求执行模型映射(包含 Codex CLI)。
|
// 对所有请求执行模型映射(包含 Codex CLI)。
|
||||||
mappedModel := account.GetMappedModel(reqModel)
|
mappedModel := account.GetMappedModel(reqModel)
|
||||||
@@ -1053,6 +1053,12 @@ func (s *OpenAIGatewayService) buildUpstreamRequest(ctx context.Context, c *gin.
|
|||||||
req.Header.Set("user-agent", customUA)
|
req.Header.Set("user-agent", customUA)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 若开启 ForceCodexCLI,则强制将上游 User-Agent 伪装为 Codex CLI。
|
||||||
|
// 用于网关未透传/改写 User-Agent 时,仍能命中 Codex 侧识别逻辑。
|
||||||
|
if s.cfg != nil && s.cfg.Gateway.ForceCodexCLI {
|
||||||
|
req.Header.Set("user-agent", "codex_cli_rs/0.98.0")
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure required headers exist
|
// Ensure required headers exist
|
||||||
if req.Header.Get("content-type") == "" {
|
if req.Header.Get("content-type") == "" {
|
||||||
req.Header.Set("content-type", "application/json")
|
req.Header.Set("content-type", "application/json")
|
||||||
|
|||||||
@@ -169,6 +169,13 @@ RATE_LIMIT_OVERLOAD_COOLDOWN_MINUTES=10
|
|||||||
# Gateway Scheduling (Optional)
|
# Gateway Scheduling (Optional)
|
||||||
# 调度缓存与受控回源配置(缓存就绪且命中时不读 DB)
|
# 调度缓存与受控回源配置(缓存就绪且命中时不读 DB)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
# Force Codex CLI mode: treat all /openai/v1/responses requests as Codex CLI.
|
||||||
|
# 强制按 Codex CLI 处理 /openai/v1/responses 请求(用于网关未透传/改写 User-Agent 的兜底)。
|
||||||
|
#
|
||||||
|
# 注意:开启后会影响所有客户端的行为(不仅限于 VS Code / Codex CLI),请谨慎开启。
|
||||||
|
#
|
||||||
|
# 默认:false
|
||||||
|
GATEWAY_FORCE_CODEX_CLI=false
|
||||||
# 上游连接池:每主机最大连接数(默认 1024;流式/HTTP1.1 场景可调大,如 2400/4096)
|
# 上游连接池:每主机最大连接数(默认 1024;流式/HTTP1.1 场景可调大,如 2400/4096)
|
||||||
GATEWAY_MAX_CONNS_PER_HOST=2048
|
GATEWAY_MAX_CONNS_PER_HOST=2048
|
||||||
# 上游连接池:最大空闲连接总数(默认 2560;账号/代理隔离 + 高并发场景可调大)
|
# 上游连接池:最大空闲连接总数(默认 2560;账号/代理隔离 + 高并发场景可调大)
|
||||||
|
|||||||
@@ -151,6 +151,11 @@ gateway:
|
|||||||
# - account_proxy: Isolate by account+proxy combination (default, finest granularity)
|
# - account_proxy: Isolate by account+proxy combination (default, finest granularity)
|
||||||
# - account_proxy: 按账户+代理组合隔离(默认,最细粒度)
|
# - account_proxy: 按账户+代理组合隔离(默认,最细粒度)
|
||||||
connection_pool_isolation: "account_proxy"
|
connection_pool_isolation: "account_proxy"
|
||||||
|
# Force Codex CLI mode: treat all /openai/v1/responses requests as Codex CLI.
|
||||||
|
# 强制按 Codex CLI 处理 /openai/v1/responses 请求(用于网关未透传/改写 User-Agent 的兜底)。
|
||||||
|
#
|
||||||
|
# 注意:开启后会影响所有客户端的行为(不仅限于 VS Code / Codex CLI),请谨慎开启。
|
||||||
|
force_codex_cli: false
|
||||||
# HTTP upstream connection pool settings (HTTP/2 + multi-proxy scenario defaults)
|
# HTTP upstream connection pool settings (HTTP/2 + multi-proxy scenario defaults)
|
||||||
# HTTP 上游连接池配置(HTTP/2 + 多代理场景默认值)
|
# HTTP 上游连接池配置(HTTP/2 + 多代理场景默认值)
|
||||||
# Max idle connections across all hosts
|
# Max idle connections across all hosts
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ services:
|
|||||||
# =======================================================================
|
# =======================================================================
|
||||||
# Gateway Configuration
|
# Gateway Configuration
|
||||||
# =======================================================================
|
# =======================================================================
|
||||||
|
- GATEWAY_FORCE_CODEX_CLI=${GATEWAY_FORCE_CODEX_CLI:-false}
|
||||||
- GATEWAY_MAX_IDLE_CONNS=${GATEWAY_MAX_IDLE_CONNS:-2560}
|
- GATEWAY_MAX_IDLE_CONNS=${GATEWAY_MAX_IDLE_CONNS:-2560}
|
||||||
- GATEWAY_MAX_IDLE_CONNS_PER_HOST=${GATEWAY_MAX_IDLE_CONNS_PER_HOST:-120}
|
- GATEWAY_MAX_IDLE_CONNS_PER_HOST=${GATEWAY_MAX_IDLE_CONNS_PER_HOST:-120}
|
||||||
- GATEWAY_MAX_CONNS_PER_HOST=${GATEWAY_MAX_CONNS_PER_HOST:-8192}
|
- GATEWAY_MAX_CONNS_PER_HOST=${GATEWAY_MAX_CONNS_PER_HOST:-8192}
|
||||||
|
|||||||
Reference in New Issue
Block a user