@runpod/mcp-server 2.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -2
- package/README.md +59 -309
- package/dist/chunk-OM6RHPQC.mjs +3023 -0
- package/dist/chunk-OM6RHPQC.mjs.map +1 -0
- package/dist/http.cjs +1621 -489
- package/dist/http.cjs.map +1 -1
- package/dist/http.d.cts +35 -2
- package/dist/http.d.ts +35 -2
- package/dist/http.mjs +247 -17
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +1420 -516
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +1420 -516
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +1293 -399
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +14 -0
- package/dist/tools.d.ts +14 -0
- package/dist/tools.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZP3TCVXG.mjs +0 -2116
- package/dist/chunk-ZP3TCVXG.mjs.map +0 -1
package/dist/stdio.cjs
CHANGED
|
@@ -448,81 +448,6 @@ var import_node_fetch2 = __toESM(require("node-fetch"), 1);
|
|
|
448
448
|
|
|
449
449
|
// src/server.ts
|
|
450
450
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
451
|
-
var SERVER_NAME = "Runpod API Server";
|
|
452
|
-
var SERVER_VERSION = true ? "2.0.0" : "dev";
|
|
453
|
-
function createServer(version = SERVER_VERSION) {
|
|
454
|
-
return new import_mcp.McpServer({
|
|
455
|
-
name: SERVER_NAME,
|
|
456
|
-
version,
|
|
457
|
-
capabilities: {
|
|
458
|
-
resources: {},
|
|
459
|
-
tools: {}
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// src/tools/runtime.ts
|
|
465
|
-
var import_node_fetch = __toESM(require("node-fetch"), 1);
|
|
466
|
-
var import_node_crypto = require("crypto");
|
|
467
|
-
|
|
468
|
-
// src/_shared/http.ts
|
|
469
|
-
var HttpError = class extends Error {
|
|
470
|
-
constructor(prefix, status, body) {
|
|
471
|
-
super(`${prefix}: ${status} - ${body}`);
|
|
472
|
-
this.name = "HttpError";
|
|
473
|
-
this.status = status;
|
|
474
|
-
this.body = body;
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
function isJsonContentType(contentType) {
|
|
478
|
-
if (!contentType) return false;
|
|
479
|
-
const ct = contentType.toLowerCase();
|
|
480
|
-
return ct.includes("application/json") || ct.includes("+json");
|
|
481
|
-
}
|
|
482
|
-
function methodSendsBody(method) {
|
|
483
|
-
return method === "POST" || method === "PATCH" || method === "PUT";
|
|
484
|
-
}
|
|
485
|
-
function buildRequestHeaders(apiKey, tracking) {
|
|
486
|
-
return {
|
|
487
|
-
Authorization: `Bearer ${apiKey}`,
|
|
488
|
-
"Content-Type": "application/json",
|
|
489
|
-
...tracking()
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
function createHttpClient(deps) {
|
|
493
|
-
return async function request(url, method = "GET", body) {
|
|
494
|
-
const init = {
|
|
495
|
-
method,
|
|
496
|
-
headers: buildRequestHeaders(deps.apiKey, deps.tracking)
|
|
497
|
-
};
|
|
498
|
-
if (body && methodSendsBody(method)) {
|
|
499
|
-
init.body = JSON.stringify(body);
|
|
500
|
-
}
|
|
501
|
-
const response = await deps.fetch(url, init);
|
|
502
|
-
if (!response.ok) {
|
|
503
|
-
throw new HttpError(
|
|
504
|
-
deps.errorPrefix,
|
|
505
|
-
response.status,
|
|
506
|
-
await response.text()
|
|
507
|
-
);
|
|
508
|
-
}
|
|
509
|
-
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// src/_shared/tracking.ts
|
|
514
|
-
function sanitizeUaToken(value) {
|
|
515
|
-
return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
|
|
516
|
-
}
|
|
517
|
-
function buildTrackingHeaders(input) {
|
|
518
|
-
const name = sanitizeUaToken(input.clientName || "unknown");
|
|
519
|
-
const version = sanitizeUaToken(input.clientVersion || "unknown");
|
|
520
|
-
const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
|
|
521
|
-
return {
|
|
522
|
-
"User-Agent": userAgent,
|
|
523
|
-
"X-Runpod-Session-Id": input.sessionId
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
451
|
|
|
527
452
|
// src/_shared/mappers.ts
|
|
528
453
|
function compact(obj) {
|
|
@@ -536,6 +461,9 @@ function persistentMount(size, path2) {
|
|
|
536
461
|
if (size === void 0 || path2 === void 0) return void 0;
|
|
537
462
|
return { persistent: { size, path: path2 } };
|
|
538
463
|
}
|
|
464
|
+
function joinStartCmd(cmd) {
|
|
465
|
+
return cmd && cmd.length ? cmd.join(" ") : void 0;
|
|
466
|
+
}
|
|
539
467
|
function containerConfigToV2(p2) {
|
|
540
468
|
return compact({
|
|
541
469
|
image: p2.imageName,
|
|
@@ -554,6 +482,13 @@ function mapPodCreateToV2(params) {
|
|
|
554
482
|
return compact({
|
|
555
483
|
...containerConfigToV2(params),
|
|
556
484
|
name: params.name,
|
|
485
|
+
// Private-image registry credential (v2 ContainerConfig `registry`). When
|
|
486
|
+
// deploying from a template, this spreads over the template's registry, so
|
|
487
|
+
// passing it overrides the inherited credential. An empty string is an
|
|
488
|
+
// explicit opt-out: emit `registry: null` (v2 accepts null) to CLEAR a
|
|
489
|
+
// template's inherited credential. Undefined is dropped by compact, so an
|
|
490
|
+
// omitted param leaves the template's registry in place.
|
|
491
|
+
registry: params.containerRegistryAuthId === "" ? null : params.containerRegistryAuthId,
|
|
557
492
|
cloud: params.cloudType,
|
|
558
493
|
// v2 CreatePodRequest takes `dataCenterIds` as an ARRAY (preferred data
|
|
559
494
|
// centers; omit/empty = scheduler chooses). Pass the v1 list straight
|
|
@@ -571,15 +506,27 @@ function mapPodUpdateToV2(params) {
|
|
|
571
506
|
...compact({ name: params.name })
|
|
572
507
|
};
|
|
573
508
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
509
|
+
var DEFAULT_ENDPOINT_TYPE = "QUEUE";
|
|
510
|
+
var DEFAULT_SCALER_VALUE = 4;
|
|
511
|
+
function defaultScalerType(endpointType) {
|
|
512
|
+
return endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
513
|
+
}
|
|
514
|
+
function endpointScaling(scalerType, value) {
|
|
515
|
+
return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
|
|
577
516
|
}
|
|
578
517
|
function endpointGpuConfig(pools, count) {
|
|
579
518
|
if (!pools?.length) return void 0;
|
|
580
519
|
return compact({ pools, count });
|
|
581
520
|
}
|
|
582
|
-
function
|
|
521
|
+
function endpointWorkers(params) {
|
|
522
|
+
const workers = compact({
|
|
523
|
+
min: params.workersMin,
|
|
524
|
+
max: params.workersMax,
|
|
525
|
+
idleTimeout: params.idleTimeout
|
|
526
|
+
});
|
|
527
|
+
return Object.keys(workers).length ? workers : void 0;
|
|
528
|
+
}
|
|
529
|
+
function endpointCommonToV2(params) {
|
|
583
530
|
return compact({
|
|
584
531
|
name: params.name,
|
|
585
532
|
image: params.imageName,
|
|
@@ -589,28 +536,39 @@ function mapEndpointToV2(params) {
|
|
|
589
536
|
env: params.env,
|
|
590
537
|
registry: params.containerRegistryAuthId,
|
|
591
538
|
gpu: endpointGpuConfig(params.gpuPoolIds, params.gpuCount),
|
|
592
|
-
workers:
|
|
593
|
-
min: params.workersMin,
|
|
594
|
-
max: params.workersMax
|
|
595
|
-
}),
|
|
596
|
-
scaling: nestedOrUndefined({
|
|
597
|
-
type: params.scalerType,
|
|
598
|
-
value: params.scalerValue,
|
|
599
|
-
idleTimeout: params.idleTimeout
|
|
600
|
-
}),
|
|
539
|
+
workers: endpointWorkers(params),
|
|
601
540
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
602
541
|
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
603
542
|
timeout: params.executionTimeoutMs,
|
|
604
543
|
flashboot: params.flashboot
|
|
605
544
|
});
|
|
606
545
|
}
|
|
607
|
-
|
|
608
|
-
|
|
546
|
+
function mapEndpointCreateToV2(params) {
|
|
547
|
+
const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
|
|
548
|
+
return compact({
|
|
549
|
+
...endpointCommonToV2(params),
|
|
550
|
+
type: endpointType,
|
|
551
|
+
scaling: endpointScaling(
|
|
552
|
+
params.scalerType ?? defaultScalerType(endpointType),
|
|
553
|
+
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
554
|
+
)
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
function mapEndpointUpdateToV2(params) {
|
|
558
|
+
return compact({
|
|
559
|
+
...endpointCommonToV2(params),
|
|
560
|
+
scaling: params.scalerType ? endpointScaling(
|
|
561
|
+
params.scalerType,
|
|
562
|
+
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
563
|
+
) : void 0
|
|
564
|
+
});
|
|
565
|
+
}
|
|
609
566
|
function mapNetworkVolumeCreateToV2(params) {
|
|
610
567
|
return compact({
|
|
611
568
|
name: params.name,
|
|
612
569
|
size: params.size,
|
|
613
|
-
dataCenter: params.dataCenterId
|
|
570
|
+
dataCenter: params.dataCenterId,
|
|
571
|
+
type: params.volumeType
|
|
614
572
|
});
|
|
615
573
|
}
|
|
616
574
|
function mapTemplateCreateToV2(params) {
|
|
@@ -619,18 +577,39 @@ function mapTemplateCreateToV2(params) {
|
|
|
619
577
|
...compact({
|
|
620
578
|
name: params.name,
|
|
621
579
|
serverless: params.isServerless,
|
|
622
|
-
registry: params.containerRegistryAuthId
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
580
|
+
registry: params.containerRegistryAuthId,
|
|
581
|
+
args: joinStartCmd(params.dockerStartCmd),
|
|
582
|
+
// No longer required by v2 (it defaults to NVIDIA server-side), so an
|
|
583
|
+
// unset category is dropped by compact instead of being forced here.
|
|
584
|
+
category: params.category
|
|
585
|
+
})
|
|
626
586
|
};
|
|
627
587
|
}
|
|
588
|
+
function podBodyFromTemplate(template) {
|
|
589
|
+
const out = {};
|
|
590
|
+
const keys = [
|
|
591
|
+
"name",
|
|
592
|
+
"image",
|
|
593
|
+
"args",
|
|
594
|
+
"disk",
|
|
595
|
+
"ports",
|
|
596
|
+
"env",
|
|
597
|
+
"mounts",
|
|
598
|
+
"registry"
|
|
599
|
+
];
|
|
600
|
+
for (const key of keys) {
|
|
601
|
+
const value = template[key];
|
|
602
|
+
if (value !== void 0 && value !== null) out[key] = value;
|
|
603
|
+
}
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
628
606
|
function mapTemplateUpdateToV2(params) {
|
|
629
607
|
return {
|
|
630
608
|
...containerConfigToV2(params),
|
|
631
609
|
...compact({
|
|
632
610
|
name: params.name,
|
|
633
|
-
registry: params.containerRegistryAuthId
|
|
611
|
+
registry: params.containerRegistryAuthId,
|
|
612
|
+
args: joinStartCmd(params.dockerStartCmd)
|
|
634
613
|
})
|
|
635
614
|
};
|
|
636
615
|
}
|
|
@@ -647,7 +626,6 @@ var V2_UNWRAP_KEY = {
|
|
|
647
626
|
dataCenters: "dataCenters",
|
|
648
627
|
endpoints: "endpoints",
|
|
649
628
|
jobs: "jobs",
|
|
650
|
-
tags: "tags",
|
|
651
629
|
workers: "workers",
|
|
652
630
|
billing: "records"
|
|
653
631
|
};
|
|
@@ -661,7 +639,6 @@ var RESOURCE_ENV_KEY = {
|
|
|
661
639
|
dataCenters: "DATA_CENTERS",
|
|
662
640
|
endpoints: "ENDPOINTS",
|
|
663
641
|
jobs: "JOBS",
|
|
664
|
-
tags: "TAGS",
|
|
665
642
|
workers: "WORKERS",
|
|
666
643
|
billing: "BILLING"
|
|
667
644
|
};
|
|
@@ -677,6 +654,9 @@ function serverlessBase(env) {
|
|
|
677
654
|
function publicGraphqlBase(env) {
|
|
678
655
|
return env.RUNPOD_PUBLIC_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
679
656
|
}
|
|
657
|
+
function authedGraphqlBase(env) {
|
|
658
|
+
return env.RUNPOD_AUTHED_GRAPHQL_URL ?? "https://api.runpod.io/graphql";
|
|
659
|
+
}
|
|
680
660
|
function unwrapList(resource, version, raw) {
|
|
681
661
|
if (version === "v1") return Array.isArray(raw) ? raw : [];
|
|
682
662
|
if (raw === null || typeof raw !== "object") return [];
|
|
@@ -700,6 +680,14 @@ function wantsAutoProbe(env) {
|
|
|
700
680
|
([k, v]) => k.startsWith("RUNPOD_REST_VERSION") && (v ?? "").toLowerCase() === "auto"
|
|
701
681
|
);
|
|
702
682
|
}
|
|
683
|
+
function restVersionSummary(env) {
|
|
684
|
+
const raw = (env.RUNPOD_REST_VERSION ?? "").trim().toLowerCase();
|
|
685
|
+
const base = raw ? `RUNPOD_REST_VERSION=${raw}` : "RUNPOD_REST_VERSION unset (default v2)";
|
|
686
|
+
const overrides = Object.keys(env).filter(
|
|
687
|
+
(k) => k.startsWith("RUNPOD_REST_VERSION_") && (env[k] ?? "").trim() !== ""
|
|
688
|
+
).length;
|
|
689
|
+
return overrides > 0 ? `${base}, +${overrides} per-resource` : base;
|
|
690
|
+
}
|
|
703
691
|
var PROBE_TIMEOUT_MS = 4e3;
|
|
704
692
|
function classifyProbeStatus(status) {
|
|
705
693
|
if (status >= 500) return void 0;
|
|
@@ -765,7 +753,6 @@ var CATALOG = /* @__PURE__ */ new Set([
|
|
|
765
753
|
"dataCenters"
|
|
766
754
|
]);
|
|
767
755
|
var V2_ONLY = /* @__PURE__ */ new Set([
|
|
768
|
-
"tags",
|
|
769
756
|
"workers",
|
|
770
757
|
"billing"
|
|
771
758
|
]);
|
|
@@ -783,7 +770,6 @@ var V2_REST_PATHS = {
|
|
|
783
770
|
list: "/catalog/datacenters",
|
|
784
771
|
get: (id) => `/catalog/datacenters/${id}`
|
|
785
772
|
},
|
|
786
|
-
tags: { list: "/tags", get: (id) => `/tags/${id}` },
|
|
787
773
|
// Endpoints live under /v2/serverless on v2 (NOT /v2/endpoints). The v2 list
|
|
788
774
|
// response wraps rows under `endpoints` (see V2_UNWRAP_KEY), so the existing
|
|
789
775
|
// unwrap key stays correct.
|
|
@@ -880,8 +866,186 @@ function resolveBackend(opts) {
|
|
|
880
866
|
return version === "v2" ? buildV2Backend(resource, env, unwrap) : buildV1Backend(resource, env, unwrap);
|
|
881
867
|
}
|
|
882
868
|
|
|
869
|
+
// src/server.ts
|
|
870
|
+
var SERVER_NAME = "Runpod API Server";
|
|
871
|
+
var SERVER_VERSION = true ? "3.1.0" : "dev";
|
|
872
|
+
function createServer(version = SERVER_VERSION) {
|
|
873
|
+
return new import_mcp.McpServer({
|
|
874
|
+
name: SERVER_NAME,
|
|
875
|
+
version: `${version} [${restVersionSummary(process.env)}]`,
|
|
876
|
+
capabilities: {
|
|
877
|
+
resources: {},
|
|
878
|
+
tools: {}
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
|
|
883
883
|
// src/tools/runtime.ts
|
|
884
|
-
var
|
|
884
|
+
var import_node_fetch = __toESM(require("node-fetch"), 1);
|
|
885
|
+
var import_node_crypto = require("crypto");
|
|
886
|
+
|
|
887
|
+
// src/_shared/http.ts
|
|
888
|
+
var HttpError = class extends Error {
|
|
889
|
+
constructor(prefix, status, body) {
|
|
890
|
+
const hint = status === 401 ? " (the Runpod API rejected the credential \u2014 the API key may be expired or revoked; re-authenticate or update RUNPOD_API_KEY)" : "";
|
|
891
|
+
super(`${prefix}: ${status} - ${body}${hint}`);
|
|
892
|
+
this.name = "HttpError";
|
|
893
|
+
this.status = status;
|
|
894
|
+
this.body = body;
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
function isJsonContentType(contentType) {
|
|
898
|
+
if (!contentType) return false;
|
|
899
|
+
const ct = contentType.toLowerCase();
|
|
900
|
+
return ct.includes("application/json") || ct.includes("+json");
|
|
901
|
+
}
|
|
902
|
+
function methodSendsBody(method) {
|
|
903
|
+
return method === "POST" || method === "PATCH" || method === "PUT";
|
|
904
|
+
}
|
|
905
|
+
function buildRequestHeaders(apiKey, tracking) {
|
|
906
|
+
return {
|
|
907
|
+
Authorization: `Bearer ${apiKey}`,
|
|
908
|
+
"Content-Type": "application/json",
|
|
909
|
+
...tracking()
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function createHttpClient(deps) {
|
|
913
|
+
return async function request(url, method = "GET", body) {
|
|
914
|
+
const init = {
|
|
915
|
+
method,
|
|
916
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking)
|
|
917
|
+
};
|
|
918
|
+
if (body && methodSendsBody(method)) {
|
|
919
|
+
init.body = JSON.stringify(body);
|
|
920
|
+
}
|
|
921
|
+
const response = await deps.fetch(url, init);
|
|
922
|
+
if (!response.ok) {
|
|
923
|
+
throw new HttpError(
|
|
924
|
+
deps.errorPrefix,
|
|
925
|
+
response.status,
|
|
926
|
+
await response.text()
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/_shared/tracking.ts
|
|
934
|
+
function sanitizeUaToken(value) {
|
|
935
|
+
return value.replace(/[()<>@,;:\\"/[\]?={}\s]/g, "_").slice(0, 64);
|
|
936
|
+
}
|
|
937
|
+
function buildTrackingHeaders(input) {
|
|
938
|
+
const name = sanitizeUaToken(input.clientName || "unknown");
|
|
939
|
+
const version = sanitizeUaToken(input.clientVersion || "unknown");
|
|
940
|
+
const userAgent = `runpod-mcp-server/${input.serverVersion} (caller=mcp; client=${name}; client_version=${version}; transport=${input.transport})`;
|
|
941
|
+
return {
|
|
942
|
+
"User-Agent": userAgent,
|
|
943
|
+
"X-Runpod-Session-Id": input.sessionId
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/tools/logs.ts
|
|
948
|
+
var import_zod = require("zod");
|
|
949
|
+
function parseLogSse(raw) {
|
|
950
|
+
const items = [];
|
|
951
|
+
for (const block of raw.split(/\r?\n\r?\n/)) {
|
|
952
|
+
const dataLines = block.split(/\r?\n/).filter((l) => l.startsWith("data:"));
|
|
953
|
+
if (!dataLines.length) continue;
|
|
954
|
+
const payload = dataLines.map((l) => l.slice(5).replace(/^ /, "")).join("\n");
|
|
955
|
+
if (!payload.trim()) continue;
|
|
956
|
+
try {
|
|
957
|
+
const parsed = JSON.parse(payload);
|
|
958
|
+
items.push(
|
|
959
|
+
parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : { raw: payload }
|
|
960
|
+
);
|
|
961
|
+
} catch {
|
|
962
|
+
items.push({ raw: payload });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return items;
|
|
966
|
+
}
|
|
967
|
+
var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
|
|
968
|
+
var LOG_STREAM_MAX_BYTES = 256 * 1024;
|
|
969
|
+
async function readSseSnapshot(fetchImpl, url, headers, opts) {
|
|
970
|
+
const controller = new AbortController();
|
|
971
|
+
const timer = setTimeout(() => controller.abort(), opts.maxWaitMs);
|
|
972
|
+
const chunks = [];
|
|
973
|
+
let bytes = 0;
|
|
974
|
+
let truncated = false;
|
|
975
|
+
try {
|
|
976
|
+
const res = await fetchImpl(url, {
|
|
977
|
+
method: "GET",
|
|
978
|
+
headers,
|
|
979
|
+
signal: controller.signal
|
|
980
|
+
});
|
|
981
|
+
if (!res.ok) {
|
|
982
|
+
throw new HttpError(
|
|
983
|
+
"Runpod API Error",
|
|
984
|
+
res.status,
|
|
985
|
+
await res.text().catch(() => "")
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
for await (const chunk of res.body ?? []) {
|
|
989
|
+
chunks.push(chunk);
|
|
990
|
+
bytes += chunk.length;
|
|
991
|
+
if (bytes >= opts.maxBytes) {
|
|
992
|
+
truncated = true;
|
|
993
|
+
controller.abort();
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
} catch (err) {
|
|
998
|
+
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
999
|
+
} finally {
|
|
1000
|
+
clearTimeout(timer);
|
|
1001
|
+
}
|
|
1002
|
+
return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
1003
|
+
}
|
|
1004
|
+
var logStreamParams = {
|
|
1005
|
+
source: import_zod.z.enum(["container", "system", "both"]).optional().describe("Which log source to read (default: both)"),
|
|
1006
|
+
tail: import_zod.z.number().int().min(0).max(5e3).optional().describe(
|
|
1007
|
+
"Historical lines to backfill before live output (API default 100, max 5000; 0 = live only). Ignored when `since` is set."
|
|
1008
|
+
),
|
|
1009
|
+
since: import_zod.z.string().optional().describe("RFC3339 timestamp to resume from; when set, `tail` is ignored."),
|
|
1010
|
+
maxWaitMs: import_zod.z.number().int().min(500).max(3e4).optional().describe("How long to read the stream, in ms (default 5000, max 30000)")
|
|
1011
|
+
};
|
|
1012
|
+
async function collectLogSnapshot(streamSse, logsUrl, params) {
|
|
1013
|
+
const qs = new URLSearchParams();
|
|
1014
|
+
if (params.source && params.source !== "both")
|
|
1015
|
+
qs.append("source", params.source);
|
|
1016
|
+
if (params.tail !== void 0) qs.append("tail", String(params.tail));
|
|
1017
|
+
if (params.since) qs.append("since", params.since);
|
|
1018
|
+
const query = qs.toString() ? `?${qs}` : "";
|
|
1019
|
+
const { raw, truncated } = await streamSse(`${logsUrl}${query}`, {
|
|
1020
|
+
maxWaitMs: params.maxWaitMs ?? LOG_STREAM_DEFAULT_WAIT_MS,
|
|
1021
|
+
maxBytes: LOG_STREAM_MAX_BYTES
|
|
1022
|
+
});
|
|
1023
|
+
const items = parseLogSse(raw);
|
|
1024
|
+
if (truncated) items.pop();
|
|
1025
|
+
return { items, count: items.length, truncated };
|
|
1026
|
+
}
|
|
1027
|
+
async function streamLogsReply(rt, tool, params) {
|
|
1028
|
+
const backend = rt.backendFor(tool.resource);
|
|
1029
|
+
if (backend.version === "v1") {
|
|
1030
|
+
return rt.jsonReply({
|
|
1031
|
+
error: `${tool.name} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
1032
|
+
status: 501
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
return rt.jsonReply(
|
|
1037
|
+
await collectLogSnapshot(rt.streamSse, tool.logsUrl(backend), params)
|
|
1038
|
+
);
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if (error instanceof HttpError) {
|
|
1041
|
+
return rt.jsonReply({ error: error.message, status: error.status });
|
|
1042
|
+
}
|
|
1043
|
+
throw error;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/tools/runtime.ts
|
|
1048
|
+
var MCP_SERVER_VERSION = true ? "3.1.0" : "dev";
|
|
885
1049
|
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
886
1050
|
function trackingHeaders(server, ctx) {
|
|
887
1051
|
const info = server.server.getClientVersion();
|
|
@@ -918,14 +1082,18 @@ var DESTRUCTIVE = {
|
|
|
918
1082
|
idempotentHint: true,
|
|
919
1083
|
openWorldHint: true
|
|
920
1084
|
};
|
|
921
|
-
async function graphqlRequest(query, tracking, fetchImpl, url) {
|
|
1085
|
+
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
922
1086
|
const response = await fetchImpl(url, {
|
|
923
1087
|
method: "POST",
|
|
924
1088
|
headers: {
|
|
925
1089
|
"Content-Type": "application/json",
|
|
1090
|
+
...options?.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : {},
|
|
926
1091
|
...tracking()
|
|
927
1092
|
},
|
|
928
|
-
body: JSON.stringify({
|
|
1093
|
+
body: JSON.stringify({
|
|
1094
|
+
query,
|
|
1095
|
+
...options?.variables ? { variables: options.variables } : {}
|
|
1096
|
+
})
|
|
929
1097
|
});
|
|
930
1098
|
const result = await response.json();
|
|
931
1099
|
if (result.errors && result.errors.length > 0) {
|
|
@@ -940,7 +1108,17 @@ async function graphqlRequest(query, tracking, fetchImpl, url) {
|
|
|
940
1108
|
}
|
|
941
1109
|
function createToolRuntime(server, ctx, deps = {}) {
|
|
942
1110
|
const tracking = () => trackingHeaders(server, ctx);
|
|
943
|
-
|
|
1111
|
+
function observeUnauthorized(fn) {
|
|
1112
|
+
const notify = ctx.onUnauthorized;
|
|
1113
|
+
if (!notify) return fn;
|
|
1114
|
+
return async (...args) => {
|
|
1115
|
+
const response = await fn(...args);
|
|
1116
|
+
if (response.status === 401) notify();
|
|
1117
|
+
return response;
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
const rawFetch = deps.fetch ?? defaultFetch;
|
|
1121
|
+
const httpFetch = observeUnauthorized(rawFetch);
|
|
944
1122
|
const v1Client = createHttpClient({
|
|
945
1123
|
apiKey: ctx.apiKey,
|
|
946
1124
|
fetch: httpFetch,
|
|
@@ -975,47 +1153,19 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
975
1153
|
"Error calling Runpod API:",
|
|
976
1154
|
() => restClient(url, method, body)
|
|
977
1155
|
);
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
if (!res.ok) {
|
|
992
|
-
throw new HttpError(
|
|
993
|
-
"Runpod API Error",
|
|
994
|
-
res.status,
|
|
995
|
-
await res.text().catch(() => "")
|
|
996
|
-
);
|
|
997
|
-
}
|
|
998
|
-
const chunks = [];
|
|
999
|
-
let bytes = 0;
|
|
1000
|
-
let truncated = false;
|
|
1001
|
-
try {
|
|
1002
|
-
for await (const chunk of res.body) {
|
|
1003
|
-
chunks.push(chunk);
|
|
1004
|
-
bytes += chunk.length;
|
|
1005
|
-
if (bytes >= opts.maxBytes) {
|
|
1006
|
-
truncated = true;
|
|
1007
|
-
controller.abort();
|
|
1008
|
-
break;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
} catch (err) {
|
|
1012
|
-
if (!(err instanceof Error && err.name === "AbortError")) throw err;
|
|
1013
|
-
}
|
|
1014
|
-
return { raw: Buffer.concat(chunks).toString("utf8"), truncated };
|
|
1015
|
-
} finally {
|
|
1016
|
-
clearTimeout(timer);
|
|
1017
|
-
}
|
|
1018
|
-
});
|
|
1156
|
+
const sseFetch = observeUnauthorized(
|
|
1157
|
+
deps.sseFetch ?? import_node_fetch.default
|
|
1158
|
+
);
|
|
1159
|
+
const streamSse = deps.streamSse ?? ((url, opts) => readSseSnapshot(
|
|
1160
|
+
sseFetch,
|
|
1161
|
+
url,
|
|
1162
|
+
{
|
|
1163
|
+
Authorization: `Bearer ${ctx.apiKey}`,
|
|
1164
|
+
Accept: "text/event-stream",
|
|
1165
|
+
...tracking()
|
|
1166
|
+
},
|
|
1167
|
+
opts
|
|
1168
|
+
));
|
|
1019
1169
|
const backendFor = (resource) => resolveBackend({
|
|
1020
1170
|
resource,
|
|
1021
1171
|
env: process.env,
|
|
@@ -1037,9 +1187,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
1037
1187
|
graphql: (query) => graphqlRequest(
|
|
1038
1188
|
query,
|
|
1039
1189
|
tracking,
|
|
1040
|
-
httpFetch
|
|
1190
|
+
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
1191
|
+
rawFetch,
|
|
1041
1192
|
publicGraphqlBase(process.env)
|
|
1042
1193
|
),
|
|
1194
|
+
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
1195
|
+
query,
|
|
1196
|
+
tracking,
|
|
1197
|
+
httpFetch,
|
|
1198
|
+
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
1199
|
+
authedGraphqlBase(process.env),
|
|
1200
|
+
{ variables, apiKey: ctx.apiKey }
|
|
1201
|
+
),
|
|
1043
1202
|
runpodRequest,
|
|
1044
1203
|
serverlessRequest,
|
|
1045
1204
|
callRestUrl,
|
|
@@ -1051,17 +1210,17 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
1051
1210
|
}
|
|
1052
1211
|
|
|
1053
1212
|
// src/tools/catalog.ts
|
|
1054
|
-
var
|
|
1213
|
+
var import_zod3 = require("zod");
|
|
1055
1214
|
|
|
1056
1215
|
// src/pagination.ts
|
|
1057
|
-
var
|
|
1216
|
+
var import_zod2 = require("zod");
|
|
1058
1217
|
var DEFAULT_LIST_LIMIT = 20;
|
|
1059
1218
|
var MAX_LIST_LIMIT = 100;
|
|
1060
1219
|
var listPaginationParams = {
|
|
1061
|
-
limit:
|
|
1220
|
+
limit: import_zod2.z.number().int().positive().max(MAX_LIST_LIMIT).optional().describe(
|
|
1062
1221
|
`Maximum number of items to return (default ${DEFAULT_LIST_LIMIT}, max ${MAX_LIST_LIMIT}). Use the returned nextCursor to fetch the next page.`
|
|
1063
1222
|
),
|
|
1064
|
-
cursor:
|
|
1223
|
+
cursor: import_zod2.z.string().optional().describe(
|
|
1065
1224
|
"Opaque pagination cursor from a previous response (nextCursor). Omit to start from the beginning."
|
|
1066
1225
|
)
|
|
1067
1226
|
};
|
|
@@ -1119,25 +1278,32 @@ function registerCatalogTools(server, rt) {
|
|
|
1119
1278
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
1120
1279
|
server.tool(
|
|
1121
1280
|
"list-gpu-types",
|
|
1122
|
-
"List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint.
|
|
1281
|
+
"List available GPU types with stock/pricing and capability filters (minimum VRAM, secure/community cloud, name search). Use this to discover valid gpuTypeIds before creating a pod or endpoint. By default the full catalog is returned: each result includes an `availability` summary (HIGH/MEDIUM/LOW/NONE) and results are sorted with the most-available GPUs first, but nothing is hidden. Set includeUnavailable:false to drop out-of-stock GPUs and list only deployable ones (set includeAvailability:false to skip the stock lookup entirely). For per-datacenter availability (to pick a dataCenterIds), use get-gpu-type.",
|
|
1123
1282
|
{
|
|
1124
1283
|
...listPaginationParams,
|
|
1125
|
-
minMemoryGb:
|
|
1126
|
-
secureCloudOnly:
|
|
1127
|
-
communityCloudOnly:
|
|
1128
|
-
searchTerm:
|
|
1284
|
+
minMemoryGb: import_zod3.z.number().optional().describe("Filter to GPUs with at least this much VRAM in GB"),
|
|
1285
|
+
secureCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in secure cloud"),
|
|
1286
|
+
communityCloudOnly: import_zod3.z.boolean().optional().describe("Filter to only GPUs available in community cloud"),
|
|
1287
|
+
searchTerm: import_zod3.z.string().optional().describe(
|
|
1129
1288
|
"Search term to filter GPUs by name (e.g., 'A100', 'RTX 4090')"
|
|
1130
1289
|
),
|
|
1131
|
-
includeUnavailable:
|
|
1132
|
-
"
|
|
1290
|
+
includeUnavailable: import_zod3.z.boolean().optional().describe(
|
|
1291
|
+
"Out-of-stock GPUs are included by default (annotated availability:NONE and sorted last). Set false to hide them and list only currently-deployable GPUs."
|
|
1292
|
+
),
|
|
1293
|
+
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1294
|
+
"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."
|
|
1133
1295
|
)
|
|
1134
1296
|
},
|
|
1135
1297
|
{ title: "List GPU types", ...READ_ONLY },
|
|
1136
1298
|
async (params) => {
|
|
1137
1299
|
const backend = backendFor("gpus");
|
|
1138
1300
|
if (backend.version === "v2") {
|
|
1139
|
-
const
|
|
1301
|
+
const wantAvailability = params.includeAvailability !== false;
|
|
1302
|
+
const raw = await callRestUrl(
|
|
1303
|
+
`${backend.base}${backend.list}${wantAvailability ? "?include=AVAILABILITY" : ""}`
|
|
1304
|
+
);
|
|
1140
1305
|
let gpus = backend.unwrap(raw);
|
|
1306
|
+
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
1141
1307
|
if (params.minMemoryGb !== void 0)
|
|
1142
1308
|
gpus = gpus.filter(
|
|
1143
1309
|
(g) => Number(g.memory ?? 0) >= params.minMemoryGb
|
|
@@ -1150,6 +1316,18 @@ function registerCatalogTools(server, rt) {
|
|
|
1150
1316
|
(g) => String(g.id ?? "").toLowerCase().includes(t) || String(g.name ?? "").toLowerCase().includes(t)
|
|
1151
1317
|
);
|
|
1152
1318
|
}
|
|
1319
|
+
if (params.includeUnavailable === false)
|
|
1320
|
+
gpus = gpus.filter((g) => g.availability !== "NONE");
|
|
1321
|
+
const rank = {
|
|
1322
|
+
HIGH: 3,
|
|
1323
|
+
MEDIUM: 2,
|
|
1324
|
+
LOW: 1,
|
|
1325
|
+
NONE: 0
|
|
1326
|
+
};
|
|
1327
|
+
gpus.sort(
|
|
1328
|
+
(a, b) => (rank[String(b.availability)] ?? 0) - (rank[String(a.availability)] ?? 0)
|
|
1329
|
+
);
|
|
1330
|
+
gpus = gpus.map(({ dataCenters: _dataCenters, ...rest }) => rest);
|
|
1153
1331
|
return capListResult(gpus, {
|
|
1154
1332
|
limit: params.limit,
|
|
1155
1333
|
cursor: params.cursor
|
|
@@ -1179,7 +1357,7 @@ function registerCatalogTools(server, rt) {
|
|
|
1179
1357
|
return !!status && status !== "Out";
|
|
1180
1358
|
};
|
|
1181
1359
|
let gpuTypes = data.gpuTypes.filter((gpu) => gpu.id !== "unknown");
|
|
1182
|
-
if (
|
|
1360
|
+
if (params.includeUnavailable === false) {
|
|
1183
1361
|
gpuTypes = gpuTypes.filter(isAvailable);
|
|
1184
1362
|
}
|
|
1185
1363
|
if (params.minMemoryGb) {
|
|
@@ -1225,7 +1403,7 @@ function registerCatalogTools(server, rt) {
|
|
|
1225
1403
|
"List Runpod data centers (id, name, region/location). Use this to discover valid dataCenterIds for placing pods, endpoints, or network volumes.",
|
|
1226
1404
|
{
|
|
1227
1405
|
...listPaginationParams,
|
|
1228
|
-
region:
|
|
1406
|
+
region: import_zod3.z.string().optional().describe(
|
|
1229
1407
|
"Filter by region/location (e.g., 'United States', 'Europe', 'Canada')"
|
|
1230
1408
|
)
|
|
1231
1409
|
},
|
|
@@ -1281,80 +1459,654 @@ function registerCatalogTools(server, rt) {
|
|
|
1281
1459
|
status: 501
|
|
1282
1460
|
});
|
|
1283
1461
|
}
|
|
1284
|
-
const raw = await callRestUrl(`${backend.base}${backend.list}`);
|
|
1285
|
-
return capListResult(backend.unwrap(raw), {
|
|
1462
|
+
const raw = await callRestUrl(`${backend.base}${backend.list}`);
|
|
1463
|
+
return capListResult(backend.unwrap(raw), {
|
|
1464
|
+
limit: params.limit,
|
|
1465
|
+
cursor: params.cursor
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
1469
|
+
server.tool(
|
|
1470
|
+
"get-gpu-type",
|
|
1471
|
+
"Get details for a single GPU type by id, including per-datacenter availability. v2-only \u2014 returns a 501 notice on the v1 API (use list-gpu-types there). Use the returned dataCenters[].availability to pick a dataCenterIds with stock before creating a pod.",
|
|
1472
|
+
{
|
|
1473
|
+
gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
|
|
1474
|
+
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1475
|
+
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1476
|
+
)
|
|
1477
|
+
},
|
|
1478
|
+
{ title: "Get GPU type", ...READ_ONLY },
|
|
1479
|
+
async (params) => {
|
|
1480
|
+
const backend = backendFor("gpus");
|
|
1481
|
+
if (backend.version === "v1") {
|
|
1482
|
+
return jsonReply2({
|
|
1483
|
+
error: "get-gpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2 (or use list-gpu-types on v1).",
|
|
1484
|
+
status: 501
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
const path2 = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1488
|
+
const query = params.includeAvailability === false ? "" : "?include=AVAILABILITY";
|
|
1489
|
+
const result = await callRestUrl(`${backend.base}${path2}${query}`);
|
|
1490
|
+
return jsonReply2(result);
|
|
1491
|
+
}
|
|
1492
|
+
);
|
|
1493
|
+
server.tool(
|
|
1494
|
+
"get-cpu-type",
|
|
1495
|
+
"Get details for a single CPU flavor type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-cpu-types there).",
|
|
1496
|
+
{
|
|
1497
|
+
cpuTypeId: import_zod3.z.string().describe("ID of the CPU type to retrieve")
|
|
1498
|
+
},
|
|
1499
|
+
{ title: "Get CPU type", ...READ_ONLY },
|
|
1500
|
+
async (params) => {
|
|
1501
|
+
const backend = backendFor("cpus");
|
|
1502
|
+
if (backend.version === "v1") {
|
|
1503
|
+
return jsonReply2({
|
|
1504
|
+
error: "get-cpu-type is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
1505
|
+
status: 501
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
const result = await callRestUrl(
|
|
1509
|
+
`${backend.base}${backend.get(params.cpuTypeId)}`
|
|
1510
|
+
);
|
|
1511
|
+
return jsonReply2(result);
|
|
1512
|
+
}
|
|
1513
|
+
);
|
|
1514
|
+
server.tool(
|
|
1515
|
+
"get-data-center",
|
|
1516
|
+
"Get details for a single data center by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-data-centers there).",
|
|
1517
|
+
{
|
|
1518
|
+
dataCenterId: import_zod3.z.string().describe("ID of the data center to retrieve")
|
|
1519
|
+
},
|
|
1520
|
+
{ title: "Get data center", ...READ_ONLY },
|
|
1521
|
+
async (params) => {
|
|
1522
|
+
const backend = backendFor("dataCenters");
|
|
1523
|
+
if (backend.version === "v1") {
|
|
1524
|
+
return jsonReply2({
|
|
1525
|
+
error: "get-data-center is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
1526
|
+
status: 501
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
const result = await callRestUrl(
|
|
1530
|
+
`${backend.base}${backend.get(params.dataCenterId)}`
|
|
1531
|
+
);
|
|
1532
|
+
return jsonReply2(result);
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/tools/hub.ts
|
|
1538
|
+
var import_zod4 = require("zod");
|
|
1539
|
+
function parseReleaseConfig(config) {
|
|
1540
|
+
if (!config) return void 0;
|
|
1541
|
+
try {
|
|
1542
|
+
return JSON.parse(config);
|
|
1543
|
+
} catch {
|
|
1544
|
+
return config;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function listingsQuery(withConfig) {
|
|
1548
|
+
return `
|
|
1549
|
+
query {
|
|
1550
|
+
listings(input: {}) {
|
|
1551
|
+
id
|
|
1552
|
+
repoId
|
|
1553
|
+
title
|
|
1554
|
+
description
|
|
1555
|
+
repoName
|
|
1556
|
+
repoOwner
|
|
1557
|
+
createdAt
|
|
1558
|
+
updatedAt
|
|
1559
|
+
views
|
|
1560
|
+
stars
|
|
1561
|
+
deploys
|
|
1562
|
+
language
|
|
1563
|
+
category
|
|
1564
|
+
tags
|
|
1565
|
+
type
|
|
1566
|
+
listedRelease {
|
|
1567
|
+
id
|
|
1568
|
+
name
|
|
1569
|
+
tagName
|
|
1570
|
+
createdAt${withConfig ? "\n config" : ""}
|
|
1571
|
+
build {
|
|
1572
|
+
id
|
|
1573
|
+
imageName
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
`;
|
|
1579
|
+
}
|
|
1580
|
+
function serializeBoolean(value, input) {
|
|
1581
|
+
if (value === "true") return input.trueValue ?? "true";
|
|
1582
|
+
if (value === "false") return input.falseValue ?? "false";
|
|
1583
|
+
return value;
|
|
1584
|
+
}
|
|
1585
|
+
function buildHubEnv(config, overrides) {
|
|
1586
|
+
const env = [];
|
|
1587
|
+
const missingRequired = [];
|
|
1588
|
+
const remaining = { ...overrides };
|
|
1589
|
+
for (const entry of config.env ?? []) {
|
|
1590
|
+
const input = entry.input ?? {};
|
|
1591
|
+
const override = entry.key in remaining ? remaining[entry.key] : void 0;
|
|
1592
|
+
delete remaining[entry.key];
|
|
1593
|
+
if (override !== void 0 && override !== "") {
|
|
1594
|
+
env.push({
|
|
1595
|
+
key: entry.key,
|
|
1596
|
+
value: input.type === "boolean" ? serializeBoolean(override, input) : override
|
|
1597
|
+
});
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
if (input.default !== void 0 && input.default !== null) {
|
|
1601
|
+
env.push({
|
|
1602
|
+
key: entry.key,
|
|
1603
|
+
value: typeof input.default === "boolean" ? input.default ? input.trueValue ?? "true" : input.falseValue ?? "false" : String(input.default)
|
|
1604
|
+
});
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
if (input.required) {
|
|
1608
|
+
missingRequired.push(entry.key);
|
|
1609
|
+
env.push({ key: entry.key, value: "" });
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
for (const [key, value] of Object.entries(remaining)) {
|
|
1614
|
+
env.push({ key, value });
|
|
1615
|
+
}
|
|
1616
|
+
return { env, missingRequired };
|
|
1617
|
+
}
|
|
1618
|
+
function minCudaVersion(versions) {
|
|
1619
|
+
if (!versions || versions.length === 0) return null;
|
|
1620
|
+
const compare = (a, b) => {
|
|
1621
|
+
const pa = a.split(".").map(Number);
|
|
1622
|
+
const pb = b.split(".").map(Number);
|
|
1623
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1624
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1625
|
+
if (d !== 0) return d;
|
|
1626
|
+
}
|
|
1627
|
+
return 0;
|
|
1628
|
+
};
|
|
1629
|
+
return [...versions].sort(compare)[0];
|
|
1630
|
+
}
|
|
1631
|
+
function randomSuffix() {
|
|
1632
|
+
return Math.random().toString(36).slice(2, 8);
|
|
1633
|
+
}
|
|
1634
|
+
function registerHubTools(server, rt) {
|
|
1635
|
+
const { graphql, graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
1636
|
+
server.tool(
|
|
1637
|
+
"list-hub-repos",
|
|
1638
|
+
"List repos published to the Runpod Hub (prebuilt Serverless workers and Pod templates, e.g. vLLM, ComfyUI). Public catalog \u2014 no auth required. Each result includes the currently listed release with its hubReleaseId and prebuilt image name. Results are sorted by deploy count (most popular first). Set includeConfig:true to also return the release config (hardware requirements and environment-variable schema) \u2014 it is large, so prefer requesting it for a single repo via the repoOwner/searchTerm filters.",
|
|
1639
|
+
{
|
|
1640
|
+
...listPaginationParams,
|
|
1641
|
+
searchTerm: import_zod4.z.string().optional().describe(
|
|
1642
|
+
"Case-insensitive search across title, description, repo name, owner, and tags (e.g. 'vllm', 'comfyui', 'fine-tuning')"
|
|
1643
|
+
),
|
|
1644
|
+
category: import_zod4.z.string().optional().describe(
|
|
1645
|
+
"Filter by category (e.g. 'language', 'image', 'audio', 'video', 'embedding')"
|
|
1646
|
+
),
|
|
1647
|
+
type: import_zod4.z.enum(["SERVERLESS", "POD"]).optional().describe(
|
|
1648
|
+
"Filter by listing type: SERVERLESS (endpoint workers) or POD (pod templates)"
|
|
1649
|
+
),
|
|
1650
|
+
repoOwner: import_zod4.z.string().optional().describe(
|
|
1651
|
+
"Filter to listings from this GitHub owner/org (e.g. 'runpod-workers', 'axolotl-ai-cloud')"
|
|
1652
|
+
),
|
|
1653
|
+
includeConfig: import_zod4.z.boolean().optional().describe(
|
|
1654
|
+
"Include the listed release config (hardware requirements + env-var input schema, parsed from .runpod/hub.json). Large \u2014 off by default."
|
|
1655
|
+
)
|
|
1656
|
+
},
|
|
1657
|
+
{ title: "List Hub repos", ...READ_ONLY },
|
|
1658
|
+
async (params) => {
|
|
1659
|
+
const data = await graphql(
|
|
1660
|
+
listingsQuery(params.includeConfig === true)
|
|
1661
|
+
);
|
|
1662
|
+
let listings = data.listings;
|
|
1663
|
+
if (params.type) {
|
|
1664
|
+
listings = listings.filter(
|
|
1665
|
+
(l) => (l.type ?? "").toUpperCase() === params.type
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
if (params.category) {
|
|
1669
|
+
const term = params.category.toLowerCase();
|
|
1670
|
+
listings = listings.filter(
|
|
1671
|
+
(l) => (l.category ?? "").toLowerCase() === term
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
if (params.repoOwner) {
|
|
1675
|
+
const term = params.repoOwner.toLowerCase();
|
|
1676
|
+
listings = listings.filter(
|
|
1677
|
+
(l) => (l.repoOwner ?? "").toLowerCase() === term
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
if (params.searchTerm) {
|
|
1681
|
+
const term = params.searchTerm.toLowerCase();
|
|
1682
|
+
listings = listings.filter(
|
|
1683
|
+
(l) => [
|
|
1684
|
+
l.title,
|
|
1685
|
+
l.description,
|
|
1686
|
+
l.repoName,
|
|
1687
|
+
l.repoOwner,
|
|
1688
|
+
...l.tags ?? []
|
|
1689
|
+
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
listings = [...listings].sort(
|
|
1693
|
+
(a, b) => (b.deploys ?? 0) - (a.deploys ?? 0) || a.id.localeCompare(b.id)
|
|
1694
|
+
);
|
|
1695
|
+
const result = listings.map((l) => ({
|
|
1696
|
+
id: l.id,
|
|
1697
|
+
repo: `${l.repoOwner}/${l.repoName}`,
|
|
1698
|
+
title: l.title,
|
|
1699
|
+
description: l.description,
|
|
1700
|
+
type: l.type,
|
|
1701
|
+
category: l.category,
|
|
1702
|
+
tags: l.tags,
|
|
1703
|
+
language: l.language,
|
|
1704
|
+
stars: l.stars,
|
|
1705
|
+
deploys: l.deploys,
|
|
1706
|
+
views: l.views,
|
|
1707
|
+
githubUrl: `https://github.com/${l.repoOwner}/${l.repoName}`,
|
|
1708
|
+
hubUrl: `https://console.runpod.io/hub/${l.repoOwner}/${l.repoName}`,
|
|
1709
|
+
createdAt: l.createdAt,
|
|
1710
|
+
updatedAt: l.updatedAt,
|
|
1711
|
+
listedRelease: l.listedRelease ? {
|
|
1712
|
+
hubReleaseId: l.listedRelease.id,
|
|
1713
|
+
name: l.listedRelease.name,
|
|
1714
|
+
tagName: l.listedRelease.tagName,
|
|
1715
|
+
createdAt: l.listedRelease.createdAt,
|
|
1716
|
+
imageName: l.listedRelease.build?.imageName,
|
|
1717
|
+
...params.includeConfig ? { config: parseReleaseConfig(l.listedRelease.config) } : {}
|
|
1718
|
+
} : null
|
|
1719
|
+
}));
|
|
1720
|
+
return capListResult(result, {
|
|
1721
|
+
limit: params.limit,
|
|
1722
|
+
cursor: params.cursor
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
);
|
|
1726
|
+
server.tool(
|
|
1727
|
+
"deploy-hub-repo",
|
|
1728
|
+
"Deploy a Runpod Hub repo's listed release as a new Serverless endpoint (the same as clicking Deploy on the Hub). Identify the repo by `repo` (\"owner/name\" from list-hub-repos) or by `hubReleaseId`. The release supplies the prebuilt image, container disk, CUDA constraints, and env-var defaults; pass `env` to override or fill in values (required keys without a default must be provided \u2014 check list-hub-repos with includeConfig:true for the schema). GPU selection comes from the release config when it specifies one; otherwise pass gpuIds (GPU pool names, e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). Uses the authenticated GraphQL API (no REST home for Hub deploys yet), so it works the same on v1 and v2.",
|
|
1729
|
+
{
|
|
1730
|
+
repo: import_zod4.z.string().optional().describe(
|
|
1731
|
+
`The Hub repo to deploy as "owner/name" (e.g. 'runpod-workers/worker-comfyui'). Deploys its currently listed release. Provide this or hubReleaseId.`
|
|
1732
|
+
),
|
|
1733
|
+
hubReleaseId: import_zod4.z.string().optional().describe(
|
|
1734
|
+
"The Hub release ID to deploy (from list-hub-repos listedRelease.hubReleaseId). Provide this or repo."
|
|
1735
|
+
),
|
|
1736
|
+
name: import_zod4.z.string().optional().describe(
|
|
1737
|
+
"Name for the new endpoint. Defaults to '<listing title> <release tag>'."
|
|
1738
|
+
),
|
|
1739
|
+
env: import_zod4.z.record(import_zod4.z.string()).optional().describe(
|
|
1740
|
+
"Environment variable overrides, merged over the release config defaults. Keys not in the release schema are passed through as-is."
|
|
1741
|
+
),
|
|
1742
|
+
gpuIds: import_zod4.z.string().optional().describe(
|
|
1743
|
+
"Comma-separated GPU pool names for workers (e.g. 'ADA_24' or 'ADA_80_PRO,AMPERE_80'). To pin specific GPU SKUs within a pool, append exclusions prefixed with '-' using GPU type ids from list-gpu-types (e.g. 'AMPERE_16,-NVIDIA RTX 2000 Ada Generation,-NVIDIA RTX A4500' allows only the pool's remaining SKUs). Defaults to the release config's gpuIds; required when the config does not specify one."
|
|
1744
|
+
),
|
|
1745
|
+
gpuCount: import_zod4.z.number().int().positive().optional().describe("GPUs per worker. Defaults to the release config (or 1)."),
|
|
1746
|
+
containerDiskInGb: import_zod4.z.number().int().positive().optional().describe("Container disk size in GB. Defaults to the release config."),
|
|
1747
|
+
workersMin: import_zod4.z.number().int().min(0).optional().describe("Minimum workers (default 0 \u2014 scale to zero)."),
|
|
1748
|
+
workersMax: import_zod4.z.number().int().positive().optional().describe("Maximum workers (default: account default)."),
|
|
1749
|
+
idleTimeout: import_zod4.z.number().int().positive().optional().describe("Seconds a worker idles before scaling down (default 5)."),
|
|
1750
|
+
scalerType: import_zod4.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe("Autoscaler type (default QUEUE_DELAY)."),
|
|
1751
|
+
scalerValue: import_zod4.z.number().optional().describe("Autoscaler target value (default 4)."),
|
|
1752
|
+
executionTimeoutMs: import_zod4.z.number().int().positive().optional().describe("Per-job execution timeout in ms (default 600000)."),
|
|
1753
|
+
flashboot: import_zod4.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (default FLASHBOOT).")
|
|
1754
|
+
},
|
|
1755
|
+
{ title: "Deploy Hub repo", ...WRITE },
|
|
1756
|
+
async (params) => {
|
|
1757
|
+
if (!params.repo && !params.hubReleaseId) {
|
|
1758
|
+
return jsonReply2({
|
|
1759
|
+
error: 'Provide either repo ("owner/name") or hubReleaseId. Use list-hub-repos to discover both.'
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
const catalog = await graphql(listingsQuery(true));
|
|
1763
|
+
let listing;
|
|
1764
|
+
if (params.hubReleaseId) {
|
|
1765
|
+
listing = catalog.listings.find(
|
|
1766
|
+
(l) => l.listedRelease?.id === params.hubReleaseId
|
|
1767
|
+
);
|
|
1768
|
+
} else {
|
|
1769
|
+
const repoKey = params.repo.toLowerCase();
|
|
1770
|
+
listing = catalog.listings.find(
|
|
1771
|
+
(l) => `${l.repoOwner}/${l.repoName}`.toLowerCase() === repoKey
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
if (!listing) {
|
|
1775
|
+
return jsonReply2({
|
|
1776
|
+
error: `No Hub listing found for ${params.hubReleaseId ? `hubReleaseId "${params.hubReleaseId}" (only each repo's currently listed release is deployable)` : `repo "${params.repo}"`}. Use list-hub-repos to see the catalog.`
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
const release = listing.listedRelease;
|
|
1780
|
+
if (!release?.build?.imageName) {
|
|
1781
|
+
return jsonReply2({
|
|
1782
|
+
error: `Hub repo ${listing.repoOwner}/${listing.repoName} has no listed release with a built image, so it cannot be deployed.`
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
if ((listing.type ?? "").toUpperCase() !== "SERVERLESS") {
|
|
1786
|
+
return jsonReply2({
|
|
1787
|
+
error: `Hub repo ${listing.repoOwner}/${listing.repoName} is a ${listing.type} listing \u2014 only SERVERLESS listings deploy as endpoints.`
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
const config = parseReleaseConfig(release.config) ?? {};
|
|
1791
|
+
const gpuIds = params.gpuIds ?? config.gpuIds;
|
|
1792
|
+
if (!gpuIds) {
|
|
1793
|
+
return jsonReply2({
|
|
1794
|
+
error: 'This release does not specify a GPU pool \u2014 pass gpuIds (comma-separated pool names, e.g. "ADA_80_PRO,AMPERE_80"; see the pool field on list-gpu-types).'
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
const { env, missingRequired } = buildHubEnv(config, params.env ?? {});
|
|
1798
|
+
if (missingRequired.length > 0) {
|
|
1799
|
+
return jsonReply2({
|
|
1800
|
+
error: `Missing required environment variables for this release: ${missingRequired.join(
|
|
1801
|
+
", "
|
|
1802
|
+
)}. Pass them via the env parameter.`
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
const endpointName = params.name ?? `${listing.title} ${release.tagName}`;
|
|
1806
|
+
const minCuda = minCudaVersion(config.allowedCudaVersions);
|
|
1807
|
+
const input = {
|
|
1808
|
+
name: endpointName,
|
|
1809
|
+
hubReleaseId: release.id,
|
|
1810
|
+
type: "QB",
|
|
1811
|
+
gpuIds,
|
|
1812
|
+
gpuCount: params.gpuCount ?? config.gpuCount ?? 1,
|
|
1813
|
+
workersMin: params.workersMin ?? 0,
|
|
1814
|
+
workersMax: params.workersMax ?? null,
|
|
1815
|
+
idleTimeout: params.idleTimeout ?? 5,
|
|
1816
|
+
scalerType: params.scalerType ?? "QUEUE_DELAY",
|
|
1817
|
+
scalerValue: params.scalerValue ?? 4,
|
|
1818
|
+
executionTimeoutMs: params.executionTimeoutMs ?? 6e5,
|
|
1819
|
+
flashBootType: params.flashboot ?? "FLASHBOOT",
|
|
1820
|
+
locations: null,
|
|
1821
|
+
networkVolumeIds: null,
|
|
1822
|
+
compliance: [],
|
|
1823
|
+
modelReferences: [],
|
|
1824
|
+
...minCuda ? {
|
|
1825
|
+
minCudaVersion: minCuda,
|
|
1826
|
+
allowedCudaVersions: config.allowedCudaVersions.join(",")
|
|
1827
|
+
} : {},
|
|
1828
|
+
template: {
|
|
1829
|
+
name: `${endpointName}__template__${randomSuffix()}`,
|
|
1830
|
+
imageName: release.build.imageName,
|
|
1831
|
+
containerDiskInGb: params.containerDiskInGb ?? config.containerDiskInGb ?? 20,
|
|
1832
|
+
containerRegistryAuthId: "",
|
|
1833
|
+
dockerArgs: "",
|
|
1834
|
+
startScript: "",
|
|
1835
|
+
ports: "",
|
|
1836
|
+
env
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
const data = await graphqlAuthed(
|
|
1840
|
+
`
|
|
1841
|
+
mutation saveEndpoint($input: EndpointInput!) {
|
|
1842
|
+
saveEndpoint(input: $input) {
|
|
1843
|
+
id
|
|
1844
|
+
name
|
|
1845
|
+
gpuIds
|
|
1846
|
+
gpuCount
|
|
1847
|
+
workersMin
|
|
1848
|
+
workersMax
|
|
1849
|
+
idleTimeout
|
|
1850
|
+
scalerType
|
|
1851
|
+
scalerValue
|
|
1852
|
+
flashBootType
|
|
1853
|
+
templateId
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
`,
|
|
1857
|
+
{ input }
|
|
1858
|
+
);
|
|
1859
|
+
return jsonReply2({
|
|
1860
|
+
endpoint: data.saveEndpoint,
|
|
1861
|
+
deployed: {
|
|
1862
|
+
repo: `${listing.repoOwner}/${listing.repoName}`,
|
|
1863
|
+
release: release.tagName,
|
|
1864
|
+
hubReleaseId: release.id,
|
|
1865
|
+
imageName: release.build.imageName
|
|
1866
|
+
},
|
|
1867
|
+
note: `Endpoint created. Submit jobs with run-endpoint/runsync-endpoint using endpointId "${data.saveEndpoint.id}".`
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/tools/public-endpoints.ts
|
|
1874
|
+
var import_zod5 = require("zod");
|
|
1875
|
+
function parseMetadata(metadata) {
|
|
1876
|
+
if (!metadata) return {};
|
|
1877
|
+
try {
|
|
1878
|
+
const parsed = JSON.parse(metadata);
|
|
1879
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
1880
|
+
} catch {
|
|
1881
|
+
return {};
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
function registerPublicEndpointTools(server, rt) {
|
|
1885
|
+
const { graphql } = rt;
|
|
1886
|
+
server.tool(
|
|
1887
|
+
"list-public-endpoints",
|
|
1888
|
+
"List Runpod Public Endpoints \u2014 managed, pay-per-use model APIs (text, image, video, audio) that require no deployment. Public catalog, no auth required. Each result includes the endpointId to call with run-endpoint/runsync-endpoint (or via https://api.runpod.ai/v2/{endpointId}), the model name, modality, owner, and pricing. Only live endpoints are returned by default; set includeOffline:true to also list ones that are not currently live.",
|
|
1889
|
+
{
|
|
1890
|
+
...listPaginationParams,
|
|
1891
|
+
searchTerm: import_zod5.z.string().optional().describe(
|
|
1892
|
+
"Case-insensitive search across display name, model name, endpoint id, description, owner, and tag (e.g. 'kimi', 'video', 'flux')"
|
|
1893
|
+
),
|
|
1894
|
+
modality: import_zod5.z.string().optional().describe(
|
|
1895
|
+
"Filter by modality/category (the metadata source field, e.g. 'language', 'image', 'video', 'audio')"
|
|
1896
|
+
),
|
|
1897
|
+
owner: import_zod5.z.string().optional().describe(
|
|
1898
|
+
"Filter by model owner (e.g. 'moonshot', 'minimax', 'google')"
|
|
1899
|
+
),
|
|
1900
|
+
includeOffline: import_zod5.z.boolean().optional().describe(
|
|
1901
|
+
"Include endpoints that are not currently live (isLive:false). Default false \u2014 only live endpoints are listed."
|
|
1902
|
+
)
|
|
1903
|
+
},
|
|
1904
|
+
{ title: "List public endpoints", ...READ_ONLY },
|
|
1905
|
+
async (params) => {
|
|
1906
|
+
const data = await graphql(`
|
|
1907
|
+
query {
|
|
1908
|
+
allAiApiPublicConfigs {
|
|
1909
|
+
id
|
|
1910
|
+
aiApiId
|
|
1911
|
+
modelName
|
|
1912
|
+
displayName
|
|
1913
|
+
description
|
|
1914
|
+
metadata
|
|
1915
|
+
isLive
|
|
1916
|
+
createdAt
|
|
1917
|
+
updatedAt
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
`);
|
|
1921
|
+
let configs = data.allAiApiPublicConfigs.map((c) => ({
|
|
1922
|
+
config: c,
|
|
1923
|
+
meta: parseMetadata(c.metadata)
|
|
1924
|
+
}));
|
|
1925
|
+
if (!params.includeOffline) {
|
|
1926
|
+
configs = configs.filter(({ config }) => config.isLive);
|
|
1927
|
+
}
|
|
1928
|
+
if (params.modality) {
|
|
1929
|
+
const term = params.modality.toLowerCase();
|
|
1930
|
+
configs = configs.filter(
|
|
1931
|
+
({ meta }) => (meta.source ?? "").toLowerCase() === term
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
if (params.owner) {
|
|
1935
|
+
const term = params.owner.toLowerCase();
|
|
1936
|
+
configs = configs.filter(
|
|
1937
|
+
({ meta }) => (meta.owner ?? "").toLowerCase() === term
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
if (params.searchTerm) {
|
|
1941
|
+
const term = params.searchTerm.toLowerCase();
|
|
1942
|
+
configs = configs.filter(
|
|
1943
|
+
({ config, meta }) => [
|
|
1944
|
+
config.displayName,
|
|
1945
|
+
config.modelName,
|
|
1946
|
+
config.aiApiId,
|
|
1947
|
+
config.description,
|
|
1948
|
+
meta.owner,
|
|
1949
|
+
meta.tag,
|
|
1950
|
+
meta.source
|
|
1951
|
+
].some((field) => (field ?? "").toLowerCase().includes(term))
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
configs = [...configs].sort(
|
|
1955
|
+
(a, b) => a.config.displayName.localeCompare(b.config.displayName)
|
|
1956
|
+
);
|
|
1957
|
+
const result = configs.map(({ config, meta }) => ({
|
|
1958
|
+
endpointId: config.aiApiId,
|
|
1959
|
+
displayName: config.displayName,
|
|
1960
|
+
modelName: config.modelName,
|
|
1961
|
+
description: config.description,
|
|
1962
|
+
modality: meta.source,
|
|
1963
|
+
tag: meta.tag,
|
|
1964
|
+
owner: meta.owner,
|
|
1965
|
+
pricing: meta.priceString,
|
|
1966
|
+
isLive: config.isLive,
|
|
1967
|
+
baseUrl: `https://api.runpod.ai/v2/${config.aiApiId}`,
|
|
1968
|
+
createdAt: config.createdAt,
|
|
1969
|
+
updatedAt: config.updatedAt
|
|
1970
|
+
}));
|
|
1971
|
+
return capListResult(result, {
|
|
1286
1972
|
limit: params.limit,
|
|
1287
1973
|
cursor: params.cursor
|
|
1288
1974
|
});
|
|
1289
1975
|
}
|
|
1290
1976
|
);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// src/tools/endpoint-gpus.ts
|
|
1980
|
+
var import_zod6 = require("zod");
|
|
1981
|
+
function registerEndpointGpuTools(server, rt) {
|
|
1982
|
+
const { graphqlAuthed, jsonReply: jsonReply2 } = rt;
|
|
1291
1983
|
server.tool(
|
|
1292
|
-
"
|
|
1293
|
-
"
|
|
1984
|
+
"set-endpoint-gpus",
|
|
1985
|
+
"Set which GPUs a Serverless endpoint's workers run on \u2014 including pinning specific GPU SKUs, which create-endpoint/update-endpoint cannot express. Provide either a raw gpuIds string, or pools plus optional excludeGpuTypeIds (GPU type ids from list-gpu-types) and the exclusion string is built for you: a pool allows every SKU in it, and excluding all but one SKU pins that SKU exactly (e.g. pools:['AMPERE_16'], excludeGpuTypeIds:['NVIDIA RTX 2000 Ada Generation','NVIDIA RTX 4000 Ada Generation','NVIDIA RTX A4500'] pins RTX A4000). All other endpoint settings (workers, scaling, timeouts, template) are read first and preserved. Works on any Serverless endpoint via the authenticated GraphQL API.",
|
|
1294
1986
|
{
|
|
1295
|
-
|
|
1987
|
+
endpointId: import_zod6.z.string().describe("ID of the Serverless endpoint to update"),
|
|
1988
|
+
gpuIds: import_zod6.z.string().optional().describe(
|
|
1989
|
+
"Raw gpuIds string, e.g. 'ADA_24' or 'AMPERE_16,-NVIDIA RTX A4500'. Takes precedence over pools/excludeGpuTypeIds."
|
|
1990
|
+
),
|
|
1991
|
+
pools: import_zod6.z.array(import_zod6.z.string()).optional().describe(
|
|
1992
|
+
"GPU pool names workers may use (e.g. ['ADA_80_PRO','AMPERE_80']). The pool field from list-gpu-types."
|
|
1993
|
+
),
|
|
1994
|
+
excludeGpuTypeIds: import_zod6.z.array(import_zod6.z.string()).optional().describe(
|
|
1995
|
+
"GPU type ids to exclude from the allowed pools (e.g. ['NVIDIA H100 NVL']). Use with pools to pin specific SKUs."
|
|
1996
|
+
),
|
|
1997
|
+
gpuCount: import_zod6.z.number().int().positive().optional().describe("GPUs per worker. Omit to keep the current value."),
|
|
1998
|
+
minCudaVersion: import_zod6.z.string().optional().describe(
|
|
1999
|
+
"Minimum host CUDA version workers may run on (e.g. '12.4'). Omit to keep the current value."
|
|
2000
|
+
),
|
|
2001
|
+
allowedCudaVersions: import_zod6.z.string().optional().describe(
|
|
2002
|
+
"Comma-separated allowed host CUDA versions (e.g. '12.8,12.7,12.6'). Omit to keep the current value. CUDA compatibility is part of GPU selection \u2014 a narrow list can leave an endpoint unable to schedule workers."
|
|
2003
|
+
)
|
|
1296
2004
|
},
|
|
1297
|
-
{ title: "
|
|
2005
|
+
{ title: "Set endpoint GPUs", ...WRITE, idempotentHint: true },
|
|
1298
2006
|
async (params) => {
|
|
1299
|
-
const
|
|
1300
|
-
|
|
2007
|
+
const gpuIds = params.gpuIds ?? (params.pools && params.pools.length > 0 ? [
|
|
2008
|
+
...params.pools,
|
|
2009
|
+
...(params.excludeGpuTypeIds ?? []).map((id) => `-${id}`)
|
|
2010
|
+
].join(",") : void 0);
|
|
2011
|
+
if (!gpuIds) {
|
|
1301
2012
|
return jsonReply2({
|
|
1302
|
-
error: "
|
|
1303
|
-
status: 501
|
|
2013
|
+
error: "Provide gpuIds (raw string) or pools (with optional excludeGpuTypeIds). See list-gpu-types for pool names and GPU type ids."
|
|
1304
2014
|
});
|
|
1305
2015
|
}
|
|
1306
|
-
const
|
|
1307
|
-
|
|
2016
|
+
const data = await graphqlAuthed(`
|
|
2017
|
+
query {
|
|
2018
|
+
myself {
|
|
2019
|
+
endpoints {
|
|
2020
|
+
id
|
|
2021
|
+
name
|
|
2022
|
+
gpuIds
|
|
2023
|
+
gpuCount
|
|
2024
|
+
workersMin
|
|
2025
|
+
workersMax
|
|
2026
|
+
idleTimeout
|
|
2027
|
+
scalerType
|
|
2028
|
+
scalerValue
|
|
2029
|
+
executionTimeoutMs
|
|
2030
|
+
flashBootType
|
|
2031
|
+
type
|
|
2032
|
+
locations
|
|
2033
|
+
templateId
|
|
2034
|
+
allowedCudaVersions
|
|
2035
|
+
minCudaVersion
|
|
2036
|
+
compliance
|
|
2037
|
+
modelReferences
|
|
2038
|
+
networkVolumeIds {
|
|
2039
|
+
networkVolumeId
|
|
2040
|
+
dataCenterId
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
`);
|
|
2046
|
+
const current = data.myself.endpoints.find(
|
|
2047
|
+
(e) => e.id === params.endpointId
|
|
1308
2048
|
);
|
|
1309
|
-
|
|
1310
|
-
}
|
|
1311
|
-
);
|
|
1312
|
-
server.tool(
|
|
1313
|
-
"get-cpu-type",
|
|
1314
|
-
"Get details for a single CPU flavor type by id. v2-only \u2014 returns a 501 notice on the v1 API (use list-cpu-types there).",
|
|
1315
|
-
{
|
|
1316
|
-
cpuTypeId: import_zod2.z.string().describe("ID of the CPU type to retrieve")
|
|
1317
|
-
},
|
|
1318
|
-
{ title: "Get CPU type", ...READ_ONLY },
|
|
1319
|
-
async (params) => {
|
|
1320
|
-
const backend = backendFor("cpus");
|
|
1321
|
-
if (backend.version === "v1") {
|
|
2049
|
+
if (!current) {
|
|
1322
2050
|
return jsonReply2({
|
|
1323
|
-
error:
|
|
1324
|
-
status: 501
|
|
2051
|
+
error: `No Serverless endpoint found with id "${params.endpointId}". Use list-endpoints to see your endpoints.`
|
|
1325
2052
|
});
|
|
1326
2053
|
}
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
2054
|
+
const input = {
|
|
2055
|
+
id: current.id,
|
|
2056
|
+
name: current.name,
|
|
2057
|
+
gpuIds,
|
|
2058
|
+
gpuCount: params.gpuCount ?? current.gpuCount,
|
|
2059
|
+
workersMin: current.workersMin,
|
|
2060
|
+
workersMax: current.workersMax,
|
|
2061
|
+
idleTimeout: current.idleTimeout,
|
|
2062
|
+
scalerType: current.scalerType,
|
|
2063
|
+
scalerValue: current.scalerValue,
|
|
2064
|
+
executionTimeoutMs: current.executionTimeoutMs,
|
|
2065
|
+
flashBootType: current.flashBootType,
|
|
2066
|
+
type: current.type,
|
|
2067
|
+
locations: current.locations,
|
|
2068
|
+
networkVolumeIds: current.networkVolumeIds && current.networkVolumeIds.length > 0 ? (
|
|
2069
|
+
// Drop dataCenterId: NetworkVolumeIdsInput takes networkVolumeId
|
|
2070
|
+
// ONLY, and the read shape is rejected outright.
|
|
2071
|
+
current.networkVolumeIds.map((v) => ({
|
|
2072
|
+
networkVolumeId: v.networkVolumeId
|
|
2073
|
+
}))
|
|
2074
|
+
) : null
|
|
2075
|
+
};
|
|
2076
|
+
for (const [key, value] of Object.entries({
|
|
2077
|
+
templateId: current.templateId,
|
|
2078
|
+
allowedCudaVersions: params.allowedCudaVersions ?? current.allowedCudaVersions,
|
|
2079
|
+
minCudaVersion: params.minCudaVersion ?? current.minCudaVersion,
|
|
2080
|
+
compliance: current.compliance,
|
|
2081
|
+
modelReferences: current.modelReferences
|
|
2082
|
+
})) {
|
|
2083
|
+
if (value !== null && value !== void 0) input[key] = value;
|
|
1347
2084
|
}
|
|
1348
|
-
const result = await
|
|
1349
|
-
|
|
2085
|
+
const result = await graphqlAuthed(
|
|
2086
|
+
`
|
|
2087
|
+
mutation saveEndpoint($input: EndpointInput!) {
|
|
2088
|
+
saveEndpoint(input: $input) {
|
|
2089
|
+
id
|
|
2090
|
+
name
|
|
2091
|
+
gpuIds
|
|
2092
|
+
gpuCount
|
|
2093
|
+
workersMin
|
|
2094
|
+
workersMax
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
`,
|
|
2098
|
+
{ input }
|
|
1350
2099
|
);
|
|
1351
|
-
return jsonReply2(
|
|
2100
|
+
return jsonReply2({
|
|
2101
|
+
endpoint: result.saveEndpoint,
|
|
2102
|
+
previousGpuIds: current.gpuIds
|
|
2103
|
+
});
|
|
1352
2104
|
}
|
|
1353
2105
|
);
|
|
1354
2106
|
}
|
|
1355
2107
|
|
|
1356
2108
|
// src/tools/pods.ts
|
|
1357
|
-
var
|
|
2109
|
+
var import_zod7 = require("zod");
|
|
1358
2110
|
function registerPodTools(server, rt) {
|
|
1359
2111
|
const { jsonReply: jsonReply2, callRestUrl, backendFor, podAction, env } = rt;
|
|
1360
2112
|
server.tool(
|
|
@@ -1362,12 +2114,12 @@ function registerPodTools(server, rt) {
|
|
|
1362
2114
|
"List your pods (running and stopped) with optional filters \u2014 compute type, GPU type, data center, name \u2014 plus machine/network-volume expansion. Paginated via limit/cursor.",
|
|
1363
2115
|
{
|
|
1364
2116
|
...listPaginationParams,
|
|
1365
|
-
computeType:
|
|
1366
|
-
gpuTypeId:
|
|
1367
|
-
dataCenterId:
|
|
1368
|
-
name:
|
|
1369
|
-
includeMachine:
|
|
1370
|
-
includeNetworkVolume:
|
|
2117
|
+
computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe("Filter to only GPU or only CPU Pods"),
|
|
2118
|
+
gpuTypeId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods with any of the listed GPU types"),
|
|
2119
|
+
dataCenterId: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to Pods in any of the provided data centers"),
|
|
2120
|
+
name: import_zod7.z.string().optional().describe("Filter to Pods with the provided name"),
|
|
2121
|
+
includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
|
|
2122
|
+
includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
|
|
1371
2123
|
},
|
|
1372
2124
|
{ title: "List pods", ...READ_ONLY },
|
|
1373
2125
|
async (params) => {
|
|
@@ -1411,9 +2163,9 @@ function registerPodTools(server, rt) {
|
|
|
1411
2163
|
"get-pod",
|
|
1412
2164
|
"Get full details for one pod by id, optionally expanding machine and attached network-volume info.",
|
|
1413
2165
|
{
|
|
1414
|
-
podId:
|
|
1415
|
-
includeMachine:
|
|
1416
|
-
includeNetworkVolume:
|
|
2166
|
+
podId: import_zod7.z.string().describe("ID of the pod to retrieve"),
|
|
2167
|
+
includeMachine: import_zod7.z.boolean().optional().describe("Include information about the machine"),
|
|
2168
|
+
includeNetworkVolume: import_zod7.z.boolean().optional().describe("Include information about attached network volumes")
|
|
1417
2169
|
},
|
|
1418
2170
|
{ title: "Get pod", ...READ_ONLY },
|
|
1419
2171
|
async (params) => {
|
|
@@ -1441,26 +2193,44 @@ function registerPodTools(server, rt) {
|
|
|
1441
2193
|
);
|
|
1442
2194
|
server.tool(
|
|
1443
2195
|
"create-pod",
|
|
1444
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). If the user
|
|
2196
|
+
'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.',
|
|
1445
2197
|
{
|
|
1446
|
-
name:
|
|
1447
|
-
imageName:
|
|
1448
|
-
|
|
2198
|
+
name: import_zod7.z.string().optional().describe("Name for the pod"),
|
|
2199
|
+
imageName: import_zod7.z.string().optional().describe("Docker image to use. Optional when templateId is provided."),
|
|
2200
|
+
templateId: import_zod7.z.string().optional().describe(
|
|
2201
|
+
"Deploy from an existing template (see list-templates / get-template). The template supplies name, image, start command, ports, env, disk, volume, and registry credential as defaults; any field you also pass replaces the template value for that field (whole-field, not a merge). Override the registry credential with containerRegistryAuthId. Requires the v2 REST API. Still pass gpuTypeIds or computeType to choose the compute \u2014 a template does not pin the pod compute."
|
|
2202
|
+
),
|
|
2203
|
+
computeType: import_zod7.z.enum(["GPU", "CPU"]).optional().describe(
|
|
1449
2204
|
"Pod type. On v2, required when not passing gpuTypeIds. 'CPU' is served by the v1 API for now (v2 has no CPU pods yet)."
|
|
1450
2205
|
),
|
|
1451
|
-
cloudType:
|
|
1452
|
-
gpuTypeIds:
|
|
1453
|
-
gpuCount:
|
|
1454
|
-
containerDiskInGb:
|
|
1455
|
-
volumeInGb:
|
|
1456
|
-
volumeMountPath:
|
|
1457
|
-
ports:
|
|
1458
|
-
env:
|
|
1459
|
-
|
|
2206
|
+
cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2207
|
+
gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
|
|
2208
|
+
gpuCount: import_zod7.z.number().optional().describe("Number of GPUs"),
|
|
2209
|
+
containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
|
|
2210
|
+
volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
|
|
2211
|
+
volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
|
|
2212
|
+
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2213
|
+
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
|
|
2214
|
+
containerRegistryAuthId: import_zod7.z.string().optional().describe(
|
|
2215
|
+
"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)."
|
|
2216
|
+
),
|
|
2217
|
+
dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers")
|
|
1460
2218
|
},
|
|
1461
2219
|
{ title: "Create pod", ...WRITE },
|
|
1462
2220
|
async (params) => {
|
|
1463
2221
|
const backend = backendFor("pods");
|
|
2222
|
+
if (params.templateId && backend.version !== "v2") {
|
|
2223
|
+
return jsonReply2({
|
|
2224
|
+
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
2225
|
+
status: 501
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
if (!params.imageName && !params.templateId) {
|
|
2229
|
+
return jsonReply2({
|
|
2230
|
+
error: "Provide imageName, or templateId to deploy from an existing template.",
|
|
2231
|
+
status: 400
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
1464
2234
|
const hasGpuTypes = (params.gpuTypeIds?.length ?? 0) > 0;
|
|
1465
2235
|
const hasGpuCount = (params.gpuCount ?? 0) > 0;
|
|
1466
2236
|
const wantsCpu = params.computeType === "CPU";
|
|
@@ -1485,6 +2255,12 @@ function registerPodTools(server, rt) {
|
|
|
1485
2255
|
});
|
|
1486
2256
|
}
|
|
1487
2257
|
if (wantsCpu) {
|
|
2258
|
+
if (params.templateId) {
|
|
2259
|
+
return jsonReply2({
|
|
2260
|
+
error: "Template-based deploy is currently supported only for GPU pods on the v2 API. Create the CPU pod with an explicit imageName instead.",
|
|
2261
|
+
status: 400
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
1488
2264
|
try {
|
|
1489
2265
|
const result = await callRestUrl(
|
|
1490
2266
|
`${restV1Base(env)}/pods`,
|
|
@@ -1509,7 +2285,24 @@ function registerPodTools(server, rt) {
|
|
|
1509
2285
|
}
|
|
1510
2286
|
}
|
|
1511
2287
|
}
|
|
1512
|
-
|
|
2288
|
+
let body = backend.mapCreate(params);
|
|
2289
|
+
if (params.templateId) {
|
|
2290
|
+
let template;
|
|
2291
|
+
try {
|
|
2292
|
+
template = await callRestUrl(
|
|
2293
|
+
`${restV2Base(env)}/templates/${params.templateId}`
|
|
2294
|
+
);
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
if (error instanceof HttpError) {
|
|
2297
|
+
return jsonReply2({
|
|
2298
|
+
error: `Failed to load template ${params.templateId}: ${error.message}`,
|
|
2299
|
+
status: error.status
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
throw error;
|
|
2303
|
+
}
|
|
2304
|
+
body = { ...podBodyFromTemplate(template), ...body };
|
|
2305
|
+
}
|
|
1513
2306
|
try {
|
|
1514
2307
|
const result = await callRestUrl(
|
|
1515
2308
|
`${backend.base}${backend.list}`,
|
|
@@ -1524,11 +2317,8 @@ function registerPodTools(server, rt) {
|
|
|
1524
2317
|
}
|
|
1525
2318
|
return jsonReply2(result);
|
|
1526
2319
|
} catch (error) {
|
|
1527
|
-
if (error instanceof HttpError
|
|
1528
|
-
return jsonReply2({
|
|
1529
|
-
error: "CPU pods are not yet supported on the v2 REST API. Use a GPU pod, or create the CPU pod via the v1 API (set RUNPOD_REST_VERSION=v1).",
|
|
1530
|
-
status: 501
|
|
1531
|
-
});
|
|
2320
|
+
if (error instanceof HttpError) {
|
|
2321
|
+
return jsonReply2({ error: error.message, status: error.status });
|
|
1532
2322
|
}
|
|
1533
2323
|
throw error;
|
|
1534
2324
|
}
|
|
@@ -1538,14 +2328,14 @@ function registerPodTools(server, rt) {
|
|
|
1538
2328
|
"update-pod",
|
|
1539
2329
|
"Update a pod's mutable fields (name, image, container disk, volume, ports, env). Only the fields you provide change.",
|
|
1540
2330
|
{
|
|
1541
|
-
podId:
|
|
1542
|
-
name:
|
|
1543
|
-
imageName:
|
|
1544
|
-
containerDiskInGb:
|
|
1545
|
-
volumeInGb:
|
|
1546
|
-
volumeMountPath:
|
|
1547
|
-
ports:
|
|
1548
|
-
env:
|
|
2331
|
+
podId: import_zod7.z.string().describe("ID of the pod to update"),
|
|
2332
|
+
name: import_zod7.z.string().optional().describe("New name for the pod"),
|
|
2333
|
+
imageName: import_zod7.z.string().optional().describe("New Docker image"),
|
|
2334
|
+
containerDiskInGb: import_zod7.z.number().optional().describe("New container disk size in GB"),
|
|
2335
|
+
volumeInGb: import_zod7.z.number().optional().describe("New volume size in GB"),
|
|
2336
|
+
volumeMountPath: import_zod7.z.string().optional().describe("New path to mount the volume"),
|
|
2337
|
+
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("New ports to expose"),
|
|
2338
|
+
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("New environment variables")
|
|
1549
2339
|
},
|
|
1550
2340
|
{ title: "Update pod", ...WRITE, idempotentHint: true },
|
|
1551
2341
|
async (params) => {
|
|
@@ -1564,7 +2354,7 @@ function registerPodTools(server, rt) {
|
|
|
1564
2354
|
"start-pod",
|
|
1565
2355
|
"Start a stopped pod (resumes the same pod and its billing).",
|
|
1566
2356
|
{
|
|
1567
|
-
podId:
|
|
2357
|
+
podId: import_zod7.z.string().describe("ID of the pod to start")
|
|
1568
2358
|
},
|
|
1569
2359
|
{ title: "Start pod", ...WRITE, idempotentHint: true },
|
|
1570
2360
|
async (params) => {
|
|
@@ -1576,7 +2366,7 @@ function registerPodTools(server, rt) {
|
|
|
1576
2366
|
"stop-pod",
|
|
1577
2367
|
"Stop a running pod. The pod and its disk persist; GPU billing stops.",
|
|
1578
2368
|
{
|
|
1579
|
-
podId:
|
|
2369
|
+
podId: import_zod7.z.string().describe("ID of the pod to stop")
|
|
1580
2370
|
},
|
|
1581
2371
|
{ title: "Stop pod", ...WRITE, idempotentHint: true },
|
|
1582
2372
|
async (params) => {
|
|
@@ -1588,7 +2378,7 @@ function registerPodTools(server, rt) {
|
|
|
1588
2378
|
"restart-pod",
|
|
1589
2379
|
"Restart a running pod (single PodAction call). v2-only \u2014 on the v1 API returns a 501 notice (stop then start the pod instead).",
|
|
1590
2380
|
{
|
|
1591
|
-
podId:
|
|
2381
|
+
podId: import_zod7.z.string().describe("ID of the pod to restart")
|
|
1592
2382
|
},
|
|
1593
2383
|
{ title: "Restart pod", ...WRITE },
|
|
1594
2384
|
async (params) => {
|
|
@@ -1603,11 +2393,29 @@ function registerPodTools(server, rt) {
|
|
|
1603
2393
|
return jsonReply2(result);
|
|
1604
2394
|
}
|
|
1605
2395
|
);
|
|
2396
|
+
server.tool(
|
|
2397
|
+
"stream-pod-logs",
|
|
2398
|
+
"Fetch a bounded snapshot of a pod's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
|
|
2399
|
+
{
|
|
2400
|
+
podId: import_zod7.z.string().describe("ID of the pod whose logs to stream"),
|
|
2401
|
+
...logStreamParams
|
|
2402
|
+
},
|
|
2403
|
+
{ title: "Stream pod logs", ...READ_ONLY },
|
|
2404
|
+
(params) => streamLogsReply(
|
|
2405
|
+
rt,
|
|
2406
|
+
{
|
|
2407
|
+
name: "stream-pod-logs",
|
|
2408
|
+
resource: "pods",
|
|
2409
|
+
logsUrl: (backend) => `${backend.base}${backend.get(encodeURIComponent(params.podId))}/logs`
|
|
2410
|
+
},
|
|
2411
|
+
params
|
|
2412
|
+
)
|
|
2413
|
+
);
|
|
1606
2414
|
server.tool(
|
|
1607
2415
|
"delete-pod",
|
|
1608
2416
|
"Permanently delete a pod and its data. This cannot be undone.",
|
|
1609
2417
|
{
|
|
1610
|
-
podId:
|
|
2418
|
+
podId: import_zod7.z.string().describe("ID of the pod to delete")
|
|
1611
2419
|
},
|
|
1612
2420
|
{ title: "Delete pod", ...DESTRUCTIVE },
|
|
1613
2421
|
async (params) => {
|
|
@@ -1622,16 +2430,27 @@ function registerPodTools(server, rt) {
|
|
|
1622
2430
|
}
|
|
1623
2431
|
|
|
1624
2432
|
// src/tools/endpoints.ts
|
|
1625
|
-
var
|
|
2433
|
+
var import_zod8 = require("zod");
|
|
2434
|
+
var MIN_QUEUE_DELAY = 0.5;
|
|
2435
|
+
var MIN_REQUEST_COUNT = 1;
|
|
2436
|
+
var MIN_IDLE_TIMEOUT = 1;
|
|
2437
|
+
var MAX_IDLE_TIMEOUT = 3600;
|
|
2438
|
+
function scalerValueError(scalerType, scalerValue) {
|
|
2439
|
+
if (scalerType !== "REQUEST_COUNT" || scalerValue === void 0) return;
|
|
2440
|
+
if (!Number.isInteger(scalerValue) || scalerValue < MIN_REQUEST_COUNT) {
|
|
2441
|
+
return `REQUEST_COUNT scales on whole in-flight requests per worker, so scalerValue must be an integer >= ${MIN_REQUEST_COUNT} (got ${scalerValue}). For a fractional target use scalerType QUEUE_DELAY, which takes seconds >= ${MIN_QUEUE_DELAY}.`;
|
|
2442
|
+
}
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
1626
2445
|
function registerEndpointTools(server, rt) {
|
|
1627
2446
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
1628
2447
|
server.tool(
|
|
1629
2448
|
"list-endpoints",
|
|
1630
|
-
"List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor.",
|
|
2449
|
+
"List your Serverless endpoints, optionally expanding template and worker details (v1 only). Paginated via limit/cursor. On v2 each endpoint carries its request routing in `type` and the URLs to call it in `requestUrls`.",
|
|
1631
2450
|
{
|
|
1632
2451
|
...listPaginationParams,
|
|
1633
|
-
includeTemplate:
|
|
1634
|
-
includeWorkers:
|
|
2452
|
+
includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
|
|
2453
|
+
includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
|
|
1635
2454
|
},
|
|
1636
2455
|
{ title: "List endpoints", ...READ_ONLY },
|
|
1637
2456
|
async (params) => {
|
|
@@ -1662,11 +2481,11 @@ function registerEndpointTools(server, rt) {
|
|
|
1662
2481
|
);
|
|
1663
2482
|
server.tool(
|
|
1664
2483
|
"get-endpoint",
|
|
1665
|
-
"Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only).",
|
|
2484
|
+
"Get one Serverless endpoint by id, optionally expanding template and worker details (v1 only). On v2 the reply carries `type` (queue-based vs load-balancing routing) and `requestUrls` \u2014 the run/runsync/status/... URLs for a queue endpoint, or the base + health URLs for a load-balancing one. Read those rather than constructing endpoint URLs by hand.",
|
|
1666
2485
|
{
|
|
1667
|
-
endpointId:
|
|
1668
|
-
includeTemplate:
|
|
1669
|
-
includeWorkers:
|
|
2486
|
+
endpointId: import_zod8.z.string().describe("ID of the endpoint to retrieve"),
|
|
2487
|
+
includeTemplate: import_zod8.z.boolean().optional().describe("Include template information (v1 only)"),
|
|
2488
|
+
includeWorkers: import_zod8.z.boolean().optional().describe("Include information about workers (v1 only)")
|
|
1670
2489
|
},
|
|
1671
2490
|
{ title: "Get endpoint", ...READ_ONLY },
|
|
1672
2491
|
async (params) => {
|
|
@@ -1694,34 +2513,43 @@ function registerEndpointTools(server, rt) {
|
|
|
1694
2513
|
);
|
|
1695
2514
|
server.tool(
|
|
1696
2515
|
"create-endpoint",
|
|
1697
|
-
"Create a Serverless endpoint. On v2 (default), pass an inline config: imageName + gpuPoolIds (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. AMPERE_80/ADA_24) plus optional workers/scaling/disk/env. On v1, pass a templateId instead. Worker min/max set autoscaling bounds (min 0 = scale to zero).",
|
|
2516
|
+
"Create a Serverless endpoint. On v2 (default), pass an inline config: imageName + gpuPoolIds (GPU pool names from list-gpu-types \u2014 the `pool` field, e.g. AMPERE_80/ADA_24) plus optional workers/scaling/disk/env. On v1, pass a templateId instead. Worker min/max set autoscaling bounds (min 0 = scale to zero). Use endpointType to pick queue-based (default) or load-balancing request routing; the response `requestUrls` carries the URLs to call the endpoint with.",
|
|
1698
2517
|
{
|
|
1699
|
-
name:
|
|
2518
|
+
name: import_zod8.z.string().optional().describe("Name for the endpoint"),
|
|
1700
2519
|
// --- v2 inline-config fields ---
|
|
1701
|
-
imageName:
|
|
1702
|
-
|
|
2520
|
+
imageName: import_zod8.z.string().optional().describe("Docker image (v2). Required on v2 instead of a templateId."),
|
|
2521
|
+
endpointType: import_zod8.z.enum(["QUEUE", "LOAD_BALANCER"]).optional().describe(
|
|
2522
|
+
"How requests reach the workers (v2). QUEUE (default) = submit jobs through the managed queue (run/runsync/status/...). LOAD_BALANCER = send HTTP requests straight to worker-defined paths, and forces scalerType REQUEST_COUNT. Fixed at creation \u2014 update-endpoint cannot change it."
|
|
2523
|
+
),
|
|
2524
|
+
gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe(
|
|
1703
2525
|
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
1704
2526
|
),
|
|
1705
|
-
gpuCount:
|
|
1706
|
-
args:
|
|
1707
|
-
containerDiskInGb:
|
|
1708
|
-
ports:
|
|
1709
|
-
env:
|
|
1710
|
-
containerRegistryAuthId:
|
|
1711
|
-
networkVolumeIds:
|
|
1712
|
-
executionTimeoutMs:
|
|
1713
|
-
flashboot:
|
|
2527
|
+
gpuCount: import_zod8.z.number().optional().describe("GPUs per worker (v2)"),
|
|
2528
|
+
args: import_zod8.z.string().optional().describe("Container start command/args (v2)"),
|
|
2529
|
+
containerDiskInGb: import_zod8.z.number().optional().describe("Container disk size in GB (v2)"),
|
|
2530
|
+
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
2531
|
+
env: import_zod8.z.record(import_zod8.z.string()).optional().describe("Environment variables (v2)"),
|
|
2532
|
+
containerRegistryAuthId: import_zod8.z.string().optional().describe("Container registry auth id for a private image (v2)"),
|
|
2533
|
+
networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("Network volume ids to attach (v2)"),
|
|
2534
|
+
executionTimeoutMs: import_zod8.z.number().optional().describe("Per-job execution timeout in ms (v2)"),
|
|
2535
|
+
flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("FlashBoot mode (v2)"),
|
|
1714
2536
|
// --- v1 template-based fields ---
|
|
1715
|
-
templateId:
|
|
1716
|
-
computeType:
|
|
1717
|
-
gpuTypeIds:
|
|
2537
|
+
templateId: import_zod8.z.string().optional().describe("Template ID (v1). Required on v1."),
|
|
2538
|
+
computeType: import_zod8.z.enum(["GPU", "CPU"]).optional().describe("GPU or CPU endpoint (v1)"),
|
|
2539
|
+
gpuTypeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of acceptable GPU types (v1)"),
|
|
1718
2540
|
// --- shared autoscaling fields ---
|
|
1719
|
-
workersMin:
|
|
1720
|
-
workersMax:
|
|
1721
|
-
scalerType:
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
2541
|
+
workersMin: import_zod8.z.number().optional().describe("Minimum number of workers"),
|
|
2542
|
+
workersMax: import_zod8.z.number().optional().describe("Maximum number of workers"),
|
|
2543
|
+
scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2544
|
+
"Autoscaler signal. QUEUE_DELAY scales on how long requests wait in the queue (queue endpoints only); REQUEST_COUNT scales on in-flight requests per worker. Defaults to QUEUE_DELAY for QUEUE endpoints and REQUEST_COUNT for LOAD_BALANCER ones."
|
|
2545
|
+
),
|
|
2546
|
+
scalerValue: import_zod8.z.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2547
|
+
"Autoscaler target for the chosen scalerType \u2014 seconds of queue delay (min 0.5) or in-flight requests per worker (integer, min 1). Defaults to 4."
|
|
2548
|
+
),
|
|
2549
|
+
idleTimeout: import_zod8.z.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2550
|
+
"Idle timeout in seconds before scaling a worker down (1-3600). Does not apply to QUEUE endpoints scaling on REQUEST_COUNT."
|
|
2551
|
+
),
|
|
2552
|
+
dataCenterIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("List of preferred data centers")
|
|
1725
2553
|
},
|
|
1726
2554
|
{ title: "Create endpoint", ...WRITE },
|
|
1727
2555
|
async (params) => {
|
|
@@ -1745,6 +2573,19 @@ function registerEndpointTools(server, rt) {
|
|
|
1745
2573
|
status: 400
|
|
1746
2574
|
});
|
|
1747
2575
|
}
|
|
2576
|
+
if (params.endpointType === "LOAD_BALANCER" && params.scalerType === "QUEUE_DELAY") {
|
|
2577
|
+
return jsonReply2({
|
|
2578
|
+
error: "LOAD_BALANCER endpoints have no request queue, so they cannot scale on QUEUE_DELAY. Use scalerType REQUEST_COUNT (the default for this endpoint type), or create a QUEUE endpoint instead.",
|
|
2579
|
+
status: 400
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
const scalerError = scalerValueError(
|
|
2583
|
+
params.scalerType ?? (params.endpointType === "LOAD_BALANCER" ? "REQUEST_COUNT" : void 0),
|
|
2584
|
+
params.scalerValue
|
|
2585
|
+
);
|
|
2586
|
+
if (scalerError) {
|
|
2587
|
+
return jsonReply2({ error: scalerError, status: 400 });
|
|
2588
|
+
}
|
|
1748
2589
|
const body = backend.mapCreate(params);
|
|
1749
2590
|
const result2 = await callRestUrl(
|
|
1750
2591
|
`${backend.base}${backend.list}`,
|
|
@@ -1782,46 +2623,67 @@ function registerEndpointTools(server, rt) {
|
|
|
1782
2623
|
);
|
|
1783
2624
|
server.tool(
|
|
1784
2625
|
"update-endpoint",
|
|
1785
|
-
"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.",
|
|
2626
|
+
"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.",
|
|
1786
2627
|
{
|
|
1787
|
-
endpointId:
|
|
1788
|
-
name:
|
|
1789
|
-
workersMin:
|
|
1790
|
-
workersMax:
|
|
1791
|
-
idleTimeout:
|
|
1792
|
-
|
|
1793
|
-
|
|
2628
|
+
endpointId: import_zod8.z.string().describe("ID of the endpoint to update"),
|
|
2629
|
+
name: import_zod8.z.string().optional().describe("New name for the endpoint"),
|
|
2630
|
+
workersMin: import_zod8.z.number().optional().describe("New minimum number of workers"),
|
|
2631
|
+
workersMax: import_zod8.z.number().optional().describe("New maximum number of workers"),
|
|
2632
|
+
idleTimeout: import_zod8.z.number().int().min(MIN_IDLE_TIMEOUT).max(MAX_IDLE_TIMEOUT).optional().describe(
|
|
2633
|
+
"New idle timeout in seconds (1-3600). Does not apply to queue endpoints scaling on REQUEST_COUNT."
|
|
2634
|
+
),
|
|
2635
|
+
scalerType: import_zod8.z.enum(["QUEUE_DELAY", "REQUEST_COUNT"]).optional().describe(
|
|
2636
|
+
"New autoscaler signal. Switchable on queue endpoints; load-balancing endpoints only accept REQUEST_COUNT."
|
|
2637
|
+
),
|
|
2638
|
+
scalerValue: import_zod8.z.number().min(MIN_QUEUE_DELAY).optional().describe(
|
|
2639
|
+
"New autoscaler target \u2014 seconds of queue delay (min 0.5) or in-flight requests per worker (integer, min 1). Applies to the endpoint's current scalerType unless you also pass a new one."
|
|
2640
|
+
),
|
|
1794
2641
|
// --- v2-only inline-config fields ---
|
|
1795
|
-
imageName:
|
|
1796
|
-
gpuPoolIds:
|
|
1797
|
-
gpuCount:
|
|
1798
|
-
args:
|
|
1799
|
-
containerDiskInGb:
|
|
1800
|
-
ports:
|
|
1801
|
-
env:
|
|
1802
|
-
containerRegistryAuthId:
|
|
1803
|
-
networkVolumeIds:
|
|
1804
|
-
executionTimeoutMs:
|
|
1805
|
-
flashboot:
|
|
2642
|
+
imageName: import_zod8.z.string().optional().describe("New Docker image (v2)"),
|
|
2643
|
+
gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
2644
|
+
gpuCount: import_zod8.z.number().optional().describe("New GPUs per worker (v2)"),
|
|
2645
|
+
args: import_zod8.z.string().optional().describe("New container args (v2)"),
|
|
2646
|
+
containerDiskInGb: import_zod8.z.number().optional().describe("New container disk size in GB (v2)"),
|
|
2647
|
+
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("New ports (v2)"),
|
|
2648
|
+
env: import_zod8.z.record(import_zod8.z.string()).optional().describe("New environment variables (v2)"),
|
|
2649
|
+
containerRegistryAuthId: import_zod8.z.string().optional().describe("New container registry auth id (v2)"),
|
|
2650
|
+
networkVolumeIds: import_zod8.z.array(import_zod8.z.string()).optional().describe("New network volume ids (v2)"),
|
|
2651
|
+
executionTimeoutMs: import_zod8.z.number().optional().describe("New per-job execution timeout in ms (v2)"),
|
|
2652
|
+
flashboot: import_zod8.z.enum(["OFF", "FLASHBOOT", "PRIORITY_FLASHBOOT"]).optional().describe("New FlashBoot mode (v2)")
|
|
1806
2653
|
},
|
|
1807
2654
|
{ title: "Update endpoint", ...WRITE, idempotentHint: true },
|
|
1808
2655
|
async (params) => {
|
|
1809
2656
|
const { endpointId, ...updateParams } = params;
|
|
1810
2657
|
const backend = backendFor("endpoints");
|
|
1811
|
-
const
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
"PATCH",
|
|
1815
|
-
|
|
2658
|
+
const url = `${backend.base}${backend.get(endpointId)}`;
|
|
2659
|
+
if (backend.version !== "v2") {
|
|
2660
|
+
const body2 = backend.mapUpdate(updateParams);
|
|
2661
|
+
return jsonReply2(await callRestUrl(url, "PATCH", body2));
|
|
2662
|
+
}
|
|
2663
|
+
let scalerType = updateParams.scalerType;
|
|
2664
|
+
if (scalerType === void 0 && updateParams.scalerValue !== void 0) {
|
|
2665
|
+
const current = await callRestUrl(url);
|
|
2666
|
+
scalerType = current?.scaling?.type === "REQUEST_COUNT" ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
2667
|
+
}
|
|
2668
|
+
const scalerError = scalerValueError(
|
|
2669
|
+
scalerType,
|
|
2670
|
+
updateParams.scalerValue
|
|
1816
2671
|
);
|
|
1817
|
-
|
|
2672
|
+
if (scalerError) {
|
|
2673
|
+
return jsonReply2({ error: scalerError, status: 400 });
|
|
2674
|
+
}
|
|
2675
|
+
const body = backend.mapUpdate({
|
|
2676
|
+
...updateParams,
|
|
2677
|
+
scalerType
|
|
2678
|
+
});
|
|
2679
|
+
return jsonReply2(await callRestUrl(url, "PATCH", body));
|
|
1818
2680
|
}
|
|
1819
2681
|
);
|
|
1820
2682
|
server.tool(
|
|
1821
2683
|
"delete-endpoint",
|
|
1822
2684
|
"Permanently delete a Serverless endpoint. This cannot be undone.",
|
|
1823
2685
|
{
|
|
1824
|
-
endpointId:
|
|
2686
|
+
endpointId: import_zod8.z.string().describe("ID of the endpoint to delete")
|
|
1825
2687
|
},
|
|
1826
2688
|
{ title: "Delete endpoint", ...DESTRUCTIVE },
|
|
1827
2689
|
async (params) => {
|
|
@@ -1833,12 +2695,39 @@ function registerEndpointTools(server, rt) {
|
|
|
1833
2695
|
return jsonReply2(result);
|
|
1834
2696
|
}
|
|
1835
2697
|
);
|
|
2698
|
+
server.tool(
|
|
2699
|
+
"list-endpoint-releases",
|
|
2700
|
+
"List a Serverless endpoint's release history and current rollout status (workers on the latest version). v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
2701
|
+
{
|
|
2702
|
+
...listPaginationParams,
|
|
2703
|
+
endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose releases to list")
|
|
2704
|
+
},
|
|
2705
|
+
{ title: "List endpoint releases", ...READ_ONLY },
|
|
2706
|
+
async (params) => {
|
|
2707
|
+
const backend = backendFor("endpoints");
|
|
2708
|
+
if (backend.version === "v1") {
|
|
2709
|
+
return jsonReply2({
|
|
2710
|
+
error: "list-endpoint-releases is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2711
|
+
status: 501
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
const raw = await callRestUrl(
|
|
2715
|
+
`${backend.base}/serverless/${params.endpointId}/releases`
|
|
2716
|
+
);
|
|
2717
|
+
const releases = Array.isArray(raw?.releases) ? raw.releases : [];
|
|
2718
|
+
return capListResult(
|
|
2719
|
+
releases,
|
|
2720
|
+
{ limit: params.limit, cursor: params.cursor },
|
|
2721
|
+
{ rollout: raw?.rollout, endpointVersion: raw?.endpointVersion }
|
|
2722
|
+
);
|
|
2723
|
+
}
|
|
2724
|
+
);
|
|
1836
2725
|
server.tool(
|
|
1837
2726
|
"list-endpoint-workers",
|
|
1838
|
-
"List the workers backing a Serverless endpoint, with their status and an aggregate summary. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
2727
|
+
"List the workers backing a Serverless endpoint, with their status and an aggregate summary. v2-only \u2014 returns a 501 notice on the v1 API. An UNHEALTHY worker usually means the container is crash-looping (repeated starts that exit before the handler runs) \u2014 jobs on the endpoint will sit IN_QUEUE even though this is a worker failure, not a capacity shortage; inspect it with stream-worker-logs. Zero workers while jobs queue means the endpoint is waiting for GPU capacity. Paginated via limit/cursor.",
|
|
1839
2728
|
{
|
|
1840
2729
|
...listPaginationParams,
|
|
1841
|
-
endpointId:
|
|
2730
|
+
endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint whose workers to list")
|
|
1842
2731
|
},
|
|
1843
2732
|
{ title: "List endpoint workers", ...READ_ONLY },
|
|
1844
2733
|
async (params) => {
|
|
@@ -1859,30 +2748,110 @@ function registerEndpointTools(server, rt) {
|
|
|
1859
2748
|
);
|
|
1860
2749
|
}
|
|
1861
2750
|
);
|
|
2751
|
+
server.tool(
|
|
2752
|
+
"stream-worker-logs",
|
|
2753
|
+
"Fetch a bounded snapshot of a serverless worker's live logs (container and/or system) via Server-Sent Events. v2-only \u2014 returns a 501 notice on the v1 API. Get the workerId from list-endpoint-workers. Use this to diagnose UNHEALTHY workers: repeated `start container` system lines with no container output is the crash-loop signature (the container exits before the handler runs). Reads for up to maxWaitMs (default 5s) and returns the collected log lines; use `tail` to backfill recent lines first. Large output is truncated (see the `truncated` flag).",
|
|
2754
|
+
{
|
|
2755
|
+
endpointId: import_zod8.z.string().describe("ID of the Serverless endpoint the worker belongs to"),
|
|
2756
|
+
workerId: import_zod8.z.string().describe(
|
|
2757
|
+
"ID of the worker whose logs to stream (from list-endpoint-workers)"
|
|
2758
|
+
),
|
|
2759
|
+
...logStreamParams
|
|
2760
|
+
},
|
|
2761
|
+
{ title: "Stream worker logs", ...READ_ONLY },
|
|
2762
|
+
(params) => streamLogsReply(
|
|
2763
|
+
rt,
|
|
2764
|
+
{
|
|
2765
|
+
name: "stream-worker-logs",
|
|
2766
|
+
resource: "workers",
|
|
2767
|
+
logsUrl: (backend) => `${backend.base}/serverless/${encodeURIComponent(params.endpointId)}/workers/${encodeURIComponent(params.workerId)}/logs`
|
|
2768
|
+
},
|
|
2769
|
+
params
|
|
2770
|
+
)
|
|
2771
|
+
);
|
|
1862
2772
|
}
|
|
1863
2773
|
|
|
1864
2774
|
// src/tools/jobs.ts
|
|
1865
|
-
var
|
|
2775
|
+
var import_zod9 = require("zod");
|
|
2776
|
+
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2777
|
+
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2778
|
+
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
1866
2779
|
function registerJobTools(server, rt) {
|
|
1867
|
-
const { jsonReply: jsonReply2, serverlessRequest } = rt;
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
2780
|
+
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
2781
|
+
async function diagnoseQueuedJob(endpointId) {
|
|
2782
|
+
const cached = diagnosisCache.get(endpointId);
|
|
2783
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
2784
|
+
return cached.value;
|
|
2785
|
+
}
|
|
2786
|
+
diagnosisCache.delete(endpointId);
|
|
2787
|
+
try {
|
|
2788
|
+
const backend = backendFor("workers");
|
|
2789
|
+
if (backend.version !== "v2") return null;
|
|
2790
|
+
const raw = await callRestUrl(
|
|
2791
|
+
`${backend.base}/serverless/${endpointId}/workers`
|
|
2792
|
+
);
|
|
2793
|
+
const summary = raw?.summary;
|
|
2794
|
+
const workers = raw?.workers ?? [];
|
|
2795
|
+
if (!summary) return null;
|
|
2796
|
+
const remember = (value) => {
|
|
2797
|
+
if (diagnosisCache.size >= MAX_DIAGNOSIS_ENTRIES) {
|
|
2798
|
+
const oldest = diagnosisCache.keys().next().value;
|
|
2799
|
+
if (oldest !== void 0) diagnosisCache.delete(oldest);
|
|
2800
|
+
}
|
|
2801
|
+
diagnosisCache.set(endpointId, {
|
|
2802
|
+
value,
|
|
2803
|
+
expiresAt: Date.now() + DIAGNOSIS_TTL_MS
|
|
2804
|
+
});
|
|
2805
|
+
return value;
|
|
2806
|
+
};
|
|
2807
|
+
if ((summary.unhealthy ?? 0) > 0) {
|
|
2808
|
+
const badIds = workers.filter((w) => w.status === "UNHEALTHY").map((w) => w.id).join(", ");
|
|
2809
|
+
return remember({
|
|
2810
|
+
workerHealth: summary,
|
|
2811
|
+
hint: `${summary.unhealthy} of ${summary.total} worker(s) on this endpoint are UNHEALTHY \u2014 the container is likely crash-looping (repeated starts that exit before the handler runs). The job can stay IN_QUEUE indefinitely; this is a worker failure, NOT a capacity shortage. Inspect the logs with stream-worker-logs${badIds ? ` (workerId: ${badIds})` : ""}.`
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
if ((summary.total ?? 0) === 0) {
|
|
2815
|
+
return remember({
|
|
2816
|
+
workerHealth: summary,
|
|
2817
|
+
hint: "No workers are scheduled for this endpoint yet \u2014 it may simply be scaling up from zero (normal for the first seconds of a cold start), or it is waiting for GPU capacity / its GPU-CUDA constraints exclude the currently-available hosts. If this persists, check availability with list-gpu-types or widen the gpuIds/CUDA settings."
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
if ((summary.throttled ?? 0) > 0 && (summary.running ?? 0) === 0) {
|
|
2821
|
+
return remember({
|
|
2822
|
+
workerHealth: summary,
|
|
2823
|
+
hint: "Workers exist but are throttled \u2014 the hosts are at capacity right now; the job should start when capacity frees up."
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
if ((summary.initializing ?? 0) > 0) {
|
|
2827
|
+
return remember({
|
|
2828
|
+
workerHealth: summary,
|
|
2829
|
+
hint: "A worker is initializing \u2014 likely a cold start (image pull / model load); the job should start soon."
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
return remember({ workerHealth: summary, hint: "" });
|
|
2833
|
+
} catch {
|
|
2834
|
+
return null;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
const endpointIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid endpoint ID format");
|
|
2838
|
+
const jobIdSchema = import_zod9.z.string().regex(/^[a-zA-Z0-9_-]+$/, "Invalid job ID format");
|
|
2839
|
+
const inputSchema = import_zod9.z.record(import_zod9.z.unknown()).describe(
|
|
1871
2840
|
"Input payload for the worker handler. The expected fields depend on the deployed model or worker."
|
|
1872
2841
|
);
|
|
1873
|
-
const webhookSchema =
|
|
2842
|
+
const webhookSchema = import_zod9.z.string().url().optional().describe(
|
|
1874
2843
|
"Webhook URL to receive job completion notifications instead of polling"
|
|
1875
2844
|
);
|
|
1876
|
-
const policySchema =
|
|
1877
|
-
executionTimeout:
|
|
1878
|
-
lowPriority:
|
|
1879
|
-
ttl:
|
|
2845
|
+
const policySchema = import_zod9.z.object({
|
|
2846
|
+
executionTimeout: import_zod9.z.number().optional().describe("Maximum execution time in milliseconds"),
|
|
2847
|
+
lowPriority: import_zod9.z.boolean().optional().describe("Submit as a low-priority job"),
|
|
2848
|
+
ttl: import_zod9.z.number().optional().describe("Time-to-live for the job result in milliseconds")
|
|
1880
2849
|
}).optional().describe("Execution policy options");
|
|
1881
|
-
const s3ConfigSchema =
|
|
1882
|
-
accessId:
|
|
1883
|
-
accessSecret:
|
|
1884
|
-
bucketName:
|
|
1885
|
-
endpointUrl:
|
|
2850
|
+
const s3ConfigSchema = import_zod9.z.object({
|
|
2851
|
+
accessId: import_zod9.z.string().describe("S3 access key ID"),
|
|
2852
|
+
accessSecret: import_zod9.z.string().describe("S3 secret access key"),
|
|
2853
|
+
bucketName: import_zod9.z.string().describe("S3 bucket name"),
|
|
2854
|
+
endpointUrl: import_zod9.z.string().describe("S3 endpoint URL")
|
|
1886
2855
|
}).optional().describe("S3-compatible storage config for large outputs");
|
|
1887
2856
|
server.tool(
|
|
1888
2857
|
"run-endpoint",
|
|
@@ -1916,7 +2885,7 @@ function registerJobTools(server, rt) {
|
|
|
1916
2885
|
"ID of the Serverless endpoint to run synchronously"
|
|
1917
2886
|
),
|
|
1918
2887
|
input: inputSchema,
|
|
1919
|
-
wait:
|
|
2888
|
+
wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
|
|
1920
2889
|
"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)."
|
|
1921
2890
|
),
|
|
1922
2891
|
webhook: webhookSchema,
|
|
@@ -1938,7 +2907,7 @@ function registerJobTools(server, rt) {
|
|
|
1938
2907
|
);
|
|
1939
2908
|
server.tool(
|
|
1940
2909
|
"get-job-status",
|
|
1941
|
-
"Check the status of an asynchronous Serverless job. Returns the current status and output when complete. Job statuses: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, TIMED_OUT.",
|
|
2910
|
+
"Check the status of an asynchronous Serverless job. Returns the current status and output when complete. Job statuses: IN_QUEUE, IN_PROGRESS, COMPLETED, FAILED, CANCELLED, TIMED_OUT. IMPORTANT: a job stuck IN_QUEUE does not necessarily mean a capacity shortage \u2014 a worker may have spun up and crash-looped (the platform marks it UNHEALTHY while the job stays queued). When the job is IN_QUEUE this tool attaches a workerHealth summary and a hint; to dig deeper, call list-endpoint-workers (look for UNHEALTHY) and stream-worker-logs.",
|
|
1942
2911
|
{
|
|
1943
2912
|
endpointId: endpointIdSchema.describe(
|
|
1944
2913
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -1951,6 +2920,16 @@ function registerJobTools(server, rt) {
|
|
|
1951
2920
|
params.endpointId,
|
|
1952
2921
|
`/status/${params.jobId}`
|
|
1953
2922
|
);
|
|
2923
|
+
if (result && typeof result === "object" && result.status === "IN_QUEUE") {
|
|
2924
|
+
const diagnosis = await diagnoseQueuedJob(params.endpointId);
|
|
2925
|
+
if (diagnosis) {
|
|
2926
|
+
return jsonReply2({
|
|
2927
|
+
...result,
|
|
2928
|
+
workerHealth: diagnosis.workerHealth,
|
|
2929
|
+
...diagnosis.hint ? { hint: diagnosis.hint } : {}
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
1954
2933
|
return jsonReply2(result);
|
|
1955
2934
|
}
|
|
1956
2935
|
);
|
|
@@ -2052,7 +3031,7 @@ function registerJobTools(server, rt) {
|
|
|
2052
3031
|
);
|
|
2053
3032
|
server.tool(
|
|
2054
3033
|
"endpoint-health",
|
|
2055
|
-
"Get the health and operational status of a Serverless endpoint, including worker counts and job statistics.",
|
|
3034
|
+
"Get the health and operational status of a Serverless endpoint, including worker counts and job statistics. Use this when jobs are stuck IN_QUEUE: unhealthy workers mean a crash-looping container (a worker failure \u2014 inspect with stream-worker-logs), while zero workers means the endpoint is waiting for GPU capacity.",
|
|
2056
3035
|
{
|
|
2057
3036
|
endpointId: endpointIdSchema.describe(
|
|
2058
3037
|
"ID of the Serverless endpoint to check health for"
|
|
@@ -2085,7 +3064,7 @@ function registerJobTools(server, rt) {
|
|
|
2085
3064
|
}
|
|
2086
3065
|
|
|
2087
3066
|
// src/tools/templates.ts
|
|
2088
|
-
var
|
|
3067
|
+
var import_zod10 = require("zod");
|
|
2089
3068
|
function registerTemplateTools(server, rt) {
|
|
2090
3069
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
2091
3070
|
server.tool(
|
|
@@ -2093,9 +3072,9 @@ function registerTemplateTools(server, rt) {
|
|
|
2093
3072
|
`List available templates. By default returns only the user's own templates. Use includeRunpodTemplates to also include official Runpod templates. The recommended default template for new pods is "Runpod Pytorch 2.8.0" (ID: runpod-torch-v280) \u2014 it has the latest CUDA and PyTorch versions.`,
|
|
2094
3073
|
{
|
|
2095
3074
|
...listPaginationParams,
|
|
2096
|
-
includeRunpodTemplates:
|
|
2097
|
-
includePublicTemplates:
|
|
2098
|
-
includeEndpointBoundTemplates:
|
|
3075
|
+
includeRunpodTemplates: import_zod10.z.boolean().optional().describe("Include official Runpod templates in the response"),
|
|
3076
|
+
includePublicTemplates: import_zod10.z.boolean().optional().describe("Include community-made public templates in the response"),
|
|
3077
|
+
includeEndpointBoundTemplates: import_zod10.z.boolean().optional().describe(
|
|
2099
3078
|
"Include templates bound to Serverless endpoints in the response"
|
|
2100
3079
|
)
|
|
2101
3080
|
},
|
|
@@ -2126,7 +3105,7 @@ function registerTemplateTools(server, rt) {
|
|
|
2126
3105
|
"get-template",
|
|
2127
3106
|
"Get one template by id (image, env, ports, disk/volume config).",
|
|
2128
3107
|
{
|
|
2129
|
-
templateId:
|
|
3108
|
+
templateId: import_zod10.z.string().describe("ID of the template to retrieve")
|
|
2130
3109
|
},
|
|
2131
3110
|
{ title: "Get template", ...READ_ONLY },
|
|
2132
3111
|
async (params) => {
|
|
@@ -2139,23 +3118,27 @@ function registerTemplateTools(server, rt) {
|
|
|
2139
3118
|
);
|
|
2140
3119
|
server.tool(
|
|
2141
3120
|
"create-template",
|
|
2142
|
-
"Create a reusable pod/endpoint template (image, ports, env, disk/volume).
|
|
3121
|
+
"Create a reusable pod/endpoint template (image, ports, env, disk/volume). Category is optional and defaults to NVIDIA.",
|
|
2143
3122
|
{
|
|
2144
|
-
name:
|
|
2145
|
-
imageName:
|
|
2146
|
-
isServerless:
|
|
2147
|
-
ports:
|
|
2148
|
-
dockerEntrypoint:
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
volumeMountPath: import_zod6.z.string().optional().describe("Path to mount the volume"),
|
|
2154
|
-
readme: import_zod6.z.string().optional().describe("README content in markdown format"),
|
|
2155
|
-
category: import_zod6.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
|
|
2156
|
-
"Hardware category for the template. On v2 a category is required and defaults to NVIDIA when omitted \u2014 set CPU or AMD for non-NVIDIA templates so they are not mis-categorized."
|
|
3123
|
+
name: import_zod10.z.string().describe("Name for the template"),
|
|
3124
|
+
imageName: import_zod10.z.string().describe("Docker image to use"),
|
|
3125
|
+
isServerless: import_zod10.z.boolean().optional().describe("Is this a serverless template"),
|
|
3126
|
+
ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("Ports to expose"),
|
|
3127
|
+
dockerEntrypoint: import_zod10.z.array(import_zod10.z.string()).optional().describe(
|
|
3128
|
+
"Docker entrypoint commands. Note: not persisted on the v2 REST API, which has no separate entrypoint field (only a start command)."
|
|
3129
|
+
),
|
|
3130
|
+
dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
|
|
3131
|
+
'Container start command. On v2 this is stored as the template\'s `args` string; a multi-element array is joined with spaces (e.g. ["python","-u","app.py"] \u2192 "python -u app.py"). The join is lossy: an element that itself contains spaces is NOT preserved as one argument \u2014 e.g. ["sh","-c","echo hi"] becomes "sh -c echo hi", so pass a shell command as a single pre-quoted element if that distinction matters. Omit or pass an empty array to leave the command unset (never overwrites with "").'
|
|
2157
3132
|
),
|
|
2158
|
-
|
|
3133
|
+
env: import_zod10.z.record(import_zod10.z.string()).optional().describe("Environment variables"),
|
|
3134
|
+
containerDiskInGb: import_zod10.z.number().optional().describe("Container disk size in GB"),
|
|
3135
|
+
volumeInGb: import_zod10.z.number().optional().describe("Volume size in GB"),
|
|
3136
|
+
volumeMountPath: import_zod10.z.string().optional().describe("Path to mount the volume"),
|
|
3137
|
+
readme: import_zod10.z.string().optional().describe("README content in markdown format"),
|
|
3138
|
+
category: import_zod10.z.enum(["CPU", "NVIDIA", "AMD"]).optional().describe(
|
|
3139
|
+
"How the template is grouped and filtered in the Runpod console \u2014 it does NOT affect hardware selection, scheduling, or billing. Optional; defaults to NVIDIA. Set CPU or AMD only to file the template correctly in the console."
|
|
3140
|
+
),
|
|
3141
|
+
containerRegistryAuthId: import_zod10.z.string().optional().describe(
|
|
2159
3142
|
"ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Required when imageName points at a private registry; without it serverless workers fail the image pull."
|
|
2160
3143
|
)
|
|
2161
3144
|
},
|
|
@@ -2173,15 +3156,18 @@ function registerTemplateTools(server, rt) {
|
|
|
2173
3156
|
);
|
|
2174
3157
|
server.tool(
|
|
2175
3158
|
"update-template",
|
|
2176
|
-
"Update a template's mutable fields (name, image, ports, env,
|
|
3159
|
+
"Update a template's mutable fields (name, image, ports, env, start command, registry auth). Only provided fields change; omitted fields are left untouched.",
|
|
2177
3160
|
{
|
|
2178
|
-
templateId:
|
|
2179
|
-
name:
|
|
2180
|
-
imageName:
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
3161
|
+
templateId: import_zod10.z.string().describe("ID of the template to update"),
|
|
3162
|
+
name: import_zod10.z.string().optional().describe("New name for the template"),
|
|
3163
|
+
imageName: import_zod10.z.string().optional().describe("New Docker image"),
|
|
3164
|
+
dockerStartCmd: import_zod10.z.array(import_zod10.z.string()).optional().describe(
|
|
3165
|
+
"New container start command. On v2 stored as the template's `args` string; a multi-element array is joined with spaces (lossy \u2014 an element containing spaces is split into separate tokens). Note: this can only set a command, not clear one \u2014 an empty or absent array is ignored so the existing command is preserved."
|
|
3166
|
+
),
|
|
3167
|
+
ports: import_zod10.z.array(import_zod10.z.string()).optional().describe("New ports to expose"),
|
|
3168
|
+
env: import_zod10.z.record(import_zod10.z.string()).optional().describe("New environment variables"),
|
|
3169
|
+
readme: import_zod10.z.string().optional().describe("New README content in markdown format"),
|
|
3170
|
+
containerRegistryAuthId: import_zod10.z.string().optional().describe(
|
|
2185
3171
|
"ID of a container registry credential (from create-container-registry-auth / list-container-registry-auths) used to pull a private image. Set this to attach a credential to a template referencing a private registry."
|
|
2186
3172
|
)
|
|
2187
3173
|
},
|
|
@@ -2202,7 +3188,7 @@ function registerTemplateTools(server, rt) {
|
|
|
2202
3188
|
"delete-template",
|
|
2203
3189
|
"Permanently delete a template. This cannot be undone.",
|
|
2204
3190
|
{
|
|
2205
|
-
templateId:
|
|
3191
|
+
templateId: import_zod10.z.string().describe("ID of the template to delete")
|
|
2206
3192
|
},
|
|
2207
3193
|
{ title: "Delete template", ...DESTRUCTIVE },
|
|
2208
3194
|
async (params) => {
|
|
@@ -2217,7 +3203,7 @@ function registerTemplateTools(server, rt) {
|
|
|
2217
3203
|
}
|
|
2218
3204
|
|
|
2219
3205
|
// src/tools/network-volumes.ts
|
|
2220
|
-
var
|
|
3206
|
+
var import_zod11 = require("zod");
|
|
2221
3207
|
function registerNetworkVolumeTools(server, rt) {
|
|
2222
3208
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
2223
3209
|
server.tool(
|
|
@@ -2238,7 +3224,7 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
2238
3224
|
"get-network-volume",
|
|
2239
3225
|
"Get one network volume by id (name, size, data center).",
|
|
2240
3226
|
{
|
|
2241
|
-
networkVolumeId:
|
|
3227
|
+
networkVolumeId: import_zod11.z.string().describe("ID of the network volume to retrieve")
|
|
2242
3228
|
},
|
|
2243
3229
|
{ title: "Get network volume", ...READ_ONLY },
|
|
2244
3230
|
async (params) => {
|
|
@@ -2251,11 +3237,17 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
2251
3237
|
);
|
|
2252
3238
|
server.tool(
|
|
2253
3239
|
"create-network-volume",
|
|
2254
|
-
"Create a persistent network volume in a data center (size
|
|
3240
|
+
"Create a persistent network volume in a data center (size 10\u20134096 GB) that pods can mount. Pass volumeType to pick a storage tier; omit it to get the data center default.",
|
|
2255
3241
|
{
|
|
2256
|
-
name:
|
|
2257
|
-
|
|
2258
|
-
|
|
3242
|
+
name: import_zod11.z.string().describe("Name for the network volume"),
|
|
3243
|
+
// Bounds mirror the v2 spec (integer, minimum 10, maximum 4096). Enforced
|
|
3244
|
+
// here as well as described, so an out-of-range value fails locally
|
|
3245
|
+
// instead of costing an API roundtrip.
|
|
3246
|
+
size: import_zod11.z.number().int().min(10).max(4096).describe("Size in GB (10-4096)"),
|
|
3247
|
+
dataCenterId: import_zod11.z.string().describe("Data center ID (see list-data-centers)"),
|
|
3248
|
+
volumeType: import_zod11.z.enum(["STANDARD", "HIGH_PERFORMANCE"]).optional().describe(
|
|
3249
|
+
"Storage tier. Omit for the data center's default (primary) tier. HIGH_PERFORMANCE provisions an HPS volume. Immutable after creation \u2014 update-network-volume cannot change it."
|
|
3250
|
+
)
|
|
2259
3251
|
},
|
|
2260
3252
|
{ title: "Create network volume", ...WRITE },
|
|
2261
3253
|
async (params) => {
|
|
@@ -2273,9 +3265,11 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
2273
3265
|
"update-network-volume",
|
|
2274
3266
|
"Update a network volume (rename, or grow size \u2014 the new size must exceed the current size). Only provided fields change.",
|
|
2275
3267
|
{
|
|
2276
|
-
networkVolumeId:
|
|
2277
|
-
name:
|
|
2278
|
-
|
|
3268
|
+
networkVolumeId: import_zod11.z.string().describe("ID of the network volume to update"),
|
|
3269
|
+
name: import_zod11.z.string().optional().describe("New name for the network volume"),
|
|
3270
|
+
// Same absolute bounds as create. "Larger than current" stays server-side —
|
|
3271
|
+
// we do not hold the current size here, so only the range is checked.
|
|
3272
|
+
size: import_zod11.z.number().int().min(10).max(4096).optional().describe("New size in GB, 10-4096 (must be larger than current)")
|
|
2279
3273
|
},
|
|
2280
3274
|
{ title: "Update network volume", ...WRITE, idempotentHint: true },
|
|
2281
3275
|
async (params) => {
|
|
@@ -2294,7 +3288,7 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
2294
3288
|
"delete-network-volume",
|
|
2295
3289
|
"Permanently delete a network volume and its data. This cannot be undone.",
|
|
2296
3290
|
{
|
|
2297
|
-
networkVolumeId:
|
|
3291
|
+
networkVolumeId: import_zod11.z.string().describe("ID of the network volume to delete")
|
|
2298
3292
|
},
|
|
2299
3293
|
{ title: "Delete network volume", ...DESTRUCTIVE },
|
|
2300
3294
|
async (params) => {
|
|
@@ -2309,9 +3303,14 @@ function registerNetworkVolumeTools(server, rt) {
|
|
|
2309
3303
|
}
|
|
2310
3304
|
|
|
2311
3305
|
// src/tools/registries.ts
|
|
2312
|
-
var
|
|
3306
|
+
var import_zod12 = require("zod");
|
|
3307
|
+
var DELEGATIONS = "/delegations";
|
|
2313
3308
|
function registerRegistryTools(server, rt) {
|
|
2314
3309
|
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
3310
|
+
const delegationsV2OnlyNotice = (tool) => ({
|
|
3311
|
+
error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
3312
|
+
status: 501
|
|
3313
|
+
});
|
|
2315
3314
|
server.tool(
|
|
2316
3315
|
"list-container-registry-auths",
|
|
2317
3316
|
"List your saved container-registry credentials (private image-pull auth). Paginated via limit/cursor.",
|
|
@@ -2330,7 +3329,7 @@ function registerRegistryTools(server, rt) {
|
|
|
2330
3329
|
"get-container-registry-auth",
|
|
2331
3330
|
"Get one saved container-registry credential by id (the secret/password is not returned).",
|
|
2332
3331
|
{
|
|
2333
|
-
containerRegistryAuthId:
|
|
3332
|
+
containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to retrieve")
|
|
2334
3333
|
},
|
|
2335
3334
|
{ title: "Get container registry auth", ...READ_ONLY },
|
|
2336
3335
|
async (params) => {
|
|
@@ -2345,9 +3344,9 @@ function registerRegistryTools(server, rt) {
|
|
|
2345
3344
|
"create-container-registry-auth",
|
|
2346
3345
|
"Save container-registry credentials (username + password/token) so pods and endpoints can pull private images.",
|
|
2347
3346
|
{
|
|
2348
|
-
name:
|
|
2349
|
-
username:
|
|
2350
|
-
password:
|
|
3347
|
+
name: import_zod12.z.string().describe("Name for the container registry auth"),
|
|
3348
|
+
username: import_zod12.z.string().describe("Registry username"),
|
|
3349
|
+
password: import_zod12.z.string().describe("Registry password or access token (stored as a secret)")
|
|
2351
3350
|
},
|
|
2352
3351
|
{ title: "Create container registry auth", ...WRITE },
|
|
2353
3352
|
async (params) => {
|
|
@@ -2365,7 +3364,7 @@ function registerRegistryTools(server, rt) {
|
|
|
2365
3364
|
"delete-container-registry-auth",
|
|
2366
3365
|
"Permanently delete a saved container-registry credential. This cannot be undone.",
|
|
2367
3366
|
{
|
|
2368
|
-
containerRegistryAuthId:
|
|
3367
|
+
containerRegistryAuthId: import_zod12.z.string().describe("ID of the container registry auth to delete")
|
|
2369
3368
|
},
|
|
2370
3369
|
{ title: "Delete container registry auth", ...DESTRUCTIVE },
|
|
2371
3370
|
async (params) => {
|
|
@@ -2377,166 +3376,69 @@ function registerRegistryTools(server, rt) {
|
|
|
2377
3376
|
return jsonReply2(result);
|
|
2378
3377
|
}
|
|
2379
3378
|
);
|
|
2380
|
-
}
|
|
2381
|
-
|
|
2382
|
-
// src/tools/tags.ts
|
|
2383
|
-
var import_zod9 = require("zod");
|
|
2384
|
-
var TAG_RESOURCE_TYPES = [
|
|
2385
|
-
"POD",
|
|
2386
|
-
"NETWORK_VOLUME",
|
|
2387
|
-
"CLUSTER",
|
|
2388
|
-
"SERVERLESS_ENDPOINT"
|
|
2389
|
-
];
|
|
2390
|
-
var taggedResourceSchema = import_zod9.z.object({
|
|
2391
|
-
id: import_zod9.z.string().describe("Resource identifier (e.g. a pod or endpoint id)"),
|
|
2392
|
-
type: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to associate with the tag")
|
|
2393
|
-
});
|
|
2394
|
-
function v2OnlyNotice(tool) {
|
|
2395
|
-
return {
|
|
2396
|
-
error: `${tool} is only available on the v2 REST API. Set RUNPOD_REST_VERSION=v2.`,
|
|
2397
|
-
status: 501
|
|
2398
|
-
};
|
|
2399
|
-
}
|
|
2400
|
-
function registerTagTools(server, rt) {
|
|
2401
|
-
const { jsonReply: jsonReply2, callRestUrl, backendFor } = rt;
|
|
2402
3379
|
server.tool(
|
|
2403
|
-
"list-
|
|
2404
|
-
"List your
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
includeResources: import_zod9.z.boolean().optional().describe("Include the resources attached to each tag in the response")
|
|
2408
|
-
},
|
|
2409
|
-
{ title: "List tags", ...READ_ONLY },
|
|
3380
|
+
"list-registry-delegations",
|
|
3381
|
+
"List your AWS ECR delegations \u2014 ECR repositories Runpod has been granted scoped pull access to, so private ECR images can be pulled without storing AWS credentials. Each entry includes the `dockerRegistryUri` to pull from. v2-only \u2014 returns a 501 notice on the v1 API. Paginated via limit/cursor.",
|
|
3382
|
+
listPaginationParams,
|
|
3383
|
+
{ title: "List registry delegations", ...READ_ONLY },
|
|
2410
3384
|
async (params) => {
|
|
2411
|
-
const backend = backendFor("
|
|
2412
|
-
if (backend.version === "v1")
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
3385
|
+
const backend = backendFor("registries");
|
|
3386
|
+
if (backend.version === "v1") {
|
|
3387
|
+
return jsonReply2(delegationsV2OnlyNotice("list-registry-delegations"));
|
|
3388
|
+
}
|
|
3389
|
+
const raw = await callRestUrl(
|
|
3390
|
+
`${backend.base}${backend.list}${DELEGATIONS}`
|
|
3391
|
+
);
|
|
3392
|
+
const delegations = Array.isArray(raw?.delegations) ? raw.delegations : [];
|
|
3393
|
+
return capListResult(delegations, {
|
|
2416
3394
|
limit: params.limit,
|
|
2417
3395
|
cursor: params.cursor
|
|
2418
3396
|
});
|
|
2419
3397
|
}
|
|
2420
3398
|
);
|
|
2421
3399
|
server.tool(
|
|
2422
|
-
"
|
|
2423
|
-
"
|
|
3400
|
+
"create-registry-delegation",
|
|
3401
|
+
"Register an AWS ECR repository so Runpod can pull private images from it without stored credentials. Pass the repository ARN; the reply carries the `dockerRegistryUri`, resolved repository/tag, and AWS region. v2-only \u2014 returns a 501 notice on the v1 API. For a non-ECR private registry use create-container-registry-auth instead.",
|
|
2424
3402
|
{
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
async (params) => {
|
|
2430
|
-
const backend = backendFor("tags");
|
|
2431
|
-
if (backend.version === "v1") return jsonReply2(v2OnlyNotice("get-tag"));
|
|
2432
|
-
const query = params.includeResources ? "?include=resources" : "";
|
|
2433
|
-
const result = await callRestUrl(
|
|
2434
|
-
`${backend.base}${backend.get(params.tagId)}${query}`
|
|
2435
|
-
);
|
|
2436
|
-
return jsonReply2(result);
|
|
2437
|
-
}
|
|
2438
|
-
);
|
|
2439
|
-
server.tool(
|
|
2440
|
-
"create-tag",
|
|
2441
|
-
"Create a key/value tag, optionally attaching resources at creation. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2442
|
-
{
|
|
2443
|
-
key: import_zod9.z.string().min(1).describe('Tag key (e.g. "project")'),
|
|
2444
|
-
value: import_zod9.z.string().min(1).describe('Tag value (e.g. "gpt-training")'),
|
|
2445
|
-
resources: import_zod9.z.array(taggedResourceSchema).optional().describe("Resources to attach to the tag at creation time")
|
|
3403
|
+
resource: import_zod12.z.string().min(1).describe(
|
|
3404
|
+
'ECR resource ARN, e.g. "arn:aws:ecr:us-east-2:123456789012:repository/my-org/my-image"'
|
|
3405
|
+
),
|
|
3406
|
+
name: import_zod12.z.string().optional().describe("Optional human-readable name for the delegation")
|
|
2446
3407
|
},
|
|
2447
|
-
{ title: "Create
|
|
3408
|
+
{ title: "Create registry delegation", ...WRITE },
|
|
2448
3409
|
async (params) => {
|
|
2449
|
-
const backend = backendFor("
|
|
2450
|
-
if (backend.version === "v1")
|
|
2451
|
-
return jsonReply2(
|
|
3410
|
+
const backend = backendFor("registries");
|
|
3411
|
+
if (backend.version === "v1") {
|
|
3412
|
+
return jsonReply2(delegationsV2OnlyNotice("create-registry-delegation"));
|
|
3413
|
+
}
|
|
3414
|
+
const body = { resource: params.resource };
|
|
3415
|
+
if (params.name !== void 0) body.name = params.name;
|
|
2452
3416
|
const result = await callRestUrl(
|
|
2453
|
-
`${backend.base}${backend.list}`,
|
|
3417
|
+
`${backend.base}${backend.list}${DELEGATIONS}`,
|
|
2454
3418
|
"POST",
|
|
2455
|
-
|
|
3419
|
+
body
|
|
2456
3420
|
);
|
|
2457
3421
|
return jsonReply2(result);
|
|
2458
3422
|
}
|
|
2459
3423
|
);
|
|
2460
3424
|
server.tool(
|
|
2461
|
-
"
|
|
2462
|
-
"
|
|
3425
|
+
"delete-registry-delegation",
|
|
3426
|
+
"Revoke an AWS ECR delegation, withdrawing Runpod's pull access to that repository. Existing workers keep running; future pulls fail. v2-only \u2014 returns a 501 notice on the v1 API. This cannot be undone.",
|
|
2463
3427
|
{
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
value: import_zod9.z.string().min(1).optional().describe("New tag value"),
|
|
2467
|
-
resources: import_zod9.z.array(taggedResourceSchema).optional().describe(
|
|
2468
|
-
"Replace the tag's resource associations with exactly these. Omit to leave unchanged; pass [] to detach all."
|
|
3428
|
+
delegationId: import_zod12.z.string().describe(
|
|
3429
|
+
"ID of the delegation to revoke (from list-registry-delegations)"
|
|
2469
3430
|
)
|
|
2470
3431
|
},
|
|
2471
|
-
|
|
2472
|
-
// retried-after-partial-failure update is not guaranteed to be a no-op.
|
|
2473
|
-
{ title: "Update tag", ...WRITE },
|
|
2474
|
-
async (params) => {
|
|
2475
|
-
const { tagId, ...updateParams } = params;
|
|
2476
|
-
const backend = backendFor("tags");
|
|
2477
|
-
if (backend.version === "v1")
|
|
2478
|
-
return jsonReply2(v2OnlyNotice("update-tag"));
|
|
2479
|
-
const result = await callRestUrl(
|
|
2480
|
-
`${backend.base}${backend.get(tagId)}`,
|
|
2481
|
-
"PATCH",
|
|
2482
|
-
updateParams
|
|
2483
|
-
);
|
|
2484
|
-
return jsonReply2(result);
|
|
2485
|
-
}
|
|
2486
|
-
);
|
|
2487
|
-
server.tool(
|
|
2488
|
-
"delete-tag",
|
|
2489
|
-
"Permanently delete a tag (removes it from all resources it is attached to). This cannot be undone. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2490
|
-
{
|
|
2491
|
-
tagId: import_zod9.z.string().describe("ID of the tag to delete")
|
|
2492
|
-
},
|
|
2493
|
-
{ title: "Delete tag", ...DESTRUCTIVE },
|
|
2494
|
-
async (params) => {
|
|
2495
|
-
const backend = backendFor("tags");
|
|
2496
|
-
if (backend.version === "v1")
|
|
2497
|
-
return jsonReply2(v2OnlyNotice("delete-tag"));
|
|
2498
|
-
const result = await callRestUrl(
|
|
2499
|
-
`${backend.base}${backend.get(params.tagId)}`,
|
|
2500
|
-
"DELETE"
|
|
2501
|
-
);
|
|
2502
|
-
return jsonReply2(result);
|
|
2503
|
-
}
|
|
2504
|
-
);
|
|
2505
|
-
server.tool(
|
|
2506
|
-
"attach-tag",
|
|
2507
|
-
"Attach a tag to a resource (pod, network volume, cluster, or serverless endpoint). Idempotent \u2014 attaching an already-associated resource is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2508
|
-
{
|
|
2509
|
-
tagId: import_zod9.z.string().describe("ID of the tag"),
|
|
2510
|
-
resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to attach the tag to"),
|
|
2511
|
-
resourceId: import_zod9.z.string().describe("ID of the resource to attach the tag to")
|
|
2512
|
-
},
|
|
2513
|
-
{ title: "Attach tag to resource", ...WRITE, idempotentHint: true },
|
|
2514
|
-
async (params) => {
|
|
2515
|
-
const backend = backendFor("tags");
|
|
2516
|
-
if (backend.version === "v1")
|
|
2517
|
-
return jsonReply2(v2OnlyNotice("attach-tag"));
|
|
2518
|
-
const result = await callRestUrl(
|
|
2519
|
-
`${backend.base}/tags/${encodeURIComponent(params.tagId)}/resources/${params.resourceType}/${encodeURIComponent(params.resourceId)}`,
|
|
2520
|
-
"PUT"
|
|
2521
|
-
);
|
|
2522
|
-
return jsonReply2(result);
|
|
2523
|
-
}
|
|
2524
|
-
);
|
|
2525
|
-
server.tool(
|
|
2526
|
-
"detach-tag",
|
|
2527
|
-
"Detach a tag from a resource (removes the association only \u2014 the resource and tag are not deleted). Idempotent \u2014 detaching a resource that is not associated is a no-op. v2-only \u2014 returns a 501 notice on the v1 API.",
|
|
2528
|
-
{
|
|
2529
|
-
tagId: import_zod9.z.string().describe("ID of the tag"),
|
|
2530
|
-
resourceType: import_zod9.z.enum(TAG_RESOURCE_TYPES).describe("Type of resource to detach the tag from"),
|
|
2531
|
-
resourceId: import_zod9.z.string().describe("ID of the resource to detach the tag from")
|
|
2532
|
-
},
|
|
2533
|
-
{ title: "Detach tag from resource", ...WRITE, idempotentHint: true },
|
|
3432
|
+
{ title: "Revoke registry delegation", ...DESTRUCTIVE },
|
|
2534
3433
|
async (params) => {
|
|
2535
|
-
const backend = backendFor("
|
|
2536
|
-
if (backend.version === "v1")
|
|
2537
|
-
return jsonReply2(
|
|
3434
|
+
const backend = backendFor("registries");
|
|
3435
|
+
if (backend.version === "v1") {
|
|
3436
|
+
return jsonReply2(delegationsV2OnlyNotice("delete-registry-delegation"));
|
|
3437
|
+
}
|
|
2538
3438
|
const result = await callRestUrl(
|
|
2539
|
-
`${backend.base}
|
|
3439
|
+
`${backend.base}${backend.list}${DELEGATIONS}/${encodeURIComponent(
|
|
3440
|
+
params.delegationId
|
|
3441
|
+
)}`,
|
|
2540
3442
|
"DELETE"
|
|
2541
3443
|
);
|
|
2542
3444
|
return jsonReply2(result);
|
|
@@ -2545,7 +3447,7 @@ function registerTagTools(server, rt) {
|
|
|
2545
3447
|
}
|
|
2546
3448
|
|
|
2547
3449
|
// src/tools/billing.ts
|
|
2548
|
-
var
|
|
3450
|
+
var import_zod13 = require("zod");
|
|
2549
3451
|
var BILLING_SCOPE_PATH = {
|
|
2550
3452
|
all: "/billing",
|
|
2551
3453
|
pods: "/billing/pods",
|
|
@@ -2561,7 +3463,7 @@ function registerBillingTools(server, rt) {
|
|
|
2561
3463
|
"Get time-bucketed Runpod spend history, either aggregated across all resources or broken down by resource type (pods, serverless, endpoints, network volumes, clusters). v2-only \u2014 returns a 501 notice on the v1 API. Records are capped via limit/cursor; narrow the window with startTime/endTime or lastN.",
|
|
2562
3464
|
{
|
|
2563
3465
|
...listPaginationParams,
|
|
2564
|
-
scope:
|
|
3466
|
+
scope: import_zod13.z.enum([
|
|
2565
3467
|
"all",
|
|
2566
3468
|
"pods",
|
|
2567
3469
|
"serverless",
|
|
@@ -2571,14 +3473,14 @@ function registerBillingTools(server, rt) {
|
|
|
2571
3473
|
]).optional().describe(
|
|
2572
3474
|
'Which billing breakdown to fetch. "all" (default) is the aggregate across every resource.'
|
|
2573
3475
|
),
|
|
2574
|
-
startTime:
|
|
3476
|
+
startTime: import_zod13.z.string().optional().describe(
|
|
2575
3477
|
"Start of the period (RFC 3339, e.g. 2026-05-01T00:00:00Z). Defaults to 30 days ago. Mutually exclusive with lastN."
|
|
2576
3478
|
),
|
|
2577
|
-
endTime:
|
|
3479
|
+
endTime: import_zod13.z.string().optional().describe(
|
|
2578
3480
|
"End of the period (RFC 3339), exclusive. Defaults to now. Mutually exclusive with lastN."
|
|
2579
3481
|
),
|
|
2580
|
-
bucketSize:
|
|
2581
|
-
lastN:
|
|
3482
|
+
bucketSize: import_zod13.z.enum(["hour", "day", "week", "month", "year"]).optional().describe("Length of each time bucket. Defaults to day."),
|
|
3483
|
+
lastN: import_zod13.z.number().int().positive().optional().describe(
|
|
2582
3484
|
"Return the last N buckets of bucketSize, ending with the current one. Mutually exclusive with startTime/endTime."
|
|
2583
3485
|
)
|
|
2584
3486
|
},
|
|
@@ -2613,13 +3515,15 @@ function registerBillingTools(server, rt) {
|
|
|
2613
3515
|
function registerTools(server, ctx, deps = {}) {
|
|
2614
3516
|
const rt = createToolRuntime(server, ctx, deps);
|
|
2615
3517
|
registerCatalogTools(server, rt);
|
|
3518
|
+
registerHubTools(server, rt);
|
|
3519
|
+
registerPublicEndpointTools(server, rt);
|
|
2616
3520
|
registerPodTools(server, rt);
|
|
2617
3521
|
registerEndpointTools(server, rt);
|
|
3522
|
+
registerEndpointGpuTools(server, rt);
|
|
2618
3523
|
registerJobTools(server, rt);
|
|
2619
3524
|
registerTemplateTools(server, rt);
|
|
2620
3525
|
registerNetworkVolumeTools(server, rt);
|
|
2621
3526
|
registerRegistryTools(server, rt);
|
|
2622
|
-
registerTagTools(server, rt);
|
|
2623
3527
|
registerBillingTools(server, rt);
|
|
2624
3528
|
}
|
|
2625
3529
|
|