@runpod/mcp-server 2.0.0 → 3.0.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 +69 -2
- package/README.md +14 -4
- package/dist/{chunk-ZP3TCVXG.mjs → chunk-GTBCNGHE.mjs} +1133 -371
- package/dist/chunk-GTBCNGHE.mjs.map +1 -0
- package/dist/http.cjs +1470 -483
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +35 -2
- package/dist/http.d.ts +35 -2
- package/dist/http.mjs +247 -17
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +1231 -472
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +1233 -474
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +1119 -370
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +14 -0
- package/dist/tools.d.ts +14 -0
- package/dist/tools.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZP3TCVXG.mjs.map +0 -1
|
@@ -5,7 +5,8 @@ import { randomUUID } from "crypto";
|
|
|
5
5
|
// src/_shared/http.ts
|
|
6
6
|
var HttpError = class extends Error {
|
|
7
7
|
constructor(prefix, status, body) {
|
|
8
|
-
|
|
8
|
+
const hint = status === 401 ? " (the Runpod API rejected the credential \u2014 the API key may be expired or revoked; re-authenticate or update RUNPOD_API_KEY)" : "";
|
|
9
|
+
super(`${prefix}: ${status} - ${body}${hint}`);
|
|
9
10
|
this.name = "HttpError";
|
|
10
11
|
this.status = status;
|
|
11
12
|
this.body = body;
|
|
@@ -61,6 +62,106 @@ function buildTrackingHeaders(input) {
|
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
// src/tools/logs.ts
|
|
66
|
+
import { z } from "zod";
|
|
67
|
+
function parseLogSse(raw) {
|
|
68
|
+
const items = [];
|
|
69
|
+
for (const block of raw.split(/\r?\n\r?\n/)) {
|
|
70
|
+
const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
|
|
71
|
+
if (!dataLines.length) continue;
|
|
72
|
+
const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
73
|
+
if (!payload.trim()) continue;
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(payload);
|
|
76
|
+
items.push(
|
|
77
|
+
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
|
|
78
|
+
);
|
|
79
|
+
} catch {
|
|
80
|
+
items.push({ raw: payload });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return items;
|
|
84
|
+
}
|
|
85
|
+
var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
|
|
86
|
+
var LOG_STREAM_MAX_BYTES = 256 * 1024;
|
|
87
|
+
async function readSseSnapshot(fetchImpl, url, headers, opts) {
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
|
|
90
|
+
const chunks = [];
|
|
91
|
+
let bytes = 0;
|
|
92
|
+
let truncated = false;
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetchImpl(url, {
|
|
95
|
+
method: "GET",
|
|
96
|
+
headers,
|
|
97
|
+
signal: controller.signal
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
throw new HttpError(
|
|
101
|
+
"Runpod API Error",
|
|
102
|
+
res.status,
|
|
103
|
+
await res.text().catch(() => "")
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
for await (const chunk of res.body ?? []) {
|
|
107
|
+
chunks.push(chunk);
|
|
108
|
+
bytes += chunk.length;
|
|
109
|
+
if (bytes >= opts.maxBytes) {
|
|
110
|
+
truncated = true;
|
|
111
|
+
controller.abort();
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
117
|
+
} finally {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
}
|
|
120
|
+
return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
121
|
+
}
|
|
122
|
+
var logStreamParams = {
|
|
123
|
+
source: z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
|
|
124
|
+
tail: z.number().int().min(0).max(5e3).optional().describe(
|
|
125
|
+
"Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
|
|
126
|
+
),
|
|
127
|
+
since: z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
|
|
128
|
+
maxWaitMs: z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
|
|
129
|
+
};
|
|
130
|
+
async function collectLogSnapshot(streamSse, logsUrl, params) {
|
|
131
|
+
const qs = new URLSearchParams();
|
|
132
|
+
if (params.source && params.source !== "both")
|
|
133
|
+
qs.append("source", params.source);
|
|
134
|
+
if (params.tail !== void 0) qs.append("tail", String(params.tail));
|
|
135
|
+
if (params.since) qs.append("since", params.since);
|
|
136
|
+
const query = qs.toString() ? `?${qs}` : "";
|
|
137
|
+
const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
|
|
138
|
+
maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
|
|
139
|
+
maxBytes: LOG_STREAM_MAX_BYTES
|
|
140
|
+
});
|
|
141
|
+
const items = parseLogSse(raw);
|
|
142
|
+
if (truncated) items.pop();
|
|
143
|
+
return { items, count: items.length, truncated };
|
|
144
|
+
}
|
|
145
|
+
async function streamLogsReply(rt, tool, params) {
|
|
146
|
+
const backend = rt.backendFor(tool.resource);
|
|
147
|
+
if (backend.version === "v1") {
|
|
148
|
+
return rt.jsonReply({
|
|
149
|
+
error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
150
|
+
status: 501
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
return rt.jsonReply(
|
|
155
|
+
await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
|
|
156
|
+
);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof HttpError) {
|
|
159
|
+
return rt.jsonReply({ error: error.message, status: error.status });
|
|
160
|
+
}
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
64
165
|
// src/_shared/mappers.ts
|
|
65
166
|
function compact(obj) {
|
|
66
167
|
const out = {};
|
|
@@ -73,6 +174,9 @@ function persistentMount(size, path) {
|
|
|
73
174
|
if (size === void 0 || path === void 0) return void 0;
|
|
74
175
|
return { persistent: { size, path } };
|
|
75
176
|
}
|
|
177
|
+
function joinStartCmd(cmd) {
|
|
178
|
+
return cmd && cmd.length ? cmd.join(" ") : void 0;
|
|
179
|
+
}
|
|
76
180
|
function containerConfigToV2(p) {
|
|
77
181
|
return compact({
|
|
78
182
|
image: p.imageName,
|
|
@@ -91,6 +195,13 @@ function mapPodCreateToV2(params) {
|
|
|
91
195
|
return compact({
|
|
92
196
|
...containerConfigToV2(params),
|
|
93
197
|
name: params.name,
|
|
198
|
+
// Private-image registry credential (v2 ContainerConfig `registry`). When
|
|
199
|
+
// deploying from a template, this spreads over the template's registry, so
|
|
200
|
+
// passing it overrides the inherited credential. An empty string is an
|
|
201
|
+
// explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
|
|
202
|
+
// template's inherited credential. Undefined is dropped by compact, so an
|
|
203
|
+
// omitted param leaves the template's registry in place.
|
|
204
|
+
registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
|
|
94
205
|
cloud: params.cloudType,
|
|
95
206
|
// v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
|
|
96
207
|
// centers; omit/empty = scheduler chooses). Pass the v1 list straight
|
|
@@ -147,7 +258,8 @@ function mapNetworkVolumeCreateToV2(params) {
|
|
|
147
258
|
return compact({
|
|
148
259
|
name: params.name,
|
|
149
260
|
size: params.size,
|
|
150
|
-
dataCenter: params.dataCenterId
|
|
261
|
+
dataCenter: params.dataCenterId,
|
|
262
|
+
type: params.volumeType
|
|
151
263
|
});
|
|
152
264
|
}
|
|
153
265
|
function mapTemplateCreateToV2(params) {
|
|
@@ -156,18 +268,39 @@ function mapTemplateCreateToV2(params) {
|
|
|
156
268
|
...compact({
|
|
157
269
|
name: params.name,
|
|
158
270
|
serverless: params.isServerless,
|
|
159
|
-
registry: params.containerRegistryAuthId
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
271
|
+
registry: params.containerRegistryAuthId,
|
|
272
|
+
args: joinStartCmd(params.dockerStartCmd),
|
|
273
|
+
// No longer required by v2 (it defaults to NVIDIA server-side), so an
|
|
274
|
+
// unset category is dropped by compact instead of being forced here.
|
|
275
|
+
category: params.category
|
|
276
|
+
})
|
|
163
277
|
};
|
|
164
278
|
}
|
|
279
|
+
function podBodyFromTemplate(template) {
|
|
280
|
+
const out = {};
|
|
281
|
+
const keys = [
|
|
282
|
+
"name",
|
|
283
|
+
"image",
|
|
284
|
+
"args",
|
|
285
|
+
"disk",
|
|
286
|
+
"ports",
|
|
287
|
+
"env",
|
|
288
|
+
"mounts",
|
|
289
|
+
"registry"
|
|
290
|
+
];
|
|
291
|
+
for (const key of keys) {
|
|
292
|
+
const value = template[key];
|
|
293
|
+
if (value !== void 0 && value !== null) out[key] = value;
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
165
297
|
function mapTemplateUpdateToV2(params) {
|
|
166
298
|
return {
|
|
167
299
|
...containerConfigToV2(params),
|
|
168
300
|
...compact({
|
|
169
301
|
name: params.name,
|
|
170
|
-
registry: params.containerRegistryAuthId
|
|
302
|
+
registry: params.containerRegistryAuthId,
|
|
303
|
+
args: joinStartCmd(params.dockerStartCmd)
|
|
171
304
|
})
|
|
172
305
|
};
|
|
173
306
|
}
|
|
@@ -184,7 +317,6 @@ var V2_UNWRAP_KEY = {
|
|
|
184
317
|
dataCenters: "dataCenters",
|
|
185
318
|
endpoints: "endpoints",
|
|
186
319
|
jobs: "jobs",
|
|
187
|
-
tags: "tags",
|
|
188
320
|
workers: "workers",
|
|
189
321
|
billing: "records"
|
|
190
322
|
};
|
|
@@ -198,7 +330,6 @@ var RESOURCE_ENV_KEY = {
|
|
|
198
330
|
dataCenters: "DATA_CENTERS",
|
|
199
331
|
endpoints: "ENDPOINTS",
|
|
200
332
|
jobs: "JOBS",
|
|
201
|
-
tags: "TAGS",
|
|
202
333
|
workers: "WORKERS",
|
|
203
334
|
billing: "BILLING"
|
|
204
335
|
};
|
|
@@ -214,6 +345,9 @@ function serverlessBase(env) {
|
|
|
214
345
|
function publicGraphqlBase(env) {
|
|
215
346
|
return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
216
347
|
}
|
|
348
|
+
function authedGraphqlBase(env) {
|
|
349
|
+
return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
350
|
+
}
|
|
217
351
|
function unwrapList(resource, version, raw) {
|
|
218
352
|
if (version === "v1") return Array.isArray(raw) ? raw : [];
|
|
219
353
|
if (raw === null || typeof raw !== "object") return [];
|
|
@@ -232,6 +366,14 @@ function resolveVersion(opts) {
|
|
|
232
366
|
}
|
|
233
367
|
return "v1";
|
|
234
368
|
}
|
|
369
|
+
function restVersionSummary(env) {
|
|
370
|
+
const raw = (env.RUNPOD_REST_VERSION ?? "").trim().toLowerCase();
|
|
371
|
+
const base = raw ? `RUNPOD_REST_VERSION=${raw}` : "RUNPOD_REST_VERSION unset (default v2)";
|
|
372
|
+
const overrides = Object.keys(env).filter(
|
|
373
|
+
(k) => k.startsWith("RUNPOD_REST_VERSION_") && (env[k] ?? "").trim() !== ""
|
|
374
|
+
).length;
|
|
375
|
+
return overrides > 0 ? `${base}, +${overrides} per-resource` : base;
|
|
376
|
+
}
|
|
235
377
|
var identity = (body) => body;
|
|
236
378
|
var V1_REST_PATHS = {
|
|
237
379
|
pods: { list: "/pods", get: (id) => `/pods/${id}` },
|
|
@@ -252,7 +394,6 @@ var CATALOG = /* @__PURE__ */ new Set([
|
|
|
252
394
|
"dataCenters"
|
|
253
395
|
]);
|
|
254
396
|
var V2_ONLY = /* @__PURE__ */ new Set([
|
|
255
|
-
"tags",
|
|
256
397
|
"workers",
|
|
257
398
|
"billing"
|
|
258
399
|
]);
|
|
@@ -270,7 +411,6 @@ var V2_REST_PATHS = {
|
|
|
270
411
|
list: "/catalog/datacenters",
|
|
271
412
|
get: (id) => `/catalog/datacenters/${id}`
|
|
272
413
|
},
|
|
273
|
-
tags: { list: "/tags", get: (id) => `/tags/${id}` },
|
|
274
414
|
// Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
|
|
275
415
|
// response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
|
|
276
416
|
// unwrap key stays correct.
|
|
@@ -368,7 +508,7 @@ function resolveBackend(opts) {
|
|
|
368
508
|
}
|
|
369
509
|
|
|
370
510
|
// src/tools/runtime.ts
|
|
371
|
-
var MCP_SERVER_VERSION = true ? "
|
|
511
|
+
var MCP_SERVER_VERSION = true ? "3.0.0" : "dev";
|
|
372
512
|
var SESSION_ID = randomUUID();
|
|
373
513
|
function trackingHeaders(server, ctx) {
|
|
374
514
|
const info = server.server.getClientVersion();
|
|
@@ -405,14 +545,18 @@ var DESTRUCTIVE = {
|
|
|
405
545
|
idempotentHint: true,
|
|
406
546
|
openWorldHint: true
|
|
407
547
|
};
|
|
408
|
-
async function graphqlRequest(query, tracking, fetchImpl, url) {
|
|
548
|
+
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
409
549
|
const response = await fetchImpl(url, {
|
|
410
550
|
method: "POST",
|
|
411
551
|
headers: {
|
|
412
552
|
"Content-Type": "application/json",
|
|
553
|
+
...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
413
554
|
...tracking()
|
|
414
555
|
},
|
|
415
|
-
body: JSON.stringify({
|
|
556
|
+
body: JSON.stringify({
|
|
557
|
+
query,
|
|
558
|
+
...options?.variables ? { variables: options.variables } : {}
|
|
559
|
+
})
|
|
416
560
|
});
|
|
417
561
|
const result = await response.json();
|
|
418
562
|
if (result.errors && result.errors.length > 0) {
|
|
@@ -427,7 +571,17 @@ async function graphqlRequest(query, tracking, fetchImpl, url) {
|
|
|
427
571
|
}
|
|
428
572
|
function createToolRuntime(server, ctx, deps = {}) {
|
|
429
573
|
const tracking = () => trackingHeaders(server, ctx);
|
|
430
|
-
|
|
574
|
+
function observeUnauthorized(fn) {
|
|
575
|
+
const notify = ctx.onUnauthorized;
|
|
576
|
+
if (!notify) return fn;
|
|
577
|
+
return async (...args) => {
|
|
578
|
+
const response = await fn(...args);
|
|
579
|
+
if (response.status === 401) notify();
|
|
580
|
+
return response;
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const rawFetch = deps.fetch ?? defaultFetch;
|
|
584
|
+
const httpFetch = observeUnauthorized(rawFetch);
|
|
431
585
|
const v1Client = createHttpClient({
|
|
432
586
|
apiKey: ctx.apiKey,
|
|
433
587
|
fetch: httpFetch,
|
|
@@ -462,47 +616,19 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
462
616
|
"Error calling Runpod API:",
|
|
463
617
|
() => restClient(url, method, body)
|
|
464
618
|
);
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (!res.ok) {
|
|
479
|
-
throw new HttpError(
|
|
480
|
-
"Runpod API Error",
|
|
481
|
-
res.status,
|
|
482
|
-
await res.text().catch(() => "")
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
const chunks = [];
|
|
486
|
-
let bytes = 0;
|
|
487
|
-
let truncated = false;
|
|
488
|
-
try {
|
|
489
|
-
for await (const chunk of res.body) {
|
|
490
|
-
chunks.push(chunk);
|
|
491
|
-
bytes += chunk.length;
|
|
492
|
-
if (bytes >= opts.maxBytes) {
|
|
493
|
-
truncated = true;
|
|
494
|
-
controller.abort();
|
|
495
|
-
break;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
} catch (err) {
|
|
499
|
-
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
500
|
-
}
|
|
501
|
-
return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
502
|
-
} finally {
|
|
503
|
-
clearTimeout(timer);
|
|
504
|
-
}
|
|
505
|
-
});
|
|
619
|
+
const sseFetch = observeUnauthorized(
|
|
620
|
+
deps.sseFetch ?? fetch
|
|
621
|
+
);
|
|
622
|
+
const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
|
|
623
|
+
sseFetch,
|
|
624
|
+
url,
|
|
625
|
+
{
|
|
626
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
627
|
+
Accept: "text/event-stream",
|
|
628
|
+
...tracking()
|
|
629
|
+
},
|
|
630
|
+
opts
|
|
631
|
+
));
|
|
506
632
|
const backendFor = (resource) => resolveBackend({
|
|
507
633
|
resource,
|
|
508
634
|
env: process.env,
|
|
@@ -524,9 +650,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
524
650
|
graphql: (query) => graphqlRequest(
|
|
525
651
|
query,
|
|
526
652
|
tracking,
|
|
527
|
-
httpFetch
|
|
653
|
+
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
654
|
+
rawFetch,
|
|
528
655
|
publicGraphqlBase(process.env)
|
|
529
656
|
),
|
|
657
|
+
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
658
|
+
query,
|
|
659
|
+
tracking,
|
|
660
|
+
httpFetch,
|
|
661
|
+
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
662
|
+
authedGraphqlBase(process.env),
|
|
663
|
+
{ variables, apiKey: ctx.apiKey }
|
|
664
|
+
),
|
|
530
665
|
runpodRequest,
|
|
531
666
|
serverlessRequest,
|
|
532
667
|
callRestUrl,
|
|
@@ -538,17 +673,17 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
538
673
|
}
|
|
539
674
|
|
|
540
675
|
// src/tools/catalog.ts
|
|
541
|
-
import { z as
|
|
676
|
+
import { z as z3 } from "zod";
|
|
542
677
|
|
|
543
678
|
// src/pagination.ts
|
|
544
|
-
import { z } from "zod";
|
|
679
|
+
import { z as z2 } from "zod";
|
|
545
680
|
var DEFAULT_LIST_LIMIT = 20;
|
|
546
681
|
var MAX_LIST_LIMIT = 100;
|
|
547
682
|
var listPaginationParams = {
|
|
548
|
-
limit:
|
|
683
|
+
limit: z2.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
|
|
549
684
|
`Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
|
|
550
685
|
),
|
|
551
|
-
cursor:
|
|
686
|
+
cursor: z2.string().optional().describe(
|
|
552
687
|
"Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
|
|
553
688
|
)
|
|
554
689
|
};
|
|
@@ -606,25 +741,32 @@ function registerCatalogTools(server, rt) {
|
|
|
606
741
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
607
742
|
server.tool(
|
|
608
743
|
"list-gpu-types",
|
|
609
|
-
"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.
|
|
744
|
+
"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.",
|
|
610
745
|
{
|
|
611
746
|
...listPaginationParams,
|
|
612
|
-
minMemoryGb:
|
|
613
|
-
secureCloudOnly:
|
|
614
|
-
communityCloudOnly:
|
|
615
|
-
searchTerm:
|
|
747
|
+
minMemoryGb: z3.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
|
|
748
|
+
secureCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
|
|
749
|
+
communityCloudOnly: z3.boolean().optional().describe("Filter to only GPUs available in community cloud"),
|
|
750
|
+
searchTerm: z3.string().optional().describe(
|
|
616
751
|
"Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
|
|
617
752
|
),
|
|
618
|
-
includeUnavailable:
|
|
619
|
-
"
|
|
753
|
+
includeUnavailable: z3.boolean().optional().describe(
|
|
754
|
+
"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."
|
|
755
|
+
),
|
|
756
|
+
includeAvailability: z3.boolean().optional().describe(
|
|
757
|
+
"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."
|
|
620
758
|
)
|
|
621
759
|
},
|
|
622
760
|
{ title: "List GPU types", ...READ_ONLY },
|
|
623
761
|
async (params) => {
|
|
624
762
|
const backend = backendFor("gpus");
|
|
625
763
|
if (backend.version === "v2") {
|
|
626
|
-
const
|
|
764
|
+
const wantAvailability = params.includeAvailability !== false;
|
|
765
|
+
const raw = await callRestUrl(
|
|
766
|
+
`${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
|
|
767
|
+
);
|
|
627
768
|
let gpus = backend.unwrap(raw);
|
|
769
|
+
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
628
770
|
if (params.minMemoryGb !== void 0)
|
|
629
771
|
gpus = gpus.filter(
|
|
630
772
|
(g) => Number(g.memory ?? 0) >= params.minMemoryGb
|
|
@@ -637,6 +779,18 @@ function registerCatalogTools(server, rt) {
|
|
|
637
779
|
(g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
|
|
638
780
|
);
|
|
639
781
|
}
|
|
782
|
+
if (params.includeUnavailable === false)
|
|
783
|
+
gpus = gpus.filter((g) => g.availability !== "NONE");
|
|
784
|
+
const rank = {
|
|
785
|
+
HIGH: 3,
|
|
786
|
+
MEDIUM: 2,
|
|
787
|
+
LOW: 1,
|
|
788
|
+
NONE: 0
|
|
789
|
+
};
|
|
790
|
+
gpus.sort(
|
|
791
|
+
(a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
|
|
792
|
+
);
|
|
793
|
+
gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
|
|
640
794
|
return capListResult(gpus, {
|
|
641
795
|
limit: params.limit,
|
|
642
796
|
cursor: params.cursor
|
|
@@ -666,7 +820,7 @@ function registerCatalogTools(server, rt) {
|
|
|
666
820
|
return !!status && status !== "Out";
|
|
667
821
|
};
|
|
668
822
|
let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
|
|
669
|
-
if (
|
|
823
|
+
if (params.includeUnavailable === false) {
|
|
670
824
|
gpuTypes = gpuTypes.filter(isAvailable);
|
|
671
825
|
}
|
|
672
826
|
if (params.minMemoryGb) {
|
|
@@ -712,7 +866,7 @@ function registerCatalogTools(server, rt) {
|
|
|
712
866
|
"List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
|
|
713
867
|
{
|
|
714
868
|
...listPaginationParams,
|
|
715
|
-
region:
|
|
869
|
+
region: z3.string().optional().describe(
|
|
716
870
|
"Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
|
|
717
871
|
)
|
|
718
872
|
},
|
|
@@ -777,9 +931,12 @@ function registerCatalogTools(server, rt) {
|
|
|
777
931
|
);
|
|
778
932
|
server.tool(
|
|
779
933
|
"get-gpu-type",
|
|
780
|
-
"Get details for a single GPU type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there).",
|
|
934
|
+
"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.",
|
|
781
935
|
{
|
|
782
|
-
gpuTypeId:
|
|
936
|
+
gpuTypeId: z3.string().describe("ID of the GPU type to retrieve"),
|
|
937
|
+
includeAvailability: z3.boolean().optional().describe(
|
|
938
|
+
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
939
|
+
)
|
|
783
940
|
},
|
|
784
941
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
785
942
|
async (params) => {
|
|
@@ -790,9 +947,9 @@ function registerCatalogTools(server, rt) {
|
|
|
790
947
|
status: 501
|
|
791
948
|
});
|
|
792
949
|
}
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
);
|
|
950
|
+
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
951
|
+
const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
|
|
952
|
+
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
796
953
|
return jsonReply2(result);
|
|
797
954
|
}
|
|
798
955
|
);
|
|
@@ -800,7 +957,7 @@ function registerCatalogTools(server, rt) {
|
|
|
800
957
|
"get-cpu-type",
|
|
801
958
|
"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).",
|
|
802
959
|
{
|
|
803
|
-
cpuTypeId:
|
|
960
|
+
cpuTypeId: z3.string().describe("ID of the CPU type to retrieve")
|
|
804
961
|
},
|
|
805
962
|
{ title: "Get CPU type", ...READ_ONLY },
|
|
806
963
|
async (params) => {
|
|
@@ -821,7 +978,7 @@ function registerCatalogTools(server, rt) {
|
|
|
821
978
|
"get-data-center",
|
|
822
979
|
"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).",
|
|
823
980
|
{
|
|
824
|
-
dataCenterId:
|
|
981
|
+
dataCenterId: z3.string().describe("ID of the data center to retrieve")
|
|
825
982
|
},
|
|
826
983
|
{ title: "Get data center", ...READ_ONLY },
|
|
827
984
|
async (params) => {
|
|
@@ -840,8 +997,579 @@ function registerCatalogTools(server, rt) {
|
|
|
840
997
|
);
|
|
841
998
|
}
|
|
842
999
|
|
|
1000
|
+
// src/tools/hub.ts
|
|
1001
|
+
import { z as z4 } from "zod";
|
|
1002
|
+
function parseReleaseConfig(config) {
|
|
1003
|
+
if (!config) return void 0;
|
|
1004
|
+
try {
|
|
1005
|
+
return JSON.parse(config);
|
|
1006
|
+
} catch {
|
|
1007
|
+
return config;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function listingsQuery(withConfig) {
|
|
1011
|
+
return `
|
|
1012
|
+
query {
|
|
1013
|
+
listings(input: {}) {
|
|
1014
|
+
id
|
|
1015
|
+
repoId
|
|
1016
|
+
title
|
|
1017
|
+
description
|
|
1018
|
+
repoName
|
|
1019
|
+
repoOwner
|
|
1020
|
+
createdAt
|
|
1021
|
+
updatedAt
|
|
1022
|
+
views
|
|
1023
|
+
stars
|
|
1024
|
+
deploys
|
|
1025
|
+
language
|
|
1026
|
+
category
|
|
1027
|
+
tags
|
|
1028
|
+
type
|
|
1029
|
+
listedRelease {
|
|
1030
|
+
id
|
|
1031
|
+
name
|
|
1032
|
+
tagName
|
|
1033
|
+
createdAt${withConfig ? "\n config" : ""}
|
|
1034
|
+
build {
|
|
1035
|
+
id
|
|
1036
|
+
imageName
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
`;
|
|
1042
|
+
}
|
|
1043
|
+
function serializeBoolean(value, input) {
|
|
1044
|
+
if (value === "true") return input.trueValue ?? "true";
|
|
1045
|
+
if (value === "false") return input.falseValue ?? "false";
|
|
1046
|
+
return value;
|
|
1047
|
+
}
|
|
1048
|
+
function buildHubEnv(config, overrides) {
|
|
1049
|
+
const env = [];
|
|
1050
|
+
const missingRequired = [];
|
|
1051
|
+
const remaining = { ...overrides };
|
|
1052
|
+
for (const entry of config.env ?? []) {
|
|
1053
|
+
const input = entry.input ?? {};
|
|
1054
|
+
const override = entry.key in remaining ? remaining[entry.key] : void 0;
|
|
1055
|
+
delete remaining[entry.key];
|
|
1056
|
+
if (override !== void 0 && override !== "") {
|
|
1057
|
+
env.push({
|
|
1058
|
+
key: entry.key,
|
|
1059
|
+
value: input.type === "boolean" ? serializeBoolean(override, input) : override
|
|
1060
|
+
});
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
if (input.default !== void 0 && input.default !== null) {
|
|
1064
|
+
env.push({
|
|
1065
|
+
key: entry.key,
|
|
1066
|
+
value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
|
|
1067
|
+
});
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
if (input.required) {
|
|
1071
|
+
missingRequired.push(entry.key);
|
|
1072
|
+
env.push({ key: entry.key, value: "" });
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
for (const [key, value] of Object.entries(remaining)) {
|
|
1077
|
+
env.push({ key, value });
|
|
1078
|
+
}
|
|
1079
|
+
return { env, missingRequired };
|
|
1080
|
+
}
|
|
1081
|
+
function minCudaVersion(versions) {
|
|
1082
|
+
if (!versions || versions.length === 0) return null;
|
|
1083
|
+
const compare = (a, b) => {
|
|
1084
|
+
const pa = a.split(".").map(Number);
|
|
1085
|
+
const pb = b.split(".").map(Number);
|
|
1086
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1087
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1088
|
+
if (d !== 0) return d;
|
|
1089
|
+
}
|
|
1090
|
+
return 0;
|
|
1091
|
+
};
|
|
1092
|
+
return [...versions].sort(compare)[0];
|
|
1093
|
+
}
|
|
1094
|
+
function randomSuffix() {
|
|
1095
|
+
return Math.random().toString(36).slice(2, 8);
|
|
1096
|
+
}
|
|
1097
|
+
function registerHubTools(server, rt) {
|
|
1098
|
+
const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
1099
|
+
server.tool(
|
|
1100
|
+
"list-hub-repos",
|
|
1101
|
+
"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.",
|
|
1102
|
+
{
|
|
1103
|
+
...listPaginationParams,
|
|
1104
|
+
searchTerm: z4.string().optional().describe(
|
|
1105
|
+
"Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
|
|
1106
|
+
),
|
|
1107
|
+
category: z4.string().optional().describe(
|
|
1108
|
+
"Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
|
|
1109
|
+
),
|
|
1110
|
+
type: z4.enum(["SERVERLESS", "POD"]).optional().describe(
|
|
1111
|
+
"Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
|
|
1112
|
+
),
|
|
1113
|
+
repoOwner: z4.string().optional().describe(
|
|
1114
|
+
"Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
|
|
1115
|
+
),
|
|
1116
|
+
includeConfig: z4.boolean().optional().describe(
|
|
1117
|
+
"Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
|
|
1118
|
+
)
|
|
1119
|
+
},
|
|
1120
|
+
{ title: "List Hub repos", ...READ_ONLY },
|
|
1121
|
+
async (params) => {
|
|
1122
|
+
const data = await graphql(
|
|
1123
|
+
listingsQuery(params.includeConfig === true)
|
|
1124
|
+
);
|
|
1125
|
+
let listings = data.listings;
|
|
1126
|
+
if (params.type) {
|
|
1127
|
+
listings = listings.filter(
|
|
1128
|
+
(l) => (l.type ?? "").toUpperCase() === params.type
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
if (params.category) {
|
|
1132
|
+
const term = params.category.toLowerCase();
|
|
1133
|
+
listings = listings.filter(
|
|
1134
|
+
(l) => (l.category ?? "").toLowerCase() === term
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
if (params.repoOwner) {
|
|
1138
|
+
const term = params.repoOwner.toLowerCase();
|
|
1139
|
+
listings = listings.filter(
|
|
1140
|
+
(l) => (l.repoOwner ?? "").toLowerCase() === term
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
if (params.searchTerm) {
|
|
1144
|
+
const term = params.searchTerm.toLowerCase();
|
|
1145
|
+
listings = listings.filter(
|
|
1146
|
+
(l) => [
|
|
1147
|
+
l.title,
|
|
1148
|
+
l.description,
|
|
1149
|
+
l.repoName,
|
|
1150
|
+
l.repoOwner,
|
|
1151
|
+
...l.tags ?? []
|
|
1152
|
+
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
listings = [...listings].sort(
|
|
1156
|
+
(a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
|
|
1157
|
+
);
|
|
1158
|
+
const result = listings.map((l) => ({
|
|
1159
|
+
id: l.id,
|
|
1160
|
+
repo: `${l.repoOwner}/${l.repoName}`,
|
|
1161
|
+
title: l.title,
|
|
1162
|
+
description: l.description,
|
|
1163
|
+
type: l.type,
|
|
1164
|
+
category: l.category,
|
|
1165
|
+
tags: l.tags,
|
|
1166
|
+
language: l.language,
|
|
1167
|
+
stars: l.stars,
|
|
1168
|
+
deploys: l.deploys,
|
|
1169
|
+
views: l.views,
|
|
1170
|
+
githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
|
|
1171
|
+
hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
|
|
1172
|
+
createdAt: l.createdAt,
|
|
1173
|
+
updatedAt: l.updatedAt,
|
|
1174
|
+
listedRelease: l.listedRelease ? {
|
|
1175
|
+
hubReleaseId: l.listedRelease.id,
|
|
1176
|
+
name: l.listedRelease.name,
|
|
1177
|
+
tagName: l.listedRelease.tagName,
|
|
1178
|
+
createdAt: l.listedRelease.createdAt,
|
|
1179
|
+
imageName: l.listedRelease.build?.imageName,
|
|
1180
|
+
...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
|
|
1181
|
+
} : null
|
|
1182
|
+
}));
|
|
1183
|
+
return capListResult(result, {
|
|
1184
|
+
limit: params.limit,
|
|
1185
|
+
cursor: params.cursor
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
);
|
|
1189
|
+
server.tool(
|
|
1190
|
+
"deploy-hub-repo",
|
|
1191
|
+
"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.",
|
|
1192
|
+
{
|
|
1193
|
+
repo: z4.string().optional().describe(
|
|
1194
|
+
`The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
|
|
1195
|
+
),
|
|
1196
|
+
hubReleaseId: z4.string().optional().describe(
|
|
1197
|
+
"The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
|
|
1198
|
+
),
|
|
1199
|
+
name: z4.string().optional().describe(
|
|
1200
|
+
"Name for the new endpoint. Defaults to '<listing title> <release tag>'."
|
|
1201
|
+
),
|
|
1202
|
+
env: z4.record(z4.string()).optional().describe(
|
|
1203
|
+
"Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
|
|
1204
|
+
),
|
|
1205
|
+
gpuIds: z4.string().optional().describe(
|
|
1206
|
+
"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."
|
|
1207
|
+
),
|
|
1208
|
+
gpuCount: z4.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
|
|
1209
|
+
containerDiskInGb: z4.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
|
|
1210
|
+
workersMin: z4.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
|
|
1211
|
+
workersMax: z4.number().int().positive().optional().describe("Maximum workers (default: account default)."),
|
|
1212
|
+
idleTimeout: z4.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
|
|
1213
|
+
scalerType: z4.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
|
|
1214
|
+
scalerValue: z4.number().optional().describe("Autoscaler target value (default 4)."),
|
|
1215
|
+
executionTimeoutMs: z4.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
|
|
1216
|
+
flashboot: z4.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
|
|
1217
|
+
},
|
|
1218
|
+
{ title: "Deploy Hub repo", ...WRITE },
|
|
1219
|
+
async (params) => {
|
|
1220
|
+
if (!params.repo && !params.hubReleaseId) {
|
|
1221
|
+
return jsonReply2({
|
|
1222
|
+
error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
const catalog = await graphql(listingsQuery(true));
|
|
1226
|
+
let listing;
|
|
1227
|
+
if (params.hubReleaseId) {
|
|
1228
|
+
listing = catalog.listings.find(
|
|
1229
|
+
(l) => l.listedRelease?.id === params.hubReleaseId
|
|
1230
|
+
);
|
|
1231
|
+
} else {
|
|
1232
|
+
const repoKey = params.repo.toLowerCase();
|
|
1233
|
+
listing = catalog.listings.find(
|
|
1234
|
+
(l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
if (!listing) {
|
|
1238
|
+
return jsonReply2({
|
|
1239
|
+
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.`
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
const release = listing.listedRelease;
|
|
1243
|
+
if (!release?.build?.imageName) {
|
|
1244
|
+
return jsonReply2({
|
|
1245
|
+
error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
|
|
1249
|
+
return jsonReply2({
|
|
1250
|
+
error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
const config = parseReleaseConfig(release.config) ?? {};
|
|
1254
|
+
const gpuIds = params.gpuIds ?? config.gpuIds;
|
|
1255
|
+
if (!gpuIds) {
|
|
1256
|
+
return jsonReply2({
|
|
1257
|
+
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).'
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
|
|
1261
|
+
if (missingRequired.length > 0) {
|
|
1262
|
+
return jsonReply2({
|
|
1263
|
+
error: `Missing required environment variables for this release: ${missingRequired.join(
|
|
1264
|
+
", "
|
|
1265
|
+
)}. Pass them via the env parameter.`
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
|
|
1269
|
+
const minCuda = minCudaVersion(config.allowedCudaVersions);
|
|
1270
|
+
const input = {
|
|
1271
|
+
name: endpointName,
|
|
1272
|
+
hubReleaseId: release.id,
|
|
1273
|
+
type: "QB",
|
|
1274
|
+
gpuIds,
|
|
1275
|
+
gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
|
|
1276
|
+
workersMin: params.workersMin ?? 0,
|
|
1277
|
+
workersMax: params.workersMax ?? null,
|
|
1278
|
+
idleTimeout: params.idleTimeout ?? 5,
|
|
1279
|
+
scalerType: params.scalerType ?? "QUEUE_DELAY",
|
|
1280
|
+
scalerValue: params.scalerValue ?? 4,
|
|
1281
|
+
executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
|
|
1282
|
+
flashBootType: params.flashboot ?? "FLASHBOOT",
|
|
1283
|
+
locations: null,
|
|
1284
|
+
networkVolumeIds: null,
|
|
1285
|
+
compliance: [],
|
|
1286
|
+
modelReferences: [],
|
|
1287
|
+
...minCuda ? {
|
|
1288
|
+
minCudaVersion: minCuda,
|
|
1289
|
+
allowedCudaVersions: config.allowedCudaVersions.join(",")
|
|
1290
|
+
} : {},
|
|
1291
|
+
template: {
|
|
1292
|
+
name: `${endpointName}__template__${randomSuffix()}`,
|
|
1293
|
+
imageName: release.build.imageName,
|
|
1294
|
+
containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
|
|
1295
|
+
containerRegistryAuthId: "",
|
|
1296
|
+
dockerArgs: "",
|
|
1297
|
+
startScript: "",
|
|
1298
|
+
ports: "",
|
|
1299
|
+
env
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
const data = await graphqlAuthed(
|
|
1303
|
+
`
|
|
1304
|
+
mutation saveEndpoint($input: EndpointInput!) {
|
|
1305
|
+
saveEndpoint(input: $input) {
|
|
1306
|
+
id
|
|
1307
|
+
name
|
|
1308
|
+
gpuIds
|
|
1309
|
+
gpuCount
|
|
1310
|
+
workersMin
|
|
1311
|
+
workersMax
|
|
1312
|
+
idleTimeout
|
|
1313
|
+
scalerType
|
|
1314
|
+
scalerValue
|
|
1315
|
+
flashBootType
|
|
1316
|
+
templateId
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
`,
|
|
1320
|
+
{ input }
|
|
1321
|
+
);
|
|
1322
|
+
return jsonReply2({
|
|
1323
|
+
endpoint: data.saveEndpoint,
|
|
1324
|
+
deployed: {
|
|
1325
|
+
repo: `${listing.repoOwner}/${listing.repoName}`,
|
|
1326
|
+
release: release.tagName,
|
|
1327
|
+
hubReleaseId: release.id,
|
|
1328
|
+
imageName: release.build.imageName
|
|
1329
|
+
},
|
|
1330
|
+
note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/tools/public-endpoints.ts
|
|
1337
|
+
import { z as z5 } from "zod";
|
|
1338
|
+
function parseMetadata(metadata) {
|
|
1339
|
+
if (!metadata) return {};
|
|
1340
|
+
try {
|
|
1341
|
+
const parsed = JSON.parse(metadata);
|
|
1342
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1343
|
+
} catch {
|
|
1344
|
+
return {};
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function registerPublicEndpointTools(server, rt) {
|
|
1348
|
+
const { graphql } = rt;
|
|
1349
|
+
server.tool(
|
|
1350
|
+
"list-public-endpoints",
|
|
1351
|
+
"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.",
|
|
1352
|
+
{
|
|
1353
|
+
...listPaginationParams,
|
|
1354
|
+
searchTerm: z5.string().optional().describe(
|
|
1355
|
+
"Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
|
|
1356
|
+
),
|
|
1357
|
+
modality: z5.string().optional().describe(
|
|
1358
|
+
"Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
|
|
1359
|
+
),
|
|
1360
|
+
owner: z5.string().optional().describe(
|
|
1361
|
+
"Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
|
|
1362
|
+
),
|
|
1363
|
+
includeOffline: z5.boolean().optional().describe(
|
|
1364
|
+
"Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
|
|
1365
|
+
)
|
|
1366
|
+
},
|
|
1367
|
+
{ title: "List public endpoints", ...READ_ONLY },
|
|
1368
|
+
async (params) => {
|
|
1369
|
+
const data = await graphql(`
|
|
1370
|
+
query {
|
|
1371
|
+
allAiApiPublicConfigs {
|
|
1372
|
+
id
|
|
1373
|
+
aiApiId
|
|
1374
|
+
modelName
|
|
1375
|
+
displayName
|
|
1376
|
+
description
|
|
1377
|
+
metadata
|
|
1378
|
+
isLive
|
|
1379
|
+
createdAt
|
|
1380
|
+
updatedAt
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
`);
|
|
1384
|
+
let configs = data.allAiApiPublicConfigs.map((c) => ({
|
|
1385
|
+
config: c,
|
|
1386
|
+
meta: parseMetadata(c.metadata)
|
|
1387
|
+
}));
|
|
1388
|
+
if (!params.includeOffline) {
|
|
1389
|
+
configs = configs.filter(({ config }) => config.isLive);
|
|
1390
|
+
}
|
|
1391
|
+
if (params.modality) {
|
|
1392
|
+
const term = params.modality.toLowerCase();
|
|
1393
|
+
configs = configs.filter(
|
|
1394
|
+
({ meta }) => (meta.source ?? "").toLowerCase() === term
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
if (params.owner) {
|
|
1398
|
+
const term = params.owner.toLowerCase();
|
|
1399
|
+
configs = configs.filter(
|
|
1400
|
+
({ meta }) => (meta.owner ?? "").toLowerCase() === term
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
if (params.searchTerm) {
|
|
1404
|
+
const term = params.searchTerm.toLowerCase();
|
|
1405
|
+
configs = configs.filter(
|
|
1406
|
+
({ config, meta }) => [
|
|
1407
|
+
config.displayName,
|
|
1408
|
+
config.modelName,
|
|
1409
|
+
config.aiApiId,
|
|
1410
|
+
config.description,
|
|
1411
|
+
meta.owner,
|
|
1412
|
+
meta.tag,
|
|
1413
|
+
meta.source
|
|
1414
|
+
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
configs = [...configs].sort(
|
|
1418
|
+
(a, b) => a.config.displayName.localeCompare(b.config.displayName)
|
|
1419
|
+
);
|
|
1420
|
+
const result = configs.map(({ config, meta }) => ({
|
|
1421
|
+
endpointId: config.aiApiId,
|
|
1422
|
+
displayName: config.displayName,
|
|
1423
|
+
modelName: config.modelName,
|
|
1424
|
+
description: config.description,
|
|
1425
|
+
modality: meta.source,
|
|
1426
|
+
tag: meta.tag,
|
|
1427
|
+
owner: meta.owner,
|
|
1428
|
+
pricing: meta.priceString,
|
|
1429
|
+
isLive: config.isLive,
|
|
1430
|
+
baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
|
|
1431
|
+
createdAt: config.createdAt,
|
|
1432
|
+
updatedAt: config.updatedAt
|
|
1433
|
+
}));
|
|
1434
|
+
return capListResult(result, {
|
|
1435
|
+
limit: params.limit,
|
|
1436
|
+
cursor: params.cursor
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// src/tools/endpoint-gpus.ts
|
|
1443
|
+
import { z as z6 } from "zod";
|
|
1444
|
+
function registerEndpointGpuTools(server, rt) {
|
|
1445
|
+
const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
1446
|
+
server.tool(
|
|
1447
|
+
"set-endpoint-gpus",
|
|
1448
|
+
"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.",
|
|
1449
|
+
{
|
|
1450
|
+
endpointId: z6.string().describe("ID of the Serverless endpoint to update"),
|
|
1451
|
+
gpuIds: z6.string().optional().describe(
|
|
1452
|
+
"Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
|
|
1453
|
+
),
|
|
1454
|
+
pools: z6.array(z6.string()).optional().describe(
|
|
1455
|
+
"GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
|
|
1456
|
+
),
|
|
1457
|
+
excludeGpuTypeIds: z6.array(z6.string()).optional().describe(
|
|
1458
|
+
"GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
|
|
1459
|
+
),
|
|
1460
|
+
gpuCount: z6.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
|
|
1461
|
+
minCudaVersion: z6.string().optional().describe(
|
|
1462
|
+
"Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
|
|
1463
|
+
),
|
|
1464
|
+
allowedCudaVersions: z6.string().optional().describe(
|
|
1465
|
+
"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."
|
|
1466
|
+
)
|
|
1467
|
+
},
|
|
1468
|
+
{ title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
|
|
1469
|
+
async (params) => {
|
|
1470
|
+
const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
|
|
1471
|
+
...params.pools,
|
|
1472
|
+
...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
|
|
1473
|
+
].join(",") : void 0);
|
|
1474
|
+
if (!gpuIds) {
|
|
1475
|
+
return jsonReply2({
|
|
1476
|
+
error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
const data = await graphqlAuthed(`
|
|
1480
|
+
query {
|
|
1481
|
+
myself {
|
|
1482
|
+
endpoints {
|
|
1483
|
+
id
|
|
1484
|
+
name
|
|
1485
|
+
gpuIds
|
|
1486
|
+
gpuCount
|
|
1487
|
+
workersMin
|
|
1488
|
+
workersMax
|
|
1489
|
+
idleTimeout
|
|
1490
|
+
scalerType
|
|
1491
|
+
scalerValue
|
|
1492
|
+
executionTimeoutMs
|
|
1493
|
+
flashBootType
|
|
1494
|
+
type
|
|
1495
|
+
locations
|
|
1496
|
+
templateId
|
|
1497
|
+
allowedCudaVersions
|
|
1498
|
+
minCudaVersion
|
|
1499
|
+
compliance
|
|
1500
|
+
modelReferences
|
|
1501
|
+
networkVolumeIds {
|
|
1502
|
+
networkVolumeId
|
|
1503
|
+
dataCenterId
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
`);
|
|
1509
|
+
const current = data.myself.endpoints.find(
|
|
1510
|
+
(e) => e.id === params.endpointId
|
|
1511
|
+
);
|
|
1512
|
+
if (!current) {
|
|
1513
|
+
return jsonReply2({
|
|
1514
|
+
error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
const input = {
|
|
1518
|
+
id: current.id,
|
|
1519
|
+
name: current.name,
|
|
1520
|
+
gpuIds,
|
|
1521
|
+
gpuCount: params.gpuCount ?? current.gpuCount,
|
|
1522
|
+
workersMin: current.workersMin,
|
|
1523
|
+
workersMax: current.workersMax,
|
|
1524
|
+
idleTimeout: current.idleTimeout,
|
|
1525
|
+
scalerType: current.scalerType,
|
|
1526
|
+
scalerValue: current.scalerValue,
|
|
1527
|
+
executionTimeoutMs: current.executionTimeoutMs,
|
|
1528
|
+
flashBootType: current.flashBootType,
|
|
1529
|
+
type: current.type,
|
|
1530
|
+
locations: current.locations,
|
|
1531
|
+
networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
|
|
1532
|
+
// Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
|
|
1533
|
+
// ONLY, and the read shape is rejected outright.
|
|
1534
|
+
current.networkVolumeIds.map((v) => ({
|
|
1535
|
+
networkVolumeId: v.networkVolumeId
|
|
1536
|
+
}))
|
|
1537
|
+
) : null
|
|
1538
|
+
};
|
|
1539
|
+
for (const [key, value] of Object.entries({
|
|
1540
|
+
templateId: current.templateId,
|
|
1541
|
+
allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
|
|
1542
|
+
minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
|
|
1543
|
+
compliance: current.compliance,
|
|
1544
|
+
modelReferences: current.modelReferences
|
|
1545
|
+
})) {
|
|
1546
|
+
if (value !== null && value !== void 0) input[key] = value;
|
|
1547
|
+
}
|
|
1548
|
+
const result = await graphqlAuthed(
|
|
1549
|
+
`
|
|
1550
|
+
mutation saveEndpoint($input: EndpointInput!) {
|
|
1551
|
+
saveEndpoint(input: $input) {
|
|
1552
|
+
id
|
|
1553
|
+
name
|
|
1554
|
+
gpuIds
|
|
1555
|
+
gpuCount
|
|
1556
|
+
workersMin
|
|
1557
|
+
workersMax
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
`,
|
|
1561
|
+
{ input }
|
|
1562
|
+
);
|
|
1563
|
+
return jsonReply2({
|
|
1564
|
+
endpoint: result.saveEndpoint,
|
|
1565
|
+
previousGpuIds: current.gpuIds
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
843
1571
|
// src/tools/pods.ts
|
|
844
|
-
import { z as
|
|
1572
|
+
import { z as z7 } from "zod";
|
|
845
1573
|
function registerPodTools(server, rt) {
|
|
846
1574
|
const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
|
|
847
1575
|
server.tool(
|
|
@@ -849,12 +1577,12 @@ function registerPodTools(server, rt) {
|
|
|
849
1577
|
"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.",
|
|
850
1578
|
{
|
|
851
1579
|
...listPaginationParams,
|
|
852
|
-
computeType:
|
|
853
|
-
gpuTypeId:
|
|
854
|
-
dataCenterId:
|
|
855
|
-
name:
|
|
856
|
-
includeMachine:
|
|
857
|
-
includeNetworkVolume:
|
|
1580
|
+
computeType: z7.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
|
|
1581
|
+
gpuTypeId: z7.array(z7.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
|
|
1582
|
+
dataCenterId: z7.array(z7.string()).optional().describe("Filter to Pods in any of the provided data centers"),
|
|
1583
|
+
name: z7.string().optional().describe("Filter to Pods with the provided name"),
|
|
1584
|
+
includeMachine: z7.boolean().optional().describe("Include information about the machine"),
|
|
1585
|
+
includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
|
|
858
1586
|
},
|
|
859
1587
|
{ title: "List pods", ...READ_ONLY },
|
|
860
1588
|
async (params) => {
|
|
@@ -898,9 +1626,9 @@ function registerPodTools(server, rt) {
|
|
|
898
1626
|
"get-pod",
|
|
899
1627
|
"Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
|
|
900
1628
|
{
|
|
901
|
-
podId:
|
|
902
|
-
includeMachine:
|
|
903
|
-
includeNetworkVolume:
|
|
1629
|
+
podId: z7.string().describe("ID of the pod to retrieve"),
|
|
1630
|
+
includeMachine: z7.boolean().optional().describe("Include information about the machine"),
|
|
1631
|
+
includeNetworkVolume: z7.boolean().optional().describe("Include information about attached network volumes")
|
|
904
1632
|
},
|
|
905
1633
|
{ title: "Get pod", ...READ_ONLY },
|
|
906
1634
|
async (params) => {
|
|
@@ -928,26 +1656,44 @@ function registerPodTools(server, rt) {
|
|
|
928
1656
|
);
|
|
929
1657
|
server.tool(
|
|
930
1658
|
"create-pod",
|
|
931
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). If the user
|
|
1659
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
932
1660
|
{
|
|
933
|
-
name:
|
|
934
|
-
imageName:
|
|
935
|
-
|
|
1661
|
+
name: z7.string().optional().describe("Name for the pod"),
|
|
1662
|
+
imageName: z7.string().optional().describe("Docker image to use. Optional when templateId is provided."),
|
|
1663
|
+
templateId: z7.string().optional().describe(
|
|
1664
|
+
"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."
|
|
1665
|
+
),
|
|
1666
|
+
computeType: z7.enum(["GPU", "CPU"]).optional().describe(
|
|
936
1667
|
"Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
|
|
937
1668
|
),
|
|
938
|
-
cloudType:
|
|
939
|
-
gpuTypeIds:
|
|
940
|
-
gpuCount:
|
|
941
|
-
containerDiskInGb:
|
|
942
|
-
volumeInGb:
|
|
943
|
-
volumeMountPath:
|
|
944
|
-
ports:
|
|
945
|
-
env:
|
|
946
|
-
|
|
1669
|
+
cloudType: z7.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
1670
|
+
gpuTypeIds: z7.array(z7.string()).optional().describe("List of acceptable GPU types"),
|
|
1671
|
+
gpuCount: z7.number().optional().describe("Number of GPUs"),
|
|
1672
|
+
containerDiskInGb: z7.number().optional().describe("Container disk size in GB"),
|
|
1673
|
+
volumeInGb: z7.number().optional().describe("Volume size in GB"),
|
|
1674
|
+
volumeMountPath: z7.string().optional().describe("Path to mount the volume"),
|
|
1675
|
+
ports: z7.array(z7.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
1676
|
+
env: z7.record(z7.string()).optional().describe("Environment variables"),
|
|
1677
|
+
containerRegistryAuthId: z7.string().optional().describe(
|
|
1678
|
+
"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)."
|
|
1679
|
+
),
|
|
1680
|
+
dataCenterIds: z7.array(z7.string()).optional().describe("List of data centers")
|
|
947
1681
|
},
|
|
948
1682
|
{ title: "Create pod", ...WRITE },
|
|
949
1683
|
async (params) => {
|
|
950
1684
|
const backend = backendFor("pods");
|
|
1685
|
+
if (params.templateId && backend.version !== "v2") {
|
|
1686
|
+
return jsonReply2({
|
|
1687
|
+
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
1688
|
+
status: 501
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
if (!params.imageName && !params.templateId) {
|
|
1692
|
+
return jsonReply2({
|
|
1693
|
+
error: "Provide imageName, or templateId to deploy from an existing template.",
|
|
1694
|
+
status: 400
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
951
1697
|
const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
|
|
952
1698
|
const hasGpuCount = (params.gpuCount ?? 0) > 0;
|
|
953
1699
|
const wantsCpu = params.computeType === "CPU";
|
|
@@ -972,6 +1718,12 @@ function registerPodTools(server, rt) {
|
|
|
972
1718
|
});
|
|
973
1719
|
}
|
|
974
1720
|
if (wantsCpu) {
|
|
1721
|
+
if (params.templateId) {
|
|
1722
|
+
return jsonReply2({
|
|
1723
|
+
error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
|
|
1724
|
+
status: 400
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
975
1727
|
try {
|
|
976
1728
|
const result = await callRestUrl(
|
|
977
1729
|
`${restV1Base(env)}/pods`,
|
|
@@ -996,7 +1748,24 @@ function registerPodTools(server, rt) {
|
|
|
996
1748
|
}
|
|
997
1749
|
}
|
|
998
1750
|
}
|
|
999
|
-
|
|
1751
|
+
let body = backend.mapCreate(params);
|
|
1752
|
+
if (params.templateId) {
|
|
1753
|
+
let template;
|
|
1754
|
+
try {
|
|
1755
|
+
template = await callRestUrl(
|
|
1756
|
+
`${restV2Base(env)}/templates/${params.templateId}`
|
|
1757
|
+
);
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
if (error instanceof HttpError) {
|
|
1760
|
+
return jsonReply2({
|
|
1761
|
+
error: `Failed to load template ${params.templateId}: ${error.message}`,
|
|
1762
|
+
status: error.status
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
throw error;
|
|
1766
|
+
}
|
|
1767
|
+
body = { ...podBodyFromTemplate(template), ...body };
|
|
1768
|
+
}
|
|
1000
1769
|
try {
|
|
1001
1770
|
const result = await callRestUrl(
|
|
1002
1771
|
`${backend.base}${backend.list}`,
|
|
@@ -1011,11 +1780,8 @@ function registerPodTools(server, rt) {
|
|
|
1011
1780
|
}
|
|
1012
1781
|
return jsonReply2(result);
|
|
1013
1782
|
} catch (error) {
|
|
1014
|
-
if (error instanceof HttpError
|
|
1015
|
-
return jsonReply2({
|
|
1016
|
-
error: "CPU pods are not yet supported on the v2 REST API. Use a GPU pod, or create the CPU pod via the v1 API (set RUNPOD_REST_VERSION=v1).",
|
|
1017
|
-
status: 501
|
|
1018
|
-
});
|
|
1783
|
+
if (error instanceof HttpError) {
|
|
1784
|
+
return jsonReply2({ error: error.message, status: error.status });
|
|
1019
1785
|
}
|
|
1020
1786
|
throw error;
|
|
1021
1787
|
}
|
|
@@ -1025,14 +1791,14 @@ function registerPodTools(server, rt) {
|
|
|
1025
1791
|
"update-pod",
|
|
1026
1792
|
"Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
|
|
1027
1793
|
{
|
|
1028
|
-
podId:
|
|
1029
|
-
name:
|
|
1030
|
-
imageName:
|
|
1031
|
-
containerDiskInGb:
|
|
1032
|
-
volumeInGb:
|
|
1033
|
-
volumeMountPath:
|
|
1034
|
-
ports:
|
|
1035
|
-
env:
|
|
1794
|
+
podId: z7.string().describe("ID of the pod to update"),
|
|
1795
|
+
name: z7.string().optional().describe("New name for the pod"),
|
|
1796
|
+
imageName: z7.string().optional().describe("New Docker image"),
|
|
1797
|
+
containerDiskInGb: z7.number().optional().describe("New container disk size in GB"),
|
|
1798
|
+
volumeInGb: z7.number().optional().describe("New volume size in GB"),
|
|
1799
|
+
volumeMountPath: z7.string().optional().describe("New path to mount the volume"),
|
|
1800
|
+
ports: z7.array(z7.string()).optional().describe("New ports to expose"),
|
|
1801
|
+
env: z7.record(z7.string()).optional().describe("New environment variables")
|
|
1036
1802
|
},
|
|
1037
1803
|
{ title: "Update pod", ...WRITE, idempotentHint: true },
|
|
1038
1804
|
async (params) => {
|
|
@@ -1051,7 +1817,7 @@ function registerPodTools(server, rt) {
|
|
|
1051
1817
|
"start-pod",
|
|
1052
1818
|
"Start a stopped pod (resumes the same pod and its billing).",
|
|
1053
1819
|
{
|
|
1054
|
-
podId:
|
|
1820
|
+
podId: z7.string().describe("ID of the pod to start")
|
|
1055
1821
|
},
|
|
1056
1822
|
{ title: "Start pod", ...WRITE, idempotentHint: true },
|
|
1057
1823
|
async (params) => {
|
|
@@ -1063,7 +1829,7 @@ function registerPodTools(server, rt) {
|
|
|
1063
1829
|
"stop-pod",
|
|
1064
1830
|
"Stop a running pod. The pod and its disk persist; GPU billing stops.",
|
|
1065
1831
|
{
|
|
1066
|
-
podId:
|
|
1832
|
+
podId: z7.string().describe("ID of the pod to stop")
|
|
1067
1833
|
},
|
|
1068
1834
|
{ title: "Stop pod", ...WRITE, idempotentHint: true },
|
|
1069
1835
|
async (params) => {
|
|
@@ -1075,7 +1841,7 @@ function registerPodTools(server, rt) {
|
|
|
1075
1841
|
"restart-pod",
|
|
1076
1842
|
"Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
|
|
1077
1843
|
{
|
|
1078
|
-
podId:
|
|
1844
|
+
podId: z7.string().describe("ID of the pod to restart")
|
|
1079
1845
|
},
|
|
1080
1846
|
{ title: "Restart pod", ...WRITE },
|
|
1081
1847
|
async (params) => {
|
|
@@ -1090,11 +1856,29 @@ function registerPodTools(server, rt) {
|
|
|
1090
1856
|
return jsonReply2(result);
|
|
1091
1857
|
}
|
|
1092
1858
|
);
|
|
1859
|
+
server.tool(
|
|
1860
|
+
"stream-pod-logs",
|
|
1861
|
+
"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).",
|
|
1862
|
+
{
|
|
1863
|
+
podId: z7.string().describe("ID of the pod whose logs to stream"),
|
|
1864
|
+
...logStreamParams
|
|
1865
|
+
},
|
|
1866
|
+
{ title: "Stream pod logs", ...READ_ONLY },
|
|
1867
|
+
(params) => streamLogsReply(
|
|
1868
|
+
rt,
|
|
1869
|
+
{
|
|
1870
|
+
name: "stream-pod-logs",
|
|
1871
|
+
resource: "pods",
|
|
1872
|
+
logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
|
|
1873
|
+
},
|
|
1874
|
+
params
|
|
1875
|
+
)
|
|
1876
|
+
);
|
|
1093
1877
|
server.tool(
|
|
1094
1878
|
"delete-pod",
|
|
1095
1879
|
"Permanently delete a pod and its data. This cannot be undone.",
|
|
1096
1880
|
{
|
|
1097
|
-
podId:
|
|
1881
|
+
podId: z7.string().describe("ID of the pod to delete")
|
|
1098
1882
|
},
|
|
1099
1883
|
{ title: "Delete pod", ...DESTRUCTIVE },
|
|
1100
1884
|
async (params) => {
|
|
@@ -1109,7 +1893,7 @@ function registerPodTools(server, rt) {
|
|
|
1109
1893
|
}
|
|
1110
1894
|
|
|
1111
1895
|
// src/tools/endpoints.ts
|
|
1112
|
-
import { z as
|
|
1896
|
+
import { z as z8 } from "zod";
|
|
1113
1897
|
function registerEndpointTools(server, rt) {
|
|
1114
1898
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1115
1899
|
server.tool(
|
|
@@ -1117,8 +1901,8 @@ function registerEndpointTools(server, rt) {
|
|
|
1117
1901
|
"List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
|
|
1118
1902
|
{
|
|
1119
1903
|
...listPaginationParams,
|
|
1120
|
-
includeTemplate:
|
|
1121
|
-
includeWorkers:
|
|
1904
|
+
includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
|
|
1905
|
+
includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
|
|
1122
1906
|
},
|
|
1123
1907
|
{ title: "List endpoints", ...READ_ONLY },
|
|
1124
1908
|
async (params) => {
|
|
@@ -1151,9 +1935,9 @@ function registerEndpointTools(server, rt) {
|
|
|
1151
1935
|
"get-endpoint",
|
|
1152
1936
|
"Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
|
|
1153
1937
|
{
|
|
1154
|
-
endpointId:
|
|
1155
|
-
includeTemplate:
|
|
1156
|
-
includeWorkers:
|
|
1938
|
+
endpointId: z8.string().describe("ID of the endpoint to retrieve"),
|
|
1939
|
+
includeTemplate: z8.boolean().optional().describe("Include template information (v1 only)"),
|
|
1940
|
+
includeWorkers: z8.boolean().optional().describe("Include information about workers (v1 only)")
|
|
1157
1941
|
},
|
|
1158
1942
|
{ title: "Get endpoint", ...READ_ONLY },
|
|
1159
1943
|
async (params) => {
|
|
@@ -1183,32 +1967,32 @@ function registerEndpointTools(server, rt) {
|
|
|
1183
1967
|
"create-endpoint",
|
|
1184
1968
|
"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).",
|
|
1185
1969
|
{
|
|
1186
|
-
name:
|
|
1970
|
+
name: z8.string().optional().describe("Name for the endpoint"),
|
|
1187
1971
|
// --- v2 inline-config fields ---
|
|
1188
|
-
imageName:
|
|
1189
|
-
gpuPoolIds:
|
|
1972
|
+
imageName: z8.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
|
|
1973
|
+
gpuPoolIds: z8.array(z8.string()).optional().describe(
|
|
1190
1974
|
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
1191
1975
|
),
|
|
1192
|
-
gpuCount:
|
|
1193
|
-
args:
|
|
1194
|
-
containerDiskInGb:
|
|
1195
|
-
ports:
|
|
1196
|
-
env:
|
|
1197
|
-
containerRegistryAuthId:
|
|
1198
|
-
networkVolumeIds:
|
|
1199
|
-
executionTimeoutMs:
|
|
1200
|
-
flashboot:
|
|
1976
|
+
gpuCount: z8.number().optional().describe("GPUs per worker (v2)"),
|
|
1977
|
+
args: z8.string().optional().describe("Container start command/args (v2)"),
|
|
1978
|
+
containerDiskInGb: z8.number().optional().describe("Container disk size in GB (v2)"),
|
|
1979
|
+
ports: z8.array(z8.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
1980
|
+
env: z8.record(z8.string()).optional().describe("Environment variables (v2)"),
|
|
1981
|
+
containerRegistryAuthId: z8.string().optional().describe("Container registry auth id for a private image (v2)"),
|
|
1982
|
+
networkVolumeIds: z8.array(z8.string()).optional().describe("Network volume ids to attach (v2)"),
|
|
1983
|
+
executionTimeoutMs: z8.number().optional().describe("Per-job execution timeout in ms (v2)"),
|
|
1984
|
+
flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
|
|
1201
1985
|
// --- v1 template-based fields ---
|
|
1202
|
-
templateId:
|
|
1203
|
-
computeType:
|
|
1204
|
-
gpuTypeIds:
|
|
1986
|
+
templateId: z8.string().optional().describe("Template ID (v1). Required on v1."),
|
|
1987
|
+
computeType: z8.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
|
|
1988
|
+
gpuTypeIds: z8.array(z8.string()).optional().describe("List of acceptable GPU types (v1)"),
|
|
1205
1989
|
// --- shared autoscaling fields ---
|
|
1206
|
-
workersMin:
|
|
1207
|
-
workersMax:
|
|
1208
|
-
scalerType:
|
|
1209
|
-
scalerValue:
|
|
1210
|
-
idleTimeout:
|
|
1211
|
-
dataCenterIds:
|
|
1990
|
+
workersMin: z8.number().optional().describe("Minimum number of workers"),
|
|
1991
|
+
workersMax: z8.number().optional().describe("Maximum number of workers"),
|
|
1992
|
+
scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type"),
|
|
1993
|
+
scalerValue: z8.number().optional().describe("Autoscaler target value"),
|
|
1994
|
+
idleTimeout: z8.number().optional().describe("Idle timeout in seconds before scaling a worker down"),
|
|
1995
|
+
dataCenterIds: z8.array(z8.string()).optional().describe("List of preferred data centers")
|
|
1212
1996
|
},
|
|
1213
1997
|
{ title: "Create endpoint", ...WRITE },
|
|
1214
1998
|
async (params) => {
|
|
@@ -1271,25 +2055,25 @@ function registerEndpointTools(server, rt) {
|
|
|
1271
2055
|
"update-endpoint",
|
|
1272
2056
|
"Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change.",
|
|
1273
2057
|
{
|
|
1274
|
-
endpointId:
|
|
1275
|
-
name:
|
|
1276
|
-
workersMin:
|
|
1277
|
-
workersMax:
|
|
1278
|
-
idleTimeout:
|
|
1279
|
-
scalerType:
|
|
1280
|
-
scalerValue:
|
|
2058
|
+
endpointId: z8.string().describe("ID of the endpoint to update"),
|
|
2059
|
+
name: z8.string().optional().describe("New name for the endpoint"),
|
|
2060
|
+
workersMin: z8.number().optional().describe("New minimum number of workers"),
|
|
2061
|
+
workersMax: z8.number().optional().describe("New maximum number of workers"),
|
|
2062
|
+
idleTimeout: z8.number().optional().describe("New idle timeout in seconds"),
|
|
2063
|
+
scalerType: z8.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Scaler type"),
|
|
2064
|
+
scalerValue: z8.number().optional().describe("Scaler value"),
|
|
1281
2065
|
// --- v2-only inline-config fields ---
|
|
1282
|
-
imageName:
|
|
1283
|
-
gpuPoolIds:
|
|
1284
|
-
gpuCount:
|
|
1285
|
-
args:
|
|
1286
|
-
containerDiskInGb:
|
|
1287
|
-
ports:
|
|
1288
|
-
env:
|
|
1289
|
-
containerRegistryAuthId:
|
|
1290
|
-
networkVolumeIds:
|
|
1291
|
-
executionTimeoutMs:
|
|
1292
|
-
flashboot:
|
|
2066
|
+
imageName: z8.string().optional().describe("New Docker image (v2)"),
|
|
2067
|
+
gpuPoolIds: z8.array(z8.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
2068
|
+
gpuCount: z8.number().optional().describe("New GPUs per worker (v2)"),
|
|
2069
|
+
args: z8.string().optional().describe("New container args (v2)"),
|
|
2070
|
+
containerDiskInGb: z8.number().optional().describe("New container disk size in GB (v2)"),
|
|
2071
|
+
ports: z8.array(z8.string()).optional().describe("New ports (v2)"),
|
|
2072
|
+
env: z8.record(z8.string()).optional().describe("New environment variables (v2)"),
|
|
2073
|
+
containerRegistryAuthId: z8.string().optional().describe("New container registry auth id (v2)"),
|
|
2074
|
+
networkVolumeIds: z8.array(z8.string()).optional().describe("New network volume ids (v2)"),
|
|
2075
|
+
executionTimeoutMs: z8.number().optional().describe("New per-job execution timeout in ms (v2)"),
|
|
2076
|
+
flashboot: z8.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
|
|
1293
2077
|
},
|
|
1294
2078
|
{ title: "Update endpoint", ...WRITE, idempotentHint: true },
|
|
1295
2079
|
async (params) => {
|
|
@@ -1308,7 +2092,7 @@ function registerEndpointTools(server, rt) {
|
|
|
1308
2092
|
"delete-endpoint",
|
|
1309
2093
|
"Permanently delete a Serverless endpoint. This cannot be undone.",
|
|
1310
2094
|
{
|
|
1311
|
-
endpointId:
|
|
2095
|
+
endpointId: z8.string().describe("ID of the endpoint to delete")
|
|
1312
2096
|
},
|
|
1313
2097
|
{ title: "Delete endpoint", ...DESTRUCTIVE },
|
|
1314
2098
|
async (params) => {
|
|
@@ -1320,12 +2104,39 @@ function registerEndpointTools(server, rt) {
|
|
|
1320
2104
|
return jsonReply2(result);
|
|
1321
2105
|
}
|
|
1322
2106
|
);
|
|
2107
|
+
server.tool(
|
|
2108
|
+
"list-endpoint-releases",
|
|
2109
|
+
"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.",
|
|
2110
|
+
{
|
|
2111
|
+
...listPaginationParams,
|
|
2112
|
+
endpointId: z8.string().describe("ID of the Serverless endpoint whose releases to list")
|
|
2113
|
+
},
|
|
2114
|
+
{ title: "List endpoint releases", ...READ_ONLY },
|
|
2115
|
+
async (params) => {
|
|
2116
|
+
const backend = backendFor("endpoints");
|
|
2117
|
+
if (backend.version === "v1") {
|
|
2118
|
+
return jsonReply2({
|
|
2119
|
+
error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2120
|
+
status: 501
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
const raw = await callRestUrl(
|
|
2124
|
+
`${backend.base}/serverless/${params.endpointId}/releases`
|
|
2125
|
+
);
|
|
2126
|
+
const releases = Array.isArray(raw?.releases) ? raw.releases : [];
|
|
2127
|
+
return capListResult(
|
|
2128
|
+
releases,
|
|
2129
|
+
{ limit: params.limit, cursor: params.cursor },
|
|
2130
|
+
{ rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
);
|
|
1323
2134
|
server.tool(
|
|
1324
2135
|
"list-endpoint-workers",
|
|
1325
2136
|
"List the workers backing a Serverless endpoint, with their status and an aggregate summary. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
1326
2137
|
{
|
|
1327
2138
|
...listPaginationParams,
|
|
1328
|
-
endpointId:
|
|
2139
|
+
endpointId: z8.string().describe("ID of the Serverless endpoint whose workers to list")
|
|
1329
2140
|
},
|
|
1330
2141
|
{ title: "List endpoint workers", ...READ_ONLY },
|
|
1331
2142
|
async (params) => {
|
|
@@ -1346,30 +2157,51 @@ function registerEndpointTools(server, rt) {
|
|
|
1346
2157
|
);
|
|
1347
2158
|
}
|
|
1348
2159
|
);
|
|
2160
|
+
server.tool(
|
|
2161
|
+
"stream-worker-logs",
|
|
2162
|
+
"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. 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).",
|
|
2163
|
+
{
|
|
2164
|
+
endpointId: z8.string().describe("ID of the Serverless endpoint the worker belongs to"),
|
|
2165
|
+
workerId: z8.string().describe(
|
|
2166
|
+
"ID of the worker whose logs to stream (from list-endpoint-workers)"
|
|
2167
|
+
),
|
|
2168
|
+
...logStreamParams
|
|
2169
|
+
},
|
|
2170
|
+
{ title: "Stream worker logs", ...READ_ONLY },
|
|
2171
|
+
(params) => streamLogsReply(
|
|
2172
|
+
rt,
|
|
2173
|
+
{
|
|
2174
|
+
name: "stream-worker-logs",
|
|
2175
|
+
resource: "workers",
|
|
2176
|
+
logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
|
|
2177
|
+
},
|
|
2178
|
+
params
|
|
2179
|
+
)
|
|
2180
|
+
);
|
|
1349
2181
|
}
|
|
1350
2182
|
|
|
1351
2183
|
// src/tools/jobs.ts
|
|
1352
|
-
import { z as
|
|
2184
|
+
import { z as z9 } from "zod";
|
|
1353
2185
|
function registerJobTools(server, rt) {
|
|
1354
2186
|
const { jsonReply: jsonReply2, serverlessRequest } = rt;
|
|
1355
|
-
const endpointIdSchema =
|
|
1356
|
-
const jobIdSchema =
|
|
1357
|
-
const inputSchema =
|
|
2187
|
+
const endpointIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
|
|
2188
|
+
const jobIdSchema = z9.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
|
|
2189
|
+
const inputSchema = z9.record(z9.unknown()).describe(
|
|
1358
2190
|
"Input payload for the worker handler. The expected fields depend on the deployed model or worker."
|
|
1359
2191
|
);
|
|
1360
|
-
const webhookSchema =
|
|
2192
|
+
const webhookSchema = z9.string().url().optional().describe(
|
|
1361
2193
|
"Webhook URL to receive job completion notifications instead of polling"
|
|
1362
2194
|
);
|
|
1363
|
-
const policySchema =
|
|
1364
|
-
executionTimeout:
|
|
1365
|
-
lowPriority:
|
|
1366
|
-
ttl:
|
|
2195
|
+
const policySchema = z9.object({
|
|
2196
|
+
executionTimeout: z9.number().optional().describe("Maximum execution time in milliseconds"),
|
|
2197
|
+
lowPriority: z9.boolean().optional().describe("Submit as a low-priority job"),
|
|
2198
|
+
ttl: z9.number().optional().describe("Time-to-live for the job result in milliseconds")
|
|
1367
2199
|
}).optional().describe("Execution policy options");
|
|
1368
|
-
const s3ConfigSchema =
|
|
1369
|
-
accessId:
|
|
1370
|
-
accessSecret:
|
|
1371
|
-
bucketName:
|
|
1372
|
-
endpointUrl:
|
|
2200
|
+
const s3ConfigSchema = z9.object({
|
|
2201
|
+
accessId: z9.string().describe("S3 access key ID"),
|
|
2202
|
+
accessSecret: z9.string().describe("S3 secret access key"),
|
|
2203
|
+
bucketName: z9.string().describe("S3 bucket name"),
|
|
2204
|
+
endpointUrl: z9.string().describe("S3 endpoint URL")
|
|
1373
2205
|
}).optional().describe("S3-compatible storage config for large outputs");
|
|
1374
2206
|
server.tool(
|
|
1375
2207
|
"run-endpoint",
|
|
@@ -1403,7 +2235,7 @@ function registerJobTools(server, rt) {
|
|
|
1403
2235
|
"ID of the Serverless endpoint to run synchronously"
|
|
1404
2236
|
),
|
|
1405
2237
|
input: inputSchema,
|
|
1406
|
-
wait:
|
|
2238
|
+
wait: z9.number().min(1e3).max(3e5).optional().describe(
|
|
1407
2239
|
"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)."
|
|
1408
2240
|
),
|
|
1409
2241
|
webhook: webhookSchema,
|
|
@@ -1572,7 +2404,7 @@ function registerJobTools(server, rt) {
|
|
|
1572
2404
|
}
|
|
1573
2405
|
|
|
1574
2406
|
// src/tools/templates.ts
|
|
1575
|
-
import { z as
|
|
2407
|
+
import { z as z10 } from "zod";
|
|
1576
2408
|
function registerTemplateTools(server, rt) {
|
|
1577
2409
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1578
2410
|
server.tool(
|
|
@@ -1580,9 +2412,9 @@ function registerTemplateTools(server, rt) {
|
|
|
1580
2412
|
`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.`,
|
|
1581
2413
|
{
|
|
1582
2414
|
...listPaginationParams,
|
|
1583
|
-
includeRunpodTemplates:
|
|
1584
|
-
includePublicTemplates:
|
|
1585
|
-
includeEndpointBoundTemplates:
|
|
2415
|
+
includeRunpodTemplates: z10.boolean().optional().describe("Include official Runpod templates in the response"),
|
|
2416
|
+
includePublicTemplates: z10.boolean().optional().describe("Include community-made public templates in the response"),
|
|
2417
|
+
includeEndpointBoundTemplates: z10.boolean().optional().describe(
|
|
1586
2418
|
"Include templates bound to Serverless endpoints in the response"
|
|
1587
2419
|
)
|
|
1588
2420
|
},
|
|
@@ -1613,7 +2445,7 @@ function registerTemplateTools(server, rt) {
|
|
|
1613
2445
|
"get-template",
|
|
1614
2446
|
"Get one template by id (image, env, ports, disk/volume config).",
|
|
1615
2447
|
{
|
|
1616
|
-
templateId:
|
|
2448
|
+
templateId: z10.string().describe("ID of the template to retrieve")
|
|
1617
2449
|
},
|
|
1618
2450
|
{ title: "Get template", ...READ_ONLY },
|
|
1619
2451
|
async (params) => {
|
|
@@ -1626,23 +2458,27 @@ function registerTemplateTools(server, rt) {
|
|
|
1626
2458
|
);
|
|
1627
2459
|
server.tool(
|
|
1628
2460
|
"create-template",
|
|
1629
|
-
"Create a reusable pod/endpoint template (image, ports, env, disk/volume).
|
|
2461
|
+
"Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
|
|
1630
2462
|
{
|
|
1631
|
-
name:
|
|
1632
|
-
imageName:
|
|
1633
|
-
isServerless:
|
|
1634
|
-
ports:
|
|
1635
|
-
dockerEntrypoint:
|
|
1636
|
-
|
|
1637
|
-
env: z6.record(z6.string()).optional().describe("Environment variables"),
|
|
1638
|
-
containerDiskInGb: z6.number().optional().describe("Container disk size in GB"),
|
|
1639
|
-
volumeInGb: z6.number().optional().describe("Volume size in GB"),
|
|
1640
|
-
volumeMountPath: z6.string().optional().describe("Path to mount the volume"),
|
|
1641
|
-
readme: z6.string().optional().describe("README content in markdown format"),
|
|
1642
|
-
category: z6.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
|
|
1643
|
-
"Hardware category for the template. On v2 a category is required and defaults to NVIDIA when omitted \u2014 set CPU or AMD for non-NVIDIA templates so they are not mis-categorized."
|
|
2463
|
+
name: z10.string().describe("Name for the template"),
|
|
2464
|
+
imageName: z10.string().describe("Docker image to use"),
|
|
2465
|
+
isServerless: z10.boolean().optional().describe("Is this a serverless template"),
|
|
2466
|
+
ports: z10.array(z10.string()).optional().describe("Ports to expose"),
|
|
2467
|
+
dockerEntrypoint: z10.array(z10.string()).optional().describe(
|
|
2468
|
+
"Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
|
|
1644
2469
|
),
|
|
1645
|
-
|
|
2470
|
+
dockerStartCmd: z10.array(z10.string()).optional().describe(
|
|
2471
|
+
'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 "").'
|
|
2472
|
+
),
|
|
2473
|
+
env: z10.record(z10.string()).optional().describe("Environment variables"),
|
|
2474
|
+
containerDiskInGb: z10.number().optional().describe("Container disk size in GB"),
|
|
2475
|
+
volumeInGb: z10.number().optional().describe("Volume size in GB"),
|
|
2476
|
+
volumeMountPath: z10.string().optional().describe("Path to mount the volume"),
|
|
2477
|
+
readme: z10.string().optional().describe("README content in markdown format"),
|
|
2478
|
+
category: z10.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
|
|
2479
|
+
"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."
|
|
2480
|
+
),
|
|
2481
|
+
containerRegistryAuthId: z10.string().optional().describe(
|
|
1646
2482
|
"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."
|
|
1647
2483
|
)
|
|
1648
2484
|
},
|
|
@@ -1660,15 +2496,18 @@ function registerTemplateTools(server, rt) {
|
|
|
1660
2496
|
);
|
|
1661
2497
|
server.tool(
|
|
1662
2498
|
"update-template",
|
|
1663
|
-
"Update a template's mutable fields (name, image, ports, env,
|
|
2499
|
+
"Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
|
|
1664
2500
|
{
|
|
1665
|
-
templateId:
|
|
1666
|
-
name:
|
|
1667
|
-
imageName:
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
2501
|
+
templateId: z10.string().describe("ID of the template to update"),
|
|
2502
|
+
name: z10.string().optional().describe("New name for the template"),
|
|
2503
|
+
imageName: z10.string().optional().describe("New Docker image"),
|
|
2504
|
+
dockerStartCmd: z10.array(z10.string()).optional().describe(
|
|
2505
|
+
"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."
|
|
2506
|
+
),
|
|
2507
|
+
ports: z10.array(z10.string()).optional().describe("New ports to expose"),
|
|
2508
|
+
env: z10.record(z10.string()).optional().describe("New environment variables"),
|
|
2509
|
+
readme: z10.string().optional().describe("New README content in markdown format"),
|
|
2510
|
+
containerRegistryAuthId: z10.string().optional().describe(
|
|
1672
2511
|
"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."
|
|
1673
2512
|
)
|
|
1674
2513
|
},
|
|
@@ -1689,7 +2528,7 @@ function registerTemplateTools(server, rt) {
|
|
|
1689
2528
|
"delete-template",
|
|
1690
2529
|
"Permanently delete a template. This cannot be undone.",
|
|
1691
2530
|
{
|
|
1692
|
-
templateId:
|
|
2531
|
+
templateId: z10.string().describe("ID of the template to delete")
|
|
1693
2532
|
},
|
|
1694
2533
|
{ title: "Delete template", ...DESTRUCTIVE },
|
|
1695
2534
|
async (params) => {
|
|
@@ -1704,7 +2543,7 @@ function registerTemplateTools(server, rt) {
|
|
|
1704
2543
|
}
|
|
1705
2544
|
|
|
1706
2545
|
// src/tools/network-volumes.ts
|
|
1707
|
-
import { z as
|
|
2546
|
+
import { z as z11 } from "zod";
|
|
1708
2547
|
function registerNetworkVolumeTools(server, rt) {
|
|
1709
2548
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1710
2549
|
server.tool(
|
|
@@ -1725,7 +2564,7 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1725
2564
|
"get-network-volume",
|
|
1726
2565
|
"Get one network volume by id (name, size, data center).",
|
|
1727
2566
|
{
|
|
1728
|
-
networkVolumeId:
|
|
2567
|
+
networkVolumeId: z11.string().describe("ID of the network volume to retrieve")
|
|
1729
2568
|
},
|
|
1730
2569
|
{ title: "Get network volume", ...READ_ONLY },
|
|
1731
2570
|
async (params) => {
|
|
@@ -1738,11 +2577,17 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1738
2577
|
);
|
|
1739
2578
|
server.tool(
|
|
1740
2579
|
"create-network-volume",
|
|
1741
|
-
"Create a persistent network volume in a data center (size
|
|
2580
|
+
"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.",
|
|
1742
2581
|
{
|
|
1743
|
-
name:
|
|
1744
|
-
|
|
1745
|
-
|
|
2582
|
+
name: z11.string().describe("Name for the network volume"),
|
|
2583
|
+
// Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
|
|
2584
|
+
// here as well as described, so an out-of-range value fails locally
|
|
2585
|
+
// instead of costing an API roundtrip.
|
|
2586
|
+
size: z11.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
|
|
2587
|
+
dataCenterId: z11.string().describe("Data center ID (see list-data-centers)"),
|
|
2588
|
+
volumeType: z11.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
|
|
2589
|
+
"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."
|
|
2590
|
+
)
|
|
1746
2591
|
},
|
|
1747
2592
|
{ title: "Create network volume", ...WRITE },
|
|
1748
2593
|
async (params) => {
|
|
@@ -1760,9 +2605,11 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1760
2605
|
"update-network-volume",
|
|
1761
2606
|
"Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
|
|
1762
2607
|
{
|
|
1763
|
-
networkVolumeId:
|
|
1764
|
-
name:
|
|
1765
|
-
|
|
2608
|
+
networkVolumeId: z11.string().describe("ID of the network volume to update"),
|
|
2609
|
+
name: z11.string().optional().describe("New name for the network volume"),
|
|
2610
|
+
// Same absolute bounds as create. "Larger than current" stays server-side —
|
|
2611
|
+
// we do not hold the current size here, so only the range is checked.
|
|
2612
|
+
size: z11.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
|
|
1766
2613
|
},
|
|
1767
2614
|
{ title: "Update network volume", ...WRITE, idempotentHint: true },
|
|
1768
2615
|
async (params) => {
|
|
@@ -1781,7 +2628,7 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1781
2628
|
"delete-network-volume",
|
|
1782
2629
|
"Permanently delete a network volume and its data. This cannot be undone.",
|
|
1783
2630
|
{
|
|
1784
|
-
networkVolumeId:
|
|
2631
|
+
networkVolumeId: z11.string().describe("ID of the network volume to delete")
|
|
1785
2632
|
},
|
|
1786
2633
|
{ title: "Delete network volume", ...DESTRUCTIVE },
|
|
1787
2634
|
async (params) => {
|
|
@@ -1796,9 +2643,14 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
1796
2643
|
}
|
|
1797
2644
|
|
|
1798
2645
|
// src/tools/registries.ts
|
|
1799
|
-
import { z as
|
|
2646
|
+
import { z as z12 } from "zod";
|
|
2647
|
+
var DELEGATIONS = "/delegations";
|
|
1800
2648
|
function registerRegistryTools(server, rt) {
|
|
1801
2649
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
2650
|
+
const delegationsV2OnlyNotice = (tool) => ({
|
|
2651
|
+
error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
2652
|
+
status: 501
|
|
2653
|
+
});
|
|
1802
2654
|
server.tool(
|
|
1803
2655
|
"list-container-registry-auths",
|
|
1804
2656
|
"List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
|
|
@@ -1817,7 +2669,7 @@ function registerRegistryTools(server, rt) {
|
|
|
1817
2669
|
"get-container-registry-auth",
|
|
1818
2670
|
"Get one saved container-registry credential by id (the secret/password is not returned).",
|
|
1819
2671
|
{
|
|
1820
|
-
containerRegistryAuthId:
|
|
2672
|
+
containerRegistryAuthId: z12.string().describe("ID of the container registry auth to retrieve")
|
|
1821
2673
|
},
|
|
1822
2674
|
{ title: "Get container registry auth", ...READ_ONLY },
|
|
1823
2675
|
async (params) => {
|
|
@@ -1832,9 +2684,9 @@ function registerRegistryTools(server, rt) {
|
|
|
1832
2684
|
"create-container-registry-auth",
|
|
1833
2685
|
"Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
|
|
1834
2686
|
{
|
|
1835
|
-
name:
|
|
1836
|
-
username:
|
|
1837
|
-
password:
|
|
2687
|
+
name: z12.string().describe("Name for the container registry auth"),
|
|
2688
|
+
username: z12.string().describe("Registry username"),
|
|
2689
|
+
password: z12.string().describe("Registry password or access token (stored as a secret)")
|
|
1838
2690
|
},
|
|
1839
2691
|
{ title: "Create container registry auth", ...WRITE },
|
|
1840
2692
|
async (params) => {
|
|
@@ -1852,7 +2704,7 @@ function registerRegistryTools(server, rt) {
|
|
|
1852
2704
|
"delete-container-registry-auth",
|
|
1853
2705
|
"Permanently delete a saved container-registry credential. This cannot be undone.",
|
|
1854
2706
|
{
|
|
1855
|
-
containerRegistryAuthId:
|
|
2707
|
+
containerRegistryAuthId: z12.string().describe("ID of the container registry auth to delete")
|
|
1856
2708
|
},
|
|
1857
2709
|
{ title: "Delete container registry auth", ...DESTRUCTIVE },
|
|
1858
2710
|
async (params) => {
|
|
@@ -1864,166 +2716,69 @@ function registerRegistryTools(server, rt) {
|
|
|
1864
2716
|
return jsonReply2(result);
|
|
1865
2717
|
}
|
|
1866
2718
|
);
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
// src/tools/tags.ts
|
|
1870
|
-
import { z as z9 } from "zod";
|
|
1871
|
-
var TAG_RESOURCE_TYPES = [
|
|
1872
|
-
"POD",
|
|
1873
|
-
"NETWORK_VOLUME",
|
|
1874
|
-
"CLUSTER",
|
|
1875
|
-
"SERVERLESS_ENDPOINT"
|
|
1876
|
-
];
|
|
1877
|
-
var taggedResourceSchema = z9.object({
|
|
1878
|
-
id: z9.string().describe("Resource identifier (e.g. a pod or endpoint id)"),
|
|
1879
|
-
type: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to associate with the tag")
|
|
1880
|
-
});
|
|
1881
|
-
function v2OnlyNotice(tool) {
|
|
1882
|
-
return {
|
|
1883
|
-
error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
1884
|
-
status: 501
|
|
1885
|
-
};
|
|
1886
|
-
}
|
|
1887
|
-
function registerTagTools(server, rt) {
|
|
1888
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1889
2719
|
server.tool(
|
|
1890
|
-
"list-
|
|
1891
|
-
"List your
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
includeResources: z9.boolean().optional().describe("Include the resources attached to each tag in the response")
|
|
1895
|
-
},
|
|
1896
|
-
{ title: "List tags", ...READ_ONLY },
|
|
2720
|
+
"list-registry-delegations",
|
|
2721
|
+
"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.",
|
|
2722
|
+
listPaginationParams,
|
|
2723
|
+
{ title: "List registry delegations", ...READ_ONLY },
|
|
1897
2724
|
async (params) => {
|
|
1898
|
-
const backend = backendFor("
|
|
1899
|
-
if (backend.version === "v1")
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2725
|
+
const backend = backendFor("registries");
|
|
2726
|
+
if (backend.version === "v1") {
|
|
2727
|
+
return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
|
|
2728
|
+
}
|
|
2729
|
+
const raw = await callRestUrl(
|
|
2730
|
+
`${backend.base}${backend.list}${DELEGATIONS}`
|
|
2731
|
+
);
|
|
2732
|
+
const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
|
|
2733
|
+
return capListResult(delegations, {
|
|
1903
2734
|
limit: params.limit,
|
|
1904
2735
|
cursor: params.cursor
|
|
1905
2736
|
});
|
|
1906
2737
|
}
|
|
1907
2738
|
);
|
|
1908
2739
|
server.tool(
|
|
1909
|
-
"
|
|
1910
|
-
"
|
|
1911
|
-
{
|
|
1912
|
-
tagId: z9.string().describe("ID of the tag to retrieve"),
|
|
1913
|
-
includeResources: z9.boolean().optional().describe("Include the resources attached to this tag")
|
|
1914
|
-
},
|
|
1915
|
-
{ title: "Get tag", ...READ_ONLY },
|
|
1916
|
-
async (params) => {
|
|
1917
|
-
const backend = backendFor("tags");
|
|
1918
|
-
if (backend.version === "v1") return jsonReply2(v2OnlyNotice("get-tag"));
|
|
1919
|
-
const query = params.includeResources ? "?include=resources" : "";
|
|
1920
|
-
const result = await callRestUrl(
|
|
1921
|
-
`${backend.base}${backend.get(params.tagId)}${query}`
|
|
1922
|
-
);
|
|
1923
|
-
return jsonReply2(result);
|
|
1924
|
-
}
|
|
1925
|
-
);
|
|
1926
|
-
server.tool(
|
|
1927
|
-
"create-tag",
|
|
1928
|
-
"Create a key/value tag, optionally attaching resources at creation. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2740
|
+
"create-registry-delegation",
|
|
2741
|
+
"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.",
|
|
1929
2742
|
{
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2743
|
+
resource: z12.string().min(1).describe(
|
|
2744
|
+
'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
|
|
2745
|
+
),
|
|
2746
|
+
name: z12.string().optional().describe("Optional human-readable name for the delegation")
|
|
1933
2747
|
},
|
|
1934
|
-
{ title: "Create
|
|
2748
|
+
{ title: "Create registry delegation", ...WRITE },
|
|
1935
2749
|
async (params) => {
|
|
1936
|
-
const backend = backendFor("
|
|
1937
|
-
if (backend.version === "v1")
|
|
1938
|
-
return jsonReply2(
|
|
2750
|
+
const backend = backendFor("registries");
|
|
2751
|
+
if (backend.version === "v1") {
|
|
2752
|
+
return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
|
|
2753
|
+
}
|
|
2754
|
+
const body = { resource: params.resource };
|
|
2755
|
+
if (params.name !== void 0) body.name = params.name;
|
|
1939
2756
|
const result = await callRestUrl(
|
|
1940
|
-
`${backend.base}${backend.list}`,
|
|
2757
|
+
`${backend.base}${backend.list}${DELEGATIONS}`,
|
|
1941
2758
|
"POST",
|
|
1942
|
-
|
|
2759
|
+
body
|
|
1943
2760
|
);
|
|
1944
2761
|
return jsonReply2(result);
|
|
1945
2762
|
}
|
|
1946
2763
|
);
|
|
1947
2764
|
server.tool(
|
|
1948
|
-
"
|
|
1949
|
-
"
|
|
2765
|
+
"delete-registry-delegation",
|
|
2766
|
+
"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.",
|
|
1950
2767
|
{
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
value: z9.string().min(1).optional().describe("New tag value"),
|
|
1954
|
-
resources: z9.array(taggedResourceSchema).optional().describe(
|
|
1955
|
-
"Replace the tag's resource associations with exactly these. Omit to leave unchanged; pass [] to detach all."
|
|
2768
|
+
delegationId: z12.string().describe(
|
|
2769
|
+
"ID of the delegation to revoke (from list-registry-delegations)"
|
|
1956
2770
|
)
|
|
1957
2771
|
},
|
|
1958
|
-
|
|
1959
|
-
// retried-after-partial-failure update is not guaranteed to be a no-op.
|
|
1960
|
-
{ title: "Update tag", ...WRITE },
|
|
1961
|
-
async (params) => {
|
|
1962
|
-
const { tagId, ...updateParams } = params;
|
|
1963
|
-
const backend = backendFor("tags");
|
|
1964
|
-
if (backend.version === "v1")
|
|
1965
|
-
return jsonReply2(v2OnlyNotice("update-tag"));
|
|
1966
|
-
const result = await callRestUrl(
|
|
1967
|
-
`${backend.base}${backend.get(tagId)}`,
|
|
1968
|
-
"PATCH",
|
|
1969
|
-
updateParams
|
|
1970
|
-
);
|
|
1971
|
-
return jsonReply2(result);
|
|
1972
|
-
}
|
|
1973
|
-
);
|
|
1974
|
-
server.tool(
|
|
1975
|
-
"delete-tag",
|
|
1976
|
-
"Permanently delete a tag (removes it from all resources it is attached to). This cannot be undone. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
1977
|
-
{
|
|
1978
|
-
tagId: z9.string().describe("ID of the tag to delete")
|
|
1979
|
-
},
|
|
1980
|
-
{ title: "Delete tag", ...DESTRUCTIVE },
|
|
2772
|
+
{ title: "Revoke registry delegation", ...DESTRUCTIVE },
|
|
1981
2773
|
async (params) => {
|
|
1982
|
-
const backend = backendFor("
|
|
1983
|
-
if (backend.version === "v1")
|
|
1984
|
-
return jsonReply2(
|
|
1985
|
-
|
|
1986
|
-
`${backend.base}${backend.get(params.tagId)}`,
|
|
1987
|
-
"DELETE"
|
|
1988
|
-
);
|
|
1989
|
-
return jsonReply2(result);
|
|
1990
|
-
}
|
|
1991
|
-
);
|
|
1992
|
-
server.tool(
|
|
1993
|
-
"attach-tag",
|
|
1994
|
-
"Attach a tag to a resource (pod, network volume, cluster, or serverless endpoint). Idempotent \u2014 attaching an already-associated resource is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
1995
|
-
{
|
|
1996
|
-
tagId: z9.string().describe("ID of the tag"),
|
|
1997
|
-
resourceType: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to attach the tag to"),
|
|
1998
|
-
resourceId: z9.string().describe("ID of the resource to attach the tag to")
|
|
1999
|
-
},
|
|
2000
|
-
{ title: "Attach tag to resource", ...WRITE, idempotentHint: true },
|
|
2001
|
-
async (params) => {
|
|
2002
|
-
const backend = backendFor("tags");
|
|
2003
|
-
if (backend.version === "v1")
|
|
2004
|
-
return jsonReply2(v2OnlyNotice("attach-tag"));
|
|
2005
|
-
const result = await callRestUrl(
|
|
2006
|
-
`${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
|
|
2007
|
-
"PUT"
|
|
2008
|
-
);
|
|
2009
|
-
return jsonReply2(result);
|
|
2010
|
-
}
|
|
2011
|
-
);
|
|
2012
|
-
server.tool(
|
|
2013
|
-
"detach-tag",
|
|
2014
|
-
"Detach a tag from a resource (removes the association only \u2014 the resource and tag are not deleted). Idempotent \u2014 detaching a resource that is not associated is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2015
|
-
{
|
|
2016
|
-
tagId: z9.string().describe("ID of the tag"),
|
|
2017
|
-
resourceType: z9.enum(TAG_RESOURCE_TYPES).describe("Type of resource to detach the tag from"),
|
|
2018
|
-
resourceId: z9.string().describe("ID of the resource to detach the tag from")
|
|
2019
|
-
},
|
|
2020
|
-
{ title: "Detach tag from resource", ...WRITE, idempotentHint: true },
|
|
2021
|
-
async (params) => {
|
|
2022
|
-
const backend = backendFor("tags");
|
|
2023
|
-
if (backend.version === "v1")
|
|
2024
|
-
return jsonReply2(v2OnlyNotice("detach-tag"));
|
|
2774
|
+
const backend = backendFor("registries");
|
|
2775
|
+
if (backend.version === "v1") {
|
|
2776
|
+
return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
|
|
2777
|
+
}
|
|
2025
2778
|
const result = await callRestUrl(
|
|
2026
|
-
`${backend.base}
|
|
2779
|
+
`${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
|
|
2780
|
+
params.delegationId
|
|
2781
|
+
)}`,
|
|
2027
2782
|
"DELETE"
|
|
2028
2783
|
);
|
|
2029
2784
|
return jsonReply2(result);
|
|
@@ -2032,7 +2787,7 @@ function registerTagTools(server, rt) {
|
|
|
2032
2787
|
}
|
|
2033
2788
|
|
|
2034
2789
|
// src/tools/billing.ts
|
|
2035
|
-
import { z as
|
|
2790
|
+
import { z as z13 } from "zod";
|
|
2036
2791
|
var BILLING_SCOPE_PATH = {
|
|
2037
2792
|
all: "/billing",
|
|
2038
2793
|
pods: "/billing/pods",
|
|
@@ -2048,7 +2803,7 @@ function registerBillingTools(server, rt) {
|
|
|
2048
2803
|
"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.",
|
|
2049
2804
|
{
|
|
2050
2805
|
...listPaginationParams,
|
|
2051
|
-
scope:
|
|
2806
|
+
scope: z13.enum([
|
|
2052
2807
|
"all",
|
|
2053
2808
|
"pods",
|
|
2054
2809
|
"serverless",
|
|
@@ -2058,14 +2813,14 @@ function registerBillingTools(server, rt) {
|
|
|
2058
2813
|
]).optional().describe(
|
|
2059
2814
|
'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
|
|
2060
2815
|
),
|
|
2061
|
-
startTime:
|
|
2816
|
+
startTime: z13.string().optional().describe(
|
|
2062
2817
|
"Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
|
|
2063
2818
|
),
|
|
2064
|
-
endTime:
|
|
2819
|
+
endTime: z13.string().optional().describe(
|
|
2065
2820
|
"End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
|
|
2066
2821
|
),
|
|
2067
|
-
bucketSize:
|
|
2068
|
-
lastN:
|
|
2822
|
+
bucketSize: z13.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
|
|
2823
|
+
lastN: z13.number().int().positive().optional().describe(
|
|
2069
2824
|
"Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
|
|
2070
2825
|
)
|
|
2071
2826
|
},
|
|
@@ -2100,17 +2855,24 @@ function registerBillingTools(server, rt) {
|
|
|
2100
2855
|
function registerTools(server, ctx, deps = {}) {
|
|
2101
2856
|
const rt = createToolRuntime(server, ctx, deps);
|
|
2102
2857
|
registerCatalogTools(server, rt);
|
|
2858
|
+
registerHubTools(server, rt);
|
|
2859
|
+
registerPublicEndpointTools(server, rt);
|
|
2103
2860
|
registerPodTools(server, rt);
|
|
2104
2861
|
registerEndpointTools(server, rt);
|
|
2862
|
+
registerEndpointGpuTools(server, rt);
|
|
2105
2863
|
registerJobTools(server, rt);
|
|
2106
2864
|
registerTemplateTools(server, rt);
|
|
2107
2865
|
registerNetworkVolumeTools(server, rt);
|
|
2108
2866
|
registerRegistryTools(server, rt);
|
|
2109
|
-
registerTagTools(server, rt);
|
|
2110
2867
|
registerBillingTools(server, rt);
|
|
2111
2868
|
}
|
|
2112
2869
|
|
|
2113
2870
|
export {
|
|
2871
|
+
restV1Base,
|
|
2872
|
+
restV2Base,
|
|
2873
|
+
serverlessBase,
|
|
2874
|
+
authedGraphqlBase,
|
|
2875
|
+
restVersionSummary,
|
|
2114
2876
|
registerTools
|
|
2115
2877
|
};
|
|
2116
|
-
//# sourceMappingURL=chunk-
|
|
2878
|
+
//# sourceMappingURL=chunk-GTBCNGHE.mjs.map
|