package executor import ( "bufio" "bytes" "context" "encoding/base64" "encoding/json" "fmt" "io" "mime" "mime/multipart" "net/http" "sort" "strconv" "strings" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" 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" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson") const ( codexOpenAIImageSourceFormat = "openai-image" codexImagesGenerationsPath = "/v1/images/generations" codexImagesEditsPath = "/v1/images/edits" codexDirectImagesGenerations = "/images/generations" codexDirectImagesEdit = "/images/edits" codexGPTImage15Model = "gpt-image-1.5" codexGPTImage25FlareModel = "gpt-image-2.5-flare" codexGPTImage25SunburstModel = "gpt-image-2.5-sunburst" codexGPTImage25Model = "gpt-image-2.5" codexOpenAIImagesMainModel = "gpt-5.4-mini") type codexOpenAIImagePreparedRequest struct { Body []byte ResponseFormat string StreamPrefix string} type codexImageCallResult struct { Result string RevisedPrompt string OutputFormat string Size string Background string Quality string} func isCodexOpenAIImageRequest(opts cliproxyexecutor.Options) bool { if !strings.EqualFold(strings.TrimSpace(opts.SourceFormat.String()), codexOpenAIImageSourceFormat) { return false } return codexIsImagesEndpointPath(helps.PayloadRequestPath(opts))} func codexIsImagesEndpointPath(path string) bool { path = strings.TrimSpace(path) if path == codexImagesGenerationsPath || path == codexImagesEditsPath { return true } return strings.HasSuffix(path, codexImagesGenerationsPath) || strings.HasSuffix(path, codexImagesEditsPath)} func (e *CodexExecutor) resolveGPTImage2BaseModel() string { if e == nil || e.cfg == nil { return codexOpenAIImagesMainModel } model := strings.TrimSpace(e.cfg.GPTImage2BaseModel) if model == "" { return codexOpenAIImagesMainModel } if strings.HasPrefix(strings.ToLower(model), "gpt-") { return model } return codexOpenAIImagesMainModel} func (e *CodexExecutor) executeOpenAIImage(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { if directEndpoint := codexDirectOpenAIImageEndpoint(req, opts); directEndpoint != "" { return e.executeDirectOpenAIImage(ctx, auth, req, opts, directEndpoint) } prepared, errPrepare := codexPrepareOpenAIImageRequest(req, opts) if errPrepare != nil { return resp, errPrepare } apiKey, baseURL := codexCreds(auth) if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } mainModel := e.resolveGPTImage2BaseModel() reporter := helps.NewExecutorUsageReporter(ctx, e, mainModel, auth) defer reporter.TrackFailure(ctx, &err) body, errBuild := e.prepareCodexOpenAIImageBody(prepared.Body, req, opts, mainModel) if errBuild != nil { return resp, errBuild } reporter.SetTranslatedReasoningEffort(body, "codex") url := strings.TrimSuffix(baseURL, "/") + "/responses" var identityState codexIdentityConfuseState httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) if errCache != nil { return resp, errCache } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg, opts.Headers) applyModelHeaderOverrides(httpReq.Header, mainModel) applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { helps.RecordAPIResponseError(ctx, e.cfg, errDo) return resp, errDo } defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } }() helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) data, errRead := io.ReadAll(httpResp.Body) if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) return resp, err } outputItemsByIndex := make(map[int64][]byte) var outputItemsFallback [][]byte for _, line := range bytes.Split(data, []byte("\n")) { if !bytes.HasPrefix(line, dataTag) { continue } eventData := bytes.TrimSpace(line[len(dataTag):]) switch gjson.GetBytes(eventData, "type").String() { case "response.output_item.done": collectCodexOutputItemDone(eventData, outputItemsByIndex, &outputItemsFallback) case "response.completed": if detail, ok := helps.ParseCodexUsage(eventData); ok { reporter.Publish(ctx, detail) } publishCodexImageToolUsage(ctx, reporter, body, eventData) results, createdAt, usageRaw, firstMeta, errExtract := codexExtractImageResults(eventData, outputItemsByIndex, outputItemsFallback) if errExtract != nil { return resp, errExtract } if len(results) == 0 { return resp, statusErr{code: http.StatusBadGateway, msg: "upstream did not return image output"} } out, errOutput := codexBuildImagesAPIResponse(results, createdAt, usageRaw, firstMeta, prepared.ResponseFormat) if errOutput != nil { return resp, errOutput } return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil } } err = statusErr{code: http.StatusGatewayTimeout, msg: "stream error: stream disconnected before completion"} return resp, err} func (e *CodexExecutor) executeOpenAIImageStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) { if directEndpoint := codexDirectOpenAIImageEndpoint(req, opts); directEndpoint != "" { return e.executeDirectOpenAIImageStream(ctx, auth, req, opts, directEndpoint) } prepared, errPrepare := codexPrepareOpenAIImageRequest(req, opts) if errPrepare != nil { return nil, errPrepare } apiKey, baseURL := codexCreds(auth) if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } mainModel := e.resolveGPTImage2BaseModel() reporter := helps.NewExecutorUsageReporter(ctx, e, mainModel, auth) defer reporter.TrackFailure(ctx, &err) body, errBuild := e.prepareCodexOpenAIImageBody(prepared.Body, req, opts, mainModel) if errBuild != nil { return nil, errBuild } reporter.SetTranslatedReasoningEffort(body, "codex") url := strings.TrimSuffix(baseURL, "/") + "/responses" var identityState codexIdentityConfuseState httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) if errCache != nil { return nil, errCache } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg, opts.Headers) applyModelHeaderOverrides(httpReq.Header, mainModel) applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { helps.RecordAPIResponseError(ctx, e.cfg, errDo) return nil, errDo } helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { data, errRead := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return nil, errRead } data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) return nil, err } out := make(chan cliproxyexecutor.StreamChunk) go func() { defer close(out) defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } }() sendPayload := func(payload []byte) bool { select { case out <- cliproxyexecutor.StreamChunk{Payload: payload}: return true case <-ctx.Done(): return false } } sendError := func(errSend error) bool { select { case out <- cliproxyexecutor.StreamChunk{Err: errSend}: return true case <-ctx.Done(): return false } } scanner := bufio.NewScanner(httpResp.Body) scanner.Buffer(nil, 52_428_800) // 50MB outputItemsByIndex := make(map[int64][]byte) var outputItemsFallback [][]byte for scanner.Scan() { line := applyCodexIdentityConfuseResponsePayload(scanner.Bytes(), identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, line) if !bytes.HasPrefix(line, dataTag) { continue } eventData := bytes.TrimSpace(line[len(dataTag):]) switch gjson.GetBytes(eventData, "type").String() { case "response.output_item.done": collectCodexOutputItemDone(eventData, outputItemsByIndex, &outputItemsFallback) case "response.image_generation_call.partial_image": frame := codexBuildImagePartialFrame(eventData, prepared.ResponseFormat, prepared.StreamPrefix) if len(frame) > 0 && !sendPayload(frame) { return } case "response.completed": if detail, ok := helps.ParseCodexUsage(eventData); ok { reporter.Publish(ctx, detail) } publishCodexImageToolUsage(ctx, reporter, body, eventData) results, _, usageRaw, _, errExtract := codexExtractImageResults(eventData, outputItemsByIndex, outputItemsFallback) if errExtract != nil { sendError(errExtract) return } if len(results) == 0 { sendError(statusErr{code: http.StatusBadGateway, msg: "upstream did not return image output"}) return } for _, img := range results { frame := codexBuildImageCompletedFrame(img, usageRaw, prepared.ResponseFormat, prepared.StreamPrefix) if len(frame) > 0 && !sendPayload(frame) { return } } return } } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) reporter.PublishFailure(ctx, errScan) sendError(errScan) } }() return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil} func (e *CodexExecutor) executeDirectOpenAIImage(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, endpointPath string) (resp cliproxyexecutor.Response, err error) { body, contentType, model, errPrepare := codexPrepareDirectOpenAIImageBody(req, opts, false) if errPrepare != nil { return resp, errPrepare } apiKey, baseURL := codexCreds(auth) if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } reporter := helps.NewExecutorUsageReporter(ctx, e, model, auth) defer reporter.TrackFailure(ctx, &err) reporter.SetTranslatedReasoningEffort(body, "openai") url := strings.TrimSuffix(baseURL, "/") + endpointPath var identityState codexIdentityConfuseState httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) if errCache != nil { return resp, errCache } applyCodexDirectImageHeaders(httpReq, auth, apiKey, false, e.cfg) applyModelHeaderOverrides(httpReq.Header, model) if contentType != "" { httpReq.Header.Set("Content-Type", contentType) } applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { helps.RecordAPIResponseError(ctx, e.cfg, errDo) return resp, errDo } defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } }() helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) data, errRead := io.ReadAll(httpResp.Body) if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) return resp, err } reporter.Publish(ctx, helps.ParseOpenAIUsage(data)) reporter.EnsurePublished(ctx) return cliproxyexecutor.Response{Payload: data, Headers: httpResp.Header.Clone()}, nil} func (e *CodexExecutor) executeDirectOpenAIImageStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, endpointPath string) (_ *cliproxyexecutor.StreamResult, err error) { body, contentType, model, errPrepare := codexPrepareDirectOpenAIImageBody(req, opts, true) if errPrepare != nil { return nil, errPrepare } apiKey, baseURL := codexCreds(auth) if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } reporter := helps.NewExecutorUsageReporter(ctx, e, model, auth) defer reporter.TrackFailure(ctx, &err) reporter.SetTranslatedReasoningEffort(body, "openai") url := strings.TrimSuffix(baseURL, "/") + endpointPath var identityState codexIdentityConfuseState httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) if errCache != nil { return nil, errCache } applyCodexDirectImageHeaders(httpReq, auth, apiKey, true, e.cfg) applyModelHeaderOverrides(httpReq.Header, model) if contentType != "" { httpReq.Header.Set("Content-Type", contentType) } applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { helps.RecordAPIResponseError(ctx, e.cfg, errDo) return nil, errDo } helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { data, errRead := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return nil, errRead } data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) return nil, err } out := make(chan cliproxyexecutor.StreamChunk) go func() { defer close(out) var streamUsage helps.StreamUsageBuffer defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } streamUsage.Publish(ctx, reporter) reporter.EnsurePublished(ctx) }() buffer := make([]byte, 32*1024) for { n, errRead := httpResp.Body.Read(buffer) if n > 0 { chunk := bytes.Clone(buffer[:n]) chunk = applyCodexIdentityConfuseResponsePayload(chunk, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, chunk) for _, line := range bytes.Split(chunk, []byte("\n")) { streamUsage.ObserveOpenAIStream(bytes.TrimSpace(line)) } select { case out <- cliproxyexecutor.StreamChunk{Payload: chunk}: case <-ctx.Done(): return } } if errRead != nil { if errRead != io.EOF { helps.RecordAPIResponseError(ctx, e.cfg, errRead) reporter.PublishFailure(ctx, errRead) select { case out <- cliproxyexecutor.StreamChunk{Err: errRead}: case <-ctx.Done(): } } return } } }() return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil} func codexDirectOpenAIImageEndpoint(req cliproxyexecutor.Request, opts cliproxyexecutor.Options) string { if codexDirectOpenAIImageModel(req) == "" { return "" } path := helps.PayloadRequestPath(opts) if strings.HasSuffix(strings.TrimSpace(path), codexImagesGenerationsPath) { return codexDirectImagesGenerations } if strings.HasSuffix(strings.TrimSpace(path), codexImagesEditsPath) { return codexDirectImagesEdit } return ""} func codexPrepareDirectOpenAIImageBody(req cliproxyexecutor.Request, opts cliproxyexecutor.Options, stream bool) ([]byte, string, string, error) { model := codexDirectOpenAIImageModel(req) if model == "" { return nil, "", "", fmt.Errorf("unsupported direct OpenAI image model %q", req.Model) } body, contentType, errPrepare := codexPrepareDirectOpenAIImagePayload(req, opts, model, stream) if errPrepare != nil { return nil, "", "", errPrepareLines 1–500 of 1126. Load more
package executor import ( "bufio" "bytes" "context" "encoding/base64" "encoding/json" "fmt" "io" "mime" "mime/multipart" "net/http" "sort" "strconv" "strings" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" 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" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) const ( codexOpenAIImageSourceFormat = "openai-image" codexImagesGenerationsPath = "/v1/images/generations" codexImagesEditsPath = "/v1/images/edits" codexDirectImagesGenerations = "/images/generations" codexDirectImagesEdit = "/images/edits" codexGPTImage15Model = "gpt-image-1.5" codexGPTImage25FlareModel = "gpt-image-2.5-flare" codexGPTImage25SunburstModel = "gpt-image-2.5-sunburst" codexGPTImage25Model = "gpt-image-2.5" codexOpenAIImagesMainModel = "gpt-5.4-mini" ) type codexOpenAIImagePreparedRequest struct { Body []byte ResponseFormat string StreamPrefix string } type codexImageCallResult struct { Result string RevisedPrompt string OutputFormat string Size string Background string Quality string } func isCodexOpenAIImageRequest(opts cliproxyexecutor.Options) bool { if !strings.EqualFold(strings.TrimSpace(opts.SourceFormat.String()), codexOpenAIImageSourceFormat) { return false } return codexIsImagesEndpointPath(helps.PayloadRequestPath(opts)) } func codexIsImagesEndpointPath(path string) bool { path = strings.TrimSpace(path) if path == codexImagesGenerationsPath || path == codexImagesEditsPath { return true } return strings.HasSuffix(path, codexImagesGenerationsPath) || strings.HasSuffix(path, codexImagesEditsPath) } func (e *CodexExecutor) resolveGPTImage2BaseModel() string { if e == nil || e.cfg == nil { return codexOpenAIImagesMainModel } model := strings.TrimSpace(e.cfg.GPTImage2BaseModel) if model == "" { return codexOpenAIImagesMainModel } if strings.HasPrefix(strings.ToLower(model), "gpt-") { return model } return codexOpenAIImagesMainModel } func (e *CodexExecutor) executeOpenAIImage(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) { if directEndpoint := codexDirectOpenAIImageEndpoint(req, opts); directEndpoint != "" { return e.executeDirectOpenAIImage(ctx, auth, req, opts, directEndpoint) } prepared, errPrepare := codexPrepareOpenAIImageRequest(req, opts) if errPrepare != nil { return resp, errPrepare } apiKey, baseURL := codexCreds(auth) if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } mainModel := e.resolveGPTImage2BaseModel() reporter := helps.NewExecutorUsageReporter(ctx, e, mainModel, auth) defer reporter.TrackFailure(ctx, &err) body, errBuild := e.prepareCodexOpenAIImageBody(prepared.Body, req, opts, mainModel) if errBuild != nil { return resp, errBuild } reporter.SetTranslatedReasoningEffort(body, "codex") url := strings.TrimSuffix(baseURL, "/") + "/responses" var identityState codexIdentityConfuseState httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) if errCache != nil { return resp, errCache } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg, opts.Headers) applyModelHeaderOverrides(httpReq.Header, mainModel) applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { helps.RecordAPIResponseError(ctx, e.cfg, errDo) return resp, errDo } defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } }() helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) data, errRead := io.ReadAll(httpResp.Body) if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) return resp, err } outputItemsByIndex := make(map[int64][]byte) var outputItemsFallback [][]byte for _, line := range bytes.Split(data, []byte("\n")) { if !bytes.HasPrefix(line, dataTag) { continue } eventData := bytes.TrimSpace(line[len(dataTag):]) switch gjson.GetBytes(eventData, "type").String() { case "response.output_item.done": collectCodexOutputItemDone(eventData, outputItemsByIndex, &outputItemsFallback) case "response.completed": if detail, ok := helps.ParseCodexUsage(eventData); ok { reporter.Publish(ctx, detail) } publishCodexImageToolUsage(ctx, reporter, body, eventData) results, createdAt, usageRaw, firstMeta, errExtract := codexExtractImageResults(eventData, outputItemsByIndex, outputItemsFallback) if errExtract != nil { return resp, errExtract } if len(results) == 0 { return resp, statusErr{code: http.StatusBadGateway, msg: "upstream did not return image output"} } out, errOutput := codexBuildImagesAPIResponse(results, createdAt, usageRaw, firstMeta, prepared.ResponseFormat) if errOutput != nil { return resp, errOutput } return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil } } err = statusErr{code: http.StatusGatewayTimeout, msg: "stream error: stream disconnected before completion"} return resp, err } func (e *CodexExecutor) executeOpenAIImageStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) { if directEndpoint := codexDirectOpenAIImageEndpoint(req, opts); directEndpoint != "" { return e.executeDirectOpenAIImageStream(ctx, auth, req, opts, directEndpoint) } prepared, errPrepare := codexPrepareOpenAIImageRequest(req, opts) if errPrepare != nil { return nil, errPrepare } apiKey, baseURL := codexCreds(auth) if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } mainModel := e.resolveGPTImage2BaseModel() reporter := helps.NewExecutorUsageReporter(ctx, e, mainModel, auth) defer reporter.TrackFailure(ctx, &err) body, errBuild := e.prepareCodexOpenAIImageBody(prepared.Body, req, opts, mainModel) if errBuild != nil { return nil, errBuild } reporter.SetTranslatedReasoningEffort(body, "codex") url := strings.TrimSuffix(baseURL, "/") + "/responses" var identityState codexIdentityConfuseState httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) if errCache != nil { return nil, errCache } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg, opts.Headers) applyModelHeaderOverrides(httpReq.Header, mainModel) applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { helps.RecordAPIResponseError(ctx, e.cfg, errDo) return nil, errDo } helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { data, errRead := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return nil, errRead } data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) return nil, err } out := make(chan cliproxyexecutor.StreamChunk) go func() { defer close(out) defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } }() sendPayload := func(payload []byte) bool { select { case out <- cliproxyexecutor.StreamChunk{Payload: payload}: return true case <-ctx.Done(): return false } } sendError := func(errSend error) bool { select { case out <- cliproxyexecutor.StreamChunk{Err: errSend}: return true case <-ctx.Done(): return false } } scanner := bufio.NewScanner(httpResp.Body) scanner.Buffer(nil, 52_428_800) // 50MB outputItemsByIndex := make(map[int64][]byte) var outputItemsFallback [][]byte for scanner.Scan() { line := applyCodexIdentityConfuseResponsePayload(scanner.Bytes(), identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, line) if !bytes.HasPrefix(line, dataTag) { continue } eventData := bytes.TrimSpace(line[len(dataTag):]) switch gjson.GetBytes(eventData, "type").String() { case "response.output_item.done": collectCodexOutputItemDone(eventData, outputItemsByIndex, &outputItemsFallback) case "response.image_generation_call.partial_image": frame := codexBuildImagePartialFrame(eventData, prepared.ResponseFormat, prepared.StreamPrefix) if len(frame) > 0 && !sendPayload(frame) { return } case "response.completed": if detail, ok := helps.ParseCodexUsage(eventData); ok { reporter.Publish(ctx, detail) } publishCodexImageToolUsage(ctx, reporter, body, eventData) results, _, usageRaw, _, errExtract := codexExtractImageResults(eventData, outputItemsByIndex, outputItemsFallback) if errExtract != nil { sendError(errExtract) return } if len(results) == 0 { sendError(statusErr{code: http.StatusBadGateway, msg: "upstream did not return image output"}) return } for _, img := range results { frame := codexBuildImageCompletedFrame(img, usageRaw, prepared.ResponseFormat, prepared.StreamPrefix) if len(frame) > 0 && !sendPayload(frame) { return } } return } } if errScan := scanner.Err(); errScan != nil { helps.RecordAPIResponseError(ctx, e.cfg, errScan) reporter.PublishFailure(ctx, errScan) sendError(errScan) } }() return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil } func (e *CodexExecutor) executeDirectOpenAIImage(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, endpointPath string) (resp cliproxyexecutor.Response, err error) { body, contentType, model, errPrepare := codexPrepareDirectOpenAIImageBody(req, opts, false) if errPrepare != nil { return resp, errPrepare } apiKey, baseURL := codexCreds(auth) if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } reporter := helps.NewExecutorUsageReporter(ctx, e, model, auth) defer reporter.TrackFailure(ctx, &err) reporter.SetTranslatedReasoningEffort(body, "openai") url := strings.TrimSuffix(baseURL, "/") + endpointPath var identityState codexIdentityConfuseState httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) if errCache != nil { return resp, errCache } applyCodexDirectImageHeaders(httpReq, auth, apiKey, false, e.cfg) applyModelHeaderOverrides(httpReq.Header, model) if contentType != "" { httpReq.Header.Set("Content-Type", contentType) } applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { helps.RecordAPIResponseError(ctx, e.cfg, errDo) return resp, errDo } defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } }() helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) data, errRead := io.ReadAll(httpResp.Body) if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) return resp, err } reporter.Publish(ctx, helps.ParseOpenAIUsage(data)) reporter.EnsurePublished(ctx) return cliproxyexecutor.Response{Payload: data, Headers: httpResp.Header.Clone()}, nil } func (e *CodexExecutor) executeDirectOpenAIImageStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, endpointPath string) (_ *cliproxyexecutor.StreamResult, err error) { body, contentType, model, errPrepare := codexPrepareDirectOpenAIImageBody(req, opts, true) if errPrepare != nil { return nil, errPrepare } apiKey, baseURL := codexCreds(auth) if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } reporter := helps.NewExecutorUsageReporter(ctx, e, model, auth) defer reporter.TrackFailure(ctx, &err) reporter.SetTranslatedReasoningEffort(body, "openai") url := strings.TrimSuffix(baseURL, "/") + endpointPath var identityState codexIdentityConfuseState httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) if errCache != nil { return nil, errCache } applyCodexDirectImageHeaders(httpReq, auth, apiKey, true, e.cfg) applyModelHeaderOverrides(httpReq.Header, model) if contentType != "" { httpReq.Header.Set("Content-Type", contentType) } applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient = reporter.TrackHTTPClient(httpClient) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { helps.RecordAPIResponseError(ctx, e.cfg, errDo) return nil, errDo } helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { data, errRead := io.ReadAll(httpResp.Body) if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } if errRead != nil { helps.RecordAPIResponseError(ctx, e.cfg, errRead) return nil, errRead } data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) return nil, err } out := make(chan cliproxyexecutor.StreamChunk) go func() { defer close(out) var streamUsage helps.StreamUsageBuffer defer func() { if errClose := httpResp.Body.Close(); errClose != nil { log.Errorf("codex executor: close response body error: %v", errClose) } streamUsage.Publish(ctx, reporter) reporter.EnsurePublished(ctx) }() buffer := make([]byte, 32*1024) for { n, errRead := httpResp.Body.Read(buffer) if n > 0 { chunk := bytes.Clone(buffer[:n]) chunk = applyCodexIdentityConfuseResponsePayload(chunk, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, chunk) for _, line := range bytes.Split(chunk, []byte("\n")) { streamUsage.ObserveOpenAIStream(bytes.TrimSpace(line)) } select { case out <- cliproxyexecutor.StreamChunk{Payload: chunk}: case <-ctx.Done(): return } } if errRead != nil { if errRead != io.EOF { helps.RecordAPIResponseError(ctx, e.cfg, errRead) reporter.PublishFailure(ctx, errRead) select { case out <- cliproxyexecutor.StreamChunk{Err: errRead}: case <-ctx.Done(): } } return } } }() return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil } func codexDirectOpenAIImageEndpoint(req cliproxyexecutor.Request, opts cliproxyexecutor.Options) string { if codexDirectOpenAIImageModel(req) == "" { return "" } path := helps.PayloadRequestPath(opts) if strings.HasSuffix(strings.TrimSpace(path), codexImagesGenerationsPath) { return codexDirectImagesGenerations } if strings.HasSuffix(strings.TrimSpace(path), codexImagesEditsPath) { return codexDirectImagesEdit } return "" } func codexPrepareDirectOpenAIImageBody(req cliproxyexecutor.Request, opts cliproxyexecutor.Options, stream bool) ([]byte, string, string, error) { model := codexDirectOpenAIImageModel(req) if model == "" { return nil, "", "", fmt.Errorf("unsupported direct OpenAI image model %q", req.Model) } body, contentType, errPrepare := codexPrepareDirectOpenAIImagePayload(req, opts, model, stream) if errPrepare != nil { return nil, "", "", errPrepare