mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-07 00:24:17 +08:00
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:
@@ -307,3 +307,62 @@ func TestCodexExecutorCacheHelper_ClaudeUsesSessionHeader(t *testing.T) {
|
||||
t.Fatalf("same Claude Code session header produced different prompt_cache_key: first=%q second=%q", firstKey, secondKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexExecutorCacheHelper_ClaudeAgentScopeUsesResolvedModelAcrossHTTPAndWebsocket(t *testing.T) {
|
||||
executor := &CodexExecutor{}
|
||||
url := "https://example.com/responses"
|
||||
req := cliproxyexecutor.Request{
|
||||
Model: "requested-alias-high",
|
||||
Payload: []byte(`{"model":"requested-alias","messages":[{"role":"user","content":"hello"}]}`),
|
||||
}
|
||||
rootHeaders := http.Header{}
|
||||
rootHeaders.Set(helps.ClaudeCodeSessionHeader, "resolved-model-session")
|
||||
childHeaders := rootHeaders.Clone()
|
||||
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)
|
||||
if errRoot != nil {
|
||||
t.Fatalf("root cacheHelper error: %v", errRoot)
|
||||
}
|
||||
rootBody, errReadRoot := io.ReadAll(rootRequest.Body)
|
||||
if errReadRoot != nil {
|
||||
t.Fatalf("read root body: %v", errReadRoot)
|
||||
}
|
||||
rootKey := gjson.GetBytes(rootBody, "prompt_cache_key").String()
|
||||
|
||||
childRequest, _, _, errChild := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), url, nil, req, req.Payload, rawJSON, childHeaders)
|
||||
if errChild != nil {
|
||||
t.Fatalf("child cacheHelper error: %v", errChild)
|
||||
}
|
||||
childBody, errReadChild := io.ReadAll(childRequest.Body)
|
||||
if errReadChild != nil {
|
||||
t.Fatalf("read child body: %v", errReadChild)
|
||||
}
|
||||
childKey := gjson.GetBytes(childBody, "prompt_cache_key").String()
|
||||
if rootKey == "" || childKey == "" || rootKey == childKey {
|
||||
t.Fatalf("agent prompt keys are not isolated: root=%q child=%q", rootKey, childKey)
|
||||
}
|
||||
|
||||
aliasReq := req
|
||||
aliasReq.Model = "another-local-alias-low"
|
||||
aliasRequest, _, _, errAlias := executor.cacheHelper(context.Background(), sdktranslator.FromString("claude"), url, nil, aliasReq, aliasReq.Payload, rawJSON, childHeaders)
|
||||
if errAlias != nil {
|
||||
t.Fatalf("alias cacheHelper error: %v", errAlias)
|
||||
}
|
||||
aliasBody, errReadAlias := io.ReadAll(aliasRequest.Body)
|
||||
if errReadAlias != nil {
|
||||
t.Fatalf("read alias body: %v", errReadAlias)
|
||||
}
|
||||
if aliasKey := gjson.GetBytes(aliasBody, "prompt_cache_key").String(); aliasKey != childKey {
|
||||
t.Fatalf("resolved model key fragmented by request alias: first=%q alias=%q", childKey, aliasKey)
|
||||
}
|
||||
|
||||
websocketBody, _, errWebsocket := applyCodexPromptCacheHeadersWithContext(context.Background(), sdktranslator.FromString("claude"), aliasReq, rawJSON, childHeaders)
|
||||
if errWebsocket != nil {
|
||||
t.Fatalf("websocket prompt cache error: %v", errWebsocket)
|
||||
}
|
||||
if websocketKey := gjson.GetBytes(websocketBody, "prompt_cache_key").String(); websocketKey != childKey {
|
||||
t.Fatalf("HTTP/WebSocket prompt keys differ: http=%q websocket=%q", childKey, websocketKey)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user