@draht/pods 2026.3.2-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +511 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +346 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/models.d.ts +39 -0
  7. package/dist/commands/models.d.ts.map +1 -0
  8. package/dist/commands/models.js +658 -0
  9. package/dist/commands/models.js.map +1 -0
  10. package/dist/commands/pods.d.ts +21 -0
  11. package/dist/commands/pods.d.ts.map +1 -0
  12. package/dist/commands/pods.js +175 -0
  13. package/dist/commands/pods.js.map +1 -0
  14. package/dist/commands/prompt.d.ts +7 -0
  15. package/dist/commands/prompt.d.ts.map +1 -0
  16. package/dist/commands/prompt.js +54 -0
  17. package/dist/commands/prompt.js.map +1 -0
  18. package/dist/config.d.ts +11 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +74 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +3 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/model-configs.d.ts +22 -0
  27. package/dist/model-configs.d.ts.map +1 -0
  28. package/dist/model-configs.js +75 -0
  29. package/dist/model-configs.js.map +1 -0
  30. package/dist/models.json +295 -0
  31. package/dist/scripts/model_run.sh +83 -0
  32. package/dist/scripts/pod_setup.sh +336 -0
  33. package/dist/ssh.d.ts +24 -0
  34. package/dist/ssh.d.ts.map +1 -0
  35. package/dist/ssh.js +115 -0
  36. package/dist/ssh.js.map +1 -0
  37. package/dist/types.d.ts +23 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/types.js +3 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +40 -0
  42. package/scripts/model_run.sh +83 -0
  43. package/scripts/pod_setup.sh +336 -0
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env bash
2
+ # GPU pod bootstrap for vLLM deployment
3
+ set -euo pipefail
4
+
5
+ # Parse arguments passed from pi CLI
6
+ MOUNT_COMMAND=""
7
+ MODELS_PATH=""
8
+ HF_TOKEN=""
9
+ PI_API_KEY=""
10
+ VLLM_VERSION="release" # Default to release
11
+
12
+ while [[ $# -gt 0 ]]; do
13
+ case $1 in
14
+ --mount)
15
+ MOUNT_COMMAND="$2"
16
+ shift 2
17
+ ;;
18
+ --models-path)
19
+ MODELS_PATH="$2"
20
+ shift 2
21
+ ;;
22
+ --hf-token)
23
+ HF_TOKEN="$2"
24
+ shift 2
25
+ ;;
26
+ --vllm-api-key)
27
+ PI_API_KEY="$2"
28
+ shift 2
29
+ ;;
30
+ --vllm)
31
+ VLLM_VERSION="$2"
32
+ shift 2
33
+ ;;
34
+ *)
35
+ echo "ERROR: Unknown option: $1" >&2
36
+ exit 1
37
+ ;;
38
+ esac
39
+ done
40
+
41
+ # Validate required parameters
42
+ if [ -z "$HF_TOKEN" ]; then
43
+ echo "ERROR: HF_TOKEN is required" >&2
44
+ exit 1
45
+ fi
46
+
47
+ if [ -z "$PI_API_KEY" ]; then
48
+ echo "ERROR: PI_API_KEY is required" >&2
49
+ exit 1
50
+ fi
51
+
52
+ if [ -z "$MODELS_PATH" ]; then
53
+ echo "ERROR: MODELS_PATH is required" >&2
54
+ exit 1
55
+ fi
56
+
57
+ echo "=== Starting pod setup ==="
58
+
59
+ # Install system dependencies
60
+ apt update -y
61
+ apt install -y python3-pip python3-venv git build-essential cmake ninja-build curl wget lsb-release htop pkg-config
62
+
63
+ # --- Install matching CUDA toolkit -------------------------------------------
64
+ echo "Checking CUDA driver version..."
65
+ DRIVER_CUDA_VERSION=$(nvidia-smi | grep "CUDA Version" | awk '{print $9}')
66
+ echo "Driver supports CUDA: $DRIVER_CUDA_VERSION"
67
+
68
+ # Check if nvcc exists and its version
69
+ if command -v nvcc &> /dev/null; then
70
+ NVCC_VERSION=$(nvcc --version | grep "release" | awk '{print $6}' | cut -d, -f1)
71
+ echo "Current nvcc version: $NVCC_VERSION"
72
+ else
73
+ NVCC_VERSION="none"
74
+ echo "nvcc not found"
75
+ fi
76
+
77
+ # Install CUDA toolkit matching driver version if needed
78
+ if [[ "$NVCC_VERSION" != "$DRIVER_CUDA_VERSION" ]]; then
79
+ echo "Installing CUDA Toolkit $DRIVER_CUDA_VERSION to match driver..."
80
+
81
+ # Detect Ubuntu version
82
+ UBUNTU_VERSION=$(lsb_release -rs)
83
+ UBUNTU_CODENAME=$(lsb_release -cs)
84
+
85
+ echo "Detected Ubuntu $UBUNTU_VERSION ($UBUNTU_CODENAME)"
86
+
87
+ # Map Ubuntu version to NVIDIA repo path
88
+ if [[ "$UBUNTU_VERSION" == "24.04" ]]; then
89
+ REPO_PATH="ubuntu2404"
90
+ elif [[ "$UBUNTU_VERSION" == "22.04" ]]; then
91
+ REPO_PATH="ubuntu2204"
92
+ elif [[ "$UBUNTU_VERSION" == "20.04" ]]; then
93
+ REPO_PATH="ubuntu2004"
94
+ else
95
+ echo "Warning: Unsupported Ubuntu version $UBUNTU_VERSION, trying ubuntu2204"
96
+ REPO_PATH="ubuntu2204"
97
+ fi
98
+
99
+ # Add NVIDIA package repositories
100
+ wget https://developer.download.nvidia.com/compute/cuda/repos/${REPO_PATH}/x86_64/cuda-keyring_1.1-1_all.deb
101
+ dpkg -i cuda-keyring_1.1-1_all.deb
102
+ rm cuda-keyring_1.1-1_all.deb
103
+ apt-get update
104
+
105
+ # Install specific CUDA toolkit version
106
+ # Convert version format (12.9 -> 12-9)
107
+ CUDA_VERSION_APT=$(echo $DRIVER_CUDA_VERSION | sed 's/\./-/')
108
+ echo "Installing cuda-toolkit-${CUDA_VERSION_APT}..."
109
+ apt-get install -y cuda-toolkit-${CUDA_VERSION_APT}
110
+
111
+ # Add CUDA to PATH
112
+ export PATH=/usr/local/cuda-${DRIVER_CUDA_VERSION}/bin:$PATH
113
+ export LD_LIBRARY_PATH=/usr/local/cuda-${DRIVER_CUDA_VERSION}/lib64:${LD_LIBRARY_PATH:-}
114
+
115
+ # Verify installation
116
+ nvcc --version
117
+ else
118
+ echo "CUDA toolkit $NVCC_VERSION matches driver version"
119
+ export PATH=/usr/local/cuda-${DRIVER_CUDA_VERSION}/bin:$PATH
120
+ export LD_LIBRARY_PATH=/usr/local/cuda-${DRIVER_CUDA_VERSION}/lib64:${LD_LIBRARY_PATH:-}
121
+ fi
122
+
123
+ # --- Install uv (fast Python package manager) --------------------------------
124
+ curl -LsSf https://astral.sh/uv/install.sh | sh
125
+ export PATH="$HOME/.local/bin:$PATH"
126
+
127
+ # --- Install Python 3.12 if not available ------------------------------------
128
+ if ! command -v python3.12 &> /dev/null; then
129
+ echo "Python 3.12 not found. Installing via uv..."
130
+ uv python install 3.12
131
+ fi
132
+
133
+ # --- Clean up existing environments and caches -------------------------------
134
+ echo "Cleaning up existing environments and caches..."
135
+
136
+ # Remove existing venv for a clean installation
137
+ VENV="$HOME/venv"
138
+ if [ -d "$VENV" ]; then
139
+ echo "Removing existing virtual environment..."
140
+ rm -rf "$VENV"
141
+ fi
142
+
143
+ # Remove uv cache to ensure fresh installs
144
+ if [ -d "$HOME/.cache/uv" ]; then
145
+ echo "Clearing uv cache..."
146
+ rm -rf "$HOME/.cache/uv"
147
+ fi
148
+
149
+ # Remove vLLM cache to avoid conflicts
150
+ if [ -d "$HOME/.cache/vllm" ]; then
151
+ echo "Clearing vLLM cache..."
152
+ rm -rf "$HOME/.cache/vllm"
153
+ fi
154
+
155
+ # --- Create and activate venv ------------------------------------------------
156
+ echo "Creating fresh virtual environment..."
157
+ uv venv --python 3.12 --seed "$VENV"
158
+ source "$VENV/bin/activate"
159
+
160
+ # --- Install PyTorch and vLLM ------------------------------------------------
161
+ echo "Installing vLLM and dependencies (version: $VLLM_VERSION)..."
162
+ case "$VLLM_VERSION" in
163
+ release)
164
+ echo "Installing vLLM release with PyTorch..."
165
+ # Install vLLM with automatic PyTorch backend selection
166
+ # vLLM will automatically install the correct PyTorch version
167
+ uv pip install vllm>=0.10.0 --torch-backend=auto || {
168
+ echo "ERROR: Failed to install vLLM"
169
+ exit 1
170
+ }
171
+ ;;
172
+ nightly)
173
+ echo "Installing vLLM nightly with PyTorch..."
174
+ echo "This will install the latest nightly build of vLLM..."
175
+
176
+ # Install vLLM nightly with PyTorch
177
+ uv pip install -U vllm \
178
+ --torch-backend=auto \
179
+ --extra-index-url https://wheels.vllm.ai/nightly || {
180
+ echo "ERROR: Failed to install vLLM nightly"
181
+ exit 1
182
+ }
183
+
184
+ echo "vLLM nightly successfully installed!"
185
+ ;;
186
+ gpt-oss)
187
+ echo "Installing GPT-OSS special build with PyTorch nightly..."
188
+ echo "WARNING: This build is ONLY for GPT-OSS models!"
189
+ echo "Installing PyTorch nightly and cutting-edge dependencies..."
190
+
191
+ # Convert CUDA version format for PyTorch (12.4 -> cu124)
192
+ PYTORCH_CUDA="cu$(echo $DRIVER_CUDA_VERSION | sed 's/\.//')"
193
+ echo "Using PyTorch nightly with ${PYTORCH_CUDA} (driver supports ${DRIVER_CUDA_VERSION})"
194
+
195
+ # The GPT-OSS build will pull PyTorch nightly and other dependencies
196
+ # via the extra index URLs. We don't pre-install torch here to avoid conflicts.
197
+ uv pip install --pre vllm==0.10.1+gptoss \
198
+ --extra-index-url https://wheels.vllm.ai/gpt-oss/ \
199
+ --extra-index-url https://download.pytorch.org/whl/nightly/${PYTORCH_CUDA} \
200
+ --index-strategy unsafe-best-match || {
201
+ echo "ERROR: Failed to install GPT-OSS vLLM build"
202
+ echo "This automatically installs PyTorch nightly with ${PYTORCH_CUDA}, Triton nightly, and other dependencies"
203
+ exit 1
204
+ }
205
+
206
+ # Install gpt-oss library for tool support
207
+ uv pip install gpt-oss || {
208
+ echo "WARNING: Failed to install gpt-oss library (needed for tool use)"
209
+ }
210
+ ;;
211
+ *)
212
+ echo "ERROR: Unknown vLLM version: $VLLM_VERSION"
213
+ exit 1
214
+ ;;
215
+ esac
216
+
217
+ # --- Install additional packages ---------------------------------------------
218
+ echo "Installing additional packages..."
219
+ # Note: tensorrt removed temporarily due to CUDA 13.0 compatibility issues
220
+ # TensorRT still depends on deprecated nvidia-cuda-runtime-cu13 package
221
+ uv pip install huggingface-hub psutil hf_transfer
222
+
223
+ # --- FlashInfer installation (optional, improves performance) ----------------
224
+ echo "Attempting FlashInfer installation (optional)..."
225
+ if uv pip install flashinfer-python; then
226
+ echo "FlashInfer installed successfully"
227
+ else
228
+ echo "FlashInfer not available, using Flash Attention instead"
229
+ fi
230
+
231
+ # --- Mount storage if provided -----------------------------------------------
232
+ if [ -n "$MOUNT_COMMAND" ]; then
233
+ echo "Setting up mount..."
234
+
235
+ # Create mount point directory if it doesn't exist
236
+ mkdir -p "$MODELS_PATH"
237
+
238
+ # Execute the mount command
239
+ eval "$MOUNT_COMMAND" || {
240
+ echo "WARNING: Mount command failed, continuing without mount"
241
+ }
242
+
243
+ # Verify mount succeeded (optional, may not always be a mount point)
244
+ if mountpoint -q "$MODELS_PATH" 2>/dev/null; then
245
+ echo "Storage successfully mounted at $MODELS_PATH"
246
+ else
247
+ echo "Note: $MODELS_PATH is not a mount point (might be local storage)"
248
+ fi
249
+ fi
250
+
251
+ # --- Model storage setup ------------------------------------------------------
252
+ echo ""
253
+ echo "=== Setting up model storage ==="
254
+ echo "Storage path: $MODELS_PATH"
255
+
256
+ # Check if the path exists and is writable
257
+ if [ ! -d "$MODELS_PATH" ]; then
258
+ echo "Creating model storage directory: $MODELS_PATH"
259
+ mkdir -p "$MODELS_PATH"
260
+ fi
261
+
262
+ if [ ! -w "$MODELS_PATH" ]; then
263
+ echo "ERROR: Model storage path is not writable: $MODELS_PATH"
264
+ echo "Please check permissions"
265
+ exit 1
266
+ fi
267
+
268
+ # Create the huggingface cache directory structure in the models path
269
+ mkdir -p "${MODELS_PATH}/huggingface/hub"
270
+
271
+ # Remove any existing cache directory or symlink
272
+ if [ -e ~/.cache/huggingface ] || [ -L ~/.cache/huggingface ]; then
273
+ echo "Removing existing ~/.cache/huggingface..."
274
+ rm -rf ~/.cache/huggingface 2>/dev/null || true
275
+ fi
276
+
277
+ # Create parent directory if needed
278
+ mkdir -p ~/.cache
279
+
280
+ # Create symlink from ~/.cache/huggingface to the models path
281
+ ln -s "${MODELS_PATH}/huggingface" ~/.cache/huggingface
282
+ echo "Created symlink: ~/.cache/huggingface -> ${MODELS_PATH}/huggingface"
283
+
284
+ # Verify the symlink works
285
+ if [ -d ~/.cache/huggingface/hub ]; then
286
+ echo "✓ Model storage configured successfully"
287
+
288
+ # Check available space
289
+ AVAILABLE_SPACE=$(df -h "$MODELS_PATH" | awk 'NR==2 {print $4}')
290
+ echo "Available space: $AVAILABLE_SPACE"
291
+ else
292
+ echo "ERROR: Could not verify model storage setup"
293
+ echo "The symlink was created but the target directory is not accessible"
294
+ exit 1
295
+ fi
296
+
297
+ # --- Configure environment ----------------------------------------------------
298
+ mkdir -p ~/.config/vllm
299
+ touch ~/.config/vllm/do_not_track
300
+
301
+ # Write environment to .bashrc for persistence
302
+ cat >> ~/.bashrc << EOF
303
+
304
+ # Pi vLLM environment
305
+ [ -d "\$HOME/venv" ] && source "\$HOME/venv/bin/activate"
306
+ export PATH="/usr/local/cuda-${DRIVER_CUDA_VERSION}/bin:\$HOME/.local/bin:\$PATH"
307
+ export LD_LIBRARY_PATH="/usr/local/cuda-${DRIVER_CUDA_VERSION}/lib64:\${LD_LIBRARY_PATH:-}"
308
+ export HF_TOKEN="${HF_TOKEN}"
309
+ export PI_API_KEY="${PI_API_KEY}"
310
+ export HUGGING_FACE_HUB_TOKEN="${HF_TOKEN}"
311
+ export HF_HUB_ENABLE_HF_TRANSFER=1
312
+ export VLLM_NO_USAGE_STATS=1
313
+ export VLLM_DO_NOT_TRACK=1
314
+ export VLLM_ALLOW_LONG_MAX_MODEL_LEN=1
315
+ export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
316
+ EOF
317
+
318
+ # Create log directory for vLLM
319
+ mkdir -p ~/.vllm_logs
320
+
321
+ # --- Output GPU info for pi CLI to parse -------------------------------------
322
+ echo ""
323
+ echo "===GPU_INFO_START==="
324
+ nvidia-smi --query-gpu=index,name,memory.total --format=csv,noheader | while IFS=, read -r id name memory; do
325
+ # Trim whitespace
326
+ id=$(echo "$id" | xargs)
327
+ name=$(echo "$name" | xargs)
328
+ memory=$(echo "$memory" | xargs)
329
+ echo "{\"id\": $id, \"name\": \"$name\", \"memory\": \"$memory\"}"
330
+ done
331
+ echo "===GPU_INFO_END==="
332
+
333
+ echo ""
334
+ echo "=== Setup complete ==="
335
+ echo "Pod is ready for vLLM deployments"
336
+ echo "Models will be cached at: $MODELS_PATH"
package/dist/ssh.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface SSHResult {
2
+ stdout: string;
3
+ stderr: string;
4
+ exitCode: number;
5
+ }
6
+ /**
7
+ * Execute an SSH command and return the result
8
+ */
9
+ export declare const sshExec: (sshCmd: string, command: string, options?: {
10
+ keepAlive?: boolean | undefined;
11
+ } | undefined) => Promise<SSHResult>;
12
+ /**
13
+ * Execute an SSH command with streaming output to console
14
+ */
15
+ export declare const sshExecStream: (sshCmd: string, command: string, options?: {
16
+ silent?: boolean | undefined;
17
+ forceTTY?: boolean | undefined;
18
+ keepAlive?: boolean | undefined;
19
+ } | undefined) => Promise<number>;
20
+ /**
21
+ * Copy a file to remote via SCP
22
+ */
23
+ export declare const scpFile: (sshCmd: string, localPath: string, remotePath: string) => Promise<boolean>;
24
+ //# sourceMappingURL=ssh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,eAAO,MAAM,OAAO;;oCAmDnB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa;;;;iCAwCzB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,6EAsCnB,CAAC","sourcesContent":["import { type SpawnOptions, spawn } from \"child_process\";\n\nexport interface SSHResult {\n\tstdout: string;\n\tstderr: string;\n\texitCode: number;\n}\n\n/**\n * Execute an SSH command and return the result\n */\nexport const sshExec = async (\n\tsshCmd: string,\n\tcommand: string,\n\toptions?: { keepAlive?: boolean },\n): Promise<SSHResult> => {\n\treturn new Promise((resolve) => {\n\t\t// Parse SSH command (e.g., \"ssh root@1.2.3.4\" or \"ssh -p 22 root@1.2.3.4\")\n\t\tconst sshParts = sshCmd.split(\" \").filter((p) => p);\n\t\tconst sshBinary = sshParts[0];\n\t\tlet sshArgs = [...sshParts.slice(1)];\n\n\t\t// Add SSH keepalive options for long-running commands\n\t\tif (options?.keepAlive) {\n\t\t\t// ServerAliveInterval=30 sends keepalive every 30 seconds\n\t\t\t// ServerAliveCountMax=120 allows up to 120 failures (60 minutes total)\n\t\t\tsshArgs = [\"-o\", \"ServerAliveInterval=30\", \"-o\", \"ServerAliveCountMax=120\", ...sshArgs];\n\t\t}\n\n\t\tsshArgs.push(command);\n\n\t\tconst proc = spawn(sshBinary, sshArgs, {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\texitCode: code || 0,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: err.message,\n\t\t\t\texitCode: 1,\n\t\t\t});\n\t\t});\n\t});\n};\n\n/**\n * Execute an SSH command with streaming output to console\n */\nexport const sshExecStream = async (\n\tsshCmd: string,\n\tcommand: string,\n\toptions?: { silent?: boolean; forceTTY?: boolean; keepAlive?: boolean },\n): Promise<number> => {\n\treturn new Promise((resolve) => {\n\t\tconst sshParts = sshCmd.split(\" \").filter((p) => p);\n\t\tconst sshBinary = sshParts[0];\n\n\t\t// Build SSH args\n\t\tlet sshArgs = [...sshParts.slice(1)];\n\n\t\t// Add -t flag if requested and not already present\n\t\tif (options?.forceTTY && !sshParts.includes(\"-t\")) {\n\t\t\tsshArgs = [\"-t\", ...sshArgs];\n\t\t}\n\n\t\t// Add SSH keepalive options for long-running commands\n\t\tif (options?.keepAlive) {\n\t\t\t// ServerAliveInterval=30 sends keepalive every 30 seconds\n\t\t\t// ServerAliveCountMax=120 allows up to 120 failures (60 minutes total)\n\t\t\tsshArgs = [\"-o\", \"ServerAliveInterval=30\", \"-o\", \"ServerAliveCountMax=120\", ...sshArgs];\n\t\t}\n\n\t\tsshArgs.push(command);\n\n\t\tconst spawnOptions: SpawnOptions = options?.silent\n\t\t\t? { stdio: [\"ignore\", \"ignore\", \"ignore\"] }\n\t\t\t: { stdio: \"inherit\" };\n\n\t\tconst proc = spawn(sshBinary, sshArgs, spawnOptions);\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve(code || 0);\n\t\t});\n\n\t\tproc.on(\"error\", () => {\n\t\t\tresolve(1);\n\t\t});\n\t});\n};\n\n/**\n * Copy a file to remote via SCP\n */\nexport const scpFile = async (sshCmd: string, localPath: string, remotePath: string): Promise<boolean> => {\n\t// Extract host from SSH command\n\tconst sshParts = sshCmd.split(\" \").filter((p) => p);\n\tlet host = \"\";\n\tlet port = \"22\";\n\tlet i = 1; // Skip 'ssh'\n\n\twhile (i < sshParts.length) {\n\t\tif (sshParts[i] === \"-p\" && i + 1 < sshParts.length) {\n\t\t\tport = sshParts[i + 1];\n\t\t\ti += 2;\n\t\t} else if (!sshParts[i].startsWith(\"-\")) {\n\t\t\thost = sshParts[i];\n\t\t\tbreak;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n\n\tif (!host) {\n\t\tconsole.error(\"Could not parse host from SSH command\");\n\t\treturn false;\n\t}\n\n\t// Build SCP command\n\tconst scpArgs = [\"-P\", port, localPath, `${host}:${remotePath}`];\n\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(\"scp\", scpArgs, { stdio: \"inherit\" });\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve(code === 0);\n\t\t});\n\n\t\tproc.on(\"error\", () => {\n\t\t\tresolve(false);\n\t\t});\n\t});\n};\n"]}
package/dist/ssh.js ADDED
@@ -0,0 +1,115 @@
1
+ import { spawn } from "child_process";
2
+ /**
3
+ * Execute an SSH command and return the result
4
+ */
5
+ export const sshExec = async (sshCmd, command, options) => {
6
+ return new Promise((resolve) => {
7
+ // Parse SSH command (e.g., "ssh root@1.2.3.4" or "ssh -p 22 root@1.2.3.4")
8
+ const sshParts = sshCmd.split(" ").filter((p) => p);
9
+ const sshBinary = sshParts[0];
10
+ let sshArgs = [...sshParts.slice(1)];
11
+ // Add SSH keepalive options for long-running commands
12
+ if (options?.keepAlive) {
13
+ // ServerAliveInterval=30 sends keepalive every 30 seconds
14
+ // ServerAliveCountMax=120 allows up to 120 failures (60 minutes total)
15
+ sshArgs = ["-o", "ServerAliveInterval=30", "-o", "ServerAliveCountMax=120", ...sshArgs];
16
+ }
17
+ sshArgs.push(command);
18
+ const proc = spawn(sshBinary, sshArgs, {
19
+ stdio: ["ignore", "pipe", "pipe"],
20
+ });
21
+ let stdout = "";
22
+ let stderr = "";
23
+ proc.stdout.on("data", (data) => {
24
+ stdout += data.toString();
25
+ });
26
+ proc.stderr.on("data", (data) => {
27
+ stderr += data.toString();
28
+ });
29
+ proc.on("close", (code) => {
30
+ resolve({
31
+ stdout,
32
+ stderr,
33
+ exitCode: code || 0,
34
+ });
35
+ });
36
+ proc.on("error", (err) => {
37
+ resolve({
38
+ stdout,
39
+ stderr: err.message,
40
+ exitCode: 1,
41
+ });
42
+ });
43
+ });
44
+ };
45
+ /**
46
+ * Execute an SSH command with streaming output to console
47
+ */
48
+ export const sshExecStream = async (sshCmd, command, options) => {
49
+ return new Promise((resolve) => {
50
+ const sshParts = sshCmd.split(" ").filter((p) => p);
51
+ const sshBinary = sshParts[0];
52
+ // Build SSH args
53
+ let sshArgs = [...sshParts.slice(1)];
54
+ // Add -t flag if requested and not already present
55
+ if (options?.forceTTY && !sshParts.includes("-t")) {
56
+ sshArgs = ["-t", ...sshArgs];
57
+ }
58
+ // Add SSH keepalive options for long-running commands
59
+ if (options?.keepAlive) {
60
+ // ServerAliveInterval=30 sends keepalive every 30 seconds
61
+ // ServerAliveCountMax=120 allows up to 120 failures (60 minutes total)
62
+ sshArgs = ["-o", "ServerAliveInterval=30", "-o", "ServerAliveCountMax=120", ...sshArgs];
63
+ }
64
+ sshArgs.push(command);
65
+ const spawnOptions = options?.silent
66
+ ? { stdio: ["ignore", "ignore", "ignore"] }
67
+ : { stdio: "inherit" };
68
+ const proc = spawn(sshBinary, sshArgs, spawnOptions);
69
+ proc.on("close", (code) => {
70
+ resolve(code || 0);
71
+ });
72
+ proc.on("error", () => {
73
+ resolve(1);
74
+ });
75
+ });
76
+ };
77
+ /**
78
+ * Copy a file to remote via SCP
79
+ */
80
+ export const scpFile = async (sshCmd, localPath, remotePath) => {
81
+ // Extract host from SSH command
82
+ const sshParts = sshCmd.split(" ").filter((p) => p);
83
+ let host = "";
84
+ let port = "22";
85
+ let i = 1; // Skip 'ssh'
86
+ while (i < sshParts.length) {
87
+ if (sshParts[i] === "-p" && i + 1 < sshParts.length) {
88
+ port = sshParts[i + 1];
89
+ i += 2;
90
+ }
91
+ else if (!sshParts[i].startsWith("-")) {
92
+ host = sshParts[i];
93
+ break;
94
+ }
95
+ else {
96
+ i++;
97
+ }
98
+ }
99
+ if (!host) {
100
+ console.error("Could not parse host from SSH command");
101
+ return false;
102
+ }
103
+ // Build SCP command
104
+ const scpArgs = ["-P", port, localPath, `${host}:${remotePath}`];
105
+ return new Promise((resolve) => {
106
+ const proc = spawn("scp", scpArgs, { stdio: "inherit" });
107
+ proc.on("close", (code) => {
108
+ resolve(code === 0);
109
+ });
110
+ proc.on("error", () => {
111
+ resolve(false);
112
+ });
113
+ });
114
+ };
115
+ //# sourceMappingURL=ssh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssh.js","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"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","sourcesContent":["import { type SpawnOptions, spawn } from \"child_process\";\n\nexport interface SSHResult {\n\tstdout: string;\n\tstderr: string;\n\texitCode: number;\n}\n\n/**\n * Execute an SSH command and return the result\n */\nexport const sshExec = async (\n\tsshCmd: string,\n\tcommand: string,\n\toptions?: { keepAlive?: boolean },\n): Promise<SSHResult> => {\n\treturn new Promise((resolve) => {\n\t\t// Parse SSH command (e.g., \"ssh root@1.2.3.4\" or \"ssh -p 22 root@1.2.3.4\")\n\t\tconst sshParts = sshCmd.split(\" \").filter((p) => p);\n\t\tconst sshBinary = sshParts[0];\n\t\tlet sshArgs = [...sshParts.slice(1)];\n\n\t\t// Add SSH keepalive options for long-running commands\n\t\tif (options?.keepAlive) {\n\t\t\t// ServerAliveInterval=30 sends keepalive every 30 seconds\n\t\t\t// ServerAliveCountMax=120 allows up to 120 failures (60 minutes total)\n\t\t\tsshArgs = [\"-o\", \"ServerAliveInterval=30\", \"-o\", \"ServerAliveCountMax=120\", ...sshArgs];\n\t\t}\n\n\t\tsshArgs.push(command);\n\n\t\tconst proc = spawn(sshBinary, sshArgs, {\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t});\n\n\t\tlet stdout = \"\";\n\t\tlet stderr = \"\";\n\n\t\tproc.stdout.on(\"data\", (data) => {\n\t\t\tstdout += data.toString();\n\t\t});\n\n\t\tproc.stderr.on(\"data\", (data) => {\n\t\t\tstderr += data.toString();\n\t\t});\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr,\n\t\t\t\texitCode: code || 0,\n\t\t\t});\n\t\t});\n\n\t\tproc.on(\"error\", (err) => {\n\t\t\tresolve({\n\t\t\t\tstdout,\n\t\t\t\tstderr: err.message,\n\t\t\t\texitCode: 1,\n\t\t\t});\n\t\t});\n\t});\n};\n\n/**\n * Execute an SSH command with streaming output to console\n */\nexport const sshExecStream = async (\n\tsshCmd: string,\n\tcommand: string,\n\toptions?: { silent?: boolean; forceTTY?: boolean; keepAlive?: boolean },\n): Promise<number> => {\n\treturn new Promise((resolve) => {\n\t\tconst sshParts = sshCmd.split(\" \").filter((p) => p);\n\t\tconst sshBinary = sshParts[0];\n\n\t\t// Build SSH args\n\t\tlet sshArgs = [...sshParts.slice(1)];\n\n\t\t// Add -t flag if requested and not already present\n\t\tif (options?.forceTTY && !sshParts.includes(\"-t\")) {\n\t\t\tsshArgs = [\"-t\", ...sshArgs];\n\t\t}\n\n\t\t// Add SSH keepalive options for long-running commands\n\t\tif (options?.keepAlive) {\n\t\t\t// ServerAliveInterval=30 sends keepalive every 30 seconds\n\t\t\t// ServerAliveCountMax=120 allows up to 120 failures (60 minutes total)\n\t\t\tsshArgs = [\"-o\", \"ServerAliveInterval=30\", \"-o\", \"ServerAliveCountMax=120\", ...sshArgs];\n\t\t}\n\n\t\tsshArgs.push(command);\n\n\t\tconst spawnOptions: SpawnOptions = options?.silent\n\t\t\t? { stdio: [\"ignore\", \"ignore\", \"ignore\"] }\n\t\t\t: { stdio: \"inherit\" };\n\n\t\tconst proc = spawn(sshBinary, sshArgs, spawnOptions);\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve(code || 0);\n\t\t});\n\n\t\tproc.on(\"error\", () => {\n\t\t\tresolve(1);\n\t\t});\n\t});\n};\n\n/**\n * Copy a file to remote via SCP\n */\nexport const scpFile = async (sshCmd: string, localPath: string, remotePath: string): Promise<boolean> => {\n\t// Extract host from SSH command\n\tconst sshParts = sshCmd.split(\" \").filter((p) => p);\n\tlet host = \"\";\n\tlet port = \"22\";\n\tlet i = 1; // Skip 'ssh'\n\n\twhile (i < sshParts.length) {\n\t\tif (sshParts[i] === \"-p\" && i + 1 < sshParts.length) {\n\t\t\tport = sshParts[i + 1];\n\t\t\ti += 2;\n\t\t} else if (!sshParts[i].startsWith(\"-\")) {\n\t\t\thost = sshParts[i];\n\t\t\tbreak;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n\n\tif (!host) {\n\t\tconsole.error(\"Could not parse host from SSH command\");\n\t\treturn false;\n\t}\n\n\t// Build SCP command\n\tconst scpArgs = [\"-P\", port, localPath, `${host}:${remotePath}`];\n\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(\"scp\", scpArgs, { stdio: \"inherit\" });\n\n\t\tproc.on(\"close\", (code) => {\n\t\t\tresolve(code === 0);\n\t\t});\n\n\t\tproc.on(\"error\", () => {\n\t\t\tresolve(false);\n\t\t});\n\t});\n};\n"]}
@@ -0,0 +1,23 @@
1
+ export interface GPU {
2
+ id: number;
3
+ name: string;
4
+ memory: string;
5
+ }
6
+ export interface Model {
7
+ model: string;
8
+ port: number;
9
+ gpu: number[];
10
+ pid: number;
11
+ }
12
+ export interface Pod {
13
+ ssh: string;
14
+ gpus: GPU[];
15
+ models: Record<string, Model>;
16
+ modelsPath?: string;
17
+ vllmVersion?: "release" | "nightly" | "gpt-oss";
18
+ }
19
+ export interface Config {
20
+ pods: Record<string, Pod>;
21
+ active?: string;
22
+ }
23
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,KAAK;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,GAAG;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;CAChD;AAED,MAAM,WAAW,MAAM;IACtB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB","sourcesContent":["// Core type definitions for pi\n\nexport interface GPU {\n\tid: number;\n\tname: string;\n\tmemory: string;\n}\n\nexport interface Model {\n\tmodel: string;\n\tport: number;\n\tgpu: number[]; // Array of GPU IDs for multi-GPU deployment\n\tpid: number;\n}\n\nexport interface Pod {\n\tssh: string;\n\tgpus: GPU[];\n\tmodels: Record<string, Model>;\n\tmodelsPath?: string;\n\tvllmVersion?: \"release\" | \"nightly\" | \"gpt-oss\"; // Track which vLLM version is installed\n}\n\nexport interface Config {\n\tpods: Record<string, Pod>;\n\tactive?: string;\n}\n"]}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // Core type definitions for pi
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,+BAA+B","sourcesContent":["// Core type definitions for pi\n\nexport interface GPU {\n\tid: number;\n\tname: string;\n\tmemory: string;\n}\n\nexport interface Model {\n\tmodel: string;\n\tport: number;\n\tgpu: number[]; // Array of GPU IDs for multi-GPU deployment\n\tpid: number;\n}\n\nexport interface Pod {\n\tssh: string;\n\tgpus: GPU[];\n\tmodels: Record<string, Model>;\n\tmodelsPath?: string;\n\tvllmVersion?: \"release\" | \"nightly\" | \"gpt-oss\"; // Track which vLLM version is installed\n}\n\nexport interface Config {\n\tpods: Record<string, Pod>;\n\tactive?: string;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@draht/pods",
3
+ "version": "2026.3.2-2",
4
+ "description": "CLI tool for managing vLLM deployments on GPU pods",
5
+ "type": "module",
6
+ "bin": {
7
+ "draht-pods": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "clean": "rm -rf dist",
11
+ "build": "tsgo -p tsconfig.build.json && cp src/models.json dist/ && cp -r scripts dist/",
12
+ "prepublishOnly": "bun run clean && bun run build"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "scripts"
17
+ ],
18
+ "keywords": [
19
+ "llm",
20
+ "vllm",
21
+ "gpu",
22
+ "ai",
23
+ "cli"
24
+ ],
25
+ "author": "Mario Zechner",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/badlogic/pi-mono.git",
30
+ "directory": "packages/pods"
31
+ },
32
+ "engines": {
33
+ "node": ">=20.0.0"
34
+ },
35
+ "dependencies": {
36
+ "@draht/agent-core": "2026.3.1-7",
37
+ "chalk": "^5.5.0"
38
+ },
39
+ "devDependencies": {}
40
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ # Model runner script - runs sequentially, killed by pi stop
3
+ set -euo pipefail
4
+
5
+ # These values are replaced before upload by pi CLI
6
+ MODEL_ID="{{MODEL_ID}}"
7
+ NAME="{{NAME}}"
8
+ PORT="{{PORT}}"
9
+ VLLM_ARGS="{{VLLM_ARGS}}"
10
+
11
+ # Trap to ensure cleanup on exit and kill any child processes
12
+ cleanup() {
13
+ local exit_code=$?
14
+ echo "Model runner exiting with code $exit_code"
15
+ # Kill any child processes
16
+ pkill -P $$ 2>/dev/null || true
17
+ exit $exit_code
18
+ }
19
+ trap cleanup EXIT TERM INT
20
+
21
+ # Force colored output even when not a TTY
22
+ export FORCE_COLOR=1
23
+ export PYTHONUNBUFFERED=1
24
+ export TERM=xterm-256color
25
+ export RICH_FORCE_TERMINAL=1
26
+ export CLICOLOR_FORCE=1
27
+
28
+ # Source virtual environment
29
+ source /root/venv/bin/activate
30
+
31
+ echo "========================================="
32
+ echo "Model Run: $NAME"
33
+ echo "Model ID: $MODEL_ID"
34
+ echo "Port: $PORT"
35
+ if [ -n "$VLLM_ARGS" ]; then
36
+ echo "vLLM Args: $VLLM_ARGS"
37
+ fi
38
+ echo "========================================="
39
+ echo ""
40
+
41
+ # Download model (with color progress bars)
42
+ echo "Downloading model (will skip if cached)..."
43
+ HF_HUB_ENABLE_HF_TRANSFER=1 hf download "$MODEL_ID"
44
+
45
+ if [ $? -ne 0 ]; then
46
+ echo "❌ ERROR: Failed to download model" >&2
47
+ exit 1
48
+ fi
49
+
50
+ echo ""
51
+ echo "✅ Model download complete"
52
+ echo ""
53
+
54
+ # Build vLLM command
55
+ VLLM_CMD="vllm serve '$MODEL_ID' --port $PORT --api-key '$PI_API_KEY'"
56
+ if [ -n "$VLLM_ARGS" ]; then
57
+ VLLM_CMD="$VLLM_CMD $VLLM_ARGS"
58
+ fi
59
+
60
+ echo "Starting vLLM server..."
61
+ echo "Command: $VLLM_CMD"
62
+ echo "========================================="
63
+ echo ""
64
+
65
+ # Run vLLM in background so we can monitor it
66
+ echo "Starting vLLM process..."
67
+ bash -c "$VLLM_CMD" &
68
+ VLLM_PID=$!
69
+
70
+ # Monitor the vLLM process
71
+ echo "Monitoring vLLM process (PID: $VLLM_PID)..."
72
+ wait $VLLM_PID
73
+ VLLM_EXIT_CODE=$?
74
+
75
+ if [ $VLLM_EXIT_CODE -ne 0 ]; then
76
+ echo "❌ ERROR: vLLM exited with code $VLLM_EXIT_CODE" >&2
77
+ # Make sure to exit the script command too
78
+ kill -TERM $$ 2>/dev/null || true
79
+ exit $VLLM_EXIT_CODE
80
+ fi
81
+
82
+ echo "✅ vLLM exited normally"
83
+ exit 0