@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
|
@@ -151,6 +151,28 @@ var HttpError = class extends Error {
|
|
|
151
151
|
this.body = body;
|
|
152
152
|
}
|
|
153
153
|
};
|
|
154
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
155
|
+
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
156
|
+
var RequestTimeoutError = class extends Error {
|
|
157
|
+
constructor(prefix, timeoutMs, method) {
|
|
158
|
+
const write = method !== "GET" && method !== "HEAD";
|
|
159
|
+
super(
|
|
160
|
+
`${prefix}: no response after ${timeoutMs}ms for ${method} (the API may be overloaded, or still be working on it). ${write ? "This request may have SUCCEEDED upstream \u2014 do not retry blindly. Check with the matching list-/get- tool first and retry only if it did not take effect." : "This was a read and changed nothing, so retrying is safe."}`
|
|
161
|
+
);
|
|
162
|
+
this.name = "RequestTimeoutError";
|
|
163
|
+
this.timeoutMs = timeoutMs;
|
|
164
|
+
this.method = method;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
168
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
169
|
+
try {
|
|
170
|
+
return await run(signal);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
173
|
+
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
154
176
|
function isJsonContentType(contentType) {
|
|
155
177
|
if (!contentType) return false;
|
|
156
178
|
const ct = contentType.toLowerCase();
|
|
@@ -166,28 +188,49 @@ function buildRequestHeaders(apiKey, tracking) {
|
|
|
166
188
|
...tracking()
|
|
167
189
|
};
|
|
168
190
|
}
|
|
191
|
+
function resolveCeiling(ceiling) {
|
|
192
|
+
if (ceiling === void 0) return Infinity;
|
|
193
|
+
if (typeof ceiling !== "function") return ceiling;
|
|
194
|
+
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
195
|
+
}
|
|
196
|
+
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
197
|
+
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
198
|
+
}
|
|
169
199
|
function createHttpClient(deps) {
|
|
170
|
-
|
|
171
|
-
|
|
200
|
+
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
201
|
+
return async function request(url, method = "GET", body, options) {
|
|
202
|
+
const timeoutMs = clampTimeout(
|
|
203
|
+
options?.timeoutMs,
|
|
204
|
+
deps.maxTimeoutMs,
|
|
205
|
+
defaultTimeoutMs
|
|
206
|
+
);
|
|
207
|
+
return withRequestTimeout(
|
|
208
|
+
deps.errorPrefix,
|
|
209
|
+
timeoutMs,
|
|
172
210
|
method,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
response.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
async (signal) => {
|
|
212
|
+
const init = {
|
|
213
|
+
method,
|
|
214
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
215
|
+
signal
|
|
216
|
+
};
|
|
217
|
+
if (body && methodSendsBody(method)) {
|
|
218
|
+
init.body = JSON.stringify(body);
|
|
219
|
+
}
|
|
220
|
+
const response = await deps.fetch(url, init);
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
const hint = response.status === 429 ? rateLimitHint(
|
|
223
|
+
response.headers.get("ratelimit"),
|
|
224
|
+
response.headers.get("retry-after")
|
|
225
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
226
|
+
const text = await response.text().catch(
|
|
227
|
+
() => "<body unavailable: the connection closed while reading it>"
|
|
228
|
+
);
|
|
229
|
+
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
230
|
+
}
|
|
231
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
232
|
+
}
|
|
233
|
+
);
|
|
191
234
|
};
|
|
192
235
|
}
|
|
193
236
|
|
|
@@ -340,7 +383,7 @@ function containerConfigToV2(p) {
|
|
|
340
383
|
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
341
384
|
const id = gpuTypeIds?.[0];
|
|
342
385
|
if (id === void 0) return void 0;
|
|
343
|
-
return
|
|
386
|
+
return { id, count: gpuCount ?? 1 };
|
|
344
387
|
}
|
|
345
388
|
function mapPodCreateToV2(params) {
|
|
346
389
|
return compact({
|
|
@@ -467,6 +510,42 @@ function podBodyFromTemplate(template) {
|
|
|
467
510
|
}
|
|
468
511
|
return out;
|
|
469
512
|
}
|
|
513
|
+
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
514
|
+
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
515
|
+
function normalizeSshPublicKey(input) {
|
|
516
|
+
if (PRIVATE_KEY_MARKER.test(input))
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
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").'
|
|
520
|
+
};
|
|
521
|
+
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
522
|
+
if (lines.length === 0)
|
|
523
|
+
return {
|
|
524
|
+
ok: false,
|
|
525
|
+
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
526
|
+
};
|
|
527
|
+
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
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.'
|
|
531
|
+
};
|
|
532
|
+
return { ok: true, key: lines.join("\n") };
|
|
533
|
+
}
|
|
534
|
+
function applySshPublicKey(body, sshPublicKey) {
|
|
535
|
+
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
536
|
+
const hasSshPort = ports.some((p) => /^\s*22\/tcp\s*$/i.test(String(p)));
|
|
537
|
+
const env = { ...body.env ?? {} };
|
|
538
|
+
const key = sshPublicKey.trim();
|
|
539
|
+
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
540
|
+
else if (!env.PUBLIC_KEY.includes(key))
|
|
541
|
+
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
542
|
+
${key}`;
|
|
543
|
+
return {
|
|
544
|
+
...body,
|
|
545
|
+
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
546
|
+
env
|
|
547
|
+
};
|
|
548
|
+
}
|
|
470
549
|
function mapTemplateUpdateToV2(params) {
|
|
471
550
|
return {
|
|
472
551
|
...containerConfigToV2(params),
|
|
@@ -681,7 +760,7 @@ function resolveBackend(opts) {
|
|
|
681
760
|
}
|
|
682
761
|
|
|
683
762
|
// src/tools/runtime.ts
|
|
684
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
763
|
+
var MCP_SERVER_VERSION = true ? "3.3.0" : "dev";
|
|
685
764
|
var SESSION_ID = randomUUID();
|
|
686
765
|
function trackingHeaders(server, ctx) {
|
|
687
766
|
const info = server.server.getClientVersion();
|
|
@@ -696,6 +775,11 @@ function trackingHeaders(server, ctx) {
|
|
|
696
775
|
sessionId: SESSION_ID
|
|
697
776
|
});
|
|
698
777
|
}
|
|
778
|
+
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
779
|
+
function invocationBudget(totalMs) {
|
|
780
|
+
const startedAt = Date.now();
|
|
781
|
+
return () => totalMs - (Date.now() - startedAt);
|
|
782
|
+
}
|
|
699
783
|
var defaultFetch = fetch;
|
|
700
784
|
async function withApiErrorLog(label, call) {
|
|
701
785
|
try {
|
|
@@ -719,6 +803,23 @@ var DESTRUCTIVE = {
|
|
|
719
803
|
openWorldHint: true
|
|
720
804
|
};
|
|
721
805
|
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
806
|
+
return withRequestTimeout(
|
|
807
|
+
"Runpod GraphQL Error",
|
|
808
|
+
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
809
|
+
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
810
|
+
// off the method, and telling a caller its list-gpu-types "may have
|
|
811
|
+
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
812
|
+
// it back at the tool that just failed. The operation keyword is what
|
|
813
|
+
// actually says whether anything could have been written.
|
|
814
|
+
isGraphqlMutation(query) ? "POST" : "GET",
|
|
815
|
+
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
function isGraphqlMutation(query) {
|
|
819
|
+
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
820
|
+
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
821
|
+
}
|
|
822
|
+
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
722
823
|
const response = await fetchImpl(url, {
|
|
723
824
|
method: "POST",
|
|
724
825
|
headers: {
|
|
@@ -729,7 +830,8 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
729
830
|
body: JSON.stringify({
|
|
730
831
|
query,
|
|
731
832
|
...options?.variables ? { variables: options.variables } : {}
|
|
732
|
-
})
|
|
833
|
+
}),
|
|
834
|
+
signal
|
|
733
835
|
});
|
|
734
836
|
if (!response.ok) {
|
|
735
837
|
const bodyText = await response.text();
|
|
@@ -785,39 +887,53 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
785
887
|
}
|
|
786
888
|
const rawFetch = deps.fetch ?? defaultFetch;
|
|
787
889
|
const httpFetch = observeUnauthorized(rawFetch);
|
|
890
|
+
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
891
|
+
const timeouts = {
|
|
892
|
+
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
893
|
+
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
894
|
+
};
|
|
788
895
|
const v1Client = createHttpClient({
|
|
789
896
|
apiKey: ctx.apiKey,
|
|
790
897
|
fetch: httpFetch,
|
|
791
898
|
tracking,
|
|
792
|
-
errorPrefix: "Runpod API Error"
|
|
899
|
+
errorPrefix: "Runpod API Error",
|
|
900
|
+
...timeouts
|
|
793
901
|
});
|
|
794
|
-
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
902
|
+
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
795
903
|
"Error calling Runpod API:",
|
|
796
|
-
() => v1Client(
|
|
904
|
+
() => v1Client(
|
|
905
|
+
`${restV1Base(process.env)}${endpoint}`,
|
|
906
|
+
method,
|
|
907
|
+
body,
|
|
908
|
+
options
|
|
909
|
+
)
|
|
797
910
|
);
|
|
798
911
|
const serverlessClient = createHttpClient({
|
|
799
912
|
apiKey: ctx.apiKey,
|
|
800
913
|
fetch: httpFetch,
|
|
801
914
|
tracking,
|
|
802
|
-
errorPrefix: "Runpod Serverless API Error"
|
|
915
|
+
errorPrefix: "Runpod Serverless API Error",
|
|
916
|
+
...timeouts
|
|
803
917
|
});
|
|
804
|
-
const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
|
|
918
|
+
const serverlessRequest = (endpointId, path, method = "GET", body, options) => withApiErrorLog(
|
|
805
919
|
"Error calling Runpod Serverless API:",
|
|
806
920
|
() => serverlessClient(
|
|
807
921
|
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
808
922
|
method,
|
|
809
|
-
body
|
|
923
|
+
body,
|
|
924
|
+
options
|
|
810
925
|
)
|
|
811
926
|
);
|
|
812
927
|
const restClient = createHttpClient({
|
|
813
928
|
apiKey: ctx.apiKey,
|
|
814
929
|
fetch: httpFetch,
|
|
815
930
|
tracking,
|
|
816
|
-
errorPrefix: "Runpod API Error"
|
|
931
|
+
errorPrefix: "Runpod API Error",
|
|
932
|
+
...timeouts
|
|
817
933
|
});
|
|
818
|
-
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
934
|
+
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
819
935
|
"Error calling Runpod API:",
|
|
820
|
-
() => restClient(url, method, body)
|
|
936
|
+
() => restClient(url, method, body, options)
|
|
821
937
|
);
|
|
822
938
|
const sseFetch = observeUnauthorized(
|
|
823
939
|
deps.sseFetch ?? fetch
|
|
@@ -850,12 +966,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
850
966
|
};
|
|
851
967
|
return {
|
|
852
968
|
jsonReply,
|
|
969
|
+
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
970
|
+
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
853
971
|
graphql: (query) => graphqlRequest(
|
|
854
972
|
query,
|
|
855
973
|
tracking,
|
|
856
974
|
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
857
975
|
rawFetch,
|
|
858
|
-
publicGraphqlBase(process.env)
|
|
976
|
+
publicGraphqlBase(process.env),
|
|
977
|
+
{
|
|
978
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
979
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
980
|
+
}
|
|
859
981
|
),
|
|
860
982
|
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
861
983
|
query,
|
|
@@ -863,7 +985,12 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
863
985
|
httpFetch,
|
|
864
986
|
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
865
987
|
authedGraphqlBase(process.env),
|
|
866
|
-
{
|
|
988
|
+
{
|
|
989
|
+
variables,
|
|
990
|
+
apiKey: ctx.apiKey,
|
|
991
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
992
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
993
|
+
}
|
|
867
994
|
),
|
|
868
995
|
runpodRequest,
|
|
869
996
|
serverlessRequest,
|
|
@@ -871,7 +998,8 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
871
998
|
backendFor,
|
|
872
999
|
streamSse,
|
|
873
1000
|
podAction,
|
|
874
|
-
env: process.env
|
|
1001
|
+
env: process.env,
|
|
1002
|
+
transport: ctx.transport
|
|
875
1003
|
};
|
|
876
1004
|
}
|
|
877
1005
|
|
|
@@ -940,6 +1068,7 @@ function capListResult(result, options, extra) {
|
|
|
940
1068
|
}
|
|
941
1069
|
|
|
942
1070
|
// src/tools/catalog.ts
|
|
1071
|
+
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
943
1072
|
function registerCatalogTools(server, rt) {
|
|
944
1073
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
945
1074
|
server.tool(
|
|
@@ -958,6 +1087,9 @@ function registerCatalogTools(server, rt) {
|
|
|
958
1087
|
),
|
|
959
1088
|
includeAvailability: z3.boolean().optional().describe(
|
|
960
1089
|
"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."
|
|
1090
|
+
),
|
|
1091
|
+
product: z3.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1092
|
+
"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."
|
|
961
1093
|
)
|
|
962
1094
|
},
|
|
963
1095
|
{ title: "List GPU types", ...READ_ONLY },
|
|
@@ -965,8 +1097,9 @@ function registerCatalogTools(server, rt) {
|
|
|
965
1097
|
const backend = backendFor("gpus");
|
|
966
1098
|
if (backend.version === "v2") {
|
|
967
1099
|
const wantAvailability = params.includeAvailability !== false;
|
|
1100
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
968
1101
|
const raw = await callRestUrl(
|
|
969
|
-
`${backend.base}${backend.list}${wantAvailability ?
|
|
1102
|
+
`${backend.base}${backend.list}${wantAvailability ? `?include=AVAILABILITY&product=${product}` : ""}`
|
|
970
1103
|
);
|
|
971
1104
|
let gpus = backend.unwrap(raw);
|
|
972
1105
|
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
@@ -1139,6 +1272,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1139
1272
|
gpuTypeId: z3.string().describe("ID of the GPU type to retrieve"),
|
|
1140
1273
|
includeAvailability: z3.boolean().optional().describe(
|
|
1141
1274
|
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1275
|
+
),
|
|
1276
|
+
product: z3.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1277
|
+
"Product context for the availability lookup \u2014 stock differs by product. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1142
1278
|
)
|
|
1143
1279
|
},
|
|
1144
1280
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
@@ -1151,7 +1287,8 @@ function registerCatalogTools(server, rt) {
|
|
|
1151
1287
|
});
|
|
1152
1288
|
}
|
|
1153
1289
|
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1154
|
-
const
|
|
1290
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1291
|
+
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1155
1292
|
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1156
1293
|
return jsonReply2(result);
|
|
1157
1294
|
}
|
|
@@ -2031,7 +2168,7 @@ function registerPodTools(server, rt) {
|
|
|
2031
2168
|
);
|
|
2032
2169
|
server.tool(
|
|
2033
2170
|
"create-pod",
|
|
2034
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2171
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). For full SSH access (including SCP/SFTP/rsync), pass sshPublicKey \u2014 it sets the PUBLIC_KEY env var and exposes 22/tcp, which official Runpod images turn into a running sshd with the key authorized. If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2035
2172
|
{
|
|
2036
2173
|
name: z7.string().optional().describe(
|
|
2037
2174
|
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
@@ -2045,12 +2182,18 @@ function registerPodTools(server, rt) {
|
|
|
2045
2182
|
),
|
|
2046
2183
|
cloudType: z7.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2047
2184
|
gpuTypeIds: z7.array(z7.string()).optional().describe("List of acceptable GPU types"),
|
|
2048
|
-
|
|
2185
|
+
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
2186
|
+
// a negative, or a fractional count reached the wire as-is and came back
|
|
2187
|
+
// as a raw 422; reject it here with a message that names the field.
|
|
2188
|
+
gpuCount: z7.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2049
2189
|
containerDiskInGb: z7.number().optional().describe("Container disk size in GB"),
|
|
2050
2190
|
volumeInGb: z7.number().optional().describe("Volume size in GB"),
|
|
2051
2191
|
volumeMountPath: z7.string().optional().describe("Path to mount the volume"),
|
|
2052
2192
|
ports: z7.array(z7.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2053
2193
|
env: z7.record(z7.string()).optional().describe("Environment variables"),
|
|
2194
|
+
sshPublicKey: z7.string().optional().describe(
|
|
2195
|
+
'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.'
|
|
2196
|
+
),
|
|
2054
2197
|
containerRegistryAuthId: z7.string().optional().describe(
|
|
2055
2198
|
"Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
|
|
2056
2199
|
),
|
|
@@ -2059,6 +2202,15 @@ function registerPodTools(server, rt) {
|
|
|
2059
2202
|
{ title: "Create pod", ...WRITE },
|
|
2060
2203
|
async (params) => {
|
|
2061
2204
|
const backend = backendFor("pods");
|
|
2205
|
+
const { sshPublicKey, ...podParams } = params;
|
|
2206
|
+
let sshKey;
|
|
2207
|
+
if (sshPublicKey !== void 0) {
|
|
2208
|
+
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
2209
|
+
if (!validated.ok)
|
|
2210
|
+
return jsonReply2({ error: validated.error, status: 400 });
|
|
2211
|
+
sshKey = validated.key;
|
|
2212
|
+
}
|
|
2213
|
+
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.";
|
|
2062
2214
|
if (params.templateId && backend.version !== "v2") {
|
|
2063
2215
|
return jsonReply2({
|
|
2064
2216
|
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
@@ -2108,13 +2260,15 @@ function registerPodTools(server, rt) {
|
|
|
2108
2260
|
});
|
|
2109
2261
|
}
|
|
2110
2262
|
try {
|
|
2263
|
+
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2111
2264
|
const result = await callRestUrl(
|
|
2112
2265
|
`${restV1Base(env)}/pods`,
|
|
2113
2266
|
"POST",
|
|
2114
|
-
|
|
2267
|
+
cpuBody
|
|
2115
2268
|
);
|
|
2116
2269
|
return jsonReply2({
|
|
2117
2270
|
...result,
|
|
2271
|
+
...sshKey ? { _ssh: sshNote } : {},
|
|
2118
2272
|
_servedBy: "v1",
|
|
2119
2273
|
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2120
2274
|
});
|
|
@@ -2131,7 +2285,7 @@ function registerPodTools(server, rt) {
|
|
|
2131
2285
|
}
|
|
2132
2286
|
}
|
|
2133
2287
|
}
|
|
2134
|
-
let body = backend.mapCreate(
|
|
2288
|
+
let body = backend.mapCreate(podParams);
|
|
2135
2289
|
if (params.templateId) {
|
|
2136
2290
|
let template;
|
|
2137
2291
|
try {
|
|
@@ -2149,19 +2303,21 @@ function registerPodTools(server, rt) {
|
|
|
2149
2303
|
}
|
|
2150
2304
|
body = { ...podBodyFromTemplate(template), ...body };
|
|
2151
2305
|
}
|
|
2306
|
+
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2152
2307
|
try {
|
|
2153
2308
|
const result = await callRestUrl(
|
|
2154
2309
|
`${backend.base}${backend.list}`,
|
|
2155
2310
|
"POST",
|
|
2156
2311
|
body
|
|
2157
2312
|
);
|
|
2313
|
+
const extras = {};
|
|
2314
|
+
if (sshKey) extras._ssh = sshNote;
|
|
2158
2315
|
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2159
|
-
|
|
2160
|
-
...result,
|
|
2161
|
-
_warning: `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`
|
|
2162
|
-
});
|
|
2316
|
+
extras._warning = `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`;
|
|
2163
2317
|
}
|
|
2164
|
-
return jsonReply2(
|
|
2318
|
+
return jsonReply2(
|
|
2319
|
+
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
2320
|
+
);
|
|
2165
2321
|
} catch (error) {
|
|
2166
2322
|
if (error instanceof HttpError) {
|
|
2167
2323
|
return jsonReply2({ error: error.message, status: error.status });
|
|
@@ -2622,8 +2778,108 @@ import { z as z9 } from "zod";
|
|
|
2622
2778
|
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2623
2779
|
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2624
2780
|
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
2781
|
+
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
2782
|
+
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
2783
|
+
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
2784
|
+
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
2785
|
+
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
2786
|
+
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
2787
|
+
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
2788
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
2789
|
+
"COMPLETED",
|
|
2790
|
+
"FAILED",
|
|
2791
|
+
"CANCELLED",
|
|
2792
|
+
"TIMED_OUT"
|
|
2793
|
+
]);
|
|
2794
|
+
function isUnfinishedJobReply(result) {
|
|
2795
|
+
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
2796
|
+
}
|
|
2797
|
+
function formatBudget(ms) {
|
|
2798
|
+
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
2799
|
+
}
|
|
2800
|
+
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
2801
|
+
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
2802
|
+
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
2803
|
+
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
2804
|
+
return Math.max(
|
|
2805
|
+
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
2806
|
+
MIN_STREAM_POLL_TIMEOUT_MS
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
function sleep(ms) {
|
|
2810
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2811
|
+
}
|
|
2812
|
+
function streamPollPlan(hosted) {
|
|
2813
|
+
return hosted ? {
|
|
2814
|
+
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
2815
|
+
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
2816
|
+
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
2817
|
+
} : {
|
|
2818
|
+
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
2819
|
+
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
2820
|
+
query: ""
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
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).";
|
|
2824
|
+
function budgetExhaustedNote(budgetMs, lastError) {
|
|
2825
|
+
return {
|
|
2826
|
+
pollingTimedOut: true,
|
|
2827
|
+
note: `Polling stopped after ${formatBudget(
|
|
2828
|
+
budgetMs
|
|
2829
|
+
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
2830
|
+
// Surface the trailing error (if any) instead of discarding it — the last
|
|
2831
|
+
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
2832
|
+
// succeeded. Cleared on success, so this is never a stale error from
|
|
2833
|
+
// minutes of healthy streaming ago.
|
|
2834
|
+
...lastError ? { lastError } : {}
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
function errorCapNote(lastError) {
|
|
2838
|
+
return {
|
|
2839
|
+
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
2840
|
+
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
async function collectJobStream(deps) {
|
|
2844
|
+
const { poll, budgetMs, holdMs } = deps;
|
|
2845
|
+
const startedAt = Date.now();
|
|
2846
|
+
const elapsed = () => Date.now() - startedAt;
|
|
2847
|
+
const chunks = [];
|
|
2848
|
+
let result = {};
|
|
2849
|
+
let consecutiveErrors = 0;
|
|
2850
|
+
let lastError;
|
|
2851
|
+
while (true) {
|
|
2852
|
+
try {
|
|
2853
|
+
const reply = await poll(
|
|
2854
|
+
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
2855
|
+
);
|
|
2856
|
+
consecutiveErrors = 0;
|
|
2857
|
+
lastError = void 0;
|
|
2858
|
+
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
2859
|
+
result = reply;
|
|
2860
|
+
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
2861
|
+
} catch (error) {
|
|
2862
|
+
consecutiveErrors++;
|
|
2863
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
2864
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
2865
|
+
result = { ...result, ...errorCapNote(lastError) };
|
|
2866
|
+
break;
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
if (elapsed() > budgetMs) {
|
|
2870
|
+
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
2871
|
+
break;
|
|
2872
|
+
}
|
|
2873
|
+
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
2874
|
+
}
|
|
2875
|
+
return { result, chunks };
|
|
2876
|
+
}
|
|
2625
2877
|
function registerJobTools(server, rt) {
|
|
2626
2878
|
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
2879
|
+
const hosted = rt.transport === "http";
|
|
2880
|
+
const streamBudget = formatBudget(
|
|
2881
|
+
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
2882
|
+
);
|
|
2627
2883
|
async function diagnoseQueuedJob(endpointId) {
|
|
2628
2884
|
const cached = diagnosisCache.get(endpointId);
|
|
2629
2885
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -2634,7 +2890,10 @@ function registerJobTools(server, rt) {
|
|
|
2634
2890
|
const backend = backendFor("workers");
|
|
2635
2891
|
if (backend.version !== "v2") return null;
|
|
2636
2892
|
const raw = await callRestUrl(
|
|
2637
|
-
`${backend.base}/serverless/${endpointId}/workers
|
|
2893
|
+
`${backend.base}/serverless/${endpointId}/workers`,
|
|
2894
|
+
"GET",
|
|
2895
|
+
void 0,
|
|
2896
|
+
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
2638
2897
|
);
|
|
2639
2898
|
const summary = raw?.summary;
|
|
2640
2899
|
const workers = raw?.workers ?? [];
|
|
@@ -2725,14 +2984,14 @@ function registerJobTools(server, rt) {
|
|
|
2725
2984
|
);
|
|
2726
2985
|
server.tool(
|
|
2727
2986
|
"runsync-endpoint",
|
|
2728
|
-
|
|
2987
|
+
`Submit a synchronous job to a Serverless endpoint and wait for the result. Best for fast tasks: if the job outlives the wait, the response returns a job ID and a non-terminal status (IN_QUEUE or IN_PROGRESS) to poll with get-job-status. Max payload 20 MB; results expire after 1 minute. ${hosted ? `This server waits up to ${HTTP_LONG_POLL_BUDGET_MS} ms \u2014 it runs behind a 60-second gateway deadline, so that is both the default and the ceiling for the wait parameter. For longer jobs use run-endpoint + get-job-status, or POST https://api.runpod.ai/v2/{endpointId}/runsync?wait=300000 directly with a Bearer API key.` : "The wait parameter extends the server-side wait to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."}`,
|
|
2729
2988
|
{
|
|
2730
2989
|
endpointId: endpointIdSchema.describe(
|
|
2731
2990
|
"ID of the Serverless endpoint to run synchronously"
|
|
2732
2991
|
),
|
|
2733
2992
|
input: inputSchema,
|
|
2734
2993
|
wait: z9.number().min(1e3).max(3e5).optional().describe(
|
|
2735
|
-
"How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
2994
|
+
hosted ? `How long in milliseconds the server should wait for a result before returning a job ID to poll. Accepted range 1000\u2013300000, but this server clamps anything above ${HTTP_LONG_POLL_BUDGET_MS} down to ${HTTP_LONG_POLL_BUDGET_MS}, which is also the effective default.` : "How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
2736
2995
|
),
|
|
2737
2996
|
webhook: webhookSchema,
|
|
2738
2997
|
policy: policySchema,
|
|
@@ -2741,13 +3000,36 @@ function registerJobTools(server, rt) {
|
|
|
2741
3000
|
{ title: "Run endpoint (sync)", ...WRITE },
|
|
2742
3001
|
async (params) => {
|
|
2743
3002
|
const { endpointId, wait, ...body } = params;
|
|
2744
|
-
const
|
|
3003
|
+
const effectiveWait = hosted ? Math.min(
|
|
3004
|
+
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3005
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3006
|
+
) : wait;
|
|
3007
|
+
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
3008
|
+
const path = `/runsync${waitQuery}`;
|
|
2745
3009
|
const result = await serverlessRequest(
|
|
2746
3010
|
endpointId,
|
|
2747
3011
|
path,
|
|
2748
3012
|
"POST",
|
|
2749
|
-
body
|
|
3013
|
+
body,
|
|
3014
|
+
{
|
|
3015
|
+
// From the wait actually sent — on http the clamped 45s, not the
|
|
3016
|
+
// 300s the caller may have asked for.
|
|
3017
|
+
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
3018
|
+
}
|
|
2750
3019
|
);
|
|
3020
|
+
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
3021
|
+
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
3022
|
+
return jsonReply2({
|
|
3023
|
+
...result,
|
|
3024
|
+
waitClamped: {
|
|
3025
|
+
requestedMs: requestedWait,
|
|
3026
|
+
effectiveMs: effectiveWait,
|
|
3027
|
+
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
3028
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3029
|
+
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
2751
3033
|
return jsonReply2(result);
|
|
2752
3034
|
}
|
|
2753
3035
|
);
|
|
@@ -2781,7 +3063,7 @@ function registerJobTools(server, rt) {
|
|
|
2781
3063
|
);
|
|
2782
3064
|
server.tool(
|
|
2783
3065
|
"stream-job",
|
|
2784
|
-
|
|
3066
|
+
`Retrieve streaming output from a Serverless job. The worker must support streaming output. Polls /stream/{jobId} and collects chunks until the status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT, for up to ${streamBudget}${hosted ? " (this server runs behind a 60-second gateway deadline)" : ""}. If the budget expires first, returns the chunks collected so far with pollingTimedOut: true \u2014 call stream-job again to resume where it left off, or get-job-status to check without streaming.${hosted ? " To collect with no budget, poll GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} yourself with a Bearer API key until the status is terminal \u2014 each request drains only the chunks buffered so far, so a single request is not the whole output." : ""}`,
|
|
2785
3067
|
{
|
|
2786
3068
|
endpointId: endpointIdSchema.describe(
|
|
2787
3069
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -2790,51 +3072,16 @@ function registerJobTools(server, rt) {
|
|
|
2790
3072
|
},
|
|
2791
3073
|
{ title: "Stream job", ...READ_ONLY },
|
|
2792
3074
|
async (params) => {
|
|
2793
|
-
const
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
let finalResult = {};
|
|
2804
|
-
let consecutiveErrors = 0;
|
|
2805
|
-
let lastError;
|
|
2806
|
-
const startTime = Date.now();
|
|
2807
|
-
while (true) {
|
|
2808
|
-
try {
|
|
2809
|
-
const result = await serverlessRequest(
|
|
2810
|
-
params.endpointId,
|
|
2811
|
-
`/stream/${params.jobId}`
|
|
2812
|
-
);
|
|
2813
|
-
consecutiveErrors = 0;
|
|
2814
|
-
if (Array.isArray(result.stream)) {
|
|
2815
|
-
allChunks.push(...result.stream);
|
|
2816
|
-
}
|
|
2817
|
-
finalResult = result;
|
|
2818
|
-
if (TERMINAL_STATUSES.has(result.status)) {
|
|
2819
|
-
break;
|
|
2820
|
-
}
|
|
2821
|
-
} catch (error) {
|
|
2822
|
-
consecutiveErrors++;
|
|
2823
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
2824
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2825
|
-
finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
|
|
2826
|
-
break;
|
|
2827
|
-
}
|
|
2828
|
-
}
|
|
2829
|
-
if (Date.now() - startTime > MAX_POLL_TIME_MS) {
|
|
2830
|
-
finalResult.pollingTimedOut = true;
|
|
2831
|
-
finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
|
|
2832
|
-
if (lastError) finalResult.lastError = lastError;
|
|
2833
|
-
break;
|
|
2834
|
-
}
|
|
2835
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2836
|
-
}
|
|
2837
|
-
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
3075
|
+
const plan = streamPollPlan(hosted);
|
|
3076
|
+
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
3077
|
+
const { result, chunks } = await collectJobStream({
|
|
3078
|
+
budgetMs: plan.budgetMs,
|
|
3079
|
+
holdMs: plan.holdMs,
|
|
3080
|
+
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
3081
|
+
timeoutMs
|
|
3082
|
+
})
|
|
3083
|
+
});
|
|
3084
|
+
return jsonReply2({ ...result, stream: chunks });
|
|
2838
3085
|
}
|
|
2839
3086
|
);
|
|
2840
3087
|
server.tool(
|
|
@@ -3381,4 +3628,4 @@ export {
|
|
|
3381
3628
|
restVersionSummary,
|
|
3382
3629
|
registerTools
|
|
3383
3630
|
};
|
|
3384
|
-
//# sourceMappingURL=chunk-
|
|
3631
|
+
//# sourceMappingURL=chunk-ICUS4KGX.mjs.map
|