mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-04 08:27:01 +08:00
feat(executor): refine session and conversation header handling for Codex
- Updated session handling to replace `Session_id` and `Conversation_id` headers with new logic ensuring consistent use of `Cache.ID` and prompt keys. - Restored `Session_id` as a priority extraction source for `ExtractSessionID`. - Added tests to validate case-sensitive and case-insensitive headers, canonical account header usage, and session key preservation. - Removed legacy support for deprecated `Conversation_id` header to clean up API.
This commit is contained in:
@@ -931,6 +931,9 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form
|
||||
if err != nil {
|
||||
return nil, nil, codexIdentityConfuseState{}, err
|
||||
}
|
||||
if cache.ID != "" {
|
||||
httpReq.Header.Set("Session_id", cache.ID)
|
||||
}
|
||||
return httpReq, rawJSON, identityState, nil
|
||||
}
|
||||
|
||||
@@ -964,7 +967,6 @@ func applyCodexIdentityConfuseHeaders(headers http.Header, state *codexIdentityC
|
||||
if headers == nil {
|
||||
return
|
||||
}
|
||||
defer deleteDeprecatedCodexConversationHeader(headers)
|
||||
if state == nil || !state.enabled {
|
||||
return
|
||||
}
|
||||
@@ -977,6 +979,12 @@ func applyCodexIdentityConfuseHeaders(headers http.Header, state *codexIdentityC
|
||||
}
|
||||
|
||||
setHeaderCasePreserved(headers, "Session-Id", state.promptCacheKey)
|
||||
if headerValueCaseInsensitive(headers, "session_id") != "" {
|
||||
setHeaderCasePreserved(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")
|
||||
@@ -1072,6 +1080,10 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s
|
||||
cfgUserAgent, _ := codexHeaderDefaults(cfg, auth)
|
||||
ensureHeaderWithConfigPrecedence(r.Header, ginHeaders, "User-Agent", cfgUserAgent, codexUserAgent)
|
||||
|
||||
if strings.Contains(r.Header.Get("User-Agent"), "Mac OS") {
|
||||
misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString())
|
||||
}
|
||||
|
||||
if stream {
|
||||
r.Header.Set("Accept", "text/event-stream")
|
||||
} else {
|
||||
@@ -1090,19 +1102,18 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s
|
||||
} else if !isAPIKey {
|
||||
r.Header.Set("Originator", codexOriginator)
|
||||
}
|
||||
// if !isAPIKey {
|
||||
// if auth != nil && auth.Metadata != nil {
|
||||
// if accountID, ok := auth.Metadata["account_id"].(string); ok {
|
||||
// r.Header.Set("Chatgpt-Account-Id", accountID)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if !isAPIKey {
|
||||
if auth != nil && auth.Metadata != nil {
|
||||
if accountID, ok := auth.Metadata["account_id"].(string); ok {
|
||||
r.Header.Set("Chatgpt-Account-Id", accountID)
|
||||
}
|
||||
}
|
||||
}
|
||||
var attrs map[string]string
|
||||
if auth != nil {
|
||||
attrs = auth.Attributes
|
||||
}
|
||||
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
||||
deleteDeprecatedCodexConversationHeader(r.Header)
|
||||
}
|
||||
|
||||
func newCodexStatusErr(statusCode int, body []byte) statusErr {
|
||||
|
||||
@@ -47,8 +47,8 @@ func TestCodexExecutorCacheHelper_OpenAIChatCompletions_StablePromptCacheKeyFrom
|
||||
if gotConversation := httpReq.Header.Get("Conversation_id"); gotConversation != "" {
|
||||
t.Fatalf("Conversation_id = %q, want empty", gotConversation)
|
||||
}
|
||||
if gotSession := httpReq.Header.Get("Session_id"); gotSession != "" {
|
||||
t.Fatalf("Session_id = %q, want empty", gotSession)
|
||||
if gotSession := httpReq.Header.Get("Session_id"); gotSession != expectedKey {
|
||||
t.Fatalf("Session_id = %q, want %q", gotSession, expectedKey)
|
||||
}
|
||||
|
||||
httpReq2, _, _, err := executor.cacheHelper(ctx, sdktranslator.FromString("openai"), url, nil, req, req.Payload, rawJSON)
|
||||
@@ -119,8 +119,8 @@ func TestCodexExecutorCacheHelper_IdentityConfuseRemapsBodyAndHeaders(t *testing
|
||||
t.Fatalf("%s = %q, want %q", headerName, gotHeader, expectedPromptCacheKey)
|
||||
}
|
||||
}
|
||||
if gotSession := httpReq.Header.Get("Session_id"); gotSession != "" {
|
||||
t.Fatalf("Session_id = %q, want empty", gotSession)
|
||||
if gotSession := httpReq.Header.Get("Session_id"); gotSession != expectedPromptCacheKey {
|
||||
t.Fatalf("Session_id = %q, want %q", gotSession, expectedPromptCacheKey)
|
||||
}
|
||||
if gotWindow := httpReq.Header.Get("X-Codex-Window-Id"); gotWindow != expectedPromptCacheKey+":0" {
|
||||
t.Fatalf("X-Codex-Window-Id = %q, want %q", gotWindow, expectedPromptCacheKey+":0")
|
||||
@@ -137,6 +137,20 @@ func TestCodexExecutorCacheHelper_IdentityConfuseRemapsBodyAndHeaders(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCodexHeadersUsesAccountHeaderForOAuth(t *testing.T) {
|
||||
httpReq := httptest.NewRequest("POST", "https://example.com/responses", nil)
|
||||
auth := &cliproxyauth.Auth{
|
||||
Provider: "codex",
|
||||
Metadata: map[string]any{"account_id": "acct-1"},
|
||||
}
|
||||
|
||||
applyCodexHeaders(httpReq, auth, "oauth-token", true, nil)
|
||||
|
||||
if got := httpReq.Header.Get("Chatgpt-Account-Id"); got != "acct-1" {
|
||||
t.Fatalf("Chatgpt-Account-Id = %q, want acct-1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexIdentityConfuseKeepsClientBodySeparateFromUpstreamBody(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Routing: config.RoutingConfig{Strategy: "fill-first"},
|
||||
|
||||
@@ -857,6 +857,8 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto
|
||||
|
||||
if cache.ID != "" {
|
||||
rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", cache.ID)
|
||||
setHeaderCasePreserved(headers, "session_id", cache.ID)
|
||||
headers.Set("Conversation_id", cache.ID)
|
||||
}
|
||||
|
||||
return rawJSON, headers
|
||||
@@ -897,27 +899,30 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *
|
||||
betaHeader = codexResponsesWebsocketBetaHeaderValue
|
||||
}
|
||||
headers.Set("OpenAI-Beta", betaHeader)
|
||||
if strings.Contains(headers.Get("User-Agent"), "Mac OS") {
|
||||
ensureHeaderCasePreserved(headers, ginHeaders, "session_id", "", uuid.NewString())
|
||||
}
|
||||
ensureHeaderCasePreserved(headers, ginHeaders, "session_id", "", "")
|
||||
if originator := strings.TrimSpace(ginHeaders.Get("Originator")); originator != "" {
|
||||
headers.Set("Originator", originator)
|
||||
} else if !isAPIKey {
|
||||
headers.Set("Originator", codexOriginator)
|
||||
}
|
||||
// if !isAPIKey {
|
||||
// if auth != nil && auth.Metadata != nil {
|
||||
// if accountID, ok := auth.Metadata["account_id"].(string); ok {
|
||||
// if trimmed := strings.TrimSpace(accountID); trimmed != "" {
|
||||
// setHeaderCasePreserved(headers, "ChatGPT-Account-ID", trimmed)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if !isAPIKey {
|
||||
if auth != nil && auth.Metadata != nil {
|
||||
if accountID, ok := auth.Metadata["account_id"].(string); ok {
|
||||
if trimmed := strings.TrimSpace(accountID); trimmed != "" {
|
||||
setHeaderCasePreserved(headers, "ChatGPT-Account-ID", trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var attrs map[string]string
|
||||
if auth != nil {
|
||||
attrs = auth.Attributes
|
||||
}
|
||||
util.ApplyCustomHeadersFromAttrs(&http.Request{Header: headers}, attrs)
|
||||
deleteDeprecatedCodexConversationHeader(headers)
|
||||
|
||||
return headers
|
||||
}
|
||||
@@ -993,10 +998,6 @@ func deleteHeaderCaseInsensitive(headers http.Header, key string) {
|
||||
}
|
||||
}
|
||||
|
||||
func deleteDeprecatedCodexConversationHeader(headers http.Header) {
|
||||
deleteHeaderCaseInsensitive(headers, "Conversation_id")
|
||||
}
|
||||
|
||||
func codexHeaderDefaults(cfg *config.Config, auth *cliproxyauth.Auth) (string, string) {
|
||||
if cfg == nil || auth == nil {
|
||||
return "", ""
|
||||
|
||||
@@ -217,8 +217,11 @@ func TestApplyCodexWebsocketHeadersPassesThroughClientIdentityHeaders(t *testing
|
||||
if got := headers.Get("X-Client-Request-Id"); got != "019d2233-e240-7162-992d-38df0a2a0e0d" {
|
||||
t.Fatalf("X-Client-Request-Id = %s, want %s", got, "019d2233-e240-7162-992d-38df0a2a0e0d")
|
||||
}
|
||||
if got := headerValueCaseInsensitive(headers, "session_id"); got != "" {
|
||||
t.Fatalf("session_id = %q, want empty", got)
|
||||
if got := headerValueCaseInsensitive(headers, "session_id"); got != "legacy-session" {
|
||||
t.Fatalf("session_id = %s, want legacy-session", got)
|
||||
}
|
||||
if _, ok := headers["session_id"]; !ok {
|
||||
t.Fatalf("expected lowercase session_id header key, got %#v", headers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,16 +344,36 @@ func TestApplyCodexWebsocketHeadersPreservesExplicitAPIKeyUserAgent(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCodexPromptCacheHeadersDoesNotSetDeprecatedConversationHeader(t *testing.T) {
|
||||
func TestApplyCodexWebsocketHeadersUsesCanonicalAccountHeader(t *testing.T) {
|
||||
auth := &cliproxyauth.Auth{Provider: "codex", Metadata: map[string]any{"account_id": "acct-1"}}
|
||||
|
||||
headers := applyCodexWebsocketHeaders(context.Background(), http.Header{}, auth, "", nil)
|
||||
|
||||
if got := headerValueCaseInsensitive(headers, "ChatGPT-Account-ID"); got != "acct-1" {
|
||||
t.Fatalf("ChatGPT-Account-ID = %s, want acct-1", got)
|
||||
}
|
||||
values, ok := headers["ChatGPT-Account-ID"]
|
||||
if !ok {
|
||||
t.Fatalf("expected exact ChatGPT-Account-ID key, got %#v", headers)
|
||||
}
|
||||
if len(values) != 1 || values[0] != "acct-1" {
|
||||
t.Fatalf("ChatGPT-Account-ID values = %#v, want [acct-1]", values)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCodexPromptCacheHeadersSetsLowercaseSessionAndLegacyConversation(t *testing.T) {
|
||||
req := cliproxyexecutor.Request{Model: "gpt-5-codex", Payload: []byte(`{"prompt_cache_key":"cache-1"}`)}
|
||||
|
||||
_, headers := applyCodexPromptCacheHeaders("openai-response", req, []byte(`{"model":"gpt-5-codex"}`))
|
||||
|
||||
if got := headerValueCaseInsensitive(headers, "session_id"); got != "" {
|
||||
t.Fatalf("session_id = %q, want empty", got)
|
||||
if got := headerValueCaseInsensitive(headers, "session_id"); got != "cache-1" {
|
||||
t.Fatalf("session_id = %s, want cache-1", got)
|
||||
}
|
||||
if got := headers.Get("Conversation_id"); got != "" {
|
||||
t.Fatalf("Conversation_id = %q, want empty", got)
|
||||
if _, ok := headers["session_id"]; !ok {
|
||||
t.Fatalf("expected lowercase session_id key, got %#v", headers)
|
||||
}
|
||||
if got := headers.Get("Conversation_id"); got != "cache-1" {
|
||||
t.Fatalf("Conversation_id = %s, want cache-1", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,8 +402,8 @@ func TestApplyCodexWebsocketHeadersIdentityConfuseRemapsPromptCacheKey(t *testin
|
||||
if gotKey := gjson.GetBytes(body, "prompt_cache_key").String(); gotKey != expectedPromptCacheKey {
|
||||
t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedPromptCacheKey)
|
||||
}
|
||||
if gotSession := headerValueCaseInsensitive(headers, "session_id"); gotSession != "" {
|
||||
t.Fatalf("session_id = %q, want empty", gotSession)
|
||||
if gotSession := headerValueCaseInsensitive(headers, "session_id"); gotSession != expectedPromptCacheKey {
|
||||
t.Fatalf("session_id = %q, want %q", gotSession, expectedPromptCacheKey)
|
||||
}
|
||||
if gotRequestID := headers.Get("X-Client-Request-Id"); gotRequestID != expectedPromptCacheKey {
|
||||
t.Fatalf("X-Client-Request-Id = %q, want %q", gotRequestID, expectedPromptCacheKey)
|
||||
@@ -388,8 +411,8 @@ func TestApplyCodexWebsocketHeadersIdentityConfuseRemapsPromptCacheKey(t *testin
|
||||
if gotThreadID := headers.Get("Thread-Id"); gotThreadID != expectedPromptCacheKey {
|
||||
t.Fatalf("Thread-Id = %q, want %q", gotThreadID, expectedPromptCacheKey)
|
||||
}
|
||||
if gotConversation := headers.Get("Conversation_id"); gotConversation != "" {
|
||||
t.Fatalf("Conversation_id = %q, want empty", gotConversation)
|
||||
if 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")
|
||||
|
||||
Reference in New Issue
Block a user