@runpod/mcp-server 3.2.0 → 3.4.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 +72 -0
- package/dist/{chunk-WGK7D2YR.mjs → chunk-G4URDODF.mjs} +504 -112
- package/dist/chunk-G4URDODF.mjs.map +1 -0
- package/dist/http.cjs +521 -112
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +19 -2
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +1116 -218
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +505 -113
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +503 -111
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +10 -1
- package/dist/tools.d.ts +10 -1
- package/dist/tools.mjs +1 -1
- package/dist/wizard-FBLOZ6YM.mjs +901 -0
- package/dist/wizard-FBLOZ6YM.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-WGK7D2YR.mjs.map +0 -1
- package/dist/wizard-37ZJSVB5.mjs +0 -402
- package/dist/wizard-37ZJSVB5.mjs.map +0 -1
package/dist/tools.cjs
CHANGED
|
@@ -187,6 +187,28 @@ var HttpError = class extends Error {
|
|
|
187
187
|
this.body = body;
|
|
188
188
|
}
|
|
189
189
|
};
|
|
190
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
191
|
+
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
192
|
+
var RequestTimeoutError = class extends Error {
|
|
193
|
+
constructor(prefix, timeoutMs, method) {
|
|
194
|
+
const write = method !== "GET" && method !== "HEAD";
|
|
195
|
+
super(
|
|
196
|
+
`${prefix}: no response after ${timeoutMs}ms for ${method} (the API may be overloaded, or still be working on it). ${write ? "This request may have SUCCEEDED upstream \u2014 do not retry blindly. Check with the matching list-/get- tool first and retry only if it did not take effect." : "This was a read and changed nothing, so retrying is safe."}`
|
|
197
|
+
);
|
|
198
|
+
this.name = "RequestTimeoutError";
|
|
199
|
+
this.timeoutMs = timeoutMs;
|
|
200
|
+
this.method = method;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
204
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
205
|
+
try {
|
|
206
|
+
return await run(signal);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
209
|
+
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
190
212
|
function isJsonContentType(contentType) {
|
|
191
213
|
if (!contentType) return false;
|
|
192
214
|
const ct = contentType.toLowerCase();
|
|
@@ -202,28 +224,49 @@ function buildRequestHeaders(apiKey, tracking) {
|
|
|
202
224
|
...tracking()
|
|
203
225
|
};
|
|
204
226
|
}
|
|
227
|
+
function resolveCeiling(ceiling) {
|
|
228
|
+
if (ceiling === void 0) return Infinity;
|
|
229
|
+
if (typeof ceiling !== "function") return ceiling;
|
|
230
|
+
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
231
|
+
}
|
|
232
|
+
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
233
|
+
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
234
|
+
}
|
|
205
235
|
function createHttpClient(deps) {
|
|
206
|
-
|
|
207
|
-
|
|
236
|
+
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
237
|
+
return async function request(url, method = "GET", body, options) {
|
|
238
|
+
const timeoutMs = clampTimeout(
|
|
239
|
+
options?.timeoutMs,
|
|
240
|
+
deps.maxTimeoutMs,
|
|
241
|
+
defaultTimeoutMs
|
|
242
|
+
);
|
|
243
|
+
return withRequestTimeout(
|
|
244
|
+
deps.errorPrefix,
|
|
245
|
+
timeoutMs,
|
|
208
246
|
method,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
response.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
247
|
+
async (signal) => {
|
|
248
|
+
const init = {
|
|
249
|
+
method,
|
|
250
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
251
|
+
signal
|
|
252
|
+
};
|
|
253
|
+
if (body && methodSendsBody(method)) {
|
|
254
|
+
init.body = JSON.stringify(body);
|
|
255
|
+
}
|
|
256
|
+
const response = await deps.fetch(url, init);
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
const hint = response.status === 429 ? rateLimitHint(
|
|
259
|
+
response.headers.get("ratelimit"),
|
|
260
|
+
response.headers.get("retry-after")
|
|
261
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
262
|
+
const text = await response.text().catch(
|
|
263
|
+
() => "<body unavailable: the connection closed while reading it>"
|
|
264
|
+
);
|
|
265
|
+
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
266
|
+
}
|
|
267
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
268
|
+
}
|
|
269
|
+
);
|
|
227
270
|
};
|
|
228
271
|
}
|
|
229
272
|
|
|
@@ -373,10 +416,15 @@ function containerConfigToV2(p) {
|
|
|
373
416
|
mounts: persistentMount(p.volumeInGb, p.volumeMountPath)
|
|
374
417
|
});
|
|
375
418
|
}
|
|
376
|
-
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
419
|
+
function gpuConfigToV2(gpuTypeIds, gpuCount, allowedCudaVersions, minCudaVersion2) {
|
|
377
420
|
const id = gpuTypeIds?.[0];
|
|
378
421
|
if (id === void 0) return void 0;
|
|
379
|
-
return compact({
|
|
422
|
+
return compact({
|
|
423
|
+
id,
|
|
424
|
+
count: gpuCount ?? 1,
|
|
425
|
+
allowedCudaVersions,
|
|
426
|
+
minCudaVersion: minCudaVersion2
|
|
427
|
+
});
|
|
380
428
|
}
|
|
381
429
|
function mapPodCreateToV2(params) {
|
|
382
430
|
return compact({
|
|
@@ -397,7 +445,12 @@ function mapPodCreateToV2(params) {
|
|
|
397
445
|
// singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
|
|
398
446
|
// create-pod handler warns when more than one gpuTypeId was supplied.
|
|
399
447
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
400
|
-
gpu: gpuConfigToV2(
|
|
448
|
+
gpu: gpuConfigToV2(
|
|
449
|
+
params.gpuTypeIds,
|
|
450
|
+
params.gpuCount,
|
|
451
|
+
params.allowedCudaVersions,
|
|
452
|
+
params.minCudaVersion
|
|
453
|
+
)
|
|
401
454
|
});
|
|
402
455
|
}
|
|
403
456
|
function mapPodUpdateToV2(params) {
|
|
@@ -414,9 +467,15 @@ function defaultScalerType(endpointType) {
|
|
|
414
467
|
function endpointScaling(scalerType, value) {
|
|
415
468
|
return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
|
|
416
469
|
}
|
|
417
|
-
function endpointGpuConfig(
|
|
418
|
-
|
|
419
|
-
|
|
470
|
+
function endpointGpuConfig(params, mode) {
|
|
471
|
+
const gpu = compact({
|
|
472
|
+
pools: params.gpuPoolIds?.length ? params.gpuPoolIds : void 0,
|
|
473
|
+
count: params.gpuCount,
|
|
474
|
+
allowedCudaVersions: params.allowedCudaVersions,
|
|
475
|
+
minCudaVersion: params.minCudaVersion
|
|
476
|
+
});
|
|
477
|
+
if (mode === "create" && !("pools" in gpu)) return void 0;
|
|
478
|
+
return Object.keys(gpu).length ? gpu : void 0;
|
|
420
479
|
}
|
|
421
480
|
function endpointWorkers(params) {
|
|
422
481
|
const workers = compact({
|
|
@@ -426,7 +485,7 @@ function endpointWorkers(params) {
|
|
|
426
485
|
});
|
|
427
486
|
return Object.keys(workers).length ? workers : void 0;
|
|
428
487
|
}
|
|
429
|
-
function endpointCommonToV2(params) {
|
|
488
|
+
function endpointCommonToV2(params, mode) {
|
|
430
489
|
return compact({
|
|
431
490
|
name: params.name,
|
|
432
491
|
image: params.imageName,
|
|
@@ -435,7 +494,7 @@ function endpointCommonToV2(params) {
|
|
|
435
494
|
ports: params.ports,
|
|
436
495
|
env: params.env,
|
|
437
496
|
registry: params.containerRegistryAuthId,
|
|
438
|
-
gpu: endpointGpuConfig(params
|
|
497
|
+
gpu: endpointGpuConfig(params, mode),
|
|
439
498
|
workers: endpointWorkers(params),
|
|
440
499
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
441
500
|
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
@@ -446,7 +505,7 @@ function endpointCommonToV2(params) {
|
|
|
446
505
|
function mapEndpointCreateToV2(params) {
|
|
447
506
|
const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
|
|
448
507
|
return compact({
|
|
449
|
-
...endpointCommonToV2(params),
|
|
508
|
+
...endpointCommonToV2(params, "create"),
|
|
450
509
|
type: endpointType,
|
|
451
510
|
scaling: endpointScaling(
|
|
452
511
|
params.scalerType ?? defaultScalerType(endpointType),
|
|
@@ -456,13 +515,28 @@ function mapEndpointCreateToV2(params) {
|
|
|
456
515
|
}
|
|
457
516
|
function mapEndpointUpdateToV2(params) {
|
|
458
517
|
return compact({
|
|
459
|
-
...endpointCommonToV2(params),
|
|
518
|
+
...endpointCommonToV2(params, "update"),
|
|
460
519
|
scaling: params.scalerType ? endpointScaling(
|
|
461
520
|
params.scalerType,
|
|
462
521
|
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
463
522
|
) : void 0
|
|
464
523
|
});
|
|
465
524
|
}
|
|
525
|
+
var CUDA_MAJOR_MINOR = /^\d+\.\d+$/;
|
|
526
|
+
function cudaConstraintError(params, opts = {}) {
|
|
527
|
+
const { allowedCudaVersions, minCudaVersion: minCudaVersion2 } = params;
|
|
528
|
+
if (allowedCudaVersions?.length && minCudaVersion2) {
|
|
529
|
+
return "allowedCudaVersions and minCudaVersion are mutually exclusive \u2014 pass either an exact set or an open-ended floor, not both.";
|
|
530
|
+
}
|
|
531
|
+
if (minCudaVersion2 !== void 0 && !CUDA_MAJOR_MINOR.test(minCudaVersion2) && !(opts.allowClear && minCudaVersion2 === "")) {
|
|
532
|
+
return `minCudaVersion must be major.minor (e.g. "12.0", not "12")${opts.allowClear ? "; an empty string clears the floor" : ""}.`;
|
|
533
|
+
}
|
|
534
|
+
const bad = allowedCudaVersions?.find((v) => !CUDA_MAJOR_MINOR.test(v));
|
|
535
|
+
if (bad !== void 0) {
|
|
536
|
+
return `allowedCudaVersions entries must be major.minor (e.g. "12.8"); got "${bad}". Discover valid values via get-capacity or list-gpu-types.`;
|
|
537
|
+
}
|
|
538
|
+
return void 0;
|
|
539
|
+
}
|
|
466
540
|
function mapNetworkVolumeCreateToV2(params) {
|
|
467
541
|
return compact({
|
|
468
542
|
name: params.name,
|
|
@@ -503,6 +577,42 @@ function podBodyFromTemplate(template) {
|
|
|
503
577
|
}
|
|
504
578
|
return out;
|
|
505
579
|
}
|
|
580
|
+
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
581
|
+
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
582
|
+
function normalizeSshPublicKey(input) {
|
|
583
|
+
if (PRIVATE_KEY_MARKER.test(input))
|
|
584
|
+
return {
|
|
585
|
+
ok: false,
|
|
586
|
+
error: 'sshPublicKey looks like a PRIVATE key. Pass the PUBLIC key instead \u2014 the contents of your ~/.ssh/<key>.pub file (e.g. "ssh-ed25519 AAAA\u2026").'
|
|
587
|
+
};
|
|
588
|
+
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
589
|
+
if (lines.length === 0)
|
|
590
|
+
return {
|
|
591
|
+
ok: false,
|
|
592
|
+
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
593
|
+
};
|
|
594
|
+
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
595
|
+
return {
|
|
596
|
+
ok: false,
|
|
597
|
+
error: 'sshPublicKey is not an SSH public key. Expected the CONTENTS of your ~/.ssh/<key>.pub file \u2014 a line like "ssh-ed25519 AAAAC3Nza\u2026 you@host" (ssh-*, ecdsa-*, or sk-* followed by the base64 key) \u2014 not a file path, a SHA256 fingerprint, or a bare base64 blob. Newline-separate multiple keys.'
|
|
598
|
+
};
|
|
599
|
+
return { ok: true, key: lines.join("\n") };
|
|
600
|
+
}
|
|
601
|
+
function applySshPublicKey(body, sshPublicKey) {
|
|
602
|
+
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
603
|
+
const hasSshPort = ports.some((p) => /^\s*22\/tcp\s*$/i.test(String(p)));
|
|
604
|
+
const env = { ...body.env ?? {} };
|
|
605
|
+
const key = sshPublicKey.trim();
|
|
606
|
+
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
607
|
+
else if (!env.PUBLIC_KEY.includes(key))
|
|
608
|
+
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
609
|
+
${key}`;
|
|
610
|
+
return {
|
|
611
|
+
...body,
|
|
612
|
+
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
613
|
+
env
|
|
614
|
+
};
|
|
615
|
+
}
|
|
506
616
|
function mapTemplateUpdateToV2(params) {
|
|
507
617
|
return {
|
|
508
618
|
...containerConfigToV2(params),
|
|
@@ -709,7 +819,7 @@ function resolveBackend(opts) {
|
|
|
709
819
|
}
|
|
710
820
|
|
|
711
821
|
// src/tools/runtime.ts
|
|
712
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
822
|
+
var MCP_SERVER_VERSION = true ? "3.4.0" : "dev";
|
|
713
823
|
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
714
824
|
function trackingHeaders(server, ctx) {
|
|
715
825
|
const info = server.server.getClientVersion();
|
|
@@ -724,6 +834,11 @@ function trackingHeaders(server, ctx) {
|
|
|
724
834
|
sessionId: SESSION_ID
|
|
725
835
|
});
|
|
726
836
|
}
|
|
837
|
+
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
838
|
+
function invocationBudget(totalMs) {
|
|
839
|
+
const startedAt = Date.now();
|
|
840
|
+
return () => totalMs - (Date.now() - startedAt);
|
|
841
|
+
}
|
|
727
842
|
var defaultFetch = import_node_fetch.default;
|
|
728
843
|
async function withApiErrorLog(label, call) {
|
|
729
844
|
try {
|
|
@@ -747,6 +862,23 @@ var DESTRUCTIVE = {
|
|
|
747
862
|
openWorldHint: true
|
|
748
863
|
};
|
|
749
864
|
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
865
|
+
return withRequestTimeout(
|
|
866
|
+
"Runpod GraphQL Error",
|
|
867
|
+
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
868
|
+
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
869
|
+
// off the method, and telling a caller its list-gpu-types "may have
|
|
870
|
+
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
871
|
+
// it back at the tool that just failed. The operation keyword is what
|
|
872
|
+
// actually says whether anything could have been written.
|
|
873
|
+
isGraphqlMutation(query) ? "POST" : "GET",
|
|
874
|
+
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
function isGraphqlMutation(query) {
|
|
878
|
+
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
879
|
+
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
880
|
+
}
|
|
881
|
+
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
750
882
|
const response = await fetchImpl(url, {
|
|
751
883
|
method: "POST",
|
|
752
884
|
headers: {
|
|
@@ -757,7 +889,8 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
757
889
|
body: JSON.stringify({
|
|
758
890
|
query,
|
|
759
891
|
...options?.variables ? { variables: options.variables } : {}
|
|
760
|
-
})
|
|
892
|
+
}),
|
|
893
|
+
signal
|
|
761
894
|
});
|
|
762
895
|
if (!response.ok) {
|
|
763
896
|
const bodyText = await response.text();
|
|
@@ -813,39 +946,53 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
813
946
|
}
|
|
814
947
|
const rawFetch = deps.fetch ?? defaultFetch;
|
|
815
948
|
const httpFetch = observeUnauthorized(rawFetch);
|
|
949
|
+
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
950
|
+
const timeouts = {
|
|
951
|
+
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
952
|
+
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
953
|
+
};
|
|
816
954
|
const v1Client = createHttpClient({
|
|
817
955
|
apiKey: ctx.apiKey,
|
|
818
956
|
fetch: httpFetch,
|
|
819
957
|
tracking,
|
|
820
|
-
errorPrefix: "Runpod API Error"
|
|
958
|
+
errorPrefix: "Runpod API Error",
|
|
959
|
+
...timeouts
|
|
821
960
|
});
|
|
822
|
-
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
961
|
+
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
823
962
|
"Error calling Runpod API:",
|
|
824
|
-
() => v1Client(
|
|
963
|
+
() => v1Client(
|
|
964
|
+
`${restV1Base(process.env)}${endpoint}`,
|
|
965
|
+
method,
|
|
966
|
+
body,
|
|
967
|
+
options
|
|
968
|
+
)
|
|
825
969
|
);
|
|
826
970
|
const serverlessClient = createHttpClient({
|
|
827
971
|
apiKey: ctx.apiKey,
|
|
828
972
|
fetch: httpFetch,
|
|
829
973
|
tracking,
|
|
830
|
-
errorPrefix: "Runpod Serverless API Error"
|
|
974
|
+
errorPrefix: "Runpod Serverless API Error",
|
|
975
|
+
...timeouts
|
|
831
976
|
});
|
|
832
|
-
const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
|
|
977
|
+
const serverlessRequest = (endpointId, path, method = "GET", body, options) => withApiErrorLog(
|
|
833
978
|
"Error calling Runpod Serverless API:",
|
|
834
979
|
() => serverlessClient(
|
|
835
980
|
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
836
981
|
method,
|
|
837
|
-
body
|
|
982
|
+
body,
|
|
983
|
+
options
|
|
838
984
|
)
|
|
839
985
|
);
|
|
840
986
|
const restClient = createHttpClient({
|
|
841
987
|
apiKey: ctx.apiKey,
|
|
842
988
|
fetch: httpFetch,
|
|
843
989
|
tracking,
|
|
844
|
-
errorPrefix: "Runpod API Error"
|
|
990
|
+
errorPrefix: "Runpod API Error",
|
|
991
|
+
...timeouts
|
|
845
992
|
});
|
|
846
|
-
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
993
|
+
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
847
994
|
"Error calling Runpod API:",
|
|
848
|
-
() => restClient(url, method, body)
|
|
995
|
+
() => restClient(url, method, body, options)
|
|
849
996
|
);
|
|
850
997
|
const sseFetch = observeUnauthorized(
|
|
851
998
|
deps.sseFetch ?? import_node_fetch.default
|
|
@@ -878,12 +1025,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
878
1025
|
};
|
|
879
1026
|
return {
|
|
880
1027
|
jsonReply,
|
|
1028
|
+
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
1029
|
+
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
881
1030
|
graphql: (query) => graphqlRequest(
|
|
882
1031
|
query,
|
|
883
1032
|
tracking,
|
|
884
1033
|
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
885
1034
|
rawFetch,
|
|
886
|
-
publicGraphqlBase(process.env)
|
|
1035
|
+
publicGraphqlBase(process.env),
|
|
1036
|
+
{
|
|
1037
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1038
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1039
|
+
}
|
|
887
1040
|
),
|
|
888
1041
|
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
889
1042
|
query,
|
|
@@ -891,7 +1044,12 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
891
1044
|
httpFetch,
|
|
892
1045
|
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
893
1046
|
authedGraphqlBase(process.env),
|
|
894
|
-
{
|
|
1047
|
+
{
|
|
1048
|
+
variables,
|
|
1049
|
+
apiKey: ctx.apiKey,
|
|
1050
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1051
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1052
|
+
}
|
|
895
1053
|
),
|
|
896
1054
|
runpodRequest,
|
|
897
1055
|
serverlessRequest,
|
|
@@ -899,7 +1057,8 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
899
1057
|
backendFor,
|
|
900
1058
|
streamSse,
|
|
901
1059
|
podAction,
|
|
902
|
-
env: process.env
|
|
1060
|
+
env: process.env,
|
|
1061
|
+
transport: ctx.transport
|
|
903
1062
|
};
|
|
904
1063
|
}
|
|
905
1064
|
|
|
@@ -968,6 +1127,7 @@ function capListResult(result, options, extra) {
|
|
|
968
1127
|
}
|
|
969
1128
|
|
|
970
1129
|
// src/tools/catalog.ts
|
|
1130
|
+
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
971
1131
|
function registerCatalogTools(server, rt) {
|
|
972
1132
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
973
1133
|
server.tool(
|
|
@@ -986,15 +1146,50 @@ function registerCatalogTools(server, rt) {
|
|
|
986
1146
|
),
|
|
987
1147
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
988
1148
|
"Request realtime stock and annotate each GPU with an availability summary (HIGH/MEDIUM/LOW/NONE). Default true. Set false to skip the availability lookup \u2014 then out-of-stock GPUs cannot be filtered."
|
|
1149
|
+
),
|
|
1150
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1151
|
+
"Product context for the availability lookup \u2014 the same GPU can be scarce for Pods and plentiful for Serverless. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1152
|
+
),
|
|
1153
|
+
minCudaVersion: import_zod3.z.string().optional().describe(
|
|
1154
|
+
'Scope the availability lookup to hosts at or above this CUDA version (v2 only). Integer major or major.minor \u2014 "12" means any 12.x release; unlike the create-pod/create-endpoint body fields, a bare major is accepted here because the filter only widens a read. Requires includeAvailability (the default).'
|
|
989
1155
|
)
|
|
990
1156
|
},
|
|
991
1157
|
{ title: "List GPU types", ...READ_ONLY },
|
|
992
1158
|
async (params) => {
|
|
993
1159
|
const backend = backendFor("gpus");
|
|
1160
|
+
if (params.minCudaVersion !== void 0 && backend.version !== "v2") {
|
|
1161
|
+
return jsonReply2({
|
|
1162
|
+
error: "The minCudaVersion filter is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or use get-capacity for per-CUDA-version availability.",
|
|
1163
|
+
status: 501
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
994
1166
|
if (backend.version === "v2") {
|
|
995
1167
|
const wantAvailability = params.includeAvailability !== false;
|
|
1168
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1169
|
+
if (params.minCudaVersion !== void 0) {
|
|
1170
|
+
if (!wantAvailability) {
|
|
1171
|
+
return jsonReply2({
|
|
1172
|
+
error: "minCudaVersion filters the availability lookup, so it requires includeAvailability (the default). Drop includeAvailability:false or the filter.",
|
|
1173
|
+
status: 400
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
if (!/^\d+(\.\d+)?$/.test(params.minCudaVersion)) {
|
|
1177
|
+
return jsonReply2({
|
|
1178
|
+
error: 'minCudaVersion must be an integer major ("12") or major.minor ("12.1").',
|
|
1179
|
+
status: 400
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
const query = new URLSearchParams();
|
|
1184
|
+
if (wantAvailability) {
|
|
1185
|
+
query.set("include", "AVAILABILITY");
|
|
1186
|
+
query.set("product", product);
|
|
1187
|
+
if (params.minCudaVersion !== void 0) {
|
|
1188
|
+
query.set("minCudaVersion", params.minCudaVersion);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
996
1191
|
const raw = await callRestUrl(
|
|
997
|
-
`${backend.base}${backend.list}${
|
|
1192
|
+
`${backend.base}${backend.list}${query.size ? `?${query}` : ""}`
|
|
998
1193
|
);
|
|
999
1194
|
let gpus = backend.unwrap(raw);
|
|
1000
1195
|
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
@@ -1167,6 +1362,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1167
1362
|
gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
|
|
1168
1363
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1169
1364
|
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1365
|
+
),
|
|
1366
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1367
|
+
"Product context for the availability lookup \u2014 stock differs by product. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1170
1368
|
)
|
|
1171
1369
|
},
|
|
1172
1370
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
@@ -1179,7 +1377,8 @@ function registerCatalogTools(server, rt) {
|
|
|
1179
1377
|
});
|
|
1180
1378
|
}
|
|
1181
1379
|
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1182
|
-
const
|
|
1380
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1381
|
+
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1183
1382
|
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1184
1383
|
return jsonReply2(result);
|
|
1185
1384
|
}
|
|
@@ -2059,7 +2258,7 @@ function registerPodTools(server, rt) {
|
|
|
2059
2258
|
);
|
|
2060
2259
|
server.tool(
|
|
2061
2260
|
"create-pod",
|
|
2062
|
-
'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.',
|
|
2261
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). For full SSH access (including SCP/SFTP/rsync), pass sshPublicKey \u2014 it sets the PUBLIC_KEY env var and exposes 22/tcp, which official Runpod images turn into a running sshd with the key authorized. If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2063
2262
|
{
|
|
2064
2263
|
name: import_zod7.z.string().optional().describe(
|
|
2065
2264
|
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
@@ -2073,20 +2272,48 @@ function registerPodTools(server, rt) {
|
|
|
2073
2272
|
),
|
|
2074
2273
|
cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2075
2274
|
gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
|
|
2076
|
-
|
|
2275
|
+
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
2276
|
+
// a negative, or a fractional count reached the wire as-is and came back
|
|
2277
|
+
// as a raw 422; reject it here with a message that names the field.
|
|
2278
|
+
gpuCount: import_zod7.z.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2077
2279
|
containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
|
|
2078
2280
|
volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
|
|
2079
2281
|
volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
|
|
2080
2282
|
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2081
2283
|
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
|
|
2284
|
+
sshPublicKey: import_zod7.z.string().optional().describe(
|
|
2285
|
+
'SSH PUBLIC key(s) to authorize on the pod \u2014 the CONTENTS of your ~/.ssh/<key>.pub file, i.e. a literal "ssh-ed25519 AAAAC3Nza\u2026 you@host" line, NOT a path to the file, a fingerprint, or the private key; newline-separate multiple keys. Merges the key into the PUBLIC_KEY env var and ensures 22/tcp is exposed, enabling full SSH (SCP/SFTP/rsync) on any image that honors the PUBLIC_KEY convention \u2014 all runpod/* official images do. Extends template ports/env rather than replacing them.'
|
|
2286
|
+
),
|
|
2082
2287
|
containerRegistryAuthId: import_zod7.z.string().optional().describe(
|
|
2083
2288
|
"Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
|
|
2084
2289
|
),
|
|
2085
|
-
dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers")
|
|
2290
|
+
dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers"),
|
|
2291
|
+
allowedCudaVersions: import_zod7.z.array(import_zod7.z.string()).optional().describe(
|
|
2292
|
+
'Acceptable host CUDA versions as major.minor, e.g. ["12.8","12.6"] (v2, GPU pods only). Exact match \u2014 discover valid values per GPU type with get-capacity. Mutually exclusive with minCudaVersion.'
|
|
2293
|
+
),
|
|
2294
|
+
minCudaVersion: import_zod7.z.string().optional().describe(
|
|
2295
|
+
'Lowest acceptable host CUDA version as major.minor, e.g. "12.4" (v2, GPU pods only). A bare major like "12" is rejected. Open-ended floor; mutually exclusive with a non-empty allowedCudaVersions.'
|
|
2296
|
+
)
|
|
2086
2297
|
},
|
|
2087
2298
|
{ title: "Create pod", ...WRITE },
|
|
2088
2299
|
async (params) => {
|
|
2089
2300
|
const backend = backendFor("pods");
|
|
2301
|
+
const { sshPublicKey, ...podParams } = params;
|
|
2302
|
+
let sshKey;
|
|
2303
|
+
if (sshPublicKey !== void 0) {
|
|
2304
|
+
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
2305
|
+
if (!validated.ok)
|
|
2306
|
+
return jsonReply2({ error: validated.error, status: 400 });
|
|
2307
|
+
sshKey = validated.key;
|
|
2308
|
+
}
|
|
2309
|
+
const sshNote = "sshPublicKey applied: PUBLIC_KEY env set and 22/tcp exposed. Full SSH (incl. SCP/SFTP/rsync) also requires an image that installs PUBLIC_KEY into authorized_keys and starts sshd (all runpod/* official images do) and a machine with a public IP \u2014 read the pod\u2019s portMappings for port 22 once it is running.";
|
|
2310
|
+
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2311
|
+
if (hasCudaParams && backend.version !== "v2") {
|
|
2312
|
+
return jsonReply2({
|
|
2313
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2314
|
+
status: 501
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2090
2317
|
if (params.templateId && backend.version !== "v2") {
|
|
2091
2318
|
return jsonReply2({
|
|
2092
2319
|
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
@@ -2122,6 +2349,16 @@ function registerPodTools(server, rt) {
|
|
|
2122
2349
|
status: 400
|
|
2123
2350
|
});
|
|
2124
2351
|
}
|
|
2352
|
+
if (hasCudaParams && !wantsGpu) {
|
|
2353
|
+
return jsonReply2({
|
|
2354
|
+
error: "allowedCudaVersions/minCudaVersion apply to GPU pods only \u2014 pass gpuTypeIds (see list-gpu-types).",
|
|
2355
|
+
status: 400
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
if (hasCudaParams) {
|
|
2359
|
+
const cudaError = cudaConstraintError(params);
|
|
2360
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2361
|
+
}
|
|
2125
2362
|
if (!wantsCpu && !params.name && !params.templateId) {
|
|
2126
2363
|
return jsonReply2({
|
|
2127
2364
|
error: "Provide a name for the pod (deploying from a templateId inherits the template name).",
|
|
@@ -2136,13 +2373,15 @@ function registerPodTools(server, rt) {
|
|
|
2136
2373
|
});
|
|
2137
2374
|
}
|
|
2138
2375
|
try {
|
|
2376
|
+
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2139
2377
|
const result = await callRestUrl(
|
|
2140
2378
|
`${restV1Base(env)}/pods`,
|
|
2141
2379
|
"POST",
|
|
2142
|
-
|
|
2380
|
+
cpuBody
|
|
2143
2381
|
);
|
|
2144
2382
|
return jsonReply2({
|
|
2145
2383
|
...result,
|
|
2384
|
+
...sshKey ? { _ssh: sshNote } : {},
|
|
2146
2385
|
_servedBy: "v1",
|
|
2147
2386
|
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2148
2387
|
});
|
|
@@ -2159,7 +2398,7 @@ function registerPodTools(server, rt) {
|
|
|
2159
2398
|
}
|
|
2160
2399
|
}
|
|
2161
2400
|
}
|
|
2162
|
-
let body = backend.mapCreate(
|
|
2401
|
+
let body = backend.mapCreate(podParams);
|
|
2163
2402
|
if (params.templateId) {
|
|
2164
2403
|
let template;
|
|
2165
2404
|
try {
|
|
@@ -2177,19 +2416,21 @@ function registerPodTools(server, rt) {
|
|
|
2177
2416
|
}
|
|
2178
2417
|
body = { ...podBodyFromTemplate(template), ...body };
|
|
2179
2418
|
}
|
|
2419
|
+
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2180
2420
|
try {
|
|
2181
2421
|
const result = await callRestUrl(
|
|
2182
2422
|
`${backend.base}${backend.list}`,
|
|
2183
2423
|
"POST",
|
|
2184
2424
|
body
|
|
2185
2425
|
);
|
|
2426
|
+
const extras = {};
|
|
2427
|
+
if (sshKey) extras._ssh = sshNote;
|
|
2186
2428
|
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2187
|
-
|
|
2188
|
-
...result,
|
|
2189
|
-
_warning: `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`
|
|
2190
|
-
});
|
|
2429
|
+
extras._warning = `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`;
|
|
2191
2430
|
}
|
|
2192
|
-
return jsonReply2(
|
|
2431
|
+
return jsonReply2(
|
|
2432
|
+
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
2433
|
+
);
|
|
2193
2434
|
} catch (error) {
|
|
2194
2435
|
if (error instanceof HttpError) {
|
|
2195
2436
|
return jsonReply2({ error: error.message, status: error.status });
|
|
@@ -2307,6 +2548,9 @@ function registerPodTools(server, rt) {
|
|
|
2307
2548
|
var import_zod8 = require("zod");
|
|
2308
2549
|
var MIN_QUEUE_DELAY = 0.5;
|
|
2309
2550
|
var MIN_REQUEST_COUNT = 1;
|
|
2551
|
+
function isRequestCountScaler(type) {
|
|
2552
|
+
return type === "REQUEST_COUNT";
|
|
2553
|
+
}
|
|
2310
2554
|
var MIN_IDLE_TIMEOUT = 1;
|
|
2311
2555
|
var MAX_IDLE_TIMEOUT = 3600;
|
|
2312
2556
|
function scalerValueError(scalerType, scalerValue) {
|
|
@@ -2399,6 +2643,12 @@ function registerEndpointTools(server, rt) {
|
|
|
2399
2643
|
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
2400
2644
|
),
|
|
2401
2645
|
gpuCount: import_zod8.z.number().optional().describe("GPUs per worker (v2)"),
|
|
2646
|
+
allowedCudaVersions: import_zod8.z.array(import_zod8.z.string()).optional().describe(
|
|
2647
|
+
'Acceptable host CUDA versions for worker placement as major.minor, e.g. ["12.8"] (v2). Exact match \u2014 see get-capacity for which versions have stock. Mutually exclusive with minCudaVersion.'
|
|
2648
|
+
),
|
|
2649
|
+
minCudaVersion: import_zod8.z.string().optional().describe(
|
|
2650
|
+
'Lowest acceptable host CUDA version as major.minor, e.g. "12.4" (v2). A bare major like "12" is rejected. Mutually exclusive with a non-empty allowedCudaVersions.'
|
|
2651
|
+
),
|
|
2402
2652
|
args: import_zod8.z.string().optional().describe("Container start command/args (v2)"),
|
|
2403
2653
|
containerDiskInGb: import_zod8.z.number().optional().describe("Container disk size in GB (v2)"),
|
|
2404
2654
|
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
@@ -2428,6 +2678,13 @@ function registerEndpointTools(server, rt) {
|
|
|
2428
2678
|
{ title: "Create endpoint", ...WRITE },
|
|
2429
2679
|
async (params) => {
|
|
2430
2680
|
const backend = backendFor("endpoints");
|
|
2681
|
+
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2682
|
+
if (hasCudaParams && backend.version !== "v2") {
|
|
2683
|
+
return jsonReply2({
|
|
2684
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2685
|
+
status: 501
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2431
2688
|
if (backend.version === "v2") {
|
|
2432
2689
|
if (!params.name) {
|
|
2433
2690
|
return jsonReply2({
|
|
@@ -2460,6 +2717,10 @@ function registerEndpointTools(server, rt) {
|
|
|
2460
2717
|
if (scalerError) {
|
|
2461
2718
|
return jsonReply2({ error: scalerError, status: 400 });
|
|
2462
2719
|
}
|
|
2720
|
+
if (hasCudaParams) {
|
|
2721
|
+
const cudaError = cudaConstraintError(params);
|
|
2722
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2723
|
+
}
|
|
2463
2724
|
const body = backend.mapCreate(params);
|
|
2464
2725
|
const result2 = await callRestUrl(
|
|
2465
2726
|
`${backend.base}${backend.list}`,
|
|
@@ -2497,7 +2758,7 @@ function registerEndpointTools(server, rt) {
|
|
|
2497
2758
|
);
|
|
2498
2759
|
server.tool(
|
|
2499
2760
|
"update-endpoint",
|
|
2500
|
-
"Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change. An endpoint's request routing (queue vs load balancer) is fixed at creation and cannot be changed here \u2014 recreate the endpoint instead.",
|
|
2761
|
+
"Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot, plus the GPU selection (gpuPoolIds/gpuCount) and its CUDA constraints (allowedCudaVersions/minCudaVersion) \u2014 CUDA-only or count-only changes do not require resending gpuPoolIds; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change. An endpoint's request routing (queue vs load balancer) is fixed at creation and cannot be changed here \u2014 recreate the endpoint instead. Note: passing gpuPoolIds replaces the GPU selection wholesale, which clears any GPU-type exclusions set elsewhere (console or set-endpoint-gpus) \u2014 the reply carries a _warning when that happens.",
|
|
2501
2762
|
{
|
|
2502
2763
|
endpointId: import_zod8.z.string().describe("ID of the endpoint to update"),
|
|
2503
2764
|
name: import_zod8.z.string().optional().describe("New name for the endpoint"),
|
|
@@ -2516,6 +2777,12 @@ function registerEndpointTools(server, rt) {
|
|
|
2516
2777
|
imageName: import_zod8.z.string().optional().describe("New Docker image (v2)"),
|
|
2517
2778
|
gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
2518
2779
|
gpuCount: import_zod8.z.number().optional().describe("New GPUs per worker (v2)"),
|
|
2780
|
+
allowedCudaVersions: import_zod8.z.array(import_zod8.z.string()).optional().describe(
|
|
2781
|
+
"New acceptable host CUDA versions as major.minor (v2). Changing only this does not require resending gpuPoolIds. An explicit [] clears the constraint. Mutually exclusive with minCudaVersion."
|
|
2782
|
+
),
|
|
2783
|
+
minCudaVersion: import_zod8.z.string().optional().describe(
|
|
2784
|
+
'New lowest acceptable host CUDA version as major.minor (v2). An empty string clears the floor; a bare major like "12" is rejected. Mutually exclusive with a non-empty allowedCudaVersions.'
|
|
2785
|
+
),
|
|
2519
2786
|
args: import_zod8.z.string().optional().describe("New container args (v2)"),
|
|
2520
2787
|
containerDiskInGb: import_zod8.z.number().optional().describe("New container disk size in GB (v2)"),
|
|
2521
2788
|
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("New ports (v2)"),
|
|
@@ -2530,14 +2797,41 @@ function registerEndpointTools(server, rt) {
|
|
|
2530
2797
|
const { endpointId, ...updateParams } = params;
|
|
2531
2798
|
const backend = backendFor("endpoints");
|
|
2532
2799
|
const url = `${backend.base}${backend.get(endpointId)}`;
|
|
2800
|
+
const hasCudaParams = updateParams.allowedCudaVersions !== void 0 || updateParams.minCudaVersion !== void 0;
|
|
2533
2801
|
if (backend.version !== "v2") {
|
|
2802
|
+
if (hasCudaParams) {
|
|
2803
|
+
return jsonReply2({
|
|
2804
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2805
|
+
status: 501
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2534
2808
|
const body2 = backend.mapUpdate(updateParams);
|
|
2535
2809
|
return jsonReply2(await callRestUrl(url, "PATCH", body2));
|
|
2536
2810
|
}
|
|
2811
|
+
if (hasCudaParams) {
|
|
2812
|
+
const cudaError = cudaConstraintError(updateParams, {
|
|
2813
|
+
allowClear: true
|
|
2814
|
+
});
|
|
2815
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2816
|
+
}
|
|
2817
|
+
if (updateParams.gpuPoolIds !== void 0 && !updateParams.gpuPoolIds.length) {
|
|
2818
|
+
return jsonReply2({
|
|
2819
|
+
error: "gpuPoolIds cannot be empty \u2014 omit it to keep the current GPU selection.",
|
|
2820
|
+
status: 400
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2537
2823
|
let scalerType = updateParams.scalerType;
|
|
2538
|
-
|
|
2824
|
+
const needScalerRead = scalerType === void 0 && updateParams.scalerValue !== void 0;
|
|
2825
|
+
const needExclusionRead = (updateParams.gpuPoolIds?.length ?? 0) > 0;
|
|
2826
|
+
let clearedExclusions = [];
|
|
2827
|
+
if (needScalerRead || needExclusionRead) {
|
|
2539
2828
|
const current = await callRestUrl(url);
|
|
2540
|
-
|
|
2829
|
+
if (needScalerRead) {
|
|
2830
|
+
scalerType = isRequestCountScaler(current?.scaling?.type) ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
2831
|
+
}
|
|
2832
|
+
if (needExclusionRead && current?.gpu?.excludedTypes?.length) {
|
|
2833
|
+
clearedExclusions = current.gpu.excludedTypes;
|
|
2834
|
+
}
|
|
2541
2835
|
}
|
|
2542
2836
|
const scalerError = scalerValueError(
|
|
2543
2837
|
scalerType,
|
|
@@ -2550,7 +2844,14 @@ function registerEndpointTools(server, rt) {
|
|
|
2550
2844
|
...updateParams,
|
|
2551
2845
|
scalerType
|
|
2552
2846
|
});
|
|
2553
|
-
|
|
2847
|
+
const result = await callRestUrl(url, "PATCH", body);
|
|
2848
|
+
if (clearedExclusions.length) {
|
|
2849
|
+
return jsonReply2({
|
|
2850
|
+
...result,
|
|
2851
|
+
_warning: `Supplying gpuPoolIds replaced the GPU selection wholesale, clearing ${clearedExclusions.length} GPU-type exclusion(s) previously set on this endpoint: ${clearedExclusions.join(", ")}. Re-apply them with set-endpoint-gpus if still wanted.`
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
return jsonReply2(result);
|
|
2554
2855
|
}
|
|
2555
2856
|
);
|
|
2556
2857
|
server.tool(
|
|
@@ -2650,8 +2951,108 @@ var import_zod9 = require("zod");
|
|
|
2650
2951
|
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2651
2952
|
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2652
2953
|
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
2954
|
+
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
2955
|
+
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
2956
|
+
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
2957
|
+
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
2958
|
+
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
2959
|
+
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
2960
|
+
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
2961
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
2962
|
+
"COMPLETED",
|
|
2963
|
+
"FAILED",
|
|
2964
|
+
"CANCELLED",
|
|
2965
|
+
"TIMED_OUT"
|
|
2966
|
+
]);
|
|
2967
|
+
function isUnfinishedJobReply(result) {
|
|
2968
|
+
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
2969
|
+
}
|
|
2970
|
+
function formatBudget(ms) {
|
|
2971
|
+
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
2972
|
+
}
|
|
2973
|
+
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
2974
|
+
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
2975
|
+
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
2976
|
+
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
2977
|
+
return Math.max(
|
|
2978
|
+
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
2979
|
+
MIN_STREAM_POLL_TIMEOUT_MS
|
|
2980
|
+
);
|
|
2981
|
+
}
|
|
2982
|
+
function sleep(ms) {
|
|
2983
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2984
|
+
}
|
|
2985
|
+
function streamPollPlan(hosted) {
|
|
2986
|
+
return hosted ? {
|
|
2987
|
+
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
2988
|
+
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
2989
|
+
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
2990
|
+
} : {
|
|
2991
|
+
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
2992
|
+
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
2993
|
+
query: ""
|
|
2994
|
+
};
|
|
2995
|
+
}
|
|
2996
|
+
var RESUME_ADVICE = "Call stream-job again to continue collecting output, get-job-status to check the job without streaming, or stream without a budget by calling the runtime API directly (GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} with a Bearer API key).";
|
|
2997
|
+
function budgetExhaustedNote(budgetMs, lastError) {
|
|
2998
|
+
return {
|
|
2999
|
+
pollingTimedOut: true,
|
|
3000
|
+
note: `Polling stopped after ${formatBudget(
|
|
3001
|
+
budgetMs
|
|
3002
|
+
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
3003
|
+
// Surface the trailing error (if any) instead of discarding it — the last
|
|
3004
|
+
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
3005
|
+
// succeeded. Cleared on success, so this is never a stale error from
|
|
3006
|
+
// minutes of healthy streaming ago.
|
|
3007
|
+
...lastError ? { lastError } : {}
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
function errorCapNote(lastError) {
|
|
3011
|
+
return {
|
|
3012
|
+
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
3013
|
+
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
async function collectJobStream(deps) {
|
|
3017
|
+
const { poll, budgetMs, holdMs } = deps;
|
|
3018
|
+
const startedAt = Date.now();
|
|
3019
|
+
const elapsed = () => Date.now() - startedAt;
|
|
3020
|
+
const chunks = [];
|
|
3021
|
+
let result = {};
|
|
3022
|
+
let consecutiveErrors = 0;
|
|
3023
|
+
let lastError;
|
|
3024
|
+
while (true) {
|
|
3025
|
+
try {
|
|
3026
|
+
const reply = await poll(
|
|
3027
|
+
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
3028
|
+
);
|
|
3029
|
+
consecutiveErrors = 0;
|
|
3030
|
+
lastError = void 0;
|
|
3031
|
+
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
3032
|
+
result = reply;
|
|
3033
|
+
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
3034
|
+
} catch (error) {
|
|
3035
|
+
consecutiveErrors++;
|
|
3036
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3037
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
3038
|
+
result = { ...result, ...errorCapNote(lastError) };
|
|
3039
|
+
break;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (elapsed() > budgetMs) {
|
|
3043
|
+
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
3044
|
+
break;
|
|
3045
|
+
}
|
|
3046
|
+
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
3047
|
+
}
|
|
3048
|
+
return { result, chunks };
|
|
3049
|
+
}
|
|
2653
3050
|
function registerJobTools(server, rt) {
|
|
2654
3051
|
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
3052
|
+
const hosted = rt.transport === "http";
|
|
3053
|
+
const streamBudget = formatBudget(
|
|
3054
|
+
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
3055
|
+
);
|
|
2655
3056
|
async function diagnoseQueuedJob(endpointId) {
|
|
2656
3057
|
const cached = diagnosisCache.get(endpointId);
|
|
2657
3058
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -2662,7 +3063,10 @@ function registerJobTools(server, rt) {
|
|
|
2662
3063
|
const backend = backendFor("workers");
|
|
2663
3064
|
if (backend.version !== "v2") return null;
|
|
2664
3065
|
const raw = await callRestUrl(
|
|
2665
|
-
`${backend.base}/serverless/${endpointId}/workers
|
|
3066
|
+
`${backend.base}/serverless/${endpointId}/workers`,
|
|
3067
|
+
"GET",
|
|
3068
|
+
void 0,
|
|
3069
|
+
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
2666
3070
|
);
|
|
2667
3071
|
const summary = raw?.summary;
|
|
2668
3072
|
const workers = raw?.workers ?? [];
|
|
@@ -2753,14 +3157,14 @@ function registerJobTools(server, rt) {
|
|
|
2753
3157
|
);
|
|
2754
3158
|
server.tool(
|
|
2755
3159
|
"runsync-endpoint",
|
|
2756
|
-
|
|
3160
|
+
`Submit a synchronous job to a Serverless endpoint and wait for the result. Best for fast tasks: if the job outlives the wait, the response returns a job ID and a non-terminal status (IN_QUEUE or IN_PROGRESS) to poll with get-job-status. Max payload 20 MB; results expire after 1 minute. ${hosted ? `This server waits up to ${HTTP_LONG_POLL_BUDGET_MS} ms \u2014 it runs behind a 60-second gateway deadline, so that is both the default and the ceiling for the wait parameter. For longer jobs use run-endpoint + get-job-status, or POST https://api.runpod.ai/v2/{endpointId}/runsync?wait=300000 directly with a Bearer API key.` : "The wait parameter extends the server-side wait to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."}`,
|
|
2757
3161
|
{
|
|
2758
3162
|
endpointId: endpointIdSchema.describe(
|
|
2759
3163
|
"ID of the Serverless endpoint to run synchronously"
|
|
2760
3164
|
),
|
|
2761
3165
|
input: inputSchema,
|
|
2762
3166
|
wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
|
|
2763
|
-
"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)."
|
|
3167
|
+
hosted ? `How long in milliseconds the server should wait for a result before returning a job ID to poll. Accepted range 1000\u2013300000, but this server clamps anything above ${HTTP_LONG_POLL_BUDGET_MS} down to ${HTTP_LONG_POLL_BUDGET_MS}, which is also the effective default.` : "How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
2764
3168
|
),
|
|
2765
3169
|
webhook: webhookSchema,
|
|
2766
3170
|
policy: policySchema,
|
|
@@ -2769,13 +3173,36 @@ function registerJobTools(server, rt) {
|
|
|
2769
3173
|
{ title: "Run endpoint (sync)", ...WRITE },
|
|
2770
3174
|
async (params) => {
|
|
2771
3175
|
const { endpointId, wait, ...body } = params;
|
|
2772
|
-
const
|
|
3176
|
+
const effectiveWait = hosted ? Math.min(
|
|
3177
|
+
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3178
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3179
|
+
) : wait;
|
|
3180
|
+
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
3181
|
+
const path = `/runsync${waitQuery}`;
|
|
2773
3182
|
const result = await serverlessRequest(
|
|
2774
3183
|
endpointId,
|
|
2775
3184
|
path,
|
|
2776
3185
|
"POST",
|
|
2777
|
-
body
|
|
3186
|
+
body,
|
|
3187
|
+
{
|
|
3188
|
+
// From the wait actually sent — on http the clamped 45s, not the
|
|
3189
|
+
// 300s the caller may have asked for.
|
|
3190
|
+
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
3191
|
+
}
|
|
2778
3192
|
);
|
|
3193
|
+
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
3194
|
+
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
3195
|
+
return jsonReply2({
|
|
3196
|
+
...result,
|
|
3197
|
+
waitClamped: {
|
|
3198
|
+
requestedMs: requestedWait,
|
|
3199
|
+
effectiveMs: effectiveWait,
|
|
3200
|
+
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
3201
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3202
|
+
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
3203
|
+
}
|
|
3204
|
+
});
|
|
3205
|
+
}
|
|
2779
3206
|
return jsonReply2(result);
|
|
2780
3207
|
}
|
|
2781
3208
|
);
|
|
@@ -2809,7 +3236,7 @@ function registerJobTools(server, rt) {
|
|
|
2809
3236
|
);
|
|
2810
3237
|
server.tool(
|
|
2811
3238
|
"stream-job",
|
|
2812
|
-
|
|
3239
|
+
`Retrieve streaming output from a Serverless job. The worker must support streaming output. Polls /stream/{jobId} and collects chunks until the status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT, for up to ${streamBudget}${hosted ? " (this server runs behind a 60-second gateway deadline)" : ""}. If the budget expires first, returns the chunks collected so far with pollingTimedOut: true \u2014 call stream-job again to resume where it left off, or get-job-status to check without streaming.${hosted ? " To collect with no budget, poll GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} yourself with a Bearer API key until the status is terminal \u2014 each request drains only the chunks buffered so far, so a single request is not the whole output." : ""}`,
|
|
2813
3240
|
{
|
|
2814
3241
|
endpointId: endpointIdSchema.describe(
|
|
2815
3242
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -2818,51 +3245,16 @@ function registerJobTools(server, rt) {
|
|
|
2818
3245
|
},
|
|
2819
3246
|
{ title: "Stream job", ...READ_ONLY },
|
|
2820
3247
|
async (params) => {
|
|
2821
|
-
const
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
let finalResult = {};
|
|
2832
|
-
let consecutiveErrors = 0;
|
|
2833
|
-
let lastError;
|
|
2834
|
-
const startTime = Date.now();
|
|
2835
|
-
while (true) {
|
|
2836
|
-
try {
|
|
2837
|
-
const result = await serverlessRequest(
|
|
2838
|
-
params.endpointId,
|
|
2839
|
-
`/stream/${params.jobId}`
|
|
2840
|
-
);
|
|
2841
|
-
consecutiveErrors = 0;
|
|
2842
|
-
if (Array.isArray(result.stream)) {
|
|
2843
|
-
allChunks.push(...result.stream);
|
|
2844
|
-
}
|
|
2845
|
-
finalResult = result;
|
|
2846
|
-
if (TERMINAL_STATUSES.has(result.status)) {
|
|
2847
|
-
break;
|
|
2848
|
-
}
|
|
2849
|
-
} catch (error) {
|
|
2850
|
-
consecutiveErrors++;
|
|
2851
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
2852
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2853
|
-
finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
|
|
2854
|
-
break;
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
if (Date.now() - startTime > MAX_POLL_TIME_MS) {
|
|
2858
|
-
finalResult.pollingTimedOut = true;
|
|
2859
|
-
finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
|
|
2860
|
-
if (lastError) finalResult.lastError = lastError;
|
|
2861
|
-
break;
|
|
2862
|
-
}
|
|
2863
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2864
|
-
}
|
|
2865
|
-
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
3248
|
+
const plan = streamPollPlan(hosted);
|
|
3249
|
+
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
3250
|
+
const { result, chunks } = await collectJobStream({
|
|
3251
|
+
budgetMs: plan.budgetMs,
|
|
3252
|
+
holdMs: plan.holdMs,
|
|
3253
|
+
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
3254
|
+
timeoutMs
|
|
3255
|
+
})
|
|
3256
|
+
});
|
|
3257
|
+
return jsonReply2({ ...result, stream: chunks });
|
|
2866
3258
|
}
|
|
2867
3259
|
);
|
|
2868
3260
|
server.tool(
|