@runpod/mcp-server 4.10.0 → 4.10.1
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 +21 -0
- package/README.md +2 -11
- package/dist/{chunk-JTK6RETN.mjs → chunk-AKX5ZMCL.mjs} +186 -32
- package/dist/chunk-AKX5ZMCL.mjs.map +1 -0
- package/dist/hosted.cjs +185 -31
- package/dist/hosted.cjs.map +1 -1
- package/dist/hosted.mjs +1 -1
- package/dist/http.cjs +185 -31
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +1 -1
- package/dist/stdio.cjs +185 -31
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +185 -31
- package/dist/stdio.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-JTK6RETN.mjs.map +0 -1
package/dist/hosted.mjs
CHANGED
package/dist/http.cjs
CHANGED
|
@@ -308,6 +308,14 @@ function validateArguments(inputSchema, args) {
|
|
|
308
308
|
}
|
|
309
309
|
return null;
|
|
310
310
|
}
|
|
311
|
+
function withoutNullOptionals(inputSchema, args) {
|
|
312
|
+
const required = new Set(inputSchema.required ?? []);
|
|
313
|
+
return Object.fromEntries(
|
|
314
|
+
Object.entries(args).filter(
|
|
315
|
+
([name, value]) => value !== null || required.has(name)
|
|
316
|
+
)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
311
319
|
async function dispatchGeneratedTool(client, tool, args) {
|
|
312
320
|
const rejected = validateArguments(tool.inputSchema, args);
|
|
313
321
|
if (rejected) return rejected;
|
|
@@ -9961,23 +9969,40 @@ function createRunpodClient(options = {}) {
|
|
|
9961
9969
|
// src/specgen/clients/sse.ts
|
|
9962
9970
|
var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
|
|
9963
9971
|
var LOG_STREAM_MAX_BYTES = 256 * 1024;
|
|
9972
|
+
var LOG_STREAM_CONNECT_TIMEOUT_MS = 1e4;
|
|
9973
|
+
var LOG_SNAPSHOT_MAX_CHARS = 32e3;
|
|
9964
9974
|
function createSseReader(options = {}) {
|
|
9965
9975
|
const apiKey = options.apiKey ?? process.env.RUNPOD_API_KEY;
|
|
9966
9976
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
9977
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? LOG_STREAM_CONNECT_TIMEOUT_MS;
|
|
9967
9978
|
return async (url, opts) => {
|
|
9968
9979
|
if (!apiKey) throw missingKeyError();
|
|
9969
9980
|
const chunks = [];
|
|
9970
9981
|
let bytes = 0;
|
|
9971
9982
|
let truncated = false;
|
|
9972
9983
|
let streamEstablished = false;
|
|
9984
|
+
const connect = new AbortController();
|
|
9985
|
+
const connectTimer = setTimeout(
|
|
9986
|
+
() => connect.abort(
|
|
9987
|
+
new DOMException(
|
|
9988
|
+
`Runpod log stream sent no headers within ${connectTimeoutMs}ms`,
|
|
9989
|
+
"TimeoutError"
|
|
9990
|
+
)
|
|
9991
|
+
),
|
|
9992
|
+
connectTimeoutMs
|
|
9993
|
+
);
|
|
9973
9994
|
try {
|
|
9974
|
-
const response = await createDeadlineFetch(
|
|
9995
|
+
const response = await createDeadlineFetch(
|
|
9996
|
+
fetchImpl,
|
|
9997
|
+
connectTimeoutMs + opts.maxWaitMs
|
|
9998
|
+
)(url, {
|
|
9975
9999
|
method: "GET",
|
|
9976
10000
|
headers: {
|
|
9977
10001
|
Authorization: `Bearer ${apiKey}`,
|
|
9978
10002
|
Accept: "text/event-stream"
|
|
9979
|
-
}
|
|
9980
|
-
|
|
10003
|
+
},
|
|
10004
|
+
signal: connect.signal
|
|
10005
|
+
}).finally(() => clearTimeout(connectTimer));
|
|
9981
10006
|
if (!response.ok) {
|
|
9982
10007
|
const body = await response.text().catch(() => "");
|
|
9983
10008
|
throw new HttpError(
|
|
@@ -9988,14 +10013,31 @@ function createSseReader(options = {}) {
|
|
|
9988
10013
|
}
|
|
9989
10014
|
streamEstablished = true;
|
|
9990
10015
|
if (response.body) {
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
10016
|
+
const reader = response.body.getReader();
|
|
10017
|
+
let timer;
|
|
10018
|
+
const windowClosed = new Promise((resolve) => {
|
|
10019
|
+
timer = setTimeout(() => resolve(null), opts.maxWaitMs);
|
|
10020
|
+
});
|
|
10021
|
+
try {
|
|
10022
|
+
for (; ; ) {
|
|
10023
|
+
const read = reader.read();
|
|
10024
|
+
read.catch(() => {
|
|
10025
|
+
});
|
|
10026
|
+
const next = await Promise.race([read, windowClosed]);
|
|
10027
|
+
if (next === null || next.done) break;
|
|
10028
|
+
const boundedChunk = next.value.subarray(0, opts.maxBytes - bytes);
|
|
10029
|
+
chunks.push(boundedChunk);
|
|
10030
|
+
bytes += boundedChunk.length;
|
|
10031
|
+
if (bytes >= opts.maxBytes) {
|
|
10032
|
+
truncated = true;
|
|
10033
|
+
break;
|
|
10034
|
+
}
|
|
9998
10035
|
}
|
|
10036
|
+
} finally {
|
|
10037
|
+
clearTimeout(timer);
|
|
10038
|
+
void reader.cancel().catch(() => {
|
|
10039
|
+
});
|
|
10040
|
+
reader.releaseLock();
|
|
9999
10041
|
}
|
|
10000
10042
|
}
|
|
10001
10043
|
} catch (err) {
|
|
@@ -10021,7 +10063,80 @@ async function collectLogSnapshot(reader, logsUrl, params) {
|
|
|
10021
10063
|
for await (const event of iterateLogEvents(new Response(raw).body)) {
|
|
10022
10064
|
items.push(event.data);
|
|
10023
10065
|
}
|
|
10024
|
-
return
|
|
10066
|
+
return fitSnapshot(items, truncated);
|
|
10067
|
+
}
|
|
10068
|
+
var serializedLength = (snapshot) => JSON.stringify(snapshot, null, 2).length;
|
|
10069
|
+
var LOG_ENTRY_MAX_CHARS = 16e3;
|
|
10070
|
+
var CLIPPABLE_KEYS = ["line", "raw"];
|
|
10071
|
+
function clipText(text, keep) {
|
|
10072
|
+
const code = text.charCodeAt(keep - 1);
|
|
10073
|
+
if (keep > 0 && code >= 55296 && code <= 56319) keep--;
|
|
10074
|
+
return `${text.slice(0, keep)}\u2026 [clipped ${text.length - keep} chars]`;
|
|
10075
|
+
}
|
|
10076
|
+
function clipEntry(entry) {
|
|
10077
|
+
let clipped = false;
|
|
10078
|
+
const out = { ...entry };
|
|
10079
|
+
for (const key of CLIPPABLE_KEYS) {
|
|
10080
|
+
const text = out[key];
|
|
10081
|
+
if (typeof text === "string" && text.length > LOG_ENTRY_MAX_CHARS) {
|
|
10082
|
+
out[key] = clipText(text, LOG_ENTRY_MAX_CHARS);
|
|
10083
|
+
clipped = true;
|
|
10084
|
+
}
|
|
10085
|
+
}
|
|
10086
|
+
return { entry: out, clipped };
|
|
10087
|
+
}
|
|
10088
|
+
function fitEntry(entry, maxChars) {
|
|
10089
|
+
const size = (e) => serializedLength({ items: [e], count: 1, truncated: false });
|
|
10090
|
+
const out = { ...entry };
|
|
10091
|
+
const keys = Object.keys(out).filter((key) => key !== "ts" && typeof out[key] === "string").sort((a, b) => out[b].length - out[a].length);
|
|
10092
|
+
for (const key of keys) {
|
|
10093
|
+
if (size(out) <= maxChars) break;
|
|
10094
|
+
const text = entry[key];
|
|
10095
|
+
let lo = 0;
|
|
10096
|
+
let hi = Math.min(text.length, LOG_ENTRY_MAX_CHARS);
|
|
10097
|
+
while (lo < hi) {
|
|
10098
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
10099
|
+
if (size({ ...out, [key]: clipText(text, mid) }) <= maxChars)
|
|
10100
|
+
lo = mid;
|
|
10101
|
+
else hi = mid - 1;
|
|
10102
|
+
}
|
|
10103
|
+
out[key] = clipText(text, lo);
|
|
10104
|
+
}
|
|
10105
|
+
return out;
|
|
10106
|
+
}
|
|
10107
|
+
var entryTs = (entry) => entry?.ts;
|
|
10108
|
+
function fitSnapshot(items, truncated, maxChars = LOG_SNAPSHOT_MAX_CHARS) {
|
|
10109
|
+
let cut = truncated;
|
|
10110
|
+
const entries = items.map((item) => {
|
|
10111
|
+
const { entry, clipped } = clipEntry(item);
|
|
10112
|
+
if (clipped) cut = true;
|
|
10113
|
+
return entry;
|
|
10114
|
+
});
|
|
10115
|
+
const snapshot = (n, isCut) => ({
|
|
10116
|
+
items: entries.slice(0, n),
|
|
10117
|
+
count: n,
|
|
10118
|
+
truncated: isCut
|
|
10119
|
+
});
|
|
10120
|
+
const whole = snapshot(entries.length, cut);
|
|
10121
|
+
if (entries.length === 0 || serializedLength(whole) <= maxChars) return whole;
|
|
10122
|
+
if (serializedLength(snapshot(1, true)) > maxChars) {
|
|
10123
|
+
entries[0] = fitEntry(items[0], maxChars);
|
|
10124
|
+
if (entries.length === 1) return snapshot(1, true);
|
|
10125
|
+
}
|
|
10126
|
+
let lo = 1;
|
|
10127
|
+
let hi = entries.length - 1;
|
|
10128
|
+
while (lo < hi) {
|
|
10129
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
10130
|
+
if (serializedLength(snapshot(mid, true)) <= maxChars) lo = mid;
|
|
10131
|
+
else hi = mid - 1;
|
|
10132
|
+
}
|
|
10133
|
+
let keep = lo;
|
|
10134
|
+
const omittedTs = entryTs(entries[keep]);
|
|
10135
|
+
if (omittedTs !== void 0) {
|
|
10136
|
+
while (keep > 0 && entryTs(entries[keep - 1]) === omittedTs) keep--;
|
|
10137
|
+
if (keep === 0) keep = lo;
|
|
10138
|
+
}
|
|
10139
|
+
return snapshot(keep, true);
|
|
10025
10140
|
}
|
|
10026
10141
|
|
|
10027
10142
|
// src/specgen/tools/logs.ts
|
|
@@ -10039,13 +10154,13 @@ var logStreamProperties = {
|
|
|
10039
10154
|
},
|
|
10040
10155
|
since: {
|
|
10041
10156
|
type: "string",
|
|
10042
|
-
description: "RFC3339 timestamp to resume
|
|
10157
|
+
description: "RFC3339 timestamp to resume after (exclusive). Replays container lines only; system lines stream live from the call. When set, `tail` is ignored."
|
|
10043
10158
|
},
|
|
10044
10159
|
maxWaitMs: {
|
|
10045
10160
|
type: "integer",
|
|
10046
10161
|
minimum: 500,
|
|
10047
10162
|
maximum: 3e4,
|
|
10048
|
-
description: "How long to read the stream, in ms (default 5000, max 30000)"
|
|
10163
|
+
description: "How long to read the stream once it opens, in ms (default 5000, max 30000)"
|
|
10049
10164
|
}
|
|
10050
10165
|
};
|
|
10051
10166
|
function baseUrl() {
|
|
@@ -10071,7 +10186,7 @@ var streamPodLogs = {
|
|
|
10071
10186
|
title: "Stream Pod logs",
|
|
10072
10187
|
holdsResponse: true,
|
|
10073
10188
|
annotations: readOnly,
|
|
10074
|
-
description: "Read a bounded snapshot of a Pod's logs (container and/or system source). Holds the live stream open for maxWaitMs and returns the parsed log lines; `truncated: true` means the
|
|
10189
|
+
description: "Read a bounded snapshot of a Pod's logs (container and/or system source). Holds the live stream open for maxWaitMs and returns the parsed log lines; `truncated: true` means the size cap cut the output. For later container lines, call again with `since` set to the last container line's `ts`; `since` does not replay system lines, so for those request a smaller `tail`.",
|
|
10075
10190
|
inputSchema: {
|
|
10076
10191
|
type: "object",
|
|
10077
10192
|
properties: {
|
|
@@ -10093,10 +10208,10 @@ var streamPodLogs = {
|
|
|
10093
10208
|
var streamWorkerLogs = {
|
|
10094
10209
|
name: "stream-worker-logs",
|
|
10095
10210
|
title: "Stream worker logs",
|
|
10096
|
-
directoryDescription: "Read a bounded snapshot of a Serverless worker's logs (container, system, or both). Holds the live stream open for maxWaitMs and returns the parsed log lines; truncated: true means the
|
|
10211
|
+
directoryDescription: "Read a bounded snapshot of a Serverless worker's logs (container, system, or both). Holds the live stream open for maxWaitMs and returns the parsed log lines; truncated: true means the size cap cut the output. A worker whose container keeps failing to start can report RUNNING or THROTTLED while its logs show the failure.",
|
|
10097
10212
|
holdsResponse: true,
|
|
10098
10213
|
annotations: readOnly,
|
|
10099
|
-
description: "Read a bounded snapshot of a Serverless worker's logs (container and/or system source). Holds the live stream open for maxWaitMs and returns the parsed log lines; `truncated: true` means the
|
|
10214
|
+
description: "Read a bounded snapshot of a Serverless worker's logs (container and/or system source). Holds the live stream open for maxWaitMs and returns the parsed log lines; `truncated: true` means the size cap cut the output. For later container lines, call again with `since` set to the last container line's `ts`; `since` does not replay system lines, so for those request a smaller `tail`. Get worker IDs from list-endpoint-workers. Use this whenever a job stays IN_QUEUE while workers exist, whatever status they report \u2014 a crash-looping container often still shows RUNNING or THROTTLED with unhealthy 0, so these logs, not the worker status, are what identify a crash loop.",
|
|
10100
10215
|
inputSchema: {
|
|
10101
10216
|
type: "object",
|
|
10102
10217
|
properties: {
|
|
@@ -10240,6 +10355,7 @@ var listPublicEndpoints = {
|
|
|
10240
10355
|
// src/specgen/tools/knowledge.ts
|
|
10241
10356
|
var import_plugin_knowledge = require("@runpod/plugin-knowledge");
|
|
10242
10357
|
var knowledge = (0, import_plugin_knowledge.loadKnowledge)();
|
|
10358
|
+
var resolveGuideId = (id) => knowledge.aliases?.[id] ?? id;
|
|
10243
10359
|
var hiddenRules = new Set(
|
|
10244
10360
|
(process.env.KNOWLEDGE_HIDDEN_RULES ?? "").split(",").map((id) => id.trim()).filter(Boolean)
|
|
10245
10361
|
);
|
|
@@ -10416,7 +10532,9 @@ var readGuide = {
|
|
|
10416
10532
|
title: "Read guide",
|
|
10417
10533
|
annotations: bundledRead,
|
|
10418
10534
|
recordedArguments: ["id"],
|
|
10419
|
-
recordedGuideKind: (args) => knowledge.guides.find(
|
|
10535
|
+
recordedGuideKind: (args) => knowledge.guides.find(
|
|
10536
|
+
(guide) => guide.id === resolveGuideId(String(args.id).trim())
|
|
10537
|
+
)?.kind,
|
|
10420
10538
|
description: 'Read one official Runpod guide in full, by the id list-guides returns (for example "runpod-usage/storage" or "golden-path/06-dev-pod").',
|
|
10421
10539
|
inputSchema: {
|
|
10422
10540
|
type: "object",
|
|
@@ -10428,7 +10546,9 @@ var readGuide = {
|
|
|
10428
10546
|
},
|
|
10429
10547
|
handler: async (_ctx, args) => {
|
|
10430
10548
|
const id = String(args.id).trim();
|
|
10431
|
-
const guide = knowledge.guides.find(
|
|
10549
|
+
const guide = knowledge.guides.find(
|
|
10550
|
+
(candidate) => candidate.id === resolveGuideId(id)
|
|
10551
|
+
);
|
|
10432
10552
|
if (!guide) {
|
|
10433
10553
|
const close = knowledge.guides.filter(
|
|
10434
10554
|
(candidate) => candidate.id.includes(id) || id.includes(candidate.id)
|
|
@@ -10583,6 +10703,12 @@ var guideResources = knowledge.guides.map((guide) => ({
|
|
|
10583
10703
|
mimeType: "text/markdown",
|
|
10584
10704
|
text: guide.body
|
|
10585
10705
|
}));
|
|
10706
|
+
var findGuideResource = (resources, uri) => {
|
|
10707
|
+
const id = uri.startsWith(GUIDE_URI_PREFIX) ? resolveGuideId(uri.slice(GUIDE_URI_PREFIX.length)) : void 0;
|
|
10708
|
+
return resources.find(
|
|
10709
|
+
(resource) => resource.uri === uri || resource.uri === GUIDE_URI_PREFIX + id
|
|
10710
|
+
);
|
|
10711
|
+
};
|
|
10586
10712
|
|
|
10587
10713
|
// src/specgen/prose.ts
|
|
10588
10714
|
var ABBREVIATIONS = ["e.g.", "i.e.", "etc.", "vs."];
|
|
@@ -10766,7 +10892,7 @@ function words2(name) {
|
|
|
10766
10892
|
|
|
10767
10893
|
// src/server.ts
|
|
10768
10894
|
var SERVER_NAME = "Runpod API Server";
|
|
10769
|
-
var SERVER_VERSION = true ? "4.10.
|
|
10895
|
+
var SERVER_VERSION = true ? "4.10.1" : "dev";
|
|
10770
10896
|
|
|
10771
10897
|
// src/specgen/analytics.ts
|
|
10772
10898
|
var import_node_crypto = require("crypto");
|
|
@@ -10834,6 +10960,7 @@ function captureToolCall(apiKey, event) {
|
|
|
10834
10960
|
server_version: event.serverVersion,
|
|
10835
10961
|
...event.clientName ? { client_name: event.clientName } : {},
|
|
10836
10962
|
...event.guideKind ? { guide_kind: event.guideKind } : {},
|
|
10963
|
+
...event.errorClass ? { error_class: event.errorClass } : {},
|
|
10837
10964
|
...event.blockedBy ? { blocked_by: event.blockedBy } : {},
|
|
10838
10965
|
...event.profile ? { profile: event.profile } : {},
|
|
10839
10966
|
...args
|
|
@@ -11367,7 +11494,7 @@ These tools manage infrastructure only. They do not do SSH sessions, file transf
|
|
|
11367
11494
|
|
|
11368
11495
|
OFFICIAL GUIDES AND FACTS. list-guides and read-guide serve the official Runpod plugin's skills, reference docs and live-verified golden paths, so you have them even without the plugin installed. For a multi-step or billable task, look for a matching golden path first. lookup-concept and search-concepts answer exact questions about how Runpod works (what a stopped pod keeps, where a volume lives) with the public source for each fact.
|
|
11369
11496
|
|
|
11370
|
-
PLAYBOOKS \u2014 READ BEFORE ACTING. Each Runpod task has a journey playbook carrying its procedure, pitfalls and report format. Before the FIRST Runpod tool call of a session, load the plugin's runpod-mcp skill if you have it, otherwise read-guide runpod-mcp (also the runpod://guides/runpod-mcp resource): it routes the request to a journey guide \u2014 deploying an endpoint reads serverless-deploy, diagnosing a broken pod reads pod-doctor, cost questions read cost-audit, and so on \u2014 and states the answer contract. A reply produced without the routed playbook loaded is out of contract.
|
|
11497
|
+
PLAYBOOKS \u2014 READ BEFORE ACTING. Each Runpod task has a journey playbook carrying its procedure, pitfalls and report format. Before the FIRST Runpod tool call of a session, load the plugin's runpod-mcp skill if you have it, otherwise read-guide runpod-mcp (also the runpod://guides/runpod-mcp resource): it routes the request to a journey guide \u2014 deploying an endpoint reads runpod-serverless-deploy, diagnosing a broken pod reads runpod-pod-doctor, cost questions read runpod-cost-audit, and so on \u2014 and states the answer contract. A reply produced without the routed playbook loaded is out of contract.
|
|
11371
11498
|
|
|
11372
11499
|
THE OFFICIAL RUNPOD PLUGIN. Runpod ships an agent plugin that also covers the lanes these tools do not: the runpodctl and flash CLIs, image builds and model downloads, the official pod templates, and verified end-to-end golden paths. Check once per session whether skills named runpod, runpod-usage, or runpodctl are available to you \u2014 read what you already have, never shell out to look. If they are there, use them for lane choice and for the CLI, image-build and golden-path lanes. For work done with these tools, the journey playbook is the procedure, wherever it is served from: the plugin's runpod-mcp skill and the journey skills it routes to when you have them, otherwise the same playbooks through read-guide here. If you do not see them, recommend the plugin to the user, and recommend updating to the latest if what they have is missing something you need. Point them at https://github.com/runpod/runpod-plugins-official \u2014 its README carries the install, update, and clone commands for each client, so read it yourself when you need the exact one rather than guessing a command. This is a recommendation, never a prerequisite: make it once, briefly, and get on with the task. Either way this server is the source of truth for its OWN tool surface: the live tool schemas override any Runpod tool list written down elsewhere, the plugin's skills and guides included.
|
|
11373
11500
|
|
|
@@ -11428,9 +11555,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
11428
11555
|
resourceTemplates: []
|
|
11429
11556
|
}));
|
|
11430
11557
|
server.setRequestHandler(import_types.ReadResourceRequestSchema, async (request) => {
|
|
11431
|
-
const guide = servedGuides.
|
|
11432
|
-
(candidate) => candidate.uri === request.params.uri
|
|
11433
|
-
);
|
|
11558
|
+
const guide = findGuideResource(servedGuides, request.params.uri);
|
|
11434
11559
|
if (opts.analytics) {
|
|
11435
11560
|
captureResourceCall(ctx.apiKey, {
|
|
11436
11561
|
method: "resources/read",
|
|
@@ -11467,6 +11592,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
11467
11592
|
ok: false,
|
|
11468
11593
|
status: 429,
|
|
11469
11594
|
durationMs: 0,
|
|
11595
|
+
errorClass: "rate_limited",
|
|
11470
11596
|
...verdict.reason ? { blockedBy: verdict.reason } : {},
|
|
11471
11597
|
profile,
|
|
11472
11598
|
...opts.analytics
|
|
@@ -11492,6 +11618,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
11492
11618
|
const generated = servedGeneratedTools.find(
|
|
11493
11619
|
(tool) => tool.name === request.params.name
|
|
11494
11620
|
);
|
|
11621
|
+
const curatedArgs = curated ? withoutNullOptionals(curated.inputSchema, args) : args;
|
|
11495
11622
|
const omitted = directory ? directoryOmittedInputs(request.params.name).filter((key) => key in args) : [];
|
|
11496
11623
|
const rejected = omitted.length ? {
|
|
11497
11624
|
ok: false,
|
|
@@ -11501,10 +11628,10 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
11501
11628
|
}
|
|
11502
11629
|
} : curated && !curated.lenientArguments ? validateArguments(
|
|
11503
11630
|
curated.inputSchema,
|
|
11504
|
-
|
|
11631
|
+
curatedArgs
|
|
11505
11632
|
) : null;
|
|
11506
11633
|
const result = rejected ?? await runTool(
|
|
11507
|
-
() => curated ? curated.handler(ctx,
|
|
11634
|
+
() => curated ? curated.handler(ctx, curatedArgs) : dispatchTool(generated, request.params.name, args)
|
|
11508
11635
|
);
|
|
11509
11636
|
logToolCall({
|
|
11510
11637
|
tool: request.params.name,
|
|
@@ -11519,6 +11646,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
11519
11646
|
ok: result.ok,
|
|
11520
11647
|
status: result.status,
|
|
11521
11648
|
durationMs: Date.now() - startedAt,
|
|
11649
|
+
...result.ok ? {} : { errorClass: errorClass(result) },
|
|
11522
11650
|
// The directory profile records which tool ran, never what was asked.
|
|
11523
11651
|
...directory ? {} : {
|
|
11524
11652
|
args: recordedArgumentValues(curated?.recordedArguments, args),
|
|
@@ -11558,24 +11686,50 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
11558
11686
|
var SHAPE_HINT = "Input shape mismatch: check this tool's schema for required fields. For job payloads, the worker's release config defines the expected input object \u2014 fix the payload and retry; don't abandon the task.";
|
|
11559
11687
|
var DETAIL_HINTS = [
|
|
11560
11688
|
[
|
|
11561
|
-
/no longer any instances available|not enough free GPUs/i,
|
|
11562
|
-
"Capacity, not input: the request is valid but no machine can take it right now. Don't change the payload shape. Retry with another GPU type, data center, cloud, CUDA version or count, or try again later. For a stopped Pod, its host's GPUs are taken; that Pod can only start once they free up."
|
|
11689
|
+
/no longer any instances available|no instances currently available|not enough free GPUs/i,
|
|
11690
|
+
"Capacity, not input: the request is valid but no machine can take it right now. Don't change the payload shape. Retry with another GPU type, data center, cloud, CUDA version or count, or try again later. For a stopped Pod, its host's GPUs are taken; that Pod can only start once they free up.",
|
|
11691
|
+
"capacity"
|
|
11563
11692
|
],
|
|
11564
11693
|
[
|
|
11565
11694
|
/at least \$\d|insufficient (?:funds|balance)|\bbalance\b/i,
|
|
11566
|
-
"Account balance, not input: this operation needs funds on the account. Ask the user to add credit in the Runpod console; list-billing shows recent spend."
|
|
11695
|
+
"Account balance, not input: this operation needs funds on the account. Ask the user to add credit in the Runpod console; list-billing shows recent spend.",
|
|
11696
|
+
"balance"
|
|
11567
11697
|
],
|
|
11568
11698
|
[
|
|
11569
11699
|
/mutually exclusive|cannot be combined with/i,
|
|
11570
|
-
"Two arguments conflict: send only one of them and leave the other out entirely. Do not change anything else."
|
|
11700
|
+
"Two arguments conflict: send only one of them and leave the other out entirely. Do not change anything else.",
|
|
11701
|
+
"conflicting_args"
|
|
11702
|
+
],
|
|
11703
|
+
// A 400 with a bare "Unauthorized" detail from a write the same key can
|
|
11704
|
+
// otherwise make is upstream's answer for an unfunded account.
|
|
11705
|
+
[
|
|
11706
|
+
/^\s*unauthorized\s*$/i,
|
|
11707
|
+
"Upstream refused this write. With a key that can read, this usually means the account has no balance: ask the user to check their balance in the Runpod console before retrying.",
|
|
11708
|
+
"balance"
|
|
11571
11709
|
]
|
|
11572
11710
|
];
|
|
11711
|
+
function errorClass(result) {
|
|
11712
|
+
const { status } = result;
|
|
11713
|
+
const payload = asRecord(result.payload) ?? { error: result.payload };
|
|
11714
|
+
const detail = detailText(payload);
|
|
11715
|
+
if (status === 400 && detail) {
|
|
11716
|
+
for (const [pattern, , cls] of DETAIL_HINTS)
|
|
11717
|
+
if (pattern.test(detail)) return cls;
|
|
11718
|
+
}
|
|
11719
|
+
if (status === 400 || status === 422) return "validation";
|
|
11720
|
+
if (status === 401) return "auth";
|
|
11721
|
+
if (status === 402) return "balance";
|
|
11722
|
+
if (status === 403) return "permission";
|
|
11723
|
+
if (status === 404) return "not_found";
|
|
11724
|
+
if (status === 429) return "rate_limited";
|
|
11725
|
+
if (status === 504) return "timeout";
|
|
11726
|
+
if (status >= 500) return "server";
|
|
11727
|
+
return "other";
|
|
11728
|
+
}
|
|
11573
11729
|
function errorHint(status, detail) {
|
|
11574
11730
|
if (status === 400 && detail) {
|
|
11575
11731
|
for (const [pattern, hint] of DETAIL_HINTS)
|
|
11576
11732
|
if (pattern.test(detail)) return hint;
|
|
11577
|
-
if (/^unauthorized$/i.test(detail.trim()))
|
|
11578
|
-
return "Upstream refused this write. With a key that can read, this usually means the account has no balance: ask the user to check their balance in the Runpod console before retrying.";
|
|
11579
11733
|
}
|
|
11580
11734
|
if (status === 400 || status === 422) return SHAPE_HINT;
|
|
11581
11735
|
if (status === 401) return "The Runpod API key is missing or invalid.";
|