diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index c8a9246e4..c7dd2d3ec 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -30,8 +30,8 @@ import ( ) const ( - codexUserAgent = "codex_cli_rs/0.118.0 (Mac OS 26.3.1; arm64) iTerm.app/3.6.9" - codexOriginator = "codex_cli_rs" + codexUserAgent = "codex-tui/0.135.0 (Mac OS 26.5.0; arm64) iTerm.app/3.6.10 (codex-tui; 0.135.0)" + codexOriginator = "codex-tui" codexDefaultImageToolModel = "gpt-image-2" ) @@ -304,7 +304,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re return resp, err } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg) - applyCodexIdentityConfuseHeaders(httpReq.Header, identityState) + applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -467,7 +467,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A return resp, err } applyCodexHeaders(httpReq, auth, apiKey, false, e.cfg) - applyCodexIdentityConfuseHeaders(httpReq.Header, identityState) + applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -575,7 +575,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au return nil, err } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg) - applyCodexIdentityConfuseHeaders(httpReq.Header, identityState) + applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -881,8 +881,16 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (* } type codexIdentityConfuseState struct { + enabled bool + authID string originalPromptCacheKey string promptCacheKey string + turnIDs []codexIdentityReplacement +} + +type codexIdentityReplacement struct { + original string + confused string } func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, userPayload []byte, rawJSON []byte) (*http.Request, []byte, codexIdentityConfuseState, error) { @@ -931,7 +939,7 @@ func applyCodexIdentityConfuseBody(cfg *config.Config, auth *cliproxyauth.Auth, return rawJSON, codexIdentityConfuseState{} } - state := codexIdentityConfuseState{} + state := codexIdentityConfuseState{enabled: true, authID: strings.TrimSpace(auth.ID)} if promptCacheKey := strings.TrimSpace(gjson.GetBytes(userPayload, "prompt_cache_key").String()); promptCacheKey != "" { state.originalPromptCacheKey = promptCacheKey state.promptCacheKey = codexIdentityConfuseUUID(auth.ID, "prompt-cache", promptCacheKey) @@ -940,10 +948,10 @@ func applyCodexIdentityConfuseBody(cfg *config.Config, auth *cliproxyauth.Auth, if installationID := strings.TrimSpace(gjson.GetBytes(userPayload, "client_metadata.x-codex-installation-id").String()); installationID != "" { rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-installation-id", codexIdentityConfuseUUID(auth.ID, "installation", installationID)) } + if turnMetadata := strings.TrimSpace(gjson.GetBytes(rawJSON, "client_metadata.x-codex-turn-metadata").String()); turnMetadata != "" { + rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-turn-metadata", applyCodexTurnMetadataIdentityConfuse(turnMetadata, &state)) + } if state.promptCacheKey != "" { - if turnMetadata := strings.TrimSpace(gjson.GetBytes(rawJSON, "client_metadata.x-codex-turn-metadata").String()); turnMetadata != "" { - rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-turn-metadata", applyCodexTurnMetadataIdentityConfuse(turnMetadata, state)) - } if windowID := strings.TrimSpace(gjson.GetBytes(rawJSON, "client_metadata.x-codex-window-id").String()); windowID != "" { rawJSON, _ = sjson.SetBytes(rawJSON, "client_metadata.x-codex-window-id", state.promptCacheKey+":0") } @@ -952,38 +960,76 @@ func applyCodexIdentityConfuseBody(cfg *config.Config, auth *cliproxyauth.Auth, return rawJSON, state } -func applyCodexIdentityConfuseHeaders(headers http.Header, state codexIdentityConfuseState) { - if headers == nil || state.promptCacheKey == "" { +func applyCodexIdentityConfuseHeaders(headers http.Header, state *codexIdentityConfuseState) { + if headers == nil { + return + } + defer deleteDeprecatedCodexConversationHeader(headers) + if state == nil || !state.enabled { return } - - setHeaderCasePreserved(headers, "Session-Id", state.promptCacheKey) - headers.Set("Conversation_id", state.promptCacheKey) - headers.Set("X-Client-Request-Id", state.promptCacheKey) - headers.Set("Thread-Id", state.promptCacheKey) - headers.Set("X-Codex-Window-Id", state.promptCacheKey+":0") if rawTurnMetadata := strings.TrimSpace(headers.Get("X-Codex-Turn-Metadata")); rawTurnMetadata != "" { headers.Set("X-Codex-Turn-Metadata", applyCodexTurnMetadataIdentityConfuse(rawTurnMetadata, state)) } + if state.promptCacheKey == "" { + return + } + + setHeaderCasePreserved(headers, "Session-Id", state.promptCacheKey) + headers.Set("X-Client-Request-Id", state.promptCacheKey) + headers.Set("Thread-Id", state.promptCacheKey) + headers.Set("X-Codex-Window-Id", state.promptCacheKey+":0") } -func applyCodexTurnMetadataIdentityConfuse(rawTurnMetadata string, state codexIdentityConfuseState) string { +func applyCodexTurnMetadataIdentityConfuse(rawTurnMetadata string, state *codexIdentityConfuseState) string { updatedTurnMetadata := rawTurnMetadata - if gjson.Get(rawTurnMetadata, "prompt_cache_key").Exists() { + if state == nil || !state.enabled { + return updatedTurnMetadata + } + if state.promptCacheKey != "" && gjson.Get(rawTurnMetadata, "prompt_cache_key").Exists() { updatedTurnMetadata, _ = sjson.Set(updatedTurnMetadata, "prompt_cache_key", state.promptCacheKey) - } else if state.originalPromptCacheKey != "" { + } else if state.promptCacheKey != "" && state.originalPromptCacheKey != "" { updatedTurnMetadata = strings.ReplaceAll(updatedTurnMetadata, state.originalPromptCacheKey, state.promptCacheKey) } + if turnID := strings.TrimSpace(gjson.Get(rawTurnMetadata, "turn_id").String()); turnID != "" { + updatedTurnMetadata, _ = sjson.Set(updatedTurnMetadata, "turn_id", state.confuseTurnID(turnID)) + } + if state.promptCacheKey != "" && gjson.Get(rawTurnMetadata, "window_id").Exists() { + updatedTurnMetadata, _ = sjson.Set(updatedTurnMetadata, "window_id", state.promptCacheKey+":0") + } return updatedTurnMetadata } func applyCodexIdentityConfuseResponsePayload(payload []byte, state codexIdentityConfuseState) []byte { - return replaceCodexIdentityResponsePayload(payload, state.originalPromptCacheKey, state.promptCacheKey) + payload = replaceCodexIdentityResponsePayload(payload, state.originalPromptCacheKey, state.promptCacheKey) + for _, turnID := range state.turnIDs { + payload = replaceCodexIdentityResponsePayload(payload, turnID.original, turnID.confused) + } + return payload } func applyCodexIdentityExposeResponsePayload(payload []byte, state codexIdentityConfuseState) []byte { - return replaceCodexIdentityResponsePayload(payload, state.promptCacheKey, state.originalPromptCacheKey) + payload = replaceCodexIdentityResponsePayload(payload, state.promptCacheKey, state.originalPromptCacheKey) + for _, turnID := range state.turnIDs { + payload = replaceCodexIdentityResponsePayload(payload, turnID.confused, turnID.original) + } + return payload +} + +func (state *codexIdentityConfuseState) confuseTurnID(turnID string) string { + turnID = strings.TrimSpace(turnID) + if state == nil || !state.enabled || strings.TrimSpace(state.authID) == "" || turnID == "" { + return turnID + } + for _, replacement := range state.turnIDs { + if replacement.original == turnID || replacement.confused == turnID { + return replacement.confused + } + } + confusedTurnID := codexIdentityConfuseUUID(state.authID, "turn", turnID) + state.turnIDs = append(state.turnIDs, codexIdentityReplacement{original: turnID, confused: confusedTurnID}) + return confusedTurnID } func replaceCodexIdentityResponsePayload(payload []byte, from string, to string) []byte { @@ -1044,18 +1090,19 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s } else if !isAPIKey { r.Header.Set("Originator", codexOriginator) } - if !isAPIKey { - if auth != nil && auth.Metadata != nil { - if accountID, ok := auth.Metadata["account_id"].(string); ok { - r.Header.Set("Chatgpt-Account-Id", accountID) - } - } - } + // if !isAPIKey { + // if auth != nil && auth.Metadata != nil { + // if accountID, ok := auth.Metadata["account_id"].(string); ok { + // r.Header.Set("Chatgpt-Account-Id", accountID) + // } + // } + // } var attrs map[string]string if auth != nil { attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(r, attrs) + deleteDeprecatedCodexConversationHeader(r.Header) } func newCodexStatusErr(statusCode int, body []byte) statusErr { diff --git a/internal/runtime/executor/codex_executor_cache_test.go b/internal/runtime/executor/codex_executor_cache_test.go index 2cf2b373b..29d244e68 100644 --- a/internal/runtime/executor/codex_executor_cache_test.go +++ b/internal/runtime/executor/codex_executor_cache_test.go @@ -69,7 +69,7 @@ func TestCodexExecutorCacheHelper_IdentityConfuseRemapsBodyAndHeaders(t *testing 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-Codex-Turn-Metadata", `{"prompt_cache_key":"cache-1","turn_id":"turn-1","window_id":"cache-1:0"}`) ginCtx.Request.Header.Set("X-Client-Request-Id", "client-request-1") ctx := context.WithValue(context.Background(), "gin", ginCtx) @@ -78,7 +78,7 @@ func TestCodexExecutorCacheHelper_IdentityConfuseRemapsBodyAndHeaders(t *testing 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"}}`) + rawJSON := []byte(`{"model":"gpt-5-codex","stream":true,"client_metadata":{"x-codex-turn-metadata":"{\"prompt_cache_key\":\"cache-1\",\"turn_id\":\"turn-1\",\"window_id\":\"cache-1:0\"}","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"}}`), @@ -90,9 +90,10 @@ func TestCodexExecutorCacheHelper_IdentityConfuseRemapsBodyAndHeaders(t *testing t.Fatalf("cacheHelper error: %v", err) } applyCodexHeaders(httpReq, auth, "oauth-token", true, executor.cfg) - applyCodexIdentityConfuseHeaders(httpReq.Header, identityState) + applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) expectedPromptCacheKey := codexIdentityConfuseUUID("auth-1", "prompt-cache", "cache-1") + expectedTurnID := codexIdentityConfuseUUID("auth-1", "turn", "turn-1") if gotKey := gjson.GetBytes(body, "prompt_cache_key").String(); gotKey != expectedPromptCacheKey { t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedPromptCacheKey) } @@ -100,8 +101,15 @@ func TestCodexExecutorCacheHelper_IdentityConfuseRemapsBodyAndHeaders(t *testing 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) + gotBodyMetadata := gjson.GetBytes(body, "client_metadata.x-codex-turn-metadata").String() + if gotMetadataPromptCacheKey := gjson.Get(gotBodyMetadata, "prompt_cache_key").String(); gotMetadataPromptCacheKey != expectedPromptCacheKey { + t.Fatalf("client_metadata.x-codex-turn-metadata.prompt_cache_key = %q, want %q", gotMetadataPromptCacheKey, expectedPromptCacheKey) + } + if gotMetadataTurnID := gjson.Get(gotBodyMetadata, "turn_id").String(); gotMetadataTurnID != expectedTurnID { + t.Fatalf("client_metadata.x-codex-turn-metadata.turn_id = %q, want %q", gotMetadataTurnID, expectedTurnID) + } + if gotMetadataWindowID := gjson.Get(gotBodyMetadata, "window_id").String(); gotMetadataWindowID != expectedPromptCacheKey+":0" { + t.Fatalf("client_metadata.x-codex-turn-metadata.window_id = %q, want %q", gotMetadataWindowID, expectedPromptCacheKey+":0") } 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") @@ -117,8 +125,15 @@ func TestCodexExecutorCacheHelper_IdentityConfuseRemapsBodyAndHeaders(t *testing 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) + gotHeaderMetadata := httpReq.Header.Get("X-Codex-Turn-Metadata") + if gotMetadataPromptCacheKey := gjson.Get(gotHeaderMetadata, "prompt_cache_key").String(); gotMetadataPromptCacheKey != expectedPromptCacheKey { + t.Fatalf("X-Codex-Turn-Metadata.prompt_cache_key = %q, want %q", gotMetadataPromptCacheKey, expectedPromptCacheKey) + } + if gotMetadataTurnID := gjson.Get(gotHeaderMetadata, "turn_id").String(); gotMetadataTurnID != expectedTurnID { + t.Fatalf("X-Codex-Turn-Metadata.turn_id = %q, want %q", gotMetadataTurnID, expectedTurnID) + } + if gotMetadataWindowID := gjson.Get(gotHeaderMetadata, "window_id").String(); gotMetadataWindowID != expectedPromptCacheKey+":0" { + t.Fatalf("X-Codex-Turn-Metadata.window_id = %q, want %q", gotMetadataWindowID, expectedPromptCacheKey+":0") } } diff --git a/internal/runtime/executor/codex_openai_images.go b/internal/runtime/executor/codex_openai_images.go index 90fe4ad3e..ffece0219 100644 --- a/internal/runtime/executor/codex_openai_images.go +++ b/internal/runtime/executor/codex_openai_images.go @@ -105,7 +105,7 @@ func (e *CodexExecutor) executeOpenAIImage(ctx context.Context, auth *cliproxyau return resp, errCache } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg) - applyCodexIdentityConfuseHeaders(httpReq.Header, identityState) + applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) @@ -198,7 +198,7 @@ func (e *CodexExecutor) executeOpenAIImageStream(ctx context.Context, auth *clip return nil, errCache } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg) - applyCodexIdentityConfuseHeaders(httpReq.Header, identityState) + applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) diff --git a/internal/runtime/executor/codex_websockets_executor.go b/internal/runtime/executor/codex_websockets_executor.go index 2680e729b..ecbf21710 100644 --- a/internal/runtime/executor/codex_websockets_executor.go +++ b/internal/runtime/executor/codex_websockets_executor.go @@ -224,12 +224,9 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut clientBody := body var identityState codexIdentityConfuseState upstreamBody, identityState := applyCodexIdentityConfuseBody(e.cfg, auth, originalPayloadSource, body) - if identityState.promptCacheKey != "" { - wsHeaders.Set("Conversation_id", identityState.promptCacheKey) - } reporter.SetTranslatedReasoningEffort(clientBody, to.String()) wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg) - applyCodexIdentityConfuseHeaders(wsHeaders, identityState) + applyCodexIdentityConfuseHeaders(wsHeaders, &identityState) var authID, authLabel, authType, authValue string if auth != nil { @@ -442,12 +439,9 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr clientBody := body var identityState codexIdentityConfuseState upstreamBody, identityState := applyCodexIdentityConfuseBody(e.cfg, auth, userPayload, body) - if identityState.promptCacheKey != "" { - wsHeaders.Set("Conversation_id", identityState.promptCacheKey) - } reporter.SetTranslatedReasoningEffort(clientBody, to.String()) wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg) - applyCodexIdentityConfuseHeaders(wsHeaders, identityState) + applyCodexIdentityConfuseHeaders(wsHeaders, &identityState) var authID, authLabel, authType, authValue string authID = auth.ID @@ -863,7 +857,6 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto if cache.ID != "" { rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", cache.ID) - headers.Set("Conversation_id", cache.ID) } return rawJSON, headers @@ -909,21 +902,22 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth * } else if !isAPIKey { headers.Set("Originator", codexOriginator) } - if !isAPIKey { - if auth != nil && auth.Metadata != nil { - if accountID, ok := auth.Metadata["account_id"].(string); ok { - if trimmed := strings.TrimSpace(accountID); trimmed != "" { - setHeaderCasePreserved(headers, "ChatGPT-Account-ID", trimmed) - } - } - } - } + // if !isAPIKey { + // if auth != nil && auth.Metadata != nil { + // if accountID, ok := auth.Metadata["account_id"].(string); ok { + // if trimmed := strings.TrimSpace(accountID); trimmed != "" { + // setHeaderCasePreserved(headers, "ChatGPT-Account-ID", trimmed) + // } + // } + // } + // } var attrs map[string]string if auth != nil { attrs = auth.Attributes } util.ApplyCustomHeadersFromAttrs(&http.Request{Header: headers}, attrs) + deleteDeprecatedCodexConversationHeader(headers) return headers } @@ -999,6 +993,10 @@ func deleteHeaderCaseInsensitive(headers http.Header, key string) { } } +func deleteDeprecatedCodexConversationHeader(headers http.Header) { + deleteHeaderCaseInsensitive(headers, "Conversation_id") +} + func codexHeaderDefaults(cfg *config.Config, auth *cliproxyauth.Auth) (string, string) { if cfg == nil || auth == nil { return "", "" diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index 4ea1e87fa..a2ef16c2c 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -341,7 +341,7 @@ func TestApplyCodexWebsocketHeadersPreservesExplicitAPIKeyUserAgent(t *testing.T } } -func TestApplyCodexPromptCacheHeadersSetsLegacyConversationOnly(t *testing.T) { +func TestApplyCodexPromptCacheHeadersDoesNotSetDeprecatedConversationHeader(t *testing.T) { req := cliproxyexecutor.Request{Model: "gpt-5-codex", Payload: []byte(`{"prompt_cache_key":"cache-1"}`)} _, headers := applyCodexPromptCacheHeaders("openai-response", req, []byte(`{"model":"gpt-5-codex"}`)) @@ -349,8 +349,8 @@ func TestApplyCodexPromptCacheHeadersSetsLegacyConversationOnly(t *testing.T) { if got := headerValueCaseInsensitive(headers, "session_id"); got != "" { t.Fatalf("session_id = %q, want empty", got) } - if got := headers.Get("Conversation_id"); got != "cache-1" { - t.Fatalf("Conversation_id = %s, want cache-1", got) + if got := headers.Get("Conversation_id"); got != "" { + t.Fatalf("Conversation_id = %q, want empty", got) } } @@ -367,17 +367,15 @@ func TestApplyCodexWebsocketHeadersIdentityConfuseRemapsPromptCacheKey(t *testin body, headers := applyCodexPromptCacheHeaders("openai-response", req, []byte(`{"model":"gpt-5-codex"}`)) body, identityState := applyCodexIdentityConfuseBody(cfg, auth, req.Payload, body) - if identityState.promptCacheKey != "" { - headers.Set("Conversation_id", identityState.promptCacheKey) - } ctx := contextWithGinHeaders(map[string]string{ - "X-Codex-Turn-Metadata": `{"prompt_cache_key":"cache-ws-1"}`, + "X-Codex-Turn-Metadata": `{"prompt_cache_key":"cache-ws-1","turn_id":"turn-ws-1","window_id":"cache-ws-1:0"}`, "X-Client-Request-Id": "client-request-1", }) headers = applyCodexWebsocketHeaders(ctx, headers, auth, "oauth-token", cfg) - applyCodexIdentityConfuseHeaders(headers, identityState) + applyCodexIdentityConfuseHeaders(headers, &identityState) expectedPromptCacheKey := codexIdentityConfuseUUID("auth-ws-1", "prompt-cache", "cache-ws-1") + expectedTurnID := codexIdentityConfuseUUID("auth-ws-1", "turn", "turn-ws-1") if gotKey := gjson.GetBytes(body, "prompt_cache_key").String(); gotKey != expectedPromptCacheKey { t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedPromptCacheKey) } @@ -390,11 +388,21 @@ func TestApplyCodexWebsocketHeadersIdentityConfuseRemapsPromptCacheKey(t *testin if gotThreadID := headers.Get("Thread-Id"); gotThreadID != expectedPromptCacheKey { t.Fatalf("Thread-Id = %q, want %q", gotThreadID, expectedPromptCacheKey) } + if gotConversation := headers.Get("Conversation_id"); gotConversation != "" { + t.Fatalf("Conversation_id = %q, want empty", gotConversation) + } if gotWindowID := headers.Get("X-Codex-Window-Id"); gotWindowID != expectedPromptCacheKey+":0" { t.Fatalf("X-Codex-Window-Id = %q, want %q", gotWindowID, expectedPromptCacheKey+":0") } - if gotMetadata := headers.Get("X-Codex-Turn-Metadata"); gotMetadata != `{"prompt_cache_key":"`+expectedPromptCacheKey+`"}` { - t.Fatalf("X-Codex-Turn-Metadata = %s", gotMetadata) + gotMetadata := headers.Get("X-Codex-Turn-Metadata") + if gotMetadataPromptCacheKey := gjson.Get(gotMetadata, "prompt_cache_key").String(); gotMetadataPromptCacheKey != expectedPromptCacheKey { + t.Fatalf("X-Codex-Turn-Metadata.prompt_cache_key = %q, want %q", gotMetadataPromptCacheKey, expectedPromptCacheKey) + } + if gotMetadataTurnID := gjson.Get(gotMetadata, "turn_id").String(); gotMetadataTurnID != expectedTurnID { + t.Fatalf("X-Codex-Turn-Metadata.turn_id = %q, want %q", gotMetadataTurnID, expectedTurnID) + } + if gotMetadataWindowID := gjson.Get(gotMetadata, "window_id").String(); gotMetadataWindowID != expectedPromptCacheKey+":0" { + t.Fatalf("X-Codex-Turn-Metadata.window_id = %q, want %q", gotMetadataWindowID, expectedPromptCacheKey+":0") } expectedInstallationID := codexIdentityConfuseUUID("auth-ws-1", "installation", "install-ws-1") if gotInstallationID := gjson.GetBytes(body, "client_metadata.x-codex-installation-id").String(); gotInstallationID != expectedInstallationID { @@ -404,52 +412,56 @@ func TestApplyCodexWebsocketHeadersIdentityConfuseRemapsPromptCacheKey(t *testin func TestCodexIdentityConfuseResponsePayloadHidesUpstreamAndRestoresClient(t *testing.T) { state := codexIdentityConfuseState{ + enabled: true, + authID: "auth-ws-1", originalPromptCacheKey: "cache-ws-1", promptCacheKey: codexIdentityConfuseUUID("auth-ws-1", "prompt-cache", "cache-ws-1"), } - rawPayload := []byte(`{"type":"response.completed","response":{"prompt_cache_key":"cache-ws-1"},"prompt_cache_key":"cache-ws-1"}`) + expectedTurnID := state.confuseTurnID("turn-ws-1") + rawPayload := []byte(`{"type":"response.completed","response":{"prompt_cache_key":"cache-ws-1","turn_id":"turn-ws-1"},"prompt_cache_key":"cache-ws-1","turn_id":"turn-ws-1"}`) upstreamPayload := applyCodexIdentityConfuseResponsePayload(rawPayload, state) if bytes.Contains(upstreamPayload, []byte(`cache-ws-1`)) { t.Fatalf("upstream payload still contains original prompt_cache_key: %s", string(upstreamPayload)) } + if bytes.Contains(upstreamPayload, []byte(`turn-ws-1`)) { + t.Fatalf("upstream payload still contains original turn_id: %s", string(upstreamPayload)) + } if !bytes.Contains(upstreamPayload, []byte(state.promptCacheKey)) { t.Fatalf("upstream payload missing confused prompt_cache_key: %s", string(upstreamPayload)) } + if !bytes.Contains(upstreamPayload, []byte(expectedTurnID)) { + t.Fatalf("upstream payload missing confused turn_id: %s", string(upstreamPayload)) + } clientPayload := applyCodexIdentityExposeResponsePayload(upstreamPayload, state) if bytes.Contains(clientPayload, []byte(state.promptCacheKey)) { t.Fatalf("client payload still contains confused prompt_cache_key: %s", string(clientPayload)) } + if bytes.Contains(clientPayload, []byte(expectedTurnID)) { + t.Fatalf("client payload still contains confused turn_id: %s", string(clientPayload)) + } if !bytes.Contains(clientPayload, []byte(`cache-ws-1`)) { t.Fatalf("client payload missing original prompt_cache_key: %s", string(clientPayload)) } + if !bytes.Contains(clientPayload, []byte(`turn-ws-1`)) { + t.Fatalf("client payload missing original turn_id: %s", string(clientPayload)) + } - rawSSE := []byte(`data: {"type":"response.completed","response":{"prompt_cache_key":"cache-ws-1"}}`) + rawSSE := []byte(`data: {"type":"response.completed","response":{"prompt_cache_key":"cache-ws-1","turn_id":"turn-ws-1"}}`) upstreamSSE := applyCodexIdentityConfuseResponsePayload(rawSSE, state) if bytes.Contains(upstreamSSE, []byte(`cache-ws-1`)) { t.Fatalf("upstream SSE still contains original prompt_cache_key: %s", string(upstreamSSE)) } + if bytes.Contains(upstreamSSE, []byte(`turn-ws-1`)) { + t.Fatalf("upstream SSE still contains original turn_id: %s", string(upstreamSSE)) + } clientSSE := applyCodexIdentityExposeResponsePayload(upstreamSSE, state) if !bytes.Contains(clientSSE, []byte(`cache-ws-1`)) || bytes.Contains(clientSSE, []byte(state.promptCacheKey)) { t.Fatalf("client SSE prompt_cache_key was not restored: %s", string(clientSSE)) } -} - -func TestApplyCodexWebsocketHeadersUsesCanonicalAccountHeader(t *testing.T) { - auth := &cliproxyauth.Auth{Provider: "codex", Metadata: map[string]any{"account_id": "acct-1"}} - - headers := applyCodexWebsocketHeaders(context.Background(), http.Header{}, auth, "", nil) - - if got := headerValueCaseInsensitive(headers, "ChatGPT-Account-ID"); got != "acct-1" { - t.Fatalf("ChatGPT-Account-ID = %s, want acct-1", got) - } - values, ok := headers["ChatGPT-Account-ID"] - if !ok { - t.Fatalf("expected exact ChatGPT-Account-ID key, got %#v", headers) - } - if len(values) != 1 || values[0] != "acct-1" { - t.Fatalf("ChatGPT-Account-ID values = %#v, want [acct-1]", values) + if !bytes.Contains(clientSSE, []byte(`turn-ws-1`)) || bytes.Contains(clientSSE, []byte(expectedTurnID)) { + t.Fatalf("client SSE turn_id was not restored: %s", string(clientSSE)) } }