From f1ba0181ab77e24074d0602bf527ebd0a699b3bb Mon Sep 17 00:00:00 2001 From: qa-engineer Date: Tue, 24 Mar 2026 12:41:14 +0800 Subject: [PATCH 1/2] Normalize codex compact auth requests --- internal/runtime/executor/codex_executor.go | 41 ++++-- .../executor/codex_executor_cache_test.go | 4 +- .../codex_executor_integration_test.go | 118 ++++++++++++++++++ 3 files changed, 151 insertions(+), 12 deletions(-) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index b752ab3d86..2270249014 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -117,7 +117,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re } url := strings.TrimSuffix(baseURL, "/") + "/responses" - httpReq, err := e.cacheHelper(ctx, from, url, req, body) + httpReq, err := e.cacheHelper(ctx, from, url, req, body, true) if err != nil { return resp, err } @@ -216,14 +216,10 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A requestedModel := payloadRequestedModel(opts, req.Model) body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) - body, _ = sjson.SetBytes(body, "model", baseModel) - body, _ = sjson.DeleteBytes(body, "stream") - if !gjson.GetBytes(body, "instructions").Exists() { - body, _ = sjson.SetBytes(body, "instructions", "") - } + body = normalizeCodexCompactRequestBody(body, baseModel) url := strings.TrimSuffix(baseURL, "/") + "/responses/compact" - httpReq, err := e.cacheHelper(ctx, from, url, req, body) + httpReq, err := e.cacheHelper(ctx, from, url, req, body, false) if err != nil { return resp, err } @@ -317,7 +313,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } url := strings.TrimSuffix(baseURL, "/") + "/responses" - httpReq, err := e.cacheHelper(ctx, from, url, req, body) + httpReq, err := e.cacheHelper(ctx, from, url, req, body, true) if err != nil { return nil, err } @@ -596,7 +592,32 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (* return auth, nil } -func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, req cliproxyexecutor.Request, rawJSON []byte) (*http.Request, error) { +func normalizeCodexCompactRequestBody(body []byte, baseModel string) []byte { + if len(body) == 0 { + return body + } + + normalizedSource := sdktranslator.TranslateRequest(sdktranslator.FromString("openai-response"), sdktranslator.FromString("codex"), baseModel, body, false) + if !gjson.ValidBytes(normalizedSource) { + normalizedSource = body + } + + normalized := []byte(`{}`) + normalized, _ = sjson.SetBytes(normalized, "model", baseModel) + for _, field := range []string{"input", "instructions", "previous_response_id"} { + value := gjson.GetBytes(normalizedSource, field) + if !value.Exists() { + continue + } + normalized, _ = sjson.SetRawBytes(normalized, field, []byte(value.Raw)) + } + if !gjson.GetBytes(normalized, "instructions").Exists() { + normalized, _ = sjson.SetBytes(normalized, "instructions", "") + } + return normalized +} + +func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, req cliproxyexecutor.Request, rawJSON []byte, includePromptCacheKey bool) (*http.Request, error) { var cache codexCache if from == "claude" { userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id") @@ -622,7 +643,7 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form } } - if cache.ID != "" { + if includePromptCacheKey && cache.ID != "" { rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", cache.ID) } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(rawJSON)) diff --git a/internal/runtime/executor/codex_executor_cache_test.go b/internal/runtime/executor/codex_executor_cache_test.go index d6dca0315d..0a778ed46e 100644 --- a/internal/runtime/executor/codex_executor_cache_test.go +++ b/internal/runtime/executor/codex_executor_cache_test.go @@ -27,7 +27,7 @@ func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFrom } url := "https://example.com/responses" - httpReq, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON) + httpReq, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON, true) if err != nil { t.Fatalf("cacheHelper error: %v", err) } @@ -49,7 +49,7 @@ func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFrom t.Fatalf("Session_id = %q, want %q", gotSession, expectedKey) } - httpReq2, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON) + httpReq2, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON, true) if err != nil { t.Fatalf("cacheHelper error (second call): %v", err) } diff --git a/internal/runtime/executor/codex_executor_integration_test.go b/internal/runtime/executor/codex_executor_integration_test.go index 980f071ce3..f6d9e94d29 100644 --- a/internal/runtime/executor/codex_executor_integration_test.go +++ b/internal/runtime/executor/codex_executor_integration_test.go @@ -23,6 +23,8 @@ type codexCapturedRequest struct { Path string Accept string Authorization string + Originator string + AccountID string Version string SessionID string Body []byte @@ -143,6 +145,8 @@ func TestCodexExecutorExecuteCompact_LocalServer_RequestShapeAndPayload(t *testi Path: r.URL.Path, Accept: r.Header.Get("Accept"), Authorization: r.Header.Get("Authorization"), + Originator: r.Header.Get("Originator"), + AccountID: r.Header.Get("Chatgpt-Account-Id"), Body: body, } @@ -185,6 +189,107 @@ func TestCodexExecutorExecuteCompact_LocalServer_RequestShapeAndPayload(t *testi } } +func TestCodexExecutorExecuteCompact_LocalServer_AuthBackedNormalizesRequest(t *testing.T) { + t.Parallel() + + var captured codexCapturedRequest + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + captured = codexCapturedRequest{ + Path: r.URL.Path, + Accept: r.Header.Get("Accept"), + Authorization: r.Header.Get("Authorization"), + Originator: r.Header.Get("Originator"), + AccountID: r.Header.Get("Chatgpt-Account-Id"), + SessionID: r.Header.Get("Session_id"), + Body: body, + } + + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"id":"resp_compact_oauth","object":"response","status":"completed","usage":{"input_tokens":2,"output_tokens":3,"total_tokens":5}}`) + })) + defer server.Close() + + req := cliproxyexecutor.Request{ + Model: "gpt-5.4", + Payload: []byte(`{ + "model":"gpt-5.4", + "instructions":"keep only compact fields", + "input":[ + {"type":"message","role":"system","content":[{"type":"input_text","text":"be precise"}]}, + {"type":"message","role":"user","content":[{"type":"input_text","text":"compact me"}]} + ], + "store":true, + "stream":true, + "temperature":0.3, + "top_p":0.7, + "functions":[{"name":"lookup_issue","parameters":{"type":"object"}}], + "function_call":{"name":"lookup_issue"}, + "context_management":[{"type":"compaction","compact_threshold":12000}], + "previous_response_id":"resp_prev_123", + "prompt_cache_key":"compact-seed", + "user":"integration-test" + }`), + } + + executor := NewCodexExecutor(&config.Config{}) + resp, err := executor.Execute( + context.Background(), + newCodexOAuthTestAuth(server.URL, "oauth-token", "chatgpt-acc"), + req, + cliproxyexecutor.Options{ + SourceFormat: sdktranslator.FromString("openai-response"), + Alt: "responses/compact", + OriginalRequest: req.Payload, + }, + ) + if err != nil { + t.Fatalf("Execute() compact oauth error = %v", err) + } + + if captured.Path != "/responses/compact" { + t.Fatalf("path = %q, want %q", captured.Path, "/responses/compact") + } + if captured.Accept != "application/json" { + t.Fatalf("Accept = %q, want %q", captured.Accept, "application/json") + } + if captured.Authorization != "Bearer oauth-token" { + t.Fatalf("Authorization = %q, want %q", captured.Authorization, "Bearer oauth-token") + } + if captured.Originator != "codex_cli_rs" { + t.Fatalf("Originator = %q, want %q", captured.Originator, "codex_cli_rs") + } + if captured.AccountID != "chatgpt-acc" { + t.Fatalf("Chatgpt-Account-Id = %q, want %q", captured.AccountID, "chatgpt-acc") + } + if captured.SessionID != "compact-seed" { + t.Fatalf("Session_id = %q, want %q", captured.SessionID, "compact-seed") + } + if gotModel := gjson.GetBytes(captured.Body, "model").String(); gotModel != "gpt-5.4" { + t.Fatalf("request model = %q, want %q", gotModel, "gpt-5.4") + } + if gotRole := gjson.GetBytes(captured.Body, "input.0.role").String(); gotRole != "developer" { + t.Fatalf("request input[0].role = %q, want %q", gotRole, "developer") + } + if gotText := gjson.GetBytes(captured.Body, "input.1.content.0.text").String(); gotText != "compact me" { + t.Fatalf("request input[1] text = %q, want %q", gotText, "compact me") + } + if gotInstructions := gjson.GetBytes(captured.Body, "instructions").String(); gotInstructions != "keep only compact fields" { + t.Fatalf("request instructions = %q, want %q", gotInstructions, "keep only compact fields") + } + if gotPrev := gjson.GetBytes(captured.Body, "previous_response_id").String(); gotPrev != "resp_prev_123" { + t.Fatalf("previous_response_id = %q, want %q", gotPrev, "resp_prev_123") + } + for _, field := range []string{"store", "stream", "temperature", "top_p", "functions", "function_call", "context_management", "prompt_cache_key", "user", "parallel_tool_calls", "include"} { + if got := gjson.GetBytes(captured.Body, field); got.Exists() { + t.Fatalf("%s should be removed for compact path, got %s", field, got.Raw) + } + } + if string(resp.Payload) != `{"id":"resp_compact_oauth","object":"response","status":"completed","usage":{"input_tokens":2,"output_tokens":3,"total_tokens":5}}` { + t.Fatalf("payload = %s", string(resp.Payload)) + } +} + func TestCodexExecutorExecute_LocalServer_Parses429RetryAfter(t *testing.T) { t.Parallel() @@ -614,6 +719,19 @@ func newCodexTestAuth(baseURL, apiKey string) *cliproxyauth.Auth { } } +func newCodexOAuthTestAuth(baseURL, accessToken, accountID string) *cliproxyauth.Auth { + return &cliproxyauth.Auth{ + Attributes: map[string]string{ + "base_url": baseURL, + }, + Metadata: map[string]any{ + "email": "user@example.com", + "access_token": accessToken, + "account_id": accountID, + }, + } +} + func newCodexResponsesRequest(prompt string) cliproxyexecutor.Request { return cliproxyexecutor.Request{ Model: "gpt-5.4", From ac0d2d8466e579ab7ba11dfb6d9d28a6ff288e0b Mon Sep 17 00:00:00 2001 From: qa-engineer Date: Tue, 24 Mar 2026 12:39:14 +0800 Subject: [PATCH 2/2] test: cover codex compact auth path --- .../openai/openai_responses_compact_test.go | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/sdk/api/handlers/openai/openai_responses_compact_test.go b/sdk/api/handlers/openai/openai_responses_compact_test.go index dcfcc99a7c..4ef6bcff97 100644 --- a/sdk/api/handlers/openai/openai_responses_compact_test.go +++ b/sdk/api/handlers/openai/openai_responses_compact_test.go @@ -3,17 +3,21 @@ package openai import ( "context" "errors" + "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/gin-gonic/gin" + internalconfig "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" + runtimeexecutor "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor" "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + "github.com/tidwall/gjson" ) type compactCaptureExecutor struct { @@ -118,3 +122,84 @@ func TestOpenAIResponsesCompactExecute(t *testing.T) { t.Fatalf("body = %s", resp.Body.String()) } } + +func TestOpenAIResponsesCompactExecute_CodexAuthManagerPath(t *testing.T) { + gin.SetMode(gin.TestMode) + + var gotPath string + var gotAuthorization string + var gotBody []byte + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.Path + gotAuthorization = r.Header.Get("Authorization") + body, _ := io.ReadAll(r.Body) + gotBody = body + + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"id":"resp_compact_auth","object":"response","status":"completed","usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}`)) + })) + defer server.Close() + + cfg := &internalconfig.Config{ + CodexKey: []internalconfig.CodexKey{ + { + APIKey: "compact-key", + BaseURL: server.URL, + Models: []internalconfig.CodexModel{ + {Name: "gpt-5-codex", Alias: "compact-alias"}, + }, + }, + }, + } + + manager := coreauth.NewManager(nil, nil, nil) + manager.SetConfig(cfg) + manager.RegisterExecutor(runtimeexecutor.NewCodexExecutor(cfg)) + + auth := &coreauth.Auth{ + ID: "codex-compact-auth", + Provider: "codex", + Status: coreauth.StatusActive, + Attributes: map[string]string{ + "api_key": "compact-key", + "base_url": server.URL, + }, + } + if _, err := manager.Register(context.Background(), auth); err != nil { + t.Fatalf("Register auth: %v", err) + } + registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "compact-alias"}}) + manager.RefreshSchedulerEntry(auth.ID) + t.Cleanup(func() { + registry.GetGlobalRegistry().UnregisterClient(auth.ID) + }) + + base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager) + h := NewOpenAIResponsesAPIHandler(base) + router := gin.New() + router.POST("/v1/responses/compact", h.Compact) + + req := httptest.NewRequest(http.MethodPost, "/v1/responses/compact", strings.NewReader(`{"model":"compact-alias","input":"hello","stream":false}`)) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() + router.ServeHTTP(resp, req) + + if resp.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", resp.Code, http.StatusOK) + } + if gotPath != "/responses/compact" { + t.Fatalf("path = %q, want %q", gotPath, "/responses/compact") + } + if gotAuthorization != "Bearer compact-key" { + t.Fatalf("Authorization = %q, want %q", gotAuthorization, "Bearer compact-key") + } + if gotModel := gjson.GetBytes(gotBody, "model").String(); gotModel != "gpt-5-codex" { + t.Fatalf("upstream model = %q, want %q", gotModel, "gpt-5-codex") + } + if gotStream := gjson.GetBytes(gotBody, "stream"); gotStream.Exists() { + t.Fatalf("upstream stream should be removed for compact path, got %s", gotStream.Raw) + } + if gotID := gjson.Get(resp.Body.String(), "id").String(); gotID != "resp_compact_auth" { + t.Fatalf("response id = %q, want %q", gotID, "resp_compact_auth") + } +}