@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/stdio.mjs
CHANGED
|
@@ -31,10 +31,15 @@ function containerConfigToV2(p) {
|
|
|
31
31
|
mounts: persistentMount(p.volumeInGb, p.volumeMountPath)
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
-
function gpuConfigToV2(gpuTypeIds, gpuCount) {
|
|
34
|
+
function gpuConfigToV2(gpuTypeIds, gpuCount, allowedCudaVersions, minCudaVersion2) {
|
|
35
35
|
const id = gpuTypeIds?.[0];
|
|
36
36
|
if (id === void 0) return void 0;
|
|
37
|
-
return compact({
|
|
37
|
+
return compact({
|
|
38
|
+
id,
|
|
39
|
+
count: gpuCount ?? 1,
|
|
40
|
+
allowedCudaVersions,
|
|
41
|
+
minCudaVersion: minCudaVersion2
|
|
42
|
+
});
|
|
38
43
|
}
|
|
39
44
|
function mapPodCreateToV2(params) {
|
|
40
45
|
return compact({
|
|
@@ -55,7 +60,12 @@ function mapPodCreateToV2(params) {
|
|
|
55
60
|
// singular on v2 (`gpu.id`): gpuConfigToV2 takes the first id; the
|
|
56
61
|
// create-pod handler warns when more than one gpuTypeId was supplied.
|
|
57
62
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
58
|
-
gpu: gpuConfigToV2(
|
|
63
|
+
gpu: gpuConfigToV2(
|
|
64
|
+
params.gpuTypeIds,
|
|
65
|
+
params.gpuCount,
|
|
66
|
+
params.allowedCudaVersions,
|
|
67
|
+
params.minCudaVersion
|
|
68
|
+
)
|
|
59
69
|
});
|
|
60
70
|
}
|
|
61
71
|
function mapPodUpdateToV2(params) {
|
|
@@ -72,9 +82,15 @@ function defaultScalerType(endpointType) {
|
|
|
72
82
|
function endpointScaling(scalerType, value) {
|
|
73
83
|
return scalerType === "REQUEST_COUNT" ? { type: "REQUEST_COUNT", requestCount: value } : { type: "QUEUE_DELAY", queueDelay: value };
|
|
74
84
|
}
|
|
75
|
-
function endpointGpuConfig(
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
function endpointGpuConfig(params, mode) {
|
|
86
|
+
const gpu = compact({
|
|
87
|
+
pools: params.gpuPoolIds?.length ? params.gpuPoolIds : void 0,
|
|
88
|
+
count: params.gpuCount,
|
|
89
|
+
allowedCudaVersions: params.allowedCudaVersions,
|
|
90
|
+
minCudaVersion: params.minCudaVersion
|
|
91
|
+
});
|
|
92
|
+
if (mode === "create" && !("pools" in gpu)) return void 0;
|
|
93
|
+
return Object.keys(gpu).length ? gpu : void 0;
|
|
78
94
|
}
|
|
79
95
|
function endpointWorkers(params) {
|
|
80
96
|
const workers = compact({
|
|
@@ -84,7 +100,7 @@ function endpointWorkers(params) {
|
|
|
84
100
|
});
|
|
85
101
|
return Object.keys(workers).length ? workers : void 0;
|
|
86
102
|
}
|
|
87
|
-
function endpointCommonToV2(params) {
|
|
103
|
+
function endpointCommonToV2(params, mode) {
|
|
88
104
|
return compact({
|
|
89
105
|
name: params.name,
|
|
90
106
|
image: params.imageName,
|
|
@@ -93,7 +109,7 @@ function endpointCommonToV2(params) {
|
|
|
93
109
|
ports: params.ports,
|
|
94
110
|
env: params.env,
|
|
95
111
|
registry: params.containerRegistryAuthId,
|
|
96
|
-
gpu: endpointGpuConfig(params
|
|
112
|
+
gpu: endpointGpuConfig(params, mode),
|
|
97
113
|
workers: endpointWorkers(params),
|
|
98
114
|
dataCenterIds: params.dataCenterIds?.length ? params.dataCenterIds : void 0,
|
|
99
115
|
networkVolumes: params.networkVolumeIds?.length ? params.networkVolumeIds : void 0,
|
|
@@ -104,7 +120,7 @@ function endpointCommonToV2(params) {
|
|
|
104
120
|
function mapEndpointCreateToV2(params) {
|
|
105
121
|
const endpointType = params.endpointType ?? DEFAULT_ENDPOINT_TYPE;
|
|
106
122
|
return compact({
|
|
107
|
-
...endpointCommonToV2(params),
|
|
123
|
+
...endpointCommonToV2(params, "create"),
|
|
108
124
|
type: endpointType,
|
|
109
125
|
scaling: endpointScaling(
|
|
110
126
|
params.scalerType ?? defaultScalerType(endpointType),
|
|
@@ -114,13 +130,28 @@ function mapEndpointCreateToV2(params) {
|
|
|
114
130
|
}
|
|
115
131
|
function mapEndpointUpdateToV2(params) {
|
|
116
132
|
return compact({
|
|
117
|
-
...endpointCommonToV2(params),
|
|
133
|
+
...endpointCommonToV2(params, "update"),
|
|
118
134
|
scaling: params.scalerType ? endpointScaling(
|
|
119
135
|
params.scalerType,
|
|
120
136
|
params.scalerValue ?? DEFAULT_SCALER_VALUE
|
|
121
137
|
) : void 0
|
|
122
138
|
});
|
|
123
139
|
}
|
|
140
|
+
var CUDA_MAJOR_MINOR = /^\d+\.\d+$/;
|
|
141
|
+
function cudaConstraintError(params, opts = {}) {
|
|
142
|
+
const { allowedCudaVersions, minCudaVersion: minCudaVersion2 } = params;
|
|
143
|
+
if (allowedCudaVersions?.length && minCudaVersion2) {
|
|
144
|
+
return "allowedCudaVersions and minCudaVersion are mutually exclusive \u2014 pass either an exact set or an open-ended floor, not both.";
|
|
145
|
+
}
|
|
146
|
+
if (minCudaVersion2 !== void 0 && !CUDA_MAJOR_MINOR.test(minCudaVersion2) && !(opts.allowClear && minCudaVersion2 === "")) {
|
|
147
|
+
return `minCudaVersion must be major.minor (e.g. "12.0", not "12")${opts.allowClear ? "; an empty string clears the floor" : ""}.`;
|
|
148
|
+
}
|
|
149
|
+
const bad = allowedCudaVersions?.find((v) => !CUDA_MAJOR_MINOR.test(v));
|
|
150
|
+
if (bad !== void 0) {
|
|
151
|
+
return `allowedCudaVersions entries must be major.minor (e.g. "12.8"); got "${bad}". Discover valid values via get-capacity or list-gpu-types.`;
|
|
152
|
+
}
|
|
153
|
+
return void 0;
|
|
154
|
+
}
|
|
124
155
|
function mapNetworkVolumeCreateToV2(params) {
|
|
125
156
|
return compact({
|
|
126
157
|
name: params.name,
|
|
@@ -161,6 +192,42 @@ function podBodyFromTemplate(template) {
|
|
|
161
192
|
}
|
|
162
193
|
return out;
|
|
163
194
|
}
|
|
195
|
+
var SSH_PUBLIC_KEY_LINE = /^(ssh-[a-z0-9-]+|ecdsa-[a-z0-9-]+|sk-[a-z0-9@.-]+)\s+\S+/i;
|
|
196
|
+
var PRIVATE_KEY_MARKER = /private[-\s]?key|PuTTY-User-Key-File/i;
|
|
197
|
+
function normalizeSshPublicKey(input) {
|
|
198
|
+
if (PRIVATE_KEY_MARKER.test(input))
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
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").'
|
|
202
|
+
};
|
|
203
|
+
const lines = input.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
204
|
+
if (lines.length === 0)
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
error: "sshPublicKey is empty. Pass the contents of your ~/.ssh/<key>.pub file, or omit the parameter entirely to create the pod without SSH."
|
|
208
|
+
};
|
|
209
|
+
if (lines.some((line) => !SSH_PUBLIC_KEY_LINE.test(line)))
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
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.'
|
|
213
|
+
};
|
|
214
|
+
return { ok: true, key: lines.join("\n") };
|
|
215
|
+
}
|
|
216
|
+
function applySshPublicKey(body, sshPublicKey) {
|
|
217
|
+
const ports = Array.isArray(body.ports) ? body.ports : [];
|
|
218
|
+
const hasSshPort = ports.some((p) => /^\s*22\/tcp\s*$/i.test(String(p)));
|
|
219
|
+
const env = { ...body.env ?? {} };
|
|
220
|
+
const key = sshPublicKey.trim();
|
|
221
|
+
if (!env.PUBLIC_KEY) env.PUBLIC_KEY = key;
|
|
222
|
+
else if (!env.PUBLIC_KEY.includes(key))
|
|
223
|
+
env.PUBLIC_KEY = `${env.PUBLIC_KEY}
|
|
224
|
+
${key}`;
|
|
225
|
+
return {
|
|
226
|
+
...body,
|
|
227
|
+
ports: hasSshPort ? ports : [...ports, "22/tcp"],
|
|
228
|
+
env
|
|
229
|
+
};
|
|
230
|
+
}
|
|
164
231
|
function mapTemplateUpdateToV2(params) {
|
|
165
232
|
return {
|
|
166
233
|
...containerConfigToV2(params),
|
|
@@ -426,7 +493,7 @@ function resolveBackend(opts) {
|
|
|
426
493
|
|
|
427
494
|
// src/server.ts
|
|
428
495
|
var SERVER_NAME = "Runpod API Server";
|
|
429
|
-
var SERVER_VERSION = true ? "3.
|
|
496
|
+
var SERVER_VERSION = true ? "3.4.0" : "dev";
|
|
430
497
|
var SERVER_INSTRUCTIONS = `These tools cover common Runpod operations, but they are curated projections: the underlying APIs expose more fields and parameters than the tools surface.
|
|
431
498
|
|
|
432
499
|
When you need data or capability beyond these tools, consult the full API contracts directly. The REST v2 API lives at https://api.runpod.io/v2 \u2014 the same host these tools call \u2014 and is machine-readable at https://api.runpod.io/v2/openapi.json (fetch and search it to enumerate every path, parameter, and response schema). REST v1 at https://rest.runpod.io/v1 is deprecated; prefer v2. The REST version these tools are actually configured for is reported in the serverInfo.version string of the initialize response, e.g. "[RUNPOD_REST_VERSION=v2]". The GraphQL API at https://api.runpod.io/graphql may expose data the REST API does not yet cover; its schema is documented at https://graphql-spec.runpod.io/, and introspection is disabled in production, so use that reference rather than __schema queries. Both APIs take Bearer auth with a Runpod API key: use the key this client is already configured with \u2014 do not search the environment or filesystem for a credential.`;
|
|
@@ -601,6 +668,28 @@ var HttpError = class extends Error {
|
|
|
601
668
|
this.body = body;
|
|
602
669
|
}
|
|
603
670
|
};
|
|
671
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
672
|
+
var MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
673
|
+
var RequestTimeoutError = class extends Error {
|
|
674
|
+
constructor(prefix, timeoutMs, method) {
|
|
675
|
+
const write = method !== "GET" && method !== "HEAD";
|
|
676
|
+
super(
|
|
677
|
+
`${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."}`
|
|
678
|
+
);
|
|
679
|
+
this.name = "RequestTimeoutError";
|
|
680
|
+
this.timeoutMs = timeoutMs;
|
|
681
|
+
this.method = method;
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
async function withRequestTimeout(errorPrefix, timeoutMs, method, run) {
|
|
685
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
686
|
+
try {
|
|
687
|
+
return await run(signal);
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if (error instanceof HttpError || !signal.aborted) throw error;
|
|
690
|
+
throw new RequestTimeoutError(errorPrefix, timeoutMs, method);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
604
693
|
function isJsonContentType(contentType) {
|
|
605
694
|
if (!contentType) return false;
|
|
606
695
|
const ct = contentType.toLowerCase();
|
|
@@ -616,28 +705,49 @@ function buildRequestHeaders(apiKey, tracking) {
|
|
|
616
705
|
...tracking()
|
|
617
706
|
};
|
|
618
707
|
}
|
|
708
|
+
function resolveCeiling(ceiling) {
|
|
709
|
+
if (ceiling === void 0) return Infinity;
|
|
710
|
+
if (typeof ceiling !== "function") return ceiling;
|
|
711
|
+
return Math.max(ceiling(), MIN_REQUEST_TIMEOUT_MS);
|
|
712
|
+
}
|
|
713
|
+
function clampTimeout(requestedMs, ceiling, defaultMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
714
|
+
return Math.min(requestedMs ?? defaultMs, resolveCeiling(ceiling));
|
|
715
|
+
}
|
|
619
716
|
function createHttpClient(deps) {
|
|
620
|
-
|
|
621
|
-
|
|
717
|
+
const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
718
|
+
return async function request(url, method = "GET", body, options) {
|
|
719
|
+
const timeoutMs = clampTimeout(
|
|
720
|
+
options?.timeoutMs,
|
|
721
|
+
deps.maxTimeoutMs,
|
|
722
|
+
defaultTimeoutMs
|
|
723
|
+
);
|
|
724
|
+
return withRequestTimeout(
|
|
725
|
+
deps.errorPrefix,
|
|
726
|
+
timeoutMs,
|
|
622
727
|
method,
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
response.
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
728
|
+
async (signal) => {
|
|
729
|
+
const init = {
|
|
730
|
+
method,
|
|
731
|
+
headers: buildRequestHeaders(deps.apiKey, deps.tracking),
|
|
732
|
+
signal
|
|
733
|
+
};
|
|
734
|
+
if (body && methodSendsBody(method)) {
|
|
735
|
+
init.body = JSON.stringify(body);
|
|
736
|
+
}
|
|
737
|
+
const response = await deps.fetch(url, init);
|
|
738
|
+
if (!response.ok) {
|
|
739
|
+
const hint = response.status === 429 ? rateLimitHint(
|
|
740
|
+
response.headers.get("ratelimit"),
|
|
741
|
+
response.headers.get("retry-after")
|
|
742
|
+
) : response.status === 401 ? EXPIRED_CREDENTIAL_HINT : void 0;
|
|
743
|
+
const text = await response.text().catch(
|
|
744
|
+
() => "<body unavailable: the connection closed while reading it>"
|
|
745
|
+
);
|
|
746
|
+
throw new HttpError(deps.errorPrefix, response.status, text, hint);
|
|
747
|
+
}
|
|
748
|
+
return isJsonContentType(response.headers.get("content-type")) ? response.json() : { success: true, status: response.status };
|
|
749
|
+
}
|
|
750
|
+
);
|
|
641
751
|
};
|
|
642
752
|
}
|
|
643
753
|
|
|
@@ -764,7 +874,7 @@ async function streamLogsReply(rt, tool, params) {
|
|
|
764
874
|
}
|
|
765
875
|
|
|
766
876
|
// src/tools/runtime.ts
|
|
767
|
-
var MCP_SERVER_VERSION = true ? "3.
|
|
877
|
+
var MCP_SERVER_VERSION = true ? "3.4.0" : "dev";
|
|
768
878
|
var SESSION_ID = randomUUID();
|
|
769
879
|
function trackingHeaders(server, ctx) {
|
|
770
880
|
const info = server.server.getClientVersion();
|
|
@@ -779,6 +889,11 @@ function trackingHeaders(server, ctx) {
|
|
|
779
889
|
sessionId: SESSION_ID
|
|
780
890
|
});
|
|
781
891
|
}
|
|
892
|
+
var HTTP_TRANSPORT_BUDGET_MS = 5e4;
|
|
893
|
+
function invocationBudget(totalMs) {
|
|
894
|
+
const startedAt = Date.now();
|
|
895
|
+
return () => totalMs - (Date.now() - startedAt);
|
|
896
|
+
}
|
|
782
897
|
var defaultFetch = fetch;
|
|
783
898
|
async function withApiErrorLog(label, call) {
|
|
784
899
|
try {
|
|
@@ -802,6 +917,23 @@ var DESTRUCTIVE = {
|
|
|
802
917
|
openWorldHint: true
|
|
803
918
|
};
|
|
804
919
|
async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
920
|
+
return withRequestTimeout(
|
|
921
|
+
"Runpod GraphQL Error",
|
|
922
|
+
clampTimeout(options?.timeoutMs, options?.maxTimeoutMs),
|
|
923
|
+
// GraphQL is POST on the wire whatever it carries, but the retry advice keys
|
|
924
|
+
// off the method, and telling a caller its list-gpu-types "may have
|
|
925
|
+
// SUCCEEDED upstream — check with the matching list-/get- tool first" points
|
|
926
|
+
// it back at the tool that just failed. The operation keyword is what
|
|
927
|
+
// actually says whether anything could have been written.
|
|
928
|
+
isGraphqlMutation(query) ? "POST" : "GET",
|
|
929
|
+
(signal) => runGraphql(query, tracking, fetchImpl, url, signal, options)
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
function isGraphqlMutation(query) {
|
|
933
|
+
const firstMeaningful = query.split("\n").map((line) => line.trim()).find((line) => line.length > 0 && !line.startsWith("#"));
|
|
934
|
+
return /^mutation\b/.test(firstMeaningful ?? "");
|
|
935
|
+
}
|
|
936
|
+
async function runGraphql(query, tracking, fetchImpl, url, signal, options) {
|
|
805
937
|
const response = await fetchImpl(url, {
|
|
806
938
|
method: "POST",
|
|
807
939
|
headers: {
|
|
@@ -812,7 +944,8 @@ async function graphqlRequest(query, tracking, fetchImpl, url, options) {
|
|
|
812
944
|
body: JSON.stringify({
|
|
813
945
|
query,
|
|
814
946
|
...options?.variables ? { variables: options.variables } : {}
|
|
815
|
-
})
|
|
947
|
+
}),
|
|
948
|
+
signal
|
|
816
949
|
});
|
|
817
950
|
if (!response.ok) {
|
|
818
951
|
const bodyText = await response.text();
|
|
@@ -868,39 +1001,53 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
868
1001
|
}
|
|
869
1002
|
const rawFetch = deps.fetch ?? defaultFetch;
|
|
870
1003
|
const httpFetch = observeUnauthorized(rawFetch);
|
|
1004
|
+
const remainingHttpBudgetMs = invocationBudget(HTTP_TRANSPORT_BUDGET_MS);
|
|
1005
|
+
const timeouts = {
|
|
1006
|
+
defaultTimeoutMs: deps.defaultTimeoutMs,
|
|
1007
|
+
maxTimeoutMs: deps.maxTimeoutMs ?? (ctx.transport === "http" ? remainingHttpBudgetMs : void 0)
|
|
1008
|
+
};
|
|
871
1009
|
const v1Client = createHttpClient({
|
|
872
1010
|
apiKey: ctx.apiKey,
|
|
873
1011
|
fetch: httpFetch,
|
|
874
1012
|
tracking,
|
|
875
|
-
errorPrefix: "Runpod API Error"
|
|
1013
|
+
errorPrefix: "Runpod API Error",
|
|
1014
|
+
...timeouts
|
|
876
1015
|
});
|
|
877
|
-
const runpodRequest = (endpoint, method = "GET", body) => withApiErrorLog(
|
|
1016
|
+
const runpodRequest = (endpoint, method = "GET", body, options) => withApiErrorLog(
|
|
878
1017
|
"Error calling Runpod API:",
|
|
879
|
-
() => v1Client(
|
|
1018
|
+
() => v1Client(
|
|
1019
|
+
`${restV1Base(process.env)}${endpoint}`,
|
|
1020
|
+
method,
|
|
1021
|
+
body,
|
|
1022
|
+
options
|
|
1023
|
+
)
|
|
880
1024
|
);
|
|
881
1025
|
const serverlessClient = createHttpClient({
|
|
882
1026
|
apiKey: ctx.apiKey,
|
|
883
1027
|
fetch: httpFetch,
|
|
884
1028
|
tracking,
|
|
885
|
-
errorPrefix: "Runpod Serverless API Error"
|
|
1029
|
+
errorPrefix: "Runpod Serverless API Error",
|
|
1030
|
+
...timeouts
|
|
886
1031
|
});
|
|
887
|
-
const serverlessRequest = (endpointId, path, method = "GET", body) => withApiErrorLog(
|
|
1032
|
+
const serverlessRequest = (endpointId, path, method = "GET", body, options) => withApiErrorLog(
|
|
888
1033
|
"Error calling Runpod Serverless API:",
|
|
889
1034
|
() => serverlessClient(
|
|
890
1035
|
`${serverlessBase(process.env)}/${endpointId}${path}`,
|
|
891
1036
|
method,
|
|
892
|
-
body
|
|
1037
|
+
body,
|
|
1038
|
+
options
|
|
893
1039
|
)
|
|
894
1040
|
);
|
|
895
1041
|
const restClient = createHttpClient({
|
|
896
1042
|
apiKey: ctx.apiKey,
|
|
897
1043
|
fetch: httpFetch,
|
|
898
1044
|
tracking,
|
|
899
|
-
errorPrefix: "Runpod API Error"
|
|
1045
|
+
errorPrefix: "Runpod API Error",
|
|
1046
|
+
...timeouts
|
|
900
1047
|
});
|
|
901
|
-
const callRestUrl = (url, method = "GET", body) => withApiErrorLog(
|
|
1048
|
+
const callRestUrl = (url, method = "GET", body, options) => withApiErrorLog(
|
|
902
1049
|
"Error calling Runpod API:",
|
|
903
|
-
() => restClient(url, method, body)
|
|
1050
|
+
() => restClient(url, method, body, options)
|
|
904
1051
|
);
|
|
905
1052
|
const sseFetch = observeUnauthorized(
|
|
906
1053
|
deps.sseFetch ?? fetch
|
|
@@ -933,12 +1080,18 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
933
1080
|
};
|
|
934
1081
|
return {
|
|
935
1082
|
jsonReply,
|
|
1083
|
+
// No GraphQL caller overrides a deadline, but both are still capped by the
|
|
1084
|
+
// shared budget: deploy-hub-repo spends one of each back to back.
|
|
936
1085
|
graphql: (query) => graphqlRequest(
|
|
937
1086
|
query,
|
|
938
1087
|
tracking,
|
|
939
1088
|
// rawFetch, not httpFetch: this call carries no credential (see above).
|
|
940
1089
|
rawFetch,
|
|
941
|
-
publicGraphqlBase(process.env)
|
|
1090
|
+
publicGraphqlBase(process.env),
|
|
1091
|
+
{
|
|
1092
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1093
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1094
|
+
}
|
|
942
1095
|
),
|
|
943
1096
|
graphqlAuthed: (query, variables) => graphqlRequest(
|
|
944
1097
|
query,
|
|
@@ -946,7 +1099,12 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
946
1099
|
httpFetch,
|
|
947
1100
|
// NOT publicGraphqlBase: this call carries the caller's API key.
|
|
948
1101
|
authedGraphqlBase(process.env),
|
|
949
|
-
{
|
|
1102
|
+
{
|
|
1103
|
+
variables,
|
|
1104
|
+
apiKey: ctx.apiKey,
|
|
1105
|
+
timeoutMs: deps.defaultTimeoutMs,
|
|
1106
|
+
maxTimeoutMs: timeouts.maxTimeoutMs
|
|
1107
|
+
}
|
|
950
1108
|
),
|
|
951
1109
|
runpodRequest,
|
|
952
1110
|
serverlessRequest,
|
|
@@ -954,7 +1112,8 @@ function createToolRuntime(server, ctx, deps = {}) {
|
|
|
954
1112
|
backendFor,
|
|
955
1113
|
streamSse,
|
|
956
1114
|
podAction,
|
|
957
|
-
env: process.env
|
|
1115
|
+
env: process.env,
|
|
1116
|
+
transport: ctx.transport
|
|
958
1117
|
};
|
|
959
1118
|
}
|
|
960
1119
|
|
|
@@ -1023,6 +1182,7 @@ function capListResult(result, options, extra) {
|
|
|
1023
1182
|
}
|
|
1024
1183
|
|
|
1025
1184
|
// src/tools/catalog.ts
|
|
1185
|
+
var GPU_PRODUCTS = /* @__PURE__ */ new Set(["POD", "CLUSTER", "SERVERLESS"]);
|
|
1026
1186
|
function registerCatalogTools(server, rt) {
|
|
1027
1187
|
const { jsonReply: jsonReply2, graphql, callRestUrl, backendFor } = rt;
|
|
1028
1188
|
server.tool(
|
|
@@ -1041,15 +1201,50 @@ function registerCatalogTools(server, rt) {
|
|
|
1041
1201
|
),
|
|
1042
1202
|
includeAvailability: z3.boolean().optional().describe(
|
|
1043
1203
|
"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."
|
|
1204
|
+
),
|
|
1205
|
+
product: z3.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1206
|
+
"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."
|
|
1207
|
+
),
|
|
1208
|
+
minCudaVersion: z3.string().optional().describe(
|
|
1209
|
+
'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).'
|
|
1044
1210
|
)
|
|
1045
1211
|
},
|
|
1046
1212
|
{ title: "List GPU types", ...READ_ONLY },
|
|
1047
1213
|
async (params) => {
|
|
1048
1214
|
const backend = backendFor("gpus");
|
|
1215
|
+
if (params.minCudaVersion !== void 0 && backend.version !== "v2") {
|
|
1216
|
+
return jsonReply2({
|
|
1217
|
+
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.",
|
|
1218
|
+
status: 501
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1049
1221
|
if (backend.version === "v2") {
|
|
1050
1222
|
const wantAvailability = params.includeAvailability !== false;
|
|
1223
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1224
|
+
if (params.minCudaVersion !== void 0) {
|
|
1225
|
+
if (!wantAvailability) {
|
|
1226
|
+
return jsonReply2({
|
|
1227
|
+
error: "minCudaVersion filters the availability lookup, so it requires includeAvailability (the default). Drop includeAvailability:false or the filter.",
|
|
1228
|
+
status: 400
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
if (!/^\d+(\.\d+)?$/.test(params.minCudaVersion)) {
|
|
1232
|
+
return jsonReply2({
|
|
1233
|
+
error: 'minCudaVersion must be an integer major ("12") or major.minor ("12.1").',
|
|
1234
|
+
status: 400
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const query = new URLSearchParams();
|
|
1239
|
+
if (wantAvailability) {
|
|
1240
|
+
query.set("include", "AVAILABILITY");
|
|
1241
|
+
query.set("product", product);
|
|
1242
|
+
if (params.minCudaVersion !== void 0) {
|
|
1243
|
+
query.set("minCudaVersion", params.minCudaVersion);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1051
1246
|
const raw = await callRestUrl(
|
|
1052
|
-
`${backend.base}${backend.list}${
|
|
1247
|
+
`${backend.base}${backend.list}${query.size ? `?${query}` : ""}`
|
|
1053
1248
|
);
|
|
1054
1249
|
let gpus = backend.unwrap(raw);
|
|
1055
1250
|
gpus = gpus.filter((g) => g.id !== "unknown");
|
|
@@ -1222,6 +1417,9 @@ function registerCatalogTools(server, rt) {
|
|
|
1222
1417
|
gpuTypeId: z3.string().describe("ID of the GPU type to retrieve"),
|
|
1223
1418
|
includeAvailability: z3.boolean().optional().describe(
|
|
1224
1419
|
"Include realtime per-datacenter availability (HIGH/MEDIUM/LOW/NONE). Default true."
|
|
1420
|
+
),
|
|
1421
|
+
product: z3.enum(["POD", "CLUSTER", "SERVERLESS"]).optional().describe(
|
|
1422
|
+
"Product context for the availability lookup \u2014 stock differs by product. Default POD. Use SERVERLESS when picking a GPU for an endpoint, CLUSTER for Instant Clusters. Ignored when includeAvailability is false."
|
|
1225
1423
|
)
|
|
1226
1424
|
},
|
|
1227
1425
|
{ title: "Get GPU type", ...READ_ONLY },
|
|
@@ -1234,7 +1432,8 @@ function registerCatalogTools(server, rt) {
|
|
|
1234
1432
|
});
|
|
1235
1433
|
}
|
|
1236
1434
|
const path = backend.get(encodeURIComponent(params.gpuTypeId));
|
|
1237
|
-
const
|
|
1435
|
+
const product = GPU_PRODUCTS.has(String(params.product)) ? String(params.product) : "POD";
|
|
1436
|
+
const query = params.includeAvailability === false ? "" : `?include=AVAILABILITY&product=${product}`;
|
|
1238
1437
|
const result = await callRestUrl(`${backend.base}${path}${query}`);
|
|
1239
1438
|
return jsonReply2(result);
|
|
1240
1439
|
}
|
|
@@ -2114,7 +2313,7 @@ function registerPodTools(server, rt) {
|
|
|
2114
2313
|
);
|
|
2115
2314
|
server.tool(
|
|
2116
2315
|
"create-pod",
|
|
2117
|
-
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2316
|
+
'Create a new GPU/CPU pod on Runpod. Pass gpuTypeIds for a GPU pod, or computeType:"CPU" for a CPU pod (CPU pods are served by the v1 API for now). Pass either imageName or templateId (templateId deploys from an existing template \u2014 its name, image, start command, ports, env, disk, volume, and registry credential are used as defaults, and any field you also pass explicitly replaces the template value for that field, e.g. passing env replaces the whole env set rather than merging; pass containerRegistryAuthId to override or supply a private-image credential, or an empty string to deploy with none). For full SSH access (including SCP/SFTP/rsync), pass sshPublicKey \u2014 it sets the PUBLIC_KEY env var and exposes 22/tcp, which official Runpod images turn into a running sshd with the key authorized. If the user specifies neither an image nor a template, recommend the "Runpod Pytorch 2.8.0" image (runpod/pytorch:1.0.2-cu1281-torch280-ubuntu2404) as the default \u2014 it has the most up-to-date CUDA and PyTorch versions.',
|
|
2118
2317
|
{
|
|
2119
2318
|
name: z7.string().optional().describe(
|
|
2120
2319
|
"Name for the pod. Required on v2 unless deploying from a templateId (the template supplies its name as the default)."
|
|
@@ -2128,20 +2327,48 @@ function registerPodTools(server, rt) {
|
|
|
2128
2327
|
),
|
|
2129
2328
|
cloudType: z7.enum(["SECURE", "COMMUNITY"]).optional().describe("SECURE or COMMUNITY cloud"),
|
|
2130
2329
|
gpuTypeIds: z7.array(z7.string()).optional().describe("List of acceptable GPU types"),
|
|
2131
|
-
|
|
2330
|
+
// v2 GpuConfig.count is `integer, minimum: 1`. Without these bounds a 0,
|
|
2331
|
+
// a negative, or a fractional count reached the wire as-is and came back
|
|
2332
|
+
// as a raw 422; reject it here with a message that names the field.
|
|
2333
|
+
gpuCount: z7.number().int().min(1).optional().describe("Number of GPUs (minimum 1)"),
|
|
2132
2334
|
containerDiskInGb: z7.number().optional().describe("Container disk size in GB"),
|
|
2133
2335
|
volumeInGb: z7.number().optional().describe("Volume size in GB"),
|
|
2134
2336
|
volumeMountPath: z7.string().optional().describe("Path to mount the volume"),
|
|
2135
2337
|
ports: z7.array(z7.string()).optional().describe("Ports to expose (e.g., '8888/http', '22/tcp')"),
|
|
2136
2338
|
env: z7.record(z7.string()).optional().describe("Environment variables"),
|
|
2339
|
+
sshPublicKey: z7.string().optional().describe(
|
|
2340
|
+
'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.'
|
|
2341
|
+
),
|
|
2137
2342
|
containerRegistryAuthId: z7.string().optional().describe(
|
|
2138
2343
|
"Container registry credential ID for pulling a private image (see list-container-registry-auths). When deploying from a templateId, this overrides the credential the template carries; pass an empty string to deploy with NO credential (clears a template\u2019s inherited registry)."
|
|
2139
2344
|
),
|
|
2140
|
-
dataCenterIds: z7.array(z7.string()).optional().describe("List of data centers")
|
|
2345
|
+
dataCenterIds: z7.array(z7.string()).optional().describe("List of data centers"),
|
|
2346
|
+
allowedCudaVersions: z7.array(z7.string()).optional().describe(
|
|
2347
|
+
'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.'
|
|
2348
|
+
),
|
|
2349
|
+
minCudaVersion: z7.string().optional().describe(
|
|
2350
|
+
'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.'
|
|
2351
|
+
)
|
|
2141
2352
|
},
|
|
2142
2353
|
{ title: "Create pod", ...WRITE },
|
|
2143
2354
|
async (params) => {
|
|
2144
2355
|
const backend = backendFor("pods");
|
|
2356
|
+
const { sshPublicKey, ...podParams } = params;
|
|
2357
|
+
let sshKey;
|
|
2358
|
+
if (sshPublicKey !== void 0) {
|
|
2359
|
+
const validated = normalizeSshPublicKey(sshPublicKey);
|
|
2360
|
+
if (!validated.ok)
|
|
2361
|
+
return jsonReply2({ error: validated.error, status: 400 });
|
|
2362
|
+
sshKey = validated.key;
|
|
2363
|
+
}
|
|
2364
|
+
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.";
|
|
2365
|
+
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2366
|
+
if (hasCudaParams && backend.version !== "v2") {
|
|
2367
|
+
return jsonReply2({
|
|
2368
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2369
|
+
status: 501
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2145
2372
|
if (params.templateId && backend.version !== "v2") {
|
|
2146
2373
|
return jsonReply2({
|
|
2147
2374
|
error: "Deploying a pod from a templateId is only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2, or pass imageName instead.",
|
|
@@ -2177,6 +2404,16 @@ function registerPodTools(server, rt) {
|
|
|
2177
2404
|
status: 400
|
|
2178
2405
|
});
|
|
2179
2406
|
}
|
|
2407
|
+
if (hasCudaParams && !wantsGpu) {
|
|
2408
|
+
return jsonReply2({
|
|
2409
|
+
error: "allowedCudaVersions/minCudaVersion apply to GPU pods only \u2014 pass gpuTypeIds (see list-gpu-types).",
|
|
2410
|
+
status: 400
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
if (hasCudaParams) {
|
|
2414
|
+
const cudaError = cudaConstraintError(params);
|
|
2415
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2416
|
+
}
|
|
2180
2417
|
if (!wantsCpu && !params.name && !params.templateId) {
|
|
2181
2418
|
return jsonReply2({
|
|
2182
2419
|
error: "Provide a name for the pod (deploying from a templateId inherits the template name).",
|
|
@@ -2191,13 +2428,15 @@ function registerPodTools(server, rt) {
|
|
|
2191
2428
|
});
|
|
2192
2429
|
}
|
|
2193
2430
|
try {
|
|
2431
|
+
const cpuBody = sshKey ? applySshPublicKey(podParams, sshKey) : podParams;
|
|
2194
2432
|
const result = await callRestUrl(
|
|
2195
2433
|
`${restV1Base(env)}/pods`,
|
|
2196
2434
|
"POST",
|
|
2197
|
-
|
|
2435
|
+
cpuBody
|
|
2198
2436
|
);
|
|
2199
2437
|
return jsonReply2({
|
|
2200
2438
|
...result,
|
|
2439
|
+
...sshKey ? { _ssh: sshNote } : {},
|
|
2201
2440
|
_servedBy: "v1",
|
|
2202
2441
|
_note: "CPU pod: created on the v1 API, because the v2 REST API does not support CPU pods yet."
|
|
2203
2442
|
});
|
|
@@ -2214,7 +2453,7 @@ function registerPodTools(server, rt) {
|
|
|
2214
2453
|
}
|
|
2215
2454
|
}
|
|
2216
2455
|
}
|
|
2217
|
-
let body = backend.mapCreate(
|
|
2456
|
+
let body = backend.mapCreate(podParams);
|
|
2218
2457
|
if (params.templateId) {
|
|
2219
2458
|
let template;
|
|
2220
2459
|
try {
|
|
@@ -2232,19 +2471,21 @@ function registerPodTools(server, rt) {
|
|
|
2232
2471
|
}
|
|
2233
2472
|
body = { ...podBodyFromTemplate(template), ...body };
|
|
2234
2473
|
}
|
|
2474
|
+
if (sshKey) body = applySshPublicKey(body, sshKey);
|
|
2235
2475
|
try {
|
|
2236
2476
|
const result = await callRestUrl(
|
|
2237
2477
|
`${backend.base}${backend.list}`,
|
|
2238
2478
|
"POST",
|
|
2239
2479
|
body
|
|
2240
2480
|
);
|
|
2481
|
+
const extras = {};
|
|
2482
|
+
if (sshKey) extras._ssh = sshNote;
|
|
2241
2483
|
if (backend.version === "v2" && (params.gpuTypeIds?.length ?? 0) > 1) {
|
|
2242
|
-
|
|
2243
|
-
...result,
|
|
2244
|
-
_warning: `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`
|
|
2245
|
-
});
|
|
2484
|
+
extras._warning = `v2 accepts one GPU type per pod; used "${params.gpuTypeIds[0]}" and ignored ${params.gpuTypeIds.length - 1} other(s): ${params.gpuTypeIds.slice(1).join(", ")}.`;
|
|
2246
2485
|
}
|
|
2247
|
-
return jsonReply2(
|
|
2486
|
+
return jsonReply2(
|
|
2487
|
+
Object.keys(extras).length ? { ...result, ...extras } : result
|
|
2488
|
+
);
|
|
2248
2489
|
} catch (error) {
|
|
2249
2490
|
if (error instanceof HttpError) {
|
|
2250
2491
|
return jsonReply2({ error: error.message, status: error.status });
|
|
@@ -2362,6 +2603,9 @@ function registerPodTools(server, rt) {
|
|
|
2362
2603
|
import { z as z8 } from "zod";
|
|
2363
2604
|
var MIN_QUEUE_DELAY = 0.5;
|
|
2364
2605
|
var MIN_REQUEST_COUNT = 1;
|
|
2606
|
+
function isRequestCountScaler(type) {
|
|
2607
|
+
return type === "REQUEST_COUNT";
|
|
2608
|
+
}
|
|
2365
2609
|
var MIN_IDLE_TIMEOUT = 1;
|
|
2366
2610
|
var MAX_IDLE_TIMEOUT = 3600;
|
|
2367
2611
|
function scalerValueError(scalerType, scalerValue) {
|
|
@@ -2454,6 +2698,12 @@ function registerEndpointTools(server, rt) {
|
|
|
2454
2698
|
'GPU pool names (v2, required). The `pool` field from list-gpu-types, e.g. ["AMPERE_80"]. NOT GPU type ids.'
|
|
2455
2699
|
),
|
|
2456
2700
|
gpuCount: z8.number().optional().describe("GPUs per worker (v2)"),
|
|
2701
|
+
allowedCudaVersions: z8.array(z8.string()).optional().describe(
|
|
2702
|
+
'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.'
|
|
2703
|
+
),
|
|
2704
|
+
minCudaVersion: z8.string().optional().describe(
|
|
2705
|
+
'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.'
|
|
2706
|
+
),
|
|
2457
2707
|
args: z8.string().optional().describe("Container start command/args (v2)"),
|
|
2458
2708
|
containerDiskInGb: z8.number().optional().describe("Container disk size in GB (v2)"),
|
|
2459
2709
|
ports: z8.array(z8.string()).optional().describe("Ports to expose (v2), e.g. ['8000/http']"),
|
|
@@ -2483,6 +2733,13 @@ function registerEndpointTools(server, rt) {
|
|
|
2483
2733
|
{ title: "Create endpoint", ...WRITE },
|
|
2484
2734
|
async (params) => {
|
|
2485
2735
|
const backend = backendFor("endpoints");
|
|
2736
|
+
const hasCudaParams = params.allowedCudaVersions !== void 0 || params.minCudaVersion !== void 0;
|
|
2737
|
+
if (hasCudaParams && backend.version !== "v2") {
|
|
2738
|
+
return jsonReply2({
|
|
2739
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2740
|
+
status: 501
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2486
2743
|
if (backend.version === "v2") {
|
|
2487
2744
|
if (!params.name) {
|
|
2488
2745
|
return jsonReply2({
|
|
@@ -2515,6 +2772,10 @@ function registerEndpointTools(server, rt) {
|
|
|
2515
2772
|
if (scalerError) {
|
|
2516
2773
|
return jsonReply2({ error: scalerError, status: 400 });
|
|
2517
2774
|
}
|
|
2775
|
+
if (hasCudaParams) {
|
|
2776
|
+
const cudaError = cudaConstraintError(params);
|
|
2777
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2778
|
+
}
|
|
2518
2779
|
const body = backend.mapCreate(params);
|
|
2519
2780
|
const result2 = await callRestUrl(
|
|
2520
2781
|
`${backend.base}${backend.list}`,
|
|
@@ -2552,7 +2813,7 @@ function registerEndpointTools(server, rt) {
|
|
|
2552
2813
|
);
|
|
2553
2814
|
server.tool(
|
|
2554
2815
|
"update-endpoint",
|
|
2555
|
-
"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.",
|
|
2816
|
+
"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.",
|
|
2556
2817
|
{
|
|
2557
2818
|
endpointId: z8.string().describe("ID of the endpoint to update"),
|
|
2558
2819
|
name: z8.string().optional().describe("New name for the endpoint"),
|
|
@@ -2571,6 +2832,12 @@ function registerEndpointTools(server, rt) {
|
|
|
2571
2832
|
imageName: z8.string().optional().describe("New Docker image (v2)"),
|
|
2572
2833
|
gpuPoolIds: z8.array(z8.string()).optional().describe('New GPU pool names (v2), e.g. ["AMPERE_80"]'),
|
|
2573
2834
|
gpuCount: z8.number().optional().describe("New GPUs per worker (v2)"),
|
|
2835
|
+
allowedCudaVersions: z8.array(z8.string()).optional().describe(
|
|
2836
|
+
"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."
|
|
2837
|
+
),
|
|
2838
|
+
minCudaVersion: z8.string().optional().describe(
|
|
2839
|
+
'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.'
|
|
2840
|
+
),
|
|
2574
2841
|
args: z8.string().optional().describe("New container args (v2)"),
|
|
2575
2842
|
containerDiskInGb: z8.number().optional().describe("New container disk size in GB (v2)"),
|
|
2576
2843
|
ports: z8.array(z8.string()).optional().describe("New ports (v2)"),
|
|
@@ -2585,14 +2852,41 @@ function registerEndpointTools(server, rt) {
|
|
|
2585
2852
|
const { endpointId, ...updateParams } = params;
|
|
2586
2853
|
const backend = backendFor("endpoints");
|
|
2587
2854
|
const url = `${backend.base}${backend.get(endpointId)}`;
|
|
2855
|
+
const hasCudaParams = updateParams.allowedCudaVersions !== void 0 || updateParams.minCudaVersion !== void 0;
|
|
2588
2856
|
if (backend.version !== "v2") {
|
|
2857
|
+
if (hasCudaParams) {
|
|
2858
|
+
return jsonReply2({
|
|
2859
|
+
error: "allowedCudaVersions/minCudaVersion are only supported on the v2 REST API. Set RUNPOD_REST_VERSION=v2.",
|
|
2860
|
+
status: 501
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2589
2863
|
const body2 = backend.mapUpdate(updateParams);
|
|
2590
2864
|
return jsonReply2(await callRestUrl(url, "PATCH", body2));
|
|
2591
2865
|
}
|
|
2866
|
+
if (hasCudaParams) {
|
|
2867
|
+
const cudaError = cudaConstraintError(updateParams, {
|
|
2868
|
+
allowClear: true
|
|
2869
|
+
});
|
|
2870
|
+
if (cudaError) return jsonReply2({ error: cudaError, status: 400 });
|
|
2871
|
+
}
|
|
2872
|
+
if (updateParams.gpuPoolIds !== void 0 && !updateParams.gpuPoolIds.length) {
|
|
2873
|
+
return jsonReply2({
|
|
2874
|
+
error: "gpuPoolIds cannot be empty \u2014 omit it to keep the current GPU selection.",
|
|
2875
|
+
status: 400
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2592
2878
|
let scalerType = updateParams.scalerType;
|
|
2593
|
-
|
|
2879
|
+
const needScalerRead = scalerType === void 0 && updateParams.scalerValue !== void 0;
|
|
2880
|
+
const needExclusionRead = (updateParams.gpuPoolIds?.length ?? 0) > 0;
|
|
2881
|
+
let clearedExclusions = [];
|
|
2882
|
+
if (needScalerRead || needExclusionRead) {
|
|
2594
2883
|
const current = await callRestUrl(url);
|
|
2595
|
-
|
|
2884
|
+
if (needScalerRead) {
|
|
2885
|
+
scalerType = isRequestCountScaler(current?.scaling?.type) ? "REQUEST_COUNT" : "QUEUE_DELAY";
|
|
2886
|
+
}
|
|
2887
|
+
if (needExclusionRead && current?.gpu?.excludedTypes?.length) {
|
|
2888
|
+
clearedExclusions = current.gpu.excludedTypes;
|
|
2889
|
+
}
|
|
2596
2890
|
}
|
|
2597
2891
|
const scalerError = scalerValueError(
|
|
2598
2892
|
scalerType,
|
|
@@ -2605,7 +2899,14 @@ function registerEndpointTools(server, rt) {
|
|
|
2605
2899
|
...updateParams,
|
|
2606
2900
|
scalerType
|
|
2607
2901
|
});
|
|
2608
|
-
|
|
2902
|
+
const result = await callRestUrl(url, "PATCH", body);
|
|
2903
|
+
if (clearedExclusions.length) {
|
|
2904
|
+
return jsonReply2({
|
|
2905
|
+
...result,
|
|
2906
|
+
_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.`
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
return jsonReply2(result);
|
|
2609
2910
|
}
|
|
2610
2911
|
);
|
|
2611
2912
|
server.tool(
|
|
@@ -2705,8 +3006,108 @@ import { z as z9 } from "zod";
|
|
|
2705
3006
|
var DIAGNOSIS_TTL_MS = 15e3;
|
|
2706
3007
|
var MAX_DIAGNOSIS_ENTRIES = 500;
|
|
2707
3008
|
var diagnosisCache = /* @__PURE__ */ new Map();
|
|
3009
|
+
var HTTP_LONG_POLL_BUDGET_MS = 45e3;
|
|
3010
|
+
var STDIO_STREAM_BUDGET_MS = 5 * 60 * 1e3;
|
|
3011
|
+
var RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS = 9e4;
|
|
3012
|
+
var STREAM_UPSTREAM_DEFAULT_WAIT_MS = 1e4;
|
|
3013
|
+
var HTTP_STREAM_POLL_WAIT_MS = 1e3;
|
|
3014
|
+
var STREAM_JOB_POLL_INTERVAL_MS = 1e3;
|
|
3015
|
+
var MAX_CONSECUTIVE_STREAM_ERRORS = 5;
|
|
3016
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
3017
|
+
"COMPLETED",
|
|
3018
|
+
"FAILED",
|
|
3019
|
+
"CANCELLED",
|
|
3020
|
+
"TIMED_OUT"
|
|
3021
|
+
]);
|
|
3022
|
+
function isUnfinishedJobReply(result) {
|
|
3023
|
+
return result !== null && typeof result === "object" && !Array.isArray(result) && !("waitClamped" in result) && !TERMINAL_STATUSES.has(result.status ?? "");
|
|
3024
|
+
}
|
|
3025
|
+
function formatBudget(ms) {
|
|
3026
|
+
return ms >= 12e4 ? `${Math.round(ms / 6e4)} minutes` : `${Math.round(ms / 1e3)} seconds`;
|
|
3027
|
+
}
|
|
3028
|
+
var UPSTREAM_HOLD_SLACK_MS = 5e3;
|
|
3029
|
+
var MIN_STREAM_POLL_TIMEOUT_MS = 2e3;
|
|
3030
|
+
var QUEUED_DIAGNOSIS_TIMEOUT_MS = 5e3;
|
|
3031
|
+
function streamPollTimeoutMs(remainingMs, holdMs) {
|
|
3032
|
+
return Math.max(
|
|
3033
|
+
Math.min(remainingMs, holdMs + UPSTREAM_HOLD_SLACK_MS),
|
|
3034
|
+
MIN_STREAM_POLL_TIMEOUT_MS
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
function sleep(ms) {
|
|
3038
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3039
|
+
}
|
|
3040
|
+
function streamPollPlan(hosted) {
|
|
3041
|
+
return hosted ? {
|
|
3042
|
+
budgetMs: HTTP_LONG_POLL_BUDGET_MS,
|
|
3043
|
+
holdMs: HTTP_STREAM_POLL_WAIT_MS,
|
|
3044
|
+
query: `?wait=${HTTP_STREAM_POLL_WAIT_MS}`
|
|
3045
|
+
} : {
|
|
3046
|
+
budgetMs: STDIO_STREAM_BUDGET_MS,
|
|
3047
|
+
holdMs: STREAM_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3048
|
+
query: ""
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
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).";
|
|
3052
|
+
function budgetExhaustedNote(budgetMs, lastError) {
|
|
3053
|
+
return {
|
|
3054
|
+
pollingTimedOut: true,
|
|
3055
|
+
note: `Polling stopped after ${formatBudget(
|
|
3056
|
+
budgetMs
|
|
3057
|
+
)} with the job possibly still running. ${RESUME_ADVICE}`,
|
|
3058
|
+
// Surface the trailing error (if any) instead of discarding it — the last
|
|
3059
|
+
// polls may have been failing (e.g. job expired) even though earlier ones
|
|
3060
|
+
// succeeded. Cleared on success, so this is never a stale error from
|
|
3061
|
+
// minutes of healthy streaming ago.
|
|
3062
|
+
...lastError ? { lastError } : {}
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
function errorCapNote(lastError) {
|
|
3066
|
+
return {
|
|
3067
|
+
error: `Polling aborted after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors: ${lastError}`,
|
|
3068
|
+
note: `Polling stopped after ${MAX_CONSECUTIVE_STREAM_ERRORS} consecutive errors with the job possibly still running. ${RESUME_ADVICE}`
|
|
3069
|
+
};
|
|
3070
|
+
}
|
|
3071
|
+
async function collectJobStream(deps) {
|
|
3072
|
+
const { poll, budgetMs, holdMs } = deps;
|
|
3073
|
+
const startedAt = Date.now();
|
|
3074
|
+
const elapsed = () => Date.now() - startedAt;
|
|
3075
|
+
const chunks = [];
|
|
3076
|
+
let result = {};
|
|
3077
|
+
let consecutiveErrors = 0;
|
|
3078
|
+
let lastError;
|
|
3079
|
+
while (true) {
|
|
3080
|
+
try {
|
|
3081
|
+
const reply = await poll(
|
|
3082
|
+
streamPollTimeoutMs(budgetMs - elapsed(), holdMs)
|
|
3083
|
+
);
|
|
3084
|
+
consecutiveErrors = 0;
|
|
3085
|
+
lastError = void 0;
|
|
3086
|
+
if (Array.isArray(reply.stream)) chunks.push(...reply.stream);
|
|
3087
|
+
result = reply;
|
|
3088
|
+
if (TERMINAL_STATUSES.has(reply.status)) break;
|
|
3089
|
+
} catch (error) {
|
|
3090
|
+
consecutiveErrors++;
|
|
3091
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3092
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_STREAM_ERRORS) {
|
|
3093
|
+
result = { ...result, ...errorCapNote(lastError) };
|
|
3094
|
+
break;
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
if (elapsed() > budgetMs) {
|
|
3098
|
+
result = { ...result, ...budgetExhaustedNote(budgetMs, lastError) };
|
|
3099
|
+
break;
|
|
3100
|
+
}
|
|
3101
|
+
await sleep(STREAM_JOB_POLL_INTERVAL_MS);
|
|
3102
|
+
}
|
|
3103
|
+
return { result, chunks };
|
|
3104
|
+
}
|
|
2708
3105
|
function registerJobTools(server, rt) {
|
|
2709
3106
|
const { jsonReply: jsonReply2, serverlessRequest, backendFor, callRestUrl } = rt;
|
|
3107
|
+
const hosted = rt.transport === "http";
|
|
3108
|
+
const streamBudget = formatBudget(
|
|
3109
|
+
hosted ? HTTP_LONG_POLL_BUDGET_MS : STDIO_STREAM_BUDGET_MS
|
|
3110
|
+
);
|
|
2710
3111
|
async function diagnoseQueuedJob(endpointId) {
|
|
2711
3112
|
const cached = diagnosisCache.get(endpointId);
|
|
2712
3113
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -2717,7 +3118,10 @@ function registerJobTools(server, rt) {
|
|
|
2717
3118
|
const backend = backendFor("workers");
|
|
2718
3119
|
if (backend.version !== "v2") return null;
|
|
2719
3120
|
const raw = await callRestUrl(
|
|
2720
|
-
`${backend.base}/serverless/${endpointId}/workers
|
|
3121
|
+
`${backend.base}/serverless/${endpointId}/workers`,
|
|
3122
|
+
"GET",
|
|
3123
|
+
void 0,
|
|
3124
|
+
{ timeoutMs: QUEUED_DIAGNOSIS_TIMEOUT_MS }
|
|
2721
3125
|
);
|
|
2722
3126
|
const summary = raw?.summary;
|
|
2723
3127
|
const workers = raw?.workers ?? [];
|
|
@@ -2808,14 +3212,14 @@ function registerJobTools(server, rt) {
|
|
|
2808
3212
|
);
|
|
2809
3213
|
server.tool(
|
|
2810
3214
|
"runsync-endpoint",
|
|
2811
|
-
|
|
3215
|
+
`Submit a synchronous job to a Serverless endpoint and wait for the result. Best for fast tasks: if the job outlives the wait, the response returns a job ID and a non-terminal status (IN_QUEUE or IN_PROGRESS) to poll with get-job-status. Max payload 20 MB; results expire after 1 minute. ${hosted ? `This server waits up to ${HTTP_LONG_POLL_BUDGET_MS} ms \u2014 it runs behind a 60-second gateway deadline, so that is both the default and the ceiling for the wait parameter. For longer jobs use run-endpoint + get-job-status, or POST https://api.runpod.ai/v2/{endpointId}/runsync?wait=300000 directly with a Bearer API key.` : "The wait parameter extends the server-side wait to up to 5 minutes (300000 ms); it defaults to 90000 (90 seconds)."}`,
|
|
2812
3216
|
{
|
|
2813
3217
|
endpointId: endpointIdSchema.describe(
|
|
2814
3218
|
"ID of the Serverless endpoint to run synchronously"
|
|
2815
3219
|
),
|
|
2816
3220
|
input: inputSchema,
|
|
2817
3221
|
wait: z9.number().min(1e3).max(3e5).optional().describe(
|
|
2818
|
-
"How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
3222
|
+
hosted ? `How long in milliseconds the server should wait for a result before returning a job ID to poll. Accepted range 1000\u2013300000, but this server clamps anything above ${HTTP_LONG_POLL_BUDGET_MS} down to ${HTTP_LONG_POLL_BUDGET_MS}, which is also the effective default.` : "How long in milliseconds the server should wait for a result before returning a job ID to poll (1000\u2013300000). Defaults to 90000 (90 seconds)."
|
|
2819
3223
|
),
|
|
2820
3224
|
webhook: webhookSchema,
|
|
2821
3225
|
policy: policySchema,
|
|
@@ -2824,13 +3228,36 @@ function registerJobTools(server, rt) {
|
|
|
2824
3228
|
{ title: "Run endpoint (sync)", ...WRITE },
|
|
2825
3229
|
async (params) => {
|
|
2826
3230
|
const { endpointId, wait, ...body } = params;
|
|
2827
|
-
const
|
|
3231
|
+
const effectiveWait = hosted ? Math.min(
|
|
3232
|
+
wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS,
|
|
3233
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3234
|
+
) : wait;
|
|
3235
|
+
const waitQuery = effectiveWait === void 0 ? "" : `?wait=${effectiveWait}`;
|
|
3236
|
+
const path = `/runsync${waitQuery}`;
|
|
2828
3237
|
const result = await serverlessRequest(
|
|
2829
3238
|
endpointId,
|
|
2830
3239
|
path,
|
|
2831
3240
|
"POST",
|
|
2832
|
-
body
|
|
3241
|
+
body,
|
|
3242
|
+
{
|
|
3243
|
+
// From the wait actually sent — on http the clamped 45s, not the
|
|
3244
|
+
// 300s the caller may have asked for.
|
|
3245
|
+
timeoutMs: (effectiveWait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS) + UPSTREAM_HOLD_SLACK_MS
|
|
3246
|
+
}
|
|
2833
3247
|
);
|
|
3248
|
+
const requestedWait = wait ?? RUNSYNC_UPSTREAM_DEFAULT_WAIT_MS;
|
|
3249
|
+
if (hosted && requestedWait > HTTP_LONG_POLL_BUDGET_MS && isUnfinishedJobReply(result)) {
|
|
3250
|
+
return jsonReply2({
|
|
3251
|
+
...result,
|
|
3252
|
+
waitClamped: {
|
|
3253
|
+
requestedMs: requestedWait,
|
|
3254
|
+
effectiveMs: effectiveWait,
|
|
3255
|
+
reason: `This server runs behind a 60-second gateway deadline, so a non-terminal status here means the job outlived ${formatBudget(
|
|
3256
|
+
HTTP_LONG_POLL_BUDGET_MS
|
|
3257
|
+
)}, not the requested wait \u2014 poll get-job-status rather than treating it as stuck.`
|
|
3258
|
+
}
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
2834
3261
|
return jsonReply2(result);
|
|
2835
3262
|
}
|
|
2836
3263
|
);
|
|
@@ -2864,7 +3291,7 @@ function registerJobTools(server, rt) {
|
|
|
2864
3291
|
);
|
|
2865
3292
|
server.tool(
|
|
2866
3293
|
"stream-job",
|
|
2867
|
-
|
|
3294
|
+
`Retrieve streaming output from a Serverless job. The worker must support streaming output. Polls /stream/{jobId} and collects chunks until the status is COMPLETED, FAILED, CANCELLED, or TIMED_OUT, for up to ${streamBudget}${hosted ? " (this server runs behind a 60-second gateway deadline)" : ""}. If the budget expires first, returns the chunks collected so far with pollingTimedOut: true \u2014 call stream-job again to resume where it left off, or get-job-status to check without streaming.${hosted ? " To collect with no budget, poll GET https://api.runpod.ai/v2/{endpointId}/stream/{jobId} yourself with a Bearer API key until the status is terminal \u2014 each request drains only the chunks buffered so far, so a single request is not the whole output." : ""}`,
|
|
2868
3295
|
{
|
|
2869
3296
|
endpointId: endpointIdSchema.describe(
|
|
2870
3297
|
"ID of the Serverless endpoint the job belongs to"
|
|
@@ -2873,51 +3300,16 @@ function registerJobTools(server, rt) {
|
|
|
2873
3300
|
},
|
|
2874
3301
|
{ title: "Stream job", ...READ_ONLY },
|
|
2875
3302
|
async (params) => {
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
let finalResult = {};
|
|
2887
|
-
let consecutiveErrors = 0;
|
|
2888
|
-
let lastError;
|
|
2889
|
-
const startTime = Date.now();
|
|
2890
|
-
while (true) {
|
|
2891
|
-
try {
|
|
2892
|
-
const result = await serverlessRequest(
|
|
2893
|
-
params.endpointId,
|
|
2894
|
-
`/stream/${params.jobId}`
|
|
2895
|
-
);
|
|
2896
|
-
consecutiveErrors = 0;
|
|
2897
|
-
if (Array.isArray(result.stream)) {
|
|
2898
|
-
allChunks.push(...result.stream);
|
|
2899
|
-
}
|
|
2900
|
-
finalResult = result;
|
|
2901
|
-
if (TERMINAL_STATUSES.has(result.status)) {
|
|
2902
|
-
break;
|
|
2903
|
-
}
|
|
2904
|
-
} catch (error) {
|
|
2905
|
-
consecutiveErrors++;
|
|
2906
|
-
lastError = error instanceof Error ? error.message : String(error);
|
|
2907
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
2908
|
-
finalResult.error = `Polling aborted after ${MAX_CONSECUTIVE_ERRORS} consecutive errors: ${lastError}`;
|
|
2909
|
-
break;
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
if (Date.now() - startTime > MAX_POLL_TIME_MS) {
|
|
2913
|
-
finalResult.pollingTimedOut = true;
|
|
2914
|
-
finalResult.note = "Polling timed out after 5 minutes. Use get-job-status to check the job later.";
|
|
2915
|
-
if (lastError) finalResult.lastError = lastError;
|
|
2916
|
-
break;
|
|
2917
|
-
}
|
|
2918
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
2919
|
-
}
|
|
2920
|
-
return jsonReply2({ ...finalResult, stream: allChunks });
|
|
3303
|
+
const plan = streamPollPlan(hosted);
|
|
3304
|
+
const streamPath = `/stream/${params.jobId}${plan.query}`;
|
|
3305
|
+
const { result, chunks } = await collectJobStream({
|
|
3306
|
+
budgetMs: plan.budgetMs,
|
|
3307
|
+
holdMs: plan.holdMs,
|
|
3308
|
+
poll: (timeoutMs) => serverlessRequest(params.endpointId, streamPath, "GET", void 0, {
|
|
3309
|
+
timeoutMs
|
|
3310
|
+
})
|
|
3311
|
+
});
|
|
3312
|
+
return jsonReply2({ ...result, stream: chunks });
|
|
2921
3313
|
}
|
|
2922
3314
|
);
|
|
2923
3315
|
server.tool(
|
|
@@ -3480,7 +3872,7 @@ async function main(apiKey) {
|
|
|
3480
3872
|
await server.connect(transport);
|
|
3481
3873
|
}
|
|
3482
3874
|
if (isWizardMode) {
|
|
3483
|
-
import("./wizard-
|
|
3875
|
+
import("./wizard-FBLOZ6YM.mjs").then(({ runWizard }) => runWizard(subcommand)).catch((error) => {
|
|
3484
3876
|
console.error(error instanceof Error ? error.message : String(error));
|
|
3485
3877
|
process.exit(1);
|
|
3486
3878
|
});
|