Files
CLIProxyAPI/internal/runtime/executor/xai_reasoning_replay.go
T
sususu98 05d1792d43 feat(xai): replay Grok reasoning for Claude messages (#3962)
Add Grok-native reasoning replay for Claude-sourced xAI Responses requests. Replay state is scoped by model and session, normalized to reasoning/function_call/custom_tool_call items, and injected with the same tool-call alignment semantics used by Codex.

Include the minimal Grok encrypted_content validator needed on upstream/dev so replay cache entries cannot accept Codex/GPT, Gemini, or Claude signature shapes. Cache completed output items from HTTP, streaming, and WebSocket xAI responses.

Cover two-turn Claude replay, tool_result continuity, valid Grok cache storage, and rejection of Codex-shaped encrypted_content.
2026-06-23 14:57:53 +08:00

172 lines
5.2 KiB
Go

package executor
import (
"context"
"strings"
internalcache "github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
"github.com/router-for-me/CLIProxyAPI/v7/internal/signature"
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
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"
)
type xaiReasoningReplayScope struct {
modelName string
sessionKey string
}
var getXAIReasoningReplayItemsRequired = internalcache.GetXAIReasoningReplayItemsRequired
func (s xaiReasoningReplayScope) valid() bool {
return strings.TrimSpace(s.modelName) != "" && strings.TrimSpace(s.sessionKey) != ""
}
func applyXAIReasoningReplayCacheRequired(ctx context.Context, from sdktranslator.Format, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, body []byte) ([]byte, xaiReasoningReplayScope, error) {
scope := xaiReasoningReplayScopeFromRequest(ctx, from, req, opts, body)
if !scope.valid() {
return body, scope, nil
}
items, ok, errReplay := getXAIReasoningReplayItemsRequired(ctx, scope.modelName, scope.sessionKey)
if errReplay != nil {
log.Warnf("xai reasoning replay cache read failed: %v", errReplay)
return body, scope, nil
}
if !ok {
return body, scope, nil
}
items = filterXAIReasoningReplayItemsForInput(body, items)
if len(items) == 0 {
return body, scope, nil
}
updated, ok := insertCodexReasoningReplayItems(body, items)
if !ok {
return body, scope, nil
}
return updated, scope, nil
}
func xaiReasoningReplayScopeFromRequest(ctx context.Context, from sdktranslator.Format, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, body []byte) xaiReasoningReplayScope {
if !xaiReasoningReplayEnabledForSource(from) {
return xaiReasoningReplayScope{}
}
return xaiReasoningReplayScope{
modelName: thinking.ParseSuffix(req.Model).ModelName,
sessionKey: codexReasoningReplaySessionKey(ctx, from, req, opts, body),
}
}
func xaiReasoningReplayEnabledForSource(from sdktranslator.Format) bool {
return sourceFormatEqual(from, sdktranslator.FormatClaude)
}
func xaiInputHasValidReasoningEncryptedContent(body []byte) bool {
input := gjson.GetBytes(body, "input")
if !input.IsArray() {
return false
}
for _, item := range input.Array() {
if strings.TrimSpace(item.Get("type").String()) != "reasoning" {
continue
}
encryptedContent := item.Get("encrypted_content")
if encryptedContent.Type != gjson.String {
continue
}
if _, err := signature.InspectGrokEncryptedContent(encryptedContent.String()); err == nil {
return true
}
}
return false
}
func filterXAIReasoningReplayItemsForInput(body []byte, items [][]byte) [][]byte {
input := gjson.GetBytes(body, "input")
if !input.IsArray() {
return nil
}
hasInputReasoning := xaiInputHasValidReasoningEncryptedContent(body)
existingCalls := make(map[string]bool)
existingOutputs := make(map[string]bool)
for _, inputItem := range input.Array() {
itemType := strings.TrimSpace(inputItem.Get("type").String())
if itemType == "function_call_output" || itemType == "custom_tool_call_output" {
callID := strings.TrimSpace(inputItem.Get("call_id").String())
if callID != "" {
for _, candidate := range codexReplayComparableCallIDs(callID) {
existingOutputs[candidate] = true
}
}
}
for _, key := range codexReplayToolCallKeys(inputItem) {
existingCalls[key] = true
}
}
filtered := make([][]byte, 0, len(items))
for _, item := range items {
itemResult := gjson.ParseBytes(item)
switch strings.TrimSpace(itemResult.Get("type").String()) {
case "reasoning":
if hasInputReasoning {
continue
}
case "function_call", "custom_tool_call":
keys := codexReplayToolCallKeys(itemResult)
if len(keys) == 0 || codexReplayAnyToolCallKeyExists(existingCalls, keys) {
continue
}
hasMatchingOutput := false
callID := strings.TrimSpace(itemResult.Get("call_id").String())
if callID != "" {
for _, candidate := range codexReplayComparableCallIDs(callID) {
if existingOutputs[candidate] {
hasMatchingOutput = true
break
}
}
}
if !hasMatchingOutput {
continue
}
for _, key := range keys {
existingCalls[key] = true
}
default:
continue
}
filtered = append(filtered, item)
}
return filtered
}
func cacheXAIReasoningReplayFromCompleted(ctx context.Context, scope xaiReasoningReplayScope, completedData []byte) {
if !scope.valid() {
return
}
if ctx == nil {
ctx = context.Background()
}
output := gjson.GetBytes(completedData, "response.output")
if !output.IsArray() {
return
}
items := make([][]byte, 0, len(output.Array()))
for _, item := range output.Array() {
switch strings.TrimSpace(item.Get("type").String()) {
case "reasoning", "function_call", "custom_tool_call":
items = append(items, []byte(item.Raw))
default:
continue
}
}
if !internalcache.CacheXAIReasoningReplayItemsBestEffort(ctx, scope.modelName, scope.sessionKey, items) {
if errDelete := internalcache.DeleteXAIReasoningReplayItemRequired(ctx, scope.modelName, scope.sessionKey); errDelete != nil {
log.Warnf("xai reasoning replay cache delete failed after completed cache store failed: %v", errDelete)
}
}
}