@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
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { DEFAULT_MEMORY_GB, DEFAULT_VCPU } from "../catalog.js";
|
|
3
|
+
import { ProviderError } from "../types.js";
|
|
4
|
+
import { dirname, optionalImport, shq, toBytes, wrapCommand } from "../util.js";
|
|
5
|
+
const DEFAULT_IMAGE = "ubuntu:22.04";
|
|
6
|
+
/** Northflank's createOptions.expiryTime bounds: 5 minutes to 7 days. */
|
|
7
|
+
const MIN_EXPIRY_SECONDS = 300;
|
|
8
|
+
const MAX_EXPIRY_SECONDS = 604_800;
|
|
9
|
+
const READY_TIMEOUT_MS = 5 * 60_000;
|
|
10
|
+
/**
|
|
11
|
+
* Northflank sandboxes via `@northflank/js-client`. Auth: NORTHFLANK_API_TOKEN plus
|
|
12
|
+
* NORTHFLANK_PROJECT_ID (the project every sandbox is created in; its region is fixed at
|
|
13
|
+
* project creation). A sandbox is a deployment service (microVM-isolated on Northflank cloud)
|
|
14
|
+
* kept alive by `tail -f /dev/null`; commands run over Northflank's exec API. Default image
|
|
15
|
+
* ubuntu:22.04; set image.northflank to any public image path (e.g. "python:3.13-slim"). The
|
|
16
|
+
* image's entrypoint is replaced, and it needs `sh` and `base64` on PATH for file transfer.
|
|
17
|
+
*/
|
|
18
|
+
export class NorthflankProvider {
|
|
19
|
+
token;
|
|
20
|
+
projectId;
|
|
21
|
+
id = "northflank";
|
|
22
|
+
client;
|
|
23
|
+
constructor(token = process.env.NORTHFLANK_API_TOKEN, projectId = process.env.NORTHFLANK_PROJECT_ID) {
|
|
24
|
+
this.token = token;
|
|
25
|
+
this.projectId = projectId;
|
|
26
|
+
}
|
|
27
|
+
configured() {
|
|
28
|
+
return !!this.token && !!this.projectId;
|
|
29
|
+
}
|
|
30
|
+
async northflank() {
|
|
31
|
+
if (!this.client) {
|
|
32
|
+
if (!this.token || !this.projectId) {
|
|
33
|
+
throw new ProviderError("northflank", "NORTHFLANK_API_TOKEN and NORTHFLANK_PROJECT_ID must both be set");
|
|
34
|
+
}
|
|
35
|
+
const { ApiClient, ApiClientInMemoryContextProvider } = await optionalImport("@northflank/js-client", "northflank");
|
|
36
|
+
const ctx = new ApiClientInMemoryContextProvider();
|
|
37
|
+
ctx.addContext({ name: "sandboxrouter", token: this.token, project: this.projectId });
|
|
38
|
+
this.client = new ApiClient(ctx);
|
|
39
|
+
}
|
|
40
|
+
return this.client;
|
|
41
|
+
}
|
|
42
|
+
async create(spec) {
|
|
43
|
+
const nf = await this.northflank();
|
|
44
|
+
const projectId = this.projectId;
|
|
45
|
+
const name = `sbr-${randomBytes(5).toString("hex")}`;
|
|
46
|
+
const expiry = Math.round((spec.timeoutMinutes ?? 30) * 60);
|
|
47
|
+
const svc = await call(() => nf.create.service.deployment({
|
|
48
|
+
parameters: { projectId },
|
|
49
|
+
data: {
|
|
50
|
+
name,
|
|
51
|
+
description: "sandboxrouter sandbox",
|
|
52
|
+
billing: { deploymentPlan: plan(spec.vcpu ?? DEFAULT_VCPU, spec.memoryGb ?? DEFAULT_MEMORY_GB) },
|
|
53
|
+
deployment: {
|
|
54
|
+
instances: 1,
|
|
55
|
+
external: { imagePath: spec.image?.northflank ?? DEFAULT_IMAGE },
|
|
56
|
+
docker: { configType: "customEntrypointCustomCommand", customEntrypoint: "/bin/sh", customCommand: "-c 'tail -f /dev/null'" },
|
|
57
|
+
},
|
|
58
|
+
runtimeEnvironment: spec.env,
|
|
59
|
+
// At expiry Northflank pauses the service (compute billing stops) and schedules its deletion.
|
|
60
|
+
createOptions: { expiryTime: Math.min(MAX_EXPIRY_SECONDS, Math.max(MIN_EXPIRY_SECONDS, expiry)) },
|
|
61
|
+
},
|
|
62
|
+
}));
|
|
63
|
+
const box = new NorthflankBox(nf, projectId, svc?.id ?? name);
|
|
64
|
+
try {
|
|
65
|
+
await box.waitReady();
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
await box.kill().catch(() => { });
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
return box;
|
|
72
|
+
}
|
|
73
|
+
/** Reattach to a running sandbox by service id. */
|
|
74
|
+
async connect(nativeId) {
|
|
75
|
+
const box = new NorthflankBox(await this.northflank(), this.projectId, nativeId);
|
|
76
|
+
const { expiredAt } = await box.info();
|
|
77
|
+
if (expiredAt)
|
|
78
|
+
throw new ProviderError("northflank", `sandbox ${nativeId} expired at ${expiredAt}`);
|
|
79
|
+
await box.waitReady();
|
|
80
|
+
return box;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
class NorthflankBox {
|
|
84
|
+
nf;
|
|
85
|
+
projectId;
|
|
86
|
+
nativeId;
|
|
87
|
+
constructor(nf, projectId, nativeId) {
|
|
88
|
+
this.nf = nf;
|
|
89
|
+
this.projectId = projectId;
|
|
90
|
+
this.nativeId = nativeId;
|
|
91
|
+
}
|
|
92
|
+
get params() {
|
|
93
|
+
return { projectId: this.projectId, serviceId: this.nativeId };
|
|
94
|
+
}
|
|
95
|
+
/** The deployment-service fields this adapter reads (GetServiceResult is a union over service types). */
|
|
96
|
+
async info() {
|
|
97
|
+
return (await call(() => this.nf.get.service({ parameters: this.params })));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Poll until the deployment is COMPLETED (container running), then until exec is accepted:
|
|
101
|
+
* the exec endpoint can refuse briefly while the instance comes up.
|
|
102
|
+
*/
|
|
103
|
+
async waitReady() {
|
|
104
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
105
|
+
for (;;) {
|
|
106
|
+
const status = (await this.info())?.status?.deployment?.status;
|
|
107
|
+
if (status === "FAILED")
|
|
108
|
+
throw new ProviderError("northflank", `sandbox ${this.nativeId} failed to deploy`);
|
|
109
|
+
if (status === "COMPLETED")
|
|
110
|
+
break;
|
|
111
|
+
if (Date.now() > deadline)
|
|
112
|
+
throw new ProviderError("northflank", `sandbox ${this.nativeId} not ready after ${READY_TIMEOUT_MS / 1000}s`);
|
|
113
|
+
await sleep(1000);
|
|
114
|
+
}
|
|
115
|
+
for (;;) {
|
|
116
|
+
try {
|
|
117
|
+
const r = await this.run(["/bin/sh", "-c", "true"], 30_000);
|
|
118
|
+
if (r.exitCode === 0)
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (Date.now() > deadline)
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
if (Date.now() > deadline)
|
|
126
|
+
throw new ProviderError("northflank", `sandbox ${this.nativeId} not accepting exec`);
|
|
127
|
+
await sleep(1000);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Exec has no cwd/env/timeout, so cwd/env are wrapped into the command, the command runs
|
|
132
|
+
* under `timeout` when the image has it, and the deadline is also enforced here.
|
|
133
|
+
*/
|
|
134
|
+
async exec(cmd, opts = {}) {
|
|
135
|
+
const t0 = Date.now();
|
|
136
|
+
const secs = opts.timeoutSeconds ?? 60;
|
|
137
|
+
const script = shq(wrapCommand(cmd, { cwd: opts.cwd, env: opts.env }));
|
|
138
|
+
const guarded = `if command -v timeout >/dev/null 2>&1; then exec timeout ${secs} /bin/sh -c ${script}; else exec /bin/sh -c ${script}; fi`;
|
|
139
|
+
const r = await this.run(["/bin/sh", "-c", guarded], secs * 1000 + 10_000, opts);
|
|
140
|
+
const durationMs = Date.now() - t0;
|
|
141
|
+
if (durationMs >= secs * 1000 && [124, 137, 143].includes(r.exitCode)) {
|
|
142
|
+
throw new ProviderError("northflank", `command timed out after ${secs}s`);
|
|
143
|
+
}
|
|
144
|
+
return { ...r, durationMs };
|
|
145
|
+
}
|
|
146
|
+
/** Stream base64 over stdin and decode in the sandbox. */
|
|
147
|
+
async writeFile(path, content) {
|
|
148
|
+
const b64 = Buffer.from(toBytes(content)).toString("base64");
|
|
149
|
+
const r = await this.run(["/bin/sh", "-c", `mkdir -p ${shq(dirname(path))} && base64 -d > ${shq(path)}`], 120_000 + b64.length / 100, {}, b64);
|
|
150
|
+
if (r.exitCode !== 0)
|
|
151
|
+
throw new ProviderError("northflank", `write ${path} failed: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
|
|
152
|
+
}
|
|
153
|
+
async readFile(path) {
|
|
154
|
+
const r = await this.run(["/bin/sh", "-c", `base64 < ${shq(path)}`], 120_000);
|
|
155
|
+
if (r.exitCode !== 0)
|
|
156
|
+
throw new ProviderError("northflank", `read ${path} failed: ${r.stderr.trim() || `exit ${r.exitCode}`}`);
|
|
157
|
+
return new Uint8Array(Buffer.from(r.stdout, "base64"));
|
|
158
|
+
}
|
|
159
|
+
async kill() {
|
|
160
|
+
await call(() => this.nf.delete.service({ parameters: this.params }));
|
|
161
|
+
}
|
|
162
|
+
/** One exec session: optional stdin, streamed stdout/stderr, exit code. */
|
|
163
|
+
async run(command, limitMs, opts = {}, stdin) {
|
|
164
|
+
const t0 = Date.now();
|
|
165
|
+
let session;
|
|
166
|
+
try {
|
|
167
|
+
session = await this.nf.exec.execServiceSession(this.params, { command, shell: "none", tty: false });
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
throw new ProviderError("northflank", err?.message ?? String(err), err?.status);
|
|
171
|
+
}
|
|
172
|
+
let stdout = "";
|
|
173
|
+
let stderr = "";
|
|
174
|
+
session.stdOut.setEncoding("utf8");
|
|
175
|
+
session.stdErr.setEncoding("utf8");
|
|
176
|
+
session.stdOut.on("data", (c) => {
|
|
177
|
+
stdout += c;
|
|
178
|
+
opts.onStdout?.(c);
|
|
179
|
+
});
|
|
180
|
+
session.stdErr.on("data", (c) => {
|
|
181
|
+
stderr += c;
|
|
182
|
+
opts.onStderr?.(c);
|
|
183
|
+
});
|
|
184
|
+
const failed = new Promise((_, reject) => session.on("error", (e) => reject(e)));
|
|
185
|
+
failed.catch(() => { });
|
|
186
|
+
// Send stdin (if any) and close it so commands that read stdin don't hang.
|
|
187
|
+
const CHUNK = 64 * 1024;
|
|
188
|
+
if (stdin)
|
|
189
|
+
for (let i = 0; i < stdin.length; i += CHUNK)
|
|
190
|
+
session.stdIn.write(stdin.slice(i, i + CHUNK));
|
|
191
|
+
session.stdIn.end();
|
|
192
|
+
let timer;
|
|
193
|
+
const timedOut = new Promise((resolve) => {
|
|
194
|
+
timer = setTimeout(() => resolve("timeout"), limitMs);
|
|
195
|
+
timer.unref();
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
const res = await Promise.race([session.waitForCommandResult(), failed, timedOut]);
|
|
199
|
+
if (res === "timeout")
|
|
200
|
+
throw new ProviderError("northflank", `command timed out after ${Math.round((Date.now() - t0) / 1000)}s`);
|
|
201
|
+
return { exitCode: typeof res.exitCode === "number" ? res.exitCode : -1, stdout, stderr };
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
if (err instanceof ProviderError)
|
|
205
|
+
throw err;
|
|
206
|
+
throw new ProviderError("northflank", err?.message ?? String(err), err?.status);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Unwrap a Northflank API response, turning an `error` body into a ProviderError with its HTTP status. */
|
|
214
|
+
async function call(fn) {
|
|
215
|
+
let res;
|
|
216
|
+
try {
|
|
217
|
+
res = await fn();
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
throw new ProviderError("northflank", err?.message ?? String(err), err?.status);
|
|
221
|
+
}
|
|
222
|
+
if (res.error)
|
|
223
|
+
throw new ProviderError("northflank", res.error.message || `HTTP ${res.error.status}`, res.error.status);
|
|
224
|
+
return res.data;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Northflank deployment plans (vCPU, GB), from
|
|
228
|
+
* https://northflank.com/docs/v1/application/billing/resource-plans-and-quotas. Northflank bills
|
|
229
|
+
* per vCPU-hour and GB-hour, so the cheapest plan that fits is the one with the least of both.
|
|
230
|
+
*/
|
|
231
|
+
const PLANS = [
|
|
232
|
+
["nf-compute-10", 0.1, 0.25],
|
|
233
|
+
["nf-compute-20", 0.2, 0.5],
|
|
234
|
+
["nf-compute-50", 0.5, 1],
|
|
235
|
+
["nf-compute-100-1", 1, 1],
|
|
236
|
+
["nf-compute-100-2", 1, 2],
|
|
237
|
+
["nf-compute-100-4", 1, 4],
|
|
238
|
+
["nf-compute-200", 2, 4],
|
|
239
|
+
["nf-compute-200-8", 2, 8],
|
|
240
|
+
["nf-compute-200-16", 2, 16],
|
|
241
|
+
["nf-compute-400", 4, 8],
|
|
242
|
+
["nf-compute-400-16", 4, 16],
|
|
243
|
+
["nf-compute-800-8", 8, 8],
|
|
244
|
+
["nf-compute-800-16", 8, 16],
|
|
245
|
+
["nf-compute-800-24", 8, 24],
|
|
246
|
+
["nf-compute-800-32", 8, 32],
|
|
247
|
+
["nf-compute-800-40", 8, 40],
|
|
248
|
+
["nf-compute-1200-24", 12, 24],
|
|
249
|
+
["nf-compute-1600-32", 16, 32],
|
|
250
|
+
["nf-compute-2000-40", 20, 40],
|
|
251
|
+
["nf-compute-32-131072", 32, 128],
|
|
252
|
+
];
|
|
253
|
+
/** Smallest plan with at least the requested vCPU and memory; the largest plan if none fits. */
|
|
254
|
+
function plan(vcpu, memoryGb) {
|
|
255
|
+
const cost = ([, c, g]) => c * 0.01667 + g * 0.00833;
|
|
256
|
+
const fits = PLANS.filter(([, c, g]) => c >= vcpu && g >= memoryGb).sort((a, b) => cost(a) - cost(b));
|
|
257
|
+
return (fits[0] ?? PLANS[PLANS.length - 1])[0];
|
|
258
|
+
}
|
|
259
|
+
function sleep(ms) {
|
|
260
|
+
return new Promise((r) => setTimeout(r, ms).unref());
|
|
261
|
+
}
|
package/dist/router.js
CHANGED
|
@@ -11,6 +11,12 @@ import { SpritesProvider } from "./providers/sprites.js";
|
|
|
11
11
|
import { BlaxelProvider } from "./providers/blaxel.js";
|
|
12
12
|
import { TensorlakeProvider } from "./providers/tensorlake.js";
|
|
13
13
|
import { RunloopProvider } from "./providers/runloop.js";
|
|
14
|
+
import { DockerProvider } from "./providers/docker.js";
|
|
15
|
+
import { HopxProvider } from "./providers/hopx.js";
|
|
16
|
+
import { CloudflareProvider } from "./providers/cloudflare.js";
|
|
17
|
+
import { NorthflankProvider } from "./providers/northflank.js";
|
|
18
|
+
import { BoatProvider } from "./providers/boat.js";
|
|
19
|
+
import { ExeDevProvider } from "./providers/exedev.js";
|
|
14
20
|
import { RouterError, } from "./types.js";
|
|
15
21
|
export function defaultProviders() {
|
|
16
22
|
return [
|
|
@@ -24,6 +30,12 @@ export function defaultProviders() {
|
|
|
24
30
|
new BlaxelProvider(),
|
|
25
31
|
new TensorlakeProvider(),
|
|
26
32
|
new RunloopProvider(),
|
|
33
|
+
new HopxProvider(),
|
|
34
|
+
new CloudflareProvider(),
|
|
35
|
+
new NorthflankProvider(),
|
|
36
|
+
new BoatProvider(),
|
|
37
|
+
new ExeDevProvider(),
|
|
38
|
+
new DockerProvider(),
|
|
27
39
|
];
|
|
28
40
|
}
|
|
29
41
|
export class Router {
|
|
@@ -50,7 +62,9 @@ export class Router {
|
|
|
50
62
|
/** The ordered list of providers a create would try, with the reason any are skipped. */
|
|
51
63
|
plan(spec = {}, route = {}) {
|
|
52
64
|
const explicit = route.provider ? [].concat(route.provider) : null;
|
|
53
|
-
|
|
65
|
+
// Local providers (Docker) are never picked automatically: they always "cost" $0 and isolate
|
|
66
|
+
// less than a cloud micro-VM. Name them explicitly to use them.
|
|
67
|
+
const ids = explicit ?? CATALOG.filter((c) => !c.local && this.providers.has(c.id)).map((c) => c.id);
|
|
54
68
|
const bench = route.strategy === "fastest" ? loadBench(this.benchFile) : {};
|
|
55
69
|
const list = ids.map((id) => {
|
|
56
70
|
const p = this.providers.get(id);
|
|
@@ -65,11 +79,16 @@ export class Router {
|
|
|
65
79
|
c.skip = unfit(id, spec, route.require) ?? (p.configured() ? undefined : "not configured");
|
|
66
80
|
return c;
|
|
67
81
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
// Neutral by construction: price (or measured speed) decides, ties break alphabetically by id.
|
|
83
|
+
// An explicit provider list keeps the caller's order.
|
|
84
|
+
if (!explicit) {
|
|
85
|
+
const byPrice = (a, b) => a.estimatedHourlyUsd - b.estimatedHourlyUsd || a.provider.localeCompare(b.provider);
|
|
86
|
+
if (route.strategy === "fastest") {
|
|
87
|
+
list.sort((a, b) => (a.medianCreateMs ?? Infinity) - (b.medianCreateMs ?? Infinity) || byPrice(a, b));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
list.sort(byPrice);
|
|
91
|
+
}
|
|
73
92
|
}
|
|
74
93
|
return list;
|
|
75
94
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ProviderId = "maritime" | "e2b" | "daytona" | "modal" | "vercel" | "morph" | "sprites" | "blaxel" | "tensorlake" | "runloop";
|
|
1
|
+
export type ProviderId = "maritime" | "e2b" | "daytona" | "modal" | "vercel" | "morph" | "sprites" | "blaxel" | "tensorlake" | "runloop" | "hopx" | "cloudflare" | "northflank" | "boat" | "exedev" | "docker";
|
|
2
2
|
/** Things a caller can require of a provider. The router skips providers that lack one. */
|
|
3
3
|
export type Capability = "gpu" | "pause" | "custom-image";
|
|
4
4
|
/** What the caller wants. Every field is optional; providers map it to their own knobs. */
|
|
@@ -21,11 +21,15 @@ export interface SandboxSpec {
|
|
|
21
21
|
/** Free-form tags stored on the sandbox where the provider supports it. */
|
|
22
22
|
metadata?: Record<string, string>;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* "cheapest" (and "default", the same thing): lowest list price for the spec, ties alphabetical.
|
|
26
|
+
* "fastest": lowest median cold start you measured with `bench`, then cheapest.
|
|
27
|
+
*/
|
|
24
28
|
export type Strategy = "default" | "cheapest" | "fastest";
|
|
25
29
|
export interface RouteOptions {
|
|
26
30
|
/** Pin to one provider, or give an ordered list to try. Omit to let the router choose. */
|
|
27
31
|
provider?: ProviderId | ProviderId[];
|
|
28
|
-
/** How to order candidates when no explicit list is given. Default:
|
|
32
|
+
/** How to order candidates when no explicit list is given. Default: cheapest. */
|
|
29
33
|
strategy?: Strategy;
|
|
30
34
|
/** Try the next candidate if a create fails (default true). */
|
|
31
35
|
fallback?: boolean;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandboxrouter/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -29,36 +29,42 @@
|
|
|
29
29
|
"site:data": "node --import tsx scripts/site-data.ts"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
|
-
"sandbox",
|
|
33
32
|
"ai-agents",
|
|
33
|
+
"blaxel",
|
|
34
|
+
"cloudflare",
|
|
34
35
|
"code-execution",
|
|
35
|
-
"router",
|
|
36
|
-
"maritime",
|
|
37
|
-
"e2b",
|
|
38
36
|
"daytona",
|
|
37
|
+
"docker",
|
|
38
|
+
"e2b",
|
|
39
|
+
"fly-sprites",
|
|
40
|
+
"hopx",
|
|
41
|
+
"maritime",
|
|
39
42
|
"modal",
|
|
40
|
-
"vercel-sandbox",
|
|
41
|
-
"openrouter",
|
|
42
43
|
"morph",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
44
|
+
"northflank",
|
|
45
|
+
"openrouter",
|
|
46
|
+
"router",
|
|
47
|
+
"runloop",
|
|
48
|
+
"sandbox",
|
|
45
49
|
"tensorlake",
|
|
46
|
-
"
|
|
50
|
+
"vercel-sandbox"
|
|
47
51
|
],
|
|
48
52
|
"license": "MIT",
|
|
49
53
|
"engines": {
|
|
50
54
|
"node": ">=20"
|
|
51
55
|
},
|
|
52
56
|
"peerDependencies": {
|
|
57
|
+
"@blaxel/core": ">=0.3",
|
|
53
58
|
"@daytonaio/sdk": ">=0.200",
|
|
59
|
+
"@fly/sprites": ">=0.2",
|
|
60
|
+
"@runloop/api-client": ">=3",
|
|
61
|
+
"@tensorlakeai/tensorlake": ">=0.5",
|
|
54
62
|
"@vercel/sandbox": ">=3",
|
|
55
63
|
"e2b": ">=2",
|
|
56
64
|
"modal": ">=0.11",
|
|
57
65
|
"morphcloud": ">=0.0.20",
|
|
58
|
-
"@
|
|
59
|
-
"@
|
|
60
|
-
"@tensorlakeai/tensorlake": ">=0.5",
|
|
61
|
-
"@runloop/api-client": ">=3"
|
|
66
|
+
"@hopx-ai/sdk": ">=0.5",
|
|
67
|
+
"@northflank/js-client": ">=0.12"
|
|
62
68
|
},
|
|
63
69
|
"peerDependenciesMeta": {
|
|
64
70
|
"e2b": {
|
|
@@ -87,12 +93,20 @@
|
|
|
87
93
|
},
|
|
88
94
|
"@runloop/api-client": {
|
|
89
95
|
"optional": true
|
|
96
|
+
},
|
|
97
|
+
"@hopx-ai/sdk": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"@northflank/js-client": {
|
|
101
|
+
"optional": true
|
|
90
102
|
}
|
|
91
103
|
},
|
|
92
104
|
"devDependencies": {
|
|
93
105
|
"@blaxel/core": "^0.3.34",
|
|
94
106
|
"@daytonaio/sdk": "^0.227.3",
|
|
95
107
|
"@fly/sprites": "^0.2.4",
|
|
108
|
+
"@hopx-ai/sdk": "^0.5.1",
|
|
109
|
+
"@northflank/js-client": "^0.12.0",
|
|
96
110
|
"@runloop/api-client": "^3.0.0",
|
|
97
111
|
"@tensorlakeai/tensorlake": "^0.5.161",
|
|
98
112
|
"@types/node": "^26.6.6",
|