Something went wrong. Try again.
sloproxy
Something went wrong. Try again.
18 kB · 491 lines
Go
at commit 42eef103
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492package executor
import ( "bytes" "context" "fmt" "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/registry" "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" "github.com/tidwall/sjson")
const ( 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" codexResponsesLiteMetadata = "client_metadata.ws_request_header_x_openai_internal_codex_responses_lite")
var dataTag = []byte("data:")
func translateCodexRequestPair(from, to sdktranslator.Format, model string, originalPayload, payload []byte, stream bool) ([]byte, []byte) { if bytes.Equal(originalPayload, payload) { body := sdktranslator.TranslateRequest(from, to, model, payload, stream) return body, body } originalTranslated := sdktranslator.TranslateRequest(from, to, model, originalPayload, stream) body := sdktranslator.TranslateRequest(from, to, model, payload, stream) return originalTranslated, body}
// PrepareRequest injects Codex credentials into the outgoing HTTP request.func (e *CodexExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error { if req == nil { return nil } apiKey, _ := codexCreds(auth) if strings.TrimSpace(apiKey) != "" { req.Header.Set("Authorization", "Bearer "+apiKey) } var attrs map[string]string if auth != nil { attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(req, attrs) return nil}
// HttpRequest injects Codex credentials into the request and executes it.func (e *CodexExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) { if req == nil { return nil, fmt.Errorf("codex executor: request is nil") } if ctx == nil { ctx = req.Context() } httpReq := req.WithContext(ctx) if err := e.PrepareRequest(httpReq, auth); err != nil { return nil, err } httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq)}
type codexIdentityConfuseState struct { enabled bool authID string originalPromptCacheKey string promptCacheKey string turnIDs []codexIdentityReplacement}
type codexIdentityReplacement struct { original string confused string}
func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, userPayload []byte, rawJSON []byte, headerSets ...http.Header) (*http.Request, []byte, codexIdentityConfuseState, error) { var headers http.Header if len(headerSets) > 0 { headers = 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, headers) if errCache != nil { return nil, nil, codexIdentityConfuseState{}, errCache } if ok { cache = cached } } else if sourceFormatEqual(from, sdktranslator.FormatOpenAIResponse) { promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key") if promptCacheKey.Exists() { cache.ID = promptCacheKey.String() } } else if sourceFormatEqual(from, sdktranslator.FormatOpenAI) { if promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key"); promptCacheKey.Exists() { cache.ID = strings.TrimSpace(promptCacheKey.String()) } if cache.ID == "" { cache.ID = helps.ProviderSessionUUID("codex", req.Metadata) } if cache.ID == "" { if apiKey := strings.TrimSpace(helps.APIKeyFromContext(ctx)); apiKey != "" { cache.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+apiKey)).String() } } } if cache.ID == "" { cache.ID = helps.ProviderSessionUUID("codex", req.Metadata) }
if cache.ID != "" { rawJSON = helps.SetStringIfDifferent(rawJSON, "prompt_cache_key", cache.ID) } rawJSON = helps.SanitizeCodexInputItemIDs(rawJSON) var identityState codexIdentityConfuseState rawJSON, identityState = applyCodexIdentityConfuseBody(e.cfg, auth, userPayload, rawJSON) if identityState.promptCacheKey != "" { cache.ID = identityState.promptCacheKey } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(rawJSON)) if err != nil { return nil, nil, codexIdentityConfuseState{}, err } if cache.ID != "" { httpReq.Header.Set("Session_id", cache.ID) } return httpReq, rawJSON, identityState, nil}
func applyCodexIdentityConfuseBody(cfg *config.Config, auth *cliproxyauth.Auth, userPayload []byte, rawJSON []byte) ([]byte, codexIdentityConfuseState) { if !codexIdentityConfuseEnabled(cfg) || auth == nil || strings.TrimSpace(auth.ID) == "" || len(rawJSON) == 0 { return rawJSON, codexIdentityConfuseState{} }
state := codexIdentityConfuseState{enabled: true, authID: strings.TrimSpace(auth.ID)} if promptCacheKey := strings.TrimSpace(gjson.GetBytes(userPayload, "prompt_cache_key").String()); promptCacheKey != "" { state.originalPromptCacheKey = promptCacheKey state.promptCacheKey = codexIdentityConfuseUUID(auth.ID, "prompt-cache", promptCacheKey) rawJSON = helps.SetStringIfDifferent(rawJSON, "prompt_cache_key", state.promptCacheKey) } if installationID := strings.TrimSpace(gjson.GetBytes(userPayload, "client_metadata.x-codex-installation-id").String()); installationID != "" { rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-installation-id", codexIdentityConfuseUUID(auth.ID, "installation", installationID)) } if turnMetadata := strings.TrimSpace(gjson.GetBytes(rawJSON, "client_metadata.x-codex-turn-metadata").String()); turnMetadata != "" { rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-turn-metadata", applyCodexTurnMetadataIdentityConfuse(turnMetadata, &state)) } if state.promptCacheKey != "" { if windowID := strings.TrimSpace(gjson.GetBytes(rawJSON, "client_metadata.x-codex-window-id").String()); windowID != "" { rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-window-id", state.promptCacheKey+":0") } }
return rawJSON, state}
func applyCodexIdentityConfuseHeaders(headers http.Header, state *codexIdentityConfuseState) { if headers == nil { return } if state == nil || !state.enabled { return }
if rawTurnMetadata := strings.TrimSpace(headers.Get("X-Codex-Turn-Metadata")); rawTurnMetadata != "" { headers.Set("X-Codex-Turn-Metadata", applyCodexTurnMetadataIdentityConfuse(rawTurnMetadata, state)) } if state.promptCacheKey == "" { return }
setCodexSessionHeaderCasePreserved(headers, "Session_id", state.promptCacheKey) if headerValueCaseInsensitive(headers, "Conversation_id") != "" { setHeaderCasePreserved(headers, "Conversation_id", state.promptCacheKey) } headers.Set("X-Client-Request-Id", state.promptCacheKey) headers.Set("Thread-Id", state.promptCacheKey) headers.Set("X-Codex-Window-Id", state.promptCacheKey+":0")}
func applyCodexTurnMetadataIdentityConfuse(rawTurnMetadata string, state *codexIdentityConfuseState) string { updatedTurnMetadata := rawTurnMetadata if state == nil || !state.enabled { return updatedTurnMetadata } if state.promptCacheKey != "" && gjson.Get(rawTurnMetadata, "prompt_cache_key").Exists() { updatedTurnMetadata, _ = sjson.Set(updatedTurnMetadata, "prompt_cache_key", state.promptCacheKey) } else if state.promptCacheKey != "" && state.originalPromptCacheKey != "" { updatedTurnMetadata = strings.ReplaceAll(updatedTurnMetadata, state.originalPromptCacheKey, state.promptCacheKey) } if turnID := strings.TrimSpace(gjson.Get(rawTurnMetadata, "turn_id").String()); turnID != "" { updatedTurnMetadata, _ = sjson.Set(updatedTurnMetadata, "turn_id", state.confuseTurnID(turnID)) } if state.promptCacheKey != "" && gjson.Get(rawTurnMetadata, "window_id").Exists() { updatedTurnMetadata, _ = sjson.Set(updatedTurnMetadata, "window_id", state.promptCacheKey+":0") } return updatedTurnMetadata}
func applyCodexIdentityConfuseResponsePayload(payload []byte, state codexIdentityConfuseState) []byte { payload = replaceCodexIdentityResponsePayload(payload, state.originalPromptCacheKey, state.promptCacheKey) for _, turnID := range state.turnIDs { payload = replaceCodexIdentityResponsePayload(payload, turnID.original, turnID.confused) } return payload}
func applyCodexIdentityExposeResponsePayload(payload []byte, state codexIdentityConfuseState) []byte { payload = replaceCodexIdentityResponsePayload(payload, state.promptCacheKey, state.originalPromptCacheKey) for _, turnID := range state.turnIDs { payload = replaceCodexIdentityResponsePayload(payload, turnID.confused, turnID.original) } return payload}
func (state *codexIdentityConfuseState) confuseTurnID(turnID string) string { turnID = strings.TrimSpace(turnID) if state == nil || !state.enabled || strings.TrimSpace(state.authID) == "" || turnID == "" { return turnID } for _, replacement := range state.turnIDs { if replacement.original == turnID || replacement.confused == turnID { return replacement.confused } } confusedTurnID := codexIdentityConfuseUUID(state.authID, "turn", turnID) state.turnIDs = append(state.turnIDs, codexIdentityReplacement{original: turnID, confused: confusedTurnID}) return confusedTurnID}
func replaceCodexIdentityResponsePayload(payload []byte, from string, to string) []byte { from = strings.TrimSpace(from) to = strings.TrimSpace(to) if len(payload) == 0 || from == "" || to == "" || from == to || !bytes.Contains(payload, []byte(from)) { return payload } return bytes.ReplaceAll(payload, []byte(from), []byte(to))}
func codexIdentityConfuseEnabled(cfg *config.Config) bool { if cfg == nil || !cfg.Codex.IdentityConfuse { return false } strategy := strings.ToLower(strings.TrimSpace(cfg.Routing.Strategy)) return cfg.Routing.SessionAffinity || strategy == "fill-first" || strategy == "fillfirst" || strategy == "ff"}
func codexIdentityConfuseUUID(authID string, kind string, value string) string { name := strings.Join([]string{"cli-proxy-api", "codex", "identity-confuse", kind, strings.TrimSpace(authID), strings.TrimSpace(value)}, ":") return uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)).String()}
func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config) { var ginHeaders http.Header if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { ginHeaders = ginCtx.Request.Header } applyCodexHeadersFromSources(r, auth, token, stream, cfg, ginHeaders)}
// applyModelHeaderOverrides forces models.json config.override_header onto upstream headers.func applyModelHeaderOverrides(headers http.Header, modelName string) { if headers == nil { return } overrides := registry.ModelOverrideHeaders(modelName) if len(overrides) == 0 { return } for key, value := range overrides { headers.Set(key, value) } if strings.Contains(headers.Get("User-Agent"), "Mac OS") && codexSessionHeaderValue(headers) == "" { headers.Set("Session_id", uuid.NewString()) }}
// applyCodexDirectImageHeaders sets Codex upstream headers for direct /images/* calls.// Downstream client User-Agent values are not forwarded to reduce Cloudflare 1010 blocks.func applyCodexDirectImageHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config) { var ginHeaders http.Header if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { ginHeaders = ginCtx.Request.Header.Clone() ginHeaders.Del("User-Agent") } applyCodexHeadersFromSources(r, auth, token, stream, cfg, ginHeaders)}
func applyCodexHeadersFromSources(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config, ginHeaders http.Header) { r.Header.Set("Content-Type", "application/json") r.Header.Set("Authorization", "Bearer "+token)
if ginHeaders != nil && ginHeaders.Get("X-Codex-Beta-Features") != "" { r.Header.Set("X-Codex-Beta-Features", ginHeaders.Get("X-Codex-Beta-Features")) } misc.EnsureHeader(r.Header, ginHeaders, "Version", "") misc.EnsureHeader(r.Header, ginHeaders, "X-Codex-Turn-Metadata", "") misc.EnsureHeader(r.Header, ginHeaders, "X-Client-Request-Id", "") cfgUserAgent, _ := codexHeaderDefaults(cfg, auth) ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)
if strings.Contains(r.Header.Get("User-Agent"), "Mac OS") { misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString()) }
if stream { r.Header.Set("Accept", "text/event-stream") } else { r.Header.Set("Accept", "application/json") } r.Header.Set("Connection", "Keep-Alive")
isAPIKey := false if auth != nil && auth.Attributes != nil { if v := strings.TrimSpace(auth.Attributes["api_key"]); v != "" { isAPIKey = true } } if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" { r.Header.Set("Originator", originator) } else if !isAPIKey { r.Header.Set("Originator", codexOriginator) } if !isAPIKey { if auth != nil && auth.Metadata != nil { if accountID, ok := auth.Metadata["account_id"].(string); ok { r.Header.Set("Chatgpt-Account-Id", accountID) } } } var attrs map[string]string if auth != nil { attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(r, attrs) applyCodexCloakingHeaders(r.Header, cfg)}
func applyCodexCloakingHeaders(headers http.Header, cfg *config.Config) { if headers == nil || cfg == nil || cfg.Codex.DisableCodexCloaking { return } headers.Set("User-Agent", codexUserAgent) headers.Set("Originator", codexOriginator)}
func normalizeCodexInstructions(body []byte) []byte { instructions := gjson.GetBytes(body, "instructions") if !instructions.Exists() || instructions.Type == gjson.Null { body, _ = sjson.SetBytes(body, "instructions", "") } return body}
var imageGenToolJSON = []byte(`{"type":"image_generation","output_format":"png"}`)var imageGenToolArrayJSON = []byte(`[{"type":"image_generation","output_format":"png"}]`)
func isCodexFreePlanAuth(auth *cliproxyauth.Auth) bool { if auth == nil || auth.Attributes == nil { return false } if !strings.EqualFold(strings.TrimSpace(auth.Provider), "codex") { return false } return strings.EqualFold(strings.TrimSpace(auth.Attributes["plan_type"]), "free")}
func isImageGenerationFunctionTool(tool gjson.Result) bool { switch tool.Get("type").String() { case "function": return tool.Get("name").String() == "image_gen.imagegen" case "namespace": if tool.Get("name").String() != "image_gen" { return false } tools := tool.Get("tools") if !tools.IsArray() { return false } for _, nestedTool := range tools.Array() { if nestedTool.Get("type").String() == "function" && nestedTool.Get("name").String() == "imagegen" { return true } } } return false}
func isCodexResponsesLiteRequest(body []byte, headers http.Header) bool { if strings.EqualFold(strings.TrimSpace(headers.Get(codexResponsesLiteHeader)), "true") { return true } // Codex Desktop mirrors websocket-only request headers into client_metadata. value := gjson.GetBytes(body, codexResponsesLiteMetadata) if !value.Exists() { return false } return value.Type == gjson.True || value.Type == gjson.String && strings.EqualFold(strings.TrimSpace(value.String()), "true")}
func ensureImageGenerationTool(body []byte, baseModel string, auth *cliproxyauth.Auth, headers http.Header) []byte { if isCodexResponsesLiteRequest(body, headers) { return body } if strings.HasSuffix(baseModel, "spark") { return body } if isCodexFreePlanAuth(auth) { return body }
tools := gjson.GetBytes(body, "tools") if !tools.Exists() || !tools.IsArray() { body, _ = sjson.SetRawBytes(body, "tools", imageGenToolArrayJSON) return body } for _, t := range tools.Array() { if t.Get("type").String() == "image_generation" || isImageGenerationFunctionTool(t) { return body } } body, _ = sjson.SetRawBytes(body, "tools.-1", imageGenToolJSON) return body}
func normalizeCodexParallelToolCalls(body []byte, headers http.Header) []byte { if isCodexResponsesLiteRequest(body, headers) { body = helps.SetBoolIfDifferent(body, "parallel_tool_calls", false) return body } return normalizeCodexParallelToolCallsForTools(body)}
func normalizeCodexParallelToolCallsForTools(body []byte) []byte { if !gjson.GetBytes(body, "parallel_tool_calls").Exists() { return body }
tools := gjson.GetBytes(body, "tools") hasTools := tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 if hasTools { return body }
body, _ = sjson.DeleteBytes(body, "parallel_tool_calls") return body}
func publishCodexImageToolUsage(ctx context.Context, reporter *helps.UsageReporter, body []byte, completedData []byte) { detail, ok := helps.ParseCodexImageToolUsage(completedData) if !ok { return } reporter.EnsurePublished(ctx) reporter.PublishAdditionalModel(ctx, codexImageGenerationToolModel(body), detail)}
func codexImageGenerationToolModel(body []byte) string { tools := gjson.GetBytes(body, "tools") if tools.IsArray() { for _, tool := range tools.Array() { if tool.Get("type").String() != "image_generation" { continue } if model := strings.TrimSpace(tool.Get("model").String()); model != "" { return model } break } } return codexDefaultImageToolModel}