Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
280
backend/internal/signature/claude_messages_sanitize.go
Normal file
280
backend/internal/signature/claude_messages_sanitize.go
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
package signature
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
type ClaudeMessagesSignatureSanitizeOptions struct {
|
||||
TargetProvider SignatureProvider
|
||||
TargetModel string
|
||||
DropEmptyMessages bool
|
||||
DropToolSignatures bool
|
||||
DropEmptyThinkingPlaceholders bool
|
||||
// PreserveEmptyThinkingBlocks preserves compatibility-mode thinking blocks
|
||||
// together with their original signatures, including opaque signatures.
|
||||
PreserveEmptyThinkingBlocks bool
|
||||
}
|
||||
|
||||
type SignatureSanitizeReport struct {
|
||||
TargetProvider SignatureProvider
|
||||
Preserved int
|
||||
DroppedBlocks int
|
||||
DroppedSignatures int
|
||||
ReplacedSignatures int
|
||||
Decisions []SignatureCompatibilityDecision
|
||||
}
|
||||
|
||||
// SanitizeClaudeMessagesSignaturesForModel removes or preserves Claude
|
||||
// /v1/messages signed history according to the provider family implied by
|
||||
// targetModel.
|
||||
func SanitizeClaudeMessagesSignaturesForModel(payload []byte, targetModel string) ([]byte, SignatureSanitizeReport) {
|
||||
return SanitizeClaudeMessagesSignaturesForTarget(payload, ClaudeMessagesSignatureSanitizeOptions{
|
||||
TargetProvider: SignatureProviderFromModelName(targetModel),
|
||||
TargetModel: targetModel,
|
||||
DropEmptyMessages: true,
|
||||
})
|
||||
}
|
||||
|
||||
// SanitizeClaudeMessagesForClaudeUpstream prepares a Claude /v1/messages body
|
||||
// for Claude-compatible upstreams. Valid Claude signatures are normalized to
|
||||
// provider-native E-form, valid Claude CAIS signatures are kept,
|
||||
// incompatible thinking blocks are dropped, and tool_use blocks keep only their
|
||||
// tool-call payload.
|
||||
func SanitizeClaudeMessagesForClaudeUpstream(payload []byte, targetModel string, preserveEmptyThinkingBlocks ...bool) ([]byte, SignatureSanitizeReport) {
|
||||
preserveEmpty := len(preserveEmptyThinkingBlocks) > 0 && preserveEmptyThinkingBlocks[0]
|
||||
return SanitizeClaudeMessagesSignaturesForTarget(payload, ClaudeMessagesSignatureSanitizeOptions{
|
||||
TargetProvider: SignatureProviderClaude,
|
||||
TargetModel: targetModel,
|
||||
DropEmptyMessages: true,
|
||||
DropToolSignatures: true,
|
||||
DropEmptyThinkingPlaceholders: !preserveEmpty,
|
||||
PreserveEmptyThinkingBlocks: preserveEmpty,
|
||||
})
|
||||
}
|
||||
|
||||
// SanitizeClaudeMessagesSignaturesForTarget applies provider-aware signature
|
||||
// compatibility rules to Claude /v1/messages history. Compatible thinking
|
||||
// signatures are preserved. Incompatible thinking blocks are removed so a user
|
||||
// can continue a conversation after switching between Claude, GPT/Codex,
|
||||
// and Gemini models.
|
||||
func SanitizeClaudeMessagesSignaturesForTarget(payload []byte, opts ClaudeMessagesSignatureSanitizeOptions) ([]byte, SignatureSanitizeReport) {
|
||||
targetProvider := normalizeSignatureTargetProvider(opts.TargetProvider)
|
||||
if targetProvider == SignatureProviderUnknown && opts.TargetModel != "" {
|
||||
targetProvider = SignatureProviderFromModelName(opts.TargetModel)
|
||||
}
|
||||
report := SignatureSanitizeReport{TargetProvider: targetProvider}
|
||||
|
||||
messages := gjson.GetBytes(payload, "messages")
|
||||
if !messages.IsArray() {
|
||||
return payload, report
|
||||
}
|
||||
|
||||
messageResults := messages.Array()
|
||||
keptMessages := make([]string, 0, len(messageResults))
|
||||
modified := false
|
||||
|
||||
for i, message := range messageResults {
|
||||
content := message.Get("content")
|
||||
if !content.IsArray() {
|
||||
keptMessages = append(keptMessages, message.Raw)
|
||||
continue
|
||||
}
|
||||
|
||||
contentResults := content.Array()
|
||||
keptParts := make([]string, 0, len(contentResults))
|
||||
messageModified := false
|
||||
|
||||
for j, part := range contentResults {
|
||||
partType := part.Get("type").String()
|
||||
if partType == "tool_use" {
|
||||
if opts.DropToolSignatures {
|
||||
updatedPart, changed := stripClaudeToolUseSignatureFields(part)
|
||||
if changed {
|
||||
messageModified = true
|
||||
report.DroppedSignatures++
|
||||
}
|
||||
keptParts = append(keptParts, updatedPart)
|
||||
continue
|
||||
}
|
||||
updatedPart, changed, decisions := sanitizeClaudeToolUseSignature(part, targetProvider, opts.TargetModel, i, j)
|
||||
report.Decisions = append(report.Decisions, decisions...)
|
||||
if changed {
|
||||
messageModified = true
|
||||
}
|
||||
for _, decision := range decisions {
|
||||
switch decision.Action {
|
||||
case SignatureActionPreserve:
|
||||
report.Preserved++
|
||||
case SignatureActionReplaceWithGeminiBypass:
|
||||
report.ReplacedSignatures++
|
||||
default:
|
||||
report.DroppedSignatures++
|
||||
}
|
||||
}
|
||||
keptParts = append(keptParts, updatedPart)
|
||||
continue
|
||||
}
|
||||
|
||||
if partType != "thinking" {
|
||||
keptParts = append(keptParts, part.Raw)
|
||||
continue
|
||||
}
|
||||
|
||||
rawSignature := part.Get("signature").String()
|
||||
if opts.PreserveEmptyThinkingBlocks {
|
||||
report.Preserved++
|
||||
keptParts = append(keptParts, part.Raw)
|
||||
continue
|
||||
}
|
||||
if targetProvider == SignatureProviderClaude && isEmptyClaudeThinkingPlaceholder(part) && !opts.DropEmptyThinkingPlaceholders {
|
||||
keptParts = append(keptParts, part.Raw)
|
||||
continue
|
||||
}
|
||||
|
||||
decision := DecideSignatureCompatibilityForModel(targetProvider, opts.TargetModel, rawSignature, SignatureBlockKindClaudeThinking)
|
||||
decision.Reason = fmt.Sprintf("messages[%d].content[%d]: %s", i, j, decision.Reason)
|
||||
report.Decisions = append(report.Decisions, decision)
|
||||
|
||||
switch decision.Action {
|
||||
case SignatureActionPreserve:
|
||||
report.Preserved++
|
||||
if decision.NormalizedSignature != "" && decision.NormalizedSignature != rawSignature {
|
||||
updated, _ := sjson.Set(part.Raw, "signature", decision.NormalizedSignature)
|
||||
keptParts = append(keptParts, updated)
|
||||
messageModified = true
|
||||
continue
|
||||
}
|
||||
keptParts = append(keptParts, part.Raw)
|
||||
case SignatureActionReplaceWithGeminiBypass:
|
||||
report.ReplacedSignatures++
|
||||
updated, _ := sjson.Set(part.Raw, "signature", decision.ReplacementSignature)
|
||||
keptParts = append(keptParts, updated)
|
||||
messageModified = true
|
||||
case SignatureActionDropSignature:
|
||||
report.DroppedSignatures++
|
||||
updated, _ := sjson.Delete(part.Raw, "signature")
|
||||
keptParts = append(keptParts, updated)
|
||||
messageModified = true
|
||||
default:
|
||||
report.DroppedBlocks++
|
||||
messageModified = true
|
||||
}
|
||||
}
|
||||
|
||||
if messageModified {
|
||||
modified = true
|
||||
if len(keptParts) == 0 && opts.DropEmptyMessages {
|
||||
continue
|
||||
}
|
||||
updated, _ := sjson.SetRaw(message.Raw, "content", "["+strings.Join(keptParts, ",")+"]")
|
||||
keptMessages = append(keptMessages, updated)
|
||||
continue
|
||||
}
|
||||
|
||||
keptMessages = append(keptMessages, message.Raw)
|
||||
}
|
||||
|
||||
if !modified {
|
||||
return payload, report
|
||||
}
|
||||
output, _ := sjson.SetRawBytes(payload, "messages", []byte("["+strings.Join(keptMessages, ",")+"]"))
|
||||
return output, report
|
||||
}
|
||||
|
||||
func stripClaudeToolUseSignatureFields(part gjson.Result) (string, bool) {
|
||||
updated := part.Raw
|
||||
changed := false
|
||||
for _, sigPath := range claudeToolUseProvenancePaths() {
|
||||
if !gjson.Get(updated, sigPath).Exists() {
|
||||
continue
|
||||
}
|
||||
updated, _ = sjson.Delete(updated, sigPath)
|
||||
changed = true
|
||||
}
|
||||
if cleaned, ok := deleteEmptyJSONObjectPath(updated, "extra_content.google"); ok {
|
||||
updated = cleaned
|
||||
changed = true
|
||||
}
|
||||
if cleaned, ok := deleteEmptyJSONObjectPath(updated, "extra_content"); ok {
|
||||
updated = cleaned
|
||||
changed = true
|
||||
}
|
||||
return updated, changed
|
||||
}
|
||||
|
||||
func sanitizeClaudeToolUseSignature(part gjson.Result, targetProvider SignatureProvider, targetModel string, messageIdx, partIdx int) (string, bool, []SignatureCompatibilityDecision) {
|
||||
updated := part.Raw
|
||||
changed := false
|
||||
var decisions []SignatureCompatibilityDecision
|
||||
|
||||
for _, sigPath := range claudeToolUseSignaturePaths() {
|
||||
sigResult := part.Get(sigPath)
|
||||
if !sigResult.Exists() {
|
||||
continue
|
||||
}
|
||||
|
||||
blockKind := SignatureBlockKindGeminiFunctionCall
|
||||
if targetProvider == SignatureProviderClaude {
|
||||
blockKind = SignatureBlockKindClaudeThinking
|
||||
} else if targetProvider == SignatureProviderGPT {
|
||||
blockKind = SignatureBlockKindGPTReasoning
|
||||
}
|
||||
decision := DecideSignatureCompatibilityForModel(targetProvider, targetModel, sigResult.String(), blockKind)
|
||||
decision.Reason = fmt.Sprintf("messages[%d].content[%d].%s: %s", messageIdx, partIdx, sigPath, decision.Reason)
|
||||
decisions = append(decisions, decision)
|
||||
|
||||
switch decision.Action {
|
||||
case SignatureActionPreserve:
|
||||
if decision.NormalizedSignature != "" && decision.NormalizedSignature != sigResult.String() {
|
||||
updated, _ = sjson.Set(updated, sigPath, decision.NormalizedSignature)
|
||||
changed = true
|
||||
}
|
||||
case SignatureActionReplaceWithGeminiBypass:
|
||||
updated, _ = sjson.Set(updated, sigPath, decision.ReplacementSignature)
|
||||
changed = true
|
||||
default:
|
||||
updated, _ = sjson.Delete(updated, sigPath)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if cleaned, ok := deleteEmptyJSONObjectPath(updated, "extra_content.google"); ok {
|
||||
updated = cleaned
|
||||
changed = true
|
||||
}
|
||||
if cleaned, ok := deleteEmptyJSONObjectPath(updated, "extra_content"); ok {
|
||||
updated = cleaned
|
||||
changed = true
|
||||
}
|
||||
|
||||
return updated, changed, decisions
|
||||
}
|
||||
|
||||
func claudeToolUseSignaturePaths() []string {
|
||||
return []string{
|
||||
"signature",
|
||||
"thoughtSignature",
|
||||
"thought_signature",
|
||||
"extra_content.google.thought_signature",
|
||||
}
|
||||
}
|
||||
|
||||
func claudeToolUseProvenancePaths() []string {
|
||||
return append(claudeToolUseSignaturePaths(), "model")
|
||||
}
|
||||
|
||||
func deleteEmptyJSONObjectPath(raw, path string) (string, bool) {
|
||||
result := gjson.Get(raw, path)
|
||||
if !result.Exists() || !result.IsObject() || len(result.Map()) != 0 {
|
||||
return raw, false
|
||||
}
|
||||
updated, err := sjson.Delete(raw, path)
|
||||
if err != nil {
|
||||
return raw, false
|
||||
}
|
||||
return updated, true
|
||||
}
|
||||
Loading…
Reference in a new issue