@runpod/mcp-server 2.0.0 → 3.1.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/CHANGELOG.md +97 -2
- package/README.md +59 -309
- package/dist/chunk-OM6RHPQC.mjs +3023 -0
- package/dist/chunk-OM6RHPQC.mjs.map +1 -0
- package/dist/http.cjs +1621 -489
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +35 -2
- package/dist/http.d.ts +35 -2
- package/dist/http.mjs +247 -17
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +1420 -516
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +1420 -516
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +1293 -399
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +14 -0
- package/dist/tools.d.ts +14 -0
- package/dist/tools.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZP3TCVXG.mjs +0 -2116
- package/dist/chunk-ZP3TCVXG.mjs.map +0 -1
package/dist/stdio.mjs
CHANGED
|
@@ -6,81 +6,6 @@ import fetch2 from "node-fetch";
|
|
|
6
6
|
|
|
7
7
|
// src/server.ts
|
|
8
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
-
var SERVER_NAME = "Runpod API Server";
|
|
10
|
-
var SERVER_VERSION = true ? "2.0.0" : "dev";
|
|
11
|
-
function createServer(version = SERVER_VERSION) {
|
|
12
|
-
return new McpServer({
|
|
13
|
-
name: SERVER_NAME,
|
|
14
|
-
version,
|
|
15
|
-
capabilities: {
|
|
16
|
-
resources: {},
|
|
17
|
-
tools: {}
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// src/tools/runtime.ts
|
|
23
|
-
import fetch from "node-fetch";
|
|
24
|
-
import { randomUUID } from "crypto";
|
|
25
|
-
|
|
26
|
-
// src/_shared/http.ts
|
|
27
|
-
var HttpError = class extends Error {
|
|
28
|
-
constructor(prefix, status, body) {
|
|
29
|
-
super(`${prefix}: ${status} - ${body}`);
|
|
30
|
-
this.name = "HttpError";
|
|
31
|
-
this.status = status;
|
|
32
|
-
this.body = body;
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
function isJsonContentType(contentType) {
|
|
36
|
-
if (!contentType) return false;
|
|
37
|
-
const ct = contentType.toLowerCase();
|
|
38
|
-
return ct.includes("application/json") || ct.includes("+json");
|
|
39
|
-
}
|
|
40
|
-
function methodSendsBody(method) {
|
|
41
|
-
return method === "POST" || method === "PATCH" || method === "PUT";
|
|
42
|
-
}
|
|
43
|
-
function buildRequestHeaders(apiKey, tracking) {
|
|
44
|
-
return {
|
|
45
|
-
Authorization: `Bearer ${apiKey}`,
|
|
46
|
-
"Content-Type": "application/json",
|
|
47
|
-
...tracking()
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function createHttpClient(deps) {
|
|
51
|
-
return async function request(url, method = "GET", body) {
|
|
52
|
-
const init = {
|
|
53
|
-
method,
|
|
54
|
-
headers: buildRequestHeaders(deps.apiKey, deps.tracking)
|
|
55
|
-
};
|
|
56
|
-
if (body && methodSendsBody(method)) {
|
|
57
|
-
init.body = JSON.stringify(body);
|
|
58
|
-
}
|
|
59
|
-
const response = await deps.fetch(url, init);
|
|
60
|
-
if (!response.ok) {
|
|
61
|
-
throw new HttpError(
|
|
62
|
-
deps.errorPrefix,
|
|
63
|
-
response.status,
|
|
64
|
-
await response.text()
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// src/_shared/tracking.ts
|
|
72
|
-
function sanitizeUaToken(value) {
|
|
73
|
-
return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
|
|
74
|
-
}
|
|
75
|
-
function buildTrackingHeaders(input) {
|
|
76
|
-
const name = sanitizeUaToken(input.clientName || "unknown");
|
|
77
|
-
const version = sanitizeUaToken(input.clientVersion || "unknown");
|
|
78
|
-
const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
|
|
79
|
-
return {
|
|
80
|
-
"User-Agent": userAgent,
|
|
81
|
-
"X-Runpod-Session-Id": input.sessionId
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
9
|
|
|
85
10
|
// src/_shared/mappers.ts
|
|
86
11
|
function compact(obj) {
|
|
@@ -94,6 +19,9 @@ function persistentMount(size, path) {
|
|
|
94
19
|
if (size === void 0 || path === void 0) return void 0;
|
|
95
20
|
return { persistent: { size, path } };
|
|
96
21
|
}
|
|
22
|
+
function joinStartCmd(cmd) {
|
|
23
|
+
return cmd && cmd.length ? cmd.join(" ") : void 0;
|
|
24
|
+
}
|
|
97
25
|
function containerConfigToV2(p) {
|
|
98
26
|
return compact({
|
|
99
27
|
image: p.imageName,
|
|
@@ -112,6 +40,13 @@ function mapPodCreateToV2(params) {
|
|
|
112
40
|
return compact({
|
|
113
41
|
...containerConfigToV2(params),
|
|
114
42
|
name: params.name,
|
|
43
|
+
// Private-image registry credential (v2 ContainerConfig `registry`). When
|
|
44
|
+
// deploying from a template, this spreads over the template's registry, so
|
|
45
|
+
// passing it overrides the inherited credential. An empty string is an
|
|
46
|
+
// explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
|
|
47
|
+
// template's inherited credential. Undefined is dropped by compact, so an
|
|
48
|
+
// omitted param leaves the template's registry in place.
|
|
49
|
+
registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
|
|
115
50
|
cloud: params.cloudType,
|
|
116
51
|
// v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
|
|
117
52
|
// centers; omit/empty = scheduler chooses). Pass the v1 list straight
|
|
@@ -129,15 +64,27 @@ function mapPodUpdateToV2(params) {
|
|
|
129
64
|
...compact({ name: params.name })
|
|
130
65
|
};
|
|
131
66
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
67
|
+
var DEFAULT_ENDPOINT_TYPE = "QUEUE";
|
|
68
|
+
var DEFAULT_SCALER_VALUE = 4;
|
|
69
|
+
function defaultScalerType(endpointType) {
|
|
70
|
+
return endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
71
|
+
}
|
|
72
|
+
function endpointScaling(scalerType, value) {
|
|
73
|
+
return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
|
|
135
74
|
}
|
|
136
75
|
function endpointGpuConfig(pools, count) {
|
|
137
76
|
if (!pools?.length) return void 0;
|
|
138
77
|
return compact({ pools, count });
|
|
139
78
|
}
|
|
140
|
-
function
|
|
79
|
+
function endpointWorkers(params) {
|
|
80
|
+
const workers = compact({
|
|
81
|
+
min: params.workersMin,
|
|
82
|
+
max: params.workersMax,
|
|
83
|
+
idleTimeout: params.idleTimeout
|
|
84
|
+
});
|
|
85
|
+
return Object.keys(workers).length ? workers : void 0;
|
|
86
|
+
}
|
|
87
|
+
function endpointCommonToV2(params) {
|
|
141
88
|
return compact({
|
|
142
89
|
name: params.name,
|
|
143
90
|
image: params.imageName,
|
|
@@ -147,28 +94,39 @@ function mapEndpointToV2(params) {
|
|
|
147
94
|
env: params.env,
|
|
148
95
|
registry: params.containerRegistryAuthId,
|
|
149
96
|
gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
|
|
150
|
-
workers:
|
|
151
|
-
min: params.workersMin,
|
|
152
|
-
max: params.workersMax
|
|
153
|
-
}),
|
|
154
|
-
scaling: nestedOrUndefined({
|
|
155
|
-
type: params.scalerType,
|
|
156
|
-
value: params.scalerValue,
|
|
157
|
-
idleTimeout: params.idleTimeout
|
|
158
|
-
}),
|
|
97
|
+
workers: endpointWorkers(params),
|
|
159
98
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
160
99
|
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
161
100
|
timeout: params.executionTimeoutMs,
|
|
162
101
|
flashboot: params.flashboot
|
|
163
102
|
});
|
|
164
103
|
}
|
|
165
|
-
|
|
166
|
-
|
|
104
|
+
function mapEndpointCreateToV2(params) {
|
|
105
|
+
const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
|
|
106
|
+
return compact({
|
|
107
|
+
...endpointCommonToV2(params),
|
|
108
|
+
type: endpointType,
|
|
109
|
+
scaling: endpointScaling(
|
|
110
|
+
params.scalerType ?? defaultScalerType(endpointType),
|
|
111
|
+
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
112
|
+
)
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function mapEndpointUpdateToV2(params) {
|
|
116
|
+
return compact({
|
|
117
|
+
...endpointCommonToV2(params),
|
|
118
|
+
scaling: params.scalerType ? endpointScaling(
|
|
119
|
+
params.scalerType,
|
|
120
|
+
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
121
|
+
) : void 0
|
|
122
|
+
});
|
|
123
|
+
}
|
|
167
124
|
function mapNetworkVolumeCreateToV2(params) {
|
|
168
125
|
return compact({
|
|
169
126
|
name: params.name,
|
|
170
127
|
size: params.size,
|
|
171
|
-
dataCenter: params.dataCenterId
|
|
128
|
+
dataCenter: params.dataCenterId,
|
|
129
|
+
type: params.volumeType
|
|
172
130
|
});
|
|
173
131
|
}
|
|
174
132
|
function mapTemplateCreateToV2(params) {
|
|
@@ -177,18 +135,39 @@ function mapTemplateCreateToV2(params) {
|
|
|
177
135
|
...compact({
|
|
178
136
|
name: params.name,
|
|
179
137
|
serverless: params.isServerless,
|
|
180
|
-
registry: params.containerRegistryAuthId
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
138
|
+
registry: params.containerRegistryAuthId,
|
|
139
|
+
args: joinStartCmd(params.dockerStartCmd),
|
|
140
|
+
// No longer required by v2 (it defaults to NVIDIA server-side), so an
|
|
141
|
+
// unset category is dropped by compact instead of being forced here.
|
|
142
|
+
category: params.category
|
|
143
|
+
})
|
|
184
144
|
};
|
|
185
145
|
}
|
|
146
|
+
function podBodyFromTemplate(template) {
|
|
147
|
+
const out = {};
|
|
148
|
+
const keys = [
|
|
149
|
+
"name",
|
|
150
|
+
"image",
|
|
151
|
+
"args",
|
|
152
|
+
"disk",
|
|
153
|
+
"ports",
|
|
154
|
+
"env",
|
|
155
|
+
"mounts",
|
|
156
|
+
"registry"
|
|
157
|
+
];
|
|
158
|
+
for (const key of keys) {
|
|
159
|
+
const value = template[key];
|
|
160
|
+
if (value !== void 0 && value !== null) out[key] = value;
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
186
164
|
function mapTemplateUpdateToV2(params) {
|
|
187
165
|
return {
|
|
188
166
|
...containerConfigToV2(params),
|
|
189
167
|
...compact({
|
|
190
168
|
name: params.name,
|
|
191
|
-
registry: params.containerRegistryAuthId
|
|
169
|
+
registry: params.containerRegistryAuthId,
|
|
170
|
+
args: joinStartCmd(params.dockerStartCmd)
|
|
192
171
|
})
|
|
193
172
|
};
|
|
194
173
|
}
|
|
@@ -205,7 +184,6 @@ var V2_UNWRAP_KEY = {
|
|
|
205
184
|
dataCenters: "dataCenters",
|
|
206
185
|
endpoints: "endpoints",
|
|
207
186
|
jobs: "jobs",
|
|
208
|
-
tags: "tags",
|
|
209
187
|
workers: "workers",
|
|
210
188
|
billing: "records"
|
|
211
189
|
};
|
|
@@ -219,7 +197,6 @@ var RESOURCE_ENV_KEY = {
|
|
|
219
197
|
dataCenters: "DATA_CENTERS",
|
|
220
198
|
endpoints: "ENDPOINTS",
|
|
221
199
|
jobs: "JOBS",
|
|
222
|
-
tags: "TAGS",
|
|
223
200
|
workers: "WORKERS",
|
|
224
201
|
billing: "BILLING"
|
|
225
202
|
};
|
|
@@ -235,6 +212,9 @@ function serverlessBase(env) {
|
|
|
235
212
|
function publicGraphqlBase(env) {
|
|
236
213
|
return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
237
214
|
}
|
|
215
|
+
function authedGraphqlBase(env) {
|
|
216
|
+
return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
217
|
+
}
|
|
238
218
|
function unwrapList(resource, version, raw) {
|
|
239
219
|
if (version === "v1") return Array.isArray(raw) ? raw : [];
|
|
240
220
|
if (raw === null || typeof raw !== "object") return [];
|
|
@@ -258,6 +238,14 @@ function wantsAutoProbe(env) {
|
|
|
258
238
|
([k, v]) => k.startsWith("RUNPOD_REST_VERSION") && (v ?? "").toLowerCase() === "auto"
|
|
259
239
|
);
|
|
260
240
|
}
|
|
241
|
+
function restVersionSummary(env) {
|
|
242
|
+
const raw = (env.RUNPOD_REST_VERSION ?? "").trim().toLowerCase();
|
|
243
|
+
const base = raw ? `RUNPOD_REST_VERSION=${raw}` : "RUNPOD_REST_VERSION unset (default v2)";
|
|
244
|
+
const overrides = Object.keys(env).filter(
|
|
245
|
+
(k) => k.startsWith("RUNPOD_REST_VERSION_") && (env[k] ?? "").trim() !== ""
|
|
246
|
+
).length;
|
|
247
|
+
return overrides > 0 ? `${base}, +${overrides} per-resource` : base;
|
|
248
|
+
}
|
|
261
249
|
var PROBE_TIMEOUT_MS = 4e3;
|
|
262
250
|
function classifyProbeStatus(status) {
|
|
263
251
|
if (status >= 500) return void 0;
|
|
@@ -323,7 +311,6 @@ var CATALOG = /* @__PURE__ */ new Set([
|
|
|
323
311
|
"dataCenters"
|
|
324
312
|
]);
|
|
325
313
|
var V2_ONLY = /* @__PURE__ */ new Set([
|
|
326
|
-
"tags",
|
|
327
314
|
"workers",
|
|
328
315
|
"billing"
|
|
329
316
|
]);
|
|
@@ -341,7 +328,6 @@ var V2_REST_PATHS = {
|
|
|
341
328
|
list: "/catalog/datacenters",
|
|
342
329
|
get: (id) => `/catalog/datacenters/${id}`
|
|
343
330
|
},
|
|
344
|
-
tags: { list: "/tags", get: (id) => `/tags/${id}` },
|
|
345
331
|
// Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
|
|
346
332
|
// response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
|
|
347
333
|
// unwrap key stays correct.
|
|
@@ -438,8 +424,186 @@ function resolveBackend(opts) {
|
|
|
438
424
|
return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
|
|
439
425
|
}
|
|
440
426
|
|
|
427
|
+
// src/server.ts
|
|
428
|
+
var SERVER_NAME = "Runpod API Server";
|
|
429
|
+
var SERVER_VERSION = true ? "3.1.0" : "dev";
|
|
430
|
+
function createServer(version = SERVER_VERSION) {
|
|
431
|
+
return new McpServer({
|
|
432
|
+
name: SERVER_NAME,
|
|
433
|
+
version: `${version} [${restVersionSummary(process.env)}]`,
|
|
434
|
+
capabilities: {
|
|
435
|
+
resources: {},
|
|
436
|
+
tools: {}
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/tools/runtime.ts
|
|
442
|
+
import fetch from "node-fetch";
|
|
443
|
+
import { randomUUID } from "crypto";
|
|
444
|
+
|
|
445
|
+
// src/_shared/http.ts
|
|
446
|
+
var HttpError = class extends Error {
|
|
447
|
+
constructor(prefix, status, body) {
|
|
448
|
+
const hint = status === 401 ? " (the Runpod API rejected the credential \u2014 the API key may be expired or revoked; re-authenticate or update RUNPOD_API_KEY)" : "";
|
|
449
|
+
super(`${prefix}: ${status} - ${body}${hint}`);
|
|
450
|
+
this.name = "HttpError";
|
|
451
|
+
this.status = status;
|
|
452
|
+
this.body = body;
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
function isJsonContentType(contentType) {
|
|
456
|
+
if (!contentType) return false;
|
|
457
|
+
const ct = contentType.toLowerCase();
|
|
458
|
+
return ct.includes("application/json") || ct.includes("+json");
|
|
459
|
+
}
|
|
460
|
+
function methodSendsBody(method) {
|
|
461
|
+
return method === "POST" || method === "PATCH" || method === "PUT";
|
|
462
|
+
}
|
|
463
|
+
function buildRequestHeaders(apiKey, tracking) {
|
|
464
|
+
return {
|
|
465
|
+
Authorization: `Bearer ${apiKey}`,
|
|
466
|
+
"Content-Type": "application/json",
|
|
467
|
+
...tracking()
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function createHttpClient(deps) {
|
|
471
|
+
return async function request(url, method = "GET", body) {
|
|
472
|
+
const init = {
|
|
473
|
+
method,
|
|
474
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking)
|
|
475
|
+
};
|
|
476
|
+
if (body && methodSendsBody(method)) {
|
|
477
|
+
init.body = JSON.stringify(body);
|
|
478
|
+
}
|
|
479
|
+
const response = await deps.fetch(url, init);
|
|
480
|
+
if (!response.ok) {
|
|
481
|
+
throw new HttpError(
|
|
482
|
+
deps.errorPrefix,
|
|
483
|
+
response.status,
|
|
484
|
+
await response.text()
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/_shared/tracking.ts
|
|
492
|
+
function sanitizeUaToken(value) {
|
|
493
|
+
return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
|
|
494
|
+
}
|
|
495
|
+
function buildTrackingHeaders(input) {
|
|
496
|
+
const name = sanitizeUaToken(input.clientName || "unknown");
|
|
497
|
+
const version = sanitizeUaToken(input.clientVersion || "unknown");
|
|
498
|
+
const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
|
|
499
|
+
return {
|
|
500
|
+
"User-Agent": userAgent,
|
|
501
|
+
"X-Runpod-Session-Id": input.sessionId
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/tools/logs.ts
|
|
506
|
+
import { z } from "zod";
|
|
507
|
+
function parseLogSse(raw) {
|
|
508
|
+
const items = [];
|
|
509
|
+
for (const block of raw.split(/\r?\n\r?\n/)) {
|
|
510
|
+
const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
|
|
511
|
+
if (!dataLines.length) continue;
|
|
512
|
+
const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
513
|
+
if (!payload.trim()) continue;
|
|
514
|
+
try {
|
|
515
|
+
const parsed = JSON.parse(payload);
|
|
516
|
+
items.push(
|
|
517
|
+
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
|
|
518
|
+
);
|
|
519
|
+
} catch {
|
|
520
|
+
items.push({ raw: payload });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return items;
|
|
524
|
+
}
|
|
525
|
+
var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
|
|
526
|
+
var LOG_STREAM_MAX_BYTES = 256 * 1024;
|
|
527
|
+
async function readSseSnapshot(fetchImpl, url, headers, opts) {
|
|
528
|
+
const controller = new AbortController();
|
|
529
|
+
const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
|
|
530
|
+
const chunks = [];
|
|
531
|
+
let bytes = 0;
|
|
532
|
+
let truncated = false;
|
|
533
|
+
try {
|
|
534
|
+
const res = await fetchImpl(url, {
|
|
535
|
+
method: "GET",
|
|
536
|
+
headers,
|
|
537
|
+
signal: controller.signal
|
|
538
|
+
});
|
|
539
|
+
if (!res.ok) {
|
|
540
|
+
throw new HttpError(
|
|
541
|
+
"Runpod API Error",
|
|
542
|
+
res.status,
|
|
543
|
+
await res.text().catch(() => "")
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
for await (const chunk of res.body ?? []) {
|
|
547
|
+
chunks.push(chunk);
|
|
548
|
+
bytes += chunk.length;
|
|
549
|
+
if (bytes >= opts.maxBytes) {
|
|
550
|
+
truncated = true;
|
|
551
|
+
controller.abort();
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} catch (err) {
|
|
556
|
+
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
557
|
+
} finally {
|
|
558
|
+
clearTimeout(timer);
|
|
559
|
+
}
|
|
560
|
+
return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
561
|
+
}
|
|
562
|
+
var logStreamParams = {
|
|
563
|
+
source: z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
|
|
564
|
+
tail: z.number().int().min(0).max(5e3).optional().describe(
|
|
565
|
+
"Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
|
|
566
|
+
),
|
|
567
|
+
since: z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
|
|
568
|
+
maxWaitMs: z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
|
|
569
|
+
};
|
|
570
|
+
async function collectLogSnapshot(streamSse, logsUrl, params) {
|
|
571
|
+
const qs = new URLSearchParams();
|
|
572
|
+
if (params.source && params.source !== "both")
|
|
573
|
+
qs.append("source", params.source);
|
|
574
|
+
if (params.tail !== void 0) qs.append("tail", String(params.tail));
|
|
575
|
+
if (params.since) qs.append("since", params.since);
|
|
576
|
+
const query = qs.toString() ? `?${qs}` : "";
|
|
577
|
+
const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
|
|
578
|
+
maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
|
|
579
|
+
maxBytes: LOG_STREAM_MAX_BYTES
|
|
580
|
+
});
|
|
581
|
+
const items = parseLogSse(raw);
|
|
582
|
+
if (truncated) items.pop();
|
|
583
|
+
return { items, count: items.length, truncated };
|
|
584
|
+
}
|
|
585
|
+
async function streamLogsReply(rt, tool, params) {
|
|
586
|
+
const backend = rt.backendFor(tool.resource);
|
|
587
|
+
if (backend.version === "v1") {
|
|
588
|
+
return rt.jsonReply({
|
|
589
|
+
error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
590
|
+
status: 501
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
return rt.jsonReply(
|
|
595
|
+
await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
|
|
596
|
+
);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (error instanceof HttpError) {
|
|
599
|
+
return rt.jsonReply({ error: error.message, status: error.status });
|
|
600
|
+
}
|
|
601
|
+
throw error;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
441
605
|
// src/tools/runtime.ts
|
|
442
|
-
var MCP_SERVER_VERSION = true ? "
|
|
606
|
+
var MCP_SERVER_VERSION = true ? "3.1.0" : "dev";
|
|
443
607
|
var SESSION_ID = randomUUID();
|
|
444
608
|
function trackingHeaders(server, ctx) {
|
|
445
609
|
const info = server.server.getClientVersion();
|
|
@@ -476,14 +640,18 @@ var DESTRUCTIVE = {
|
|
|
476
640
|
idempotentHint: true,
|
|
477
641
|
openWorldHint: true
|
|
478
642
|
};
|
|
479
|
-
async function graphqlRequest(query, tracking, fetchImpl, url) {
|
|
643
|
+
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
480
644
|
const response = await fetchImpl(url, {
|
|
481
645
|
method: "POST",
|
|
482
646
|
headers: {
|
|
483
647
|
"Content-Type": "application/json",
|
|
648
|
+
...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
484
649
|
...tracking()
|
|
485
650
|
},
|
|
486
|
-
body: JSON.stringify({
|
|
651
|
+
body: JSON.stringify({
|
|
652
|
+
query,
|
|
653
|
+
...options?.variables ? { variables: options.variables } : {}
|
|
654
|
+
})
|
|
487
655
|
});
|
|
488
656
|
const result = await response.json();
|
|
489
657
|
if (result.errors && result.errors.length > 0) {
|
|
@@ -498,7 +666,17 @@ async function graphqlRequest(query, tracking, fetchImpl, url) {
|
|
|
498
666
|
}
|
|
499
667
|
function createToolRuntime(server, ctx, deps = {}) {
|
|
500
668
|
const tracking = () => trackingHeaders(server, ctx);
|
|
501
|
-
|
|
669
|
+
function observeUnauthorized(fn) {
|
|
670
|
+
const notify = ctx.onUnauthorized;
|
|
671
|
+
if (!notify) return fn;
|
|
672
|
+
return async (...args) => {
|
|
673
|
+
const response = await fn(...args);
|
|
674
|
+
if (response.status === 401) notify();
|
|
675
|
+
return response;
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
const rawFetch = deps.fetch ?? defaultFetch;
|
|
679
|
+
const httpFetch = observeUnauthorized(rawFetch);
|
|
502
680
|
const v1Client = createHttpClient({
|
|
503
681
|
apiKey: ctx.apiKey,
|
|
504
682
|
fetch: httpFetch,
|
|
@@ -533,47 +711,19 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
533
711
|
"Error calling Runpod API:",
|
|
534
712
|
() => restClient(url, method, body)
|
|
535
713
|
);
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (!res.ok) {
|
|
550
|
-
throw new HttpError(
|
|
551
|
-
"Runpod API Error",
|
|
552
|
-
res.status,
|
|
553
|
-
await res.text().catch(() => "")
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
const chunks = [];
|
|
557
|
-
let bytes = 0;
|
|
558
|
-
let truncated = false;
|
|
559
|
-
try {
|
|
560
|
-
for await (const chunk of res.body) {
|
|
561
|
-
chunks.push(chunk);
|
|
562
|
-
bytes += chunk.length;
|
|
563
|
-
if (bytes >= opts.maxBytes) {
|
|
564
|
-
truncated = true;
|
|
565
|
-
controller.abort();
|
|
566
|
-
break;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
} catch (err) {
|
|
570
|
-
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
571
|
-
}
|
|
572
|
-
return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
573
|
-
} finally {
|
|
574
|
-
clearTimeout(timer);
|
|
575
|
-
}
|
|
576
|
-
});
|
|
714
|
+
const sseFetch = observeUnauthorized(
|
|
715
|
+
deps.sseFetch ?? fetch
|
|
716
|
+
);
|
|
717
|
+
const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
|
|
718
|
+
sseFetch,
|
|
719
|
+
url,
|
|
720
|
+
{
|
|
721
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
722
|
+
Accept: "text/event-stream",
|
|
723
|
+
...tracking()
|
|
724
|
+
},
|
|
725
|
+
opts
|
|
726
|
+
));
|
|
577
727
|
const backendFor = (resource) => resolveBackend({
|
|
578
728
|
resource,
|
|
579
729
|
env: process.env,
|
|
@@ -595,9 +745,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
595
745
|
graphql: (query) => graphqlRequest(
|
|
596
746
|
query,
|
|
597
747
|
tracking,
|
|
598
|
-
httpFetch
|
|
748
|
+
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
749
|
+
rawFetch,
|
|
599
750
|
publicGraphqlBase(process.env)
|
|
600
751
|
),
|
|
752
|
+
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
753
|
+
query,
|
|
754
|
+
tracking,
|
|
755
|
+
httpFetch,
|
|
756
|
+
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
757
|
+
authedGraphqlBase(process.env),
|
|
758
|
+
{ variables, apiKey: ctx.apiKey }
|
|
759
|
+
),
|
|
601
760
|
runpodRequest,
|
|
602
761
|
serverlessRequest,
|
|
603
762
|
callRestUrl,
|
|
@@ -609,17 +768,17 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
609
768
|
}
|
|
610
769
|
|
|
611
770
|
// src/tools/catalog.ts
|
|
612
|
-
import { z as
|
|
771
|
+
import { z as z3 } from "zod";
|
|
613
772
|
|
|
614
773
|
// src/pagination.ts
|
|
615
|
-
import { z } from "zod";
|
|
774
|
+
import { z as z2 } from "zod";
|
|
616
775
|
var DEFAULT_LIST_LIMIT = 20;
|
|
617
776
|
var MAX_LIST_LIMIT = 100;
|
|
618
777
|
var listPaginationParams = {
|
|
619
|
-
limit:
|
|
778
|
+
limit: z2.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
|
|
620
779
|
`Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
|
|
621
780
|
),
|
|
622
|
-
cursor:
|
|
781
|
+
cursor: z2.string().optional().describe(
|
|
623
782
|
"Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
|
|
624
783
|
)
|
|
625
784
|
};
|
|
@@ -677,25 +836,32 @@ function registerCatalogTools(server, rt) {
|
|
|
677
836
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
678
837
|
server.tool(
|
|
679
838
|
"list-gpu-types",
|
|
680
|
-
"List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint.
|
|
839
|
+
"List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint. By default the full catalog is returned: each result includes an `availability` summary (HIGH/MEDIUM/LOW/NONE) and results are sorted with the most-available GPUs first, but nothing is hidden. Set includeUnavailable:false to drop out-of-stock GPUs and list only deployable ones (set includeAvailability:false to skip the stock lookup entirely). For per-datacenter availability (to pick a dataCenterIds), use get-gpu-type.",
|
|
681
840
|
{
|
|
682
841
|
...listPaginationParams,
|
|
683
|
-
minMemoryGb:
|
|
684
|
-
secureCloudOnly:
|
|
685
|
-
communityCloudOnly:
|
|
686
|
-
searchTerm:
|
|
842
|
+
minMemoryGb: z3.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
|
|
843
|
+
secureCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
|
|
844
|
+
communityCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in community cloud"),
|
|
845
|
+
searchTerm: z3.string().optional().describe(
|
|
687
846
|
"Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
|
|
688
847
|
),
|
|
689
|
-
includeUnavailable:
|
|
690
|
-
"
|
|
848
|
+
includeUnavailable: z3.boolean().optional().describe(
|
|
849
|
+
"Out-of-stock GPUs are included by default (annotated availability:NONE and sorted last). Set false to hide them and list only currently-deployable GPUs."
|
|
850
|
+
),
|
|
851
|
+
includeAvailability: z3.boolean().optional().describe(
|
|
852
|
+
"Request realtime stock and annotate each GPU with an availability summary (HIGH/MEDIUM/LOW/NONE). Default true. Set false to skip the availability lookup \u2014 then out-of-stock GPUs cannot be filtered."
|
|
691
853
|
)
|
|
692
854
|
},
|
|
693
855
|
{ title: "List GPU types", ...READ_ONLY },
|
|
694
856
|
async (params) => {
|
|
695
857
|
const backend = backendFor("gpus");
|
|
696
858
|
if (backend.version === "v2") {
|
|
697
|
-
const
|
|
859
|
+
const wantAvailability = params.includeAvailability !== false;
|
|
860
|
+
const raw = await callRestUrl(
|
|
861
|
+
`${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
|
|
862
|
+
);
|
|
698
863
|
let gpus = backend.unwrap(raw);
|
|
864
|
+
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
699
865
|
if (params.minMemoryGb !== void 0)
|
|
700
866
|
gpus = gpus.filter(
|
|
701
867
|
(g) => Number(g.memory ?? 0) >= params.minMemoryGb
|
|
@@ -708,6 +874,18 @@ function registerCatalogTools(server, rt) {
|
|
|
708
874
|
(g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
|
|
709
875
|
);
|
|
710
876
|
}
|
|
877
|
+
if (params.includeUnavailable === false)
|
|
878
|
+
gpus = gpus.filter((g) => g.availability !== "NONE");
|
|
879
|
+
const rank = {
|
|
880
|
+
HIGH: 3,
|
|
881
|
+
MEDIUM: 2,
|
|
882
|
+
LOW: 1,
|
|
883
|
+
NONE: 0
|
|
884
|
+
};
|
|
885
|
+
gpus.sort(
|
|
886
|
+
(a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
|
|
887
|
+
);
|
|
888
|
+
gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
|
|
711
889
|
return capListResult(gpus, {
|
|
712
890
|
limit: params.limit,
|
|
713
891
|
cursor: params.cursor
|
|
@@ -737,7 +915,7 @@ function registerCatalogTools(server, rt) {
|
|
|
737
915
|
return !!status && status !== "Out";
|
|
738
916
|
};
|
|
739
917
|
let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
|
|
740
|
-
if (
|
|
918
|
+
if (params.includeUnavailable === false) {
|
|
741
919
|
gpuTypes = gpuTypes.filter(isAvailable);
|
|
742
920
|
}
|
|
743
921
|
if (params.minMemoryGb) {
|
|
@@ -783,7 +961,7 @@ function registerCatalogTools(server, rt) {
|
|
|
783
961
|
"List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
|
|
784
962
|
{
|
|
785
963
|
...listPaginationParams,
|
|
786
|
-
region:
|
|
964
|
+
region: z3.string().optional().describe(
|
|
787
965
|
"Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
|
|
788
966
|
)
|
|
789
967
|
},
|
|
@@ -839,80 +1017,654 @@ function registerCatalogTools(server, rt) {
|
|
|
839
1017
|
status: 501
|
|
840
1018
|
});
|
|
841
1019
|
}
|
|
842
|
-
const raw = await callRestUrl(`${backend.base}${backend.list}`);
|
|
843
|
-
return capListResult(backend.unwrap(raw), {
|
|
1020
|
+
const raw = await callRestUrl(`${backend.base}${backend.list}`);
|
|
1021
|
+
return capListResult(backend.unwrap(raw), {
|
|
1022
|
+
limit: params.limit,
|
|
1023
|
+
cursor: params.cursor
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
server.tool(
|
|
1028
|
+
"get-gpu-type",
|
|
1029
|
+
"Get details for a single GPU type by id, including per-datacenter availability. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there). Use the returned dataCenters[].availability to pick a dataCenterIds with stock before creating a pod.",
|
|
1030
|
+
{
|
|
1031
|
+
gpuTypeId: z3.string().describe("ID of the GPU type to retrieve"),
|
|
1032
|
+
includeAvailability: z3.boolean().optional().describe(
|
|
1033
|
+
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1034
|
+
)
|
|
1035
|
+
},
|
|
1036
|
+
{ title: "Get GPU type", ...READ_ONLY },
|
|
1037
|
+
async (params) => {
|
|
1038
|
+
const backend = backendFor("gpus");
|
|
1039
|
+
if (backend.version === "v1") {
|
|
1040
|
+
return jsonReply2({
|
|
1041
|
+
error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
|
|
1042
|
+
status: 501
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1046
|
+
const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
|
|
1047
|
+
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1048
|
+
return jsonReply2(result);
|
|
1049
|
+
}
|
|
1050
|
+
);
|
|
1051
|
+
server.tool(
|
|
1052
|
+
"get-cpu-type",
|
|
1053
|
+
"Get details for a single CPU flavor type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-cpu-types there).",
|
|
1054
|
+
{
|
|
1055
|
+
cpuTypeId: z3.string().describe("ID of the CPU type to retrieve")
|
|
1056
|
+
},
|
|
1057
|
+
{ title: "Get CPU type", ...READ_ONLY },
|
|
1058
|
+
async (params) => {
|
|
1059
|
+
const backend = backendFor("cpus");
|
|
1060
|
+
if (backend.version === "v1") {
|
|
1061
|
+
return jsonReply2({
|
|
1062
|
+
error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
1063
|
+
status: 501
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
const result = await callRestUrl(
|
|
1067
|
+
`${backend.base}${backend.get(params.cpuTypeId)}`
|
|
1068
|
+
);
|
|
1069
|
+
return jsonReply2(result);
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
server.tool(
|
|
1073
|
+
"get-data-center",
|
|
1074
|
+
"Get details for a single data center by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-data-centers there).",
|
|
1075
|
+
{
|
|
1076
|
+
dataCenterId: z3.string().describe("ID of the data center to retrieve")
|
|
1077
|
+
},
|
|
1078
|
+
{ title: "Get data center", ...READ_ONLY },
|
|
1079
|
+
async (params) => {
|
|
1080
|
+
const backend = backendFor("dataCenters");
|
|
1081
|
+
if (backend.version === "v1") {
|
|
1082
|
+
return jsonReply2({
|
|
1083
|
+
error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
1084
|
+
status: 501
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
const result = await callRestUrl(
|
|
1088
|
+
`${backend.base}${backend.get(params.dataCenterId)}`
|
|
1089
|
+
);
|
|
1090
|
+
return jsonReply2(result);
|
|
1091
|
+
}
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/tools/hub.ts
|
|
1096
|
+
import { z as z4 } from "zod";
|
|
1097
|
+
function parseReleaseConfig(config) {
|
|
1098
|
+
if (!config) return void 0;
|
|
1099
|
+
try {
|
|
1100
|
+
return JSON.parse(config);
|
|
1101
|
+
} catch {
|
|
1102
|
+
return config;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function listingsQuery(withConfig) {
|
|
1106
|
+
return `
|
|
1107
|
+
query {
|
|
1108
|
+
listings(input: {}) {
|
|
1109
|
+
id
|
|
1110
|
+
repoId
|
|
1111
|
+
title
|
|
1112
|
+
description
|
|
1113
|
+
repoName
|
|
1114
|
+
repoOwner
|
|
1115
|
+
createdAt
|
|
1116
|
+
updatedAt
|
|
1117
|
+
views
|
|
1118
|
+
stars
|
|
1119
|
+
deploys
|
|
1120
|
+
language
|
|
1121
|
+
category
|
|
1122
|
+
tags
|
|
1123
|
+
type
|
|
1124
|
+
listedRelease {
|
|
1125
|
+
id
|
|
1126
|
+
name
|
|
1127
|
+
tagName
|
|
1128
|
+
createdAt${withConfig ? "\n config" : ""}
|
|
1129
|
+
build {
|
|
1130
|
+
id
|
|
1131
|
+
imageName
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
`;
|
|
1137
|
+
}
|
|
1138
|
+
function serializeBoolean(value, input) {
|
|
1139
|
+
if (value === "true") return input.trueValue ?? "true";
|
|
1140
|
+
if (value === "false") return input.falseValue ?? "false";
|
|
1141
|
+
return value;
|
|
1142
|
+
}
|
|
1143
|
+
function buildHubEnv(config, overrides) {
|
|
1144
|
+
const env = [];
|
|
1145
|
+
const missingRequired = [];
|
|
1146
|
+
const remaining = { ...overrides };
|
|
1147
|
+
for (const entry of config.env ?? []) {
|
|
1148
|
+
const input = entry.input ?? {};
|
|
1149
|
+
const override = entry.key in remaining ? remaining[entry.key] : void 0;
|
|
1150
|
+
delete remaining[entry.key];
|
|
1151
|
+
if (override !== void 0 && override !== "") {
|
|
1152
|
+
env.push({
|
|
1153
|
+
key: entry.key,
|
|
1154
|
+
value: input.type === "boolean" ? serializeBoolean(override, input) : override
|
|
1155
|
+
});
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (input.default !== void 0 && input.default !== null) {
|
|
1159
|
+
env.push({
|
|
1160
|
+
key: entry.key,
|
|
1161
|
+
value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
|
|
1162
|
+
});
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
if (input.required) {
|
|
1166
|
+
missingRequired.push(entry.key);
|
|
1167
|
+
env.push({ key: entry.key, value: "" });
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
for (const [key, value] of Object.entries(remaining)) {
|
|
1172
|
+
env.push({ key, value });
|
|
1173
|
+
}
|
|
1174
|
+
return { env, missingRequired };
|
|
1175
|
+
}
|
|
1176
|
+
function minCudaVersion(versions) {
|
|
1177
|
+
if (!versions || versions.length === 0) return null;
|
|
1178
|
+
const compare = (a, b) => {
|
|
1179
|
+
const pa = a.split(".").map(Number);
|
|
1180
|
+
const pb = b.split(".").map(Number);
|
|
1181
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1182
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1183
|
+
if (d !== 0) return d;
|
|
1184
|
+
}
|
|
1185
|
+
return 0;
|
|
1186
|
+
};
|
|
1187
|
+
return [...versions].sort(compare)[0];
|
|
1188
|
+
}
|
|
1189
|
+
function randomSuffix() {
|
|
1190
|
+
return Math.random().toString(36).slice(2, 8);
|
|
1191
|
+
}
|
|
1192
|
+
function registerHubTools(server, rt) {
|
|
1193
|
+
const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
1194
|
+
server.tool(
|
|
1195
|
+
"list-hub-repos",
|
|
1196
|
+
"List repos published to the Runpod Hub (prebuilt Serverless workers and Pod templates, e.g. vLLM, ComfyUI). Public catalog \u2014 no auth required. Each result includes the currently listed release with its hubReleaseId and prebuilt image name. Results are sorted by deploy count (most popular first). Set includeConfig:true to also return the release config (hardware requirements and environment-variable schema) \u2014 it is large, so prefer requesting it for a single repo via the repoOwner/searchTerm filters.",
|
|
1197
|
+
{
|
|
1198
|
+
...listPaginationParams,
|
|
1199
|
+
searchTerm: z4.string().optional().describe(
|
|
1200
|
+
"Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
|
|
1201
|
+
),
|
|
1202
|
+
category: z4.string().optional().describe(
|
|
1203
|
+
"Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
|
|
1204
|
+
),
|
|
1205
|
+
type: z4.enum(["SERVERLESS", "POD"]).optional().describe(
|
|
1206
|
+
"Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
|
|
1207
|
+
),
|
|
1208
|
+
repoOwner: z4.string().optional().describe(
|
|
1209
|
+
"Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
|
|
1210
|
+
),
|
|
1211
|
+
includeConfig: z4.boolean().optional().describe(
|
|
1212
|
+
"Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
|
|
1213
|
+
)
|
|
1214
|
+
},
|
|
1215
|
+
{ title: "List Hub repos", ...READ_ONLY },
|
|
1216
|
+
async (params) => {
|
|
1217
|
+
const data = await graphql(
|
|
1218
|
+
listingsQuery(params.includeConfig === true)
|
|
1219
|
+
);
|
|
1220
|
+
let listings = data.listings;
|
|
1221
|
+
if (params.type) {
|
|
1222
|
+
listings = listings.filter(
|
|
1223
|
+
(l) => (l.type ?? "").toUpperCase() === params.type
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
if (params.category) {
|
|
1227
|
+
const term = params.category.toLowerCase();
|
|
1228
|
+
listings = listings.filter(
|
|
1229
|
+
(l) => (l.category ?? "").toLowerCase() === term
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
if (params.repoOwner) {
|
|
1233
|
+
const term = params.repoOwner.toLowerCase();
|
|
1234
|
+
listings = listings.filter(
|
|
1235
|
+
(l) => (l.repoOwner ?? "").toLowerCase() === term
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
if (params.searchTerm) {
|
|
1239
|
+
const term = params.searchTerm.toLowerCase();
|
|
1240
|
+
listings = listings.filter(
|
|
1241
|
+
(l) => [
|
|
1242
|
+
l.title,
|
|
1243
|
+
l.description,
|
|
1244
|
+
l.repoName,
|
|
1245
|
+
l.repoOwner,
|
|
1246
|
+
...l.tags ?? []
|
|
1247
|
+
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
listings = [...listings].sort(
|
|
1251
|
+
(a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
|
|
1252
|
+
);
|
|
1253
|
+
const result = listings.map((l) => ({
|
|
1254
|
+
id: l.id,
|
|
1255
|
+
repo: `${l.repoOwner}/${l.repoName}`,
|
|
1256
|
+
title: l.title,
|
|
1257
|
+
description: l.description,
|
|
1258
|
+
type: l.type,
|
|
1259
|
+
category: l.category,
|
|
1260
|
+
tags: l.tags,
|
|
1261
|
+
language: l.language,
|
|
1262
|
+
stars: l.stars,
|
|
1263
|
+
deploys: l.deploys,
|
|
1264
|
+
views: l.views,
|
|
1265
|
+
githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
|
|
1266
|
+
hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
|
|
1267
|
+
createdAt: l.createdAt,
|
|
1268
|
+
updatedAt: l.updatedAt,
|
|
1269
|
+
listedRelease: l.listedRelease ? {
|
|
1270
|
+
hubReleaseId: l.listedRelease.id,
|
|
1271
|
+
name: l.listedRelease.name,
|
|
1272
|
+
tagName: l.listedRelease.tagName,
|
|
1273
|
+
createdAt: l.listedRelease.createdAt,
|
|
1274
|
+
imageName: l.listedRelease.build?.imageName,
|
|
1275
|
+
...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
|
|
1276
|
+
} : null
|
|
1277
|
+
}));
|
|
1278
|
+
return capListResult(result, {
|
|
1279
|
+
limit: params.limit,
|
|
1280
|
+
cursor: params.cursor
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
);
|
|
1284
|
+
server.tool(
|
|
1285
|
+
"deploy-hub-repo",
|
|
1286
|
+
"Deploy a Runpod Hub repo's listed release as a new Serverless endpoint (the same as clicking Deploy on the Hub). Identify the repo by `repo` (\"owner/name\" from list-hub-repos) or by `hubReleaseId`. The release supplies the prebuilt image, container disk, CUDA constraints, and env-var defaults; pass `env` to override or fill in values (required keys without a default must be provided \u2014 check list-hub-repos with includeConfig:true for the schema). GPU selection comes from the release config when it specifies one; otherwise pass gpuIds (GPU pool names, e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). Uses the authenticated GraphQL API (no REST home for Hub deploys yet), so it works the same on v1 and v2.",
|
|
1287
|
+
{
|
|
1288
|
+
repo: z4.string().optional().describe(
|
|
1289
|
+
`The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
|
|
1290
|
+
),
|
|
1291
|
+
hubReleaseId: z4.string().optional().describe(
|
|
1292
|
+
"The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
|
|
1293
|
+
),
|
|
1294
|
+
name: z4.string().optional().describe(
|
|
1295
|
+
"Name for the new endpoint. Defaults to '<listing title> <release tag>'."
|
|
1296
|
+
),
|
|
1297
|
+
env: z4.record(z4.string()).optional().describe(
|
|
1298
|
+
"Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
|
|
1299
|
+
),
|
|
1300
|
+
gpuIds: z4.string().optional().describe(
|
|
1301
|
+
"Comma-separated GPU pool names for workers (e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). To pin specific GPU SKUs within a pool, append exclusions prefixed with '-' using GPU type ids from list-gpu-types (e.g. 'AMPERE_16,-NVIDIA RTX 2000 Ada Generation,-NVIDIA RTX A4500' allows only the pool's remaining SKUs). Defaults to the release config's gpuIds; required when the config does not specify one."
|
|
1302
|
+
),
|
|
1303
|
+
gpuCount: z4.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
|
|
1304
|
+
containerDiskInGb: z4.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
|
|
1305
|
+
workersMin: z4.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
|
|
1306
|
+
workersMax: z4.number().int().positive().optional().describe("Maximum workers (default: account default)."),
|
|
1307
|
+
idleTimeout: z4.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
|
|
1308
|
+
scalerType: z4.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
|
|
1309
|
+
scalerValue: z4.number().optional().describe("Autoscaler target value (default 4)."),
|
|
1310
|
+
executionTimeoutMs: z4.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
|
|
1311
|
+
flashboot: z4.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
|
|
1312
|
+
},
|
|
1313
|
+
{ title: "Deploy Hub repo", ...WRITE },
|
|
1314
|
+
async (params) => {
|
|
1315
|
+
if (!params.repo && !params.hubReleaseId) {
|
|
1316
|
+
return jsonReply2({
|
|
1317
|
+
error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
const catalog = await graphql(listingsQuery(true));
|
|
1321
|
+
let listing;
|
|
1322
|
+
if (params.hubReleaseId) {
|
|
1323
|
+
listing = catalog.listings.find(
|
|
1324
|
+
(l) => l.listedRelease?.id === params.hubReleaseId
|
|
1325
|
+
);
|
|
1326
|
+
} else {
|
|
1327
|
+
const repoKey = params.repo.toLowerCase();
|
|
1328
|
+
listing = catalog.listings.find(
|
|
1329
|
+
(l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (!listing) {
|
|
1333
|
+
return jsonReply2({
|
|
1334
|
+
error: `No Hub listing found for ${params.hubReleaseId ? `hubReleaseId "${params.hubReleaseId}" (only each repo's currently listed release is deployable)` : `repo "${params.repo}"`}. Use list-hub-repos to see the catalog.`
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
const release = listing.listedRelease;
|
|
1338
|
+
if (!release?.build?.imageName) {
|
|
1339
|
+
return jsonReply2({
|
|
1340
|
+
error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
|
|
1344
|
+
return jsonReply2({
|
|
1345
|
+
error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
const config = parseReleaseConfig(release.config) ?? {};
|
|
1349
|
+
const gpuIds = params.gpuIds ?? config.gpuIds;
|
|
1350
|
+
if (!gpuIds) {
|
|
1351
|
+
return jsonReply2({
|
|
1352
|
+
error: 'This release does not specify a GPU pool \u2014 pass gpuIds (comma-separated pool names, e.g. "ADA_80_PRO,AMPERE_80"; see the pool field on list-gpu-types).'
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
|
|
1356
|
+
if (missingRequired.length > 0) {
|
|
1357
|
+
return jsonReply2({
|
|
1358
|
+
error: `Missing required environment variables for this release: ${missingRequired.join(
|
|
1359
|
+
", "
|
|
1360
|
+
)}. Pass them via the env parameter.`
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
|
|
1364
|
+
const minCuda = minCudaVersion(config.allowedCudaVersions);
|
|
1365
|
+
const input = {
|
|
1366
|
+
name: endpointName,
|
|
1367
|
+
hubReleaseId: release.id,
|
|
1368
|
+
type: "QB",
|
|
1369
|
+
gpuIds,
|
|
1370
|
+
gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
|
|
1371
|
+
workersMin: params.workersMin ?? 0,
|
|
1372
|
+
workersMax: params.workersMax ?? null,
|
|
1373
|
+
idleTimeout: params.idleTimeout ?? 5,
|
|
1374
|
+
scalerType: params.scalerType ?? "QUEUE_DELAY",
|
|
1375
|
+
scalerValue: params.scalerValue ?? 4,
|
|
1376
|
+
executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
|
|
1377
|
+
flashBootType: params.flashboot ?? "FLASHBOOT",
|
|
1378
|
+
locations: null,
|
|
1379
|
+
networkVolumeIds: null,
|
|
1380
|
+
compliance: [],
|
|
1381
|
+
modelReferences: [],
|
|
1382
|
+
...minCuda ? {
|
|
1383
|
+
minCudaVersion: minCuda,
|
|
1384
|
+
allowedCudaVersions: config.allowedCudaVersions.join(",")
|
|
1385
|
+
} : {},
|
|
1386
|
+
template: {
|
|
1387
|
+
name: `${endpointName}__template__${randomSuffix()}`,
|
|
1388
|
+
imageName: release.build.imageName,
|
|
1389
|
+
containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
|
|
1390
|
+
containerRegistryAuthId: "",
|
|
1391
|
+
dockerArgs: "",
|
|
1392
|
+
startScript: "",
|
|
1393
|
+
ports: "",
|
|
1394
|
+
env
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
const data = await graphqlAuthed(
|
|
1398
|
+
`
|
|
1399
|
+
mutation saveEndpoint($input: EndpointInput!) {
|
|
1400
|
+
saveEndpoint(input: $input) {
|
|
1401
|
+
id
|
|
1402
|
+
name
|
|
1403
|
+
gpuIds
|
|
1404
|
+
gpuCount
|
|
1405
|
+
workersMin
|
|
1406
|
+
workersMax
|
|
1407
|
+
idleTimeout
|
|
1408
|
+
scalerType
|
|
1409
|
+
scalerValue
|
|
1410
|
+
flashBootType
|
|
1411
|
+
templateId
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
`,
|
|
1415
|
+
{ input }
|
|
1416
|
+
);
|
|
1417
|
+
return jsonReply2({
|
|
1418
|
+
endpoint: data.saveEndpoint,
|
|
1419
|
+
deployed: {
|
|
1420
|
+
repo: `${listing.repoOwner}/${listing.repoName}`,
|
|
1421
|
+
release: release.tagName,
|
|
1422
|
+
hubReleaseId: release.id,
|
|
1423
|
+
imageName: release.build.imageName
|
|
1424
|
+
},
|
|
1425
|
+
note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/tools/public-endpoints.ts
|
|
1432
|
+
import { z as z5 } from "zod";
|
|
1433
|
+
function parseMetadata(metadata) {
|
|
1434
|
+
if (!metadata) return {};
|
|
1435
|
+
try {
|
|
1436
|
+
const parsed = JSON.parse(metadata);
|
|
1437
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1438
|
+
} catch {
|
|
1439
|
+
return {};
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
function registerPublicEndpointTools(server, rt) {
|
|
1443
|
+
const { graphql } = rt;
|
|
1444
|
+
server.tool(
|
|
1445
|
+
"list-public-endpoints",
|
|
1446
|
+
"List Runpod Public Endpoints \u2014 managed, pay-per-use model APIs (text, image, video, audio) that require no deployment. Public catalog, no auth required. Each result includes the endpointId to call with run-endpoint/runsync-endpoint (or via https://api.runpod.ai/v2/{endpointId}), the model name, modality, owner, and pricing. Only live endpoints are returned by default; set includeOffline:true to also list ones that are not currently live.",
|
|
1447
|
+
{
|
|
1448
|
+
...listPaginationParams,
|
|
1449
|
+
searchTerm: z5.string().optional().describe(
|
|
1450
|
+
"Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
|
|
1451
|
+
),
|
|
1452
|
+
modality: z5.string().optional().describe(
|
|
1453
|
+
"Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
|
|
1454
|
+
),
|
|
1455
|
+
owner: z5.string().optional().describe(
|
|
1456
|
+
"Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
|
|
1457
|
+
),
|
|
1458
|
+
includeOffline: z5.boolean().optional().describe(
|
|
1459
|
+
"Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
|
|
1460
|
+
)
|
|
1461
|
+
},
|
|
1462
|
+
{ title: "List public endpoints", ...READ_ONLY },
|
|
1463
|
+
async (params) => {
|
|
1464
|
+
const data = await graphql(`
|
|
1465
|
+
query {
|
|
1466
|
+
allAiApiPublicConfigs {
|
|
1467
|
+
id
|
|
1468
|
+
aiApiId
|
|
1469
|
+
modelName
|
|
1470
|
+
displayName
|
|
1471
|
+
description
|
|
1472
|
+
metadata
|
|
1473
|
+
isLive
|
|
1474
|
+
createdAt
|
|
1475
|
+
updatedAt
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
`);
|
|
1479
|
+
let configs = data.allAiApiPublicConfigs.map((c) => ({
|
|
1480
|
+
config: c,
|
|
1481
|
+
meta: parseMetadata(c.metadata)
|
|
1482
|
+
}));
|
|
1483
|
+
if (!params.includeOffline) {
|
|
1484
|
+
configs = configs.filter(({ config }) => config.isLive);
|
|
1485
|
+
}
|
|
1486
|
+
if (params.modality) {
|
|
1487
|
+
const term = params.modality.toLowerCase();
|
|
1488
|
+
configs = configs.filter(
|
|
1489
|
+
({ meta }) => (meta.source ?? "").toLowerCase() === term
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
if (params.owner) {
|
|
1493
|
+
const term = params.owner.toLowerCase();
|
|
1494
|
+
configs = configs.filter(
|
|
1495
|
+
({ meta }) => (meta.owner ?? "").toLowerCase() === term
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
if (params.searchTerm) {
|
|
1499
|
+
const term = params.searchTerm.toLowerCase();
|
|
1500
|
+
configs = configs.filter(
|
|
1501
|
+
({ config, meta }) => [
|
|
1502
|
+
config.displayName,
|
|
1503
|
+
config.modelName,
|
|
1504
|
+
config.aiApiId,
|
|
1505
|
+
config.description,
|
|
1506
|
+
meta.owner,
|
|
1507
|
+
meta.tag,
|
|
1508
|
+
meta.source
|
|
1509
|
+
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
configs = [...configs].sort(
|
|
1513
|
+
(a, b) => a.config.displayName.localeCompare(b.config.displayName)
|
|
1514
|
+
);
|
|
1515
|
+
const result = configs.map(({ config, meta }) => ({
|
|
1516
|
+
endpointId: config.aiApiId,
|
|
1517
|
+
displayName: config.displayName,
|
|
1518
|
+
modelName: config.modelName,
|
|
1519
|
+
description: config.description,
|
|
1520
|
+
modality: meta.source,
|
|
1521
|
+
tag: meta.tag,
|
|
1522
|
+
owner: meta.owner,
|
|
1523
|
+
pricing: meta.priceString,
|
|
1524
|
+
isLive: config.isLive,
|
|
1525
|
+
baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
|
|
1526
|
+
createdAt: config.createdAt,
|
|
1527
|
+
updatedAt: config.updatedAt
|
|
1528
|
+
}));
|
|
1529
|
+
return capListResult(result, {
|
|
844
1530
|
limit: params.limit,
|
|
845
1531
|
cursor: params.cursor
|
|
846
1532
|
});
|
|
847
1533
|
}
|
|
848
1534
|
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/tools/endpoint-gpus.ts
|
|
1538
|
+
import { z as z6 } from "zod";
|
|
1539
|
+
function registerEndpointGpuTools(server, rt) {
|
|
1540
|
+
const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
849
1541
|
server.tool(
|
|
850
|
-
"
|
|
851
|
-
"
|
|
1542
|
+
"set-endpoint-gpus",
|
|
1543
|
+
"Set which GPUs a Serverless endpoint's workers run on \u2014 including pinning specific GPU SKUs, which create-endpoint/update-endpoint cannot express. Provide either a raw gpuIds string, or pools plus optional excludeGpuTypeIds (GPU type ids from list-gpu-types) and the exclusion string is built for you: a pool allows every SKU in it, and excluding all but one SKU pins that SKU exactly (e.g. pools:['AMPERE_16'], excludeGpuTypeIds:['NVIDIA RTX 2000 Ada Generation','NVIDIA RTX 4000 Ada Generation','NVIDIA RTX A4500'] pins RTX A4000). All other endpoint settings (workers, scaling, timeouts, template) are read first and preserved. Works on any Serverless endpoint via the authenticated GraphQL API.",
|
|
852
1544
|
{
|
|
853
|
-
|
|
1545
|
+
endpointId: z6.string().describe("ID of the Serverless endpoint to update"),
|
|
1546
|
+
gpuIds: z6.string().optional().describe(
|
|
1547
|
+
"Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
|
|
1548
|
+
),
|
|
1549
|
+
pools: z6.array(z6.string()).optional().describe(
|
|
1550
|
+
"GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
|
|
1551
|
+
),
|
|
1552
|
+
excludeGpuTypeIds: z6.array(z6.string()).optional().describe(
|
|
1553
|
+
"GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
|
|
1554
|
+
),
|
|
1555
|
+
gpuCount: z6.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
|
|
1556
|
+
minCudaVersion: z6.string().optional().describe(
|
|
1557
|
+
"Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
|
|
1558
|
+
),
|
|
1559
|
+
allowedCudaVersions: z6.string().optional().describe(
|
|
1560
|
+
"Comma-separated allowed host CUDA versions (e.g. '12.8,12.7,12.6'). Omit to keep the current value. CUDA compatibility is part of GPU selection \u2014 a narrow list can leave an endpoint unable to schedule workers."
|
|
1561
|
+
)
|
|
854
1562
|
},
|
|
855
|
-
{ title: "
|
|
1563
|
+
{ title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
|
|
856
1564
|
async (params) => {
|
|
857
|
-
const
|
|
858
|
-
|
|
1565
|
+
const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
|
|
1566
|
+
...params.pools,
|
|
1567
|
+
...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
|
|
1568
|
+
].join(",") : void 0);
|
|
1569
|
+
if (!gpuIds) {
|
|
859
1570
|
return jsonReply2({
|
|
860
|
-
error: "
|
|
861
|
-
status: 501
|
|
1571
|
+
error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
|
|
862
1572
|
});
|
|
863
1573
|
}
|
|
864
|
-
const
|
|
865
|
-
|
|
1574
|
+
const data = await graphqlAuthed(`
|
|
1575
|
+
query {
|
|
1576
|
+
myself {
|
|
1577
|
+
endpoints {
|
|
1578
|
+
id
|
|
1579
|
+
name
|
|
1580
|
+
gpuIds
|
|
1581
|
+
gpuCount
|
|
1582
|
+
workersMin
|
|
1583
|
+
workersMax
|
|
1584
|
+
idleTimeout
|
|
1585
|
+
scalerType
|
|
1586
|
+
scalerValue
|
|
1587
|
+
executionTimeoutMs
|
|
1588
|
+
flashBootType
|
|
1589
|
+
type
|
|
1590
|
+
locations
|
|
1591
|
+
templateId
|
|
1592
|
+
allowedCudaVersions
|
|
1593
|
+
minCudaVersion
|
|
1594
|
+
compliance
|
|
1595
|
+
modelReferences
|
|
1596
|
+
networkVolumeIds {
|
|
1597
|
+
networkVolumeId
|
|
1598
|
+
dataCenterId
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
`);
|
|
1604
|
+
const current = data.myself.endpoints.find(
|
|
1605
|
+
(e) => e.id === params.endpointId
|
|
866
1606
|
);
|
|
867
|
-
|
|
868
|
-
}
|
|
869
|
-
);
|
|
870
|
-
server.tool(
|
|
871
|
-
"get-cpu-type",
|
|
872
|
-
"Get details for a single CPU flavor type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-cpu-types there).",
|
|
873
|
-
{
|
|
874
|
-
cpuTypeId: z2.string().describe("ID of the CPU type to retrieve")
|
|
875
|
-
},
|
|
876
|
-
{ title: "Get CPU type", ...READ_ONLY },
|
|
877
|
-
async (params) => {
|
|
878
|
-
const backend = backendFor("cpus");
|
|
879
|
-
if (backend.version === "v1") {
|
|
1607
|
+
if (!current) {
|
|
880
1608
|
return jsonReply2({
|
|
881
|
-
error:
|
|
882
|
-
status: 501
|
|
1609
|
+
error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
|
|
883
1610
|
});
|
|
884
1611
|
}
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1612
|
+
const input = {
|
|
1613
|
+
id: current.id,
|
|
1614
|
+
name: current.name,
|
|
1615
|
+
gpuIds,
|
|
1616
|
+
gpuCount: params.gpuCount ?? current.gpuCount,
|
|
1617
|
+
workersMin: current.workersMin,
|
|
1618
|
+
workersMax: current.workersMax,
|
|
1619
|
+
idleTimeout: current.idleTimeout,
|
|
1620
|
+
scalerType: current.scalerType,
|
|
1621
|
+
scalerValue: current.scalerValue,
|
|
1622
|
+
executionTimeoutMs: current.executionTimeoutMs,
|
|
1623
|
+
flashBootType: current.flashBootType,
|
|
1624
|
+
type: current.type,
|
|
1625
|
+
locations: current.locations,
|
|
1626
|
+
networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
|
|
1627
|
+
// Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
|
|
1628
|
+
// ONLY, and the read shape is rejected outright.
|
|
1629
|
+
current.networkVolumeIds.map((v) => ({
|
|
1630
|
+
networkVolumeId: v.networkVolumeId
|
|
1631
|
+
}))
|
|
1632
|
+
) : null
|
|
1633
|
+
};
|
|
1634
|
+
for (const [key, value] of Object.entries({
|
|
1635
|
+
templateId: current.templateId,
|
|
1636
|
+
allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
|
|
1637
|
+
minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
|
|
1638
|
+
compliance: current.compliance,
|
|
1639
|
+
modelReferences: current.modelReferences
|
|
1640
|
+
})) {
|
|
1641
|
+
if (value !== null && value !== void 0) input[key] = value;
|
|
905
1642
|
}
|
|
906
|
-
const result = await
|
|
907
|
-
|
|
1643
|
+
const result = await graphqlAuthed(
|
|
1644
|
+
`
|
|
1645
|
+
mutation saveEndpoint($input: EndpointInput!) {
|
|
1646
|
+
saveEndpoint(input: $input) {
|
|
1647
|
+
id
|
|
1648
|
+
name
|
|
1649
|
+
gpuIds
|
|
1650
|
+
gpuCount
|
|
1651
|
+
workersMin
|
|
1652
|
+
workersMax
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
`,
|
|
1656
|
+
{ input }
|
|
908
1657
|
);
|
|
909
|
-
return jsonReply2(
|
|
1658
|
+
return jsonReply2({
|
|
1659
|
+
endpoint: result.saveEndpoint,
|
|
1660
|
+
previousGpuIds: current.gpuIds
|
|
1661
|
+
});
|
|
910
1662
|
}
|
|
911
1663
|
);
|
|
912
1664
|
}
|
|
913
1665
|
|
|
914
1666
|
// src/tools/pods.ts
|
|
915
|
-
import { z as
|
|
1667
|
+
import { z as z7 } from "zod";
|
|
916
1668
|
function registerPodTools(server, rt) {
|
|
917
1669
|
const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
|
|
918
1670
|
server.tool(
|
|
@@ -920,12 +1672,12 @@ function registerPodTools(server, rt) {
|
|
|
920
1672
|
"List your pods (running and stopped) with optional filters \u2014 compute type, GPU type, data center, name \u2014 plus machine/network-volume expansion. Paginated via limit/cursor.",
|
|
921
1673
|
{
|
|
922
1674
|
...listPaginationParams,
|
|
923
|
-
computeType:
|
|
924
|
-
gpuTypeId:
|
|
925
|
-
dataCenterId:
|
|
926
|
-
name:
|
|
927
|
-
includeMachine:
|
|
928
|
-
includeNetworkVolume:
|
|
1675
|
+
computeType: z7.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
|
|
1676
|
+
gpuTypeId: z7.array(z7.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
|
|
1677
|
+
dataCenterId: z7.array(z7.string()).optional().describe("Filter to Pods in any of the provided data centers"),
|
|
1678
|
+
name: z7.string().optional().describe("Filter to Pods with the provided name"),
|
|
1679
|
+
includeMachine: z7.boolean().optional().describe("Include information about the machine"),
|
|
1680
|
+
includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
|
|
929
1681
|
},
|
|
930
1682
|
{ title: "List pods", ...READ_ONLY },
|
|
931
1683
|
async (params) => {
|
|
@@ -969,9 +1721,9 @@ function registerPodTools(server, rt) {
|
|
|
969
1721
|
"get-pod",
|
|
970
1722
|
"Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
|
|
971
1723
|
{
|
|
972
|
-
podId:
|
|
973
|
-
includeMachine:
|
|
974
|
-
includeNetworkVolume:
|
|
1724
|
+
podId: z7.string().describe("ID of the pod to retrieve"),
|
|
1725
|
+
includeMachine: z7.boolean().optional().describe("Include information about the machine"),
|
|
1726
|
+
includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
|
|
975
1727
|
},
|
|
976
1728
|
{ title: "Get pod", ...READ_ONLY },
|
|
977
1729
|
async (params) => {
|
|
@@ -999,26 +1751,44 @@ function registerPodTools(server, rt) {
|
|
|
999
1751
|
);
|
|
1000
1752
|
server.tool(
|
|
1001
1753
|
"create-pod",
|
|
1002
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). If the user
|
|
1754
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
1003
1755
|
{
|
|
1004
|
-
name:
|
|
1005
|
-
imageName:
|
|
1006
|
-
|
|
1756
|
+
name: z7.string().optional().describe("Name for the pod"),
|
|
1757
|
+
imageName: z7.string().optional().describe("Docker image to use. Optional when templateId is provided."),
|
|
1758
|
+
templateId: z7.string().optional().describe(
|
|
1759
|
+
"Deploy from an existing template (see list-templates / get-template). The template supplies name, image, start command, ports, env, disk, volume, and registry credential as defaults; any field you also pass replaces the template value for that field (whole-field, not a merge). Override the registry credential with containerRegistryAuthId. Requires the v2 REST API. Still pass gpuTypeIds or computeType to choose the compute \u2014 a template does not pin the pod compute."
|
|
1760
|
+
),
|
|
1761
|
+
computeType: z7.enum(["GPU", "CPU"]).optional().describe(
|
|
1007
1762
|
"Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
|
|
1008
1763
|
),
|
|
1009
|
-
cloudType:
|
|
1010
|
-
gpuTypeIds:
|
|
1011
|
-
gpuCount:
|
|
1012
|
-
containerDiskInGb:
|
|
1013
|
-
volumeInGb:
|
|
1014
|
-
volumeMountPath:
|
|
1015
|
-
ports:
|
|
1016
|
-
env:
|
|
1017
|
-
|
|
1764
|
+
cloudType: z7.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
1765
|
+
gpuTypeIds: z7.array(z7.string()).optional().describe("List of acceptable GPU types"),
|
|
1766
|
+
gpuCount: z7.number().optional().describe("Number of GPUs"),
|
|
1767
|
+
containerDiskInGb: z7.number().optional().describe("Container disk size in GB"),
|
|
1768
|
+
volumeInGb: z7.number().optional().describe("Volume size in GB"),
|
|
1769
|
+
volumeMountPath: z7.string().optional().describe("Path to mount the volume"),
|
|
1770
|
+
ports: z7.array(z7.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
1771
|
+
env: z7.record(z7.string()).optional().describe("Environment variables"),
|
|
1772
|
+
containerRegistryAuthId: z7.string().optional().describe(
|
|
1773
|
+
"Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
|
|
1774
|
+
),
|
|
1775
|
+
dataCenterIds: z7.array(z7.string()).optional().describe("List of data centers")
|
|
1018
1776
|
},
|
|
1019
1777
|
{ title: "Create pod", ...WRITE },
|
|
1020
1778
|
async (params) => {
|
|
1021
1779
|
const backend = backendFor("pods");
|
|
1780
|
+
if (params.templateId && backend.version !== "v2") {
|
|
1781
|
+
return jsonReply2({
|
|
1782
|
+
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
1783
|
+
status: 501
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
if (!params.imageName && !params.templateId) {
|
|
1787
|
+
return jsonReply2({
|
|
1788
|
+
error: "Provide imageName, or templateId to deploy from an existing template.",
|
|
1789
|
+
status: 400
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1022
1792
|
const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
|
|
1023
1793
|
const hasGpuCount = (params.gpuCount ?? 0) > 0;
|
|
1024
1794
|
const wantsCpu = params.computeType === "CPU";
|
|
@@ -1043,6 +1813,12 @@ function registerPodTools(server, rt) {
|
|
|
1043
1813
|
});
|
|
1044
1814
|
}
|
|
1045
1815
|
if (wantsCpu) {
|
|
1816
|
+
if (params.templateId) {
|
|
1817
|
+
return jsonReply2({
|
|
1818
|
+
error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
|
|
1819
|
+
status: 400
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1046
1822
|
try {
|
|
1047
1823
|
const result = await callRestUrl(
|
|
1048
1824
|
`${restV1Base(env)}/pods`,
|
|
@@ -1067,7 +1843,24 @@ function registerPodTools(server, rt) {
|
|
|
1067
1843
|
}
|
|
1068
1844
|
}
|
|
1069
1845
|
}
|
|
1070
|
-
|
|
1846
|
+
let body = backend.mapCreate(params);
|
|
1847
|
+
if (params.templateId) {
|
|
1848
|
+
let template;
|
|
1849
|
+
try {
|
|
1850
|
+
template = await callRestUrl(
|
|
1851
|
+
`${restV2Base(env)}/templates/${params.templateId}`
|
|
1852
|
+
);
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
if (error instanceof HttpError) {
|
|
1855
|
+
return jsonReply2({
|
|
1856
|
+
error: `Failed to load template ${params.templateId}: ${error.message}`,
|
|
1857
|
+
status: error.status
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
throw error;
|
|
1861
|
+
}
|
|
1862
|
+
body = { ...podBodyFromTemplate(template), ...body };
|
|
1863
|
+
}
|
|
1071
1864
|
try {
|
|
1072
1865
|
const result = await callRestUrl(
|
|
1073
1866
|
`${backend.base}${backend.list}`,
|
|
@@ -1082,11 +1875,8 @@ function registerPodTools(server, rt) {
|
|
|
1082
1875
|
}
|
|
1083
1876
|
return jsonReply2(result);
|
|
1084
1877
|
} catch (error) {
|
|
1085
|
-
if (error instanceof HttpError
|
|
1086
|
-
return jsonReply2({
|
|
1087
|
-
error: "CPU pods are not yet supported on the v2 REST API. Use a GPU pod, or create the CPU pod via the v1 API (set RUNPOD_REST_VERSION=v1).",
|
|
1088
|
-
status: 501
|
|
1089
|
-
});
|
|
1878
|
+
if (error instanceof HttpError) {
|
|
1879
|
+
return jsonReply2({ error: error.message, status: error.status });
|
|
1090
1880
|
}
|
|
1091
1881
|
throw error;
|
|
1092
1882
|
}
|
|
@@ -1096,14 +1886,14 @@ function registerPodTools(server, rt) {
|
|
|
1096
1886
|
"update-pod",
|
|
1097
1887
|
"Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
|
|
1098
1888
|
{
|
|
1099
|
-
podId:
|
|
1100
|
-
name:
|
|
1101
|
-
imageName:
|
|
1102
|
-
containerDiskInGb:
|
|
1103
|
-
volumeInGb:
|
|
1104
|
-
volumeMountPath:
|
|
1105
|
-
ports:
|
|
1106
|
-
env:
|
|
1889
|
+
podId: z7.string().describe("ID of the pod to update"),
|
|
1890
|
+
name: z7.string().optional().describe("New name for the pod"),
|
|
1891
|
+
imageName: z7.string().optional().describe("New Docker image"),
|
|
1892
|
+
containerDiskInGb: z7.number().optional().describe("New container disk size in GB"),
|
|
1893
|
+
volumeInGb: z7.number().optional().describe("New volume size in GB"),
|
|
1894
|
+
volumeMountPath: z7.string().optional().describe("New path to mount the volume"),
|
|
1895
|
+
ports: z7.array(z7.string()).optional().describe("New ports to expose"),
|
|
1896
|
+
env: z7.record(z7.string()).optional().describe("New environment variables")
|
|
1107
1897
|
},
|
|
1108
1898
|
{ title: "Update pod", ...WRITE, idempotentHint: true },
|
|
1109
1899
|
async (params) => {
|
|
@@ -1122,7 +1912,7 @@ function registerPodTools(server, rt) {
|
|
|
1122
1912
|
"start-pod",
|
|
1123
1913
|
"Start a stopped pod (resumes the same pod and its billing).",
|
|
1124
1914
|
{
|
|
1125
|
-
podId:
|
|
1915
|
+
podId: z7.string().describe("ID of the pod to start")
|
|
1126
1916
|
},
|
|
1127
1917
|
{ title: "Start pod", ...WRITE, idempotentHint: true },
|
|
1128
1918
|
async (params) => {
|
|
@@ -1134,7 +1924,7 @@ function registerPodTools(server, rt) {
|
|
|
1134
1924
|
"stop-pod",
|
|
1135
1925
|
"Stop a running pod. The pod and its disk persist; GPU billing stops.",
|
|
1136
1926
|
{
|
|
1137
|
-
podId:
|
|
1927
|
+
podId: z7.string().describe("ID of the pod to stop")
|
|
1138
1928
|
},
|
|
1139
1929
|
{ title: "Stop pod", ...WRITE, idempotentHint: true },
|
|
1140
1930
|
async (params) => {
|
|
@@ -1146,7 +1936,7 @@ function registerPodTools(server, rt) {
|
|
|
1146
1936
|
"restart-pod",
|
|
1147
1937
|
"Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
|
|
1148
1938
|
{
|
|
1149
|
-
podId:
|
|
1939
|
+
podId: z7.string().describe("ID of the pod to restart")
|
|
1150
1940
|
},
|
|
1151
1941
|
{ title: "Restart pod", ...WRITE },
|
|
1152
1942
|
async (params) => {
|
|
@@ -1161,11 +1951,29 @@ function registerPodTools(server, rt) {
|
|
|
1161
1951
|
return jsonReply2(result);
|
|
1162
1952
|
}
|
|
1163
1953
|
);
|
|
1954
|
+
server.tool(
|
|
1955
|
+
"stream-pod-logs",
|
|
1956
|
+
"Fetch a bounded snapshot of a pod's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
|
|
1957
|
+
{
|
|
1958
|
+
podId: z7.string().describe("ID of the pod whose logs to stream"),
|
|
1959
|
+
...logStreamParams
|
|
1960
|
+
},
|
|
1961
|
+
{ title: "Stream pod logs", ...READ_ONLY },
|
|
1962
|
+
(params) => streamLogsReply(
|
|
1963
|
+
rt,
|
|
1964
|
+
{
|
|
1965
|
+
name: "stream-pod-logs",
|
|
1966
|
+
resource: "pods",
|
|
1967
|
+
logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
|
|
1968
|
+
},
|
|
1969
|
+
params
|
|
1970
|
+
)
|
|
1971
|
+
);
|
|
1164
1972
|
server.tool(
|
|
1165
1973
|
"delete-pod",
|
|
1166
1974
|
"Permanently delete a pod and its data. This cannot be undone.",
|
|
1167
1975
|
{
|
|
1168
|
-
podId:
|
|
1976
|
+
podId: z7.string().describe("ID of the pod to delete")
|
|
1169
1977
|
},
|
|
1170
1978
|
{ title: "Delete pod", ...DESTRUCTIVE },
|
|
1171
1979
|
async (params) => {
|
|
@@ -1180,16 +1988,27 @@ function registerPodTools(server, rt) {
|
|
|
1180
1988
|
}
|
|
1181
1989
|
|
|
1182
1990
|
// src/tools/endpoints.ts
|
|
1183
|
-
import { z as
|
|
1991
|
+
import { z as z8 } from "zod";
|
|
1992
|
+
var MIN_QUEUE_DELAY = 0.5;
|
|
1993
|
+
var MIN_REQUEST_COUNT = 1;
|
|
1994
|
+
var MIN_IDLE_TIMEOUT = 1;
|
|
1995
|
+
var MAX_IDLE_TIMEOUT = 3600;
|
|
1996
|
+
function scalerValueError(scalerType, scalerValue) {
|
|
1997
|
+
if (scalerType !== "REQUEST_COUNT" || scalerValue === void 0) return;
|
|
1998
|
+
if (!Number.isInteger(scalerValue) || scalerValue < MIN_REQUEST_COUNT) {
|
|
1999
|
+
return `REQUEST_COUNT scales on whole in-flight requests per worker, so scalerValue must be an integer >= ${MIN_REQUEST_COUNT} (got ${scalerValue}). For a fractional target use scalerType QUEUE_DELAY, which takes seconds >= ${MIN_QUEUE_DELAY}.`;
|
|
2000
|
+
}
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
1184
2003
|
function registerEndpointTools(server, rt) {
|
|
1185
2004
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1186
2005
|
server.tool(
|
|
1187
2006
|
"list-endpoints",
|
|
1188
|
-
"List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
|
|
2007
|
+
"List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor. On v2 each endpoint carries its request routing in `type` and the URLs to call it in `requestUrls`.",
|
|
1189
2008
|
{
|
|
1190
2009
|
...listPaginationParams,
|
|
1191
|
-
includeTemplate:
|
|
1192
|
-
includeWorkers:
|
|
2010
|
+
includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
|
|
2011
|
+
includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
|
|
1193
2012
|
},
|
|
1194
2013
|
{ title: "List endpoints", ...READ_ONLY },
|
|
1195
2014
|
async (params) => {
|
|
@@ -1220,11 +2039,11 @@ function registerEndpointTools(server, rt) {
|
|
|
1220
2039
|
);
|
|
1221
2040
|
server.tool(
|
|
1222
2041
|
"get-endpoint",
|
|
1223
|
-
"Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
|
|
2042
|
+
"Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only). On v2 the reply carries `type` (queue-based vs load-balancing routing) and `requestUrls` \u2014 the run/runsync/status/... URLs for a queue endpoint, or the base + health URLs for a load-balancing one. Read those rather than constructing endpoint URLs by hand.",
|
|
1224
2043
|
{
|
|
1225
|
-
endpointId:
|
|
1226
|
-
includeTemplate:
|
|
1227
|
-
includeWorkers:
|
|
2044
|
+
endpointId: z8.string().describe("ID of the endpoint to retrieve"),
|
|
2045
|
+
includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
|
|
2046
|
+
includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
|
|
1228
2047
|
},
|
|
1229
2048
|
{ title: "Get endpoint", ...READ_ONLY },
|
|
1230
2049
|
async (params) => {
|
|
@@ -1252,34 +2071,43 @@ function registerEndpointTools(server, rt) {
|
|
|
1252
2071
|
);
|
|
1253
2072
|
server.tool(
|
|
1254
2073
|
"create-endpoint",
|
|
1255
|
-
"Create a Serverless endpoint. On v2 (default), pass an inline config: imageName + gpuPoolIds (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. AMPERE_80/ADA_24) plus optional workers/scaling/disk/env. On v1, pass a templateId instead. Worker min/max set autoscaling bounds (min 0 = scale to zero).",
|
|
2074
|
+
"Create a Serverless endpoint. On v2 (default), pass an inline config: imageName + gpuPoolIds (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. AMPERE_80/ADA_24) plus optional workers/scaling/disk/env. On v1, pass a templateId instead. Worker min/max set autoscaling bounds (min 0 = scale to zero). Use endpointType to pick queue-based (default) or load-balancing request routing; the response `requestUrls` carries the URLs to call the endpoint with.",
|
|
1256
2075
|
{
|
|
1257
|
-
name:
|
|
2076
|
+
name: z8.string().optional().describe("Name for the endpoint"),
|
|
1258
2077
|
// --- v2 inline-config fields ---
|
|
1259
|
-
imageName:
|
|
1260
|
-
|
|
2078
|
+
imageName: z8.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
|
|
2079
|
+
endpointType: z8.enum(["QUEUE", "LOAD_BALANCER"]).optional().describe(
|
|
2080
|
+
"How requests reach the workers (v2). QUEUE (default) = submit jobs through the managed queue (run/runsync/status/...). LOAD_BALANCER = send HTTP requests straight to worker-defined paths, and forces scalerType REQUEST_COUNT. Fixed at creation \u2014 update-endpoint cannot change it."
|
|
2081
|
+
),
|
|
2082
|
+
gpuPoolIds: z8.array(z8.string()).optional().describe(
|
|
1261
2083
|
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
1262
2084
|
),
|
|
1263
|
-
gpuCount:
|
|
1264
|
-
args:
|
|
1265
|
-
containerDiskInGb:
|
|
1266
|
-
ports:
|
|
1267
|
-
env:
|
|
1268
|
-
containerRegistryAuthId:
|
|
1269
|
-
networkVolumeIds:
|
|
1270
|
-
executionTimeoutMs:
|
|
1271
|
-
flashboot:
|
|
2085
|
+
gpuCount: z8.number().optional().describe("GPUs per worker (v2)"),
|
|
2086
|
+
args: z8.string().optional().describe("Container start command/args (v2)"),
|
|
2087
|
+
containerDiskInGb: z8.number().optional().describe("Container disk size in GB (v2)"),
|
|
2088
|
+
ports: z8.array(z8.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
2089
|
+
env: z8.record(z8.string()).optional().describe("Environment variables (v2)"),
|
|
2090
|
+
containerRegistryAuthId: z8.string().optional().describe("Container registry auth id for a private image (v2)"),
|
|
2091
|
+
networkVolumeIds: z8.array(z8.string()).optional().describe("Network volume ids to attach (v2)"),
|
|
2092
|
+
executionTimeoutMs: z8.number().optional().describe("Per-job execution timeout in ms (v2)"),
|
|
2093
|
+
flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
|
|
1272
2094
|
// --- v1 template-based fields ---
|
|
1273
|
-
templateId:
|
|
1274
|
-
computeType:
|
|
1275
|
-
gpuTypeIds:
|
|
2095
|
+
templateId: z8.string().optional().describe("Template ID (v1). Required on v1."),
|
|
2096
|
+
computeType: z8.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
|
|
2097
|
+
gpuTypeIds: z8.array(z8.string()).optional().describe("List of acceptable GPU types (v1)"),
|
|
1276
2098
|
// --- shared autoscaling fields ---
|
|
1277
|
-
workersMin:
|
|
1278
|
-
workersMax:
|
|
1279
|
-
scalerType:
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
2099
|
+
workersMin: z8.number().optional().describe("Minimum number of workers"),
|
|
2100
|
+
workersMax: z8.number().optional().describe("Maximum number of workers"),
|
|
2101
|
+
scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2102
|
+
"Autoscaler signal. QUEUE_DELAY scales on how long requests wait in the queue (queue endpoints only); REQUEST_COUNT scales on in-flight requests per worker. Defaults to QUEUE_DELAY for QUEUE endpoints and REQUEST_COUNT for LOAD_BALANCER ones."
|
|
2103
|
+
),
|
|
2104
|
+
scalerValue: z8.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2105
|
+
"Autoscaler target for the chosen scalerType \u2014 seconds of queue delay (min 0.5) or in-flight requests per worker (integer, min 1). Defaults to 4."
|
|
2106
|
+
),
|
|
2107
|
+
idleTimeout: z8.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2108
|
+
"Idle timeout in seconds before scaling a worker down (1-3600). Does not apply to QUEUE endpoints scaling on REQUEST_COUNT."
|
|
2109
|
+
),
|
|
2110
|
+
dataCenterIds: z8.array(z8.string()).optional().describe("List of preferred data centers")
|
|
1283
2111
|
},
|
|
1284
2112
|
{ title: "Create endpoint", ...WRITE },
|
|
1285
2113
|
async (params) => {
|
|
@@ -1303,6 +2131,19 @@ function registerEndpointTools(server, rt) {
|
|
|
1303
2131
|
status: 400
|
|
1304
2132
|
});
|
|
1305
2133
|
}
|
|
2134
|
+
if (params.endpointType === "LOAD_BALANCER" && params.scalerType === "QUEUE_DELAY") {
|
|
2135
|
+
return jsonReply2({
|
|
2136
|
+
error: "LOAD_BALANCER endpoints have no request queue, so they cannot scale on QUEUE_DELAY. Use scalerType REQUEST_COUNT (the default for this endpoint type), or create a QUEUE endpoint instead.",
|
|
2137
|
+
status: 400
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
const scalerError = scalerValueError(
|
|
2141
|
+
params.scalerType ?? (params.endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : void 0),
|
|
2142
|
+
params.scalerValue
|
|
2143
|
+
);
|
|
2144
|
+
if (scalerError) {
|
|
2145
|
+
return jsonReply2({ error: scalerError, status: 400 });
|
|
2146
|
+
}
|
|
1306
2147
|
const body = backend.mapCreate(params);
|
|
1307
2148
|
const result2 = await callRestUrl(
|
|
1308
2149
|
`${backend.base}${backend.list}`,
|
|
@@ -1340,46 +2181,67 @@ function registerEndpointTools(server, rt) {
|
|
|
1340
2181
|
);
|
|
1341
2182
|
server.tool(
|
|
1342
2183
|
"update-endpoint",
|
|
1343
|
-
"Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change.",
|
|
2184
|
+
"Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change. An endpoint's request routing (queue vs load balancer) is fixed at creation and cannot be changed here \u2014 recreate the endpoint instead.",
|
|
1344
2185
|
{
|
|
1345
|
-
endpointId:
|
|
1346
|
-
name:
|
|
1347
|
-
workersMin:
|
|
1348
|
-
workersMax:
|
|
1349
|
-
idleTimeout:
|
|
1350
|
-
|
|
1351
|
-
|
|
2186
|
+
endpointId: z8.string().describe("ID of the endpoint to update"),
|
|
2187
|
+
name: z8.string().optional().describe("New name for the endpoint"),
|
|
2188
|
+
workersMin: z8.number().optional().describe("New minimum number of workers"),
|
|
2189
|
+
workersMax: z8.number().optional().describe("New maximum number of workers"),
|
|
2190
|
+
idleTimeout: z8.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2191
|
+
"New idle timeout in seconds (1-3600). Does not apply to queue endpoints scaling on REQUEST_COUNT."
|
|
2192
|
+
),
|
|
2193
|
+
scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2194
|
+
"New autoscaler signal. Switchable on queue endpoints; load-balancing endpoints only accept REQUEST_COUNT."
|
|
2195
|
+
),
|
|
2196
|
+
scalerValue: z8.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2197
|
+
"New autoscaler target \u2014 seconds of queue delay (min 0.5) or in-flight requests per worker (integer, min 1). Applies to the endpoint's current scalerType unless you also pass a new one."
|
|
2198
|
+
),
|
|
1352
2199
|
// --- v2-only inline-config fields ---
|
|
1353
|
-
imageName:
|
|
1354
|
-
gpuPoolIds:
|
|
1355
|
-
gpuCount:
|
|
1356
|
-
args:
|
|
1357
|
-
containerDiskInGb:
|
|
1358
|
-
ports:
|
|
1359
|
-
env:
|
|
1360
|
-
containerRegistryAuthId:
|
|
1361
|
-
networkVolumeIds:
|
|
1362
|
-
executionTimeoutMs:
|
|
1363
|
-
flashboot:
|
|
2200
|
+
imageName: z8.string().optional().describe("New Docker image (v2)"),
|
|
2201
|
+
gpuPoolIds: z8.array(z8.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
2202
|
+
gpuCount: z8.number().optional().describe("New GPUs per worker (v2)"),
|
|
2203
|
+
args: z8.string().optional().describe("New container args (v2)"),
|
|
2204
|
+
containerDiskInGb: z8.number().optional().describe("New container disk size in GB (v2)"),
|
|
2205
|
+
ports: z8.array(z8.string()).optional().describe("New ports (v2)"),
|
|
2206
|
+
env: z8.record(z8.string()).optional().describe("New environment variables (v2)"),
|
|
2207
|
+
containerRegistryAuthId: z8.string().optional().describe("New container registry auth id (v2)"),
|
|
2208
|
+
networkVolumeIds: z8.array(z8.string()).optional().describe("New network volume ids (v2)"),
|
|
2209
|
+
executionTimeoutMs: z8.number().optional().describe("New per-job execution timeout in ms (v2)"),
|
|
2210
|
+
flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
|
|
1364
2211
|
},
|
|
1365
2212
|
{ title: "Update endpoint", ...WRITE, idempotentHint: true },
|
|
1366
2213
|
async (params) => {
|
|
1367
2214
|
const { endpointId, ...updateParams } = params;
|
|
1368
2215
|
const backend = backendFor("endpoints");
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
"PATCH",
|
|
1373
|
-
|
|
2216
|
+
const url = `${backend.base}${backend.get(endpointId)}`;
|
|
2217
|
+
if (backend.version !== "v2") {
|
|
2218
|
+
const body2 = backend.mapUpdate(updateParams);
|
|
2219
|
+
return jsonReply2(await callRestUrl(url, "PATCH", body2));
|
|
2220
|
+
}
|
|
2221
|
+
let scalerType = updateParams.scalerType;
|
|
2222
|
+
if (scalerType === void 0 && updateParams.scalerValue !== void 0) {
|
|
2223
|
+
const current = await callRestUrl(url);
|
|
2224
|
+
scalerType = current?.scaling?.type === "REQUEST_COUNT" ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
2225
|
+
}
|
|
2226
|
+
const scalerError = scalerValueError(
|
|
2227
|
+
scalerType,
|
|
2228
|
+
updateParams.scalerValue
|
|
1374
2229
|
);
|
|
1375
|
-
|
|
2230
|
+
if (scalerError) {
|
|
2231
|
+
return jsonReply2({ error: scalerError, status: 400 });
|
|
2232
|
+
}
|
|
2233
|
+
const body = backend.mapUpdate({
|
|
2234
|
+
...updateParams,
|
|
2235
|
+
scalerType
|
|
2236
|
+
});
|
|
2237
|
+
return jsonReply2(await callRestUrl(url, "PATCH", body));
|
|
1376
2238
|
}
|
|
1377
2239
|
);
|
|
1378
2240
|
server.tool(
|
|
1379
2241
|
"delete-endpoint",
|
|
1380
2242
|
"Permanently delete a Serverless endpoint. This cannot be undone.",
|
|
1381
2243
|
{
|
|
1382
|
-
endpointId:
|
|
2244
|
+
endpointId: z8.string().describe("ID of the endpoint to delete")
|
|
1383
2245
|
},
|
|
1384
2246
|
{ title: "Delete endpoint", ...DESTRUCTIVE },
|
|
1385
2247
|
async (params) => {
|
|
@@ -1391,12 +2253,39 @@ function registerEndpointTools(server, rt) {
|
|
|
1391
2253
|
return jsonReply2(result);
|
|
1392
2254
|
}
|
|
1393
2255
|
);
|
|
2256
|
+
server.tool(
|
|
2257
|
+
"list-endpoint-releases",
|
|
2258
|
+
"List a Serverless endpoint's release history and current rollout status (workers on the latest version). v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
2259
|
+
{
|
|
2260
|
+
...listPaginationParams,
|
|
2261
|
+
endpointId: z8.string().describe("ID of the Serverless endpoint whose releases to list")
|
|
2262
|
+
},
|
|
2263
|
+
{ title: "List endpoint releases", ...READ_ONLY },
|
|
2264
|
+
async (params) => {
|
|
2265
|
+
const backend = backendFor("endpoints");
|
|
2266
|
+
if (backend.version === "v1") {
|
|
2267
|
+
return jsonReply2({
|
|
2268
|
+
error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2269
|
+
status: 501
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
const raw = await callRestUrl(
|
|
2273
|
+
`${backend.base}/serverless/${params.endpointId}/releases`
|
|
2274
|
+
);
|
|
2275
|
+
const releases = Array.isArray(raw?.releases) ? raw.releases : [];
|
|
2276
|
+
return capListResult(
|
|
2277
|
+
releases,
|
|
2278
|
+
{ limit: params.limit, cursor: params.cursor },
|
|
2279
|
+
{ rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
|
|
2280
|
+
);
|
|
2281
|
+
}
|
|
2282
|
+
);
|
|
1394
2283
|
server.tool(
|
|
1395
2284
|
"list-endpoint-workers",
|
|
1396
|
-
"List the workers backing a Serverless endpoint, with their status and an aggregate summary. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
2285
|
+
"List the workers backing a Serverless endpoint, with their status and an aggregate summary. v2-only \u2014 returns a 501 notice on the v1 API. An UNHEALTHY worker usually means the container is crash-looping (repeated starts that exit before the handler runs) \u2014 jobs on the endpoint will sit IN_QUEUE even though this is a worker failure, not a capacity shortage; inspect it with stream-worker-logs. Zero workers while jobs queue means the endpoint is waiting for GPU capacity. Paginated via limit/cursor.",
|
|
1397
2286
|
{
|
|
1398
2287
|
...listPaginationParams,
|
|
1399
|
-
endpointId:
|
|
2288
|
+
endpointId: z8.string().describe("ID of the Serverless endpoint whose workers to list")
|
|
1400
2289
|
},
|
|
1401
2290
|
{ title: "List endpoint workers", ...READ_ONLY },
|
|
1402
2291
|
async (params) => {
|
|
@@ -1417,30 +2306,110 @@ function registerEndpointTools(server, rt) {
|
|
|
1417
2306
|
);
|
|
1418
2307
|
}
|
|
1419
2308
|
);
|
|
2309
|
+
server.tool(
|
|
2310
|
+
"stream-worker-logs",
|
|
2311
|
+
"Fetch a bounded snapshot of a serverless worker's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Get the workerId from list-endpoint-workers. Use this to diagnose UNHEALTHY workers: repeated `start container` system lines with no container output is the crash-loop signature (the container exits before the handler runs). Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
|
|
2312
|
+
{
|
|
2313
|
+
endpointId: z8.string().describe("ID of the Serverless endpoint the worker belongs to"),
|
|
2314
|
+
workerId: z8.string().describe(
|
|
2315
|
+
"ID of the worker whose logs to stream (from list-endpoint-workers)"
|
|
2316
|
+
),
|
|
2317
|
+
...logStreamParams
|
|
2318
|
+
},
|
|
2319
|
+
{ title: "Stream worker logs", ...READ_ONLY },
|
|
2320
|
+
(params) => streamLogsReply(
|
|
2321
|
+
rt,
|
|
2322
|
+
{
|
|
2323
|
+
name: "stream-worker-logs",
|
|
2324
|
+
resource: "workers",
|
|
2325
|
+
logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
|
|
2326
|
+
},
|
|
2327
|
+
params
|
|
2328
|
+
)
|
|
2329
|
+
);
|
|
1420
2330
|
}
|
|
1421
2331
|
|
|
1422
2332
|
// src/tools/jobs.ts
|
|
1423
|
-
import { z as
|
|
2333
|
+
import { z as z9 } from "zod";
|
|
2334
|
+
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2335
|
+
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2336
|
+
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
1424
2337
|
function registerJobTools(server, rt) {
|
|
1425
|
-
const { jsonReply: jsonReply2, serverlessRequest } = rt;
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
2338
|
+
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
2339
|
+
async function diagnoseQueuedJob(endpointId) {
|
|
2340
|
+
const cached = diagnosisCache.get(endpointId);
|
|
2341
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
2342
|
+
return cached.value;
|
|
2343
|
+
}
|
|
2344
|
+
diagnosisCache.delete(endpointId);
|
|
2345
|
+
try {
|
|
2346
|
+
const backend = backendFor("workers");
|
|
2347
|
+
if (backend.version !== "v2") return null;
|
|
2348
|
+
const raw = await callRestUrl(
|
|
2349
|
+
`${backend.base}/serverless/${endpointId}/workers`
|
|
2350
|
+
);
|
|
2351
|
+
const summary = raw?.summary;
|
|
2352
|
+
const workers = raw?.workers ?? [];
|
|
2353
|
+
if (!summary) return null;
|
|
2354
|
+
const remember = (value) => {
|
|
2355
|
+
if (diagnosisCache.size >= MAX_DIAGNOSIS_ENTRIES) {
|
|
2356
|
+
const oldest = diagnosisCache.keys().next().value;
|
|
2357
|
+
if (oldest !== void 0) diagnosisCache.delete(oldest);
|
|
2358
|
+
}
|
|
2359
|
+
diagnosisCache.set(endpointId, {
|
|
2360
|
+
value,
|
|
2361
|
+
expiresAt: Date.now() + DIAGNOSIS_TTL_MS
|
|
2362
|
+
});
|
|
2363
|
+
return value;
|
|
2364
|
+
};
|
|
2365
|
+
if ((summary.unhealthy ?? 0) > 0) {
|
|
2366
|
+
const badIds = workers.filter((w) => w.status === "UNHEALTHY").map((w) => w.id).join(", ");
|
|
2367
|
+
return remember({
|
|
2368
|
+
workerHealth: summary,
|
|
2369
|
+
hint: `${summary.unhealthy} of ${summary.total} worker(s) on this endpoint are UNHEALTHY \u2014 the container is likely crash-looping (repeated starts that exit before the handler runs). The job can stay IN_QUEUE indefinitely; this is a worker failure, NOT a capacity shortage. Inspect the logs with stream-worker-logs${badIds ? ` (workerId: ${badIds})` : ""}.`
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
if ((summary.total ?? 0) === 0) {
|
|
2373
|
+
return remember({
|
|
2374
|
+
workerHealth: summary,
|
|
2375
|
+
hint: "No workers are scheduled for this endpoint yet \u2014 it may simply be scaling up from zero (normal for the first seconds of a cold start), or it is waiting for GPU capacity / its GPU-CUDA constraints exclude the currently-available hosts. If this persists, check availability with list-gpu-types or widen the gpuIds/CUDA settings."
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
if ((summary.throttled ?? 0) > 0 && (summary.running ?? 0) === 0) {
|
|
2379
|
+
return remember({
|
|
2380
|
+
workerHealth: summary,
|
|
2381
|
+
hint: "Workers exist but are throttled \u2014 the hosts are at capacity right now; the job should start when capacity frees up."
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
if ((summary.initializing ?? 0) > 0) {
|
|
2385
|
+
return remember({
|
|
2386
|
+
workerHealth: summary,
|
|
2387
|
+
hint: "A worker is initializing \u2014 likely a cold start (image pull / model load); the job should start soon."
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
return remember({ workerHealth: summary, hint: "" });
|
|
2391
|
+
} catch {
|
|
2392
|
+
return null;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
const endpointIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
|
|
2396
|
+
const jobIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
|
|
2397
|
+
const inputSchema = z9.record(z9.unknown()).describe(
|
|
1429
2398
|
"Input payload for the worker handler. The expected fields depend on the deployed model or worker."
|
|
1430
2399
|
);
|
|
1431
|
-
const webhookSchema =
|
|
2400
|
+
const webhookSchema = z9.string().url().optional().describe(
|
|
1432
2401
|
"Webhook URL to receive job completion notifications instead of polling"
|
|
1433
2402
|
);
|
|
1434
|
-
const policySchema =
|
|
1435
|
-
executionTimeout:
|
|
1436
|
-
lowPriority:
|
|
1437
|
-
ttl:
|
|
2403
|
+
const policySchema = z9.object({
|
|
2404
|
+
executionTimeout: z9.number().optional().describe("Maximum execution time in milliseconds"),
|
|
2405
|
+
lowPriority: z9.boolean().optional().describe("Submit as a low-priority job"),
|
|
2406
|
+
ttl: z9.number().optional().describe("Time-to-live for the job result in milliseconds")
|
|
1438
2407
|
}).optional().describe("Execution policy options");
|
|
1439
|
-
const s3ConfigSchema =
|
|
1440
|
-
accessId:
|
|
1441
|
-
accessSecret:
|
|
1442
|
-
bucketName:
|
|
1443
|
-
endpointUrl:
|
|
2408
|
+
const s3ConfigSchema = z9.object({
|
|
2409
|
+
accessId: z9.string().describe("S3 access key ID"),
|
|
2410
|
+
accessSecret: z9.string().describe("S3 secret access key"),
|
|
2411
|
+
bucketName: z9.string().describe("S3 bucket name"),
|
|
2412
|
+
endpointUrl: z9.string().describe("S3 endpoint URL")
|
|
1444
2413
|
}).optional().describe("S3-compatible storage config for large outputs");
|
|
1445
2414
|
server.tool(
|
|
1446
2415
|
"run-endpoint",
|
|
@@ -1474,7 +2443,7 @@ function registerJobTools(server, rt) {
|
|
|
1474
2443
|
"ID of the Serverless endpoint to run synchronously"
|
|
1475
2444
|
),
|
|
1476
2445
|
input: inputSchema,
|
|
1477
|
-
wait:
|
|
2446
|
+
wait: z9.number().min(1e3).max(3e5).optional().describe(
|
|
1478
2447
|
"How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
1479
2448
|
),
|
|
1480
2449
|
webhook: webhookSchema,
|
|
@@ -1496,7 +2465,7 @@ function registerJobTools(server, rt) {
|
|
|
1496
2465
|
);
|
|
1497
2466
|
server.tool(
|
|
1498
2467
|
"get-job-status",
|
|
1499
|
-
"Check the status of an asynchronous Serverless job. Returns the current status and output when complete. Job statuses: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, TIMED_OUT.",
|
|
2468
|
+
"Check the status of an asynchronous Serverless job. Returns the current status and output when complete. Job statuses: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, TIMED_OUT. IMPORTANT: a job stuck IN_QUEUE does not necessarily mean a capacity shortage \u2014 a worker may have spun up and crash-looped (the platform marks it UNHEALTHY while the job stays queued). When the job is IN_QUEUE this tool attaches a workerHealth summary and a hint; to dig deeper, call list-endpoint-workers (look for UNHEALTHY) and stream-worker-logs.",
|
|
1500
2469
|
{
|
|
1501
2470
|
endpointId: endpointIdSchema.describe(
|
|
1502
2471
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -1509,6 +2478,16 @@ function registerJobTools(server, rt) {
|
|
|
1509
2478
|
params.endpointId,
|
|
1510
2479
|
`/status/${params.jobId}`
|
|
1511
2480
|
);
|
|
2481
|
+
if (result && typeof result === "object" && result.status === "IN_QUEUE") {
|
|
2482
|
+
const diagnosis = await diagnoseQueuedJob(params.endpointId);
|
|
2483
|
+
if (diagnosis) {
|
|
2484
|
+
return jsonReply2({
|
|
2485
|
+
...result,
|
|
2486
|
+
workerHealth: diagnosis.workerHealth,
|
|
2487
|
+
...diagnosis.hint ? { hint: diagnosis.hint } : {}
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
1512
2491
|
return jsonReply2(result);
|
|
1513
2492
|
}
|
|
1514
2493
|
);
|
|
@@ -1610,7 +2589,7 @@ function registerJobTools(server, rt) {
|
|
|
1610
2589
|
);
|
|
1611
2590
|
server.tool(
|
|
1612
2591
|
"endpoint-health",
|
|
1613
|
-
"Get the health and operational status of a Serverless endpoint, including worker counts and job statistics.",
|
|
2592
|
+
"Get the health and operational status of a Serverless endpoint, including worker counts and job statistics. Use this when jobs are stuck IN_QUEUE: unhealthy workers mean a crash-looping container (a worker failure \u2014 inspect with stream-worker-logs), while zero workers means the endpoint is waiting for GPU capacity.",
|
|
1614
2593
|
{
|
|
1615
2594
|
endpointId: endpointIdSchema.describe(
|
|
1616
2595
|
"ID of the Serverless endpoint to check health for"
|
|
@@ -1643,7 +2622,7 @@ function registerJobTools(server, rt) {
|
|
|
1643
2622
|
}
|
|
1644
2623
|
|
|
1645
2624
|
// src/tools/templates.ts
|
|
1646
|
-
import { z as
|
|
2625
|
+
import { z as z10 } from "zod";
|
|
1647
2626
|
function registerTemplateTools(server, rt) {
|
|
1648
2627
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1649
2628
|
server.tool(
|
|
@@ -1651,9 +2630,9 @@ function registerTemplateTools(server, rt) {
|
|
|
1651
2630
|
`List available templates. By default returns only the user's own templates. Use includeRunpodTemplates to also include official Runpod templates. The recommended default template for new pods is "Runpod Pytorch 2.8.0" (ID: runpod-torch-v280) \u2014 it has the latest CUDA and PyTorch versions.`,
|
|
1652
2631
|
{
|
|
1653
2632
|
...listPaginationParams,
|
|
1654
|
-
includeRunpodTemplates:
|
|
1655
|
-
includePublicTemplates:
|
|
1656
|
-
includeEndpointBoundTemplates:
|
|
2633
|
+
includeRunpodTemplates: z10.boolean().optional().describe("Include official Runpod templates in the response"),
|
|
2634
|
+
includePublicTemplates: z10.boolean().optional().describe("Include community-made public templates in the response"),
|
|
2635
|
+
includeEndpointBoundTemplates: z10.boolean().optional().describe(
|
|
1657
2636
|
"Include templates bound to Serverless endpoints in the response"
|
|
1658
2637
|
)
|
|
1659
2638
|
},
|
|
@@ -1684,7 +2663,7 @@ function registerTemplateTools(server, rt) {
|
|
|
1684
2663
|
"get-template",
|
|
1685
2664
|
"Get one template by id (image, env, ports, disk/volume config).",
|
|
1686
2665
|
{
|
|
1687
|
-
templateId:
|
|
2666
|
+
templateId: z10.string().describe("ID of the template to retrieve")
|
|
1688
2667
|
},
|
|
1689
2668
|
{ title: "Get template", ...READ_ONLY },
|
|
1690
2669
|
async (params) => {
|
|
@@ -1697,23 +2676,27 @@ function registerTemplateTools(server, rt) {
|
|
|
1697
2676
|
);
|
|
1698
2677
|
server.tool(
|
|
1699
2678
|
"create-template",
|
|
1700
|
-
"Create a reusable pod/endpoint template (image, ports, env, disk/volume).
|
|
2679
|
+
"Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
|
|
1701
2680
|
{
|
|
1702
|
-
name:
|
|
1703
|
-
imageName:
|
|
1704
|
-
isServerless:
|
|
1705
|
-
ports:
|
|
1706
|
-
dockerEntrypoint:
|
|
1707
|
-
|
|
1708
|
-
env: z6.record(z6.string()).optional().describe("Environment variables"),
|
|
1709
|
-
containerDiskInGb: z6.number().optional().describe("Container disk size in GB"),
|
|
1710
|
-
volumeInGb: z6.number().optional().describe("Volume size in GB"),
|
|
1711
|
-
volumeMountPath: z6.string().optional().describe("Path to mount the volume"),
|
|
1712
|
-
readme: z6.string().optional().describe("README content in markdown format"),
|
|
1713
|
-
category: z6.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
|
|
1714
|
-
"Hardware category for the template. On v2 a category is required and defaults to NVIDIA when omitted \u2014 set CPU or AMD for non-NVIDIA templates so they are not mis-categorized."
|
|
2681
|
+
name: z10.string().describe("Name for the template"),
|
|
2682
|
+
imageName: z10.string().describe("Docker image to use"),
|
|
2683
|
+
isServerless: z10.boolean().optional().describe("Is this a serverless template"),
|
|
2684
|
+
ports: z10.array(z10.string()).optional().describe("Ports to expose"),
|
|
2685
|
+
dockerEntrypoint: z10.array(z10.string()).optional().describe(
|
|
2686
|
+
"Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
|
|
1715
2687
|
),
|
|
1716
|
-
|
|
2688
|
+
dockerStartCmd: z10.array(z10.string()).optional().describe(
|
|
2689
|
+
'Container start command. On v2 this is stored as the template\'s `args` string; a multi-element array is joined with spaces (e.g. ["python","-u","app.py"] \u2192 "python -u app.py"). The join is lossy: an element that itself contains spaces is NOT preserved as one argument \u2014 e.g. ["sh","-c","echo hi"] becomes "sh -c echo hi", so pass a shell command as a single pre-quoted element if that distinction matters. Omit or pass an empty array to leave the command unset (never overwrites with "").'
|
|
2690
|
+
),
|
|
2691
|
+
env: z10.record(z10.string()).optional().describe("Environment variables"),
|
|
2692
|
+
containerDiskInGb: z10.number().optional().describe("Container disk size in GB"),
|
|
2693
|
+
volumeInGb: z10.number().optional().describe("Volume size in GB"),
|
|
2694
|
+
volumeMountPath: z10.string().optional().describe("Path to mount the volume"),
|
|
2695
|
+
readme: z10.string().optional().describe("README content in markdown format"),
|
|
2696
|
+
category: z10.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
|
|
2697
|
+
"How the template is grouped and filtered in the Runpod console \u2014 it does NOT affect hardware selection, scheduling, or billing. Optional; defaults to NVIDIA. Set CPU or AMD only to file the template correctly in the console."
|
|
2698
|
+
),
|
|
2699
|
+
containerRegistryAuthId: z10.string().optional().describe(
|
|
1717
2700
|
"ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Required when imageName points at a private registry; without it serverless workers fail the image pull."
|
|
1718
2701
|
)
|
|
1719
2702
|
},
|
|
@@ -1731,15 +2714,18 @@ function registerTemplateTools(server, rt) {
|
|
|
1731
2714
|
);
|
|
1732
2715
|
server.tool(
|
|
1733
2716
|
"update-template",
|
|
1734
|
-
"Update a template's mutable fields (name, image, ports, env,
|
|
2717
|
+
"Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
|
|
1735
2718
|
{
|
|
1736
|
-
templateId:
|
|
1737
|
-
name:
|
|
1738
|
-
imageName:
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2719
|
+
templateId: z10.string().describe("ID of the template to update"),
|
|
2720
|
+
name: z10.string().optional().describe("New name for the template"),
|
|
2721
|
+
imageName: z10.string().optional().describe("New Docker image"),
|
|
2722
|
+
dockerStartCmd: z10.array(z10.string()).optional().describe(
|
|
2723
|
+
"New container start command. On v2 stored as the template's `args` string; a multi-element array is joined with spaces (lossy \u2014 an element containing spaces is split into separate tokens). Note: this can only set a command, not clear one \u2014 an empty or absent array is ignored so the existing command is preserved."
|
|
2724
|
+
),
|
|
2725
|
+
ports: z10.array(z10.string()).optional().describe("New ports to expose"),
|
|
2726
|
+
env: z10.record(z10.string()).optional().describe("New environment variables"),
|
|
2727
|
+
readme: z10.string().optional().describe("New README content in markdown format"),
|
|
2728
|
+
containerRegistryAuthId: z10.string().optional().describe(
|
|
1743
2729
|
"ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Set this to attach a credential to a template referencing a private registry."
|
|
1744
2730
|
)
|
|
1745
2731
|
},
|
|
@@ -1760,7 +2746,7 @@ function registerTemplateTools(server, rt) {
|
|
|
1760
2746
|
"delete-template",
|
|
1761
2747
|
"Permanently delete a template. This cannot be undone.",
|
|
1762
2748
|
{
|
|
1763
|
-
templateId:
|
|
2749
|
+
templateId: z10.string().describe("ID of the template to delete")
|
|
1764
2750
|
},
|
|
1765
2751
|
{ title: "Delete template", ...DESTRUCTIVE },
|
|
1766
2752
|
async (params) => {
|
|
@@ -1775,7 +2761,7 @@ function registerTemplateTools(server, rt) {
|
|
|
1775
2761
|
}
|
|
1776
2762
|
|
|
1777
2763
|
// src/tools/network-volumes.ts
|
|
1778
|
-
import { z as
|
|
2764
|
+
import { z as z11 } from "zod";
|
|
1779
2765
|
function registerNetworkVolumeTools(server, rt) {
|
|
1780
2766
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1781
2767
|
server.tool(
|
|
@@ -1796,7 +2782,7 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1796
2782
|
"get-network-volume",
|
|
1797
2783
|
"Get one network volume by id (name, size, data center).",
|
|
1798
2784
|
{
|
|
1799
|
-
networkVolumeId:
|
|
2785
|
+
networkVolumeId: z11.string().describe("ID of the network volume to retrieve")
|
|
1800
2786
|
},
|
|
1801
2787
|
{ title: "Get network volume", ...READ_ONLY },
|
|
1802
2788
|
async (params) => {
|
|
@@ -1809,11 +2795,17 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1809
2795
|
);
|
|
1810
2796
|
server.tool(
|
|
1811
2797
|
"create-network-volume",
|
|
1812
|
-
"Create a persistent network volume in a data center (size
|
|
2798
|
+
"Create a persistent network volume in a data center (size 10\u20134096 GB) that pods can mount. Pass volumeType to pick a storage tier; omit it to get the data center default.",
|
|
1813
2799
|
{
|
|
1814
|
-
name:
|
|
1815
|
-
|
|
1816
|
-
|
|
2800
|
+
name: z11.string().describe("Name for the network volume"),
|
|
2801
|
+
// Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
|
|
2802
|
+
// here as well as described, so an out-of-range value fails locally
|
|
2803
|
+
// instead of costing an API roundtrip.
|
|
2804
|
+
size: z11.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
|
|
2805
|
+
dataCenterId: z11.string().describe("Data center ID (see list-data-centers)"),
|
|
2806
|
+
volumeType: z11.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
|
|
2807
|
+
"Storage tier. Omit for the data center's default (primary) tier. HIGH_PERFORMANCE provisions an HPS volume. Immutable after creation \u2014 update-network-volume cannot change it."
|
|
2808
|
+
)
|
|
1817
2809
|
},
|
|
1818
2810
|
{ title: "Create network volume", ...WRITE },
|
|
1819
2811
|
async (params) => {
|
|
@@ -1831,9 +2823,11 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1831
2823
|
"update-network-volume",
|
|
1832
2824
|
"Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
|
|
1833
2825
|
{
|
|
1834
|
-
networkVolumeId:
|
|
1835
|
-
name:
|
|
1836
|
-
|
|
2826
|
+
networkVolumeId: z11.string().describe("ID of the network volume to update"),
|
|
2827
|
+
name: z11.string().optional().describe("New name for the network volume"),
|
|
2828
|
+
// Same absolute bounds as create. "Larger than current" stays server-side —
|
|
2829
|
+
// we do not hold the current size here, so only the range is checked.
|
|
2830
|
+
size: z11.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
|
|
1837
2831
|
},
|
|
1838
2832
|
{ title: "Update network volume", ...WRITE, idempotentHint: true },
|
|
1839
2833
|
async (params) => {
|
|
@@ -1852,7 +2846,7 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1852
2846
|
"delete-network-volume",
|
|
1853
2847
|
"Permanently delete a network volume and its data. This cannot be undone.",
|
|
1854
2848
|
{
|
|
1855
|
-
networkVolumeId:
|
|
2849
|
+
networkVolumeId: z11.string().describe("ID of the network volume to delete")
|
|
1856
2850
|
},
|
|
1857
2851
|
{ title: "Delete network volume", ...DESTRUCTIVE },
|
|
1858
2852
|
async (params) => {
|
|
@@ -1867,9 +2861,14 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1867
2861
|
}
|
|
1868
2862
|
|
|
1869
2863
|
// src/tools/registries.ts
|
|
1870
|
-
import { z as
|
|
2864
|
+
import { z as z12 } from "zod";
|
|
2865
|
+
var DELEGATIONS = "/delegations";
|
|
1871
2866
|
function registerRegistryTools(server, rt) {
|
|
1872
2867
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
2868
|
+
const delegationsV2OnlyNotice = (tool) => ({
|
|
2869
|
+
error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
2870
|
+
status: 501
|
|
2871
|
+
});
|
|
1873
2872
|
server.tool(
|
|
1874
2873
|
"list-container-registry-auths",
|
|
1875
2874
|
"List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
|
|
@@ -1888,7 +2887,7 @@ function registerRegistryTools(server, rt) {
|
|
|
1888
2887
|
"get-container-registry-auth",
|
|
1889
2888
|
"Get one saved container-registry credential by id (the secret/password is not returned).",
|
|
1890
2889
|
{
|
|
1891
|
-
containerRegistryAuthId:
|
|
2890
|
+
containerRegistryAuthId: z12.string().describe("ID of the container registry auth to retrieve")
|
|
1892
2891
|
},
|
|
1893
2892
|
{ title: "Get container registry auth", ...READ_ONLY },
|
|
1894
2893
|
async (params) => {
|
|
@@ -1903,9 +2902,9 @@ function registerRegistryTools(server, rt) {
|
|
|
1903
2902
|
"create-container-registry-auth",
|
|
1904
2903
|
"Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
|
|
1905
2904
|
{
|
|
1906
|
-
name:
|
|
1907
|
-
username:
|
|
1908
|
-
password:
|
|
2905
|
+
name: z12.string().describe("Name for the container registry auth"),
|
|
2906
|
+
username: z12.string().describe("Registry username"),
|
|
2907
|
+
password: z12.string().describe("Registry password or access token (stored as a secret)")
|
|
1909
2908
|
},
|
|
1910
2909
|
{ title: "Create container registry auth", ...WRITE },
|
|
1911
2910
|
async (params) => {
|
|
@@ -1923,7 +2922,7 @@ function registerRegistryTools(server, rt) {
|
|
|
1923
2922
|
"delete-container-registry-auth",
|
|
1924
2923
|
"Permanently delete a saved container-registry credential. This cannot be undone.",
|
|
1925
2924
|
{
|
|
1926
|
-
containerRegistryAuthId:
|
|
2925
|
+
containerRegistryAuthId: z12.string().describe("ID of the container registry auth to delete")
|
|
1927
2926
|
},
|
|
1928
2927
|
{ title: "Delete container registry auth", ...DESTRUCTIVE },
|
|
1929
2928
|
async (params) => {
|
|
@@ -1935,166 +2934,69 @@ function registerRegistryTools(server, rt) {
|
|
|
1935
2934
|
return jsonReply2(result);
|
|
1936
2935
|
}
|
|
1937
2936
|
);
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
// src/tools/tags.ts
|
|
1941
|
-
import { z as z9 } from "zod";
|
|
1942
|
-
var TAG_RESOURCE_TYPES = [
|
|
1943
|
-
"POD",
|
|
1944
|
-
"NETWORK_VOLUME",
|
|
1945
|
-
"CLUSTER",
|
|
1946
|
-
"SERVERLESS_ENDPOINT"
|
|
1947
|
-
];
|
|
1948
|
-
var taggedResourceSchema = z9.object({
|
|
1949
|
-
id: z9.string().describe("Resource identifier (e.g. a pod or endpoint id)"),
|
|
1950
|
-
type: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to associate with the tag")
|
|
1951
|
-
});
|
|
1952
|
-
function v2OnlyNotice(tool) {
|
|
1953
|
-
return {
|
|
1954
|
-
error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
1955
|
-
status: 501
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
function registerTagTools(server, rt) {
|
|
1959
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1960
2937
|
server.tool(
|
|
1961
|
-
"list-
|
|
1962
|
-
"List your
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
includeResources: z9.boolean().optional().describe("Include the resources attached to each tag in the response")
|
|
1966
|
-
},
|
|
1967
|
-
{ title: "List tags", ...READ_ONLY },
|
|
2938
|
+
"list-registry-delegations",
|
|
2939
|
+
"List your AWS ECR delegations \u2014 ECR repositories Runpod has been granted scoped pull access to, so private ECR images can be pulled without storing AWS credentials. Each entry includes the `dockerRegistryUri` to pull from. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
2940
|
+
listPaginationParams,
|
|
2941
|
+
{ title: "List registry delegations", ...READ_ONLY },
|
|
1968
2942
|
async (params) => {
|
|
1969
|
-
const backend = backendFor("
|
|
1970
|
-
if (backend.version === "v1")
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2943
|
+
const backend = backendFor("registries");
|
|
2944
|
+
if (backend.version === "v1") {
|
|
2945
|
+
return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
|
|
2946
|
+
}
|
|
2947
|
+
const raw = await callRestUrl(
|
|
2948
|
+
`${backend.base}${backend.list}${DELEGATIONS}`
|
|
2949
|
+
);
|
|
2950
|
+
const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
|
|
2951
|
+
return capListResult(delegations, {
|
|
1974
2952
|
limit: params.limit,
|
|
1975
2953
|
cursor: params.cursor
|
|
1976
2954
|
});
|
|
1977
2955
|
}
|
|
1978
2956
|
);
|
|
1979
2957
|
server.tool(
|
|
1980
|
-
"
|
|
1981
|
-
"
|
|
2958
|
+
"create-registry-delegation",
|
|
2959
|
+
"Register an AWS ECR repository so Runpod can pull private images from it without stored credentials. Pass the repository ARN; the reply carries the `dockerRegistryUri`, resolved repository/tag, and AWS region. v2-only \u2014 returns a 501 notice on the v1 API. For a non-ECR private registry use create-container-registry-auth instead.",
|
|
1982
2960
|
{
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
async (params) => {
|
|
1988
|
-
const backend = backendFor("tags");
|
|
1989
|
-
if (backend.version === "v1") return jsonReply2(v2OnlyNotice("get-tag"));
|
|
1990
|
-
const query = params.includeResources ? "?include=resources" : "";
|
|
1991
|
-
const result = await callRestUrl(
|
|
1992
|
-
`${backend.base}${backend.get(params.tagId)}${query}`
|
|
1993
|
-
);
|
|
1994
|
-
return jsonReply2(result);
|
|
1995
|
-
}
|
|
1996
|
-
);
|
|
1997
|
-
server.tool(
|
|
1998
|
-
"create-tag",
|
|
1999
|
-
"Create a key/value tag, optionally attaching resources at creation. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2000
|
-
{
|
|
2001
|
-
key: z9.string().min(1).describe('Tag key (e.g. "project")'),
|
|
2002
|
-
value: z9.string().min(1).describe('Tag value (e.g. "gpt-training")'),
|
|
2003
|
-
resources: z9.array(taggedResourceSchema).optional().describe("Resources to attach to the tag at creation time")
|
|
2961
|
+
resource: z12.string().min(1).describe(
|
|
2962
|
+
'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
|
|
2963
|
+
),
|
|
2964
|
+
name: z12.string().optional().describe("Optional human-readable name for the delegation")
|
|
2004
2965
|
},
|
|
2005
|
-
{ title: "Create
|
|
2966
|
+
{ title: "Create registry delegation", ...WRITE },
|
|
2006
2967
|
async (params) => {
|
|
2007
|
-
const backend = backendFor("
|
|
2008
|
-
if (backend.version === "v1")
|
|
2009
|
-
return jsonReply2(
|
|
2968
|
+
const backend = backendFor("registries");
|
|
2969
|
+
if (backend.version === "v1") {
|
|
2970
|
+
return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
|
|
2971
|
+
}
|
|
2972
|
+
const body = { resource: params.resource };
|
|
2973
|
+
if (params.name !== void 0) body.name = params.name;
|
|
2010
2974
|
const result = await callRestUrl(
|
|
2011
|
-
`${backend.base}${backend.list}`,
|
|
2975
|
+
`${backend.base}${backend.list}${DELEGATIONS}`,
|
|
2012
2976
|
"POST",
|
|
2013
|
-
|
|
2977
|
+
body
|
|
2014
2978
|
);
|
|
2015
2979
|
return jsonReply2(result);
|
|
2016
2980
|
}
|
|
2017
2981
|
);
|
|
2018
2982
|
server.tool(
|
|
2019
|
-
"
|
|
2020
|
-
"
|
|
2983
|
+
"delete-registry-delegation",
|
|
2984
|
+
"Revoke an AWS ECR delegation, withdrawing Runpod's pull access to that repository. Existing workers keep running; future pulls fail. v2-only \u2014 returns a 501 notice on the v1 API. This cannot be undone.",
|
|
2021
2985
|
{
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
value: z9.string().min(1).optional().describe("New tag value"),
|
|
2025
|
-
resources: z9.array(taggedResourceSchema).optional().describe(
|
|
2026
|
-
"Replace the tag's resource associations with exactly these. Omit to leave unchanged; pass [] to detach all."
|
|
2986
|
+
delegationId: z12.string().describe(
|
|
2987
|
+
"ID of the delegation to revoke (from list-registry-delegations)"
|
|
2027
2988
|
)
|
|
2028
2989
|
},
|
|
2029
|
-
|
|
2030
|
-
// retried-after-partial-failure update is not guaranteed to be a no-op.
|
|
2031
|
-
{ title: "Update tag", ...WRITE },
|
|
2032
|
-
async (params) => {
|
|
2033
|
-
const { tagId, ...updateParams } = params;
|
|
2034
|
-
const backend = backendFor("tags");
|
|
2035
|
-
if (backend.version === "v1")
|
|
2036
|
-
return jsonReply2(v2OnlyNotice("update-tag"));
|
|
2037
|
-
const result = await callRestUrl(
|
|
2038
|
-
`${backend.base}${backend.get(tagId)}`,
|
|
2039
|
-
"PATCH",
|
|
2040
|
-
updateParams
|
|
2041
|
-
);
|
|
2042
|
-
return jsonReply2(result);
|
|
2043
|
-
}
|
|
2044
|
-
);
|
|
2045
|
-
server.tool(
|
|
2046
|
-
"delete-tag",
|
|
2047
|
-
"Permanently delete a tag (removes it from all resources it is attached to). This cannot be undone. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2048
|
-
{
|
|
2049
|
-
tagId: z9.string().describe("ID of the tag to delete")
|
|
2050
|
-
},
|
|
2051
|
-
{ title: "Delete tag", ...DESTRUCTIVE },
|
|
2052
|
-
async (params) => {
|
|
2053
|
-
const backend = backendFor("tags");
|
|
2054
|
-
if (backend.version === "v1")
|
|
2055
|
-
return jsonReply2(v2OnlyNotice("delete-tag"));
|
|
2056
|
-
const result = await callRestUrl(
|
|
2057
|
-
`${backend.base}${backend.get(params.tagId)}`,
|
|
2058
|
-
"DELETE"
|
|
2059
|
-
);
|
|
2060
|
-
return jsonReply2(result);
|
|
2061
|
-
}
|
|
2062
|
-
);
|
|
2063
|
-
server.tool(
|
|
2064
|
-
"attach-tag",
|
|
2065
|
-
"Attach a tag to a resource (pod, network volume, cluster, or serverless endpoint). Idempotent \u2014 attaching an already-associated resource is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2066
|
-
{
|
|
2067
|
-
tagId: z9.string().describe("ID of the tag"),
|
|
2068
|
-
resourceType: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to attach the tag to"),
|
|
2069
|
-
resourceId: z9.string().describe("ID of the resource to attach the tag to")
|
|
2070
|
-
},
|
|
2071
|
-
{ title: "Attach tag to resource", ...WRITE, idempotentHint: true },
|
|
2072
|
-
async (params) => {
|
|
2073
|
-
const backend = backendFor("tags");
|
|
2074
|
-
if (backend.version === "v1")
|
|
2075
|
-
return jsonReply2(v2OnlyNotice("attach-tag"));
|
|
2076
|
-
const result = await callRestUrl(
|
|
2077
|
-
`${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
|
|
2078
|
-
"PUT"
|
|
2079
|
-
);
|
|
2080
|
-
return jsonReply2(result);
|
|
2081
|
-
}
|
|
2082
|
-
);
|
|
2083
|
-
server.tool(
|
|
2084
|
-
"detach-tag",
|
|
2085
|
-
"Detach a tag from a resource (removes the association only \u2014 the resource and tag are not deleted). Idempotent \u2014 detaching a resource that is not associated is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2086
|
-
{
|
|
2087
|
-
tagId: z9.string().describe("ID of the tag"),
|
|
2088
|
-
resourceType: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to detach the tag from"),
|
|
2089
|
-
resourceId: z9.string().describe("ID of the resource to detach the tag from")
|
|
2090
|
-
},
|
|
2091
|
-
{ title: "Detach tag from resource", ...WRITE, idempotentHint: true },
|
|
2990
|
+
{ title: "Revoke registry delegation", ...DESTRUCTIVE },
|
|
2092
2991
|
async (params) => {
|
|
2093
|
-
const backend = backendFor("
|
|
2094
|
-
if (backend.version === "v1")
|
|
2095
|
-
return jsonReply2(
|
|
2992
|
+
const backend = backendFor("registries");
|
|
2993
|
+
if (backend.version === "v1") {
|
|
2994
|
+
return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
|
|
2995
|
+
}
|
|
2096
2996
|
const result = await callRestUrl(
|
|
2097
|
-
`${backend.base}
|
|
2997
|
+
`${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
|
|
2998
|
+
params.delegationId
|
|
2999
|
+
)}`,
|
|
2098
3000
|
"DELETE"
|
|
2099
3001
|
);
|
|
2100
3002
|
return jsonReply2(result);
|
|
@@ -2103,7 +3005,7 @@ function registerTagTools(server, rt) {
|
|
|
2103
3005
|
}
|
|
2104
3006
|
|
|
2105
3007
|
// src/tools/billing.ts
|
|
2106
|
-
import { z as
|
|
3008
|
+
import { z as z13 } from "zod";
|
|
2107
3009
|
var BILLING_SCOPE_PATH = {
|
|
2108
3010
|
all: "/billing",
|
|
2109
3011
|
pods: "/billing/pods",
|
|
@@ -2119,7 +3021,7 @@ function registerBillingTools(server, rt) {
|
|
|
2119
3021
|
"Get time-bucketed Runpod spend history, either aggregated across all resources or broken down by resource type (pods, serverless, endpoints, network volumes, clusters). v2-only \u2014 returns a 501 notice on the v1 API. Records are capped via limit/cursor; narrow the window with startTime/endTime or lastN.",
|
|
2120
3022
|
{
|
|
2121
3023
|
...listPaginationParams,
|
|
2122
|
-
scope:
|
|
3024
|
+
scope: z13.enum([
|
|
2123
3025
|
"all",
|
|
2124
3026
|
"pods",
|
|
2125
3027
|
"serverless",
|
|
@@ -2129,14 +3031,14 @@ function registerBillingTools(server, rt) {
|
|
|
2129
3031
|
]).optional().describe(
|
|
2130
3032
|
'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
|
|
2131
3033
|
),
|
|
2132
|
-
startTime:
|
|
3034
|
+
startTime: z13.string().optional().describe(
|
|
2133
3035
|
"Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
|
|
2134
3036
|
),
|
|
2135
|
-
endTime:
|
|
3037
|
+
endTime: z13.string().optional().describe(
|
|
2136
3038
|
"End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
|
|
2137
3039
|
),
|
|
2138
|
-
bucketSize:
|
|
2139
|
-
lastN:
|
|
3040
|
+
bucketSize: z13.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
|
|
3041
|
+
lastN: z13.number().int().positive().optional().describe(
|
|
2140
3042
|
"Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
|
|
2141
3043
|
)
|
|
2142
3044
|
},
|
|
@@ -2171,13 +3073,15 @@ function registerBillingTools(server, rt) {
|
|
|
2171
3073
|
function registerTools(server, ctx, deps = {}) {
|
|
2172
3074
|
const rt = createToolRuntime(server, ctx, deps);
|
|
2173
3075
|
registerCatalogTools(server, rt);
|
|
3076
|
+
registerHubTools(server, rt);
|
|
3077
|
+
registerPublicEndpointTools(server, rt);
|
|
2174
3078
|
registerPodTools(server, rt);
|
|
2175
3079
|
registerEndpointTools(server, rt);
|
|
3080
|
+
registerEndpointGpuTools(server, rt);
|
|
2176
3081
|
registerJobTools(server, rt);
|
|
2177
3082
|
registerTemplateTools(server, rt);
|
|
2178
3083
|
registerNetworkVolumeTools(server, rt);
|
|
2179
3084
|
registerRegistryTools(server, rt);
|
|
2180
|
-
registerTagTools(server, rt);
|
|
2181
3085
|
registerBillingTools(server, rt);
|
|
2182
3086
|
}
|
|
2183
3087
|
|