@@ -134,6 +134,43 @@ func (s *httpUpstreamStub) DoWithTLS(_ *http.Request, _ string, _ int64, _ int,
134134 return s .resp , s .err
135135}
136136
137+ type queuedHTTPUpstreamStub struct {
138+ responses []* http.Response
139+ errors []error
140+ requestBodies [][]byte
141+ callCount int
142+ }
143+
144+ func (s * queuedHTTPUpstreamStub ) Do (req * http.Request , _ string , _ int64 , _ int ) (* http.Response , error ) {
145+ if req != nil && req .Body != nil {
146+ body , _ := io .ReadAll (req .Body )
147+ s .requestBodies = append (s .requestBodies , body )
148+ req .Body = io .NopCloser (bytes .NewReader (body ))
149+ } else {
150+ s .requestBodies = append (s .requestBodies , nil )
151+ }
152+
153+ idx := s .callCount
154+ s .callCount ++
155+
156+ var resp * http.Response
157+ if idx < len (s .responses ) {
158+ resp = s .responses [idx ]
159+ }
160+ var err error
161+ if idx < len (s .errors ) {
162+ err = s .errors [idx ]
163+ }
164+ if resp == nil && err == nil {
165+ return nil , errors .New ("unexpected upstream call" )
166+ }
167+ return resp , err
168+ }
169+
170+ func (s * queuedHTTPUpstreamStub ) DoWithTLS (req * http.Request , proxyURL string , accountID int64 , concurrency int , _ bool ) (* http.Response , error ) {
171+ return s .Do (req , proxyURL , accountID , concurrency )
172+ }
173+
137174type antigravitySettingRepoStub struct {}
138175
139176func (s * antigravitySettingRepoStub ) Get (ctx context.Context , key string ) (* Setting , error ) {
@@ -556,6 +593,92 @@ func TestAntigravityGatewayService_ForwardGemini_BillsWithMappedModel(t *testing
556593 require .Equal (t , mappedModel , result .Model )
557594}
558595
596+ func TestAntigravityGatewayService_ForwardGemini_RetriesCorruptedThoughtSignature (t * testing.T ) {
597+ gin .SetMode (gin .TestMode )
598+ writer := httptest .NewRecorder ()
599+ c , _ := gin .CreateTestContext (writer )
600+
601+ body , err := json .Marshal (map [string ]any {
602+ "contents" : []map [string ]any {
603+ {"role" : "user" , "parts" : []map [string ]any {{"text" : "hello" }}},
604+ {"role" : "model" , "parts" : []map [string ]any {{"text" : "thinking" , "thought" : true , "thoughtSignature" : "sig_bad_1" }}},
605+ {"role" : "model" , "parts" : []map [string ]any {{"functionCall" : map [string ]any {"name" : "toolA" , "args" : map [string ]any {"x" : 1 }}, "thoughtSignature" : "sig_bad_2" }}},
606+ },
607+ })
608+ require .NoError (t , err )
609+
610+ req := httptest .NewRequest (http .MethodPost , "/antigravity/v1beta/models/gemini-3.1-pro-preview:streamGenerateContent" , bytes .NewReader (body ))
611+ c .Request = req
612+
613+ firstRespBody := []byte (`{"response":{"error":{"code":400,"message":"Corrupted thought signature.","status":"INVALID_ARGUMENT"}}}` )
614+ secondRespBody := []byte ("data: {\" response\" :{\" candidates\" :[{\" content\" :{\" parts\" :[{\" text\" :\" ok\" }]},\" finishReason\" :\" STOP\" }],\" usageMetadata\" :{\" promptTokenCount\" :8,\" candidatesTokenCount\" :3}}}\n \n " )
615+
616+ upstream := & queuedHTTPUpstreamStub {
617+ responses : []* http.Response {
618+ {
619+ StatusCode : http .StatusBadRequest ,
620+ Header : http.Header {
621+ "Content-Type" : []string {"application/json" },
622+ "X-Request-Id" : []string {"req-sig-1" },
623+ },
624+ Body : io .NopCloser (bytes .NewReader (firstRespBody )),
625+ },
626+ {
627+ StatusCode : http .StatusOK ,
628+ Header : http.Header {
629+ "Content-Type" : []string {"text/event-stream" },
630+ "X-Request-Id" : []string {"req-sig-2" },
631+ },
632+ Body : io .NopCloser (bytes .NewReader (secondRespBody )),
633+ },
634+ },
635+ }
636+
637+ svc := & AntigravityGatewayService {
638+ settingService : NewSettingService (& antigravitySettingRepoStub {}, & config.Config {Gateway : config.GatewayConfig {MaxLineSize : defaultMaxLineSize }}),
639+ tokenProvider : & AntigravityTokenProvider {},
640+ httpUpstream : upstream ,
641+ }
642+
643+ const originalModel = "gemini-3.1-pro-preview"
644+ const mappedModel = "gemini-3.1-pro-high"
645+ account := & Account {
646+ ID : 7 ,
647+ Name : "acc-gemini-signature" ,
648+ Platform : PlatformAntigravity ,
649+ Type : AccountTypeOAuth ,
650+ Status : StatusActive ,
651+ Concurrency : 1 ,
652+ Credentials : map [string ]any {
653+ "access_token" : "token" ,
654+ "model_mapping" : map [string ]any {
655+ originalModel : mappedModel ,
656+ },
657+ },
658+ }
659+
660+ result , err := svc .ForwardGemini (context .Background (), c , account , originalModel , "streamGenerateContent" , true , body , false )
661+ require .NoError (t , err )
662+ require .NotNil (t , result )
663+ require .Equal (t , mappedModel , result .Model )
664+ require .Len (t , upstream .requestBodies , 2 , "signature error should trigger exactly one retry" )
665+
666+ firstReq := string (upstream .requestBodies [0 ])
667+ secondReq := string (upstream .requestBodies [1 ])
668+ require .Contains (t , firstReq , `"thoughtSignature":"sig_bad_1"` )
669+ require .Contains (t , firstReq , `"thoughtSignature":"sig_bad_2"` )
670+ require .Contains (t , secondReq , `"thoughtSignature":"skip_thought_signature_validator"` )
671+ require .NotContains (t , secondReq , `"thoughtSignature":"sig_bad_1"` )
672+ require .NotContains (t , secondReq , `"thoughtSignature":"sig_bad_2"` )
673+
674+ raw , ok := c .Get (OpsUpstreamErrorsKey )
675+ require .True (t , ok )
676+ events , ok := raw .([]* OpsUpstreamErrorEvent )
677+ require .True (t , ok )
678+ require .NotEmpty (t , events )
679+ require .Equal (t , "signature_error" , events [0 ].Kind )
680+ }
681+
559682// TestStreamUpstreamResponse_UsageAndFirstToken
560683// 验证:usage 字段可被累积/覆盖更新,并且能记录首 token 时间
561684func TestStreamUpstreamResponse_UsageAndFirstToken (t * testing.T ) {
0 commit comments