@sandboxrouter/sdk 0.8.0 → 0.8.2
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 +5 -5
- package/dist/cli.js +8 -3
- package/dist/providers/tensorlake.js +2 -7
- package/dist/router.d.ts +11 -3
- package/dist/router.js +18 -6
- package/dist/types.d.ts +8 -0
- package/dist/validate.d.ts +1 -1
- package/dist/validate.js +2 -2
- package/dist/verified.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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: Agent37, Alibaba Cloud, AWS (Bedrock AgentCore and Lambda MicroVMs), Azure Container Apps, Beam, Blaxel, Boat, Cloudflare, CodeSandbox, Daytona, Dedalus Machines, Deno, E2B, exe.dev, Fly.io Sprites, Freestyle, HOPX, InstaBox, Koyeb, Maritime, Modal, Morph, Northflank, Novita, OpenScale, Runloop, Sail, Superserve, Tensorlake, Upstash Box 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
|
|
|
@@ -27,7 +27,7 @@ Site and docs: [sandboxrouter.ai](https://sandboxrouter.ai) · npm: [`@sandboxro
|
|
|
27
27
|
|
|
28
28
|
## Why
|
|
29
29
|
|
|
30
|
-
AI agents need a disposable computer to run code a model wrote: install packages, run tests, do an RL rollout. Dozens of providers sell one, each with its own SDK and auth, and the same 2 vCPU / 4 GB sandbox costs from under a cent to over thirty cents an hour.
|
|
30
|
+
AI agents need a disposable computer to run code a model wrote: install packages, run tests, do an RL rollout. Dozens of providers sell one, each with its own SDK and auth, and the same 2 vCPU / 4 GB sandbox costs from under a cent to over thirty cents an hour. sandboxRouter is one interface over all of them:
|
|
31
31
|
|
|
32
32
|
- **Same code everywhere.** `create`, `exec` (streamed), `writeFile`, `readFile`, `pause`, `kill`. Switch providers by changing which keys are set.
|
|
33
33
|
- **Cheapest real cost, then speed, with fallback.** Or `cheapest` for list price alone, `fastest` by start times, `verified` for tested providers first, or a list of providers you name. `requireVerified` limits a router to providers that passed the real-account check.
|
|
@@ -43,9 +43,9 @@ List price while running, 2 vCPU / 4 GB, cheapest first; waitlisted sign-ups fol
|
|
|
43
43
|
| Provider | $/hr | Plan needed | Real-account check | Source |
|
|
44
44
|
|---|---|---|---|---|
|
|
45
45
|
| [Agent37](https://sandboxrouter.ai/providers/agent37) | $0.007 | none | passed (2026-11-13) | [pricing](https://www.agent37.com/docs/agents-api/billing) |
|
|
46
|
-
| [Boat](https://sandboxrouter.ai/providers/boat) | $0.018 | $20/mo (credited) |
|
|
46
|
+
| [Boat](https://sandboxrouter.ai/providers/boat) | $0.018 | $20/mo (credited) | passed (2026-11-14) | [pricing](https://docs.boat.dev/pricing.md) |
|
|
47
47
|
| [Maritime](https://sandboxrouter.ai/providers/maritime) | $0.022 | $20/mo (credited) | passed (2026-11-13) | [pricing](https://maritime.sh/pricing) |
|
|
48
|
-
| [Sailboxes (Sail Research)](https://sandboxrouter.ai/providers/sail) | up to $0.062 | none | passed (2026-11-
|
|
48
|
+
| [Sailboxes (Sail Research)](https://sandboxrouter.ai/providers/sail) | up to $0.062 | none | passed (2026-11-14) | [pricing](https://docs.sailresearch.com/sailboxes-pricing.md) |
|
|
49
49
|
| [Northflank](https://sandboxrouter.ai/providers/northflank) | $0.067 | none | not run yet | [pricing](https://northflank.com/pricing) |
|
|
50
50
|
| [Morph Cloud](https://sandboxrouter.ai/providers/morph) | $0.100 | none | not run yet | [pricing](https://cloud.morph.so/web/pricing) |
|
|
51
51
|
| [exe.dev](https://sandboxrouter.ai/providers/exedev) | $0.105 | $15/mo | not run yet | [pricing](https://exe.dev/docs/standalone-vms.md) |
|
|
@@ -57,7 +57,7 @@ List price while running, 2 vCPU / 4 GB, cheapest first; waitlisted sign-ups fol
|
|
|
57
57
|
| [Blaxel](https://sandboxrouter.ai/providers/blaxel) | $0.166 | none | passed (2026-11-13) | [pricing](https://blaxel.ai/pricing) |
|
|
58
58
|
| [HOPX](https://sandboxrouter.ai/providers/hopx) | $0.166 | none | not run yet | [pricing](https://hopx.ai/pricing) |
|
|
59
59
|
| [Superserve](https://sandboxrouter.ai/providers/superserve) | $0.166 | maybe $20/mo (credited) | passed (2026-11-13) | [pricing](https://superserve.ai/pricing) |
|
|
60
|
-
| [Tensorlake](https://sandboxrouter.ai/providers/tensorlake) | up to $0.200 | none |
|
|
60
|
+
| [Tensorlake](https://sandboxrouter.ai/providers/tensorlake) | up to $0.200 | none | passed (2026-11-14) | [pricing](https://www.tensorlake.ai/pricing) |
|
|
61
61
|
| [Upstash Box](https://sandboxrouter.ai/providers/upstash) | up to $0.200 | none | not run yet | [pricing](https://upstash.com/pricing/box) |
|
|
62
62
|
| [Cloudflare Sandbox](https://sandboxrouter.ai/providers/cloudflare) | up to $0.216 | $5/mo | passed (2026-11-13) | [pricing](https://developers.cloudflare.com/containers/platform/pricing/) |
|
|
63
63
|
| [Amazon Bedrock AgentCore Code Interpreter](https://sandboxrouter.ai/providers/agentcore) | up to $0.217 | none | passed (2026-11-13) | [pricing](https://aws.amazon.com/bedrock/agentcore/pricing/) |
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { gc, listOpen } from "./gc.js";
|
|
|
9
9
|
import { contributor, publishableCheck } from "./results.js";
|
|
10
10
|
import { loadReports, makeReport, renderLeaderboard } from "./leaderboard.js";
|
|
11
11
|
import { DEFAULT_MEMORY_GB, DEFAULT_VCPU, accessLabel, benchmarksPublishable, hourlyCost, listingOrder, planLabel, unfit } from "./catalog.js";
|
|
12
|
-
import { Router } from "./router.js";
|
|
12
|
+
import { Router, rankUsd, runHours } from "./router.js";
|
|
13
13
|
import { createApp } from "./server.js";
|
|
14
14
|
import { RouterError } from "./types.js";
|
|
15
15
|
const HELP = `sandboxrouter: one API for every sandbox provider
|
|
@@ -40,7 +40,7 @@ Usage:
|
|
|
40
40
|
sandboxrouter screenshot <provider:id> [--out screen.png]
|
|
41
41
|
sandboxrouter serve [--port 8787] [--host 127.0.0.1] [--cors https://site.example,http://localhost:3000]
|
|
42
42
|
|
|
43
|
-
Spec flags: --vcpu N --memory GB --gpu T4 --timeout MIN
|
|
43
|
+
Spec flags: --vcpu N --memory GB --gpu T4 --timeout MIN --expect SEC (how long you'll use it; ranks by the real bill)
|
|
44
44
|
Route flags: --provider e2b[,modal,...] --strategy balanced|verified|cheapest|fastest (default balanced: cheapest real cost, speed breaks near-ties)
|
|
45
45
|
--no-fallback --fallback-any (also on setup errors like a bad key)
|
|
46
46
|
--create-timeout SEC (per provider, default 120) --require gpu,custom-image
|
|
@@ -54,6 +54,7 @@ const { values, positionals } = parseArgs({
|
|
|
54
54
|
memory: { type: "string" },
|
|
55
55
|
gpu: { type: "string" },
|
|
56
56
|
timeout: { type: "string" },
|
|
57
|
+
expect: { type: "string" },
|
|
57
58
|
provider: { type: "string" },
|
|
58
59
|
strategy: { type: "string" },
|
|
59
60
|
"no-fallback": { type: "boolean" },
|
|
@@ -96,6 +97,7 @@ const spec = {
|
|
|
96
97
|
memoryGb: num(values.memory),
|
|
97
98
|
gpu: values.gpu,
|
|
98
99
|
timeoutMinutes: num(values.timeout),
|
|
100
|
+
expectedSeconds: num(values.expect),
|
|
99
101
|
};
|
|
100
102
|
const providers = list(values.provider);
|
|
101
103
|
const by = (() => {
|
|
@@ -165,7 +167,10 @@ async function main() {
|
|
|
165
167
|
const size = `${c.size.vcpu} vCPU / ${c.size.memoryGb} GB`;
|
|
166
168
|
const speed = c.medianCreateMs !== undefined ? `${c.medianCreateMs} ms` : "";
|
|
167
169
|
const fee = c.perSandboxUsd ? ` + $${c.perSandboxUsd}/sandbox` : "";
|
|
168
|
-
|
|
170
|
+
// With --expect, what that run would be billed (minimum charge and fee included): what it's ranked by.
|
|
171
|
+
const hours = runHours(spec);
|
|
172
|
+
const run = spec.expectedSeconds ? ` ≈ $${(rankUsd(c, hours) * hours).toFixed(6)} for ${Math.round(hours * 3600)} s` : "";
|
|
173
|
+
const cols = [`$${c.estimatedHourlyUsd.toFixed(4)}/hr${list}${fee}${run}`, size, speed, tested].filter(Boolean).join(" ");
|
|
169
174
|
console.log(`${mark} ${c.provider.padEnd(11)} ${cols}${c.skip ? ` (skipped: ${c.skip})` : ""}`);
|
|
170
175
|
}
|
|
171
176
|
return;
|
|
@@ -24,6 +24,7 @@ export class TensorlakeProvider {
|
|
|
24
24
|
try {
|
|
25
25
|
sbx = await Sandbox.create({
|
|
26
26
|
apiKey: this.apiKey,
|
|
27
|
+
name: `sbr-${randomUUID()}`,
|
|
27
28
|
image,
|
|
28
29
|
cpus: spec.vcpu ?? DEFAULT_VCPU,
|
|
29
30
|
memoryMb: Math.round((spec.memoryGb ?? DEFAULT_MEMORY_GB) * 1024),
|
|
@@ -160,15 +161,9 @@ class TensorlakeBox {
|
|
|
160
161
|
throw fail(err);
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
|
-
/**
|
|
164
|
-
* Suspend, keeping memory and disk. Only named sandboxes can suspend, so an unnamed one is
|
|
165
|
-
* named first. router.get() resumes it.
|
|
166
|
-
*/
|
|
164
|
+
/** Suspend, keeping memory and disk. Names are assigned at creation and are immutable. */
|
|
167
165
|
async pause() {
|
|
168
166
|
try {
|
|
169
|
-
const info = await this.sbx.info();
|
|
170
|
-
if (!info.name)
|
|
171
|
-
await this.sbx.update({ name: `sbr-${randomUUID()}` });
|
|
172
167
|
await this.sbx.suspend();
|
|
173
168
|
}
|
|
174
169
|
catch (err) {
|
package/dist/router.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface Candidate {
|
|
|
31
31
|
failing: boolean;
|
|
32
32
|
/**
|
|
33
33
|
* A fixed charge per sandbox, USD, on top of `estimatedHourlyUsd` (Sail's free plan: $0.01). Ranking
|
|
34
|
-
* spreads it over the
|
|
34
|
+
* spreads it over the run (`expectedSeconds`, else `timeoutMinutes`, default 30), so it weighs more on short ones.
|
|
35
35
|
*/
|
|
36
36
|
perSandboxUsd?: number;
|
|
37
37
|
/** Why this provider won't be tried, if it won't. */
|
|
@@ -184,10 +184,18 @@ export declare function parseId(id: string): {
|
|
|
184
184
|
* The default order. Providers whose last real-account check failed go last. The rest go cheapest
|
|
185
185
|
* first by effective price (list price x what the provider really billed, where measured), except that
|
|
186
186
|
* providers within PRICE_TIE of the cheapest remaining one count as a tie and go fastest first (by
|
|
187
|
-
* median create time; unknown speed after known), then cheapest, then alphabetical.
|
|
188
|
-
*
|
|
187
|
+
* median create time; unknown speed after known), then cheapest, then alphabetical. Price is what a run
|
|
188
|
+
* of `lifetimeHours` would be billed (`rankUsd`), as USD per hour.
|
|
189
189
|
*/
|
|
190
190
|
export declare function balanced(list: Candidate[], lifetimeHours?: number): Candidate[];
|
|
191
|
+
/** Hours ranking assumes a sandbox runs: `expectedSeconds` (no longer than the lifetime), else `timeoutMinutes` (default 30). */
|
|
192
|
+
export declare function runHours(spec: SandboxSpec): number;
|
|
193
|
+
/**
|
|
194
|
+
* USD per hour a candidate is ranked by: what a run of `hours` would be billed, divided by `hours`. The
|
|
195
|
+
* run is rounded up to the provider's minimum and billing increment (`billingRules`), and its
|
|
196
|
+
* per-sandbox charge is added, so short runs favor providers that bill by the second without a fee.
|
|
197
|
+
*/
|
|
198
|
+
export declare function rankUsd(c: Candidate, hours: number): number;
|
|
191
199
|
/** Seconds to wait for one provider's create before trying the next (RouteOptions.createTimeoutSeconds). */
|
|
192
200
|
export declare const DEFAULT_CREATE_TIMEOUT_S = 120;
|
|
193
201
|
/** Seconds a whole create may take over every provider it tries (RouteOptions.deadlineSeconds). */
|
package/dist/router.js
CHANGED
|
@@ -181,7 +181,7 @@ export class Router {
|
|
|
181
181
|
// depends on who the provider is, only on its price, what it really billed, how fast it started for
|
|
182
182
|
// this router and whether its last real-account check failed. An explicit list keeps the caller's order.
|
|
183
183
|
if (!explicit) {
|
|
184
|
-
const hours = (spec
|
|
184
|
+
const hours = runHours(spec);
|
|
185
185
|
const byCost = (a, b) => rankUsd(a, hours) - rankUsd(b, hours) || a.provider.localeCompare(b.provider);
|
|
186
186
|
const byList = (a, b) => a.listHourlyUsd - b.listHourlyUsd || a.provider.localeCompare(b.provider);
|
|
187
187
|
if (strategy === "fastest") {
|
|
@@ -439,8 +439,8 @@ function round(n) {
|
|
|
439
439
|
* The default order. Providers whose last real-account check failed go last. The rest go cheapest
|
|
440
440
|
* first by effective price (list price x what the provider really billed, where measured), except that
|
|
441
441
|
* providers within PRICE_TIE of the cheapest remaining one count as a tie and go fastest first (by
|
|
442
|
-
* median create time; unknown speed after known), then cheapest, then alphabetical.
|
|
443
|
-
*
|
|
442
|
+
* median create time; unknown speed after known), then cheapest, then alphabetical. Price is what a run
|
|
443
|
+
* of `lifetimeHours` would be billed (`rankUsd`), as USD per hour.
|
|
444
444
|
*/
|
|
445
445
|
export function balanced(list, lifetimeHours = 0.5) {
|
|
446
446
|
const cost = (c) => rankUsd(c, lifetimeHours);
|
|
@@ -460,9 +460,21 @@ export function balanced(list, lifetimeHours = 0.5) {
|
|
|
460
460
|
};
|
|
461
461
|
return [...order(list.filter((c) => !c.failing)), ...order(list.filter((c) => c.failing))];
|
|
462
462
|
}
|
|
463
|
-
/**
|
|
464
|
-
function
|
|
465
|
-
|
|
463
|
+
/** Hours ranking assumes a sandbox runs: `expectedSeconds` (no longer than the lifetime), else `timeoutMinutes` (default 30). */
|
|
464
|
+
export function runHours(spec) {
|
|
465
|
+
const lifetime = (spec.timeoutMinutes ?? 30) * 60;
|
|
466
|
+
return Math.min(spec.expectedSeconds ?? lifetime, lifetime) / 3600;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* USD per hour a candidate is ranked by: what a run of `hours` would be billed, divided by `hours`. The
|
|
470
|
+
* run is rounded up to the provider's minimum and billing increment (`billingRules`), and its
|
|
471
|
+
* per-sandbox charge is added, so short runs favor providers that bill by the second without a fee.
|
|
472
|
+
*/
|
|
473
|
+
export function rankUsd(c, hours) {
|
|
474
|
+
const { minSeconds, incrementSeconds } = billingRules(c.provider);
|
|
475
|
+
const seconds = hours * 3600;
|
|
476
|
+
const billed = Math.max(minSeconds, Math.ceil(seconds / incrementSeconds - 1e-9) * incrementSeconds);
|
|
477
|
+
return (c.estimatedHourlyUsd * billed) / Math.max(seconds, 1) + (c.perSandboxUsd ?? 0) / Math.max(hours, 1 / 3600);
|
|
466
478
|
}
|
|
467
479
|
/** Seconds to wait for one provider's create before trying the next (RouteOptions.createTimeoutSeconds). */
|
|
468
480
|
export const DEFAULT_CREATE_TIMEOUT_S = 120;
|
package/dist/types.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ export interface SandboxSpec {
|
|
|
18
18
|
env?: Record<string, string>;
|
|
19
19
|
/** Hard lifetime in minutes (default 30). */
|
|
20
20
|
timeoutMinutes?: number;
|
|
21
|
+
/**
|
|
22
|
+
* How long you expect to use the sandbox, in seconds. Only a ranking hint: the sandbox still lives
|
|
23
|
+
* until you kill it or `timeoutMinutes` runs out. Without it, ranking assumes the whole lifetime.
|
|
24
|
+
* With it, providers are ranked by what that run would really be billed, minimum charge, billing
|
|
25
|
+
* increment and per-sandbox fee included, so a provider that bills per second can beat one that is
|
|
26
|
+
* cheaper per hour but charges a full minute.
|
|
27
|
+
*/
|
|
28
|
+
expectedSeconds?: number;
|
|
21
29
|
/**
|
|
22
30
|
* Provider-specific image/template, keyed by provider, e.g.
|
|
23
31
|
* { maritime: "python", e2b: "code-interpreter-v1", modal: "python:3.13-slim" }.
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type RouteOptions, type SandboxSpec, type Strategy } from "./types.js";
|
|
2
2
|
/** Every key a SandboxSpec can have. Anything else in a request is a mistake (e.g. "memory" for "memoryGb"). */
|
|
3
|
-
export declare const SPEC_KEYS: readonly ["vcpu", "memoryGb", "gpu", "env", "timeoutMinutes", "image", "metadata"];
|
|
3
|
+
export declare const SPEC_KEYS: readonly ["vcpu", "memoryGb", "gpu", "env", "timeoutMinutes", "expectedSeconds", "image", "metadata"];
|
|
4
4
|
/** Every key RouteOptions can have. */
|
|
5
5
|
export declare const ROUTE_KEYS: readonly ["provider", "strategy", "fallback", "require", "createTimeoutSeconds", "deadlineSeconds"];
|
|
6
6
|
export declare const STRATEGIES: readonly Strategy[];
|
package/dist/validate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CATALOG } from "./catalog.js";
|
|
2
2
|
import { RouterError } from "./types.js";
|
|
3
3
|
/** Every key a SandboxSpec can have. Anything else in a request is a mistake (e.g. "memory" for "memoryGb"). */
|
|
4
|
-
export const SPEC_KEYS = ["vcpu", "memoryGb", "gpu", "env", "timeoutMinutes", "image", "metadata"];
|
|
4
|
+
export const SPEC_KEYS = ["vcpu", "memoryGb", "gpu", "env", "timeoutMinutes", "expectedSeconds", "image", "metadata"];
|
|
5
5
|
/** Every key RouteOptions can have. */
|
|
6
6
|
export const ROUTE_KEYS = ["provider", "strategy", "fallback", "require", "createTimeoutSeconds", "deadlineSeconds"];
|
|
7
7
|
export const STRATEGIES = ["default", "balanced", "verified", "cheapest", "fastest"];
|
|
@@ -16,7 +16,7 @@ const stringMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v
|
|
|
16
16
|
* Called by `plan()`, so every create, route and CLI command goes through it.
|
|
17
17
|
*/
|
|
18
18
|
export function validate(spec = {}, route = {}) {
|
|
19
|
-
for (const k of ["vcpu", "memoryGb", "timeoutMinutes"]) {
|
|
19
|
+
for (const k of ["vcpu", "memoryGb", "timeoutMinutes", "expectedSeconds"]) {
|
|
20
20
|
if (spec[k] !== undefined && !positive(spec[k]))
|
|
21
21
|
throw bad(`${k} must be a positive number, got ${show(spec[k])}`);
|
|
22
22
|
}
|
package/dist/verified.js
CHANGED
|
@@ -3,6 +3,7 @@ export const VERIFIED = {
|
|
|
3
3
|
agentcore: "2026-11-13",
|
|
4
4
|
lambda: "2026-11-13",
|
|
5
5
|
blaxel: "2026-11-13",
|
|
6
|
+
boat: "2026-11-14",
|
|
6
7
|
cloudflare: "2026-11-13",
|
|
7
8
|
daytona: "2026-11-13",
|
|
8
9
|
e2b: "2026-11-13",
|
|
@@ -12,8 +13,9 @@ export const VERIFIED = {
|
|
|
12
13
|
modal: "2026-11-13",
|
|
13
14
|
novita: "2026-11-13",
|
|
14
15
|
openscale: "2026-11-13",
|
|
15
|
-
sail: "2026-11-
|
|
16
|
+
sail: "2026-11-14",
|
|
16
17
|
superserve: "2026-11-13",
|
|
18
|
+
tensorlake: "2026-11-14",
|
|
17
19
|
vercel: "2026-11-13",
|
|
18
20
|
};
|
|
19
21
|
/** Providers whose last saved real-account check failed a step. The default strategy tries them last. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandboxrouter/sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "One API for every sandbox provider: route AI agents' code by price, speed or capability, with automatic fallback. Free and open source.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|