@sandboxrouter/sdk 0.6.0 → 0.7.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 CHANGED
@@ -44,8 +44,8 @@ List price while running, 2 vCPU / 4 GB, cheapest first; waitlisted sign-ups fol
44
44
  |---|---|---|---|---|
45
45
  | [Agent37](https://sandboxrouter.vercel.app/providers/agent37) | $0.007 | none | passed (2026-11-11) | [pricing](https://www.agent37.com/docs/agents-api/billing) |
46
46
  | [Boat](https://sandboxrouter.vercel.app/providers/boat) | $0.018 | $20/mo (credited) | not run yet | [pricing](https://docs.boat.dev/pricing.md) |
47
- | [Maritime](https://sandboxrouter.vercel.app/providers/maritime) | $0.022 | $20/mo (credited) | passed (2026-11-11) | [pricing](https://maritime.sh/pricing) |
48
- | [Sailboxes (Sail Research)](https://sandboxrouter.vercel.app/providers/sail) | up to $0.062 | none | passed (2026-11-11) | [pricing](https://docs.sailresearch.com/sailboxes-pricing.md) |
47
+ | [Maritime](https://sandboxrouter.vercel.app/providers/maritime) | $0.022 | $20/mo (credited) | passed (2026-11-13) | [pricing](https://maritime.sh/pricing) |
48
+ | [Sailboxes (Sail Research)](https://sandboxrouter.vercel.app/providers/sail) | up to $0.062 | none | passed (2026-11-13) | [pricing](https://docs.sailresearch.com/sailboxes-pricing.md) |
49
49
  | [Northflank](https://sandboxrouter.vercel.app/providers/northflank) | $0.067 | none | not run yet | [pricing](https://northflank.com/pricing) |
50
50
  | [Morph Cloud](https://sandboxrouter.vercel.app/providers/morph) | $0.100 | none | not run yet | [pricing](https://cloud.morph.so/web/pricing) |
51
51
  | [exe.dev](https://sandboxrouter.vercel.app/providers/exedev) | $0.105 | $15/mo | not run yet | [pricing](https://exe.dev/docs/standalone-vms.md) |
@@ -54,15 +54,15 @@ List price while running, 2 vCPU / 4 GB, cheapest first; waitlisted sign-ups fol
54
54
  | [Beam](https://sandboxrouter.vercel.app/providers/beam) | $0.140 | $20/mo (credited) | not run yet | [pricing](https://www.beam.cloud/pricing) |
55
55
  | [CodeSandbox SDK (Together)](https://sandboxrouter.vercel.app/providers/codesandbox) | $0.149 | none | not run yet | [pricing](https://docs.together.ai/docs/together-code-sandbox-legacy) |
56
56
  | [Fly.io Sprites](https://sandboxrouter.vercel.app/providers/sprites) | up to $0.164 | none | passed (2026-11-11) | [pricing](https://fly.io/sprites) |
57
- | [Blaxel](https://sandboxrouter.vercel.app/providers/blaxel) | $0.166 | none | passed (2026-11-11) | [pricing](https://blaxel.ai/pricing) |
57
+ | [Blaxel](https://sandboxrouter.vercel.app/providers/blaxel) | $0.166 | none | passed (2026-11-13) | [pricing](https://blaxel.ai/pricing) |
58
58
  | [HOPX](https://sandboxrouter.vercel.app/providers/hopx) | $0.166 | none | not run yet | [pricing](https://hopx.ai/pricing) |
59
59
  | [Superserve](https://sandboxrouter.vercel.app/providers/superserve) | $0.166 | maybe $20/mo (credited) | passed (2026-11-11) | [pricing](https://superserve.ai/pricing) |
60
60
  | [Tensorlake](https://sandboxrouter.vercel.app/providers/tensorlake) | up to $0.200 | none | not run yet | [pricing](https://www.tensorlake.ai/pricing) |
61
61
  | [Upstash Box](https://sandboxrouter.vercel.app/providers/upstash) | up to $0.200 | none | not run yet | [pricing](https://upstash.com/pricing/box) |
62
62
  | [Cloudflare Sandbox](https://sandboxrouter.vercel.app/providers/cloudflare) | up to $0.216 | $5/mo | passed (2026-11-11) | [pricing](https://developers.cloudflare.com/containers/platform/pricing/) |
63
- | [Amazon Bedrock AgentCore Code Interpreter](https://sandboxrouter.vercel.app/providers/agentcore) | up to $0.217 | none | passed (2026-11-11) | [pricing](https://aws.amazon.com/bedrock/agentcore/pricing/) |
63
+ | [Amazon Bedrock AgentCore Code Interpreter](https://sandboxrouter.vercel.app/providers/agentcore) | up to $0.217 | none | passed (2026-11-13) | [pricing](https://aws.amazon.com/bedrock/agentcore/pricing/) |
64
64
  | [Modal](https://sandboxrouter.vercel.app/providers/modal) | up to $0.238 | none | passed (2026-11-11) | [pricing](https://modal.com/pricing) |
65
- | [AWS Lambda MicroVMs](https://sandboxrouter.vercel.app/providers/lambda) | $0.252 | none | passed (2026-11-11) | [pricing](https://aws.amazon.com/lambda/pricing/) |
65
+ | [AWS Lambda MicroVMs](https://sandboxrouter.vercel.app/providers/lambda) | $0.252 | none | passed (2026-11-13) | [pricing](https://aws.amazon.com/lambda/pricing/) |
66
66
  | [Deno Sandbox](https://sandboxrouter.vercel.app/providers/deno) | up to $0.300 | $20/mo | not run yet | [pricing](https://deno.com/sandbox) |
67
67
  | [Runloop](https://sandboxrouter.vercel.app/providers/runloop) | $0.317 | none | not run yet | [pricing](https://www.runloop.ai/pricing) |
68
68
  | [Vercel Sandbox](https://sandboxrouter.vercel.app/providers/vercel) | up to $0.341 | none | passed (2026-11-11) | [pricing](https://vercel.com/docs/vercel-sandbox/pricing) |
@@ -70,12 +70,12 @@ List price while running, 2 vCPU / 4 GB, cheapest first; waitlisted sign-ups fol
70
70
  | [Koyeb Sandboxes (not taking new accounts)](https://sandboxrouter.vercel.app/providers/koyeb) | $0.058 | $29/mo | not run yet | [pricing](https://www.koyeb.com/docs/reference/instances) |
71
71
  | [Dedalus Machines (sign-up waitlisted)](https://sandboxrouter.vercel.app/providers/dedalus) | $0.149 | none | not run yet | [pricing](https://docs.dedaluslabs.ai/dcs/dm/pricing) |
72
72
  | [LangSmith Sandboxes (sign-up waitlisted)](https://sandboxrouter.vercel.app/providers/langsmith) | $0.189 | none | not run yet | [pricing](https://www.langchain.com/pricing) |
73
- | [OpenScale Sandboxes](https://sandboxrouter.vercel.app/providers/openscale) | skipped (default image is 2 vCPU / 1 GB; a bigger size needs image.openscale) | none | passed (2026-11-11) | [pricing](https://openscale.so/sandboxes) |
73
+ | [OpenScale Sandboxes](https://sandboxrouter.vercel.app/providers/openscale) | skipped (default image is 2 vCPU / 1 GB; a bigger size needs image.openscale) | none | passed (2026-11-13) | [pricing](https://openscale.so/sandboxes) |
74
74
  | [Azure Container Apps dynamic sessions](https://sandboxrouter.vercel.app/providers/azure) | skipped (max 1 vCPU) | none | not run yet | [pricing](https://learn.microsoft.com/en-us/azure/container-apps/billing#dynamic-sessions) |
75
- | [Daytona](https://sandboxrouter.vercel.app/providers/daytona) | skipped (default image is 1 vCPU / 1 GB; a bigger size needs image.daytona) | none | passed (2026-11-11) | [pricing](https://www.daytona.io/pricing) |
75
+ | [Daytona](https://sandboxrouter.vercel.app/providers/daytona) | skipped (default image is 1 vCPU / 1 GB; a bigger size needs image.daytona) | none | passed (2026-11-13) | [pricing](https://www.daytona.io/pricing) |
76
76
  | [Alibaba Cloud Agent Sandbox](https://sandboxrouter.vercel.app/providers/alibaba) | skipped (max 2 GB memory) | none | not run yet | [pricing](https://www.alibabacloud.com/help/en/agent-sandbox/product-overview/billing-overview) |
77
- | [E2B](https://sandboxrouter.vercel.app/providers/e2b) | skipped (default image is 2 vCPU / 0.5 GB; a bigger size needs image.e2b) | none | passed (2026-11-11) | [pricing](https://e2b.dev/pricing) |
78
- | [Docker (local)](https://sandboxrouter.vercel.app/providers/docker) | free | none | passed (2026-11-09) | [pricing](https://docs.docker.com/engine/) |
77
+ | [E2B](https://sandboxrouter.vercel.app/providers/e2b) | skipped (default image is 2 vCPU / 0.5 GB; a bigger size needs image.e2b) | none | passed (2026-11-13) | [pricing](https://e2b.dev/pricing) |
78
+ | [Docker (local)](https://sandboxrouter.vercel.app/providers/docker) | free | none | passed (2026-11-13) | [pricing](https://docs.docker.com/engine/) |
79
79
 
80
80
  Checked 2026-11-04 to 2026-11-06.
81
81
 
package/dist/cli.js CHANGED
@@ -18,10 +18,12 @@ Usage:
18
18
  sandboxrouter providers [--vcpu N] [--memory GB] price table + which keys are set
19
19
  sandboxrouter route [spec flags] [route flags] dry run: the order providers would be tried
20
20
  sandboxrouter run "<cmd>" [spec flags] [route flags] [--keep]
21
- sandboxrouter check --provider ID[,ID] | --all [--json] [--save [dir]] [--by GITHUB_USER]
21
+ sandboxrouter check --provider ID[,ID] | --all [--json] [--save [dir]] [--retry] [--out FILE] [--by GITHUB_USER]
22
22
  test providers for real: exec, env, UTF-8, streaming, files, timeout, pause,
23
23
  kill; exits 1 if any step fails. --all = every configured provider;
24
- --save writes results/conformance/<id>.json for the compatibility table
24
+ --save writes results/conformance/<id>.json for the compatibility table;
25
+ --retry runs a failed provider once more and keeps that result;
26
+ --out writes every report as JSON (with the first try's failure, if retried)
25
27
  sandboxrouter ls [--json] sandboxes this machine started that weren't killed
26
28
  sandboxrouter gc [--all | --older-than MIN] [--dry-run]
27
29
  kill leaked sandboxes: by default those past their lifetime
@@ -73,6 +75,7 @@ const { values, positionals } = parseArgs({
73
75
  label: { type: "string" },
74
76
  all: { type: "boolean" },
75
77
  save: { type: "boolean" },
78
+ retry: { type: "boolean" },
76
79
  "older-than": { type: "string" },
77
80
  "dry-run": { type: "boolean" },
78
81
  minutes: { type: "string" },
@@ -208,16 +211,23 @@ async function main() {
208
211
  const saveDir = values.save ? (rest[0] ?? "results/conformance") : undefined;
209
212
  const mark = { pass: "ok ", fail: "FAIL", skip: "skip", info: "note" };
210
213
  const reports = [];
214
+ const run = (p) => checkProvider(router, p, {
215
+ spec,
216
+ onStep: (r) => {
217
+ if (!values.json)
218
+ console.log(` ${mark[r.status]} ${r.step.padEnd(32)} ${String(r.ms).padStart(6)} ms ${r.detail ?? ""}`);
219
+ },
220
+ });
211
221
  for (const p of ids) {
212
222
  if (!values.json)
213
223
  console.log(`\n${p}`);
214
- const report = await checkProvider(router, p, {
215
- spec,
216
- onStep: (r) => {
217
- if (!values.json)
218
- console.log(` ${mark[r.status]} ${r.step.padEnd(32)} ${String(r.ms).padStart(6)} ms ${r.detail ?? ""}`);
219
- },
220
- });
224
+ let report = await run(p);
225
+ if (values.retry && !report.ok && !report.inconclusive) {
226
+ const failed = report.results.find((r) => r.status === "fail");
227
+ if (!values.json)
228
+ console.log(` failed; trying ${p} once more`);
229
+ report = { ...(await run(p)), firstTry: failed ? `${failed.step}: ${failed.detail ?? "failed"}` : "failed" };
230
+ }
221
231
  reports.push(report);
222
232
  if (report.inconclusive) {
223
233
  if (!values.json)
@@ -226,10 +236,13 @@ async function main() {
226
236
  }
227
237
  if (saveDir) {
228
238
  mkdirSync(saveDir, { recursive: true });
229
- const saved = publishableCheck({ tool: "sandboxrouter-check", version, date: new Date().toISOString(), platform: `${platform()}-${arch()}`, ...(by ? { by } : {}), ...report });
239
+ const { firstTry: _, ...result } = report;
240
+ const saved = publishableCheck({ tool: "sandboxrouter-check", version, date: new Date().toISOString(), platform: `${platform()}-${arch()}`, ...(by ? { by } : {}), ...result });
230
241
  writeFileSync(`${saveDir}/${p}.json`, JSON.stringify(saved, null, 2) + "\n");
231
242
  }
232
243
  }
244
+ if (values.out)
245
+ writeFileSync(values.out, JSON.stringify(reports, null, 2) + "\n");
233
246
  if (values.json)
234
247
  console.log(JSON.stringify(reports, null, 2));
235
248
  else {
package/dist/router.d.ts CHANGED
@@ -63,6 +63,18 @@ export interface RouterOptions {
63
63
  requireVerified?: boolean;
64
64
  /** Billed / list-price ratio per provider, behind `estimatedHourlyUsd`. Default: the ratios in `BILLED_VS_LIST`, from saved cost runs. */
65
65
  billedVsList?: Partial<Record<ProviderId, number>>;
66
+ /**
67
+ * When a create runs well past a provider's usual start time, try the next provider sooner than
68
+ * `createTimeoutSeconds`. The wait becomes `factor` x the provider's median start time (or 1.5x the
69
+ * slowest of its recent starts, if longer), at least `minSeconds`. It applies only when the caller
70
+ * didn't set `createTimeoutSeconds`, the router has a start time for the provider (3 of its own creates,
71
+ * or the bench file), and another provider is left to try. Default `{ factor: 5, minSeconds: 10 }`;
72
+ * `false` always waits the full `createTimeoutSeconds`.
73
+ */
74
+ quickFallback?: false | {
75
+ factor?: number;
76
+ minSeconds?: number;
77
+ };
66
78
  }
67
79
  /** Desktop-only providers. Filled in as adapters are added. */
68
80
  export declare function defaultComputerProviders(): ComputerProvider[];
@@ -77,6 +89,7 @@ export declare class Router {
77
89
  /** Create times this router measured, most recent last. */
78
90
  private readonly live;
79
91
  private computerOnly;
92
+ private readonly quick;
80
93
  /** The local record of sandboxes this router created, or undefined when turned off. */
81
94
  readonly ledger?: Ledger;
82
95
  constructor(opts?: RouterOptions);
@@ -94,6 +107,11 @@ export declare class Router {
94
107
  private whyNone;
95
108
  /** Start a sandbox on the best available provider, falling back down the list on failure. */
96
109
  create(spec?: SandboxSpec, route?: RouteOptions): Promise<RoutedSandbox>;
110
+ /**
111
+ * Seconds to wait for this provider's create: the caller's `createTimeoutSeconds`, or (see
112
+ * `quickFallback`) a few times its usual start time when another provider is left to try.
113
+ */
114
+ private waitFor;
97
115
  /** Remember a successful create's time, for the speed tie-break and strategy "fastest". */
98
116
  private observe;
99
117
  private record;
package/dist/router.js CHANGED
@@ -103,6 +103,7 @@ export class Router {
103
103
  /** Create times this router measured, most recent last. */
104
104
  live = new Map();
105
105
  computerOnly;
106
+ quick;
106
107
  /** The local record of sandboxes this router created, or undefined when turned off. */
107
108
  ledger;
108
109
  constructor(opts = {}) {
@@ -117,6 +118,7 @@ export class Router {
117
118
  this.billed =
118
119
  opts.billedVsList ?? Object.fromEntries(Object.entries(BILLED_VS_LIST).map(([id, b]) => [id, b.ratio]));
119
120
  this.computerOnly = opts.computers ?? (opts.providers ? [] : defaultComputerProviders());
121
+ this.quick = opts.quickFallback === false ? false : { factor: 5, minSeconds: 10, ...opts.quickFallback };
120
122
  }
121
123
  /** Registered and has credentials. False for providers this router doesn't know. */
122
124
  isConfigured(id) {
@@ -233,22 +235,23 @@ export class Router {
233
235
  }
234
236
  const attempts = [];
235
237
  const tries = route.fallback === false ? usable.slice(0, 1) : usable;
236
- const limitS = route.createTimeoutSeconds ?? DEFAULT_CREATE_TIMEOUT_S;
237
238
  const deadline = Date.now() + (route.deadlineSeconds ?? DEFAULT_CREATE_DEADLINE_S) * 1000;
238
- for (const c of tries) {
239
+ for (const [i, c] of tries.entries()) {
239
240
  const left = deadline - Date.now();
240
241
  if (left <= 0) {
241
242
  throw new RouterError(`create ran out of time (deadlineSeconds) after trying ${attempts.map((a) => a.provider).join(", ")}: ${attempts.map((a) => `${a.provider}: ${a.error}`).join(" | ")}`, attempts, "timeout");
242
243
  }
243
244
  const t0 = Date.now();
244
245
  const pending = this.provider(c.provider).create(spec);
246
+ const { limitS, typicalMs } = this.waitFor(c.provider, route, i === tries.length - 1);
245
247
  const waitS = Math.min(limitS, left / 1000);
246
248
  const price = { estimatedHourlyUsd: c.estimatedHourlyUsd, listHourlyUsd: c.listHourlyUsd, size: c.size };
247
249
  let timedOut = false;
248
250
  try {
249
251
  const box = await withDeadline(pending, waitS * 1000, () => {
250
252
  timedOut = true;
251
- return new ProviderError(c.provider, `create timed out after ${Number(waitS.toFixed(1))}s`);
253
+ const usual = typicalMs !== undefined && waitS < (route.createTimeoutSeconds ?? DEFAULT_CREATE_TIMEOUT_S) ? `; it usually starts in ${Number((typicalMs / 1000).toFixed(1))}s` : "";
254
+ return new ProviderError(c.provider, `create timed out after ${Number(waitS.toFixed(1))}s${usual}`);
252
255
  });
253
256
  attempts.push({ provider: c.provider, ok: true, ms: Date.now() - t0 });
254
257
  this.observe(c.provider, Date.now() - t0);
@@ -273,6 +276,22 @@ export class Router {
273
276
  }
274
277
  throw new RouterError(`all providers failed: ${attempts.map((a) => `${a.provider}: ${a.error}`).join(" | ")}`, attempts, "all_failed");
275
278
  }
279
+ /**
280
+ * Seconds to wait for this provider's create: the caller's `createTimeoutSeconds`, or (see
281
+ * `quickFallback`) a few times its usual start time when another provider is left to try.
282
+ */
283
+ waitFor(id, route, last) {
284
+ const full = route.createTimeoutSeconds ?? DEFAULT_CREATE_TIMEOUT_S;
285
+ if (!this.quick || last || route.createTimeoutSeconds !== undefined)
286
+ return { limitS: full };
287
+ const live = this.live.get(id);
288
+ const samples = live && live.length >= LIVE_MIN_SAMPLES ? live : loadBench(this.benchFile)[id]?.createMs;
289
+ const typicalMs = median(samples);
290
+ if (typicalMs === undefined || !samples?.length)
291
+ return { limitS: full };
292
+ const s = Math.max(this.quick.factor * typicalMs, 1.5 * Math.max(...samples)) / 1000;
293
+ return { limitS: Math.min(full, Math.max(this.quick.minSeconds, s)), typicalMs };
294
+ }
276
295
  /** Remember a successful create's time, for the speed tie-break and strategy "fastest". */
277
296
  observe(id, ms) {
278
297
  const xs = this.live.get(id) ?? [];
package/dist/verified.js CHANGED
@@ -1,18 +1,18 @@
1
1
  export const VERIFIED = {
2
2
  agent37: "2026-11-11",
3
- agentcore: "2026-11-11",
4
- lambda: "2026-11-11",
5
- blaxel: "2026-11-11",
3
+ agentcore: "2026-11-13",
4
+ lambda: "2026-11-13",
5
+ blaxel: "2026-11-13",
6
6
  cloudflare: "2026-11-11",
7
- daytona: "2026-11-11",
8
- e2b: "2026-11-11",
7
+ daytona: "2026-11-13",
8
+ e2b: "2026-11-13",
9
9
  sprites: "2026-11-11",
10
10
  freestyle: "2026-11-11",
11
- maritime: "2026-11-11",
11
+ maritime: "2026-11-13",
12
12
  modal: "2026-11-11",
13
13
  novita: "2026-11-11",
14
- openscale: "2026-11-11",
15
- sail: "2026-11-11",
14
+ openscale: "2026-11-13",
15
+ sail: "2026-11-13",
16
16
  superserve: "2026-11-11",
17
17
  vercel: "2026-11-11",
18
18
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandboxrouter/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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",
@@ -175,7 +175,7 @@
175
175
  "@aws-sdk/client-appstream": "^3.1162.0",
176
176
  "@aws-sdk/credential-provider-node": "^3.972.93",
177
177
  "@blaxel/core": "^0.3.34",
178
- "@daytonaio/sdk": "^0.227.3",
178
+ "@daytonaio/sdk": "^0.230.0",
179
179
  "@e2b/desktop": "^2.5.0",
180
180
  "@fly/sprites": "^0.2.4",
181
181
  "@hopx-ai/sdk": "^0.5.1",