mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
Prefer cached signatures and avoid injecting dummy thinking blocks; instead remove unsigned thinking blocks and add a skip sentinel for tool calls without a valid signature. Generate stable session IDs from the first user message, apply schema cleaning only for Claude models, and reorder thinking parts so thinking appears first. For Gemini, remove thinking blocks and attach a skip sentinel to function calls. Simplify response handling by passing raw function args through (remove special Bash conversion). Update and add tests to reflect the new behavior. These changes prevent rejected dummy signatures, improve compatibility with Antigravity’s signature validation, provide more stable session IDs for conversation grouping, and make request/response translation more robust.
317 lines
8.4 KiB
Go
317 lines
8.4 KiB
Go
package claude
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
|
)
|
|
|
|
// ============================================================================
|
|
// Signature Caching Tests
|
|
// ============================================================================
|
|
|
|
func TestConvertAntigravityResponseToClaude_SessionIDDerived(t *testing.T) {
|
|
cache.ClearSignatureCache("")
|
|
|
|
// Request with user message - should derive session ID
|
|
requestJSON := []byte(`{
|
|
"messages": [
|
|
{"role": "user", "content": [{"type": "text", "text": "Hello world"}]}
|
|
]
|
|
}`)
|
|
|
|
// First response chunk with thinking
|
|
responseJSON := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": "Let me think...", "thought": true}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
var param any
|
|
ctx := context.Background()
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, responseJSON, ¶m)
|
|
|
|
// Verify session ID was set
|
|
params := param.(*Params)
|
|
if params.SessionID == "" {
|
|
t.Error("SessionID should be derived from request")
|
|
}
|
|
}
|
|
|
|
func TestConvertAntigravityResponseToClaude_ThinkingTextAccumulated(t *testing.T) {
|
|
cache.ClearSignatureCache("")
|
|
|
|
requestJSON := []byte(`{
|
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "Test"}]}]
|
|
}`)
|
|
|
|
// First thinking chunk
|
|
chunk1 := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": "First part of thinking...", "thought": true}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
// Second thinking chunk (continuation)
|
|
chunk2 := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": " Second part of thinking...", "thought": true}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
var param any
|
|
ctx := context.Background()
|
|
|
|
// Process first chunk - starts new thinking block
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, chunk1, ¶m)
|
|
params := param.(*Params)
|
|
|
|
if params.CurrentThinkingText.Len() == 0 {
|
|
t.Error("Thinking text should be accumulated after first chunk")
|
|
}
|
|
|
|
// Process second chunk - continues thinking block
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, chunk2, ¶m)
|
|
|
|
text := params.CurrentThinkingText.String()
|
|
if !strings.Contains(text, "First part") || !strings.Contains(text, "Second part") {
|
|
t.Errorf("Thinking text should accumulate both parts, got: %s", text)
|
|
}
|
|
}
|
|
|
|
func TestConvertAntigravityResponseToClaude_SignatureCached(t *testing.T) {
|
|
cache.ClearSignatureCache("")
|
|
|
|
requestJSON := []byte(`{
|
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "Cache test"}]}]
|
|
}`)
|
|
|
|
// Thinking chunk
|
|
thinkingChunk := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": "My thinking process here", "thought": true}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
// Signature chunk
|
|
validSignature := "abc123validSignature1234567890123456789012345678901234567890"
|
|
signatureChunk := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": "", "thought": true, "thoughtSignature": "` + validSignature + `"}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
var param any
|
|
ctx := context.Background()
|
|
|
|
// Process thinking chunk
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, thinkingChunk, ¶m)
|
|
params := param.(*Params)
|
|
sessionID := params.SessionID
|
|
thinkingText := params.CurrentThinkingText.String()
|
|
|
|
if sessionID == "" {
|
|
t.Fatal("SessionID should be set")
|
|
}
|
|
if thinkingText == "" {
|
|
t.Fatal("Thinking text should be accumulated")
|
|
}
|
|
|
|
// Process signature chunk - should cache the signature
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, signatureChunk, ¶m)
|
|
|
|
// Verify signature was cached
|
|
cachedSig := cache.GetCachedSignature(sessionID, thinkingText)
|
|
if cachedSig != validSignature {
|
|
t.Errorf("Expected cached signature '%s', got '%s'", validSignature, cachedSig)
|
|
}
|
|
|
|
// Verify thinking text was reset after caching
|
|
if params.CurrentThinkingText.Len() != 0 {
|
|
t.Error("Thinking text should be reset after signature is cached")
|
|
}
|
|
}
|
|
|
|
func TestConvertAntigravityResponseToClaude_MultipleThinkingBlocks(t *testing.T) {
|
|
cache.ClearSignatureCache("")
|
|
|
|
requestJSON := []byte(`{
|
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "Multi block test"}]}]
|
|
}`)
|
|
|
|
validSig1 := "signature1_12345678901234567890123456789012345678901234567"
|
|
validSig2 := "signature2_12345678901234567890123456789012345678901234567"
|
|
|
|
// First thinking block with signature
|
|
block1Thinking := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": "First thinking block", "thought": true}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
block1Sig := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": "", "thought": true, "thoughtSignature": "` + validSig1 + `"}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
// Text content (breaks thinking)
|
|
textBlock := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": "Regular text output"}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
// Second thinking block with signature
|
|
block2Thinking := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": "Second thinking block", "thought": true}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
block2Sig := []byte(`{
|
|
"response": {
|
|
"candidates": [{
|
|
"content": {
|
|
"parts": [{"text": "", "thought": true, "thoughtSignature": "` + validSig2 + `"}]
|
|
}
|
|
}]
|
|
}
|
|
}`)
|
|
|
|
var param any
|
|
ctx := context.Background()
|
|
|
|
// Process first thinking block
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, block1Thinking, ¶m)
|
|
params := param.(*Params)
|
|
sessionID := params.SessionID
|
|
firstThinkingText := params.CurrentThinkingText.String()
|
|
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, block1Sig, ¶m)
|
|
|
|
// Verify first signature cached
|
|
if cache.GetCachedSignature(sessionID, firstThinkingText) != validSig1 {
|
|
t.Error("First thinking block signature should be cached")
|
|
}
|
|
|
|
// Process text (transitions out of thinking)
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, textBlock, ¶m)
|
|
|
|
// Process second thinking block
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, block2Thinking, ¶m)
|
|
secondThinkingText := params.CurrentThinkingText.String()
|
|
|
|
ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, block2Sig, ¶m)
|
|
|
|
// Verify second signature cached
|
|
if cache.GetCachedSignature(sessionID, secondThinkingText) != validSig2 {
|
|
t.Error("Second thinking block signature should be cached")
|
|
}
|
|
}
|
|
|
|
func TestDeriveSessionIDFromRequest(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []byte
|
|
wantEmpty bool
|
|
}{
|
|
{
|
|
name: "valid user message",
|
|
input: []byte(`{"messages": [{"role": "user", "content": "Hello"}]}`),
|
|
wantEmpty: false,
|
|
},
|
|
{
|
|
name: "user message with content array",
|
|
input: []byte(`{"messages": [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]}`),
|
|
wantEmpty: false,
|
|
},
|
|
{
|
|
name: "no user message",
|
|
input: []byte(`{"messages": [{"role": "assistant", "content": "Hi"}]}`),
|
|
wantEmpty: true,
|
|
},
|
|
{
|
|
name: "empty messages",
|
|
input: []byte(`{"messages": []}`),
|
|
wantEmpty: true,
|
|
},
|
|
{
|
|
name: "no messages field",
|
|
input: []byte(`{}`),
|
|
wantEmpty: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := deriveSessionID(tt.input)
|
|
if tt.wantEmpty && result != "" {
|
|
t.Errorf("Expected empty session ID, got '%s'", result)
|
|
}
|
|
if !tt.wantEmpty && result == "" {
|
|
t.Error("Expected non-empty session ID")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeriveSessionIDFromRequest_Deterministic(t *testing.T) {
|
|
input := []byte(`{"messages": [{"role": "user", "content": "Same message"}]}`)
|
|
|
|
id1 := deriveSessionID(input)
|
|
id2 := deriveSessionID(input)
|
|
|
|
if id1 != id2 {
|
|
t.Errorf("Session ID should be deterministic: '%s' != '%s'", id1, id2)
|
|
}
|
|
}
|
|
|
|
func TestDeriveSessionIDFromRequest_DifferentMessages(t *testing.T) {
|
|
input1 := []byte(`{"messages": [{"role": "user", "content": "Message A"}]}`)
|
|
input2 := []byte(`{"messages": [{"role": "user", "content": "Message B"}]}`)
|
|
|
|
id1 := deriveSessionID(input1)
|
|
id2 := deriveSessionID(input2)
|
|
|
|
if id1 == id2 {
|
|
t.Error("Different messages should produce different session IDs")
|
|
}
|
|
}
|