Skip to content

Commit 70988d3

Browse files
committed
Add Codex websocket header defaults
1 parent ddcf1f2 commit 70988d3

File tree

6 files changed

+287
-11
lines changed

6 files changed

+287
-11
lines changed

config.example.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ nonstream-keepalive-interval: 0
173173
# runtime-version: "v24.3.0"
174174
# timeout: "600"
175175

176+
# Default headers for Codex OAuth model requests.
177+
# These are used only for file-backed/OAuth Codex requests when the client
178+
# does not send the header. `user-agent` applies to HTTP and websocket requests;
179+
# `beta-features` only applies to websocket requests. They do not apply to codex-api-key entries.
180+
# codex-header-defaults:
181+
# user-agent: "my-codex-client/1.0"
182+
# beta-features: "feature-a,feature-b"
183+
176184
# OpenAI compatibility providers
177185
# openai-compatibility:
178186
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package config
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestLoadConfigOptional_CodexHeaderDefaults(t *testing.T) {
10+
dir := t.TempDir()
11+
configPath := filepath.Join(dir, "config.yaml")
12+
configYAML := []byte(`
13+
codex-header-defaults:
14+
user-agent: " my-codex-client/1.0 "
15+
beta-features: " feature-a,feature-b "
16+
`)
17+
if err := os.WriteFile(configPath, configYAML, 0o600); err != nil {
18+
t.Fatalf("failed to write config: %v", err)
19+
}
20+
21+
cfg, err := LoadConfigOptional(configPath, false)
22+
if err != nil {
23+
t.Fatalf("LoadConfigOptional() error = %v", err)
24+
}
25+
26+
if got := cfg.CodexHeaderDefaults.UserAgent; got != "my-codex-client/1.0" {
27+
t.Fatalf("UserAgent = %q, want %q", got, "my-codex-client/1.0")
28+
}
29+
if got := cfg.CodexHeaderDefaults.BetaFeatures; got != "feature-a,feature-b" {
30+
t.Fatalf("BetaFeatures = %q, want %q", got, "feature-a,feature-b")
31+
}
32+
}

internal/config/config.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ type Config struct {
9090
// Codex defines a list of Codex API key configurations as specified in the YAML configuration file.
9191
CodexKey []CodexKey `yaml:"codex-api-key" json:"codex-api-key"`
9292

93+
// CodexHeaderDefaults configures fallback headers for Codex OAuth model requests.
94+
// These are used only when the client does not send its own headers.
95+
CodexHeaderDefaults CodexHeaderDefaults `yaml:"codex-header-defaults" json:"codex-header-defaults"`
96+
9397
// ClaudeKey defines a list of Claude API key configurations as specified in the YAML configuration file.
9498
ClaudeKey []ClaudeKey `yaml:"claude-api-key" json:"claude-api-key"`
9599

@@ -133,6 +137,14 @@ type ClaudeHeaderDefaults struct {
133137
Timeout string `yaml:"timeout" json:"timeout"`
134138
}
135139

140+
// CodexHeaderDefaults configures fallback header values injected into Codex
141+
// model requests for OAuth/file-backed auth when the client omits them.
142+
// UserAgent applies to HTTP and websocket requests; BetaFeatures only applies to websockets.
143+
type CodexHeaderDefaults struct {
144+
UserAgent string `yaml:"user-agent" json:"user-agent"`
145+
BetaFeatures string `yaml:"beta-features" json:"beta-features"`
146+
}
147+
136148
// TLSConfig holds HTTPS server settings.
137149
type TLSConfig struct {
138150
// Enable toggles HTTPS server mode.
@@ -615,6 +627,9 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
615627
// Sanitize Codex keys: drop entries without base-url
616628
cfg.SanitizeCodexKeys()
617629

630+
// Sanitize Codex header defaults.
631+
cfg.SanitizeCodexHeaderDefaults()
632+
618633
// Sanitize Claude key headers
619634
cfg.SanitizeClaudeKeys()
620635

@@ -704,6 +719,16 @@ func payloadRawString(value any) ([]byte, bool) {
704719
}
705720
}
706721

722+
// SanitizeCodexHeaderDefaults trims surrounding whitespace from the
723+
// configured Codex header fallback values.
724+
func (cfg *Config) SanitizeCodexHeaderDefaults() {
725+
if cfg == nil {
726+
return
727+
}
728+
cfg.CodexHeaderDefaults.UserAgent = strings.TrimSpace(cfg.CodexHeaderDefaults.UserAgent)
729+
cfg.CodexHeaderDefaults.BetaFeatures = strings.TrimSpace(cfg.CodexHeaderDefaults.BetaFeatures)
730+
}
731+
707732
// SanitizeOAuthModelAlias normalizes and deduplicates global OAuth model name aliases.
708733
// It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
709734
// allows multiple aliases per upstream name, and ensures aliases are unique within each channel.

internal/runtime/executor/codex_executor.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
122122
if err != nil {
123123
return resp, err
124124
}
125-
applyCodexHeaders(httpReq, auth, apiKey, true)
125+
applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg)
126126
var authID, authLabel, authType, authValue string
127127
if auth != nil {
128128
authID = auth.ID
@@ -226,7 +226,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
226226
if err != nil {
227227
return resp, err
228228
}
229-
applyCodexHeaders(httpReq, auth, apiKey, false)
229+
applyCodexHeaders(httpReq, auth, apiKey, false, e.cfg)
230230
var authID, authLabel, authType, authValue string
231231
if auth != nil {
232232
authID = auth.ID
@@ -321,7 +321,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
321321
if err != nil {
322322
return nil, err
323323
}
324-
applyCodexHeaders(httpReq, auth, apiKey, true)
324+
applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg)
325325
var authID, authLabel, authType, authValue string
326326
if auth != nil {
327327
authID = auth.ID
@@ -636,7 +636,7 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form
636636
return httpReq, nil
637637
}
638638

639-
func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool) {
639+
func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config) {
640640
r.Header.Set("Content-Type", "application/json")
641641
r.Header.Set("Authorization", "Bearer "+token)
642642

@@ -647,7 +647,8 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s
647647

648648
misc.EnsureHeader(r.Header, ginHeaders, "Version", codexClientVersion)
649649
misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString())
650-
misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", codexUserAgent)
650+
cfgUserAgent, _ := codexHeaderDefaults(cfg, auth)
651+
ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)
651652

652653
if stream {
653654
r.Header.Set("Accept", "text/event-stream")

internal/runtime/executor/codex_websockets_executor.go

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
190190
}
191191

192192
body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body)
193-
wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey)
193+
wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg)
194194

195195
var authID, authLabel, authType, authValue string
196196
if auth != nil {
@@ -385,7 +385,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
385385
}
386386

387387
body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body)
388-
wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey)
388+
wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg)
389389

390390
var authID, authLabel, authType, authValue string
391391
authID = auth.ID
@@ -787,7 +787,7 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto
787787
return rawJSON, headers
788788
}
789789

790-
func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *cliproxyauth.Auth, token string) http.Header {
790+
func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *cliproxyauth.Auth, token string, cfg *config.Config) http.Header {
791791
if headers == nil {
792792
headers = http.Header{}
793793
}
@@ -800,7 +800,8 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *
800800
ginHeaders = ginCtx.Request.Header
801801
}
802802

803-
misc.EnsureHeader(headers, ginHeaders, "x-codex-beta-features", "")
803+
cfgUserAgent, cfgBetaFeatures := codexHeaderDefaults(cfg, auth)
804+
ensureHeaderWithPriority(headers, ginHeaders, "x-codex-beta-features", cfgBetaFeatures, "")
804805
misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-state", "")
805806
misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-metadata", "")
806807
misc.EnsureHeader(headers, ginHeaders, "x-responsesapi-include-timing-metrics", "")
@@ -815,7 +816,7 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *
815816
}
816817
headers.Set("OpenAI-Beta", betaHeader)
817818
misc.EnsureHeader(headers, ginHeaders, "Session_id", uuid.NewString())
818-
misc.EnsureHeader(headers, ginHeaders, "User-Agent", codexUserAgent)
819+
ensureHeaderWithConfigPrecedence(headers, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)
819820

820821
isAPIKey := false
821822
if auth != nil && auth.Attributes != nil {
@@ -843,6 +844,62 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *
843844
return headers
844845
}
845846

847+
func codexHeaderDefaults(cfg *config.Config, auth *cliproxyauth.Auth) (string, string) {
848+
if cfg == nil || auth == nil {
849+
return "", ""
850+
}
851+
if auth.Attributes != nil {
852+
if v := strings.TrimSpace(auth.Attributes["api_key"]); v != "" {
853+
return "", ""
854+
}
855+
}
856+
return strings.TrimSpace(cfg.CodexHeaderDefaults.UserAgent), strings.TrimSpace(cfg.CodexHeaderDefaults.BetaFeatures)
857+
}
858+
859+
func ensureHeaderWithPriority(target http.Header, source http.Header, key, configValue, fallbackValue string) {
860+
if target == nil {
861+
return
862+
}
863+
if strings.TrimSpace(target.Get(key)) != "" {
864+
return
865+
}
866+
if source != nil {
867+
if val := strings.TrimSpace(source.Get(key)); val != "" {
868+
target.Set(key, val)
869+
return
870+
}
871+
}
872+
if val := strings.TrimSpace(configValue); val != "" {
873+
target.Set(key, val)
874+
return
875+
}
876+
if val := strings.TrimSpace(fallbackValue); val != "" {
877+
target.Set(key, val)
878+
}
879+
}
880+
881+
func ensureHeaderWithConfigPrecedence(target http.Header, source http.Header, key, configValue, fallbackValue string) {
882+
if target == nil {
883+
return
884+
}
885+
if strings.TrimSpace(target.Get(key)) != "" {
886+
return
887+
}
888+
if val := strings.TrimSpace(configValue); val != "" {
889+
target.Set(key, val)
890+
return
891+
}
892+
if source != nil {
893+
if val := strings.TrimSpace(source.Get(key)); val != "" {
894+
target.Set(key, val)
895+
return
896+
}
897+
}
898+
if val := strings.TrimSpace(fallbackValue); val != "" {
899+
target.Set(key, val)
900+
}
901+
}
902+
846903
type statusErrWithHeaders struct {
847904
statusErr
848905
headers http.Header

0 commit comments

Comments
 (0)