@nodetool-ai/compute 0.8.1
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 +46 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/manager.d.ts +150 -0
- package/dist/manager.d.ts.map +1 -0
- package/dist/manager.js +408 -0
- package/dist/manager.js.map +1 -0
- package/dist/providers/runpod-rest.d.ts +151 -0
- package/dist/providers/runpod-rest.d.ts.map +1 -0
- package/dist/providers/runpod-rest.js +202 -0
- package/dist/providers/runpod-rest.js.map +1 -0
- package/dist/providers/runpod.d.ts +15 -0
- package/dist/providers/runpod.d.ts.map +1 -0
- package/dist/providers/runpod.js +100 -0
- package/dist/providers/runpod.js.map +1 -0
- package/dist/providers/types.d.ts +107 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +18 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/providers/vast.d.ts +27 -0
- package/dist/providers/vast.d.ts.map +1 -0
- package/dist/providers/vast.js +243 -0
- package/dist/providers/vast.js.map +1 -0
- package/dist/reaper.d.ts +37 -0
- package/dist/reaper.d.ts.map +1 -0
- package/dist/reaper.js +88 -0
- package/dist/reaper.js.map +1 -0
- package/dist/worker-heartbeat.d.ts +49 -0
- package/dist/worker-heartbeat.d.ts.map +1 -0
- package/dist/worker-heartbeat.js +68 -0
- package/dist/worker-heartbeat.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @nodetool-ai/compute
|
|
2
|
+
|
|
3
|
+
Remote compute orchestration for [NodeTool](https://nodetool.ai) — provisions and reaps GPU workers on RunPod and Vast.ai.
|
|
4
|
+
|
|
5
|
+
This package manages the lifecycle of remote GPU workers: it provisions instances from worker profiles, tracks their connections, reconciles desired against actual state, and runs a reaper that tears down idle or orphaned instances. Providers for RunPod and Vast.ai sit behind a common `WorkerProvider` interface.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @nodetool-ai/compute
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Exported symbols
|
|
14
|
+
|
|
15
|
+
| Symbol | Kind | Description |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `WorkerManager` | class | Manages worker profiles, provisioning, and reconciliation |
|
|
18
|
+
| `WorkerManagerDeps` | type | Injectable dependencies for `WorkerManager` |
|
|
19
|
+
| `ReconcileSummary` | type | Result of a reconcile pass |
|
|
20
|
+
| `WorkerConnection` / `WorkerOrphan` | type | Tracked worker and orphaned-instance shapes |
|
|
21
|
+
| `startReaper` | function | Starts the background loop that reaps idle/orphaned workers |
|
|
22
|
+
| `runReaperOnce` | function | Runs a single reaper pass |
|
|
23
|
+
| `ReaperDeps` / `ReaperHandle` / `ReaperManager` | type | Reaper wiring and control handle |
|
|
24
|
+
| `RunpodPodProvider` | class | `WorkerProvider` implementation for RunPod |
|
|
25
|
+
| `VastProvider` | class | `WorkerProvider` implementation for Vast.ai |
|
|
26
|
+
| `WorkerProvider` | interface | Provision/status/terminate contract for a compute backend |
|
|
27
|
+
| `WorkerSpec` / `WorkerTarget` / `WorkerStatus` | type | Worker specification and status shapes |
|
|
28
|
+
| `ProviderInstance` / `ProvisionResult` | type | Provider-level instance and provisioning results |
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { WorkerManager, startReaper } from "@nodetool-ai/compute";
|
|
34
|
+
|
|
35
|
+
const manager = new WorkerManager();
|
|
36
|
+
const summary = await manager.reconcile();
|
|
37
|
+
|
|
38
|
+
const reaper = startReaper({ manager });
|
|
39
|
+
// ... later
|
|
40
|
+
reaper.stop();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Links
|
|
44
|
+
|
|
45
|
+
- [NodeTool](https://nodetool.ai)
|
|
46
|
+
- [GitHub](https://github.com/nodetool-ai/nodetool)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2026-09-28T09:19:06.232Z
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { WorkerManager } from "./manager.js";
|
|
2
|
+
export type { ReconcileSummary, WorkerConnection, WorkerManagerDeps, WorkerOrphan, } from "./manager.js";
|
|
3
|
+
export { runReaperOnce, startReaper } from "./reaper.js";
|
|
4
|
+
export { attachWorkerActivityHeartbeat, installWorkerActivityHeartbeat, } from "./worker-heartbeat.js";
|
|
5
|
+
export type { BridgeCreationRegistrar, WorkerActivityHeartbeatOptions, WorkerActivitySource, } from "./worker-heartbeat.js";
|
|
6
|
+
export type { ReaperDeps, ReaperHandle, ReaperManager } from "./reaper.js";
|
|
7
|
+
export { RunpodPodProvider } from "./providers/runpod.js";
|
|
8
|
+
export { VastProvider } from "./providers/vast.js";
|
|
9
|
+
export type { ProviderInstance, ProvisionResult, WorkerProvider, WorkerSpec, WorkerStatus, WorkerTarget, } from "./providers/types.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,GAC/B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,uBAAuB,EACvB,8BAA8B,EAC9B,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,cAAc,EACd,UAAU,EACV,YAAY,EACZ,YAAY,GACb,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Worker provisioning package (`@nodetool-ai/compute`).
|
|
2
|
+
export { WorkerManager } from "./manager.js";
|
|
3
|
+
export { runReaperOnce, startReaper } from "./reaper.js";
|
|
4
|
+
export { attachWorkerActivityHeartbeat, installWorkerActivityHeartbeat, } from "./worker-heartbeat.js";
|
|
5
|
+
export { RunpodPodProvider } from "./providers/runpod.js";
|
|
6
|
+
export { VastProvider } from "./providers/vast.js";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAO7C,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,GAC/B,MAAM,uBAAuB,CAAC;AAO/B,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { type CreateWorkerInstanceInput, type CreateWorkerProfileInput, type ListWorkerInstancesOptions, type WorkerInstance, type WorkerInstancePatch, type WorkerProfile } from "@nodetool-ai/models";
|
|
2
|
+
import type { WorkerProvider, WorkerStatus, WorkerTarget } from "./providers/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The connection info a caller applies to the Python bridge when attaching to
|
|
5
|
+
* a worker — the worker's WebSocket URL and its bearer token (if any).
|
|
6
|
+
*/
|
|
7
|
+
export interface WorkerConnection {
|
|
8
|
+
wsUrl: string;
|
|
9
|
+
token: string | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A provider-live worker that the DB registry doesn't track — a "true orphan"
|
|
13
|
+
* billing out-of-band. Surfaced by `reconcile()` for one-click stop-all.
|
|
14
|
+
*/
|
|
15
|
+
export interface WorkerOrphan {
|
|
16
|
+
target: WorkerTarget;
|
|
17
|
+
providerRef: string;
|
|
18
|
+
status: WorkerStatus;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Result of an orphan reconcile pass: the untracked live workers, the number of
|
|
22
|
+
* tracked instances still live, and their summed estimated cost.
|
|
23
|
+
*/
|
|
24
|
+
export interface ReconcileSummary {
|
|
25
|
+
orphans: WorkerOrphan[];
|
|
26
|
+
liveCount: number;
|
|
27
|
+
estimatedCostUsd: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The dependencies the manager delegates to — the models accessors, the secret
|
|
31
|
+
* getter, and a provider factory. Defaults bind to the real `@nodetool-ai/models`
|
|
32
|
+
* implementations; tests inject in-memory fakes.
|
|
33
|
+
*/
|
|
34
|
+
export interface WorkerManagerDeps {
|
|
35
|
+
createWorkerProfile: (input: CreateWorkerProfileInput) => Promise<WorkerProfile>;
|
|
36
|
+
getWorkerProfile: (name: string) => Promise<WorkerProfile | null>;
|
|
37
|
+
listWorkerProfiles: () => Promise<WorkerProfile[]>;
|
|
38
|
+
deleteWorkerProfile: (name: string) => Promise<void>;
|
|
39
|
+
createWorkerInstance: (input: CreateWorkerInstanceInput) => Promise<WorkerInstance>;
|
|
40
|
+
getWorkerInstance: (id: string) => Promise<WorkerInstance | null>;
|
|
41
|
+
listWorkerInstances: (options?: ListWorkerInstancesOptions) => Promise<WorkerInstance[]>;
|
|
42
|
+
updateWorkerInstance: (id: string, patch: WorkerInstancePatch) => Promise<WorkerInstance>;
|
|
43
|
+
getSecret: (key: string, userId?: string, defaultValue?: string) => Promise<string | null>;
|
|
44
|
+
getSetting: (key: string) => Promise<string | null>;
|
|
45
|
+
setSetting: (key: string, value: string) => Promise<void>;
|
|
46
|
+
deleteSetting: (key: string) => Promise<void>;
|
|
47
|
+
providerFactory: (target: WorkerTarget, apiKey: string) => WorkerProvider;
|
|
48
|
+
}
|
|
49
|
+
export declare class WorkerManager {
|
|
50
|
+
private readonly deps;
|
|
51
|
+
constructor(deps?: Partial<WorkerManagerDeps>);
|
|
52
|
+
createProfile(input: CreateWorkerProfileInput): Promise<WorkerProfile>;
|
|
53
|
+
listProfiles(): Promise<WorkerProfile[]>;
|
|
54
|
+
deleteProfile(name: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Provision a worker from a profile: resolve the provider, generate a token
|
|
57
|
+
* when the profile's policy requires it, call the provider, and persist the
|
|
58
|
+
* resulting instance transitioning `provisioning → running`.
|
|
59
|
+
*/
|
|
60
|
+
provision(profileName: string): Promise<WorkerInstance>;
|
|
61
|
+
/**
|
|
62
|
+
* Pause a worker: release its GPU compute but keep its volume (and cached
|
|
63
|
+
* models). Marks the instance `stopped` — resumable via `resume`. The volume
|
|
64
|
+
* still bills a little while paused; `terminate` to stop all cost.
|
|
65
|
+
*/
|
|
66
|
+
stop(instanceId: string): Promise<WorkerInstance>;
|
|
67
|
+
/**
|
|
68
|
+
* Resume a paused worker: bring it back to running and adopt its fresh
|
|
69
|
+
* endpoint/cost (the URL can change across a stop/resume). Does NOT re-attach
|
|
70
|
+
* the bridge — the caller attaches as a separate step. May throw if the
|
|
71
|
+
* provider cannot re-allocate a GPU.
|
|
72
|
+
*/
|
|
73
|
+
resume(instanceId: string): Promise<WorkerInstance>;
|
|
74
|
+
/**
|
|
75
|
+
* Destroy a worker AND its volume — the real teardown that stops all billing.
|
|
76
|
+
* The cached models are gone for good. Marks the instance `terminated` (a
|
|
77
|
+
* tombstone) and drops the active-worker pointer if it referenced it.
|
|
78
|
+
*/
|
|
79
|
+
terminate(instanceId: string): Promise<WorkerInstance>;
|
|
80
|
+
/** Pause every instance that is still live (not stopped/terminated). */
|
|
81
|
+
stopAll(): Promise<void>;
|
|
82
|
+
/** Return all registered instances. */
|
|
83
|
+
list(): Promise<WorkerInstance[]>;
|
|
84
|
+
/**
|
|
85
|
+
* Return one instance by id with its bearer token decrypted, or null. Use
|
|
86
|
+
* this — not `list` — whenever the token is needed: bulk reads withhold it.
|
|
87
|
+
*/
|
|
88
|
+
getInstance(instanceId: string): Promise<WorkerInstance | null>;
|
|
89
|
+
/** Refresh an instance's status from its provider. */
|
|
90
|
+
status(instanceId: string): Promise<WorkerStatus>;
|
|
91
|
+
/**
|
|
92
|
+
* Reconcile the DB registry against what each provider reports live. Tracked
|
|
93
|
+
* instances that are `running`/`attached` but absent from their provider's
|
|
94
|
+
* live list died or were killed out-of-band — mark them `stopped`. Live
|
|
95
|
+
* provider workers with no matching tracked instance are true orphans,
|
|
96
|
+
* surfaced (with a live-count and estimated-cost summary) so the cost guard
|
|
97
|
+
* can offer a one-click stop-all. Already-`stopped` rows are left untouched.
|
|
98
|
+
*/
|
|
99
|
+
reconcile(): Promise<ReconcileSummary>;
|
|
100
|
+
/**
|
|
101
|
+
* Adopt an instance as the active worker: persist the singleton pointer,
|
|
102
|
+
* mark the instance `attached`, and return its connection info for the
|
|
103
|
+
* caller to apply to the Python bridge (`bridge.setTarget`). The manager
|
|
104
|
+
* stays free of a runtime dependency — re-pointing is the caller's job.
|
|
105
|
+
*/
|
|
106
|
+
attach(instanceId: string): Promise<WorkerConnection>;
|
|
107
|
+
/**
|
|
108
|
+
* Read-only connection target for an instance — the same `{wsUrl, token}`
|
|
109
|
+
* attach returns, but without mutating the attach pointer or status. Used to
|
|
110
|
+
* health-probe a worker that is `running` but not yet attached.
|
|
111
|
+
*/
|
|
112
|
+
connectionInfo(instanceId: string): Promise<WorkerConnection>;
|
|
113
|
+
/**
|
|
114
|
+
* Release the active worker: clear the pointer and revert the
|
|
115
|
+
* previously-attached instance to `running`. The caller points the bridge
|
|
116
|
+
* back at its env/stdio default.
|
|
117
|
+
*/
|
|
118
|
+
detach(): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Return the currently-attached instance, or null when none is attached.
|
|
121
|
+
* Never returns a stopped/dead instance: if the pointer references a row that
|
|
122
|
+
* has since been stopped/terminated (or vanished), clear it and return null
|
|
123
|
+
* so consumers don't re-point the bridge at a worker that is no longer live.
|
|
124
|
+
*/
|
|
125
|
+
getActiveWorker(): Promise<WorkerInstance | null>;
|
|
126
|
+
/** Drop the active-worker pointer if it currently references `instanceId`. */
|
|
127
|
+
private clearActivePointerIfMatches;
|
|
128
|
+
private requireInstance;
|
|
129
|
+
private resolveProvider;
|
|
130
|
+
/**
|
|
131
|
+
* Report whether each target's API key is available — store OR environment,
|
|
132
|
+
* the SAME resolution `provision` uses. The UI must check this rather than the
|
|
133
|
+
* secrets list alone, which only sees the store and so false-warns on an
|
|
134
|
+
* env-provided key.
|
|
135
|
+
*/
|
|
136
|
+
apiKeyStatus(): Promise<Record<WorkerTarget, boolean>>;
|
|
137
|
+
/**
|
|
138
|
+
* Load a target's API key from the secret store, falling back to the
|
|
139
|
+
* environment when the keychain is unreachable (headless/sandboxed). Mirrors
|
|
140
|
+
* the resolution the deleted `runpod-worker.ts` script used.
|
|
141
|
+
*/
|
|
142
|
+
private loadApiKey;
|
|
143
|
+
/**
|
|
144
|
+
* Resolve a target's API key from the secret store, then the environment.
|
|
145
|
+
* Returns `null` when neither has it (no throw) — shared by `loadApiKey` (which
|
|
146
|
+
* throws) and `apiKeyStatus` (which reports).
|
|
147
|
+
*/
|
|
148
|
+
private resolveApiKey;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"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"}
|
package/dist/manager.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// WorkerManager — the orchestration layer over the per-target providers.
|
|
2
|
+
//
|
|
3
|
+
// It owns the profiles→instances identity model: profiles are declarative
|
|
4
|
+
// presets persisted in `worker_profiles`, instances are ephemeral,
|
|
5
|
+
// billing-sensitive handles persisted in `worker_instances`. All state lives in
|
|
6
|
+
// the DB (via the models accessors) — never only in memory — so the UI, CLI,
|
|
7
|
+
// and reaper share a single source of truth and a forgotten GPU pod is always
|
|
8
|
+
// recoverable from the registry.
|
|
9
|
+
//
|
|
10
|
+
// The manager resolves a per-target `WorkerProvider` (RunPod pod today, Vast in
|
|
11
|
+
// Group F), loading the provider's API key from the secret store with an env
|
|
12
|
+
// fallback. Every model accessor, the secret getter, and the provider factory
|
|
13
|
+
// are injectable so the lifecycle can be unit-tested with fakes.
|
|
14
|
+
import { randomBytes } from "node:crypto";
|
|
15
|
+
import { createLogger } from "@nodetool-ai/config";
|
|
16
|
+
import { createWorkerInstance as realCreateWorkerInstance, createWorkerProfile as realCreateWorkerProfile, deleteWorkerProfile as realDeleteWorkerProfile, getSecret as realGetSecret, getWorkerInstance as realGetWorkerInstance, getWorkerProfile as realGetWorkerProfile, listWorkerInstances as realListWorkerInstances, listWorkerProfiles as realListWorkerProfiles, updateWorkerInstance as realUpdateWorkerInstance, Setting, } from "@nodetool-ai/models";
|
|
17
|
+
import { RunpodPodProvider } from "./providers/runpod.js";
|
|
18
|
+
import { VastProvider } from "./providers/vast.js";
|
|
19
|
+
const log = createLogger("nodetool.compute.manager");
|
|
20
|
+
/** Local desktop user id used for secret-store reads (shared with the CLI). */
|
|
21
|
+
const LOCAL_USER_ID = "1";
|
|
22
|
+
/**
|
|
23
|
+
* Settings key holding the id of the worker instance the local NodeTool
|
|
24
|
+
* instance has attached to. Singleton for the desktop MVP — any NodeTool
|
|
25
|
+
* instance reads it to re-point its Python bridge.
|
|
26
|
+
*/
|
|
27
|
+
const ACTIVE_WORKER_KEY = "active_worker_instance_id";
|
|
28
|
+
/** Secret-store key holding each target's API key. */
|
|
29
|
+
const API_KEY_SECRET = {
|
|
30
|
+
runpod: "RUNPOD_API_KEY",
|
|
31
|
+
vast: "VAST_API_KEY",
|
|
32
|
+
};
|
|
33
|
+
/** Construct the concrete provider for a target. */
|
|
34
|
+
function defaultProviderFactory(target, apiKey) {
|
|
35
|
+
switch (target) {
|
|
36
|
+
case "runpod":
|
|
37
|
+
return new RunpodPodProvider(apiKey);
|
|
38
|
+
case "vast":
|
|
39
|
+
return new VastProvider(apiKey);
|
|
40
|
+
default:
|
|
41
|
+
throw new Error(`Unsupported worker target: ${target}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function defaultDeps() {
|
|
45
|
+
return {
|
|
46
|
+
createWorkerProfile: realCreateWorkerProfile,
|
|
47
|
+
getWorkerProfile: realGetWorkerProfile,
|
|
48
|
+
listWorkerProfiles: realListWorkerProfiles,
|
|
49
|
+
deleteWorkerProfile: realDeleteWorkerProfile,
|
|
50
|
+
createWorkerInstance: realCreateWorkerInstance,
|
|
51
|
+
getWorkerInstance: realGetWorkerInstance,
|
|
52
|
+
listWorkerInstances: realListWorkerInstances,
|
|
53
|
+
updateWorkerInstance: realUpdateWorkerInstance,
|
|
54
|
+
getSecret: realGetSecret,
|
|
55
|
+
getSetting: async (key) => {
|
|
56
|
+
const setting = await Setting.find(LOCAL_USER_ID, key);
|
|
57
|
+
return setting ? setting.getValue() : null;
|
|
58
|
+
},
|
|
59
|
+
setSetting: async (key, value) => {
|
|
60
|
+
await Setting.upsert({ userId: LOCAL_USER_ID, key, value });
|
|
61
|
+
},
|
|
62
|
+
deleteSetting: async (key) => {
|
|
63
|
+
await Setting.deleteSetting(LOCAL_USER_ID, key);
|
|
64
|
+
},
|
|
65
|
+
providerFactory: defaultProviderFactory,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export class WorkerManager {
|
|
69
|
+
deps;
|
|
70
|
+
constructor(deps = {}) {
|
|
71
|
+
this.deps = { ...defaultDeps(), ...deps };
|
|
72
|
+
}
|
|
73
|
+
// --- Profiles -----------------------------------------------------------
|
|
74
|
+
createProfile(input) {
|
|
75
|
+
return this.deps.createWorkerProfile(input);
|
|
76
|
+
}
|
|
77
|
+
listProfiles() {
|
|
78
|
+
return this.deps.listWorkerProfiles();
|
|
79
|
+
}
|
|
80
|
+
deleteProfile(name) {
|
|
81
|
+
return this.deps.deleteWorkerProfile(name);
|
|
82
|
+
}
|
|
83
|
+
// --- Instance lifecycle -------------------------------------------------
|
|
84
|
+
/**
|
|
85
|
+
* Provision a worker from a profile: resolve the provider, generate a token
|
|
86
|
+
* when the profile's policy requires it, call the provider, and persist the
|
|
87
|
+
* resulting instance transitioning `provisioning → running`.
|
|
88
|
+
*/
|
|
89
|
+
async provision(profileName) {
|
|
90
|
+
const profile = await this.deps.getWorkerProfile(profileName);
|
|
91
|
+
if (!profile) {
|
|
92
|
+
throw new Error(`Worker profile not found: ${profileName}`);
|
|
93
|
+
}
|
|
94
|
+
const target = profile.target;
|
|
95
|
+
const provider = await this.resolveProvider(target);
|
|
96
|
+
const token = profile.token_policy === "generate" ? generateToken() : undefined;
|
|
97
|
+
const spec = specFromProfile(profile, target, token);
|
|
98
|
+
const result = await provider.provision(spec);
|
|
99
|
+
// The provider now holds a billable resource. If persisting it to the
|
|
100
|
+
// registry fails, the resource would be orphaned — untracked and billing,
|
|
101
|
+
// recoverable only via reconcile(). Compensate by DESTROYING it (terminate,
|
|
102
|
+
// not pause — a paused orphan still bills volume and is never tracked), then
|
|
103
|
+
// re-throw the original DB error so the caller sees the real cause.
|
|
104
|
+
try {
|
|
105
|
+
const instance = await this.deps.createWorkerInstance({
|
|
106
|
+
profile_name: profile.name,
|
|
107
|
+
target,
|
|
108
|
+
provider_ref: result.providerRef,
|
|
109
|
+
ws_url: result.wsUrl,
|
|
110
|
+
token: result.token ?? null,
|
|
111
|
+
estimated_cost_usd: result.costUsd ?? null,
|
|
112
|
+
});
|
|
113
|
+
return await this.deps.updateWorkerInstance(instance.id, {
|
|
114
|
+
status: result.status,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
log.error("Failed to persist provisioned worker; tearing down the orphaned " +
|
|
119
|
+
"provider resource to stop billing", { target, providerRef: result.providerRef, error });
|
|
120
|
+
try {
|
|
121
|
+
await provider.terminate(result.providerRef);
|
|
122
|
+
}
|
|
123
|
+
catch (terminateError) {
|
|
124
|
+
log.error("Compensating terminate() of the orphaned provider resource also " +
|
|
125
|
+
"failed; it may still be billing and require manual teardown", { target, providerRef: result.providerRef, error: terminateError });
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Pause a worker: release its GPU compute but keep its volume (and cached
|
|
132
|
+
* models). Marks the instance `stopped` — resumable via `resume`. The volume
|
|
133
|
+
* still bills a little while paused; `terminate` to stop all cost.
|
|
134
|
+
*/
|
|
135
|
+
async stop(instanceId) {
|
|
136
|
+
const instance = await this.requireInstance(instanceId);
|
|
137
|
+
const provider = await this.resolveProvider(instance.target);
|
|
138
|
+
await provider.stop(instance.provider_ref);
|
|
139
|
+
await this.clearActivePointerIfMatches(instance.id);
|
|
140
|
+
return this.deps.updateWorkerInstance(instance.id, { status: "stopped" });
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Resume a paused worker: bring it back to running and adopt its fresh
|
|
144
|
+
* endpoint/cost (the URL can change across a stop/resume). Does NOT re-attach
|
|
145
|
+
* the bridge — the caller attaches as a separate step. May throw if the
|
|
146
|
+
* provider cannot re-allocate a GPU.
|
|
147
|
+
*/
|
|
148
|
+
async resume(instanceId) {
|
|
149
|
+
const instance = await this.requireInstance(instanceId);
|
|
150
|
+
const provider = await this.resolveProvider(instance.target);
|
|
151
|
+
const result = await provider.resume(instance.provider_ref);
|
|
152
|
+
return this.deps.updateWorkerInstance(instance.id, {
|
|
153
|
+
status: "running",
|
|
154
|
+
ws_url: result.wsUrl,
|
|
155
|
+
estimated_cost_usd: result.costUsd ?? instance.estimated_cost_usd,
|
|
156
|
+
// Resuming is activity — reset the idle clock so the reaper doesn't
|
|
157
|
+
// immediately re-stop it before any work runs.
|
|
158
|
+
last_activity_at: new Date().toISOString(),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Destroy a worker AND its volume — the real teardown that stops all billing.
|
|
163
|
+
* The cached models are gone for good. Marks the instance `terminated` (a
|
|
164
|
+
* tombstone) and drops the active-worker pointer if it referenced it.
|
|
165
|
+
*/
|
|
166
|
+
async terminate(instanceId) {
|
|
167
|
+
const instance = await this.requireInstance(instanceId);
|
|
168
|
+
const provider = await this.resolveProvider(instance.target);
|
|
169
|
+
await provider.terminate(instance.provider_ref);
|
|
170
|
+
await this.clearActivePointerIfMatches(instance.id);
|
|
171
|
+
return this.deps.updateWorkerInstance(instance.id, {
|
|
172
|
+
status: "terminated",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** Pause every instance that is still live (not stopped/terminated). */
|
|
176
|
+
async stopAll() {
|
|
177
|
+
const instances = await this.deps.listWorkerInstances();
|
|
178
|
+
for (const instance of instances) {
|
|
179
|
+
if (instance.status === "stopped" || instance.status === "terminated") {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
await this.stop(instance.id);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Return all registered instances. */
|
|
186
|
+
list() {
|
|
187
|
+
return this.deps.listWorkerInstances();
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Return one instance by id with its bearer token decrypted, or null. Use
|
|
191
|
+
* this — not `list` — whenever the token is needed: bulk reads withhold it.
|
|
192
|
+
*/
|
|
193
|
+
getInstance(instanceId) {
|
|
194
|
+
return this.deps.getWorkerInstance(instanceId);
|
|
195
|
+
}
|
|
196
|
+
/** Refresh an instance's status from its provider. */
|
|
197
|
+
async status(instanceId) {
|
|
198
|
+
const instance = await this.requireInstance(instanceId);
|
|
199
|
+
const provider = await this.resolveProvider(instance.target);
|
|
200
|
+
return provider.status(instance.provider_ref);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Reconcile the DB registry against what each provider reports live. Tracked
|
|
204
|
+
* instances that are `running`/`attached` but absent from their provider's
|
|
205
|
+
* live list died or were killed out-of-band — mark them `stopped`. Live
|
|
206
|
+
* provider workers with no matching tracked instance are true orphans,
|
|
207
|
+
* surfaced (with a live-count and estimated-cost summary) so the cost guard
|
|
208
|
+
* can offer a one-click stop-all. Already-`stopped` rows are left untouched.
|
|
209
|
+
*/
|
|
210
|
+
async reconcile() {
|
|
211
|
+
const instances = await this.deps.listWorkerInstances();
|
|
212
|
+
const targets = [
|
|
213
|
+
...new Set(instances.map((i) => i.target)),
|
|
214
|
+
];
|
|
215
|
+
const orphans = [];
|
|
216
|
+
let liveCount = 0;
|
|
217
|
+
let estimatedCostUsd = 0;
|
|
218
|
+
for (const target of targets) {
|
|
219
|
+
const provider = await this.resolveProvider(target);
|
|
220
|
+
const live = await provider.list();
|
|
221
|
+
const liveRefs = new Set(live.map((l) => l.providerRef));
|
|
222
|
+
const tracked = instances.filter((i) => i.target === target);
|
|
223
|
+
const trackedRefs = new Set(tracked.map((i) => i.provider_ref));
|
|
224
|
+
// Tracked-but-dead: running/attached instances missing from the live
|
|
225
|
+
// list died/were killed out-of-band. The survivors count toward the
|
|
226
|
+
// live summary; already-stopped rows are skipped entirely.
|
|
227
|
+
for (const instance of tracked) {
|
|
228
|
+
if (instance.status !== "running" && instance.status !== "attached") {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (liveRefs.has(instance.provider_ref)) {
|
|
232
|
+
liveCount += 1;
|
|
233
|
+
estimatedCostUsd += instance.estimated_cost_usd ?? 0;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
await this.clearActivePointerIfMatches(instance.id);
|
|
237
|
+
await this.deps.updateWorkerInstance(instance.id, {
|
|
238
|
+
status: "stopped",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Live-but-untracked: provider workers with no matching instance row.
|
|
243
|
+
// Only running/attached entries are cost orphans — a stopped/paused or
|
|
244
|
+
// terminated provider entry isn't burning GPU billing, so don't surface
|
|
245
|
+
// it for one-click stop-all.
|
|
246
|
+
for (const entry of live) {
|
|
247
|
+
if (trackedRefs.has(entry.providerRef))
|
|
248
|
+
continue;
|
|
249
|
+
if (entry.status === "stopped" || entry.status === "terminated") {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
orphans.push({
|
|
253
|
+
target,
|
|
254
|
+
providerRef: entry.providerRef,
|
|
255
|
+
status: entry.status,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { orphans, liveCount, estimatedCostUsd };
|
|
260
|
+
}
|
|
261
|
+
// --- Attach pointer -----------------------------------------------------
|
|
262
|
+
/**
|
|
263
|
+
* Adopt an instance as the active worker: persist the singleton pointer,
|
|
264
|
+
* mark the instance `attached`, and return its connection info for the
|
|
265
|
+
* caller to apply to the Python bridge (`bridge.setTarget`). The manager
|
|
266
|
+
* stays free of a runtime dependency — re-pointing is the caller's job.
|
|
267
|
+
*/
|
|
268
|
+
async attach(instanceId) {
|
|
269
|
+
const instance = await this.requireInstance(instanceId);
|
|
270
|
+
await this.deps.setSetting(ACTIVE_WORKER_KEY, instance.id);
|
|
271
|
+
// Attaching is itself activity: reset the idle clock alongside the status.
|
|
272
|
+
// A worker whose provision + attach outran its idle window still carries a
|
|
273
|
+
// creation-time `last_activity_at` and has emitted no bridge frame yet, so
|
|
274
|
+
// without this the reaper would stop the worker on its next pass before any
|
|
275
|
+
// work runs.
|
|
276
|
+
await this.deps.updateWorkerInstance(instance.id, {
|
|
277
|
+
status: "attached",
|
|
278
|
+
last_activity_at: new Date().toISOString(),
|
|
279
|
+
});
|
|
280
|
+
return { wsUrl: instance.ws_url, token: instance.token };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Read-only connection target for an instance — the same `{wsUrl, token}`
|
|
284
|
+
* attach returns, but without mutating the attach pointer or status. Used to
|
|
285
|
+
* health-probe a worker that is `running` but not yet attached.
|
|
286
|
+
*/
|
|
287
|
+
async connectionInfo(instanceId) {
|
|
288
|
+
const instance = await this.requireInstance(instanceId);
|
|
289
|
+
return { wsUrl: instance.ws_url, token: instance.token };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Release the active worker: clear the pointer and revert the
|
|
293
|
+
* previously-attached instance to `running`. The caller points the bridge
|
|
294
|
+
* back at its env/stdio default.
|
|
295
|
+
*/
|
|
296
|
+
async detach() {
|
|
297
|
+
const active = await this.getActiveWorker();
|
|
298
|
+
await this.deps.deleteSetting(ACTIVE_WORKER_KEY);
|
|
299
|
+
if (active && active.status === "attached") {
|
|
300
|
+
await this.deps.updateWorkerInstance(active.id, { status: "running" });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Return the currently-attached instance, or null when none is attached.
|
|
305
|
+
* Never returns a stopped/dead instance: if the pointer references a row that
|
|
306
|
+
* has since been stopped/terminated (or vanished), clear it and return null
|
|
307
|
+
* so consumers don't re-point the bridge at a worker that is no longer live.
|
|
308
|
+
*/
|
|
309
|
+
async getActiveWorker() {
|
|
310
|
+
const id = await this.deps.getSetting(ACTIVE_WORKER_KEY);
|
|
311
|
+
if (!id) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const instance = await this.deps.getWorkerInstance(id);
|
|
315
|
+
if (!instance ||
|
|
316
|
+
instance.status === "stopped" ||
|
|
317
|
+
instance.status === "terminated") {
|
|
318
|
+
await this.deps.deleteSetting(ACTIVE_WORKER_KEY);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return instance;
|
|
322
|
+
}
|
|
323
|
+
// --- Internals ----------------------------------------------------------
|
|
324
|
+
/** Drop the active-worker pointer if it currently references `instanceId`. */
|
|
325
|
+
async clearActivePointerIfMatches(instanceId) {
|
|
326
|
+
const activeId = await this.deps.getSetting(ACTIVE_WORKER_KEY);
|
|
327
|
+
if (activeId === instanceId) {
|
|
328
|
+
await this.deps.deleteSetting(ACTIVE_WORKER_KEY);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async requireInstance(instanceId) {
|
|
332
|
+
const instance = await this.deps.getWorkerInstance(instanceId);
|
|
333
|
+
if (!instance) {
|
|
334
|
+
throw new Error(`Worker instance not found: ${instanceId}`);
|
|
335
|
+
}
|
|
336
|
+
return instance;
|
|
337
|
+
}
|
|
338
|
+
async resolveProvider(target) {
|
|
339
|
+
const apiKey = await this.loadApiKey(target);
|
|
340
|
+
return this.deps.providerFactory(target, apiKey);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Report whether each target's API key is available — store OR environment,
|
|
344
|
+
* the SAME resolution `provision` uses. The UI must check this rather than the
|
|
345
|
+
* secrets list alone, which only sees the store and so false-warns on an
|
|
346
|
+
* env-provided key.
|
|
347
|
+
*/
|
|
348
|
+
async apiKeyStatus() {
|
|
349
|
+
const targets = Object.keys(API_KEY_SECRET);
|
|
350
|
+
const entries = await Promise.all(targets.map(async (target) => [target, (await this.resolveApiKey(target)) !== null]));
|
|
351
|
+
return Object.fromEntries(entries);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Load a target's API key from the secret store, falling back to the
|
|
355
|
+
* environment when the keychain is unreachable (headless/sandboxed). Mirrors
|
|
356
|
+
* the resolution the deleted `runpod-worker.ts` script used.
|
|
357
|
+
*/
|
|
358
|
+
async loadApiKey(target) {
|
|
359
|
+
const key = await this.resolveApiKey(target);
|
|
360
|
+
if (key !== null) {
|
|
361
|
+
return key;
|
|
362
|
+
}
|
|
363
|
+
const secretName = API_KEY_SECRET[target];
|
|
364
|
+
throw new Error(`${secretName} not found in the secret store or environment. ` +
|
|
365
|
+
`Store it with: nodetool secrets store ${secretName}`);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Resolve a target's API key from the secret store, then the environment.
|
|
369
|
+
* Returns `null` when neither has it (no throw) — shared by `loadApiKey` (which
|
|
370
|
+
* throws) and `apiKeyStatus` (which reports).
|
|
371
|
+
*/
|
|
372
|
+
async resolveApiKey(target) {
|
|
373
|
+
const key = API_KEY_SECRET[target];
|
|
374
|
+
if (!key) {
|
|
375
|
+
throw new Error(`No API key mapping for worker target: ${target}`);
|
|
376
|
+
}
|
|
377
|
+
const fromStore = await this.deps.getSecret(key, LOCAL_USER_ID);
|
|
378
|
+
if (fromStore) {
|
|
379
|
+
return fromStore;
|
|
380
|
+
}
|
|
381
|
+
return process.env[key] ?? null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Build a provision spec from a profile's declarative fields. */
|
|
385
|
+
function specFromProfile(profile, target, token) {
|
|
386
|
+
const spec = profile.spec ?? {};
|
|
387
|
+
return {
|
|
388
|
+
name: profile.name,
|
|
389
|
+
image: profile.image,
|
|
390
|
+
target,
|
|
391
|
+
gpu: typeof spec.gpu === "string" ? spec.gpu : undefined,
|
|
392
|
+
vcpu: typeof spec.vcpu === "number" ? spec.vcpu : undefined,
|
|
393
|
+
disk: typeof spec.disk === "number" ? spec.disk : undefined,
|
|
394
|
+
env: isStringRecord(spec.env) ? spec.env : undefined,
|
|
395
|
+
sshPublicKey: typeof spec.sshPublicKey === "string" ? spec.sshPublicKey : undefined,
|
|
396
|
+
token,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function isStringRecord(value) {
|
|
400
|
+
return (typeof value === "object" &&
|
|
401
|
+
value !== null &&
|
|
402
|
+
Object.values(value).every((v) => typeof v === "string"));
|
|
403
|
+
}
|
|
404
|
+
/** Generate a high-entropy worker bearer token. */
|
|
405
|
+
function generateToken() {
|
|
406
|
+
return randomBytes(32).toString("hex");
|
|
407
|
+
}
|
|
408
|
+
//# sourceMappingURL=manager.js.map
|