vibe-proxy/backend/internal/runtime/executor/claude_thinking_replay.go
2026-08-24 00:10:41 +02:00

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)
}