From dd71c73a9f4d6960e55929f2f7b97b102804279a Mon Sep 17 00:00:00 2001 From: maplelove Date: Sun, 22 Feb 2026 17:07:17 +0800 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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") From d6cc976d1f55ab4f59756ee8db04d16e6b134a06 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Mon, 2 Mar 2026 03:40:54 +0800 Subject: [PATCH 15/20] chore(executor): remove unused header scrubbing function --- internal/runtime/executor/header_scrub.go | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 internal/runtime/executor/header_scrub.go diff --git a/internal/runtime/executor/header_scrub.go b/internal/runtime/executor/header_scrub.go deleted file mode 100644 index 41eb80d3..00000000 --- a/internal/runtime/executor/header_scrub.go +++ /dev/null @@ -1,12 +0,0 @@ -package executor - -import ( - "net/http" - - "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" -) - -// scrubProxyAndFingerprintHeaders delegates to the shared utility in internal/misc. -func scrubProxyAndFingerprintHeaders(req *http.Request) { - misc.ScrubProxyAndFingerprintHeaders(req) -} From b907d21851af9031264b5b5e7380a3b430e68f7c Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:54:15 +0800 Subject: [PATCH 16/20] revert(executor): revert antigravity_executor.go changes from PR #1735 --- .../runtime/executor/antigravity_executor.go | 177 +++--------------- 1 file changed, 24 insertions(+), 153 deletions(-) diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index bd32a422..919d96fa 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -8,7 +8,6 @@ import ( "bytes" "context" "crypto/sha256" - "crypto/tls" "encoding/binary" "encoding/json" "errors" @@ -46,10 +45,10 @@ const ( antigravityModelsPath = "/v1internal:fetchAvailableModels" antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" antigravityClientSecret = "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" - defaultAntigravityAgent = "antigravity/1.19.6 windows/amd64" + defaultAntigravityAgent = "antigravity/1.104.0 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. 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. " + 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 ( @@ -143,62 +142,6 @@ 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 } @@ -219,8 +162,6 @@ 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") @@ -229,29 +170,10 @@ 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 := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) return httpClient.Do(httpReq) } @@ -263,7 +185,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") || strings.Contains(baseModel, "gemini-3.1-flash-image") { + if isClaude || strings.Contains(baseModel, "gemini-3-pro") { return e.executeClaudeNonStream(ctx, auth, req, opts) } @@ -298,7 +220,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 := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) attempts := antigravityRetryAttempts(auth, e.cfg) @@ -440,7 +362,7 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth * translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) baseURLs := antigravityBaseURLFallbackOrder(auth) - httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) attempts := antigravityRetryAttempts(auth, e.cfg) @@ -832,7 +754,7 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel) baseURLs := antigravityBaseURLFallbackOrder(auth) - httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) attempts := antigravityRetryAttempts(auth, e.cfg) @@ -1034,7 +956,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut payload = deleteJSONField(payload, "request.safetySettings") baseURLs := antigravityBaseURLFallbackOrder(auth) - httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) var authID, authLabel, authType, authValue string if auth != nil { @@ -1065,10 +987,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 } @@ -1162,26 +1084,14 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c } baseURLs := antigravityBaseURLFallbackOrder(auth) - httpClient := newAntigravityHTTPClient(ctx, cfg, auth, 0) + httpClient := newProxyAwareHTTPClient(ctx, cfg, auth, 0) for idx, baseURL := range baseURLs { modelsURL := baseURL + antigravityModelsPath - - 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)) + httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodPost, modelsURL, bytes.NewReader([]byte(`{}`))) 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)) @@ -1242,8 +1152,7 @@ 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", - "tab_jump_flash_lite_preview", "tab_flash_lite_preview", "gemini-2.5-flash-lite": + case "chat_20706", "chat_23310", "tab_flash_lite_preview", "tab_jump_flash_lite_preview", "gemini-2.5-flash-thinking", "gemini-2.5-pro": continue } modelCfg := modelConfig[modelID] @@ -1265,29 +1174,6 @@ 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 { @@ -1355,11 +1241,10 @@ 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) + httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpResp, errDo := httpClient.Do(httpReq) if errDo != nil { return auth, errDo @@ -1430,7 +1315,7 @@ func (e *AntigravityExecutor) ensureAntigravityProjectID(ctx context.Context, au return nil } - httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0) + httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) projectID, errFetch := sdkAuth.FetchAntigravityProjectID(ctx, token, httpClient) if errFetch != nil { return errFetch @@ -1484,7 +1369,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") || strings.Contains(modelName, "gemini-3.1-pro") + useAntigravitySchema := strings.Contains(modelName, "claude") || strings.Contains(modelName, "gemini-3-pro-high") payloadStr := string(payload) paths := make([]string, 0) util.Walk(gjson.Parse(payloadStr), "", "parametersJsonSchema", &paths) @@ -1521,10 +1406,14 @@ 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 } @@ -1736,16 +1625,7 @@ 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") - - isImageModel := strings.Contains(modelName, "image") - - var reqType string - if isImageModel { - reqType = "image_gen" - } else { - reqType = "agent" - } - template, _ = sjson.Set(template, "requestType", reqType) + template, _ = sjson.Set(template, "requestType", "agent") // Use real project ID from auth if available, otherwise generate random (legacy fallback) if projectID != "" { @@ -1753,13 +1633,8 @@ func geminiToAntigravity(modelName string, payload []byte, projectID string) []b } else { template, _ = sjson.Set(template, "project", generateProjectID()) } - - 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.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() { @@ -1773,10 +1648,6 @@ 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 660bd7eff59bc815e856e9744401030c9b49033d Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:02:15 +0800 Subject: [PATCH 17/20] refactor(config): remove oauth-model-alias migration logic and related tests --- internal/config/config.go | 13 - .../config/oauth_model_alias_migration.go | 286 ------------------ .../oauth_model_alias_migration_test.go | 245 --------------- 3 files changed, 544 deletions(-) delete mode 100644 internal/config/oauth_model_alias_migration.go delete mode 100644 internal/config/oauth_model_alias_migration_test.go diff --git a/internal/config/config.go b/internal/config/config.go index d6e2bdc8..5a6595f7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -516,16 +516,6 @@ func LoadConfig(configFile string) (*Config, error) { // If optional is true and the file is missing, it returns an empty Config. // If optional is true and the file is empty or invalid, it returns an empty Config. func LoadConfigOptional(configFile string, optional bool) (*Config, error) { - // NOTE: Startup oauth-model-alias migration is intentionally disabled. - // Reason: avoid mutating config.yaml during server startup. - // Re-enable the block below if automatic startup migration is needed again. - // if migrated, err := MigrateOAuthModelAlias(configFile); err != nil { - // // Log warning but don't fail - config loading should still work - // fmt.Printf("Warning: oauth-model-alias migration failed: %v\n", err) - // } else if migrated { - // fmt.Println("Migrated oauth-model-mappings to oauth-model-alias") - // } - // Read the entire configuration file into memory. data, err := os.ReadFile(configFile) if err != nil { @@ -1560,9 +1550,6 @@ func pruneMappingToGeneratedKeys(dstRoot, srcRoot *yaml.Node, key string) { srcIdx := findMapKeyIndex(srcRoot, key) if srcIdx < 0 { // Keep an explicit empty mapping for oauth-model-alias when it was previously present. - // - // Rationale: LoadConfig runs MigrateOAuthModelAlias before unmarshalling. If the - // oauth-model-alias key is missing, migration will add the default antigravity aliases. // When users delete the last channel from oauth-model-alias via the management API, // we want that deletion to persist across hot reloads and restarts. if key == "oauth-model-alias" { diff --git a/internal/config/oauth_model_alias_migration.go b/internal/config/oauth_model_alias_migration.go deleted file mode 100644 index 71613d03..00000000 --- a/internal/config/oauth_model_alias_migration.go +++ /dev/null @@ -1,286 +0,0 @@ -package config - -import ( - "os" - "strings" - - "gopkg.in/yaml.v3" -) - -// antigravityModelConversionTable maps old built-in aliases to actual model names -// for the antigravity channel during migration. -var antigravityModelConversionTable = map[string]string{ - "gemini-2.5-computer-use-preview-10-2025": "rev19-uic3-1p", - "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-6", - "gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-6-thinking", - "gemini-claude-opus-4-5-thinking": "claude-opus-4-6-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-thinking", - "claude-opus-4-5-thinking": "claude-opus-4-6-thinking", -} - -// defaultAntigravityAliases returns the default oauth-model-alias configuration -// for the antigravity channel when neither field exists. -func defaultAntigravityAliases() []OAuthModelAlias { - return []OAuthModelAlias{ - {Name: "rev19-uic3-1p", Alias: "gemini-2.5-computer-use-preview-10-2025"}, - {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: "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-thinking", Alias: "gemini-claude-sonnet-4-5-thinking"}, - {Name: "claude-sonnet-4-6", Alias: "claude-sonnet-4-5"}, - {Name: "claude-sonnet-4-6-thinking", 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"}, - } -} - -// MigrateOAuthModelAlias checks for and performs migration from oauth-model-mappings -// to oauth-model-alias at startup. Returns true if migration was performed. -// -// Migration flow: -// 1. Check if oauth-model-alias exists -> skip migration -// 2. Check if oauth-model-mappings exists -> convert and migrate -// - For antigravity channel, convert old built-in aliases to actual model names -// -// 3. Neither exists -> add default antigravity config -func MigrateOAuthModelAlias(configFile string) (bool, error) { - data, err := os.ReadFile(configFile) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - if len(data) == 0 { - return false, nil - } - - // Parse YAML into node tree to preserve structure - var root yaml.Node - if err := yaml.Unmarshal(data, &root); err != nil { - return false, nil - } - if root.Kind != yaml.DocumentNode || len(root.Content) == 0 { - return false, nil - } - rootMap := root.Content[0] - if rootMap == nil || rootMap.Kind != yaml.MappingNode { - return false, nil - } - - // Check if oauth-model-alias already exists - if findMapKeyIndex(rootMap, "oauth-model-alias") >= 0 { - return false, nil - } - - // Check if oauth-model-mappings exists - oldIdx := findMapKeyIndex(rootMap, "oauth-model-mappings") - if oldIdx >= 0 { - // Migrate from old field - return migrateFromOldField(configFile, &root, rootMap, oldIdx) - } - - // Neither field exists - add default antigravity config - return addDefaultAntigravityConfig(configFile, &root, rootMap) -} - -// migrateFromOldField converts oauth-model-mappings to oauth-model-alias -func migrateFromOldField(configFile string, root *yaml.Node, rootMap *yaml.Node, oldIdx int) (bool, error) { - if oldIdx+1 >= len(rootMap.Content) { - return false, nil - } - oldValue := rootMap.Content[oldIdx+1] - if oldValue == nil || oldValue.Kind != yaml.MappingNode { - return false, nil - } - - // Parse the old aliases - oldAliases := parseOldAliasNode(oldValue) - if len(oldAliases) == 0 { - // Remove the old field and write - removeMapKeyByIndex(rootMap, oldIdx) - return writeYAMLNode(configFile, root) - } - - // Convert model names for antigravity channel - newAliases := make(map[string][]OAuthModelAlias, len(oldAliases)) - for channel, entries := range oldAliases { - converted := make([]OAuthModelAlias, 0, len(entries)) - for _, entry := range entries { - newEntry := OAuthModelAlias{ - Name: entry.Name, - Alias: entry.Alias, - Fork: entry.Fork, - } - // Convert model names for antigravity channel - if strings.EqualFold(channel, "antigravity") { - if actual, ok := antigravityModelConversionTable[entry.Name]; ok { - newEntry.Name = actual - } - } - converted = append(converted, newEntry) - } - newAliases[channel] = converted - } - - // For antigravity channel, supplement missing default aliases - if antigravityEntries, exists := newAliases["antigravity"]; exists { - // Build a set of already configured model names (upstream names) - configuredModels := make(map[string]bool, len(antigravityEntries)) - for _, entry := range antigravityEntries { - configuredModels[entry.Name] = true - } - - // Add missing default aliases - for _, defaultAlias := range defaultAntigravityAliases() { - if !configuredModels[defaultAlias.Name] { - antigravityEntries = append(antigravityEntries, defaultAlias) - } - } - newAliases["antigravity"] = antigravityEntries - } - - // Build new node - newNode := buildOAuthModelAliasNode(newAliases) - - // Replace old key with new key and value - rootMap.Content[oldIdx].Value = "oauth-model-alias" - rootMap.Content[oldIdx+1] = newNode - - return writeYAMLNode(configFile, root) -} - -// addDefaultAntigravityConfig adds the default antigravity configuration -func addDefaultAntigravityConfig(configFile string, root *yaml.Node, rootMap *yaml.Node) (bool, error) { - defaults := map[string][]OAuthModelAlias{ - "antigravity": defaultAntigravityAliases(), - } - newNode := buildOAuthModelAliasNode(defaults) - - // Add new key-value pair - keyNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "oauth-model-alias"} - rootMap.Content = append(rootMap.Content, keyNode, newNode) - - return writeYAMLNode(configFile, root) -} - -// parseOldAliasNode parses the old oauth-model-mappings node structure -func parseOldAliasNode(node *yaml.Node) map[string][]OAuthModelAlias { - if node == nil || node.Kind != yaml.MappingNode { - return nil - } - result := make(map[string][]OAuthModelAlias) - for i := 0; i+1 < len(node.Content); i += 2 { - channelNode := node.Content[i] - entriesNode := node.Content[i+1] - if channelNode == nil || entriesNode == nil { - continue - } - channel := strings.ToLower(strings.TrimSpace(channelNode.Value)) - if channel == "" || entriesNode.Kind != yaml.SequenceNode { - continue - } - entries := make([]OAuthModelAlias, 0, len(entriesNode.Content)) - for _, entryNode := range entriesNode.Content { - if entryNode == nil || entryNode.Kind != yaml.MappingNode { - continue - } - entry := parseAliasEntry(entryNode) - if entry.Name != "" && entry.Alias != "" { - entries = append(entries, entry) - } - } - if len(entries) > 0 { - result[channel] = entries - } - } - return result -} - -// parseAliasEntry parses a single alias entry node -func parseAliasEntry(node *yaml.Node) OAuthModelAlias { - var entry OAuthModelAlias - for i := 0; i+1 < len(node.Content); i += 2 { - keyNode := node.Content[i] - valNode := node.Content[i+1] - if keyNode == nil || valNode == nil { - continue - } - switch strings.ToLower(strings.TrimSpace(keyNode.Value)) { - case "name": - entry.Name = strings.TrimSpace(valNode.Value) - case "alias": - entry.Alias = strings.TrimSpace(valNode.Value) - case "fork": - entry.Fork = strings.ToLower(strings.TrimSpace(valNode.Value)) == "true" - } - } - return entry -} - -// buildOAuthModelAliasNode creates a YAML node for oauth-model-alias -func buildOAuthModelAliasNode(aliases map[string][]OAuthModelAlias) *yaml.Node { - node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - for channel, entries := range aliases { - channelNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: channel} - entriesNode := &yaml.Node{Kind: yaml.SequenceNode, Tag: "!!seq"} - for _, entry := range entries { - entryNode := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"} - entryNode.Content = append(entryNode.Content, - &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "name"}, - &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: entry.Name}, - &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "alias"}, - &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: entry.Alias}, - ) - if entry.Fork { - entryNode.Content = append(entryNode.Content, - &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "fork"}, - &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!bool", Value: "true"}, - ) - } - entriesNode.Content = append(entriesNode.Content, entryNode) - } - node.Content = append(node.Content, channelNode, entriesNode) - } - return node -} - -// removeMapKeyByIndex removes a key-value pair from a mapping node by index -func removeMapKeyByIndex(mapNode *yaml.Node, keyIdx int) { - if mapNode == nil || mapNode.Kind != yaml.MappingNode { - return - } - if keyIdx < 0 || keyIdx+1 >= len(mapNode.Content) { - return - } - mapNode.Content = append(mapNode.Content[:keyIdx], mapNode.Content[keyIdx+2:]...) -} - -// writeYAMLNode writes the YAML node tree back to file -func writeYAMLNode(configFile string, root *yaml.Node) (bool, error) { - f, err := os.Create(configFile) - if err != nil { - return false, err - } - defer f.Close() - - enc := yaml.NewEncoder(f) - enc.SetIndent(2) - if err := enc.Encode(root); err != nil { - return false, err - } - if err := enc.Close(); err != nil { - return false, err - } - return true, nil -} diff --git a/internal/config/oauth_model_alias_migration_test.go b/internal/config/oauth_model_alias_migration_test.go deleted file mode 100644 index cd73b9d5..00000000 --- a/internal/config/oauth_model_alias_migration_test.go +++ /dev/null @@ -1,245 +0,0 @@ -package config - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "gopkg.in/yaml.v3" -) - -func TestMigrateOAuthModelAlias_SkipsIfNewFieldExists(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - content := `oauth-model-alias: - gemini-cli: - - name: "gemini-2.5-pro" - alias: "g2.5p" -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if migrated { - t.Fatal("expected no migration when oauth-model-alias already exists") - } - - // Verify file unchanged - data, _ := os.ReadFile(configFile) - if !strings.Contains(string(data), "oauth-model-alias:") { - t.Fatal("file should still contain oauth-model-alias") - } -} - -func TestMigrateOAuthModelAlias_MigratesOldField(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - content := `oauth-model-mappings: - gemini-cli: - - name: "gemini-2.5-pro" - alias: "g2.5p" - fork: true -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !migrated { - t.Fatal("expected migration to occur") - } - - // Verify new field exists and old field removed - data, _ := os.ReadFile(configFile) - if strings.Contains(string(data), "oauth-model-mappings:") { - t.Fatal("old field should be removed") - } - if !strings.Contains(string(data), "oauth-model-alias:") { - t.Fatal("new field should exist") - } - - // Parse and verify structure - var root yaml.Node - if err := yaml.Unmarshal(data, &root); err != nil { - t.Fatal(err) - } -} - -func TestMigrateOAuthModelAlias_ConvertsAntigravityModels(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - // Use old model names that should be converted - content := `oauth-model-mappings: - antigravity: - - name: "gemini-2.5-computer-use-preview-10-2025" - alias: "computer-use" - - name: "gemini-3-pro-preview" - alias: "g3p" -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !migrated { - t.Fatal("expected migration to occur") - } - - // Verify model names were converted - data, _ := os.ReadFile(configFile) - content = string(data) - if !strings.Contains(content, "rev19-uic3-1p") { - t.Fatal("expected gemini-2.5-computer-use-preview-10-2025 to be converted to rev19-uic3-1p") - } - if !strings.Contains(content, "gemini-3-pro-high") { - t.Fatal("expected gemini-3-pro-preview to be converted to gemini-3-pro-high") - } - - // Verify missing default aliases were supplemented - if !strings.Contains(content, "gemini-3-pro-image") { - t.Fatal("expected missing default alias gemini-3-pro-image to be added") - } - if !strings.Contains(content, "gemini-3-flash") { - t.Fatal("expected missing default alias gemini-3-flash to be added") - } - if !strings.Contains(content, "claude-sonnet-4-5") { - t.Fatal("expected missing default alias claude-sonnet-4-5 to be added") - } - if !strings.Contains(content, "claude-sonnet-4-5-thinking") { - t.Fatal("expected missing default alias claude-sonnet-4-5-thinking to be added") - } - if !strings.Contains(content, "claude-opus-4-5-thinking") { - t.Fatal("expected missing default alias claude-opus-4-5-thinking to be added") - } - if !strings.Contains(content, "claude-opus-4-6-thinking") { - t.Fatal("expected missing default alias claude-opus-4-6-thinking to be added") - } -} - -func TestMigrateOAuthModelAlias_AddsDefaultIfNeitherExists(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - content := `debug: true -port: 8080 -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !migrated { - t.Fatal("expected migration to add default config") - } - - // Verify default antigravity config was added - data, _ := os.ReadFile(configFile) - content = string(data) - if !strings.Contains(content, "oauth-model-alias:") { - t.Fatal("expected oauth-model-alias to be added") - } - if !strings.Contains(content, "antigravity:") { - t.Fatal("expected antigravity channel to be added") - } - if !strings.Contains(content, "rev19-uic3-1p") { - t.Fatal("expected default antigravity aliases to include rev19-uic3-1p") - } -} - -func TestMigrateOAuthModelAlias_PreservesOtherConfig(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - content := `debug: true -port: 8080 -oauth-model-mappings: - gemini-cli: - - name: "test" - alias: "t" -api-keys: - - "key1" - - "key2" -` - if err := os.WriteFile(configFile, []byte(content), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !migrated { - t.Fatal("expected migration to occur") - } - - // Verify other config preserved - data, _ := os.ReadFile(configFile) - content = string(data) - if !strings.Contains(content, "debug: true") { - t.Fatal("expected debug field to be preserved") - } - if !strings.Contains(content, "port: 8080") { - t.Fatal("expected port field to be preserved") - } - if !strings.Contains(content, "api-keys:") { - t.Fatal("expected api-keys field to be preserved") - } -} - -func TestMigrateOAuthModelAlias_NonexistentFile(t *testing.T) { - t.Parallel() - - migrated, err := MigrateOAuthModelAlias("/nonexistent/path/config.yaml") - if err != nil { - t.Fatalf("unexpected error for nonexistent file: %v", err) - } - if migrated { - t.Fatal("expected no migration for nonexistent file") - } -} - -func TestMigrateOAuthModelAlias_EmptyFile(t *testing.T) { - t.Parallel() - - dir := t.TempDir() - configFile := filepath.Join(dir, "config.yaml") - - if err := os.WriteFile(configFile, []byte(""), 0644); err != nil { - t.Fatal(err) - } - - migrated, err := MigrateOAuthModelAlias(configFile) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if migrated { - t.Fatal("expected no migration for empty file") - } -} From 914db94e79285e3fd2b8f235a349c72f97fa6601 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:04:30 +0800 Subject: [PATCH 18/20] refactor(headers): streamline User-Agent handling and introduce GeminiCLI versioning --- .../api/handlers/management/auth_files.go | 21 +++++------- internal/cmd/login.go | 14 +++----- internal/misc/header_utils.go | 33 +++++++++++++++++-- .../runtime/executor/gemini_cli_executor.go | 11 +++---- .../codex/claude/codex_claude_response.go | 4 +-- .../codex_openai-responses_request_test.go | 16 ++++----- 6 files changed, 58 insertions(+), 41 deletions(-) diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 10edfa29..bb5606db 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -13,7 +13,6 @@ import ( "net/http" "os" "path/filepath" - "runtime" "sort" "strconv" "strings" @@ -43,17 +42,13 @@ import ( var lastRefreshKeys = []string{"last_refresh", "lastRefresh", "last_refreshed_at", "lastRefreshedAt"} const ( - anthropicCallbackPort = 54545 - geminiCallbackPort = 8085 - codexCallbackPort = 1455 - geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" - geminiCLIVersion = "v1internal" + anthropicCallbackPort = 54545 + geminiCallbackPort = 8085 + codexCallbackPort = 1455 + geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" + geminiCLIVersion = "v1internal" ) -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 @@ -2287,7 +2282,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", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo := httpClient.Do(req) if errDo != nil { @@ -2357,7 +2352,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", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo := httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) @@ -2378,7 +2373,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", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) 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 1162dc68..16af718e 100644 --- a/internal/cmd/login.go +++ b/internal/cmd/login.go @@ -28,14 +28,10 @@ import ( ) const ( - geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" - geminiCLIVersion = "v1internal" + geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" + geminiCLIVersion = "v1internal" ) -func getGeminiCLIUserAgent() string { - return misc.GeminiCLIUserAgent("") -} - type projectSelectionRequiredError struct{} func (e *projectSelectionRequiredError) Error() string { @@ -411,7 +407,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", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo := httpClient.Do(req) if errDo != nil { @@ -630,7 +626,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", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo := httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) @@ -651,7 +647,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", getGeminiCLIUserAgent()) + req.Header.Set("User-Agent", misc.GeminiCLIUserAgent("")) resp, errDo = httpClient.Do(req) if errDo != nil { return false, fmt.Errorf("failed to execute request: %w", errDo) diff --git a/internal/misc/header_utils.go b/internal/misc/header_utils.go index e3711e43..5752a269 100644 --- a/internal/misc/header_utils.go +++ b/internal/misc/header_utils.go @@ -10,13 +10,43 @@ import ( "strings" ) +const ( + // GeminiCLIVersion is the version string reported in the User-Agent for upstream requests. + GeminiCLIVersion = "0.31.0" + + // GeminiCLIApiClientHeader is the value for the X-Goog-Api-Client header sent to the Gemini CLI upstream. + GeminiCLIApiClientHeader = "google-genai-sdk/1.41.0 gl-node/v22.19.0" +) + +// geminiCLIOS maps Go runtime OS names to the Node.js-style platform strings used by Gemini CLI. +func geminiCLIOS() string { + switch runtime.GOOS { + case "windows": + return "win32" + default: + return runtime.GOOS + } +} + +// geminiCLIArch maps Go runtime architecture names to the Node.js-style arch strings used by Gemini CLI. +func geminiCLIArch() string { + switch runtime.GOARCH { + case "amd64": + return "x64" + case "386": + return "x86" + default: + return runtime.GOARCH + } +} + // 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) + return fmt.Sprintf("GeminiCLI/%s/%s (%s; %s)", GeminiCLIVersion, model, geminiCLIOS(), geminiCLIArch()) } // ScrubProxyAndFingerprintHeaders removes all headers that could reveal @@ -93,4 +123,3 @@ 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 504f32c8..1be245b7 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -16,7 +16,6 @@ import ( "strings" "time" - "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli" @@ -738,13 +737,11 @@ func stringValue(m map[string]any, key string) string { } // applyGeminiCLIHeaders sets required headers for the Gemini CLI upstream. +// User-Agent is always forced to the GeminiCLI format regardless of the client's value, +// so that upstream identifies the request as a native GeminiCLI client. 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", misc.GeminiCLIUserAgent(model)) + r.Header.Set("User-Agent", misc.GeminiCLIUserAgent(model)) + r.Header.Set("X-Goog-Api-Client", misc.GeminiCLIApiClientHeader) } // cliPreviewFallbackOrder returns preview model candidates for a base model. diff --git a/internal/translator/codex/claude/codex_claude_response.go b/internal/translator/codex/claude/codex_claude_response.go index cdcf2e4f..7f597062 100644 --- a/internal/translator/codex/claude/codex_claude_response.go +++ b/internal/translator/codex/claude/codex_claude_response.go @@ -22,8 +22,8 @@ var ( // ConvertCodexResponseToClaudeParams holds parameters for response conversion. type ConvertCodexResponseToClaudeParams struct { - HasToolCall bool - BlockIndex int + HasToolCall bool + BlockIndex int HasReceivedArgumentsDelta bool } diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go b/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go index 65732c3f..a2ede1b8 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go @@ -264,18 +264,18 @@ func TestConvertSystemRoleToDeveloper_AssistantRole(t *testing.T) { } } -func TestUserFieldDeletion(t *testing.T) { +func TestUserFieldDeletion(t *testing.T) { inputJSON := []byte(`{ "model": "gpt-5.2", "user": "test-user", "input": [{"role": "user", "content": "Hello"}] - }`) - - output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false) - outputStr := string(output) - - // Verify user field is deleted - userField := gjson.Get(outputStr, "user") + }`) + + output := ConvertOpenAIResponsesRequestToCodex("gpt-5.2", inputJSON, false) + outputStr := string(output) + + // Verify user field is deleted + userField := gjson.Get(outputStr, "user") if userField.Exists() { t.Errorf("user field should be deleted, but it was found with value: %s", userField.Raw) } From 9229708b6cc6a7490241f22b867f31d86b3d2ad9 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:30:32 +0800 Subject: [PATCH 19/20] revert(executor): re-apply PR #1735 antigravity changes with cleanup --- .../runtime/executor/antigravity_executor.go | 196 +++++++++++++++--- 1 file changed, 162 insertions(+), 34 deletions(-) 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) From 09fec34e1cdfd99ac79be458fff29f94b834dbcc Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Mon, 2 Mar 2026 20:30:07 +0800 Subject: [PATCH 20/20] chore(docs): update sponsor info and GLM model details in README files --- README.md | 4 ++-- README_CN.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d15e4196..80f6fbd0 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ So you can use local or multi-account CLI access with OpenAI(include Responses)/ ## Sponsor -[![z.ai](https://assets.router-for.me/english-4.7.png)](https://z.ai/subscribe?ic=8JVLJQFSKB) +[![z.ai](https://assets.router-for.me/english-5.png)](https://z.ai/subscribe?ic=8JVLJQFSKB) This project is sponsored by Z.ai, supporting us with their GLM CODING PLAN. -GLM CODING PLAN is a subscription service designed for AI coding, starting at just $3/month. It provides access to their flagship GLM-4.7 model across 10+ popular AI coding tools (Claude Code, Cline, Roo Code, etc.), offering developers top-tier, fast, and stable coding experiences. +GLM CODING PLAN is a subscription service designed for AI coding, starting at just $10/month. It provides access to their flagship GLM-4.7 & (GLM-5 Only Available for Pro Users)model across 10+ popular AI coding tools (Claude Code, Cline, Roo Code, etc.), offering developers top-tier, fast, and stable coding experiences. Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB diff --git a/README_CN.md b/README_CN.md index 8be15461..add9c5cf 100644 --- a/README_CN.md +++ b/README_CN.md @@ -10,13 +10,13 @@ ## 赞助商 -[![bigmodel.cn](https://assets.router-for.me/chinese-4.7.png)](https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII) +[![bigmodel.cn](https://assets.router-for.me/chinese-5.png)](https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII) 本项目由 Z智谱 提供赞助, 他们通过 GLM CODING PLAN 对本项目提供技术支持。 -GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元,即可在十余款主流AI编码工具如 Claude Code、Cline、Roo Code 中畅享智谱旗舰模型GLM-4.7,为开发者提供顶尖的编码体验。 +GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元,即可在十余款主流AI编码工具如 Claude Code、Cline、Roo Code 中畅享智谱旗舰模型GLM-4.7(受限于算力,目前仅限Pro用户开放),为开发者提供顶尖的编码体验。 -智谱AI为本软件提供了特别优惠,使用以下链接购买可以享受九折优惠:https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII +智谱AI为本产品提供了特别优惠,使用以下链接购买可以享受九折优惠:https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII ---