140 lines
5.2 KiB
Go
140 lines
5.2 KiB
Go
package executor
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"strings"
|
|
|
|
internalcache "github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
// claudeThinkingReplayScope reuses the bounded replay state shape shared with Kimi.
|
|
type claudeThinkingReplayScope = kimiThinkingReplayScope
|
|
|
|
func claudeThinkingReplayEnabled(auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) bool {
|
|
if auth == nil || !sourceFormatEqual(opts.SourceFormat, sdktranslator.FormatClaude) {
|
|
return false
|
|
}
|
|
if !strings.EqualFold(strings.TrimSpace(auth.Provider), "claude") || auth.AuthKind() != cliproxyauth.AuthKindAPIKey {
|
|
return false
|
|
}
|
|
if !helps.APIKeyModelIsCompat(req) {
|
|
return false
|
|
}
|
|
apiKey, _ := claudeCreds(auth)
|
|
return strings.TrimSpace(apiKey) != "" && !isClaudeOAuthToken(apiKey)
|
|
}
|
|
|
|
// A missing session identity intentionally disables replay instead of sharing hidden reasoning across callers.
|
|
func claudeThinkingReplayScopeFromRequest(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) claudeThinkingReplayScope {
|
|
sessionKey := codexReasoningReplaySessionKey(ctx, sdktranslator.FormatClaude, req, opts, req.Payload)
|
|
sessionKey = xaiReasoningReplayIsolateSessionKey(ctx, sessionKey)
|
|
return claudeThinkingReplayScope{
|
|
modelFamily: claudeThinkingReplayModelFamily(auth, req.Model),
|
|
sessionKey: sessionKey,
|
|
}
|
|
}
|
|
|
|
func claudeThinkingReplayModelFamily(auth *cliproxyauth.Auth, model string) string {
|
|
baseModel := thinking.ParseSuffix(strings.TrimSpace(model)).ModelName
|
|
if baseModel == "" {
|
|
return ""
|
|
}
|
|
identity := ""
|
|
if auth != nil {
|
|
identity = strings.TrimSpace(auth.ID)
|
|
if identity == "" {
|
|
apiKey, baseURL := claudeCreds(auth)
|
|
identity = strings.TrimSpace(baseURL)
|
|
if identity == "" {
|
|
identity = strings.TrimSpace(apiKey)
|
|
}
|
|
}
|
|
}
|
|
if identity == "" {
|
|
return "claude:" + baseModel
|
|
}
|
|
sum := sha256.Sum256([]byte(identity))
|
|
return "claude:" + hex.EncodeToString(sum[:8]) + ":" + baseModel
|
|
}
|
|
|
|
func prepareClaudeThinkingReplayRequest(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Request, claudeThinkingReplayScope) {
|
|
scope := claudeThinkingReplayScopeFromRequest(ctx, auth, req, opts)
|
|
if !scope.valid() {
|
|
return req, scope
|
|
}
|
|
contents, snapshot, found, errGet := internalcache.GetClaudeThinkingReplayWithSnapshotRequired(ctx, scope.modelFamily, scope.sessionKey)
|
|
scope.snapshot = snapshot
|
|
scope.cacheReady = errGet == nil
|
|
if errGet != nil {
|
|
log.Warnf("claude compatible thinking replay cache read failed: %v", errGet)
|
|
return req, scope
|
|
}
|
|
if !found {
|
|
return req, scope
|
|
}
|
|
updated, restored := restoreClaudeThinkingReplayContents(req.Payload, contents)
|
|
if restored {
|
|
req.Payload = updated
|
|
scope.replayApplied = true
|
|
}
|
|
return req, scope
|
|
}
|
|
|
|
func restoreClaudeThinkingReplayContents(body []byte, cachedContents [][]byte) ([]byte, bool) {
|
|
updated := body
|
|
restored := false
|
|
for _, cachedContent := range cachedContents {
|
|
var restoredTurn bool
|
|
updated, restoredTurn = restoreKimiThinkingReplayContent(updated, cachedContent)
|
|
restored = restored || restoredTurn
|
|
}
|
|
return updated, restored
|
|
}
|
|
|
|
func cacheClaudeThinkingReplayResponse(ctx context.Context, scope claudeThinkingReplayScope, response []byte) {
|
|
content := gjson.GetBytes(response, "content")
|
|
if content.IsArray() {
|
|
cacheClaudeThinkingReplayContent(ctx, scope, []byte(content.Raw))
|
|
return
|
|
}
|
|
accumulator := newKimiThinkingReplayStreamAccumulator()
|
|
accumulator.observe(response)
|
|
if content, completed := accumulator.content(); completed {
|
|
cacheClaudeThinkingReplayContent(ctx, scope, content)
|
|
}
|
|
}
|
|
|
|
func cacheClaudeThinkingReplayContent(ctx context.Context, scope claudeThinkingReplayScope, content []byte) {
|
|
if !scope.valid() || !scope.cacheReady {
|
|
return
|
|
}
|
|
if kimiThinkingReplayContentIsReplayable(content) {
|
|
if _, errReplace := internalcache.ReplaceClaudeThinkingReplayIfUnchanged(ctx, scope.modelFamily, scope.sessionKey, scope.snapshot, content); errReplace != nil {
|
|
log.Warnf("claude compatible thinking replay cache replace failed: %v", errReplace)
|
|
}
|
|
return
|
|
}
|
|
clearClaudeThinkingReplayContent(ctx, scope)
|
|
}
|
|
|
|
func clearClaudeThinkingReplayContent(ctx context.Context, scope claudeThinkingReplayScope) {
|
|
if !scope.valid() || !scope.cacheReady {
|
|
return
|
|
}
|
|
if _, errDelete := internalcache.DeleteClaudeThinkingReplayIfUnchanged(ctx, scope.modelFamily, scope.sessionKey, scope.snapshot); errDelete != nil {
|
|
log.Warnf("claude compatible thinking replay cache delete failed: %v", errDelete)
|
|
}
|
|
}
|
|
|
|
func wrapClaudeThinkingReplayStream(ctx context.Context, result *cliproxyexecutor.StreamResult, scope claudeThinkingReplayScope) *cliproxyexecutor.StreamResult {
|
|
return wrapThinkingReplayStream(ctx, result, scope, cacheClaudeThinkingReplayContent, clearClaudeThinkingReplayContent)
|
|
}
|