From 48686ccc8fbe898c2d048ac4815a7b2f1e409e27 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Mon, 28 Sep 2026 10:46:32 +0800 Subject: [PATCH] feat(config): preserve unknown nested fields as comments during v8 migration - Traverse nested configuration paths to comment out unrecognized fields during v8 migration - Remove deprecated Codex `identity-confuse` configuration option and request handling - Clean up identity confuse mapping logic from Codex HTTP and WebSocket executors --- config.example.yaml | 5 - .../api/handlers/management/config_v8_test.go | 58 ++++++- .../codex_websocket_header_defaults_test.go | 6 +- internal/config/config_types.go | 1 - internal/config/config_v8.go | 97 ++++++++++-- internal/config/config_v8_test.go | 43 ++++- .../executor/codex_executor_cache_test.go | 119 ++------------ .../executor/codex_executor_execute.go | 26 +-- .../executor/codex_executor_request.go | 148 +----------------- .../runtime/executor/codex_executor_stream.go | 11 +- .../runtime/executor/codex_openai_images.go | 23 +-- .../executor/codex_websockets_duplex.go | 8 +- .../codex_websockets_duplex_rejection_test.go | 2 - ...bsockets_duplex_successor_metadata_test.go | 2 - .../executor/codex_websockets_execute.go | 16 +- .../codex_websockets_executor_test.go | 114 -------------- .../executor/codex_websockets_stream.go | 34 ++-- internal/watcher/diff/config_diff.go | 3 - 18 files changed, 233 insertions(+), 483 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index dea889de0..8924f6d5f 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -836,11 +836,6 @@ oauth: # Experimental full-duplex Codex Responses WebSocket steering (default: false). # Keeps one upstream account/model per socket; accepted input is never replayed. response-steering: false - # When true, and routing.strategy is fill-first or routing.session-affinity is true, - # remap Codex prompt_cache_key and installation identity per selected auth. - # Some superstitious users believe request tracking identifiers can be used - # as evidence for TOS enforcement bans; this option only satisfies those odd concerns. - identity-confuse: false # Disable forcing the official Codex User-Agent and Originator headers on HTTP/SSE and WebSocket requests. disable-codex-cloaking: false # Hold back the frames that carry nothing the client has seen - the handshake (response.created, diff --git a/internal/api/handlers/management/config_v8_test.go b/internal/api/handlers/management/config_v8_test.go index a4d0e04b6..8eac084d8 100644 --- a/internal/api/handlers/management/config_v8_test.go +++ b/internal/api/handlers/management/config_v8_test.go @@ -175,6 +175,62 @@ func TestConfigV8CommentsUnknownLegacySectionsOnWrite(t *testing.T) { } } +func TestConfigV8CommentsUnknownNestedFieldsOnWrite(t *testing.T) { + gin.SetMode(gin.TestMode) + path := filepath.Join(t.TempDir(), "config.yaml") + raw := "server: {port: 8317}\noauth: {providers: {codex: {disable-codex-cloaking: true, retired-setting: false}}}\n" + if err := os.WriteFile(path, []byte(raw), 0600); err != nil { + t.Fatal(err) + } + cfg, err := config.LoadConfig(path) + if err != nil { + t.Fatal(err) + } + h := &Handler{cfg: cfg, configFilePath: path} + router := gin.New() + router.GET("/v8/management/config", h.ConfigV8) + router.PATCH("/v8/management/config", h.ConfigV8) + router.PUT("/v8/management/config/*path", h.ConfigV8) + router.DELETE("/v8/management/config/*path", h.ConfigV8) + request := func(method, url, body string, status int) { + t.Helper() + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, httptest.NewRequest(method, url, strings.NewReader(body))) + if recorder.Code != status { + t.Fatalf("%s %s: status=%d body=%s", method, url, recorder.Code, recorder.Body.String()) + } + } + request(http.MethodGet, "/v8/management/config", "", http.StatusOK) + if saved, errRead := os.ReadFile(path); errRead != nil || string(saved) != raw { + t.Fatalf("GET changed existing config: %v", errRead) + } + request(http.MethodPatch, "/v8/management/config", `{"server":{"port":8318}}`, http.StatusOK) + saved, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if err = config.ValidateV8Config(saved); err != nil { + t.Fatalf("saved config is invalid: %v\n%s", err, saved) + } + if strings.Count(string(saved), "# oauth.providers.codex.retired-setting: false") != 1 { + t.Fatalf("existing unknown field was not preserved as a comment: %s", saved) + } + loaded, err := config.LoadConfig(path) + if err != nil || loaded.Port != 8318 || !loaded.Codex.DisableCodexCloaking { + t.Fatalf("unrelated write changed known settings: cfg=%+v error=%v", loaded, err) + } + request(http.MethodPut, "/v8/management/config/oauth/providers/codex/new-setting", `true`, http.StatusBadRequest) + unchanged, err := os.ReadFile(path) + if err != nil || string(unchanged) != string(saved) { + t.Fatalf("invalid new setting changed the config: %v", err) + } + request(http.MethodDelete, "/v8/management/config/oauth/providers/codex/disable-codex-cloaking", "", http.StatusOK) + saved, err = os.ReadFile(path) + if err != nil || strings.Count(string(saved), "# oauth.providers.codex.retired-setting: false") != 1 { + t.Fatalf("deleting the neighboring setting lost the archived comment: %v\n%s", err, saved) + } +} + func TestV8NestedWriteMigratesOnlyOnSuccess(t *testing.T) { for _, tc := range []struct { name, body string @@ -319,7 +375,7 @@ func TestConfigV8DeleteLastField(t *testing.T) { {"websocket", "ws-auth: false\n", "oauth/providers/aistudio/ws-auth", func(cfg *config.Config) bool { return cfg.WebsocketAuth }}, {"debug", "debug: true\n", "observability/logs/debug", func(cfg *config.Config) bool { return !cfg.Debug }}, {"sibling", "routing: {strategy: fill-first, retry: {request-retry: 3}}\n", "routing/retry/request-retry", func(cfg *config.Config) bool { return cfg.RequestRetry == 0 && cfg.Routing.Strategy == "fill-first" }}, - {"provider", "oauth: {providers: {codex: {identity-confuse: true}}}\n", "oauth/providers/codex/identity-confuse", func(cfg *config.Config) bool { return !cfg.Codex.IdentityConfuse }}, + {"provider", "oauth: {providers: {codex: {disable-codex-cloaking: true}}}\n", "oauth/providers/codex/disable-codex-cloaking", func(cfg *config.Config) bool { return !cfg.Codex.DisableCodexCloaking }}, {"excluded models", "oauth: {excluded-models: {codex: [blocked-model]}}\n", "oauth/excluded-models", func(cfg *config.Config) bool { return len(cfg.OAuthExcludedModels) == 0 }}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/config/codex_websocket_header_defaults_test.go b/internal/config/codex_websocket_header_defaults_test.go index 032d7e248..dd664dcb1 100644 --- a/internal/config/codex_websocket_header_defaults_test.go +++ b/internal/config/codex_websocket_header_defaults_test.go @@ -34,12 +34,11 @@ codex-header-defaults: } } -func TestLoadConfigOptional_CodexIdentityConfuse(t *testing.T) { +func TestLoadConfigOptional_CodexOptions(t *testing.T) { dir := t.TempDir() configPath := filepath.Join(dir, "config.yaml") configYAML := []byte(` codex: - identity-confuse: true disable-codex-cloaking: true optimize-multi-agent-v2: true `) @@ -52,9 +51,6 @@ codex: t.Fatalf("LoadConfigOptional() error = %v", err) } - if !cfg.Codex.IdentityConfuse { - t.Fatalf("IdentityConfuse = false, want true") - } if !cfg.Codex.DisableCodexCloaking { t.Fatal("DisableCodexCloaking = false, want true") } diff --git a/internal/config/config_types.go b/internal/config/config_types.go index eea6f3b02..5158d933a 100644 --- a/internal/config/config_types.go +++ b/internal/config/config_types.go @@ -180,7 +180,6 @@ type AntigravityConnectionPoolConfig struct { // CodexConfig configures provider-wide Codex request behavior. type CodexConfig struct { - IdentityConfuse bool `yaml:"identity-confuse" json:"identity-confuse"` // DisableCodexCloaking disables forcing the official Codex identity headers on HTTP/SSE and WebSocket requests. DisableCodexCloaking bool `yaml:"disable-codex-cloaking" json:"disable-codex-cloaking"` // StreamBootstrapBuffering holds back the frames that arrive before generation starts, none of diff --git a/internal/config/config_v8.go b/internal/config/config_v8.go index 304514225..9404d61d3 100644 --- a/internal/config/config_v8.go +++ b/internal/config/config_v8.go @@ -401,7 +401,7 @@ func NormalizeConfigLayout(data []byte, migrate bool) ([]byte, bool, error) { changed = true } if migrate { - // Existing unknown sections are ignored by the runtime. Retain their + // Existing unknown fields are ignored by the runtime. Retain their // contents as comments while keeping new v8 writes strictly validated. if err := commentUnknownV8Sections(root); err != nil { return nil, false, err @@ -450,27 +450,104 @@ func warnUnrecognizedV8Section(section string) { } func commentUnknownV8Sections(root *yaml.Node) error { - allowed := v8AllowedRoots() + if err := commentUnknownV8Fields(root, root, v8AllowedRoots(), ""); err != nil { + return err + } + + children := make(map[string]map[string]bool) + for _, path := range append(append([]configPath(nil), v8Paths...), v8StructPaths...) { + parts := strings.Split(path.current, ".") + for i := 1; i < len(parts); i++ { + parent := strings.Join(parts[:i], ".") + if children[parent] == nil { + children[parent] = make(map[string]bool) + } + children[parent][parts[i]] = true + } + } + // Some structs already live at their v8 path and therefore have no remap entry. + allowedRoots := v8AllowedRoots() + var includeNative func(reflect.Type, string) + includeNative = func(t reflect.Type, path string) { + if t.Kind() != reflect.Struct { + return + } + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + tag := strings.Split(field.Tag.Get("yaml"), ",")[0] + if tag == "-" || field.PkgPath != "" { + continue + } + if field.Anonymous { + includeNative(field.Type, path) + continue + } + if path == "" && !allowedRoots[tag] { + continue + } + if children[path] == nil { + children[path] = make(map[string]bool) + } + children[path][tag] = true + child := tag + if path != "" { + child = path + "." + tag + } + includeNative(field.Type, child) + } + } + includeNative(reflect.TypeOf(Config{}), "") + var walk func(*yaml.Node, string) error + walk = func(node *yaml.Node, path string) error { + if node == nil || node.Kind != yaml.MappingNode || children[path] == nil { + return nil + } + if err := commentUnknownV8Fields(node, root, children[path], path); err != nil { + return err + } + for i := 0; i+1 < len(node.Content); i += 2 { + child := path + "." + node.Content[i].Value + if err := walk(node.Content[i+1], child); err != nil { + return err + } + } + return nil + } + for i := 0; i+1 < len(root.Content); i += 2 { + if err := walk(root.Content[i+1], root.Content[i].Value); err != nil { + return err + } + } + return nil +} + +func commentUnknownV8Fields(node *yaml.Node, archive *yaml.Node, allowed map[string]bool, path string) error { var comments []string - for i := 0; i+1 < len(root.Content); { - if allowed[root.Content[i].Value] { + for i := 0; i+1 < len(node.Content); { + key := node.Content[i].Value + if allowed[key] { i += 2 continue } - key := root.Content[i].Value - warnUnrecognizedV8Section(key) - entry := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Content: root.Content[i : i+2]} + section := key + if path != "" { + section = path + "." + key + } + warnUnrecognizedV8Section(section) + keyNode := *node.Content[i] + keyNode.Value = section + entry := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Content: []*yaml.Node{&keyNode, node.Content[i+1]}} data, errMarshal := yaml.Marshal(entry) if errMarshal != nil { return errMarshal } text := strings.TrimSuffix(string(data), "\n") comments = append(comments, "# "+strings.ReplaceAll(text, "\n", "\n# ")) - root.Content = append(root.Content[:i], root.Content[i+2:]...) + node.Content = append(node.Content[:i], node.Content[i+2:]...) } - // A root-level comment survives later deletion of any neighboring setting. + // Archive at the root so deleting a neighboring nested field cannot drop it. if len(comments) > 0 { - root.FootComment = strings.TrimSpace(root.FootComment + "\n" + strings.Join(comments, "\n")) + archive.FootComment = strings.TrimSpace(archive.FootComment + "\n" + strings.Join(comments, "\n")) } return nil } diff --git a/internal/config/config_v8_test.go b/internal/config/config_v8_test.go index 7c27deeb2..e1aff549d 100644 --- a/internal/config/config_v8_test.go +++ b/internal/config/config_v8_test.go @@ -307,6 +307,43 @@ proxy-url: old } } +func TestV8MigrationCommentsUnknownNestedFields(t *testing.T) { + raw := []byte(`server: {port: 8317} +routing: {strategy: fill-first, session-affinity: true} +oauth: + providers: + codex: + disable-codex-cloaking: true + retired-setting: {mode: old} +`) + unchanged, changed, err := NormalizeConfigLayout(raw, false) + if err != nil || changed || string(unchanged) != string(raw) { + t.Fatalf("read-only normalization changed existing config: changed=%v error=%v", changed, err) + } + migrated, _, err := NormalizeConfigLayout(raw, true) + if err != nil { + t.Fatal(err) + } + if err = ValidateV8Config(migrated); err != nil { + t.Fatalf("migrated config is invalid: %v\n%s", err, migrated) + } + var doc yaml.Node + if err = yaml.Unmarshal(migrated, &doc); err != nil { + t.Fatal(err) + } + if yamlPath(doc.Content[0], "oauth.providers.codex.retired-setting") != nil || !strings.Contains(string(migrated), "# oauth.providers.codex.retired-setting:") { + t.Fatalf("unknown nested field was not preserved as a comment: %s", migrated) + } + cfg, err := ParseConfigBytes(migrated) + if err != nil || cfg.Routing.Strategy != "fill-first" || !cfg.Routing.SessionAffinity || !cfg.Codex.DisableCodexCloaking { + t.Fatalf("migration changed known settings: cfg=%+v error=%v", cfg, err) + } + remigrated, _, err := NormalizeConfigLayout(migrated, true) + if err != nil || strings.Count(string(remigrated), "# oauth.providers.codex.retired-setting:") != 1 { + t.Fatalf("repeated migration lost or duplicated the comment: %v\n%s", err, remigrated) + } +} + func TestV8MigrationCommentsUnknownLegacySectionsWarnsConsole(t *testing.T) { logger := log.StandardLogger() previousHooks := logger.ReplaceHooks(make(log.LevelHooks)) @@ -554,7 +591,7 @@ func TestV8MigrationPreservesEmptyLegacyContainers(t *testing.T) { func TestV8EmptyLegacyContainersKeepNewValues(t *testing.T) { raw := []byte(`port: 8317 tls: null -codex: {identity-confuse: true, live-media-relay: {}} +codex: {disable-codex-cloaking: true, live-media-relay: {}} server: {tls: {enable: true, cert: server.crt, key: server.key}} oauth: {providers: {codex: {live-media-relay: {max-sessions: 12}}}} `) @@ -567,7 +604,7 @@ oauth: {providers: {codex: {live-media-relay: {max-sessions: 12}}}} if err != nil { t.Fatal(err) } - if !cfg.TLS.Enable || cfg.TLS.Cert != "server.crt" || cfg.TLS.Key != "server.key" || cfg.Codex.LiveMediaRelay.MaxSessions != 12 || !cfg.Codex.IdentityConfuse { + if !cfg.TLS.Enable || cfg.TLS.Cert != "server.crt" || cfg.TLS.Key != "server.key" || cfg.Codex.LiveMediaRelay.MaxSessions != 12 || !cfg.Codex.DisableCodexCloaking { t.Fatal("empty legacy block overwrote new values or a non-empty sibling") } var doc yaml.Node @@ -577,7 +614,7 @@ oauth: {providers: {codex: {live-media-relay: {max-sessions: 12}}}} if yamlPath(doc.Content[0], "tls") != nil || yamlPath(doc.Content[0], "codex.live-media-relay") != nil { t.Fatal("conflicting empty legacy blocks were not removed") } - if !migrate && yamlPath(doc.Content[0], "codex.identity-confuse") == nil { + if !migrate && yamlPath(doc.Content[0], "codex.disable-codex-cloaking") == nil { t.Fatal("conflict cleanup migrated a non-conflicting legacy sibling") } } diff --git a/internal/runtime/executor/codex_executor_cache_test.go b/internal/runtime/executor/codex_executor_cache_test.go index 9cee6c646..3fecad0c9 100644 --- a/internal/runtime/executor/codex_executor_cache_test.go +++ b/internal/runtime/executor/codex_executor_cache_test.go @@ -9,7 +9,6 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" - "github.com/router-for-me/CLIProxyAPI/v8/internal/config" "github.com/router-for-me/CLIProxyAPI/v8/internal/runtime/executor/helps" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor" @@ -31,7 +30,7 @@ func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFrom } url := "https://example.com/responses" - httpReq, _, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, nil, req, req.Payload, rawJSON) + httpReq, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON) if err != nil { t.Fatalf("cacheHelper error: %v", err) } @@ -56,7 +55,7 @@ func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFrom t.Fatalf("Session_id = %q, want empty", gotLegacySession) } - httpReq2, _, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, nil, req, req.Payload, rawJSON) + httpReq2, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, req, rawJSON) if err != nil { t.Fatalf("cacheHelper error (second call): %v", err) } @@ -81,7 +80,7 @@ func TestCodexExecutorCacheHelper_UsesDerivedSessionUUID(t *testing.T) { } expectedKey := helps.DerivedSessionUUID("codex", req.Metadata) - httpReq, body, _, err := executor.cacheHelper(context.Background(), sdktranslator.FormatOpenAI, "https://example.com/responses", nil, req, req.Payload, []byte(`{"model":"gpt-5.4","stream":true}`)) + httpReq, body, err := executor.cacheHelper(context.Background(), sdktranslator.FormatOpenAI, "https://example.com/responses", req, []byte(`{"model":"gpt-5.4","stream":true}`)) if err != nil { t.Fatalf("cacheHelper error: %v", err) } @@ -118,11 +117,11 @@ func TestCodexExecutorCacheHelper_ClaudeUsesClaudeCodeSessionID(t *testing.T) { }`), } - firstHTTPReq, _, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("claude"), url, nil, firstReq, firstReq.Payload, rawJSON) + firstHTTPReq, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("claude"), url, firstReq, rawJSON) if err != nil { t.Fatalf("cacheHelper first error: %v", err) } - secondHTTPReq, _, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("claude"), url, nil, secondReq, secondReq.Payload, rawJSON) + secondHTTPReq, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("claude"), url, secondReq, rawJSON) if err != nil { t.Fatalf("cacheHelper second error: %v", err) } @@ -158,7 +157,7 @@ func TestCodexExecutorCacheHelper_ClaudeRejectsBareUserID(t *testing.T) { Payload: []byte(`{"model":"gpt-5.4","metadata":{"user_id":"same-user-across-chats"},"messages":[{"role":"user","content":[{"type":"text","text":"first"}]}]}`), } - httpReq, _, _, err := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), "https://example.com/responses", nil, req, req.Payload, []byte(`{"model":"gpt-5.4","stream":true}`)) + httpReq, _, err := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), "https://example.com/responses", req, []byte(`{"model":"gpt-5.4","stream":true}`)) if err != nil { t.Fatalf("cacheHelper error: %v", err) } @@ -178,81 +177,6 @@ func TestCodexExecutorCacheHelper_ClaudeRejectsBareUserID(t *testing.T) { } } -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","window_id":"cache-1:0"}`) - 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\",\"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"}}`), - } - 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") - 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) - } - 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) - } - 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") - } - if gotHeader := httpReq.Header["Session-Id"]; len(gotHeader) != 1 || gotHeader[0] != expectedPromptCacheKey { - t.Fatalf("Session-Id = %#v, want [%q]", gotHeader, expectedPromptCacheKey) - } - for _, headerName := range []string{"X-Client-Request-Id", "Thread-Id"} { - if gotHeader := httpReq.Header.Get(headerName); gotHeader != expectedPromptCacheKey { - t.Fatalf("%s = %q, want %q", headerName, gotHeader, expectedPromptCacheKey) - } - } - if gotLegacySession := httpReq.Header.Get("Session_id"); gotLegacySession != "" { - t.Fatalf("Session_id = %q, want empty", gotLegacySession) - } - if gotWindow := httpReq.Header.Get("X-Codex-Window-Id"); gotWindow != expectedPromptCacheKey+":0" { - t.Fatalf("X-Codex-Window-Id = %q, want %q", gotWindow, expectedPromptCacheKey+":0") - } - 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") - } -} - func TestApplyCodexHeadersUsesAccountHeaderForOAuth(t *testing.T) { httpReq := httptest.NewRequest("POST", "https://example.com/responses", nil) auth := &cliproxyauth.Auth{ @@ -267,27 +191,6 @@ func TestApplyCodexHeadersUsesAccountHeaderForOAuth(t *testing.T) { } } -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) - } -} - func TestCodexExecutorCacheHelper_ClaudeUsesSessionHeader(t *testing.T) { executor := &CodexExecutor{} recorder := httptest.NewRecorder() @@ -307,11 +210,11 @@ func TestCodexExecutorCacheHelper_ClaudeUsesSessionHeader(t *testing.T) { rawJSON := []byte(`{"model":"gpt-5.4","stream":true}`) url := "https://example.com/responses" - firstHTTPReq, _, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("claude"), url, nil, firstReq, firstReq.Payload, rawJSON) + firstHTTPReq, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("claude"), url, firstReq, rawJSON) if err != nil { t.Fatalf("cacheHelper first error: %v", err) } - secondHTTPReq, _, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("claude"), url, nil, secondReq, secondReq.Payload, rawJSON) + secondHTTPReq, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("claude"), url, secondReq, rawJSON) if err != nil { t.Fatalf("cacheHelper second error: %v", err) } @@ -347,7 +250,7 @@ func TestCodexExecutorCacheHelper_ClaudeAgentScopeUsesResolvedModelAcrossHTTPAnd childHeaders.Set(helps.ClaudeCodeAgentHeader, "agent-a") rawJSON := []byte(`{"model":"gpt-5.4","stream":true}`) - rootRequest, _, _, errRoot := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), url, nil, req, req.Payload, rawJSON, rootHeaders) + rootRequest, _, errRoot := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), url, req, rawJSON, rootHeaders) if errRoot != nil { t.Fatalf("root cacheHelper error: %v", errRoot) } @@ -357,7 +260,7 @@ func TestCodexExecutorCacheHelper_ClaudeAgentScopeUsesResolvedModelAcrossHTTPAnd } rootKey := gjson.GetBytes(rootBody, "prompt_cache_key").String() - childRequest, _, _, errChild := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), url, nil, req, req.Payload, rawJSON, childHeaders) + childRequest, _, errChild := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), url, req, rawJSON, childHeaders) if errChild != nil { t.Fatalf("child cacheHelper error: %v", errChild) } @@ -372,7 +275,7 @@ func TestCodexExecutorCacheHelper_ClaudeAgentScopeUsesResolvedModelAcrossHTTPAnd aliasReq := req aliasReq.Model = "another-local-alias-low" - aliasRequest, _, _, errAlias := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), url, nil, aliasReq, aliasReq.Payload, rawJSON, childHeaders) + aliasRequest, _, errAlias := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), url, aliasReq, rawJSON, childHeaders) if errAlias != nil { t.Fatalf("alias cacheHelper error: %v", errAlias) } diff --git a/internal/runtime/executor/codex_executor_execute.go b/internal/runtime/executor/codex_executor_execute.go index fae49f1a1..9637e5224 100644 --- a/internal/runtime/executor/codex_executor_execute.go +++ b/internal/runtime/executor/codex_executor_execute.go @@ -77,15 +77,13 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re reporter.SetTranslatedReasoningEffort(body, to.String()) url := strings.TrimSuffix(baseURL, "/") + "/responses" - var identityState codexIdentityConfuseState - httpReq, upstreamBody, identityState, err := e.cacheHelper(ctx, from, url, auth, req, originalPayloadSource, body, opts.Headers) + httpReq, upstreamBody, err := e.cacheHelper(ctx, from, url, req, body, opts.Headers) if err != nil { return resp, err } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg, opts.Headers) applyCodexRoutingHint(ctx, httpReq.Header, auth, baseModel, upstreamBody, opts.Headers) applyModelHeaderOverrides(httpReq.Header, baseModel) - applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -118,7 +116,6 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { b, _ := io.ReadAll(httpResp.Body) - b = applyCodexIdentityConfuseResponsePayload(b, identityState) if errClearReplay := clearCodexReasoningReplayOnInvalidSignature(ctx, replayScope, httpResp.StatusCode, b); errClearReplay != nil { return resp, errClearReplay } @@ -128,10 +125,9 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re return resp, err } data, errRead := io.ReadAll(httpResp.Body) - upstreamData := applyCodexIdentityConfuseResponsePayload(data, identityState) - helps.AppendAPIResponseChunk(ctx, e.cfg, upstreamData) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) - lines := bytes.Split(upstreamData, []byte("\n")) + lines := bytes.Split(data, []byte("\n")) outputItemsByIndex := make(map[int64][]byte) var outputItemsFallback [][]byte sawOutputDelta := false @@ -191,8 +187,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re } var param any - clientCompletedData := applyCodexIdentityExposeResponsePayload(completedData, identityState) - out := sdktranslator.TranslateNonStream(ctx, to, responseFormat, req.Model, originalPayload, body, clientCompletedData, ¶m) + out := sdktranslator.TranslateNonStream(ctx, to, responseFormat, req.Model, originalPayload, body, completedData, ¶m) if responseFormat == sdktranslator.FormatOpenAIResponse { out = helps.EnsureResponsesUsageDetails(out) } @@ -251,15 +246,13 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A reporter.SetTranslatedReasoningEffort(body, to.String()) url := strings.TrimSuffix(baseURL, "/") + "/responses/compact" - var identityState codexIdentityConfuseState - httpReq, upstreamBody, identityState, err := e.cacheHelper(ctx, from, url, auth, req, originalPayloadSource, body, opts.Headers) + httpReq, upstreamBody, err := e.cacheHelper(ctx, from, url, req, body, opts.Headers) if err != nil { return resp, err } applyCodexHeaders(httpReq, auth, apiKey, false, e.cfg, opts.Headers) applyCodexRoutingHint(ctx, httpReq.Header, auth, baseModel, upstreamBody, opts.Headers) applyModelHeaderOverrides(httpReq.Header, baseModel) - applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -292,7 +285,6 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone()) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { b, _ := io.ReadAll(httpResp.Body) - b = applyCodexIdentityConfuseResponsePayload(b, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, b) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, b, e.modelLevelCooling()) @@ -303,14 +295,12 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A helps.RecordAPIResponseError(ctx, e.cfg, err) return resp, err } - upstreamData := applyCodexIdentityConfuseResponsePayload(data, identityState) - helps.AppendAPIResponseChunk(ctx, e.cfg, upstreamData) - upstreamData = helps.RestoreCodexMultiAgentV2Response(upstreamData, optimizeMultiAgentV2) + helps.AppendAPIResponseChunk(ctx, e.cfg, data) + upstreamData := helps.RestoreCodexMultiAgentV2Response(data, optimizeMultiAgentV2) reporter.Publish(ctx, helps.ParseOpenAIUsage(upstreamData)) reporter.EnsurePublished(ctx) var param any - clientData := applyCodexIdentityExposeResponsePayload(upstreamData, identityState) - out := sdktranslator.TranslateNonStream(ctx, to, responseFormat, req.Model, originalPayload, body, clientData, ¶m) + out := sdktranslator.TranslateNonStream(ctx, to, responseFormat, req.Model, originalPayload, body, upstreamData, ¶m) if responseFormat == sdktranslator.FormatOpenAIResponse { out = helps.EnsureResponsesUsageDetails(out) } diff --git a/internal/runtime/executor/codex_executor_request.go b/internal/runtime/executor/codex_executor_request.go index fb50aa74d..6e948f9bd 100644 --- a/internal/runtime/executor/codex_executor_request.go +++ b/internal/runtime/executor/codex_executor_request.go @@ -91,20 +91,7 @@ func (e *CodexExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth return httpClient.Do(httpReq) } -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, headerSets ...http.Header) (*http.Request, []byte, codexIdentityConfuseState, error) { +func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, req cliproxyexecutor.Request, rawJSON []byte, headerSets ...http.Header) (*http.Request, []byte, error) { var headers http.Header if len(headerSets) > 0 { headers = headerSets[0] @@ -117,7 +104,7 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form } cached, ok, errCache := helps.ClaudeCodePromptCache(ctx, modelName, req.Payload, headers) if errCache != nil { - return nil, nil, codexIdentityConfuseState{}, errCache + return nil, nil, errCache } if ok { cache = cached @@ -148,141 +135,14 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form rawJSON = helps.SetStringIfDifferent(rawJSON, "prompt_cache_key", cache.ID) } rawJSON = helps.SanitizeCodexInputItemIDs(rawJSON) - var identityState codexIdentityConfuseState - rawJSON, identityState = applyCodexIdentityConfuseBody(e.cfg, auth, userPayload, rawJSON) - if identityState.promptCacheKey != "" { - cache.ID = identityState.promptCacheKey - } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(rawJSON)) if err != nil { - return nil, nil, codexIdentityConfuseState{}, err + return nil, nil, err } if cache.ID != "" { httpReq.Header.Set("Session-Id", cache.ID) } - return httpReq, rawJSON, identityState, nil -} - -func applyCodexIdentityConfuseBody(cfg *config.Config, auth *cliproxyauth.Auth, userPayload []byte, rawJSON []byte) ([]byte, codexIdentityConfuseState) { - if !codexIdentityConfuseEnabled(cfg) || auth == nil || strings.TrimSpace(auth.ID) == "" || len(rawJSON) == 0 { - return rawJSON, 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) - rawJSON = helps.SetStringIfDifferent(rawJSON, "prompt_cache_key", state.promptCacheKey) - } - 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 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") - } - } - - return rawJSON, state -} - -func applyCodexIdentityConfuseHeaders(headers http.Header, state *codexIdentityConfuseState) { - if headers == nil { - return - } - if state == nil || !state.enabled { - return - } - - if rawTurnMetadata := strings.TrimSpace(headers.Get("X-Codex-Turn-Metadata")); rawTurnMetadata != "" { - headers.Set("X-Codex-Turn-Metadata", applyCodexTurnMetadataIdentityConfuse(rawTurnMetadata, state)) - } - if state.promptCacheKey == "" { - return - } - - setCodexSessionHeaderCasePreserved(headers, "Session-Id", state.promptCacheKey) - if headerValueCaseInsensitive(headers, "Conversation_id") != "" { - setHeaderCasePreserved(headers, "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") -} - -func applyCodexTurnMetadataIdentityConfuse(rawTurnMetadata string, state *codexIdentityConfuseState) string { - updatedTurnMetadata := rawTurnMetadata - 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.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 { - 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 { - 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 { - from = strings.TrimSpace(from) - to = strings.TrimSpace(to) - if len(payload) == 0 || from == "" || to == "" || from == to || !bytes.Contains(payload, []byte(from)) { - return payload - } - return bytes.ReplaceAll(payload, []byte(from), []byte(to)) -} - -func codexIdentityConfuseEnabled(cfg *config.Config) bool { - if cfg == nil || !cfg.Codex.IdentityConfuse { - return false - } - strategy := strings.ToLower(strings.TrimSpace(cfg.Routing.Strategy)) - return cfg.Routing.SessionAffinity || strategy == "fill-first" || strategy == "fillfirst" || strategy == "ff" -} - -func codexIdentityConfuseUUID(authID string, kind string, value string) string { - name := strings.Join([]string{"cli-proxy-api", "codex", "identity-confuse", kind, strings.TrimSpace(authID), strings.TrimSpace(value)}, ":") - return uuid.NewSHA1(uuid.NameSpaceOID, []byte(name)).String() + return httpReq, rawJSON, nil } func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config, clientHeaders ...http.Header) { diff --git a/internal/runtime/executor/codex_executor_stream.go b/internal/runtime/executor/codex_executor_stream.go index 36e884a17..bcacb36f1 100644 --- a/internal/runtime/executor/codex_executor_stream.go +++ b/internal/runtime/executor/codex_executor_stream.go @@ -85,15 +85,13 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au reporter.SetTranslatedReasoningEffort(body, to.String()) url := strings.TrimSuffix(baseURL, "/") + "/responses" - var identityState codexIdentityConfuseState - httpReq, upstreamBody, identityState, err := e.cacheHelper(ctx, from, url, auth, req, originalPayloadSource, body, opts.Headers) + httpReq, upstreamBody, err := e.cacheHelper(ctx, from, url, req, body, opts.Headers) if err != nil { return nil, err } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg, opts.Headers) applyCodexRoutingHint(ctx, httpReq.Header, auth, baseModel, upstreamBody, opts.Headers) applyModelHeaderOverrides(httpReq.Header, baseModel) - applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) var authID, authLabel, authType, authValue string if auth != nil { authID = auth.ID @@ -129,7 +127,6 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au helps.RecordAPIResponseError(ctx, e.cfg, readErr) return nil, readErr } - data = applyCodexIdentityConfuseResponsePayload(data, identityState) if errClearReplay := clearCodexReasoningReplayOnInvalidSignature(ctx, replayScope, httpResp.StatusCode, data); errClearReplay != nil { return nil, errClearReplay } @@ -185,7 +182,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au sawOutputDelta := false if buffering { for scanner.Scan() { - line := applyCodexIdentityConfuseResponsePayload(scanner.Bytes(), identityState) + line := scanner.Bytes() helps.AppendAPIResponseChunk(ctx, e.cfg, line) var translatedLine []byte isHandshake := false @@ -273,7 +270,6 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au isHandshake = true } - translatedLine = applyCodexIdentityExposeResponsePayload(translatedLine, identityState) chunks := helps.TranslateStreamWithClaudeInputTokens(ctx, to, responseFormat, req.Model, originalPayload, body, translatedLine, ¶m, claudeInputTokens) if isHandshake && !terminalSuccess { frameBytes := len(line) @@ -359,7 +355,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } }() for scanner.Scan() { - line := applyCodexIdentityConfuseResponsePayload(scanner.Bytes(), identityState) + line := scanner.Bytes() helps.AppendAPIResponseChunk(ctx, e.cfg, line) var translatedLine []byte terminalSuccess := false @@ -427,7 +423,6 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au translatedLine = bytes.Clone(line) } - translatedLine = applyCodexIdentityExposeResponsePayload(translatedLine, identityState) chunks := helps.TranslateStreamWithClaudeInputTokens(ctx, to, responseFormat, req.Model, originalPayload, body, translatedLine, ¶m, claudeInputTokens) for i := range chunks { select { diff --git a/internal/runtime/executor/codex_openai_images.go b/internal/runtime/executor/codex_openai_images.go index 94811fa1f..65797a3ad 100644 --- a/internal/runtime/executor/codex_openai_images.go +++ b/internal/runtime/executor/codex_openai_images.go @@ -110,14 +110,12 @@ func (e *CodexExecutor) executeOpenAIImage(ctx context.Context, auth *cliproxyau reporter.SetTranslatedReasoningEffort(body, "codex") url := strings.TrimSuffix(baseURL, "/") + "/responses" - var identityState codexIdentityConfuseState - httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) + httpReq, body, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, req, body) if errCache != nil { return resp, errCache } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg, opts.Headers) applyModelHeaderOverrides(httpReq.Header, mainModel) - applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) @@ -139,7 +137,6 @@ func (e *CodexExecutor) executeOpenAIImage(ctx context.Context, auth *cliproxyau helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } - data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) @@ -208,14 +205,12 @@ func (e *CodexExecutor) executeOpenAIImageStream(ctx context.Context, auth *clip reporter.SetTranslatedReasoningEffort(body, "codex") url := strings.TrimSuffix(baseURL, "/") + "/responses" - var identityState codexIdentityConfuseState - httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) + httpReq, body, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, req, body) if errCache != nil { return nil, errCache } applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg, opts.Headers) applyModelHeaderOverrides(httpReq.Header, mainModel) - applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) @@ -235,7 +230,6 @@ func (e *CodexExecutor) executeOpenAIImageStream(ctx context.Context, auth *clip helps.RecordAPIResponseError(ctx, e.cfg, errRead) return nil, errRead } - data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) @@ -273,7 +267,7 @@ func (e *CodexExecutor) executeOpenAIImageStream(ctx context.Context, auth *clip outputItemsByIndex := make(map[int64][]byte) var outputItemsFallback [][]byte for scanner.Scan() { - line := applyCodexIdentityConfuseResponsePayload(scanner.Bytes(), identityState) + line := scanner.Bytes() helps.AppendAPIResponseChunk(ctx, e.cfg, line) if !bytes.HasPrefix(line, dataTag) { continue @@ -336,8 +330,7 @@ func (e *CodexExecutor) executeDirectOpenAIImage(ctx context.Context, auth *clip reporter.SetTranslatedReasoningEffort(body, "openai") url := strings.TrimSuffix(baseURL, "/") + endpointPath - var identityState codexIdentityConfuseState - httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) + httpReq, body, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, req, body) if errCache != nil { return resp, errCache } @@ -346,7 +339,6 @@ func (e *CodexExecutor) executeDirectOpenAIImage(ctx context.Context, auth *clip if contentType != "" { httpReq.Header.Set("Content-Type", contentType) } - applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) @@ -368,7 +360,6 @@ func (e *CodexExecutor) executeDirectOpenAIImage(ctx context.Context, auth *clip helps.RecordAPIResponseError(ctx, e.cfg, errRead) return resp, errRead } - data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 { helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) @@ -397,8 +388,7 @@ func (e *CodexExecutor) executeDirectOpenAIImageStream(ctx context.Context, auth reporter.SetTranslatedReasoningEffort(body, "openai") url := strings.TrimSuffix(baseURL, "/") + endpointPath - var identityState codexIdentityConfuseState - httpReq, body, identityState, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, auth, req, req.Payload, body) + httpReq, body, errCache := e.cacheHelper(ctx, sdktranslator.FromString(codexOpenAIImageSourceFormat), url, req, body) if errCache != nil { return nil, errCache } @@ -407,7 +397,6 @@ func (e *CodexExecutor) executeDirectOpenAIImageStream(ctx context.Context, auth if contentType != "" { httpReq.Header.Set("Content-Type", contentType) } - applyCodexIdentityConfuseHeaders(httpReq.Header, &identityState) recordCodexOpenAIImageRequest(ctx, e.cfg, e.Identifier(), auth, url, httpReq.Header.Clone(), body) httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0) @@ -427,7 +416,6 @@ func (e *CodexExecutor) executeDirectOpenAIImageStream(ctx context.Context, auth helps.RecordAPIResponseError(ctx, e.cfg, errRead) return nil, errRead } - data = applyCodexIdentityConfuseResponsePayload(data, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, data) helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data)) err = newCodexStatusErrWithCooling(httpResp.StatusCode, data, e.modelLevelCooling()) @@ -451,7 +439,6 @@ func (e *CodexExecutor) executeDirectOpenAIImageStream(ctx context.Context, auth n, errRead := httpResp.Body.Read(buffer) if n > 0 { chunk := bytes.Clone(buffer[:n]) - chunk = applyCodexIdentityConfuseResponsePayload(chunk, identityState) helps.AppendAPIResponseChunk(ctx, e.cfg, chunk) for _, line := range bytes.Split(chunk, []byte("\n")) { streamUsage.ObserveOpenAIStream(bytes.TrimSpace(line)) diff --git a/internal/runtime/executor/codex_websockets_duplex.go b/internal/runtime/executor/codex_websockets_duplex.go index 2c003665c..132e41693 100644 --- a/internal/runtime/executor/codex_websockets_duplex.go +++ b/internal/runtime/executor/codex_websockets_duplex.go @@ -200,7 +200,7 @@ func (e *CodexWebsocketsExecutor) streamCodexDuplex( fail(cliproxyexecutor.NewUpstreamWebsocketReplayRequiredError()) return false } - payload = buildCodexWebsocketRequestBody(prepared.upstreamBody) + payload = buildCodexWebsocketRequestBody(prepared.clientBody) metadataMu.Lock() if len(pending) >= 16 { metadataMu.Unlock() @@ -413,7 +413,7 @@ func (e *CodexWebsocketsExecutor) streamCodexDuplex( // Retain response settings, not request history or authorization headers. // In-flight steering pins its parent's settings independently of this window. snapshot := *current - snapshot.originalPayload, snapshot.upstreamBody, snapshot.wsHeaders = nil, nil, nil + snapshot.originalPayload, snapshot.wsHeaders = nil, nil snapshot.clientBody = []byte("{}") if reasoning := gjson.GetBytes(current.clientBody, "reasoning"); reasoning.Exists() { snapshot.clientBody, _ = sjson.SetRawBytes(snapshot.clientBody, "reasoning", []byte(reasoning.Raw)) @@ -511,7 +511,7 @@ func (e *CodexWebsocketsExecutor) streamCodexDuplex( metadataMu.Lock() // A failure for the running response must not consume a queued create. // A rejection before response.created instead owns the oldest pending - // create, including its identity mapping and reasoning replay scope. + // create, including its reasoning replay scope. currentFailure := failedID != "" && failedID == responseID ambiguous := failedID == "" && ((len(pending) > 0 && responseActive) || len(unacknowledgedSteers) > 0) if len(pending) > 0 && !currentFailure && !ambiguous { @@ -536,7 +536,6 @@ func (e *CodexWebsocketsExecutor) streamCodexDuplex( return } } - payload = applyCodexIdentityConfuseResponsePayload(payload, eventPrepared.identityState) restoreMultiAgent := !eventPrepared.multiAgentV2Conflict && (eventPrepared.optimizeMultiAgentV2 || sess.isMultiAgentV2Optimized(conn)) payload = helps.RestoreCodexMultiAgentV2Response(payload, restoreMultiAgent) // Parse and invalidate replay for every rejected request, using the @@ -586,7 +585,6 @@ func (e *CodexWebsocketsExecutor) streamCodexDuplex( reporter.EnsurePublished(ctx) } } - payload = applyCodexIdentityExposeResponsePayload(payload, eventPrepared.identityState) if !send(cliproxyexecutor.StreamChunk{Payload: helps.EnsureResponsesUsageDetails(payload)}) { return } diff --git a/internal/runtime/executor/codex_websockets_duplex_rejection_test.go b/internal/runtime/executor/codex_websockets_duplex_rejection_test.go index d45ea5612..1408f700b 100644 --- a/internal/runtime/executor/codex_websockets_duplex_rejection_test.go +++ b/internal/runtime/executor/codex_websockets_duplex_rejection_test.go @@ -79,8 +79,6 @@ func TestCodexDuplexRejectedCreateMetadata(t *testing.T) { cfg := &config.Config{} cfg.Codex.ResponseSteering = true cfg.CodexResponseSteering = true - cfg.Codex.IdentityConfuse = true - cfg.Routing.SessionAffinity = true exec := NewCodexWebsocketsExecutor(cfg) exec.store = &codexWebsocketSessionStore{sessions: make(map[string]*codexWebsocketSession)} credential := &auth.Auth{ID: "metadata-account", Provider: "codex", Attributes: map[string]string{"api_key": "test", "base_url": upstream.URL, "websockets": "true"}} diff --git a/internal/runtime/executor/codex_websockets_duplex_successor_metadata_test.go b/internal/runtime/executor/codex_websockets_duplex_successor_metadata_test.go index 4c65be817..e971552ca 100644 --- a/internal/runtime/executor/codex_websockets_duplex_successor_metadata_test.go +++ b/internal/runtime/executor/codex_websockets_duplex_successor_metadata_test.go @@ -125,8 +125,6 @@ func TestCodexDuplexAutomaticSuccessorMetadata(t *testing.T) { cfg := &config.Config{} cfg.Codex.ResponseSteering = true cfg.CodexResponseSteering = true - cfg.Codex.IdentityConfuse = true - cfg.Routing.SessionAffinity = true executor := NewCodexWebsocketsExecutor(cfg) executor.store = &codexWebsocketSessionStore{sessions: make(map[string]*codexWebsocketSession)} credential := &auth.Auth{ID: t.Name(), Provider: "codex", Attributes: map[string]string{"api_key": "test", "base_url": upstream.URL, "websockets": "true"}} diff --git a/internal/runtime/executor/codex_websockets_execute.go b/internal/runtime/executor/codex_websockets_execute.go index 94a946a61..806b31288 100644 --- a/internal/runtime/executor/codex_websockets_execute.go +++ b/internal/runtime/executor/codex_websockets_execute.go @@ -83,14 +83,10 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut if errPromptCache != nil { return resp, errPromptCache } - clientBody := body - var identityState codexIdentityConfuseState - upstreamBody, identityState := applyCodexIdentityConfuseBody(e.cfg, auth, originalPayloadSource, body) - reporter.SetTranslatedReasoningEffort(clientBody, to.String()) + reporter.SetTranslatedReasoningEffort(body, to.String()) wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg, nativeRequest, opts.Headers) - applyCodexRoutingHint(ctx, wsHeaders, auth, baseModel, upstreamBody, opts.Headers) + applyCodexRoutingHint(ctx, wsHeaders, auth, baseModel, body, opts.Headers) applyModelHeaderOverrides(wsHeaders, baseModel) - applyCodexIdentityConfuseHeaders(wsHeaders, &identityState) var authID, authLabel, authType, authValue string if auth != nil { @@ -119,7 +115,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut sess = newEphemeralCodexWebsocketSession() } - wsReqBody := buildCodexWebsocketRequestBody(upstreamBody) + wsReqBody := buildCodexWebsocketRequestBody(body) wsReqLog := helps.UpstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", @@ -230,7 +226,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut } readCh = sess.activate(conn) restoreMultiAgentV2 = !multiAgentV2Conflict && (optimizeMultiAgentV2 || sess.isMultiAgentV2Optimized(conn)) - wsReqBodyRetry := buildCodexWebsocketRequestBody(upstreamBody) + wsReqBodyRetry := buildCodexWebsocketRequestBody(body) helps.RecordAPIWebsocketRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", @@ -305,7 +301,6 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut continue } observeCodexTokenEvent(reporter, payload) - payload = applyCodexIdentityConfuseResponsePayload(payload, identityState) helps.AppendCodexAPIWebsocketResponse(ctx, e.cfg, payload) helps.EmitWebSocketResponseEvent(ctx, opts, auth, e.Identifier(), req.Model, payload) payload = helps.RestoreCodexMultiAgentV2Response(payload, restoreMultiAgentV2) @@ -360,8 +355,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut reporter.EnsurePublished(ctx) } var param any - clientPayload := applyCodexIdentityExposeResponsePayload(payload, identityState) - out := sdktranslator.TranslateNonStream(ctx, to, responseFormat, req.Model, originalPayload, clientBody, clientPayload, ¶m) + out := sdktranslator.TranslateNonStream(ctx, to, responseFormat, req.Model, originalPayload, body, payload, ¶m) if responseFormat == sdktranslator.FormatOpenAIResponse { out = helps.EnsureResponsesUsageDetails(out) } diff --git a/internal/runtime/executor/codex_websockets_executor_test.go b/internal/runtime/executor/codex_websockets_executor_test.go index 0f2c04d56..47e957f3f 100644 --- a/internal/runtime/executor/codex_websockets_executor_test.go +++ b/internal/runtime/executor/codex_websockets_executor_test.go @@ -1589,120 +1589,6 @@ func TestApplyCodexPromptCacheHeadersClaudeRejectsBareUserID(t *testing.T) { } } -func TestApplyCodexWebsocketHeadersIdentityConfuseRemapsPromptCacheKey(t *testing.T) { - cfg := &config.Config{ - Routing: config.RoutingConfig{SessionAffinity: true}, - Codex: config.CodexConfig{IdentityConfuse: true}, - } - auth := &cliproxyauth.Auth{ID: "auth-ws-1", Provider: "codex"} - req := cliproxyexecutor.Request{ - Model: "gpt-5-codex", - Payload: []byte(`{"prompt_cache_key":"cache-ws-1","client_metadata":{"x-codex-installation-id":"install-ws-1"}}`), - } - - body, headers := applyCodexPromptCacheHeaders("openai-response", req, []byte(`{"model":"gpt-5-codex"}`)) - body, identityState := applyCodexIdentityConfuseBody(cfg, auth, req.Payload, body) - ctx := contextWithGinHeaders(map[string]string{ - "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, false) - 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) - } - if gotSession := headers["session_id"]; len(gotSession) != 1 || gotSession[0] != expectedPromptCacheKey { - t.Fatalf("session_id = %#v, want [%q]", gotSession, expectedPromptCacheKey) - } - if gotCanonicalSession := headers.Get("Session-Id"); gotCanonicalSession != "" { - t.Fatalf("Session-Id = %q, want empty", gotCanonicalSession) - } - if gotRequestID := headers.Get("X-Client-Request-Id"); gotRequestID != expectedPromptCacheKey { - t.Fatalf("X-Client-Request-Id = %q, want %q", gotRequestID, expectedPromptCacheKey) - } - if gotThreadID := headers.Get("Thread-Id"); gotThreadID != expectedPromptCacheKey { - t.Fatalf("Thread-Id = %q, want %q", gotThreadID, expectedPromptCacheKey) - } - if gotConversation := headers.Get("Conversation_id"); gotConversation != expectedPromptCacheKey { - t.Fatalf("Conversation_id = %q, want %q", gotConversation, expectedPromptCacheKey) - } - if gotWindowID := headers.Get("X-Codex-Window-Id"); gotWindowID != expectedPromptCacheKey+":0" { - t.Fatalf("X-Codex-Window-Id = %q, want %q", gotWindowID, expectedPromptCacheKey+":0") - } - 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 { - t.Fatalf("installation id = %q, want %q", gotInstallationID, expectedInstallationID) - } -} - -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"), - } - 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","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)) - } - 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)) - } -} - func TestBuildCodexResponsesWebsocketURLRequiresHTTPURL(t *testing.T) { if got, err := buildCodexResponsesWebsocketURL("https://example.com/backend/responses"); err != nil || got != "wss://example.com/backend/responses" { t.Fatalf("https URL = %q, %v; want wss URL", got, err) diff --git a/internal/runtime/executor/codex_websockets_stream.go b/internal/runtime/executor/codex_websockets_stream.go index e9911f330..1c603c780 100644 --- a/internal/runtime/executor/codex_websockets_stream.go +++ b/internal/runtime/executor/codex_websockets_stream.go @@ -43,10 +43,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr preserveNativeOutput := prepared.preserveNativeOutput originalPayload := prepared.originalPayload clientBody := prepared.clientBody - upstreamBody := prepared.upstreamBody wsURL := prepared.wsURL wsHeaders := prepared.wsHeaders - identityState := prepared.identityState replayScope := prepared.replayScope optimizeMultiAgentV2 := prepared.optimizeMultiAgentV2 multiAgentV2Conflict := prepared.multiAgentV2Conflict @@ -77,7 +75,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr } } - wsReqBody := buildCodexWebsocketRequestBody(upstreamBody) + wsReqBody := buildCodexWebsocketRequestBody(clientBody) wsReqLog := helps.UpstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", @@ -195,7 +193,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr } readCh = sess.activate(conn) restoreMultiAgentV2 = !multiAgentV2Conflict && (optimizeMultiAgentV2 || sess.isMultiAgentV2Optimized(conn)) - wsReqBodyRetry := buildCodexWebsocketRequestBody(upstreamBody) + wsReqBodyRetry := buildCodexWebsocketRequestBody(clientBody) helps.RecordAPIWebsocketRequest(ctx, e.cfg, helps.UpstreamRequestLog{ URL: wsURL, Method: "WEBSOCKET", @@ -354,7 +352,6 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr continue } observeCodexTokenEvent(reporter, payload) - payload = applyCodexIdentityConfuseResponsePayload(payload, identityState) helps.AppendCodexAPIWebsocketResponse(ctx, e.cfg, payload) helps.EmitWebSocketResponseEvent(ctx, opts, auth, e.Identifier(), req.Model, payload) payload = helps.RestoreCodexMultiAgentV2Response(payload, restoreMultiAgentV2) @@ -469,16 +466,14 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr if eventType == "response.completed" || eventType == "response.done" || eventType == "response.incomplete" { payload = completedPayload } - clientPayload := applyCodexIdentityExposeResponsePayload(payload, identityState) - downstreamPayload := helps.EnsureResponsesUsageDetails(clientPayload) + downstreamPayload := helps.EnsureResponsesUsageDetails(payload) currentChunks = [][]byte{downstreamPayload} } else { payload = normalizeCodexWebsocketCompletion(payload) if eventType == "response.completed" || eventType == "response.done" || eventType == "response.incomplete" { payload = completedPayload } - clientPayload := applyCodexIdentityExposeResponsePayload(payload, identityState) - line := encodeCodexWebsocketAsSSE(clientPayload) + line := encodeCodexWebsocketAsSSE(payload) currentChunks = helps.TranslateStreamWithClaudeInputTokens(ctx, to, responseFormat, req.Model, originalPayload, clientBody, line, ¶m, claudeInputTokens) } @@ -621,7 +616,6 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr continue } observeCodexTokenEvent(reporter, payload) - payload = applyCodexIdentityConfuseResponsePayload(payload, identityState) helps.AppendCodexAPIWebsocketResponse(ctx, e.cfg, payload) helps.EmitWebSocketResponseEvent(ctx, opts, auth, e.Identifier(), req.Model, payload) payload = helps.RestoreCodexMultiAgentV2Response(payload, restoreMultiAgentV2) @@ -701,12 +695,11 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr } } - clientPayload := applyCodexIdentityExposeResponsePayload(payload, identityState) if cliproxyexecutor.DownstreamWebsocket(ctx) { if eventType == "response.completed" || eventType == "response.done" || eventType == "response.incomplete" { - clientPayload = applyCodexIdentityExposeResponsePayload(completedPayload, identityState) + payload = completedPayload } - downstreamPayload := helps.EnsureResponsesUsageDetails(clientPayload) + downstreamPayload := helps.EnsureResponsesUsageDetails(payload) if !send(cliproxyexecutor.StreamChunk{Payload: downstreamPayload}) { terminateReason = "context_done" terminateErr = ctx.Err() @@ -723,8 +716,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr payload = completedPayload } eventType = gjson.GetBytes(payload, "type").String() - clientPayload = applyCodexIdentityExposeResponsePayload(payload, identityState) - line := encodeCodexWebsocketAsSSE(clientPayload) + line := encodeCodexWebsocketAsSSE(payload) chunks := helps.TranslateStreamWithClaudeInputTokens(ctx, to, responseFormat, req.Model, originalPayload, clientBody, line, ¶m, claudeInputTokens) for i := range chunks { if !send(cliproxyexecutor.StreamChunk{Payload: chunks[i]}) { @@ -751,10 +743,8 @@ type codexWebsocketPrepared struct { preserveNativeOutput bool originalPayload []byte clientBody []byte - upstreamBody []byte wsURL string wsHeaders http.Header - identityState codexIdentityConfuseState replayScope codexReasoningReplayScope optimizeMultiAgentV2 bool multiAgentV2Conflict bool @@ -812,13 +802,9 @@ func (e *CodexWebsocketsExecutor) prepareCodexWebsocketStream(ctx context.Contex if errPromptCache != nil { return nil, errPromptCache } - clientBody := body - var identityState codexIdentityConfuseState - upstreamBody, identityState := applyCodexIdentityConfuseBody(e.cfg, auth, originalPayloadSource, body) wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg, preserveNativeOutput, opts.Headers) - applyCodexRoutingHint(ctx, wsHeaders, auth, baseModel, upstreamBody, opts.Headers) + applyCodexRoutingHint(ctx, wsHeaders, auth, baseModel, body, opts.Headers) applyModelHeaderOverrides(wsHeaders, baseModel) - applyCodexIdentityConfuseHeaders(wsHeaders, &identityState) return &codexWebsocketPrepared{ from: from, @@ -826,11 +812,9 @@ func (e *CodexWebsocketsExecutor) prepareCodexWebsocketStream(ctx context.Contex to: to, preserveNativeOutput: preserveNativeOutput, originalPayload: originalPayload, - clientBody: clientBody, - upstreamBody: upstreamBody, + clientBody: body, wsURL: wsURL, wsHeaders: wsHeaders, - identityState: identityState, replayScope: replayScope, optimizeMultiAgentV2: optimizeMultiAgentV2, multiAgentV2Conflict: multiAgentV2Conflict, diff --git a/internal/watcher/diff/config_diff.go b/internal/watcher/diff/config_diff.go index 3de98ceea..eff31003d 100644 --- a/internal/watcher/diff/config_diff.go +++ b/internal/watcher/diff/config_diff.go @@ -137,9 +137,6 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string { changes = append(changes, fmt.Sprintf("antigravity.connection-pool.max-idle-conns-per-host: %s -> %s", oldMaxIdle, newMaxIdle)) } - if oldCfg.Codex.IdentityConfuse != newCfg.Codex.IdentityConfuse { - changes = append(changes, fmt.Sprintf("codex.identity-confuse: %t -> %t", oldCfg.Codex.IdentityConfuse, newCfg.Codex.IdentityConfuse)) - } if oldCfg.Codex.DisableCodexCloaking != newCfg.Codex.DisableCodexCloaking { changes = append(changes, fmt.Sprintf("codex.disable-codex-cloaking: %t -> %t", oldCfg.Codex.DisableCodexCloaking, newCfg.Codex.DisableCodexCloaking)) }