package executor import ( "context" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/misc" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" "github.com/router-for-me/CLIProxyAPI/v7/internal/util" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson") func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecutor.Request, rawJSON []byte) ([]byte, http.Header) { body, headers, _ := applyCodexPromptCacheHeadersWithContext(context.Background(), from, req, rawJSON) return body, headers} func applyCodexPromptCacheHeadersWithContext(ctx context.Context, from sdktranslator.Format, req cliproxyexecutor.Request, rawJSON []byte, headerSets ...http.Header) ([]byte, http.Header, error) { headers := http.Header{} if len(rawJSON) == 0 { return rawJSON, headers, nil } var requestHeaders http.Header if len(headerSets) > 0 { requestHeaders = headerSets[0] } var cache helps.CodexCache if sourceFormatEqual(from, sdktranslator.FormatClaude) { modelName := strings.TrimSpace(gjson.GetBytes(rawJSON, "model").String()) if modelName == "" { modelName = thinking.ParseSuffix(req.Model).ModelName } cached, ok, errCache := helps.ClaudeCodePromptCache(ctx, modelName, req.Payload, requestHeaders) if errCache != nil { return nil, nil, errCache } if ok { cache = cached } } else if sourceFormatEqual(from, sdktranslator.FormatOpenAIResponse) { if promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key"); promptCacheKey.Exists() { cache.ID = promptCacheKey.String() } } if cache.ID == "" { cache.ID = helps.ProviderSessionUUID("codex", req.Metadata) } if cache.ID != "" { rawJSON = helps.SetStringIfDifferent(rawJSON, "prompt_cache_key", cache.ID) setHeaderCasePreserved(headers, "session_id", cache.ID) headers.Set("Conversation_id", cache.ID) } return rawJSON, headers, nil} func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *cliproxyauth.Auth, token string, cfg *config.Config, nativeRequest bool, clientHeaders ...http.Header) http.Header { if headers == nil { headers = http.Header{} } if strings.TrimSpace(token) != "" { headers.Set("Authorization", "Bearer "+token) } else { headers.Del("Authorization") } var ginHeaders http.Header if len(clientHeaders) > 0 && clientHeaders[0] != nil { ginHeaders = clientHeaders[0].Clone() } else if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { ginHeaders = ginCtx.Request.Header.Clone() } isAPIKey := codexAuthUsesAPIKey(auth) 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-metadata", "") misc.EnsureHeader(headers, ginHeaders, "x-client-request-id", "") misc.EnsureHeader(headers, ginHeaders, "x-responsesapi-include-timing-metrics", "") misc.EnsureHeader(headers, ginHeaders, "Version", "") if nativeRequest { misc.EnsureHeader(headers, ginHeaders, codexResponsesLiteHeader, "") } if isAPIKey { ensureHeaderWithPriority(headers, ginHeaders, "User-Agent", "", "") } else { ensureHeaderWithConfigPrecedence(headers, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent) } betaHeader := strings.TrimSpace(headers.Get("OpenAI-Beta")) if betaHeader == "" && ginHeaders != nil { betaHeader = strings.TrimSpace(ginHeaders.Get("OpenAI-Beta")) } if betaHeader == "" || !strings.Contains(betaHeader, "responses_websockets=") { betaHeader = codexResponsesWebsocketBetaHeaderValue } headers.Set("OpenAI-Beta", betaHeader) sessionFallback := "" if strings.Contains(headers.Get("User-Agent"), "Mac OS") { sessionFallback = uuid.NewString() } ensureCodexWebsocketSessionHeader(headers, ginHeaders, sessionFallback) if nativeRequest && cfg != nil && cfg.Codex.DisableCodexCloaking { deleteHeaderCaseInsensitive(headers, "session_id") deleteHeaderCaseInsensitive(headers, "conversation_id") for key, values := range ginHeaders { switch strings.ToLower(key) { case "session-id", "session_id", "conversation_id", "thread-id", "x-codex-routing-hint", "x-codex-window-id": deleteHeaderCaseInsensitive(headers, key) headers[key] = append([]string(nil), values...) } } } if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" { headers.Set("Originator", originator) } else if !isAPIKey { headers.Set("Originator", codexOriginator) } if !isAPIKey { if auth != nil && auth.Metadata != nil { if accountID, ok := auth.Metadata["account_id"].(string); ok { if trimmed := strings.TrimSpace(accountID); trimmed != "" { setHeaderCasePreserved(headers, "ChatGPT-Account-ID", trimmed) } } } } var attrs map[string]string if auth != nil { attrs = auth.Attributes } req := (&http.Request{Header: headers}).WithContext(ctx) util.ApplyCustomHeadersFromAttrs(req, attrs, ginHeaders) applyCodexCloakingHeaders(headers, cfg) return headers} func ensureCodexWebsocketSessionHeader(target http.Header, source http.Header, fallbackValue string) { if target == nil { return } sessionID := codexSessionHeaderValue(target) if sessionID == "" { sessionID = codexSessionHeaderValue(source) } if sessionID == "" { sessionID = strings.TrimSpace(fallbackValue) } if sessionID != "" { setHeaderCasePreserved(target, "session_id", sessionID) } deleteHeaderCaseInsensitive(target, "Session-Id")} func codexSessionHeaderValue(headers http.Header) string { for _, key := range []string{"Session-Id", "Session_id", "session_id"} { if value := strings.TrimSpace(headerValueCaseInsensitive(headers, key)); value != "" { return value } } return ""} func codexAuthUsesAPIKey(auth *cliproxyauth.Auth) bool { if auth == nil { return false } if auth.AuthKind() == cliproxyauth.AuthKindAPIKey { return true } if auth.Attributes != nil { return strings.TrimSpace(auth.Attributes["api_key"]) != "" } return false} func ensureHeaderCasePreserved(target http.Header, source http.Header, key, configValue, fallbackValue string) { if target == nil { return } if strings.TrimSpace(headerValueCaseInsensitive(target, key)) != "" { return } if source != nil { if val := strings.TrimSpace(headerValueCaseInsensitive(source, key)); val != "" { setHeaderCasePreserved(target, key, val) return } } if val := strings.TrimSpace(configValue); val != "" { setHeaderCasePreserved(target, key, val) return } if val := strings.TrimSpace(fallbackValue); val != "" { setHeaderCasePreserved(target, key, val) }} func setHeaderCasePreserved(headers http.Header, key string, value string) { if headers == nil { return } key = strings.TrimSpace(key) value = strings.TrimSpace(value) if key == "" || value == "" { return } deleteHeaderCaseInsensitive(headers, key) headers[key] = []string{value}} func setCodexSessionHeaderCasePreserved(headers http.Header, fallbackKey string, value string) { if headers == nil { return } fallbackKey = strings.TrimSpace(fallbackKey) value = strings.TrimSpace(value) if fallbackKey == "" || value == "" { return } selectedKey := "" if _, ok := headers[fallbackKey]; ok && codexSessionHeaderKeyUsesUnderscore(fallbackKey) { selectedKey = fallbackKey } else { for existingKey := range headers { if codexSessionHeaderKeyUsesUnderscore(existingKey) { selectedKey = existingKey break } } } if selectedKey == "" { selectedKey = fallbackKey } for existingKey := range headers { if codexSessionHeaderKey(existingKey) && existingKey != selectedKey { delete(headers, existingKey) } } headers[selectedKey] = []string{value}} func codexSessionHeaderKey(key string) bool { normalized := strings.ToLower(strings.TrimSpace(key)) return normalized == "session_id" || normalized == "session-id"} func codexSessionHeaderKeyUsesUnderscore(key string) bool { return strings.ToLower(strings.TrimSpace(key)) == "session_id"} func headerValueCaseInsensitive(headers http.Header, key string) string { key = strings.TrimSpace(key) if headers == nil || key == "" { return "" } if val := strings.TrimSpace(headers.Get(key)); val != "" { return val } for existingKey, values := range headers { if !strings.EqualFold(existingKey, key) { continue } for _, value := range values { if trimmed := strings.TrimSpace(value); trimmed != "" { return trimmed } } } return ""} func deleteHeaderCaseInsensitive(headers http.Header, key string) { for existingKey := range headers { if strings.EqualFold(existingKey, key) { delete(headers, existingKey) } }} func codexHeaderDefaults(cfg *config.Config, auth *cliproxyauth.Auth) (string, string) { if cfg == nil || auth == nil || codexAuthUsesAPIKey(auth) { 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) }}
package executor
import (
"context"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/misc"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
"github.com/tidwall/gjson"
)
func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecutor.Request, rawJSON []byte) ([]byte, http.Header) {
body, headers, _ := applyCodexPromptCacheHeadersWithContext(context.Background(), from, req, rawJSON)
return body, headers
}
func applyCodexPromptCacheHeadersWithContext(ctx context.Context, from sdktranslator.Format, req cliproxyexecutor.Request, rawJSON []byte, headerSets ...http.Header) ([]byte, http.Header, error) {
headers := http.Header{}
if len(rawJSON) == 0 {
return rawJSON, headers, nil
}
var requestHeaders http.Header
if len(headerSets) > 0 {
requestHeaders = headerSets[0]
}
var cache helps.CodexCache
if sourceFormatEqual(from, sdktranslator.FormatClaude) {
modelName := strings.TrimSpace(gjson.GetBytes(rawJSON, "model").String())
if modelName == "" {
modelName = thinking.ParseSuffix(req.Model).ModelName
}
cached, ok, errCache := helps.ClaudeCodePromptCache(ctx, modelName, req.Payload, requestHeaders)
if errCache != nil {
return nil, nil, errCache
}
if ok {
cache = cached
}
} else if sourceFormatEqual(from, sdktranslator.FormatOpenAIResponse) {
if promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key"); promptCacheKey.Exists() {
cache.ID = promptCacheKey.String()
}
}
if cache.ID == "" {
cache.ID = helps.ProviderSessionUUID("codex", req.Metadata)
}
if cache.ID != "" {
rawJSON = helps.SetStringIfDifferent(rawJSON, "prompt_cache_key", cache.ID)
setHeaderCasePreserved(headers, "session_id", cache.ID)
headers.Set("Conversation_id", cache.ID)
}
return rawJSON, headers, nil
}
func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *cliproxyauth.Auth, token string, cfg *config.Config, nativeRequest bool, clientHeaders ...http.Header) http.Header {
if headers == nil {
headers = http.Header{}
}
if strings.TrimSpace(token) != "" {
headers.Set("Authorization", "Bearer "+token)
} else {
headers.Del("Authorization")
}
var ginHeaders http.Header
if len(clientHeaders) > 0 && clientHeaders[0] != nil {
ginHeaders = clientHeaders[0].Clone()
} else if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
ginHeaders = ginCtx.Request.Header.Clone()
}
isAPIKey := codexAuthUsesAPIKey(auth)
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-metadata", "")
misc.EnsureHeader(headers, ginHeaders, "x-client-request-id", "")
misc.EnsureHeader(headers, ginHeaders, "x-responsesapi-include-timing-metrics", "")
misc.EnsureHeader(headers, ginHeaders, "Version", "")
if nativeRequest {
misc.EnsureHeader(headers, ginHeaders, codexResponsesLiteHeader, "")
}
if isAPIKey {
ensureHeaderWithPriority(headers, ginHeaders, "User-Agent", "", "")
} else {
ensureHeaderWithConfigPrecedence(headers, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)
}
betaHeader := strings.TrimSpace(headers.Get("OpenAI-Beta"))
if betaHeader == "" && ginHeaders != nil {
betaHeader = strings.TrimSpace(ginHeaders.Get("OpenAI-Beta"))
}
if betaHeader == "" || !strings.Contains(betaHeader, "responses_websockets=") {
betaHeader = codexResponsesWebsocketBetaHeaderValue
}
headers.Set("OpenAI-Beta", betaHeader)
sessionFallback := ""
if strings.Contains(headers.Get("User-Agent"), "Mac OS") {
sessionFallback = uuid.NewString()
}
ensureCodexWebsocketSessionHeader(headers, ginHeaders, sessionFallback)
if nativeRequest && cfg != nil && cfg.Codex.DisableCodexCloaking {
deleteHeaderCaseInsensitive(headers, "session_id")
deleteHeaderCaseInsensitive(headers, "conversation_id")
for key, values := range ginHeaders {
switch strings.ToLower(key) {
case "session-id", "session_id", "conversation_id", "thread-id", "x-codex-routing-hint", "x-codex-window-id":
deleteHeaderCaseInsensitive(headers, key)
headers[key] = append([]string(nil), values...)
}
}
}
if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" {
headers.Set("Originator", originator)
} else if !isAPIKey {
headers.Set("Originator", codexOriginator)
}
if !isAPIKey {
if auth != nil && auth.Metadata != nil {
if accountID, ok := auth.Metadata["account_id"].(string); ok {
if trimmed := strings.TrimSpace(accountID); trimmed != "" {
setHeaderCasePreserved(headers, "ChatGPT-Account-ID", trimmed)
}
}
}
}
var attrs map[string]string
if auth != nil {
attrs = auth.Attributes
}
req := (&http.Request{Header: headers}).WithContext(ctx)
util.ApplyCustomHeadersFromAttrs(req, attrs, ginHeaders)
applyCodexCloakingHeaders(headers, cfg)
return headers
}
func ensureCodexWebsocketSessionHeader(target http.Header, source http.Header, fallbackValue string) {
if target == nil {
return
}
sessionID := codexSessionHeaderValue(target)
if sessionID == "" {
sessionID = codexSessionHeaderValue(source)
}
if sessionID == "" {
sessionID = strings.TrimSpace(fallbackValue)
}
if sessionID != "" {
setHeaderCasePreserved(target, "session_id", sessionID)
}
deleteHeaderCaseInsensitive(target, "Session-Id")
}
func codexSessionHeaderValue(headers http.Header) string {
for _, key := range []string{"Session-Id", "Session_id", "session_id"} {
if value := strings.TrimSpace(headerValueCaseInsensitive(headers, key)); value != "" {
return value
}
}
return ""
}
func codexAuthUsesAPIKey(auth *cliproxyauth.Auth) bool {
if auth == nil {
return false
}
if auth.AuthKind() == cliproxyauth.AuthKindAPIKey {
return true
}
if auth.Attributes != nil {
return strings.TrimSpace(auth.Attributes["api_key"]) != ""
}
return false
}
func ensureHeaderCasePreserved(target http.Header, source http.Header, key, configValue, fallbackValue string) {
if target == nil {
return
}
if strings.TrimSpace(headerValueCaseInsensitive(target, key)) != "" {
return
}
if source != nil {
if val := strings.TrimSpace(headerValueCaseInsensitive(source, key)); val != "" {
setHeaderCasePreserved(target, key, val)
return
}
}
if val := strings.TrimSpace(configValue); val != "" {
setHeaderCasePreserved(target, key, val)
return
}
if val := strings.TrimSpace(fallbackValue); val != "" {
setHeaderCasePreserved(target, key, val)
}
}
func setHeaderCasePreserved(headers http.Header, key string, value string) {
if headers == nil {
return
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" || value == "" {
return
}
deleteHeaderCaseInsensitive(headers, key)
headers[key] = []string{value}
}
func setCodexSessionHeaderCasePreserved(headers http.Header, fallbackKey string, value string) {
if headers == nil {
return
}
fallbackKey = strings.TrimSpace(fallbackKey)
value = strings.TrimSpace(value)
if fallbackKey == "" || value == "" {
return
}
selectedKey := ""
if _, ok := headers[fallbackKey]; ok && codexSessionHeaderKeyUsesUnderscore(fallbackKey) {
selectedKey = fallbackKey
} else {
for existingKey := range headers {
if codexSessionHeaderKeyUsesUnderscore(existingKey) {
selectedKey = existingKey
break
}
}
}
if selectedKey == "" {
selectedKey = fallbackKey
}
for existingKey := range headers {
if codexSessionHeaderKey(existingKey) && existingKey != selectedKey {
delete(headers, existingKey)
}
}
headers[selectedKey] = []string{value}
}
func codexSessionHeaderKey(key string) bool {
normalized := strings.ToLower(strings.TrimSpace(key))
return normalized == "session_id" || normalized == "session-id"
}
func codexSessionHeaderKeyUsesUnderscore(key string) bool {
return strings.ToLower(strings.TrimSpace(key)) == "session_id"
}
func headerValueCaseInsensitive(headers http.Header, key string) string {
key = strings.TrimSpace(key)
if headers == nil || key == "" {
return ""
}
if val := strings.TrimSpace(headers.Get(key)); val != "" {
return val
}
for existingKey, values := range headers {
if !strings.EqualFold(existingKey, key) {
continue
}
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
}
return ""
}
func deleteHeaderCaseInsensitive(headers http.Header, key string) {
for existingKey := range headers {
if strings.EqualFold(existingKey, key) {
delete(headers, existingKey)
}
}
}
func codexHeaderDefaults(cfg *config.Config, auth *cliproxyauth.Auth) (string, string) {
if cfg == nil || auth == nil || codexAuthUsesAPIKey(auth) {
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)
}
}