@runpod/mcp-server 3.2.0 → 3.3.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 +66 -0
- package/dist/{chunk-WGK7D2YR.mjs → chunk-ICUS4KGX.mjs} +345 -98
- package/dist/chunk-ICUS4KGX.mjs.map +1 -0
- package/dist/http.cjs +362 -98
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +19 -2
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +957 -204
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +346 -99
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +344 -97
- package/dist/tools.cjs.map +1 -1
- package/dist/tools.d.cts +10 -1
- package/dist/tools.d.ts +10 -1
- package/dist/tools.mjs +1 -1
- package/dist/wizard-FBLOZ6YM.mjs +901 -0
- package/dist/wizard-FBLOZ6YM.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/chunk-WGK7D2YR.mjs.map +0 -1
- package/dist/wizard-37ZJSVB5.mjs +0 -402
- package/dist/wizard-37ZJSVB5.mjs.map +0 -1
package/dist/stdio.mjs
CHANGED
|
@@ -34,7 +34,7 @@ function containerConfigToV2(p) {
|
|
|
34
34
|
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
35
35
|
const id = gpuTypeIds?.[0];
|
|
36
36
|
if (id === void 0) return void 0;
|
|
37
|
-
return
|
|
37
|
+
return { id, count: gpuCount ?? 1 };
|
|
38
38
|
}
|
|
39
39
|
function mapPodCreateToV2(params) {
|
|
40
40
|
return compact({
|
|
@@ -161,6 +161,42 @@ function podBodyFromTemplate(template) {
|
|
|
161
161
|
}
|
|
162
162
|
return out;
|
|
163
163
|
}
|
|
164
|
+
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
165
|
+
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
166
|
+
function normalizeSshPublicKey(input) {
|
|
167
|
+
if (PRIVATE_KEY_MARKER.test(input))
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
error: 'sshPublicKey looks like a PRIVATE key. Pass the PUBLIC key instead \u2014 the contents of your ~/.ssh/<key>.pub file (e.g. "ssh-ed25519 AAAA\u2026").'
|
|
171
|
+
};
|
|
172
|
+
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
173
|
+
if (lines.length === 0)
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
177
|
+
};
|
|
178
|
+
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: 'sshPublicKey is not an SSH public key. Expected the CONTENTS of your ~/.ssh/<key>.pub file \u2014 a line like "ssh-ed25519 AAAAC3Nza\u2026 you@host" (ssh-*, ecdsa-*, or sk-* followed by the base64 key) \u2014 not a file path, a SHA256 fingerprint, or a bare base64 blob. Newline-separate multiple keys.'
|
|
182
|
+
};
|
|
183
|
+
return { ok: true, key: lines.join("\n") };
|
|
184
|
+
}
|
|
185
|
+
function applySshPublicKey(body, sshPublicKey) {
|
|
186
|
+
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
187
|
+
const hasSshPort = ports.some((p) => /^\s*22\/tcp\s*$/i.test(String(p)));
|
|
188
|
+
const env = { ...body.env ?? {} };
|
|
189
|
+
const key = sshPublicKey.trim();
|
|
190
|
+
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
191
|
+
else if (!env.PUBLIC_KEY.includes(key))
|
|
192
|
+
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
193
|
+
${key}`;
|
|
194
|
+
return {
|
|
195
|
+
...body,
|
|
196
|
+
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
197
|
+
env
|
|
198
|
+
};
|
|
199
|
+
}
|
|
164
200
|
function mapTemplateUpdateToV2(params) {
|
|
165
201
|
return {
|
|
166
202
|
...containerConfigToV2(params),
|
|
@@ -426,7 +462,7 @@ function resolveBackend(opts) {
|
|
|
426
462
|
|
|
427
463
|
// src/server.ts
|
|
428
464
|
var SERVER_NAME = "Runpod API Server";
|
|
429
|
-
var SERVER_VERSION = true ? "3.
|
|
465
|
+
var SERVER_VERSION = true ? "3.3.0" : "dev";
|
|
430
466
|
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.
|
|
431
467
|
|
|
432
468
|
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.`;
|
|
@@ -601,6 +637,28 @@ var HttpError = class extends Error {
|
|
|
601
637
|
this.body = body;
|
|
602
638
|
}
|
|
603
639
|
};
|
|
640
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
641
|
+
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
642
|
+
var RequestTimeoutError = class extends Error {
|
|
643
|
+
constructor(prefix, timeoutMs, method) {
|
|
644
|
+
const write = method !== "GET" && method !== "HEAD";
|
|
645
|
+
super(
|
|
646
|
+
`${prefix}: no response after ${timeoutMs}ms for ${method} (the API may be overloaded, or still be working on it). ${write ? "This request may have SUCCEEDED upstream \u2014 do not retry blindly. Check with the matching list-/get- tool first and retry only if it did not take effect." : "This was a read and changed nothing, so retrying is safe."}`
|
|
647
|
+
);
|
|
648
|
+
this.name = "RequestTimeoutError";
|
|
649
|
+
this.timeoutMs = timeoutMs;
|
|
650
|
+
this.method = method;
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
654
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
655
|
+
try {
|
|
656
|
+
return await run(signal);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
659
|
+
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
604
662
|
function isJsonContentType(contentType) {
|
|
605
663
|
if (!contentType) return false;
|
|
606
664
|
const ct = contentType.toLowerCase();
|
|
@@ -616,28 +674,49 @@ function buildRequestHeaders(apiKey, tracking) {
|
|
|
616
674
|
...tracking()
|
|
617
675
|
};
|
|
618
676
|
}
|
|
677
|
+
function resolveCeiling(ceiling) {
|
|
678
|
+
if (ceiling === void 0) return Infinity;
|
|
679
|
+
if (typeof ceiling !== "function") return ceiling;
|
|
680
|
+
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
681
|
+
}
|
|
682
|
+
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
683
|
+
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
684
|
+
}
|
|
619
685
|
function createHttpClient(deps) {
|
|
620
|
-
|
|
621
|
-
|
|
686
|
+
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
687
|
+
return async function request(url, method = "GET", body, options) {
|
|
688
|
+
const timeoutMs = clampTimeout(
|
|
689
|
+
options?.timeoutMs,
|
|
690
|
+
deps.maxTimeoutMs,
|
|
691
|
+
defaultTimeoutMs
|
|
692
|
+
);
|
|
693
|
+
return withRequestTimeout(
|
|
694
|
+
deps.errorPrefix,
|
|
695
|
+
timeoutMs,
|
|
622
696
|
method,
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
response.
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
697
|
+
async (signal) => {
|
|
698
|
+
const init = {
|
|
699
|
+
method,
|
|
700
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
701
|
+
signal
|
|
702
|
+
};
|
|
703
|
+
if (body && methodSendsBody(method)) {
|
|
704
|
+
init.body = JSON.stringify(body);
|
|
705
|
+
}
|
|
706
|
+
const response = await deps.fetch(url, init);
|
|
707
|
+
if (!response.ok) {
|
|
708
|
+
const hint = response.status === 429 ? rateLimitHint(
|
|
709
|
+
response.headers.get("ratelimit"),
|
|
710
|
+
response.headers.get("retry-after")
|
|
711
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
712
|
+
const text = await response.text().catch(
|
|
713
|
+
() => "<body unavailable: the connection closed while reading it>"
|
|
714
|
+
);
|
|
715
|
+
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
716
|
+
}
|
|
717
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
718
|
+
}
|
|
719
|
+
);
|
|
641
720
|
};
|
|
642
721
|
}
|
|
643
722
|
|
|
@@ -764,7 +843,7 @@ async function streamLogsReply(rt, tool, params) {
|
|
|
764
843
|
}
|
|
765
844
|
|
|
766
845
|
// src/tools/runtime.ts
|
|
767
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
846
|
+
var MCP_SERVER_VERSION = true ? "3.3.0" : "dev";
|
|
768
847
|
var SESSION_ID = randomUUID();
|
|
769
848
|
function trackingHeaders(server, ctx) {
|
|
770
849
|
const info = server.server.getClientVersion();
|
|
@@ -779,6 +858,11 @@ function trackingHeaders(server, ctx) {
|
|
|
779
858
|
sessionId: SESSION_ID
|
|
780
859
|
});
|
|
781
860
|
}
|
|
861
|
+
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
862
|
+
function invocationBudget(totalMs) {
|
|
863
|
+
const startedAt = Date.now();
|
|
864
|
+
return () => totalMs - (Date.now() - startedAt);
|
|
865
|
+
}
|
|
782
866
|
var defaultFetch = fetch;
|
|
783
867
|
async function withApiErrorLog(label, call) {
|
|
784
868
|
try {
|
|
@@ -802,6 +886,23 @@ var DESTRUCTIVE = {
|
|
|
802
886
|
openWorldHint: true
|
|
803
887
|
};
|
|
804
888
|
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
889
|
+
return withRequestTimeout(
|
|
890
|
+
"Runpod GraphQL Error",
|
|
891
|
+
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
892
|
+
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
893
|
+
// off the method, and telling a caller its list-gpu-types "may have
|
|
894
|
+
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
895
|
+
// it back at the tool that just failed. The operation keyword is what
|
|
896
|
+
// actually says whether anything could have been written.
|
|
897
|
+
isGraphqlMutation(query) ? "POST" : "GET",
|
|
898
|
+
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
function isGraphqlMutation(query) {
|
|
902
|
+
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
903
|
+
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
904
|
+
}
|
|
905
|
+
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
805
906
|
const response = await fetchImpl(url, {
|
|
806
907
|
method: "POST",
|
|
807
908
|
headers: {
|
|
@@ -812,7 +913,8 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
812
913
|
body: JSON.stringify({
|
|
813
914
|
query,
|
|
814
915
|
...options?.variables ? { variables: options.variables } : {}
|
|
815
|
-
})
|
|
916
|
+
}),
|
|
917
|
+
signal
|
|
816
918
|
});
|
|
817
919
|
if (!response.ok) {
|
|
818
920
|
const bodyText = await response.text();
|
|
@@ -868,39 +970,53 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
868
970
|
}
|
|
869
971
|
const rawFetch = deps.fetch ?? defaultFetch;
|
|
870
972
|
const httpFetch = observeUnauthorized(rawFetch);
|
|
973
|
+
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
974
|
+
const timeouts = {
|
|
975
|
+
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
976
|
+
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
977
|
+
};
|
|
871
978
|
const v1Client = createHttpClient({
|
|
872
979
|
apiKey: ctx.apiKey,
|
|
873
980
|
fetch: httpFetch,
|
|
874
981
|
tracking,
|
|
875
|
-
errorPrefix: "Runpod API Error"
|
|
982
|
+
errorPrefix: "Runpod API Error",
|
|
983
|
+
...timeouts
|
|
876
984
|
});
|
|
877
|
-
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
985
|
+
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
878
986
|
"Error calling Runpod API:",
|
|
879
|
-
() => v1Client(
|
|
987
|
+
() => v1Client(
|
|
988
|
+
`${restV1Base(process.env)}${endpoint}`,
|
|
989
|
+
method,
|
|
990
|
+
body,
|
|
991
|
+
options
|
|
992
|
+
)
|
|
880
993
|
);
|
|
881
994
|
const serverlessClient = createHttpClient({
|
|
882
995
|
apiKey: ctx.apiKey,
|
|
883
996
|
fetch: httpFetch,
|
|
884
997
|
tracking,
|
|
885
|
-
errorPrefix: "Runpod Serverless API Error"
|
|
998
|
+
errorPrefix: "Runpod Serverless API Error",
|
|
999
|
+
...timeouts
|
|
886
1000
|
});
|
|
887
|
-
const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
|
|
1001
|
+
const serverlessRequest = (endpointId, path, method = "GET", body, options) => withApiErrorLog(
|
|
888
1002
|
"Error calling Runpod Serverless API:",
|
|
889
1003
|
() => serverlessClient(
|
|
890
1004
|
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
891
1005
|
method,
|
|
892
|
-
body
|
|
1006
|
+
body,
|
|
1007
|
+
options
|
|
893
1008
|
)
|
|
894
1009
|
);
|
|
895
1010
|
const restClient = createHttpClient({
|
|
896
1011
|
apiKey: ctx.apiKey,
|
|
897
1012
|
fetch: httpFetch,
|
|
898
1013
|
tracking,
|
|
899
|
-
errorPrefix: "Runpod API Error"
|
|
1014
|
+
errorPrefix: "Runpod API Error",
|
|
1015
|
+
...timeouts
|
|
900
1016
|
});
|
|
901
|
-
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
1017
|
+
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
902
1018
|
"Error calling Runpod API:",
|
|
903
|
-
() => restClient(url, method, body)
|
|
1019
|
+
() => restClient(url, method, body, options)
|
|
904
1020
|
);
|
|
905
1021
|
const sseFetch = observeUnauthorized(
|
|
906
1022
|
deps.sseFetch ?? fetch
|
|
@@ -933,12 +1049,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
933
1049
|
};
|
|
934
1050
|
return {
|
|
935
1051
|
jsonReply,
|
|
1052
|
+
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
1053
|
+
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
936
1054
|
graphql: (query) => graphqlRequest(
|
|
937
1055
|
query,
|
|
938
1056
|
tracking,
|
|
939
1057
|
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
940
1058
|
rawFetch,
|
|
941
|
-
publicGraphqlBase(process.env)
|
|
1059
|
+
publicGraphqlBase(process.env),
|
|
1060
|
+
{
|
|
1061
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1062
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1063
|
+
}
|
|
942
1064
|
),
|
|
943
1065
|
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
944
1066
|
query,
|
|
@@ -946,7 +1068,12 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
946
1068
|
httpFetch,
|
|
947
1069
|
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
948
1070
|
authedGraphqlBase(process.env),
|
|
949
|
-
{
|
|
1071
|
+
{
|
|
1072
|
+
variables,
|
|
1073
|
+
apiKey: ctx.apiKey,
|
|
1074
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1075
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1076
|
+
}
|
|
950
1077
|
),
|
|
951
1078
|
runpodRequest,
|
|
952
1079
|
serverlessRequest,
|
|
@@ -954,7 +1081,8 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
954
1081
|
backendFor,
|
|
955
1082
|
streamSse,
|
|
956
1083
|
podAction,
|
|
957
|
-
env: process.env
|
|
1084
|
+
env: process.env,
|
|
1085
|
+
transport: ctx.transport
|
|
958
1086
|
};
|
|
959
1087
|
}
|
|
960
1088
|
|
|
@@ -1023,6 +1151,7 @@ function capListResult(result, options, extra) {
|
|
|
1023
1151
|
}
|
|
1024
1152
|
|
|
1025
1153
|
// src/tools/catalog.ts
|
|
1154
|
+
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
1026
1155
|
function registerCatalogTools(server, rt) {
|
|
1027
1156
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
1028
1157
|
server.tool(
|
|
@@ -1041,6 +1170,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1041
1170
|
),
|
|
1042
1171
|
includeAvailability: z3.boolean().optional().describe(
|
|
1043
1172
|
"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."
|
|
1173
|
+
),
|
|
1174
|
+
product: z3.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1175
|
+
"Product context for the availability lookup \u2014 the same GPU can be scarce for Pods and plentiful for Serverless. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1044
1176
|
)
|
|
1045
1177
|
},
|
|
1046
1178
|
{ title: "List GPU types", ...READ_ONLY },
|
|
@@ -1048,8 +1180,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1048
1180
|
const backend = backendFor("gpus");
|
|
1049
1181
|
if (backend.version === "v2") {
|
|
1050
1182
|
const wantAvailability = params.includeAvailability !== false;
|
|
1183
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1051
1184
|
const raw = await callRestUrl(
|
|
1052
|
-
`${backend.base}${backend.list}${wantAvailability ?
|
|
1185
|
+
`${backend.base}${backend.list}${wantAvailability ? `?include=AVAILABILITY&product=${product}` : ""}`
|
|
1053
1186
|
);
|
|
1054
1187
|
let gpus = backend.unwrap(raw);
|
|
1055
1188
|
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
@@ -1222,6 +1355,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1222
1355
|
gpuTypeId: z3.string().describe("ID of the GPU type to retrieve"),
|
|
1223
1356
|
includeAvailability: z3.boolean().optional().describe(
|
|
1224
1357
|
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1358
|
+
),
|
|
1359
|
+
product: z3.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1360
|
+
"Product context for the availability lookup \u2014 stock differs by product. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1225
1361
|
)
|
|
1226
1362
|
},
|
|
1227
1363
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
@@ -1234,7 +1370,8 @@ function registerCatalogTools(server, rt) {
|
|
|
1234
1370
|
});
|
|
1235
1371
|
}
|
|
1236
1372
|
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1237
|
-
const
|
|
1373
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1374
|
+
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1238
1375
|
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1239
1376
|
return jsonReply2(result);
|
|
1240
1377
|
}
|
|
@@ -2114,7 +2251,7 @@ function registerPodTools(server, rt) {
|
|
|
2114
2251
|
);
|
|
2115
2252
|
server.tool(
|
|
2116
2253
|
"create-pod",
|
|
2117
|
-
'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.',
|
|
2254
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). For full SSH access (including SCP/SFTP/rsync), pass sshPublicKey \u2014 it sets the PUBLIC_KEY env var and exposes 22/tcp, which official Runpod images turn into a running sshd with the key authorized. If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2118
2255
|
{
|
|
2119
2256
|
name: z7.string().optional().describe(
|
|
2120
2257
|
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
@@ -2128,12 +2265,18 @@ function registerPodTools(server, rt) {
|
|
|
2128
2265
|
),
|
|
2129
2266
|
cloudType: z7.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2130
2267
|
gpuTypeIds: z7.array(z7.string()).optional().describe("List of acceptable GPU types"),
|
|
2131
|
-
|
|
2268
|
+
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
2269
|
+
// a negative, or a fractional count reached the wire as-is and came back
|
|
2270
|
+
// as a raw 422; reject it here with a message that names the field.
|
|
2271
|
+
gpuCount: z7.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2132
2272
|
containerDiskInGb: z7.number().optional().describe("Container disk size in GB"),
|
|
2133
2273
|
volumeInGb: z7.number().optional().describe("Volume size in GB"),
|
|
2134
2274
|
volumeMountPath: z7.string().optional().describe("Path to mount the volume"),
|
|
2135
2275
|
ports: z7.array(z7.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2136
2276
|
env: z7.record(z7.string()).optional().describe("Environment variables"),
|
|
2277
|
+
sshPublicKey: z7.string().optional().describe(
|
|
2278
|
+
'SSH PUBLIC key(s) to authorize on the pod \u2014 the CONTENTS of your ~/.ssh/<key>.pub file, i.e. a literal "ssh-ed25519 AAAAC3Nza\u2026 you@host" line, NOT a path to the file, a fingerprint, or the private key; newline-separate multiple keys. Merges the key into the PUBLIC_KEY env var and ensures 22/tcp is exposed, enabling full SSH (SCP/SFTP/rsync) on any image that honors the PUBLIC_KEY convention \u2014 all runpod/* official images do. Extends template ports/env rather than replacing them.'
|
|
2279
|
+
),
|
|
2137
2280
|
containerRegistryAuthId: z7.string().optional().describe(
|
|
2138
2281
|
"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)."
|
|
2139
2282
|
),
|
|
@@ -2142,6 +2285,15 @@ function registerPodTools(server, rt) {
|
|
|
2142
2285
|
{ title: "Create pod", ...WRITE },
|
|
2143
2286
|
async (params) => {
|
|
2144
2287
|
const backend = backendFor("pods");
|
|
2288
|
+
const { sshPublicKey, ...podParams } = params;
|
|
2289
|
+
let sshKey;
|
|
2290
|
+
if (sshPublicKey !== void 0) {
|
|
2291
|
+
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
2292
|
+
if (!validated.ok)
|
|
2293
|
+
return jsonReply2({ error: validated.error, status: 400 });
|
|
2294
|
+
sshKey = validated.key;
|
|
2295
|
+
}
|
|
2296
|
+
const sshNote = "sshPublicKey applied: PUBLIC_KEY env set and 22/tcp exposed. Full SSH (incl. SCP/SFTP/rsync) also requires an image that installs PUBLIC_KEY into authorized_keys and starts sshd (all runpod/* official images do) and a machine with a public IP \u2014 read the pod\u2019s portMappings for port 22 once it is running.";
|
|
2145
2297
|
if (params.templateId && backend.version !== "v2") {
|
|
2146
2298
|
return jsonReply2({
|
|
2147
2299
|
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
@@ -2191,13 +2343,15 @@ function registerPodTools(server, rt) {
|
|
|
2191
2343
|
});
|
|
2192
2344
|
}
|
|
2193
2345
|
try {
|
|
2346
|
+
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2194
2347
|
const result = await callRestUrl(
|
|
2195
2348
|
`${restV1Base(env)}/pods`,
|
|
2196
2349
|
"POST",
|
|
2197
|
-
|
|
2350
|
+
cpuBody
|
|
2198
2351
|
);
|
|
2199
2352
|
return jsonReply2({
|
|
2200
2353
|
...result,
|
|
2354
|
+
...sshKey ? { _ssh: sshNote } : {},
|
|
2201
2355
|
_servedBy: "v1",
|
|
2202
2356
|
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2203
2357
|
});
|
|
@@ -2214,7 +2368,7 @@ function registerPodTools(server, rt) {
|
|
|
2214
2368
|
}
|
|
2215
2369
|
}
|
|
2216
2370
|
}
|
|
2217
|
-
let body = backend.mapCreate(
|
|
2371
|
+
let body = backend.mapCreate(podParams);
|
|
2218
2372
|
if (params.templateId) {
|
|
2219
2373
|
let template;
|
|
2220
2374
|
try {
|
|
@@ -2232,19 +2386,21 @@ function registerPodTools(server, rt) {
|
|
|
2232
2386
|
}
|
|
2233
2387
|
body = { ...podBodyFromTemplate(template), ...body };
|
|
2234
2388
|
}
|
|
2389
|
+
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2235
2390
|
try {
|
|
2236
2391
|
const result = await callRestUrl(
|
|
2237
2392
|
`${backend.base}${backend.list}`,
|
|
2238
2393
|
"POST",
|
|
2239
2394
|
body
|
|
2240
2395
|
);
|
|
2396
|
+
const extras = {};
|
|
2397
|
+
if (sshKey) extras._ssh = sshNote;
|
|
2241
2398
|
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2242
|
-
|
|
2243
|
-
...result,
|
|
2244
|
-
_warning: `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`
|
|
2245
|
-
});
|
|
2399
|
+
extras._warning = `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`;
|
|
2246
2400
|
}
|
|
2247
|
-
return jsonReply2(
|
|
2401
|
+
return jsonReply2(
|
|
2402
|
+
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
2403
|
+
);
|
|
2248
2404
|
} catch (error) {
|
|
2249
2405
|
if (error instanceof HttpError) {
|
|
2250
2406
|
return jsonReply2({ error: error.message, status: error.status });
|
|
@@ -2705,8 +2861,108 @@ import { z as z9 } from "zod";
|
|
|
2705
2861
|
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2706
2862
|
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2707
2863
|
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
2864
|
+
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
2865
|
+
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
2866
|
+
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
2867
|
+
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
2868
|
+
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
2869
|
+
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
2870
|
+
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
2871
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
2872
|
+
"COMPLETED",
|
|
2873
|
+
"FAILED",
|
|
2874
|
+
"CANCELLED",
|
|
2875
|
+
"TIMED_OUT"
|
|
2876
|
+
]);
|
|
2877
|
+
function isUnfinishedJobReply(result) {
|
|
2878
|
+
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
2879
|
+
}
|
|
2880
|
+
function formatBudget(ms) {
|
|
2881
|
+
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
2882
|
+
}
|
|
2883
|
+
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
2884
|
+
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
2885
|
+
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
2886
|
+
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
2887
|
+
return Math.max(
|
|
2888
|
+
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
2889
|
+
MIN_STREAM_POLL_TIMEOUT_MS
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
function sleep(ms) {
|
|
2893
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2894
|
+
}
|
|
2895
|
+
function streamPollPlan(hosted) {
|
|
2896
|
+
return hosted ? {
|
|
2897
|
+
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
2898
|
+
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
2899
|
+
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
2900
|
+
} : {
|
|
2901
|
+
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
2902
|
+
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
2903
|
+
query: ""
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
var RESUME_ADVICE = "Call stream-job again to continue collecting output, get-job-status to check the job without streaming, or stream without a budget by calling the runtime API directly (GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} with a Bearer API key).";
|
|
2907
|
+
function budgetExhaustedNote(budgetMs, lastError) {
|
|
2908
|
+
return {
|
|
2909
|
+
pollingTimedOut: true,
|
|
2910
|
+
note: `Polling stopped after ${formatBudget(
|
|
2911
|
+
budgetMs
|
|
2912
|
+
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
2913
|
+
// Surface the trailing error (if any) instead of discarding it — the last
|
|
2914
|
+
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
2915
|
+
// succeeded. Cleared on success, so this is never a stale error from
|
|
2916
|
+
// minutes of healthy streaming ago.
|
|
2917
|
+
...lastError ? { lastError } : {}
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
function errorCapNote(lastError) {
|
|
2921
|
+
return {
|
|
2922
|
+
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
2923
|
+
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
2924
|
+
};
|
|
2925
|
+
}
|
|
2926
|
+
async function collectJobStream(deps) {
|
|
2927
|
+
const { poll, budgetMs, holdMs } = deps;
|
|
2928
|
+
const startedAt = Date.now();
|
|
2929
|
+
const elapsed = () => Date.now() - startedAt;
|
|
2930
|
+
const chunks = [];
|
|
2931
|
+
let result = {};
|
|
2932
|
+
let consecutiveErrors = 0;
|
|
2933
|
+
let lastError;
|
|
2934
|
+
while (true) {
|
|
2935
|
+
try {
|
|
2936
|
+
const reply = await poll(
|
|
2937
|
+
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
2938
|
+
);
|
|
2939
|
+
consecutiveErrors = 0;
|
|
2940
|
+
lastError = void 0;
|
|
2941
|
+
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
2942
|
+
result = reply;
|
|
2943
|
+
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
2944
|
+
} catch (error) {
|
|
2945
|
+
consecutiveErrors++;
|
|
2946
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
2947
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
2948
|
+
result = { ...result, ...errorCapNote(lastError) };
|
|
2949
|
+
break;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
if (elapsed() > budgetMs) {
|
|
2953
|
+
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
2954
|
+
break;
|
|
2955
|
+
}
|
|
2956
|
+
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
2957
|
+
}
|
|
2958
|
+
return { result, chunks };
|
|
2959
|
+
}
|
|
2708
2960
|
function registerJobTools(server, rt) {
|
|
2709
2961
|
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
2962
|
+
const hosted = rt.transport === "http";
|
|
2963
|
+
const streamBudget = formatBudget(
|
|
2964
|
+
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
2965
|
+
);
|
|
2710
2966
|
async function diagnoseQueuedJob(endpointId) {
|
|
2711
2967
|
const cached = diagnosisCache.get(endpointId);
|
|
2712
2968
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -2717,7 +2973,10 @@ function registerJobTools(server, rt) {
|
|
|
2717
2973
|
const backend = backendFor("workers");
|
|
2718
2974
|
if (backend.version !== "v2") return null;
|
|
2719
2975
|
const raw = await callRestUrl(
|
|
2720
|
-
`${backend.base}/serverless/${endpointId}/workers
|
|
2976
|
+
`${backend.base}/serverless/${endpointId}/workers`,
|
|
2977
|
+
"GET",
|
|
2978
|
+
void 0,
|
|
2979
|
+
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
2721
2980
|
);
|
|
2722
2981
|
const summary = raw?.summary;
|
|
2723
2982
|
const workers = raw?.workers ?? [];
|
|
@@ -2808,14 +3067,14 @@ function registerJobTools(server, rt) {
|
|
|
2808
3067
|
);
|
|
2809
3068
|
server.tool(
|
|
2810
3069
|
"runsync-endpoint",
|
|
2811
|
-
|
|
3070
|
+
`Submit a synchronous job to a Serverless endpoint and wait for the result. Best for fast tasks: if the job outlives the wait, the response returns a job ID and a non-terminal status (IN_QUEUE or IN_PROGRESS) to poll with get-job-status. Max payload 20 MB; results expire after 1 minute. ${hosted ? `This server waits up to ${HTTP_LONG_POLL_BUDGET_MS} ms \u2014 it runs behind a 60-second gateway deadline, so that is both the default and the ceiling for the wait parameter. For longer jobs use run-endpoint + get-job-status, or POST https://api.runpod.ai/v2/{endpointId}/runsync?wait=300000 directly with a Bearer API key.` : "The wait parameter extends the server-side wait to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."}`,
|
|
2812
3071
|
{
|
|
2813
3072
|
endpointId: endpointIdSchema.describe(
|
|
2814
3073
|
"ID of the Serverless endpoint to run synchronously"
|
|
2815
3074
|
),
|
|
2816
3075
|
input: inputSchema,
|
|
2817
3076
|
wait: z9.number().min(1e3).max(3e5).optional().describe(
|
|
2818
|
-
"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)."
|
|
3077
|
+
hosted ? `How long in milliseconds the server should wait for a result before returning a job ID to poll. Accepted range 1000\u2013300000, but this server clamps anything above ${HTTP_LONG_POLL_BUDGET_MS} down to ${HTTP_LONG_POLL_BUDGET_MS}, which is also the effective default.` : "How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
2819
3078
|
),
|
|
2820
3079
|
webhook: webhookSchema,
|
|
2821
3080
|
policy: policySchema,
|
|
@@ -2824,13 +3083,36 @@ function registerJobTools(server, rt) {
|
|
|
2824
3083
|
{ title: "Run endpoint (sync)", ...WRITE },
|
|
2825
3084
|
async (params) => {
|
|
2826
3085
|
const { endpointId, wait, ...body } = params;
|
|
2827
|
-
const
|
|
3086
|
+
const effectiveWait = hosted ? Math.min(
|
|
3087
|
+
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3088
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3089
|
+
) : wait;
|
|
3090
|
+
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
3091
|
+
const path = `/runsync${waitQuery}`;
|
|
2828
3092
|
const result = await serverlessRequest(
|
|
2829
3093
|
endpointId,
|
|
2830
3094
|
path,
|
|
2831
3095
|
"POST",
|
|
2832
|
-
body
|
|
3096
|
+
body,
|
|
3097
|
+
{
|
|
3098
|
+
// From the wait actually sent — on http the clamped 45s, not the
|
|
3099
|
+
// 300s the caller may have asked for.
|
|
3100
|
+
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
3101
|
+
}
|
|
2833
3102
|
);
|
|
3103
|
+
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
3104
|
+
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
3105
|
+
return jsonReply2({
|
|
3106
|
+
...result,
|
|
3107
|
+
waitClamped: {
|
|
3108
|
+
requestedMs: requestedWait,
|
|
3109
|
+
effectiveMs: effectiveWait,
|
|
3110
|
+
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
3111
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3112
|
+
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
3113
|
+
}
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
2834
3116
|
return jsonReply2(result);
|
|
2835
3117
|
}
|
|
2836
3118
|
);
|
|
@@ -2864,7 +3146,7 @@ function registerJobTools(server, rt) {
|
|
|
2864
3146
|
);
|
|
2865
3147
|
server.tool(
|
|
2866
3148
|
"stream-job",
|
|
2867
|
-
|
|
3149
|
+
`Retrieve streaming output from a Serverless job. The worker must support streaming output. Polls /stream/{jobId} and collects chunks until the status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT, for up to ${streamBudget}${hosted ? " (this server runs behind a 60-second gateway deadline)" : ""}. If the budget expires first, returns the chunks collected so far with pollingTimedOut: true \u2014 call stream-job again to resume where it left off, or get-job-status to check without streaming.${hosted ? " To collect with no budget, poll GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} yourself with a Bearer API key until the status is terminal \u2014 each request drains only the chunks buffered so far, so a single request is not the whole output." : ""}`,
|
|
2868
3150
|
{
|
|
2869
3151
|
endpointId: endpointIdSchema.describe(
|
|
2870
3152
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -2873,51 +3155,16 @@ function registerJobTools(server, rt) {
|
|
|
2873
3155
|
},
|
|
2874
3156
|
{ title: "Stream job", ...READ_ONLY },
|
|
2875
3157
|
async (params) => {
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
let finalResult = {};
|
|
2887
|
-
let consecutiveErrors = 0;
|
|
2888
|
-
let lastError;
|
|
2889
|
-
const startTime = Date.now();
|
|
2890
|
-
while (true) {
|
|
2891
|
-
try {
|
|
2892
|
-
const result = await serverlessRequest(
|
|
2893
|
-
params.endpointId,
|
|
2894
|
-
`/stream/${params.jobId}`
|
|
2895
|
-
);
|
|
2896
|
-
consecutiveErrors = 0;
|
|
2897
|
-
if (Array.isArray(result.stream)) {
|
|
2898
|
-
allChunks.push(...result.stream);
|
|
2899
|
-
}
|
|
2900
|
-
finalResult = result;
|
|
2901
|
-
if (TERMINAL_STATUSES.has(result.status)) {
|
|
2902
|
-
break;
|
|
2903
|
-
}
|
|
2904
|
-
} catch (error) {
|
|
2905
|
-
consecutiveErrors++;
|
|
2906
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
2907
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2908
|
-
finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
|
|
2909
|
-
break;
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
if (Date.now() - startTime > MAX_POLL_TIME_MS) {
|
|
2913
|
-
finalResult.pollingTimedOut = true;
|
|
2914
|
-
finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
|
|
2915
|
-
if (lastError) finalResult.lastError = lastError;
|
|
2916
|
-
break;
|
|
2917
|
-
}
|
|
2918
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2919
|
-
}
|
|
2920
|
-
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
3158
|
+
const plan = streamPollPlan(hosted);
|
|
3159
|
+
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
3160
|
+
const { result, chunks } = await collectJobStream({
|
|
3161
|
+
budgetMs: plan.budgetMs,
|
|
3162
|
+
holdMs: plan.holdMs,
|
|
3163
|
+
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
3164
|
+
timeoutMs
|
|
3165
|
+
})
|
|
3166
|
+
});
|
|
3167
|
+
return jsonReply2({ ...result, stream: chunks });
|
|
2921
3168
|
}
|
|
2922
3169
|
);
|
|
2923
3170
|
server.tool(
|
|
@@ -3480,7 +3727,7 @@ async function main(apiKey) {
|
|
|
3480
3727
|
await server.connect(transport);
|
|
3481
3728
|
}
|
|
3482
3729
|
if (isWizardMode) {
|
|
3483
|
-
import("./wizard-
|
|
3730
|
+
import("./wizard-FBLOZ6YM.mjs").then(({ runWizard }) => runWizard(subcommand)).catch((error) => {
|
|
3484
3731
|
console.error(error instanceof Error ? error.message : String(error));
|
|
3485
3732
|
process.exit(1);
|
|
3486
3733
|
});
|