-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
fix(codex): restore prompt cache continuity for Codex requests #2374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
511b8a9
62b17f4
26eca8b
4c4cbd4
6962e09
35f158d
79755e7
e5d3541
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package executor | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net/http" | ||
| "strings" | ||
|
|
||
| "github.com/google/uuid" | ||
| cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | ||
| cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" | ||
| log "github.com/sirupsen/logrus" | ||
| "github.com/tidwall/gjson" | ||
| "github.com/tidwall/sjson" | ||
| ) | ||
|
|
||
| type codexContinuity struct { | ||
| Key string | ||
| Source string | ||
| } | ||
|
|
||
| func metadataString(meta map[string]any, key string) string { | ||
| if len(meta) == 0 { | ||
| return "" | ||
| } | ||
| raw, ok := meta[key] | ||
| if !ok || raw == nil { | ||
| return "" | ||
| } | ||
| switch v := raw.(type) { | ||
| case string: | ||
| return strings.TrimSpace(v) | ||
| case []byte: | ||
| return strings.TrimSpace(string(v)) | ||
| default: | ||
| return "" | ||
| } | ||
| } | ||
|
|
||
| func principalString(raw any) string { | ||
| switch v := raw.(type) { | ||
| case string: | ||
| return strings.TrimSpace(v) | ||
| case fmt.Stringer: | ||
| return strings.TrimSpace(v.String()) | ||
| default: | ||
| return strings.TrimSpace(fmt.Sprintf("%v", raw)) | ||
| } | ||
| } | ||
|
|
||
| func resolveCodexContinuity(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) codexContinuity { | ||
| if promptCacheKey := strings.TrimSpace(gjson.GetBytes(req.Payload, "prompt_cache_key").String()); promptCacheKey != "" { | ||
| return codexContinuity{Key: promptCacheKey, Source: "prompt_cache_key"} | ||
| } | ||
| if executionSession := metadataString(opts.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey); executionSession != "" { | ||
| return codexContinuity{Key: executionSession, Source: "execution_session"} | ||
| } | ||
| if ginCtx := ginContextFrom(ctx); ginCtx != nil { | ||
| if ginCtx.Request != nil { | ||
| if v := strings.TrimSpace(ginCtx.GetHeader("Idempotency-Key")); v != "" { | ||
| return codexContinuity{Key: v, Source: "idempotency_key"} | ||
| } | ||
| } | ||
| if v, exists := ginCtx.Get("apiKey"); exists && v != nil { | ||
| if trimmed := principalString(v); trimmed != "" { | ||
| return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+trimmed)).String(), Source: "client_principal"} | ||
| } | ||
| } | ||
| } | ||
| if auth != nil { | ||
| if authID := strings.TrimSpace(auth.ID); authID != "" { | ||
| return codexContinuity{Key: uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:auth:"+authID)).String(), Source: "auth_id"} | ||
| } | ||
| } | ||
| return codexContinuity{} | ||
| } | ||
|
|
||
| func applyCodexContinuityBody(rawJSON []byte, continuity codexContinuity) []byte { | ||
| if continuity.Key == "" { | ||
| return rawJSON | ||
| } | ||
| rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", continuity.Key) | ||
| return rawJSON | ||
| } | ||
|
|
||
| func applyCodexContinuityHeaders(headers http.Header, continuity codexContinuity) { | ||
| if headers == nil || continuity.Key == "" { | ||
| return | ||
| } | ||
| headers.Set("session_id", continuity.Key) | ||
| } | ||
|
|
||
| func logCodexRequestDiagnostics(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, headers http.Header, body []byte, continuity codexContinuity) { | ||
| if !log.IsLevelEnabled(log.DebugLevel) { | ||
| return | ||
| } | ||
| entry := logWithRequestID(ctx) | ||
| authID := "" | ||
| authFile := "" | ||
| if auth != nil { | ||
| authID = strings.TrimSpace(auth.ID) | ||
| authFile = strings.TrimSpace(auth.FileName) | ||
| } | ||
| selectedAuthID := metadataString(opts.Metadata, cliproxyexecutor.SelectedAuthMetadataKey) | ||
| executionSessionID := metadataString(opts.Metadata, cliproxyexecutor.ExecutionSessionMetadataKey) | ||
| entry.Debugf( | ||
| "codex request diagnostics auth_id=%s selected_auth_id=%s auth_file=%s exec_session=%s continuity_source=%s session_id=%s prompt_cache_key=%s prompt_cache_retention=%s store=%t has_instructions=%t reasoning_effort=%s reasoning_summary=%s chatgpt_account_id=%t originator=%s model=%s source_format=%s", | ||
| authID, | ||
| selectedAuthID, | ||
| authFile, | ||
| executionSessionID, | ||
| continuity.Source, | ||
| strings.TrimSpace(headers.Get("session_id")), | ||
| gjson.GetBytes(body, "prompt_cache_key").String(), | ||
| gjson.GetBytes(body, "prompt_cache_retention").String(), | ||
| gjson.GetBytes(body, "store").Bool(), | ||
| gjson.GetBytes(body, "instructions").Exists(), | ||
| gjson.GetBytes(body, "reasoning.effort").String(), | ||
| gjson.GetBytes(body, "reasoning.summary").String(), | ||
| strings.TrimSpace(headers.Get("Chatgpt-Account-Id")) != "", | ||
| strings.TrimSpace(headers.Get("Originator")), | ||
| req.Model, | ||
| opts.SourceFormat.String(), | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,18 +111,18 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re | |
| body, _ = sjson.SetBytes(body, "model", baseModel) | ||
| body, _ = sjson.SetBytes(body, "stream", true) | ||
| body, _ = sjson.DeleteBytes(body, "previous_response_id") | ||
| body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") | ||
| body, _ = sjson.DeleteBytes(body, "safety_identifier") | ||
| if !gjson.GetBytes(body, "instructions").Exists() { | ||
| body, _ = sjson.SetBytes(body, "instructions", "") | ||
| } | ||
|
|
||
| url := strings.TrimSuffix(baseURL, "/") + "/responses" | ||
| httpReq, err := e.cacheHelper(ctx, from, url, req, body) | ||
| httpReq, continuity, err := e.cacheHelper(ctx, auth, from, url, req, opts, body) | ||
| if err != nil { | ||
| return resp, err | ||
| } | ||
| applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg) | ||
| logCodexRequestDiagnostics(ctx, auth, req, opts, httpReq.Header, body, continuity) | ||
| var authID, authLabel, authType, authValue string | ||
| if auth != nil { | ||
| authID = auth.ID | ||
|
|
@@ -222,11 +222,12 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A | |
| body, _ = sjson.DeleteBytes(body, "stream") | ||
|
|
||
| url := strings.TrimSuffix(baseURL, "/") + "/responses/compact" | ||
| httpReq, err := e.cacheHelper(ctx, from, url, req, body) | ||
| httpReq, continuity, err := e.cacheHelper(ctx, auth, from, url, req, opts, body) | ||
| if err != nil { | ||
| return resp, err | ||
| } | ||
| applyCodexHeaders(httpReq, auth, apiKey, false, e.cfg) | ||
| logCodexRequestDiagnostics(ctx, auth, req, opts, httpReq.Header, body, continuity) | ||
| var authID, authLabel, authType, authValue string | ||
| if auth != nil { | ||
| authID = auth.ID | ||
|
|
@@ -309,19 +310,19 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au | |
| requestedModel := payloadRequestedModel(opts, req.Model) | ||
| body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel) | ||
| body, _ = sjson.DeleteBytes(body, "previous_response_id") | ||
| body, _ = sjson.DeleteBytes(body, "prompt_cache_retention") | ||
| body, _ = sjson.DeleteBytes(body, "safety_identifier") | ||
| body, _ = sjson.SetBytes(body, "model", baseModel) | ||
| if !gjson.GetBytes(body, "instructions").Exists() { | ||
| body, _ = sjson.SetBytes(body, "instructions", "") | ||
| } | ||
|
|
||
| url := strings.TrimSuffix(baseURL, "/") + "/responses" | ||
| httpReq, err := e.cacheHelper(ctx, from, url, req, body) | ||
| httpReq, continuity, err := e.cacheHelper(ctx, auth, from, url, req, opts, body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| applyCodexHeaders(httpReq, auth, apiKey, true, e.cfg) | ||
| logCodexRequestDiagnostics(ctx, auth, req, opts, httpReq.Header, body, continuity) | ||
| var authID, authLabel, authType, authValue string | ||
| if auth != nil { | ||
| authID = auth.ID | ||
|
|
@@ -596,8 +597,9 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (* | |
| return auth, nil | ||
| } | ||
|
|
||
| func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, req cliproxyexecutor.Request, rawJSON []byte) (*http.Request, error) { | ||
| func (e *CodexExecutor) cacheHelper(ctx context.Context, auth *cliproxyauth.Auth, from sdktranslator.Format, url string, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, rawJSON []byte) (*http.Request, codexContinuity, error) { | ||
| var cache codexCache | ||
| continuity := codexContinuity{} | ||
| if from == "claude" { | ||
| userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id") | ||
| if userIDResult.Exists() { | ||
|
|
@@ -610,30 +612,26 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form | |
| } | ||
| setCodexCache(key, cache) | ||
| } | ||
| continuity = codexContinuity{Key: cache.ID, Source: "claude_user_cache"} | ||
| } | ||
| } else if from == "openai-response" { | ||
| promptCacheKey := gjson.GetBytes(req.Payload, "prompt_cache_key") | ||
| if promptCacheKey.Exists() { | ||
| cache.ID = promptCacheKey.String() | ||
| continuity = codexContinuity{Key: cache.ID, Source: "prompt_cache_key"} | ||
| } | ||
| } else if from == "openai" { | ||
| if apiKey := strings.TrimSpace(apiKeyFromContext(ctx)); apiKey != "" { | ||
| cache.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+apiKey)).String() | ||
| } | ||
| continuity = resolveCodexContinuity(ctx, auth, req, opts) | ||
| cache.ID = continuity.Key | ||
| } | ||
|
|
||
| if cache.ID != "" { | ||
| rawJSON, _ = sjson.SetBytes(rawJSON, "prompt_cache_key", cache.ID) | ||
| } | ||
| rawJSON = applyCodexContinuityBody(rawJSON, continuity) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎.
Comment on lines
+624
to
+628
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Claude branch still computes Useful? React with 👍 / 👎. |
||
| httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(rawJSON)) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if cache.ID != "" { | ||
| httpReq.Header.Set("Conversation_id", cache.ID) | ||
| httpReq.Header.Set("Session_id", cache.ID) | ||
| return nil, continuity, err | ||
| } | ||
| return httpReq, nil | ||
| applyCodexContinuityHeaders(httpReq.Header, continuity) | ||
| return httpReq, continuity, nil | ||
| } | ||
|
|
||
| func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool, cfg *config.Config) { | ||
|
|
@@ -646,7 +644,7 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s | |
| } | ||
|
|
||
| misc.EnsureHeader(r.Header, ginHeaders, "Version", "") | ||
| misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString()) | ||
| misc.EnsureHeader(r.Header, ginHeaders, "session_id", uuid.NewString()) | ||
| misc.EnsureHeader(r.Header, ginHeaders, "X-Codex-Turn-Metadata", "") | ||
| misc.EnsureHeader(r.Header, ginHeaders, "X-Client-Request-Id", "") | ||
| cfgUserAgent, _ := codexHeaderDefaults(cfg, auth) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
resolveCodexContinuity,Idempotency-Keyis chosen before stable identities (client_principal/auth_id), so any client that sends a different idempotency key per request will get a differentprompt_cache_key/session_idon every turn. That breaks the continuity this change is trying to restore (cache/session reuse across turns) and can regress performance for common retry-safe clients that auto-generate per-request idempotency keys. Consider demoting this source below stable caller/auth-derived keys (or only using it for explicit retry contexts).Useful? React with 👍 / 👎.