Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
493 changes: 453 additions & 40 deletions internal/runtime/executor/codex_executor_auth.go

Large diffs are not rendered by default.

97 changes: 66 additions & 31 deletions internal/runtime/executor/codex_executor_execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
}
baseModel := thinking.ParseSuffix(req.Model).ModelName

apiKey, baseURL := codexCreds(auth)
apiKey, baseURL := codexCredsForRequest(ctx, auth, opts.Headers)
if errSIWC := ensureCodexSIWCCredentialsForRequest(ctx, auth, apiKey, opts.Headers); errSIWC != nil {
return resp, errSIWC
}
if baseURL == "" {
baseURL = "https://chatgpt.com/backend-api/codex"
}
Expand All @@ -45,45 +48,60 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
}
originalPayload := originalPayloadSource
isCompat := e.resolveCodexModelIsCompat(auth, req, baseModel)
originalTranslated, body, updatesChanged := translateCodexRequestPairWithUpdateIntent(from, to, baseModel, originalPayload, req.Payload, false, isCompat)
targetModel := resolveResponsesSignatureTargetModel(baseModel, req, opts)
originalTranslated, body, updatesChanged, err := translateCodexRequestPairWithUpdateIntent(from, to, targetModel, originalPayload, req.Payload, false, isCompat)
if err != nil {
return resp, err
}
originalTranslated = helps.SetStringIfDifferent(originalTranslated, "model", baseModel)
body = helps.SetStringIfDifferent(body, "model", baseModel)

body, err = helps.ApplyRequestThinking(body, req, opts, from.String(), to.String(), e.Identifier(), updatesChanged)
if err != nil {
return resp, err
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
requestPath := helps.PayloadRequestPath(opts)
body = helps.ApplyPayloadConfigWithRequestForExecutor(e.cfg, e.Identifier(), baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
ctx = helps.WithPayloadFinalizer(ctx, helps.NewPayloadFinalizer(e.cfg, e.Identifier(), baseModel, to.String(), "", originalTranslated, req, opts))
body = helps.SetStringIfDifferent(body, "model", baseModel)
body = helps.SetBoolIfDifferent(body, "stream", true)
body, _ = sjson.DeleteBytes(body, "previous_response_id")
body, _ = sjson.DeleteBytes(body, "generate")
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
body, _ = sjson.DeleteBytes(body, "safety_identifier")
body, _ = sjson.DeleteBytes(body, "stream_options")
body = normalizeCodexInstructions(body, helps.IsNativeCodexRequest(req.Payload, opts))
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
body = ensureImageGenerationTool(body, baseModel, auth, opts.Headers)
useSIWC := codexUseSIWCForRequest(ctx, auth, opts.Headers)
hasExplicitInstructions := gjson.GetBytes(req.Payload, "instructions").Exists() && strings.TrimSpace(gjson.GetBytes(req.Payload, "instructions").String()) != ""
body = ensureWebSearchToolForHistory(body, opts.Headers)
if !useSIWC {
body = normalizeCodexInstructions(body, helps.IsNativeCodexRequest(req.Payload, opts))
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
body = ensureImageGenerationTool(body, baseModel, auth, opts.Headers)
}
} else {
body = applyCodexSIWCRequestBodyRules(body, hasExplicitInstructions, cliproxyexecutor.Options{KeepTopLevelEffort: thinking.HasThinkingSuffix(req.Model)})
}
body = sanitizeOpenAIResponsesReasoningEncryptedContentWithCompat(ctx, "codex executor", body, isCompat)
body = normalizeCodexParallelToolCalls(body, opts.Headers)
body = helps.NormalizeCodexToolSchemas(body)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, isCompat)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return resp, errReplay
}
reporter.SetTranslatedReasoningEffort(body, to.String())
body = sanitizeOpenAIResponsesReasoningEncryptedContentWithCompatModelAndBaseURL(ctx, "codex executor", body, isCompat, targetModel, baseURL)

