From cf6400ec5e5ecf1da20b92488f3926e57ee7db70 Mon Sep 17 00:00:00 2001 From: yummybomb <19238148+yummybomb@users.noreply.github.com> Date: Tue, 6 Oct 2026 19:34:55 +0000 Subject: [PATCH] Accept --memory 12GiB for the large GPU browser --- README.md | 2 ++ cmd/browser_pools.go | 4 ++-- cmd/browser_pools_test.go | 22 ++++++++++++++++++++++ cmd/browsers.go | 29 ++++++++++++++++++----------- cmd/browsers_test.go | 10 ++++++++-- 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index eb863c84..f8267c1a 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,8 @@ kernel search contents srch_01jsearchresult --limit 3 --content-source browser - `-s, --stealth` - Launch browser in stealth mode to avoid detection - `-H, --headless` - Launch browser without GUI access - `--kiosk` - Launch browser in kiosk mode + - `--gpu` - Launch browser with hardware-accelerated GPU rendering + - `--memory 8GiB|12GiB|16GiB` - Memory for a headful browser: `8GiB` (default) or `16GiB` for CPU browsers, or `12GiB` with `--gpu` for the large GPU browser. Standard GPU browsers get 6GiB. - `--region us-east|eu-west|ap-southeast` - Geographic region for the session. Fixed once the session is created; requires a Start-Up or Enterprise plan and defaults to `us-east`. - `--private-host ` - Destination the browser reaches directly through the session's own network instead of Kernel-managed egress, for private hosts on a VPN or tunnel the session joins (repeatable or comma-separated, max 32). Accepts hostname patterns (`*.example.ts.net`), IPs (`10.1.30.63`, `[fd00::1]`), and private CIDRs (`100.64.0.0/10`). Replaces the default private ranges (RFC1918, `100.64.0.0/10`, `fc00::/7`); omit to keep them. Fixed once the session is created. Unrelated to a proxy's `--bypass-host`, which only chooses between upstream proxy and Kernel-managed direct egress. - `--proxy-route '[,...]='` - Route matching browser requests through a selected proxy (repeatable, max 10 routes with 1–50 hosts each). Example: `--proxy-route 'api.ipify.org,*.ipify.org=name:my-dc-proxy'`. The proxy is an ID by default; use `id:` or `name:` explicitly. Exact hostnames beat wildcards; longer wildcard suffixes beat shorter ones. `*.example.com` matches subdomains, not `example.com`. Matching ignores case and ports. Unmatched hosts use `--proxy-*` or default egress, while `--start-url` uses the top-level proxy during setup. Routes are create-only and are not available on pool sessions. diff --git a/cmd/browser_pools.go b/cmd/browser_pools.go index 18ffba7d..360712da 100644 --- a/cmd/browser_pools.go +++ b/cmd/browser_pools.go @@ -197,7 +197,7 @@ func (c BrowserPoolsCmd) Create(ctx context.Context, in BrowserPoolsCreateInput) if in.Kiosk.Set { params.KioskMode = kernel.Bool(in.Kiosk.Value) } - memory, err := parseMemoryFlag(in.Memory) + memory, err := parseMemoryFlag(in.Memory, poolMemorySizes()) if err != nil { return err } @@ -432,7 +432,7 @@ func (c BrowserPoolsCmd) Update(ctx context.Context, in BrowserPoolsUpdateInput) if in.Kiosk.Set { params.KioskMode = kernel.Bool(in.Kiosk.Value) } - memory, err := parseMemoryFlag(in.Memory) + memory, err := parseMemoryFlag(in.Memory, poolMemorySizes()) if err != nil { return err } diff --git a/cmd/browser_pools_test.go b/cmd/browser_pools_test.go index ea3b65bd..c8fcf985 100644 --- a/cmd/browser_pools_test.go +++ b/cmd/browser_pools_test.go @@ -765,3 +765,25 @@ func TestBrowserPoolsAcquire_WithTelemetryOverride(t *testing.T) { assert.NoError(t, err) assert.True(t, captured.Telemetry.Browser.Page.Enabled.Value) } + +func TestBrowserPoolsMemoryRejectsGPUTier(t *testing.T) { + setupStdoutCapture(t) + + fake := &FakeBrowserPoolsService{ + NewFunc: func(ctx context.Context, body kernel.BrowserPoolNewParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + t.Fatal("create must not reach the API with a GPU-only memory size") + return nil, nil + }, + UpdateFunc: func(ctx context.Context, id string, body kernel.BrowserPoolUpdateParams, opts ...option.RequestOption) (*kernel.BrowserPool, error) { + t.Fatal("update must not reach the API with a GPU-only memory size") + return nil, nil + }, + } + c := BrowserPoolsCmd{client: fake} + + err := c.Create(context.Background(), BrowserPoolsCreateInput{Name: "pool", Size: 1, Memory: "12GiB"}) + require.EqualError(t, err, "invalid --memory value: 12GiB (must be one of 8GiB, 16GiB)") + + err = c.Update(context.Background(), BrowserPoolsUpdateInput{IDOrName: "pool", Memory: "12GiB"}) + require.EqualError(t, err, "invalid --memory value: 12GiB (must be one of 8GiB, 16GiB)") +} diff --git a/cmd/browsers.go b/cmd/browsers.go index 1e79b836..3a50ba6d 100644 --- a/cmd/browsers.go +++ b/cmd/browsers.go @@ -184,25 +184,32 @@ func parseRegionFlag(region string) (string, error) { return "", fmt.Errorf("invalid --region value: %s (must be one of %s)", region, strings.Join(availableRegions(), ", ")) } -// availableMemorySizes returns the memory sizes the API accepts when creating a -// browser session. Only headful, non-GPU sessions can request memory; every -// other configuration gets a fixed allocation. -func availableMemorySizes() []string { +// browserMemorySizes returns the memory sizes the API accepts when creating a +// browser session. Headful CPU browsers take 8GiB or 16GiB and headful GPU +// browsers accept 12GiB (the large tier); every other configuration gets a +// fixed allocation. +func browserMemorySizes() []string { + return []string{"8GiB", "12GiB", "16GiB"} +} + +// poolMemorySizes returns the memory sizes the API accepts for browser pools. +// Pools never run GPU browsers, so the 12GiB GPU tier is not offered. +func poolMemorySizes() []string { return []string{"8GiB", "16GiB"} } -// parseMemoryFlag validates a --memory value. An empty value means the flag was -// not set, which lets the API apply its default (8GiB). -func parseMemoryFlag(memory string) (kernel.BrowserMemoryRequest, error) { +// parseMemoryFlag validates a --memory value against sizes. An empty value +// means the flag was not set, which lets the API apply its per-type default. +func parseMemoryFlag(memory string, sizes []string) (kernel.BrowserMemoryRequest, error) { if memory == "" { return "", nil } - for _, m := range availableMemorySizes() { + for _, m := range sizes { if strings.EqualFold(memory, m) { return kernel.BrowserMemoryRequest(m), nil } } - return "", fmt.Errorf("invalid --memory value: %s (must be one of %s)", memory, strings.Join(availableMemorySizes(), ", ")) + return "", fmt.Errorf("invalid --memory value: %s (must be one of %s)", memory, strings.Join(sizes, ", ")) } // maxPrivateHosts mirrors the API's cap on network.private_hosts entries. @@ -673,7 +680,7 @@ func (b BrowsersCmd) Create(ctx context.Context, in BrowsersCreateInput) error { if in.GPU.Set { params.GPU = kernel.Opt(in.GPU.Value) } - memory, err := parseMemoryFlag(in.Memory) + memory, err := parseMemoryFlag(in.Memory, browserMemorySizes()) if err != nil { return err } @@ -3241,7 +3248,7 @@ unrestricted code execution inside the browser VM and is not sandboxed.`, browsersCreateCmd.Flags().BoolP("stealth", "s", false, "Launch browser in stealth mode to avoid detection") browsersCreateCmd.Flags().BoolP("headless", "H", false, "Launch browser without GUI access") browsersCreateCmd.Flags().Bool("gpu", false, "Launch browser with hardware-accelerated GPU rendering") - browsersCreateCmd.Flags().String("memory", "", "Memory for a headful, non-GPU browser session: '8GiB' (default) or '16GiB'") + browsersCreateCmd.Flags().String("memory", "", "Memory for a headful browser session: '8GiB' (default) or '16GiB' for CPU browsers, '12GiB' for the large GPU browser (with --gpu)") browsersCreateCmd.Flags().String("invocation-id", "", "Associate the browser session with an invocation") browsersCreateCmd.Flags().Bool("kiosk", false, "Launch browser in kiosk mode") browsersCreateCmd.Flags().IntP("timeout", "t", 60, "Timeout in seconds for the browser session") diff --git a/cmd/browsers_test.go b/cmd/browsers_test.go index 801fe04c..c37bf69b 100644 --- a/cmd/browsers_test.go +++ b/cmd/browsers_test.go @@ -783,7 +783,13 @@ func TestBrowsersCreate_WithMemory(t *testing.T) { require.NoError(t, err) assert.Equal(t, kernel.BrowserMemoryRequest8GiB, captured.Memory) - // Omitting the flag sends nothing; the server defaults to 8GiB. + // The large GPU tier is requested through the same flag. + err = b.Create(context.Background(), BrowsersCreateInput{Memory: "12GiB", GPU: BoolFlag{Set: true, Value: true}}) + require.NoError(t, err) + assert.Equal(t, kernel.BrowserMemoryRequest("12GiB"), captured.Memory) + assert.True(t, captured.GPU.Value) + + // Omitting the flag sends nothing; the server applies its per-type default. err = b.Create(context.Background(), BrowsersCreateInput{}) require.NoError(t, err) assert.Empty(t, captured.Memory) @@ -793,7 +799,7 @@ func TestBrowsersCreate_WithMemory(t *testing.T) { assert.NotContains(t, string(raw), "memory") // An unsupported size is rejected before the request is made. - assert.Error(t, b.Create(context.Background(), BrowsersCreateInput{Memory: "4GiB"})) + assert.EqualError(t, b.Create(context.Background(), BrowsersCreateInput{Memory: "4GiB"}), "invalid --memory value: 4GiB (must be one of 8GiB, 12GiB, 16GiB)") } func TestBrowsersCreate_WithChromePolicy(t *testing.T) {