Merge PR #1735 into dev with conflict resolution and fixes

This commit is contained in:
Luis Pater
2026-03-02 03:22:51 +08:00
12 changed files with 296 additions and 60 deletions

View File

@@ -13,6 +13,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -47,11 +48,12 @@ const (
codexCallbackPort = 1455 codexCallbackPort = 1455
geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com"
geminiCLIVersion = "v1internal" 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 { type callbackForwarder struct {
provider string provider string
server *http.Server server *http.Server
@@ -2285,9 +2287,7 @@ func callGeminiCLI(ctx context.Context, httpClient *http.Client, endpoint string
return fmt.Errorf("create request: %w", errRequest) return fmt.Errorf("create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", geminiCLIUserAgent) req.Header.Set("User-Agent", getGeminiCLIUserAgent())
req.Header.Set("X-Goog-Api-Client", geminiCLIApiClient)
req.Header.Set("Client-Metadata", geminiCLIClientMetadata)
resp, errDo := httpClient.Do(req) resp, errDo := httpClient.Do(req)
if errDo != nil { 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) return false, fmt.Errorf("failed to create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", geminiCLIUserAgent) req.Header.Set("User-Agent", getGeminiCLIUserAgent())
resp, errDo := httpClient.Do(req) resp, errDo := httpClient.Do(req)
if errDo != nil { if errDo != nil {
return false, fmt.Errorf("failed to execute request: %w", errDo) 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) return false, fmt.Errorf("failed to create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", geminiCLIUserAgent) req.Header.Set("User-Agent", getGeminiCLIUserAgent())
resp, errDo = httpClient.Do(req) resp, errDo = httpClient.Do(req)
if errDo != nil { if errDo != nil {
return false, fmt.Errorf("failed to execute request: %w", errDo) return false, fmt.Errorf("failed to execute request: %w", errDo)

View File

@@ -14,6 +14,7 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
log "github.com/sirupsen/logrus" 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-Api-Key")
req.Header.Del("X-Goog-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. // Remove query-based credentials if they match the authenticated client API key.
// This prevents leaking client auth material to the Amp upstream while avoiding // This prevents leaking client auth material to the Amp upstream while avoiding
// breaking unrelated upstream query parameters. // breaking unrelated upstream query parameters.

View File

@@ -20,6 +20,7 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "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/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "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" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -29,11 +30,12 @@ import (
const ( const (
geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com" geminiCLIEndpoint = "https://cloudcode-pa.googleapis.com"
geminiCLIVersion = "v1internal" 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{} type projectSelectionRequiredError struct{}
func (e *projectSelectionRequiredError) Error() string { 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) return fmt.Errorf("create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", geminiCLIUserAgent) req.Header.Set("User-Agent", getGeminiCLIUserAgent())
req.Header.Set("X-Goog-Api-Client", geminiCLIApiClient)
req.Header.Set("Client-Metadata", geminiCLIClientMetadata)
resp, errDo := httpClient.Do(req) resp, errDo := httpClient.Do(req)
if errDo != nil { 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) return false, fmt.Errorf("failed to create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", geminiCLIUserAgent) req.Header.Set("User-Agent", getGeminiCLIUserAgent())
resp, errDo := httpClient.Do(req) resp, errDo := httpClient.Do(req)
if errDo != nil { if errDo != nil {
return false, fmt.Errorf("failed to execute request: %w", errDo) 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) return false, fmt.Errorf("failed to create request: %w", errRequest)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", geminiCLIUserAgent) req.Header.Set("User-Agent", getGeminiCLIUserAgent())
resp, errDo = httpClient.Do(req) resp, errDo = httpClient.Do(req)
if errDo != nil { if errDo != nil {
return false, fmt.Errorf("failed to execute request: %w", errDo) return false, fmt.Errorf("failed to execute request: %w", errDo)

View File

@@ -14,10 +14,15 @@ var antigravityModelConversionTable = map[string]string{
"gemini-3-pro-image-preview": "gemini-3-pro-image", "gemini-3-pro-image-preview": "gemini-3-pro-image",
"gemini-3-pro-preview": "gemini-3-pro-high", "gemini-3-pro-preview": "gemini-3-pro-high",
"gemini-3-flash-preview": "gemini-3-flash", "gemini-3-flash-preview": "gemini-3-flash",
"gemini-claude-sonnet-4-5": "claude-sonnet-4-5", "gemini-3.1-pro-preview": "gemini-3.1-pro-high",
"gemini-claude-sonnet-4-5-thinking": "claude-sonnet-4-5-thinking", "gemini-claude-sonnet-4-5": "claude-sonnet-4-6",
"gemini-claude-opus-4-5-thinking": "claude-opus-4-5-thinking", "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-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 // 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-image", Alias: "gemini-3-pro-image-preview"},
{Name: "gemini-3-pro-high", Alias: "gemini-3-pro-preview"}, {Name: "gemini-3-pro-high", Alias: "gemini-3-pro-preview"},
{Name: "gemini-3-flash", Alias: "gemini-3-flash-preview"}, {Name: "gemini-3-flash", Alias: "gemini-3-flash-preview"},
{Name: "claude-sonnet-4-5", Alias: "gemini-claude-sonnet-4-5"}, {Name: "gemini-3.1-pro-high", Alias: "gemini-3.1-pro-preview"},
{Name: "claude-sonnet-4-5-thinking", Alias: "gemini-claude-sonnet-4-5-thinking"}, {Name: "claude-sonnet-4-6", Alias: "gemini-claude-sonnet-4-5"},
{Name: "claude-opus-4-5-thinking", Alias: "gemini-claude-opus-4-5-thinking"}, {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"}, {Name: "claude-opus-4-6-thinking", Alias: "gemini-claude-opus-4-6-thinking"},
} }
} }

View File

@@ -4,10 +4,68 @@
package misc package misc
import ( import (
"fmt"
"net/http" "net/http"
"runtime"
"strings" "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 // EnsureHeader ensures that a header exists in the target header map by checking
// multiple sources in order of priority: source headers, existing target headers, // 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 // 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) target.Set(key, val)
} }
} }

View File

@@ -47,6 +47,10 @@ type ModelInfo struct {
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
// SupportedParameters lists supported parameters // SupportedParameters lists supported parameters
SupportedParameters []string `json:"supported_parameters,omitempty"` 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. // Thinking holds provider-specific reasoning/thinking budget capabilities.
// This is optional and currently used for Gemini thinking budget normalization. // 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 { if len(model.SupportedParameters) > 0 {
copyModel.SupportedParameters = append([]string(nil), model.SupportedParameters...) 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 &copyModel return &copyModel
} }
@@ -1067,6 +1077,12 @@ func (r *ModelRegistry) convertModelToMap(model *ModelInfo, handlerType string)
if len(model.SupportedGenerationMethods) > 0 { if len(model.SupportedGenerationMethods) > 0 {
result["supportedGenerationMethods"] = model.SupportedGenerationMethods result["supportedGenerationMethods"] = model.SupportedGenerationMethods
} }
if len(model.SupportedInputModalities) > 0 {
result["supportedInputModalities"] = model.SupportedInputModalities
}
if len(model.SupportedOutputModalities) > 0 {
result["supportedOutputModalities"] = model.SupportedOutputModalities
}
return result return result
default: default:

View File

@@ -8,6 +8,7 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/sha256" "crypto/sha256"
"crypto/tls"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
@@ -45,10 +46,10 @@ const (
antigravityModelsPath = "/v1internal:fetchAvailableModels" antigravityModelsPath = "/v1internal:fetchAvailableModels"
antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com" antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
antigravityClientSecret = "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" antigravityClientSecret = "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"
defaultAntigravityAgent = "antigravity/1.104.0 darwin/arm64" defaultAntigravityAgent = "antigravity/1.19.6 windows/amd64"
antigravityAuthType = "antigravity" antigravityAuthType = "antigravity"
refreshSkew = 3000 * time.Second 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 = "<identity> 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. </identity>"
) )
var ( var (
@@ -142,6 +143,62 @@ func NewAntigravityExecutor(cfg *config.Config) *AntigravityExecutor {
return &AntigravityExecutor{cfg: cfg} 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. // Identifier returns the executor identifier.
func (e *AntigravityExecutor) Identifier() string { return antigravityAuthType } 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. // 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) { func (e *AntigravityExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) {
if req == nil { if req == nil {
return nil, fmt.Errorf("antigravity executor: request is 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() ctx = req.Context()
} }
httpReq := req.WithContext(ctx) 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 <token>
if err := e.PrepareRequest(httpReq, auth); err != nil { if err := e.PrepareRequest(httpReq, auth); err != nil {
return nil, err return nil, err
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
return httpClient.Do(httpReq) return httpClient.Do(httpReq)
} }
@@ -185,7 +263,7 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
baseModel := thinking.ParseSuffix(req.Model).ModelName baseModel := thinking.ParseSuffix(req.Model).ModelName
isClaude := strings.Contains(strings.ToLower(baseModel), "claude") 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) 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) translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
baseURLs := antigravityBaseURLFallbackOrder(auth) baseURLs := antigravityBaseURLFallbackOrder(auth)
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
attempts := antigravityRetryAttempts(auth, e.cfg) 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) translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
baseURLs := antigravityBaseURLFallbackOrder(auth) baseURLs := antigravityBaseURLFallbackOrder(auth)
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
attempts := antigravityRetryAttempts(auth, e.cfg) 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) translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
baseURLs := antigravityBaseURLFallbackOrder(auth) baseURLs := antigravityBaseURLFallbackOrder(auth)
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
attempts := antigravityRetryAttempts(auth, e.cfg) attempts := antigravityRetryAttempts(auth, e.cfg)
@@ -956,7 +1034,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
payload = deleteJSONField(payload, "request.safetySettings") payload = deleteJSONField(payload, "request.safetySettings")
baseURLs := antigravityBaseURLFallbackOrder(auth) baseURLs := antigravityBaseURLFallbackOrder(auth)
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
var authID, authLabel, authType, authValue string var authID, authLabel, authType, authValue string
if auth != nil { if auth != nil {
@@ -987,10 +1065,10 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
if errReq != nil { if errReq != nil {
return cliproxyexecutor.Response{}, errReq return cliproxyexecutor.Response{}, errReq
} }
httpReq.Close = true
httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("Authorization", "Bearer "+token)
httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) httpReq.Header.Set("User-Agent", resolveUserAgent(auth))
httpReq.Header.Set("Accept", "application/json")
if host := resolveHost(base); host != "" { if host := resolveHost(base); host != "" {
httpReq.Host = host httpReq.Host = host
} }
@@ -1084,14 +1162,26 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c
} }
baseURLs := antigravityBaseURLFallbackOrder(auth) baseURLs := antigravityBaseURLFallbackOrder(auth)
httpClient := newProxyAwareHTTPClient(ctx, cfg, auth, 0) httpClient := newAntigravityHTTPClient(ctx, cfg, auth, 0)
for idx, baseURL := range baseURLs { for idx, baseURL := range baseURLs {
modelsURL := baseURL + antigravityModelsPath 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 { if errReq != nil {
return fallbackAntigravityPrimaryModels() return fallbackAntigravityPrimaryModels()
} }
httpReq.Close = true
httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("Authorization", "Bearer "+token)
httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) httpReq.Header.Set("User-Agent", resolveUserAgent(auth))
@@ -1152,7 +1242,8 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c
continue continue
} }
switch modelID { 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 continue
} }
modelCfg := modelConfig[modelID] modelCfg := modelConfig[modelID]
@@ -1174,6 +1265,29 @@ func FetchAntigravityModels(ctx context.Context, auth *cliproxyauth.Auth, cfg *c
OwnedBy: antigravityAuthType, OwnedBy: antigravityAuthType,
Type: 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. // Look up Thinking support from static config using upstream model name.
if modelCfg != nil { if modelCfg != nil {
if modelCfg.Thinking != nil { if modelCfg.Thinking != nil {
@@ -1241,10 +1355,11 @@ func (e *AntigravityExecutor) refreshToken(ctx context.Context, auth *cliproxyau
return auth, errReq return auth, errReq
} }
httpReq.Header.Set("Host", "oauth2.googleapis.com") httpReq.Header.Set("Host", "oauth2.googleapis.com")
httpReq.Header.Set("User-Agent", defaultAntigravityAgent)
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") 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) httpResp, errDo := httpClient.Do(httpReq)
if errDo != nil { if errDo != nil {
return auth, errDo return auth, errDo
@@ -1315,7 +1430,7 @@ func (e *AntigravityExecutor) ensureAntigravityProjectID(ctx context.Context, au
return nil return nil
} }
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0) httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
projectID, errFetch := sdkAuth.FetchAntigravityProjectID(ctx, token, httpClient) projectID, errFetch := sdkAuth.FetchAntigravityProjectID(ctx, token, httpClient)
if errFetch != nil { if errFetch != nil {
return errFetch return errFetch
@@ -1369,7 +1484,7 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
payload = geminiToAntigravity(modelName, payload, projectID) payload = geminiToAntigravity(modelName, payload, projectID)
payload, _ = sjson.SetBytes(payload, "model", modelName) 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) payloadStr := string(payload)
paths := make([]string, 0) paths := make([]string, 0)
util.Walk(gjson.Parse(payloadStr), "", "parametersJsonSchema", &paths) util.Walk(gjson.Parse(payloadStr), "", "parametersJsonSchema", &paths)
@@ -1406,14 +1521,10 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
if errReq != nil { if errReq != nil {
return nil, errReq return nil, errReq
} }
httpReq.Close = true
httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token) httpReq.Header.Set("Authorization", "Bearer "+token)
httpReq.Header.Set("User-Agent", resolveUserAgent(auth)) 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 != "" { if host := resolveHost(base); host != "" {
httpReq.Host = host httpReq.Host = host
} }
@@ -1625,7 +1736,16 @@ func resolveCustomAntigravityBaseURL(auth *cliproxyauth.Auth) string {
func geminiToAntigravity(modelName string, payload []byte, projectID string) []byte { func geminiToAntigravity(modelName string, payload []byte, projectID string) []byte {
template, _ := sjson.Set(string(payload), "model", modelName) template, _ := sjson.Set(string(payload), "model", modelName)
template, _ = sjson.Set(template, "userAgent", "antigravity") 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) // Use real project ID from auth if available, otherwise generate random (legacy fallback)
if projectID != "" { if projectID != "" {
@@ -1633,8 +1753,13 @@ func geminiToAntigravity(modelName string, payload []byte, projectID string) []b
} else { } else {
template, _ = sjson.Set(template, "project", generateProjectID()) 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") template, _ = sjson.Delete(template, "request.safetySettings")
if toolConfig := gjson.Get(template, "toolConfig"); toolConfig.Exists() && !gjson.Get(template, "request.toolConfig").Exists() { 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() return "agent-" + uuid.NewString()
} }
func generateImageGenRequestID() string {
return fmt.Sprintf("image_gen/%d/%s/12", time.Now().UnixMilli(), uuid.NewString())
}
func generateSessionID() string { func generateSessionID() string {
randSourceMutex.Lock() randSourceMutex.Lock()
n := randSource.Int63n(9_000_000_000_000_000_000) n := randSource.Int63n(9_000_000_000_000_000_000)

View File

@@ -81,7 +81,7 @@ func (e *GeminiCLIExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth
return statusErr{code: http.StatusUnauthorized, msg: "missing access token"} return statusErr{code: http.StatusUnauthorized, msg: "missing access token"}
} }
req.Header.Set("Authorization", "Bearer "+tok.AccessToken) req.Header.Set("Authorization", "Bearer "+tok.AccessToken)
applyGeminiCLIHeaders(req) applyGeminiCLIHeaders(req, "unknown")
return nil 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("Content-Type", "application/json")
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
applyGeminiCLIHeaders(reqHTTP) applyGeminiCLIHeaders(reqHTTP, attemptModel)
reqHTTP.Header.Set("Accept", "application/json") reqHTTP.Header.Set("Accept", "application/json")
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
URL: url, 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("Content-Type", "application/json")
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
applyGeminiCLIHeaders(reqHTTP) applyGeminiCLIHeaders(reqHTTP, attemptModel)
reqHTTP.Header.Set("Accept", "text/event-stream") reqHTTP.Header.Set("Accept", "text/event-stream")
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
URL: url, 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("Content-Type", "application/json")
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken) reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
applyGeminiCLIHeaders(reqHTTP) applyGeminiCLIHeaders(reqHTTP, baseModel)
reqHTTP.Header.Set("Accept", "application/json") reqHTTP.Header.Set("Accept", "application/json")
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{ recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
URL: url, URL: url,
@@ -738,21 +738,13 @@ func stringValue(m map[string]any, key string) string {
} }
// applyGeminiCLIHeaders sets required headers for the Gemini CLI upstream. // 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 var ginHeaders http.Header
if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil { if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
ginHeaders = ginCtx.Request.Header ginHeaders = ginCtx.Request.Header
} }
misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", "google-api-nodejs-client/9.15.1") misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", misc.GeminiCLIUserAgent(model))
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"
} }
// cliPreviewFallbackOrder returns preview model candidates for a base model. // cliPreviewFallbackOrder returns preview model candidates for a base model.

View File

@@ -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)
}

View File

@@ -34,6 +34,11 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
// Model // Model
out, _ = sjson.SetBytes(out, "model", modelName) 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. // Apply thinking configuration: convert OpenAI reasoning_effort to Gemini CLI thinkingConfig.
// Inline translation-only mapping; capability checks happen later in ApplyThinking. // Inline translation-only mapping; capability checks happen later in ApplyThinking.
re := gjson.GetBytes(rawJSON, "reasoning_effort") re := gjson.GetBytes(rawJSON, "reasoning_effort")

View File

@@ -34,6 +34,11 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
// Model // Model
out, _ = sjson.SetBytes(out, "model", modelName) 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. // Apply thinking configuration: convert OpenAI reasoning_effort to Gemini CLI thinkingConfig.
// Inline translation-only mapping; capability checks happen later in ApplyThinking. // Inline translation-only mapping; capability checks happen later in ApplyThinking.
re := gjson.GetBytes(rawJSON, "reasoning_effort") re := gjson.GetBytes(rawJSON, "reasoning_effort")

View File

@@ -34,6 +34,11 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
// Model // Model
out, _ = sjson.SetBytes(out, "model", modelName) 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. // Apply thinking configuration: convert OpenAI reasoning_effort to Gemini thinkingConfig.
// Inline translation-only mapping; capability checks happen later in ApplyThinking. // Inline translation-only mapping; capability checks happen later in ApplyThinking.
re := gjson.GetBytes(rawJSON, "reasoning_effort") re := gjson.GetBytes(rawJSON, "reasoning_effort")