url := strings.TrimSuffix(baseURL, "/") + "/responses"
httpReq, upstreamBody, err := e.cacheHelper(ctx, from, url, req, body, opts.Headers)
httpReq, upstreamBody, err := e.cacheHelper(ctx, from, url, req, body, auth, opts.Headers)
if err != nil {
return resp, err
}
applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg, opts.Headers)
applyCodexRoutingHint(ctx, httpReq.Header, auth, baseModel, upstreamBody, opts.Headers)
applyModelHeaderOverrides(httpReq.Header, baseModel)
reporter.SetTranslatedReasoningEffort(upstreamBody, to.String())
Comment on lines +93 to +97

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Use the finalized payload for image-tool usage attribution

Payload rules now run inside cacheHelper, but the finalized upstreamBody never replaces body. For a request declaring image_generation with model gpt-image-2 and an override setting tools.0.model to gpt-image-1.5, upstream receives gpt-image-1.5 while publishCodexImageToolUsage reads the old body and attributes its tokens to gpt-image-2. This reproduces in both Execute here and ExecuteStream at codex_executor_stream.go:100. Also reported for this defect: - Use the finalized tool model for image usage (internal/runtime/executor/codex_executor_execute.go:93): After cacheHelper applies payload rules, only upstreamBody contains the configured payload; body remains unconfigured. With an override changing tools.0.model from gpt-image-2 to gpt-image-1.5, the request correctly sends gpt-image-1.5, but publishCodexImageToolUsage still receives body and records the image tokens against gpt-image-2. This reproduces in both Execute and ExecuteStream, including its buffered and unbuffered completion branches, producing incorrect model-specific usage accounting.

Also affects:

