Repository navigation
Review only: HOUSE-79 upstream v8.0.44, part 3 of 13: codex executor #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
| } | ||
|
|
@@ -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()) | ||
| prepareHeaders := func(request *http.Request) { | ||
| applyCodexHeaders(request, auth, apiKey, true, e.cfg, opts.Headers) | ||
| applyCodexRoutingHint(ctx, request.Header, auth, baseModel, upstreamBody, opts.Headers) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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 | ||
|
|
@@ -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) { | ||
|
|
@@ -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" | ||
| } | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
||
There was a problem hiding this comment.
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: