diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 919d96fa..f3a052bf 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "crypto/sha256" + "crypto/tls" "encoding/binary" "encoding/json" "errors" @@ -45,10 +46,10 @@ const ( antigravityModelsPath = "/v1internal:fetchAvailableModels" antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" antigravityClientSecret = "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" - defaultAntigravityAgent = "antigravity/1.104.0 darwin/arm64" + defaultAntigravityAgent = "antigravity/1.19.6 darwin/arm64" antigravityAuthType = "antigravity" refreshSkew = 3000 * time.Second - systemInstruction = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**" + // systemInstruction = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**" ) var ( @@ -142,6 +143,62 @@ func NewAntigravityExecutor(cfg *config.Config) *AntigravityExecutor { return &AntigravityExecutor{cfg: cfg} } +// antigravityTransport is a singleton HTTP/1.1 transport shared by all Antigravity requests. +// It is initialized once via antigravityTransportOnce to avoid leaking a new connection pool +// (and the goroutines managing it) on every request. +var ( + antigravityTransport *http.Transport + antigravityTransportOnce sync.Once +) + +func cloneTransportWithHTTP11(base *http.Transport) *http.Transport { + if base == nil { + return nil + } + + clone := base.Clone() + clone.ForceAttemptHTTP2 = false + // Wipe TLSNextProto to prevent implicit HTTP/2 upgrade. + clone.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) + if clone.TLSClientConfig == nil { + clone.TLSClientConfig = &tls.Config{} + } else { + clone.TLSClientConfig = clone.TLSClientConfig.Clone() + } + // Actively advertise only HTTP/1.1 in the ALPN handshake. + clone.TLSClientConfig.NextProtos = []string{"http/1.1"} + return clone +} + +// initAntigravityTransport creates the shared HTTP/1.1 transport exactly once. +func initAntigravityTransport() { + base, ok := http.DefaultTransport.(*http.Transport) + if !ok { + base = &http.Transport{} + } + antigravityTransport = cloneTransportWithHTTP11(base) +} + +// newAntigravityHTTPClient creates an HTTP client specifically for Antigravity, +// enforcing HTTP/1.1 by disabling HTTP/2 to perfectly mimic Node.js https defaults. +// The underlying Transport is a singleton to avoid leaking connection pools. +func newAntigravityHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client { + antigravityTransportOnce.Do(initAntigravityTransport) + + client := newProxyAwareHTTPClient(ctx, cfg, auth, timeout) + // If no transport is set, use the shared HTTP/1.1 transport. + if client.Transport == nil { + client.Transport = antigravityTransport + return client + } + + // Preserve proxy settings from proxy-aware transports while forcing HTTP/1.1. + if transport, ok := client.Transport.(*http.Transport); ok { + client.Transport = cloneTransportWithHTTP11(transport) + } + return client +} + // Identifier returns the executor identifier. func (e *AntigravityExecutor) Identifier() string { return antigravityAuthType } @@ -162,6 +219,8 @@ func (e *AntigravityExecutor) PrepareRequest(req *http.Request, auth *cliproxyau } // HttpRequest injects Antigravity credentials into the request and executes it. +// It uses a whitelist approach: all incoming headers are stripped and only +// the minimum set required by the Antigravity protocol is explicitly set. func (e *AntigravityExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) { if req == nil { return nil, fmt.Errorf("antigravity executor: request is nil") @@ -170,10 +229,29 @@ func (e *AntigravityExecutor) HttpRequest(ctx context.Context, auth *cliproxyaut ctx = req.Context() } httpReq := req.WithContext(ctx) + + // --- Whitelist: save only the headers we need from the original request --- + contentType := httpReq.Header.Get("Content-Type") + + // Wipe ALL incoming headers + for k := range httpReq.Header { + delete(httpReq.Header, k) + } + + // --- Set only the headers Antigravity actually sends --- + if contentType != "" { + httpReq.Header.Set("Content-Type", contentType) + } + // Content-Length is managed automatically by Go's http.Client from the Body + httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) + httpReq.Close = true // sends Connection: close + + // Inject Authorization: Bearer if err := e.PrepareRequest(httpReq, auth); err != nil { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + + httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -185,7 +263,7 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au baseModel := thinking.ParseSuffix(req.Model).ModelName isClaude := strings.Contains(strings.ToLower(baseModel), "claude") - if isClaude || strings.Contains(baseModel, "gemini-3-pro") { + if isClaude || strings.Contains(baseModel, "gemini-3-pro") || strings.Contains(baseModel, "gemini-3.1-flash-image") { return e.executeClaudeNonStream(ctx, auth, req, opts) } @@ -220,7 +298,7 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) baseURLs := antigravityBaseURLFallbackOrder(auth) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) attempts := antigravityRetryAttempts(auth, e.cfg) @@ -362,7 +440,7 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth * translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) baseURLs := antigravityBaseURLFallbackOrder(auth) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) attempts := antigravityRetryAttempts(auth, e.cfg) @@ -754,7 +832,7 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) baseURLs := antigravityBaseURLFallbackOrder(auth) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) attempts := antigravityRetryAttempts(auth, e.cfg) @@ -956,7 +1034,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut payload = deleteJSONField(payload, "request.safetySettings") baseURLs := antigravityBaseURLFallbackOrder(auth) - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) var authID, authLabel, authType, authValue string if auth != nil { @@ -987,10 +1065,10 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut if errReq != nil { return cliproxyexecutor.Response{}, errReq } + httpReq.Close = true httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) - httpReq.Header.Set("Accept", "application/json") if host := resolveHost(base); host != "" { httpReq.Host = host } @@ -1084,14 +1162,26 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c } baseURLs := antigravityBaseURLFallbackOrder(auth) - httpClient := newProxyAwareHTTPClient(ctx, cfg, auth, 0) + httpClient := newAntigravityHTTPClient(ctx, cfg, auth, 0) for idx, baseURL := range baseURLs { modelsURL := baseURL + antigravityModelsPath - httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, modelsURL, bytes.NewReader([]byte(`{}`))) + + var payload []byte + if auth != nil && auth.Metadata != nil { + if pid, ok := auth.Metadata["project_id"].(string); ok && strings.TrimSpace(pid) != "" { + payload = []byte(fmt.Sprintf(`{"project": "%s"}`, strings.TrimSpace(pid))) + } + } + if len(payload) == 0 { + payload = []byte(`{}`) + } + + httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, modelsURL, bytes.NewReader(payload)) if errReq != nil { return fallbackAntigravityPrimaryModels() } + httpReq.Close = true httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) @@ -1174,6 +1264,29 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c OwnedBy: antigravityAuthType, Type: antigravityAuthType, } + + // Build input modalities from upstream capability flags. + inputModalities := []string{"TEXT"} + if modelData.Get("supportsImages").Bool() { + inputModalities = append(inputModalities, "IMAGE") + } + if modelData.Get("supportsVideo").Bool() { + inputModalities = append(inputModalities, "VIDEO") + } + modelInfo.SupportedInputModalities = inputModalities + modelInfo.SupportedOutputModalities = []string{"TEXT"} + + // Token limits from upstream. + if maxTok := modelData.Get("maxTokens").Int(); maxTok > 0 { + modelInfo.InputTokenLimit = int(maxTok) + } + if maxOut := modelData.Get("maxOutputTokens").Int(); maxOut > 0 { + modelInfo.OutputTokenLimit = int(maxOut) + } + + // Supported generation methods (Gemini v1beta convention). + modelInfo.SupportedGenerationMethods = []string{"generateContent", "countTokens"} + // Look up Thinking support from static config using upstream model name. if modelCfg != nil { if modelCfg.Thinking != nil { @@ -1241,10 +1354,11 @@ func (e *AntigravityExecutor) refreshToken(ctx context.Context, auth *cliproxyau return auth, errReq } httpReq.Header.Set("Host", "oauth2.googleapis.com") - httpReq.Header.Set("User-Agent", defaultAntigravityAgent) httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + // Real Antigravity uses Go's default User-Agent for OAuth token refresh + httpReq.Header.Set("User-Agent", "Go-http-client/2.0") - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { return auth, errDo @@ -1315,7 +1429,7 @@ func (e *AntigravityExecutor) ensureAntigravityProjectID(ctx context.Context, au return nil } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) projectID, errFetch := sdkAuth.FetchAntigravityProjectID(ctx, token, httpClient) if errFetch != nil { return errFetch @@ -1369,7 +1483,7 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau payload = geminiToAntigravity(modelName, payload, projectID) payload, _ = sjson.SetBytes(payload, "model", modelName) - useAntigravitySchema := strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") + useAntigravitySchema := strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro") || strings.Contains(modelName, "gemini-3.1-pro") payloadStr := string(payload) paths := make([]string, 0) util.Walk(gjson.Parse(payloadStr), "", "parametersJsonSchema", &paths) @@ -1383,18 +1497,18 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau payloadStr = util.CleanJSONSchemaForGemini(payloadStr) } - if useAntigravitySchema { - systemInstructionPartsResult := gjson.Get(payloadStr, "request.systemInstruction.parts") - payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.role", "user") - payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.parts.0.text", systemInstruction) - payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.parts.1.text", fmt.Sprintf("Please ignore following [ignore]%s[/ignore]", systemInstruction)) + // if useAntigravitySchema { + // systemInstructionPartsResult := gjson.Get(payloadStr, "request.systemInstruction.parts") + // payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.role", "user") + // payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.parts.0.text", systemInstruction) + // payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.parts.1.text", fmt.Sprintf("Please ignore following [ignore]%s[/ignore]", systemInstruction)) - if systemInstructionPartsResult.Exists() && systemInstructionPartsResult.IsArray() { - for _, partResult := range systemInstructionPartsResult.Array() { - payloadStr, _ = sjson.SetRaw(payloadStr, "request.systemInstruction.parts.-1", partResult.Raw) - } - } - } + // if systemInstructionPartsResult.Exists() && systemInstructionPartsResult.IsArray() { + // for _, partResult := range systemInstructionPartsResult.Array() { + // payloadStr, _ = sjson.SetRaw(payloadStr, "request.systemInstruction.parts.-1", partResult.Raw) + // } + // } + // } if strings.Contains(modelName, "claude") { payloadStr, _ = sjson.Set(payloadStr, "request.toolConfig.functionCallingConfig.mode", "VALIDATED") @@ -1406,14 +1520,10 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau if errReq != nil { return nil, errReq } + httpReq.Close = true httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) - if stream { - httpReq.Header.Set("Accept", "text/event-stream") - } else { - httpReq.Header.Set("Accept", "application/json") - } if host := resolveHost(base); host != "" { httpReq.Host = host } @@ -1625,7 +1735,16 @@ func resolveCustomAntigravityBaseURL(auth *cliproxyauth.Auth) string { func geminiToAntigravity(modelName string, payload []byte, projectID string) []byte { template, _ := sjson.Set(string(payload), "model", modelName) template, _ = sjson.Set(template, "userAgent", "antigravity") - template, _ = sjson.Set(template, "requestType", "agent") + + isImageModel := strings.Contains(modelName, "image") + + var reqType string + if isImageModel { + reqType = "image_gen" + } else { + reqType = "agent" + } + template, _ = sjson.Set(template, "requestType", reqType) // Use real project ID from auth if available, otherwise generate random (legacy fallback) if projectID != "" { @@ -1633,8 +1752,13 @@ func geminiToAntigravity(modelName string, payload []byte, projectID string) []b } else { template, _ = sjson.Set(template, "project", generateProjectID()) } - template, _ = sjson.Set(template, "requestId", generateRequestID()) - template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload)) + + if isImageModel { + template, _ = sjson.Set(template, "requestId", generateImageGenRequestID()) + } else { + template, _ = sjson.Set(template, "requestId", generateRequestID()) + template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload)) + } template, _ = sjson.Delete(template, "request.safetySettings") if toolConfig := gjson.Get(template, "toolConfig"); toolConfig.Exists() && !gjson.Get(template, "request.toolConfig").Exists() { @@ -1648,6 +1772,10 @@ func generateRequestID() string { return "agent-" + uuid.NewString() } +func generateImageGenRequestID() string { + return fmt.Sprintf("image_gen/%d/%s/12", time.Now().UnixMilli(), uuid.NewString()) +} + func generateSessionID() string { randSourceMutex.Lock() n := randSource.Int63n(9_000_000_000_000_000_000)