@runpod/mcp-server 3.2.0 → 3.4.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 +72 -0
- package/dist/{chunk-WGK7D2YR.mjs → chunk-G4URDODF.mjs} +504 -112
- package/dist/chunk-G4URDODF.mjs.map +1 -0
- package/dist/http.cjs +521 -112
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +19 -2
- package/dist/http.mjs.map +1 -1
- package/dist/stdio.cjs +1116 -218
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +505 -113
- package/dist/stdio.mjs.map +1 -1
- package/dist/tools.cjs +503 -111
- 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/http.cjs
CHANGED
|
@@ -66,10 +66,15 @@ function containerConfigToV2(p) {
|
|
|
66
66
|
mounts: persistentMount(p.volumeInGb, p.volumeMountPath)
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
-
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
69
|
+
function gpuConfigToV2(gpuTypeIds, gpuCount, allowedCudaVersions, minCudaVersion2) {
|
|
70
70
|
const id = gpuTypeIds?.[0];
|
|
71
71
|
if (id === void 0) return void 0;
|
|
72
|
-
return compact({
|
|
72
|
+
return compact({
|
|
73
|
+
id,
|
|
74
|
+
count: gpuCount ?? 1,
|
|
75
|
+
allowedCudaVersions,
|
|
76
|
+
minCudaVersion: minCudaVersion2
|
|
77
|
+
});
|
|
73
78
|
}
|
|
74
79
|
function mapPodCreateToV2(params) {
|
|
75
80
|
return compact({
|
|
@@ -90,7 +95,12 @@ function mapPodCreateToV2(params) {
|
|
|
90
95
|
// singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
|
|
91
96
|
// create-pod handler warns when more than one gpuTypeId was supplied.
|
|
92
97
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
93
|
-
gpu: gpuConfigToV2(
|
|
98
|
+
gpu: gpuConfigToV2(
|
|
99
|
+
params.gpuTypeIds,
|
|
100
|
+
params.gpuCount,
|
|
101
|
+
params.allowedCudaVersions,
|
|
102
|
+
params.minCudaVersion
|
|
103
|
+
)
|
|
94
104
|
});
|
|
95
105
|
}
|
|
96
106
|
function mapPodUpdateToV2(params) {
|
|
@@ -107,9 +117,15 @@ function defaultScalerType(endpointType) {
|
|
|
107
117
|
function endpointScaling(scalerType, value) {
|
|
108
118
|
return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
|
|
109
119
|
}
|
|
110
|
-
function endpointGpuConfig(
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
function endpointGpuConfig(params, mode) {
|
|
121
|
+
const gpu = compact({
|
|
122
|
+
pools: params.gpuPoolIds?.length ? params.gpuPoolIds : void 0,
|
|
123
|
+
count: params.gpuCount,
|
|
124
|
+
allowedCudaVersions: params.allowedCudaVersions,
|
|
125
|
+
minCudaVersion: params.minCudaVersion
|
|
126
|
+
});
|
|
127
|
+
if (mode === "create" && !("pools" in gpu)) return void 0;
|
|
128
|
+
return Object.keys(gpu).length ? gpu : void 0;
|
|
113
129
|
}
|
|
114
130
|
function endpointWorkers(params) {
|
|
115
131
|
const workers = compact({
|
|
@@ -119,7 +135,7 @@ function endpointWorkers(params) {
|
|
|
119
135
|
});
|
|
120
136
|
return Object.keys(workers).length ? workers : void 0;
|
|
121
137
|
}
|
|
122
|
-
function endpointCommonToV2(params) {
|
|
138
|
+
function endpointCommonToV2(params, mode) {
|
|
123
139
|
return compact({
|
|
124
140
|
name: params.name,
|
|
125
141
|
image: params.imageName,
|
|
@@ -128,7 +144,7 @@ function endpointCommonToV2(params) {
|
|
|
128
144
|
ports: params.ports,
|
|
129
145
|
env: params.env,
|
|
130
146
|
registry: params.containerRegistryAuthId,
|
|
131
|
-
gpu: endpointGpuConfig(params
|
|
147
|
+
gpu: endpointGpuConfig(params, mode),
|
|
132
148
|
workers: endpointWorkers(params),
|
|
133
149
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
134
150
|
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
@@ -139,7 +155,7 @@ function endpointCommonToV2(params) {
|
|
|
139
155
|
function mapEndpointCreateToV2(params) {
|
|
140
156
|
const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
|
|
141
157
|
return compact({
|
|
142
|
-
...endpointCommonToV2(params),
|
|
158
|
+
...endpointCommonToV2(params, "create"),
|
|
143
159
|
type: endpointType,
|
|
144
160
|
scaling: endpointScaling(
|
|
145
161
|
params.scalerType ?? defaultScalerType(endpointType),
|
|
@@ -149,13 +165,28 @@ function mapEndpointCreateToV2(params) {
|
|
|
149
165
|
}
|
|
150
166
|
function mapEndpointUpdateToV2(params) {
|
|
151
167
|
return compact({
|
|
152
|
-
...endpointCommonToV2(params),
|
|
168
|
+
...endpointCommonToV2(params, "update"),
|
|
153
169
|
scaling: params.scalerType ? endpointScaling(
|
|
154
170
|
params.scalerType,
|
|
155
171
|
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
156
172
|
) : void 0
|
|
157
173
|
});
|
|
158
174
|
}
|
|
175
|
+
var CUDA_MAJOR_MINOR = /^\d+\.\d+$/;
|
|
176
|
+
function cudaConstraintError(params, opts = {}) {
|
|
177
|
+
const { allowedCudaVersions, minCudaVersion: minCudaVersion2 } = params;
|
|
178
|
+
if (allowedCudaVersions?.length && minCudaVersion2) {
|
|
179
|
+
return "allowedCudaVersions and minCudaVersion are mutually exclusive \u2014 pass either an exact set or an open-ended floor, not both.";
|
|
180
|
+
}
|
|
181
|
+
if (minCudaVersion2 !== void 0 && !CUDA_MAJOR_MINOR.test(minCudaVersion2) && !(opts.allowClear && minCudaVersion2 === "")) {
|
|
182
|
+
return `minCudaVersion must be major.minor (e.g. "12.0", not "12")${opts.allowClear ? "; an empty string clears the floor" : ""}.`;
|
|
183
|
+
}
|
|
184
|
+
const bad = allowedCudaVersions?.find((v) => !CUDA_MAJOR_MINOR.test(v));
|
|
185
|
+
if (bad !== void 0) {
|
|
186
|
+
return `allowedCudaVersions entries must be major.minor (e.g. "12.8"); got "${bad}". Discover valid values via get-capacity or list-gpu-types.`;
|
|
187
|
+
}
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
159
190
|
function mapNetworkVolumeCreateToV2(params) {
|
|
160
191
|
return compact({
|
|
161
192
|
name: params.name,
|
|
@@ -196,6 +227,42 @@ function podBodyFromTemplate(template) {
|
|
|
196
227
|
}
|
|
197
228
|
return out;
|
|
198
229
|
}
|
|
230
|
+
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
231
|
+
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
232
|
+
function normalizeSshPublicKey(input) {
|
|
233
|
+
if (PRIVATE_KEY_MARKER.test(input))
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
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").'
|
|
237
|
+
};
|
|
238
|
+
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
239
|
+
if (lines.length === 0)
|
|
240
|
+
return {
|
|
241
|
+
ok: false,
|
|
242
|
+
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
243
|
+
};
|
|
244
|
+
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
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.'
|
|
248
|
+
};
|
|
249
|
+
return { ok: true, key: lines.join("\n") };
|
|
250
|
+
}
|
|
251
|
+
function applySshPublicKey(body, sshPublicKey) {
|
|
252
|
+
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
253
|
+
const hasSshPort = ports.some((p) => /^\s*22\/tcp\s*$/i.test(String(p)));
|
|
254
|
+
const env = { ...body.env ?? {} };
|
|
255
|
+
const key = sshPublicKey.trim();
|
|
256
|
+
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
257
|
+
else if (!env.PUBLIC_KEY.includes(key))
|
|
258
|
+
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
259
|
+
${key}`;
|
|
260
|
+
return {
|
|
261
|
+
...body,
|
|
262
|
+
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
263
|
+
env
|
|
264
|
+
};
|
|
265
|
+
}
|
|
199
266
|
function mapTemplateUpdateToV2(params) {
|
|
200
267
|
return {
|
|
201
268
|
...containerConfigToV2(params),
|
|
@@ -411,7 +478,7 @@ function resolveBackend(opts) {
|
|
|
411
478
|
|
|
412
479
|
// src/server.ts
|
|
413
480
|
var SERVER_NAME = "Runpod API Server";
|
|
414
|
-
var SERVER_VERSION = true ? "3.
|
|
481
|
+
var SERVER_VERSION = true ? "3.4.0" : "dev";
|
|
415
482
|
var SERVER_INSTRUCTIONS = `These tools cover common Runpod operations, but they are curated projections: the underlying APIs expose more fields and parameters than the tools surface.
|
|
416
483
|
|
|
417
484
|
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.`;
|
|
@@ -586,6 +653,28 @@ var HttpError = class extends Error {
|
|
|
586
653
|
this.body = body;
|
|
587
654
|
}
|
|
588
655
|
};
|
|
656
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
657
|
+
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
658
|
+
var RequestTimeoutError = class extends Error {
|
|
659
|
+
constructor(prefix, timeoutMs, method) {
|
|
660
|
+
const write = method !== "GET" && method !== "HEAD";
|
|
661
|
+
super(
|
|
662
|
+
`${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."}`
|
|
663
|
+
);
|
|
664
|
+
this.name = "RequestTimeoutError";
|
|
665
|
+
this.timeoutMs = timeoutMs;
|
|
666
|
+
this.method = method;
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
670
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
671
|
+
try {
|
|
672
|
+
return await run(signal);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
675
|
+
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
589
678
|
function isJsonContentType(contentType) {
|
|
590
679
|
if (!contentType) return false;
|
|
591
680
|
const ct = contentType.toLowerCase();
|
|
@@ -601,28 +690,49 @@ function buildRequestHeaders(apiKey, tracking) {
|
|
|
601
690
|
...tracking()
|
|
602
691
|
};
|
|
603
692
|
}
|
|
693
|
+
function resolveCeiling(ceiling) {
|
|
694
|
+
if (ceiling === void 0) return Infinity;
|
|
695
|
+
if (typeof ceiling !== "function") return ceiling;
|
|
696
|
+
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
697
|
+
}
|
|
698
|
+
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
699
|
+
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
700
|
+
}
|
|
604
701
|
function createHttpClient(deps) {
|
|
605
|
-
|
|
606
|
-
|
|
702
|
+
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
703
|
+
return async function request(url, method = "GET", body, options) {
|
|
704
|
+
const timeoutMs = clampTimeout(
|
|
705
|
+
options?.timeoutMs,
|
|
706
|
+
deps.maxTimeoutMs,
|
|
707
|
+
defaultTimeoutMs
|
|
708
|
+
);
|
|
709
|
+
return withRequestTimeout(
|
|
710
|
+
deps.errorPrefix,
|
|
711
|
+
timeoutMs,
|
|
607
712
|
method,
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
response.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
713
|
+
async (signal) => {
|
|
714
|
+
const init = {
|
|
715
|
+
method,
|
|
716
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
717
|
+
signal
|
|
718
|
+
};
|
|
719
|
+
if (body && methodSendsBody(method)) {
|
|
720
|
+
init.body = JSON.stringify(body);
|
|
721
|
+
}
|
|
722
|
+
const response = await deps.fetch(url, init);
|
|
723
|
+
if (!response.ok) {
|
|
724
|
+
const hint = response.status === 429 ? rateLimitHint(
|
|
725
|
+
response.headers.get("ratelimit"),
|
|
726
|
+
response.headers.get("retry-after")
|
|
727
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
728
|
+
const text = await response.text().catch(
|
|
729
|
+
() => "<body unavailable: the connection closed while reading it>"
|
|
730
|
+
);
|
|
731
|
+
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
732
|
+
}
|
|
733
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
734
|
+
}
|
|
735
|
+
);
|
|
626
736
|
};
|
|
627
737
|
}
|
|
628
738
|
|
|
@@ -749,7 +859,7 @@ async function streamLogsReply(rt, tool, params) {
|
|
|
749
859
|
}
|
|
750
860
|
|
|
751
861
|
// src/tools/runtime.ts
|
|
752
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
862
|
+
var MCP_SERVER_VERSION = true ? "3.4.0" : "dev";
|
|
753
863
|
var SESSION_ID = (0, import_node_crypto.randomUUID)();
|
|
754
864
|
function trackingHeaders(server, ctx) {
|
|
755
865
|
const info = server.server.getClientVersion();
|
|
@@ -764,6 +874,11 @@ function trackingHeaders(server, ctx) {
|
|
|
764
874
|
sessionId: SESSION_ID
|
|
765
875
|
});
|
|
766
876
|
}
|
|
877
|
+
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
878
|
+
function invocationBudget(totalMs) {
|
|
879
|
+
const startedAt = Date.now();
|
|
880
|
+
return () => totalMs - (Date.now() - startedAt);
|
|
881
|
+
}
|
|
767
882
|
var defaultFetch = import_node_fetch.default;
|
|
768
883
|
async function withApiErrorLog(label, call) {
|
|
769
884
|
try {
|
|
@@ -787,6 +902,23 @@ var DESTRUCTIVE = {
|
|
|
787
902
|
openWorldHint: true
|
|
788
903
|
};
|
|
789
904
|
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
905
|
+
return withRequestTimeout(
|
|
906
|
+
"Runpod GraphQL Error",
|
|
907
|
+
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
908
|
+
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
909
|
+
// off the method, and telling a caller its list-gpu-types "may have
|
|
910
|
+
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
911
|
+
// it back at the tool that just failed. The operation keyword is what
|
|
912
|
+
// actually says whether anything could have been written.
|
|
913
|
+
isGraphqlMutation(query) ? "POST" : "GET",
|
|
914
|
+
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
function isGraphqlMutation(query) {
|
|
918
|
+
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
919
|
+
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
920
|
+
}
|
|
921
|
+
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
790
922
|
const response = await fetchImpl(url, {
|
|
791
923
|
method: "POST",
|
|
792
924
|
headers: {
|
|
@@ -797,7 +929,8 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
797
929
|
body: JSON.stringify({
|
|
798
930
|
query,
|
|
799
931
|
...options?.variables ? { variables: options.variables } : {}
|
|
800
|
-
})
|
|
932
|
+
}),
|
|
933
|
+
signal
|
|
801
934
|
});
|
|
802
935
|
if (!response.ok) {
|
|
803
936
|
const bodyText = await response.text();
|
|
@@ -853,39 +986,53 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
853
986
|
}
|
|
854
987
|
const rawFetch = deps.fetch ?? defaultFetch;
|
|
855
988
|
const httpFetch = observeUnauthorized(rawFetch);
|
|
989
|
+
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
990
|
+
const timeouts = {
|
|
991
|
+
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
992
|
+
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
993
|
+
};
|
|
856
994
|
const v1Client = createHttpClient({
|
|
857
995
|
apiKey: ctx.apiKey,
|
|
858
996
|
fetch: httpFetch,
|
|
859
997
|
tracking,
|
|
860
|
-
errorPrefix: "Runpod API Error"
|
|
998
|
+
errorPrefix: "Runpod API Error",
|
|
999
|
+
...timeouts
|
|
861
1000
|
});
|
|
862
|
-
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
1001
|
+
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
863
1002
|
"Error calling Runpod API:",
|
|
864
|
-
() => v1Client(
|
|
1003
|
+
() => v1Client(
|
|
1004
|
+
`${restV1Base(process.env)}${endpoint}`,
|
|
1005
|
+
method,
|
|
1006
|
+
body,
|
|
1007
|
+
options
|
|
1008
|
+
)
|
|
865
1009
|
);
|
|
866
1010
|
const serverlessClient = createHttpClient({
|
|
867
1011
|
apiKey: ctx.apiKey,
|
|
868
1012
|
fetch: httpFetch,
|
|
869
1013
|
tracking,
|
|
870
|
-
errorPrefix: "Runpod Serverless API Error"
|
|
1014
|
+
errorPrefix: "Runpod Serverless API Error",
|
|
1015
|
+
...timeouts
|
|
871
1016
|
});
|
|
872
|
-
const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
|
|
1017
|
+
const serverlessRequest = (endpointId, path, method = "GET", body, options) => withApiErrorLog(
|
|
873
1018
|
"Error calling Runpod Serverless API:",
|
|
874
1019
|
() => serverlessClient(
|
|
875
1020
|
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
876
1021
|
method,
|
|
877
|
-
body
|
|
1022
|
+
body,
|
|
1023
|
+
options
|
|
878
1024
|
)
|
|
879
1025
|
);
|
|
880
1026
|
const restClient = createHttpClient({
|
|
881
1027
|
apiKey: ctx.apiKey,
|
|
882
1028
|
fetch: httpFetch,
|
|
883
1029
|
tracking,
|
|
884
|
-
errorPrefix: "Runpod API Error"
|
|
1030
|
+
errorPrefix: "Runpod API Error",
|
|
1031
|
+
...timeouts
|
|
885
1032
|
});
|
|
886
|
-
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
1033
|
+
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
887
1034
|
"Error calling Runpod API:",
|
|
888
|
-
() => restClient(url, method, body)
|
|
1035
|
+
() => restClient(url, method, body, options)
|
|
889
1036
|
);
|
|
890
1037
|
const sseFetch = observeUnauthorized(
|
|
891
1038
|
deps.sseFetch ?? import_node_fetch.default
|
|
@@ -918,12 +1065,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
918
1065
|
};
|
|
919
1066
|
return {
|
|
920
1067
|
jsonReply,
|
|
1068
|
+
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
1069
|
+
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
921
1070
|
graphql: (query) => graphqlRequest(
|
|
922
1071
|
query,
|
|
923
1072
|
tracking,
|
|
924
1073
|
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
925
1074
|
rawFetch,
|
|
926
|
-
publicGraphqlBase(process.env)
|
|
1075
|
+
publicGraphqlBase(process.env),
|
|
1076
|
+
{
|
|
1077
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1078
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1079
|
+
}
|
|
927
1080
|
),
|
|
928
1081
|
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
929
1082
|
query,
|
|
@@ -931,7 +1084,12 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
931
1084
|
httpFetch,
|
|
932
1085
|
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
933
1086
|
authedGraphqlBase(process.env),
|
|
934
|
-
{
|
|
1087
|
+
{
|
|
1088
|
+
variables,
|
|
1089
|
+
apiKey: ctx.apiKey,
|
|
1090
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1091
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1092
|
+
}
|
|
935
1093
|
),
|
|
936
1094
|
runpodRequest,
|
|
937
1095
|
serverlessRequest,
|
|
@@ -939,7 +1097,8 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
939
1097
|
backendFor,
|
|
940
1098
|
streamSse,
|
|
941
1099
|
podAction,
|
|
942
|
-
env: process.env
|
|
1100
|
+
env: process.env,
|
|
1101
|
+
transport: ctx.transport
|
|
943
1102
|
};
|
|
944
1103
|
}
|
|
945
1104
|
|
|
@@ -1008,6 +1167,7 @@ function capListResult(result, options, extra) {
|
|
|
1008
1167
|
}
|
|
1009
1168
|
|
|
1010
1169
|
// src/tools/catalog.ts
|
|
1170
|
+
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
1011
1171
|
function registerCatalogTools(server, rt) {
|
|
1012
1172
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
1013
1173
|
server.tool(
|
|
@@ -1026,15 +1186,50 @@ function registerCatalogTools(server, rt) {
|
|
|
1026
1186
|
),
|
|
1027
1187
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1028
1188
|
"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."
|
|
1189
|
+
),
|
|
1190
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1191
|
+
"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."
|
|
1192
|
+
),
|
|
1193
|
+
minCudaVersion: import_zod3.z.string().optional().describe(
|
|
1194
|
+
'Scope the availability lookup to hosts at or above this CUDA version (v2 only). Integer major or major.minor \u2014 "12" means any 12.x release; unlike the create-pod/create-endpoint body fields, a bare major is accepted here because the filter only widens a read. Requires includeAvailability (the default).'
|
|
1029
1195
|
)
|
|
1030
1196
|
},
|
|
1031
1197
|
{ title: "List GPU types", ...READ_ONLY },
|
|
1032
1198
|
async (params) => {
|
|
1033
1199
|
const backend = backendFor("gpus");
|
|
1200
|
+
if (params.minCudaVersion !== void 0 && backend.version !== "v2") {
|
|
1201
|
+
return jsonReply2({
|
|
1202
|
+
error: "The minCudaVersion filter is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or use get-capacity for per-CUDA-version availability.",
|
|
1203
|
+
status: 501
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1034
1206
|
if (backend.version === "v2") {
|
|
1035
1207
|
const wantAvailability = params.includeAvailability !== false;
|
|
1208
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1209
|
+
if (params.minCudaVersion !== void 0) {
|
|
1210
|
+
if (!wantAvailability) {
|
|
1211
|
+
return jsonReply2({
|
|
1212
|
+
error: "minCudaVersion filters the availability lookup, so it requires includeAvailability (the default). Drop includeAvailability:false or the filter.",
|
|
1213
|
+
status: 400
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
if (!/^\d+(\.\d+)?$/.test(params.minCudaVersion)) {
|
|
1217
|
+
return jsonReply2({
|
|
1218
|
+
error: 'minCudaVersion must be an integer major ("12") or major.minor ("12.1").',
|
|
1219
|
+
status: 400
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const query = new URLSearchParams();
|
|
1224
|
+
if (wantAvailability) {
|
|
1225
|
+
query.set("include", "AVAILABILITY");
|
|
1226
|
+
query.set("product", product);
|
|
1227
|
+
if (params.minCudaVersion !== void 0) {
|
|
1228
|
+
query.set("minCudaVersion", params.minCudaVersion);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1036
1231
|
const raw = await callRestUrl(
|
|
1037
|
-
`${backend.base}${backend.list}${
|
|
1232
|
+
`${backend.base}${backend.list}${query.size ? `?${query}` : ""}`
|
|
1038
1233
|
);
|
|
1039
1234
|
let gpus = backend.unwrap(raw);
|
|
1040
1235
|
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
@@ -1207,6 +1402,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1207
1402
|
gpuTypeId: import_zod3.z.string().describe("ID of the GPU type to retrieve"),
|
|
1208
1403
|
includeAvailability: import_zod3.z.boolean().optional().describe(
|
|
1209
1404
|
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1405
|
+
),
|
|
1406
|
+
product: import_zod3.z.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1407
|
+
"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."
|
|
1210
1408
|
)
|
|
1211
1409
|
},
|
|
1212
1410
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
@@ -1219,7 +1417,8 @@ function registerCatalogTools(server, rt) {
|
|
|
1219
1417
|
});
|
|
1220
1418
|
}
|
|
1221
1419
|
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1222
|
-
const
|
|
1420
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1421
|
+
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1223
1422
|
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1224
1423
|
return jsonReply2(result);
|
|
1225
1424
|
}
|
|
@@ -2099,7 +2298,7 @@ function registerPodTools(server, rt) {
|
|
|
2099
2298
|
);
|
|
2100
2299
|
server.tool(
|
|
2101
2300
|
"create-pod",
|
|
2102
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2301
|
+
'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.',
|
|
2103
2302
|
{
|
|
2104
2303
|
name: import_zod7.z.string().optional().describe(
|
|
2105
2304
|
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
@@ -2113,20 +2312,48 @@ function registerPodTools(server, rt) {
|
|
|
2113
2312
|
),
|
|
2114
2313
|
cloudType: import_zod7.z.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2115
2314
|
gpuTypeIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of acceptable GPU types"),
|
|
2116
|
-
|
|
2315
|
+
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
2316
|
+
// a negative, or a fractional count reached the wire as-is and came back
|
|
2317
|
+
// as a raw 422; reject it here with a message that names the field.
|
|
2318
|
+
gpuCount: import_zod7.z.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2117
2319
|
containerDiskInGb: import_zod7.z.number().optional().describe("Container disk size in GB"),
|
|
2118
2320
|
volumeInGb: import_zod7.z.number().optional().describe("Volume size in GB"),
|
|
2119
2321
|
volumeMountPath: import_zod7.z.string().optional().describe("Path to mount the volume"),
|
|
2120
2322
|
ports: import_zod7.z.array(import_zod7.z.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2121
2323
|
env: import_zod7.z.record(import_zod7.z.string()).optional().describe("Environment variables"),
|
|
2324
|
+
sshPublicKey: import_zod7.z.string().optional().describe(
|
|
2325
|
+
'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.'
|
|
2326
|
+
),
|
|
2122
2327
|
containerRegistryAuthId: import_zod7.z.string().optional().describe(
|
|
2123
2328
|
"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)."
|
|
2124
2329
|
),
|
|
2125
|
-
dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers")
|
|
2330
|
+
dataCenterIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("List of data centers"),
|
|
2331
|
+
allowedCudaVersions: import_zod7.z.array(import_zod7.z.string()).optional().describe(
|
|
2332
|
+
'Acceptable host CUDA versions as major.minor, e.g. ["12.8","12.6"] (v2, GPU pods only). Exact match \u2014 discover valid values per GPU type with get-capacity. Mutually exclusive with minCudaVersion.'
|
|
2333
|
+
),
|
|
2334
|
+
minCudaVersion: import_zod7.z.string().optional().describe(
|
|
2335
|
+
'Lowest acceptable host CUDA version as major.minor, e.g. "12.4" (v2, GPU pods only). A bare major like "12" is rejected. Open-ended floor; mutually exclusive with a non-empty allowedCudaVersions.'
|
|
2336
|
+
)
|
|
2126
2337
|
},
|
|
2127
2338
|
{ title: "Create pod", ...WRITE },
|
|
2128
2339
|
async (params) => {
|
|
2129
2340
|
const backend = backendFor("pods");
|
|
2341
|
+
const { sshPublicKey, ...podParams } = params;
|
|
2342
|
+
let sshKey;
|
|
2343
|
+
if (sshPublicKey !== void 0) {
|
|
2344
|
+
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
2345
|
+
if (!validated.ok)
|
|
2346
|
+
return jsonReply2({ error: validated.error, status: 400 });
|
|
2347
|
+
sshKey = validated.key;
|
|
2348
|
+
}
|
|
2349
|
+
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.";
|
|
2350
|
+
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2351
|
+
if (hasCudaParams && backend.version !== "v2") {
|
|
2352
|
+
return jsonReply2({
|
|
2353
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2354
|
+
status: 501
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2130
2357
|
if (params.templateId && backend.version !== "v2") {
|
|
2131
2358
|
return jsonReply2({
|
|
2132
2359
|
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
@@ -2162,6 +2389,16 @@ function registerPodTools(server, rt) {
|
|
|
2162
2389
|
status: 400
|
|
2163
2390
|
});
|
|
2164
2391
|
}
|
|
2392
|
+
if (hasCudaParams && !wantsGpu) {
|
|
2393
|
+
return jsonReply2({
|
|
2394
|
+
error: "allowedCudaVersions/minCudaVersion apply to GPU pods only \u2014 pass gpuTypeIds (see list-gpu-types).",
|
|
2395
|
+
status: 400
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
if (hasCudaParams) {
|
|
2399
|
+
const cudaError = cudaConstraintError(params);
|
|
2400
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2401
|
+
}
|
|
2165
2402
|
if (!wantsCpu && !params.name && !params.templateId) {
|
|
2166
2403
|
return jsonReply2({
|
|
2167
2404
|
error: "Provide a name for the pod (deploying from a templateId inherits the template name).",
|
|
@@ -2176,13 +2413,15 @@ function registerPodTools(server, rt) {
|
|
|
2176
2413
|
});
|
|
2177
2414
|
}
|
|
2178
2415
|
try {
|
|
2416
|
+
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2179
2417
|
const result = await callRestUrl(
|
|
2180
2418
|
`${restV1Base(env)}/pods`,
|
|
2181
2419
|
"POST",
|
|
2182
|
-
|
|
2420
|
+
cpuBody
|
|
2183
2421
|
);
|
|
2184
2422
|
return jsonReply2({
|
|
2185
2423
|
...result,
|
|
2424
|
+
...sshKey ? { _ssh: sshNote } : {},
|
|
2186
2425
|
_servedBy: "v1",
|
|
2187
2426
|
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2188
2427
|
});
|
|
@@ -2199,7 +2438,7 @@ function registerPodTools(server, rt) {
|
|
|
2199
2438
|
}
|
|
2200
2439
|
}
|
|
2201
2440
|
}
|
|
2202
|
-
let body = backend.mapCreate(
|
|
2441
|
+
let body = backend.mapCreate(podParams);
|
|
2203
2442
|
if (params.templateId) {
|
|
2204
2443
|
let template;
|
|
2205
2444
|
try {
|
|
@@ -2217,19 +2456,21 @@ function registerPodTools(server, rt) {
|
|
|
2217
2456
|
}
|
|
2218
2457
|
body = { ...podBodyFromTemplate(template), ...body };
|
|
2219
2458
|
}
|
|
2459
|
+
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2220
2460
|
try {
|
|
2221
2461
|
const result = await callRestUrl(
|
|
2222
2462
|
`${backend.base}${backend.list}`,
|
|
2223
2463
|
"POST",
|
|
2224
2464
|
body
|
|
2225
2465
|
);
|
|
2466
|
+
const extras = {};
|
|
2467
|
+
if (sshKey) extras._ssh = sshNote;
|
|
2226
2468
|
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2227
|
-
|
|
2228
|
-
...result,
|
|
2229
|
-
_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(", ")}.`
|
|
2230
|
-
});
|
|
2469
|
+
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(", ")}.`;
|
|
2231
2470
|
}
|
|
2232
|
-
return jsonReply2(
|
|
2471
|
+
return jsonReply2(
|
|
2472
|
+
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
2473
|
+
);
|
|
2233
2474
|
} catch (error) {
|
|
2234
2475
|
if (error instanceof HttpError) {
|
|
2235
2476
|
return jsonReply2({ error: error.message, status: error.status });
|
|
@@ -2347,6 +2588,9 @@ function registerPodTools(server, rt) {
|
|
|
2347
2588
|
var import_zod8 = require("zod");
|
|
2348
2589
|
var MIN_QUEUE_DELAY = 0.5;
|
|
2349
2590
|
var MIN_REQUEST_COUNT = 1;
|
|
2591
|
+
function isRequestCountScaler(type) {
|
|
2592
|
+
return type === "REQUEST_COUNT";
|
|
2593
|
+
}
|
|
2350
2594
|
var MIN_IDLE_TIMEOUT = 1;
|
|
2351
2595
|
var MAX_IDLE_TIMEOUT = 3600;
|
|
2352
2596
|
function scalerValueError(scalerType, scalerValue) {
|
|
@@ -2439,6 +2683,12 @@ function registerEndpointTools(server, rt) {
|
|
|
2439
2683
|
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
2440
2684
|
),
|
|
2441
2685
|
gpuCount: import_zod8.z.number().optional().describe("GPUs per worker (v2)"),
|
|
2686
|
+
allowedCudaVersions: import_zod8.z.array(import_zod8.z.string()).optional().describe(
|
|
2687
|
+
'Acceptable host CUDA versions for worker placement as major.minor, e.g. ["12.8"] (v2). Exact match \u2014 see get-capacity for which versions have stock. Mutually exclusive with minCudaVersion.'
|
|
2688
|
+
),
|
|
2689
|
+
minCudaVersion: import_zod8.z.string().optional().describe(
|
|
2690
|
+
'Lowest acceptable host CUDA version as major.minor, e.g. "12.4" (v2). A bare major like "12" is rejected. Mutually exclusive with a non-empty allowedCudaVersions.'
|
|
2691
|
+
),
|
|
2442
2692
|
args: import_zod8.z.string().optional().describe("Container start command/args (v2)"),
|
|
2443
2693
|
containerDiskInGb: import_zod8.z.number().optional().describe("Container disk size in GB (v2)"),
|
|
2444
2694
|
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
@@ -2468,6 +2718,13 @@ function registerEndpointTools(server, rt) {
|
|
|
2468
2718
|
{ title: "Create endpoint", ...WRITE },
|
|
2469
2719
|
async (params) => {
|
|
2470
2720
|
const backend = backendFor("endpoints");
|
|
2721
|
+
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2722
|
+
if (hasCudaParams && backend.version !== "v2") {
|
|
2723
|
+
return jsonReply2({
|
|
2724
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2725
|
+
status: 501
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2471
2728
|
if (backend.version === "v2") {
|
|
2472
2729
|
if (!params.name) {
|
|
2473
2730
|
return jsonReply2({
|
|
@@ -2500,6 +2757,10 @@ function registerEndpointTools(server, rt) {
|
|
|
2500
2757
|
if (scalerError) {
|
|
2501
2758
|
return jsonReply2({ error: scalerError, status: 400 });
|
|
2502
2759
|
}
|
|
2760
|
+
if (hasCudaParams) {
|
|
2761
|
+
const cudaError = cudaConstraintError(params);
|
|
2762
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2763
|
+
}
|
|
2503
2764
|
const body = backend.mapCreate(params);
|
|
2504
2765
|
const result2 = await callRestUrl(
|
|
2505
2766
|
`${backend.base}${backend.list}`,
|
|
@@ -2537,7 +2798,7 @@ function registerEndpointTools(server, rt) {
|
|
|
2537
2798
|
);
|
|
2538
2799
|
server.tool(
|
|
2539
2800
|
"update-endpoint",
|
|
2540
|
-
"Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change. An endpoint's request routing (queue vs load balancer) is fixed at creation and cannot be changed here \u2014 recreate the endpoint instead.",
|
|
2801
|
+
"Update a Serverless endpoint's config. On v2 you can change image/disk/env/ports/registry/workers/scaling/networkVolumes/timeout/flashboot, plus the GPU selection (gpuPoolIds/gpuCount) and its CUDA constraints (allowedCudaVersions/minCudaVersion) \u2014 CUDA-only or count-only changes do not require resending gpuPoolIds; on v1, scaling fields (worker min/max, idle timeout, scaler type/value, name). Only provided fields change. An endpoint's request routing (queue vs load balancer) is fixed at creation and cannot be changed here \u2014 recreate the endpoint instead. Note: passing gpuPoolIds replaces the GPU selection wholesale, which clears any GPU-type exclusions set elsewhere (console or set-endpoint-gpus) \u2014 the reply carries a _warning when that happens.",
|
|
2541
2802
|
{
|
|
2542
2803
|
endpointId: import_zod8.z.string().describe("ID of the endpoint to update"),
|
|
2543
2804
|
name: import_zod8.z.string().optional().describe("New name for the endpoint"),
|
|
@@ -2556,6 +2817,12 @@ function registerEndpointTools(server, rt) {
|
|
|
2556
2817
|
imageName: import_zod8.z.string().optional().describe("New Docker image (v2)"),
|
|
2557
2818
|
gpuPoolIds: import_zod8.z.array(import_zod8.z.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
2558
2819
|
gpuCount: import_zod8.z.number().optional().describe("New GPUs per worker (v2)"),
|
|
2820
|
+
allowedCudaVersions: import_zod8.z.array(import_zod8.z.string()).optional().describe(
|
|
2821
|
+
"New acceptable host CUDA versions as major.minor (v2). Changing only this does not require resending gpuPoolIds. An explicit [] clears the constraint. Mutually exclusive with minCudaVersion."
|
|
2822
|
+
),
|
|
2823
|
+
minCudaVersion: import_zod8.z.string().optional().describe(
|
|
2824
|
+
'New lowest acceptable host CUDA version as major.minor (v2). An empty string clears the floor; a bare major like "12" is rejected. Mutually exclusive with a non-empty allowedCudaVersions.'
|
|
2825
|
+
),
|
|
2559
2826
|
args: import_zod8.z.string().optional().describe("New container args (v2)"),
|
|
2560
2827
|
containerDiskInGb: import_zod8.z.number().optional().describe("New container disk size in GB (v2)"),
|
|
2561
2828
|
ports: import_zod8.z.array(import_zod8.z.string()).optional().describe("New ports (v2)"),
|
|
@@ -2570,14 +2837,41 @@ function registerEndpointTools(server, rt) {
|
|
|
2570
2837
|
const { endpointId, ...updateParams } = params;
|
|
2571
2838
|
const backend = backendFor("endpoints");
|
|
2572
2839
|
const url = `${backend.base}${backend.get(endpointId)}`;
|
|
2840
|
+
const hasCudaParams = updateParams.allowedCudaVersions !== void 0 || updateParams.minCudaVersion !== void 0;
|
|
2573
2841
|
if (backend.version !== "v2") {
|
|
2842
|
+
if (hasCudaParams) {
|
|
2843
|
+
return jsonReply2({
|
|
2844
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2845
|
+
status: 501
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2574
2848
|
const body2 = backend.mapUpdate(updateParams);
|
|
2575
2849
|
return jsonReply2(await callRestUrl(url, "PATCH", body2));
|
|
2576
2850
|
}
|
|
2851
|
+
if (hasCudaParams) {
|
|
2852
|
+
const cudaError = cudaConstraintError(updateParams, {
|
|
2853
|
+
allowClear: true
|
|
2854
|
+
});
|
|
2855
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2856
|
+
}
|
|
2857
|
+
if (updateParams.gpuPoolIds !== void 0 && !updateParams.gpuPoolIds.length) {
|
|
2858
|
+
return jsonReply2({
|
|
2859
|
+
error: "gpuPoolIds cannot be empty \u2014 omit it to keep the current GPU selection.",
|
|
2860
|
+
status: 400
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2577
2863
|
let scalerType = updateParams.scalerType;
|
|
2578
|
-
|
|
2864
|
+
const needScalerRead = scalerType === void 0 && updateParams.scalerValue !== void 0;
|
|
2865
|
+
const needExclusionRead = (updateParams.gpuPoolIds?.length ?? 0) > 0;
|
|
2866
|
+
let clearedExclusions = [];
|
|
2867
|
+
if (needScalerRead || needExclusionRead) {
|
|
2579
2868
|
const current = await callRestUrl(url);
|
|
2580
|
-
|
|
2869
|
+
if (needScalerRead) {
|
|
2870
|
+
scalerType = isRequestCountScaler(current?.scaling?.type) ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
2871
|
+
}
|
|
2872
|
+
if (needExclusionRead && current?.gpu?.excludedTypes?.length) {
|
|
2873
|
+
clearedExclusions = current.gpu.excludedTypes;
|
|
2874
|
+
}
|
|
2581
2875
|
}
|
|
2582
2876
|
const scalerError = scalerValueError(
|
|
2583
2877
|
scalerType,
|
|
@@ -2590,7 +2884,14 @@ function registerEndpointTools(server, rt) {
|
|
|
2590
2884
|
...updateParams,
|
|
2591
2885
|
scalerType
|
|
2592
2886
|
});
|
|
2593
|
-
|
|
2887
|
+
const result = await callRestUrl(url, "PATCH", body);
|
|
2888
|
+
if (clearedExclusions.length) {
|
|
2889
|
+
return jsonReply2({
|
|
2890
|
+
...result,
|
|
2891
|
+
_warning: `Supplying gpuPoolIds replaced the GPU selection wholesale, clearing ${clearedExclusions.length} GPU-type exclusion(s) previously set on this endpoint: ${clearedExclusions.join(", ")}. Re-apply them with set-endpoint-gpus if still wanted.`
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
return jsonReply2(result);
|
|
2594
2895
|
}
|
|
2595
2896
|
);
|
|
2596
2897
|
server.tool(
|
|
@@ -2690,8 +2991,108 @@ var import_zod9 = require("zod");
|
|
|
2690
2991
|
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2691
2992
|
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2692
2993
|
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
2994
|
+
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
2995
|
+
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
2996
|
+
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
2997
|
+
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
2998
|
+
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
2999
|
+
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
3000
|
+
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
3001
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
3002
|
+
"COMPLETED",
|
|
3003
|
+
"FAILED",
|
|
3004
|
+
"CANCELLED",
|
|
3005
|
+
"TIMED_OUT"
|
|
3006
|
+
]);
|
|
3007
|
+
function isUnfinishedJobReply(result) {
|
|
3008
|
+
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
3009
|
+
}
|
|
3010
|
+
function formatBudget(ms) {
|
|
3011
|
+
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
3012
|
+
}
|
|
3013
|
+
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
3014
|
+
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
3015
|
+
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
3016
|
+
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
3017
|
+
return Math.max(
|
|
3018
|
+
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
3019
|
+
MIN_STREAM_POLL_TIMEOUT_MS
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
function sleep(ms) {
|
|
3023
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3024
|
+
}
|
|
3025
|
+
function streamPollPlan(hosted) {
|
|
3026
|
+
return hosted ? {
|
|
3027
|
+
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
3028
|
+
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
3029
|
+
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
3030
|
+
} : {
|
|
3031
|
+
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
3032
|
+
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3033
|
+
query: ""
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
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).";
|
|
3037
|
+
function budgetExhaustedNote(budgetMs, lastError) {
|
|
3038
|
+
return {
|
|
3039
|
+
pollingTimedOut: true,
|
|
3040
|
+
note: `Polling stopped after ${formatBudget(
|
|
3041
|
+
budgetMs
|
|
3042
|
+
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
3043
|
+
// Surface the trailing error (if any) instead of discarding it — the last
|
|
3044
|
+
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
3045
|
+
// succeeded. Cleared on success, so this is never a stale error from
|
|
3046
|
+
// minutes of healthy streaming ago.
|
|
3047
|
+
...lastError ? { lastError } : {}
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
function errorCapNote(lastError) {
|
|
3051
|
+
return {
|
|
3052
|
+
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
3053
|
+
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
3056
|
+
async function collectJobStream(deps) {
|
|
3057
|
+
const { poll, budgetMs, holdMs } = deps;
|
|
3058
|
+
const startedAt = Date.now();
|
|
3059
|
+
const elapsed = () => Date.now() - startedAt;
|
|
3060
|
+
const chunks = [];
|
|
3061
|
+
let result = {};
|
|
3062
|
+
let consecutiveErrors = 0;
|
|
3063
|
+
let lastError;
|
|
3064
|
+
while (true) {
|
|
3065
|
+
try {
|
|
3066
|
+
const reply = await poll(
|
|
3067
|
+
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
3068
|
+
);
|
|
3069
|
+
consecutiveErrors = 0;
|
|
3070
|
+
lastError = void 0;
|
|
3071
|
+
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
3072
|
+
result = reply;
|
|
3073
|
+
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
3074
|
+
} catch (error) {
|
|
3075
|
+
consecutiveErrors++;
|
|
3076
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3077
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
3078
|
+
result = { ...result, ...errorCapNote(lastError) };
|
|
3079
|
+
break;
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
if (elapsed() > budgetMs) {
|
|
3083
|
+
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
3084
|
+
break;
|
|
3085
|
+
}
|
|
3086
|
+
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
3087
|
+
}
|
|
3088
|
+
return { result, chunks };
|
|
3089
|
+
}
|
|
2693
3090
|
function registerJobTools(server, rt) {
|
|
2694
3091
|
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
3092
|
+
const hosted = rt.transport === "http";
|
|
3093
|
+
const streamBudget = formatBudget(
|
|
3094
|
+
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
3095
|
+
);
|
|
2695
3096
|
async function diagnoseQueuedJob(endpointId) {
|
|
2696
3097
|
const cached = diagnosisCache.get(endpointId);
|
|
2697
3098
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -2702,7 +3103,10 @@ function registerJobTools(server, rt) {
|
|
|
2702
3103
|
const backend = backendFor("workers");
|
|
2703
3104
|
if (backend.version !== "v2") return null;
|
|
2704
3105
|
const raw = await callRestUrl(
|
|
2705
|
-
`${backend.base}/serverless/${endpointId}/workers
|
|
3106
|
+
`${backend.base}/serverless/${endpointId}/workers`,
|
|
3107
|
+
"GET",
|
|
3108
|
+
void 0,
|
|
3109
|
+
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
2706
3110
|
);
|
|
2707
3111
|
const summary = raw?.summary;
|
|
2708
3112
|
const workers = raw?.workers ?? [];
|
|
@@ -2793,14 +3197,14 @@ function registerJobTools(server, rt) {
|
|
|
2793
3197
|
);
|
|
2794
3198
|
server.tool(
|
|
2795
3199
|
"runsync-endpoint",
|
|
2796
|
-
|
|
3200
|
+
`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)."}`,
|
|
2797
3201
|
{
|
|
2798
3202
|
endpointId: endpointIdSchema.describe(
|
|
2799
3203
|
"ID of the Serverless endpoint to run synchronously"
|
|
2800
3204
|
),
|
|
2801
3205
|
input: inputSchema,
|
|
2802
3206
|
wait: import_zod9.z.number().min(1e3).max(3e5).optional().describe(
|
|
2803
|
-
"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)."
|
|
3207
|
+
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)."
|
|
2804
3208
|
),
|
|
2805
3209
|
webhook: webhookSchema,
|
|
2806
3210
|
policy: policySchema,
|
|
@@ -2809,13 +3213,36 @@ function registerJobTools(server, rt) {
|
|
|
2809
3213
|
{ title: "Run endpoint (sync)", ...WRITE },
|
|
2810
3214
|
async (params) => {
|
|
2811
3215
|
const { endpointId, wait, ...body } = params;
|
|
2812
|
-
const
|
|
3216
|
+
const effectiveWait = hosted ? Math.min(
|
|
3217
|
+
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3218
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3219
|
+
) : wait;
|
|
3220
|
+
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
3221
|
+
const path = `/runsync${waitQuery}`;
|
|
2813
3222
|
const result = await serverlessRequest(
|
|
2814
3223
|
endpointId,
|
|
2815
3224
|
path,
|
|
2816
3225
|
"POST",
|
|
2817
|
-
body
|
|
3226
|
+
body,
|
|
3227
|
+
{
|
|
3228
|
+
// From the wait actually sent — on http the clamped 45s, not the
|
|
3229
|
+
// 300s the caller may have asked for.
|
|
3230
|
+
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
3231
|
+
}
|
|
2818
3232
|
);
|
|
3233
|
+
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
3234
|
+
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
3235
|
+
return jsonReply2({
|
|
3236
|
+
...result,
|
|
3237
|
+
waitClamped: {
|
|
3238
|
+
requestedMs: requestedWait,
|
|
3239
|
+
effectiveMs: effectiveWait,
|
|
3240
|
+
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
3241
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3242
|
+
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
3243
|
+
}
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
2819
3246
|
return jsonReply2(result);
|
|
2820
3247
|
}
|
|
2821
3248
|
);
|
|
@@ -2849,7 +3276,7 @@ function registerJobTools(server, rt) {
|
|
|
2849
3276
|
);
|
|
2850
3277
|
server.tool(
|
|
2851
3278
|
"stream-job",
|
|
2852
|
-
|
|
3279
|
+
`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." : ""}`,
|
|
2853
3280
|
{
|
|
2854
3281
|
endpointId: endpointIdSchema.describe(
|
|
2855
3282
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -2858,51 +3285,16 @@ function registerJobTools(server, rt) {
|
|
|
2858
3285
|
},
|
|
2859
3286
|
{ title: "Stream job", ...READ_ONLY },
|
|
2860
3287
|
async (params) => {
|
|
2861
|
-
const
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
let finalResult = {};
|
|
2872
|
-
let consecutiveErrors = 0;
|
|
2873
|
-
let lastError;
|
|
2874
|
-
const startTime = Date.now();
|
|
2875
|
-
while (true) {
|
|
2876
|
-
try {
|
|
2877
|
-
const result = await serverlessRequest(
|
|
2878
|
-
params.endpointId,
|
|
2879
|
-
`/stream/${params.jobId}`
|
|
2880
|
-
);
|
|
2881
|
-
consecutiveErrors = 0;
|
|
2882
|
-
if (Array.isArray(result.stream)) {
|
|
2883
|
-
allChunks.push(...result.stream);
|
|
2884
|
-
}
|
|
2885
|
-
finalResult = result;
|
|
2886
|
-
if (TERMINAL_STATUSES.has(result.status)) {
|
|
2887
|
-
break;
|
|
2888
|
-
}
|
|
2889
|
-
} catch (error) {
|
|
2890
|
-
consecutiveErrors++;
|
|
2891
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
2892
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2893
|
-
finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
|
|
2894
|
-
break;
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
if (Date.now() - startTime > MAX_POLL_TIME_MS) {
|
|
2898
|
-
finalResult.pollingTimedOut = true;
|
|
2899
|
-
finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
|
|
2900
|
-
if (lastError) finalResult.lastError = lastError;
|
|
2901
|
-
break;
|
|
2902
|
-
}
|
|
2903
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2904
|
-
}
|
|
2905
|
-
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
3288
|
+
const plan = streamPollPlan(hosted);
|
|
3289
|
+
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
3290
|
+
const { result, chunks } = await collectJobStream({
|
|
3291
|
+
budgetMs: plan.budgetMs,
|
|
3292
|
+
holdMs: plan.holdMs,
|
|
3293
|
+
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
3294
|
+
timeoutMs
|
|
3295
|
+
})
|
|
3296
|
+
});
|
|
3297
|
+
return jsonReply2({ ...result, stream: chunks });
|
|
2906
3298
|
}
|
|
2907
3299
|
);
|
|
2908
3300
|
server.tool(
|
|
@@ -3674,6 +4066,23 @@ async function handleMcpRequest(req, res, opts = {}) {
|
|
|
3674
4066
|
url: req.url,
|
|
3675
4067
|
hasBearerToken: !!bearerToken
|
|
3676
4068
|
});
|
|
4069
|
+
if (req.method === "GET") {
|
|
4070
|
+
res.writeHead(405, {
|
|
4071
|
+
Allow: "POST, DELETE",
|
|
4072
|
+
"Content-Type": "application/json"
|
|
4073
|
+
});
|
|
4074
|
+
res.end(
|
|
4075
|
+
JSON.stringify({
|
|
4076
|
+
jsonrpc: "2.0",
|
|
4077
|
+
error: {
|
|
4078
|
+
code: -32e3,
|
|
4079
|
+
message: "Method Not Allowed: this server does not offer a standalone SSE stream."
|
|
4080
|
+
},
|
|
4081
|
+
id: null
|
|
4082
|
+
})
|
|
4083
|
+
);
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
3677
4086
|
if (!bearerToken) {
|
|
3678
4087
|
writeUnauthorized(
|
|
3679
4088
|
req,
|