228 lines
8.6 KiB
Go
228 lines
8.6 KiB
Go
// Claude thinking signature validation wrappers for Antigravity bypass mode.
|
|
package claude
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"strings"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/signature"
|
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
const (
|
|
maxBypassSignatureLen = signature.MaxClaudeThinkingSignatureLen
|
|
|
|
// Gemini carrier envelopes exist only on the Claude-facing wire. The request
|
|
// translator validates and unwraps them before writing native Gemini parts.
|
|
geminiClaudeCarrierPrefix = "cpa-gemini-carrier-v1:"
|
|
geminiClaudeCarrierNext = "next"
|
|
geminiClaudeCarrierPrevious = "previous"
|
|
geminiClaudeCarrierStandalone = "standalone"
|
|
geminiClaudeCarrierText = "text"
|
|
geminiClaudeCarrierFunction = "function"
|
|
geminiClaudeCarrierAny = "any"
|
|
)
|
|
|
|
type claudeSignatureTree = signature.ClaudeSignatureTree
|
|
|
|
func encodeGeminiClaudeCarrierSignature(rawSignature, direction, targetKind string) string {
|
|
rawSignature = strings.TrimSpace(rawSignature)
|
|
if rawSignature == "" {
|
|
return ""
|
|
}
|
|
return geminiClaudeCarrierPrefix + direction + ":" + targetKind + ":" + base64.RawStdEncoding.EncodeToString([]byte(rawSignature))
|
|
}
|
|
|
|
func decodeGeminiClaudeCarrierSignature(rawSignature string) (signatureValue, direction, targetKind string, marked, ok bool) {
|
|
rawSignature = strings.TrimSpace(rawSignature)
|
|
if !strings.HasPrefix(rawSignature, geminiClaudeCarrierPrefix) {
|
|
return rawSignature, "", "", false, true
|
|
}
|
|
marked = true
|
|
if len(rawSignature) > (signature.MaxGeminiThoughtSignatureLen*4/3)+1024 {
|
|
return "", "", "", true, false
|
|
}
|
|
fields := strings.SplitN(strings.TrimPrefix(rawSignature, geminiClaudeCarrierPrefix), ":", 3)
|
|
if len(fields) != 3 {
|
|
return "", "", "", true, false
|
|
}
|
|
direction, targetKind = fields[0], fields[1]
|
|
switch direction {
|
|
case geminiClaudeCarrierNext, geminiClaudeCarrierPrevious, geminiClaudeCarrierStandalone:
|
|
default:
|
|
return "", "", "", true, false
|
|
}
|
|
switch targetKind {
|
|
case geminiClaudeCarrierText, geminiClaudeCarrierFunction, geminiClaudeCarrierAny:
|
|
default:
|
|
return "", "", "", true, false
|
|
}
|
|
decoded, errDecode := base64.RawStdEncoding.DecodeString(fields[2])
|
|
if errDecode != nil || len(decoded) == 0 || strings.HasPrefix(string(decoded), geminiClaudeCarrierPrefix) {
|
|
return "", "", "", true, false
|
|
}
|
|
blockKind := signature.SignatureBlockKindGeminiModelPart
|
|
if targetKind == geminiClaudeCarrierFunction {
|
|
blockKind = signature.SignatureBlockKindGeminiFunctionCall
|
|
}
|
|
normalized, compatible := signature.CompatibleSignatureForProviderBlock(signature.SignatureProviderGemini, string(decoded), blockKind)
|
|
if !compatible || signature.IsGeminiThoughtSignatureBypass(signature.SignaturePayloadWithoutProviderPrefix(normalized)) {
|
|
return "", "", "", true, false
|
|
}
|
|
return normalized, direction, targetKind, true, true
|
|
}
|
|
|
|
func geminiClaudeSemanticTargetKind(block gjson.Result) string {
|
|
switch block.Get("type").String() {
|
|
case "text":
|
|
return geminiClaudeCarrierText
|
|
case "tool_use":
|
|
return geminiClaudeCarrierFunction
|
|
case "thinking":
|
|
if strings.TrimSpace(block.Get("thinking").String()) != "" {
|
|
return geminiClaudeCarrierText
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func geminiClaudeCarrierMatchesAdjacent(blocks []gjson.Result, index int, direction, targetKind string) bool {
|
|
step := 1
|
|
if direction == geminiClaudeCarrierPrevious {
|
|
step = -1
|
|
}
|
|
for adjacent := index + step; adjacent >= 0 && adjacent < len(blocks); adjacent += step {
|
|
if kind := geminiClaudeSemanticTargetKind(blocks[adjacent]); kind != "" {
|
|
return targetKind == geminiClaudeCarrierAny || targetKind == kind
|
|
}
|
|
if blocks[adjacent].Get("type").String() != "thinking" || strings.TrimSpace(blocks[adjacent].Get("thinking").String()) != "" {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// StripEmptySignatureThinkingBlocks removes thinking blocks whose signatures
|
|
// are empty or not valid Claude thinking signatures. These usually come from
|
|
// proxy-generated responses where no real Claude signature exists.
|
|
func StripEmptySignatureThinkingBlocks(payload []byte) []byte {
|
|
return signature.StripInvalidClaudeThinkingBlocks(payload, signature.ClaudeSignatureValidationOptions{PrefixOnly: true})
|
|
}
|
|
|
|
// StripInvalidGeminiSignatureThinkingBlocks preserves only thinking carriers
|
|
// whose signatures can be replayed to Gemini. Claude Code uses these carriers
|
|
// to return provider-native signatures from prior translated responses.
|
|
func StripInvalidGeminiSignatureThinkingBlocks(payload []byte) []byte {
|
|
messages := gjson.GetBytes(payload, "messages")
|
|
if !messages.IsArray() {
|
|
return payload
|
|
}
|
|
changed := false
|
|
messageItems := make([][]byte, 0, len(messages.Array()))
|
|
for _, message := range messages.Array() {
|
|
messageJSON := []byte(message.Raw)
|
|
content := message.Get("content")
|
|
if !content.IsArray() {
|
|
messageItems = append(messageItems, messageJSON)
|
|
continue
|
|
}
|
|
contentChanged := false
|
|
assistantMessage := strings.EqualFold(message.Get("role").String(), "assistant")
|
|
contentBlocks := content.Array()
|
|
contentItems := make([][]byte, 0, len(contentBlocks))
|
|
pendingCarrierTargetKind := ""
|
|
for blockIndex, block := range contentBlocks {
|
|
if block.Get("type").String() == "thinking" {
|
|
rawSignature := strings.TrimSpace(block.Get("signature").String())
|
|
thinkingText := strings.TrimSpace(block.Get("thinking").String())
|
|
if rawSignature == "" && thinkingText != "" && (pendingCarrierTargetKind == geminiClaudeCarrierAny || pendingCarrierTargetKind == geminiClaudeCarrierText) {
|
|
pendingCarrierTargetKind = ""
|
|
contentItems = append(contentItems, []byte(block.Raw))
|
|
continue
|
|
}
|
|
innerSignature, direction, targetKind, marked, okCarrier := decodeGeminiClaudeCarrierSignature(rawSignature)
|
|
blockKind := signature.SignatureBlockKindGeminiModelPart
|
|
if marked && targetKind == geminiClaudeCarrierFunction {
|
|
blockKind = signature.SignatureBlockKindGeminiFunctionCall
|
|
}
|
|
invalidMarkedPlacement := false
|
|
if marked {
|
|
switch direction {
|
|
case geminiClaudeCarrierNext, geminiClaudeCarrierPrevious:
|
|
invalidMarkedPlacement = !geminiClaudeCarrierMatchesAdjacent(contentBlocks, blockIndex, direction, targetKind)
|
|
case geminiClaudeCarrierStandalone:
|
|
invalidMarkedPlacement = thinkingText != "" && targetKind == geminiClaudeCarrierFunction
|
|
}
|
|
if thinkingText != "" && direction == geminiClaudeCarrierPrevious {
|
|
invalidMarkedPlacement = true
|
|
}
|
|
}
|
|
if !okCarrier || !assistantMessage || invalidMarkedPlacement {
|
|
pendingCarrierTargetKind = ""
|
|
contentChanged = true
|
|
continue
|
|
}
|
|
if !marked {
|
|
innerSignature = rawSignature
|
|
}
|
|
if _, ok := signature.CompatibleSignatureForProviderBlock(signature.SignatureProviderGemini, innerSignature, blockKind); !ok {
|
|
pendingCarrierTargetKind = ""
|
|
contentChanged = true
|
|
continue
|
|
}
|
|
if marked && direction == geminiClaudeCarrierNext {
|
|
pendingCarrierTargetKind = targetKind
|
|
} else {
|
|
pendingCarrierTargetKind = ""
|
|
}
|
|
} else {
|
|
pendingCarrierTargetKind = ""
|
|
}
|
|
contentItems = append(contentItems, []byte(block.Raw))
|
|
}
|
|
if contentChanged {
|
|
messageJSON, _ = sjson.SetRawBytes(messageJSON, "content", translatorcommon.JoinRawArray(contentItems))
|
|
changed = true
|
|
}
|
|
messageItems = append(messageItems, messageJSON)
|
|
}
|
|
if !changed {
|
|
return payload
|
|
}
|
|
updated, errSet := sjson.SetRawBytes(payload, "messages", translatorcommon.JoinRawArray(messageItems))
|
|
if errSet != nil {
|
|
return payload
|
|
}
|
|
return updated
|
|
}
|
|
|
|
func StripInvalidBypassSignatureThinkingBlocks(payload []byte) []byte {
|
|
return signature.StripInvalidClaudeThinkingBlocks(payload, claudeBypassSignatureValidationOptions())
|
|
}
|
|
|
|
func ValidateClaudeBypassSignatures(inputRawJSON []byte) error {
|
|
return signature.ValidateClaudeThinkingSignatures(inputRawJSON, claudeBypassSignatureValidationOptions())
|
|
}
|
|
|
|
func normalizeClaudeBypassSignature(rawSignature string) (string, error) {
|
|
return signature.NormalizeClaudeThinkingSignature(rawSignature, claudeBypassSignatureValidationOptions())
|
|
}
|
|
|
|
func inspectDoubleLayerSignature(sig string) (*claudeSignatureTree, error) {
|
|
return signature.InspectClaudeDoubleLayerSignature(sig)
|
|
}
|
|
|
|
func inspectSingleLayerSignature(sig string) (*claudeSignatureTree, error) {
|
|
return signature.InspectClaudeSingleLayerSignature(sig)
|
|
}
|
|
|
|
func inspectClaudeSignaturePayload(payload []byte, encodingLayers int) (*claudeSignatureTree, error) {
|
|
return signature.InspectClaudeSignaturePayload(payload, encodingLayers)
|
|
}
|
|
|
|
func claudeBypassSignatureValidationOptions() signature.ClaudeSignatureValidationOptions {
|
|
return signature.ClaudeSignatureValidationOptions{Strict: cache.SignatureBypassStrictMode()}
|
|
}
|