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
12 changes: 12 additions & 0 deletions apps/sim/lib/billing/sandbox-pricing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ describe('sandbox pricing', () => {
expect(priceSandboxUsage(pricing, 3_600_000, 3_600_000).rawCost).toBeCloseTo(0.117, 8)
})

it.each([
['e2b', 0.3312],
['daytona', 0.33228],
] as const)(
'prices one hour of the Pi sandbox on %s at its build resources',
(provider, expected) => {
const pricing = createSandboxPricing(provider, 'pi', 1)

expect(priceSandboxUsage(pricing, 3_600_000, 3_600_000).rawCost).toBeCloseTo(expected, 8)
}
)

it('applies the multiplier once and rounds the final cost to eight decimals', () => {
const pricing = createSandboxPricing('e2b', 'code', 1.75)

Expand Down
13 changes: 13 additions & 0 deletions apps/sim/lib/billing/sandbox-pricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import {
FUNCTION_SANDBOX_CPU_COUNT,
FUNCTION_SANDBOX_MEMORY_GB,
} from '@/lib/execution/remote-sandbox/function-resources'
import {
PI_DAYTONA_DISK_GB,
PI_SANDBOX_CPU_COUNT,
PI_SANDBOX_MEMORY_GB,
} from '@/lib/execution/remote-sandbox/pi-resources'
import type { SandboxKind, SandboxProviderId } from '@/lib/execution/remote-sandbox/types'

const E2B_CPU_USD_PER_VCPU_SECOND = 0.000014
Expand Down Expand Up @@ -53,6 +58,7 @@ const PRICING_BY_PROVIDER: Record<SandboxProviderId, ProviderPricing> = {
// The Mothership template is built without explicit resources, so E2B runs it on its
// default 2 vCPU and 1 GiB.
mothership: { vcpu: 2, memoryGiB: 1, diskGiB: 0 },
pi: { vcpu: PI_SANDBOX_CPU_COUNT, memoryGiB: PI_SANDBOX_MEMORY_GB, diskGiB: 0 },
},
rates: {
cpuUsdPerVcpuSecond: E2B_CPU_USD_PER_VCPU_SECOND,
Expand All @@ -66,6 +72,13 @@ const PRICING_BY_PROVIDER: Record<SandboxProviderId, ProviderPricing> = {
memoryGiB: FUNCTION_SANDBOX_MEMORY_GB,
diskGiB: FUNCTION_DAYTONA_DISK_GB,
},
resourcesByKind: {
pi: {
vcpu: PI_SANDBOX_CPU_COUNT,
memoryGiB: PI_SANDBOX_MEMORY_GB,
diskGiB: PI_DAYTONA_DISK_GB,
},
},
rates: {
cpuUsdPerVcpuSecond: DAYTONA_CPU_USD_PER_VCPU_SECOND,
memoryUsdPerGiBSecond: DAYTONA_MEMORY_USD_PER_GIB_SECOND,
Expand Down
36 changes: 36 additions & 0 deletions apps/sim/lib/execution/remote-sandbox/pi-resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* vCPU and RAM for the Pi sandbox, shared by both provider builds and by sandbox
* pricing. The two providers had already drifted here. Daytona asked for 4 CPU / 8 GB
* while the E2B template asked for nothing and inherited its base default of
* 2 vCPU / 512 MB — a 16x memory gap between the provider Pi normally runs on and
* the one it fails over to, which would surface as the agent being killed on E2B
* for work that succeeded on Daytona.
*
* 512 MB is the real problem: the Pi CLI is a Node process holding an LLM
* context, running beside a `git clone` of the user's repository, and Node has no
* graceful behaviour at that ceiling — it is OOM-killed, which reaches the user
* as an opaque agent failure rather than a diagnosable one.
*
* Both numbers are at E2B's Professional maximum (8 vCPU / 8192 MB per build,
* 1 vCPU / 512 MB minimum). Sizing is per-second billed against allocated
Comment thread
waleedlatif1 marked this conversation as resolved.
* resources rather than used ones, so this costs roughly 3x the previous E2B
* default per sandbox-second. On the compute-bound phases that is close to
* neutral — the work finishes proportionally sooner — but Babysit deliberately
* idles between review polls, and idle seconds bill at the same rate. Lower
* {@link PI_SANDBOX_CPU_COUNT} first if that idle time proves dominant: vCPU is
* ~3x the hourly rate of a GB of RAM, and RAM is the dimension that prevents
* hard failures.
*/
export const PI_SANDBOX_CPU_COUNT = 4

/** Kept in GB and MB because Daytona takes GB and E2B takes MB. */
export const PI_SANDBOX_MEMORY_GB = 8
export const PI_SANDBOX_MEMORY_MB = PI_SANDBOX_MEMORY_GB * 1024

/**
* Daytona disk for the Pi sandbox. 10 GB is a hard per-sandbox cap there: the API rejects anything
* larger regardless of plan tier, and raising it requires contacting Daytona. E2B allows 20 GB, so
* this bounds how large a repository Pi can clone on the failover provider, and it is the one
* dimension where the two images legitimately differ.
*/
export const PI_DAYTONA_DISK_GB = 10
21 changes: 7 additions & 14 deletions apps/sim/scripts/build-pi-daytona-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,27 @@

import { Daytona, Image } from '@daytona/sdk'
import { getErrorMessage } from '@sim/utils/errors'
import {
PI_DAYTONA_DISK_GB,
PI_SANDBOX_CPU_COUNT,
PI_SANDBOX_MEMORY_GB,
} from '@/lib/execution/remote-sandbox/pi-resources'
import {
PI_APT,
PI_BUN_VERSION_ASSERT,
PI_GLOBAL_NPM_PACKAGES,
PI_NODE_MAJOR,
PI_NODE_VERSION_ASSERT,
PI_SANDBOX_CPU_COUNT,
PI_SANDBOX_MEMORY_GB,
} from '@/scripts/pi-sandbox-packages'

/** Matches E2B's base: Debian 13 (trixie) with Python 3.13 installed to /usr/local. */
const BASE_IMAGE = 'python:3.13-slim-trixie'

/**
* CPU and memory come from the shared module so the two providers cannot drift
* apart on sizing the way they already had — see {@link PI_SANDBOX_CPU_COUNT}.
*
* Disk stays local because it is not shareable: 10 GB is a HARD per-sandbox cap
* here — the API rejects anything larger ("Disk request 20GB exceeds maximum
* allowed per sandbox (10GB)"), regardless of plan tier, and raising it requires
* contacting Daytona. E2B allows 20 GB, so this is the binding constraint on how
* large a repo Pi can clone on the failover provider, and the one dimension where
* the two images legitimately differ.
*/
/** Sizing comes from the shared module so the two providers cannot drift apart. */
const RESOURCES = {
cpu: PI_SANDBOX_CPU_COUNT,
memory: PI_SANDBOX_MEMORY_GB,
disk: 10,
disk: PI_DAYTONA_DISK_GB,
} as const

const APT_PREFIX = 'DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends'
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/scripts/build-pi-e2b-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
*/

import { defaultBuildLogger, Template, waitForTimeout } from '@e2b/code-interpreter'
import {
PI_SANDBOX_CPU_COUNT,
PI_SANDBOX_MEMORY_MB,
} from '@/lib/execution/remote-sandbox/pi-resources'
import {
PI_APT,
PI_BUN_VERSION_ASSERT,
PI_GLOBAL_NPM_PACKAGES,
PI_NODE_MAJOR,
PI_NODE_VERSION_ASSERT,
PI_SANDBOX_CPU_COUNT,
PI_SANDBOX_MEMORY_MB,
} from '@/scripts/pi-sandbox-packages'

const DEFAULT_TEMPLATE_NAME = 'sim-pi'
Expand Down
29 changes: 0 additions & 29 deletions apps/sim/scripts/pi-sandbox-packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,32 +60,3 @@ export const PI_NODE_VERSION_ASSERT =

/** Fails the build loudly if the sandbox does not contain the repository's Bun version. */
export const PI_BUN_VERSION_ASSERT = `test "$(bun --version)" = "${PI_BUN_VERSION}"`

/**
* vCPU and RAM for the Pi sandbox, shared for the same reason the package lists
* are: the two providers had already drifted here. Daytona asked for 4 CPU / 8 GB
* while the E2B template asked for nothing and inherited its base default of
* 2 vCPU / 512 MB — a 16x memory gap between the provider Pi normally runs on and
* the one it fails over to, which would surface as the agent being killed on E2B
* for work that succeeded on Daytona.
*
* 512 MB is the real problem: the Pi CLI is a Node process holding an LLM
* context, running beside a `git clone` of the user's repository, and Node has no
* graceful behaviour at that ceiling — it is OOM-killed, which reaches the user
* as an opaque agent failure rather than a diagnosable one.
*
* Both numbers are at E2B's Professional maximum (8 vCPU / 8192 MB per build,
* 1 vCPU / 512 MB minimum). Sizing is per-second billed against allocated
* resources rather than used ones, so this costs roughly 3x the previous E2B
* default per sandbox-second. On the compute-bound phases that is close to
* neutral — the work finishes proportionally sooner — but Babysit deliberately
* idles between review polls, and idle seconds bill at the same rate. Lower
* {@link PI_SANDBOX_CPU_COUNT} first if that idle time proves dominant: vCPU is
* ~3x the hourly rate of a GB of RAM, and RAM is the dimension that prevents
* hard failures.
*/
export const PI_SANDBOX_CPU_COUNT = 4

/** Kept in GB and MB because Daytona takes GB and E2B takes MB. */
export const PI_SANDBOX_MEMORY_GB = 8
export const PI_SANDBOX_MEMORY_MB = PI_SANDBOX_MEMORY_GB * 1024
Loading