prepareHeaders := func(request *http.Request) {
applyCodexHeaders(request, auth, apiKey, true, e.cfg, opts.Headers)
applyCodexRoutingHint(ctx, request.Header, auth, baseModel, upstreamBody, opts.Headers)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Derive routing hints from the finalized model

Payload model overrides now survive finalization, but the routing hint still uses the original baseModel. For example, overriding a gpt-5.4 request to gpt-5.5 sends body.model=gpt-5.5 alongside X-Codex-Routing-Hint: model=gpt-5.4, so the generated routing metadata names the wrong model. The same mismatch affects executeCompact, ExecuteStream, CodexWebsocketsExecutor.Execute, and prepareCodexWebsocketStream. Derive the model from the final payload while retaining explicit operator-header precedence.

applyModelHeaderOverrides(request.Header, baseModel)
alignCodexContentEncoding(request)
}
prepareHeaders(httpReq)
var authID, authLabel, authType, authValue string
if auth != nil {
authID = auth.ID
Expand All @@ -103,7 +121,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
})
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
httpClient = reporter.TrackHTTPClient(httpClient)
httpResp, err := httpClient.Do(httpReq)
httpResp, err := doCodexRequest(httpClient, httpReq, upstreamBody, prepareHeaders, nil)
if err != nil {
helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err
Expand All @@ -127,18 +145,19 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
data, errRead := io.ReadAll(httpResp.Body)
helps.AppendAPIResponseChunk(ctx, e.cfg, data)

lines := bytes.Split(data, []byte("\n"))
events := helps.NewCodexSSEScanner(ctx, bytes.NewReader(data), len(data)+1)
outputItemsByIndex := make(map[int64][]byte)
var outputItemsFallback [][]byte
sawOutputDelta := false
for _, line := range lines {
for events.Scan() {
line := events.Bytes()
if !bytes.HasPrefix(line, dataTag) {
continue
}

eventData := bytes.TrimSpace(line[5:])
eventData = helps.RestoreCodexMultiAgentV2Response(eventData, optimizeMultiAgentV2)
reporter.ObserveCodexResponseModel(eventData)
reporter.ObserveResponseModel(eventData)
eventType := gjson.GetBytes(eventData, "type").String()

if helps.HasMeaningfulCodexOutputDelta(eventData) {
Expand Down Expand Up @@ -211,7 +230,10 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
baseModel := thinking.ParseSuffix(req.Model).ModelName

apiKey, baseURL := codexCreds(auth)
apiKey, baseURL := codexCredsForRequest(ctx, auth, opts.Headers)
if errSIWC := ensureCodexSIWCCredentialsForRequest(ctx, auth, apiKey, opts.Headers); errSIWC != nil {
return resp, errSIWC
}
if baseURL == "" {
baseURL = "https://chatgpt.com/backend-api/codex"
}
Expand All @@ -228,33 +250,46 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
}
originalPayload := originalPayloadSource
isCompat := e.resolveCodexModelIsCompat(auth, req, baseModel)
originalTranslated, body, updatesChanged := translateCodexRequestPairWithUpdateIntent(from, to, baseModel, originalPayload, req.Payload, false, isCompat)
targetModel := resolveResponsesSignatureTargetModel(baseModel, req, opts)
originalTranslated, body, updatesChanged, err := translateCodexRequestPairWithUpdateIntent(from, to, targetModel, originalPayload, req.Payload, false, isCompat)
if err != nil {
return resp, err
}
originalTranslated = helps.SetStringIfDifferent(originalTranslated, "model", baseModel)

body, err = helps.ApplyRequestThinking(body, req, opts, from.String(), to.String(), e.Identifier(), updatesChanged)
if err != nil {
return resp, err
}

requestedModel := helps.PayloadRequestedModel(opts, req.Model)
requestPath := helps.PayloadRequestPath(opts)
body = helps.ApplyPayloadConfigWithRequestForExecutor(e.cfg, e.Identifier(), baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
ctx = helps.WithPayloadFinalizer(ctx, helps.NewPayloadFinalizer(e.cfg, e.Identifier(), baseModel, to.String(), "", originalTranslated, req, opts))
body = helps.SetStringIfDifferent(body, "model", baseModel)
body, _ = sjson.DeleteBytes(body, "stream")
body = normalizeCodexInstructions(body, helps.IsNativeCodexRequest(req.Payload, opts))
body = sanitizeOpenAIResponsesReasoningEncryptedContentWithCompat(ctx, "codex executor", body, isCompat)
if !codexUseSIWCForRequest(ctx, auth, opts.Headers) {
body = normalizeCodexInstructions(body, helps.IsNativeCodexRequest(req.Payload, opts))
} else {
hasExplicitInstructions := gjson.GetBytes(req.Payload, "instructions").Exists() && strings.TrimSpace(gjson.GetBytes(req.Payload, "instructions").String()) != ""
body = applyCodexSIWCCompactBodyRules(body, hasExplicitInstructions, cliproxyexecutor.Options{KeepTopLevelEffort: thinking.HasThinkingSuffix(req.Model)})
}
body = ensureWebSearchToolForHistory(body, opts.Headers)
body = sanitizeOpenAIResponsesReasoningEncryptedContentWithCompatModelAndBaseURL(ctx, "codex executor", body, isCompat, resolveResponsesSignatureTargetModel(baseModel, req, opts), baseURL)
body = normalizeCodexParallelToolCalls(body, opts.Headers)
body = helps.NormalizeCodexToolSchemas(body)
body, optimizeMultiAgentV2 := helps.OptimizeCodexMultiAgentV2RequestForAuth(ctx, opts.Headers, body, e.cfg, auth, isCompat)
reporter.SetTranslatedReasoningEffort(body, to.String())

url := strings.TrimSuffix(baseURL, "/") + "/responses/compact"
httpReq, upstreamBody, err := e.cacheHelper(ctx, from, url, req, body, opts.Headers)
httpReq, upstreamBody, err := e.cacheHelper(ctx, from, url, req, body, auth, opts.Headers)
if err != nil {
return resp, err
}
applyCodexHeaders(httpReq, auth, apiKey, false, e.cfg, opts.Headers)
applyCodexRoutingHint(ctx, httpReq.Header, auth, baseModel, upstreamBody, opts.Headers)
applyModelHeaderOverrides(httpReq.Header, baseModel)
reporter.SetTranslatedReasoningEffort(upstreamBody, to.String())
prepareHeaders := func(request *http.Request) {
applyCodexHeaders(request, auth, apiKey, false, e.cfg, opts.Headers)
applyCodexRoutingHint(ctx, request.Header, auth, baseModel, upstreamBody, opts.Headers)
applyModelHeaderOverrides(request.Header, baseModel)
alignCodexContentEncoding(request)
}
prepareHeaders(httpReq)
var authID, authLabel, authType, authValue string
if auth != nil {
authID = auth.ID
Expand All @@ -274,7 +309,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
})
httpClient := helps.NewUtlsHTTPClient(ctx, e.cfg, auth, 0)
httpClient = reporter.TrackHTTPClient(httpClient)
httpResp, err := httpClient.Do(httpReq)
httpResp, err := doCodexRequest(httpClient, httpReq, upstreamBody, prepareHeaders, nil)
if err != nil {
helps.RecordAPIResponseError(ctx, e.cfg, err)
return resp, err
Expand Down
21 changes: 18 additions & 3 deletions internal/runtime/executor/codex_executor_reasoning.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ func codexInputHasValidReasoningEncryptedContent(body []byte) bool {
if encryptedContent.Type != gjson.String {
continue
}
if _, err := signature.InspectGPTReasoningSignature(encryptedContent.String()); err == nil {
rawSig := signature.SignaturePayloadWithoutProviderPrefix(encryptedContent.String())
if signature.IsValidGPTReasoningSignature(rawSig) || signature.IsValidMetaReasoningSignature(rawSig) || signature.IsValidGrokEncryptedContent(rawSig) {
return true
}
}
Expand Down Expand Up @@ -361,6 +362,20 @@ func codexReasoningReplayTurnAnchorIndex(inputItems []gjson.Result, turn codexRe
return 0, false
}

func canonicalCodexReasoningReplaySignatureKey(rawSignature string) string {
sig := signature.SignaturePayloadWithoutProviderPrefix(strings.TrimSpace(rawSignature))
if gptInfo, err := signature.InspectGPTReasoningSignature(sig); err == nil && gptInfo.RouterSuffix != "" {
return stripRouterSuffix(sig, gptInfo.RouterSuffix)
}
if metaInfo, err := signature.InspectMetaReasoningSignature(sig); err == nil && metaInfo.RouterSuffix != "" {
return stripRouterSuffix(sig, metaInfo.RouterSuffix)
}
if grokInfo, err := signature.InspectGrokEncryptedContent(sig); err == nil && grokInfo.RouterSuffix != "" {
return stripRouterSuffix(sig, grokInfo.RouterSuffix)
}
return sig
}

func filterCodexReasoningReplayTurnItems(inputItems []gjson.Result, items [][]byte) [][]byte {
existingReasoning := make(map[string]bool)
existingCalls := make(map[string]bool)
Expand All @@ -369,7 +384,7 @@ func filterCodexReasoningReplayTurnItems(inputItems []gjson.Result, items [][]by
itemType := strings.TrimSpace(inputItem.Get("type").String())
switch itemType {
case "reasoning":
if encryptedContent := strings.TrimSpace(inputItem.Get("encrypted_content").String()); encryptedContent != "" {
if encryptedContent := canonicalCodexReasoningReplaySignatureKey(inputItem.Get("encrypted_content").String()); encryptedContent != "" {
existingReasoning[encryptedContent] = true
}
case "function_call_output", "custom_tool_call_output":
Expand All @@ -387,7 +402,7 @@ func filterCodexReasoningReplayTurnItems(inputItems []gjson.Result, items [][]by
itemResult := gjson.ParseBytes(item)
switch strings.TrimSpace(itemResult.Get("type").String()) {
case "reasoning":
if existingReasoning[strings.TrimSpace(itemResult.Get("encrypted_content").String())] {
if existingReasoning[canonicalCodexReasoningReplaySignatureKey(itemResult.Get("encrypted_content").String())] {
continue
}
case "function_call", "custom_tool_call":
Expand Down
Loading
Loading