@sandboxrouter/sdk 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -42
- package/dist/catalog.d.ts +75 -5
- package/dist/catalog.js +126 -4
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/providers/boat.d.ts +25 -0
- package/dist/providers/boat.js +258 -0
- package/dist/providers/cloudflare.d.ts +30 -0
- package/dist/providers/cloudflare.js +147 -0
- package/dist/providers/docker.d.ts +17 -0
- package/dist/providers/docker.js +148 -0
- package/dist/providers/exedev.d.ts +23 -0
- package/dist/providers/exedev.js +278 -0
- package/dist/providers/hopx.d.ts +15 -0
- package/dist/providers/hopx.js +168 -0
- package/dist/providers/northflank.d.ts +21 -0
- package/dist/providers/northflank.js +261 -0
- package/dist/router.js +25 -6
- package/dist/types.d.ts +6 -2
- package/package.json +29 -15
package/README.md
CHANGED
|
@@ -1,85 +1,112 @@
|
|
|
1
1
|
# sandboxrouter
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**One API for every sandbox.** A free, open-source router for the places an AI agent can run code: Blaxel, Boat, Cloudflare, Daytona, E2B, exe.dev, Fly.io Sprites, HOPX, Maritime, Modal, Morph, Northflank, Runloop, Tensorlake and Vercel, plus Docker on your own machine. It picks a provider by price, speed or capability, and falls back to the next one when a provider is down or rate-limited.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
import { Router } from "@sandboxrouter/sdk";
|
|
7
7
|
|
|
8
|
-
const router = new Router(); //
|
|
8
|
+
const router = new Router(); // uses whichever provider keys you've set
|
|
9
9
|
await using sbx = await router.create({ vcpu: 2, memoryGb: 4 });
|
|
10
10
|
|
|
11
11
|
const r = await sbx.exec("python3 -c 'print(6*7)'");
|
|
12
|
-
console.log(r.stdout, "ran on", sbx.provider);
|
|
12
|
+
console.log(r.stdout, "ran on", sbx.provider);
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Change providers without changing code. If one returns a 429, the next one takes the sandbox.
|
|
16
16
|
|
|
17
|
+
Site: [sandboxrouter.vercel.app](https://sandboxrouter.vercel.app) · npm: [`@sandboxrouter/sdk`](https://www.npmjs.com/package/@sandboxrouter/sdk)
|
|
18
|
+
|
|
17
19
|
## Why
|
|
18
20
|
|
|
19
|
-
Agents need a disposable computer: to run code a model wrote, install packages, run tests, scrape, or do an RL rollout.
|
|
21
|
+
Agents need a disposable computer: to run code a model wrote, install packages, run tests, scrape, or do an RL rollout. There are more than a dozen sandbox providers, each with its own SDK, auth, lifecycle and price, and the same 2 vCPU / 4 GB box costs anywhere from a few cents to over thirty cents an hour.
|
|
20
22
|
|
|
21
23
|
So every agent team either locks into one provider or writes its own adapter layer. sandboxrouter is that adapter layer, done once, in the open:
|
|
22
24
|
|
|
23
25
|
- **One interface.** `create`, `exec`, `writeFile`, `readFile`, `pause`, `kill`, the same on every provider. `exec` streams stdout and stderr as they happen.
|
|
24
|
-
- **Routing.** `
|
|
26
|
+
- **Routing.** `cheapest` (the default: lowest list price for your size) or `fastest` (lowest cold start you measured with `bench`), or name providers yourself.
|
|
25
27
|
- **Fallback.** A failed create moves to the next candidate, and every attempt is recorded on `sbx.attempts`.
|
|
26
28
|
- **Capability filters.** Ask for a GPU and only GPU-capable providers are tried. Ask for 32 vCPU and providers that can't do it are skipped, with the reason.
|
|
27
|
-
- **
|
|
29
|
+
- **Your keys.** Your accounts, your bills. Nothing is proxied through a third party.
|
|
28
30
|
- **HTTP server and Python client.** Run `sandboxrouter serve` and any language gets the same API.
|
|
29
|
-
- **Computers.**
|
|
30
|
-
- **Open
|
|
31
|
+
- **Computers.** Desktops and phones for computer-use models (Maritime today): actions, screenshots, and a takeover link for a person when the model hits a login.
|
|
32
|
+
- **Open benchmark.** `bench --share` produces a report; cold starts on the site come from reports submitted to [`results/`](results/).
|
|
33
|
+
|
|
34
|
+
## Fair by construction
|
|
35
|
+
|
|
36
|
+
No provider gets a better slot, and none pays for placement.
|
|
37
|
+
|
|
38
|
+
- With no strategy, a sandbox goes to the **lowest list price for the size you asked for**, among providers you have keys for. Ties break **alphabetically**.
|
|
39
|
+
- `fastest` uses only cold starts **you** measured with `bench`, then price.
|
|
40
|
+
- Prices are each provider's public rate, with a source link, in [`src/catalog.ts`](src/catalog.ts). There's a test that the catalog has no privileged ordering.
|
|
41
|
+
- Docker is never picked automatically, because it always "costs" $0 and isolates less than a cloud micro-VM. Name it to use it.
|
|
42
|
+
|
|
43
|
+
If a price is wrong, open a PR with the source link.
|
|
31
44
|
|
|
32
45
|
## Prices
|
|
33
46
|
|
|
34
|
-
Estimated list price while running, 2 vCPU / 4 GB (`npx @sandboxrouter/sdk providers --vcpu 2 --memory 4`):
|
|
47
|
+
Estimated list price while running, 2 vCPU / 4 GB (`npx @sandboxrouter/sdk providers --vcpu 2 --memory 4`), in the order the router would try them:
|
|
35
48
|
|
|
36
49
|
| Provider | $/hr | Notes | Source |
|
|
37
50
|
|---|---|---|---|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
| Boat | $0.018 | Fixed sizes billed per second ($0.018/h for 2 vCPU / 4 GB up to $0.20/h for 16 / 32); needs a plan from $20/mo, paid back as VM time | [pricing](https://docs.boat.dev/pricing.md) |
|
|
52
|
+
| Maritime | $0.022 | Per second, disk included | [pricing](https://maritime.sh/pricing) |
|
|
53
|
+
| Northflank | $0.067 | Per second on Northflank Cloud, ephemeral disk included; sandboxes run as deployment services (microVMs) | [pricing](https://northflank.com/pricing) |
|
|
54
|
+
| Morph Cloud | $0.100 | $0.05/MCU; MCU-hours = max(vCPU, RAM/4 GB, disk/16 GB); snapshots extra | [pricing](https://cloud.morph.so/web/pricing) |
|
|
55
|
+
| exe.dev | $0.105 | $0.105/h per 2 vCPU + 4 GiB, per second, up to 2 GiB per vCPU; needs an exe.dev subscription | [pricing](https://exe.dev/docs/standalone-vms.md) |
|
|
56
|
+
| Fly.io Sprites | $0.164 | Bills actual CPU and memory use; this is the full-use price; storage extra | [pricing](https://fly.io/sprites) |
|
|
57
|
+
| Blaxel | $0.166 | Billed on memory only ($0.0000115/GB-s), CPU scales with memory (~2 GB per core) | [pricing](https://blaxel.ai/pricing) |
|
|
58
|
+
| Daytona | $0.166 | Per second, plus disk above 5 GiB | [pricing](https://www.daytona.io/pricing) |
|
|
59
|
+
| E2B | $0.166 | Per second; Pro plan $150/mo | [pricing](https://e2b.dev/pricing) |
|
|
60
|
+
| HOPX | $0.166 | Per second, plus storage; size comes from the template, not the request | [pricing](https://hopx.ai/pricing) |
|
|
61
|
+
| Tensorlake | $0.200 | Bills active CPU (floor 20% of allocation); this is the full-use price; Pro plan is lower | [pricing](https://www.tensorlake.ai/pricing) |
|
|
62
|
+
| Cloudflare Sandbox | $0.216 | Bills active CPU and provisioned memory of the instance type; this is the full-CPU price; needs Workers Paid ($5/mo) and the bridge Worker in bridges/cloudflare | [pricing](https://developers.cloudflare.com/containers/platform/pricing/) |
|
|
63
|
+
| Modal | $0.239 | Sandbox rates, GPUs extra; region pinning x1.15-1.75 | [pricing](https://modal.com/pricing) |
|
|
64
|
+
| Runloop | $0.317 | Per second, plus disk; pause (suspend) is a Pro-plan feature | [pricing](https://www.runloop.ai/pricing) |
|
|
65
|
+
| Vercel Sandbox | $0.341 | Bills active CPU only; this is the 100%-CPU price, idle work costs less | [pricing](https://vercel.com/docs/vercel-sandbox/pricing) |
|
|
66
|
+
| Docker (local) | free | Runs on your own machine; free, but shares your kernel, so weaker isolation than a cloud micro-VM | [pricing](https://docs.docker.com/engine/) |
|
|
67
|
+
|
|
68
|
+
Checked 2026-11-04.
|
|
52
69
|
|
|
53
70
|
## Install
|
|
54
71
|
|
|
55
72
|
```bash
|
|
56
73
|
npm i @sandboxrouter/sdk
|
|
57
|
-
# then
|
|
58
|
-
npm i e2b
|
|
59
|
-
npm i @daytonaio/sdk
|
|
60
|
-
npm i modal
|
|
61
|
-
npm i @vercel/sandbox
|
|
62
|
-
npm i morphcloud
|
|
63
|
-
npm i @fly/sprites
|
|
64
|
-
npm i @blaxel/core
|
|
74
|
+
# then the SDK of each provider you use (Boat, Cloudflare, exe.dev, Maritime and Docker need none):
|
|
75
|
+
npm i e2b # E2B
|
|
76
|
+
npm i @daytonaio/sdk # Daytona
|
|
77
|
+
npm i modal # Modal
|
|
78
|
+
npm i @vercel/sandbox # Vercel Sandbox
|
|
79
|
+
npm i morphcloud # Morph Cloud
|
|
80
|
+
npm i @fly/sprites # Fly.io Sprites (its package asks for Node 24+)
|
|
81
|
+
npm i @blaxel/core # Blaxel
|
|
65
82
|
npm i @tensorlakeai/tensorlake # Tensorlake
|
|
66
|
-
npm i @runloop/api-client
|
|
83
|
+
npm i @runloop/api-client # Runloop
|
|
84
|
+
npm i @hopx-ai/sdk # HOPX
|
|
85
|
+
npm i @northflank/js-client # Northflank
|
|
67
86
|
```
|
|
68
87
|
|
|
69
|
-
|
|
88
|
+
Cloudflare sandboxes only run inside a Cloudflare Worker, so you deploy a small bridge Worker to your own account first: see [`bridges/cloudflare`](bridges/cloudflare).
|
|
89
|
+
|
|
90
|
+
Set the keys for the providers you want. Providers without keys are skipped.
|
|
70
91
|
|
|
71
92
|
| Provider | Env |
|
|
72
93
|
|---|---|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
94
|
+
| Blaxel | `BL_API_KEY` + `BL_WORKSPACE` |
|
|
95
|
+
| Boat | `BOAT_API_KEY` |
|
|
96
|
+
| Cloudflare Sandbox | `CLOUDFLARE_SANDBOX_URL` + `CLOUDFLARE_SANDBOX_TOKEN` |
|
|
75
97
|
| Daytona | `DAYTONA_API_KEY` |
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
98
|
+
| Docker (local) | none: Docker running locally |
|
|
99
|
+
| E2B | `E2B_API_KEY` |
|
|
100
|
+
| exe.dev | `EXE_TOKEN` |
|
|
79
101
|
| Fly.io Sprites | `SPRITES_TOKEN` |
|
|
80
|
-
|
|
|
81
|
-
|
|
|
102
|
+
| HOPX | `HOPX_API_KEY` |
|
|
103
|
+
| Maritime | `MARITIME_API_KEY` |
|
|
104
|
+
| Modal | `MODAL_TOKEN_ID` + `MODAL_TOKEN_SECRET` |
|
|
105
|
+
| Morph Cloud | `MORPH_API_KEY` |
|
|
106
|
+
| Northflank | `NORTHFLANK_API_TOKEN` + `NORTHFLANK_PROJECT_ID` |
|
|
82
107
|
| Runloop | `RUNLOOP_API_KEY` |
|
|
108
|
+
| Tensorlake | `TENSORLAKE_API_KEY` |
|
|
109
|
+
| Vercel Sandbox | `VERCEL_OIDC_TOKEN`, or `VERCEL_TOKEN` + `VERCEL_TEAM_ID` + `VERCEL_PROJECT_ID` |
|
|
83
110
|
|
|
84
111
|
## Library
|
|
85
112
|
|
|
@@ -218,18 +245,24 @@ with sr.create(vcpu=2, memory_gb=4, strategy="cheapest") as sbx:
|
|
|
218
245
|
| Blaxel | memory (CPU follows) | `ttl` | image | yes (archive, disk only) | Idle sandboxes go to free standby on their own. |
|
|
219
246
|
| Tensorlake | `cpus`, `memoryMb` | idle timeout | image | yes (memory + disk) | Output is line-based. |
|
|
220
247
|
| Runloop | snapped to Runloop's custom sizes | `keep_alive_time_seconds` (max 48 h) | blueprint | yes (Pro plan) | Commands start in the home directory. |
|
|
248
|
+
| HOPX | set by the template | timeout | template | yes (memory + disk) | Size requests aren't applied, so the router only sends default-size work. |
|
|
249
|
+
| Northflank | cheapest `nf-compute` plan that fits | `expiryTime` (5 min to 7 days) | any public image | no | Runs as a deployment service; files move as base64 over exec. |
|
|
250
|
+
| Cloudflare | smallest instance type that fits | alarm in the bridge Worker | Dockerfile in the bridge | no | Through the bridge Worker you deploy ([`bridges/cloudflare`](bridges/cloudflare)). |
|
|
251
|
+
| Boat | smallest of 4 fixed sizes that fits | auto-stop timer | saved snapshot | yes (stop: disk kept, processes end) | Plain REST. Commands that wait are capped at 600 s; files API up to 5 MiB. |
|
|
252
|
+
| exe.dev | `--cpu`, `--memory` (2 GiB per vCPU max) | none: `kill()` deletes | `--image` | no | Plain REST over exe.dev's `/exec`; long commands run detached and are polled. |
|
|
253
|
+
| Docker (local) | `--cpus`, `--memory` | container exits at the lifetime | any image | yes (`docker pause`) | Your machine, shared kernel. Never auto-routed; use `{ provider: "docker" }`. |
|
|
221
254
|
|
|
222
255
|
Capabilities in the catalog describe what each **adapter** implements, not everything a provider sells. Size limits are self-serve defaults and may be higher on your plan.
|
|
223
256
|
|
|
224
257
|
## Roadmap
|
|
225
258
|
|
|
226
|
-
- [x] More providers: Morph, Fly.io Sprites, Blaxel, Tensorlake, Runloop
|
|
259
|
+
- [x] More providers: Morph, Fly.io Sprites, Blaxel, Tensorlake, Runloop, HOPX, Northflank, Boat, exe.dev, Cloudflare (via a bridge Worker), Docker (local)
|
|
260
|
+
- [x] Neutral routing: price decides, ties alphabetical, no privileged slot
|
|
227
261
|
- [x] Desktops and phones (Maritime: Linux, Windows, Android)
|
|
228
262
|
- [x] Streaming exec output
|
|
229
263
|
- [x] Pause/resume where providers support it
|
|
230
264
|
- [x] Python client
|
|
231
265
|
- [x] Leaderboard built from `bench --share` reports
|
|
232
|
-
- [ ] Cloudflare Sandbox. Its SDK runs only inside a Worker with a Durable Object binding, so it needs a small Worker shim the router can call
|
|
233
266
|
- [ ] Computers on more providers (E2B Desktop, Daytona computer use)
|
|
234
267
|
- [ ] Snapshots and forks as a routed operation
|
|
235
268
|
- [ ] A hosted copy of the leaderboard page
|
package/dist/catalog.d.ts
CHANGED
|
@@ -16,16 +16,86 @@ export interface ProviderInfo {
|
|
|
16
16
|
maxVcpu: number;
|
|
17
17
|
maxMemoryGb: number;
|
|
18
18
|
};
|
|
19
|
+
/** Runs on your own machine (Docker). Never chosen automatically; name it to use it. */
|
|
20
|
+
local?: boolean;
|
|
19
21
|
/** Env vars the adapter reads. Any one group being fully set means "configured". */
|
|
20
22
|
credentials: string[][];
|
|
21
23
|
docs: string;
|
|
22
24
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Catalog order is the default routing order. Maritime is first because it is the cheapest
|
|
25
|
-
* managed sandbox per hour in this list at every shape; `npx @sandboxrouter/sdk providers` prints the
|
|
26
|
-
* numbers so you can check, and PRs that correct a price are welcome.
|
|
27
|
-
*/
|
|
28
25
|
export declare const CATALOG: ProviderInfo[];
|
|
26
|
+
/** Cloudflare only rents fixed instance types: https://developers.cloudflare.com/containers/platform/limits/ */
|
|
27
|
+
export declare const CLOUDFLARE_INSTANCES: readonly [{
|
|
28
|
+
readonly name: "lite";
|
|
29
|
+
readonly vcpu: number;
|
|
30
|
+
readonly memoryGb: 0.25;
|
|
31
|
+
}, {
|
|
32
|
+
readonly name: "basic";
|
|
33
|
+
readonly vcpu: number;
|
|
34
|
+
readonly memoryGb: 1;
|
|
35
|
+
}, {
|
|
36
|
+
readonly name: "standard-1";
|
|
37
|
+
readonly vcpu: number;
|
|
38
|
+
readonly memoryGb: 4;
|
|
39
|
+
}, {
|
|
40
|
+
readonly name: "standard-2";
|
|
41
|
+
readonly vcpu: 1;
|
|
42
|
+
readonly memoryGb: 6;
|
|
43
|
+
}, {
|
|
44
|
+
readonly name: "standard-3";
|
|
45
|
+
readonly vcpu: 2;
|
|
46
|
+
readonly memoryGb: 8;
|
|
47
|
+
}, {
|
|
48
|
+
readonly name: "standard-4";
|
|
49
|
+
readonly vcpu: 4;
|
|
50
|
+
readonly memoryGb: 12;
|
|
51
|
+
}];
|
|
52
|
+
/** Boat's fixed machine sizes and hourly prices: https://docs.boat.dev/machines.md */
|
|
53
|
+
export declare const BOAT_SIZES: readonly [{
|
|
54
|
+
readonly type: "small";
|
|
55
|
+
readonly vcpu: 2;
|
|
56
|
+
readonly memoryGb: 4;
|
|
57
|
+
readonly usdHour: 0.018;
|
|
58
|
+
}, {
|
|
59
|
+
readonly type: "default";
|
|
60
|
+
readonly vcpu: 4;
|
|
61
|
+
readonly memoryGb: 8;
|
|
62
|
+
readonly usdHour: 0.036;
|
|
63
|
+
}, {
|
|
64
|
+
readonly type: "large";
|
|
65
|
+
readonly vcpu: 8;
|
|
66
|
+
readonly memoryGb: 16;
|
|
67
|
+
readonly usdHour: 0.072;
|
|
68
|
+
}, {
|
|
69
|
+
readonly type: "xlarge";
|
|
70
|
+
readonly vcpu: 16;
|
|
71
|
+
readonly memoryGb: 32;
|
|
72
|
+
readonly usdHour: 0.2;
|
|
73
|
+
}];
|
|
74
|
+
export declare function cloudflareInstance(vcpu: number, memoryGb: number): {
|
|
75
|
+
readonly name: "lite";
|
|
76
|
+
readonly vcpu: number;
|
|
77
|
+
readonly memoryGb: 0.25;
|
|
78
|
+
} | {
|
|
79
|
+
readonly name: "basic";
|
|
80
|
+
readonly vcpu: number;
|
|
81
|
+
readonly memoryGb: 1;
|
|
82
|
+
} | {
|
|
83
|
+
readonly name: "standard-1";
|
|
84
|
+
readonly vcpu: number;
|
|
85
|
+
readonly memoryGb: 4;
|
|
86
|
+
} | {
|
|
87
|
+
readonly name: "standard-2";
|
|
88
|
+
readonly vcpu: 1;
|
|
89
|
+
readonly memoryGb: 6;
|
|
90
|
+
} | {
|
|
91
|
+
readonly name: "standard-3";
|
|
92
|
+
readonly vcpu: 2;
|
|
93
|
+
readonly memoryGb: 8;
|
|
94
|
+
} | {
|
|
95
|
+
readonly name: "standard-4";
|
|
96
|
+
readonly vcpu: 4;
|
|
97
|
+
readonly memoryGb: 12;
|
|
98
|
+
};
|
|
29
99
|
export declare function info(id: ProviderId): ProviderInfo;
|
|
30
100
|
export declare const DEFAULT_VCPU = 2;
|
|
31
101
|
export declare const DEFAULT_MEMORY_GB = 4;
|
package/dist/catalog.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Every provider is treated the same: the router orders candidates by list price for the requested
|
|
3
|
+
* size (or by your measured cold starts), ties broken alphabetically. Nobody gets a better slot.
|
|
4
|
+
* Prices come from each provider's public pricing page; PRs that correct one are welcome.
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
const ENTRIES = [
|
|
7
|
+
{
|
|
8
|
+
id: "docker",
|
|
9
|
+
name: "Docker (local)",
|
|
10
|
+
pricing: {
|
|
11
|
+
vcpuHour: 0,
|
|
12
|
+
gbHour: 0,
|
|
13
|
+
note: "runs on your own machine; free, but shares your kernel, so weaker isolation than a cloud micro-VM",
|
|
14
|
+
source: "https://docs.docker.com/engine/",
|
|
15
|
+
checked: "2026-11-04",
|
|
16
|
+
},
|
|
17
|
+
capabilities: ["pause", "custom-image"],
|
|
18
|
+
limits: { maxVcpu: 64, maxMemoryGb: 256 },
|
|
19
|
+
local: true,
|
|
20
|
+
credentials: [],
|
|
21
|
+
docs: "https://docs.docker.com/engine/",
|
|
22
|
+
},
|
|
7
23
|
{
|
|
8
24
|
id: "maritime",
|
|
9
25
|
name: "Maritime",
|
|
@@ -157,6 +173,99 @@ export const CATALOG = [
|
|
|
157
173
|
docs: "https://docs.runloop.ai",
|
|
158
174
|
},
|
|
159
175
|
];
|
|
176
|
+
ENTRIES.push({
|
|
177
|
+
id: "boat",
|
|
178
|
+
name: "Boat",
|
|
179
|
+
pricing: {
|
|
180
|
+
vcpuHour: 0.009,
|
|
181
|
+
gbHour: 0,
|
|
182
|
+
note: "fixed sizes billed per second ($0.018/h for 2 vCPU / 4 GB up to $0.20/h for 16 / 32); needs a plan from $20/mo, paid back as VM time",
|
|
183
|
+
source: "https://docs.boat.dev/pricing.md",
|
|
184
|
+
checked: "2026-11-04",
|
|
185
|
+
},
|
|
186
|
+
// "pause" here is Boat's stop: the disk is kept, running processes end.
|
|
187
|
+
capabilities: ["pause", "custom-image"],
|
|
188
|
+
limits: { maxVcpu: 16, maxMemoryGb: 32 },
|
|
189
|
+
credentials: [["BOAT_API_KEY"]],
|
|
190
|
+
docs: "https://docs.boat.dev",
|
|
191
|
+
}, {
|
|
192
|
+
id: "exedev",
|
|
193
|
+
name: "exe.dev",
|
|
194
|
+
pricing: {
|
|
195
|
+
vcpuHour: 0.0525,
|
|
196
|
+
gbHour: 0,
|
|
197
|
+
note: "$0.105/h per 2 vCPU + 4 GiB, per second, up to 2 GiB per vCPU; needs an exe.dev subscription",
|
|
198
|
+
source: "https://exe.dev/docs/standalone-vms.md",
|
|
199
|
+
checked: "2026-11-04",
|
|
200
|
+
},
|
|
201
|
+
capabilities: ["custom-image"],
|
|
202
|
+
limits: { maxVcpu: 16, maxMemoryGb: 32 },
|
|
203
|
+
credentials: [["EXE_TOKEN"]],
|
|
204
|
+
docs: "https://exe.dev/docs/https-api.md",
|
|
205
|
+
}, {
|
|
206
|
+
id: "northflank",
|
|
207
|
+
name: "Northflank",
|
|
208
|
+
pricing: {
|
|
209
|
+
vcpuHour: 0.01667,
|
|
210
|
+
gbHour: 0.00833,
|
|
211
|
+
note: "per second on Northflank Cloud, ephemeral disk included; sandboxes run as deployment services (microVMs)",
|
|
212
|
+
source: "https://northflank.com/pricing",
|
|
213
|
+
checked: "2026-11-04",
|
|
214
|
+
},
|
|
215
|
+
capabilities: ["custom-image"],
|
|
216
|
+
limits: { maxVcpu: 8, maxMemoryGb: 16 },
|
|
217
|
+
credentials: [["NORTHFLANK_API_TOKEN", "NORTHFLANK_PROJECT_ID"]],
|
|
218
|
+
docs: "https://northflank.com/docs/v1/application/overview",
|
|
219
|
+
}, {
|
|
220
|
+
id: "hopx",
|
|
221
|
+
name: "HOPX",
|
|
222
|
+
pricing: {
|
|
223
|
+
vcpuHour: 0.0504,
|
|
224
|
+
gbHour: 0.0162,
|
|
225
|
+
note: "per second, plus storage; size comes from the template, not the request",
|
|
226
|
+
source: "https://hopx.ai/pricing",
|
|
227
|
+
checked: "2026-11-04",
|
|
228
|
+
},
|
|
229
|
+
capabilities: ["pause", "custom-image"],
|
|
230
|
+
// HOPX takes no size at create; capped at the default size so bigger requests go elsewhere.
|
|
231
|
+
limits: { maxVcpu: 2, maxMemoryGb: 4 },
|
|
232
|
+
credentials: [["HOPX_API_KEY"]],
|
|
233
|
+
docs: "https://docs.hopx.ai",
|
|
234
|
+
}, {
|
|
235
|
+
id: "cloudflare",
|
|
236
|
+
name: "Cloudflare Sandbox",
|
|
237
|
+
pricing: {
|
|
238
|
+
vcpuHour: 0.072,
|
|
239
|
+
gbHour: 0.009,
|
|
240
|
+
note: "bills active CPU and provisioned memory of the instance type; this is the full-CPU price; needs Workers Paid ($5/mo) and the bridge Worker in bridges/cloudflare",
|
|
241
|
+
source: "https://developers.cloudflare.com/containers/platform/pricing/",
|
|
242
|
+
checked: "2026-11-04",
|
|
243
|
+
},
|
|
244
|
+
capabilities: [],
|
|
245
|
+
limits: { maxVcpu: 4, maxMemoryGb: 12 },
|
|
246
|
+
credentials: [["CLOUDFLARE_SANDBOX_URL", "CLOUDFLARE_SANDBOX_TOKEN"]],
|
|
247
|
+
docs: "https://developers.cloudflare.com/sandbox/",
|
|
248
|
+
});
|
|
249
|
+
export const CATALOG = [...ENTRIES].sort((a, b) => a.name.localeCompare(b.name));
|
|
250
|
+
/** Cloudflare only rents fixed instance types: https://developers.cloudflare.com/containers/platform/limits/ */
|
|
251
|
+
export const CLOUDFLARE_INSTANCES = [
|
|
252
|
+
{ name: "lite", vcpu: 1 / 16, memoryGb: 0.25 },
|
|
253
|
+
{ name: "basic", vcpu: 1 / 4, memoryGb: 1 },
|
|
254
|
+
{ name: "standard-1", vcpu: 1 / 2, memoryGb: 4 },
|
|
255
|
+
{ name: "standard-2", vcpu: 1, memoryGb: 6 },
|
|
256
|
+
{ name: "standard-3", vcpu: 2, memoryGb: 8 },
|
|
257
|
+
{ name: "standard-4", vcpu: 4, memoryGb: 12 },
|
|
258
|
+
];
|
|
259
|
+
/** Boat's fixed machine sizes and hourly prices: https://docs.boat.dev/machines.md */
|
|
260
|
+
export const BOAT_SIZES = [
|
|
261
|
+
{ type: "small", vcpu: 2, memoryGb: 4, usdHour: 0.018 },
|
|
262
|
+
{ type: "default", vcpu: 4, memoryGb: 8, usdHour: 0.036 },
|
|
263
|
+
{ type: "large", vcpu: 8, memoryGb: 16, usdHour: 0.072 },
|
|
264
|
+
{ type: "xlarge", vcpu: 16, memoryGb: 32, usdHour: 0.2 },
|
|
265
|
+
];
|
|
266
|
+
export function cloudflareInstance(vcpu, memoryGb) {
|
|
267
|
+
return CLOUDFLARE_INSTANCES.find((t) => t.vcpu >= vcpu && t.memoryGb >= memoryGb) ?? CLOUDFLARE_INSTANCES[CLOUDFLARE_INSTANCES.length - 1];
|
|
268
|
+
}
|
|
160
269
|
export function info(id) {
|
|
161
270
|
const p = CATALOG.find((c) => c.id === id);
|
|
162
271
|
if (!p)
|
|
@@ -176,6 +285,19 @@ export function hourlyCost(id, spec = {}) {
|
|
|
176
285
|
// Blaxel sizes by memory, about 2 GB per core.
|
|
177
286
|
if (id === "blaxel")
|
|
178
287
|
mem = Math.max(mem, vcpu * 2);
|
|
288
|
+
// Boat rents the smallest fixed size that fits.
|
|
289
|
+
if (id === "boat") {
|
|
290
|
+
const s = BOAT_SIZES.find((b) => b.vcpu >= vcpu && b.memoryGb >= mem) ?? BOAT_SIZES[BOAT_SIZES.length - 1];
|
|
291
|
+
return s.usdHour;
|
|
292
|
+
}
|
|
293
|
+
// exe.dev bills per vCPU with up to 2 GiB each, so memory beyond that adds vCPUs.
|
|
294
|
+
if (id === "exedev")
|
|
295
|
+
return Math.max(Math.ceil(vcpu), Math.ceil(mem / 2)) * pricing.vcpuHour;
|
|
296
|
+
// Cloudflare bills the instance type that fits, not the exact request.
|
|
297
|
+
if (id === "cloudflare") {
|
|
298
|
+
const t = cloudflareInstance(vcpu, mem);
|
|
299
|
+
return t.vcpu * pricing.vcpuHour + t.memoryGb * pricing.gbHour;
|
|
300
|
+
}
|
|
179
301
|
// Morph bills whole MCUs: the larger of vCPUs and 4 GB blocks of memory.
|
|
180
302
|
if (id === "morph")
|
|
181
303
|
return Math.max(vcpu, Math.ceil(mem / 4)) * pricing.vcpuHour;
|
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ Usage:
|
|
|
21
21
|
sandboxrouter serve [--port 8787] [--host 127.0.0.1] [--cors https://site.example,http://localhost:3000]
|
|
22
22
|
|
|
23
23
|
Spec flags: --vcpu N --memory GB --gpu T4 --timeout MIN
|
|
24
|
-
Route flags: --provider
|
|
24
|
+
Route flags: --provider e2b[,modal,...] --strategy cheapest|fastest
|
|
25
25
|
--no-fallback --require gpu,custom-image
|
|
26
26
|
|
|
27
27
|
Keys: MARITIME_API_KEY, E2B_API_KEY, DAYTONA_API_KEY, MODAL_TOKEN_ID+MODAL_TOKEN_SECRET,
|
package/dist/index.d.ts
CHANGED
|
@@ -14,4 +14,10 @@ export { SpritesProvider } from "./providers/sprites.js";
|
|
|
14
14
|
export { BlaxelProvider } from "./providers/blaxel.js";
|
|
15
15
|
export { TensorlakeProvider } from "./providers/tensorlake.js";
|
|
16
16
|
export { RunloopProvider } from "./providers/runloop.js";
|
|
17
|
+
export { DockerProvider } from "./providers/docker.js";
|
|
18
|
+
export { HopxProvider } from "./providers/hopx.js";
|
|
19
|
+
export { CloudflareProvider } from "./providers/cloudflare.js";
|
|
20
|
+
export { NorthflankProvider } from "./providers/northflank.js";
|
|
21
|
+
export { BoatProvider } from "./providers/boat.js";
|
|
22
|
+
export { ExeDevProvider } from "./providers/exedev.js";
|
|
17
23
|
export * from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -14,4 +14,10 @@ export { SpritesProvider } from "./providers/sprites.js";
|
|
|
14
14
|
export { BlaxelProvider } from "./providers/blaxel.js";
|
|
15
15
|
export { TensorlakeProvider } from "./providers/tensorlake.js";
|
|
16
16
|
export { RunloopProvider } from "./providers/runloop.js";
|
|
17
|
+
export { DockerProvider } from "./providers/docker.js";
|
|
18
|
+
export { HopxProvider } from "./providers/hopx.js";
|
|
19
|
+
export { CloudflareProvider } from "./providers/cloudflare.js";
|
|
20
|
+
export { NorthflankProvider } from "./providers/northflank.js";
|
|
21
|
+
export { BoatProvider } from "./providers/boat.js";
|
|
22
|
+
export { ExeDevProvider } from "./providers/exedev.js";
|
|
17
23
|
export * from "./types.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Provider, type ProviderSandbox, type SandboxSpec } from "../types.js";
|
|
2
|
+
export declare class BoatProvider implements Provider {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private baseUrl;
|
|
5
|
+
readonly id: "boat";
|
|
6
|
+
constructor(apiKey?: string | undefined, baseUrl?: string);
|
|
7
|
+
configured(): boolean;
|
|
8
|
+
/** @internal */
|
|
9
|
+
request(method: string, path: string, init?: {
|
|
10
|
+
json?: unknown;
|
|
11
|
+
headers?: Record<string, string>;
|
|
12
|
+
}): Promise<Response>;
|
|
13
|
+
/** @internal */
|
|
14
|
+
json<T>(method: string, path: string, json?: unknown, headers?: Record<string, string>): Promise<T>;
|
|
15
|
+
/** @internal Poll until the sandbox can take commands. Resumes it first when it is stopped. */
|
|
16
|
+
ready(id: string, timeoutMs?: number): Promise<BoatSandboxObject>;
|
|
17
|
+
create(spec: SandboxSpec): Promise<ProviderSandbox>;
|
|
18
|
+
connect(nativeId: string): Promise<ProviderSandbox>;
|
|
19
|
+
}
|
|
20
|
+
interface BoatSandboxObject {
|
|
21
|
+
id: string;
|
|
22
|
+
state: string;
|
|
23
|
+
error?: string | null;
|
|
24
|
+
}
|
|
25
|
+
export {};
|