@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
|
@@ -151,6 +151,28 @@ var HttpError = class extends Error {
|
|
|
151
151
|
this.body = body;
|
|
152
152
|
}
|
|
153
153
|
};
|
|
154
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
155
|
+
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
156
|
+
var RequestTimeoutError = class extends Error {
|
|
157
|
+
constructor(prefix, timeoutMs, method) {
|
|
158
|
+
const write = method !== "GET" && method !== "HEAD";
|
|
159
|
+
super(
|
|
160
|
+
`${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."}`
|
|
161
|
+
);
|
|
162
|
+
this.name = "RequestTimeoutError";
|
|
163
|
+
this.timeoutMs = timeoutMs;
|
|
164
|
+
this.method = method;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
168
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
169
|
+
try {
|
|
170
|
+
return await run(signal);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
173
|
+
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
154
176
|
function isJsonContentType(contentType) {
|
|
155
177
|
if (!contentType) return false;
|
|
156
178
|
const ct = contentType.toLowerCase();
|
|
@@ -166,28 +188,49 @@ function buildRequestHeaders(apiKey, tracking) {
|
|
|
166
188
|
...tracking()
|
|
167
189
|
};
|
|
168
190
|
}
|
|
191
|
+
function resolveCeiling(ceiling) {
|
|
192
|
+
if (ceiling === void 0) return Infinity;
|
|
193
|
+
if (typeof ceiling !== "function") return ceiling;
|
|
194
|
+
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
195
|
+
}
|
|
196
|
+
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
197
|
+
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
198
|
+
}
|
|
169
199
|
function createHttpClient(deps) {
|
|
170
|
-
|
|
171
|
-
|
|
200
|
+
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
201
|
+
return async function request(url, method = "GET", body, options) {
|
|
202
|
+
const timeoutMs = clampTimeout(
|
|
203
|
+
options?.timeoutMs,
|
|
204
|
+
deps.maxTimeoutMs,
|
|
205
|
+
defaultTimeoutMs
|
|
206
|
+
);
|
|
207
|
+
return withRequestTimeout(
|
|
208
|
+
deps.errorPrefix,
|
|
209
|
+
timeoutMs,
|
|
172
210
|
method,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
response.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
async (signal) => {
|
|
212
|
+
const init = {
|
|
213
|
+
method,
|
|
214
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
215
|
+
signal
|
|
216
|
+
};
|
|
217
|
+
if (body && methodSendsBody(method)) {
|
|
218
|
+
init.body = JSON.stringify(body);
|
|
219
|
+
}
|
|
220
|
+
const response = await deps.fetch(url, init);
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
const hint = response.status === 429 ? rateLimitHint(
|
|
223
|
+
response.headers.get("ratelimit"),
|
|
224
|
+
response.headers.get("retry-after")
|
|
225
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
226
|
+
const text = await response.text().catch(
|
|
227
|
+
() => "<body unavailable: the connection closed while reading it>"
|
|
228
|
+
);
|
|
229
|
+
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
230
|
+
}
|
|
231
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
232
|
+
}
|
|
233
|
+
);
|
|
191
234
|
};
|
|
192
235
|
}
|
|
193
236
|
|
|
@@ -337,10 +380,15 @@ function containerConfigToV2(p) {
|
|
|
337
380
|
mounts: persistentMount(p.volumeInGb, p.volumeMountPath)
|
|
338
381
|
});
|
|
339
382
|
}
|
|
340
|
-
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
383
|
+
function gpuConfigToV2(gpuTypeIds, gpuCount, allowedCudaVersions, minCudaVersion2) {
|
|
341
384
|
const id = gpuTypeIds?.[0];
|
|
342
385
|
if (id === void 0) return void 0;
|
|
343
|
-
return compact({
|
|
386
|
+
return compact({
|
|
387
|
+
id,
|
|
388
|
+
count: gpuCount ?? 1,
|
|
389
|
+
allowedCudaVersions,
|
|
390
|
+
minCudaVersion: minCudaVersion2
|
|
391
|
+
});
|
|
344
392
|
}
|
|
345
393
|
function mapPodCreateToV2(params) {
|
|
346
394
|
return compact({
|
|
@@ -361,7 +409,12 @@ function mapPodCreateToV2(params) {
|
|
|
361
409
|
// singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
|
|
362
410
|
// create-pod handler warns when more than one gpuTypeId was supplied.
|
|
363
411
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
364
|
-
gpu: gpuConfigToV2(
|
|
412
|
+
gpu: gpuConfigToV2(
|
|
413
|
+
params.gpuTypeIds,
|
|
414
|
+
params.gpuCount,
|
|
415
|
+
params.allowedCudaVersions,
|
|
416
|
+
params.minCudaVersion
|
|
417
|
+
)
|
|
365
418
|
});
|
|
366
419
|
}
|
|
367
420
|
function mapPodUpdateToV2(params) {
|
|
@@ -378,9 +431,15 @@ function defaultScalerType(endpointType) {
|
|
|
378
431
|
function endpointScaling(scalerType, value) {
|
|
379
432
|
return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
|
|
380
433
|
}
|
|
381
|
-
function endpointGpuConfig(
|
|
382
|
-
|
|
383
|
-
|
|
434
|
+
function endpointGpuConfig(params, mode) {
|
|
435
|
+
const gpu = compact({
|
|
436
|
+
pools: params.gpuPoolIds?.length ? params.gpuPoolIds : void 0,
|
|
437
|
+
count: params.gpuCount,
|
|
438
|
+
allowedCudaVersions: params.allowedCudaVersions,
|
|
439
|
+
minCudaVersion: params.minCudaVersion
|
|
440
|
+
});
|
|
441
|
+
if (mode === "create" && !("pools" in gpu)) return void 0;
|
|
442
|
+
return Object.keys(gpu).length ? gpu : void 0;
|
|
384
443
|
}
|
|
385
444
|
function endpointWorkers(params) {
|
|
386
445
|
const workers = compact({
|
|
@@ -390,7 +449,7 @@ function endpointWorkers(params) {
|
|
|
390
449
|
});
|
|
391
450
|
return Object.keys(workers).length ? workers : void 0;
|
|
392
451
|
}
|
|
393
|
-
function endpointCommonToV2(params) {
|
|
452
|
+
function endpointCommonToV2(params, mode) {
|
|
394
453
|
return compact({
|
|
395
454
|
name: params.name,
|
|
396
455
|
image: params.imageName,
|
|
@@ -399,7 +458,7 @@ function endpointCommonToV2(params) {
|
|
|
399
458
|
ports: params.ports,
|
|
400
459
|
env: params.env,
|
|
401
460
|
registry: params.containerRegistryAuthId,
|
|
402
|
-
gpu: endpointGpuConfig(params
|
|
461
|
+
gpu: endpointGpuConfig(params, mode),
|
|
403
462
|
workers: endpointWorkers(params),
|
|
404
463
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
405
464
|
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
@@ -410,7 +469,7 @@ function endpointCommonToV2(params) {
|
|
|
410
469
|
function mapEndpointCreateToV2(params) {
|
|
411
470
|
const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
|
|
412
471
|
return compact({
|
|
413
|
-
...endpointCommonToV2(params),
|
|
472
|
+
...endpointCommonToV2(params, "create"),
|
|
414
473
|
type: endpointType,
|
|
415
474
|
scaling: endpointScaling(
|
|
416
475
|
params.scalerType ?? defaultScalerType(endpointType),
|
|
@@ -420,13 +479,28 @@ function mapEndpointCreateToV2(params) {
|
|
|
420
479
|
}
|
|
421
480
|
function mapEndpointUpdateToV2(params) {
|
|
422
481
|
return compact({
|
|
423
|
-
...endpointCommonToV2(params),
|
|
482
|
+
...endpointCommonToV2(params, "update"),
|
|
424
483
|
scaling: params.scalerType ? endpointScaling(
|
|
425
484
|
params.scalerType,
|
|
426
485
|
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
427
486
|
) : void 0
|
|
428
487
|
});
|
|
429
488
|
}
|
|
489
|
+
var CUDA_MAJOR_MINOR = /^\d+\.\d+$/;
|
|
490
|
+
function cudaConstraintError(params, opts = {}) {
|
|
491
|
+
const { allowedCudaVersions, minCudaVersion: minCudaVersion2 } = params;
|
|
492
|
+
if (allowedCudaVersions?.length && minCudaVersion2) {
|
|
493
|
+
return "allowedCudaVersions and minCudaVersion are mutually exclusive \u2014 pass either an exact set or an open-ended floor, not both.";
|
|
494
|
+
}
|
|
495
|
+
if (minCudaVersion2 !== void 0 && !CUDA_MAJOR_MINOR.test(minCudaVersion2) && !(opts.allowClear && minCudaVersion2 === "")) {
|
|
496
|
+
return `minCudaVersion must be major.minor (e.g. "12.0", not "12")${opts.allowClear ? "; an empty string clears the floor" : ""}.`;
|
|
497
|
+
}
|
|
498
|
+
const bad = allowedCudaVersions?.find((v) => !CUDA_MAJOR_MINOR.test(v));
|
|
499
|
+
if (bad !== void 0) {
|
|
500
|
+
return `allowedCudaVersions entries must be major.minor (e.g. "12.8"); got "${bad}". Discover valid values via get-capacity or list-gpu-types.`;
|
|
501
|
+
}
|
|
502
|
+
return void 0;
|
|
503
|
+
}
|
|
430
504
|
function mapNetworkVolumeCreateToV2(params) {
|
|
431
505
|
return compact({
|
|
432
506
|
name: params.name,
|
|
@@ -467,6 +541,42 @@ function podBodyFromTemplate(template) {
|
|
|
467
541
|
}
|
|
468
542
|
return out;
|
|
469
543
|
}
|
|
544
|
+
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
545
|
+
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
546
|
+
function normalizeSshPublicKey(input) {
|
|
547
|
+
if (PRIVATE_KEY_MARKER.test(input))
|
|
548
|
+
return {
|
|
549
|
+
ok: false,
|
|
550
|
+
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").'
|
|
551
|
+
};
|
|
552
|
+
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
553
|
+
if (lines.length === 0)
|
|
554
|
+
return {
|
|
555
|
+
ok: false,
|
|
556
|
+
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
557
|
+
};
|
|
558
|
+
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
559
|
+
return {
|
|
560
|
+
ok: false,
|
|
561
|
+
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.'
|
|
562
|
+
};
|
|
563
|
+
return { ok: true, key: lines.join("\n") };
|
|
564
|
+
}
|
|
565
|
+
function applySshPublicKey(body, sshPublicKey) {
|
|
566
|
+
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
567
|
+
const hasSshPort = ports.some((p) => /^\s*22\/tcp\s*$/i.test(String(p)));
|
|
568
|
+
const env = { ...body.env ?? {} };
|
|
569
|
+
const key = sshPublicKey.trim();
|
|
570
|
+
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
571
|
+
else if (!env.PUBLIC_KEY.includes(key))
|
|
572
|
+
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
573
|
+
${key}`;
|
|
574
|
+
return {
|
|
575
|
+
...body,
|
|
576
|
+
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
577
|
+
env
|
|
578
|
+
};
|
|
579
|
+
}
|
|
470
580
|
function mapTemplateUpdateToV2(params) {
|
|
471
581
|
return {
|
|
472
582
|
...containerConfigToV2(params),
|
|
@@ -681,7 +791,7 @@ function resolveBackend(opts) {
|
|
|
681
791
|
}
|
|
682
792
|
|
|
683
793
|
// src/tools/runtime.ts
|
|
684
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
794
|
+
var MCP_SERVER_VERSION = true ? "3.4.0" : "dev";
|
|
685
795
|
var SESSION_ID = randomUUID();
|
|
686
796
|
function trackingHeaders(server, ctx) {
|
|
687
797
|
const info = server.server.getClientVersion();
|
|
@@ -696,6 +806,11 @@ function trackingHeaders(server, ctx) {
|
|
|
696
806
|
sessionId: SESSION_ID
|
|
697
807
|
});
|
|
698
808
|
}
|
|
809
|
+
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
810
|
+
function invocationBudget(totalMs) {
|
|
811
|
+
const startedAt = Date.now();
|
|
812
|
+
return () => totalMs - (Date.now() - startedAt);
|
|
813
|
+
}
|
|
699
814
|
var defaultFetch = fetch;
|
|
700
815
|
async function withApiErrorLog(label, call) {
|
|
701
816
|
try {
|
|
@@ -719,6 +834,23 @@ var DESTRUCTIVE = {
|
|
|
719
834
|
openWorldHint: true
|
|
720
835
|
};
|
|
721
836
|
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
837
|
+
return withRequestTimeout(
|
|
838
|
+
"Runpod GraphQL Error",
|
|
839
|
+
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
840
|
+
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
841
|
+
// off the method, and telling a caller its list-gpu-types "may have
|
|
842
|
+
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
843
|
+
// it back at the tool that just failed. The operation keyword is what
|
|
844
|
+
// actually says whether anything could have been written.
|
|
845
|
+
isGraphqlMutation(query) ? "POST" : "GET",
|
|
846
|
+
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
function isGraphqlMutation(query) {
|
|
850
|
+
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
851
|
+
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
852
|
+
}
|
|
853
|
+
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
722
854
|
const response = await fetchImpl(url, {
|
|
723
855
|
method: "POST",
|
|
724
856
|
headers: {
|
|
@@ -729,7 +861,8 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
729
861
|
body: JSON.stringify({
|
|
730
862
|
query,
|
|
731
863
|
...options?.variables ? { variables: options.variables } : {}
|
|
732
|
-
})
|
|
864
|
+
}),
|
|
865
|
+
signal
|
|
733
866
|
});
|
|
734
867
|
if (!response.ok) {
|
|
735
868
|
const bodyText = await response.text();
|
|
@@ -785,39 +918,53 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
785
918
|
}
|
|
786
919
|
const rawFetch = deps.fetch ?? defaultFetch;
|
|
787
920
|
const httpFetch = observeUnauthorized(rawFetch);
|
|
921
|
+
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
922
|
+
const timeouts = {
|
|
923
|
+
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
924
|
+
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
925
|
+
};
|
|
788
926
|
const v1Client = createHttpClient({
|
|
789
927
|
apiKey: ctx.apiKey,
|
|
790
928
|
fetch: httpFetch,
|
|
791
929
|
tracking,
|
|
792
|
-
errorPrefix: "Runpod API Error"
|
|
930
|
+
errorPrefix: "Runpod API Error",
|
|
931
|
+
...timeouts
|
|
793
932
|
});
|
|
794
|
-
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
933
|
+
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
795
934
|
"Error calling Runpod API:",
|
|
796
|
-
() => v1Client(
|
|
935
|
+
() => v1Client(
|
|
936
|
+
`${restV1Base(process.env)}${endpoint}`,
|
|
937
|
+
method,
|
|
938
|
+
body,
|
|
939
|
+
options
|
|
940
|
+
)
|
|
797
941
|
);
|
|
798
942
|
const serverlessClient = createHttpClient({
|
|
799
943
|
apiKey: ctx.apiKey,
|
|
800
944
|
fetch: httpFetch,
|
|
801
945
|
tracking,
|
|
802
|
-
errorPrefix: "Runpod Serverless API Error"
|
|
946
|
+
errorPrefix: "Runpod Serverless API Error",
|
|
947
|
+
...timeouts
|
|
803
948
|
});
|
|
804
|
-
const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
|
|
949
|
+
const serverlessRequest = (endpointId, path, method = "GET", body, options) => withApiErrorLog(
|
|
805
950
|
"Error calling Runpod Serverless API:",
|
|
806
951
|
() => serverlessClient(
|
|
807
952
|
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
808
953
|
method,
|
|
809
|
-
body
|
|
954
|
+
body,
|
|
955
|
+
options
|
|
810
956
|
)
|
|
811
957
|
);
|
|
812
958
|
const restClient = createHttpClient({
|
|
813
959
|
apiKey: ctx.apiKey,
|
|
814
960
|
fetch: httpFetch,
|
|
815
961
|
tracking,
|
|
816
|
-
errorPrefix: "Runpod API Error"
|
|
962
|
+
errorPrefix: "Runpod API Error",
|
|
963
|
+
...timeouts
|
|
817
964
|
});
|
|
818
|
-
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
965
|
+
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
819
966
|
"Error calling Runpod API:",
|
|
820
|
-
() => restClient(url, method, body)
|
|
967
|
+
() => restClient(url, method, body, options)
|
|
821
968
|
);
|
|
822
969
|
const sseFetch = observeUnauthorized(
|
|
823
970
|
deps.sseFetch ?? fetch
|
|
@@ -850,12 +997,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
850
997
|
};
|
|
851
998
|
return {
|
|
852
999
|
jsonReply,
|
|
1000
|
+
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
1001
|
+
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
853
1002
|
graphql: (query) => graphqlRequest(
|
|
854
1003
|
query,
|
|
855
1004
|
tracking,
|
|
856
1005
|
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
857
1006
|
rawFetch,
|
|
858
|
-
publicGraphqlBase(process.env)
|
|
1007
|
+
publicGraphqlBase(process.env),
|
|
1008
|
+
{
|
|
1009
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1010
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1011
|
+
}
|
|
859
1012
|
),
|
|
860
1013
|
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
861
1014
|
query,
|
|
@@ -863,7 +1016,12 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
863
1016
|
httpFetch,
|
|
864
1017
|
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
865
1018
|
authedGraphqlBase(process.env),
|
|
866
|
-
{
|
|
1019
|
+
{
|
|
1020
|
+
variables,
|
|
1021
|
+
apiKey: ctx.apiKey,
|
|
1022
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1023
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1024
|
+
}
|
|
867
1025
|
),
|
|
868
1026
|
runpodRequest,
|
|
869
1027
|
serverlessRequest,
|
|
@@ -871,7 +1029,8 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
871
1029
|
backendFor,
|
|
872
1030
|
streamSse,
|
|
873
1031
|
podAction,
|
|
874
|
-
env: process.env
|
|
1032
|
+
env: process.env,
|
|
1033
|
+
transport: ctx.transport
|
|
875
1034
|
};
|
|
876
1035
|
}
|
|
877
1036
|
|
|
@@ -940,6 +1099,7 @@ function capListResult(result, options, extra) {
|
|
|
940
1099
|
}
|
|
941
1100
|
|
|
942
1101
|
// src/tools/catalog.ts
|
|
1102
|
+
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
943
1103
|
function registerCatalogTools(server, rt) {
|
|
944
1104
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
945
1105
|
server.tool(
|
|
@@ -958,15 +1118,50 @@ function registerCatalogTools(server, rt) {
|
|
|
958
1118
|
),
|
|
959
1119
|
includeAvailability: z3.boolean().optional().describe(
|
|
960
1120
|
"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."
|
|
1121
|
+
),
|
|
1122
|
+
product: z3.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1123
|
+
"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."
|
|
1124
|
+
),
|
|
1125
|
+
minCudaVersion: z3.string().optional().describe(
|
|
1126
|
+
'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).'
|
|
961
1127
|
)
|
|
962
1128
|
},
|
|
963
1129
|
{ title: "List GPU types", ...READ_ONLY },
|
|
964
1130
|
async (params) => {
|
|
965
1131
|
const backend = backendFor("gpus");
|
|
1132
|
+
if (params.minCudaVersion !== void 0 && backend.version !== "v2") {
|
|
1133
|
+
return jsonReply2({
|
|
1134
|
+
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.",
|
|
1135
|
+
status: 501
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
966
1138
|
if (backend.version === "v2") {
|
|
967
1139
|
const wantAvailability = params.includeAvailability !== false;
|
|
1140
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1141
|
+
if (params.minCudaVersion !== void 0) {
|
|
1142
|
+
if (!wantAvailability) {
|
|
1143
|
+
return jsonReply2({
|
|
1144
|
+
error: "minCudaVersion filters the availability lookup, so it requires includeAvailability (the default). Drop includeAvailability:false or the filter.",
|
|
1145
|
+
status: 400
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
if (!/^\d+(\.\d+)?$/.test(params.minCudaVersion)) {
|
|
1149
|
+
return jsonReply2({
|
|
1150
|
+
error: 'minCudaVersion must be an integer major ("12") or major.minor ("12.1").',
|
|
1151
|
+
status: 400
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
const query = new URLSearchParams();
|
|
1156
|
+
if (wantAvailability) {
|
|
1157
|
+
query.set("include", "AVAILABILITY");
|
|
1158
|
+
query.set("product", product);
|
|
1159
|
+
if (params.minCudaVersion !== void 0) {
|
|
1160
|
+
query.set("minCudaVersion", params.minCudaVersion);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
968
1163
|
const raw = await callRestUrl(
|
|
969
|
-
`${backend.base}${backend.list}${
|
|
1164
|
+
`${backend.base}${backend.list}${query.size ? `?${query}` : ""}`
|
|
970
1165
|
);
|
|
971
1166
|
let gpus = backend.unwrap(raw);
|
|
972
1167
|
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
@@ -1139,6 +1334,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1139
1334
|
gpuTypeId: z3.string().describe("ID of the GPU type to retrieve"),
|
|
1140
1335
|
includeAvailability: z3.boolean().optional().describe(
|
|
1141
1336
|
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1337
|
+
),
|
|
1338
|
+
product: z3.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1339
|
+
"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."
|
|
1142
1340
|
)
|
|
1143
1341
|
},
|
|
1144
1342
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
@@ -1151,7 +1349,8 @@ function registerCatalogTools(server, rt) {
|
|
|
1151
1349
|
});
|
|
1152
1350
|
}
|
|
1153
1351
|
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1154
|
-
const
|
|
1352
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1353
|
+
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1155
1354
|
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1156
1355
|
return jsonReply2(result);
|
|
1157
1356
|
}
|
|
@@ -2031,7 +2230,7 @@ function registerPodTools(server, rt) {
|
|
|
2031
2230
|
);
|
|
2032
2231
|
server.tool(
|
|
2033
2232
|
"create-pod",
|
|
2034
|
-
'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.',
|
|
2233
|
+
'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.',
|
|
2035
2234
|
{
|
|
2036
2235
|
name: z7.string().optional().describe(
|
|
2037
2236
|
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
@@ -2045,20 +2244,48 @@ function registerPodTools(server, rt) {
|
|
|
2045
2244
|
),
|
|
2046
2245
|
cloudType: z7.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2047
2246
|
gpuTypeIds: z7.array(z7.string()).optional().describe("List of acceptable GPU types"),
|
|
2048
|
-
|
|
2247
|
+
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
2248
|
+
// a negative, or a fractional count reached the wire as-is and came back
|
|
2249
|
+
// as a raw 422; reject it here with a message that names the field.
|
|
2250
|
+
gpuCount: z7.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2049
2251
|
containerDiskInGb: z7.number().optional().describe("Container disk size in GB"),
|
|
2050
2252
|
volumeInGb: z7.number().optional().describe("Volume size in GB"),
|
|
2051
2253
|
volumeMountPath: z7.string().optional().describe("Path to mount the volume"),
|
|
2052
2254
|
ports: z7.array(z7.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2053
2255
|
env: z7.record(z7.string()).optional().describe("Environment variables"),
|
|
2256
|
+
sshPublicKey: z7.string().optional().describe(
|
|
2257
|
+
'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.'
|
|
2258
|
+
),
|
|
2054
2259
|
containerRegistryAuthId: z7.string().optional().describe(
|
|
2055
2260
|
"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)."
|
|
2056
2261
|
),
|
|
2057
|
-
dataCenterIds: z7.array(z7.string()).optional().describe("List of data centers")
|
|
2262
|
+
dataCenterIds: z7.array(z7.string()).optional().describe("List of data centers"),
|
|
2263
|
+
allowedCudaVersions: z7.array(z7.string()).optional().describe(
|
|
2264
|
+
'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.'
|
|
2265
|
+
),
|
|
2266
|
+
minCudaVersion: z7.string().optional().describe(
|
|
2267
|
+
'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.'
|
|
2268
|
+
)
|
|
2058
2269
|
},
|
|
2059
2270
|
{ title: "Create pod", ...WRITE },
|
|
2060
2271
|
async (params) => {
|
|
2061
2272
|
const backend = backendFor("pods");
|
|
2273
|
+
const { sshPublicKey, ...podParams } = params;
|
|
2274
|
+
let sshKey;
|
|
2275
|
+
if (sshPublicKey !== void 0) {
|
|
2276
|
+
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
2277
|
+
if (!validated.ok)
|
|
2278
|
+
return jsonReply2({ error: validated.error, status: 400 });
|
|
2279
|
+
sshKey = validated.key;
|
|
2280
|
+
}
|
|
2281
|
+
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.";
|
|
2282
|
+
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2283
|
+
if (hasCudaParams && backend.version !== "v2") {
|
|
2284
|
+
return jsonReply2({
|
|
2285
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2286
|
+
status: 501
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2062
2289
|
if (params.templateId && backend.version !== "v2") {
|
|
2063
2290
|
return jsonReply2({
|
|
2064
2291
|
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
@@ -2094,6 +2321,16 @@ function registerPodTools(server, rt) {
|
|
|
2094
2321
|
status: 400
|
|
2095
2322
|
});
|
|
2096
2323
|
}
|
|
2324
|
+
if (hasCudaParams && !wantsGpu) {
|
|
2325
|
+
return jsonReply2({
|
|
2326
|
+
error: "allowedCudaVersions/minCudaVersion apply to GPU pods only \u2014 pass gpuTypeIds (see list-gpu-types).",
|
|
2327
|
+
status: 400
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
if (hasCudaParams) {
|
|
2331
|
+
const cudaError = cudaConstraintError(params);
|
|
2332
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2333
|
+
}
|
|
2097
2334
|
if (!wantsCpu && !params.name && !params.templateId) {
|
|
2098
2335
|
return jsonReply2({
|
|
2099
2336
|
error: "Provide a name for the pod (deploying from a templateId inherits the template name).",
|
|
@@ -2108,13 +2345,15 @@ function registerPodTools(server, rt) {
|
|
|
2108
2345
|
});
|
|
2109
2346
|
}
|
|
2110
2347
|
try {
|
|
2348
|
+
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2111
2349
|
const result = await callRestUrl(
|
|
2112
2350
|
`${restV1Base(env)}/pods`,
|
|
2113
2351
|
"POST",
|
|
2114
|
-
|
|
2352
|
+
cpuBody
|
|
2115
2353
|
);
|
|
2116
2354
|
return jsonReply2({
|
|
2117
2355
|
...result,
|
|
2356
|
+
...sshKey ? { _ssh: sshNote } : {},
|
|
2118
2357
|
_servedBy: "v1",
|
|
2119
2358
|
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2120
2359
|
});
|
|
@@ -2131,7 +2370,7 @@ function registerPodTools(server, rt) {
|
|
|
2131
2370
|
}
|
|
2132
2371
|
}
|
|
2133
2372
|
}
|
|
2134
|
-
let body = backend.mapCreate(
|
|
2373
|
+
let body = backend.mapCreate(podParams);
|
|
2135
2374
|
if (params.templateId) {
|
|
2136
2375
|
let template;
|
|
2137
2376
|
try {
|
|
@@ -2149,19 +2388,21 @@ function registerPodTools(server, rt) {
|
|
|
2149
2388
|
}
|
|
2150
2389
|
body = { ...podBodyFromTemplate(template), ...body };
|
|
2151
2390
|
}
|
|
2391
|
+
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2152
2392
|
try {
|
|
2153
2393
|
const result = await callRestUrl(
|
|
2154
2394
|
`${backend.base}${backend.list}`,
|
|
2155
2395
|
"POST",
|
|
2156
2396
|
body
|
|
2157
2397
|
);
|
|
2398
|
+
const extras = {};
|
|
2399
|
+
if (sshKey) extras._ssh = sshNote;
|
|
2158
2400
|
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2159
|
-
|
|
2160
|
-
...result,
|
|
2161
|
-
_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(", ")}.`
|
|
2162
|
-
});
|
|
2401
|
+
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(", ")}.`;
|
|
2163
2402
|
}
|
|
2164
|
-
return jsonReply2(
|
|
2403
|
+
return jsonReply2(
|
|
2404
|
+
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
2405
|
+
);
|
|
2165
2406
|
} catch (error) {
|
|
2166
2407
|
if (error instanceof HttpError) {
|
|
2167
2408
|
return jsonReply2({ error: error.message, status: error.status });
|
|
@@ -2279,6 +2520,9 @@ function registerPodTools(server, rt) {
|
|
|
2279
2520
|
import { z as z8 } from "zod";
|
|
2280
2521
|
var MIN_QUEUE_DELAY = 0.5;
|
|
2281
2522
|
var MIN_REQUEST_COUNT = 1;
|
|
2523
|
+
function isRequestCountScaler(type) {
|
|
2524
|
+
return type === "REQUEST_COUNT";
|
|
2525
|
+
}
|
|
2282
2526
|
var MIN_IDLE_TIMEOUT = 1;
|
|
2283
2527
|
var MAX_IDLE_TIMEOUT = 3600;
|
|
2284
2528
|
function scalerValueError(scalerType, scalerValue) {
|
|
@@ -2371,6 +2615,12 @@ function registerEndpointTools(server, rt) {
|
|
|
2371
2615
|
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
2372
2616
|
),
|
|
2373
2617
|
gpuCount: z8.number().optional().describe("GPUs per worker (v2)"),
|
|
2618
|
+
allowedCudaVersions: z8.array(z8.string()).optional().describe(
|
|
2619
|
+
'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.'
|
|
2620
|
+
),
|
|
2621
|
+
minCudaVersion: z8.string().optional().describe(
|
|
2622
|
+
'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.'
|
|
2623
|
+
),
|
|
2374
2624
|
args: z8.string().optional().describe("Container start command/args (v2)"),
|
|
2375
2625
|
containerDiskInGb: z8.number().optional().describe("Container disk size in GB (v2)"),
|
|
2376
2626
|
ports: z8.array(z8.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
@@ -2400,6 +2650,13 @@ function registerEndpointTools(server, rt) {
|
|
|
2400
2650
|
{ title: "Create endpoint", ...WRITE },
|
|
2401
2651
|
async (params) => {
|
|
2402
2652
|
const backend = backendFor("endpoints");
|
|
2653
|
+
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2654
|
+
if (hasCudaParams && backend.version !== "v2") {
|
|
2655
|
+
return jsonReply2({
|
|
2656
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2657
|
+
status: 501
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2403
2660
|
if (backend.version === "v2") {
|
|
2404
2661
|
if (!params.name) {
|
|
2405
2662
|
return jsonReply2({
|
|
@@ -2432,6 +2689,10 @@ function registerEndpointTools(server, rt) {
|
|
|
2432
2689
|
if (scalerError) {
|
|
2433
2690
|
return jsonReply2({ error: scalerError, status: 400 });
|
|
2434
2691
|
}
|
|
2692
|
+
if (hasCudaParams) {
|
|
2693
|
+
const cudaError = cudaConstraintError(params);
|
|
2694
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2695
|
+
}
|
|
2435
2696
|
const body = backend.mapCreate(params);
|
|
2436
2697
|
const result2 = await callRestUrl(
|
|
2437
2698
|
`${backend.base}${backend.list}`,
|
|
@@ -2469,7 +2730,7 @@ function registerEndpointTools(server, rt) {
|
|
|
2469
2730
|
);
|
|
2470
2731
|
server.tool(
|
|
2471
2732
|
"update-endpoint",
|
|
2472
|
-
"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.",
|
|
2733
|
+
"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.",
|
|
2473
2734
|
{
|
|
2474
2735
|
endpointId: z8.string().describe("ID of the endpoint to update"),
|
|
2475
2736
|
name: z8.string().optional().describe("New name for the endpoint"),
|
|
@@ -2488,6 +2749,12 @@ function registerEndpointTools(server, rt) {
|
|
|
2488
2749
|
imageName: z8.string().optional().describe("New Docker image (v2)"),
|
|
2489
2750
|
gpuPoolIds: z8.array(z8.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
2490
2751
|
gpuCount: z8.number().optional().describe("New GPUs per worker (v2)"),
|
|
2752
|
+
allowedCudaVersions: z8.array(z8.string()).optional().describe(
|
|
2753
|
+
"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."
|
|
2754
|
+
),
|
|
2755
|
+
minCudaVersion: z8.string().optional().describe(
|
|
2756
|
+
'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.'
|
|
2757
|
+
),
|
|
2491
2758
|
args: z8.string().optional().describe("New container args (v2)"),
|
|
2492
2759
|
containerDiskInGb: z8.number().optional().describe("New container disk size in GB (v2)"),
|
|
2493
2760
|
ports: z8.array(z8.string()).optional().describe("New ports (v2)"),
|
|
@@ -2502,14 +2769,41 @@ function registerEndpointTools(server, rt) {
|
|
|
2502
2769
|
const { endpointId, ...updateParams } = params;
|
|
2503
2770
|
const backend = backendFor("endpoints");
|
|
2504
2771
|
const url = `${backend.base}${backend.get(endpointId)}`;
|
|
2772
|
+
const hasCudaParams = updateParams.allowedCudaVersions !== void 0 || updateParams.minCudaVersion !== void 0;
|
|
2505
2773
|
if (backend.version !== "v2") {
|
|
2774
|
+
if (hasCudaParams) {
|
|
2775
|
+
return jsonReply2({
|
|
2776
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2777
|
+
status: 501
|
|
2778
|
+
});
|
|
2779
|
+
}
|
|
2506
2780
|
const body2 = backend.mapUpdate(updateParams);
|
|
2507
2781
|
return jsonReply2(await callRestUrl(url, "PATCH", body2));
|
|
2508
2782
|
}
|
|
2783
|
+
if (hasCudaParams) {
|
|
2784
|
+
const cudaError = cudaConstraintError(updateParams, {
|
|
2785
|
+
allowClear: true
|
|
2786
|
+
});
|
|
2787
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2788
|
+
}
|
|
2789
|
+
if (updateParams.gpuPoolIds !== void 0 && !updateParams.gpuPoolIds.length) {
|
|
2790
|
+
return jsonReply2({
|
|
2791
|
+
error: "gpuPoolIds cannot be empty \u2014 omit it to keep the current GPU selection.",
|
|
2792
|
+
status: 400
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2509
2795
|
let scalerType = updateParams.scalerType;
|
|
2510
|
-
|
|
2796
|
+
const needScalerRead = scalerType === void 0 && updateParams.scalerValue !== void 0;
|
|
2797
|
+
const needExclusionRead = (updateParams.gpuPoolIds?.length ?? 0) > 0;
|
|
2798
|
+
let clearedExclusions = [];
|
|
2799
|
+
if (needScalerRead || needExclusionRead) {
|
|
2511
2800
|
const current = await callRestUrl(url);
|
|
2512
|
-
|
|
2801
|
+
if (needScalerRead) {
|
|
2802
|
+
scalerType = isRequestCountScaler(current?.scaling?.type) ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
2803
|
+
}
|
|
2804
|
+
if (needExclusionRead && current?.gpu?.excludedTypes?.length) {
|
|
2805
|
+
clearedExclusions = current.gpu.excludedTypes;
|
|
2806
|
+
}
|
|
2513
2807
|
}
|
|
2514
2808
|
const scalerError = scalerValueError(
|
|
2515
2809
|
scalerType,
|
|
@@ -2522,7 +2816,14 @@ function registerEndpointTools(server, rt) {
|
|
|
2522
2816
|
...updateParams,
|
|
2523
2817
|
scalerType
|
|
2524
2818
|
});
|
|
2525
|
-
|
|
2819
|
+
const result = await callRestUrl(url, "PATCH", body);
|
|
2820
|
+
if (clearedExclusions.length) {
|
|
2821
|
+
return jsonReply2({
|
|
2822
|
+
...result,
|
|
2823
|
+
_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.`
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
return jsonReply2(result);
|
|
2526
2827
|
}
|
|
2527
2828
|
);
|
|
2528
2829
|
server.tool(
|
|
@@ -2622,8 +2923,108 @@ import { z as z9 } from "zod";
|
|
|
2622
2923
|
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2623
2924
|
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2624
2925
|
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
2926
|
+
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
2927
|
+
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
2928
|
+
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
2929
|
+
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
2930
|
+
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
2931
|
+
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
2932
|
+
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
2933
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
2934
|
+
"COMPLETED",
|
|
2935
|
+
"FAILED",
|
|
2936
|
+
"CANCELLED",
|
|
2937
|
+
"TIMED_OUT"
|
|
2938
|
+
]);
|
|
2939
|
+
function isUnfinishedJobReply(result) {
|
|
2940
|
+
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
2941
|
+
}
|
|
2942
|
+
function formatBudget(ms) {
|
|
2943
|
+
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
2944
|
+
}
|
|
2945
|
+
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
2946
|
+
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
2947
|
+
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
2948
|
+
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
2949
|
+
return Math.max(
|
|
2950
|
+
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
2951
|
+
MIN_STREAM_POLL_TIMEOUT_MS
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
function sleep(ms) {
|
|
2955
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2956
|
+
}
|
|
2957
|
+
function streamPollPlan(hosted) {
|
|
2958
|
+
return hosted ? {
|
|
2959
|
+
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
2960
|
+
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
2961
|
+
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
2962
|
+
} : {
|
|
2963
|
+
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
2964
|
+
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
2965
|
+
query: ""
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
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).";
|
|
2969
|
+
function budgetExhaustedNote(budgetMs, lastError) {
|
|
2970
|
+
return {
|
|
2971
|
+
pollingTimedOut: true,
|
|
2972
|
+
note: `Polling stopped after ${formatBudget(
|
|
2973
|
+
budgetMs
|
|
2974
|
+
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
2975
|
+
// Surface the trailing error (if any) instead of discarding it — the last
|
|
2976
|
+
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
2977
|
+
// succeeded. Cleared on success, so this is never a stale error from
|
|
2978
|
+
// minutes of healthy streaming ago.
|
|
2979
|
+
...lastError ? { lastError } : {}
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2982
|
+
function errorCapNote(lastError) {
|
|
2983
|
+
return {
|
|
2984
|
+
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
2985
|
+
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
async function collectJobStream(deps) {
|
|
2989
|
+
const { poll, budgetMs, holdMs } = deps;
|
|
2990
|
+
const startedAt = Date.now();
|
|
2991
|
+
const elapsed = () => Date.now() - startedAt;
|
|
2992
|
+
const chunks = [];
|
|
2993
|
+
let result = {};
|
|
2994
|
+
let consecutiveErrors = 0;
|
|
2995
|
+
let lastError;
|
|
2996
|
+
while (true) {
|
|
2997
|
+
try {
|
|
2998
|
+
const reply = await poll(
|
|
2999
|
+
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
3000
|
+
);
|
|
3001
|
+
consecutiveErrors = 0;
|
|
3002
|
+
lastError = void 0;
|
|
3003
|
+
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
3004
|
+
result = reply;
|
|
3005
|
+
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
3006
|
+
} catch (error) {
|
|
3007
|
+
consecutiveErrors++;
|
|
3008
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3009
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
3010
|
+
result = { ...result, ...errorCapNote(lastError) };
|
|
3011
|
+
break;
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
if (elapsed() > budgetMs) {
|
|
3015
|
+
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
3016
|
+
break;
|
|
3017
|
+
}
|
|
3018
|
+
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
3019
|
+
}
|
|
3020
|
+
return { result, chunks };
|
|
3021
|
+
}
|
|
2625
3022
|
function registerJobTools(server, rt) {
|
|
2626
3023
|
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
3024
|
+
const hosted = rt.transport === "http";
|
|
3025
|
+
const streamBudget = formatBudget(
|
|
3026
|
+
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
3027
|
+
);
|
|
2627
3028
|
async function diagnoseQueuedJob(endpointId) {
|
|
2628
3029
|
const cached = diagnosisCache.get(endpointId);
|
|
2629
3030
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -2634,7 +3035,10 @@ function registerJobTools(server, rt) {
|
|
|
2634
3035
|
const backend = backendFor("workers");
|
|
2635
3036
|
if (backend.version !== "v2") return null;
|
|
2636
3037
|
const raw = await callRestUrl(
|
|
2637
|
-
`${backend.base}/serverless/${endpointId}/workers
|
|
3038
|
+
`${backend.base}/serverless/${endpointId}/workers`,
|
|
3039
|
+
"GET",
|
|
3040
|
+
void 0,
|
|
3041
|
+
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
2638
3042
|
);
|
|
2639
3043
|
const summary = raw?.summary;
|
|
2640
3044
|
const workers = raw?.workers ?? [];
|
|
@@ -2725,14 +3129,14 @@ function registerJobTools(server, rt) {
|
|
|
2725
3129
|
);
|
|
2726
3130
|
server.tool(
|
|
2727
3131
|
"runsync-endpoint",
|
|
2728
|
-
|
|
3132
|
+
`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)."}`,
|
|
2729
3133
|
{
|
|
2730
3134
|
endpointId: endpointIdSchema.describe(
|
|
2731
3135
|
"ID of the Serverless endpoint to run synchronously"
|
|
2732
3136
|
),
|
|
2733
3137
|
input: inputSchema,
|
|
2734
3138
|
wait: z9.number().min(1e3).max(3e5).optional().describe(
|
|
2735
|
-
"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)."
|
|
3139
|
+
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)."
|
|
2736
3140
|
),
|
|
2737
3141
|
webhook: webhookSchema,
|
|
2738
3142
|
policy: policySchema,
|
|
@@ -2741,13 +3145,36 @@ function registerJobTools(server, rt) {
|
|
|
2741
3145
|
{ title: "Run endpoint (sync)", ...WRITE },
|
|
2742
3146
|
async (params) => {
|
|
2743
3147
|
const { endpointId, wait, ...body } = params;
|
|
2744
|
-
const
|
|
3148
|
+
const effectiveWait = hosted ? Math.min(
|
|
3149
|
+
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3150
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3151
|
+
) : wait;
|
|
3152
|
+
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
3153
|
+
const path = `/runsync${waitQuery}`;
|
|
2745
3154
|
const result = await serverlessRequest(
|
|
2746
3155
|
endpointId,
|
|
2747
3156
|
path,
|
|
2748
3157
|
"POST",
|
|
2749
|
-
body
|
|
3158
|
+
body,
|
|
3159
|
+
{
|
|
3160
|
+
// From the wait actually sent — on http the clamped 45s, not the
|
|
3161
|
+
// 300s the caller may have asked for.
|
|
3162
|
+
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
3163
|
+
}
|
|
2750
3164
|
);
|
|
3165
|
+
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
3166
|
+
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
3167
|
+
return jsonReply2({
|
|
3168
|
+
...result,
|
|
3169
|
+
waitClamped: {
|
|
3170
|
+
requestedMs: requestedWait,
|
|
3171
|
+
effectiveMs: effectiveWait,
|
|
3172
|
+
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
3173
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3174
|
+
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
3175
|
+
}
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
2751
3178
|
return jsonReply2(result);
|
|
2752
3179
|
}
|
|
2753
3180
|
);
|
|
@@ -2781,7 +3208,7 @@ function registerJobTools(server, rt) {
|
|
|
2781
3208
|
);
|
|
2782
3209
|
server.tool(
|
|
2783
3210
|
"stream-job",
|
|
2784
|
-
|
|
3211
|
+
`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." : ""}`,
|
|
2785
3212
|
{
|
|
2786
3213
|
endpointId: endpointIdSchema.describe(
|
|
2787
3214
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -2790,51 +3217,16 @@ function registerJobTools(server, rt) {
|
|
|
2790
3217
|
},
|
|
2791
3218
|
{ title: "Stream job", ...READ_ONLY },
|
|
2792
3219
|
async (params) => {
|
|
2793
|
-
const
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
let finalResult = {};
|
|
2804
|
-
let consecutiveErrors = 0;
|
|
2805
|
-
let lastError;
|
|
2806
|
-
const startTime = Date.now();
|
|
2807
|
-
while (true) {
|
|
2808
|
-
try {
|
|
2809
|
-
const result = await serverlessRequest(
|
|
2810
|
-
params.endpointId,
|
|
2811
|
-
`/stream/${params.jobId}`
|
|
2812
|
-
);
|
|
2813
|
-
consecutiveErrors = 0;
|
|
2814
|
-
if (Array.isArray(result.stream)) {
|
|
2815
|
-
allChunks.push(...result.stream);
|
|
2816
|
-
}
|
|
2817
|
-
finalResult = result;
|
|
2818
|
-
if (TERMINAL_STATUSES.has(result.status)) {
|
|
2819
|
-
break;
|
|
2820
|
-
}
|
|
2821
|
-
} catch (error) {
|
|
2822
|
-
consecutiveErrors++;
|
|
2823
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
2824
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2825
|
-
finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
|
|
2826
|
-
break;
|
|
2827
|
-
}
|
|
2828
|
-
}
|
|
2829
|
-
if (Date.now() - startTime > MAX_POLL_TIME_MS) {
|
|
2830
|
-
finalResult.pollingTimedOut = true;
|
|
2831
|
-
finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
|
|
2832
|
-
if (lastError) finalResult.lastError = lastError;
|
|
2833
|
-
break;
|
|
2834
|
-
}
|
|
2835
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2836
|
-
}
|
|
2837
|
-
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
3220
|
+
const plan = streamPollPlan(hosted);
|
|
3221
|
+
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
3222
|
+
const { result, chunks } = await collectJobStream({
|
|
3223
|
+
budgetMs: plan.budgetMs,
|
|
3224
|
+
holdMs: plan.holdMs,
|
|
3225
|
+
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
3226
|
+
timeoutMs
|
|
3227
|
+
})
|
|
3228
|
+
});
|
|
3229
|
+
return jsonReply2({ ...result, stream: chunks });
|
|
2838
3230
|
}
|
|
2839
3231
|
);
|
|
2840
3232
|
server.tool(
|
|
@@ -3381,4 +3773,4 @@ export {
|
|
|
3381
3773
|
restVersionSummary,
|
|
3382
3774
|
registerTools
|
|
3383
3775
|
};
|
|
3384
|
-
//# sourceMappingURL=chunk-
|
|
3776
|
+
//# sourceMappingURL=chunk-G4URDODF.mjs.map
|