1126 lines · 41.1 KB · Go
RawDownload
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

Lines 1–500 of 1126.