Skip to content

Commit 01b2363

Browse files
authored
fix(billing): price Pi sandboxes at their build resources (#9453)
- Pi sandboxes are built at 4 vCPU / 8 GiB (Daytona adds a 10 GiB disk) but were priced on the 2 vCPU / 4 GiB Function profile, so Pi runs were billed at about half their provider cost. - Pi sizing moves from the build-script package list into lib/execution/remote-sandbox/pi-resources.ts, which both provider builds and sandbox pricing now read, so the build and the price cannot drift apart.
1 parent 322abb2 commit 01b2363

6 files changed

Lines changed: 72 additions & 45 deletions

File tree

‎apps/sim/lib/billing/sandbox-pricing.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ describe('sandbox pricing', () => {
2828
expect(priceSandboxUsage(pricing, 3_600_000, 3_600_000).rawCost).toBeCloseTo(0.117, 8)
2929
})
3030

31+
it.each([
32+
['e2b', 0.3312],
33+
['daytona', 0.33228],
34+
] as const)(
35+
'prices one hour of the Pi sandbox on %s at its build resources',
36+
(provider, expected) => {
37+
const pricing = createSandboxPricing(provider, 'pi', 1)
38+
39+
expect(priceSandboxUsage(pricing, 3_600_000, 3_600_000).rawCost).toBeCloseTo(expected, 8)
40+
}
41+
)
42+
3143
it('applies the multiplier once and rounds the final cost to eight decimals', () => {
3244
const pricing = createSandboxPricing('e2b', 'code', 1.75)
3345

‎apps/sim/lib/billing/sandbox-pricing.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import {
44
FUNCTION_SANDBOX_CPU_COUNT,
55
FUNCTION_SANDBOX_MEMORY_GB,
66
} from '@/lib/execution/remote-sandbox/function-resources'
7+
import {
8+
PI_DAYTONA_DISK_GB,
9+
PI_SANDBOX_CPU_COUNT,
10+
PI_SANDBOX_MEMORY_GB,
11+
} from '@/lib/execution/remote-sandbox/pi-resources'
712
import type { SandboxKind, SandboxProviderId } from '@/lib/execution/remote-sandbox/types'
813

914
const E2B_CPU_USD_PER_VCPU_SECOND = 0.000014
@@ -53,6 +58,7 @@ const PRICING_BY_PROVIDER: Record<SandboxProviderId, ProviderPricing> = {
5358
// The Mothership template is built without explicit resources, so E2B runs it on its
5459
// default 2 vCPU and 1 GiB.
5560
mothership: { vcpu: 2, memoryGiB: 1, diskGiB: 0 },
61+
pi: { vcpu: PI_SANDBOX_CPU_COUNT, memoryGiB: PI_SANDBOX_MEMORY_GB, diskGiB: 0 },
5662
},
5763
rates: {
5864
cpuUsdPerVcpuSecond: E2B_CPU_USD_PER_VCPU_SECOND,
@@ -66,6 +72,13 @@ const PRICING_BY_PROVIDER: Record<SandboxProviderId, ProviderPricing> = {
6672
memoryGiB: FUNCTION_SANDBOX_MEMORY_GB,
6773
diskGiB: FUNCTION_DAYTONA_DISK_GB,
6874
},
75+
resourcesByKind: {
76+
pi: {
77+
vcpu: PI_SANDBOX_CPU_COUNT,
78+
memoryGiB: PI_SANDBOX_MEMORY_GB,
79+
diskGiB: PI_DAYTONA_DISK_GB,
80+
},
81+
},
6982
rates: {
7083
cpuUsdPerVcpuSecond: DAYTONA_CPU_USD_PER_VCPU_SECOND,
7184
memoryUsdPerGiBSecond: DAYTONA_MEMORY_USD_PER_GIB_SECOND,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* vCPU and RAM for the Pi sandbox, shared by both provider builds and by sandbox
3+
* pricing. The two providers had already drifted here. Daytona asked for 4 CPU / 8 GB
4+
* while the E2B template asked for nothing and inherited its base default of
5+
* 2 vCPU / 512 MB — a 16x memory gap between the provider Pi normally runs on and
6+
* the one it fails over to, which would surface as the agent being killed on E2B
7+
* for work that succeeded on Daytona.
8+
*
9+
* 512 MB is the real problem: the Pi CLI is a Node process holding an LLM
10+
* context, running beside a `git clone` of the user's repository, and Node has no
11+
* graceful behaviour at that ceiling — it is OOM-killed, which reaches the user
12+
* as an opaque agent failure rather than a diagnosable one.
13+
*
14+
* Both numbers are at E2B's Professional maximum (8 vCPU / 8192 MB per build,
15+
* 1 vCPU / 512 MB minimum). Sizing is per-second billed against allocated
16+
* resources rather than used ones, so this costs roughly 3x the previous E2B
17+
* default per sandbox-second. On the compute-bound phases that is close to
18+
* neutral — the work finishes proportionally sooner — but Babysit deliberately
19+
* idles between review polls, and idle seconds bill at the same rate. Lower
20+
* {@link PI_SANDBOX_CPU_COUNT} first if that idle time proves dominant: vCPU is
21+
* ~3x the hourly rate of a GB of RAM, and RAM is the dimension that prevents
22+
* hard failures.
23+
*/
24+
export const PI_SANDBOX_CPU_COUNT = 4
25+
26+
/** Kept in GB and MB because Daytona takes GB and E2B takes MB. */
27+
export const PI_SANDBOX_MEMORY_GB = 8
28+
export const PI_SANDBOX_MEMORY_MB = PI_SANDBOX_MEMORY_GB * 1024
29+
30+
/**
31+
* Daytona disk for the Pi sandbox. 10 GB is a hard per-sandbox cap there: the API rejects anything
32+
* larger regardless of plan tier, and raising it requires contacting Daytona. E2B allows 20 GB, so
33+
* this bounds how large a repository Pi can clone on the failover provider, and it is the one
34+
* dimension where the two images legitimately differ.
35+
*/
36+
export const PI_DAYTONA_DISK_GB = 10

‎apps/sim/scripts/build-pi-daytona-snapshot.ts‎

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,27 @@
2626

2727
import { Daytona, Image } from '@daytona/sdk'
2828
import { getErrorMessage } from '@sim/utils/errors'
29+
import {
30+
PI_DAYTONA_DISK_GB,
31+
PI_SANDBOX_CPU_COUNT,
32+
PI_SANDBOX_MEMORY_GB,
33+
} from '@/lib/execution/remote-sandbox/pi-resources'
2934
import {
3035
PI_APT,
3136
PI_BUN_VERSION_ASSERT,
3237
PI_GLOBAL_NPM_PACKAGES,
3338
PI_NODE_MAJOR,
3439
PI_NODE_VERSION_ASSERT,
35-
PI_SANDBOX_CPU_COUNT,
36-
PI_SANDBOX_MEMORY_GB,
3740
} from '@/scripts/pi-sandbox-packages'
3841

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

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

5952
const APT_PREFIX = 'DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends'

‎apps/sim/scripts/build-pi-e2b-template.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@
1717
*/
1818

1919
import { defaultBuildLogger, Template, waitForTimeout } from '@e2b/code-interpreter'
20+
import {
21+
PI_SANDBOX_CPU_COUNT,
22+
PI_SANDBOX_MEMORY_MB,
23+
} from '@/lib/execution/remote-sandbox/pi-resources'
2024
import {
2125
PI_APT,
2226
PI_BUN_VERSION_ASSERT,
2327
PI_GLOBAL_NPM_PACKAGES,
2428
PI_NODE_MAJOR,
2529
PI_NODE_VERSION_ASSERT,
26-
PI_SANDBOX_CPU_COUNT,
27-
PI_SANDBOX_MEMORY_MB,
2830
} from '@/scripts/pi-sandbox-packages'
2931

3032
const DEFAULT_TEMPLATE_NAME = 'sim-pi'

‎apps/sim/scripts/pi-sandbox-packages.ts‎

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -60,32 +60,3 @@ export const PI_NODE_VERSION_ASSERT =
6060

6161
/** Fails the build loudly if the sandbox does not contain the repository's Bun version. */
6262
export const PI_BUN_VERSION_ASSERT = `test "$(bun --version)" = "${PI_BUN_VERSION}"`
63-
64-
/**
65-
* vCPU and RAM for the Pi sandbox, shared for the same reason the package lists
66-
* are: the two providers had already drifted here. Daytona asked for 4 CPU / 8 GB
67-
* while the E2B template asked for nothing and inherited its base default of
68-
* 2 vCPU / 512 MB — a 16x memory gap between the provider Pi normally runs on and
69-
* the one it fails over to, which would surface as the agent being killed on E2B
70-
* for work that succeeded on Daytona.
71-
*
72-
* 512 MB is the real problem: the Pi CLI is a Node process holding an LLM
73-
* context, running beside a `git clone` of the user's repository, and Node has no
74-
* graceful behaviour at that ceiling — it is OOM-killed, which reaches the user
75-
* as an opaque agent failure rather than a diagnosable one.
76-
*
77-
* Both numbers are at E2B's Professional maximum (8 vCPU / 8192 MB per build,
78-
* 1 vCPU / 512 MB minimum). Sizing is per-second billed against allocated
79-
* resources rather than used ones, so this costs roughly 3x the previous E2B
80-
* default per sandbox-second. On the compute-bound phases that is close to
81-
* neutral — the work finishes proportionally sooner — but Babysit deliberately
82-
* idles between review polls, and idle seconds bill at the same rate. Lower
83-
* {@link PI_SANDBOX_CPU_COUNT} first if that idle time proves dominant: vCPU is
84-
* ~3x the hourly rate of a GB of RAM, and RAM is the dimension that prevents
85-
* hard failures.
86-
*/
87-
export const PI_SANDBOX_CPU_COUNT = 4
88-
89-
/** Kept in GB and MB because Daytona takes GB and E2B takes MB. */
90-
export const PI_SANDBOX_MEMORY_GB = 8
91-
export const PI_SANDBOX_MEMORY_MB = PI_SANDBOX_MEMORY_GB * 1024

0 commit comments

Comments
 (0)