diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go
index 9f03c5b4..10edfa29 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
@@ -2285,9 +2287,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 {
@@ -2357,7 +2357,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)
@@ -2378,7 +2378,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/api/modules/amp/proxy.go b/internal/api/modules/amp/proxy.go
index b7d10760..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"
)
@@ -76,6 +77,9 @@ func createReverseProxy(upstreamURL string, secretSource SecretSource) (*httputi
req.Header.Del("X-Api-Key")
req.Header.Del("X-Goog-Api-Key")
+ // 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
// breaking unrelated upstream query parameters.
diff --git a/internal/cmd/login.go b/internal/cmd/login.go
index 1d8a1ae3..1162dc68 100644
--- a/internal/cmd/login.go
+++ b/internal/cmd/login.go
@@ -20,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"
@@ -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 misc.GeminiCLIUserAgent("")
+}
+
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/config/oauth_model_alias_migration.go b/internal/config/oauth_model_alias_migration.go
index f52df27a..71613d03 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-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-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
@@ -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-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"},
}
}
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/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 919d96fa..bd32a422 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 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 (
@@ -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))
@@ -1152,7 +1242,8 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c
continue
}
switch modelID {
- case "chat_20706", "chat_23310", "tab_flash_lite_preview", "tab_jump_flash_lite_preview", "gemini-2.5-flash-thinking", "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]
@@ -1174,6 +1265,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 +1355,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 +1430,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 +1484,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)
@@ -1406,14 +1521,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 +1736,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 +1753,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 +1773,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)
diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go
index cb3ffb59..504f32c8 100644
--- a/internal/runtime/executor/gemini_cli_executor.go
+++ b/internal/runtime/executor/gemini_cli_executor.go
@@ -81,7 +81,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 +189,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 +334,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 +515,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 +738,13 @@ 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())
-}
-
-// 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"
+ 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
new file mode 100644
index 00000000..41eb80d3
--- /dev/null
+++ b/internal/runtime/executor/header_scrub.go
@@ -0,0 +1,12 @@
+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)
+}
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")