Add Codex websocket header defaults
This commit is contained in:
@@ -173,6 +173,14 @@ nonstream-keepalive-interval: 0
|
|||||||
# runtime-version: "v24.3.0"
|
# runtime-version: "v24.3.0"
|
||||||
# timeout: "600"
|
# timeout: "600"
|
||||||
|
|
||||||
|
# Default headers for Codex OAuth model requests.
|
||||||
|
# These are used only for file-backed/OAuth Codex requests when the client
|
||||||
|
# does not send the header. `user-agent` applies to HTTP and websocket requests;
|
||||||
|
# `beta-features` only applies to websocket requests. They do not apply to codex-api-key entries.
|
||||||
|
# codex-header-defaults:
|
||||||
|
# user-agent: "my-codex-client/1.0"
|
||||||
|
# beta-features: "feature-a,feature-b"
|
||||||
|
|
||||||
# OpenAI compatibility providers
|
# OpenAI compatibility providers
|
||||||
# openai-compatibility:
|
# openai-compatibility:
|
||||||
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
# - name: "openrouter" # The name of the provider; it will be used in the user agent and other places.
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadConfigOptional_CodexHeaderDefaults(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "config.yaml")
|
||||||
|
configYAML := []byte(`
|
||||||
|
codex-header-defaults:
|
||||||
|
user-agent: " my-codex-client/1.0 "
|
||||||
|
beta-features: " feature-a,feature-b "
|
||||||
|
`)
|
||||||
|
if err := os.WriteFile(configPath, configYAML, 0o600); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := LoadConfigOptional(configPath, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfigOptional() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := cfg.CodexHeaderDefaults.UserAgent; got != "my-codex-client/1.0" {
|
||||||
|
t.Fatalf("UserAgent = %q, want %q", got, "my-codex-client/1.0")
|
||||||
|
}
|
||||||
|
if got := cfg.CodexHeaderDefaults.BetaFeatures; got != "feature-a,feature-b" {
|
||||||
|
t.Fatalf("BetaFeatures = %q, want %q", got, "feature-a,feature-b")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,6 +90,10 @@ type Config struct {
|
|||||||
// Codex defines a list of Codex API key configurations as specified in the YAML configuration file.
|
// Codex defines a list of Codex API key configurations as specified in the YAML configuration file.
|
||||||
CodexKey []CodexKey `yaml:"codex-api-key" json:"codex-api-key"`
|
CodexKey []CodexKey `yaml:"codex-api-key" json:"codex-api-key"`
|
||||||
|
|
||||||
|
// CodexHeaderDefaults configures fallback headers for Codex OAuth model requests.
|
||||||
|
// These are used only when the client does not send its own headers.
|
||||||
|
CodexHeaderDefaults CodexHeaderDefaults `yaml:"codex-header-defaults" json:"codex-header-defaults"`
|
||||||
|
|
||||||
// ClaudeKey defines a list of Claude API key configurations as specified in the YAML configuration file.
|
// ClaudeKey defines a list of Claude API key configurations as specified in the YAML configuration file.
|
||||||
ClaudeKey []ClaudeKey `yaml:"claude-api-key" json:"claude-api-key"`
|
ClaudeKey []ClaudeKey `yaml:"claude-api-key" json:"claude-api-key"`
|
||||||
|
|
||||||
@@ -133,6 +137,14 @@ type ClaudeHeaderDefaults struct {
|
|||||||
Timeout string `yaml:"timeout" json:"timeout"`
|
Timeout string `yaml:"timeout" json:"timeout"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CodexHeaderDefaults configures fallback header values injected into Codex
|
||||||
|
// model requests for OAuth/file-backed auth when the client omits them.
|
||||||
|
// UserAgent applies to HTTP and websocket requests; BetaFeatures only applies to websockets.
|
||||||
|
type CodexHeaderDefaults struct {
|
||||||
|
UserAgent string `yaml:"user-agent" json:"user-agent"`
|
||||||
|
BetaFeatures string `yaml:"beta-features" json:"beta-features"`
|
||||||
|
}
|
||||||
|
|
||||||
// TLSConfig holds HTTPS server settings.
|
// TLSConfig holds HTTPS server settings.
|
||||||
type TLSConfig struct {
|
type TLSConfig struct {
|
||||||
// Enable toggles HTTPS server mode.
|
// Enable toggles HTTPS server mode.
|
||||||
@@ -615,6 +627,9 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|||||||
// Sanitize Codex keys: drop entries without base-url
|
// Sanitize Codex keys: drop entries without base-url
|
||||||
cfg.SanitizeCodexKeys()
|
cfg.SanitizeCodexKeys()
|
||||||
|
|
||||||
|
// Sanitize Codex header defaults.
|
||||||
|
cfg.SanitizeCodexHeaderDefaults()
|
||||||
|
|
||||||
// Sanitize Claude key headers
|
// Sanitize Claude key headers
|
||||||
cfg.SanitizeClaudeKeys()
|
cfg.SanitizeClaudeKeys()
|
||||||
|
|
||||||
@@ -704,6 +719,16 @@ func payloadRawString(value any) ([]byte, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SanitizeCodexHeaderDefaults trims surrounding whitespace from the
|
||||||
|
// configured Codex header fallback values.
|
||||||
|
func (cfg *Config) SanitizeCodexHeaderDefaults() {
|
||||||
|
if cfg == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfg.CodexHeaderDefaults.UserAgent = strings.TrimSpace(cfg.CodexHeaderDefaults.UserAgent)
|
||||||
|
cfg.CodexHeaderDefaults.BetaFeatures = strings.TrimSpace(cfg.CodexHeaderDefaults.BetaFeatures)
|
||||||
|
}
|
||||||
|
|
||||||
// SanitizeOAuthModelAlias normalizes and deduplicates global OAuth model name aliases.
|
// SanitizeOAuthModelAlias normalizes and deduplicates global OAuth model name aliases.
|
||||||
// It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
|
// It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
|
||||||
// allows multiple aliases per upstream name, and ensures aliases are unique within each channel.
|
// allows multiple aliases per upstream name, and ensures aliases are unique within each channel.
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
applyCodexHeaders(httpReq, auth, apiKey, true)
|
applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -226,7 +226,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
applyCodexHeaders(httpReq, auth, apiKey, false)
|
applyCodexHeaders(httpReq, auth, apiKey, false, e.cfg)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -321,7 +321,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
applyCodexHeaders(httpReq, auth, apiKey, true)
|
applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -636,7 +636,7 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form
|
|||||||
return httpReq, nil
|
return httpReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool) {
|
func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config) {
|
||||||
r.Header.Set("Content-Type", "application/json")
|
r.Header.Set("Content-Type", "application/json")
|
||||||
r.Header.Set("Authorization", "Bearer "+token)
|
r.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
@@ -647,7 +647,8 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s
|
|||||||
|
|
||||||
misc.EnsureHeader(r.Header, ginHeaders, "Version", codexClientVersion)
|
misc.EnsureHeader(r.Header, ginHeaders, "Version", codexClientVersion)
|
||||||
misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString())
|
misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString())
|
||||||
misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", codexUserAgent)
|
cfgUserAgent, _ := codexHeaderDefaults(cfg, auth)
|
||||||
|
ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)
|
||||||
|
|
||||||
if stream {
|
if stream {
|
||||||
r.Header.Set("Accept", "text/event-stream")
|
r.Header.Set("Accept", "text/event-stream")
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
}
|
}
|
||||||
|
|
||||||
body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body)
|
body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body)
|
||||||
wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey)
|
wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
@@ -385,7 +385,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
}
|
}
|
||||||
|
|
||||||
body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body)
|
body, wsHeaders := applyCodexPromptCacheHeaders(from, req, body)
|
||||||
wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey)
|
wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg)
|
||||||
|
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -787,7 +787,7 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto
|
|||||||
return rawJSON, headers
|
return rawJSON, headers
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *cliproxyauth.Auth, token string) http.Header {
|
func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *cliproxyauth.Auth, token string, cfg *config.Config) http.Header {
|
||||||
if headers == nil {
|
if headers == nil {
|
||||||
headers = http.Header{}
|
headers = http.Header{}
|
||||||
}
|
}
|
||||||
@@ -800,7 +800,8 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *
|
|||||||
ginHeaders = ginCtx.Request.Header
|
ginHeaders = ginCtx.Request.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
misc.EnsureHeader(headers, ginHeaders, "x-codex-beta-features", "")
|
cfgUserAgent, cfgBetaFeatures := codexHeaderDefaults(cfg, auth)
|
||||||
|
ensureHeaderWithPriority(headers, ginHeaders, "x-codex-beta-features", cfgBetaFeatures, "")
|
||||||
misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-state", "")
|
misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-state", "")
|
||||||
misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-metadata", "")
|
misc.EnsureHeader(headers, ginHeaders, "x-codex-turn-metadata", "")
|
||||||
misc.EnsureHeader(headers, ginHeaders, "x-responsesapi-include-timing-metrics", "")
|
misc.EnsureHeader(headers, ginHeaders, "x-responsesapi-include-timing-metrics", "")
|
||||||
@@ -815,7 +816,7 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *
|
|||||||
}
|
}
|
||||||
headers.Set("OpenAI-Beta", betaHeader)
|
headers.Set("OpenAI-Beta", betaHeader)
|
||||||
misc.EnsureHeader(headers, ginHeaders, "Session_id", uuid.NewString())
|
misc.EnsureHeader(headers, ginHeaders, "Session_id", uuid.NewString())
|
||||||
misc.EnsureHeader(headers, ginHeaders, "User-Agent", codexUserAgent)
|
ensureHeaderWithConfigPrecedence(headers, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)
|
||||||
|
|
||||||
isAPIKey := false
|
isAPIKey := false
|
||||||
if auth != nil && auth.Attributes != nil {
|
if auth != nil && auth.Attributes != nil {
|
||||||
@@ -843,6 +844,62 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *
|
|||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func codexHeaderDefaults(cfg *config.Config, auth *cliproxyauth.Auth) (string, string) {
|
||||||
|
if cfg == nil || auth == nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
if auth.Attributes != nil {
|
||||||
|
if v := strings.TrimSpace(auth.Attributes["api_key"]); v != "" {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(cfg.CodexHeaderDefaults.UserAgent), strings.TrimSpace(cfg.CodexHeaderDefaults.BetaFeatures)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureHeaderWithPriority(target http.Header, source http.Header, key, configValue, fallbackValue string) {
|
||||||
|
if target == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(target.Get(key)) != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if source != nil {
|
||||||
|
if val := strings.TrimSpace(source.Get(key)); val != "" {
|
||||||
|
target.Set(key, val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val := strings.TrimSpace(configValue); val != "" {
|
||||||
|
target.Set(key, val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if val := strings.TrimSpace(fallbackValue); val != "" {
|
||||||
|
target.Set(key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureHeaderWithConfigPrecedence(target http.Header, source http.Header, key, configValue, fallbackValue string) {
|
||||||
|
if target == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(target.Get(key)) != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if val := strings.TrimSpace(configValue); val != "" {
|
||||||
|
target.Set(key, val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if source != nil {
|
||||||
|
if val := strings.TrimSpace(source.Get(key)); val != "" {
|
||||||
|
target.Set(key, val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val := strings.TrimSpace(fallbackValue); val != "" {
|
||||||
|
target.Set(key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type statusErrWithHeaders struct {
|
type statusErrWithHeaders struct {
|
||||||
statusErr
|
statusErr
|
||||||
headers http.Header
|
headers http.Header
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ package executor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,9 +32,158 @@ func TestBuildCodexWebsocketRequestBodyPreservesPreviousResponseID(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyCodexWebsocketHeadersDefaultsToCurrentResponsesBeta(t *testing.T) {
|
func TestApplyCodexWebsocketHeadersDefaultsToCurrentResponsesBeta(t *testing.T) {
|
||||||
headers := applyCodexWebsocketHeaders(context.Background(), http.Header{}, nil, "")
|
headers := applyCodexWebsocketHeaders(context.Background(), http.Header{}, nil, "", nil)
|
||||||
|
|
||||||
if got := headers.Get("OpenAI-Beta"); got != codexResponsesWebsocketBetaHeaderValue {
|
if got := headers.Get("OpenAI-Beta"); got != codexResponsesWebsocketBetaHeaderValue {
|
||||||
t.Fatalf("OpenAI-Beta = %s, want %s", got, codexResponsesWebsocketBetaHeaderValue)
|
t.Fatalf("OpenAI-Beta = %s, want %s", got, codexResponsesWebsocketBetaHeaderValue)
|
||||||
}
|
}
|
||||||
|
if got := headers.Get("User-Agent"); got != codexUserAgent {
|
||||||
|
t.Fatalf("User-Agent = %s, want %s", got, codexUserAgent)
|
||||||
|
}
|
||||||
|
if got := headers.Get("x-codex-beta-features"); got != "" {
|
||||||
|
t.Fatalf("x-codex-beta-features = %q, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexWebsocketHeadersUsesConfigDefaultsForOAuth(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
CodexHeaderDefaults: config.CodexHeaderDefaults{
|
||||||
|
UserAgent: "my-codex-client/1.0",
|
||||||
|
BetaFeatures: "feature-a,feature-b",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
auth := &cliproxyauth.Auth{
|
||||||
|
Provider: "codex",
|
||||||
|
Metadata: map[string]any{"email": "user@example.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := applyCodexWebsocketHeaders(context.Background(), http.Header{}, auth, "", cfg)
|
||||||
|
|
||||||
|
if got := headers.Get("User-Agent"); got != "my-codex-client/1.0" {
|
||||||
|
t.Fatalf("User-Agent = %s, want %s", got, "my-codex-client/1.0")
|
||||||
|
}
|
||||||
|
if got := headers.Get("x-codex-beta-features"); got != "feature-a,feature-b" {
|
||||||
|
t.Fatalf("x-codex-beta-features = %s, want %s", got, "feature-a,feature-b")
|
||||||
|
}
|
||||||
|
if got := headers.Get("OpenAI-Beta"); got != codexResponsesWebsocketBetaHeaderValue {
|
||||||
|
t.Fatalf("OpenAI-Beta = %s, want %s", got, codexResponsesWebsocketBetaHeaderValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexWebsocketHeadersPrefersExistingHeadersOverClientAndConfig(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
CodexHeaderDefaults: config.CodexHeaderDefaults{
|
||||||
|
UserAgent: "config-ua",
|
||||||
|
BetaFeatures: "config-beta",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
auth := &cliproxyauth.Auth{
|
||||||
|
Provider: "codex",
|
||||||
|
Metadata: map[string]any{"email": "user@example.com"},
|
||||||
|
}
|
||||||
|
ctx := contextWithGinHeaders(map[string]string{
|
||||||
|
"User-Agent": "client-ua",
|
||||||
|
"X-Codex-Beta-Features": "client-beta",
|
||||||
|
})
|
||||||
|
headers := http.Header{}
|
||||||
|
headers.Set("User-Agent", "existing-ua")
|
||||||
|
headers.Set("X-Codex-Beta-Features", "existing-beta")
|
||||||
|
|
||||||
|
got := applyCodexWebsocketHeaders(ctx, headers, auth, "", cfg)
|
||||||
|
|
||||||
|
if gotVal := got.Get("User-Agent"); gotVal != "existing-ua" {
|
||||||
|
t.Fatalf("User-Agent = %s, want %s", gotVal, "existing-ua")
|
||||||
|
}
|
||||||
|
if gotVal := got.Get("x-codex-beta-features"); gotVal != "existing-beta" {
|
||||||
|
t.Fatalf("x-codex-beta-features = %s, want %s", gotVal, "existing-beta")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexWebsocketHeadersConfigUserAgentOverridesClientHeader(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
CodexHeaderDefaults: config.CodexHeaderDefaults{
|
||||||
|
UserAgent: "config-ua",
|
||||||
|
BetaFeatures: "config-beta",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
auth := &cliproxyauth.Auth{
|
||||||
|
Provider: "codex",
|
||||||
|
Metadata: map[string]any{"email": "user@example.com"},
|
||||||
|
}
|
||||||
|
ctx := contextWithGinHeaders(map[string]string{
|
||||||
|
"User-Agent": "client-ua",
|
||||||
|
"X-Codex-Beta-Features": "client-beta",
|
||||||
|
})
|
||||||
|
|
||||||
|
headers := applyCodexWebsocketHeaders(ctx, http.Header{}, auth, "", cfg)
|
||||||
|
|
||||||
|
if got := headers.Get("User-Agent"); got != "config-ua" {
|
||||||
|
t.Fatalf("User-Agent = %s, want %s", got, "config-ua")
|
||||||
|
}
|
||||||
|
if got := headers.Get("x-codex-beta-features"); got != "client-beta" {
|
||||||
|
t.Fatalf("x-codex-beta-features = %s, want %s", got, "client-beta")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexWebsocketHeadersIgnoresConfigForAPIKeyAuth(t *testing.T) {
|
||||||
|
cfg := &config.Config{
|
||||||
|
CodexHeaderDefaults: config.CodexHeaderDefaults{
|
||||||
|
UserAgent: "config-ua",
|
||||||
|
BetaFeatures: "config-beta",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
auth := &cliproxyauth.Auth{
|
||||||
|
Provider: "codex",
|
||||||
|
Attributes: map[string]string{"api_key": "sk-test"},
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := applyCodexWebsocketHeaders(context.Background(), http.Header{}, auth, "sk-test", cfg)
|
||||||
|
|
||||||
|
if got := headers.Get("User-Agent"); got != codexUserAgent {
|
||||||
|
t.Fatalf("User-Agent = %s, want %s", got, codexUserAgent)
|
||||||
|
}
|
||||||
|
if got := headers.Get("x-codex-beta-features"); got != "" {
|
||||||
|
t.Fatalf("x-codex-beta-features = %q, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexHeadersUsesConfigUserAgentForOAuth(t *testing.T) {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, "https://example.com/responses", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRequest() error = %v", err)
|
||||||
|
}
|
||||||
|
cfg := &config.Config{
|
||||||
|
CodexHeaderDefaults: config.CodexHeaderDefaults{
|
||||||
|
UserAgent: "config-ua",
|
||||||
|
BetaFeatures: "config-beta",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
auth := &cliproxyauth.Auth{
|
||||||
|
Provider: "codex",
|
||||||
|
Metadata: map[string]any{"email": "user@example.com"},
|
||||||
|
}
|
||||||
|
req = req.WithContext(contextWithGinHeaders(map[string]string{
|
||||||
|
"User-Agent": "client-ua",
|
||||||
|
}))
|
||||||
|
|
||||||
|
applyCodexHeaders(req, auth, "oauth-token", true, cfg)
|
||||||
|
|
||||||
|
if got := req.Header.Get("User-Agent"); got != "config-ua" {
|
||||||
|
t.Fatalf("User-Agent = %s, want %s", got, "config-ua")
|
||||||
|
}
|
||||||
|
if got := req.Header.Get("x-codex-beta-features"); got != "" {
|
||||||
|
t.Fatalf("x-codex-beta-features = %q, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func contextWithGinHeaders(headers map[string]string) context.Context {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ginCtx, _ := gin.CreateTestContext(recorder)
|
||||||
|
ginCtx.Request = httptest.NewRequest(http.MethodPost, "/", nil)
|
||||||
|
ginCtx.Request.Header = make(http.Header, len(headers))
|
||||||
|
for key, value := range headers {
|
||||||
|
ginCtx.Request.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
return context.WithValue(context.Background(), "gin", ginCtx)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user