fix(antigravity): strip thinking blocks with empty signatures instead of rejecting
Thinking blocks with empty signatures come from proxy-generated responses (Antigravity/Gemini routed as Claude). These should be silently dropped from the request payload before forwarding, not rejected with 400. Fixes 10 "missing thinking signature" errors.
This commit is contained in:
@@ -184,22 +184,24 @@ func newAntigravityHTTPClient(ctx context.Context, cfg *config.Config, auth *cli
|
|||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateAntigravityRequestSignatures(from sdktranslator.Format, rawJSON []byte) error {
|
func validateAntigravityRequestSignatures(from sdktranslator.Format, rawJSON []byte) ([]byte, error) {
|
||||||
if from.String() != "claude" {
|
if from.String() != "claude" {
|
||||||
return nil
|
return rawJSON, nil
|
||||||
}
|
}
|
||||||
|
// Always strip thinking blocks with empty signatures (proxy-generated).
|
||||||
|
rawJSON = antigravityclaude.StripEmptySignatureThinkingBlocks(rawJSON)
|
||||||
if cache.SignatureCacheEnabled() {
|
if cache.SignatureCacheEnabled() {
|
||||||
return nil
|
return rawJSON, nil
|
||||||
}
|
}
|
||||||
if !cache.SignatureBypassStrictMode() {
|
if !cache.SignatureBypassStrictMode() {
|
||||||
// Non-strict bypass: let the translator handle invalid signatures
|
// Non-strict bypass: let the translator handle invalid signatures
|
||||||
// by dropping unsigned thinking blocks silently (no 400).
|
// by dropping unsigned thinking blocks silently (no 400).
|
||||||
return nil
|
return rawJSON, nil
|
||||||
}
|
}
|
||||||
if err := antigravityclaude.ValidateClaudeBypassSignatures(rawJSON); err != nil {
|
if err := antigravityclaude.ValidateClaudeBypassSignatures(rawJSON); err != nil {
|
||||||
return statusErr{code: http.StatusBadRequest, msg: err.Error()}
|
return rawJSON, statusErr{code: http.StatusBadRequest, msg: err.Error()}
|
||||||
}
|
}
|
||||||
return nil
|
return rawJSON, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Identifier returns the executor identifier.
|
// Identifier returns the executor identifier.
|
||||||
@@ -695,9 +697,11 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
originalPayloadSource = opts.OriginalRequest
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
originalPayload := originalPayloadSource
|
originalPayload := originalPayloadSource
|
||||||
if errValidate := validateAntigravityRequestSignatures(from, originalPayload); errValidate != nil {
|
originalPayload, errValidate := validateAntigravityRequestSignatures(from, originalPayload)
|
||||||
|
if errValidate != nil {
|
||||||
return resp, errValidate
|
return resp, errValidate
|
||||||
}
|
}
|
||||||
|
req.Payload = originalPayload
|
||||||
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
||||||
if errToken != nil {
|
if errToken != nil {
|
||||||
return resp, errToken
|
return resp, errToken
|
||||||
@@ -907,9 +911,11 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
|
|||||||
originalPayloadSource = opts.OriginalRequest
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
originalPayload := originalPayloadSource
|
originalPayload := originalPayloadSource
|
||||||
if errValidate := validateAntigravityRequestSignatures(from, originalPayload); errValidate != nil {
|
originalPayload, errValidate := validateAntigravityRequestSignatures(from, originalPayload)
|
||||||
|
if errValidate != nil {
|
||||||
return resp, errValidate
|
return resp, errValidate
|
||||||
}
|
}
|
||||||
|
req.Payload = originalPayload
|
||||||
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
||||||
if errToken != nil {
|
if errToken != nil {
|
||||||
return resp, errToken
|
return resp, errToken
|
||||||
@@ -1370,9 +1376,11 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
|
|||||||
originalPayloadSource = opts.OriginalRequest
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
originalPayload := originalPayloadSource
|
originalPayload := originalPayloadSource
|
||||||
if errValidate := validateAntigravityRequestSignatures(from, originalPayload); errValidate != nil {
|
originalPayload, errValidate := validateAntigravityRequestSignatures(from, originalPayload)
|
||||||
|
if errValidate != nil {
|
||||||
return nil, errValidate
|
return nil, errValidate
|
||||||
}
|
}
|
||||||
|
req.Payload = originalPayload
|
||||||
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
||||||
if errToken != nil {
|
if errToken != nil {
|
||||||
return nil, errToken
|
return nil, errToken
|
||||||
@@ -1626,9 +1634,11 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
|
|||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayloadSource = opts.OriginalRequest
|
originalPayloadSource = opts.OriginalRequest
|
||||||
}
|
}
|
||||||
if errValidate := validateAntigravityRequestSignatures(from, originalPayloadSource); errValidate != nil {
|
originalPayloadSource, errValidate := validateAntigravityRequestSignatures(from, originalPayloadSource)
|
||||||
|
if errValidate != nil {
|
||||||
return cliproxyexecutor.Response{}, errValidate
|
return cliproxyexecutor.Response{}, errValidate
|
||||||
}
|
}
|
||||||
|
req.Payload = originalPayloadSource
|
||||||
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
token, updatedAuth, errToken := e.ensureAccessToken(ctx, auth)
|
||||||
if errToken != nil {
|
if errToken != nil {
|
||||||
return cliproxyexecutor.Response{}, errToken
|
return cliproxyexecutor.Response{}, errToken
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func TestAntigravityExecutor_NonStrictBypassSkipsPrecheck(t *testing.T) {
|
|||||||
payload := invalidClaudeThinkingPayload()
|
payload := invalidClaudeThinkingPayload()
|
||||||
from := sdktranslator.FromString("claude")
|
from := sdktranslator.FromString("claude")
|
||||||
|
|
||||||
err := validateAntigravityRequestSignatures(from, payload)
|
_, err := validateAntigravityRequestSignatures(from, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("non-strict bypass should skip precheck, got: %v", err)
|
t.Fatalf("non-strict bypass should skip precheck, got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ func TestAntigravityExecutor_CacheModeSkipsPrecheck(t *testing.T) {
|
|||||||
payload := invalidClaudeThinkingPayload()
|
payload := invalidClaudeThinkingPayload()
|
||||||
from := sdktranslator.FromString("claude")
|
from := sdktranslator.FromString("claude")
|
||||||
|
|
||||||
err := validateAntigravityRequestSignatures(from, payload)
|
_, err := validateAntigravityRequestSignatures(from, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("cache mode should skip precheck, got: %v", err)
|
t.Fatalf("cache mode should skip precheck, got: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import (
|
|||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
"github.com/tidwall/sjson"
|
||||||
"google.golang.org/protobuf/encoding/protowire"
|
"google.golang.org/protobuf/encoding/protowire"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,6 +73,44 @@ type claudeSignatureTree struct {
|
|||||||
HasField7 bool
|
HasField7 bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StripEmptySignatureThinkingBlocks removes thinking blocks with empty signatures
|
||||||
|
// from messages[].content[]. These come from proxy-generated responses (Antigravity/Gemini)
|
||||||
|
// where no real Claude signature exists.
|
||||||
|
func StripEmptySignatureThinkingBlocks(payload []byte) []byte {
|
||||||
|
messages := gjson.GetBytes(payload, "messages")
|
||||||
|
if !messages.IsArray() {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
modified := false
|
||||||
|
for i, msg := range messages.Array() {
|
||||||
|
content := msg.Get("content")
|
||||||
|
if !content.IsArray() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var kept []string
|
||||||
|
stripped := false
|
||||||
|
for _, part := range content.Array() {
|
||||||
|
if part.Get("type").String() == "thinking" && strings.TrimSpace(part.Get("signature").String()) == "" {
|
||||||
|
stripped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kept = append(kept, part.Raw)
|
||||||
|
}
|
||||||
|
if stripped {
|
||||||
|
modified = true
|
||||||
|
if len(kept) == 0 {
|
||||||
|
payload, _ = sjson.SetRawBytes(payload, fmt.Sprintf("messages.%d.content", i), []byte("[]"))
|
||||||
|
} else {
|
||||||
|
payload, _ = sjson.SetRawBytes(payload, fmt.Sprintf("messages.%d.content", i), []byte("["+strings.Join(kept, ",")+"]"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !modified {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
func ValidateClaudeBypassSignatures(inputRawJSON []byte) error {
|
func ValidateClaudeBypassSignatures(inputRawJSON []byte) error {
|
||||||
messages := gjson.GetBytes(inputRawJSON, "messages")
|
messages := gjson.GetBytes(inputRawJSON, "messages")
|
||||||
if !messages.IsArray() {
|
if !messages.IsArray() {
|
||||||
|
|||||||
Reference in New Issue
Block a user