From dd71c73a9f4d6960e55929f2f7b97b102804279a Mon Sep 17 00:00:00 2001 From: maplelove Date: Sun, 22 Feb 2026 17:07:17 +0800 Subject: [PATCH 01/14] fix: align gemini-cli upstream communication headers Removed legacy Client-Metadata and explicit API-Client headers. Dynamically generating accurate User-Agent strings matching the official cli. --- .../api/handlers/management/auth_files.go | 16 ++++++------- internal/cmd/login.go | 16 ++++++------- .../runtime/executor/gemini_cli_executor.go | 24 +++++++++---------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 7f7fad15..e133a436 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -13,6 +13,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" "sort" "strconv" "strings" @@ -47,11 +48,12 @@ const ( codexCallbackPort = 1455 geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" geminiCLIVersion = "v1internal" - geminiCLIUserAgent = "google-api-nodejs-client/9.15.1" - geminiCLIApiClient = "gl-node/22.17.0" - geminiCLIClientMetadata = "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI" ) +func getGeminiCLIUserAgent() string { + return fmt.Sprintf("GeminiCLI/1.0.0/unknown (%s; %s)", runtime.GOOS, runtime.GOARCH) +} + type callbackForwarder struct { provider string server *http.Server @@ -2270,9 +2272,7 @@ func callGeminiCLI(ctx context.Context, httpClient *http.Client, endpoint string return fmt.Errorf("create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", geminiCLIUserAgent) - req.Header.Set("X-Goog-Api-Client", geminiCLIApiClient) - req.Header.Set("Client-Metadata", geminiCLIClientMetadata) + req.Header.Set("User-Agent", getGeminiCLIUserAgent()) resp, errDo := httpClient.Do(req) if errDo != nil { @@ -2342,7 +2342,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec return false, fmt.Errorf("failed to create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", geminiCLIUserAgent) + req.Header.Set("User-Agent", getGeminiCLIUserAgent()) resp, errDo := httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) @@ -2363,7 +2363,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec return false, fmt.Errorf("failed to create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", geminiCLIUserAgent) + req.Header.Set("User-Agent", getGeminiCLIUserAgent()) resp, errDo = httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) diff --git a/internal/cmd/login.go b/internal/cmd/login.go index 1d8a1ae3..5f4061b2 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -13,6 +13,7 @@ import ( "io" "net/http" "os" + "runtime" "strconv" "strings" "time" @@ -29,11 +30,12 @@ import ( const ( geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" geminiCLIVersion = "v1internal" - geminiCLIUserAgent = "google-api-nodejs-client/9.15.1" - geminiCLIApiClient = "gl-node/22.17.0" - geminiCLIClientMetadata = "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI" ) +func getGeminiCLIUserAgent() string { + return fmt.Sprintf("GeminiCLI/1.0.0/unknown (%s; %s)", runtime.GOOS, runtime.GOARCH) +} + type projectSelectionRequiredError struct{} func (e *projectSelectionRequiredError) Error() string { @@ -409,9 +411,7 @@ func callGeminiCLI(ctx context.Context, httpClient *http.Client, endpoint string return fmt.Errorf("create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", geminiCLIUserAgent) - req.Header.Set("X-Goog-Api-Client", geminiCLIApiClient) - req.Header.Set("Client-Metadata", geminiCLIClientMetadata) + req.Header.Set("User-Agent", getGeminiCLIUserAgent()) resp, errDo := httpClient.Do(req) if errDo != nil { @@ -630,7 +630,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec return false, fmt.Errorf("failed to create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", geminiCLIUserAgent) + req.Header.Set("User-Agent", getGeminiCLIUserAgent()) resp, errDo := httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) @@ -651,7 +651,7 @@ func checkCloudAPIIsEnabled(ctx context.Context, httpClient *http.Client, projec return false, fmt.Errorf("failed to create request: %w", errRequest) } req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", geminiCLIUserAgent) + req.Header.Set("User-Agent", getGeminiCLIUserAgent()) resp, errDo = httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index cb3ffb59..3746ae8a 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -12,6 +12,7 @@ import ( "io" "net/http" "regexp" + "runtime" "strconv" "strings" "time" @@ -81,7 +82,7 @@ func (e *GeminiCLIExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth return statusErr{code: http.StatusUnauthorized, msg: "missing access token"} } req.Header.Set("Authorization", "Bearer "+tok.AccessToken) - applyGeminiCLIHeaders(req) + applyGeminiCLIHeaders(req, "unknown") return nil } @@ -189,7 +190,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth } reqHTTP.Header.Set("Content-Type", "application/json") reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) - applyGeminiCLIHeaders(reqHTTP) + applyGeminiCLIHeaders(reqHTTP, attemptModel) reqHTTP.Header.Set("Accept", "application/json") recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ URL: url, @@ -334,7 +335,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut } reqHTTP.Header.Set("Content-Type", "application/json") reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) - applyGeminiCLIHeaders(reqHTTP) + applyGeminiCLIHeaders(reqHTTP, attemptModel) reqHTTP.Header.Set("Accept", "text/event-stream") recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ URL: url, @@ -515,7 +516,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. } reqHTTP.Header.Set("Content-Type", "application/json") reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) - applyGeminiCLIHeaders(reqHTTP) + applyGeminiCLIHeaders(reqHTTP, baseModel) reqHTTP.Header.Set("Accept", "application/json") recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ URL: url, @@ -738,21 +739,18 @@ func stringValue(m map[string]any, key string) string { } // applyGeminiCLIHeaders sets required headers for the Gemini CLI upstream. -func applyGeminiCLIHeaders(r *http.Request) { +func applyGeminiCLIHeaders(r *http.Request, model string) { var ginHeaders http.Header if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { ginHeaders = ginCtx.Request.Header } - misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", "google-api-nodejs-client/9.15.1") - misc.EnsureHeader(r.Header, ginHeaders, "X-Goog-Api-Client", "gl-node/22.17.0") - misc.EnsureHeader(r.Header, ginHeaders, "Client-Metadata", geminiCLIClientMetadata()) -} + if model == "" { + model = "unknown" + } -// geminiCLIClientMetadata returns a compact metadata string required by upstream. -func geminiCLIClientMetadata() string { - // Keep parity with CLI client defaults - return "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI" + userAgent := fmt.Sprintf("GeminiCLI/1.0.0/%s (%s; %s)", model, runtime.GOOS, runtime.GOARCH) + misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", userAgent) } // cliPreviewFallbackOrder returns preview model candidates for a base model. From c8d809131bc45b790114ba47914de370fb7b8dce Mon Sep 17 00:00:00 2001 From: maplelove Date: Sun, 22 Feb 2026 18:41:58 +0800 Subject: [PATCH 02/14] fix(executor): improve antigravity reverse proxy emulation - force http/1.1 instead of http/2 - explicit connection close - strip proxy headers X-Forwarded-For and X-Real-IP - add project id to fetch models payload --- internal/api/modules/amp/proxy.go | 4 ++ .../runtime/executor/antigravity_executor.go | 69 ++++++++++++++----- 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/internal/api/modules/amp/proxy.go b/internal/api/modules/amp/proxy.go index c460a0d6..d298e255 100644 --- a/internal/api/modules/amp/proxy.go +++ b/internal/api/modules/amp/proxy.go @@ -73,6 +73,10 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi req.Header.Del("Authorization") req.Header.Del("X-Api-Key") req.Header.Del("X-Goog-Api-Key") + + // Remove proxy tracing headers to avoid upstream detection + req.Header.Del("X-Forwarded-For") + req.Header.Del("X-Real-IP") // Remove query-based credentials if they match the authenticated client API key. // This prevents leaking client auth material to the Amp upstream while avoiding diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 9d395a9c..749bbbc3 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.18.4 windows/amd64" 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. The USER will send you requests, which you must always prioritize addressing. Along with each USER request, we will attach additional metadata about their current state, such as what files they have open and where their cursor is. This information may or may not be relevant to the coding task, it is up for you to decide. " ) var ( @@ -72,6 +73,22 @@ func NewAntigravityExecutor(cfg *config.Config) *AntigravityExecutor { return &AntigravityExecutor{cfg: cfg} } +// newAntigravityHTTPClient creates an HTTP client specifically for Antigravity, +// enforcing HTTP/1.1 by disabling HTTP/2 to perfectly mimic Node.js https defaults. +func newAntigravityHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client { + client := newProxyAwareHTTPClient(ctx, cfg, auth, timeout) + if client.Transport == nil { + client.Transport = http.DefaultTransport + } + if tr, ok := client.Transport.(*http.Transport); ok { + trClone := tr.Clone() + trClone.ForceAttemptHTTP2 = false + trClone.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) + client.Transport = trClone + } + return client +} + // Identifier returns the executor identifier. func (e *AntigravityExecutor) Identifier() string { return antigravityAuthType } @@ -103,7 +120,11 @@ func (e *AntigravityExecutor) HttpRequest(ctx context.Context, auth *cliproxyaut if err := e.PrepareRequest(httpReq, auth); err != nil { return nil, err } - httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) + httpReq.Close = true + httpReq.Header.Del("Accept") + httpReq.Header.Del("X-Forwarded-For") + httpReq.Header.Del("X-Real-IP") + httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -150,7 +171,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) @@ -292,7 +313,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) @@ -684,7 +705,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) @@ -886,7 +907,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 { @@ -917,10 +938,12 @@ 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") + httpReq.Header.Del("X-Forwarded-For") + httpReq.Header.Del("X-Real-IP") if host := resolveHost(base); host != "" { httpReq.Host = host } @@ -1014,17 +1037,31 @@ 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 nil } + 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.Del("X-Forwarded-For") + httpReq.Header.Del("X-Real-IP") if host := resolveHost(baseURL); host != "" { httpReq.Host = host } @@ -1157,7 +1194,7 @@ func (e *AntigravityExecutor) refreshToken(ctx context.Context, auth *cliproxyau httpReq.Header.Set("User-Agent", defaultAntigravityAgent) httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") - 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 @@ -1228,7 +1265,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 @@ -1319,14 +1356,12 @@ 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") - } + httpReq.Header.Del("X-Forwarded-For") + httpReq.Header.Del("X-Real-IP") if host := resolveHost(base); host != "" { httpReq.Host = host } From abb51a0d93732b85cdc74f9c82ebadef44f3cc32 Mon Sep 17 00:00:00 2001 From: maplelove Date: Sun, 22 Feb 2026 19:23:48 +0800 Subject: [PATCH 03/14] fix(executor): correctly disable http2 ALPN in Antigravity client to resolve connection reset errors --- internal/runtime/executor/antigravity_executor.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 749bbbc3..851e7269 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -83,7 +83,14 @@ func newAntigravityHTTPClient(ctx context.Context, cfg *config.Config, auth *cli if tr, ok := client.Transport.(*http.Transport); ok { trClone := tr.Clone() trClone.ForceAttemptHTTP2 = false + // Also wiping TLSNextProto is good practice trClone.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) + // Crucial: The transport must actively advertise only http/1.1 in the ALPN handshake + if trClone.TLSClientConfig == nil { + trClone.TLSClientConfig = &tls.Config{} + } + trClone.TLSClientConfig.NextProtos = []string{"http/1.1"} + client.Transport = trClone } return client @@ -1038,7 +1045,7 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c baseURLs := antigravityBaseURLFallbackOrder(auth) httpClient := newAntigravityHTTPClient(ctx, cfg, auth, 0) - + for idx, baseURL := range baseURLs { modelsURL := baseURL + antigravityModelsPath @@ -1075,6 +1082,7 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c log.Debugf("antigravity executor: models request error on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1]) continue } + log.Errorf("antigravity executor: models request failed: %v", errDo) return nil } @@ -1087,6 +1095,7 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c log.Debugf("antigravity executor: models read error on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1]) continue } + log.Errorf("antigravity executor: models read body failed: %v", errRead) return nil } if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices { @@ -1094,6 +1103,7 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c log.Debugf("antigravity executor: models request rate limited on base url %s, retrying with fallback base url: %s", baseURL, baseURLs[idx+1]) continue } + log.Errorf("antigravity executor: models request error status %d: %s", httpResp.StatusCode, string(bodyBytes)) return nil } From 9370b5bd044b7f4952f832f1ab286aa667aa9a6c Mon Sep 17 00:00:00 2001 From: maplelove Date: Sun, 22 Feb 2026 19:43:10 +0800 Subject: [PATCH 04/14] fix(executor): completely scrub all proxy tracing headers in executor --- internal/api/modules/amp/proxy.go | 5 +++++ .../runtime/executor/antigravity_executor.go | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/internal/api/modules/amp/proxy.go b/internal/api/modules/amp/proxy.go index d298e255..21ed9e57 100644 --- a/internal/api/modules/amp/proxy.go +++ b/internal/api/modules/amp/proxy.go @@ -76,7 +76,12 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi // Remove proxy tracing headers to avoid upstream detection req.Header.Del("X-Forwarded-For") + req.Header.Del("X-Forwarded-Host") + req.Header.Del("X-Forwarded-Proto") + req.Header.Del("X-Forwarded-Port") req.Header.Del("X-Real-IP") + req.Header.Del("Forwarded") + req.Header.Del("Via") // Remove query-based credentials if they match the authenticated client API key. // This prevents leaking client auth material to the Amp upstream while avoiding diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 851e7269..638678b3 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -130,7 +130,12 @@ func (e *AntigravityExecutor) HttpRequest(ctx context.Context, auth *cliproxyaut httpReq.Close = true httpReq.Header.Del("Accept") httpReq.Header.Del("X-Forwarded-For") + httpReq.Header.Del("X-Forwarded-Host") + httpReq.Header.Del("X-Forwarded-Proto") + httpReq.Header.Del("X-Forwarded-Port") httpReq.Header.Del("X-Real-IP") + httpReq.Header.Del("Forwarded") + httpReq.Header.Del("Via") httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -950,7 +955,12 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) httpReq.Header.Del("X-Forwarded-For") + httpReq.Header.Del("X-Forwarded-Host") + httpReq.Header.Del("X-Forwarded-Proto") + httpReq.Header.Del("X-Forwarded-Port") httpReq.Header.Del("X-Real-IP") + httpReq.Header.Del("Forwarded") + httpReq.Header.Del("Via") if host := resolveHost(base); host != "" { httpReq.Host = host } @@ -1068,7 +1078,12 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) httpReq.Header.Del("X-Forwarded-For") + httpReq.Header.Del("X-Forwarded-Host") + httpReq.Header.Del("X-Forwarded-Proto") + httpReq.Header.Del("X-Forwarded-Port") httpReq.Header.Del("X-Real-IP") + httpReq.Header.Del("Forwarded") + httpReq.Header.Del("Via") if host := resolveHost(baseURL); host != "" { httpReq.Host = host } @@ -1371,7 +1386,12 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) httpReq.Header.Del("X-Forwarded-For") + httpReq.Header.Del("X-Forwarded-Host") + httpReq.Header.Del("X-Forwarded-Proto") + httpReq.Header.Del("X-Forwarded-Port") httpReq.Header.Del("X-Real-IP") + httpReq.Header.Del("Forwarded") + httpReq.Header.Del("Via") if host := resolveHost(base); host != "" { httpReq.Host = host } From 9491517b2664d20ef05e7d2ae9c96865187bf2c5 Mon Sep 17 00:00:00 2001 From: maplelove Date: Sun, 22 Feb 2026 20:17:30 +0800 Subject: [PATCH 05/14] fix(executor): use singleton transport to prevent OOM from connection pool leaks --- .../runtime/executor/antigravity_executor.go | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 638678b3..9de6cb08 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -73,25 +73,45 @@ 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 +) + +// initAntigravityTransport creates the shared HTTP/1.1 transport exactly once. +func initAntigravityTransport() { + base, ok := http.DefaultTransport.(*http.Transport) + if !ok { + base = &http.Transport{} + } + antigravityTransport = base.Clone() + antigravityTransport.ForceAttemptHTTP2 = false + // Wipe TLSNextProto to prevent implicit HTTP/2 upgrade + antigravityTransport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) + // Crucial: actively advertise only HTTP/1.1 in the ALPN handshake + if antigravityTransport.TLSClientConfig == nil { + antigravityTransport.TLSClientConfig = &tls.Config{} + } + antigravityTransport.TLSClientConfig.NextProtos = []string{"http/1.1"} +} + // 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 the proxy helper didn't set a custom transport (e.g. SOCKS5), use + // the shared HTTP/1.1 transport. Custom proxy transports are left as-is + // because they already carry their own dialer configuration. if client.Transport == nil { - client.Transport = http.DefaultTransport - } - if tr, ok := client.Transport.(*http.Transport); ok { - trClone := tr.Clone() - trClone.ForceAttemptHTTP2 = false - // Also wiping TLSNextProto is good practice - trClone.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) - // Crucial: The transport must actively advertise only http/1.1 in the ALPN handshake - if trClone.TLSClientConfig == nil { - trClone.TLSClientConfig = &tls.Config{} - } - trClone.TLSClientConfig.NextProtos = []string{"http/1.1"} - - client.Transport = trClone + client.Transport = antigravityTransport + } else if _, isDefault := client.Transport.(*http.Transport); isDefault { + client.Transport = antigravityTransport } return client } From 5dc1848466eddc8f9b2f34dcb45eb31cecc342fb Mon Sep 17 00:00:00 2001 From: maplelove Date: Sun, 22 Feb 2026 20:51:00 +0800 Subject: [PATCH 06/14] feat(scrub): add comprehensive browser fingerprint and client identity header scrubbing --- internal/api/modules/amp/proxy.go | 21 ++++++++ .../runtime/executor/antigravity_executor.go | 16 +----- internal/runtime/executor/header_scrub.go | 50 +++++++++++++++++++ 3 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 internal/runtime/executor/header_scrub.go diff --git a/internal/api/modules/amp/proxy.go b/internal/api/modules/amp/proxy.go index 21ed9e57..163c408c 100644 --- a/internal/api/modules/amp/proxy.go +++ b/internal/api/modules/amp/proxy.go @@ -83,6 +83,27 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi req.Header.Del("Forwarded") req.Header.Del("Via") + // Remove client identity headers that reveal third-party clients + req.Header.Del("X-Title") + req.Header.Del("X-Stainless-Lang") + req.Header.Del("X-Stainless-Package-Version") + req.Header.Del("X-Stainless-Os") + req.Header.Del("X-Stainless-Arch") + req.Header.Del("X-Stainless-Runtime") + req.Header.Del("X-Stainless-Runtime-Version") + req.Header.Del("Http-Referer") + req.Header.Del("Referer") + + // Remove browser / Chromium fingerprint headers + req.Header.Del("Sec-Ch-Ua") + req.Header.Del("Sec-Ch-Ua-Mobile") + req.Header.Del("Sec-Ch-Ua-Platform") + req.Header.Del("Sec-Fetch-Mode") + req.Header.Del("Sec-Fetch-Site") + req.Header.Del("Sec-Fetch-Dest") + req.Header.Del("Priority") + req.Header.Del("Accept-Encoding") + // Remove query-based credentials if they match the authenticated client API key. // This prevents leaking client auth material to the Amp upstream while avoiding // breaking unrelated upstream query parameters. diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 9de6cb08..fdd2f1b7 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -149,13 +149,7 @@ func (e *AntigravityExecutor) HttpRequest(ctx context.Context, auth *cliproxyaut } httpReq.Close = true httpReq.Header.Del("Accept") - httpReq.Header.Del("X-Forwarded-For") - httpReq.Header.Del("X-Forwarded-Host") - httpReq.Header.Del("X-Forwarded-Proto") - httpReq.Header.Del("X-Forwarded-Port") - httpReq.Header.Del("X-Real-IP") - httpReq.Header.Del("Forwarded") - httpReq.Header.Del("Via") + scrubProxyAndFingerprintHeaders(httpReq) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -1405,13 +1399,7 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) - httpReq.Header.Del("X-Forwarded-For") - httpReq.Header.Del("X-Forwarded-Host") - httpReq.Header.Del("X-Forwarded-Proto") - httpReq.Header.Del("X-Forwarded-Port") - httpReq.Header.Del("X-Real-IP") - httpReq.Header.Del("Forwarded") - httpReq.Header.Del("Via") + scrubProxyAndFingerprintHeaders(httpReq) if host := resolveHost(base); host != "" { httpReq.Host = host } diff --git a/internal/runtime/executor/header_scrub.go b/internal/runtime/executor/header_scrub.go new file mode 100644 index 00000000..f20558e2 --- /dev/null +++ b/internal/runtime/executor/header_scrub.go @@ -0,0 +1,50 @@ +package executor + +import "net/http" + +// scrubProxyAndFingerprintHeaders removes all headers that could reveal +// proxy infrastructure, client identity, or browser fingerprints from an +// outgoing request. This ensures requests to Google look like they +// originate directly from the Antigravity IDE (Node.js) rather than +// a third-party client behind a reverse proxy. +func scrubProxyAndFingerprintHeaders(req *http.Request) { + if req == nil { + return + } + + // --- Proxy tracing headers --- + req.Header.Del("X-Forwarded-For") + req.Header.Del("X-Forwarded-Host") + req.Header.Del("X-Forwarded-Proto") + req.Header.Del("X-Forwarded-Port") + req.Header.Del("X-Real-IP") + req.Header.Del("Forwarded") + req.Header.Del("Via") + + // --- Client identity headers --- + req.Header.Del("X-Title") + req.Header.Del("X-Stainless-Lang") + req.Header.Del("X-Stainless-Package-Version") + req.Header.Del("X-Stainless-Os") + req.Header.Del("X-Stainless-Arch") + req.Header.Del("X-Stainless-Runtime") + req.Header.Del("X-Stainless-Runtime-Version") + req.Header.Del("Http-Referer") + req.Header.Del("Referer") + + // --- Browser / Chromium fingerprint headers --- + // These are sent by Electron-based clients (e.g. CherryStudio) using the + // Fetch API, but NOT by Node.js https module (which Antigravity uses). + req.Header.Del("Sec-Ch-Ua") + req.Header.Del("Sec-Ch-Ua-Mobile") + req.Header.Del("Sec-Ch-Ua-Platform") + req.Header.Del("Sec-Fetch-Mode") + req.Header.Del("Sec-Fetch-Site") + req.Header.Del("Sec-Fetch-Dest") + req.Header.Del("Priority") + + // --- Encoding negotiation --- + // Antigravity (Node.js) sends "gzip, deflate, br" by default; + // Electron-based clients may add "zstd" which is a fingerprint mismatch. + req.Header.Del("Accept-Encoding") +} From d887716ebd7db9e3620bd917015ebe2a569e9578 Mon Sep 17 00:00:00 2001 From: maplelove Date: Sun, 22 Feb 2026 21:00:12 +0800 Subject: [PATCH 07/14] refactor(executor): switch HttpRequest to whitelist-based header filtering --- .../runtime/executor/antigravity_executor.go | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index fdd2f1b7..fbc0369f 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -136,6 +136,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") @@ -144,12 +146,28 @@ 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 } - httpReq.Close = true - httpReq.Header.Del("Accept") - scrubProxyAndFingerprintHeaders(httpReq) + httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } From 8b5af2ab8444e7d07e1e65c001b7f1598e984e97 Mon Sep 17 00:00:00 2001 From: maplelove Date: Sun, 22 Feb 2026 23:20:12 +0800 Subject: [PATCH 08/14] fix(executor): match real Antigravity OAuth UA, remove redundant header scrubbing on new requests --- .../runtime/executor/antigravity_executor.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index fbc0369f..7e480a97 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -986,13 +986,6 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) - httpReq.Header.Del("X-Forwarded-For") - httpReq.Header.Del("X-Forwarded-Host") - httpReq.Header.Del("X-Forwarded-Proto") - httpReq.Header.Del("X-Forwarded-Port") - httpReq.Header.Del("X-Real-IP") - httpReq.Header.Del("Forwarded") - httpReq.Header.Del("Via") if host := resolveHost(base); host != "" { httpReq.Host = host } @@ -1109,13 +1102,6 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) - httpReq.Header.Del("X-Forwarded-For") - httpReq.Header.Del("X-Forwarded-Host") - httpReq.Header.Del("X-Forwarded-Proto") - httpReq.Header.Del("X-Forwarded-Port") - httpReq.Header.Del("X-Real-IP") - httpReq.Header.Del("Forwarded") - httpReq.Header.Del("Via") if host := resolveHost(baseURL); host != "" { httpReq.Host = host } @@ -1248,8 +1234,9 @@ 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 := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) httpResp, errDo := httpClient.Do(httpReq) @@ -1417,7 +1404,6 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) - scrubProxyAndFingerprintHeaders(httpReq) if host := resolveHost(base); host != "" { httpReq.Host = host } From 8f97a5f77c93eebb3e98ff68d5ff5734611edb64 Mon Sep 17 00:00:00 2001 From: maplelove Date: Mon, 23 Feb 2026 13:33:51 +0800 Subject: [PATCH 09/14] feat(registry): expose input modalities, token limits, and generation methods for Antigravity models --- internal/registry/model_registry.go | 16 +++++++++++++ .../runtime/executor/antigravity_executor.go | 23 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/internal/registry/model_registry.go b/internal/registry/model_registry.go index 7b8b262e..e036a04f 100644 --- a/internal/registry/model_registry.go +++ b/internal/registry/model_registry.go @@ -47,6 +47,10 @@ type ModelInfo struct { MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // SupportedParameters lists supported parameters SupportedParameters []string `json:"supported_parameters,omitempty"` + // SupportedInputModalities lists supported input modalities (e.g., TEXT, IMAGE, VIDEO, AUDIO) + SupportedInputModalities []string `json:"supportedInputModalities,omitempty"` + // SupportedOutputModalities lists supported output modalities (e.g., TEXT, IMAGE) + SupportedOutputModalities []string `json:"supportedOutputModalities,omitempty"` // Thinking holds provider-specific reasoning/thinking budget capabilities. // This is optional and currently used for Gemini thinking budget normalization. @@ -499,6 +503,12 @@ func cloneModelInfo(model *ModelInfo) *ModelInfo { if len(model.SupportedParameters) > 0 { copyModel.SupportedParameters = append([]string(nil), model.SupportedParameters...) } + if len(model.SupportedInputModalities) > 0 { + copyModel.SupportedInputModalities = append([]string(nil), model.SupportedInputModalities...) + } + if len(model.SupportedOutputModalities) > 0 { + copyModel.SupportedOutputModalities = append([]string(nil), model.SupportedOutputModalities...) + } return ©Model } @@ -1067,6 +1077,12 @@ func (r *ModelRegistry) convertModelToMap(model *ModelInfo, handlerType string) if len(model.SupportedGenerationMethods) > 0 { result["supportedGenerationMethods"] = model.SupportedGenerationMethods } + if len(model.SupportedInputModalities) > 0 { + result["supportedInputModalities"] = model.SupportedInputModalities + } + if len(model.SupportedOutputModalities) > 0 { + result["supportedOutputModalities"] = model.SupportedOutputModalities + } return result default: diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 7e480a97..e697b64e 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -1176,6 +1176,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 { From f3c164d34523e9ece5130c16d4c2d79e80a12371 Mon Sep 17 00:00:00 2001 From: maplelove Date: Fri, 27 Feb 2026 10:34:27 +0800 Subject: [PATCH 10/14] feat(antigravity): update to v1.19.5 with new models and Claude 4-6 migration --- internal/config/oauth_model_alias_migration.go | 15 ++++++++++++--- .../registry/model_definitions_static_data.go | 4 +++- internal/runtime/executor/antigravity_executor.go | 7 ++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/internal/config/oauth_model_alias_migration.go b/internal/config/oauth_model_alias_migration.go index f52df27a..717f0235 100644 --- a/internal/config/oauth_model_alias_migration.go +++ b/internal/config/oauth_model_alias_migration.go @@ -14,10 +14,15 @@ var antigravityModelConversionTable = map[string]string{ "gemini-3-pro-image-preview": "gemini-3-pro-image", "gemini-3-pro-preview": "gemini-3-pro-high", "gemini-3-flash-preview": "gemini-3-flash", + "gemini-3.1-pro-preview": "gemini-3.1-pro-high", "gemini-claude-sonnet-4-5": "claude-sonnet-4-5", "gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking", "gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking", "gemini-claude-opus-4-6-thinking": "claude-opus-4-6-thinking", + "gemini-claude-sonnet-4-6": "claude-sonnet-4-6", + "claude-sonnet-4-5": "claude-sonnet-4-6", + "claude-sonnet-4-5-thinking": "claude-sonnet-4-6", + "claude-opus-4-5-thinking": "claude-opus-4-6-thinking", } // defaultAntigravityAliases returns the default oauth-model-alias configuration @@ -28,9 +33,13 @@ func defaultAntigravityAliases() []OAuthModelAlias { {Name: "gemini-3-pro-image", Alias: "gemini-3-pro-image-preview"}, {Name: "gemini-3-pro-high", Alias: "gemini-3-pro-preview"}, {Name: "gemini-3-flash", Alias: "gemini-3-flash-preview"}, - {Name: "claude-sonnet-4-5", Alias: "gemini-claude-sonnet-4-5"}, - {Name: "claude-sonnet-4-5-thinking", Alias: "gemini-claude-sonnet-4-5-thinking"}, - {Name: "claude-opus-4-5-thinking", Alias: "gemini-claude-opus-4-5-thinking"}, + {Name: "gemini-3.1-pro-high", Alias: "gemini-3.1-pro-preview"}, + {Name: "claude-sonnet-4-6", Alias: "gemini-claude-sonnet-4-5"}, + {Name: "claude-sonnet-4-6", Alias: "gemini-claude-sonnet-4-5-thinking"}, + {Name: "claude-sonnet-4-6", Alias: "claude-sonnet-4-5"}, + {Name: "claude-sonnet-4-6", Alias: "claude-sonnet-4-5-thinking"}, + {Name: "claude-opus-4-6-thinking", Alias: "gemini-claude-opus-4-5-thinking"}, + {Name: "claude-opus-4-6-thinking", Alias: "claude-opus-4-5-thinking"}, {Name: "claude-opus-4-6-thinking", Alias: "gemini-claude-opus-4-6-thinking"}, } } diff --git a/internal/registry/model_definitions_static_data.go b/internal/registry/model_definitions_static_data.go index e03d878b..ca68b55a 100644 --- a/internal/registry/model_definitions_static_data.go +++ b/internal/registry/model_definitions_static_data.go @@ -954,13 +954,15 @@ func GetAntigravityModelConfig() map[string]*AntigravityModelConfig { "gemini-3-pro-image": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}}}, "gemini-3.1-pro-high": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}}}, "gemini-3-flash": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"minimal", "low", "medium", "high"}}}, + "gemini-3.1-pro-low": {Thinking: &ThinkingSupport{Min: 128, Max: 32768, ZeroAllowed: false, DynamicAllowed: true, Levels: []string{"low", "high"}}}, + "gemini-3.1-flash-image": {}, "claude-sonnet-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000}, "claude-opus-4-5-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000}, "claude-opus-4-6-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000}, "claude-sonnet-4-5": {MaxCompletionTokens: 64000}, "claude-sonnet-4-6": {MaxCompletionTokens: 64000}, "claude-sonnet-4-6-thinking": {Thinking: &ThinkingSupport{Min: 1024, Max: 128000, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 64000}, - "gpt-oss-120b-medium": {}, + "gpt-oss-120b-medium": {Thinking: &ThinkingSupport{Min: 0, Max: 8192, ZeroAllowed: true, DynamicAllowed: true}, MaxCompletionTokens: 32768}, "tab_flash_lite_preview": {}, } } diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index aa2be677..c35df260 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -46,7 +46,7 @@ const ( antigravityModelsPath = "/v1internal:fetchAvailableModels" antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" antigravityClientSecret = "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" - defaultAntigravityAgent = "antigravity/1.18.4 windows/amd64" + defaultAntigravityAgent = "antigravity/1.19.5 windows/amd64" 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. The USER will send you requests, which you must always prioritize addressing. Along with each USER request, we will attach additional metadata about their current state, such as what files they have open and where their cursor is. This information may or may not be relevant to the coding task, it is up for you to decide. " @@ -1229,7 +1229,8 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c continue } switch modelID { - case "chat_20706", "chat_23310", "gemini-2.5-flash-thinking", "gemini-3-pro-low", "gemini-2.5-pro": + case "chat_20706", "chat_23310", "gemini-2.5-flash-thinking", "gemini-3-pro-low", "gemini-2.5-pro", + "tab_jump_flash_lite_preview", "tab_flash_lite_preview", "gemini-2.5-flash-lite": continue } modelCfg := modelConfig[modelID] @@ -1470,7 +1471,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-high") || strings.Contains(modelName, "gemini-3.1-pro") payloadStr := string(payload) paths := make([]string, 0) util.Walk(gjson.Parse(payloadStr), "", "parametersJsonSchema", &paths) From fc0257d6d9da96de34ff30fd97702ee3f6353415 Mon Sep 17 00:00:00 2001 From: maplelove Date: Fri, 27 Feb 2026 10:57:13 +0800 Subject: [PATCH 11/14] refactor: consolidate duplicate UA and header scrubbing into shared misc functions --- internal/api/modules/amp/proxy.go | 32 +--------- internal/cmd/login.go | 4 +- internal/misc/header_utils.go | 59 +++++++++++++++++++ .../runtime/executor/gemini_cli_executor.go | 8 +-- internal/runtime/executor/header_scrub.go | 52 +++------------- 5 files changed, 72 insertions(+), 83 deletions(-) diff --git a/internal/api/modules/amp/proxy.go b/internal/api/modules/amp/proxy.go index 072aeb65..ecc9da77 100644 --- a/internal/api/modules/amp/proxy.go +++ b/internal/api/modules/amp/proxy.go @@ -14,6 +14,7 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" log "github.com/sirupsen/logrus" ) @@ -75,36 +76,9 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi req.Header.Del("Authorization") req.Header.Del("X-Api-Key") req.Header.Del("X-Goog-Api-Key") - - // Remove proxy tracing headers to avoid upstream detection - req.Header.Del("X-Forwarded-For") - req.Header.Del("X-Forwarded-Host") - req.Header.Del("X-Forwarded-Proto") - req.Header.Del("X-Forwarded-Port") - req.Header.Del("X-Real-IP") - req.Header.Del("Forwarded") - req.Header.Del("Via") - // Remove client identity headers that reveal third-party clients - req.Header.Del("X-Title") - req.Header.Del("X-Stainless-Lang") - req.Header.Del("X-Stainless-Package-Version") - req.Header.Del("X-Stainless-Os") - req.Header.Del("X-Stainless-Arch") - req.Header.Del("X-Stainless-Runtime") - req.Header.Del("X-Stainless-Runtime-Version") - req.Header.Del("Http-Referer") - req.Header.Del("Referer") - - // Remove browser / Chromium fingerprint headers - req.Header.Del("Sec-Ch-Ua") - req.Header.Del("Sec-Ch-Ua-Mobile") - req.Header.Del("Sec-Ch-Ua-Platform") - req.Header.Del("Sec-Fetch-Mode") - req.Header.Del("Sec-Fetch-Site") - req.Header.Del("Sec-Fetch-Dest") - req.Header.Del("Priority") - req.Header.Del("Accept-Encoding") + // Remove proxy, client identity, and browser fingerprint headers + misc.ScrubProxyAndFingerprintHeaders(req) // Remove query-based credentials if they match the authenticated client API key. // This prevents leaking client auth material to the Amp upstream while avoiding diff --git a/internal/cmd/login.go b/internal/cmd/login.go index 5f4061b2..1162dc68 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -13,7 +13,6 @@ import ( "io" "net/http" "os" - "runtime" "strconv" "strings" "time" @@ -21,6 +20,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" + "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" log "github.com/sirupsen/logrus" @@ -33,7 +33,7 @@ const ( ) func getGeminiCLIUserAgent() string { - return fmt.Sprintf("GeminiCLI/1.0.0/unknown (%s; %s)", runtime.GOOS, runtime.GOARCH) + return misc.GeminiCLIUserAgent("") } type projectSelectionRequiredError struct{} diff --git a/internal/misc/header_utils.go b/internal/misc/header_utils.go index c6279a4c..e3711e43 100644 --- a/internal/misc/header_utils.go +++ b/internal/misc/header_utils.go @@ -4,10 +4,68 @@ package misc import ( + "fmt" "net/http" + "runtime" "strings" ) +// GeminiCLIUserAgent returns a User-Agent string that matches the Gemini CLI format. +// The model parameter is included in the UA; pass "" or "unknown" when the model is not applicable. +func GeminiCLIUserAgent(model string) string { + if model == "" { + model = "unknown" + } + return fmt.Sprintf("GeminiCLI/1.0.0/%s (%s; %s)", model, runtime.GOOS, runtime.GOARCH) +} + +// ScrubProxyAndFingerprintHeaders removes all headers that could reveal +// proxy infrastructure, client identity, or browser fingerprints from an +// outgoing request. This ensures requests to upstream services look like they +// originate directly from a native client rather than a third-party client +// behind a reverse proxy. +func ScrubProxyAndFingerprintHeaders(req *http.Request) { + if req == nil { + return + } + + // --- Proxy tracing headers --- + req.Header.Del("X-Forwarded-For") + req.Header.Del("X-Forwarded-Host") + req.Header.Del("X-Forwarded-Proto") + req.Header.Del("X-Forwarded-Port") + req.Header.Del("X-Real-IP") + req.Header.Del("Forwarded") + req.Header.Del("Via") + + // --- Client identity headers --- + req.Header.Del("X-Title") + req.Header.Del("X-Stainless-Lang") + req.Header.Del("X-Stainless-Package-Version") + req.Header.Del("X-Stainless-Os") + req.Header.Del("X-Stainless-Arch") + req.Header.Del("X-Stainless-Runtime") + req.Header.Del("X-Stainless-Runtime-Version") + req.Header.Del("Http-Referer") + req.Header.Del("Referer") + + // --- Browser / Chromium fingerprint headers --- + // These are sent by Electron-based clients (e.g. CherryStudio) using the + // Fetch API, but NOT by Node.js https module (which Antigravity uses). + req.Header.Del("Sec-Ch-Ua") + req.Header.Del("Sec-Ch-Ua-Mobile") + req.Header.Del("Sec-Ch-Ua-Platform") + req.Header.Del("Sec-Fetch-Mode") + req.Header.Del("Sec-Fetch-Site") + req.Header.Del("Sec-Fetch-Dest") + req.Header.Del("Priority") + + // --- Encoding negotiation --- + // Antigravity (Node.js) sends "gzip, deflate, br" by default; + // Electron-based clients may add "zstd" which is a fingerprint mismatch. + req.Header.Del("Accept-Encoding") +} + // EnsureHeader ensures that a header exists in the target header map by checking // multiple sources in order of priority: source headers, existing target headers, // and finally the default value. It only sets the header if it's not already present @@ -35,3 +93,4 @@ func EnsureHeader(target http.Header, source http.Header, key, defaultValue stri target.Set(key, val) } } + diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index 3746ae8a..504f32c8 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -12,7 +12,6 @@ import ( "io" "net/http" "regexp" - "runtime" "strconv" "strings" "time" @@ -745,12 +744,7 @@ func applyGeminiCLIHeaders(r *http.Request, model string) { ginHeaders = ginCtx.Request.Header } - if model == "" { - model = "unknown" - } - - userAgent := fmt.Sprintf("GeminiCLI/1.0.0/%s (%s; %s)", model, runtime.GOOS, runtime.GOARCH) - misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", userAgent) + misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", misc.GeminiCLIUserAgent(model)) } // cliPreviewFallbackOrder returns preview model candidates for a base model. diff --git a/internal/runtime/executor/header_scrub.go b/internal/runtime/executor/header_scrub.go index f20558e2..41eb80d3 100644 --- a/internal/runtime/executor/header_scrub.go +++ b/internal/runtime/executor/header_scrub.go @@ -1,50 +1,12 @@ package executor -import "net/http" +import ( + "net/http" -// scrubProxyAndFingerprintHeaders removes all headers that could reveal -// proxy infrastructure, client identity, or browser fingerprints from an -// outgoing request. This ensures requests to Google look like they -// originate directly from the Antigravity IDE (Node.js) rather than -// a third-party client behind a reverse proxy. + "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" +) + +// scrubProxyAndFingerprintHeaders delegates to the shared utility in internal/misc. func scrubProxyAndFingerprintHeaders(req *http.Request) { - if req == nil { - return - } - - // --- Proxy tracing headers --- - req.Header.Del("X-Forwarded-For") - req.Header.Del("X-Forwarded-Host") - req.Header.Del("X-Forwarded-Proto") - req.Header.Del("X-Forwarded-Port") - req.Header.Del("X-Real-IP") - req.Header.Del("Forwarded") - req.Header.Del("Via") - - // --- Client identity headers --- - req.Header.Del("X-Title") - req.Header.Del("X-Stainless-Lang") - req.Header.Del("X-Stainless-Package-Version") - req.Header.Del("X-Stainless-Os") - req.Header.Del("X-Stainless-Arch") - req.Header.Del("X-Stainless-Runtime") - req.Header.Del("X-Stainless-Runtime-Version") - req.Header.Del("Http-Referer") - req.Header.Del("Referer") - - // --- Browser / Chromium fingerprint headers --- - // These are sent by Electron-based clients (e.g. CherryStudio) using the - // Fetch API, but NOT by Node.js https module (which Antigravity uses). - req.Header.Del("Sec-Ch-Ua") - req.Header.Del("Sec-Ch-Ua-Mobile") - req.Header.Del("Sec-Ch-Ua-Platform") - req.Header.Del("Sec-Fetch-Mode") - req.Header.Del("Sec-Fetch-Site") - req.Header.Del("Sec-Fetch-Dest") - req.Header.Del("Priority") - - // --- Encoding negotiation --- - // Antigravity (Node.js) sends "gzip, deflate, br" by default; - // Electron-based clients may add "zstd" which is a fingerprint mismatch. - req.Header.Del("Accept-Encoding") + misc.ScrubProxyAndFingerprintHeaders(req) } From 846e75b89319214fb9fa6fbea8d52f5af427cd8e Mon Sep 17 00:00:00 2001 From: maplelove Date: Fri, 27 Feb 2026 13:32:06 +0800 Subject: [PATCH 12/14] feat(gemini): route gemini-3.1-flash-image identically to gemini-3-pro-image --- internal/runtime/executor/antigravity_executor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index c35df260..031f65b5 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -250,7 +250,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) } From 2baf35b3ef5b441154b61a11afa3a78c00a9b487 Mon Sep 17 00:00:00 2001 From: maplelove Date: Fri, 27 Feb 2026 14:09:37 +0800 Subject: [PATCH 13/14] fix(executor): bump antigravity UA to 1.19.6 and align image_gen payload --- .../runtime/executor/antigravity_executor.go | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index 031f65b5..412958f1 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -46,7 +46,7 @@ const ( antigravityModelsPath = "/v1internal:fetchAvailableModels" antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" antigravityClientSecret = "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" - defaultAntigravityAgent = "antigravity/1.19.5 windows/amd64" + defaultAntigravityAgent = "antigravity/1.19.6 windows/amd64" 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. The USER will send you requests, which you must always prioritize addressing. Along with each USER request, we will attach additional metadata about their current state, such as what files they have open and where their cursor is. This information may or may not be relevant to the coding task, it is up for you to decide. " @@ -1723,7 +1723,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 != "" { @@ -1731,8 +1740,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() { @@ -1746,6 +1760,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) From 68dd2bfe82656b8fbda7f001b477ddd6f88c79d7 Mon Sep 17 00:00:00 2001 From: maplelove Date: Fri, 27 Feb 2026 17:13:42 +0800 Subject: [PATCH 14/14] fix(translator): allow passthrough of custom generationConfig for all Gemini-like providers --- .../openai/chat-completions/antigravity_openai_request.go | 5 +++++ .../openai/chat-completions/gemini-cli_openai_request.go | 5 +++++ .../gemini/openai/chat-completions/gemini_openai_request.go | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go index 85b28b8b..e9a62426 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go @@ -34,6 +34,11 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ // Model out, _ = sjson.SetBytes(out, "model", modelName) + // Let user-provided generationConfig pass through + if genConfig := gjson.GetBytes(rawJSON, "generationConfig"); genConfig.Exists() { + out, _ = sjson.SetRawBytes(out, "request.generationConfig", []byte(genConfig.Raw)) + } + // Apply thinking configuration: convert OpenAI reasoning_effort to Gemini CLI thinkingConfig. // Inline translation-only mapping; capability checks happen later in ApplyThinking. re := gjson.GetBytes(rawJSON, "reasoning_effort") diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go index 53da71f4..b0a6bddd 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go @@ -34,6 +34,11 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo // Model out, _ = sjson.SetBytes(out, "model", modelName) + // Let user-provided generationConfig pass through + if genConfig := gjson.GetBytes(rawJSON, "generationConfig"); genConfig.Exists() { + out, _ = sjson.SetRawBytes(out, "request.generationConfig", []byte(genConfig.Raw)) + } + // Apply thinking configuration: convert OpenAI reasoning_effort to Gemini CLI thinkingConfig. // Inline translation-only mapping; capability checks happen later in ApplyThinking. re := gjson.GetBytes(rawJSON, "reasoning_effort") diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index 5de35681..f18f45be 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -34,6 +34,11 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) // Model out, _ = sjson.SetBytes(out, "model", modelName) + // Let user-provided generationConfig pass through + if genConfig := gjson.GetBytes(rawJSON, "generationConfig"); genConfig.Exists() { + out, _ = sjson.SetRawBytes(out, "generationConfig", []byte(genConfig.Raw)) + } + // Apply thinking configuration: convert OpenAI reasoning_effort to Gemini thinkingConfig. // Inline translation-only mapping; capability checks happen later in ApplyThinking. re := gjson.GetBytes(rawJSON, "reasoning_effort")