feat(antigravity): configurable signature cache with bypass-mode validation
Antigravity 的 Claude thinking signature 处理新增 cache/bypass 双模式, 并为 bypass 模式实现按 SIGNATURE-CHANNEL-SPEC.md 的签名校验。 新增 antigravity-signature-cache-enabled 配置项(默认 true): - cache mode(true):使用服务端缓存的签名,行为与原有逻辑完全一致 - bypass mode(false):直接使用客户端提供的签名,经过校验和归一化 支持配置热重载,运行时可切换模式。 校验流程: 1. 剥离历史 cache-mode 的 'modelGroup#' 前缀(如 claude#Exxxx → Exxxx) 2. 首字符必须为 'E'(单层编码)或 'R'(双层编码),否则拒绝 3. R 开头:base64 解码 → 内层必须以 'E' 开头 → 继续单层校验 4. E 开头:base64 解码 → 首字节必须为 0x12(Claude protobuf 标识) 5. 所有合法签名归一化为 R 形式(双层 base64)发往 Antigravity 后端 非法签名处理策略: - 非严格模式(默认):translator 静默丢弃无签名的 thinking block - 严格模式(antigravity-signature-bypass-strict: true): executor 层在请求发往上游前直接返回 HTTP 400 按 SIGNATURE-CHANNEL-SPEC.md 解析 Claude 签名的完整 protobuf 结构: - Top-level Field 2(容器)→ Field 1(渠道块) - 渠道块提取:channel_id (Field 1)、infrastructure (Field 2)、 model_text (Field 6)、field7 (Field 7) - 计算 routing_class、infrastructure_class、schema_features - 使用 google.golang.org/protobuf/encoding/protowire 解析 - resolveThinkingSignature 拆分为 resolveCacheModeSignature / resolveBypassModeSignature - hasResolvedThinkingSignature:mode-aware 签名有效性判断 (cache: len>=50 via HasValidSignature,bypass: non-empty) - validateAntigravityRequestSignatures:executor 预检, 仅在 bypass + strict 模式下拦截非法签名返回 400 - 响应侧签名缓存逻辑与 cache mode 集成 - Cache mode 行为完全保留:无 '#' 前缀的原生签名静默丢弃
This commit is contained in:
@@ -1,13 +1,97 @@
|
||||
package claude
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
||||
"github.com/tidwall/gjson"
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
func testAnthropicNativeSignature(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
payload := buildClaudeSignaturePayload(t, 12, uint64Ptr(2), "claude-sonnet-4-6", true)
|
||||
signature := base64.StdEncoding.EncodeToString(payload)
|
||||
if len(signature) < cache.MinValidSignatureLen {
|
||||
t.Fatalf("test signature too short: %d", len(signature))
|
||||
}
|
||||
return signature
|
||||
}
|
||||
|
||||
func testMinimalAnthropicSignature(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
payload := buildClaudeSignaturePayload(t, 12, nil, "", false)
|
||||
return base64.StdEncoding.EncodeToString(payload)
|
||||
}
|
||||
|
||||
func buildClaudeSignaturePayload(t *testing.T, channelID uint64, field2 *uint64, modelText string, includeField7 bool) []byte {
|
||||
t.Helper()
|
||||
|
||||
channelBlock := []byte{}
|
||||
channelBlock = protowire.AppendTag(channelBlock, 1, protowire.VarintType)
|
||||
channelBlock = protowire.AppendVarint(channelBlock, channelID)
|
||||
if field2 != nil {
|
||||
channelBlock = protowire.AppendTag(channelBlock, 2, protowire.VarintType)
|
||||
channelBlock = protowire.AppendVarint(channelBlock, *field2)
|
||||
}
|
||||
if modelText != "" {
|
||||
channelBlock = protowire.AppendTag(channelBlock, 6, protowire.BytesType)
|
||||
channelBlock = protowire.AppendString(channelBlock, modelText)
|
||||
}
|
||||
if includeField7 {
|
||||
channelBlock = protowire.AppendTag(channelBlock, 7, protowire.VarintType)
|
||||
channelBlock = protowire.AppendVarint(channelBlock, 0)
|
||||
}
|
||||
|
||||
container := []byte{}
|
||||
container = protowire.AppendTag(container, 1, protowire.BytesType)
|
||||
container = protowire.AppendBytes(container, channelBlock)
|
||||
container = protowire.AppendTag(container, 2, protowire.BytesType)
|
||||
container = protowire.AppendBytes(container, bytes.Repeat([]byte{0x11}, 12))
|
||||
container = protowire.AppendTag(container, 3, protowire.BytesType)
|
||||
container = protowire.AppendBytes(container, bytes.Repeat([]byte{0x22}, 12))
|
||||
container = protowire.AppendTag(container, 4, protowire.BytesType)
|
||||
container = protowire.AppendBytes(container, bytes.Repeat([]byte{0x33}, 48))
|
||||
|
||||
payload := []byte{}
|
||||
payload = protowire.AppendTag(payload, 2, protowire.BytesType)
|
||||
payload = protowire.AppendBytes(payload, container)
|
||||
payload = protowire.AppendTag(payload, 3, protowire.VarintType)
|
||||
payload = protowire.AppendVarint(payload, 1)
|
||||
return payload
|
||||
}
|
||||
|
||||
func uint64Ptr(v uint64) *uint64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
func testNonAnthropicRawSignature(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
payload := bytes.Repeat([]byte{0x34}, 48)
|
||||
signature := base64.StdEncoding.EncodeToString(payload)
|
||||
if len(signature) < cache.MinValidSignatureLen {
|
||||
t.Fatalf("test signature too short: %d", len(signature))
|
||||
}
|
||||
return signature
|
||||
}
|
||||
|
||||
func testGeminiRawSignature(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
payload := append([]byte{0x0A}, bytes.Repeat([]byte{0x56}, 48)...)
|
||||
signature := base64.StdEncoding.EncodeToString(payload)
|
||||
if len(signature) < cache.MinValidSignatureLen {
|
||||
t.Fatalf("test signature too short: %d", len(signature))
|
||||
}
|
||||
return signature
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToAntigravity_BasicStructure(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"model": "claude-3-5-sonnet-20240620",
|
||||
@@ -116,6 +200,545 @@ func TestConvertClaudeRequestToAntigravity_ThinkingBlocks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_AcceptsClaudeSingleAndDoubleLayer(t *testing.T) {
|
||||
rawSignature := testAnthropicNativeSignature(t)
|
||||
doubleEncoded := base64.StdEncoding.EncodeToString([]byte(rawSignature))
|
||||
|
||||
inputJSON := []byte(`{
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "one", "signature": "` + rawSignature + `"},
|
||||
{"type": "thinking", "thinking": "two", "signature": "claude#` + doubleEncoded + `"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
if err := ValidateClaudeBypassSignatures(inputJSON); err != nil {
|
||||
t.Fatalf("ValidateBypassModeSignatures returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_RejectsGeminiSignature(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "one", "signature": "` + testGeminiRawSignature(t) + `"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected Gemini signature to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_RejectsMissingSignature(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "one"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected missing signature to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "missing thinking signature") {
|
||||
t.Fatalf("expected missing signature message, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_RejectsNonREPrefix(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "one", "signature": "` + testNonAnthropicRawSignature(t) + `"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected non-R/E signature to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_RejectsEPrefixWrongFirstByte(t *testing.T) {
|
||||
t.Parallel()
|
||||
payload := append([]byte{0x10}, bytes.Repeat([]byte{0x34}, 48)...)
|
||||
sig := base64.StdEncoding.EncodeToString(payload)
|
||||
if sig[0] != 'E' {
|
||||
t.Fatalf("test setup: expected E prefix, got %c", sig[0])
|
||||
}
|
||||
|
||||
inputJSON := []byte(`{
|
||||
"messages": [{"role": "assistant", "content": [
|
||||
{"type": "thinking", "thinking": "t", "signature": "` + sig + `"}
|
||||
]}]
|
||||
}`)
|
||||
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected E-prefix with wrong first byte (0x10) to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "0x10") {
|
||||
t.Fatalf("expected error to mention 0x10, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_RejectsTopLevel12WithoutClaudeTree(t *testing.T) {
|
||||
previous := cache.SignatureBypassStrictMode()
|
||||
cache.SetSignatureBypassStrictMode(true)
|
||||
t.Cleanup(func() {
|
||||
cache.SetSignatureBypassStrictMode(previous)
|
||||
})
|
||||
|
||||
payload := append([]byte{0x12}, bytes.Repeat([]byte{0x34}, 48)...)
|
||||
sig := base64.StdEncoding.EncodeToString(payload)
|
||||
|
||||
inputJSON := []byte(`{
|
||||
"messages": [{"role": "assistant", "content": [
|
||||
{"type": "thinking", "thinking": "t", "signature": "` + sig + `"}
|
||||
]}]
|
||||
}`)
|
||||
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected non-Claude protobuf tree to be rejected in strict mode")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "malformed protobuf") && !strings.Contains(err.Error(), "Field 2") {
|
||||
t.Fatalf("expected protobuf tree error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_NonStrictAccepts12WithoutClaudeTree(t *testing.T) {
|
||||
previous := cache.SignatureBypassStrictMode()
|
||||
cache.SetSignatureBypassStrictMode(false)
|
||||
t.Cleanup(func() {
|
||||
cache.SetSignatureBypassStrictMode(previous)
|
||||
})
|
||||
|
||||
payload := append([]byte{0x12}, bytes.Repeat([]byte{0x34}, 48)...)
|
||||
sig := base64.StdEncoding.EncodeToString(payload)
|
||||
|
||||
inputJSON := []byte(`{
|
||||
"messages": [{"role": "assistant", "content": [
|
||||
{"type": "thinking", "thinking": "t", "signature": "` + sig + `"}
|
||||
]}]
|
||||
}`)
|
||||
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("non-strict mode should accept 0x12 without protobuf tree, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_RejectsRPrefixInnerNotE(t *testing.T) {
|
||||
t.Parallel()
|
||||
inner := "F" + strings.Repeat("a", 60)
|
||||
outer := base64.StdEncoding.EncodeToString([]byte(inner))
|
||||
if outer[0] != 'R' {
|
||||
t.Fatalf("test setup: expected R prefix, got %c", outer[0])
|
||||
}
|
||||
|
||||
inputJSON := []byte(`{
|
||||
"messages": [{"role": "assistant", "content": [
|
||||
{"type": "thinking", "thinking": "t", "signature": "` + outer + `"}
|
||||
]}]
|
||||
}`)
|
||||
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected R-prefix with non-E inner to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_RejectsInvalidBase64(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
sig string
|
||||
}{
|
||||
{"E invalid", "E!!!invalid!!!"},
|
||||
{"R invalid", "R$$$invalid$$$"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"messages": [{"role": "assistant", "content": [
|
||||
{"type": "thinking", "thinking": "t", "signature": "` + tt.sig + `"}
|
||||
]}]
|
||||
}`)
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid base64 to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "base64") {
|
||||
t.Fatalf("expected base64 error, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_RejectsPrefixStrippedToEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
sig string
|
||||
}{
|
||||
{"prefix only", "claude#"},
|
||||
{"prefix with spaces", "claude# "},
|
||||
{"hash only", "#"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"messages": [{"role": "assistant", "content": [
|
||||
{"type": "thinking", "thinking": "t", "signature": "` + tt.sig + `"}
|
||||
]}]
|
||||
}`)
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected prefix-only signature to be rejected")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_HandlesMultipleHashMarks(t *testing.T) {
|
||||
t.Parallel()
|
||||
rawSignature := testAnthropicNativeSignature(t)
|
||||
sig := "claude#" + rawSignature + "#extra"
|
||||
|
||||
inputJSON := []byte(`{
|
||||
"messages": [{"role": "assistant", "content": [
|
||||
{"type": "thinking", "thinking": "t", "signature": "` + sig + `"}
|
||||
]}]
|
||||
}`)
|
||||
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected signature with trailing # to be rejected (invalid base64)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_HandlesWhitespace(t *testing.T) {
|
||||
t.Parallel()
|
||||
rawSignature := testAnthropicNativeSignature(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
sig string
|
||||
}{
|
||||
{"leading space", " " + rawSignature},
|
||||
{"trailing space", rawSignature + " "},
|
||||
{"both spaces", " " + rawSignature + " "},
|
||||
{"leading tab", "\t" + rawSignature},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
inputJSON := []byte(`{
|
||||
"messages": [{"role": "assistant", "content": [
|
||||
{"type": "thinking", "thinking": "t", "signature": "` + tt.sig + `"}
|
||||
]}]
|
||||
}`)
|
||||
if err := ValidateClaudeBypassSignatures(inputJSON); err != nil {
|
||||
t.Fatalf("expected whitespace-padded signature to be accepted, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateBypassMode_RejectsOversizedSignature(t *testing.T) {
|
||||
t.Parallel()
|
||||
payload := append([]byte{0x12}, bytes.Repeat([]byte{0x34}, maxBypassSignatureLen)...)
|
||||
sig := base64.StdEncoding.EncodeToString(payload)
|
||||
if len(sig) <= maxBypassSignatureLen {
|
||||
t.Fatalf("test setup: signature should exceed max length, got %d", len(sig))
|
||||
}
|
||||
|
||||
inputJSON := []byte(`{
|
||||
"messages": [{"role": "assistant", "content": [
|
||||
{"type": "thinking", "thinking": "t", "signature": "` + sig + `"}
|
||||
]}]
|
||||
}`)
|
||||
|
||||
err := ValidateClaudeBypassSignatures(inputJSON)
|
||||
if err == nil {
|
||||
t.Fatal("expected oversized signature to be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "maximum length") {
|
||||
t.Fatalf("expected length error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBypassModeSignature_TrimsWhitespace(t *testing.T) {
|
||||
previous := cache.SignatureCacheEnabled()
|
||||
cache.SetSignatureCacheEnabled(false)
|
||||
t.Cleanup(func() {
|
||||
cache.SetSignatureCacheEnabled(previous)
|
||||
})
|
||||
|
||||
rawSignature := testAnthropicNativeSignature(t)
|
||||
expected := resolveBypassModeSignature(rawSignature)
|
||||
if expected == "" {
|
||||
t.Fatal("test setup: expected non-empty normalized signature")
|
||||
}
|
||||
|
||||
got := resolveBypassModeSignature(rawSignature + " ")
|
||||
if got != expected {
|
||||
t.Fatalf("expected trailing whitespace to be trimmed:\n got: %q\n want: %q", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToAntigravity_BypassModeNormalizesESignature(t *testing.T) {
|
||||
cache.ClearSignatureCache("")
|
||||
previous := cache.SignatureCacheEnabled()
|
||||
cache.SetSignatureCacheEnabled(false)
|
||||
t.Cleanup(func() {
|
||||
cache.SetSignatureCacheEnabled(previous)
|
||||
cache.ClearSignatureCache("")
|
||||
})
|
||||
|
||||
thinkingText := "Let me think..."
|
||||
cachedSignature := "cachedSignature1234567890123456789012345678901234567890123"
|
||||
rawSignature := testAnthropicNativeSignature(t)
|
||||
expectedSignature := base64.StdEncoding.EncodeToString([]byte(rawSignature))
|
||||
|
||||
cache.CacheSignature("claude-sonnet-4-5-thinking", thinkingText, cachedSignature)
|
||||
|
||||
inputJSON := []byte(`{
|
||||
"model": "claude-sonnet-4-5-thinking",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "` + thinkingText + `", "signature": "` + rawSignature + `"},
|
||||
{"type": "text", "text": "Answer"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
||||
outputStr := string(output)
|
||||
|
||||
part := gjson.Get(outputStr, "request.contents.0.parts.0")
|
||||
if part.Get("thoughtSignature").String() != expectedSignature {
|
||||
t.Fatalf("Expected bypass-mode signature '%s', got '%s'", expectedSignature, part.Get("thoughtSignature").String())
|
||||
}
|
||||
if part.Get("thoughtSignature").String() == cachedSignature {
|
||||
t.Fatal("Bypass mode should not reuse cached signature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToAntigravity_BypassModePreservesShortValidSignature(t *testing.T) {
|
||||
cache.ClearSignatureCache("")
|
||||
previous := cache.SignatureCacheEnabled()
|
||||
cache.SetSignatureCacheEnabled(false)
|
||||
t.Cleanup(func() {
|
||||
cache.SetSignatureCacheEnabled(previous)
|
||||
cache.ClearSignatureCache("")
|
||||
})
|
||||
|
||||
rawSignature := testMinimalAnthropicSignature(t)
|
||||
expectedSignature := base64.StdEncoding.EncodeToString([]byte(rawSignature))
|
||||
inputJSON := []byte(`{
|
||||
"model": "claude-sonnet-4-5-thinking",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "tiny", "signature": "` + rawSignature + `"},
|
||||
{"type": "text", "text": "Answer"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
||||
parts := gjson.GetBytes(output, "request.contents.0.parts").Array()
|
||||
if len(parts) != 2 {
|
||||
t.Fatalf("expected thinking part to be preserved in bypass mode, got %d parts", len(parts))
|
||||
}
|
||||
if parts[0].Get("thoughtSignature").String() != expectedSignature {
|
||||
t.Fatalf("expected normalized short signature %q, got %q", expectedSignature, parts[0].Get("thoughtSignature").String())
|
||||
}
|
||||
if !parts[0].Get("thought").Bool() {
|
||||
t.Fatalf("expected first part to remain a thought block, got %s", parts[0].Raw)
|
||||
}
|
||||
if parts[1].Get("text").String() != "Answer" {
|
||||
t.Fatalf("expected trailing text part, got %s", parts[1].Raw)
|
||||
}
|
||||
if thoughtSig := gjson.GetBytes(output, "request.contents.0.parts.1.thoughtSignature").String(); thoughtSig != "" {
|
||||
t.Fatalf("expected plain text part to have no thought signature, got %q", thoughtSig)
|
||||
}
|
||||
if functionSig := gjson.GetBytes(output, "request.contents.0.parts.0.functionCall.thoughtSignature").String(); functionSig != "" {
|
||||
t.Fatalf("unexpected functionCall payload in thinking part: %q", functionSig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectClaudeSignaturePayload_ExtractsSpecTree(t *testing.T) {
|
||||
t.Parallel()
|
||||
payload := buildClaudeSignaturePayload(t, 12, uint64Ptr(2), "claude-sonnet-4-6", true)
|
||||
|
||||
tree, err := inspectClaudeSignaturePayload(payload, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("expected structured Claude payload to parse, got: %v", err)
|
||||
}
|
||||
if tree.RoutingClass != "routing_class_12" {
|
||||
t.Fatalf("routing_class = %q, want routing_class_12", tree.RoutingClass)
|
||||
}
|
||||
if tree.InfrastructureClass != "infra_google" {
|
||||
t.Fatalf("infrastructure_class = %q, want infra_google", tree.InfrastructureClass)
|
||||
}
|
||||
if tree.SchemaFeatures != "extended_model_tagged_schema" {
|
||||
t.Fatalf("schema_features = %q, want extended_model_tagged_schema", tree.SchemaFeatures)
|
||||
}
|
||||
if tree.ModelText != "claude-sonnet-4-6" {
|
||||
t.Fatalf("model_text = %q, want claude-sonnet-4-6", tree.ModelText)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectDoubleLayerSignature_TracksEncodingLayers(t *testing.T) {
|
||||
t.Parallel()
|
||||
inner := base64.StdEncoding.EncodeToString(buildClaudeSignaturePayload(t, 11, uint64Ptr(2), "", false))
|
||||
outer := base64.StdEncoding.EncodeToString([]byte(inner))
|
||||
|
||||
tree, err := inspectDoubleLayerSignature(outer)
|
||||
if err != nil {
|
||||
t.Fatalf("expected double-layer Claude signature to parse, got: %v", err)
|
||||
}
|
||||
if tree.EncodingLayers != 2 {
|
||||
t.Fatalf("encoding_layers = %d, want 2", tree.EncodingLayers)
|
||||
}
|
||||
if tree.LegacyRouteHint != "legacy_vertex_direct" {
|
||||
t.Fatalf("legacy_route_hint = %q, want legacy_vertex_direct", tree.LegacyRouteHint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToAntigravity_CacheModeDropsRawSignature(t *testing.T) {
|
||||
cache.ClearSignatureCache("")
|
||||
previous := cache.SignatureCacheEnabled()
|
||||
cache.SetSignatureCacheEnabled(true)
|
||||
t.Cleanup(func() {
|
||||
cache.SetSignatureCacheEnabled(previous)
|
||||
cache.ClearSignatureCache("")
|
||||
})
|
||||
|
||||
rawSignature := testAnthropicNativeSignature(t)
|
||||
inputJSON := []byte(`{
|
||||
"model": "claude-sonnet-4-5-thinking",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "Let me think...", "signature": "` + rawSignature + `"},
|
||||
{"type": "text", "text": "Answer"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
||||
parts := gjson.GetBytes(output, "request.contents.0.parts").Array()
|
||||
if len(parts) != 1 {
|
||||
t.Fatalf("Expected raw signature thinking block to be dropped in cache mode, got %d parts", len(parts))
|
||||
}
|
||||
if parts[0].Get("text").String() != "Answer" {
|
||||
t.Fatalf("Expected remaining text part, got %s", parts[0].Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToAntigravity_BypassModeDropsInvalidSignature(t *testing.T) {
|
||||
cache.ClearSignatureCache("")
|
||||
previous := cache.SignatureCacheEnabled()
|
||||
cache.SetSignatureCacheEnabled(false)
|
||||
t.Cleanup(func() {
|
||||
cache.SetSignatureCacheEnabled(previous)
|
||||
cache.ClearSignatureCache("")
|
||||
})
|
||||
|
||||
invalidRawSignature := testNonAnthropicRawSignature(t)
|
||||
inputJSON := []byte(`{
|
||||
"model": "claude-sonnet-4-5-thinking",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "Let me think...", "signature": "` + invalidRawSignature + `"},
|
||||
{"type": "text", "text": "Answer"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
||||
outputStr := string(output)
|
||||
|
||||
parts := gjson.Get(outputStr, "request.contents.0.parts").Array()
|
||||
if len(parts) != 1 {
|
||||
t.Fatalf("Expected invalid thinking block to be removed, got %d parts", len(parts))
|
||||
}
|
||||
if parts[0].Get("text").String() != "Answer" {
|
||||
t.Fatalf("Expected remaining text part, got %s", parts[0].Raw)
|
||||
}
|
||||
if parts[0].Get("thought").Bool() {
|
||||
t.Fatal("Invalid raw signature should not preserve thinking block")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToAntigravity_BypassModeDropsGeminiSignature(t *testing.T) {
|
||||
cache.ClearSignatureCache("")
|
||||
previous := cache.SignatureCacheEnabled()
|
||||
cache.SetSignatureCacheEnabled(false)
|
||||
t.Cleanup(func() {
|
||||
cache.SetSignatureCacheEnabled(previous)
|
||||
cache.ClearSignatureCache("")
|
||||
})
|
||||
|
||||
geminiPayload := append([]byte{0x0A}, bytes.Repeat([]byte{0x56}, 48)...)
|
||||
geminiSig := base64.StdEncoding.EncodeToString(geminiPayload)
|
||||
inputJSON := []byte(`{
|
||||
"model": "claude-sonnet-4-5-thinking",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "thinking", "thinking": "hmm", "signature": "` + geminiSig + `"},
|
||||
{"type": "text", "text": "Answer"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`)
|
||||
|
||||
output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5-thinking", inputJSON, false)
|
||||
parts := gjson.GetBytes(output, "request.contents.0.parts").Array()
|
||||
if len(parts) != 1 {
|
||||
t.Fatalf("expected Gemini-signed thinking block to be dropped, got %d parts", len(parts))
|
||||
}
|
||||
if parts[0].Get("text").String() != "Answer" {
|
||||
t.Fatalf("expected remaining text part, got %s", parts[0].Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertClaudeRequestToAntigravity_ThinkingBlockWithoutSignature(t *testing.T) {
|
||||
cache.ClearSignatureCache("")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user