feat(executor): implement agent-scoped cache key isolation and enhance replay handling

- Introduced agent-specific cache key isolation to ensure distinct prompt caching across agents.
- Improved reasoning replay handling for Claude models, including cumulative tool turn restoration and validation.
- Enhanced HTTP and WebSocket session consistency during execution scope and cache replay synchronization.
- Added new unit tests and extended coverage for prompt caching, agent isolation, and replay cache functionality.

Closes: #4352
This commit is contained in:
Luis Pater
2026-07-18 02:18:09 +08:00
parent da087d7fad
commit baffbe2cc7
12 changed files with 1285 additions and 98 deletions

View File

@@ -275,6 +275,10 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
body = ensureImageGenerationTool(body, baseModel, auth, opts.Headers)
}
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex websockets executor", body)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return resp, errReplay
}
httpURL := strings.TrimSuffix(baseURL, "/") + "/responses"
wsURL, err := buildCodexResponsesWebsocketURL(httpURL)
@@ -282,7 +286,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
return resp, err
}
body, wsHeaders, errPromptCache := applyCodexPromptCacheHeadersWithContext(ctx, from, req, body)
body, wsHeaders, errPromptCache := applyCodexPromptCacheHeadersWithContext(ctx, from, req, body, opts.Headers)
if errPromptCache != nil {
return resp, errPromptCache
}
@@ -405,6 +409,8 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
}
}
outputItemsByIndex := make(map[int64][]byte)
var outputItemsFallback [][]byte
for {
if ctx != nil && ctx.Err() != nil {
return resp, ctx.Err()
@@ -439,13 +445,27 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
if sess != nil {
e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr)
}
if errClearReplay := clearCodexReasoningReplayOnWebsocketError(ctx, replayScope, payload); errClearReplay != nil {
return resp, errClearReplay
}
helps.RecordAPIWebsocketError(ctx, e.cfg, "upstream_error", wsErr)
return resp, wsErr
}
if streamErr, terminalBody, ok := codexTerminalFailureErr(payload); ok {
if errClearReplay := clearCodexReasoningReplayOnInvalidSignature(ctx, replayScope, streamErr.StatusCode(), terminalBody); errClearReplay != nil {
return resp, errClearReplay
}
return resp, streamErr
}
payload = normalizeCodexWebsocketCompletion(payload)
eventType := gjson.GetBytes(payload, "type").String()
if eventType == "response.completed" {
switch eventType {
case "response.output_item.done":
collectCodexOutputItemDone(payload, outputItemsByIndex, &outputItemsFallback)
case "response.completed":
payload = patchCodexCompletedOutput(payload, outputItemsByIndex, outputItemsFallback)
cacheCodexReasoningReplayFromCompleted(replayScope, payload)
if detail, ok := helps.ParseCodexUsage(payload); ok {
reporter.Publish(ctx, detail)
}
@@ -479,11 +499,12 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
from := opts.SourceFormat
responseFormat := cliproxyexecutor.ResponseFormatOrSource(opts)
to := sdktranslator.FromString("codex")
body := req.Payload
userPayload := req.Payload
originalPayloadSource := req.Payload
if len(opts.OriginalRequest) > 0 {
userPayload = opts.OriginalRequest
originalPayloadSource = opts.OriginalRequest
}
originalPayload := originalPayloadSource
originalTranslated, body := translateCodexRequestPair(from, to, baseModel, originalPayload, req.Payload, true)
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
if err != nil {
@@ -492,13 +513,17 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
requestPath := helps.PayloadRequestPath(opts)
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, body, requestedModel, requestPath, opts.Headers)
body = helps.ApplyPayloadConfigWithRequest(e.cfg, baseModel, to.String(), from.String(), "", body, originalTranslated, requestedModel, requestPath, opts.Headers)
body, _ = sjson.SetBytes(body, "model", baseModel)
body = normalizeCodexInstructions(body)
if e.cfg == nil || e.cfg.DisableImageGeneration == config.DisableImageGenerationOff {
body = ensureImageGenerationTool(body, baseModel, auth, opts.Headers)
}
body = sanitizeOpenAIResponsesReasoningEncryptedContent(ctx, "codex websockets executor", body)
body, replayScope, errReplay := applyCodexReasoningReplayCacheRequired(ctx, from, req, opts, body)
if errReplay != nil {
return nil, errReplay
}
httpURL := strings.TrimSuffix(baseURL, "/") + "/responses"
wsURL, err := buildCodexResponsesWebsocketURL(httpURL)
@@ -506,13 +531,13 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
return nil, err
}
body, wsHeaders, errPromptCache := applyCodexPromptCacheHeadersWithContext(ctx, from, req, body)
body, wsHeaders, errPromptCache := applyCodexPromptCacheHeadersWithContext(ctx, from, req, body, opts.Headers)
if errPromptCache != nil {
return nil, errPromptCache
}
clientBody := body
var identityState codexIdentityConfuseState
upstreamBody, identityState := applyCodexIdentityConfuseBody(e.cfg, auth, userPayload, body)
upstreamBody, identityState := applyCodexIdentityConfuseBody(e.cfg, auth, originalPayloadSource, body)
reporter.SetTranslatedReasoningEffort(clientBody, to.String())
wsHeaders = applyCodexWebsocketHeaders(ctx, wsHeaders, auth, apiKey, e.cfg)
applyModelHeaderOverrides(wsHeaders, baseModel)
@@ -659,6 +684,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
}
var param any
outputItemsByIndex := make(map[int64][]byte)
var outputItemsFallback [][]byte
for {
if ctx != nil && ctx.Err() != nil {
terminateReason = "context_done"
@@ -709,24 +736,54 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
if wsErr, ok := parseCodexWebsocketError(payload); ok {
terminateReason = "upstream_error"
terminateErr = wsErr
helps.RecordAPIWebsocketError(ctx, e.cfg, "upstream_error", wsErr)
reporter.PublishFailure(ctx, wsErr)
if sess != nil {
e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr)
}
if errClearReplay := clearCodexReasoningReplayOnWebsocketError(ctx, replayScope, payload); errClearReplay != nil {
terminateErr = errClearReplay
helps.RecordAPIWebsocketError(ctx, e.cfg, "replay_clear_error", errClearReplay)
reporter.PublishFailure(ctx, errClearReplay)
_ = send(cliproxyexecutor.StreamChunk{Err: errClearReplay})
return
}
helps.RecordAPIWebsocketError(ctx, e.cfg, "upstream_error", wsErr)
reporter.PublishFailure(ctx, wsErr)
_ = send(cliproxyexecutor.StreamChunk{Err: wsErr})
return
}
if streamErr, terminalBody, ok := codexTerminalFailureErr(payload); ok {
terminateReason = "upstream_error"
terminateErr = streamErr
if errClearReplay := clearCodexReasoningReplayOnInvalidSignature(ctx, replayScope, streamErr.StatusCode(), terminalBody); errClearReplay != nil {
terminateErr = errClearReplay
helps.RecordAPIWebsocketError(ctx, e.cfg, "replay_clear_error", errClearReplay)
reporter.PublishFailure(ctx, errClearReplay)
_ = send(cliproxyexecutor.StreamChunk{Err: errClearReplay})
return
}
helps.RecordAPIWebsocketError(ctx, e.cfg, "upstream_error", streamErr)
reporter.PublishFailure(ctx, streamErr)
_ = send(cliproxyexecutor.StreamChunk{Err: streamErr})
return
}
eventType := gjson.GetBytes(payload, "type").String()
isTerminalEvent := eventType == "response.completed" || eventType == "response.done" || eventType == "error"
if eventType == "response.output_item.done" {
collectCodexOutputItemDone(payload, outputItemsByIndex, &outputItemsFallback)
}
completedPayload := payload
if eventType == "response.completed" || eventType == "response.done" {
completedPayload = normalizeCodexWebsocketCompletion(completedPayload)
completedPayload = patchCodexCompletedOutput(completedPayload, outputItemsByIndex, outputItemsFallback)
cacheCodexReasoningReplayFromCompleted(replayScope, completedPayload)
if detail, ok := helps.ParseCodexUsage(completedPayload); ok {
reporter.Publish(ctx, detail)
}
}
clientPayload := applyCodexIdentityExposeResponsePayload(payload, identityState)
if cliproxyexecutor.DownstreamWebsocket(ctx) {
if eventType == "response.completed" || eventType == "response.done" {
if detail, ok := helps.ParseCodexUsage(payload); ok {
reporter.Publish(ctx, detail)
}
}
if !send(cliproxyexecutor.StreamChunk{Payload: clientPayload}) {
terminateReason = "context_done"
terminateErr = ctx.Err()
@@ -739,16 +796,13 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
}
payload = normalizeCodexWebsocketCompletion(payload)
eventType = gjson.GetBytes(payload, "type").String()
if eventType == "response.completed" || eventType == "response.done" {
if detail, ok := helps.ParseCodexUsage(payload); ok {
reporter.Publish(ctx, detail)
}
payload = completedPayload
}
eventType = gjson.GetBytes(payload, "type").String()
clientPayload = applyCodexIdentityExposeResponsePayload(payload, identityState)
line := encodeCodexWebsocketAsSSE(clientPayload)
chunks := sdktranslator.TranslateStream(ctx, to, responseFormat, req.Model, clientBody, clientBody, line, &param)
chunks := sdktranslator.TranslateStream(ctx, to, responseFormat, req.Model, originalPayload, clientBody, line, &param)
for i := range chunks {
if !send(cliproxyexecutor.StreamChunk{Payload: chunks[i]}) {
terminateReason = "context_done"
@@ -941,15 +995,23 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto
return body, headers
}
func applyCodexPromptCacheHeadersWithContext(ctx context.Context, from sdktranslator.Format, req cliproxyexecutor.Request, rawJSON []byte) ([]byte, http.Header, error) {
func applyCodexPromptCacheHeadersWithContext(ctx context.Context, from sdktranslator.Format, req cliproxyexecutor.Request, rawJSON []byte, headerSets ...http.Header) ([]byte, http.Header, error) {
headers := http.Header{}
if len(rawJSON) == 0 {
return rawJSON, headers, nil
}
var requestHeaders http.Header
if len(headerSets) > 0 {
requestHeaders = headerSets[0]
}
var cache helps.CodexCache
if sourceFormatEqual(from, sdktranslator.FormatClaude) {
cached, ok, errCache := helps.ClaudeCodePromptCache(ctx, req.Model, req.Payload, nil)
modelName := strings.TrimSpace(gjson.GetBytes(rawJSON, "model").String())
if modelName == "" {
modelName = thinking.ParseSuffix(req.Model).ModelName
}
cached, ok, errCache := helps.ClaudeCodePromptCache(ctx, modelName, req.Payload, requestHeaders)
if errCache != nil {
return nil, nil, errCache
}
@@ -1271,6 +1333,17 @@ func parseCodexWebsocketError(payload []byte) (error, bool) {
}, true
}
func clearCodexReasoningReplayOnWebsocketError(ctx context.Context, scope codexReasoningReplayScope, payload []byte) error {
status := int(gjson.GetBytes(payload, "status").Int())
if status == 0 {
status = int(gjson.GetBytes(payload, "status_code").Int())
}
if status <= 0 {
return nil
}
return clearCodexReasoningReplayOnInvalidSignature(ctx, scope, status, buildCodexWebsocketErrorPayload(payload, status))
}
func buildCodexWebsocketErrorPayload(payload []byte, status int) []byte {
out := []byte(`{}`)
out, _ = sjson.SetBytes(out, "status", status)