From 34e73c75a3a4f9d6b5d7c2c98c44fdffcc060ec9 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Thu, 8 Oct 2026 02:39:32 +0800 Subject: [PATCH] fix(xai): add proxy support and semver validation to Grok CLI updater - Pass configured proxy URL to the npm registry updater client - Validate fetched versions against strict semver and minimum server version floor (`1.0.13`) - Update default fallback client version to `1.0.46` - Manage background updater lifecycle and context via service lifecycle initialization --- cmd/server/main.go | 3 - .../runtime/executor/helps/xai_version.go | 151 +++++++++--- .../executor/helps/xai_version_test.go | 214 ++++++++++++++++-- .../executor/xai_client_version_test.go | 2 +- internal/runtime/executor/xai_executor.go | 5 +- sdk/cliproxy/service_lifecycle.go | 4 +- 6 files changed, 324 insertions(+), 55 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 507f7b66d3..3bcd5e0d7a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -32,7 +32,6 @@ import ( "github.com/router-for-me/CLIProxyAPI/v8/internal/pluginhost" "github.com/router-for-me/CLIProxyAPI/v8/internal/redisqueue" "github.com/router-for-me/CLIProxyAPI/v8/internal/registry" - "github.com/router-for-me/CLIProxyAPI/v8/internal/runtime/executor" "github.com/router-for-me/CLIProxyAPI/v8/internal/safemode" "github.com/router-for-me/CLIProxyAPI/v8/internal/store" _ "github.com/router-for-me/CLIProxyAPI/v8/internal/translator" @@ -752,7 +751,6 @@ func main() { // Standalone mode: start an embedded local server and connect TUI client to it. managementasset.StartAutoUpdater(context.Background(), configFilePath) misc.StartAntigravityVersionUpdater(context.Background()) - executor.StartXAIVersionUpdater(context.Background()) registry.SetLocalModelCatalogs(localModel) hook := tui.NewLogHook(2000) hook.SetFormatter(&logging.LogFormatter{}) @@ -828,7 +826,6 @@ func main() { // Start the main proxy service managementasset.StartAutoUpdater(context.Background(), configFilePath) misc.StartAntigravityVersionUpdater(context.Background()) - executor.StartXAIVersionUpdater(context.Background()) registry.SetLocalModelCatalogs(localModel) cmd.StartServiceWithPluginHost(cfg, configFilePath, password, pluginHost, serverOptions...) } diff --git a/internal/runtime/executor/helps/xai_version.go b/internal/runtime/executor/helps/xai_version.go index 9e70163ac7..f6d1e83fb0 100644 --- a/internal/runtime/executor/helps/xai_version.go +++ b/internal/runtime/executor/helps/xai_version.go @@ -6,18 +6,23 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "sync" - "testing" "time" + "github.com/router-for-me/CLIProxyAPI/v8/internal/config" log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" ) const ( - // DefaultXAIFallbackClientVersion is the fallback Grok CLI version when npm registry resolution fails. - DefaultXAIFallbackClientVersion = "1.0.50" + // DefaultXAIFallbackClientVersion is the stable Grok CLI version used when npm + // registry resolution fails. dist-tag latest is 1.0.46; 1.0.50 is the alpha tag. + DefaultXAIFallbackClientVersion = "1.0.46" + // XAIClientVersionServerFloor is the minimum Grok CLI version cli-chat-proxy accepts. + // Older clients are rejected with HTTP 426 (#6249). + XAIClientVersionServerFloor = "1.0.13" // XAIVersionRefreshInterval is the periodic interval to check for Grok CLI updates from npm. XAIVersionRefreshInterval = 3 * time.Hour // XAIVersionFetchTimeout is the maximum duration for a single npm registry lookup. @@ -30,12 +35,14 @@ var ( var ( cachedXAIClientVersion = DefaultXAIFallbackClientVersion + xaiVersionProxyURL string xaiClientVersionMu sync.RWMutex - xaiVersionUpdaterOnce sync.Once + xaiUpdaterCancel context.CancelFunc + xaiVersionRefreshed chan struct{} ) // GetXAIClientVersion returns the current Grok CLI client version. -// If the background updater has fetched a newer version from npm, it returns that version; +// If the background updater has fetched a newer acceptable version from npm, it returns that version; // otherwise it returns DefaultXAIFallbackClientVersion. func GetXAIClientVersion() string { xaiClientVersionMu.RLock() @@ -44,19 +51,26 @@ func GetXAIClientVersion() string { } // StartXAIVersionUpdater starts a background goroutine that periodically refreshes the Grok CLI version from npm. +// A later call cancels the previous goroutine and binds the new service context and proxy URL. // It executes a single fetch on startup and then polls every 3 hours without multiple retries on failure. -func StartXAIVersionUpdater(ctx context.Context) { - xaiVersionUpdaterOnce.Do(func() { - go runXAIVersionUpdater(ctx) - }) -} - -func runXAIVersionUpdater(ctx context.Context) { +func StartXAIVersionUpdater(ctx context.Context, proxyURL string) { if ctx == nil { ctx = context.Background() } + runCtx, cancel := context.WithCancel(ctx) - // Fetch once on startup + xaiClientVersionMu.Lock() + if xaiUpdaterCancel != nil { + xaiUpdaterCancel() + } + xaiUpdaterCancel = cancel + xaiVersionProxyURL = strings.TrimSpace(proxyURL) + xaiClientVersionMu.Unlock() + + go runXAIVersionUpdater(runCtx) +} + +func runXAIVersionUpdater(ctx context.Context) { refreshXAIClientVersion(ctx) ticker := time.NewTicker(XAIVersionRefreshInterval) @@ -75,25 +89,28 @@ func runXAIVersionUpdater(ctx context.Context) { } func refreshXAIClientVersion(ctx context.Context) { + defer notifyXAIVersionRefreshed() + version, errFetch := FetchXAINPMLatestVersion(ctx, nil) if errFetch != nil { log.WithError(errFetch).Warn("failed to fetch latest Grok CLI version from npm, keeping fallback/cached version") return } - if version == "" { - log.Warn("fetched empty Grok CLI version from npm, keeping fallback/cached version") - return - } - xaiClientVersionMu.Lock() - cachedXAIClientVersion = version + changed := cachedXAIClientVersion != version + if changed { + cachedXAIClientVersion = version + } xaiClientVersionMu.Unlock() - log.WithField("version", version).Info("updated Grok CLI client version from npm") + if changed { + log.WithField("version", version).Info("updated Grok CLI client version from npm") + } } // FetchXAINPMLatestVersion performs a single request to the npm registry to query the latest version of @xai-official/grok. +// The returned version is a strict numeric semver at or above XAIClientVersionServerFloor. func FetchXAINPMLatestVersion(ctx context.Context, client *http.Client) (string, error) { if ctx == nil { ctx = context.Background() @@ -114,7 +131,7 @@ func FetchXAINPMLatestVersion(ctx context.Context, client *http.Client) (string, req.Header.Set("User-Agent", "CLIProxyAPI") if client == nil { - client = &http.Client{Timeout: XAIVersionFetchTimeout} + client = xaiVersionHTTPClient() } resp, errDo := client.Do(req) @@ -140,12 +157,88 @@ func FetchXAINPMLatestVersion(ctx context.Context, client *http.Client) (string, if version == "" { return "", errors.New("version not found in npm response") } + if !acceptableXAIClientVersion(version) { + return "", fmt.Errorf("npm registry returned unacceptable Grok CLI version %s", strconv.Quote(version)) + } return version, nil } +func xaiVersionHTTPClient() *http.Client { + xaiClientVersionMu.RLock() + proxyURL := xaiVersionProxyURL + xaiClientVersionMu.RUnlock() + if strings.TrimSpace(proxyURL) == "" { + return &http.Client{Timeout: XAIVersionFetchTimeout} + } + return NewProxyAwareHTTPClient(context.Background(), &config.Config{SDKConfig: config.SDKConfig{ProxyURL: proxyURL}}, nil, XAIVersionFetchTimeout) +} + +func acceptableXAIClientVersion(version string) bool { + if !isStrictXAISemver(version) { + return false + } + return xaiVersionAtLeast(version, XAIClientVersionServerFloor) +} + +func isStrictXAISemver(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 +} + +func xaiVersionAtLeast(got, floor string) bool { + gotParts := strings.Split(got, ".") + floorParts := strings.Split(floor, ".") + for i := 0; i < len(gotParts) || i < len(floorParts); i++ { + var g, f int + var errG, errF error + if i < len(gotParts) { + g, errG = strconv.Atoi(gotParts[i]) + if errG != nil { + return false + } + } + if i < len(floorParts) { + f, errF = strconv.Atoi(floorParts[i]) + if errF != nil { + return false + } + } + if g != f { + return g > f + } + } + return true +} + +func notifyXAIVersionRefreshed() { + xaiClientVersionMu.RLock() + ch := xaiVersionRefreshed + xaiClientVersionMu.RUnlock() + if ch == nil { + return + } + select { + case ch <- struct{}{}: + default: + } +} + // OverrideXAINPMRegistryURLForTest overrides the npm registry URL for testing purposes. -func OverrideXAINPMRegistryURLForTest(t *testing.T, url string) func() { +func OverrideXAINPMRegistryURLForTest(url string) func() { xaiClientVersionMu.Lock() oldURL := xaiNPMRegistryURL xaiNPMRegistryURL = url @@ -158,7 +251,7 @@ func OverrideXAINPMRegistryURLForTest(t *testing.T, url string) func() { } // SetXAIClientVersionForTest sets the cached Grok CLI version directly for testing purposes. -func SetXAIClientVersionForTest(t *testing.T, version string) func() { +func SetXAIClientVersionForTest(version string) func() { xaiClientVersionMu.Lock() old := cachedXAIClientVersion cachedXAIClientVersion = version @@ -170,9 +263,15 @@ func SetXAIClientVersionForTest(t *testing.T, version string) func() { } } -// ResetXAIVersionUpdaterOnceForTest resets the sync.Once for testing updater initialization. -func ResetXAIVersionUpdaterOnceForTest(t *testing.T) { +// SetXAIVersionRefreshedHookForTest receives one signal after each refresh attempt. +func SetXAIVersionRefreshedHookForTest(ch chan struct{}) func() { xaiClientVersionMu.Lock() - xaiVersionUpdaterOnce = sync.Once{} + old := xaiVersionRefreshed + xaiVersionRefreshed = ch xaiClientVersionMu.Unlock() + return func() { + xaiClientVersionMu.Lock() + xaiVersionRefreshed = old + xaiClientVersionMu.Unlock() + } } diff --git a/internal/runtime/executor/helps/xai_version_test.go b/internal/runtime/executor/helps/xai_version_test.go index 700202ced1..cbcc5f44bd 100644 --- a/internal/runtime/executor/helps/xai_version_test.go +++ b/internal/runtime/executor/helps/xai_version_test.go @@ -2,14 +2,16 @@ package helps import ( "context" + "io" "net/http" "net/http/httptest" + "strconv" + "sync/atomic" "testing" - "time" ) func TestGetXAIClientVersionDefault(t *testing.T) { - restore := SetXAIClientVersionForTest(t, DefaultXAIFallbackClientVersion) + restore := SetXAIClientVersionForTest(DefaultXAIFallbackClientVersion) defer restore() if got := GetXAIClientVersion(); got != DefaultXAIFallbackClientVersion { @@ -27,7 +29,7 @@ func TestFetchXAINPMLatestVersionSuccess(t *testing.T) { })) defer server.Close() - restore := OverrideXAINPMRegistryURLForTest(t, server.URL) + restore := OverrideXAINPMRegistryURLForTest(server.URL) defer restore() version, err := FetchXAINPMLatestVersion(context.Background(), server.Client()) @@ -45,7 +47,7 @@ func TestFetchXAINPMLatestVersionHttpError(t *testing.T) { })) defer server.Close() - restore := OverrideXAINPMRegistryURLForTest(t, server.URL) + restore := OverrideXAINPMRegistryURLForTest(server.URL) defer restore() _, err := FetchXAINPMLatestVersion(context.Background(), server.Client()) @@ -61,7 +63,7 @@ func TestFetchXAINPMLatestVersionMissingVersion(t *testing.T) { })) defer server.Close() - restore := OverrideXAINPMRegistryURLForTest(t, server.URL) + restore := OverrideXAINPMRegistryURLForTest(server.URL) defer restore() _, err := FetchXAINPMLatestVersion(context.Background(), server.Client()) @@ -71,7 +73,7 @@ func TestFetchXAINPMLatestVersionMissingVersion(t *testing.T) { } func TestRefreshXAIClientVersionKeepsFallbackOnError(t *testing.T) { - restoreVersion := SetXAIClientVersionForTest(t, "1.0.50") + restoreVersion := SetXAIClientVersionForTest("1.0.50") defer restoreVersion() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -79,7 +81,7 @@ func TestRefreshXAIClientVersionKeepsFallbackOnError(t *testing.T) { })) defer server.Close() - restoreURL := OverrideXAINPMRegistryURLForTest(t, server.URL) + restoreURL := OverrideXAINPMRegistryURLForTest(server.URL) defer restoreURL() refreshXAIClientVersion(context.Background()) @@ -90,7 +92,7 @@ func TestRefreshXAIClientVersionKeepsFallbackOnError(t *testing.T) { } func TestRefreshXAIClientVersionUpdatesOnSuccess(t *testing.T) { - restoreVersion := SetXAIClientVersionForTest(t, "1.0.50") + restoreVersion := SetXAIClientVersionForTest("1.0.50") defer restoreVersion() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -99,7 +101,7 @@ func TestRefreshXAIClientVersionUpdatesOnSuccess(t *testing.T) { })) defer server.Close() - restoreURL := OverrideXAINPMRegistryURLForTest(t, server.URL) + restoreURL := OverrideXAINPMRegistryURLForTest(server.URL) defer restoreURL() refreshXAIClientVersion(context.Background()) @@ -109,9 +111,123 @@ func TestRefreshXAIClientVersionUpdatesOnSuccess(t *testing.T) { } } +func TestRefreshXAIClientVersionAcceptsStableLatest(t *testing.T) { + restoreVersion := SetXAIClientVersionForTest("1.0.44") + defer restoreVersion() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"version":"1.0.46"}`)) + })) + defer server.Close() + + restoreURL := OverrideXAINPMRegistryURLForTest(server.URL) + defer restoreURL() + + version, errFetch := FetchXAINPMLatestVersion(context.Background(), server.Client()) + if errFetch != nil { + t.Fatalf("FetchXAINPMLatestVersion() error = %v", errFetch) + } + if version != "1.0.46" { + t.Fatalf("FetchXAINPMLatestVersion() = %q, want 1.0.46", version) + } + refreshXAIClientVersion(context.Background()) + if got := GetXAIClientVersion(); got != "1.0.46" { + t.Fatalf("GetXAIClientVersion() = %q, want stable latest 1.0.46", got) + } +} + +func TestRefreshXAIClientVersionRejectsUnsafeVersions(t *testing.T) { + restoreVersion := SetXAIClientVersionForTest(DefaultXAIFallbackClientVersion) + defer restoreVersion() + + unsafeVersions := []string{ + "0.2.93", + "1.0.12", + "1.0.55-beta.1", + "1.0.55\r\nX-Injected: yes", + } + for _, unsafeVersion := range unsafeVersions { + t.Run(unsafeVersion, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"version":` + strconv.Quote(unsafeVersion) + `}`)) + })) + defer server.Close() + + restoreURL := OverrideXAINPMRegistryURLForTest(server.URL) + defer restoreURL() + + if _, errFetch := FetchXAINPMLatestVersion(context.Background(), server.Client()); errFetch == nil { + t.Fatal("FetchXAINPMLatestVersion() expected error for unsafe version, got nil") + } + refreshXAIClientVersion(context.Background()) + if got := GetXAIClientVersion(); got != DefaultXAIFallbackClientVersion { + t.Fatalf("GetXAIClientVersion() = %q, want fallback %s preserved", got, DefaultXAIFallbackClientVersion) + } + }) + } +} + +func TestRefreshXAIClientVersionUsesConfiguredProxy(t *testing.T) { + restoreVersion := SetXAIClientVersionForTest(DefaultXAIFallbackClientVersion) + defer restoreVersion() + + registry := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"version":"1.0.61"}`)) + })) + defer registry.Close() + + var proxied atomic.Bool + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + proxied.Store(true) + if r.URL.Scheme == "" || r.URL.Host == "" { + http.Error(w, "expected absolute proxy URI", http.StatusBadRequest) + return + } + outbound, errReq := http.NewRequestWithContext(r.Context(), r.Method, r.URL.String(), nil) + if errReq != nil { + http.Error(w, errReq.Error(), http.StatusBadGateway) + return + } + outbound.Header = r.Header.Clone() + direct := &http.Client{Transport: &http.Transport{Proxy: nil}} + resp, errDo := direct.Do(outbound) + if errDo != nil { + http.Error(w, errDo.Error(), http.StatusBadGateway) + return + } + defer func() { + _ = resp.Body.Close() + }() + for key, values := range resp.Header { + for _, value := range values { + w.Header().Add(key, value) + } + } + w.WriteHeader(resp.StatusCode) + _, _ = io.Copy(w, resp.Body) + })) + defer proxyServer.Close() + + restoreURL := OverrideXAINPMRegistryURLForTest(registry.URL) + defer restoreURL() + restoreProxy := setXAIVersionProxyURLForTest(proxyServer.URL) + defer restoreProxy() + + refreshXAIClientVersion(context.Background()) + + if !proxied.Load() { + t.Fatal("expected npm lookup to use the configured proxy") + } + if got := GetXAIClientVersion(); got != "1.0.61" { + t.Fatalf("GetXAIClientVersion() = %q, want 1.0.61 via proxy", got) + } +} + func TestStartXAIVersionUpdaterLifecycle(t *testing.T) { - ResetXAIVersionUpdaterOnceForTest(t) - restoreVersion := SetXAIClientVersionForTest(t, "1.0.50") + restoreVersion := SetXAIClientVersionForTest(DefaultXAIFallbackClientVersion) defer restoreVersion() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -120,24 +236,78 @@ func TestStartXAIVersionUpdaterLifecycle(t *testing.T) { })) defer server.Close() - restoreURL := OverrideXAINPMRegistryURLForTest(t, server.URL) + restoreURL := OverrideXAINPMRegistryURLForTest(server.URL) defer restoreURL() + notified := make(chan struct{}, 1) + restoreHook := SetXAIVersionRefreshedHookForTest(notified) + defer restoreHook() ctx, cancel := context.WithCancel(context.Background()) defer cancel() - StartXAIVersionUpdater(ctx) - - // Wait briefly for the startup fetch to settle - deadline := time.Now().Add(2 * time.Second) - for time.Now().Before(deadline) { - if GetXAIClientVersion() == "1.0.60" { - break - } - time.Sleep(20 * time.Millisecond) - } + StartXAIVersionUpdater(ctx, "") + waitXAIVersionRefresh(t, notified) if got := GetXAIClientVersion(); got != "1.0.60" { t.Fatalf("GetXAIClientVersion() = %q, want 1.0.60 after StartXAIVersionUpdater", got) } } + +func TestStartXAIVersionUpdaterRestartsWithNewContext(t *testing.T) { + restoreVersion := SetXAIClientVersionForTest(DefaultXAIFallbackClientVersion) + defer restoreVersion() + + var published atomic.Value + published.Store("1.0.70") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + version, _ := published.Load().(string) + _, _ = w.Write([]byte(`{"version":` + strconv.Quote(version) + `}`)) + })) + defer server.Close() + + restoreURL := OverrideXAINPMRegistryURLForTest(server.URL) + defer restoreURL() + notified := make(chan struct{}, 2) + restoreHook := SetXAIVersionRefreshedHookForTest(notified) + defer restoreHook() + + ctx, cancel := context.WithCancel(context.Background()) + StartXAIVersionUpdater(ctx, "") + waitXAIVersionRefresh(t, notified) + if got := GetXAIClientVersion(); got != "1.0.70" { + cancel() + t.Fatalf("GetXAIClientVersion() = %q, want 1.0.70", got) + } + cancel() + + published.Store("1.0.71") + restartCtx, restartCancel := context.WithCancel(context.Background()) + defer restartCancel() + StartXAIVersionUpdater(restartCtx, "") + waitXAIVersionRefresh(t, notified) + if got := GetXAIClientVersion(); got != "1.0.71" { + t.Fatalf("GetXAIClientVersion() = %q, want 1.0.71 after restart", got) + } +} + +func waitXAIVersionRefresh(t *testing.T, notified <-chan struct{}) { + t.Helper() + select { + case <-notified: + case <-t.Context().Done(): + t.Fatal("timed out waiting for Grok CLI version refresh") + } +} + +func setXAIVersionProxyURLForTest(proxyURL string) func() { + xaiClientVersionMu.Lock() + old := xaiVersionProxyURL + xaiVersionProxyURL = proxyURL + xaiClientVersionMu.Unlock() + return func() { + xaiClientVersionMu.Lock() + xaiVersionProxyURL = old + xaiClientVersionMu.Unlock() + } +} diff --git a/internal/runtime/executor/xai_client_version_test.go b/internal/runtime/executor/xai_client_version_test.go index 6b153132a0..bc7f3eed08 100644 --- a/internal/runtime/executor/xai_client_version_test.go +++ b/internal/runtime/executor/xai_client_version_test.go @@ -77,7 +77,7 @@ func TestXAIChatProxyCustomHeadersOverridePinnedVersion(t *testing.T) { } func TestXAIChatProxyIdentityHeadersFollowDynamicVersion(t *testing.T) { - restore := helps.SetXAIClientVersionForTest(t, "1.2.34") + restore := helps.SetXAIClientVersionForTest("1.2.34") defer restore() auth := &cliproxyauth.Auth{ diff --git a/internal/runtime/executor/xai_executor.go b/internal/runtime/executor/xai_executor.go index 7f97b2b544..d0ad9b744b 100644 --- a/internal/runtime/executor/xai_executor.go +++ b/internal/runtime/executor/xai_executor.go @@ -85,8 +85,9 @@ func xaiClientVersion() string { } // StartXAIVersionUpdater starts the periodic Grok CLI version updater from npm. -func StartXAIVersionUpdater(ctx context.Context) { - helps.StartXAIVersionUpdater(ctx) +// proxyURL is the global outbound proxy; an empty value inherits the process environment. +func StartXAIVersionUpdater(ctx context.Context, proxyURL string) { + helps.StartXAIVersionUpdater(ctx, proxyURL) } // Identifier returns the provider identifier. diff --git a/sdk/cliproxy/service_lifecycle.go b/sdk/cliproxy/service_lifecycle.go index 631e56f2c6..250c75c155 100644 --- a/sdk/cliproxy/service_lifecycle.go +++ b/sdk/cliproxy/service_lifecycle.go @@ -395,8 +395,10 @@ func (s *Service) startModelCatalogUpdaters(ctx context.Context) { s.cfgMu.RLock() cfg := s.cfg s.cfgMu.RUnlock() + proxyURL := "" if cfg != nil { registry.StartModelCatalogUpdaters(ctx, cfg.Models, cfg.Home.Enabled) + proxyURL = cfg.ProxyURL } - executor.StartXAIVersionUpdater(ctx) + executor.StartXAIVersionUpdater(ctx, proxyURL) }