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" 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) { 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) 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 = newCodexStatusErr(httpResp.StatusCode, data) 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) { 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) 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 = newCodexStatusErr(httpResp.StatusCode, data) 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) prepareCodexOpenAIImageBody(body []byte, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, mainModel string) ([]byte, error) { out := body mainModel = strings.TrimSpace(mainModel) if mainModel == "" { mainModel = codexOpenAIImagesMainModel } var errThinking error out, errThinking = thinking.ApplyThinking(out, mainModel, codexOpenAIImageSourceFormat, "codex", e.Identifier()) if errThinking != nil { return nil, errThinking } requestedModel := helps.PayloadRequestedModel(opts, req.Model) requestPath := helps.PayloadRequestPath(opts) out = helps.ApplyPayloadConfigWithRequest(e.cfg, mainModel, "codex", codexOpenAIImageSourceFormat, "", out, body, requestedModel, requestPath, opts.Headers) out, _ = sjson.SetBytes(out, "model", mainModel) out, _ = sjson.SetBytes(out, "stream", true) out, _ = sjson.DeleteBytes(out, "previous_response_id") out, _ = sjson.DeleteBytes(out, "prompt_cache_retention") out, _ = sjson.DeleteBytes(out, "safety_identifier") out, _ = sjson.DeleteBytes(out, "stream_options") return normalizeCodexInstructions(out), nil } func recordCodexOpenAIImageRequest(ctx context.Context, cfg *config.Config, provider string, auth *cliproxyauth.Auth, url string, headers http.Header, body []byte) { var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID authLabel = auth.Label authType, authValue = auth.AccountInfo() } helps.RecordAPIRequest(ctx, cfg, helps.UpstreamRequestLog{ URL: url, Method: http.MethodPost, Headers: headers, Body: body, Provider: provider, AuthID: authID, AuthLabel: authLabel, AuthType: authType, AuthValue: authValue, }) } func codexPrepareOpenAIImageRequest(req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (codexOpenAIImagePreparedRequest, error) { path := helps.PayloadRequestPath(opts) if strings.HasSuffix(path, codexImagesGenerationsPath) { return codexPrepareOpenAIImageGenerationJSON(req.Payload, req.Model) } if !strings.HasSuffix(path, codexImagesEditsPath) { return codexOpenAIImagePreparedRequest{}, fmt.Errorf("unsupported OpenAI image endpoint path %q", path) } contentType := codexImageContentType(opts.Headers) mediaType, _, _ := mime.ParseMediaType(contentType) if strings.HasPrefix(strings.ToLower(mediaType), "multipart/") { return codexPrepareOpenAIImageEditMultipart(req.Payload, req.Model, contentType) } return codexPrepareOpenAIImageEditJSON(req.Payload, req.Model) } func codexPrepareOpenAIImageGenerationJSON(rawJSON []byte, routeModel string) (codexOpenAIImagePreparedRequest, error) { if !json.Valid(rawJSON) { return codexOpenAIImagePreparedRequest{}, fmt.Errorf("invalid OpenAI image generation request JSON") } prompt := strings.TrimSpace(gjson.GetBytes(rawJSON, "prompt").String()) tool := codexBuildOpenAIImageTool(rawJSON, routeModel, "generate", []string{"size", "quality", "background", "output_format", "moderation"}, []string{"output_compression", "partial_images"}) body := codexBuildImagesResponsesRequest(prompt, nil, tool) return codexOpenAIImagePreparedRequest{ Body: body, ResponseFormat: codexOpenAIImageResponseFormatFromJSON(rawJSON), StreamPrefix: "image_generation", }, nil } func codexPrepareOpenAIImageEditJSON(rawJSON []byte, routeModel string) (codexOpenAIImagePreparedRequest, error) { if !json.Valid(rawJSON) { return codexOpenAIImagePreparedRequest{}, fmt.Errorf("invalid OpenAI image edit request JSON") } prompt := strings.TrimSpace(gjson.GetBytes(rawJSON, "prompt").String()) images := make([]string, 0) if imagesResult := gjson.GetBytes(rawJSON, "images"); imagesResult.IsArray() { for _, img := range imagesResult.Array() { url := strings.TrimSpace(img.Get("image_url").String()) if url != "" { images = append(images, url) } } } tool := codexBuildOpenAIImageTool(rawJSON, routeModel, "edit", []string{"size", "quality", "background", "output_format", "input_fidelity", "moderation"}, []string{"output_compression", "partial_images"}) if mask := strings.TrimSpace(gjson.GetBytes(rawJSON, "mask.image_url").String()); mask != "" { tool, _ = sjson.SetBytes(tool, "input_image_mask.image_url", mask) } body := codexBuildImagesResponsesRequest(prompt, images, tool) return codexOpenAIImagePreparedRequest{ Body: body, ResponseFormat: codexOpenAIImageResponseFormatFromJSON(rawJSON), StreamPrefix: "image_edit", }, nil } func codexPrepareOpenAIImageEditMultipart(rawBody []byte, routeModel string, contentType string) (codexOpenAIImagePreparedRequest, error) { _, params, errMedia := mime.ParseMediaType(contentType) if errMedia != nil { return codexOpenAIImagePreparedRequest{}, fmt.Errorf("parse multipart content type failed: %w", errMedia) } boundary := strings.TrimSpace(params["boundary"]) if boundary == "" { return codexOpenAIImagePreparedRequest{}, fmt.Errorf("multipart boundary is required") } reader := multipart.NewReader(bytes.NewReader(rawBody), boundary) form, errForm := reader.ReadForm(32 << 20) if errForm != nil { return codexOpenAIImagePreparedRequest{}, fmt.Errorf("parse multipart form failed: %w", errForm) } defer func() { if errRemove := form.RemoveAll(); errRemove != nil { log.Errorf("codex openai images: remove multipart temp files error: %v", errRemove) } }() prompt := strings.TrimSpace(codexFormValue(form, "prompt")) responseFormat := codexNormalizeImageResponseFormat(codexFormValue(form, "response_format")) tool := []byte(`{"type":"image_generation","action":"edit"}`) tool, _ = sjson.SetBytes(tool, "model", codexOpenAIImageToolModel(codexFormValue(form, "model"), routeModel)) for _, field := range []string{"size", "quality", "background", "output_format", "input_fidelity", "moderation"} { if value := strings.TrimSpace(codexFormValue(form, field)); value != "" { tool, _ = sjson.SetBytes(tool, field, value) } } for _, field := range []string{"output_compression", "partial_images"} { if value := strings.TrimSpace(codexFormValue(form, field)); value != "" { if parsed, errParse := strconv.ParseInt(value, 10, 64); errParse == nil { tool, _ = sjson.SetBytes(tool, field, parsed) } } } images := make([]string, 0) for _, fh := range codexMultipartImageFiles(form) { dataURL, errData := codexMultipartFileToDataURL(fh) if errData != nil { return codexOpenAIImagePreparedRequest{}, errData } images = append(images, dataURL) } if maskFiles := form.File["mask"]; len(maskFiles) > 0 && maskFiles[0] != nil { dataURL, errData := codexMultipartFileToDataURL(maskFiles[0]) if errData != nil { return codexOpenAIImagePreparedRequest{}, errData } tool, _ = sjson.SetBytes(tool, "input_image_mask.image_url", dataURL) } body := codexBuildImagesResponsesRequest(prompt, images, tool) return codexOpenAIImagePreparedRequest{ Body: body, ResponseFormat: responseFormat, StreamPrefix: "image_edit", }, nil } func codexImageContentType(headers http.Header) string { if headers == nil { return "" } return strings.TrimSpace(headers.Get("Content-Type")) } func codexOpenAIImageResponseFormatFromJSON(rawJSON []byte) string { return codexNormalizeImageResponseFormat(gjson.GetBytes(rawJSON, "response_format").String()) } func codexNormalizeImageResponseFormat(responseFormat string) string { if strings.EqualFold(strings.TrimSpace(responseFormat), "url") { return "url" } return "b64_json" } func codexOpenAIImageToolModel(requestModel string, routeModel string) string { model := strings.TrimSpace(requestModel) if model == "" { model = strings.TrimSpace(routeModel) } if model == "" { model = codexDefaultImageToolModel } return model } func codexBuildOpenAIImageTool(rawJSON []byte, routeModel string, action string, stringFields []string, numberFields []string) []byte { tool := []byte(`{"type":"image_generation","action":""}`) tool, _ = sjson.SetBytes(tool, "action", action) tool, _ = sjson.SetBytes(tool, "model", codexOpenAIImageToolModel(gjson.GetBytes(rawJSON, "model").String(), routeModel)) for _, field := range stringFields { if value := strings.TrimSpace(gjson.GetBytes(rawJSON, field).String()); value != "" { tool, _ = sjson.SetBytes(tool, field, value) } } for _, field := range numberFields { if value := gjson.GetBytes(rawJSON, field); value.Exists() && value.Type == gjson.Number { tool, _ = sjson.SetBytes(tool, field, value.Int()) } } return tool } func codexBuildImagesResponsesRequest(prompt string, images []string, toolJSON []byte) []byte { req := []byte(`{"instructions":"","stream":true,"reasoning":{"effort":"medium","summary":"auto"},"parallel_tool_calls":true,"include":["reasoning.encrypted_content"],"model":"","store":false,"tool_choice":{"type":"image_generation"}}`) req, _ = sjson.SetBytes(req, "model", codexOpenAIImagesMainModel) input := []byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`) input, _ = sjson.SetBytes(input, "0.content.0.text", prompt) contentIndex := 1 for _, img := range images { if strings.TrimSpace(img) == "" { continue } part := []byte(`{"type":"input_image","image_url":""}`) part, _ = sjson.SetBytes(part, "image_url", img) input, _ = sjson.SetRawBytes(input, fmt.Sprintf("0.content.%d", contentIndex), part) contentIndex++ } req, _ = sjson.SetRawBytes(req, "input", input) req, _ = sjson.SetRawBytes(req, "tools", []byte(`[]`)) if len(toolJSON) > 0 && json.Valid(toolJSON) { req, _ = sjson.SetRawBytes(req, "tools.-1", toolJSON) } return req } func codexFormValue(form *multipart.Form, key string) string { if form == nil || len(form.Value[key]) == 0 { return "" } return strings.TrimSpace(form.Value[key][0]) } func codexMultipartImageFiles(form *multipart.Form) []*multipart.FileHeader { if form == nil { return nil } if files := form.File["image[]"]; len(files) > 0 { return files } return form.File["image"] } func codexMultipartFileToDataURL(fileHeader *multipart.FileHeader) (string, error) { if fileHeader == nil { return "", fmt.Errorf("upload file is nil") } f, errOpen := fileHeader.Open() if errOpen != nil { return "", fmt.Errorf("open upload file failed: %w", errOpen) } defer func() { if errClose := f.Close(); errClose != nil { log.Errorf("codex openai images: close upload file error: %v", errClose) } }() data, errRead := io.ReadAll(f) if errRead != nil { return "", fmt.Errorf("read upload file failed: %w", errRead) } mediaType := strings.TrimSpace(fileHeader.Header.Get("Content-Type")) if mediaType == "" { mediaType = http.DetectContentType(data) } return "data:" + mediaType + ";base64," + base64.StdEncoding.EncodeToString(data), nil } // codexExtractImageResults extracts image generation results directly from the // completed event and the items collected from response.output_item.done events, // without rebuilding the full completed JSON. // // It prefers image_generation_call items already present in the completed event's // response.output and only falls back to the collected items when that output is // empty, mirroring the semantics of patchCodexCompletedOutput + the previous // extractor. Skipping the concatenate-and-reparse step avoids two large copies of // the base64 payload, which matters for multi-megabyte generated images. func codexExtractImageResults(completed []byte, itemsByIndex map[int64][]byte, fallback [][]byte) (results []codexImageCallResult, createdAt int64, usageRaw []byte, firstMeta codexImageCallResult, err error) { if gjson.GetBytes(completed, "type").String() != "response.completed" { return nil, 0, nil, codexImageCallResult{}, fmt.Errorf("unexpected event type") } createdAt = gjson.GetBytes(completed, "response.created_at").Int() if createdAt <= 0 { createdAt = time.Now().Unix() } appendItem := func(item gjson.Result) { if item.Get("type").String() != "image_generation_call" { return } res := strings.TrimSpace(item.Get("result").String()) if res == "" { return } entry := codexImageCallResult{ Result: res, RevisedPrompt: strings.TrimSpace(item.Get("revised_prompt").String()), OutputFormat: strings.TrimSpace(item.Get("output_format").String()), Size: strings.TrimSpace(item.Get("size").String()), Background: strings.TrimSpace(item.Get("background").String()), Quality: strings.TrimSpace(item.Get("quality").String()), } if len(results) == 0 { firstMeta = entry } results = append(results, entry) } var outputItems []gjson.Result if output := gjson.GetBytes(completed, "response.output"); output.Exists() && output.IsArray() { outputItems = output.Array() } if len(outputItems) > 0 { // Completed event already carries the output; extract from it in place. results = make([]codexImageCallResult, 0, len(outputItems)) for _, item := range outputItems { appendItem(item) } } else if len(itemsByIndex) > 0 || len(fallback) > 0 { // Completed output was empty; extract directly from the collected items, // preserving their original output_index ordering. results = make([]codexImageCallResult, 0, len(itemsByIndex)+len(fallback)) if len(itemsByIndex) > 0 { indexes := make([]int64, 0, len(itemsByIndex)) for idx := range itemsByIndex { indexes = append(indexes, idx) } sort.Slice(indexes, func(i, j int) bool { return indexes[i] < indexes[j] }) for _, idx := range indexes { appendItem(gjson.ParseBytes(itemsByIndex[idx])) } } for _, raw := range fallback { appendItem(gjson.ParseBytes(raw)) } } if usage := gjson.GetBytes(completed, "response.tool_usage.image_gen"); usage.Exists() && usage.IsObject() { usageRaw = []byte(usage.Raw) } return results, createdAt, usageRaw, firstMeta, nil } func codexBuildImagesAPIResponse(results []codexImageCallResult, createdAt int64, usageRaw []byte, firstMeta codexImageCallResult, responseFormat string) ([]byte, error) { out := []byte(`{"created":0,"data":[]}`) out, _ = sjson.SetBytes(out, "created", createdAt) responseFormat = codexNormalizeImageResponseFormat(responseFormat) for _, img := range results { item := []byte(`{}`) if responseFormat == "url" { item, _ = sjson.SetBytes(item, "url", "data:"+codexMimeTypeFromOutputFormat(img.OutputFormat)+";base64,"+img.Result) } else { item, _ = sjson.SetBytes(item, "b64_json", img.Result) } if img.RevisedPrompt != "" { item, _ = sjson.SetBytes(item, "revised_prompt", img.RevisedPrompt) } out, _ = sjson.SetRawBytes(out, "data.-1", item) } if firstMeta.Background != "" { out, _ = sjson.SetBytes(out, "background", firstMeta.Background) } if firstMeta.OutputFormat != "" { out, _ = sjson.SetBytes(out, "output_format", firstMeta.OutputFormat) } if firstMeta.Quality != "" { out, _ = sjson.SetBytes(out, "quality", firstMeta.Quality) } if firstMeta.Size != "" { out, _ = sjson.SetBytes(out, "size", firstMeta.Size) } if len(usageRaw) > 0 && json.Valid(usageRaw) { out, _ = sjson.SetRawBytes(out, "usage", usageRaw) } return out, nil } func codexBuildImagePartialFrame(payload []byte, responseFormat string, streamPrefix string) []byte { b64 := strings.TrimSpace(gjson.GetBytes(payload, "partial_image_b64").String()) if b64 == "" { return nil } outputFormat := strings.TrimSpace(gjson.GetBytes(payload, "output_format").String()) eventName := strings.TrimSpace(streamPrefix) + ".partial_image" data := []byte(`{"type":"","partial_image_index":0}`) data, _ = sjson.SetBytes(data, "type", eventName) data, _ = sjson.SetBytes(data, "partial_image_index", gjson.GetBytes(payload, "partial_image_index").Int()) if codexNormalizeImageResponseFormat(responseFormat) == "url" { data, _ = sjson.SetBytes(data, "url", "data:"+codexMimeTypeFromOutputFormat(outputFormat)+";base64,"+b64) } else { data, _ = sjson.SetBytes(data, "b64_json", b64) } return codexBuildSSEFrame(eventName, data) } func codexBuildImageCompletedFrame(img codexImageCallResult, usageRaw []byte, responseFormat string, streamPrefix string) []byte { eventName := strings.TrimSpace(streamPrefix) + ".completed" data := []byte(`{"type":""}`) data, _ = sjson.SetBytes(data, "type", eventName) if codexNormalizeImageResponseFormat(responseFormat) == "url" { data, _ = sjson.SetBytes(data, "url", "data:"+codexMimeTypeFromOutputFormat(img.OutputFormat)+";base64,"+img.Result) } else { data, _ = sjson.SetBytes(data, "b64_json", img.Result) } if len(usageRaw) > 0 && json.Valid(usageRaw) { data, _ = sjson.SetRawBytes(data, "usage", usageRaw) } return codexBuildSSEFrame(eventName, data) } func codexBuildSSEFrame(eventName string, data []byte) []byte { var buf bytes.Buffer if strings.TrimSpace(eventName) != "" { buf.WriteString("event: ") buf.WriteString(eventName) buf.WriteString("\n") } buf.WriteString("data: ") buf.Write(data) buf.WriteString("\n\n") return buf.Bytes() } func codexMimeTypeFromOutputFormat(outputFormat string) string { switch strings.ToLower(strings.TrimSpace(outputFormat)) { case "jpg", "jpeg": return "image/jpeg" case "webp": return "image/webp" default: return "image/png" } }