Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions docs/VERCEL_SANDBOX_PROVIDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,19 @@ keep the current snapshot, and delete old snapshots manually if you need to recl

## CPU and Memory

Open-Inspect does not currently send a Vercel `resources` setting when creating sandboxes. Vercel
therefore applies its default sandbox size.
Open-Inspect maps sandbox resource settings to Vercel's `resources.vcpus` setting when creating or
restoring sandboxes. `cpuCores` is treated as the requested vCPU count. `memoryMib` is treated as a
minimum memory request and converted using Vercel's documented `2 GB` per vCPU ratio. If both are
set, Open-Inspect uses enough vCPUs to satisfy both requests.

Vercel currently documents the unspecified default as `2 vCPU / 4 GB RAM`. Its pricing limits state
that each vCPU includes `2 GB` of memory, with a maximum of `8` vCPUs and `16 GB` memory per
sandbox. The `resources.vcpus` option can be used with `1`, `2`, `4`, or `8` vCPUs.

If Open-Inspect needs to control this later, add a provider config value such as
`VERCEL_SANDBOX_VCPUS`, thread it into the Vercel create-sandbox request as `resources.vcpus`, and
let Vercel derive memory from that vCPU count.
When a request falls between supported Vercel sizes, Open-Inspect rounds up to the next supported
vCPU size. Requests above Vercel's maximum supported size fail locally with a clear provider error.
If resource fields are unset or explicitly `null`, no `resources` setting is sent and Vercel applies
its provider default.

References:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ describe("VercelSandboxClient", () => {
name: "sandbox-1",
runtime: "node24",
timeoutMs: 7200000,
resources: { vcpus: 4 },
ports: [8080],
env: { FOO: "bar" },
tags: { openinspect_framework: "open-inspect" },
Expand Down Expand Up @@ -120,6 +121,7 @@ describe("VercelSandboxClient", () => {
name: "sandbox-1",
runtime: "node24",
timeout: 7200000,
resources: { vcpus: 4 },
ports: [8080],
env: { FOO: "bar" },
tags: { openinspect_framework: "open-inspect" },
Expand Down
4 changes: 4 additions & 0 deletions packages/control-plane/src/sandbox/providers/vercel/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface VercelSandboxSession {
timeout: number;
}

export type VercelVcpus = 1 | 2 | 4 | 8;

export interface VercelSandboxMetadata {
name: string;
currentSessionId: string;
Expand All @@ -47,6 +49,7 @@ export interface VercelCreateSandboxRequest {
name: string;
runtime?: string;
timeoutMs?: number;
resources?: { vcpus: VercelVcpus };
ports?: number[];
env?: Record<string, string>;
tags?: Record<string, string>;
Expand Down Expand Up @@ -142,6 +145,7 @@ export class VercelSandboxClient {
name: request.name,
runtime: request.runtime,
timeout: request.timeoutMs,
resources: request.resources,
ports: request.ports ?? [],
env: request.env,
tags: request.tags,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,58 @@ describe("VercelSandboxProvider", () => {
);
});

it("maps sandbox CPU and memory settings to Vercel vCPU resources", async () => {
const client = createMockClient();
const provider = new VercelSandboxProvider(client, providerConfig);

await provider.createSandbox({
...baseCreateConfig,
sandboxSettings: { cpuCores: 2, memoryMib: 6144 },
});

expect(vi.mocked(client.createSandbox).mock.calls[0][0].resources).toEqual({ vcpus: 4 });
});

it("omits Vercel resources when sandbox CPU and memory settings use provider defaults", async () => {
const client = createMockClient();
const provider = new VercelSandboxProvider(client, providerConfig);

await provider.createSandbox({
...baseCreateConfig,
sandboxSettings: { cpuCores: null, memoryMib: null },
});

expect(vi.mocked(client.createSandbox).mock.calls[0][0].resources).toBeUndefined();
});

it("maps restore sandbox memory settings to Vercel vCPU resources", async () => {
const client = createMockClient();
const provider = new VercelSandboxProvider(client, providerConfig);

await provider.restoreFromSnapshot({
...baseRestoreConfig,
sandboxSettings: { memoryMib: 4096 },
});

expect(vi.mocked(client.createSandbox).mock.calls[0][0].resources).toEqual({ vcpus: 2 });
});

it("rejects Vercel resource requests above the maximum supported vCPU size", async () => {
const client = createMockClient();
const provider = new VercelSandboxProvider(client, providerConfig);

await expect(
provider.createSandbox({
...baseCreateConfig,
sandboxSettings: { memoryMib: 18432 },
})
).rejects.toMatchObject({
message: expect.stringContaining("support up to 8 vCPUs; requested 9"),
});

expect(vi.mocked(client.createSandbox)).not.toHaveBeenCalled();
});

it("resolves a configured base snapshot name before creating a fresh sandbox", async () => {
const client = createMockClient();
const provider = new VercelSandboxProvider(client, {
Expand Down
28 changes: 28 additions & 0 deletions packages/control-plane/src/sandbox/providers/vercel/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type {
VercelCreateSandboxResponse,
VercelSandboxClient,
VercelSandboxRoute,
VercelVcpus,
} from "./client";
import { VercelSandboxApiError } from "./client";
import { DEFAULT_VERCEL_RUNTIME, VERCEL_PYTHON_BIN } from "./bootstrap";
Expand All @@ -39,6 +40,9 @@ const EXPECTED_TUNNEL_PORTS_ENV_VAR = "EXPECTED_TUNNEL_PORTS";
const DEFAULT_SNAPSHOT_EXPIRATION_MS = 0;
const BUILD_TIMEOUT_SECONDS = 1800;
const VERCEL_MAX_SANDBOX_TIMEOUT_MS = 45 * 60 * 1000;
const VERCEL_MEMORY_MIB_PER_VCPU = 2048;
const VERCEL_SUPPORTED_VCPUS: readonly VercelVcpus[] = [1, 2, 4, 8];
const VERCEL_MAX_VCPUS = VERCEL_SUPPORTED_VCPUS[VERCEL_SUPPORTED_VCPUS.length - 1];
const VERCEL_TUNNEL_ENV_WRITE_TIMEOUT_MS = 30_000;
const REPO_IMAGE_CALLBACK_ENV_KEYS = [
"OI_REPO_IMAGE_PROVIDER_SESSION_ID",
Expand Down Expand Up @@ -126,6 +130,7 @@ export class VercelSandboxProvider implements SandboxProvider {
name: config.sandboxId,
runtime: this.providerConfig.runtime || DEFAULT_VERCEL_RUNTIME,
timeoutMs: resolveVercelTimeoutMs(config.timeoutSeconds),
resources: resolveVercelResources(config.sandboxSettings),
ports,
env,
tags: this.buildTags(config),
Expand Down Expand Up @@ -172,6 +177,7 @@ export class VercelSandboxProvider implements SandboxProvider {
name: config.sandboxId,
runtime: this.providerConfig.runtime || DEFAULT_VERCEL_RUNTIME,
timeoutMs: resolveVercelTimeoutMs(config.timeoutSeconds),
resources: resolveVercelResources(config.sandboxSettings),
ports,
env,
tags: this.buildTags(config),
Expand Down Expand Up @@ -618,6 +624,28 @@ function resolveTunnelPorts(rawPorts: number[] | undefined): number[] {
return ports;
}

function resolveVercelResources(
sandboxSettings: SandboxSettings | undefined
): { vcpus: VercelVcpus } | undefined {
const requestedCpuCores = sandboxSettings?.cpuCores ?? undefined;
const requestedMemoryMib = sandboxSettings?.memoryMib ?? undefined;
const vcpusForMemory =
requestedMemoryMib === undefined
? undefined
: Math.ceil(requestedMemoryMib / VERCEL_MEMORY_MIB_PER_VCPU);
const requestedVcpus = Math.max(requestedCpuCores ?? 0, vcpusForMemory ?? 0);

if (requestedVcpus <= 0) return undefined;
Comment thread
ColeMurray marked this conversation as resolved.
const supportedVcpus = VERCEL_SUPPORTED_VCPUS.find((vcpus) => vcpus >= requestedVcpus);
if (supportedVcpus === undefined) {
throw new SandboxProviderError(
`Vercel sandbox resources support up to ${VERCEL_MAX_VCPUS} vCPUs; requested ${requestedVcpus}`,
"permanent"
);
}
return { vcpus: supportedVcpus };
}

function routeToUrl(route: VercelSandboxRoute | undefined): string | undefined {
if (!route) return undefined;
if (route.url) return route.url.startsWith("http") ? route.url : `https://${route.url}`;
Expand Down
20 changes: 11 additions & 9 deletions packages/shared/src/types/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ export const DEFAULT_MAX_TOTAL_CHILD_SESSIONS = 15;
/**
* Sandbox environment settings. Provider-agnostic: describes what the user
* wants, not how it's done. Resource fields (`cpuCores`, `memoryMib`) are
* advisory and provider-dependent — Modal honors them; providers without
* resource reservations (e.g. Daytona) ignore them. We only check they're
* positive; the provider enforces its own real limits. When unset, the
* provider's own default applies. At repo scope, `null` explicitly uses the
* provider default instead of inheriting a global resource default.
* advisory and provider-dependent — Modal maps them directly, Vercel maps
* them to vCPUs, and providers without resource reservations ignore them. We
* only check they're positive; the provider enforces its own real limits. When
* unset, the provider's own default applies. At repo scope, `null` explicitly
* uses the provider default instead of inheriting a global resource default.
*/
export interface SandboxSettings {
/** Extra ports to expose via tunnels (e.g., dev server ports 3000, 5173). */
Expand All @@ -67,13 +67,15 @@ export interface SandboxSettings {
/** Maximum total agent-spawned child sessions per parent session. */
maxTotalChildSessions?: number;
/**
* CPU cores to reserve for the sandbox (maps to Modal's `cpu`). Fractional
* values are allowed. Unset → inherit/default; null → provider default.
* CPU cores to reserve for the sandbox. Fractional values are allowed, but
* providers may round to their supported resource shapes. Unset →
* inherit/default; null → provider default.
*/
cpuCores?: number | null;
/**
* Memory to reserve for the sandbox, in MiB (maps to Modal's `memory`).
* Unset → inherit/default; null → provider default.
* Memory to reserve for the sandbox, in MiB. Providers may map this to their
* closest supported resource shape. Unset → inherit/default; null → provider
* default.
*/
memoryMib?: number | null;
}
Expand Down
Loading