mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-18 05:29:00 +08:00
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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"}}
|
||||
|
||||
@@ -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"}}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user