@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/tools.cjs
CHANGED
|
@@ -187,6 +187,28 @@ var HttpError = class extends Error {
|
|
|
187
187
|
this.body = body;
|
|
188
188
|
}
|
|
189
189
|
};
|
|
190
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
191
|
+
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
192
|
+
var RequestTimeoutError = class extends Error {
|
|
193
|
+
constructor(prefix, timeoutMs, method) {
|
|
194
|
+
const write = method !== "GET" && method !== "HEAD";
|
|
195
|
+
super(
|
|
196
|
+
`${prefix}: no response after ${timeoutMs}ms for ${method} (the API may be overloaded, or still be working on it). ${write ? "This request may have SUCCEEDED upstream \u2014 do not retry blindly. Check with the matching list-/get- tool first and retry only if it did not take effect." : "This was a read and changed nothing, so retrying is safe."}`
|
|
197
|
+
);
|
|
198
|
+
this.name = "RequestTimeoutError";
|
|
199
|
+
this.timeoutMs = timeoutMs;
|
|
200
|
+
this.method = method;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
204
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
205
|
+
try {
|
|
206
|
+
return await run(signal);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
209
|
+
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
190
212
|
function isJsonContentType(contentType) {
|
|
191
213
|
if (!contentType) return false;
|
|
192
214
|
const ct = contentType.toLowerCase();
|
|
@@ -202,28 +224,49 @@ function buildRequestHeaders(apiKey, tracking) {
|
|
|
202
224
|
...tracking()
|
|
203
225
|
};
|
|
204
226
|
}
|
|
227
|
+
function resolveCeiling(ceiling) {
|
|
228
|
+
if (ceiling === void 0) return Infinity;
|
|
229
|
+
if (typeof ceiling !== "function") return ceiling;
|
|
230
|
+
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
231
|
+
}
|
|
232
|
+
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
233
|
+
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
234
|
+
}
|
|
205
235
|
function createHttpClient(deps) {
|
|
206
|
-
|
|
207
|
-
|
|
236
|
+
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
237
|
+
return async function request(url, method = "GET", body, options) {
|
|
238
|
+
const timeoutMs = clampTimeout(
|
|
239
|
+
options?.timeoutMs,
|
|
240
|
+
deps.maxTimeoutMs,
|
|
241
|
+
defaultTimeoutMs
|
|
242
|
+
);
|
|
243
|
+
return withRequestTimeout(
|
|
244
|
+
deps.errorPrefix,
|
|
245
|
+
timeoutMs,
|
|
208
246
|
method,
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
response.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
247
|
+
async (signal) => {
|
|
248
|
+
const init = {
|
|
249
|
+
method,
|
|
250
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
251
|
+
signal
|
|
252
|
+
};
|
|
253
|
+
if (body && methodSendsBody(method)) {
|
|
254
|
+
init.body = JSON.stringify(body);
|
|
255
|
+
}
|
|
256
|
+
const response = await deps.fetch(url, init);
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
const hint = response.status === 429 ? rateLimitHint(
|
|
259
|
+
response.headers.get("ratelimit"),
|
|
260
|
+
response.headers.get("retry-after")
|
|
261
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
262
|
+
const text = await response.text().catch(
|
|
263
|
+
() => "<body unavailable: the connection closed while reading it>"
|
|
264
|
+
);
|
|
265
|
+
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
266
|
+
}
|
|
267
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
268
|
+
}
|
|
269
|
+
);
|
|
227
270
|
};
|
|
228
271
|
}
|
|
229
272
|
|
|
@@ -376,7 +419,7 @@ function containerConfigToV2(p) {
|
|
|
376
419
|
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
377
420
|
const id = gpuTypeIds?.[0];
|
|
378
421
|
if (id === void 0) return void 0;
|
|
379
|
-
return
|
|
422
|
+
return { id, count: gpuCount ?? 1 };
|
|
380
423
|
}
|
|
381
424
|
function mapPodCreateToV2(params) {
|
|
382
425
|
return compact({
|
|
@@ -503,6 +546,42 @@ function podBodyFromTemplate(template) {
|
|
|
503
546
|
}
|
|
504
547
|
return out;
|
|
505
548
|
}
|
|
549
|
+
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
550
|
+
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
551
|
+
function normalizeSshPublicKey(input) {
|
|
552
|
+
if (PRIVATE_KEY_MARKER.test(input))
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
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").'
|
|
556
|
+
};
|
|
557
|
+
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
558
|
+
if (lines.length === 0)
|
|
559
|
+
return {
|
|
560
|
+
ok: false,
|
|
561
|
+
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
562
|
+
};
|
|
563
|
+
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
564
|
+
return {
|
|
565
|
+
ok: false,
|
|
566
|
+
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.'
|
|
567
|
+
};
|
|
568
|
+
return { ok: true, key: lines.join("\n") };
|
|
569
|
+
}
|
|
570
|
+
function applySshPublicKey(body, sshPublicKey) {
|
|
571
|
+
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
572
|
+
const hasSshPort = ports.some((p) => /^\s*22\/tcp\s*$/i.test(String(p)));
|
|
573
|
+
const env = { ...body.env ?? {} };
|
|
574
|
+
const key = sshPublicKey.trim();
|
|
575
|
+
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
576
|
+
else if (!env.PUBLIC_KEY.includes(key))
|
|
577
|
+
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
578
|
+
${key}`;
|
|
579
|
+
return {
|
|
580
|
+
...body,
|
|
581
|
+
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
582
|
+
env
|
|
583
|
+
};
|
|
584
|
+
}
|
|
506
585
|
function mapTemplateUpdateToV2(params) {
|
|
507
586
|
return {
|
|
508
587
|
...containerConfigToV2(params),
|
|
@@ -709,7 +788,7 @@ function resolveBackend(opts) {
|
|
|
709
788
|
}
|
|
710
789
|
|
|
711
790
|
// src/tools/runtime.ts
|
|
712
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
791
|
+
var MCP_SERVER_VERSION = true ? "3.3.0" : "dev";
|
|
713
792
|
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
714
793
|
function trackingHeaders(server, ctx) {
|
|
715
794
|
const info = server.server.getClientVersion();
|
|
@@ -724,6 +803,11 @@ function trackingHeaders(server, ctx) {
|
|
|
724
803
|
sessionId: SESSION_ID
|
|
725
804
|
});
|
|
726
805
|
}
|
|
806
|
+
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
807
|
+
function invocationBudget(totalMs) {
|
|
808
|
+
const startedAt = Date.now();
|
|
809
|
+
return () => totalMs - (Date.now() - startedAt);
|
|
810
|
+
}
|
|
727
811
|
var defaultFetch = import_node_fetch.default;
|
|
728
812
|
async function withApiErrorLog(label, call) {
|
|
729
813
|
try {
|
|
@@ -747,6 +831,23 @@ var DESTRUCTIVE = {
|
|
|
747
831
|
openWorldHint: true
|
|
748
832
|
};
|
|
749
833
|
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
834
|
+
return withRequestTimeout(
|
|
835
|
+
"Runpod GraphQL Error",
|
|
836
|
+
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
837
|
+
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
838
|
+
// off the method, and telling a caller its list-gpu-types "may have
|
|
839
|
+
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
840
|
+
// it back at the tool that just failed. The operation keyword is what
|
|
841
|
+
// actually says whether anything could have been written.
|
|
842
|
+
isGraphqlMutation(query) ? "POST" : "GET",
|
|
843
|
+
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
function isGraphqlMutation(query) {
|
|
847
|
+
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
848
|
+
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
849
|
+
}
|
|
850
|
+
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
750
851
|
const response = await fetchImpl(url, {
|
|
751
852
|
method: "POST",
|
|
752
853
|
headers: {
|
|
@@ -757,7 +858,8 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
757
858
|
body: JSON.stringify({
|
|
758
859
|
query,
|
|
759
860
|
...options?.variables ? { variables: options.variables } : {}
|
|
760
|
-
})
|
|
861
|
+
}),
|
|
862
|
+
signal
|
|
761
863
|
});
|
|
762
864
|
if (!response.ok) {
|
|
763
865
|
const bodyText = await response.text();
|
|
@@ -813,39 +915,53 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
813
915
|
}
|
|
814
916
|
const rawFetch = deps.fetch ?? defaultFetch;
|
|
815
917
|
const httpFetch = observeUnauthorized(rawFetch);
|
|
918
|
+
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
919
|
+
const timeouts = {
|
|
920
|
+
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
921
|
+
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
922
|
+
};
|
|
816
923
|
const v1Client = createHttpClient({
|
|
817
924
|
apiKey: ctx.apiKey,
|
|
818
925
|
fetch: httpFetch,
|
|
819
926
|
tracking,
|
|
820
|
-
errorPrefix: "Runpod API Error"
|
|
927
|
+
errorPrefix: "Runpod API Error",
|
|
928
|
+
...timeouts
|
|
821
929
|
});
|
|
822
|
-
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
930
|
+
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
823
931
|
"Error calling Runpod API:",
|
|
824
|
-
() => v1Client(
|
|
932
|
+
() => v1Client(
|
|
933
|
+
`${restV1Base(process.env)}${endpoint}`,
|
|
934
|
+
method,
|
|
935
|
+
body,
|
|
936
|
+
options
|
|
937
|
+
)
|
|
825
938
|
);
|
|
826
939
|
const serverlessClient = createHttpClient({
|
|
827
940
|
apiKey: ctx.apiKey,
|
|
828
941
|
fetch: httpFetch,
|
|
829
942
|
tracking,
|
|
830
|
-
errorPrefix: "Runpod Serverless API Error"
|
|
943
|
+
errorPrefix: "Runpod Serverless API Error",
|
|
944
|
+
...timeouts
|
|
831
945
|
});
|
|
832
|
-
const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
|
|
946
|
+
const serverlessRequest = (endpointId, path, method = "GET", body, options) => withApiErrorLog(
|
|
833
947
|
"Error calling Runpod Serverless API:",
|
|
834
948
|
() => serverlessClient(
|
|
835
949
|
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
836
950
|
method,
|
|
837
|
-
body
|
|
951
|
+
body,
|
|
952
|
+
options
|
|
838
953
|
)
|
|
839
954
|
);
|
|
840
955
|
const restClient = createHttpClient({
|
|
841
956
|
apiKey: ctx.apiKey,
|
|
842
957
|
fetch: httpFetch,
|
|
843
958
|
tracking,
|
|
844
|
-
errorPrefix: "Runpod API Error"
|
|
959
|
+
errorPrefix: "Runpod API Error",
|
|
960
|
+
...timeouts
|
|
845
961
|
});
|
|
846
|
-
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
962
|
+
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
847
963
|
"Error calling Runpod API:",
|
|
848
|
-
() => restClient(url, method, body)
|
|
964
|
+
() => restClient(url, method, body, options)
|
|
849
965
|
);
|
|
850
966
|
const sseFetch = observeUnauthorized(
|
|
851
967
|
deps.sseFetch ?? import_node_fetch.default
|
|
@@ -878,12 +994,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
878
994
|
};
|
|
879
995
|
return {
|
|
880
996
|
jsonReply,
|
|
997
|
+
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
998
|
+
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
881
999
|
graphql: (query) => graphqlRequest(
|
|
882
1000
|
query,
|
|
883
1001
|
tracking,
|
|
884
1002
|
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
885
1003
|
rawFetch,
|
|
886
|
-
publicGraphqlBase(process.env)
|
|
1004
|
+
publicGraphqlBase(process.env),
|
|
1005
|
+
{
|
|
1006
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1007
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1008
|
+
}
|
|
887
1009
|
),
|
|
888
1010
|
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
889
1011
|
query,
|
|
@@ -891,7 +1013,12 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
891
1013
|
httpFetch,
|
|
892
1014
|
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
893
1015
|
authedGraphqlBase(process.env),
|
|
894
|
-
{
|
|
1016
|
+
{
|
|
1017
|
+
variables,
|
|
1018
|
+
apiKey: ctx.apiKey,
|
|
1019
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1020
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1021
|
+
}
|
|
895
1022
|
),
|
|
896
1023
|
runpodRequest,
|
|
897
1024
|
serverlessRequest,
|
|
@@ -899,7 +1026,8 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
899
1026
|
backendFor,
|
|
900
1027
|
streamSse,
|
|
901
1028
|
podAction,
|
|
902
|
-
env: process.env
|
|
1029
|
+
env: process.env,
|
|
1030
|
+
transport: ctx.transport
|
|
903
1031
|
};
|
|
904
1032
|
}
|
|
905
1033
|
|
|
@@ -968,6 +1096,7 @@ function capListResult(result, options, extra) {
|
|
|
968
1096
|
}
|
|
969
1097
|
|
|
970
1098
|
// src/tools/catalog.ts
|
|
1099
|
+
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
971
1100
|
function registerCatalogTools(server, rt) {
|
|
972
1101
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
973
1102
|
server.tool(
|
|
@@ -986,6 +1115,9 @@ function registerCatalogTools(server, rt) {
|
|
|
986
1115
|
),
|
|
987
1116
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
988
1117
|
"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."
|
|
1118
|
+
),
|
|
1119
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1120
|
+
"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."
|
|
989
1121
|
)
|
|
990
1122
|
},
|
|
991
1123
|
{ title: "List GPU types", ...READ_ONLY },
|
|
@@ -993,8 +1125,9 @@ function registerCatalogTools(server, rt) {
|
|
|
993
1125
|
const backend = backendFor("gpus");
|
|
994
1126
|
if (backend.version === "v2") {
|
|
995
1127
|
const wantAvailability = params.includeAvailability !== false;
|
|
1128
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
996
1129
|
const raw = await callRestUrl(
|
|
997
|
-
`${backend.base}${backend.list}${wantAvailability ?
|
|
1130
|
+
`${backend.base}${backend.list}${wantAvailability ? `?include=AVAILABILITY&product=${product}` : ""}`
|
|
998
1131
|
);
|
|
999
1132
|
let gpus = backend.unwrap(raw);
|
|
1000
1133
|
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
@@ -1167,6 +1300,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1167
1300
|
gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
|
|
1168
1301
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1169
1302
|
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1303
|
+
),
|
|
1304
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1305
|
+
"Product context for the availability lookup \u2014 stock differs by product. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1170
1306
|
)
|
|
1171
1307
|
},
|
|
1172
1308
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
@@ -1179,7 +1315,8 @@ function registerCatalogTools(server, rt) {
|
|
|
1179
1315
|
});
|
|
1180
1316
|
}
|
|
1181
1317
|
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1182
|
-
const
|
|
1318
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1319
|
+
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1183
1320
|
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1184
1321
|
return jsonReply2(result);
|
|
1185
1322
|
}
|
|
@@ -2059,7 +2196,7 @@ function registerPodTools(server, rt) {
|
|
|
2059
2196
|
);
|
|
2060
2197
|
server.tool(
|
|
2061
2198
|
"create-pod",
|
|
2062
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2199
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). For full SSH access (including SCP/SFTP/rsync), pass sshPublicKey \u2014 it sets the PUBLIC_KEY env var and exposes 22/tcp, which official Runpod images turn into a running sshd with the key authorized. If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2063
2200
|
{
|
|
2064
2201
|
name: import_zod7.z.string().optional().describe(
|
|
2065
2202
|
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
@@ -2073,12 +2210,18 @@ function registerPodTools(server, rt) {
|
|
|
2073
2210
|
),
|
|
2074
2211
|
cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2075
2212
|
gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
|
|
2076
|
-
|
|
2213
|
+
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
2214
|
+
// a negative, or a fractional count reached the wire as-is and came back
|
|
2215
|
+
// as a raw 422; reject it here with a message that names the field.
|
|
2216
|
+
gpuCount: import_zod7.z.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2077
2217
|
containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
|
|
2078
2218
|
volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
|
|
2079
2219
|
volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
|
|
2080
2220
|
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2081
2221
|
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
|
|
2222
|
+
sshPublicKey: import_zod7.z.string().optional().describe(
|
|
2223
|
+
'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.'
|
|
2224
|
+
),
|
|
2082
2225
|
containerRegistryAuthId: import_zod7.z.string().optional().describe(
|
|
2083
2226
|
"Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
|
|
2084
2227
|
),
|
|
@@ -2087,6 +2230,15 @@ function registerPodTools(server, rt) {
|
|
|
2087
2230
|
{ title: "Create pod", ...WRITE },
|
|
2088
2231
|
async (params) => {
|
|
2089
2232
|
const backend = backendFor("pods");
|
|
2233
|
+
const { sshPublicKey, ...podParams } = params;
|
|
2234
|
+
let sshKey;
|
|
2235
|
+
if (sshPublicKey !== void 0) {
|
|
2236
|
+
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
2237
|
+
if (!validated.ok)
|
|
2238
|
+
return jsonReply2({ error: validated.error, status: 400 });
|
|
2239
|
+
sshKey = validated.key;
|
|
2240
|
+
}
|
|
2241
|
+
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.";
|
|
2090
2242
|
if (params.templateId && backend.version !== "v2") {
|
|
2091
2243
|
return jsonReply2({
|
|
2092
2244
|
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
@@ -2136,13 +2288,15 @@ function registerPodTools(server, rt) {
|
|
|
2136
2288
|
});
|
|
2137
2289
|
}
|
|
2138
2290
|
try {
|
|
2291
|
+
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2139
2292
|
const result = await callRestUrl(
|
|
2140
2293
|
`${restV1Base(env)}/pods`,
|
|
2141
2294
|
"POST",
|
|
2142
|
-
|
|
2295
|
+
cpuBody
|
|
2143
2296
|
);
|
|
2144
2297
|
return jsonReply2({
|
|
2145
2298
|
...result,
|
|
2299
|
+
...sshKey ? { _ssh: sshNote } : {},
|
|
2146
2300
|
_servedBy: "v1",
|
|
2147
2301
|
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2148
2302
|
});
|
|
@@ -2159,7 +2313,7 @@ function registerPodTools(server, rt) {
|
|
|
2159
2313
|
}
|
|
2160
2314
|
}
|
|
2161
2315
|
}
|
|
2162
|
-
let body = backend.mapCreate(
|
|
2316
|
+
let body = backend.mapCreate(podParams);
|
|
2163
2317
|
if (params.templateId) {
|
|
2164
2318
|
let template;
|
|
2165
2319
|
try {
|
|
@@ -2177,19 +2331,21 @@ function registerPodTools(server, rt) {
|
|
|
2177
2331
|
}
|
|
2178
2332
|
body = { ...podBodyFromTemplate(template), ...body };
|
|
2179
2333
|
}
|
|
2334
|
+
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2180
2335
|
try {
|
|
2181
2336
|
const result = await callRestUrl(
|
|
2182
2337
|
`${backend.base}${backend.list}`,
|
|
2183
2338
|
"POST",
|
|
2184
2339
|
body
|
|
2185
2340
|
);
|
|
2341
|
+
const extras = {};
|
|
2342
|
+
if (sshKey) extras._ssh = sshNote;
|
|
2186
2343
|
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2187
|
-
|
|
2188
|
-
...result,
|
|
2189
|
-
_warning: `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`
|
|
2190
|
-
});
|
|
2344
|
+
extras._warning = `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`;
|
|
2191
2345
|
}
|
|
2192
|
-
return jsonReply2(
|
|
2346
|
+
return jsonReply2(
|
|
2347
|
+
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
2348
|
+
);
|
|
2193
2349
|
} catch (error) {
|
|
2194
2350
|
if (error instanceof HttpError) {
|
|
2195
2351
|
return jsonReply2({ error: error.message, status: error.status });
|
|
@@ -2650,8 +2806,108 @@ var import_zod9 = require("zod");
|
|
|
2650
2806
|
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2651
2807
|
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2652
2808
|
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
2809
|
+
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
2810
|
+
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
2811
|
+
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
2812
|
+
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
2813
|
+
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
2814
|
+
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
2815
|
+
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
2816
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
2817
|
+
"COMPLETED",
|
|
2818
|
+
"FAILED",
|
|
2819
|
+
"CANCELLED",
|
|
2820
|
+
"TIMED_OUT"
|
|
2821
|
+
]);
|
|
2822
|
+
function isUnfinishedJobReply(result) {
|
|
2823
|
+
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
2824
|
+
}
|
|
2825
|
+
function formatBudget(ms) {
|
|
2826
|
+
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
2827
|
+
}
|
|
2828
|
+
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
2829
|
+
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
2830
|
+
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
2831
|
+
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
2832
|
+
return Math.max(
|
|
2833
|
+
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
2834
|
+
MIN_STREAM_POLL_TIMEOUT_MS
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
function sleep(ms) {
|
|
2838
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2839
|
+
}
|
|
2840
|
+
function streamPollPlan(hosted) {
|
|
2841
|
+
return hosted ? {
|
|
2842
|
+
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
2843
|
+
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
2844
|
+
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
2845
|
+
} : {
|
|
2846
|
+
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
2847
|
+
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
2848
|
+
query: ""
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
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).";
|
|
2852
|
+
function budgetExhaustedNote(budgetMs, lastError) {
|
|
2853
|
+
return {
|
|
2854
|
+
pollingTimedOut: true,
|
|
2855
|
+
note: `Polling stopped after ${formatBudget(
|
|
2856
|
+
budgetMs
|
|
2857
|
+
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
2858
|
+
// Surface the trailing error (if any) instead of discarding it — the last
|
|
2859
|
+
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
2860
|
+
// succeeded. Cleared on success, so this is never a stale error from
|
|
2861
|
+
// minutes of healthy streaming ago.
|
|
2862
|
+
...lastError ? { lastError } : {}
|
|
2863
|
+
};
|
|
2864
|
+
}
|
|
2865
|
+
function errorCapNote(lastError) {
|
|
2866
|
+
return {
|
|
2867
|
+
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
2868
|
+
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
async function collectJobStream(deps) {
|
|
2872
|
+
const { poll, budgetMs, holdMs } = deps;
|
|
2873
|
+
const startedAt = Date.now();
|
|
2874
|
+
const elapsed = () => Date.now() - startedAt;
|
|
2875
|
+
const chunks = [];
|
|
2876
|
+
let result = {};
|
|
2877
|
+
let consecutiveErrors = 0;
|
|
2878
|
+
let lastError;
|
|
2879
|
+
while (true) {
|
|
2880
|
+
try {
|
|
2881
|
+
const reply = await poll(
|
|
2882
|
+
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
2883
|
+
);
|
|
2884
|
+
consecutiveErrors = 0;
|
|
2885
|
+
lastError = void 0;
|
|
2886
|
+
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
2887
|
+
result = reply;
|
|
2888
|
+
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
2889
|
+
} catch (error) {
|
|
2890
|
+
consecutiveErrors++;
|
|
2891
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
2892
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
2893
|
+
result = { ...result, ...errorCapNote(lastError) };
|
|
2894
|
+
break;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
if (elapsed() > budgetMs) {
|
|
2898
|
+
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
2899
|
+
break;
|
|
2900
|
+
}
|
|
2901
|
+
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
2902
|
+
}
|
|
2903
|
+
return { result, chunks };
|
|
2904
|
+
}
|
|
2653
2905
|
function registerJobTools(server, rt) {
|
|
2654
2906
|
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
2907
|
+
const hosted = rt.transport === "http";
|
|
2908
|
+
const streamBudget = formatBudget(
|
|
2909
|
+
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
2910
|
+
);
|
|
2655
2911
|
async function diagnoseQueuedJob(endpointId) {
|
|
2656
2912
|
const cached = diagnosisCache.get(endpointId);
|
|
2657
2913
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -2662,7 +2918,10 @@ function registerJobTools(server, rt) {
|
|
|
2662
2918
|
const backend = backendFor("workers");
|
|
2663
2919
|
if (backend.version !== "v2") return null;
|
|
2664
2920
|
const raw = await callRestUrl(
|
|
2665
|
-
`${backend.base}/serverless/${endpointId}/workers
|
|
2921
|
+
`${backend.base}/serverless/${endpointId}/workers`,
|
|
2922
|
+
"GET",
|
|
2923
|
+
void 0,
|
|
2924
|
+
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
2666
2925
|
);
|
|
2667
2926
|
const summary = raw?.summary;
|
|
2668
2927
|
const workers = raw?.workers ?? [];
|
|
@@ -2753,14 +3012,14 @@ function registerJobTools(server, rt) {
|
|
|
2753
3012
|
);
|
|
2754
3013
|
server.tool(
|
|
2755
3014
|
"runsync-endpoint",
|
|
2756
|
-
|
|
3015
|
+
`Submit a synchronous job to a Serverless endpoint and wait for the result. Best for fast tasks: if the job outlives the wait, the response returns a job ID and a non-terminal status (IN_QUEUE or IN_PROGRESS) to poll with get-job-status. Max payload 20 MB; results expire after 1 minute. ${hosted ? `This server waits up to ${HTTP_LONG_POLL_BUDGET_MS} ms \u2014 it runs behind a 60-second gateway deadline, so that is both the default and the ceiling for the wait parameter. For longer jobs use run-endpoint + get-job-status, or POST https://api.runpod.ai/v2/{endpointId}/runsync?wait=300000 directly with a Bearer API key.` : "The wait parameter extends the server-side wait to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."}`,
|
|
2757
3016
|
{
|
|
2758
3017
|
endpointId: endpointIdSchema.describe(
|
|
2759
3018
|
"ID of the Serverless endpoint to run synchronously"
|
|
2760
3019
|
),
|
|
2761
3020
|
input: inputSchema,
|
|
2762
3021
|
wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
|
|
2763
|
-
"How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
3022
|
+
hosted ? `How long in milliseconds the server should wait for a result before returning a job ID to poll. Accepted range 1000\u2013300000, but this server clamps anything above ${HTTP_LONG_POLL_BUDGET_MS} down to ${HTTP_LONG_POLL_BUDGET_MS}, which is also the effective default.` : "How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
2764
3023
|
),
|
|
2765
3024
|
webhook: webhookSchema,
|
|
2766
3025
|
policy: policySchema,
|
|
@@ -2769,13 +3028,36 @@ function registerJobTools(server, rt) {
|
|
|
2769
3028
|
{ title: "Run endpoint (sync)", ...WRITE },
|
|
2770
3029
|
async (params) => {
|
|
2771
3030
|
const { endpointId, wait, ...body } = params;
|
|
2772
|
-
const
|
|
3031
|
+
const effectiveWait = hosted ? Math.min(
|
|
3032
|
+
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3033
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3034
|
+
) : wait;
|
|
3035
|
+
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
3036
|
+
const path = `/runsync${waitQuery}`;
|
|
2773
3037
|
const result = await serverlessRequest(
|
|
2774
3038
|
endpointId,
|
|
2775
3039
|
path,
|
|
2776
3040
|
"POST",
|
|
2777
|
-
body
|
|
3041
|
+
body,
|
|
3042
|
+
{
|
|
3043
|
+
// From the wait actually sent — on http the clamped 45s, not the
|
|
3044
|
+
// 300s the caller may have asked for.
|
|
3045
|
+
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
3046
|
+
}
|
|
2778
3047
|
);
|
|
3048
|
+
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
3049
|
+
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
3050
|
+
return jsonReply2({
|
|
3051
|
+
...result,
|
|
3052
|
+
waitClamped: {
|
|
3053
|
+
requestedMs: requestedWait,
|
|
3054
|
+
effectiveMs: effectiveWait,
|
|
3055
|
+
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
3056
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3057
|
+
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
3058
|
+
}
|
|
3059
|
+
});
|
|
3060
|
+
}
|
|
2779
3061
|
return jsonReply2(result);
|
|
2780
3062
|
}
|
|
2781
3063
|
);
|
|
@@ -2809,7 +3091,7 @@ function registerJobTools(server, rt) {
|
|
|
2809
3091
|
);
|
|
2810
3092
|
server.tool(
|
|
2811
3093
|
"stream-job",
|
|
2812
|
-
|
|
3094
|
+
`Retrieve streaming output from a Serverless job. The worker must support streaming output. Polls /stream/{jobId} and collects chunks until the status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT, for up to ${streamBudget}${hosted ? " (this server runs behind a 60-second gateway deadline)" : ""}. If the budget expires first, returns the chunks collected so far with pollingTimedOut: true \u2014 call stream-job again to resume where it left off, or get-job-status to check without streaming.${hosted ? " To collect with no budget, poll GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} yourself with a Bearer API key until the status is terminal \u2014 each request drains only the chunks buffered so far, so a single request is not the whole output." : ""}`,
|
|
2813
3095
|
{
|
|
2814
3096
|
endpointId: endpointIdSchema.describe(
|
|
2815
3097
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -2818,51 +3100,16 @@ function registerJobTools(server, rt) {
|
|
|
2818
3100
|
},
|
|
2819
3101
|
{ title: "Stream job", ...READ_ONLY },
|
|
2820
3102
|
async (params) => {
|
|
2821
|
-
const
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
let finalResult = {};
|
|
2832
|
-
let consecutiveErrors = 0;
|
|
2833
|
-
let lastError;
|
|
2834
|
-
const startTime = Date.now();
|
|
2835
|
-
while (true) {
|
|
2836
|
-
try {
|
|
2837
|
-
const result = await serverlessRequest(
|
|
2838
|
-
params.endpointId,
|
|
2839
|
-
`/stream/${params.jobId}`
|
|
2840
|
-
);
|
|
2841
|
-
consecutiveErrors = 0;
|
|
2842
|
-
if (Array.isArray(result.stream)) {
|
|
2843
|
-
allChunks.push(...result.stream);
|
|
2844
|
-
}
|
|
2845
|
-
finalResult = result;
|
|
2846
|
-
if (TERMINAL_STATUSES.has(result.status)) {
|
|
2847
|
-
break;
|
|
2848
|
-
}
|
|
2849
|
-
} catch (error) {
|
|
2850
|
-
consecutiveErrors++;
|
|
2851
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
2852
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2853
|
-
finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
|
|
2854
|
-
break;
|
|
2855
|
-
}
|
|
2856
|
-
}
|
|
2857
|
-
if (Date.now() - startTime > MAX_POLL_TIME_MS) {
|
|
2858
|
-
finalResult.pollingTimedOut = true;
|
|
2859
|
-
finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
|
|
2860
|
-
if (lastError) finalResult.lastError = lastError;
|
|
2861
|
-
break;
|
|
2862
|
-
}
|
|
2863
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2864
|
-
}
|
|
2865
|
-
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
3103
|
+
const plan = streamPollPlan(hosted);
|
|
3104
|
+
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
3105
|
+
const { result, chunks } = await collectJobStream({
|
|
3106
|
+
budgetMs: plan.budgetMs,
|
|
3107
|
+
holdMs: plan.holdMs,
|
|
3108
|
+
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
3109
|
+
timeoutMs
|
|
3110
|
+
})
|
|
3111
|
+
});
|
|
3112
|
+
return jsonReply2({ ...result, stream: chunks });
|
|
2866
3113
|
}
|
|
2867
3114
|
);
|
|
2868
3115
|
server.tool(
|