feat(codex): add disable-codex-cloaking config option and refine header management

- Introduced `disable-codex-cloaking` to allow disabling enforced `User-Agent` and `Originator` headers for Codex requests.
- Updated header application logic to conditionally include `codexUserAgent` and `codexOriginator` based on configuration.
- Enhanced config diff tracking to detect changes in `disable-codex-cloaking`.
- Expanded tests to cover new config behavior and header application scenarios.
This commit is contained in:
Luis Pater
2026-07-30 16:37:40 +08:00
parent 928478e4b9
commit a80e8082ef
8 changed files with 61 additions and 4 deletions

View File

@@ -220,6 +220,8 @@ codex:
# Some superstitious users believe request tracking identifiers can be used
# as evidence for TOS enforcement bans; this option only satisfies those odd concerns.
identity-confuse: false
# Disable forcing the official Codex User-Agent and Originator headers on HTTP requests.
disable-codex-cloaking: false
# When true, optimize Codex Desktop and codex-tui requests for multi-agent v2.
# This refreshes Codex spawn_agent model details, removes message parameter encryption,
# normalizes encrypted agent_message content for Codex, and converts agent_message input

View File

@@ -29,6 +29,9 @@ codex-header-defaults:
if got := cfg.CodexHeaderDefaults.BetaFeatures; got != "feature-a,feature-b" {
t.Fatalf("BetaFeatures = %q, want %q", got, "feature-a,feature-b")
}
if cfg.Codex.DisableCodexCloaking {
t.Fatal("DisableCodexCloaking = true, want default false")
}
}
func TestLoadConfigOptional_CodexIdentityConfuse(t *testing.T) {
@@ -37,6 +40,7 @@ func TestLoadConfigOptional_CodexIdentityConfuse(t *testing.T) {
configYAML := []byte(`
codex:
identity-confuse: true
disable-codex-cloaking: true
optimize-multi-agent-v2: true
`)
if err := os.WriteFile(configPath, configYAML, 0o600); err != nil {
@@ -51,6 +55,9 @@ codex:
if !cfg.Codex.IdentityConfuse {
t.Fatalf("IdentityConfuse = false, want true")
}
if !cfg.Codex.DisableCodexCloaking {
t.Fatal("DisableCodexCloaking = false, want true")
}
if !cfg.Codex.OptimizeMultiAgentV2 {
t.Fatalf("OptimizeMultiAgentV2 = false, want true")
}

View File

@@ -127,6 +127,8 @@ type XAIConfig struct {
// CodexConfig configures provider-wide Codex request behavior.
type CodexConfig struct {
IdentityConfuse bool `yaml:"identity-confuse" json:"identity-confuse"`
// DisableCodexCloaking disables forcing the official Codex identity headers on HTTP requests.
DisableCodexCloaking bool `yaml:"disable-codex-cloaking" json:"disable-codex-cloaking"`
// OptimizeMultiAgentV2 optimizes official Codex multi-agent requests.
OptimizeMultiAgentV2 bool `yaml:"optimize-multi-agent-v2" json:"optimize-multi-agent-v2"`
// LiveMediaRelay terminates and relays Codex Live WebRTC media in this process.

View File

@@ -23,7 +23,7 @@ import (
)
const (
codexUserAgent = "codex-tui/0.135.0 (Mac OS 26.5.0; arm64) iTerm.app/3.6.10 (codex-tui; 0.135.0)"
codexUserAgent = "codex-tui/0.146.0 (Mac OS 26.5.0; arm64) iTerm.app/3.6.10 (codex-tui; 0.146.0)"
codexOriginator = "codex-tui"
codexDefaultImageToolModel = "gpt-image-2"
codexResponsesLiteHeader = "X-OpenAI-Internal-Codex-Responses-Lite"
@@ -352,6 +352,10 @@ func applyCodexHeadersFromSources(r *http.Request, auth *cliproxyauth.Auth, toke
attrs = auth.Attributes
}
util.ApplyCustomHeadersFromAttrs(r, attrs)
if cfg != nil && !cfg.Codex.DisableCodexCloaking {
r.Header.Set("User-Agent", codexUserAgent)
r.Header.Set("Originator", codexOriginator)
}
}
func normalizeCodexInstructions(body []byte) []byte {

View File

@@ -105,8 +105,8 @@ func TestCodexExecutorDirectOpenAIImageGenerationUsesImagesEndpoint(t *testing.T
if gotClientRequestID != "client-request-1" {
t.Fatalf("X-Client-Request-Id = %q, want %q", gotClientRequestID, "client-request-1")
}
if gotOriginator != "Codex Desktop" {
t.Fatalf("Originator = %q, want %q", gotOriginator, "Codex Desktop")
if gotOriginator != codexOriginator {
t.Fatalf("Originator = %q, want %q", gotOriginator, codexOriginator)
}
if got := gjson.GetBytes(gotBody, "model").String(); got != "gpt-image-1.5" {
t.Fatalf("model = %q, want gpt-image-1.5; body=%s", got, string(gotBody))

View File

@@ -1510,6 +1510,7 @@ func TestApplyCodexHeadersUsesConfigUserAgentForOAuth(t *testing.T) {
t.Fatalf("NewRequest() error = %v", err)
}
cfg := &config.Config{
Codex: config.CodexConfig{DisableCodexCloaking: true},
CodexHeaderDefaults: config.CodexHeaderDefaults{
UserAgent: "config-ua",
BetaFeatures: "config-beta",
@@ -1533,6 +1534,41 @@ func TestApplyCodexHeadersUsesConfigUserAgentForOAuth(t *testing.T) {
}
}
func TestApplyCodexHeadersDefaultsToCodexCloaking(t *testing.T) {
req, err := http.NewRequest(http.MethodPost, "https://example.com/responses", nil)
if err != nil {
t.Fatalf("NewRequest() error = %v", err)
}
req.Header.Set("User-Agent", "existing-ua")
req.Header.Set("Originator", "existing-origin")
cfg := &config.Config{
CodexHeaderDefaults: config.CodexHeaderDefaults{
UserAgent: "config-ua",
},
}
auth := &cliproxyauth.Auth{
Provider: "codex",
Attributes: map[string]string{
"api_key": "api-key",
"header:User-Agent": "custom-ua",
"header:Originator": "custom-origin",
},
}
ginHeaders := http.Header{
"User-Agent": []string{"client-ua"},
"Originator": []string{"client-origin"},
}
applyCodexHeadersFromSources(req, auth, "api-key", false, cfg, ginHeaders)
if got := req.Header.Get("User-Agent"); got != codexUserAgent {
t.Fatalf("User-Agent = %q, want %q", got, codexUserAgent)
}
if got := req.Header.Get("Originator"); got != codexOriginator {
t.Fatalf("Originator = %q, want %q", got, codexOriginator)
}
}
func TestApplyModelHeaderOverridesFromModelConfig(t *testing.T) {
const wantUA = "codex-tui/0.144.0 (Mac OS 26.5.1; arm64) iTerm.app/3.6.11 (codex-tui; 0.144.0)"
req, err := http.NewRequest(http.MethodPost, "https://example.com/responses", nil)
@@ -1614,7 +1650,8 @@ func TestApplyCodexHeadersPassesThroughClientIdentityHeaders(t *testing.T) {
"X-Client-Request-Id": "019d2233-e240-7162-992d-38df0a2a0e0d",
}))
applyCodexHeaders(req, auth, "oauth-token", true, nil)
cfg := &config.Config{Codex: config.CodexConfig{DisableCodexCloaking: true}}
applyCodexHeaders(req, auth, "oauth-token", true, cfg)
if got := req.Header.Get("Originator"); got != "Codex Desktop" {
t.Fatalf("Originator = %s, want %s", got, "Codex Desktop")

View File

@@ -108,6 +108,9 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
if oldCfg.Codex.IdentityConfuse != newCfg.Codex.IdentityConfuse {
changes = append(changes, fmt.Sprintf("codex.identity-confuse: %t -> %t", oldCfg.Codex.IdentityConfuse, newCfg.Codex.IdentityConfuse))
}
if oldCfg.Codex.DisableCodexCloaking != newCfg.Codex.DisableCodexCloaking {
changes = append(changes, fmt.Sprintf("codex.disable-codex-cloaking: %t -> %t", oldCfg.Codex.DisableCodexCloaking, newCfg.Codex.DisableCodexCloaking))
}
if oldCfg.Codex.OptimizeMultiAgentV2 != newCfg.Codex.OptimizeMultiAgentV2 {
changes = append(changes, fmt.Sprintf("codex.optimize-multi-agent-v2: %t -> %t", oldCfg.Codex.OptimizeMultiAgentV2, newCfg.Codex.OptimizeMultiAgentV2))
}

View File

@@ -39,6 +39,7 @@ func TestBuildConfigChangeDetails(t *testing.T) {
newCfg := &config.Config{
Port: 9090,
AuthDir: "/tmp/auth-new",
Codex: config.CodexConfig{DisableCodexCloaking: true},
GeminiKey: []config.GeminiKey{
{APIKey: "old", BaseURL: "http://old", ExcludedModels: []string{"old-model", "extra"}},
},
@@ -78,6 +79,7 @@ func TestBuildConfigChangeDetails(t *testing.T) {
expectContains(t, details, "remote-management.allow-remote: false -> true")
expectContains(t, details, "remote-management.disable-auto-update-panel: false -> true")
expectContains(t, details, "remote-management.secret-key: updated")
expectContains(t, details, "codex.disable-codex-cloaking: false -> true")
expectContains(t, details, "oauth-excluded-models[providera]: updated (1 -> 2 entries)")
expectContains(t, details, "oauth-excluded-models[providerb]: added (1 entries)")
expectContains(t, details, "openai-compatibility:")