package executor import ( "context" "io" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" 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" "github.com/tidwall/gjson" ) func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFromAPIKey(t *testing.T) { recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Set("userApiKey", "test-api-key") ctx := context.WithValue(context.Background(), "gin", ginCtx) executor := &CodexExecutor{} rawJSON := []byte(`{"model":"gpt-5.3-codex","stream":true}`) req := cliproxyexecutor.Request{ Model: "gpt-5.3-codex", Payload: []byte(`{"model":"gpt-5.3-codex"}`), } url := "https://example.com/responses" httpReq, _, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, nil, req, req.Payload, rawJSON) if err != nil { t.Fatalf("cacheHelper error: %v", err) } body, errRead := io.ReadAll(httpReq.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } expectedKey := uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:test-api-key")).String() gotKey := gjson.GetBytes(body, "prompt_cache_key").String() if gotKey != expectedKey { t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedKey) } if gotConversation := httpReq.Header.Get("Conversation_id"); gotConversation != "" { t.Fatalf("Conversation_id = %q, want empty", gotConversation) } if gotSession := httpReq.Header.Get("Session_id"); gotSession != "" { t.Fatalf("Session_id = %q, want empty", gotSession) } httpReq2, _, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, nil, req, req.Payload, rawJSON) if err != nil { t.Fatalf("cacheHelper error (second call): %v", err) } body2, errRead2 := io.ReadAll(httpReq2.Body) if errRead2 != nil { t.Fatalf("read request body (second call): %v", errRead2) } gotKey2 := gjson.GetBytes(body2, "prompt_cache_key").String() if gotKey2 != expectedKey { t.Fatalf("prompt_cache_key (second call) = %q, want %q", gotKey2, expectedKey) } } func TestCodexExecutorCacheHelper_IdentityConfuseRemapsBodyAndHeaders(t *testing.T) { recorder := httptest.NewRecorder() ginCtx, _ := gin.CreateTestContext(recorder) ginCtx.Request = httptest.NewRequest("POST", "/v1/responses", nil) ginCtx.Request.Header.Set("X-Codex-Turn-Metadata", `{"prompt_cache_key":"cache-1","turn_id":"turn-1"}`) ginCtx.Request.Header.Set("X-Client-Request-Id", "client-request-1") ctx := context.WithValue(context.Background(), "gin", ginCtx) executor := &CodexExecutor{cfg: &config.Config{ Routing: config.RoutingConfig{Strategy: "fill-first"}, Codex: config.CodexConfig{IdentityConfuse: true}, }} auth := &cliproxyauth.Auth{ID: "auth-1", Provider: "codex"} rawJSON := []byte(`{"model":"gpt-5-codex","stream":true,"client_metadata":{"x-codex-turn-metadata":"{\"prompt_cache_key\":\"cache-1\",\"turn_id\":\"turn-1\"}","x-codex-window-id":"cache-1:0"}}`) req := cliproxyexecutor.Request{ Model: "gpt-5-codex", Payload: []byte(`{"model":"gpt-5-codex","prompt_cache_key":"cache-1","client_metadata":{"x-codex-installation-id":"install-1"}}`), } url := "https://example.com/responses" httpReq, body, identityState, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai-response"), url, auth, req, req.Payload, rawJSON) if err != nil { t.Fatalf("cacheHelper error: %v", err) } applyCodexHeaders(httpReq, auth, "oauth-token", true, executor.cfg) applyCodexIdentityConfuseHeaders(httpReq.Header, identityState) expectedPromptCacheKey := codexIdentityConfuseUUID("auth-1", "prompt-cache", "cache-1") if gotKey := gjson.GetBytes(body, "prompt_cache_key").String(); gotKey != expectedPromptCacheKey { t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedPromptCacheKey) } expectedInstallationID := codexIdentityConfuseUUID("auth-1", "installation", "install-1") if gotID := gjson.GetBytes(body, "client_metadata.x-codex-installation-id").String(); gotID != expectedInstallationID { t.Fatalf("installation id = %q, want %q", gotID, expectedInstallationID) } if gotMetadata := gjson.GetBytes(body, "client_metadata.x-codex-turn-metadata").String(); gotMetadata != `{"prompt_cache_key":"`+expectedPromptCacheKey+`","turn_id":"turn-1"}` { t.Fatalf("client_metadata.x-codex-turn-metadata = %s", gotMetadata) } if gotWindowID := gjson.GetBytes(body, "client_metadata.x-codex-window-id").String(); gotWindowID != expectedPromptCacheKey+":0" { t.Fatalf("client_metadata.x-codex-window-id = %q, want %q", gotWindowID, expectedPromptCacheKey+":0") } for _, headerName := range []string{"Session-Id", "X-Client-Request-Id", "Thread-Id"} { if gotHeader := httpReq.Header.Get(headerName); gotHeader != expectedPromptCacheKey { t.Fatalf("%s = %q, want %q", headerName, gotHeader, expectedPromptCacheKey) } } if gotSession := httpReq.Header.Get("Session_id"); gotSession != "" { t.Fatalf("Session_id = %q, want empty", gotSession) } if gotWindow := httpReq.Header.Get("X-Codex-Window-Id"); gotWindow != expectedPromptCacheKey+":0" { t.Fatalf("X-Codex-Window-Id = %q, want %q", gotWindow, expectedPromptCacheKey+":0") } if gotMetadata := httpReq.Header.Get("X-Codex-Turn-Metadata"); gotMetadata != `{"prompt_cache_key":"`+expectedPromptCacheKey+`","turn_id":"turn-1"}` { t.Fatalf("X-Codex-Turn-Metadata = %s", gotMetadata) } } func TestCodexIdentityConfuseKeepsClientBodySeparateFromUpstreamBody(t *testing.T) { cfg := &config.Config{ Routing: config.RoutingConfig{Strategy: "fill-first"}, Codex: config.CodexConfig{IdentityConfuse: true}, } auth := &cliproxyauth.Auth{ID: "auth-1", Provider: "codex"} clientBody := []byte(`{"model":"gpt-5-codex","prompt_cache_key":"cache-1"}`) upstreamBody, identityState := applyCodexIdentityConfuseBody(cfg, auth, clientBody, clientBody) expectedPromptCacheKey := codexIdentityConfuseUUID("auth-1", "prompt-cache", "cache-1") if identityState.promptCacheKey != expectedPromptCacheKey { t.Fatalf("identity prompt_cache_key = %q, want %q", identityState.promptCacheKey, expectedPromptCacheKey) } if gotKey := gjson.GetBytes(upstreamBody, "prompt_cache_key").String(); gotKey != expectedPromptCacheKey { t.Fatalf("upstream prompt_cache_key = %q, want %q", gotKey, expectedPromptCacheKey) } if gotKey := gjson.GetBytes(clientBody, "prompt_cache_key").String(); gotKey != "cache-1" { t.Fatalf("client prompt_cache_key = %q, want cache-1", gotKey) } }