@runpod/mcp-server 3.1.0 → 3.2.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 +14 -0
- package/dist/{chunk-OM6RHPQC.mjs → chunk-WGK7D2YR.mjs} +370 -9
- package/dist/chunk-WGK7D2YR.mjs.map +1 -0
- package/dist/http.cjs +392 -17
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +24 -10
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +387 -16
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +387 -16
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +369 -8
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +3 -0
- package/dist/tools.d.ts +3 -0
- package/dist/tools.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-OM6RHPQC.mjs.map +0 -1
package/dist/http.cjs
CHANGED
|
@@ -239,7 +239,7 @@ function restV1Base(env) {
|
|
|
239
239
|
return env.RUNPOD_REST_API_URL ?? "https://rest.runpod.io/v1";
|
|
240
240
|
}
|
|
241
241
|
function restV2Base(env) {
|
|
242
|
-
return env.RUNPOD_REST_V2_API_URL ?? "https://
|
|
242
|
+
return env.RUNPOD_REST_V2_API_URL ?? "https://api.runpod.io/v2";
|
|
243
243
|
}
|
|
244
244
|
function serverlessBase(env) {
|
|
245
245
|
return env.RUNPOD_SERVERLESS_API_URL ?? "https://api.runpod.ai/v2";
|
|
@@ -411,27 +411,176 @@ function resolveBackend(opts) {
|
|
|
411
411
|
|
|
412
412
|
// src/server.ts
|
|
413
413
|
var SERVER_NAME = "Runpod API Server";
|
|
414
|
-
var SERVER_VERSION = true ? "3.
|
|
414
|
+
var SERVER_VERSION = true ? "3.2.0" : "dev";
|
|
415
|
+
var SERVER_INSTRUCTIONS = `These tools cover common Runpod operations, but they are curated projections: the underlying APIs expose more fields and parameters than the tools surface.
|
|
416
|
+
|
|
417
|
+
When you need data or capability beyond these tools, consult the full API contracts directly. The REST v2 API lives at https://api.runpod.io/v2 \u2014 the same host these tools call \u2014 and is machine-readable at https://api.runpod.io/v2/openapi.json (fetch and search it to enumerate every path, parameter, and response schema). REST v1 at https://rest.runpod.io/v1 is deprecated; prefer v2. The REST version these tools are actually configured for is reported in the serverInfo.version string of the initialize response, e.g. "[RUNPOD_REST_VERSION=v2]". The GraphQL API at https://api.runpod.io/graphql may expose data the REST API does not yet cover; its schema is documented at https://graphql-spec.runpod.io/, and introspection is disabled in production, so use that reference rather than __schema queries. Both APIs take Bearer auth with a Runpod API key: use the key this client is already configured with \u2014 do not search the environment or filesystem for a credential.`;
|
|
415
418
|
function createServer(version = SERVER_VERSION) {
|
|
416
|
-
return new import_mcp.McpServer(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
419
|
+
return new import_mcp.McpServer(
|
|
420
|
+
{
|
|
421
|
+
name: SERVER_NAME,
|
|
422
|
+
version: `${version} [${restVersionSummary(process.env)}]`
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
// Only advertise capabilities the server implements. It registers
|
|
426
|
+
// tools and no resources — advertising `resources` would invite
|
|
427
|
+
// `resources/list` calls that answer -32601.
|
|
428
|
+
capabilities: {
|
|
429
|
+
tools: {}
|
|
430
|
+
},
|
|
431
|
+
instructions: SERVER_INSTRUCTIONS
|
|
422
432
|
}
|
|
423
|
-
|
|
433
|
+
);
|
|
424
434
|
}
|
|
425
435
|
|
|
426
436
|
// src/tools/runtime.ts
|
|
427
437
|
var import_node_fetch = __toESM(require("node-fetch"), 1);
|
|
428
438
|
var import_node_crypto = require("crypto");
|
|
429
439
|
|
|
440
|
+
// src/_shared/rate-limit.ts
|
|
441
|
+
var GENERIC_RATE_LIMIT_HINT = "rate limited \u2014 back off before retrying, and pace bulk operations";
|
|
442
|
+
var MAX_PLAUSIBLE_WAIT_S = 7 * 86400;
|
|
443
|
+
function remainingRequests(raw) {
|
|
444
|
+
const value = raw?.trim();
|
|
445
|
+
return value && /^\d+$/.test(value) ? Number(value) : null;
|
|
446
|
+
}
|
|
447
|
+
function waitSeconds(raw) {
|
|
448
|
+
const value = raw?.trim();
|
|
449
|
+
if (!value || !/^\d+$/.test(value)) return null;
|
|
450
|
+
const seconds = Number(value);
|
|
451
|
+
return seconds <= MAX_PLAUSIBLE_WAIT_S ? seconds : null;
|
|
452
|
+
}
|
|
453
|
+
var WINDOW_NAME_RE = /^(?:"([\x20-\x21\x23-\x5b\x5d-\x7e]{1,32})"|([A-Za-z*][A-Za-z0-9!#$%&'*+.^_`|~:/-]{0,31}))$/;
|
|
454
|
+
function windowName(raw) {
|
|
455
|
+
const match = WINDOW_NAME_RE.exec(raw.trim());
|
|
456
|
+
return match ? match[1] ?? match[2] : null;
|
|
457
|
+
}
|
|
458
|
+
var QUOTED_VALUE_RE = /^"(?:[\x20-\x21\x23-\x5b\x5d-\x7e]|\\["\\])*"$/;
|
|
459
|
+
var DISPLAY_VALUE_RE = /^%"(?:[\x20-\x21\x23-\x24\x26-\x5b\x5d-\x7e]|\\|%[0-9a-f]{2})*"$/;
|
|
460
|
+
function isWellFormedParam(param) {
|
|
461
|
+
const eq = param.indexOf("=");
|
|
462
|
+
const key = (eq === -1 ? param : param.slice(0, eq)).trim();
|
|
463
|
+
if (key.includes('"')) return false;
|
|
464
|
+
if (eq === -1) return true;
|
|
465
|
+
const value = param.slice(eq + 1).trim();
|
|
466
|
+
if (value.startsWith('"')) return QUOTED_VALUE_RE.test(value);
|
|
467
|
+
if (value.startsWith('%"')) return DISPLAY_VALUE_RE.test(value);
|
|
468
|
+
return !/[\s"]/.test(value);
|
|
469
|
+
}
|
|
470
|
+
function splitOutsideStrings(input, sep) {
|
|
471
|
+
const parts = [];
|
|
472
|
+
let current = "";
|
|
473
|
+
let inString = false;
|
|
474
|
+
let inDisplay = false;
|
|
475
|
+
let escaped = false;
|
|
476
|
+
let prev = "";
|
|
477
|
+
for (const ch of input) {
|
|
478
|
+
if (escaped) escaped = false;
|
|
479
|
+
else if (inString && !inDisplay && ch === "\\") escaped = true;
|
|
480
|
+
else if (ch === '"') {
|
|
481
|
+
if (!inString) inDisplay = prev === "%";
|
|
482
|
+
inString = !inString;
|
|
483
|
+
} else if (ch === sep && !inString) {
|
|
484
|
+
parts.push(current);
|
|
485
|
+
current = "";
|
|
486
|
+
prev = ch;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
current += ch;
|
|
490
|
+
prev = ch;
|
|
491
|
+
}
|
|
492
|
+
parts.push(current);
|
|
493
|
+
return parts;
|
|
494
|
+
}
|
|
495
|
+
function readParams(params) {
|
|
496
|
+
const values = /* @__PURE__ */ new Map();
|
|
497
|
+
let unreadable = false;
|
|
498
|
+
let valuelessAt = -1;
|
|
499
|
+
for (const [index, param] of params.entries()) {
|
|
500
|
+
const eq = param.indexOf("=");
|
|
501
|
+
const key = (eq === -1 ? param : param.slice(0, eq)).trim().toLowerCase();
|
|
502
|
+
if (!key) continue;
|
|
503
|
+
if (eq === -1) {
|
|
504
|
+
if (key === "r") unreadable = true;
|
|
505
|
+
if (key === "t") values.delete("t");
|
|
506
|
+
valuelessAt = index;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if ((key === "r" || key === "t") && (values.has(key) || valuelessAt !== -1 && index > valuelessAt)) {
|
|
510
|
+
unreadable = true;
|
|
511
|
+
}
|
|
512
|
+
values.set(key, param.slice(eq + 1));
|
|
513
|
+
}
|
|
514
|
+
return { values, unreadable };
|
|
515
|
+
}
|
|
516
|
+
function parseExhaustedWindows(header) {
|
|
517
|
+
if (!header) return { windows: [], dropped: false };
|
|
518
|
+
const windows = [];
|
|
519
|
+
let dropped = false;
|
|
520
|
+
for (const item of splitOutsideStrings(header, ",")) {
|
|
521
|
+
if (item.trim() === "") continue;
|
|
522
|
+
const [rawName, ...params] = splitOutsideStrings(item, ";");
|
|
523
|
+
if (!params.every(isWellFormedParam)) {
|
|
524
|
+
dropped = true;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const { values, unreadable } = readParams(params);
|
|
528
|
+
if (unreadable) {
|
|
529
|
+
dropped = true;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const remaining = remainingRequests(values.get("r"));
|
|
533
|
+
if (remaining === null) {
|
|
534
|
+
dropped = true;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (remaining !== 0) continue;
|
|
538
|
+
windows.push({
|
|
539
|
+
window: windowName(rawName),
|
|
540
|
+
resetS: waitSeconds(values.get("t"))
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
return { windows, dropped };
|
|
544
|
+
}
|
|
545
|
+
function humanWait(seconds) {
|
|
546
|
+
if (seconds < 120) return `~${seconds}s`;
|
|
547
|
+
if (seconds < 3600)
|
|
548
|
+
return `~${Math.ceil(seconds / 60)} minutes (${seconds}s)`;
|
|
549
|
+
if (seconds < 2 * 86400) {
|
|
550
|
+
const hours = Math.ceil(seconds / 3600);
|
|
551
|
+
return `~${hours} hour${hours === 1 ? "" : "s"} (${seconds}s)`;
|
|
552
|
+
}
|
|
553
|
+
return `~${Math.ceil(seconds / 86400)} days (${seconds}s)`;
|
|
554
|
+
}
|
|
555
|
+
function rateLimitHint(rateLimitHeader, retryAfterHeader) {
|
|
556
|
+
const { windows, dropped } = parseExhaustedWindows(rateLimitHeader);
|
|
557
|
+
windows.sort((a, b) => (b.resetS ?? -1) - (a.resetS ?? -1));
|
|
558
|
+
const longestWindow = windows[0];
|
|
559
|
+
const waitCandidates = [
|
|
560
|
+
waitSeconds(retryAfterHeader),
|
|
561
|
+
longestWindow?.resetS
|
|
562
|
+
].filter((n) => typeof n === "number" && n > 0);
|
|
563
|
+
const waitS = waitCandidates.length ? Math.max(...waitCandidates) : null;
|
|
564
|
+
const retryAfterUnreadable = typeof retryAfterHeader === "string" && retryAfterHeader.trim() !== "" && waitSeconds(retryAfterHeader) === null;
|
|
565
|
+
if (!longestWindow && waitS === null) {
|
|
566
|
+
if (!retryAfterUnreadable) return GENERIC_RATE_LIMIT_HINT;
|
|
567
|
+
}
|
|
568
|
+
const namedClause = longestWindow?.window ? `the ${longestWindow.window} quota is exhausted` : "";
|
|
569
|
+
if (waitS === null) {
|
|
570
|
+
const cause = namedClause ? `${namedClause}, but the response gave no usable reset time` : "the response gave no usable reset time";
|
|
571
|
+
return `rate limited \u2014 ${cause}; back off substantially before retrying, and pace bulk operations`;
|
|
572
|
+
}
|
|
573
|
+
const hedge = dropped || retryAfterUnreadable || windows.some((w) => w.resetS === null) ? "at least " : "";
|
|
574
|
+
const wait = `wait ${hedge}${humanWait(waitS)} before retrying, and pace bulk operations`;
|
|
575
|
+
return `rate limited \u2014 ${namedClause ? `${namedClause}; ` : ""}${wait}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
430
578
|
// src/_shared/http.ts
|
|
579
|
+
var EXPIRED_CREDENTIAL_HINT = "the Runpod API rejected the credential \u2014 the API key may be expired or revoked; re-authenticate or update RUNPOD_API_KEY";
|
|
431
580
|
var HttpError = class extends Error {
|
|
432
|
-
constructor(prefix, status, body) {
|
|
433
|
-
const
|
|
434
|
-
super(`${prefix}: ${status} - ${
|
|
581
|
+
constructor(prefix, status, body, hint) {
|
|
582
|
+
const shownBody = body.length > 2048 ? `${body.slice(0, 2048)}\u2026 [truncated ${body.length - 2048} chars]` : body;
|
|
583
|
+
super(`${prefix}: ${status} - ${shownBody}${hint ? ` (${hint})` : ""}`);
|
|
435
584
|
this.name = "HttpError";
|
|
436
585
|
this.status = status;
|
|
437
586
|
this.body = body;
|
|
@@ -466,7 +615,11 @@ function createHttpClient(deps) {
|
|
|
466
615
|
throw new HttpError(
|
|
467
616
|
deps.errorPrefix,
|
|
468
617
|
response.status,
|
|
469
|
-
await response.text()
|
|
618
|
+
await response.text(),
|
|
619
|
+
response.status === 429 ? rateLimitHint(
|
|
620
|
+
response.headers.get("ratelimit"),
|
|
621
|
+
response.headers.get("retry-after")
|
|
622
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0
|
|
470
623
|
);
|
|
471
624
|
}
|
|
472
625
|
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
@@ -525,7 +678,15 @@ async function readSseSnapshot(fetchImpl, url, headers, opts) {
|
|
|
525
678
|
throw new HttpError(
|
|
526
679
|
"Runpod API Error",
|
|
527
680
|
res.status,
|
|
528
|
-
await res.text().catch(() => "")
|
|
681
|
+
await res.text().catch(() => ""),
|
|
682
|
+
// This throw bypasses createHttpClient, so the status hints are built
|
|
683
|
+
// here too or these two tools alone return a bare 429/401. The 401
|
|
684
|
+
// hint applies: this request always carries Authorization (see the
|
|
685
|
+
// headers bound in runtime.ts streamSse).
|
|
686
|
+
res.status === 429 ? rateLimitHint(
|
|
687
|
+
res.headers.get("ratelimit"),
|
|
688
|
+
res.headers.get("retry-after")
|
|
689
|
+
) : res.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0
|
|
529
690
|
);
|
|
530
691
|
}
|
|
531
692
|
for await (const chunk of res.body ?? []) {
|
|
@@ -588,7 +749,7 @@ async function streamLogsReply(rt, tool, params) {
|
|
|
588
749
|
}
|
|
589
750
|
|
|
590
751
|
// src/tools/runtime.ts
|
|
591
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
752
|
+
var MCP_SERVER_VERSION = true ? "3.2.0" : "dev";
|
|
592
753
|
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
593
754
|
function trackingHeaders(server, ctx) {
|
|
594
755
|
const info = server.server.getClientVersion();
|
|
@@ -638,6 +799,36 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
638
799
|
...options?.variables ? { variables: options.variables } : {}
|
|
639
800
|
})
|
|
640
801
|
});
|
|
802
|
+
if (!response.ok) {
|
|
803
|
+
const bodyText = await response.text();
|
|
804
|
+
let gqlErrors;
|
|
805
|
+
try {
|
|
806
|
+
gqlErrors = JSON.parse(bodyText).errors;
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
if (Array.isArray(gqlErrors) && gqlErrors.length > 0 && response.status !== 401 && response.status !== 429) {
|
|
810
|
+
throw new HttpError(
|
|
811
|
+
"Runpod GraphQL Error",
|
|
812
|
+
response.status,
|
|
813
|
+
gqlErrors.map((e) => String(e?.message ?? JSON.stringify(e))).join(", ")
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
throw new HttpError(
|
|
817
|
+
"Runpod GraphQL Error",
|
|
818
|
+
response.status,
|
|
819
|
+
bodyText,
|
|
820
|
+
response.status === 429 ? rateLimitHint(
|
|
821
|
+
response.headers.get("ratelimit"),
|
|
822
|
+
response.headers.get("retry-after")
|
|
823
|
+
) : (
|
|
824
|
+
// Only when this request actually carried the caller's API key: the
|
|
825
|
+
// public path sends no Authorization header, so a 401 from that
|
|
826
|
+
// host (WAF, misconfigured RUNPOD_PUBLIC_GRAPHQL_URL) says nothing
|
|
827
|
+
// about the credential — same rationale as observeUnauthorized.
|
|
828
|
+
response.status === 401 && options?.apiKey ? EXPIRED_CREDENTIAL_HINT : void 0
|
|
829
|
+
)
|
|
830
|
+
);
|
|
831
|
+
}
|
|
641
832
|
const result = await response.json();
|
|
642
833
|
if (result.errors && result.errors.length > 0) {
|
|
643
834
|
throw new Error(
|
|
@@ -1054,6 +1245,178 @@ function registerCatalogTools(server, rt) {
|
|
|
1054
1245
|
return jsonReply2(result);
|
|
1055
1246
|
}
|
|
1056
1247
|
);
|
|
1248
|
+
server.tool(
|
|
1249
|
+
"get-capacity",
|
|
1250
|
+
"GPU capacity across host CUDA versions, as a matrix. Use this to choose an endpoint's allowedCudaVersions/minCudaVersion (or diagnose/widen a capacity-starved one) and to distinguish capacity problems from compatibility problems. Default mode is one call returning, per GPU type, overall stock plus AVAILABLE/UNAVAILABLE per host-reported CUDA version. Pass cudaVersions to deep-probe instead: one stock lookup per listed version, returning graded stock (High/Medium/Low/Out) and the lowest on-demand price per version; a probe that fails transiently reports a per-version error instead of failing the call. Nothing is hidden by default \u2014 set includeUnavailable:false to drop GPUs with no stock on any listed version. Credential-free public catalog data; works on both v1 and v2 APIs. Stock is live, so page cursors can shift between calls. Note: the matrix reflects host-reported versions, but endpoint allowedCudaVersions only accepts values from the platform enum \u2014 check create-endpoint/update-endpoint for the accepted list.",
|
|
1251
|
+
{
|
|
1252
|
+
...listPaginationParams,
|
|
1253
|
+
cudaVersions: import_zod3.z.array(import_zod3.z.string().regex(/^\d{1,2}\.\d{1,2}$/)).min(1).max(12).optional().describe(
|
|
1254
|
+
'Deep-probe these host CUDA versions (e.g. ["12.8", "13.0"], max 12): one stock lookup per version returns graded stock and price. Omit for the single-call AVAILABLE/UNAVAILABLE matrix across all versions the fleet currently reports.'
|
|
1255
|
+
),
|
|
1256
|
+
includeUnavailable: import_zod3.z.boolean().optional().describe(
|
|
1257
|
+
'Out-of-stock GPUs are included by default (explicit "Out" cells / all-UNAVAILABLE rows, sorted last). Set false to hide GPUs with no stock on any listed version.'
|
|
1258
|
+
),
|
|
1259
|
+
gpuTypeIds: import_zod3.z.array(import_zod3.z.string()).optional().describe(
|
|
1260
|
+
"Filter to GPU types matching any of these ids or display names (case-insensitive substring, e.g. ['NVIDIA GeForce RTX 4090'] or ['4090', 'H200'])."
|
|
1261
|
+
),
|
|
1262
|
+
gpuCount: import_zod3.z.number().int().min(1).max(8).optional().describe("GPUs per worker/pod to check stock for (default 1)."),
|
|
1263
|
+
secureCloudOnly: import_zod3.z.boolean().optional().describe("Restrict the stock lookup to Secure Cloud hosts.")
|
|
1264
|
+
},
|
|
1265
|
+
{ title: "Get GPU capacity by CUDA version", ...READ_ONLY },
|
|
1266
|
+
async (params) => {
|
|
1267
|
+
const gpuCount = Math.min(
|
|
1268
|
+
8,
|
|
1269
|
+
Math.max(1, Math.floor(Number(params.gpuCount)) || 1)
|
|
1270
|
+
);
|
|
1271
|
+
const secureArg = params.secureCloudOnly ? ", secureCloud: true" : "";
|
|
1272
|
+
const filterTerms = (params.gpuTypeIds ?? []).filter((t) => typeof t === "string" && t.trim().length > 0).map((t) => t.toLowerCase());
|
|
1273
|
+
const matchesFilter = (gpu) => {
|
|
1274
|
+
if (filterTerms.length === 0) return true;
|
|
1275
|
+
const id = gpu.id.toLowerCase();
|
|
1276
|
+
const name = gpu.displayName.toLowerCase();
|
|
1277
|
+
return filterTerms.some(
|
|
1278
|
+
(term) => id.includes(term) || name.includes(term)
|
|
1279
|
+
);
|
|
1280
|
+
};
|
|
1281
|
+
const stockPriority = {
|
|
1282
|
+
High: 3,
|
|
1283
|
+
Medium: 2,
|
|
1284
|
+
Low: 1
|
|
1285
|
+
};
|
|
1286
|
+
if (!params.cudaVersions?.length) {
|
|
1287
|
+
const data = await graphql(`
|
|
1288
|
+
query {
|
|
1289
|
+
gpuTypes {
|
|
1290
|
+
id
|
|
1291
|
+
displayName
|
|
1292
|
+
memoryInGb
|
|
1293
|
+
secureCloud
|
|
1294
|
+
communityCloud
|
|
1295
|
+
lowestPrice(input: { gpuCount: ${gpuCount}${secureArg} }) {
|
|
1296
|
+
stockStatus
|
|
1297
|
+
uninterruptablePrice
|
|
1298
|
+
gpuTypeCudaVersions {
|
|
1299
|
+
cudaVersion
|
|
1300
|
+
availability
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
`);
|
|
1306
|
+
let rows2 = data.gpuTypes.filter((gpu) => gpu.id !== "unknown" && matchesFilter(gpu)).map((gpu) => {
|
|
1307
|
+
const cuda = {};
|
|
1308
|
+
for (const c of gpu.lowestPrice?.gpuTypeCudaVersions ?? []) {
|
|
1309
|
+
cuda[c.cudaVersion] = c.availability;
|
|
1310
|
+
}
|
|
1311
|
+
return {
|
|
1312
|
+
id: gpu.id,
|
|
1313
|
+
displayName: gpu.displayName,
|
|
1314
|
+
memoryGb: gpu.memoryInGb,
|
|
1315
|
+
secureCloud: gpu.secureCloud,
|
|
1316
|
+
communityCloud: gpu.communityCloud,
|
|
1317
|
+
stockStatus: gpu.lowestPrice?.stockStatus || "unavailable",
|
|
1318
|
+
pricePerHr: gpu.lowestPrice?.uninterruptablePrice ?? null,
|
|
1319
|
+
cudaVersions: cuda
|
|
1320
|
+
};
|
|
1321
|
+
});
|
|
1322
|
+
const availableCount = (r) => Object.values(r.cudaVersions).filter((a) => a === "AVAILABLE").length;
|
|
1323
|
+
if (params.includeUnavailable === false)
|
|
1324
|
+
rows2 = rows2.filter((r) => availableCount(r) > 0);
|
|
1325
|
+
rows2.sort((a, b) => {
|
|
1326
|
+
const diff = availableCount(b) - availableCount(a);
|
|
1327
|
+
if (diff !== 0) return diff;
|
|
1328
|
+
const stockDiff = (stockPriority[b.stockStatus] || 0) - (stockPriority[a.stockStatus] || 0);
|
|
1329
|
+
if (stockDiff !== 0) return stockDiff;
|
|
1330
|
+
return b.memoryGb - a.memoryGb;
|
|
1331
|
+
});
|
|
1332
|
+
return capListResult(rows2, {
|
|
1333
|
+
limit: params.limit,
|
|
1334
|
+
cursor: params.cursor
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
const versions = [...new Set(params.cudaVersions)].slice(0, 12);
|
|
1338
|
+
const invalid = versions.filter((v) => !/^\d{1,2}\.\d{1,2}$/.test(v));
|
|
1339
|
+
if (invalid.length > 0) {
|
|
1340
|
+
return jsonReply2({
|
|
1341
|
+
error: `Invalid CUDA version format: ${invalid.join(", ")}. Use "major.minor" strings like "12.8".`,
|
|
1342
|
+
status: 400
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
const perVersion = await Promise.allSettled(
|
|
1346
|
+
versions.map(
|
|
1347
|
+
(v) => graphql(`
|
|
1348
|
+
query {
|
|
1349
|
+
gpuTypes {
|
|
1350
|
+
id
|
|
1351
|
+
displayName
|
|
1352
|
+
memoryInGb
|
|
1353
|
+
secureCloud
|
|
1354
|
+
communityCloud
|
|
1355
|
+
lowestPrice(input: { gpuCount: ${gpuCount}, allowedCudaVersions: ["${v}"]${secureArg} }) {
|
|
1356
|
+
stockStatus
|
|
1357
|
+
uninterruptablePrice
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
`)
|
|
1362
|
+
)
|
|
1363
|
+
);
|
|
1364
|
+
const probeErrors = {};
|
|
1365
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1366
|
+
versions.forEach((v, i) => {
|
|
1367
|
+
const settled = perVersion[i];
|
|
1368
|
+
if (settled.status === "rejected") {
|
|
1369
|
+
probeErrors[v] = settled.reason instanceof Error ? settled.reason.message : String(settled.reason);
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
for (const gpu of settled.value.gpuTypes) {
|
|
1373
|
+
if (gpu.id === "unknown" || !matchesFilter(gpu)) continue;
|
|
1374
|
+
let row = byId.get(gpu.id);
|
|
1375
|
+
if (!row) {
|
|
1376
|
+
row = {
|
|
1377
|
+
id: gpu.id,
|
|
1378
|
+
displayName: gpu.displayName,
|
|
1379
|
+
memoryGb: gpu.memoryInGb,
|
|
1380
|
+
secureCloud: gpu.secureCloud,
|
|
1381
|
+
communityCloud: gpu.communityCloud,
|
|
1382
|
+
cudaVersions: {}
|
|
1383
|
+
};
|
|
1384
|
+
byId.set(gpu.id, row);
|
|
1385
|
+
}
|
|
1386
|
+
const stock = gpu.lowestPrice?.stockStatus;
|
|
1387
|
+
row.cudaVersions[v] = !stock || stock === "Out" ? { stock: "Out", pricePerHr: null } : {
|
|
1388
|
+
stock,
|
|
1389
|
+
pricePerHr: gpu.lowestPrice?.uninterruptablePrice ?? null
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
let rows = [...byId.values()];
|
|
1394
|
+
const inStockCount = (r) => Object.values(r.cudaVersions).filter((c) => c.stock !== "Out").length;
|
|
1395
|
+
if (params.includeUnavailable === false)
|
|
1396
|
+
rows = rows.filter((r) => inStockCount(r) > 0);
|
|
1397
|
+
rows.sort((a, b) => {
|
|
1398
|
+
const diff = inStockCount(b) - inStockCount(a);
|
|
1399
|
+
if (diff !== 0) return diff;
|
|
1400
|
+
const best = (r) => Math.max(
|
|
1401
|
+
0,
|
|
1402
|
+
...Object.values(r.cudaVersions).map(
|
|
1403
|
+
(c) => stockPriority[c.stock] || 0
|
|
1404
|
+
)
|
|
1405
|
+
);
|
|
1406
|
+
const bestDiff = best(b) - best(a);
|
|
1407
|
+
if (bestDiff !== 0) return bestDiff;
|
|
1408
|
+
return b.memoryGb - a.memoryGb;
|
|
1409
|
+
});
|
|
1410
|
+
return capListResult(
|
|
1411
|
+
rows,
|
|
1412
|
+
{ limit: params.limit, cursor: params.cursor },
|
|
1413
|
+
{
|
|
1414
|
+
probedCudaVersions: versions,
|
|
1415
|
+
...Object.keys(probeErrors).length > 0 ? { probeErrors } : {}
|
|
1416
|
+
}
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
);
|
|
1057
1420
|
server.tool(
|
|
1058
1421
|
"get-data-center",
|
|
1059
1422
|
"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).",
|
|
@@ -1738,7 +2101,9 @@ function registerPodTools(server, rt) {
|
|
|
1738
2101
|
"create-pod",
|
|
1739
2102
|
'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.',
|
|
1740
2103
|
{
|
|
1741
|
-
name: import_zod7.z.string().optional().describe(
|
|
2104
|
+
name: import_zod7.z.string().optional().describe(
|
|
2105
|
+
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
2106
|
+
),
|
|
1742
2107
|
imageName: import_zod7.z.string().optional().describe("Docker image to use. Optional when templateId is provided."),
|
|
1743
2108
|
templateId: import_zod7.z.string().optional().describe(
|
|
1744
2109
|
"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."
|
|
@@ -1797,6 +2162,12 @@ function registerPodTools(server, rt) {
|
|
|
1797
2162
|
status: 400
|
|
1798
2163
|
});
|
|
1799
2164
|
}
|
|
2165
|
+
if (!wantsCpu && !params.name && !params.templateId) {
|
|
2166
|
+
return jsonReply2({
|
|
2167
|
+
error: "Provide a name for the pod (deploying from a templateId inherits the template name).",
|
|
2168
|
+
status: 400
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
1800
2171
|
if (wantsCpu) {
|
|
1801
2172
|
if (params.templateId) {
|
|
1802
2173
|
return jsonReply2({
|
|
@@ -3212,10 +3583,14 @@ var defaultCredentialChecker = createCredentialChecker({
|
|
|
3212
3583
|
url: () => authGraphqlUrl(process.env)
|
|
3213
3584
|
});
|
|
3214
3585
|
var sameHost = (a, b) => normalizeUrl(a) === normalizeUrl(b);
|
|
3586
|
+
var HOST_ALIASES = {
|
|
3587
|
+
"v2-rest.runpod.io": "api.runpod.io"
|
|
3588
|
+
};
|
|
3215
3589
|
function normalizeUrl(raw) {
|
|
3216
3590
|
try {
|
|
3217
3591
|
const u = new URL(raw);
|
|
3218
|
-
|
|
3592
|
+
const host = u.host.toLowerCase();
|
|
3593
|
+
return `${u.protocol}//${HOST_ALIASES[host] ?? host}${u.pathname.replace(/\/+$/, "")}`;
|
|
3219
3594
|
} catch {
|
|
3220
3595
|
return raw.trim().toLowerCase().replace(/\/+$/, "");
|
|
3221
3596
|
}
|