package executor import ( "bufio" "bytes" "context" "io" "net/http" "strings" "time" codexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" log "github.com/sirupsen/logrus" "github.com/tidwall/sjson" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // CodexExecutor is a stateless executor for Codex (OpenAI Responses API entrypoint). // If api_key is unavailable on auth, it falls back to legacy via ClientAdapter. type CodexExecutor struct { cfg *config.Config } func NewCodexExecutor(cfg *config.Config) *CodexExecutor { return &CodexExecutor{cfg: cfg} } func (e *CodexExecutor) Identifier() string { return "codex" } func (e *CodexExecutor) PrepareRequest(_ *http.Request, _ *cliproxyauth.Auth) error { return nil } func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) { apiKey, baseURL := codexCreds(auth) if apiKey == "" { return NewClientAdapter("codex").Execute(ctx, auth, req, opts) } if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } from := opts.SourceFormat to := sdktranslator.FromString("codex") body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), false) if util.InArray([]string{"gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, req.Model) { body, _ = sjson.SetBytes(body, "model", "gpt-5") switch req.Model { case "gpt-5-minimal": body, _ = sjson.SetBytes(body, "reasoning.effort", "minimal") case "gpt-5-low": body, _ = sjson.SetBytes(body, "reasoning.effort", "low") case "gpt-5-medium": body, _ = sjson.SetBytes(body, "reasoning.effort", "medium") case "gpt-5-high": body, _ = sjson.SetBytes(body, "reasoning.effort", "high") } } else if util.InArray([]string{"gpt-5-codex", "gpt-5-codex-low", "gpt-5-codex-medium", "gpt-5-codex-high"}, req.Model) { body, _ = sjson.SetBytes(body, "model", "gpt-5-codex") switch req.Model { case "gpt-5-codex": body, _ = sjson.SetBytes(body, "reasoning.effort", "medium") case "gpt-5-codex-low": body, _ = sjson.SetBytes(body, "reasoning.effort", "low") case "gpt-5-codex-medium": body, _ = sjson.SetBytes(body, "reasoning.effort", "medium") case "gpt-5-codex-high": body, _ = sjson.SetBytes(body, "reasoning.effort", "high") } } url := strings.TrimSuffix(baseURL, "/") + "/responses" recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return cliproxyexecutor.Response{}, err } applyCodexHeaders(httpReq, auth, apiKey) httpClient := &http.Client{} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { httpClient.Transport = rt } resp, err := httpClient.Do(httpReq) if err != nil { return cliproxyexecutor.Response{}, err } defer func() { _ = resp.Body.Close() }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) log.Debugf("request error, error status: %d, error body: %s", resp.StatusCode, string(b)) return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(b)} } data, err := io.ReadAll(resp.Body) if err != nil { return cliproxyexecutor.Response{}, err } appendAPIResponseChunk(ctx, e.cfg, data) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m) return cliproxyexecutor.Response{Payload: []byte(out)}, nil } func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (<-chan cliproxyexecutor.StreamChunk, error) { apiKey, baseURL := codexCreds(auth) if apiKey == "" { return NewClientAdapter("codex").ExecuteStream(ctx, auth, req, opts) } if baseURL == "" { baseURL = "https://chatgpt.com/backend-api/codex" } from := opts.SourceFormat to := sdktranslator.FromString("codex") body := sdktranslator.TranslateRequest(from, to, req.Model, bytes.Clone(req.Payload), true) if util.InArray([]string{"gpt-5-minimal", "gpt-5-low", "gpt-5-medium", "gpt-5-high"}, req.Model) { body, _ = sjson.SetBytes(body, "model", "gpt-5") switch req.Model { case "gpt-5-minimal": body, _ = sjson.SetBytes(body, "reasoning.effort", "minimal") case "gpt-5-low": body, _ = sjson.SetBytes(body, "reasoning.effort", "low") case "gpt-5-medium": body, _ = sjson.SetBytes(body, "reasoning.effort", "medium") case "gpt-5-high": body, _ = sjson.SetBytes(body, "reasoning.effort", "high") } } else if util.InArray([]string{"gpt-5-codex", "gpt-5-codex-low", "gpt-5-codex-medium", "gpt-5-codex-high"}, req.Model) { body, _ = sjson.SetBytes(body, "model", "gpt-5-codex") switch req.Model { case "gpt-5-codex": body, _ = sjson.SetBytes(body, "reasoning.effort", "medium") case "gpt-5-codex-low": body, _ = sjson.SetBytes(body, "reasoning.effort", "low") case "gpt-5-codex-medium": body, _ = sjson.SetBytes(body, "reasoning.effort", "medium") case "gpt-5-codex-high": body, _ = sjson.SetBytes(body, "reasoning.effort", "high") } } url := strings.TrimSuffix(baseURL, "/") + "/responses" recordAPIRequest(ctx, e.cfg, body) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, err } applyCodexHeaders(httpReq, auth, apiKey) httpClient := &http.Client{Timeout: 0} if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { httpClient.Transport = rt } resp, err := httpClient.Do(httpReq) if err != nil { return nil, err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { defer func() { _ = resp.Body.Close() }() b, _ := io.ReadAll(resp.Body) appendAPIResponseChunk(ctx, e.cfg, b) log.Debugf("request error, error status: %d, error body: %s", resp.StatusCode, string(b)) return nil, statusErr{code: resp.StatusCode, msg: string(b)} } out := make(chan cliproxyexecutor.StreamChunk) go func() { defer close(out) defer func() { _ = resp.Body.Close() }() scanner := bufio.NewScanner(resp.Body) buf := make([]byte, 1024*1024) scanner.Buffer(buf, 1024*1024) var param any for scanner.Scan() { line := scanner.Bytes() appendAPIResponseChunk(ctx, e.cfg, line) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m) for i := range chunks { out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} } } if err = scanner.Err(); err != nil { out <- cliproxyexecutor.StreamChunk{Err: err} } }() return out, nil } func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { log.Debugf("codex executor: refresh called") if auth == nil { return nil, statusErr{code: 500, msg: "codex executor: auth is nil"} } var refreshToken string if auth.Metadata != nil { if v, ok := auth.Metadata["refresh_token"].(string); ok && v != "" { refreshToken = v } } if refreshToken == "" { return auth, nil } svc := codexauth.NewCodexAuth(e.cfg) td, err := svc.RefreshTokensWithRetry(ctx, refreshToken, 3) if err != nil { return nil, err } if auth.Metadata == nil { auth.Metadata = make(map[string]any) } auth.Metadata["id_token"] = td.IDToken auth.Metadata["access_token"] = td.AccessToken if td.RefreshToken != "" { auth.Metadata["refresh_token"] = td.RefreshToken } if td.AccountID != "" { auth.Metadata["account_id"] = td.AccountID } auth.Metadata["email"] = td.Email // Use unified key in files auth.Metadata["expired"] = td.Expire auth.Metadata["type"] = "codex" now := time.Now().Format(time.RFC3339) auth.Metadata["last_refresh"] = now return auth, nil } func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string) { r.Header.Set("Content-Type", "application/json") r.Header.Set("Authorization", "Bearer "+token) var ginHeaders http.Header if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { ginHeaders = ginCtx.Request.Header } misc.EnsureHeader(r.Header, ginHeaders, "Version", "0.21.0") misc.EnsureHeader(r.Header, ginHeaders, "Openai-Beta", "responses=experimental") misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString()) r.Header.Set("Accept", "text/event-stream") r.Header.Set("Connection", "Keep-Alive") isAPIKey := false if auth != nil && auth.Attributes != nil { if v := strings.TrimSpace(auth.Attributes["api_key"]); v != "" { isAPIKey = true } } if !isAPIKey { r.Header.Set("Originator", "codex_cli_rs") if auth != nil && auth.Metadata != nil { if accountID, ok := auth.Metadata["account_id"].(string); ok { r.Header.Set("Chatgpt-Account-Id", accountID) } } } } func codexCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) { if a == nil { return "", "" } if a.Attributes != nil { apiKey = a.Attributes["api_key"] baseURL = a.Attributes["base_url"] } if apiKey == "" && a.Metadata != nil { if v, ok := a.Metadata["access_token"].(string); ok { apiKey = v } } return }