feat(executor): enhance Codex identity obfuscation with turn and window metadata handling

- Modified `applyCodexIdentityConfuse*` functions to include `turn_id` and `window_id` in metadata transformations.
- Updated test cases to validate the inclusion and restoration of these fields.
- Removed deprecated `Conversation_id` header support and related logic for cleaner implementation.
This commit is contained in:
Luis Pater
2026-06-01 00:50:46 +08:00
parent 0f24cafbdd
commit bbcdaab79d
5 changed files with 156 additions and 84 deletions

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -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)

View File

@@ -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 "", ""

View File

@@ -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))
}
}