mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
Cloud Code resolves newer Antigravity models only for clients reporting at least 2.9.0; below that it answers 404 Requested entity was not found. The offline fallback still reported 2.9.0's predecessor 2.2.1, so every request sent before the hub manifest is first fetched — or from a deployment that cannot reach the manifest at all — asked for models such as gemini-3.7-flash-high with a version the backend rejects. The hub manifest the updater already polls currently publishes 2.9.1, so this only aligns the offline floor with what the online path resolves. The test now asserts the floor rather than a literal, so a future downgrade below 2.9.0 fails instead of silently reintroducing the 404.
271 lines
8.0 KiB
Go
271 lines
8.0 KiB
Go
// Package misc provides miscellaneous utility functions for the CLI Proxy API server.
|
|
package misc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
// antigravityFallbackVersion is the client version reported when the hub
|
|
// manifest has not been fetched yet or cannot be reached. Cloud Code rejects
|
|
// newer models for clients below 2.9.0, so this floor must stay at or above
|
|
// that version.
|
|
antigravityFallbackVersion = "2.9.1"
|
|
antigravityHubPlatform = "darwin/arm64"
|
|
antigravityVersionCacheTTL = 6 * time.Hour
|
|
antigravityFetchTimeout = 10 * time.Second
|
|
AntigravityNodeAPIClientUA = "google-api-nodejs-client/10.3.0"
|
|
AntigravityGoogAPIClientUA = "gl-node/22.21.1"
|
|
)
|
|
|
|
var (
|
|
antigravityHubLatestManifestURL = "https://antigravity-hub-auto-updater-974169037036.us-central1.run.app/manifest/latest-arm64-mac.yml"
|
|
)
|
|
|
|
type antigravityHubUpdaterManifest struct {
|
|
Version string `yaml:"version"`
|
|
}
|
|
|
|
var (
|
|
cachedAntigravityVersion = antigravityFallbackVersion
|
|
antigravityVersionMu sync.RWMutex
|
|
antigravityVersionExpiry time.Time
|
|
antigravityUpdaterOnce sync.Once
|
|
)
|
|
|
|
// StartAntigravityVersionUpdater starts a background goroutine that periodically refreshes the cached antigravity version.
|
|
// This is intentionally decoupled from request execution to avoid blocking executors on version lookups.
|
|
func StartAntigravityVersionUpdater(ctx context.Context) {
|
|
antigravityUpdaterOnce.Do(func() {
|
|
go runAntigravityVersionUpdater(ctx)
|
|
})
|
|
}
|
|
|
|
func runAntigravityVersionUpdater(ctx context.Context) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
ticker := time.NewTicker(antigravityVersionCacheTTL / 2)
|
|
defer ticker.Stop()
|
|
|
|
log.Infof("periodic antigravity version refresh started (interval=%s)", antigravityVersionCacheTTL/2)
|
|
|
|
refreshAntigravityVersion(ctx)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
refreshAntigravityVersion(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
func refreshAntigravityVersion(ctx context.Context) {
|
|
version, errFetch := fetchAntigravityLatestVersion(ctx)
|
|
|
|
antigravityVersionMu.Lock()
|
|
defer antigravityVersionMu.Unlock()
|
|
|
|
now := time.Now()
|
|
|
|
if errFetch == nil {
|
|
cachedAntigravityVersion = version
|
|
antigravityVersionExpiry = now.Add(antigravityVersionCacheTTL)
|
|
log.WithField("version", version).Info("fetched latest antigravity version")
|
|
return
|
|
}
|
|
|
|
if cachedAntigravityVersion == "" || now.After(antigravityVersionExpiry) {
|
|
cachedAntigravityVersion = antigravityFallbackVersion
|
|
antigravityVersionExpiry = now.Add(antigravityVersionCacheTTL)
|
|
log.WithError(errFetch).Warn("failed to refresh antigravity version, using fallback version")
|
|
return
|
|
}
|
|
|
|
log.WithError(errFetch).Debug("failed to refresh antigravity version, keeping cached value")
|
|
}
|
|
|
|
// AntigravityLatestVersion returns the cached antigravity version refreshed by StartAntigravityVersionUpdater.
|
|
// It falls back to antigravityFallbackVersion if the cache is empty or stale.
|
|
func AntigravityLatestVersion() string {
|
|
antigravityVersionMu.RLock()
|
|
if cachedAntigravityVersion != "" && time.Now().Before(antigravityVersionExpiry) {
|
|
v := cachedAntigravityVersion
|
|
antigravityVersionMu.RUnlock()
|
|
return v
|
|
}
|
|
antigravityVersionMu.RUnlock()
|
|
|
|
return antigravityFallbackVersion
|
|
}
|
|
|
|
// AntigravityUserAgent returns the User-Agent string used by the Antigravity Hub family.
|
|
func AntigravityUserAgent() string {
|
|
return fmt.Sprintf("antigravity/hub/%s %s", AntigravityLatestVersion(), antigravityHubPlatform)
|
|
}
|
|
|
|
func isAntigravityFamilyUserAgent(lower string) bool {
|
|
return strings.HasPrefix(lower, "antigravity/hub/") || strings.HasPrefix(lower, "antigravity/")
|
|
}
|
|
|
|
func antigravityBaseUserAgent(userAgent string) string {
|
|
userAgent = strings.TrimSpace(userAgent)
|
|
if userAgent == "" {
|
|
return AntigravityUserAgent()
|
|
}
|
|
lower := strings.ToLower(userAgent)
|
|
if isAntigravityFamilyUserAgent(lower) {
|
|
if idx := strings.Index(lower, " google-api-nodejs-client/"); idx >= 0 {
|
|
trimmed := strings.TrimSpace(userAgent[:idx])
|
|
if trimmed != "" {
|
|
return trimmed
|
|
}
|
|
}
|
|
}
|
|
return userAgent
|
|
}
|
|
|
|
// AntigravityRequestUserAgent returns the short Antigravity runtime UA used by
|
|
// generate/stream/model-list requests.
|
|
func AntigravityRequestUserAgent(userAgent string) string {
|
|
return antigravityBaseUserAgent(userAgent)
|
|
}
|
|
|
|
// AntigravityLoadCodeAssistUserAgent returns the short Antigravity UA used by
|
|
// loadCodeAssist requests.
|
|
func AntigravityLoadCodeAssistUserAgent(userAgent string) string {
|
|
return AntigravityRequestUserAgent(userAgent)
|
|
}
|
|
|
|
// AntigravityOnboardUserUserAgent returns the long Antigravity control-plane UA
|
|
// used by onboardUser requests.
|
|
func AntigravityOnboardUserUserAgent(userAgent string) string {
|
|
userAgent = strings.TrimSpace(userAgent)
|
|
if userAgent == "" {
|
|
return AntigravityUserAgent() + " " + AntigravityNodeAPIClientUA
|
|
}
|
|
lower := strings.ToLower(userAgent)
|
|
if !isAntigravityFamilyUserAgent(lower) {
|
|
return userAgent
|
|
}
|
|
if strings.Contains(lower, "google-api-nodejs-client/") {
|
|
return userAgent
|
|
}
|
|
return antigravityBaseUserAgent(userAgent) + " " + AntigravityNodeAPIClientUA
|
|
}
|
|
|
|
// AntigravityVersionFromUserAgent extracts the Antigravity version prefix from
|
|
// either the short or long Antigravity UA forms.
|
|
func AntigravityVersionFromUserAgent(userAgent string) string {
|
|
base := antigravityBaseUserAgent(userAgent)
|
|
lower := strings.ToLower(base)
|
|
if strings.HasPrefix(lower, "antigravity/hub/") {
|
|
rest := base[len("antigravity/hub/"):]
|
|
if idx := strings.IndexAny(rest, " "); idx >= 0 {
|
|
rest = rest[:idx]
|
|
}
|
|
rest = strings.TrimSpace(rest)
|
|
if rest == "" {
|
|
return AntigravityLatestVersion()
|
|
}
|
|
return rest
|
|
}
|
|
const legacyPrefix = "antigravity/"
|
|
if !strings.HasPrefix(lower, legacyPrefix) {
|
|
return AntigravityLatestVersion()
|
|
}
|
|
rest := base[len(legacyPrefix):]
|
|
if idx := strings.IndexAny(rest, " "); idx >= 0 {
|
|
rest = rest[:idx]
|
|
}
|
|
rest = strings.TrimSpace(rest)
|
|
if rest == "" {
|
|
return AntigravityLatestVersion()
|
|
}
|
|
return rest
|
|
}
|
|
|
|
func fetchAntigravityLatestVersion(ctx context.Context) (string, error) {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
client := &http.Client{Timeout: antigravityFetchTimeout}
|
|
return fetchAntigravityHubLatestManifestVersion(ctx, client)
|
|
}
|
|
|
|
func fetchAntigravityHubLatestManifestVersion(ctx context.Context, client *http.Client) (string, error) {
|
|
httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodGet, antigravityHubLatestManifestURL, nil)
|
|
if errReq != nil {
|
|
return "", fmt.Errorf("build antigravity Hub updater manifest request: %w", errReq)
|
|
}
|
|
httpReq.Header.Set("User-Agent", "electron-builder")
|
|
httpReq.Header.Set("Cache-Control", "no-cache")
|
|
|
|
resp, errDo := client.Do(httpReq)
|
|
if errDo != nil {
|
|
return "", fmt.Errorf("fetch antigravity Hub updater manifest: %w", errDo)
|
|
}
|
|
defer func() {
|
|
if errClose := resp.Body.Close(); errClose != nil {
|
|
log.WithError(errClose).Warn("antigravity Hub updater manifest response body close error")
|
|
}
|
|
}()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("antigravity Hub updater manifest returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
raw, errRead := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
if errRead != nil {
|
|
return "", fmt.Errorf("read antigravity Hub updater manifest: %w", errRead)
|
|
}
|
|
|
|
var manifest antigravityHubUpdaterManifest
|
|
if errDecode := yaml.Unmarshal(raw, &manifest); errDecode != nil {
|
|
return "", fmt.Errorf("decode antigravity Hub updater manifest: %w", errDecode)
|
|
}
|
|
|
|
version := strings.TrimSpace(manifest.Version)
|
|
if version == "" {
|
|
return "", errors.New("antigravity Hub updater manifest returned empty version")
|
|
}
|
|
if !isValidAntigravitySemVersion(version) {
|
|
return "", fmt.Errorf("antigravity Hub updater manifest returned invalid version %q", version)
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
func isValidAntigravitySemVersion(version string) bool {
|
|
parts := strings.Split(version, ".")
|
|
if len(parts) != 3 {
|
|
return false
|
|
}
|
|
|
|
for _, part := range parts {
|
|
if part == "" {
|
|
return false
|
|
}
|
|
for _, ch := range part {
|
|
if ch < '0' || ch > '9' {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|