@sandboxrouter/sdk 0.3.2 → 0.3.3
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 +19 -18
- package/dist/catalog.d.ts +13 -0
- package/dist/catalog.js +14 -0
- package/dist/cli.js +11 -4
- package/dist/cost.d.ts +7 -0
- package/dist/cost.js +13 -1
- package/dist/leaderboard.js +3 -2
- package/dist/results.d.ts +12 -0
- package/dist/results.js +48 -3
- package/dist/router.js +7 -3
- package/dist/types.d.ts +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,8 +66,8 @@ How to read it:
|
|
|
66
66
|
|---|---|---|---|---|---|
|
|
67
67
|
| Agent37 | $0.007 | none | not run yet | Whole-minute billing: monthly shape total / 730 / 60; running estimate includes default disk (2 vCPU / 4 GB / 4 GB disk = $4.76/mo); sleeping/stopped disk $0.09/GB-mo; gVisor; $5 minimum top-up, wallet must hold one running day | [pricing](https://www.agent37.com/docs/agents-api/billing) |
|
|
68
68
|
| Boat | $0.018 | $20/mo (credited) | not run yet | 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) |
|
|
69
|
-
| Maritime | $0.022 | $20/mo (credited) | passed (2026-11-
|
|
70
|
-
| Sailboxes (Sail Research) | up to $0.062 | none | passed (2026-11-
|
|
69
|
+
| Maritime | $0.022 | $20/mo (credited) | passed (2026-11-07) | Per second, no minimum runtime; running disk included; Starter $20/mo incl. $20 usage; paused disk $0.08/GB-month (default idle action deletes sandbox) | [pricing](https://maritime.sh/pricing) |
|
|
70
|
+
| Sailboxes (Sail Research) | up to $0.062 | none | passed (2026-11-07) | Per second on what the box uses (CPU time, RAM it holds, disk $0.0007/GiB-h), billed per minute; RAM counts the VM's file cache, up to the 16 GiB minimum limit, so file-heavy work can hold more than you asked for; paused/sleeping free; creation $0.01 at 2-4 vCPU on the free plan ($5/mo credit with a card), waived on Pro $250/mo | [pricing](https://docs.sailresearch.com/sailboxes-pricing.md) |
|
|
71
71
|
| Northflank | $0.067 | none | not run yet | Per second on Northflank Cloud, ephemeral disk included; sandboxes run as deployment services (microVMs) | [pricing](https://northflank.com/pricing) |
|
|
72
72
|
| Morph Cloud | $0.100 | none | not run yet | $0.05/MCU; MCU-hours = max(vCPU, RAM/4 GB, disk/16 GB); snapshots extra | [pricing](https://cloud.morph.so/web/pricing) |
|
|
73
73
|
| exe.dev | $0.105 | $15/mo | not run yet | Standalone reserved size, per second: $0.105/h per 2 vCPU / 4 GiB unit; stopped CPU/RAM free, retained disk separate; Personal from $15/mo (up to 16 vCPU / 32 GiB), Work $75/mo (32 / 64 without approval) | [pricing](https://exe.dev/docs/standalone-vms.md) |
|
|
@@ -76,10 +76,9 @@ How to read it:
|
|
|
76
76
|
| Beam | $0.140 | $20/mo (credited) | not run yet | Per second, Performance tier (sandbox default); Developer plan $20/mo incl. $20 credits; GPUs billed separately | [pricing](https://www.beam.cloud/pricing) |
|
|
77
77
|
| CodeSandbox SDK (Together) | $0.149 | none | not run yet | Fixed tiers: Pico 2 vCPU / 1 GB $0.0743/h, Nano 2 / 4 $0.1486/h, up to 64 / 128 $4.7552/h; credits $0.01486, billed per started minute; Build free, 10 concurrent; Scale $170/mo incl. 1,100 credits | [pricing](https://docs.together.ai/docs/together-code-sandbox-legacy) |
|
|
78
78
|
| Fly.io Sprites | up to $0.164 | none | not run yet | Bills actual CPU and memory use; this is the full-use price; storage extra | [pricing](https://fly.io/sprites) |
|
|
79
|
-
| Blaxel | $0.166 | none | passed (2026-11-
|
|
79
|
+
| Blaxel | $0.166 | none | passed (2026-11-07) | Billed on memory only ($0.0000115/GB-s), CPU scales with memory (~2 GB per core) | [pricing](https://blaxel.ai/pricing) |
|
|
80
80
|
| HOPX | $0.166 | none | not run yet | Per second, plus storage; size comes from the template, not the request | [pricing](https://hopx.ai/pricing) |
|
|
81
81
|
| Superserve | $0.166 | maybe $20/mo (credited) | not run yet | Per second active; superserve/base fixed at 2 vCPU / 4 GiB / 8 GiB disk; retained storage $0.000108/GiB-h (shared template plus changed blocks); Developer $20/mo incl. $20, max 4/16 with team templates; recurring Free allowance not published on pricing page | [pricing](https://superserve.ai/pricing) |
|
|
82
|
-
| LangSmith Sandboxes | $0.189 | none | not run yet | Per second; memory must be 2 to 6 GiB per vCPU; storage $0.000123/GiB-h; 8 LSU ($8) of sandbox usage a month included; Developer plan max 10 sandboxes | [pricing](https://www.langchain.com/pricing) |
|
|
83
82
|
| Tensorlake | up to $0.200 | none | not run yet | Bills active CPU (floor 20% of allocation); this is the full-use price; Pro plan is lower | [pricing](https://www.tensorlake.ai/pricing) |
|
|
84
83
|
| Upstash Box | up to $0.200 | none | not run yet | Billed per active CPU core-hour only (memory and idle time free); estimate assumes every core of the box busy; fixed sizes 2/4, 4/8, 8/16 | [pricing](https://upstash.com/pricing/box) |
|
|
85
84
|
| Cloudflare Sandbox | up to $0.216 | $5/mo | not run yet | 10 ms billing on active CPU and allocated instance memory; full-CPU estimate excludes allocated disk ($0.000252/GB-h), Worker and Durable Objects charges; needs Workers Paid ($5/mo) and bridges/cloudflare | [pricing](https://developers.cloudflare.com/containers/platform/pricing/) |
|
|
@@ -92,12 +91,13 @@ How to read it:
|
|
|
92
91
|
| InstaBox (sign-up waitlisted) | $0.017 | none | not run yet | Per second while running on fixed sizes (2/4, 4/8, 8/16); paused or stopped boxes bill only /data beyond the included disk ($0.00011/GiB-h); Pro $20/mo includes $20 credits; free plan: 1 box at 4/8 | [pricing](https://instabox.sh/docs/pricing.md) |
|
|
93
92
|
| Koyeb Sandboxes (not taking new accounts) | $0.058 | $29/mo | not run yet | Per second; fixed Standard instance sizes (1 vCPU + 1 GB = $0.0144/h); sandboxes need a paid plan (Pro $29/mo); public preview | [pricing](https://www.koyeb.com/docs/reference/instances) |
|
|
94
93
|
| Dedalus Machines (sign-up waitlisted) | $0.149 | none | not run yet | Per second while awake; sleeping machines bill storage only (10 GiB included on Hobby, 20 on Pro); Hobby plan max 4 vCPU / 16 GiB; private beta | [pricing](https://docs.dedaluslabs.ai/dcs/dm/pricing) |
|
|
95
|
-
|
|
|
94
|
+
| LangSmith Sandboxes (sign-up waitlisted) | $0.189 | none | not run yet | Per second; memory must be 2 to 6 GiB per vCPU; storage $0.000123/GiB-h; 8 LSU ($8) of sandbox usage a month included; Developer plan max 10 sandboxes | [pricing](https://www.langchain.com/pricing) |
|
|
95
|
+
| OpenScale Sandboxes | skipped (default image is 2 vCPU / 1 GB; a bigger size needs image.openscale) | none | passed (2026-11-07) | Per second on allocated size; no platform fee or minimum; paused = free; size comes from the template (E2B-compatible API); Singapore only; needs a pay-as-you-go key | [pricing](https://openscale.so/sandboxes) |
|
|
96
96
|
| Azure Container Apps dynamic sessions | skipped (max 1 vCPU) | none | not run yet | Flat $0.03 per session-hour (built-in Shell pools), billed per allocated session in 1-hour increments, East US; needs a session pool you create once | [pricing](https://learn.microsoft.com/en-us/azure/container-apps/billing#dynamic-sessions) |
|
|
97
|
-
| Daytona | skipped (default image is 1 vCPU / 1 GB; a bigger size needs image.daytona) | none |
|
|
97
|
+
| Daytona | skipped (default image is 1 vCPU / 1 GB; a bigger size needs image.daytona) | none | passed (2026-11-07) | Per second, plus disk above 5 GiB | [pricing](https://www.daytona.io/pricing) |
|
|
98
98
|
| Alibaba Cloud Agent Sandbox | skipped (max 2 GB memory) | none | not run yet | Per second; Default QoS, Hong Kong and regions outside mainland China; disk separately $0.00025308/GiB-h; code-interpreter-v1 template is 2 vCPU / 2 GiB with 10 GiB disk (E2B-compatible API) | [pricing](https://www.alibabacloud.com/help/en/agent-sandbox/product-overview/billing-overview) |
|
|
99
|
-
| E2B | skipped (default image is 2 vCPU / 0.5 GB; a bigger size needs image.e2b) | none | passed (2026-11-
|
|
100
|
-
| Docker (local) | free | none | passed (2026-11-
|
|
99
|
+
| E2B | skipped (default image is 2 vCPU / 0.5 GB; a bigger size needs image.e2b) | none | passed (2026-11-07) | Per second; free Hobby tier ($100 one-time credit, 1-hour sessions, 20 at once); Pro $150/mo for longer sessions | [pricing](https://e2b.dev/pricing) |
|
|
100
|
+
| Docker (local) | free | none | passed (2026-11-07) | Runs on your own machine; free, but shares your kernel, so weaker isolation than a cloud micro-VM | [pricing](https://docs.docker.com/engine/) |
|
|
101
101
|
|
|
102
102
|
Checked 2026-11-04 to 2026-11-06.
|
|
103
103
|
|
|
@@ -185,8 +185,9 @@ const c = await router.create({}, { provider: ["e2b", "daytona"], fallback: fals
|
|
|
185
185
|
|
|
186
186
|
// Fallback moves on when a provider is down: a timeout, a network error, 429 or 5xx, and a create
|
|
187
187
|
// that takes longer than createTimeoutSeconds (default 120; a sandbox that turns up later is
|
|
188
|
-
// killed, or left in the ledger for `gc`)
|
|
189
|
-
//
|
|
188
|
+
// killed, or left in the ledger for `gc`), and when an account can't pay (402: no balance, plan
|
|
189
|
+
// needed). A setup error (400, 401, 403, 404, 422: a bad key, a bad image or request) stops instead,
|
|
190
|
+
// so a mistake isn't hidden by a pricier provider. Every failed attempt is on sbx.attempts.
|
|
190
191
|
// fallback: "any" falls back on every error.
|
|
191
192
|
const e = await router.create({}, { fallback: "any", createTimeoutSeconds: 300 });
|
|
192
193
|
|
|
@@ -306,8 +307,8 @@ It prints one line per step and exits 1 if any fails. `checkProvider(router, id)
|
|
|
306
307
|
|---|---|---|
|
|
307
308
|
| Agent37 | not run yet | |
|
|
308
309
|
| Boat | not run yet | |
|
|
309
|
-
| Maritime | 14/14 passed | 2026-11-
|
|
310
|
-
| Sailboxes (Sail Research) | 14/14 passed | 2026-11-
|
|
310
|
+
| Maritime | 14/14 passed | 2026-11-07, v0.3.2 |
|
|
311
|
+
| Sailboxes (Sail Research) | 14/14 passed | 2026-11-07, v0.3.2 |
|
|
311
312
|
| Northflank | not run yet | |
|
|
312
313
|
| Morph Cloud | not run yet | |
|
|
313
314
|
| exe.dev | not run yet | |
|
|
@@ -316,10 +317,9 @@ It prints one line per step and exits 1 if any fails. `checkProvider(router, id)
|
|
|
316
317
|
| Beam | not run yet | |
|
|
317
318
|
| CodeSandbox SDK (Together) | not run yet | |
|
|
318
319
|
| Fly.io Sprites | not run yet | |
|
|
319
|
-
| Blaxel | 13/13 passed; n/a: pause and resume | 2026-11-
|
|
320
|
+
| Blaxel | 13/13 passed; n/a: pause and resume | 2026-11-07, v0.3.2 |
|
|
320
321
|
| HOPX | not run yet | |
|
|
321
322
|
| Superserve | not run yet | |
|
|
322
|
-
| LangSmith Sandboxes | not run yet | |
|
|
323
323
|
| Tensorlake | not run yet | |
|
|
324
324
|
| Upstash Box | not run yet | |
|
|
325
325
|
| Cloudflare Sandbox | not run yet | |
|
|
@@ -332,12 +332,13 @@ It prints one line per step and exits 1 if any fails. `checkProvider(router, id)
|
|
|
332
332
|
| InstaBox | not run yet | |
|
|
333
333
|
| Koyeb Sandboxes | not run yet | |
|
|
334
334
|
| Dedalus Machines | not run yet | |
|
|
335
|
-
|
|
|
335
|
+
| LangSmith Sandboxes | not run yet | |
|
|
336
|
+
| OpenScale Sandboxes | 14/14 passed | 2026-11-07, v0.3.2 |
|
|
336
337
|
| Azure Container Apps dynamic sessions | not run yet | |
|
|
337
|
-
| Daytona |
|
|
338
|
+
| Daytona | 14/14 passed | 2026-11-07, v0.3.2 |
|
|
338
339
|
| Alibaba Cloud Agent Sandbox | not run yet | |
|
|
339
|
-
| E2B | 14/14 passed | 2026-11-
|
|
340
|
-
| Docker (local) | 14/14 passed | 2026-11-
|
|
340
|
+
| E2B | 14/14 passed | 2026-11-07, v0.3.2 |
|
|
341
|
+
| Docker (local) | 14/14 passed | 2026-11-07, v0.3.2 |
|
|
341
342
|
|
|
342
343
|
## HTTP API
|
|
343
344
|
|
package/dist/catalog.d.ts
CHANGED
|
@@ -58,7 +58,20 @@ export interface ProviderInfo {
|
|
|
58
58
|
note: string;
|
|
59
59
|
checked: string;
|
|
60
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Set when the provider's terms forbid publishing benchmarks or performance information about it.
|
|
63
|
+
* Its cold starts, success rate, measured size and real bills are then left out of shared reports,
|
|
64
|
+
* results/ and the website, and saved checks keep pass/fail without timings. List prices (public
|
|
65
|
+
* pricing pages) and the adapter itself are unaffected.
|
|
66
|
+
*/
|
|
67
|
+
noBenchmarks?: {
|
|
68
|
+
clause: string;
|
|
69
|
+
source: string;
|
|
70
|
+
checked: string;
|
|
71
|
+
};
|
|
61
72
|
}
|
|
73
|
+
/** False when the provider's terms forbid publishing its benchmarks (see `noBenchmarks`). */
|
|
74
|
+
export declare function benchmarksPublishable(id: string): boolean;
|
|
62
75
|
/** Short label for a gated provider's sign-up status, or null when anyone can sign up. */
|
|
63
76
|
export declare function accessLabel(id: ProviderId): string | null;
|
|
64
77
|
/** Short label for the plan a provider needs before you can use it: "none", "$20/mo (credited)", "$5/mo". */
|
package/dist/catalog.js
CHANGED
|
@@ -3,6 +3,10 @@ import { AGENT37_SHAPES, agent37Shape } from "./providers/agent37.js";
|
|
|
3
3
|
import { INSTABOX_SIZES, instaboxSize } from "./providers/instabox.js";
|
|
4
4
|
import { FREESTYLE_SIZES, freestyleSize } from "./providers/freestyle.js";
|
|
5
5
|
import { langsmithSize } from "./providers/langsmith.js";
|
|
6
|
+
/** False when the provider's terms forbid publishing its benchmarks (see `noBenchmarks`). */
|
|
7
|
+
export function benchmarksPublishable(id) {
|
|
8
|
+
return !CATALOG.find((c) => c.id === id)?.noBenchmarks;
|
|
9
|
+
}
|
|
6
10
|
/** Short label for a gated provider's sign-up status, or null when anyone can sign up. */
|
|
7
11
|
export function accessLabel(id) {
|
|
8
12
|
const a = CATALOG.find((c) => c.id === id)?.access;
|
|
@@ -172,6 +176,11 @@ const ENTRIES = [
|
|
|
172
176
|
limits: { maxVcpu: 8, maxMemoryGb: 16 },
|
|
173
177
|
credentials: [["BL_API_KEY", "BL_WORKSPACE"]],
|
|
174
178
|
docs: "https://docs.blaxel.ai/Sandboxes/Overview",
|
|
179
|
+
noBenchmarks: {
|
|
180
|
+
clause: "Blaxel's terms (§2.6(f)) don't allow publishing benchmarks or performance information about it",
|
|
181
|
+
source: "https://blaxel.ai/legal/terms/2025-12-10-blaxel-terms-and-conditions.pdf",
|
|
182
|
+
checked: "2026-11-07",
|
|
183
|
+
},
|
|
175
184
|
},
|
|
176
185
|
{
|
|
177
186
|
id: "tensorlake",
|
|
@@ -524,6 +533,11 @@ ENTRIES.push({
|
|
|
524
533
|
limits: { maxVcpu: 16, maxMemoryGb: 96 },
|
|
525
534
|
credentials: [["LANGSMITH_API_KEY"]],
|
|
526
535
|
docs: "https://docs.langchain.com/langsmith/sandboxes",
|
|
536
|
+
access: {
|
|
537
|
+
status: "waitlist",
|
|
538
|
+
note: "Sandboxes are a private preview: until LangChain enables them for your organization, the API answers \"Sandbox feature is not enabled for this organization\"",
|
|
539
|
+
checked: "2026-11-07",
|
|
540
|
+
},
|
|
527
541
|
}, {
|
|
528
542
|
id: "novita",
|
|
529
543
|
name: "Novita AI Agent Sandbox",
|
package/dist/cli.js
CHANGED
|
@@ -6,9 +6,9 @@ import { bench, benchMarkdown } from "./bench.js";
|
|
|
6
6
|
import { costMarkdown, costRun, settle } from "./cost.js";
|
|
7
7
|
import { checkProvider } from "./check.js";
|
|
8
8
|
import { gc, listOpen } from "./gc.js";
|
|
9
|
-
import { contributor } from "./results.js";
|
|
9
|
+
import { contributor, publishableCheck } from "./results.js";
|
|
10
10
|
import { loadReports, makeReport, renderLeaderboard } from "./leaderboard.js";
|
|
11
|
-
import { DEFAULT_MEMORY_GB, DEFAULT_VCPU, accessLabel, hourlyCost, listingOrder, planLabel, unfit } from "./catalog.js";
|
|
11
|
+
import { DEFAULT_MEMORY_GB, DEFAULT_VCPU, accessLabel, benchmarksPublishable, hourlyCost, listingOrder, planLabel, unfit } from "./catalog.js";
|
|
12
12
|
import { Router } from "./router.js";
|
|
13
13
|
import { createApp } from "./server.js";
|
|
14
14
|
import { RouterError } from "./types.js";
|
|
@@ -215,7 +215,7 @@ async function main() {
|
|
|
215
215
|
reports.push(report);
|
|
216
216
|
if (saveDir) {
|
|
217
217
|
mkdirSync(saveDir, { recursive: true });
|
|
218
|
-
const saved = { tool: "sandboxrouter-check", version, date: new Date().toISOString(), platform: `${platform()}-${arch()}`, ...(by ? { by } : {}), ...report };
|
|
218
|
+
const saved = publishableCheck({ tool: "sandboxrouter-check", version, date: new Date().toISOString(), platform: `${platform()}-${arch()}`, ...(by ? { by } : {}), ...report });
|
|
219
219
|
writeFileSync(`${saveDir}/${p}.json`, JSON.stringify(saved, null, 2) + "\n");
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -277,6 +277,9 @@ async function main() {
|
|
|
277
277
|
if (values.share) {
|
|
278
278
|
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
279
279
|
const report = makeReport(rows, { version, runs: rows[0].runs, spec, label: values.label, by });
|
|
280
|
+
const left = rows.filter((r) => !benchmarksPublishable(r.provider)).map((r) => r.provider);
|
|
281
|
+
if (left.length)
|
|
282
|
+
console.log(`\nleft out of the shared report: ${left.join(", ")} (their terms don't allow publishing benchmarks)`);
|
|
280
283
|
const file = values.out ?? `sandboxrouter-bench-${report.date.slice(0, 10)}-${Math.random().toString(36).slice(2, 8)}.json`;
|
|
281
284
|
writeFileSync(file, JSON.stringify(report, null, 2) + "\n");
|
|
282
285
|
console.log(`\nwrote ${file}. Add it to results/ in a PR to github.com/gigialc/sandboxrouter to put it on the leaderboard.`);
|
|
@@ -301,7 +304,11 @@ async function main() {
|
|
|
301
304
|
});
|
|
302
305
|
const file = values.out ?? `results/cost/cost-${run.date.slice(0, 16).replace(/[:T]/g, "-")}.json`;
|
|
303
306
|
mkdirSync(file.replace(/\/[^/]*$/, "") || ".", { recursive: true });
|
|
304
|
-
|
|
307
|
+
const settled = await settle(run);
|
|
308
|
+
const left = settled.entries.filter((e) => !benchmarksPublishable(e.provider)).map((e) => e.provider);
|
|
309
|
+
writeFileSync(file, JSON.stringify({ ...settled, entries: settled.entries.filter((e) => benchmarksPublishable(e.provider)) }, null, 2) + "\n");
|
|
310
|
+
if (left.length)
|
|
311
|
+
console.log(`left out of ${file}: ${left.join(", ")} (their terms don't allow publishing benchmarks; the table above is the only copy)`);
|
|
305
312
|
console.log(costMarkdown(run));
|
|
306
313
|
console.log(`\nwrote ${file}. Bills post late on some providers: run "sandboxrouter cost settle ${file}" again later.`);
|
|
307
314
|
return;
|
package/dist/cost.d.ts
CHANGED
|
@@ -52,6 +52,13 @@ export interface CostRun {
|
|
|
52
52
|
by?: string;
|
|
53
53
|
entries: CostEntry[];
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The size the sandbox really has, as "<vcpus> <memoryMb>". nproc and MemTotal can show the whole host
|
|
57
|
+
* (Daytona's containers report 64 CPUs and 755 GB while limited to 1 vCPU and 1 GB), so the cgroup limits
|
|
58
|
+
* win when they're set: cgroup v2 cpu.max / memory.max, or v1 cfs quota / memory limit. vCPUs can be
|
|
59
|
+
* fractional (a 50% quota is 0.5).
|
|
60
|
+
*/
|
|
61
|
+
export declare const PROBE: string;
|
|
55
62
|
export declare function costRun(opts: {
|
|
56
63
|
providers: ProviderId[];
|
|
57
64
|
minutes: number;
|
package/dist/cost.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { fittingSpec, hourlyCost } from "./catalog.js";
|
|
2
2
|
import { Router } from "./router.js";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The size the sandbox really has, as "<vcpus> <memoryMb>". nproc and MemTotal can show the whole host
|
|
5
|
+
* (Daytona's containers report 64 CPUs and 755 GB while limited to 1 vCPU and 1 GB), so the cgroup limits
|
|
6
|
+
* win when they're set: cgroup v2 cpu.max / memory.max, or v1 cfs quota / memory limit. vCPUs can be
|
|
7
|
+
* fractional (a 50% quota is 0.5).
|
|
8
|
+
*/
|
|
9
|
+
export const PROBE = [
|
|
10
|
+
`n=$(nproc); m=$(awk '/MemTotal/{printf "%d", $2/1024}' /proc/meminfo)`,
|
|
11
|
+
`q=; p=; l=`,
|
|
12
|
+
`if [ -r /sys/fs/cgroup/cpu.max ]; then read q p < /sys/fs/cgroup/cpu.max; elif [ -r /sys/fs/cgroup/cpu/cpu.cfs_quota_us ]; then q=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us); p=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us); fi`,
|
|
13
|
+
`if [ -r /sys/fs/cgroup/memory.max ]; then l=$(cat /sys/fs/cgroup/memory.max); elif [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then l=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes); fi`,
|
|
14
|
+
`awk -v n="$n" -v m="$m" -v q="$q" -v p="$p" -v l="$l" 'BEGIN { c = n; if (q ~ /^[0-9]+$/ && p ~ /^[0-9]+$/ && p > 0 && q / p < c) c = q / p; if (l ~ /^[0-9]+$/ && l / 1048576 < m) m = int(l / 1048576); printf "%s %d\\n", (c == int(c) ? c : sprintf("%.2f", c)), m }'`,
|
|
15
|
+
].join("; ");
|
|
4
16
|
// One `yes` per core, detached so the exec returns; they die with the sandbox.
|
|
5
17
|
const LOAD = `nohup sh -c 'for i in $(seq $(nproc)); do yes > /dev/null & done; wait' > /dev/null 2>&1 &`;
|
|
6
18
|
const round = (n, d = 6) => Math.round(n * 10 ** d) / 10 ** d;
|
package/dist/leaderboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { CATALOG, fittingSpec, hourlyCost } from "./catalog.js";
|
|
3
|
+
import { benchmarksPublishable, CATALOG, fittingSpec, hourlyCost } from "./catalog.js";
|
|
4
4
|
import { median } from "./bench-store.js";
|
|
5
5
|
export function makeReport(rows, opts) {
|
|
6
6
|
return {
|
|
@@ -14,7 +14,8 @@ export function makeReport(rows, opts) {
|
|
|
14
14
|
spec: { vcpu: opts.spec.vcpu, memoryGb: opts.spec.memoryGb },
|
|
15
15
|
node: process.version,
|
|
16
16
|
platform: `${process.platform}-${process.arch}`,
|
|
17
|
-
|
|
17
|
+
// Providers whose terms forbid publishing benchmarks stay out of anything meant to be shared.
|
|
18
|
+
rows: rows.filter((r) => benchmarksPublishable(r.provider)).map(({ lastError: _drop, ...r }) => r),
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
export function loadReports(dir) {
|
package/dist/results.d.ts
CHANGED
|
@@ -14,6 +14,17 @@ export interface Issue {
|
|
|
14
14
|
}
|
|
15
15
|
/** A GitHub handle, without the "@": what `--by` accepts and what reports store. */
|
|
16
16
|
export declare function contributor(by?: string): string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* A saved check as it may be published: for a provider whose terms forbid publishing benchmarks,
|
|
19
|
+
* pass/fail stays and every timing goes (the per-step `ms`, and notes that quote a duration).
|
|
20
|
+
*/
|
|
21
|
+
export declare function publishableCheck<T extends {
|
|
22
|
+
provider: string;
|
|
23
|
+
results: {
|
|
24
|
+
ms?: number;
|
|
25
|
+
detail?: string;
|
|
26
|
+
}[];
|
|
27
|
+
}>(saved: T): T;
|
|
17
28
|
/** Problems with one parsed result file. `now` is injectable for tests. */
|
|
18
29
|
export declare function validateResult(j: any, now?: number): string[];
|
|
19
30
|
/** Problems found anywhere in a file's raw text: secrets, emails, IP addresses. */
|
|
@@ -29,5 +40,6 @@ export declare function validateResults(dir?: string, opts?: {
|
|
|
29
40
|
status: string;
|
|
30
41
|
path: string;
|
|
31
42
|
}[];
|
|
43
|
+
previous?: (path: string) => string | undefined;
|
|
32
44
|
now?: number;
|
|
33
45
|
}): Issue[];
|
package/dist/results.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
|
-
import { CATALOG } from "./catalog.js";
|
|
3
|
+
import { benchmarksPublishable, CATALOG } from "./catalog.js";
|
|
4
4
|
import { CHECK_STEPS } from "./check.js";
|
|
5
5
|
/** A GitHub handle, without the "@": what `--by` accepts and what reports store. */
|
|
6
6
|
export function contributor(by) {
|
|
@@ -13,6 +13,22 @@ export function contributor(by) {
|
|
|
13
13
|
return h;
|
|
14
14
|
}
|
|
15
15
|
const IDS = new Set(CATALOG.map((c) => c.id));
|
|
16
|
+
/** A timing in a check step's note, e.g. "first output after 100 ms" or "threw after 3.0 s". */
|
|
17
|
+
const TIMING = /\d[\d,.]*\s?(?:ms|s|sec|seconds?)\b/i;
|
|
18
|
+
/**
|
|
19
|
+
* A saved check as it may be published: for a provider whose terms forbid publishing benchmarks,
|
|
20
|
+
* pass/fail stays and every timing goes (the per-step `ms`, and notes that quote a duration).
|
|
21
|
+
*/
|
|
22
|
+
export function publishableCheck(saved) {
|
|
23
|
+
if (benchmarksPublishable(saved.provider))
|
|
24
|
+
return saved;
|
|
25
|
+
return {
|
|
26
|
+
...saved,
|
|
27
|
+
results: saved.results.map(({ ms: _ms, detail, ...r }) => ({ ...r, ...(detail && !TIMING.test(detail) ? { detail } : {}) })),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const noBench = (id) => typeof id === "string" && IDS.has(id) && !benchmarksPublishable(id);
|
|
31
|
+
const NO_BENCH = "its terms don't allow publishing benchmarks (noBenchmarks in src/catalog.ts); leave it out of shared results";
|
|
16
32
|
const STEPS = new Set(CHECK_STEPS);
|
|
17
33
|
const MAX_MS = 15 * 60_000;
|
|
18
34
|
/** Key and token shapes from the providers here and the usual suspects. Matches print as a prefix only. */
|
|
@@ -88,6 +104,8 @@ export function validateResult(j, now = Date.now()) {
|
|
|
88
104
|
out.push("rows is empty");
|
|
89
105
|
for (const r of Array.isArray(j.rows) ? j.rows : []) {
|
|
90
106
|
provider(r?.provider, "row provider");
|
|
107
|
+
if (noBench(r?.provider))
|
|
108
|
+
out.push(`${r.provider}: ${NO_BENCH}`);
|
|
91
109
|
if (!isNum(r?.runs, 1, 1000))
|
|
92
110
|
out.push(`${r?.provider}: runs is not a positive number`);
|
|
93
111
|
if (!isNum(r?.failures, 0, r?.runs ?? 0))
|
|
@@ -113,8 +131,11 @@ export function validateResult(j, now = Date.now()) {
|
|
|
113
131
|
out.push(`unknown check step "${r?.step}"`);
|
|
114
132
|
if (!["pass", "fail", "skip", "info"].includes(r?.status))
|
|
115
133
|
out.push(`${r?.step}: unknown status "${r?.status}"`);
|
|
116
|
-
if (!isNum(r
|
|
134
|
+
if (r?.ms !== undefined && !isNum(r.ms, 0, MAX_MS))
|
|
117
135
|
out.push(`${r?.step}: ms must be between 0 and 15 min`);
|
|
136
|
+
if (noBench(j.provider) && (r?.ms !== undefined || (typeof r?.detail === "string" && TIMING.test(r.detail)))) {
|
|
137
|
+
out.push(`${r?.step}: has timings, but ${j.provider}'s terms don't allow publishing performance information; save with check --save`);
|
|
138
|
+
}
|
|
118
139
|
}
|
|
119
140
|
}
|
|
120
141
|
else if (j.tool === "sandboxrouter-cost") {
|
|
@@ -126,6 +147,8 @@ export function validateResult(j, now = Date.now()) {
|
|
|
126
147
|
out.push("entries is empty");
|
|
127
148
|
for (const e of Array.isArray(j.entries) ? j.entries : []) {
|
|
128
149
|
provider(e?.provider, "entry provider");
|
|
150
|
+
if (noBench(e?.provider))
|
|
151
|
+
out.push(`${e.provider}: ${NO_BENCH}`);
|
|
129
152
|
if (e?.seconds !== undefined && !isNum(e.seconds, 0, 2 * 24 * 3600))
|
|
130
153
|
out.push(`${e?.provider}: seconds is out of range`);
|
|
131
154
|
if (e?.predictedUsd !== undefined && !isNum(e.predictedUsd, 0, 1000))
|
|
@@ -206,10 +229,32 @@ export function validateResults(dir = "results", opts = {}) {
|
|
|
206
229
|
continue;
|
|
207
230
|
if (p.startsWith(`${dir}/conformance/`) || p.startsWith(`${dir}/cost/`))
|
|
208
231
|
continue;
|
|
209
|
-
if (/^[MR]/.test(c.status))
|
|
232
|
+
if (/^[MR]/.test(c.status) && !onlyDropsUnpublishable(opts.previous?.(p), p)) {
|
|
210
233
|
issues.push({ file: p, problem: "edits an existing bench report; add your run as a new file instead" });
|
|
234
|
+
}
|
|
211
235
|
if (c.status.startsWith("D"))
|
|
212
236
|
issues.push({ file: p, problem: "deletes a bench report; reports are only removed by a maintainer" });
|
|
213
237
|
}
|
|
214
238
|
return issues;
|
|
215
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* True when a bench report's only change is removing rows for providers whose terms forbid publishing
|
|
242
|
+
* benchmarks: the one edit to someone's report that's allowed, so those numbers can be taken down.
|
|
243
|
+
*/
|
|
244
|
+
function onlyDropsUnpublishable(before, path) {
|
|
245
|
+
if (before === undefined || !existsSync(path))
|
|
246
|
+
return false;
|
|
247
|
+
try {
|
|
248
|
+
const a = JSON.parse(before);
|
|
249
|
+
const b = JSON.parse(readFileSync(path, "utf8"));
|
|
250
|
+
const { rows: ra, ...restA } = a;
|
|
251
|
+
const { rows: rb, ...restB } = b;
|
|
252
|
+
if (JSON.stringify(restA) !== JSON.stringify(restB))
|
|
253
|
+
return false;
|
|
254
|
+
const kept = ra.filter((r) => benchmarksPublishable(r.provider));
|
|
255
|
+
return JSON.stringify(kept) === JSON.stringify(rb);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}
|
package/dist/router.js
CHANGED
|
@@ -211,7 +211,7 @@ export class Router {
|
|
|
211
211
|
const setup = err instanceof SetupError || (status !== undefined && SETUP_ERRORS.has(status));
|
|
212
212
|
if (route.fallback !== "any" && setup) {
|
|
213
213
|
const msg = err.message.replace(/[.\s]+$/, "");
|
|
214
|
-
throw new RouterError(`${msg.startsWith(c.provider) ? msg : `${c.provider}: ${msg}`}. This looks like a setup problem with ${c.provider} (key,
|
|
214
|
+
throw new RouterError(`${msg.startsWith(c.provider) ? msg : `${c.provider}: ${msg}`}. This looks like a setup problem with ${c.provider} (key, image, request or a missing package), ` +
|
|
215
215
|
`so the router stopped instead of moving the work to another provider; pass fallback: "any" to fall back on every error`, attempts);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
@@ -336,8 +336,12 @@ function round(n) {
|
|
|
336
336
|
}
|
|
337
337
|
/** Seconds to wait for one provider's create before trying the next (RouteOptions.createTimeoutSeconds). */
|
|
338
338
|
export const DEFAULT_CREATE_TIMEOUT_S = 120;
|
|
339
|
-
/**
|
|
340
|
-
|
|
339
|
+
/**
|
|
340
|
+
* HTTP statuses that mean the request or the key is wrong, not that the provider is down. 402 (out of
|
|
341
|
+
* balance, plan needed) isn't one: an unfunded account is exactly when the next provider should take the
|
|
342
|
+
* sandbox, and the failed attempt stays on `attempts` (the CLI prints it), so it isn't hidden.
|
|
343
|
+
*/
|
|
344
|
+
const SETUP_ERRORS = new Set([400, 401, 403, 404, 422]);
|
|
341
345
|
/** Seconds past a command's timeout before the router stops waiting for the adapter. */
|
|
342
346
|
export const EXEC_GRACE_S = 30;
|
|
343
347
|
/** Resolve with `work`, or reject with `onExpire()` after `ms`. A late `work` rejection is swallowed. */
|
package/dist/types.d.ts
CHANGED
|
@@ -33,9 +33,10 @@ export interface RouteOptions {
|
|
|
33
33
|
strategy?: Strategy;
|
|
34
34
|
/**
|
|
35
35
|
* What to do when a create fails. `true` (default): try the next candidate when the provider
|
|
36
|
-
* looks unavailable (timeout, network error, 429, 5xx, or an error without a status)
|
|
37
|
-
* a setup error (400, 401,
|
|
38
|
-
* a mistake isn't hidden by quietly moving the work to another, maybe
|
|
36
|
+
* looks unavailable (timeout, network error, 429, 5xx, or an error without a status) or can't bill
|
|
37
|
+
* (402: no balance, plan needed), but stop on a setup error (400, 401, 403, 404, 422: a bad key, a
|
|
38
|
+
* bad image or request), so a mistake isn't hidden by quietly moving the work to another, maybe
|
|
39
|
+
* pricier, provider. Every failed attempt is on `sandbox.attempts`.
|
|
39
40
|
* `"any"`: try the next candidate on every error. `false`: only the first candidate.
|
|
40
41
|
*/
|
|
41
42
|
fallback?: boolean | "any";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandboxrouter/sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|