@runpod/mcp-server 3.4.0 → 4.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 +55 -1
- package/LICENSE +1 -1
- package/README.md +30 -12
- package/THIRD_PARTY_NOTICES.txt +73 -0
- package/dist/chunk-DDX42UTT.mjs +26 -0
- package/dist/chunk-DDX42UTT.mjs.map +1 -0
- package/dist/http.cjs +8384 -3445
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +8 -8
- package/dist/http.d.ts +8 -8
- package/dist/http.mjs +8773 -55
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +8441 -3466
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +8487 -3527
- package/dist/stdio.mjs.map +1 -1
- package/dist/{wizard-FBLOZ6YM.mjs → wizard-YYU3OXFW.mjs} +5 -3
- package/dist/{wizard-FBLOZ6YM.mjs.map → wizard-YYU3OXFW.mjs.map} +1 -1
- package/package.json +17 -11
- package/dist/chunk-G4URDODF.mjs +0 -3776
- package/dist/chunk-G4URDODF.mjs.map +0 -1
- package/dist/tools.cjs +0 -3799
- package/dist/tools.cjs.map +0 -1
- package/dist/tools.d.cts +0 -95
- package/dist/tools.d.ts +0 -95
- package/dist/tools.mjs +0 -7
- package/dist/tools.mjs.map +0 -1
package/dist/tools.cjs
DELETED
|
@@ -1,3799 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// src/tools.ts
|
|
31
|
-
var tools_exports = {};
|
|
32
|
-
__export(tools_exports, {
|
|
33
|
-
registerTools: () => registerTools
|
|
34
|
-
});
|
|
35
|
-
module.exports = __toCommonJS(tools_exports);
|
|
36
|
-
|
|
37
|
-
// src/tools/runtime.ts
|
|
38
|
-
var import_node_fetch = __toESM(require("node-fetch"), 1);
|
|
39
|
-
var import_node_crypto = require("crypto");
|
|
40
|
-
|
|
41
|
-
// src/_shared/rate-limit.ts
|
|
42
|
-
var GENERIC_RATE_LIMIT_HINT = "rate limited \u2014 back off before retrying, and pace bulk operations";
|
|
43
|
-
var MAX_PLAUSIBLE_WAIT_S = 7 * 86400;
|
|
44
|
-
function remainingRequests(raw) {
|
|
45
|
-
const value = raw?.trim();
|
|
46
|
-
return value && /^\d+$/.test(value) ? Number(value) : null;
|
|
47
|
-
}
|
|
48
|
-
function waitSeconds(raw) {
|
|
49
|
-
const value = raw?.trim();
|
|
50
|
-
if (!value || !/^\d+$/.test(value)) return null;
|
|
51
|
-
const seconds = Number(value);
|
|
52
|
-
return seconds <= MAX_PLAUSIBLE_WAIT_S ? seconds : null;
|
|
53
|
-
}
|
|
54
|
-
var WINDOW_NAME_RE = /^(?:"([\x20-\x21\x23-\x5b\x5d-\x7e]{1,32})"|([A-Za-z*][A-Za-z0-9!#$%&'*+.^_`|~:/-]{0,31}))$/;
|
|
55
|
-
function windowName(raw) {
|
|
56
|
-
const match = WINDOW_NAME_RE.exec(raw.trim());
|
|
57
|
-
return match ? match[1] ?? match[2] : null;
|
|
58
|
-
}
|
|
59
|
-
var QUOTED_VALUE_RE = /^"(?:[\x20-\x21\x23-\x5b\x5d-\x7e]|\\["\\])*"$/;
|
|
60
|
-
var DISPLAY_VALUE_RE = /^%"(?:[\x20-\x21\x23-\x24\x26-\x5b\x5d-\x7e]|\\|%[0-9a-f]{2})*"$/;
|
|
61
|
-
function isWellFormedParam(param) {
|
|
62
|
-
const eq = param.indexOf("=");
|
|
63
|
-
const key = (eq === -1 ? param : param.slice(0, eq)).trim();
|
|
64
|
-
if (key.includes('"')) return false;
|
|
65
|
-
if (eq === -1) return true;
|
|
66
|
-
const value = param.slice(eq + 1).trim();
|
|
67
|
-
if (value.startsWith('"')) return QUOTED_VALUE_RE.test(value);
|
|
68
|
-
if (value.startsWith('%"')) return DISPLAY_VALUE_RE.test(value);
|
|
69
|
-
return !/[\s"]/.test(value);
|
|
70
|
-
}
|
|
71
|
-
function splitOutsideStrings(input, sep) {
|
|
72
|
-
const parts = [];
|
|
73
|
-
let current = "";
|
|
74
|
-
let inString = false;
|
|
75
|
-
let inDisplay = false;
|
|
76
|
-
let escaped = false;
|
|
77
|
-
let prev = "";
|
|
78
|
-
for (const ch of input) {
|
|
79
|
-
if (escaped) escaped = false;
|
|
80
|
-
else if (inString && !inDisplay && ch === "\\") escaped = true;
|
|
81
|
-
else if (ch === '"') {
|
|
82
|
-
if (!inString) inDisplay = prev === "%";
|
|
83
|
-
inString = !inString;
|
|
84
|
-
} else if (ch === sep && !inString) {
|
|
85
|
-
parts.push(current);
|
|
86
|
-
current = "";
|
|
87
|
-
prev = ch;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
current += ch;
|
|
91
|
-
prev = ch;
|
|
92
|
-
}
|
|
93
|
-
parts.push(current);
|
|
94
|
-
return parts;
|
|
95
|
-
}
|
|
96
|
-
function readParams(params) {
|
|
97
|
-
const values = /* @__PURE__ */ new Map();
|
|
98
|
-
let unreadable = false;
|
|
99
|
-
let valuelessAt = -1;
|
|
100
|
-
for (const [index, param] of params.entries()) {
|
|
101
|
-
const eq = param.indexOf("=");
|
|
102
|
-
const key = (eq === -1 ? param : param.slice(0, eq)).trim().toLowerCase();
|
|
103
|
-
if (!key) continue;
|
|
104
|
-
if (eq === -1) {
|
|
105
|
-
if (key === "r") unreadable = true;
|
|
106
|
-
if (key === "t") values.delete("t");
|
|
107
|
-
valuelessAt = index;
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if ((key === "r" || key === "t") && (values.has(key) || valuelessAt !== -1 && index > valuelessAt)) {
|
|
111
|
-
unreadable = true;
|
|
112
|
-
}
|
|
113
|
-
values.set(key, param.slice(eq + 1));
|
|
114
|
-
}
|
|
115
|
-
return { values, unreadable };
|
|
116
|
-
}
|
|
117
|
-
function parseExhaustedWindows(header) {
|
|
118
|
-
if (!header) return { windows: [], dropped: false };
|
|
119
|
-
const windows = [];
|
|
120
|
-
let dropped = false;
|
|
121
|
-
for (const item of splitOutsideStrings(header, ",")) {
|
|
122
|
-
if (item.trim() === "") continue;
|
|
123
|
-
const [rawName, ...params] = splitOutsideStrings(item, ";");
|
|
124
|
-
if (!params.every(isWellFormedParam)) {
|
|
125
|
-
dropped = true;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const { values, unreadable } = readParams(params);
|
|
129
|
-
if (unreadable) {
|
|
130
|
-
dropped = true;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
const remaining = remainingRequests(values.get("r"));
|
|
134
|
-
if (remaining === null) {
|
|
135
|
-
dropped = true;
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
if (remaining !== 0) continue;
|
|
139
|
-
windows.push({
|
|
140
|
-
window: windowName(rawName),
|
|
141
|
-
resetS: waitSeconds(values.get("t"))
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
return { windows, dropped };
|
|
145
|
-
}
|
|
146
|
-
function humanWait(seconds) {
|
|
147
|
-
if (seconds < 120) return `~${seconds}s`;
|
|
148
|
-
if (seconds < 3600)
|
|
149
|
-
return `~${Math.ceil(seconds / 60)} minutes (${seconds}s)`;
|
|
150
|
-
if (seconds < 2 * 86400) {
|
|
151
|
-
const hours = Math.ceil(seconds / 3600);
|
|
152
|
-
return `~${hours} hour${hours === 1 ? "" : "s"} (${seconds}s)`;
|
|
153
|
-
}
|
|
154
|
-
return `~${Math.ceil(seconds / 86400)} days (${seconds}s)`;
|
|
155
|
-
}
|
|
156
|
-
function rateLimitHint(rateLimitHeader, retryAfterHeader) {
|
|
157
|
-
const { windows, dropped } = parseExhaustedWindows(rateLimitHeader);
|
|
158
|
-
windows.sort((a, b) => (b.resetS ?? -1) - (a.resetS ?? -1));
|
|
159
|
-
const longestWindow = windows[0];
|
|
160
|
-
const waitCandidates = [
|
|
161
|
-
waitSeconds(retryAfterHeader),
|
|
162
|
-
longestWindow?.resetS
|
|
163
|
-
].filter((n) => typeof n === "number" && n > 0);
|
|
164
|
-
const waitS = waitCandidates.length ? Math.max(...waitCandidates) : null;
|
|
165
|
-
const retryAfterUnreadable = typeof retryAfterHeader === "string" && retryAfterHeader.trim() !== "" && waitSeconds(retryAfterHeader) === null;
|
|
166
|
-
if (!longestWindow && waitS === null) {
|
|
167
|
-
if (!retryAfterUnreadable) return GENERIC_RATE_LIMIT_HINT;
|
|
168
|
-
}
|
|
169
|
-
const namedClause = longestWindow?.window ? `the ${longestWindow.window} quota is exhausted` : "";
|
|
170
|
-
if (waitS === null) {
|
|
171
|
-
const cause = namedClause ? `${namedClause}, but the response gave no usable reset time` : "the response gave no usable reset time";
|
|
172
|
-
return `rate limited \u2014 ${cause}; back off substantially before retrying, and pace bulk operations`;
|
|
173
|
-
}
|
|
174
|
-
const hedge = dropped || retryAfterUnreadable || windows.some((w) => w.resetS === null) ? "at least " : "";
|
|
175
|
-
const wait = `wait ${hedge}${humanWait(waitS)} before retrying, and pace bulk operations`;
|
|
176
|
-
return `rate limited \u2014 ${namedClause ? `${namedClause}; ` : ""}${wait}`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// src/_shared/http.ts
|
|
180
|
-
var EXPIRED_CREDENTIAL_HINT = "the Runpod API rejected the credential \u2014 the API key may be expired or revoked; re-authenticate or update RUNPOD_API_KEY";
|
|
181
|
-
var HttpError = class extends Error {
|
|
182
|
-
constructor(prefix, status, body, hint) {
|
|
183
|
-
const shownBody = body.length > 2048 ? `${body.slice(0, 2048)}\u2026 [truncated ${body.length - 2048} chars]` : body;
|
|
184
|
-
super(`${prefix}: ${status} - ${shownBody}${hint ? ` (${hint})` : ""}`);
|
|
185
|
-
this.name = "HttpError";
|
|
186
|
-
this.status = status;
|
|
187
|
-
this.body = body;
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
191
|
-
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
192
|
-
var RequestTimeoutError = class extends Error {
|
|
193
|
-
constructor(prefix, timeoutMs, method) {
|
|
194
|
-
const write = method !== "GET" && method !== "HEAD";
|
|
195
|
-
super(
|
|
196
|
-
`${prefix}: no response after ${timeoutMs}ms for ${method} (the API may be overloaded, or still be working on it). ${write ? "This request may have SUCCEEDED upstream \u2014 do not retry blindly. Check with the matching list-/get- tool first and retry only if it did not take effect." : "This was a read and changed nothing, so retrying is safe."}`
|
|
197
|
-
);
|
|
198
|
-
this.name = "RequestTimeoutError";
|
|
199
|
-
this.timeoutMs = timeoutMs;
|
|
200
|
-
this.method = method;
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
204
|
-
const signal = AbortSignal.timeout(timeoutMs);
|
|
205
|
-
try {
|
|
206
|
-
return await run(signal);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
209
|
-
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
function isJsonContentType(contentType) {
|
|
213
|
-
if (!contentType) return false;
|
|
214
|
-
const ct = contentType.toLowerCase();
|
|
215
|
-
return ct.includes("application/json") || ct.includes("+json");
|
|
216
|
-
}
|
|
217
|
-
function methodSendsBody(method) {
|
|
218
|
-
return method === "POST" || method === "PATCH" || method === "PUT";
|
|
219
|
-
}
|
|
220
|
-
function buildRequestHeaders(apiKey, tracking) {
|
|
221
|
-
return {
|
|
222
|
-
Authorization: `Bearer ${apiKey}`,
|
|
223
|
-
"Content-Type": "application/json",
|
|
224
|
-
...tracking()
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
function resolveCeiling(ceiling) {
|
|
228
|
-
if (ceiling === void 0) return Infinity;
|
|
229
|
-
if (typeof ceiling !== "function") return ceiling;
|
|
230
|
-
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
231
|
-
}
|
|
232
|
-
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
233
|
-
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
234
|
-
}
|
|
235
|
-
function createHttpClient(deps) {
|
|
236
|
-
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
237
|
-
return async function request(url, method = "GET", body, options) {
|
|
238
|
-
const timeoutMs = clampTimeout(
|
|
239
|
-
options?.timeoutMs,
|
|
240
|
-
deps.maxTimeoutMs,
|
|
241
|
-
defaultTimeoutMs
|
|
242
|
-
);
|
|
243
|
-
return withRequestTimeout(
|
|
244
|
-
deps.errorPrefix,
|
|
245
|
-
timeoutMs,
|
|
246
|
-
method,
|
|
247
|
-
async (signal) => {
|
|
248
|
-
const init = {
|
|
249
|
-
method,
|
|
250
|
-
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
251
|
-
signal
|
|
252
|
-
};
|
|
253
|
-
if (body && methodSendsBody(method)) {
|
|
254
|
-
init.body = JSON.stringify(body);
|
|
255
|
-
}
|
|
256
|
-
const response = await deps.fetch(url, init);
|
|
257
|
-
if (!response.ok) {
|
|
258
|
-
const hint = response.status === 429 ? rateLimitHint(
|
|
259
|
-
response.headers.get("ratelimit"),
|
|
260
|
-
response.headers.get("retry-after")
|
|
261
|
-
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
262
|
-
const text = await response.text().catch(
|
|
263
|
-
() => "<body unavailable: the connection closed while reading it>"
|
|
264
|
-
);
|
|
265
|
-
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
266
|
-
}
|
|
267
|
-
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
268
|
-
}
|
|
269
|
-
);
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// src/_shared/tracking.ts
|
|
274
|
-
function sanitizeUaToken(value) {
|
|
275
|
-
return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
|
|
276
|
-
}
|
|
277
|
-
function buildTrackingHeaders(input) {
|
|
278
|
-
const name = sanitizeUaToken(input.clientName || "unknown");
|
|
279
|
-
const version = sanitizeUaToken(input.clientVersion || "unknown");
|
|
280
|
-
const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
|
|
281
|
-
return {
|
|
282
|
-
"User-Agent": userAgent,
|
|
283
|
-
"X-Runpod-Session-Id": input.sessionId
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// src/tools/logs.ts
|
|
288
|
-
var import_zod = require("zod");
|
|
289
|
-
function parseLogSse(raw) {
|
|
290
|
-
const items = [];
|
|
291
|
-
for (const block of raw.split(/\r?\n\r?\n/)) {
|
|
292
|
-
const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
|
|
293
|
-
if (!dataLines.length) continue;
|
|
294
|
-
const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
295
|
-
if (!payload.trim()) continue;
|
|
296
|
-
try {
|
|
297
|
-
const parsed = JSON.parse(payload);
|
|
298
|
-
items.push(
|
|
299
|
-
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
|
|
300
|
-
);
|
|
301
|
-
} catch {
|
|
302
|
-
items.push({ raw: payload });
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return items;
|
|
306
|
-
}
|
|
307
|
-
var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
|
|
308
|
-
var LOG_STREAM_MAX_BYTES = 256 * 1024;
|
|
309
|
-
async function readSseSnapshot(fetchImpl, url, headers, opts) {
|
|
310
|
-
const controller = new AbortController();
|
|
311
|
-
const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
|
|
312
|
-
const chunks = [];
|
|
313
|
-
let bytes = 0;
|
|
314
|
-
let truncated = false;
|
|
315
|
-
try {
|
|
316
|
-
const res = await fetchImpl(url, {
|
|
317
|
-
method: "GET",
|
|
318
|
-
headers,
|
|
319
|
-
signal: controller.signal
|
|
320
|
-
});
|
|
321
|
-
if (!res.ok) {
|
|
322
|
-
throw new HttpError(
|
|
323
|
-
"Runpod API Error",
|
|
324
|
-
res.status,
|
|
325
|
-
await res.text().catch(() => ""),
|
|
326
|
-
// This throw bypasses createHttpClient, so the status hints are built
|
|
327
|
-
// here too or these two tools alone return a bare 429/401. The 401
|
|
328
|
-
// hint applies: this request always carries Authorization (see the
|
|
329
|
-
// headers bound in runtime.ts streamSse).
|
|
330
|
-
res.status === 429 ? rateLimitHint(
|
|
331
|
-
res.headers.get("ratelimit"),
|
|
332
|
-
res.headers.get("retry-after")
|
|
333
|
-
) : res.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
for await (const chunk of res.body ?? []) {
|
|
337
|
-
chunks.push(chunk);
|
|
338
|
-
bytes += chunk.length;
|
|
339
|
-
if (bytes >= opts.maxBytes) {
|
|
340
|
-
truncated = true;
|
|
341
|
-
controller.abort();
|
|
342
|
-
break;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
} catch (err) {
|
|
346
|
-
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
347
|
-
} finally {
|
|
348
|
-
clearTimeout(timer);
|
|
349
|
-
}
|
|
350
|
-
return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
351
|
-
}
|
|
352
|
-
var logStreamParams = {
|
|
353
|
-
source: import_zod.z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
|
|
354
|
-
tail: import_zod.z.number().int().min(0).max(5e3).optional().describe(
|
|
355
|
-
"Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
|
|
356
|
-
),
|
|
357
|
-
since: import_zod.z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
|
|
358
|
-
maxWaitMs: import_zod.z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
|
|
359
|
-
};
|
|
360
|
-
async function collectLogSnapshot(streamSse, logsUrl, params) {
|
|
361
|
-
const qs = new URLSearchParams();
|
|
362
|
-
if (params.source && params.source !== "both")
|
|
363
|
-
qs.append("source", params.source);
|
|
364
|
-
if (params.tail !== void 0) qs.append("tail", String(params.tail));
|
|
365
|
-
if (params.since) qs.append("since", params.since);
|
|
366
|
-
const query = qs.toString() ? `?${qs}` : "";
|
|
367
|
-
const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
|
|
368
|
-
maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
|
|
369
|
-
maxBytes: LOG_STREAM_MAX_BYTES
|
|
370
|
-
});
|
|
371
|
-
const items = parseLogSse(raw);
|
|
372
|
-
if (truncated) items.pop();
|
|
373
|
-
return { items, count: items.length, truncated };
|
|
374
|
-
}
|
|
375
|
-
async function streamLogsReply(rt, tool, params) {
|
|
376
|
-
const backend = rt.backendFor(tool.resource);
|
|
377
|
-
if (backend.version === "v1") {
|
|
378
|
-
return rt.jsonReply({
|
|
379
|
-
error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
380
|
-
status: 501
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
try {
|
|
384
|
-
return rt.jsonReply(
|
|
385
|
-
await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
|
|
386
|
-
);
|
|
387
|
-
} catch (error) {
|
|
388
|
-
if (error instanceof HttpError) {
|
|
389
|
-
return rt.jsonReply({ error: error.message, status: error.status });
|
|
390
|
-
}
|
|
391
|
-
throw error;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// src/_shared/mappers.ts
|
|
396
|
-
function compact(obj) {
|
|
397
|
-
const out = {};
|
|
398
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
399
|
-
if (v !== void 0) out[k] = v;
|
|
400
|
-
}
|
|
401
|
-
return out;
|
|
402
|
-
}
|
|
403
|
-
function persistentMount(size, path) {
|
|
404
|
-
if (size === void 0 || path === void 0) return void 0;
|
|
405
|
-
return { persistent: { size, path } };
|
|
406
|
-
}
|
|
407
|
-
function joinStartCmd(cmd) {
|
|
408
|
-
return cmd && cmd.length ? cmd.join(" ") : void 0;
|
|
409
|
-
}
|
|
410
|
-
function containerConfigToV2(p) {
|
|
411
|
-
return compact({
|
|
412
|
-
image: p.imageName,
|
|
413
|
-
disk: p.containerDiskInGb,
|
|
414
|
-
ports: p.ports,
|
|
415
|
-
env: p.env,
|
|
416
|
-
mounts: persistentMount(p.volumeInGb, p.volumeMountPath)
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
function gpuConfigToV2(gpuTypeIds, gpuCount, allowedCudaVersions, minCudaVersion2) {
|
|
420
|
-
const id = gpuTypeIds?.[0];
|
|
421
|
-
if (id === void 0) return void 0;
|
|
422
|
-
return compact({
|
|
423
|
-
id,
|
|
424
|
-
count: gpuCount ?? 1,
|
|
425
|
-
allowedCudaVersions,
|
|
426
|
-
minCudaVersion: minCudaVersion2
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
function mapPodCreateToV2(params) {
|
|
430
|
-
return compact({
|
|
431
|
-
...containerConfigToV2(params),
|
|
432
|
-
name: params.name,
|
|
433
|
-
// Private-image registry credential (v2 ContainerConfig `registry`). When
|
|
434
|
-
// deploying from a template, this spreads over the template's registry, so
|
|
435
|
-
// passing it overrides the inherited credential. An empty string is an
|
|
436
|
-
// explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
|
|
437
|
-
// template's inherited credential. Undefined is dropped by compact, so an
|
|
438
|
-
// omitted param leaves the template's registry in place.
|
|
439
|
-
registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
|
|
440
|
-
cloud: params.cloudType,
|
|
441
|
-
// v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
|
|
442
|
-
// centers; omit/empty = scheduler chooses). Pass the v1 list straight
|
|
443
|
-
// through — the earlier singular `dataCenter` key did not exist on the pod
|
|
444
|
-
// schema, so placement was being silently dropped. GPU, by contrast, IS
|
|
445
|
-
// singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
|
|
446
|
-
// create-pod handler warns when more than one gpuTypeId was supplied.
|
|
447
|
-
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
448
|
-
gpu: gpuConfigToV2(
|
|
449
|
-
params.gpuTypeIds,
|
|
450
|
-
params.gpuCount,
|
|
451
|
-
params.allowedCudaVersions,
|
|
452
|
-
params.minCudaVersion
|
|
453
|
-
)
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
function mapPodUpdateToV2(params) {
|
|
457
|
-
return {
|
|
458
|
-
...containerConfigToV2(params),
|
|
459
|
-
...compact({ name: params.name })
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
var DEFAULT_ENDPOINT_TYPE = "QUEUE";
|
|
463
|
-
var DEFAULT_SCALER_VALUE = 4;
|
|
464
|
-
function defaultScalerType(endpointType) {
|
|
465
|
-
return endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
466
|
-
}
|
|
467
|
-
function endpointScaling(scalerType, value) {
|
|
468
|
-
return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
|
|
469
|
-
}
|
|
470
|
-
function endpointGpuConfig(params, mode) {
|
|
471
|
-
const gpu = compact({
|
|
472
|
-
pools: params.gpuPoolIds?.length ? params.gpuPoolIds : void 0,
|
|
473
|
-
count: params.gpuCount,
|
|
474
|
-
allowedCudaVersions: params.allowedCudaVersions,
|
|
475
|
-
minCudaVersion: params.minCudaVersion
|
|
476
|
-
});
|
|
477
|
-
if (mode === "create" && !("pools" in gpu)) return void 0;
|
|
478
|
-
return Object.keys(gpu).length ? gpu : void 0;
|
|
479
|
-
}
|
|
480
|
-
function endpointWorkers(params) {
|
|
481
|
-
const workers = compact({
|
|
482
|
-
min: params.workersMin,
|
|
483
|
-
max: params.workersMax,
|
|
484
|
-
idleTimeout: params.idleTimeout
|
|
485
|
-
});
|
|
486
|
-
return Object.keys(workers).length ? workers : void 0;
|
|
487
|
-
}
|
|
488
|
-
function endpointCommonToV2(params, mode) {
|
|
489
|
-
return compact({
|
|
490
|
-
name: params.name,
|
|
491
|
-
image: params.imageName,
|
|
492
|
-
args: params.args,
|
|
493
|
-
disk: params.containerDiskInGb,
|
|
494
|
-
ports: params.ports,
|
|
495
|
-
env: params.env,
|
|
496
|
-
registry: params.containerRegistryAuthId,
|
|
497
|
-
gpu: endpointGpuConfig(params, mode),
|
|
498
|
-
workers: endpointWorkers(params),
|
|
499
|
-
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
500
|
-
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
501
|
-
timeout: params.executionTimeoutMs,
|
|
502
|
-
flashboot: params.flashboot
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
function mapEndpointCreateToV2(params) {
|
|
506
|
-
const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
|
|
507
|
-
return compact({
|
|
508
|
-
...endpointCommonToV2(params, "create"),
|
|
509
|
-
type: endpointType,
|
|
510
|
-
scaling: endpointScaling(
|
|
511
|
-
params.scalerType ?? defaultScalerType(endpointType),
|
|
512
|
-
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
513
|
-
)
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
function mapEndpointUpdateToV2(params) {
|
|
517
|
-
return compact({
|
|
518
|
-
...endpointCommonToV2(params, "update"),
|
|
519
|
-
scaling: params.scalerType ? endpointScaling(
|
|
520
|
-
params.scalerType,
|
|
521
|
-
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
522
|
-
) : void 0
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
var CUDA_MAJOR_MINOR = /^\d+\.\d+$/;
|
|
526
|
-
function cudaConstraintError(params, opts = {}) {
|
|
527
|
-
const { allowedCudaVersions, minCudaVersion: minCudaVersion2 } = params;
|
|
528
|
-
if (allowedCudaVersions?.length && minCudaVersion2) {
|
|
529
|
-
return "allowedCudaVersions and minCudaVersion are mutually exclusive \u2014 pass either an exact set or an open-ended floor, not both.";
|
|
530
|
-
}
|
|
531
|
-
if (minCudaVersion2 !== void 0 && !CUDA_MAJOR_MINOR.test(minCudaVersion2) && !(opts.allowClear && minCudaVersion2 === "")) {
|
|
532
|
-
return `minCudaVersion must be major.minor (e.g. "12.0", not "12")${opts.allowClear ? "; an empty string clears the floor" : ""}.`;
|
|
533
|
-
}
|
|
534
|
-
const bad = allowedCudaVersions?.find((v) => !CUDA_MAJOR_MINOR.test(v));
|
|
535
|
-
if (bad !== void 0) {
|
|
536
|
-
return `allowedCudaVersions entries must be major.minor (e.g. "12.8"); got "${bad}". Discover valid values via get-capacity or list-gpu-types.`;
|
|
537
|
-
}
|
|
538
|
-
return void 0;
|
|
539
|
-
}
|
|
540
|
-
function mapNetworkVolumeCreateToV2(params) {
|
|
541
|
-
return compact({
|
|
542
|
-
name: params.name,
|
|
543
|
-
size: params.size,
|
|
544
|
-
dataCenter: params.dataCenterId,
|
|
545
|
-
type: params.volumeType
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
function mapTemplateCreateToV2(params) {
|
|
549
|
-
return {
|
|
550
|
-
...containerConfigToV2(params),
|
|
551
|
-
...compact({
|
|
552
|
-
name: params.name,
|
|
553
|
-
serverless: params.isServerless,
|
|
554
|
-
registry: params.containerRegistryAuthId,
|
|
555
|
-
args: joinStartCmd(params.dockerStartCmd),
|
|
556
|
-
// No longer required by v2 (it defaults to NVIDIA server-side), so an
|
|
557
|
-
// unset category is dropped by compact instead of being forced here.
|
|
558
|
-
category: params.category
|
|
559
|
-
})
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
function podBodyFromTemplate(template) {
|
|
563
|
-
const out = {};
|
|
564
|
-
const keys = [
|
|
565
|
-
"name",
|
|
566
|
-
"image",
|
|
567
|
-
"args",
|
|
568
|
-
"disk",
|
|
569
|
-
"ports",
|
|
570
|
-
"env",
|
|
571
|
-
"mounts",
|
|
572
|
-
"registry"
|
|
573
|
-
];
|
|
574
|
-
for (const key of keys) {
|
|
575
|
-
const value = template[key];
|
|
576
|
-
if (value !== void 0 && value !== null) out[key] = value;
|
|
577
|
-
}
|
|
578
|
-
return out;
|
|
579
|
-
}
|
|
580
|
-
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
581
|
-
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
582
|
-
function normalizeSshPublicKey(input) {
|
|
583
|
-
if (PRIVATE_KEY_MARKER.test(input))
|
|
584
|
-
return {
|
|
585
|
-
ok: false,
|
|
586
|
-
error: 'sshPublicKey looks like a PRIVATE key. Pass the PUBLIC key instead \u2014 the contents of your ~/.ssh/<key>.pub file (e.g. "ssh-ed25519 AAAA\u2026").'
|
|
587
|
-
};
|
|
588
|
-
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
589
|
-
if (lines.length === 0)
|
|
590
|
-
return {
|
|
591
|
-
ok: false,
|
|
592
|
-
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
593
|
-
};
|
|
594
|
-
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
595
|
-
return {
|
|
596
|
-
ok: false,
|
|
597
|
-
error: 'sshPublicKey is not an SSH public key. Expected the CONTENTS of your ~/.ssh/<key>.pub file \u2014 a line like "ssh-ed25519 AAAAC3Nza\u2026 you@host" (ssh-*, ecdsa-*, or sk-* followed by the base64 key) \u2014 not a file path, a SHA256 fingerprint, or a bare base64 blob. Newline-separate multiple keys.'
|
|
598
|
-
};
|
|
599
|
-
return { ok: true, key: lines.join("\n") };
|
|
600
|
-
}
|
|
601
|
-
function applySshPublicKey(body, sshPublicKey) {
|
|
602
|
-
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
603
|
-
const hasSshPort = ports.some((p) => /^\s*22\/tcp\s*$/i.test(String(p)));
|
|
604
|
-
const env = { ...body.env ?? {} };
|
|
605
|
-
const key = sshPublicKey.trim();
|
|
606
|
-
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
607
|
-
else if (!env.PUBLIC_KEY.includes(key))
|
|
608
|
-
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
609
|
-
${key}`;
|
|
610
|
-
return {
|
|
611
|
-
...body,
|
|
612
|
-
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
613
|
-
env
|
|
614
|
-
};
|
|
615
|
-
}
|
|
616
|
-
function mapTemplateUpdateToV2(params) {
|
|
617
|
-
return {
|
|
618
|
-
...containerConfigToV2(params),
|
|
619
|
-
...compact({
|
|
620
|
-
name: params.name,
|
|
621
|
-
registry: params.containerRegistryAuthId,
|
|
622
|
-
args: joinStartCmd(params.dockerStartCmd)
|
|
623
|
-
})
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// src/_shared/backend.ts
|
|
628
|
-
var V1_ONLY = /* @__PURE__ */ new Set(["jobs"]);
|
|
629
|
-
var V2_UNWRAP_KEY = {
|
|
630
|
-
pods: "pods",
|
|
631
|
-
templates: "templates",
|
|
632
|
-
networkVolumes: "networkVolumes",
|
|
633
|
-
registries: "registries",
|
|
634
|
-
gpus: "gpus",
|
|
635
|
-
cpus: "cpus",
|
|
636
|
-
dataCenters: "dataCenters",
|
|
637
|
-
endpoints: "endpoints",
|
|
638
|
-
jobs: "jobs",
|
|
639
|
-
workers: "workers",
|
|
640
|
-
billing: "records"
|
|
641
|
-
};
|
|
642
|
-
var RESOURCE_ENV_KEY = {
|
|
643
|
-
pods: "PODS",
|
|
644
|
-
templates: "TEMPLATES",
|
|
645
|
-
networkVolumes: "NETWORK_VOLUMES",
|
|
646
|
-
registries: "REGISTRIES",
|
|
647
|
-
gpus: "GPUS",
|
|
648
|
-
cpus: "CPUS",
|
|
649
|
-
dataCenters: "DATA_CENTERS",
|
|
650
|
-
endpoints: "ENDPOINTS",
|
|
651
|
-
jobs: "JOBS",
|
|
652
|
-
workers: "WORKERS",
|
|
653
|
-
billing: "BILLING"
|
|
654
|
-
};
|
|
655
|
-
function restV1Base(env) {
|
|
656
|
-
return env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
|
|
657
|
-
}
|
|
658
|
-
function restV2Base(env) {
|
|
659
|
-
return env.RUNPOD_REST_V2_API_URL ?? "https://api.runpod.io/v2";
|
|
660
|
-
}
|
|
661
|
-
function serverlessBase(env) {
|
|
662
|
-
return env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
|
|
663
|
-
}
|
|
664
|
-
function publicGraphqlBase(env) {
|
|
665
|
-
return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
666
|
-
}
|
|
667
|
-
function authedGraphqlBase(env) {
|
|
668
|
-
return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
669
|
-
}
|
|
670
|
-
function unwrapList(resource, version, raw) {
|
|
671
|
-
if (version === "v1") return Array.isArray(raw) ? raw : [];
|
|
672
|
-
if (raw === null || typeof raw !== "object") return [];
|
|
673
|
-
const arr = raw[V2_UNWRAP_KEY[resource]];
|
|
674
|
-
return Array.isArray(arr) ? arr : [];
|
|
675
|
-
}
|
|
676
|
-
function resolveVersion(opts) {
|
|
677
|
-
const { resource, env, ctx, v2Available } = opts;
|
|
678
|
-
if (V1_ONLY.has(resource)) return "v1";
|
|
679
|
-
const perResource = env[`RUNPOD_REST_VERSION_${RESOURCE_ENV_KEY[resource]}`];
|
|
680
|
-
const setting = (perResource ?? env.RUNPOD_REST_VERSION ?? "v2").toLowerCase();
|
|
681
|
-
if (setting === "v2") return "v2";
|
|
682
|
-
if (setting === "auto") {
|
|
683
|
-
if (ctx.transport === "http") return "v1";
|
|
684
|
-
return v2Available === true ? "v2" : "v1";
|
|
685
|
-
}
|
|
686
|
-
return "v1";
|
|
687
|
-
}
|
|
688
|
-
var identity = (body) => body;
|
|
689
|
-
var V1_REST_PATHS = {
|
|
690
|
-
pods: { list: "/pods", get: (id) => `/pods/${id}` },
|
|
691
|
-
templates: { list: "/templates", get: (id) => `/templates/${id}` },
|
|
692
|
-
networkVolumes: {
|
|
693
|
-
list: "/networkvolumes",
|
|
694
|
-
get: (id) => `/networkvolumes/${id}`
|
|
695
|
-
},
|
|
696
|
-
registries: {
|
|
697
|
-
list: "/containerregistryauth",
|
|
698
|
-
get: (id) => `/containerregistryauth/${id}`
|
|
699
|
-
},
|
|
700
|
-
endpoints: { list: "/endpoints", get: (id) => `/endpoints/${id}` }
|
|
701
|
-
};
|
|
702
|
-
var CATALOG = /* @__PURE__ */ new Set([
|
|
703
|
-
"gpus",
|
|
704
|
-
"cpus",
|
|
705
|
-
"dataCenters"
|
|
706
|
-
]);
|
|
707
|
-
var V2_ONLY = /* @__PURE__ */ new Set([
|
|
708
|
-
"workers",
|
|
709
|
-
"billing"
|
|
710
|
-
]);
|
|
711
|
-
var V2_REST_PATHS = {
|
|
712
|
-
pods: { list: "/pods", get: (id) => `/pods/${id}` },
|
|
713
|
-
templates: { list: "/templates", get: (id) => `/templates/${id}` },
|
|
714
|
-
networkVolumes: {
|
|
715
|
-
list: "/network-volumes",
|
|
716
|
-
get: (id) => `/network-volumes/${id}`
|
|
717
|
-
},
|
|
718
|
-
registries: { list: "/registries", get: (id) => `/registries/${id}` },
|
|
719
|
-
gpus: { list: "/catalog/gpus", get: (id) => `/catalog/gpus/${id}` },
|
|
720
|
-
cpus: { list: "/catalog/cpus", get: (id) => `/catalog/cpus/${id}` },
|
|
721
|
-
dataCenters: {
|
|
722
|
-
list: "/catalog/datacenters",
|
|
723
|
-
get: (id) => `/catalog/datacenters/${id}`
|
|
724
|
-
},
|
|
725
|
-
// Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
|
|
726
|
-
// response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
|
|
727
|
-
// unwrap key stays correct.
|
|
728
|
-
endpoints: { list: "/serverless", get: (id) => `/serverless/${id}` },
|
|
729
|
-
// workers/billing build their own sub-paths in the tool from `base`; the
|
|
730
|
-
// list entry just satisfies buildV2Backend (which requires a paths entry).
|
|
731
|
-
workers: { list: "/serverless" },
|
|
732
|
-
billing: { list: "/billing" }
|
|
733
|
-
};
|
|
734
|
-
var V2_MAPPERS = {
|
|
735
|
-
pods: {
|
|
736
|
-
create: (b) => mapPodCreateToV2(b),
|
|
737
|
-
update: (b) => mapPodUpdateToV2(b)
|
|
738
|
-
},
|
|
739
|
-
templates: {
|
|
740
|
-
create: (b) => mapTemplateCreateToV2(b),
|
|
741
|
-
update: (b) => mapTemplateUpdateToV2(b)
|
|
742
|
-
},
|
|
743
|
-
networkVolumes: {
|
|
744
|
-
create: (b) => mapNetworkVolumeCreateToV2(
|
|
745
|
-
b
|
|
746
|
-
),
|
|
747
|
-
update: identity
|
|
748
|
-
},
|
|
749
|
-
registries: { create: identity, update: identity },
|
|
750
|
-
endpoints: {
|
|
751
|
-
create: (b) => mapEndpointCreateToV2(b),
|
|
752
|
-
update: (b) => mapEndpointUpdateToV2(b)
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
function buildV2Backend(resource, env, unwrap) {
|
|
756
|
-
const paths = V2_REST_PATHS[resource];
|
|
757
|
-
if (!paths) throw new Error(`no v2 REST paths for resource "${resource}"`);
|
|
758
|
-
const mappers = V2_MAPPERS[resource];
|
|
759
|
-
return {
|
|
760
|
-
kind: "rest",
|
|
761
|
-
version: "v2",
|
|
762
|
-
base: restV2Base(env),
|
|
763
|
-
list: paths.list,
|
|
764
|
-
get: paths.get,
|
|
765
|
-
unwrap,
|
|
766
|
-
mapCreate: mappers?.create ?? identity,
|
|
767
|
-
mapUpdate: mappers?.update ?? identity
|
|
768
|
-
};
|
|
769
|
-
}
|
|
770
|
-
function buildV1Backend(resource, env, unwrap) {
|
|
771
|
-
if (resource === "jobs") {
|
|
772
|
-
return {
|
|
773
|
-
kind: "rest",
|
|
774
|
-
version: "v1",
|
|
775
|
-
base: serverlessBase(env),
|
|
776
|
-
unwrap,
|
|
777
|
-
mapCreate: identity,
|
|
778
|
-
mapUpdate: identity
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
if (V2_ONLY.has(resource)) {
|
|
782
|
-
return {
|
|
783
|
-
kind: "rest",
|
|
784
|
-
version: "v1",
|
|
785
|
-
base: restV1Base(env),
|
|
786
|
-
unwrap,
|
|
787
|
-
mapCreate: identity,
|
|
788
|
-
mapUpdate: identity
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
if (CATALOG.has(resource)) {
|
|
792
|
-
return {
|
|
793
|
-
kind: "graphql",
|
|
794
|
-
version: "v1",
|
|
795
|
-
base: publicGraphqlBase(env),
|
|
796
|
-
unwrap,
|
|
797
|
-
mapCreate: identity,
|
|
798
|
-
mapUpdate: identity
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
const paths = V1_REST_PATHS[resource];
|
|
802
|
-
if (!paths) throw new Error(`no v1 REST paths for resource "${resource}"`);
|
|
803
|
-
return {
|
|
804
|
-
kind: "rest",
|
|
805
|
-
version: "v1",
|
|
806
|
-
base: restV1Base(env),
|
|
807
|
-
list: paths.list,
|
|
808
|
-
get: paths.get,
|
|
809
|
-
unwrap,
|
|
810
|
-
mapCreate: identity,
|
|
811
|
-
mapUpdate: identity
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
function resolveBackend(opts) {
|
|
815
|
-
const { resource, env } = opts;
|
|
816
|
-
const version = resolveVersion(opts);
|
|
817
|
-
const unwrap = (raw) => unwrapList(resource, version, raw);
|
|
818
|
-
return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// src/tools/runtime.ts
|
|
822
|
-
var MCP_SERVER_VERSION = true ? "3.4.0" : "dev";
|
|
823
|
-
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
824
|
-
function trackingHeaders(server, ctx) {
|
|
825
|
-
const info = server.server.getClientVersion();
|
|
826
|
-
return buildTrackingHeaders({
|
|
827
|
-
clientName: info?.name || ctx.clientUserAgent || "unknown",
|
|
828
|
-
clientVersion: info?.version || "unknown",
|
|
829
|
-
transport: ctx.transport,
|
|
830
|
-
// Prefer a server version supplied by the caller (the hosted entrypoint reads
|
|
831
|
-
// it from package.json at runtime, since tsup's build-time define does not
|
|
832
|
-
// run when Vercel compiles api/index.ts). Fall back to the build-time value.
|
|
833
|
-
serverVersion: ctx.serverVersion || MCP_SERVER_VERSION,
|
|
834
|
-
sessionId: SESSION_ID
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
838
|
-
function invocationBudget(totalMs) {
|
|
839
|
-
const startedAt = Date.now();
|
|
840
|
-
return () => totalMs - (Date.now() - startedAt);
|
|
841
|
-
}
|
|
842
|
-
var defaultFetch = import_node_fetch.default;
|
|
843
|
-
async function withApiErrorLog(label, call) {
|
|
844
|
-
try {
|
|
845
|
-
return await call();
|
|
846
|
-
} catch (error) {
|
|
847
|
-
console.error(label, error);
|
|
848
|
-
throw error;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
function jsonReply(result) {
|
|
852
|
-
return {
|
|
853
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
854
|
-
};
|
|
855
|
-
}
|
|
856
|
-
var READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
857
|
-
var WRITE = { readOnlyHint: false, openWorldHint: true };
|
|
858
|
-
var DESTRUCTIVE = {
|
|
859
|
-
readOnlyHint: false,
|
|
860
|
-
destructiveHint: true,
|
|
861
|
-
idempotentHint: true,
|
|
862
|
-
openWorldHint: true
|
|
863
|
-
};
|
|
864
|
-
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
865
|
-
return withRequestTimeout(
|
|
866
|
-
"Runpod GraphQL Error",
|
|
867
|
-
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
868
|
-
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
869
|
-
// off the method, and telling a caller its list-gpu-types "may have
|
|
870
|
-
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
871
|
-
// it back at the tool that just failed. The operation keyword is what
|
|
872
|
-
// actually says whether anything could have been written.
|
|
873
|
-
isGraphqlMutation(query) ? "POST" : "GET",
|
|
874
|
-
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
function isGraphqlMutation(query) {
|
|
878
|
-
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
879
|
-
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
880
|
-
}
|
|
881
|
-
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
882
|
-
const response = await fetchImpl(url, {
|
|
883
|
-
method: "POST",
|
|
884
|
-
headers: {
|
|
885
|
-
"Content-Type": "application/json",
|
|
886
|
-
...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
887
|
-
...tracking()
|
|
888
|
-
},
|
|
889
|
-
body: JSON.stringify({
|
|
890
|
-
query,
|
|
891
|
-
...options?.variables ? { variables: options.variables } : {}
|
|
892
|
-
}),
|
|
893
|
-
signal
|
|
894
|
-
});
|
|
895
|
-
if (!response.ok) {
|
|
896
|
-
const bodyText = await response.text();
|
|
897
|
-
let gqlErrors;
|
|
898
|
-
try {
|
|
899
|
-
gqlErrors = JSON.parse(bodyText).errors;
|
|
900
|
-
} catch {
|
|
901
|
-
}
|
|
902
|
-
if (Array.isArray(gqlErrors) && gqlErrors.length > 0 && response.status !== 401 && response.status !== 429) {
|
|
903
|
-
throw new HttpError(
|
|
904
|
-
"Runpod GraphQL Error",
|
|
905
|
-
response.status,
|
|
906
|
-
gqlErrors.map((e) => String(e?.message ?? JSON.stringify(e))).join(", ")
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
throw new HttpError(
|
|
910
|
-
"Runpod GraphQL Error",
|
|
911
|
-
response.status,
|
|
912
|
-
bodyText,
|
|
913
|
-
response.status === 429 ? rateLimitHint(
|
|
914
|
-
response.headers.get("ratelimit"),
|
|
915
|
-
response.headers.get("retry-after")
|
|
916
|
-
) : (
|
|
917
|
-
// Only when this request actually carried the caller's API key: the
|
|
918
|
-
// public path sends no Authorization header, so a 401 from that
|
|
919
|
-
// host (WAF, misconfigured RUNPOD_PUBLIC_GRAPHQL_URL) says nothing
|
|
920
|
-
// about the credential — same rationale as observeUnauthorized.
|
|
921
|
-
response.status === 401 && options?.apiKey ? EXPIRED_CREDENTIAL_HINT : void 0
|
|
922
|
-
)
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
const result = await response.json();
|
|
926
|
-
if (result.errors && result.errors.length > 0) {
|
|
927
|
-
throw new Error(
|
|
928
|
-
`GraphQL Error: ${result.errors.map((e) => e.message).join(", ")}`
|
|
929
|
-
);
|
|
930
|
-
}
|
|
931
|
-
if (!result.data) {
|
|
932
|
-
throw new Error("No data returned from GraphQL query");
|
|
933
|
-
}
|
|
934
|
-
return result.data;
|
|
935
|
-
}
|
|
936
|
-
function createToolRuntime(server, ctx, deps = {}) {
|
|
937
|
-
const tracking = () => trackingHeaders(server, ctx);
|
|
938
|
-
function observeUnauthorized(fn) {
|
|
939
|
-
const notify = ctx.onUnauthorized;
|
|
940
|
-
if (!notify) return fn;
|
|
941
|
-
return async (...args) => {
|
|
942
|
-
const response = await fn(...args);
|
|
943
|
-
if (response.status === 401) notify();
|
|
944
|
-
return response;
|
|
945
|
-
};
|
|
946
|
-
}
|
|
947
|
-
const rawFetch = deps.fetch ?? defaultFetch;
|
|
948
|
-
const httpFetch = observeUnauthorized(rawFetch);
|
|
949
|
-
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
950
|
-
const timeouts = {
|
|
951
|
-
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
952
|
-
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
953
|
-
};
|
|
954
|
-
const v1Client = createHttpClient({
|
|
955
|
-
apiKey: ctx.apiKey,
|
|
956
|
-
fetch: httpFetch,
|
|
957
|
-
tracking,
|
|
958
|
-
errorPrefix: "Runpod API Error",
|
|
959
|
-
...timeouts
|
|
960
|
-
});
|
|
961
|
-
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
962
|
-
"Error calling Runpod API:",
|
|
963
|
-
() => v1Client(
|
|
964
|
-
`${restV1Base(process.env)}${endpoint}`,
|
|
965
|
-
method,
|
|
966
|
-
body,
|
|
967
|
-
options
|
|
968
|
-
)
|
|
969
|
-
);
|
|
970
|
-
const serverlessClient = createHttpClient({
|
|
971
|
-
apiKey: ctx.apiKey,
|
|
972
|
-
fetch: httpFetch,
|
|
973
|
-
tracking,
|
|
974
|
-
errorPrefix: "Runpod Serverless API Error",
|
|
975
|
-
...timeouts
|
|
976
|
-
});
|
|
977
|
-
const serverlessRequest = (endpointId, path, method = "GET", body, options) => withApiErrorLog(
|
|
978
|
-
"Error calling Runpod Serverless API:",
|
|
979
|
-
() => serverlessClient(
|
|
980
|
-
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
981
|
-
method,
|
|
982
|
-
body,
|
|
983
|
-
options
|
|
984
|
-
)
|
|
985
|
-
);
|
|
986
|
-
const restClient = createHttpClient({
|
|
987
|
-
apiKey: ctx.apiKey,
|
|
988
|
-
fetch: httpFetch,
|
|
989
|
-
tracking,
|
|
990
|
-
errorPrefix: "Runpod API Error",
|
|
991
|
-
...timeouts
|
|
992
|
-
});
|
|
993
|
-
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
994
|
-
"Error calling Runpod API:",
|
|
995
|
-
() => restClient(url, method, body, options)
|
|
996
|
-
);
|
|
997
|
-
const sseFetch = observeUnauthorized(
|
|
998
|
-
deps.sseFetch ?? import_node_fetch.default
|
|
999
|
-
);
|
|
1000
|
-
const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
|
|
1001
|
-
sseFetch,
|
|
1002
|
-
url,
|
|
1003
|
-
{
|
|
1004
|
-
Authorization: `Bearer ${ctx.apiKey}`,
|
|
1005
|
-
Accept: "text/event-stream",
|
|
1006
|
-
...tracking()
|
|
1007
|
-
},
|
|
1008
|
-
opts
|
|
1009
|
-
));
|
|
1010
|
-
const backendFor = (resource) => resolveBackend({
|
|
1011
|
-
resource,
|
|
1012
|
-
env: process.env,
|
|
1013
|
-
ctx: { transport: ctx.transport },
|
|
1014
|
-
v2Available: deps.v2Available
|
|
1015
|
-
});
|
|
1016
|
-
const podAction = async (podId, action) => {
|
|
1017
|
-
const backend = backendFor("pods");
|
|
1018
|
-
const podPath = backend.get(podId);
|
|
1019
|
-
if (backend.version === "v2") {
|
|
1020
|
-
return callRestUrl(`${backend.base}${podPath}/action`, "POST", {
|
|
1021
|
-
action
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
return callRestUrl(`${backend.base}${podPath}/${action}`, "POST");
|
|
1025
|
-
};
|
|
1026
|
-
return {
|
|
1027
|
-
jsonReply,
|
|
1028
|
-
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
1029
|
-
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
1030
|
-
graphql: (query) => graphqlRequest(
|
|
1031
|
-
query,
|
|
1032
|
-
tracking,
|
|
1033
|
-
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
1034
|
-
rawFetch,
|
|
1035
|
-
publicGraphqlBase(process.env),
|
|
1036
|
-
{
|
|
1037
|
-
timeoutMs: deps.defaultTimeoutMs,
|
|
1038
|
-
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1039
|
-
}
|
|
1040
|
-
),
|
|
1041
|
-
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
1042
|
-
query,
|
|
1043
|
-
tracking,
|
|
1044
|
-
httpFetch,
|
|
1045
|
-
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
1046
|
-
authedGraphqlBase(process.env),
|
|
1047
|
-
{
|
|
1048
|
-
variables,
|
|
1049
|
-
apiKey: ctx.apiKey,
|
|
1050
|
-
timeoutMs: deps.defaultTimeoutMs,
|
|
1051
|
-
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1052
|
-
}
|
|
1053
|
-
),
|
|
1054
|
-
runpodRequest,
|
|
1055
|
-
serverlessRequest,
|
|
1056
|
-
callRestUrl,
|
|
1057
|
-
backendFor,
|
|
1058
|
-
streamSse,
|
|
1059
|
-
podAction,
|
|
1060
|
-
env: process.env,
|
|
1061
|
-
transport: ctx.transport
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// src/tools/catalog.ts
|
|
1066
|
-
var import_zod3 = require("zod");
|
|
1067
|
-
|
|
1068
|
-
// src/pagination.ts
|
|
1069
|
-
var import_zod2 = require("zod");
|
|
1070
|
-
var DEFAULT_LIST_LIMIT = 20;
|
|
1071
|
-
var MAX_LIST_LIMIT = 100;
|
|
1072
|
-
var listPaginationParams = {
|
|
1073
|
-
limit: import_zod2.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
|
|
1074
|
-
`Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
|
|
1075
|
-
),
|
|
1076
|
-
cursor: import_zod2.z.string().optional().describe(
|
|
1077
|
-
"Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
|
|
1078
|
-
)
|
|
1079
|
-
};
|
|
1080
|
-
function decodeCursorOffset(cursor) {
|
|
1081
|
-
if (!cursor) return 0;
|
|
1082
|
-
try {
|
|
1083
|
-
const decoded = Buffer.from(cursor, "base64").toString("utf8");
|
|
1084
|
-
const offset = Number.parseInt(decoded, 10);
|
|
1085
|
-
return Number.isFinite(offset) && offset >= 0 ? offset : 0;
|
|
1086
|
-
} catch {
|
|
1087
|
-
return 0;
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
function encodeCursorOffset(offset) {
|
|
1091
|
-
return Buffer.from(String(offset), "utf8").toString("base64");
|
|
1092
|
-
}
|
|
1093
|
-
function capListResult(result, options, extra) {
|
|
1094
|
-
const limit = Math.min(options.limit ?? DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT);
|
|
1095
|
-
const offset = decodeCursorOffset(options.cursor);
|
|
1096
|
-
if (!Array.isArray(result)) {
|
|
1097
|
-
return {
|
|
1098
|
-
content: [
|
|
1099
|
-
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
1100
|
-
]
|
|
1101
|
-
};
|
|
1102
|
-
}
|
|
1103
|
-
const total = result.length;
|
|
1104
|
-
const page = result.slice(offset, offset + limit);
|
|
1105
|
-
const nextOffset = offset + page.length;
|
|
1106
|
-
const hasMore = nextOffset < total;
|
|
1107
|
-
const { items: _i, pagination: _p, ...safeExtra } = extra ?? {};
|
|
1108
|
-
void _i;
|
|
1109
|
-
void _p;
|
|
1110
|
-
const envelope = {
|
|
1111
|
-
...safeExtra,
|
|
1112
|
-
items: page,
|
|
1113
|
-
pagination: {
|
|
1114
|
-
total,
|
|
1115
|
-
returned: page.length,
|
|
1116
|
-
offset,
|
|
1117
|
-
truncated: hasMore,
|
|
1118
|
-
nextCursor: hasMore ? encodeCursorOffset(nextOffset) : null,
|
|
1119
|
-
note: hasMore ? `Showing ${page.length} of ${total}. Pass cursor=nextCursor to fetch more, or narrow the query with filters.` : void 0
|
|
1120
|
-
}
|
|
1121
|
-
};
|
|
1122
|
-
return {
|
|
1123
|
-
content: [
|
|
1124
|
-
{ type: "text", text: JSON.stringify(envelope, null, 2) }
|
|
1125
|
-
]
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// src/tools/catalog.ts
|
|
1130
|
-
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
1131
|
-
function registerCatalogTools(server, rt) {
|
|
1132
|
-
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
1133
|
-
server.tool(
|
|
1134
|
-
"list-gpu-types",
|
|
1135
|
-
"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.",
|
|
1136
|
-
{
|
|
1137
|
-
...listPaginationParams,
|
|
1138
|
-
minMemoryGb: import_zod3.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
|
|
1139
|
-
secureCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
|
|
1140
|
-
communityCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
|
|
1141
|
-
searchTerm: import_zod3.z.string().optional().describe(
|
|
1142
|
-
"Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
|
|
1143
|
-
),
|
|
1144
|
-
includeUnavailable: import_zod3.z.boolean().optional().describe(
|
|
1145
|
-
"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."
|
|
1146
|
-
),
|
|
1147
|
-
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1148
|
-
"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."
|
|
1149
|
-
),
|
|
1150
|
-
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1151
|
-
"Product context for the availability lookup \u2014 the same GPU can be scarce for Pods and plentiful for Serverless. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1152
|
-
),
|
|
1153
|
-
minCudaVersion: import_zod3.z.string().optional().describe(
|
|
1154
|
-
'Scope the availability lookup to hosts at or above this CUDA version (v2 only). Integer major or major.minor \u2014 "12" means any 12.x release; unlike the create-pod/create-endpoint body fields, a bare major is accepted here because the filter only widens a read. Requires includeAvailability (the default).'
|
|
1155
|
-
)
|
|
1156
|
-
},
|
|
1157
|
-
{ title: "List GPU types", ...READ_ONLY },
|
|
1158
|
-
async (params) => {
|
|
1159
|
-
const backend = backendFor("gpus");
|
|
1160
|
-
if (params.minCudaVersion !== void 0 && backend.version !== "v2") {
|
|
1161
|
-
return jsonReply2({
|
|
1162
|
-
error: "The minCudaVersion filter is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or use get-capacity for per-CUDA-version availability.",
|
|
1163
|
-
status: 501
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
if (backend.version === "v2") {
|
|
1167
|
-
const wantAvailability = params.includeAvailability !== false;
|
|
1168
|
-
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1169
|
-
if (params.minCudaVersion !== void 0) {
|
|
1170
|
-
if (!wantAvailability) {
|
|
1171
|
-
return jsonReply2({
|
|
1172
|
-
error: "minCudaVersion filters the availability lookup, so it requires includeAvailability (the default). Drop includeAvailability:false or the filter.",
|
|
1173
|
-
status: 400
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
if (!/^\d+(\.\d+)?$/.test(params.minCudaVersion)) {
|
|
1177
|
-
return jsonReply2({
|
|
1178
|
-
error: 'minCudaVersion must be an integer major ("12") or major.minor ("12.1").',
|
|
1179
|
-
status: 400
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
const query = new URLSearchParams();
|
|
1184
|
-
if (wantAvailability) {
|
|
1185
|
-
query.set("include", "AVAILABILITY");
|
|
1186
|
-
query.set("product", product);
|
|
1187
|
-
if (params.minCudaVersion !== void 0) {
|
|
1188
|
-
query.set("minCudaVersion", params.minCudaVersion);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
const raw = await callRestUrl(
|
|
1192
|
-
`${backend.base}${backend.list}${query.size ? `?${query}` : ""}`
|
|
1193
|
-
);
|
|
1194
|
-
let gpus = backend.unwrap(raw);
|
|
1195
|
-
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
1196
|
-
if (params.minMemoryGb !== void 0)
|
|
1197
|
-
gpus = gpus.filter(
|
|
1198
|
-
(g) => Number(g.memory ?? 0) >= params.minMemoryGb
|
|
1199
|
-
);
|
|
1200
|
-
if (params.secureCloudOnly) gpus = gpus.filter((g) => g.secure);
|
|
1201
|
-
if (params.communityCloudOnly) gpus = gpus.filter((g) => g.community);
|
|
1202
|
-
if (params.searchTerm) {
|
|
1203
|
-
const t = params.searchTerm.toLowerCase();
|
|
1204
|
-
gpus = gpus.filter(
|
|
1205
|
-
(g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
1208
|
-
if (params.includeUnavailable === false)
|
|
1209
|
-
gpus = gpus.filter((g) => g.availability !== "NONE");
|
|
1210
|
-
const rank = {
|
|
1211
|
-
HIGH: 3,
|
|
1212
|
-
MEDIUM: 2,
|
|
1213
|
-
LOW: 1,
|
|
1214
|
-
NONE: 0
|
|
1215
|
-
};
|
|
1216
|
-
gpus.sort(
|
|
1217
|
-
(a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
|
|
1218
|
-
);
|
|
1219
|
-
gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
|
|
1220
|
-
return capListResult(gpus, {
|
|
1221
|
-
limit: params.limit,
|
|
1222
|
-
cursor: params.cursor
|
|
1223
|
-
});
|
|
1224
|
-
}
|
|
1225
|
-
const data = await graphql(`
|
|
1226
|
-
query {
|
|
1227
|
-
gpuTypes {
|
|
1228
|
-
id
|
|
1229
|
-
displayName
|
|
1230
|
-
memoryInGb
|
|
1231
|
-
secureCloud
|
|
1232
|
-
communityCloud
|
|
1233
|
-
lowestPrice(input: { gpuCount: 1 }) {
|
|
1234
|
-
stockStatus
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
`);
|
|
1239
|
-
const stockPriority = {
|
|
1240
|
-
High: 3,
|
|
1241
|
-
Medium: 2,
|
|
1242
|
-
Low: 1
|
|
1243
|
-
};
|
|
1244
|
-
const isAvailable = (gpu) => {
|
|
1245
|
-
const status = gpu.lowestPrice?.stockStatus;
|
|
1246
|
-
return !!status && status !== "Out";
|
|
1247
|
-
};
|
|
1248
|
-
let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
|
|
1249
|
-
if (params.includeUnavailable === false) {
|
|
1250
|
-
gpuTypes = gpuTypes.filter(isAvailable);
|
|
1251
|
-
}
|
|
1252
|
-
if (params.minMemoryGb) {
|
|
1253
|
-
gpuTypes = gpuTypes.filter(
|
|
1254
|
-
(gpu) => gpu.memoryInGb >= params.minMemoryGb
|
|
1255
|
-
);
|
|
1256
|
-
}
|
|
1257
|
-
if (params.secureCloudOnly) {
|
|
1258
|
-
gpuTypes = gpuTypes.filter((gpu) => gpu.secureCloud);
|
|
1259
|
-
}
|
|
1260
|
-
if (params.communityCloudOnly) {
|
|
1261
|
-
gpuTypes = gpuTypes.filter((gpu) => gpu.communityCloud);
|
|
1262
|
-
}
|
|
1263
|
-
if (params.searchTerm) {
|
|
1264
|
-
const term = params.searchTerm.toLowerCase();
|
|
1265
|
-
gpuTypes = gpuTypes.filter(
|
|
1266
|
-
(gpu) => gpu.id.toLowerCase().includes(term) || gpu.displayName.toLowerCase().includes(term)
|
|
1267
|
-
);
|
|
1268
|
-
}
|
|
1269
|
-
gpuTypes.sort((a, b) => {
|
|
1270
|
-
const aP = stockPriority[a.lowestPrice?.stockStatus || ""] || 0;
|
|
1271
|
-
const bP = stockPriority[b.lowestPrice?.stockStatus || ""] || 0;
|
|
1272
|
-
if (bP !== aP) return bP - aP;
|
|
1273
|
-
return b.memoryInGb - a.memoryInGb;
|
|
1274
|
-
});
|
|
1275
|
-
const result = gpuTypes.map((gpu) => ({
|
|
1276
|
-
id: gpu.id,
|
|
1277
|
-
displayName: gpu.displayName,
|
|
1278
|
-
memoryGb: gpu.memoryInGb,
|
|
1279
|
-
secureCloud: gpu.secureCloud,
|
|
1280
|
-
communityCloud: gpu.communityCloud,
|
|
1281
|
-
stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
|
|
1282
|
-
available: isAvailable(gpu)
|
|
1283
|
-
}));
|
|
1284
|
-
return capListResult(result, {
|
|
1285
|
-
limit: params.limit,
|
|
1286
|
-
cursor: params.cursor
|
|
1287
|
-
});
|
|
1288
|
-
}
|
|
1289
|
-
);
|
|
1290
|
-
server.tool(
|
|
1291
|
-
"list-data-centers",
|
|
1292
|
-
"List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
|
|
1293
|
-
{
|
|
1294
|
-
...listPaginationParams,
|
|
1295
|
-
region: import_zod3.z.string().optional().describe(
|
|
1296
|
-
"Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
|
|
1297
|
-
)
|
|
1298
|
-
},
|
|
1299
|
-
{ title: "List data centers", ...READ_ONLY },
|
|
1300
|
-
async (params) => {
|
|
1301
|
-
const backend = backendFor("dataCenters");
|
|
1302
|
-
if (backend.version === "v2") {
|
|
1303
|
-
const raw = await callRestUrl(`${backend.base}${backend.list}`);
|
|
1304
|
-
let dcs = backend.unwrap(raw);
|
|
1305
|
-
if (params.region) {
|
|
1306
|
-
const t = params.region.toLowerCase();
|
|
1307
|
-
dcs = dcs.filter(
|
|
1308
|
-
(dc) => String(dc.region ?? "").toLowerCase().includes(t)
|
|
1309
|
-
);
|
|
1310
|
-
}
|
|
1311
|
-
return capListResult(dcs, {
|
|
1312
|
-
limit: params.limit,
|
|
1313
|
-
cursor: params.cursor
|
|
1314
|
-
});
|
|
1315
|
-
}
|
|
1316
|
-
const data = await graphql(`
|
|
1317
|
-
query {
|
|
1318
|
-
dataCenters {
|
|
1319
|
-
id
|
|
1320
|
-
name
|
|
1321
|
-
location
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
`);
|
|
1325
|
-
let dataCenters = data.dataCenters;
|
|
1326
|
-
if (params.region) {
|
|
1327
|
-
const term = params.region.toLowerCase();
|
|
1328
|
-
dataCenters = dataCenters.filter(
|
|
1329
|
-
(dc) => dc.location.toLowerCase().includes(term)
|
|
1330
|
-
);
|
|
1331
|
-
}
|
|
1332
|
-
return capListResult(dataCenters, {
|
|
1333
|
-
limit: params.limit,
|
|
1334
|
-
cursor: params.cursor
|
|
1335
|
-
});
|
|
1336
|
-
}
|
|
1337
|
-
);
|
|
1338
|
-
server.tool(
|
|
1339
|
-
"list-cpu-types",
|
|
1340
|
-
"List available CPU flavor types for CPU pods/endpoints. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
1341
|
-
{ ...listPaginationParams },
|
|
1342
|
-
{ title: "List CPU types", ...READ_ONLY },
|
|
1343
|
-
async (params) => {
|
|
1344
|
-
const backend = backendFor("cpus");
|
|
1345
|
-
if (backend.version === "v1") {
|
|
1346
|
-
return jsonReply2({
|
|
1347
|
-
error: "list-cpu-types is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
1348
|
-
status: 501
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
const raw = await callRestUrl(`${backend.base}${backend.list}`);
|
|
1352
|
-
return capListResult(backend.unwrap(raw), {
|
|
1353
|
-
limit: params.limit,
|
|
1354
|
-
cursor: params.cursor
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
);
|
|
1358
|
-
server.tool(
|
|
1359
|
-
"get-gpu-type",
|
|
1360
|
-
"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.",
|
|
1361
|
-
{
|
|
1362
|
-
gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
|
|
1363
|
-
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1364
|
-
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1365
|
-
),
|
|
1366
|
-
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1367
|
-
"Product context for the availability lookup \u2014 stock differs by product. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1368
|
-
)
|
|
1369
|
-
},
|
|
1370
|
-
{ title: "Get GPU type", ...READ_ONLY },
|
|
1371
|
-
async (params) => {
|
|
1372
|
-
const backend = backendFor("gpus");
|
|
1373
|
-
if (backend.version === "v1") {
|
|
1374
|
-
return jsonReply2({
|
|
1375
|
-
error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
|
|
1376
|
-
status: 501
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1380
|
-
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1381
|
-
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1382
|
-
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1383
|
-
return jsonReply2(result);
|
|
1384
|
-
}
|
|
1385
|
-
);
|
|
1386
|
-
server.tool(
|
|
1387
|
-
"get-cpu-type",
|
|
1388
|
-
"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).",
|
|
1389
|
-
{
|
|
1390
|
-
cpuTypeId: import_zod3.z.string().describe("ID of the CPU type to retrieve")
|
|
1391
|
-
},
|
|
1392
|
-
{ title: "Get CPU type", ...READ_ONLY },
|
|
1393
|
-
async (params) => {
|
|
1394
|
-
const backend = backendFor("cpus");
|
|
1395
|
-
if (backend.version === "v1") {
|
|
1396
|
-
return jsonReply2({
|
|
1397
|
-
error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
1398
|
-
status: 501
|
|
1399
|
-
});
|
|
1400
|
-
}
|
|
1401
|
-
const result = await callRestUrl(
|
|
1402
|
-
`${backend.base}${backend.get(params.cpuTypeId)}`
|
|
1403
|
-
);
|
|
1404
|
-
return jsonReply2(result);
|
|
1405
|
-
}
|
|
1406
|
-
);
|
|
1407
|
-
server.tool(
|
|
1408
|
-
"get-capacity",
|
|
1409
|
-
"GPU capacity across host CUDA versions, as a matrix. Use this to choose an endpoint's allowedCudaVersions/minCudaVersion (or diagnose/widen a capacity-starved one) and to distinguish capacity problems from compatibility problems. Default mode is one call returning, per GPU type, overall stock plus AVAILABLE/UNAVAILABLE per host-reported CUDA version. Pass cudaVersions to deep-probe instead: one stock lookup per listed version, returning graded stock (High/Medium/Low/Out) and the lowest on-demand price per version; a probe that fails transiently reports a per-version error instead of failing the call. Nothing is hidden by default \u2014 set includeUnavailable:false to drop GPUs with no stock on any listed version. Credential-free public catalog data; works on both v1 and v2 APIs. Stock is live, so page cursors can shift between calls. Note: the matrix reflects host-reported versions, but endpoint allowedCudaVersions only accepts values from the platform enum \u2014 check create-endpoint/update-endpoint for the accepted list.",
|
|
1410
|
-
{
|
|
1411
|
-
...listPaginationParams,
|
|
1412
|
-
cudaVersions: import_zod3.z.array(import_zod3.z.string().regex(/^\d{1,2}\.\d{1,2}$/)).min(1).max(12).optional().describe(
|
|
1413
|
-
'Deep-probe these host CUDA versions (e.g. ["12.8", "13.0"], max 12): one stock lookup per version returns graded stock and price. Omit for the single-call AVAILABLE/UNAVAILABLE matrix across all versions the fleet currently reports.'
|
|
1414
|
-
),
|
|
1415
|
-
includeUnavailable: import_zod3.z.boolean().optional().describe(
|
|
1416
|
-
'Out-of-stock GPUs are included by default (explicit "Out" cells / all-UNAVAILABLE rows, sorted last). Set false to hide GPUs with no stock on any listed version.'
|
|
1417
|
-
),
|
|
1418
|
-
gpuTypeIds: import_zod3.z.array(import_zod3.z.string()).optional().describe(
|
|
1419
|
-
"Filter to GPU types matching any of these ids or display names (case-insensitive substring, e.g. ['NVIDIA GeForce RTX 4090'] or ['4090', 'H200'])."
|
|
1420
|
-
),
|
|
1421
|
-
gpuCount: import_zod3.z.number().int().min(1).max(8).optional().describe("GPUs per worker/pod to check stock for (default 1)."),
|
|
1422
|
-
secureCloudOnly: import_zod3.z.boolean().optional().describe("Restrict the stock lookup to Secure Cloud hosts.")
|
|
1423
|
-
},
|
|
1424
|
-
{ title: "Get GPU capacity by CUDA version", ...READ_ONLY },
|
|
1425
|
-
async (params) => {
|
|
1426
|
-
const gpuCount = Math.min(
|
|
1427
|
-
8,
|
|
1428
|
-
Math.max(1, Math.floor(Number(params.gpuCount)) || 1)
|
|
1429
|
-
);
|
|
1430
|
-
const secureArg = params.secureCloudOnly ? ", secureCloud: true" : "";
|
|
1431
|
-
const filterTerms = (params.gpuTypeIds ?? []).filter((t) => typeof t === "string" && t.trim().length > 0).map((t) => t.toLowerCase());
|
|
1432
|
-
const matchesFilter = (gpu) => {
|
|
1433
|
-
if (filterTerms.length === 0) return true;
|
|
1434
|
-
const id = gpu.id.toLowerCase();
|
|
1435
|
-
const name = gpu.displayName.toLowerCase();
|
|
1436
|
-
return filterTerms.some(
|
|
1437
|
-
(term) => id.includes(term) || name.includes(term)
|
|
1438
|
-
);
|
|
1439
|
-
};
|
|
1440
|
-
const stockPriority = {
|
|
1441
|
-
High: 3,
|
|
1442
|
-
Medium: 2,
|
|
1443
|
-
Low: 1
|
|
1444
|
-
};
|
|
1445
|
-
if (!params.cudaVersions?.length) {
|
|
1446
|
-
const data = await graphql(`
|
|
1447
|
-
query {
|
|
1448
|
-
gpuTypes {
|
|
1449
|
-
id
|
|
1450
|
-
displayName
|
|
1451
|
-
memoryInGb
|
|
1452
|
-
secureCloud
|
|
1453
|
-
communityCloud
|
|
1454
|
-
lowestPrice(input: { gpuCount: ${gpuCount}${secureArg} }) {
|
|
1455
|
-
stockStatus
|
|
1456
|
-
uninterruptablePrice
|
|
1457
|
-
gpuTypeCudaVersions {
|
|
1458
|
-
cudaVersion
|
|
1459
|
-
availability
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
`);
|
|
1465
|
-
let rows2 = data.gpuTypes.filter((gpu) => gpu.id !== "unknown" && matchesFilter(gpu)).map((gpu) => {
|
|
1466
|
-
const cuda = {};
|
|
1467
|
-
for (const c of gpu.lowestPrice?.gpuTypeCudaVersions ?? []) {
|
|
1468
|
-
cuda[c.cudaVersion] = c.availability;
|
|
1469
|
-
}
|
|
1470
|
-
return {
|
|
1471
|
-
id: gpu.id,
|
|
1472
|
-
displayName: gpu.displayName,
|
|
1473
|
-
memoryGb: gpu.memoryInGb,
|
|
1474
|
-
secureCloud: gpu.secureCloud,
|
|
1475
|
-
communityCloud: gpu.communityCloud,
|
|
1476
|
-
stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
|
|
1477
|
-
pricePerHr: gpu.lowestPrice?.uninterruptablePrice ?? null,
|
|
1478
|
-
cudaVersions: cuda
|
|
1479
|
-
};
|
|
1480
|
-
});
|
|
1481
|
-
const availableCount = (r) => Object.values(r.cudaVersions).filter((a) => a === "AVAILABLE").length;
|
|
1482
|
-
if (params.includeUnavailable === false)
|
|
1483
|
-
rows2 = rows2.filter((r) => availableCount(r) > 0);
|
|
1484
|
-
rows2.sort((a, b) => {
|
|
1485
|
-
const diff = availableCount(b) - availableCount(a);
|
|
1486
|
-
if (diff !== 0) return diff;
|
|
1487
|
-
const stockDiff = (stockPriority[b.stockStatus] || 0) - (stockPriority[a.stockStatus] || 0);
|
|
1488
|
-
if (stockDiff !== 0) return stockDiff;
|
|
1489
|
-
return b.memoryGb - a.memoryGb;
|
|
1490
|
-
});
|
|
1491
|
-
return capListResult(rows2, {
|
|
1492
|
-
limit: params.limit,
|
|
1493
|
-
cursor: params.cursor
|
|
1494
|
-
});
|
|
1495
|
-
}
|
|
1496
|
-
const versions = [...new Set(params.cudaVersions)].slice(0, 12);
|
|
1497
|
-
const invalid = versions.filter((v) => !/^\d{1,2}\.\d{1,2}$/.test(v));
|
|
1498
|
-
if (invalid.length > 0) {
|
|
1499
|
-
return jsonReply2({
|
|
1500
|
-
error: `Invalid CUDA version format: ${invalid.join(", ")}. Use "major.minor" strings like "12.8".`,
|
|
1501
|
-
status: 400
|
|
1502
|
-
});
|
|
1503
|
-
}
|
|
1504
|
-
const perVersion = await Promise.allSettled(
|
|
1505
|
-
versions.map(
|
|
1506
|
-
(v) => graphql(`
|
|
1507
|
-
query {
|
|
1508
|
-
gpuTypes {
|
|
1509
|
-
id
|
|
1510
|
-
displayName
|
|
1511
|
-
memoryInGb
|
|
1512
|
-
secureCloud
|
|
1513
|
-
communityCloud
|
|
1514
|
-
lowestPrice(input: { gpuCount: ${gpuCount}, allowedCudaVersions: ["${v}"]${secureArg} }) {
|
|
1515
|
-
stockStatus
|
|
1516
|
-
uninterruptablePrice
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
`)
|
|
1521
|
-
)
|
|
1522
|
-
);
|
|
1523
|
-
const probeErrors = {};
|
|
1524
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1525
|
-
versions.forEach((v, i) => {
|
|
1526
|
-
const settled = perVersion[i];
|
|
1527
|
-
if (settled.status === "rejected") {
|
|
1528
|
-
probeErrors[v] = settled.reason instanceof Error ? settled.reason.message : String(settled.reason);
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
for (const gpu of settled.value.gpuTypes) {
|
|
1532
|
-
if (gpu.id === "unknown" || !matchesFilter(gpu)) continue;
|
|
1533
|
-
let row = byId.get(gpu.id);
|
|
1534
|
-
if (!row) {
|
|
1535
|
-
row = {
|
|
1536
|
-
id: gpu.id,
|
|
1537
|
-
displayName: gpu.displayName,
|
|
1538
|
-
memoryGb: gpu.memoryInGb,
|
|
1539
|
-
secureCloud: gpu.secureCloud,
|
|
1540
|
-
communityCloud: gpu.communityCloud,
|
|
1541
|
-
cudaVersions: {}
|
|
1542
|
-
};
|
|
1543
|
-
byId.set(gpu.id, row);
|
|
1544
|
-
}
|
|
1545
|
-
const stock = gpu.lowestPrice?.stockStatus;
|
|
1546
|
-
row.cudaVersions[v] = !stock || stock === "Out" ? { stock: "Out", pricePerHr: null } : {
|
|
1547
|
-
stock,
|
|
1548
|
-
pricePerHr: gpu.lowestPrice?.uninterruptablePrice ?? null
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
});
|
|
1552
|
-
let rows = [...byId.values()];
|
|
1553
|
-
const inStockCount = (r) => Object.values(r.cudaVersions).filter((c) => c.stock !== "Out").length;
|
|
1554
|
-
if (params.includeUnavailable === false)
|
|
1555
|
-
rows = rows.filter((r) => inStockCount(r) > 0);
|
|
1556
|
-
rows.sort((a, b) => {
|
|
1557
|
-
const diff = inStockCount(b) - inStockCount(a);
|
|
1558
|
-
if (diff !== 0) return diff;
|
|
1559
|
-
const best = (r) => Math.max(
|
|
1560
|
-
0,
|
|
1561
|
-
...Object.values(r.cudaVersions).map(
|
|
1562
|
-
(c) => stockPriority[c.stock] || 0
|
|
1563
|
-
)
|
|
1564
|
-
);
|
|
1565
|
-
const bestDiff = best(b) - best(a);
|
|
1566
|
-
if (bestDiff !== 0) return bestDiff;
|
|
1567
|
-
return b.memoryGb - a.memoryGb;
|
|
1568
|
-
});
|
|
1569
|
-
return capListResult(
|
|
1570
|
-
rows,
|
|
1571
|
-
{ limit: params.limit, cursor: params.cursor },
|
|
1572
|
-
{
|
|
1573
|
-
probedCudaVersions: versions,
|
|
1574
|
-
...Object.keys(probeErrors).length > 0 ? { probeErrors } : {}
|
|
1575
|
-
}
|
|
1576
|
-
);
|
|
1577
|
-
}
|
|
1578
|
-
);
|
|
1579
|
-
server.tool(
|
|
1580
|
-
"get-data-center",
|
|
1581
|
-
"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).",
|
|
1582
|
-
{
|
|
1583
|
-
dataCenterId: import_zod3.z.string().describe("ID of the data center to retrieve")
|
|
1584
|
-
},
|
|
1585
|
-
{ title: "Get data center", ...READ_ONLY },
|
|
1586
|
-
async (params) => {
|
|
1587
|
-
const backend = backendFor("dataCenters");
|
|
1588
|
-
if (backend.version === "v1") {
|
|
1589
|
-
return jsonReply2({
|
|
1590
|
-
error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
1591
|
-
status: 501
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
const result = await callRestUrl(
|
|
1595
|
-
`${backend.base}${backend.get(params.dataCenterId)}`
|
|
1596
|
-
);
|
|
1597
|
-
return jsonReply2(result);
|
|
1598
|
-
}
|
|
1599
|
-
);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
// src/tools/hub.ts
|
|
1603
|
-
var import_zod4 = require("zod");
|
|
1604
|
-
function parseReleaseConfig(config) {
|
|
1605
|
-
if (!config) return void 0;
|
|
1606
|
-
try {
|
|
1607
|
-
return JSON.parse(config);
|
|
1608
|
-
} catch {
|
|
1609
|
-
return config;
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
function listingsQuery(withConfig) {
|
|
1613
|
-
return `
|
|
1614
|
-
query {
|
|
1615
|
-
listings(input: {}) {
|
|
1616
|
-
id
|
|
1617
|
-
repoId
|
|
1618
|
-
title
|
|
1619
|
-
description
|
|
1620
|
-
repoName
|
|
1621
|
-
repoOwner
|
|
1622
|
-
createdAt
|
|
1623
|
-
updatedAt
|
|
1624
|
-
views
|
|
1625
|
-
stars
|
|
1626
|
-
deploys
|
|
1627
|
-
language
|
|
1628
|
-
category
|
|
1629
|
-
tags
|
|
1630
|
-
type
|
|
1631
|
-
listedRelease {
|
|
1632
|
-
id
|
|
1633
|
-
name
|
|
1634
|
-
tagName
|
|
1635
|
-
createdAt${withConfig ? "\n config" : ""}
|
|
1636
|
-
build {
|
|
1637
|
-
id
|
|
1638
|
-
imageName
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
`;
|
|
1644
|
-
}
|
|
1645
|
-
function serializeBoolean(value, input) {
|
|
1646
|
-
if (value === "true") return input.trueValue ?? "true";
|
|
1647
|
-
if (value === "false") return input.falseValue ?? "false";
|
|
1648
|
-
return value;
|
|
1649
|
-
}
|
|
1650
|
-
function buildHubEnv(config, overrides) {
|
|
1651
|
-
const env = [];
|
|
1652
|
-
const missingRequired = [];
|
|
1653
|
-
const remaining = { ...overrides };
|
|
1654
|
-
for (const entry of config.env ?? []) {
|
|
1655
|
-
const input = entry.input ?? {};
|
|
1656
|
-
const override = entry.key in remaining ? remaining[entry.key] : void 0;
|
|
1657
|
-
delete remaining[entry.key];
|
|
1658
|
-
if (override !== void 0 && override !== "") {
|
|
1659
|
-
env.push({
|
|
1660
|
-
key: entry.key,
|
|
1661
|
-
value: input.type === "boolean" ? serializeBoolean(override, input) : override
|
|
1662
|
-
});
|
|
1663
|
-
continue;
|
|
1664
|
-
}
|
|
1665
|
-
if (input.default !== void 0 && input.default !== null) {
|
|
1666
|
-
env.push({
|
|
1667
|
-
key: entry.key,
|
|
1668
|
-
value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
|
|
1669
|
-
});
|
|
1670
|
-
continue;
|
|
1671
|
-
}
|
|
1672
|
-
if (input.required) {
|
|
1673
|
-
missingRequired.push(entry.key);
|
|
1674
|
-
env.push({ key: entry.key, value: "" });
|
|
1675
|
-
continue;
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
for (const [key, value] of Object.entries(remaining)) {
|
|
1679
|
-
env.push({ key, value });
|
|
1680
|
-
}
|
|
1681
|
-
return { env, missingRequired };
|
|
1682
|
-
}
|
|
1683
|
-
function minCudaVersion(versions) {
|
|
1684
|
-
if (!versions || versions.length === 0) return null;
|
|
1685
|
-
const compare = (a, b) => {
|
|
1686
|
-
const pa = a.split(".").map(Number);
|
|
1687
|
-
const pb = b.split(".").map(Number);
|
|
1688
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1689
|
-
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1690
|
-
if (d !== 0) return d;
|
|
1691
|
-
}
|
|
1692
|
-
return 0;
|
|
1693
|
-
};
|
|
1694
|
-
return [...versions].sort(compare)[0];
|
|
1695
|
-
}
|
|
1696
|
-
function randomSuffix() {
|
|
1697
|
-
return Math.random().toString(36).slice(2, 8);
|
|
1698
|
-
}
|
|
1699
|
-
function registerHubTools(server, rt) {
|
|
1700
|
-
const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
1701
|
-
server.tool(
|
|
1702
|
-
"list-hub-repos",
|
|
1703
|
-
"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.",
|
|
1704
|
-
{
|
|
1705
|
-
...listPaginationParams,
|
|
1706
|
-
searchTerm: import_zod4.z.string().optional().describe(
|
|
1707
|
-
"Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
|
|
1708
|
-
),
|
|
1709
|
-
category: import_zod4.z.string().optional().describe(
|
|
1710
|
-
"Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
|
|
1711
|
-
),
|
|
1712
|
-
type: import_zod4.z.enum(["SERVERLESS", "POD"]).optional().describe(
|
|
1713
|
-
"Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
|
|
1714
|
-
),
|
|
1715
|
-
repoOwner: import_zod4.z.string().optional().describe(
|
|
1716
|
-
"Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
|
|
1717
|
-
),
|
|
1718
|
-
includeConfig: import_zod4.z.boolean().optional().describe(
|
|
1719
|
-
"Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
|
|
1720
|
-
)
|
|
1721
|
-
},
|
|
1722
|
-
{ title: "List Hub repos", ...READ_ONLY },
|
|
1723
|
-
async (params) => {
|
|
1724
|
-
const data = await graphql(
|
|
1725
|
-
listingsQuery(params.includeConfig === true)
|
|
1726
|
-
);
|
|
1727
|
-
let listings = data.listings;
|
|
1728
|
-
if (params.type) {
|
|
1729
|
-
listings = listings.filter(
|
|
1730
|
-
(l) => (l.type ?? "").toUpperCase() === params.type
|
|
1731
|
-
);
|
|
1732
|
-
}
|
|
1733
|
-
if (params.category) {
|
|
1734
|
-
const term = params.category.toLowerCase();
|
|
1735
|
-
listings = listings.filter(
|
|
1736
|
-
(l) => (l.category ?? "").toLowerCase() === term
|
|
1737
|
-
);
|
|
1738
|
-
}
|
|
1739
|
-
if (params.repoOwner) {
|
|
1740
|
-
const term = params.repoOwner.toLowerCase();
|
|
1741
|
-
listings = listings.filter(
|
|
1742
|
-
(l) => (l.repoOwner ?? "").toLowerCase() === term
|
|
1743
|
-
);
|
|
1744
|
-
}
|
|
1745
|
-
if (params.searchTerm) {
|
|
1746
|
-
const term = params.searchTerm.toLowerCase();
|
|
1747
|
-
listings = listings.filter(
|
|
1748
|
-
(l) => [
|
|
1749
|
-
l.title,
|
|
1750
|
-
l.description,
|
|
1751
|
-
l.repoName,
|
|
1752
|
-
l.repoOwner,
|
|
1753
|
-
...l.tags ?? []
|
|
1754
|
-
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
1755
|
-
);
|
|
1756
|
-
}
|
|
1757
|
-
listings = [...listings].sort(
|
|
1758
|
-
(a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
|
|
1759
|
-
);
|
|
1760
|
-
const result = listings.map((l) => ({
|
|
1761
|
-
id: l.id,
|
|
1762
|
-
repo: `${l.repoOwner}/${l.repoName}`,
|
|
1763
|
-
title: l.title,
|
|
1764
|
-
description: l.description,
|
|
1765
|
-
type: l.type,
|
|
1766
|
-
category: l.category,
|
|
1767
|
-
tags: l.tags,
|
|
1768
|
-
language: l.language,
|
|
1769
|
-
stars: l.stars,
|
|
1770
|
-
deploys: l.deploys,
|
|
1771
|
-
views: l.views,
|
|
1772
|
-
githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
|
|
1773
|
-
hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
|
|
1774
|
-
createdAt: l.createdAt,
|
|
1775
|
-
updatedAt: l.updatedAt,
|
|
1776
|
-
listedRelease: l.listedRelease ? {
|
|
1777
|
-
hubReleaseId: l.listedRelease.id,
|
|
1778
|
-
name: l.listedRelease.name,
|
|
1779
|
-
tagName: l.listedRelease.tagName,
|
|
1780
|
-
createdAt: l.listedRelease.createdAt,
|
|
1781
|
-
imageName: l.listedRelease.build?.imageName,
|
|
1782
|
-
...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
|
|
1783
|
-
} : null
|
|
1784
|
-
}));
|
|
1785
|
-
return capListResult(result, {
|
|
1786
|
-
limit: params.limit,
|
|
1787
|
-
cursor: params.cursor
|
|
1788
|
-
});
|
|
1789
|
-
}
|
|
1790
|
-
);
|
|
1791
|
-
server.tool(
|
|
1792
|
-
"deploy-hub-repo",
|
|
1793
|
-
"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.",
|
|
1794
|
-
{
|
|
1795
|
-
repo: import_zod4.z.string().optional().describe(
|
|
1796
|
-
`The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
|
|
1797
|
-
),
|
|
1798
|
-
hubReleaseId: import_zod4.z.string().optional().describe(
|
|
1799
|
-
"The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
|
|
1800
|
-
),
|
|
1801
|
-
name: import_zod4.z.string().optional().describe(
|
|
1802
|
-
"Name for the new endpoint. Defaults to '<listing title> <release tag>'."
|
|
1803
|
-
),
|
|
1804
|
-
env: import_zod4.z.record(import_zod4.z.string()).optional().describe(
|
|
1805
|
-
"Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
|
|
1806
|
-
),
|
|
1807
|
-
gpuIds: import_zod4.z.string().optional().describe(
|
|
1808
|
-
"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."
|
|
1809
|
-
),
|
|
1810
|
-
gpuCount: import_zod4.z.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
|
|
1811
|
-
containerDiskInGb: import_zod4.z.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
|
|
1812
|
-
workersMin: import_zod4.z.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
|
|
1813
|
-
workersMax: import_zod4.z.number().int().positive().optional().describe("Maximum workers (default: account default)."),
|
|
1814
|
-
idleTimeout: import_zod4.z.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
|
|
1815
|
-
scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
|
|
1816
|
-
scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value (default 4)."),
|
|
1817
|
-
executionTimeoutMs: import_zod4.z.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
|
|
1818
|
-
flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
|
|
1819
|
-
},
|
|
1820
|
-
{ title: "Deploy Hub repo", ...WRITE },
|
|
1821
|
-
async (params) => {
|
|
1822
|
-
if (!params.repo && !params.hubReleaseId) {
|
|
1823
|
-
return jsonReply2({
|
|
1824
|
-
error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
const catalog = await graphql(listingsQuery(true));
|
|
1828
|
-
let listing;
|
|
1829
|
-
if (params.hubReleaseId) {
|
|
1830
|
-
listing = catalog.listings.find(
|
|
1831
|
-
(l) => l.listedRelease?.id === params.hubReleaseId
|
|
1832
|
-
);
|
|
1833
|
-
} else {
|
|
1834
|
-
const repoKey = params.repo.toLowerCase();
|
|
1835
|
-
listing = catalog.listings.find(
|
|
1836
|
-
(l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
|
|
1837
|
-
);
|
|
1838
|
-
}
|
|
1839
|
-
if (!listing) {
|
|
1840
|
-
return jsonReply2({
|
|
1841
|
-
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.`
|
|
1842
|
-
});
|
|
1843
|
-
}
|
|
1844
|
-
const release = listing.listedRelease;
|
|
1845
|
-
if (!release?.build?.imageName) {
|
|
1846
|
-
return jsonReply2({
|
|
1847
|
-
error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
|
|
1851
|
-
return jsonReply2({
|
|
1852
|
-
error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
|
|
1853
|
-
});
|
|
1854
|
-
}
|
|
1855
|
-
const config = parseReleaseConfig(release.config) ?? {};
|
|
1856
|
-
const gpuIds = params.gpuIds ?? config.gpuIds;
|
|
1857
|
-
if (!gpuIds) {
|
|
1858
|
-
return jsonReply2({
|
|
1859
|
-
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).'
|
|
1860
|
-
});
|
|
1861
|
-
}
|
|
1862
|
-
const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
|
|
1863
|
-
if (missingRequired.length > 0) {
|
|
1864
|
-
return jsonReply2({
|
|
1865
|
-
error: `Missing required environment variables for this release: ${missingRequired.join(
|
|
1866
|
-
", "
|
|
1867
|
-
)}. Pass them via the env parameter.`
|
|
1868
|
-
});
|
|
1869
|
-
}
|
|
1870
|
-
const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
|
|
1871
|
-
const minCuda = minCudaVersion(config.allowedCudaVersions);
|
|
1872
|
-
const input = {
|
|
1873
|
-
name: endpointName,
|
|
1874
|
-
hubReleaseId: release.id,
|
|
1875
|
-
type: "QB",
|
|
1876
|
-
gpuIds,
|
|
1877
|
-
gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
|
|
1878
|
-
workersMin: params.workersMin ?? 0,
|
|
1879
|
-
workersMax: params.workersMax ?? null,
|
|
1880
|
-
idleTimeout: params.idleTimeout ?? 5,
|
|
1881
|
-
scalerType: params.scalerType ?? "QUEUE_DELAY",
|
|
1882
|
-
scalerValue: params.scalerValue ?? 4,
|
|
1883
|
-
executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
|
|
1884
|
-
flashBootType: params.flashboot ?? "FLASHBOOT",
|
|
1885
|
-
locations: null,
|
|
1886
|
-
networkVolumeIds: null,
|
|
1887
|
-
compliance: [],
|
|
1888
|
-
modelReferences: [],
|
|
1889
|
-
...minCuda ? {
|
|
1890
|
-
minCudaVersion: minCuda,
|
|
1891
|
-
allowedCudaVersions: config.allowedCudaVersions.join(",")
|
|
1892
|
-
} : {},
|
|
1893
|
-
template: {
|
|
1894
|
-
name: `${endpointName}__template__${randomSuffix()}`,
|
|
1895
|
-
imageName: release.build.imageName,
|
|
1896
|
-
containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
|
|
1897
|
-
containerRegistryAuthId: "",
|
|
1898
|
-
dockerArgs: "",
|
|
1899
|
-
startScript: "",
|
|
1900
|
-
ports: "",
|
|
1901
|
-
env
|
|
1902
|
-
}
|
|
1903
|
-
};
|
|
1904
|
-
const data = await graphqlAuthed(
|
|
1905
|
-
`
|
|
1906
|
-
mutation saveEndpoint($input: EndpointInput!) {
|
|
1907
|
-
saveEndpoint(input: $input) {
|
|
1908
|
-
id
|
|
1909
|
-
name
|
|
1910
|
-
gpuIds
|
|
1911
|
-
gpuCount
|
|
1912
|
-
workersMin
|
|
1913
|
-
workersMax
|
|
1914
|
-
idleTimeout
|
|
1915
|
-
scalerType
|
|
1916
|
-
scalerValue
|
|
1917
|
-
flashBootType
|
|
1918
|
-
templateId
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
`,
|
|
1922
|
-
{ input }
|
|
1923
|
-
);
|
|
1924
|
-
return jsonReply2({
|
|
1925
|
-
endpoint: data.saveEndpoint,
|
|
1926
|
-
deployed: {
|
|
1927
|
-
repo: `${listing.repoOwner}/${listing.repoName}`,
|
|
1928
|
-
release: release.tagName,
|
|
1929
|
-
hubReleaseId: release.id,
|
|
1930
|
-
imageName: release.build.imageName
|
|
1931
|
-
},
|
|
1932
|
-
note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
|
|
1933
|
-
});
|
|
1934
|
-
}
|
|
1935
|
-
);
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
// src/tools/public-endpoints.ts
|
|
1939
|
-
var import_zod5 = require("zod");
|
|
1940
|
-
function parseMetadata(metadata) {
|
|
1941
|
-
if (!metadata) return {};
|
|
1942
|
-
try {
|
|
1943
|
-
const parsed = JSON.parse(metadata);
|
|
1944
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1945
|
-
} catch {
|
|
1946
|
-
return {};
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
function registerPublicEndpointTools(server, rt) {
|
|
1950
|
-
const { graphql } = rt;
|
|
1951
|
-
server.tool(
|
|
1952
|
-
"list-public-endpoints",
|
|
1953
|
-
"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.",
|
|
1954
|
-
{
|
|
1955
|
-
...listPaginationParams,
|
|
1956
|
-
searchTerm: import_zod5.z.string().optional().describe(
|
|
1957
|
-
"Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
|
|
1958
|
-
),
|
|
1959
|
-
modality: import_zod5.z.string().optional().describe(
|
|
1960
|
-
"Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
|
|
1961
|
-
),
|
|
1962
|
-
owner: import_zod5.z.string().optional().describe(
|
|
1963
|
-
"Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
|
|
1964
|
-
),
|
|
1965
|
-
includeOffline: import_zod5.z.boolean().optional().describe(
|
|
1966
|
-
"Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
|
|
1967
|
-
)
|
|
1968
|
-
},
|
|
1969
|
-
{ title: "List public endpoints", ...READ_ONLY },
|
|
1970
|
-
async (params) => {
|
|
1971
|
-
const data = await graphql(`
|
|
1972
|
-
query {
|
|
1973
|
-
allAiApiPublicConfigs {
|
|
1974
|
-
id
|
|
1975
|
-
aiApiId
|
|
1976
|
-
modelName
|
|
1977
|
-
displayName
|
|
1978
|
-
description
|
|
1979
|
-
metadata
|
|
1980
|
-
isLive
|
|
1981
|
-
createdAt
|
|
1982
|
-
updatedAt
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
`);
|
|
1986
|
-
let configs = data.allAiApiPublicConfigs.map((c) => ({
|
|
1987
|
-
config: c,
|
|
1988
|
-
meta: parseMetadata(c.metadata)
|
|
1989
|
-
}));
|
|
1990
|
-
if (!params.includeOffline) {
|
|
1991
|
-
configs = configs.filter(({ config }) => config.isLive);
|
|
1992
|
-
}
|
|
1993
|
-
if (params.modality) {
|
|
1994
|
-
const term = params.modality.toLowerCase();
|
|
1995
|
-
configs = configs.filter(
|
|
1996
|
-
({ meta }) => (meta.source ?? "").toLowerCase() === term
|
|
1997
|
-
);
|
|
1998
|
-
}
|
|
1999
|
-
if (params.owner) {
|
|
2000
|
-
const term = params.owner.toLowerCase();
|
|
2001
|
-
configs = configs.filter(
|
|
2002
|
-
({ meta }) => (meta.owner ?? "").toLowerCase() === term
|
|
2003
|
-
);
|
|
2004
|
-
}
|
|
2005
|
-
if (params.searchTerm) {
|
|
2006
|
-
const term = params.searchTerm.toLowerCase();
|
|
2007
|
-
configs = configs.filter(
|
|
2008
|
-
({ config, meta }) => [
|
|
2009
|
-
config.displayName,
|
|
2010
|
-
config.modelName,
|
|
2011
|
-
config.aiApiId,
|
|
2012
|
-
config.description,
|
|
2013
|
-
meta.owner,
|
|
2014
|
-
meta.tag,
|
|
2015
|
-
meta.source
|
|
2016
|
-
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
2017
|
-
);
|
|
2018
|
-
}
|
|
2019
|
-
configs = [...configs].sort(
|
|
2020
|
-
(a, b) => a.config.displayName.localeCompare(b.config.displayName)
|
|
2021
|
-
);
|
|
2022
|
-
const result = configs.map(({ config, meta }) => ({
|
|
2023
|
-
endpointId: config.aiApiId,
|
|
2024
|
-
displayName: config.displayName,
|
|
2025
|
-
modelName: config.modelName,
|
|
2026
|
-
description: config.description,
|
|
2027
|
-
modality: meta.source,
|
|
2028
|
-
tag: meta.tag,
|
|
2029
|
-
owner: meta.owner,
|
|
2030
|
-
pricing: meta.priceString,
|
|
2031
|
-
isLive: config.isLive,
|
|
2032
|
-
baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
|
|
2033
|
-
createdAt: config.createdAt,
|
|
2034
|
-
updatedAt: config.updatedAt
|
|
2035
|
-
}));
|
|
2036
|
-
return capListResult(result, {
|
|
2037
|
-
limit: params.limit,
|
|
2038
|
-
cursor: params.cursor
|
|
2039
|
-
});
|
|
2040
|
-
}
|
|
2041
|
-
);
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
// src/tools/endpoint-gpus.ts
|
|
2045
|
-
var import_zod6 = require("zod");
|
|
2046
|
-
function registerEndpointGpuTools(server, rt) {
|
|
2047
|
-
const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
2048
|
-
server.tool(
|
|
2049
|
-
"set-endpoint-gpus",
|
|
2050
|
-
"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.",
|
|
2051
|
-
{
|
|
2052
|
-
endpointId: import_zod6.z.string().describe("ID of the Serverless endpoint to update"),
|
|
2053
|
-
gpuIds: import_zod6.z.string().optional().describe(
|
|
2054
|
-
"Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
|
|
2055
|
-
),
|
|
2056
|
-
pools: import_zod6.z.array(import_zod6.z.string()).optional().describe(
|
|
2057
|
-
"GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
|
|
2058
|
-
),
|
|
2059
|
-
excludeGpuTypeIds: import_zod6.z.array(import_zod6.z.string()).optional().describe(
|
|
2060
|
-
"GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
|
|
2061
|
-
),
|
|
2062
|
-
gpuCount: import_zod6.z.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
|
|
2063
|
-
minCudaVersion: import_zod6.z.string().optional().describe(
|
|
2064
|
-
"Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
|
|
2065
|
-
),
|
|
2066
|
-
allowedCudaVersions: import_zod6.z.string().optional().describe(
|
|
2067
|
-
"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."
|
|
2068
|
-
)
|
|
2069
|
-
},
|
|
2070
|
-
{ title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
|
|
2071
|
-
async (params) => {
|
|
2072
|
-
const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
|
|
2073
|
-
...params.pools,
|
|
2074
|
-
...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
|
|
2075
|
-
].join(",") : void 0);
|
|
2076
|
-
if (!gpuIds) {
|
|
2077
|
-
return jsonReply2({
|
|
2078
|
-
error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
|
|
2079
|
-
});
|
|
2080
|
-
}
|
|
2081
|
-
const data = await graphqlAuthed(`
|
|
2082
|
-
query {
|
|
2083
|
-
myself {
|
|
2084
|
-
endpoints {
|
|
2085
|
-
id
|
|
2086
|
-
name
|
|
2087
|
-
gpuIds
|
|
2088
|
-
gpuCount
|
|
2089
|
-
workersMin
|
|
2090
|
-
workersMax
|
|
2091
|
-
idleTimeout
|
|
2092
|
-
scalerType
|
|
2093
|
-
scalerValue
|
|
2094
|
-
executionTimeoutMs
|
|
2095
|
-
flashBootType
|
|
2096
|
-
type
|
|
2097
|
-
locations
|
|
2098
|
-
templateId
|
|
2099
|
-
allowedCudaVersions
|
|
2100
|
-
minCudaVersion
|
|
2101
|
-
compliance
|
|
2102
|
-
modelReferences
|
|
2103
|
-
networkVolumeIds {
|
|
2104
|
-
networkVolumeId
|
|
2105
|
-
dataCenterId
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
`);
|
|
2111
|
-
const current = data.myself.endpoints.find(
|
|
2112
|
-
(e) => e.id === params.endpointId
|
|
2113
|
-
);
|
|
2114
|
-
if (!current) {
|
|
2115
|
-
return jsonReply2({
|
|
2116
|
-
error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
|
|
2117
|
-
});
|
|
2118
|
-
}
|
|
2119
|
-
const input = {
|
|
2120
|
-
id: current.id,
|
|
2121
|
-
name: current.name,
|
|
2122
|
-
gpuIds,
|
|
2123
|
-
gpuCount: params.gpuCount ?? current.gpuCount,
|
|
2124
|
-
workersMin: current.workersMin,
|
|
2125
|
-
workersMax: current.workersMax,
|
|
2126
|
-
idleTimeout: current.idleTimeout,
|
|
2127
|
-
scalerType: current.scalerType,
|
|
2128
|
-
scalerValue: current.scalerValue,
|
|
2129
|
-
executionTimeoutMs: current.executionTimeoutMs,
|
|
2130
|
-
flashBootType: current.flashBootType,
|
|
2131
|
-
type: current.type,
|
|
2132
|
-
locations: current.locations,
|
|
2133
|
-
networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
|
|
2134
|
-
// Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
|
|
2135
|
-
// ONLY, and the read shape is rejected outright.
|
|
2136
|
-
current.networkVolumeIds.map((v) => ({
|
|
2137
|
-
networkVolumeId: v.networkVolumeId
|
|
2138
|
-
}))
|
|
2139
|
-
) : null
|
|
2140
|
-
};
|
|
2141
|
-
for (const [key, value] of Object.entries({
|
|
2142
|
-
templateId: current.templateId,
|
|
2143
|
-
allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
|
|
2144
|
-
minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
|
|
2145
|
-
compliance: current.compliance,
|
|
2146
|
-
modelReferences: current.modelReferences
|
|
2147
|
-
})) {
|
|
2148
|
-
if (value !== null && value !== void 0) input[key] = value;
|
|
2149
|
-
}
|
|
2150
|
-
const result = await graphqlAuthed(
|
|
2151
|
-
`
|
|
2152
|
-
mutation saveEndpoint($input: EndpointInput!) {
|
|
2153
|
-
saveEndpoint(input: $input) {
|
|
2154
|
-
id
|
|
2155
|
-
name
|
|
2156
|
-
gpuIds
|
|
2157
|
-
gpuCount
|
|
2158
|
-
workersMin
|
|
2159
|
-
workersMax
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
`,
|
|
2163
|
-
{ input }
|
|
2164
|
-
);
|
|
2165
|
-
return jsonReply2({
|
|
2166
|
-
endpoint: result.saveEndpoint,
|
|
2167
|
-
previousGpuIds: current.gpuIds
|
|
2168
|
-
});
|
|
2169
|
-
}
|
|
2170
|
-
);
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// src/tools/pods.ts
|
|
2174
|
-
var import_zod7 = require("zod");
|
|
2175
|
-
function registerPodTools(server, rt) {
|
|
2176
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
|
|
2177
|
-
server.tool(
|
|
2178
|
-
"list-pods",
|
|
2179
|
-
"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.",
|
|
2180
|
-
{
|
|
2181
|
-
...listPaginationParams,
|
|
2182
|
-
computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
|
|
2183
|
-
gpuTypeId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
|
|
2184
|
-
dataCenterId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
|
|
2185
|
-
name: import_zod7.z.string().optional().describe("Filter to Pods with the provided name"),
|
|
2186
|
-
includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
|
|
2187
|
-
includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
|
|
2188
|
-
},
|
|
2189
|
-
{ title: "List pods", ...READ_ONLY },
|
|
2190
|
-
async (params) => {
|
|
2191
|
-
const backend = backendFor("pods");
|
|
2192
|
-
let queryString = "";
|
|
2193
|
-
if (backend.version === "v1") {
|
|
2194
|
-
const queryParams = new URLSearchParams();
|
|
2195
|
-
if (params.computeType)
|
|
2196
|
-
queryParams.append("computeType", params.computeType);
|
|
2197
|
-
if (params.gpuTypeId)
|
|
2198
|
-
params.gpuTypeId.forEach(
|
|
2199
|
-
(type) => queryParams.append("gpuTypeId", type)
|
|
2200
|
-
);
|
|
2201
|
-
if (params.dataCenterId)
|
|
2202
|
-
params.dataCenterId.forEach(
|
|
2203
|
-
(dc) => queryParams.append("dataCenterId", dc)
|
|
2204
|
-
);
|
|
2205
|
-
if (params.name) queryParams.append("name", params.name);
|
|
2206
|
-
if (params.includeMachine)
|
|
2207
|
-
queryParams.append(
|
|
2208
|
-
"includeMachine",
|
|
2209
|
-
params.includeMachine.toString()
|
|
2210
|
-
);
|
|
2211
|
-
if (params.includeNetworkVolume)
|
|
2212
|
-
queryParams.append(
|
|
2213
|
-
"includeNetworkVolume",
|
|
2214
|
-
params.includeNetworkVolume.toString()
|
|
2215
|
-
);
|
|
2216
|
-
queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
2217
|
-
}
|
|
2218
|
-
const result = await callRestUrl(
|
|
2219
|
-
`${backend.base}${backend.list}${queryString}`
|
|
2220
|
-
);
|
|
2221
|
-
return capListResult(backend.unwrap(result), {
|
|
2222
|
-
limit: params.limit,
|
|
2223
|
-
cursor: params.cursor
|
|
2224
|
-
});
|
|
2225
|
-
}
|
|
2226
|
-
);
|
|
2227
|
-
server.tool(
|
|
2228
|
-
"get-pod",
|
|
2229
|
-
"Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
|
|
2230
|
-
{
|
|
2231
|
-
podId: import_zod7.z.string().describe("ID of the pod to retrieve"),
|
|
2232
|
-
includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
|
|
2233
|
-
includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
|
|
2234
|
-
},
|
|
2235
|
-
{ title: "Get pod", ...READ_ONLY },
|
|
2236
|
-
async (params) => {
|
|
2237
|
-
const backend = backendFor("pods");
|
|
2238
|
-
let queryString = "";
|
|
2239
|
-
if (backend.version === "v1") {
|
|
2240
|
-
const queryParams = new URLSearchParams();
|
|
2241
|
-
if (params.includeMachine)
|
|
2242
|
-
queryParams.append(
|
|
2243
|
-
"includeMachine",
|
|
2244
|
-
params.includeMachine.toString()
|
|
2245
|
-
);
|
|
2246
|
-
if (params.includeNetworkVolume)
|
|
2247
|
-
queryParams.append(
|
|
2248
|
-
"includeNetworkVolume",
|
|
2249
|
-
params.includeNetworkVolume.toString()
|
|
2250
|
-
);
|
|
2251
|
-
queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
2252
|
-
}
|
|
2253
|
-
const result = await callRestUrl(
|
|
2254
|
-
`${backend.base}${backend.get(params.podId)}${queryString}`
|
|
2255
|
-
);
|
|
2256
|
-
return jsonReply2(result);
|
|
2257
|
-
}
|
|
2258
|
-
);
|
|
2259
|
-
server.tool(
|
|
2260
|
-
"create-pod",
|
|
2261
|
-
'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). For full SSH access (including SCP/SFTP/rsync), pass sshPublicKey \u2014 it sets the PUBLIC_KEY env var and exposes 22/tcp, which official Runpod images turn into a running sshd with the key authorized. 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.',
|
|
2262
|
-
{
|
|
2263
|
-
name: import_zod7.z.string().optional().describe(
|
|
2264
|
-
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
2265
|
-
),
|
|
2266
|
-
imageName: import_zod7.z.string().optional().describe("Docker image to use. Optional when templateId is provided."),
|
|
2267
|
-
templateId: import_zod7.z.string().optional().describe(
|
|
2268
|
-
"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."
|
|
2269
|
-
),
|
|
2270
|
-
computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe(
|
|
2271
|
-
"Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
|
|
2272
|
-
),
|
|
2273
|
-
cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2274
|
-
gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
|
|
2275
|
-
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
2276
|
-
// a negative, or a fractional count reached the wire as-is and came back
|
|
2277
|
-
// as a raw 422; reject it here with a message that names the field.
|
|
2278
|
-
gpuCount: import_zod7.z.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2279
|
-
containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
|
|
2280
|
-
volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
|
|
2281
|
-
volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
|
|
2282
|
-
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2283
|
-
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
|
|
2284
|
-
sshPublicKey: import_zod7.z.string().optional().describe(
|
|
2285
|
-
'SSH PUBLIC key(s) to authorize on the pod \u2014 the CONTENTS of your ~/.ssh/<key>.pub file, i.e. a literal "ssh-ed25519 AAAAC3Nza\u2026 you@host" line, NOT a path to the file, a fingerprint, or the private key; newline-separate multiple keys. Merges the key into the PUBLIC_KEY env var and ensures 22/tcp is exposed, enabling full SSH (SCP/SFTP/rsync) on any image that honors the PUBLIC_KEY convention \u2014 all runpod/* official images do. Extends template ports/env rather than replacing them.'
|
|
2286
|
-
),
|
|
2287
|
-
containerRegistryAuthId: import_zod7.z.string().optional().describe(
|
|
2288
|
-
"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)."
|
|
2289
|
-
),
|
|
2290
|
-
dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers"),
|
|
2291
|
-
allowedCudaVersions: import_zod7.z.array(import_zod7.z.string()).optional().describe(
|
|
2292
|
-
'Acceptable host CUDA versions as major.minor, e.g. ["12.8","12.6"] (v2, GPU pods only). Exact match \u2014 discover valid values per GPU type with get-capacity. Mutually exclusive with minCudaVersion.'
|
|
2293
|
-
),
|
|
2294
|
-
minCudaVersion: import_zod7.z.string().optional().describe(
|
|
2295
|
-
'Lowest acceptable host CUDA version as major.minor, e.g. "12.4" (v2, GPU pods only). A bare major like "12" is rejected. Open-ended floor; mutually exclusive with a non-empty allowedCudaVersions.'
|
|
2296
|
-
)
|
|
2297
|
-
},
|
|
2298
|
-
{ title: "Create pod", ...WRITE },
|
|
2299
|
-
async (params) => {
|
|
2300
|
-
const backend = backendFor("pods");
|
|
2301
|
-
const { sshPublicKey, ...podParams } = params;
|
|
2302
|
-
let sshKey;
|
|
2303
|
-
if (sshPublicKey !== void 0) {
|
|
2304
|
-
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
2305
|
-
if (!validated.ok)
|
|
2306
|
-
return jsonReply2({ error: validated.error, status: 400 });
|
|
2307
|
-
sshKey = validated.key;
|
|
2308
|
-
}
|
|
2309
|
-
const sshNote = "sshPublicKey applied: PUBLIC_KEY env set and 22/tcp exposed. Full SSH (incl. SCP/SFTP/rsync) also requires an image that installs PUBLIC_KEY into authorized_keys and starts sshd (all runpod/* official images do) and a machine with a public IP \u2014 read the pod\u2019s portMappings for port 22 once it is running.";
|
|
2310
|
-
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2311
|
-
if (hasCudaParams && backend.version !== "v2") {
|
|
2312
|
-
return jsonReply2({
|
|
2313
|
-
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2314
|
-
status: 501
|
|
2315
|
-
});
|
|
2316
|
-
}
|
|
2317
|
-
if (params.templateId && backend.version !== "v2") {
|
|
2318
|
-
return jsonReply2({
|
|
2319
|
-
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
2320
|
-
status: 501
|
|
2321
|
-
});
|
|
2322
|
-
}
|
|
2323
|
-
if (!params.imageName && !params.templateId) {
|
|
2324
|
-
return jsonReply2({
|
|
2325
|
-
error: "Provide imageName, or templateId to deploy from an existing template.",
|
|
2326
|
-
status: 400
|
|
2327
|
-
});
|
|
2328
|
-
}
|
|
2329
|
-
const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
|
|
2330
|
-
const hasGpuCount = (params.gpuCount ?? 0) > 0;
|
|
2331
|
-
const wantsCpu = params.computeType === "CPU";
|
|
2332
|
-
const wantsGpu = hasGpuTypes || hasGpuCount || params.computeType === "GPU";
|
|
2333
|
-
if (backend.version === "v2") {
|
|
2334
|
-
if (wantsGpu && wantsCpu) {
|
|
2335
|
-
return jsonReply2({
|
|
2336
|
-
error: "Specify a GPU pod (gpuTypeIds or computeType:'GPU') OR a CPU pod (computeType:'CPU'), not both.",
|
|
2337
|
-
status: 400
|
|
2338
|
-
});
|
|
2339
|
-
}
|
|
2340
|
-
if (!wantsGpu && !wantsCpu) {
|
|
2341
|
-
return jsonReply2({
|
|
2342
|
-
error: "No pod type specified. Pass gpuTypeIds (see list-gpu-types) for a GPU pod, or computeType:'CPU' for a CPU pod.",
|
|
2343
|
-
status: 400
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
|
-
if (wantsGpu && !hasGpuTypes) {
|
|
2347
|
-
return jsonReply2({
|
|
2348
|
-
error: "A GPU pod needs gpuTypeIds (see list-gpu-types); gpuCount or computeType alone is under-specified.",
|
|
2349
|
-
status: 400
|
|
2350
|
-
});
|
|
2351
|
-
}
|
|
2352
|
-
if (hasCudaParams && !wantsGpu) {
|
|
2353
|
-
return jsonReply2({
|
|
2354
|
-
error: "allowedCudaVersions/minCudaVersion apply to GPU pods only \u2014 pass gpuTypeIds (see list-gpu-types).",
|
|
2355
|
-
status: 400
|
|
2356
|
-
});
|
|
2357
|
-
}
|
|
2358
|
-
if (hasCudaParams) {
|
|
2359
|
-
const cudaError = cudaConstraintError(params);
|
|
2360
|
-
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2361
|
-
}
|
|
2362
|
-
if (!wantsCpu && !params.name && !params.templateId) {
|
|
2363
|
-
return jsonReply2({
|
|
2364
|
-
error: "Provide a name for the pod (deploying from a templateId inherits the template name).",
|
|
2365
|
-
status: 400
|
|
2366
|
-
});
|
|
2367
|
-
}
|
|
2368
|
-
if (wantsCpu) {
|
|
2369
|
-
if (params.templateId) {
|
|
2370
|
-
return jsonReply2({
|
|
2371
|
-
error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
|
|
2372
|
-
status: 400
|
|
2373
|
-
});
|
|
2374
|
-
}
|
|
2375
|
-
try {
|
|
2376
|
-
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2377
|
-
const result = await callRestUrl(
|
|
2378
|
-
`${restV1Base(env)}/pods`,
|
|
2379
|
-
"POST",
|
|
2380
|
-
cpuBody
|
|
2381
|
-
);
|
|
2382
|
-
return jsonReply2({
|
|
2383
|
-
...result,
|
|
2384
|
-
...sshKey ? { _ssh: sshNote } : {},
|
|
2385
|
-
_servedBy: "v1",
|
|
2386
|
-
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2387
|
-
});
|
|
2388
|
-
} catch (error) {
|
|
2389
|
-
if (error instanceof HttpError) {
|
|
2390
|
-
return jsonReply2({
|
|
2391
|
-
error: error.message,
|
|
2392
|
-
status: error.status,
|
|
2393
|
-
_servedBy: "v1",
|
|
2394
|
-
_note: "CPU pod create was routed to the v1 API and failed."
|
|
2395
|
-
});
|
|
2396
|
-
}
|
|
2397
|
-
throw error;
|
|
2398
|
-
}
|
|
2399
|
-
}
|
|
2400
|
-
}
|
|
2401
|
-
let body = backend.mapCreate(podParams);
|
|
2402
|
-
if (params.templateId) {
|
|
2403
|
-
let template;
|
|
2404
|
-
try {
|
|
2405
|
-
template = await callRestUrl(
|
|
2406
|
-
`${restV2Base(env)}/templates/${params.templateId}`
|
|
2407
|
-
);
|
|
2408
|
-
} catch (error) {
|
|
2409
|
-
if (error instanceof HttpError) {
|
|
2410
|
-
return jsonReply2({
|
|
2411
|
-
error: `Failed to load template ${params.templateId}: ${error.message}`,
|
|
2412
|
-
status: error.status
|
|
2413
|
-
});
|
|
2414
|
-
}
|
|
2415
|
-
throw error;
|
|
2416
|
-
}
|
|
2417
|
-
body = { ...podBodyFromTemplate(template), ...body };
|
|
2418
|
-
}
|
|
2419
|
-
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2420
|
-
try {
|
|
2421
|
-
const result = await callRestUrl(
|
|
2422
|
-
`${backend.base}${backend.list}`,
|
|
2423
|
-
"POST",
|
|
2424
|
-
body
|
|
2425
|
-
);
|
|
2426
|
-
const extras = {};
|
|
2427
|
-
if (sshKey) extras._ssh = sshNote;
|
|
2428
|
-
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2429
|
-
extras._warning = `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`;
|
|
2430
|
-
}
|
|
2431
|
-
return jsonReply2(
|
|
2432
|
-
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
2433
|
-
);
|
|
2434
|
-
} catch (error) {
|
|
2435
|
-
if (error instanceof HttpError) {
|
|
2436
|
-
return jsonReply2({ error: error.message, status: error.status });
|
|
2437
|
-
}
|
|
2438
|
-
throw error;
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
);
|
|
2442
|
-
server.tool(
|
|
2443
|
-
"update-pod",
|
|
2444
|
-
"Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
|
|
2445
|
-
{
|
|
2446
|
-
podId: import_zod7.z.string().describe("ID of the pod to update"),
|
|
2447
|
-
name: import_zod7.z.string().optional().describe("New name for the pod"),
|
|
2448
|
-
imageName: import_zod7.z.string().optional().describe("New Docker image"),
|
|
2449
|
-
containerDiskInGb: import_zod7.z.number().optional().describe("New container disk size in GB"),
|
|
2450
|
-
volumeInGb: import_zod7.z.number().optional().describe("New volume size in GB"),
|
|
2451
|
-
volumeMountPath: import_zod7.z.string().optional().describe("New path to mount the volume"),
|
|
2452
|
-
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("New ports to expose"),
|
|
2453
|
-
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("New environment variables")
|
|
2454
|
-
},
|
|
2455
|
-
{ title: "Update pod", ...WRITE, idempotentHint: true },
|
|
2456
|
-
async (params) => {
|
|
2457
|
-
const { podId, ...updateParams } = params;
|
|
2458
|
-
const backend = backendFor("pods");
|
|
2459
|
-
const body = backend.mapUpdate(updateParams);
|
|
2460
|
-
const result = await callRestUrl(
|
|
2461
|
-
`${backend.base}${backend.get(podId)}`,
|
|
2462
|
-
"PATCH",
|
|
2463
|
-
body
|
|
2464
|
-
);
|
|
2465
|
-
return jsonReply2(result);
|
|
2466
|
-
}
|
|
2467
|
-
);
|
|
2468
|
-
server.tool(
|
|
2469
|
-
"start-pod",
|
|
2470
|
-
"Start a stopped pod (resumes the same pod and its billing).",
|
|
2471
|
-
{
|
|
2472
|
-
podId: import_zod7.z.string().describe("ID of the pod to start")
|
|
2473
|
-
},
|
|
2474
|
-
{ title: "Start pod", ...WRITE, idempotentHint: true },
|
|
2475
|
-
async (params) => {
|
|
2476
|
-
const result = await podAction(params.podId, "start");
|
|
2477
|
-
return jsonReply2(result);
|
|
2478
|
-
}
|
|
2479
|
-
);
|
|
2480
|
-
server.tool(
|
|
2481
|
-
"stop-pod",
|
|
2482
|
-
"Stop a running pod. The pod and its disk persist; GPU billing stops.",
|
|
2483
|
-
{
|
|
2484
|
-
podId: import_zod7.z.string().describe("ID of the pod to stop")
|
|
2485
|
-
},
|
|
2486
|
-
{ title: "Stop pod", ...WRITE, idempotentHint: true },
|
|
2487
|
-
async (params) => {
|
|
2488
|
-
const result = await podAction(params.podId, "stop");
|
|
2489
|
-
return jsonReply2(result);
|
|
2490
|
-
}
|
|
2491
|
-
);
|
|
2492
|
-
server.tool(
|
|
2493
|
-
"restart-pod",
|
|
2494
|
-
"Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
|
|
2495
|
-
{
|
|
2496
|
-
podId: import_zod7.z.string().describe("ID of the pod to restart")
|
|
2497
|
-
},
|
|
2498
|
-
{ title: "Restart pod", ...WRITE },
|
|
2499
|
-
async (params) => {
|
|
2500
|
-
const backend = backendFor("pods");
|
|
2501
|
-
if (backend.version === "v1") {
|
|
2502
|
-
return jsonReply2({
|
|
2503
|
-
error: "restart-pod is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or stop then start the pod on v1).",
|
|
2504
|
-
status: 501
|
|
2505
|
-
});
|
|
2506
|
-
}
|
|
2507
|
-
const result = await podAction(params.podId, "restart");
|
|
2508
|
-
return jsonReply2(result);
|
|
2509
|
-
}
|
|
2510
|
-
);
|
|
2511
|
-
server.tool(
|
|
2512
|
-
"stream-pod-logs",
|
|
2513
|
-
"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).",
|
|
2514
|
-
{
|
|
2515
|
-
podId: import_zod7.z.string().describe("ID of the pod whose logs to stream"),
|
|
2516
|
-
...logStreamParams
|
|
2517
|
-
},
|
|
2518
|
-
{ title: "Stream pod logs", ...READ_ONLY },
|
|
2519
|
-
(params) => streamLogsReply(
|
|
2520
|
-
rt,
|
|
2521
|
-
{
|
|
2522
|
-
name: "stream-pod-logs",
|
|
2523
|
-
resource: "pods",
|
|
2524
|
-
logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
|
|
2525
|
-
},
|
|
2526
|
-
params
|
|
2527
|
-
)
|
|
2528
|
-
);
|
|
2529
|
-
server.tool(
|
|
2530
|
-
"delete-pod",
|
|
2531
|
-
"Permanently delete a pod and its data. This cannot be undone.",
|
|
2532
|
-
{
|
|
2533
|
-
podId: import_zod7.z.string().describe("ID of the pod to delete")
|
|
2534
|
-
},
|
|
2535
|
-
{ title: "Delete pod", ...DESTRUCTIVE },
|
|
2536
|
-
async (params) => {
|
|
2537
|
-
const backend = backendFor("pods");
|
|
2538
|
-
const result = await callRestUrl(
|
|
2539
|
-
`${backend.base}${backend.get(params.podId)}`,
|
|
2540
|
-
"DELETE"
|
|
2541
|
-
);
|
|
2542
|
-
return jsonReply2(result);
|
|
2543
|
-
}
|
|
2544
|
-
);
|
|
2545
|
-
}
|
|
2546
|
-
|
|
2547
|
-
// src/tools/endpoints.ts
|
|
2548
|
-
var import_zod8 = require("zod");
|
|
2549
|
-
var MIN_QUEUE_DELAY = 0.5;
|
|
2550
|
-
var MIN_REQUEST_COUNT = 1;
|
|
2551
|
-
function isRequestCountScaler(type) {
|
|
2552
|
-
return type === "REQUEST_COUNT";
|
|
2553
|
-
}
|
|
2554
|
-
var MIN_IDLE_TIMEOUT = 1;
|
|
2555
|
-
var MAX_IDLE_TIMEOUT = 3600;
|
|
2556
|
-
function scalerValueError(scalerType, scalerValue) {
|
|
2557
|
-
if (scalerType !== "REQUEST_COUNT" || scalerValue === void 0) return;
|
|
2558
|
-
if (!Number.isInteger(scalerValue) || scalerValue < MIN_REQUEST_COUNT) {
|
|
2559
|
-
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}.`;
|
|
2560
|
-
}
|
|
2561
|
-
return;
|
|
2562
|
-
}
|
|
2563
|
-
function registerEndpointTools(server, rt) {
|
|
2564
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
2565
|
-
server.tool(
|
|
2566
|
-
"list-endpoints",
|
|
2567
|
-
"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`.",
|
|
2568
|
-
{
|
|
2569
|
-
...listPaginationParams,
|
|
2570
|
-
includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
|
|
2571
|
-
includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
|
|
2572
|
-
},
|
|
2573
|
-
{ title: "List endpoints", ...READ_ONLY },
|
|
2574
|
-
async (params) => {
|
|
2575
|
-
const backend = backendFor("endpoints");
|
|
2576
|
-
let queryString = "";
|
|
2577
|
-
if (backend.version === "v1") {
|
|
2578
|
-
const queryParams = new URLSearchParams();
|
|
2579
|
-
if (params.includeTemplate)
|
|
2580
|
-
queryParams.append(
|
|
2581
|
-
"includeTemplate",
|
|
2582
|
-
params.includeTemplate.toString()
|
|
2583
|
-
);
|
|
2584
|
-
if (params.includeWorkers)
|
|
2585
|
-
queryParams.append(
|
|
2586
|
-
"includeWorkers",
|
|
2587
|
-
params.includeWorkers.toString()
|
|
2588
|
-
);
|
|
2589
|
-
queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
2590
|
-
}
|
|
2591
|
-
const result = await callRestUrl(
|
|
2592
|
-
`${backend.base}${backend.list}${queryString}`
|
|
2593
|
-
);
|
|
2594
|
-
return capListResult(backend.unwrap(result), {
|
|
2595
|
-
limit: params.limit,
|
|
2596
|
-
cursor: params.cursor
|
|
2597
|
-
});
|
|
2598
|
-
}
|
|
2599
|
-
);
|
|
2600
|
-
server.tool(
|
|
2601
|
-
"get-endpoint",
|
|
2602
|
-
"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.",
|
|
2603
|
-
{
|
|
2604
|
-
endpointId: import_zod8.z.string().describe("ID of the endpoint to retrieve"),
|
|
2605
|
-
includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
|
|
2606
|
-
includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
|
|
2607
|
-
},
|
|
2608
|
-
{ title: "Get endpoint", ...READ_ONLY },
|
|
2609
|
-
async (params) => {
|
|
2610
|
-
const backend = backendFor("endpoints");
|
|
2611
|
-
let queryString = "";
|
|
2612
|
-
if (backend.version === "v1") {
|
|
2613
|
-
const queryParams = new URLSearchParams();
|
|
2614
|
-
if (params.includeTemplate)
|
|
2615
|
-
queryParams.append(
|
|
2616
|
-
"includeTemplate",
|
|
2617
|
-
params.includeTemplate.toString()
|
|
2618
|
-
);
|
|
2619
|
-
if (params.includeWorkers)
|
|
2620
|
-
queryParams.append(
|
|
2621
|
-
"includeWorkers",
|
|
2622
|
-
params.includeWorkers.toString()
|
|
2623
|
-
);
|
|
2624
|
-
queryString = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
2625
|
-
}
|
|
2626
|
-
const result = await callRestUrl(
|
|
2627
|
-
`${backend.base}${backend.get(params.endpointId)}${queryString}`
|
|
2628
|
-
);
|
|
2629
|
-
return jsonReply2(result);
|
|
2630
|
-
}
|
|
2631
|
-
);
|
|
2632
|
-
server.tool(
|
|
2633
|
-
"create-endpoint",
|
|
2634
|
-
"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.",
|
|
2635
|
-
{
|
|
2636
|
-
name: import_zod8.z.string().optional().describe("Name for the endpoint"),
|
|
2637
|
-
// --- v2 inline-config fields ---
|
|
2638
|
-
imageName: import_zod8.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
|
|
2639
|
-
endpointType: import_zod8.z.enum(["QUEUE", "LOAD_BALANCER"]).optional().describe(
|
|
2640
|
-
"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."
|
|
2641
|
-
),
|
|
2642
|
-
gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe(
|
|
2643
|
-
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
2644
|
-
),
|
|
2645
|
-
gpuCount: import_zod8.z.number().optional().describe("GPUs per worker (v2)"),
|
|
2646
|
-
allowedCudaVersions: import_zod8.z.array(import_zod8.z.string()).optional().describe(
|
|
2647
|
-
'Acceptable host CUDA versions for worker placement as major.minor, e.g. ["12.8"] (v2). Exact match \u2014 see get-capacity for which versions have stock. Mutually exclusive with minCudaVersion.'
|
|
2648
|
-
),
|
|
2649
|
-
minCudaVersion: import_zod8.z.string().optional().describe(
|
|
2650
|
-
'Lowest acceptable host CUDA version as major.minor, e.g. "12.4" (v2). A bare major like "12" is rejected. Mutually exclusive with a non-empty allowedCudaVersions.'
|
|
2651
|
-
),
|
|
2652
|
-
args: import_zod8.z.string().optional().describe("Container start command/args (v2)"),
|
|
2653
|
-
containerDiskInGb: import_zod8.z.number().optional().describe("Container disk size in GB (v2)"),
|
|
2654
|
-
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
2655
|
-
env: import_zod8.z.record(import_zod8.z.string()).optional().describe("Environment variables (v2)"),
|
|
2656
|
-
containerRegistryAuthId: import_zod8.z.string().optional().describe("Container registry auth id for a private image (v2)"),
|
|
2657
|
-
networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("Network volume ids to attach (v2)"),
|
|
2658
|
-
executionTimeoutMs: import_zod8.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
|
|
2659
|
-
flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
|
|
2660
|
-
// --- v1 template-based fields ---
|
|
2661
|
-
templateId: import_zod8.z.string().optional().describe("Template ID (v1). Required on v1."),
|
|
2662
|
-
computeType: import_zod8.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
|
|
2663
|
-
gpuTypeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of acceptable GPU types (v1)"),
|
|
2664
|
-
// --- shared autoscaling fields ---
|
|
2665
|
-
workersMin: import_zod8.z.number().optional().describe("Minimum number of workers"),
|
|
2666
|
-
workersMax: import_zod8.z.number().optional().describe("Maximum number of workers"),
|
|
2667
|
-
scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2668
|
-
"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."
|
|
2669
|
-
),
|
|
2670
|
-
scalerValue: import_zod8.z.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2671
|
-
"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."
|
|
2672
|
-
),
|
|
2673
|
-
idleTimeout: import_zod8.z.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2674
|
-
"Idle timeout in seconds before scaling a worker down (1-3600). Does not apply to QUEUE endpoints scaling on REQUEST_COUNT."
|
|
2675
|
-
),
|
|
2676
|
-
dataCenterIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of preferred data centers")
|
|
2677
|
-
},
|
|
2678
|
-
{ title: "Create endpoint", ...WRITE },
|
|
2679
|
-
async (params) => {
|
|
2680
|
-
const backend = backendFor("endpoints");
|
|
2681
|
-
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2682
|
-
if (hasCudaParams && backend.version !== "v2") {
|
|
2683
|
-
return jsonReply2({
|
|
2684
|
-
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2685
|
-
status: 501
|
|
2686
|
-
});
|
|
2687
|
-
}
|
|
2688
|
-
if (backend.version === "v2") {
|
|
2689
|
-
if (!params.name) {
|
|
2690
|
-
return jsonReply2({
|
|
2691
|
-
error: "On the v2 REST API, create-endpoint requires a name.",
|
|
2692
|
-
status: 400
|
|
2693
|
-
});
|
|
2694
|
-
}
|
|
2695
|
-
if (!params.imageName) {
|
|
2696
|
-
return jsonReply2({
|
|
2697
|
-
error: "On the v2 REST API, create-endpoint needs an imageName (v2 endpoints are image-based, not template-based). To use a templateId, set RUNPOD_REST_VERSION=v1.",
|
|
2698
|
-
status: 400
|
|
2699
|
-
});
|
|
2700
|
-
}
|
|
2701
|
-
if (!params.gpuPoolIds?.length) {
|
|
2702
|
-
return jsonReply2({
|
|
2703
|
-
error: 'create-endpoint needs gpuPoolIds on v2 (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. ["AMPERE_80"]).',
|
|
2704
|
-
status: 400
|
|
2705
|
-
});
|
|
2706
|
-
}
|
|
2707
|
-
if (params.endpointType === "LOAD_BALANCER" && params.scalerType === "QUEUE_DELAY") {
|
|
2708
|
-
return jsonReply2({
|
|
2709
|
-
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.",
|
|
2710
|
-
status: 400
|
|
2711
|
-
});
|
|
2712
|
-
}
|
|
2713
|
-
const scalerError = scalerValueError(
|
|
2714
|
-
params.scalerType ?? (params.endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : void 0),
|
|
2715
|
-
params.scalerValue
|
|
2716
|
-
);
|
|
2717
|
-
if (scalerError) {
|
|
2718
|
-
return jsonReply2({ error: scalerError, status: 400 });
|
|
2719
|
-
}
|
|
2720
|
-
if (hasCudaParams) {
|
|
2721
|
-
const cudaError = cudaConstraintError(params);
|
|
2722
|
-
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2723
|
-
}
|
|
2724
|
-
const body = backend.mapCreate(params);
|
|
2725
|
-
const result2 = await callRestUrl(
|
|
2726
|
-
`${backend.base}${backend.list}`,
|
|
2727
|
-
"POST",
|
|
2728
|
-
body
|
|
2729
|
-
);
|
|
2730
|
-
return jsonReply2(result2);
|
|
2731
|
-
}
|
|
2732
|
-
if (!params.templateId) {
|
|
2733
|
-
return jsonReply2({
|
|
2734
|
-
error: "On the v1 REST API, create-endpoint needs a templateId. (On v2 \u2014 the default \u2014 pass imageName + gpuPoolIds instead.)",
|
|
2735
|
-
status: 400
|
|
2736
|
-
});
|
|
2737
|
-
}
|
|
2738
|
-
const v1Body = {};
|
|
2739
|
-
for (const k of [
|
|
2740
|
-
"templateId",
|
|
2741
|
-
"name",
|
|
2742
|
-
"computeType",
|
|
2743
|
-
"gpuTypeIds",
|
|
2744
|
-
"gpuCount",
|
|
2745
|
-
"workersMin",
|
|
2746
|
-
"workersMax",
|
|
2747
|
-
"dataCenterIds"
|
|
2748
|
-
]) {
|
|
2749
|
-
if (params[k] !== void 0) v1Body[k] = params[k];
|
|
2750
|
-
}
|
|
2751
|
-
const result = await callRestUrl(
|
|
2752
|
-
`${backend.base}${backend.list}`,
|
|
2753
|
-
"POST",
|
|
2754
|
-
v1Body
|
|
2755
|
-
);
|
|
2756
|
-
return jsonReply2(result);
|
|
2757
|
-
}
|
|
2758
|
-
);
|
|
2759
|
-
server.tool(
|
|
2760
|
-
"update-endpoint",
|
|
2761
|
-
"Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot, plus the GPU selection (gpuPoolIds/gpuCount) and its CUDA constraints (allowedCudaVersions/minCudaVersion) \u2014 CUDA-only or count-only changes do not require resending gpuPoolIds; 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. Note: passing gpuPoolIds replaces the GPU selection wholesale, which clears any GPU-type exclusions set elsewhere (console or set-endpoint-gpus) \u2014 the reply carries a _warning when that happens.",
|
|
2762
|
-
{
|
|
2763
|
-
endpointId: import_zod8.z.string().describe("ID of the endpoint to update"),
|
|
2764
|
-
name: import_zod8.z.string().optional().describe("New name for the endpoint"),
|
|
2765
|
-
workersMin: import_zod8.z.number().optional().describe("New minimum number of workers"),
|
|
2766
|
-
workersMax: import_zod8.z.number().optional().describe("New maximum number of workers"),
|
|
2767
|
-
idleTimeout: import_zod8.z.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2768
|
-
"New idle timeout in seconds (1-3600). Does not apply to queue endpoints scaling on REQUEST_COUNT."
|
|
2769
|
-
),
|
|
2770
|
-
scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2771
|
-
"New autoscaler signal. Switchable on queue endpoints; load-balancing endpoints only accept REQUEST_COUNT."
|
|
2772
|
-
),
|
|
2773
|
-
scalerValue: import_zod8.z.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2774
|
-
"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."
|
|
2775
|
-
),
|
|
2776
|
-
// --- v2-only inline-config fields ---
|
|
2777
|
-
imageName: import_zod8.z.string().optional().describe("New Docker image (v2)"),
|
|
2778
|
-
gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
2779
|
-
gpuCount: import_zod8.z.number().optional().describe("New GPUs per worker (v2)"),
|
|
2780
|
-
allowedCudaVersions: import_zod8.z.array(import_zod8.z.string()).optional().describe(
|
|
2781
|
-
"New acceptable host CUDA versions as major.minor (v2). Changing only this does not require resending gpuPoolIds. An explicit [] clears the constraint. Mutually exclusive with minCudaVersion."
|
|
2782
|
-
),
|
|
2783
|
-
minCudaVersion: import_zod8.z.string().optional().describe(
|
|
2784
|
-
'New lowest acceptable host CUDA version as major.minor (v2). An empty string clears the floor; a bare major like "12" is rejected. Mutually exclusive with a non-empty allowedCudaVersions.'
|
|
2785
|
-
),
|
|
2786
|
-
args: import_zod8.z.string().optional().describe("New container args (v2)"),
|
|
2787
|
-
containerDiskInGb: import_zod8.z.number().optional().describe("New container disk size in GB (v2)"),
|
|
2788
|
-
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("New ports (v2)"),
|
|
2789
|
-
env: import_zod8.z.record(import_zod8.z.string()).optional().describe("New environment variables (v2)"),
|
|
2790
|
-
containerRegistryAuthId: import_zod8.z.string().optional().describe("New container registry auth id (v2)"),
|
|
2791
|
-
networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("New network volume ids (v2)"),
|
|
2792
|
-
executionTimeoutMs: import_zod8.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
|
|
2793
|
-
flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
|
|
2794
|
-
},
|
|
2795
|
-
{ title: "Update endpoint", ...WRITE, idempotentHint: true },
|
|
2796
|
-
async (params) => {
|
|
2797
|
-
const { endpointId, ...updateParams } = params;
|
|
2798
|
-
const backend = backendFor("endpoints");
|
|
2799
|
-
const url = `${backend.base}${backend.get(endpointId)}`;
|
|
2800
|
-
const hasCudaParams = updateParams.allowedCudaVersions !== void 0 || updateParams.minCudaVersion !== void 0;
|
|
2801
|
-
if (backend.version !== "v2") {
|
|
2802
|
-
if (hasCudaParams) {
|
|
2803
|
-
return jsonReply2({
|
|
2804
|
-
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2805
|
-
status: 501
|
|
2806
|
-
});
|
|
2807
|
-
}
|
|
2808
|
-
const body2 = backend.mapUpdate(updateParams);
|
|
2809
|
-
return jsonReply2(await callRestUrl(url, "PATCH", body2));
|
|
2810
|
-
}
|
|
2811
|
-
if (hasCudaParams) {
|
|
2812
|
-
const cudaError = cudaConstraintError(updateParams, {
|
|
2813
|
-
allowClear: true
|
|
2814
|
-
});
|
|
2815
|
-
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2816
|
-
}
|
|
2817
|
-
if (updateParams.gpuPoolIds !== void 0 && !updateParams.gpuPoolIds.length) {
|
|
2818
|
-
return jsonReply2({
|
|
2819
|
-
error: "gpuPoolIds cannot be empty \u2014 omit it to keep the current GPU selection.",
|
|
2820
|
-
status: 400
|
|
2821
|
-
});
|
|
2822
|
-
}
|
|
2823
|
-
let scalerType = updateParams.scalerType;
|
|
2824
|
-
const needScalerRead = scalerType === void 0 && updateParams.scalerValue !== void 0;
|
|
2825
|
-
const needExclusionRead = (updateParams.gpuPoolIds?.length ?? 0) > 0;
|
|
2826
|
-
let clearedExclusions = [];
|
|
2827
|
-
if (needScalerRead || needExclusionRead) {
|
|
2828
|
-
const current = await callRestUrl(url);
|
|
2829
|
-
if (needScalerRead) {
|
|
2830
|
-
scalerType = isRequestCountScaler(current?.scaling?.type) ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
2831
|
-
}
|
|
2832
|
-
if (needExclusionRead && current?.gpu?.excludedTypes?.length) {
|
|
2833
|
-
clearedExclusions = current.gpu.excludedTypes;
|
|
2834
|
-
}
|
|
2835
|
-
}
|
|
2836
|
-
const scalerError = scalerValueError(
|
|
2837
|
-
scalerType,
|
|
2838
|
-
updateParams.scalerValue
|
|
2839
|
-
);
|
|
2840
|
-
if (scalerError) {
|
|
2841
|
-
return jsonReply2({ error: scalerError, status: 400 });
|
|
2842
|
-
}
|
|
2843
|
-
const body = backend.mapUpdate({
|
|
2844
|
-
...updateParams,
|
|
2845
|
-
scalerType
|
|
2846
|
-
});
|
|
2847
|
-
const result = await callRestUrl(url, "PATCH", body);
|
|
2848
|
-
if (clearedExclusions.length) {
|
|
2849
|
-
return jsonReply2({
|
|
2850
|
-
...result,
|
|
2851
|
-
_warning: `Supplying gpuPoolIds replaced the GPU selection wholesale, clearing ${clearedExclusions.length} GPU-type exclusion(s) previously set on this endpoint: ${clearedExclusions.join(", ")}. Re-apply them with set-endpoint-gpus if still wanted.`
|
|
2852
|
-
});
|
|
2853
|
-
}
|
|
2854
|
-
return jsonReply2(result);
|
|
2855
|
-
}
|
|
2856
|
-
);
|
|
2857
|
-
server.tool(
|
|
2858
|
-
"delete-endpoint",
|
|
2859
|
-
"Permanently delete a Serverless endpoint. This cannot be undone.",
|
|
2860
|
-
{
|
|
2861
|
-
endpointId: import_zod8.z.string().describe("ID of the endpoint to delete")
|
|
2862
|
-
},
|
|
2863
|
-
{ title: "Delete endpoint", ...DESTRUCTIVE },
|
|
2864
|
-
async (params) => {
|
|
2865
|
-
const backend = backendFor("endpoints");
|
|
2866
|
-
const result = await callRestUrl(
|
|
2867
|
-
`${backend.base}${backend.get(params.endpointId)}`,
|
|
2868
|
-
"DELETE"
|
|
2869
|
-
);
|
|
2870
|
-
return jsonReply2(result);
|
|
2871
|
-
}
|
|
2872
|
-
);
|
|
2873
|
-
server.tool(
|
|
2874
|
-
"list-endpoint-releases",
|
|
2875
|
-
"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.",
|
|
2876
|
-
{
|
|
2877
|
-
...listPaginationParams,
|
|
2878
|
-
endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose releases to list")
|
|
2879
|
-
},
|
|
2880
|
-
{ title: "List endpoint releases", ...READ_ONLY },
|
|
2881
|
-
async (params) => {
|
|
2882
|
-
const backend = backendFor("endpoints");
|
|
2883
|
-
if (backend.version === "v1") {
|
|
2884
|
-
return jsonReply2({
|
|
2885
|
-
error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2886
|
-
status: 501
|
|
2887
|
-
});
|
|
2888
|
-
}
|
|
2889
|
-
const raw = await callRestUrl(
|
|
2890
|
-
`${backend.base}/serverless/${params.endpointId}/releases`
|
|
2891
|
-
);
|
|
2892
|
-
const releases = Array.isArray(raw?.releases) ? raw.releases : [];
|
|
2893
|
-
return capListResult(
|
|
2894
|
-
releases,
|
|
2895
|
-
{ limit: params.limit, cursor: params.cursor },
|
|
2896
|
-
{ rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
|
|
2897
|
-
);
|
|
2898
|
-
}
|
|
2899
|
-
);
|
|
2900
|
-
server.tool(
|
|
2901
|
-
"list-endpoint-workers",
|
|
2902
|
-
"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.",
|
|
2903
|
-
{
|
|
2904
|
-
...listPaginationParams,
|
|
2905
|
-
endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose workers to list")
|
|
2906
|
-
},
|
|
2907
|
-
{ title: "List endpoint workers", ...READ_ONLY },
|
|
2908
|
-
async (params) => {
|
|
2909
|
-
const backend = backendFor("workers");
|
|
2910
|
-
if (backend.version === "v1") {
|
|
2911
|
-
return jsonReply2({
|
|
2912
|
-
error: "list-endpoint-workers is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (on v1, use get-endpoint with includeWorkers).",
|
|
2913
|
-
status: 501
|
|
2914
|
-
});
|
|
2915
|
-
}
|
|
2916
|
-
const raw = await callRestUrl(
|
|
2917
|
-
`${backend.base}/serverless/${params.endpointId}/workers`
|
|
2918
|
-
);
|
|
2919
|
-
return capListResult(
|
|
2920
|
-
backend.unwrap(raw),
|
|
2921
|
-
{ limit: params.limit, cursor: params.cursor },
|
|
2922
|
-
{ summary: raw?.summary, endpointVersion: raw?.endpointVersion }
|
|
2923
|
-
);
|
|
2924
|
-
}
|
|
2925
|
-
);
|
|
2926
|
-
server.tool(
|
|
2927
|
-
"stream-worker-logs",
|
|
2928
|
-
"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).",
|
|
2929
|
-
{
|
|
2930
|
-
endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint the worker belongs to"),
|
|
2931
|
-
workerId: import_zod8.z.string().describe(
|
|
2932
|
-
"ID of the worker whose logs to stream (from list-endpoint-workers)"
|
|
2933
|
-
),
|
|
2934
|
-
...logStreamParams
|
|
2935
|
-
},
|
|
2936
|
-
{ title: "Stream worker logs", ...READ_ONLY },
|
|
2937
|
-
(params) => streamLogsReply(
|
|
2938
|
-
rt,
|
|
2939
|
-
{
|
|
2940
|
-
name: "stream-worker-logs",
|
|
2941
|
-
resource: "workers",
|
|
2942
|
-
logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
|
|
2943
|
-
},
|
|
2944
|
-
params
|
|
2945
|
-
)
|
|
2946
|
-
);
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
// src/tools/jobs.ts
|
|
2950
|
-
var import_zod9 = require("zod");
|
|
2951
|
-
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2952
|
-
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2953
|
-
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
2954
|
-
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
2955
|
-
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
2956
|
-
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
2957
|
-
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
2958
|
-
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
2959
|
-
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
2960
|
-
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
2961
|
-
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
2962
|
-
"COMPLETED",
|
|
2963
|
-
"FAILED",
|
|
2964
|
-
"CANCELLED",
|
|
2965
|
-
"TIMED_OUT"
|
|
2966
|
-
]);
|
|
2967
|
-
function isUnfinishedJobReply(result) {
|
|
2968
|
-
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
2969
|
-
}
|
|
2970
|
-
function formatBudget(ms) {
|
|
2971
|
-
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
2972
|
-
}
|
|
2973
|
-
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
2974
|
-
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
2975
|
-
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
2976
|
-
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
2977
|
-
return Math.max(
|
|
2978
|
-
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
2979
|
-
MIN_STREAM_POLL_TIMEOUT_MS
|
|
2980
|
-
);
|
|
2981
|
-
}
|
|
2982
|
-
function sleep(ms) {
|
|
2983
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2984
|
-
}
|
|
2985
|
-
function streamPollPlan(hosted) {
|
|
2986
|
-
return hosted ? {
|
|
2987
|
-
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
2988
|
-
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
2989
|
-
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
2990
|
-
} : {
|
|
2991
|
-
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
2992
|
-
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
2993
|
-
query: ""
|
|
2994
|
-
};
|
|
2995
|
-
}
|
|
2996
|
-
var RESUME_ADVICE = "Call stream-job again to continue collecting output, get-job-status to check the job without streaming, or stream without a budget by calling the runtime API directly (GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} with a Bearer API key).";
|
|
2997
|
-
function budgetExhaustedNote(budgetMs, lastError) {
|
|
2998
|
-
return {
|
|
2999
|
-
pollingTimedOut: true,
|
|
3000
|
-
note: `Polling stopped after ${formatBudget(
|
|
3001
|
-
budgetMs
|
|
3002
|
-
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
3003
|
-
// Surface the trailing error (if any) instead of discarding it — the last
|
|
3004
|
-
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
3005
|
-
// succeeded. Cleared on success, so this is never a stale error from
|
|
3006
|
-
// minutes of healthy streaming ago.
|
|
3007
|
-
...lastError ? { lastError } : {}
|
|
3008
|
-
};
|
|
3009
|
-
}
|
|
3010
|
-
function errorCapNote(lastError) {
|
|
3011
|
-
return {
|
|
3012
|
-
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
3013
|
-
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
3014
|
-
};
|
|
3015
|
-
}
|
|
3016
|
-
async function collectJobStream(deps) {
|
|
3017
|
-
const { poll, budgetMs, holdMs } = deps;
|
|
3018
|
-
const startedAt = Date.now();
|
|
3019
|
-
const elapsed = () => Date.now() - startedAt;
|
|
3020
|
-
const chunks = [];
|
|
3021
|
-
let result = {};
|
|
3022
|
-
let consecutiveErrors = 0;
|
|
3023
|
-
let lastError;
|
|
3024
|
-
while (true) {
|
|
3025
|
-
try {
|
|
3026
|
-
const reply = await poll(
|
|
3027
|
-
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
3028
|
-
);
|
|
3029
|
-
consecutiveErrors = 0;
|
|
3030
|
-
lastError = void 0;
|
|
3031
|
-
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
3032
|
-
result = reply;
|
|
3033
|
-
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
3034
|
-
} catch (error) {
|
|
3035
|
-
consecutiveErrors++;
|
|
3036
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
3037
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
3038
|
-
result = { ...result, ...errorCapNote(lastError) };
|
|
3039
|
-
break;
|
|
3040
|
-
}
|
|
3041
|
-
}
|
|
3042
|
-
if (elapsed() > budgetMs) {
|
|
3043
|
-
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
3044
|
-
break;
|
|
3045
|
-
}
|
|
3046
|
-
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
3047
|
-
}
|
|
3048
|
-
return { result, chunks };
|
|
3049
|
-
}
|
|
3050
|
-
function registerJobTools(server, rt) {
|
|
3051
|
-
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
3052
|
-
const hosted = rt.transport === "http";
|
|
3053
|
-
const streamBudget = formatBudget(
|
|
3054
|
-
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
3055
|
-
);
|
|
3056
|
-
async function diagnoseQueuedJob(endpointId) {
|
|
3057
|
-
const cached = diagnosisCache.get(endpointId);
|
|
3058
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
3059
|
-
return cached.value;
|
|
3060
|
-
}
|
|
3061
|
-
diagnosisCache.delete(endpointId);
|
|
3062
|
-
try {
|
|
3063
|
-
const backend = backendFor("workers");
|
|
3064
|
-
if (backend.version !== "v2") return null;
|
|
3065
|
-
const raw = await callRestUrl(
|
|
3066
|
-
`${backend.base}/serverless/${endpointId}/workers`,
|
|
3067
|
-
"GET",
|
|
3068
|
-
void 0,
|
|
3069
|
-
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
3070
|
-
);
|
|
3071
|
-
const summary = raw?.summary;
|
|
3072
|
-
const workers = raw?.workers ?? [];
|
|
3073
|
-
if (!summary) return null;
|
|
3074
|
-
const remember = (value) => {
|
|
3075
|
-
if (diagnosisCache.size >= MAX_DIAGNOSIS_ENTRIES) {
|
|
3076
|
-
const oldest = diagnosisCache.keys().next().value;
|
|
3077
|
-
if (oldest !== void 0) diagnosisCache.delete(oldest);
|
|
3078
|
-
}
|
|
3079
|
-
diagnosisCache.set(endpointId, {
|
|
3080
|
-
value,
|
|
3081
|
-
expiresAt: Date.now() + DIAGNOSIS_TTL_MS
|
|
3082
|
-
});
|
|
3083
|
-
return value;
|
|
3084
|
-
};
|
|
3085
|
-
if ((summary.unhealthy ?? 0) > 0) {
|
|
3086
|
-
const badIds = workers.filter((w) => w.status === "UNHEALTHY").map((w) => w.id).join(", ");
|
|
3087
|
-
return remember({
|
|
3088
|
-
workerHealth: summary,
|
|
3089
|
-
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})` : ""}.`
|
|
3090
|
-
});
|
|
3091
|
-
}
|
|
3092
|
-
if ((summary.total ?? 0) === 0) {
|
|
3093
|
-
return remember({
|
|
3094
|
-
workerHealth: summary,
|
|
3095
|
-
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."
|
|
3096
|
-
});
|
|
3097
|
-
}
|
|
3098
|
-
if ((summary.throttled ?? 0) > 0 && (summary.running ?? 0) === 0) {
|
|
3099
|
-
return remember({
|
|
3100
|
-
workerHealth: summary,
|
|
3101
|
-
hint: "Workers exist but are throttled \u2014 the hosts are at capacity right now; the job should start when capacity frees up."
|
|
3102
|
-
});
|
|
3103
|
-
}
|
|
3104
|
-
if ((summary.initializing ?? 0) > 0) {
|
|
3105
|
-
return remember({
|
|
3106
|
-
workerHealth: summary,
|
|
3107
|
-
hint: "A worker is initializing \u2014 likely a cold start (image pull / model load); the job should start soon."
|
|
3108
|
-
});
|
|
3109
|
-
}
|
|
3110
|
-
return remember({ workerHealth: summary, hint: "" });
|
|
3111
|
-
} catch {
|
|
3112
|
-
return null;
|
|
3113
|
-
}
|
|
3114
|
-
}
|
|
3115
|
-
const endpointIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
|
|
3116
|
-
const jobIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
|
|
3117
|
-
const inputSchema = import_zod9.z.record(import_zod9.z.unknown()).describe(
|
|
3118
|
-
"Input payload for the worker handler. The expected fields depend on the deployed model or worker."
|
|
3119
|
-
);
|
|
3120
|
-
const webhookSchema = import_zod9.z.string().url().optional().describe(
|
|
3121
|
-
"Webhook URL to receive job completion notifications instead of polling"
|
|
3122
|
-
);
|
|
3123
|
-
const policySchema = import_zod9.z.object({
|
|
3124
|
-
executionTimeout: import_zod9.z.number().optional().describe("Maximum execution time in milliseconds"),
|
|
3125
|
-
lowPriority: import_zod9.z.boolean().optional().describe("Submit as a low-priority job"),
|
|
3126
|
-
ttl: import_zod9.z.number().optional().describe("Time-to-live for the job result in milliseconds")
|
|
3127
|
-
}).optional().describe("Execution policy options");
|
|
3128
|
-
const s3ConfigSchema = import_zod9.z.object({
|
|
3129
|
-
accessId: import_zod9.z.string().describe("S3 access key ID"),
|
|
3130
|
-
accessSecret: import_zod9.z.string().describe("S3 secret access key"),
|
|
3131
|
-
bucketName: import_zod9.z.string().describe("S3 bucket name"),
|
|
3132
|
-
endpointUrl: import_zod9.z.string().describe("S3 endpoint URL")
|
|
3133
|
-
}).optional().describe("S3-compatible storage config for large outputs");
|
|
3134
|
-
server.tool(
|
|
3135
|
-
"run-endpoint",
|
|
3136
|
-
"Submit an asynchronous job to a Serverless endpoint. Returns a job ID immediately \u2014 use get-job-status to poll for results. Async results are available for 30 minutes after completion.",
|
|
3137
|
-
{
|
|
3138
|
-
endpointId: endpointIdSchema.describe(
|
|
3139
|
-
"ID of the Serverless endpoint to run"
|
|
3140
|
-
),
|
|
3141
|
-
input: inputSchema,
|
|
3142
|
-
webhook: webhookSchema,
|
|
3143
|
-
policy: policySchema,
|
|
3144
|
-
s3Config: s3ConfigSchema
|
|
3145
|
-
},
|
|
3146
|
-
{ title: "Run endpoint (async)", ...WRITE },
|
|
3147
|
-
async (params) => {
|
|
3148
|
-
const { endpointId, ...body } = params;
|
|
3149
|
-
const result = await serverlessRequest(
|
|
3150
|
-
endpointId,
|
|
3151
|
-
"/run",
|
|
3152
|
-
"POST",
|
|
3153
|
-
body
|
|
3154
|
-
);
|
|
3155
|
-
return jsonReply2(result);
|
|
3156
|
-
}
|
|
3157
|
-
);
|
|
3158
|
-
server.tool(
|
|
3159
|
-
"runsync-endpoint",
|
|
3160
|
-
`Submit a synchronous job to a Serverless endpoint and wait for the result. Best for fast tasks: if the job outlives the wait, the response returns a job ID and a non-terminal status (IN_QUEUE or IN_PROGRESS) to poll with get-job-status. Max payload 20 MB; results expire after 1 minute. ${hosted ? `This server waits up to ${HTTP_LONG_POLL_BUDGET_MS} ms \u2014 it runs behind a 60-second gateway deadline, so that is both the default and the ceiling for the wait parameter. For longer jobs use run-endpoint + get-job-status, or POST https://api.runpod.ai/v2/{endpointId}/runsync?wait=300000 directly with a Bearer API key.` : "The wait parameter extends the server-side wait to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."}`,
|
|
3161
|
-
{
|
|
3162
|
-
endpointId: endpointIdSchema.describe(
|
|
3163
|
-
"ID of the Serverless endpoint to run synchronously"
|
|
3164
|
-
),
|
|
3165
|
-
input: inputSchema,
|
|
3166
|
-
wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
|
|
3167
|
-
hosted ? `How long in milliseconds the server should wait for a result before returning a job ID to poll. Accepted range 1000\u2013300000, but this server clamps anything above ${HTTP_LONG_POLL_BUDGET_MS} down to ${HTTP_LONG_POLL_BUDGET_MS}, which is also the effective default.` : "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)."
|
|
3168
|
-
),
|
|
3169
|
-
webhook: webhookSchema,
|
|
3170
|
-
policy: policySchema,
|
|
3171
|
-
s3Config: s3ConfigSchema
|
|
3172
|
-
},
|
|
3173
|
-
{ title: "Run endpoint (sync)", ...WRITE },
|
|
3174
|
-
async (params) => {
|
|
3175
|
-
const { endpointId, wait, ...body } = params;
|
|
3176
|
-
const effectiveWait = hosted ? Math.min(
|
|
3177
|
-
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3178
|
-
HTTP_LONG_POLL_BUDGET_MS
|
|
3179
|
-
) : wait;
|
|
3180
|
-
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
3181
|
-
const path = `/runsync${waitQuery}`;
|
|
3182
|
-
const result = await serverlessRequest(
|
|
3183
|
-
endpointId,
|
|
3184
|
-
path,
|
|
3185
|
-
"POST",
|
|
3186
|
-
body,
|
|
3187
|
-
{
|
|
3188
|
-
// From the wait actually sent — on http the clamped 45s, not the
|
|
3189
|
-
// 300s the caller may have asked for.
|
|
3190
|
-
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
3191
|
-
}
|
|
3192
|
-
);
|
|
3193
|
-
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
3194
|
-
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
3195
|
-
return jsonReply2({
|
|
3196
|
-
...result,
|
|
3197
|
-
waitClamped: {
|
|
3198
|
-
requestedMs: requestedWait,
|
|
3199
|
-
effectiveMs: effectiveWait,
|
|
3200
|
-
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
3201
|
-
HTTP_LONG_POLL_BUDGET_MS
|
|
3202
|
-
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
3203
|
-
}
|
|
3204
|
-
});
|
|
3205
|
-
}
|
|
3206
|
-
return jsonReply2(result);
|
|
3207
|
-
}
|
|
3208
|
-
);
|
|
3209
|
-
server.tool(
|
|
3210
|
-
"get-job-status",
|
|
3211
|
-
"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.",
|
|
3212
|
-
{
|
|
3213
|
-
endpointId: endpointIdSchema.describe(
|
|
3214
|
-
"ID of the Serverless endpoint the job belongs to"
|
|
3215
|
-
),
|
|
3216
|
-
jobId: jobIdSchema.describe("ID of the job to check")
|
|
3217
|
-
},
|
|
3218
|
-
{ title: "Get job status", ...READ_ONLY },
|
|
3219
|
-
async (params) => {
|
|
3220
|
-
const result = await serverlessRequest(
|
|
3221
|
-
params.endpointId,
|
|
3222
|
-
`/status/${params.jobId}`
|
|
3223
|
-
);
|
|
3224
|
-
if (result && typeof result === "object" && result.status === "IN_QUEUE") {
|
|
3225
|
-
const diagnosis = await diagnoseQueuedJob(params.endpointId);
|
|
3226
|
-
if (diagnosis) {
|
|
3227
|
-
return jsonReply2({
|
|
3228
|
-
...result,
|
|
3229
|
-
workerHealth: diagnosis.workerHealth,
|
|
3230
|
-
...diagnosis.hint ? { hint: diagnosis.hint } : {}
|
|
3231
|
-
});
|
|
3232
|
-
}
|
|
3233
|
-
}
|
|
3234
|
-
return jsonReply2(result);
|
|
3235
|
-
}
|
|
3236
|
-
);
|
|
3237
|
-
server.tool(
|
|
3238
|
-
"stream-job",
|
|
3239
|
-
`Retrieve streaming output from a Serverless job. The worker must support streaming output. Polls /stream/{jobId} and collects chunks until the status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT, for up to ${streamBudget}${hosted ? " (this server runs behind a 60-second gateway deadline)" : ""}. If the budget expires first, returns the chunks collected so far with pollingTimedOut: true \u2014 call stream-job again to resume where it left off, or get-job-status to check without streaming.${hosted ? " To collect with no budget, poll GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} yourself with a Bearer API key until the status is terminal \u2014 each request drains only the chunks buffered so far, so a single request is not the whole output." : ""}`,
|
|
3240
|
-
{
|
|
3241
|
-
endpointId: endpointIdSchema.describe(
|
|
3242
|
-
"ID of the Serverless endpoint the job belongs to"
|
|
3243
|
-
),
|
|
3244
|
-
jobId: jobIdSchema.describe("ID of the job to stream results from")
|
|
3245
|
-
},
|
|
3246
|
-
{ title: "Stream job", ...READ_ONLY },
|
|
3247
|
-
async (params) => {
|
|
3248
|
-
const plan = streamPollPlan(hosted);
|
|
3249
|
-
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
3250
|
-
const { result, chunks } = await collectJobStream({
|
|
3251
|
-
budgetMs: plan.budgetMs,
|
|
3252
|
-
holdMs: plan.holdMs,
|
|
3253
|
-
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
3254
|
-
timeoutMs
|
|
3255
|
-
})
|
|
3256
|
-
});
|
|
3257
|
-
return jsonReply2({ ...result, stream: chunks });
|
|
3258
|
-
}
|
|
3259
|
-
);
|
|
3260
|
-
server.tool(
|
|
3261
|
-
"cancel-job",
|
|
3262
|
-
"Cancel a Serverless job that is queued or in progress.",
|
|
3263
|
-
{
|
|
3264
|
-
endpointId: endpointIdSchema.describe(
|
|
3265
|
-
"ID of the Serverless endpoint the job belongs to"
|
|
3266
|
-
),
|
|
3267
|
-
jobId: jobIdSchema.describe("ID of the job to cancel")
|
|
3268
|
-
},
|
|
3269
|
-
{ title: "Cancel job", ...WRITE, idempotentHint: true },
|
|
3270
|
-
async (params) => {
|
|
3271
|
-
const result = await serverlessRequest(
|
|
3272
|
-
params.endpointId,
|
|
3273
|
-
`/cancel/${params.jobId}`,
|
|
3274
|
-
"POST"
|
|
3275
|
-
);
|
|
3276
|
-
return jsonReply2(result);
|
|
3277
|
-
}
|
|
3278
|
-
);
|
|
3279
|
-
server.tool(
|
|
3280
|
-
"retry-job",
|
|
3281
|
-
"Retry a failed or timed-out Serverless job. Only works for jobs with FAILED or TIMED_OUT status. The previous output is removed and the job is requeued.",
|
|
3282
|
-
{
|
|
3283
|
-
endpointId: endpointIdSchema.describe(
|
|
3284
|
-
"ID of the Serverless endpoint the job belongs to"
|
|
3285
|
-
),
|
|
3286
|
-
jobId: jobIdSchema.describe("ID of the job to retry")
|
|
3287
|
-
},
|
|
3288
|
-
{ title: "Retry job", ...WRITE },
|
|
3289
|
-
async (params) => {
|
|
3290
|
-
const result = await serverlessRequest(
|
|
3291
|
-
params.endpointId,
|
|
3292
|
-
`/retry/${params.jobId}`,
|
|
3293
|
-
"POST"
|
|
3294
|
-
);
|
|
3295
|
-
return jsonReply2(result);
|
|
3296
|
-
}
|
|
3297
|
-
);
|
|
3298
|
-
server.tool(
|
|
3299
|
-
"endpoint-health",
|
|
3300
|
-
"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.",
|
|
3301
|
-
{
|
|
3302
|
-
endpointId: endpointIdSchema.describe(
|
|
3303
|
-
"ID of the Serverless endpoint to check health for"
|
|
3304
|
-
)
|
|
3305
|
-
},
|
|
3306
|
-
{ title: "Endpoint health", ...READ_ONLY },
|
|
3307
|
-
async (params) => {
|
|
3308
|
-
const result = await serverlessRequest(params.endpointId, "/health");
|
|
3309
|
-
return jsonReply2(result);
|
|
3310
|
-
}
|
|
3311
|
-
);
|
|
3312
|
-
server.tool(
|
|
3313
|
-
"purge-endpoint-queue",
|
|
3314
|
-
"Remove all pending jobs from a Serverless endpoint queue. Only affects queued jobs \u2014 in-progress jobs continue running. Use this for error recovery or clearing outdated requests.",
|
|
3315
|
-
{
|
|
3316
|
-
endpointId: endpointIdSchema.describe(
|
|
3317
|
-
"ID of the Serverless endpoint to purge the queue for"
|
|
3318
|
-
)
|
|
3319
|
-
},
|
|
3320
|
-
{ title: "Purge endpoint queue", ...WRITE, idempotentHint: true },
|
|
3321
|
-
async (params) => {
|
|
3322
|
-
const result = await serverlessRequest(
|
|
3323
|
-
params.endpointId,
|
|
3324
|
-
"/purge-queue",
|
|
3325
|
-
"POST"
|
|
3326
|
-
);
|
|
3327
|
-
return jsonReply2(result);
|
|
3328
|
-
}
|
|
3329
|
-
);
|
|
3330
|
-
}
|
|
3331
|
-
|
|
3332
|
-
// src/tools/templates.ts
|
|
3333
|
-
var import_zod10 = require("zod");
|
|
3334
|
-
function registerTemplateTools(server, rt) {
|
|
3335
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
3336
|
-
server.tool(
|
|
3337
|
-
"list-templates",
|
|
3338
|
-
`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.`,
|
|
3339
|
-
{
|
|
3340
|
-
...listPaginationParams,
|
|
3341
|
-
includeRunpodTemplates: import_zod10.z.boolean().optional().describe("Include official Runpod templates in the response"),
|
|
3342
|
-
includePublicTemplates: import_zod10.z.boolean().optional().describe("Include community-made public templates in the response"),
|
|
3343
|
-
includeEndpointBoundTemplates: import_zod10.z.boolean().optional().describe(
|
|
3344
|
-
"Include templates bound to Serverless endpoints in the response"
|
|
3345
|
-
)
|
|
3346
|
-
},
|
|
3347
|
-
{ title: "List templates", ...READ_ONLY },
|
|
3348
|
-
async (params) => {
|
|
3349
|
-
const backend = backendFor("templates");
|
|
3350
|
-
let query = "";
|
|
3351
|
-
if (backend.version === "v1") {
|
|
3352
|
-
const queryParams = new URLSearchParams();
|
|
3353
|
-
if (params.includeRunpodTemplates)
|
|
3354
|
-
queryParams.set("includeRunpodTemplates", "true");
|
|
3355
|
-
if (params.includePublicTemplates)
|
|
3356
|
-
queryParams.set("includePublicTemplates", "true");
|
|
3357
|
-
if (params.includeEndpointBoundTemplates)
|
|
3358
|
-
queryParams.set("includeEndpointBoundTemplates", "true");
|
|
3359
|
-
query = queryParams.toString() ? `?${queryParams.toString()}` : "";
|
|
3360
|
-
}
|
|
3361
|
-
const result = await callRestUrl(
|
|
3362
|
-
`${backend.base}${backend.list}${query}`
|
|
3363
|
-
);
|
|
3364
|
-
return capListResult(backend.unwrap(result), {
|
|
3365
|
-
limit: params.limit,
|
|
3366
|
-
cursor: params.cursor
|
|
3367
|
-
});
|
|
3368
|
-
}
|
|
3369
|
-
);
|
|
3370
|
-
server.tool(
|
|
3371
|
-
"get-template",
|
|
3372
|
-
"Get one template by id (image, env, ports, disk/volume config).",
|
|
3373
|
-
{
|
|
3374
|
-
templateId: import_zod10.z.string().describe("ID of the template to retrieve")
|
|
3375
|
-
},
|
|
3376
|
-
{ title: "Get template", ...READ_ONLY },
|
|
3377
|
-
async (params) => {
|
|
3378
|
-
const backend = backendFor("templates");
|
|
3379
|
-
const result = await callRestUrl(
|
|
3380
|
-
`${backend.base}${backend.get(params.templateId)}`
|
|
3381
|
-
);
|
|
3382
|
-
return jsonReply2(result);
|
|
3383
|
-
}
|
|
3384
|
-
);
|
|
3385
|
-
server.tool(
|
|
3386
|
-
"create-template",
|
|
3387
|
-
"Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
|
|
3388
|
-
{
|
|
3389
|
-
name: import_zod10.z.string().describe("Name for the template"),
|
|
3390
|
-
imageName: import_zod10.z.string().describe("Docker image to use"),
|
|
3391
|
-
isServerless: import_zod10.z.boolean().optional().describe("Is this a serverless template"),
|
|
3392
|
-
ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("Ports to expose"),
|
|
3393
|
-
dockerEntrypoint: import_zod10.z.array(import_zod10.z.string()).optional().describe(
|
|
3394
|
-
"Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
|
|
3395
|
-
),
|
|
3396
|
-
dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
|
|
3397
|
-
'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 "").'
|
|
3398
|
-
),
|
|
3399
|
-
env: import_zod10.z.record(import_zod10.z.string()).optional().describe("Environment variables"),
|
|
3400
|
-
containerDiskInGb: import_zod10.z.number().optional().describe("Container disk size in GB"),
|
|
3401
|
-
volumeInGb: import_zod10.z.number().optional().describe("Volume size in GB"),
|
|
3402
|
-
volumeMountPath: import_zod10.z.string().optional().describe("Path to mount the volume"),
|
|
3403
|
-
readme: import_zod10.z.string().optional().describe("README content in markdown format"),
|
|
3404
|
-
category: import_zod10.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
|
|
3405
|
-
"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."
|
|
3406
|
-
),
|
|
3407
|
-
containerRegistryAuthId: import_zod10.z.string().optional().describe(
|
|
3408
|
-
"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."
|
|
3409
|
-
)
|
|
3410
|
-
},
|
|
3411
|
-
{ title: "Create template", ...WRITE },
|
|
3412
|
-
async (params) => {
|
|
3413
|
-
const backend = backendFor("templates");
|
|
3414
|
-
const body = backend.mapCreate(params);
|
|
3415
|
-
const result = await callRestUrl(
|
|
3416
|
-
`${backend.base}${backend.list}`,
|
|
3417
|
-
"POST",
|
|
3418
|
-
body
|
|
3419
|
-
);
|
|
3420
|
-
return jsonReply2(result);
|
|
3421
|
-
}
|
|
3422
|
-
);
|
|
3423
|
-
server.tool(
|
|
3424
|
-
"update-template",
|
|
3425
|
-
"Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
|
|
3426
|
-
{
|
|
3427
|
-
templateId: import_zod10.z.string().describe("ID of the template to update"),
|
|
3428
|
-
name: import_zod10.z.string().optional().describe("New name for the template"),
|
|
3429
|
-
imageName: import_zod10.z.string().optional().describe("New Docker image"),
|
|
3430
|
-
dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
|
|
3431
|
-
"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."
|
|
3432
|
-
),
|
|
3433
|
-
ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("New ports to expose"),
|
|
3434
|
-
env: import_zod10.z.record(import_zod10.z.string()).optional().describe("New environment variables"),
|
|
3435
|
-
readme: import_zod10.z.string().optional().describe("New README content in markdown format"),
|
|
3436
|
-
containerRegistryAuthId: import_zod10.z.string().optional().describe(
|
|
3437
|
-
"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."
|
|
3438
|
-
)
|
|
3439
|
-
},
|
|
3440
|
-
{ title: "Update template", ...WRITE, idempotentHint: true },
|
|
3441
|
-
async (params) => {
|
|
3442
|
-
const { templateId, ...updateParams } = params;
|
|
3443
|
-
const backend = backendFor("templates");
|
|
3444
|
-
const body = backend.mapUpdate(updateParams);
|
|
3445
|
-
const result = await callRestUrl(
|
|
3446
|
-
`${backend.base}${backend.get(templateId)}`,
|
|
3447
|
-
"PATCH",
|
|
3448
|
-
body
|
|
3449
|
-
);
|
|
3450
|
-
return jsonReply2(result);
|
|
3451
|
-
}
|
|
3452
|
-
);
|
|
3453
|
-
server.tool(
|
|
3454
|
-
"delete-template",
|
|
3455
|
-
"Permanently delete a template. This cannot be undone.",
|
|
3456
|
-
{
|
|
3457
|
-
templateId: import_zod10.z.string().describe("ID of the template to delete")
|
|
3458
|
-
},
|
|
3459
|
-
{ title: "Delete template", ...DESTRUCTIVE },
|
|
3460
|
-
async (params) => {
|
|
3461
|
-
const backend = backendFor("templates");
|
|
3462
|
-
const result = await callRestUrl(
|
|
3463
|
-
`${backend.base}${backend.get(params.templateId)}`,
|
|
3464
|
-
"DELETE"
|
|
3465
|
-
);
|
|
3466
|
-
return jsonReply2(result);
|
|
3467
|
-
}
|
|
3468
|
-
);
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
|
-
// src/tools/network-volumes.ts
|
|
3472
|
-
var import_zod11 = require("zod");
|
|
3473
|
-
function registerNetworkVolumeTools(server, rt) {
|
|
3474
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
3475
|
-
server.tool(
|
|
3476
|
-
"list-network-volumes",
|
|
3477
|
-
"List your network volumes (persistent storage attachable to pods). Paginated via limit/cursor.",
|
|
3478
|
-
listPaginationParams,
|
|
3479
|
-
{ title: "List network volumes", ...READ_ONLY },
|
|
3480
|
-
async (params) => {
|
|
3481
|
-
const backend = backendFor("networkVolumes");
|
|
3482
|
-
const result = await callRestUrl(`${backend.base}${backend.list}`);
|
|
3483
|
-
return capListResult(backend.unwrap(result), {
|
|
3484
|
-
limit: params.limit,
|
|
3485
|
-
cursor: params.cursor
|
|
3486
|
-
});
|
|
3487
|
-
}
|
|
3488
|
-
);
|
|
3489
|
-
server.tool(
|
|
3490
|
-
"get-network-volume",
|
|
3491
|
-
"Get one network volume by id (name, size, data center).",
|
|
3492
|
-
{
|
|
3493
|
-
networkVolumeId: import_zod11.z.string().describe("ID of the network volume to retrieve")
|
|
3494
|
-
},
|
|
3495
|
-
{ title: "Get network volume", ...READ_ONLY },
|
|
3496
|
-
async (params) => {
|
|
3497
|
-
const backend = backendFor("networkVolumes");
|
|
3498
|
-
const result = await callRestUrl(
|
|
3499
|
-
`${backend.base}${backend.get(params.networkVolumeId)}`
|
|
3500
|
-
);
|
|
3501
|
-
return jsonReply2(result);
|
|
3502
|
-
}
|
|
3503
|
-
);
|
|
3504
|
-
server.tool(
|
|
3505
|
-
"create-network-volume",
|
|
3506
|
-
"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.",
|
|
3507
|
-
{
|
|
3508
|
-
name: import_zod11.z.string().describe("Name for the network volume"),
|
|
3509
|
-
// Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
|
|
3510
|
-
// here as well as described, so an out-of-range value fails locally
|
|
3511
|
-
// instead of costing an API roundtrip.
|
|
3512
|
-
size: import_zod11.z.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
|
|
3513
|
-
dataCenterId: import_zod11.z.string().describe("Data center ID (see list-data-centers)"),
|
|
3514
|
-
volumeType: import_zod11.z.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
|
|
3515
|
-
"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."
|
|
3516
|
-
)
|
|
3517
|
-
},
|
|
3518
|
-
{ title: "Create network volume", ...WRITE },
|
|
3519
|
-
async (params) => {
|
|
3520
|
-
const backend = backendFor("networkVolumes");
|
|
3521
|
-
const body = backend.mapCreate(params);
|
|
3522
|
-
const result = await callRestUrl(
|
|
3523
|
-
`${backend.base}${backend.list}`,
|
|
3524
|
-
"POST",
|
|
3525
|
-
body
|
|
3526
|
-
);
|
|
3527
|
-
return jsonReply2(result);
|
|
3528
|
-
}
|
|
3529
|
-
);
|
|
3530
|
-
server.tool(
|
|
3531
|
-
"update-network-volume",
|
|
3532
|
-
"Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
|
|
3533
|
-
{
|
|
3534
|
-
networkVolumeId: import_zod11.z.string().describe("ID of the network volume to update"),
|
|
3535
|
-
name: import_zod11.z.string().optional().describe("New name for the network volume"),
|
|
3536
|
-
// Same absolute bounds as create. "Larger than current" stays server-side —
|
|
3537
|
-
// we do not hold the current size here, so only the range is checked.
|
|
3538
|
-
size: import_zod11.z.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
|
|
3539
|
-
},
|
|
3540
|
-
{ title: "Update network volume", ...WRITE, idempotentHint: true },
|
|
3541
|
-
async (params) => {
|
|
3542
|
-
const { networkVolumeId, ...updateParams } = params;
|
|
3543
|
-
const backend = backendFor("networkVolumes");
|
|
3544
|
-
const body = backend.mapUpdate(updateParams);
|
|
3545
|
-
const result = await callRestUrl(
|
|
3546
|
-
`${backend.base}${backend.get(networkVolumeId)}`,
|
|
3547
|
-
"PATCH",
|
|
3548
|
-
body
|
|
3549
|
-
);
|
|
3550
|
-
return jsonReply2(result);
|
|
3551
|
-
}
|
|
3552
|
-
);
|
|
3553
|
-
server.tool(
|
|
3554
|
-
"delete-network-volume",
|
|
3555
|
-
"Permanently delete a network volume and its data. This cannot be undone.",
|
|
3556
|
-
{
|
|
3557
|
-
networkVolumeId: import_zod11.z.string().describe("ID of the network volume to delete")
|
|
3558
|
-
},
|
|
3559
|
-
{ title: "Delete network volume", ...DESTRUCTIVE },
|
|
3560
|
-
async (params) => {
|
|
3561
|
-
const backend = backendFor("networkVolumes");
|
|
3562
|
-
const result = await callRestUrl(
|
|
3563
|
-
`${backend.base}${backend.get(params.networkVolumeId)}`,
|
|
3564
|
-
"DELETE"
|
|
3565
|
-
);
|
|
3566
|
-
return jsonReply2(result);
|
|
3567
|
-
}
|
|
3568
|
-
);
|
|
3569
|
-
}
|
|
3570
|
-
|
|
3571
|
-
// src/tools/registries.ts
|
|
3572
|
-
var import_zod12 = require("zod");
|
|
3573
|
-
var DELEGATIONS = "/delegations";
|
|
3574
|
-
function registerRegistryTools(server, rt) {
|
|
3575
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
3576
|
-
const delegationsV2OnlyNotice = (tool) => ({
|
|
3577
|
-
error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
3578
|
-
status: 501
|
|
3579
|
-
});
|
|
3580
|
-
server.tool(
|
|
3581
|
-
"list-container-registry-auths",
|
|
3582
|
-
"List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
|
|
3583
|
-
listPaginationParams,
|
|
3584
|
-
{ title: "List container registry auths", ...READ_ONLY },
|
|
3585
|
-
async (params) => {
|
|
3586
|
-
const backend = backendFor("registries");
|
|
3587
|
-
const result = await callRestUrl(`${backend.base}${backend.list}`);
|
|
3588
|
-
return capListResult(backend.unwrap(result), {
|
|
3589
|
-
limit: params.limit,
|
|
3590
|
-
cursor: params.cursor
|
|
3591
|
-
});
|
|
3592
|
-
}
|
|
3593
|
-
);
|
|
3594
|
-
server.tool(
|
|
3595
|
-
"get-container-registry-auth",
|
|
3596
|
-
"Get one saved container-registry credential by id (the secret/password is not returned).",
|
|
3597
|
-
{
|
|
3598
|
-
containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to retrieve")
|
|
3599
|
-
},
|
|
3600
|
-
{ title: "Get container registry auth", ...READ_ONLY },
|
|
3601
|
-
async (params) => {
|
|
3602
|
-
const backend = backendFor("registries");
|
|
3603
|
-
const result = await callRestUrl(
|
|
3604
|
-
`${backend.base}${backend.get(params.containerRegistryAuthId)}`
|
|
3605
|
-
);
|
|
3606
|
-
return jsonReply2(result);
|
|
3607
|
-
}
|
|
3608
|
-
);
|
|
3609
|
-
server.tool(
|
|
3610
|
-
"create-container-registry-auth",
|
|
3611
|
-
"Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
|
|
3612
|
-
{
|
|
3613
|
-
name: import_zod12.z.string().describe("Name for the container registry auth"),
|
|
3614
|
-
username: import_zod12.z.string().describe("Registry username"),
|
|
3615
|
-
password: import_zod12.z.string().describe("Registry password or access token (stored as a secret)")
|
|
3616
|
-
},
|
|
3617
|
-
{ title: "Create container registry auth", ...WRITE },
|
|
3618
|
-
async (params) => {
|
|
3619
|
-
const backend = backendFor("registries");
|
|
3620
|
-
const body = backend.mapCreate(params);
|
|
3621
|
-
const result = await callRestUrl(
|
|
3622
|
-
`${backend.base}${backend.list}`,
|
|
3623
|
-
"POST",
|
|
3624
|
-
body
|
|
3625
|
-
);
|
|
3626
|
-
return jsonReply2(result);
|
|
3627
|
-
}
|
|
3628
|
-
);
|
|
3629
|
-
server.tool(
|
|
3630
|
-
"delete-container-registry-auth",
|
|
3631
|
-
"Permanently delete a saved container-registry credential. This cannot be undone.",
|
|
3632
|
-
{
|
|
3633
|
-
containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to delete")
|
|
3634
|
-
},
|
|
3635
|
-
{ title: "Delete container registry auth", ...DESTRUCTIVE },
|
|
3636
|
-
async (params) => {
|
|
3637
|
-
const backend = backendFor("registries");
|
|
3638
|
-
const result = await callRestUrl(
|
|
3639
|
-
`${backend.base}${backend.get(params.containerRegistryAuthId)}`,
|
|
3640
|
-
"DELETE"
|
|
3641
|
-
);
|
|
3642
|
-
return jsonReply2(result);
|
|
3643
|
-
}
|
|
3644
|
-
);
|
|
3645
|
-
server.tool(
|
|
3646
|
-
"list-registry-delegations",
|
|
3647
|
-
"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.",
|
|
3648
|
-
listPaginationParams,
|
|
3649
|
-
{ title: "List registry delegations", ...READ_ONLY },
|
|
3650
|
-
async (params) => {
|
|
3651
|
-
const backend = backendFor("registries");
|
|
3652
|
-
if (backend.version === "v1") {
|
|
3653
|
-
return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
|
|
3654
|
-
}
|
|
3655
|
-
const raw = await callRestUrl(
|
|
3656
|
-
`${backend.base}${backend.list}${DELEGATIONS}`
|
|
3657
|
-
);
|
|
3658
|
-
const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
|
|
3659
|
-
return capListResult(delegations, {
|
|
3660
|
-
limit: params.limit,
|
|
3661
|
-
cursor: params.cursor
|
|
3662
|
-
});
|
|
3663
|
-
}
|
|
3664
|
-
);
|
|
3665
|
-
server.tool(
|
|
3666
|
-
"create-registry-delegation",
|
|
3667
|
-
"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.",
|
|
3668
|
-
{
|
|
3669
|
-
resource: import_zod12.z.string().min(1).describe(
|
|
3670
|
-
'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
|
|
3671
|
-
),
|
|
3672
|
-
name: import_zod12.z.string().optional().describe("Optional human-readable name for the delegation")
|
|
3673
|
-
},
|
|
3674
|
-
{ title: "Create registry delegation", ...WRITE },
|
|
3675
|
-
async (params) => {
|
|
3676
|
-
const backend = backendFor("registries");
|
|
3677
|
-
if (backend.version === "v1") {
|
|
3678
|
-
return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
|
|
3679
|
-
}
|
|
3680
|
-
const body = { resource: params.resource };
|
|
3681
|
-
if (params.name !== void 0) body.name = params.name;
|
|
3682
|
-
const result = await callRestUrl(
|
|
3683
|
-
`${backend.base}${backend.list}${DELEGATIONS}`,
|
|
3684
|
-
"POST",
|
|
3685
|
-
body
|
|
3686
|
-
);
|
|
3687
|
-
return jsonReply2(result);
|
|
3688
|
-
}
|
|
3689
|
-
);
|
|
3690
|
-
server.tool(
|
|
3691
|
-
"delete-registry-delegation",
|
|
3692
|
-
"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.",
|
|
3693
|
-
{
|
|
3694
|
-
delegationId: import_zod12.z.string().describe(
|
|
3695
|
-
"ID of the delegation to revoke (from list-registry-delegations)"
|
|
3696
|
-
)
|
|
3697
|
-
},
|
|
3698
|
-
{ title: "Revoke registry delegation", ...DESTRUCTIVE },
|
|
3699
|
-
async (params) => {
|
|
3700
|
-
const backend = backendFor("registries");
|
|
3701
|
-
if (backend.version === "v1") {
|
|
3702
|
-
return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
|
|
3703
|
-
}
|
|
3704
|
-
const result = await callRestUrl(
|
|
3705
|
-
`${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
|
|
3706
|
-
params.delegationId
|
|
3707
|
-
)}`,
|
|
3708
|
-
"DELETE"
|
|
3709
|
-
);
|
|
3710
|
-
return jsonReply2(result);
|
|
3711
|
-
}
|
|
3712
|
-
);
|
|
3713
|
-
}
|
|
3714
|
-
|
|
3715
|
-
// src/tools/billing.ts
|
|
3716
|
-
var import_zod13 = require("zod");
|
|
3717
|
-
var BILLING_SCOPE_PATH = {
|
|
3718
|
-
all: "/billing",
|
|
3719
|
-
pods: "/billing/pods",
|
|
3720
|
-
serverless: "/billing/serverless",
|
|
3721
|
-
endpoints: "/billing/endpoints",
|
|
3722
|
-
networkvolumes: "/billing/networkvolumes",
|
|
3723
|
-
clusters: "/billing/clusters"
|
|
3724
|
-
};
|
|
3725
|
-
function registerBillingTools(server, rt) {
|
|
3726
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
3727
|
-
server.tool(
|
|
3728
|
-
"get-billing",
|
|
3729
|
-
"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.",
|
|
3730
|
-
{
|
|
3731
|
-
...listPaginationParams,
|
|
3732
|
-
scope: import_zod13.z.enum([
|
|
3733
|
-
"all",
|
|
3734
|
-
"pods",
|
|
3735
|
-
"serverless",
|
|
3736
|
-
"endpoints",
|
|
3737
|
-
"networkvolumes",
|
|
3738
|
-
"clusters"
|
|
3739
|
-
]).optional().describe(
|
|
3740
|
-
'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
|
|
3741
|
-
),
|
|
3742
|
-
startTime: import_zod13.z.string().optional().describe(
|
|
3743
|
-
"Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
|
|
3744
|
-
),
|
|
3745
|
-
endTime: import_zod13.z.string().optional().describe(
|
|
3746
|
-
"End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
|
|
3747
|
-
),
|
|
3748
|
-
bucketSize: import_zod13.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
|
|
3749
|
-
lastN: import_zod13.z.number().int().positive().optional().describe(
|
|
3750
|
-
"Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
|
|
3751
|
-
)
|
|
3752
|
-
},
|
|
3753
|
-
{ title: "Get billing history", ...READ_ONLY },
|
|
3754
|
-
async (params) => {
|
|
3755
|
-
const backend = backendFor("billing");
|
|
3756
|
-
if (backend.version === "v1") {
|
|
3757
|
-
return jsonReply2({
|
|
3758
|
-
error: "get-billing is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
3759
|
-
status: 501
|
|
3760
|
-
});
|
|
3761
|
-
}
|
|
3762
|
-
const subPath = BILLING_SCOPE_PATH[params.scope ?? "all"];
|
|
3763
|
-
const query = new URLSearchParams();
|
|
3764
|
-
if (params.startTime) query.set("startTime", params.startTime);
|
|
3765
|
-
if (params.endTime) query.set("endTime", params.endTime);
|
|
3766
|
-
if (params.bucketSize) query.set("bucketSize", params.bucketSize);
|
|
3767
|
-
if (params.lastN !== void 0 && !params.startTime && !params.endTime)
|
|
3768
|
-
query.set("lastN", String(params.lastN));
|
|
3769
|
-
const qs = query.toString() ? `?${query.toString()}` : "";
|
|
3770
|
-
const raw = await callRestUrl(`${backend.base}${subPath}${qs}`);
|
|
3771
|
-
return capListResult(
|
|
3772
|
-
backend.unwrap(raw),
|
|
3773
|
-
{ limit: params.limit, cursor: params.cursor },
|
|
3774
|
-
{ metadata: raw?.metadata }
|
|
3775
|
-
);
|
|
3776
|
-
}
|
|
3777
|
-
);
|
|
3778
|
-
}
|
|
3779
|
-
|
|
3780
|
-
// src/tools.ts
|
|
3781
|
-
function registerTools(server, ctx, deps = {}) {
|
|
3782
|
-
const rt = createToolRuntime(server, ctx, deps);
|
|
3783
|
-
registerCatalogTools(server, rt);
|
|
3784
|
-
registerHubTools(server, rt);
|
|
3785
|
-
registerPublicEndpointTools(server, rt);
|
|
3786
|
-
registerPodTools(server, rt);
|
|
3787
|
-
registerEndpointTools(server, rt);
|
|
3788
|
-
registerEndpointGpuTools(server, rt);
|
|
3789
|
-
registerJobTools(server, rt);
|
|
3790
|
-
registerTemplateTools(server, rt);
|
|
3791
|
-
registerNetworkVolumeTools(server, rt);
|
|
3792
|
-
registerRegistryTools(server, rt);
|
|
3793
|
-
registerBillingTools(server, rt);
|
|
3794
|
-
}
|
|
3795
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
3796
|
-
0 && (module.exports = {
|
|
3797
|
-
registerTools
|
|
3798
|
-
});
|
|
3799
|
-
//# sourceMappingURL=tools.cjs.map
|