diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 64d1995178..a7e564bd47 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -1318,6 +1318,12 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau payload, _ = sjson.DeleteBytes(payload, "request.generationConfig.maxOutputTokens") } + // Sanitize request.contents to remove any invalid entries that contain + // request-level metadata fields (safetySettings, model, systemInstruction, etc.) + // This prevents "Invalid JSON payload" errors from the Gemini/Antigravity API + // when malformed history entries are accidentally included in contents. + payload = sanitizeRequestContents(payload) + httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(payload)) if errReq != nil { return nil, errReq @@ -1594,3 +1600,69 @@ func generateProjectID() string { randomPart := strings.ToLower(uuid.NewString())[:5] return adj + "-" + noun + "-" + randomPart } + +// sanitizeRequestContents removes invalid entries from request.contents that contain +// request-level metadata fields instead of proper message content. +// Valid content entries should only have: role, parts +// Invalid entries may contain: safetySettings, model, userAgent, requestType, requestId, +// sessionId, systemInstruction, toolConfig, generationConfig, etc. +func sanitizeRequestContents(payload []byte) []byte { + contentsPath := "request.contents" + contentsResult := gjson.GetBytes(payload, contentsPath) + if !contentsResult.Exists() || !contentsResult.IsArray() { + return payload + } + + invalidFieldsSet := map[string]bool{ + "safetySettings": true, + "model": true, + "userAgent": true, + "requestType": true, + "requestId": true, + "sessionId": true, + "systemInstruction": true, + "toolConfig": true, + "generationConfig": true, + "project": true, + "request": true, + "contents": true, + } + + validContents := make([]gjson.Result, 0) + contentsResult.ForEach(func(_, content gjson.Result) bool { + if !content.IsObject() { + return true + } + + hasInvalidField := false + content.ForEach(func(key, _ gjson.Result) bool { + if invalidFieldsSet[key.String()] { + hasInvalidField = true + log.Warnf("sanitizeRequestContents: dropping invalid content entry with field %q", key.String()) + return false + } + return true + }) + + if !hasInvalidField { + validContents = append(validContents, content) + } + return true + }) + + if len(validContents) == len(contentsResult.Array()) { + return payload + } + + newContentsJSON := "[" + for i, content := range validContents { + if i > 0 { + newContentsJSON += "," + } + newContentsJSON += content.Raw + } + newContentsJSON += "]" + + result, _ := sjson.SetRawBytes(payload, contentsPath, []byte(newContentsJSON)) + return result +} diff --git a/internal/runtime/executor/antigravity_executor_test.go b/internal/runtime/executor/antigravity_executor_test.go new file mode 100644 index 0000000000..87bb4974f7 --- /dev/null +++ b/internal/runtime/executor/antigravity_executor_test.go @@ -0,0 +1,140 @@ +package executor + +import ( + "testing" + + "github.com/tidwall/gjson" +) + +func TestSanitizeRequestContents(t *testing.T) { + tests := []struct { + name string + input string + expectedCount int + shouldModify bool + }{ + { + name: "valid contents unchanged", + input: `{ + "request": { + "contents": [ + {"role": "user", "parts": [{"text": "hello"}]}, + {"role": "model", "parts": [{"text": "hi"}]} + ] + } + }`, + expectedCount: 2, + shouldModify: false, + }, + { + name: "removes entry with safetySettings", + input: `{ + "request": { + "contents": [ + {"role": "user", "parts": [{"text": "hello"}]}, + {"safetySettings": [], "model": "test"} + ] + } + }`, + expectedCount: 1, + shouldModify: true, + }, + { + name: "removes entry with model field", + input: `{ + "request": { + "contents": [ + {"role": "user", "parts": [{"text": "hello"}]}, + {"model": "gemini-pro", "userAgent": "test"} + ] + } + }`, + expectedCount: 1, + shouldModify: true, + }, + { + name: "removes entry with systemInstruction", + input: `{ + "request": { + "contents": [ + {"systemInstruction": {}, "toolConfig": {}} + ] + } + }`, + expectedCount: 0, + shouldModify: true, + }, + { + name: "removes entry with request metadata fields", + input: `{ + "request": { + "contents": [ + {"role": "user", "parts": [{"text": "hello"}]}, + {"requestId": "123", "requestType": "agent", "sessionId": "456"} + ] + } + }`, + expectedCount: 1, + shouldModify: true, + }, + { + name: "keeps function call/response entries", + input: `{ + "request": { + "contents": [ + {"role": "user", "parts": [{"text": "hello"}]}, + {"role": "model", "parts": [{"functionCall": {"name": "test", "args": {}}}]}, + {"role": "function", "parts": [{"functionResponse": {"name": "test", "response": {}}}]} + ] + } + }`, + expectedCount: 3, + shouldModify: false, + }, + { + name: "handles empty contents", + input: `{"request": {"contents": []}}`, + expectedCount: 0, + shouldModify: false, + }, + { + name: "handles missing contents", + input: `{"request": {}}`, + expectedCount: -1, + shouldModify: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sanitizeRequestContents([]byte(tt.input)) + + contentsResult := gjson.GetBytes(result, "request.contents") + + if tt.expectedCount == -1 { + if contentsResult.Exists() { + t.Errorf("expected no contents field, but got one") + } + return + } + + if !contentsResult.IsArray() { + t.Fatalf("expected contents to be an array") + } + + actualCount := len(contentsResult.Array()) + if actualCount != tt.expectedCount { + t.Errorf("expected %d contents, got %d", tt.expectedCount, actualCount) + } + + for i, content := range contentsResult.Array() { + invalidFields := []string{"safetySettings", "model", "userAgent", "requestType", "requestId", "sessionId", "systemInstruction", "toolConfig", "generationConfig", "project", "request", "contents"} + for _, field := range invalidFields { + if content.Get(field).Exists() { + t.Errorf("content[%d] should not have field %q", i, field) + } + } + } + }) + } +}