@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/http.mjs
CHANGED
package/dist/stdio.cjs
CHANGED
|
@@ -1183,7 +1183,7 @@ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
1183
1183
|
|
|
1184
1184
|
// src/server.ts
|
|
1185
1185
|
var SERVER_NAME = "Runpod API Server";
|
|
1186
|
-
var SERVER_VERSION = true ? "4.10.
|
|
1186
|
+
var SERVER_VERSION = true ? "4.10.1" : "dev";
|
|
1187
1187
|
|
|
1188
1188
|
// src/specgen/server.ts
|
|
1189
1189
|
var import_server3 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
@@ -1384,6 +1384,14 @@ function validateArguments(inputSchema, args) {
|
|
|
1384
1384
|
}
|
|
1385
1385
|
return null;
|
|
1386
1386
|
}
|
|
1387
|
+
function withoutNullOptionals(inputSchema, args) {
|
|
1388
|
+
const required = new Set(inputSchema.required ?? []);
|
|
1389
|
+
return Object.fromEntries(
|
|
1390
|
+
Object.entries(args).filter(
|
|
1391
|
+
([name, value]) => value !== null || required.has(name)
|
|
1392
|
+
)
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1387
1395
|
async function dispatchGeneratedTool(client, tool, args) {
|
|
1388
1396
|
const rejected = validateArguments(tool.inputSchema, args);
|
|
1389
1397
|
if (rejected) return rejected;
|
|
@@ -11016,23 +11024,40 @@ function createRunpodClient(options = {}) {
|
|
|
11016
11024
|
// src/specgen/clients/sse.ts
|
|
11017
11025
|
var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
|
|
11018
11026
|
var LOG_STREAM_MAX_BYTES = 256 * 1024;
|
|
11027
|
+
var LOG_STREAM_CONNECT_TIMEOUT_MS = 1e4;
|
|
11028
|
+
var LOG_SNAPSHOT_MAX_CHARS = 32e3;
|
|
11019
11029
|
function createSseReader(options = {}) {
|
|
11020
11030
|
const apiKey = options.apiKey ?? process.env.RUNPOD_API_KEY;
|
|
11021
11031
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
11032
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? LOG_STREAM_CONNECT_TIMEOUT_MS;
|
|
11022
11033
|
return async (url, opts) => {
|
|
11023
11034
|
if (!apiKey) throw missingKeyError();
|
|
11024
11035
|
const chunks = [];
|
|
11025
11036
|
let bytes = 0;
|
|
11026
11037
|
let truncated = false;
|
|
11027
11038
|
let streamEstablished = false;
|
|
11039
|
+
const connect = new AbortController();
|
|
11040
|
+
const connectTimer = setTimeout(
|
|
11041
|
+
() => connect.abort(
|
|
11042
|
+
new DOMException(
|
|
11043
|
+
`Runpod log stream sent no headers within ${connectTimeoutMs}ms`,
|
|
11044
|
+
"TimeoutError"
|
|
11045
|
+
)
|
|
11046
|
+
),
|
|
11047
|
+
connectTimeoutMs
|
|
11048
|
+
);
|
|
11028
11049
|
try {
|
|
11029
|
-
const response = await createDeadlineFetch(
|
|
11050
|
+
const response = await createDeadlineFetch(
|
|
11051
|
+
fetchImpl,
|
|
11052
|
+
connectTimeoutMs + opts.maxWaitMs
|
|
11053
|
+
)(url, {
|
|
11030
11054
|
method: "GET",
|
|
11031
11055
|
headers: {
|
|
11032
11056
|
Authorization: `Bearer ${apiKey}`,
|
|
11033
11057
|
Accept: "text/event-stream"
|
|
11034
|
-
}
|
|
11035
|
-
|
|
11058
|
+
},
|
|
11059
|
+
signal: connect.signal
|
|
11060
|
+
}).finally(() => clearTimeout(connectTimer));
|
|
11036
11061
|
if (!response.ok) {
|
|
11037
11062
|
const body = await response.text().catch(() => "");
|
|
11038
11063
|
throw new HttpError(
|
|
@@ -11043,14 +11068,31 @@ function createSseReader(options = {}) {
|
|
|
11043
11068
|
}
|
|
11044
11069
|
streamEstablished = true;
|
|
11045
11070
|
if (response.body) {
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11071
|
+
const reader = response.body.getReader();
|
|
11072
|
+
let timer;
|
|
11073
|
+
const windowClosed = new Promise((resolve2) => {
|
|
11074
|
+
timer = setTimeout(() => resolve2(null), opts.maxWaitMs);
|
|
11075
|
+
});
|
|
11076
|
+
try {
|
|
11077
|
+
for (; ; ) {
|
|
11078
|
+
const read = reader.read();
|
|
11079
|
+
read.catch(() => {
|
|
11080
|
+
});
|
|
11081
|
+
const next = await Promise.race([read, windowClosed]);
|
|
11082
|
+
if (next === null || next.done) break;
|
|
11083
|
+
const boundedChunk = next.value.subarray(0, opts.maxBytes - bytes);
|
|
11084
|
+
chunks.push(boundedChunk);
|
|
11085
|
+
bytes += boundedChunk.length;
|
|
11086
|
+
if (bytes >= opts.maxBytes) {
|
|
11087
|
+
truncated = true;
|
|
11088
|
+
break;
|
|
11089
|
+
}
|
|
11053
11090
|
}
|
|
11091
|
+
} finally {
|
|
11092
|
+
clearTimeout(timer);
|
|
11093
|
+
void reader.cancel().catch(() => {
|
|
11094
|
+
});
|
|
11095
|
+
reader.releaseLock();
|
|
11054
11096
|
}
|
|
11055
11097
|
}
|
|
11056
11098
|
} catch (err) {
|
|
@@ -11076,7 +11118,80 @@ async function collectLogSnapshot(reader, logsUrl, params) {
|
|
|
11076
11118
|
for await (const event of iterateLogEvents(new Response(raw).body)) {
|
|
11077
11119
|
items.push(event.data);
|
|
11078
11120
|
}
|
|
11079
|
-
return
|
|
11121
|
+
return fitSnapshot(items, truncated);
|
|
11122
|
+
}
|
|
11123
|
+
var serializedLength = (snapshot) => JSON.stringify(snapshot, null, 2).length;
|
|
11124
|
+
var LOG_ENTRY_MAX_CHARS = 16e3;
|
|
11125
|
+
var CLIPPABLE_KEYS = ["line", "raw"];
|
|
11126
|
+
function clipText(text, keep) {
|
|
11127
|
+
const code = text.charCodeAt(keep - 1);
|
|
11128
|
+
if (keep > 0 && code >= 55296 && code <= 56319) keep--;
|
|
11129
|
+
return `${text.slice(0, keep)}\u2026 [clipped ${text.length - keep} chars]`;
|
|
11130
|
+
}
|
|
11131
|
+
function clipEntry(entry) {
|
|
11132
|
+
let clipped = false;
|
|
11133
|
+
const out = { ...entry };
|
|
11134
|
+
for (const key of CLIPPABLE_KEYS) {
|
|
11135
|
+
const text = out[key];
|
|
11136
|
+
if (typeof text === "string" && text.length > LOG_ENTRY_MAX_CHARS) {
|
|
11137
|
+
out[key] = clipText(text, LOG_ENTRY_MAX_CHARS);
|
|
11138
|
+
clipped = true;
|
|
11139
|
+
}
|
|
11140
|
+
}
|
|
11141
|
+
return { entry: out, clipped };
|
|
11142
|
+
}
|
|
11143
|
+
function fitEntry(entry, maxChars) {
|
|
11144
|
+
const size = (e) => serializedLength({ items: [e], count: 1, truncated: false });
|
|
11145
|
+
const out = { ...entry };
|
|
11146
|
+
const keys = Object.keys(out).filter((key) => key !== "ts" && typeof out[key] === "string").sort((a, b) => out[b].length - out[a].length);
|
|
11147
|
+
for (const key of keys) {
|
|
11148
|
+
if (size(out) <= maxChars) break;
|
|
11149
|
+
const text = entry[key];
|
|
11150
|
+
let lo = 0;
|
|
11151
|
+
let hi = Math.min(text.length, LOG_ENTRY_MAX_CHARS);
|
|
11152
|
+
while (lo < hi) {
|
|
11153
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
11154
|
+
if (size({ ...out, [key]: clipText(text, mid) }) <= maxChars)
|
|
11155
|
+
lo = mid;
|
|
11156
|
+
else hi = mid - 1;
|
|
11157
|
+
}
|
|
11158
|
+
out[key] = clipText(text, lo);
|
|
11159
|
+
}
|
|
11160
|
+
return out;
|
|
11161
|
+
}
|
|
11162
|
+
var entryTs = (entry) => entry?.ts;
|
|
11163
|
+
function fitSnapshot(items, truncated, maxChars = LOG_SNAPSHOT_MAX_CHARS) {
|
|
11164
|
+
let cut = truncated;
|
|
11165
|
+
const entries = items.map((item) => {
|
|
11166
|
+
const { entry, clipped } = clipEntry(item);
|
|
11167
|
+
if (clipped) cut = true;
|
|
11168
|
+
return entry;
|
|
11169
|
+
});
|
|
11170
|
+
const snapshot = (n, isCut) => ({
|
|
11171
|
+
items: entries.slice(0, n),
|
|
11172
|
+
count: n,
|
|
11173
|
+
truncated: isCut
|
|
11174
|
+
});
|
|
11175
|
+
const whole = snapshot(entries.length, cut);
|
|
11176
|
+
if (entries.length === 0 || serializedLength(whole) <= maxChars) return whole;
|
|
11177
|
+
if (serializedLength(snapshot(1, true)) > maxChars) {
|
|
11178
|
+
entries[0] = fitEntry(items[0], maxChars);
|
|
11179
|
+
if (entries.length === 1) return snapshot(1, true);
|
|
11180
|
+
}
|
|
11181
|
+
let lo = 1;
|
|
11182
|
+
let hi = entries.length - 1;
|
|
11183
|
+
while (lo < hi) {
|
|
11184
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
11185
|
+
if (serializedLength(snapshot(mid, true)) <= maxChars) lo = mid;
|
|
11186
|
+
else hi = mid - 1;
|
|
11187
|
+
}
|
|
11188
|
+
let keep = lo;
|
|
11189
|
+
const omittedTs = entryTs(entries[keep]);
|
|
11190
|
+
if (omittedTs !== void 0) {
|
|
11191
|
+
while (keep > 0 && entryTs(entries[keep - 1]) === omittedTs) keep--;
|
|
11192
|
+
if (keep === 0) keep = lo;
|
|
11193
|
+
}
|
|
11194
|
+
return snapshot(keep, true);
|
|
11080
11195
|
}
|
|
11081
11196
|
|
|
11082
11197
|
// src/specgen/tools/logs.ts
|
|
@@ -11094,13 +11209,13 @@ var logStreamProperties = {
|
|
|
11094
11209
|
},
|
|
11095
11210
|
since: {
|
|
11096
11211
|
type: "string",
|
|
11097
|
-
description: "RFC3339 timestamp to resume
|
|
11212
|
+
description: "RFC3339 timestamp to resume after (exclusive). Replays container lines only; system lines stream live from the call. When set, `tail` is ignored."
|
|
11098
11213
|
},
|
|
11099
11214
|
maxWaitMs: {
|
|
11100
11215
|
type: "integer",
|
|
11101
11216
|
minimum: 500,
|
|
11102
11217
|
maximum: 3e4,
|
|
11103
|
-
description: "How long to read the stream, in ms (default 5000, max 30000)"
|
|
11218
|
+
description: "How long to read the stream once it opens, in ms (default 5000, max 30000)"
|
|
11104
11219
|
}
|
|
11105
11220
|
};
|
|
11106
11221
|
function baseUrl() {
|
|
@@ -11126,7 +11241,7 @@ var streamPodLogs = {
|
|
|
11126
11241
|
title: "Stream Pod logs",
|
|
11127
11242
|
holdsResponse: true,
|
|
11128
11243
|
annotations: readOnly,
|
|
11129
|
-
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
|
|
11244
|
+
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`.",
|
|
11130
11245
|
inputSchema: {
|
|
11131
11246
|
type: "object",
|
|
11132
11247
|
properties: {
|
|
@@ -11148,10 +11263,10 @@ var streamPodLogs = {
|
|
|
11148
11263
|
var streamWorkerLogs = {
|
|
11149
11264
|
name: "stream-worker-logs",
|
|
11150
11265
|
title: "Stream worker logs",
|
|
11151
|
-
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
|
|
11266
|
+
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.",
|
|
11152
11267
|
holdsResponse: true,
|
|
11153
11268
|
annotations: readOnly,
|
|
11154
|
-
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
|
|
11269
|
+
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.",
|
|
11155
11270
|
inputSchema: {
|
|
11156
11271
|
type: "object",
|
|
11157
11272
|
properties: {
|
|
@@ -11295,6 +11410,7 @@ var listPublicEndpoints = {
|
|
|
11295
11410
|
// src/specgen/tools/knowledge.ts
|
|
11296
11411
|
var import_plugin_knowledge = require("@runpod/plugin-knowledge");
|
|
11297
11412
|
var knowledge = (0, import_plugin_knowledge.loadKnowledge)();
|
|
11413
|
+
var resolveGuideId = (id) => knowledge.aliases?.[id] ?? id;
|
|
11298
11414
|
var hiddenRules = new Set(
|
|
11299
11415
|
(process.env.KNOWLEDGE_HIDDEN_RULES ?? "").split(",").map((id) => id.trim()).filter(Boolean)
|
|
11300
11416
|
);
|
|
@@ -11471,7 +11587,9 @@ var readGuide = {
|
|
|
11471
11587
|
title: "Read guide",
|
|
11472
11588
|
annotations: bundledRead,
|
|
11473
11589
|
recordedArguments: ["id"],
|
|
11474
|
-
recordedGuideKind: (args) => knowledge.guides.find(
|
|
11590
|
+
recordedGuideKind: (args) => knowledge.guides.find(
|
|
11591
|
+
(guide) => guide.id === resolveGuideId(String(args.id).trim())
|
|
11592
|
+
)?.kind,
|
|
11475
11593
|
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").',
|
|
11476
11594
|
inputSchema: {
|
|
11477
11595
|
type: "object",
|
|
@@ -11483,7 +11601,9 @@ var readGuide = {
|
|
|
11483
11601
|
},
|
|
11484
11602
|
handler: async (_ctx, args) => {
|
|
11485
11603
|
const id = String(args.id).trim();
|
|
11486
|
-
const guide = knowledge.guides.find(
|
|
11604
|
+
const guide = knowledge.guides.find(
|
|
11605
|
+
(candidate) => candidate.id === resolveGuideId(id)
|
|
11606
|
+
);
|
|
11487
11607
|
if (!guide) {
|
|
11488
11608
|
const close = knowledge.guides.filter(
|
|
11489
11609
|
(candidate) => candidate.id.includes(id) || id.includes(candidate.id)
|
|
@@ -11638,6 +11758,12 @@ var guideResources = knowledge.guides.map((guide) => ({
|
|
|
11638
11758
|
mimeType: "text/markdown",
|
|
11639
11759
|
text: guide.body
|
|
11640
11760
|
}));
|
|
11761
|
+
var findGuideResource = (resources, uri) => {
|
|
11762
|
+
const id = uri.startsWith(GUIDE_URI_PREFIX) ? resolveGuideId(uri.slice(GUIDE_URI_PREFIX.length)) : void 0;
|
|
11763
|
+
return resources.find(
|
|
11764
|
+
(resource) => resource.uri === uri || resource.uri === GUIDE_URI_PREFIX + id
|
|
11765
|
+
);
|
|
11766
|
+
};
|
|
11641
11767
|
|
|
11642
11768
|
// src/specgen/prose.ts
|
|
11643
11769
|
var ABBREVIATIONS = ["e.g.", "i.e.", "etc.", "vs."];
|
|
@@ -11885,6 +12011,7 @@ function captureToolCall(apiKey, event) {
|
|
|
11885
12011
|
server_version: event.serverVersion,
|
|
11886
12012
|
...event.clientName ? { client_name: event.clientName } : {},
|
|
11887
12013
|
...event.guideKind ? { guide_kind: event.guideKind } : {},
|
|
12014
|
+
...event.errorClass ? { error_class: event.errorClass } : {},
|
|
11888
12015
|
...event.blockedBy ? { blocked_by: event.blockedBy } : {},
|
|
11889
12016
|
...event.profile ? { profile: event.profile } : {},
|
|
11890
12017
|
...args
|
|
@@ -12663,7 +12790,7 @@ These tools manage infrastructure only. They do not do SSH sessions, file transf
|
|
|
12663
12790
|
|
|
12664
12791
|
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.
|
|
12665
12792
|
|
|
12666
|
-
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.
|
|
12793
|
+
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.
|
|
12667
12794
|
|
|
12668
12795
|
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.
|
|
12669
12796
|
|
|
@@ -12724,9 +12851,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12724
12851
|
resourceTemplates: []
|
|
12725
12852
|
}));
|
|
12726
12853
|
server.setRequestHandler(import_types2.ReadResourceRequestSchema, async (request) => {
|
|
12727
|
-
const guide = servedGuides.
|
|
12728
|
-
(candidate) => candidate.uri === request.params.uri
|
|
12729
|
-
);
|
|
12854
|
+
const guide = findGuideResource(servedGuides, request.params.uri);
|
|
12730
12855
|
if (opts.analytics) {
|
|
12731
12856
|
captureResourceCall(ctx.apiKey, {
|
|
12732
12857
|
method: "resources/read",
|
|
@@ -12763,6 +12888,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12763
12888
|
ok: false,
|
|
12764
12889
|
status: 429,
|
|
12765
12890
|
durationMs: 0,
|
|
12891
|
+
errorClass: "rate_limited",
|
|
12766
12892
|
...verdict.reason ? { blockedBy: verdict.reason } : {},
|
|
12767
12893
|
profile,
|
|
12768
12894
|
...opts.analytics
|
|
@@ -12788,6 +12914,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12788
12914
|
const generated = servedGeneratedTools.find(
|
|
12789
12915
|
(tool) => tool.name === request.params.name
|
|
12790
12916
|
);
|
|
12917
|
+
const curatedArgs = curated ? withoutNullOptionals(curated.inputSchema, args) : args;
|
|
12791
12918
|
const omitted = directory ? directoryOmittedInputs(request.params.name).filter((key) => key in args) : [];
|
|
12792
12919
|
const rejected = omitted.length ? {
|
|
12793
12920
|
ok: false,
|
|
@@ -12797,10 +12924,10 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12797
12924
|
}
|
|
12798
12925
|
} : curated && !curated.lenientArguments ? validateArguments(
|
|
12799
12926
|
curated.inputSchema,
|
|
12800
|
-
|
|
12927
|
+
curatedArgs
|
|
12801
12928
|
) : null;
|
|
12802
12929
|
const result = rejected ?? await runTool(
|
|
12803
|
-
() => curated ? curated.handler(ctx,
|
|
12930
|
+
() => curated ? curated.handler(ctx, curatedArgs) : dispatchTool(generated, request.params.name, args)
|
|
12804
12931
|
);
|
|
12805
12932
|
logToolCall({
|
|
12806
12933
|
tool: request.params.name,
|
|
@@ -12815,6 +12942,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12815
12942
|
ok: result.ok,
|
|
12816
12943
|
status: result.status,
|
|
12817
12944
|
durationMs: Date.now() - startedAt,
|
|
12945
|
+
...result.ok ? {} : { errorClass: errorClass(result) },
|
|
12818
12946
|
// The directory profile records which tool ran, never what was asked.
|
|
12819
12947
|
...directory ? {} : {
|
|
12820
12948
|
args: recordedArgumentValues(curated?.recordedArguments, args),
|
|
@@ -12854,24 +12982,50 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12854
12982
|
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.";
|
|
12855
12983
|
var DETAIL_HINTS = [
|
|
12856
12984
|
[
|
|
12857
|
-
/no longer any instances available|not enough free GPUs/i,
|
|
12858
|
-
"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."
|
|
12985
|
+
/no longer any instances available|no instances currently available|not enough free GPUs/i,
|
|
12986
|
+
"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.",
|
|
12987
|
+
"capacity"
|
|
12859
12988
|
],
|
|
12860
12989
|
[
|
|
12861
12990
|
/at least \$\d|insufficient (?:funds|balance)|\bbalance\b/i,
|
|
12862
|
-
"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."
|
|
12991
|
+
"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.",
|
|
12992
|
+
"balance"
|
|
12863
12993
|
],
|
|
12864
12994
|
[
|
|
12865
12995
|
/mutually exclusive|cannot be combined with/i,
|
|
12866
|
-
"Two arguments conflict: send only one of them and leave the other out entirely. Do not change anything else."
|
|
12996
|
+
"Two arguments conflict: send only one of them and leave the other out entirely. Do not change anything else.",
|
|
12997
|
+
"conflicting_args"
|
|
12998
|
+
],
|
|
12999
|
+
// A 400 with a bare "Unauthorized" detail from a write the same key can
|
|
13000
|
+
// otherwise make is upstream's answer for an unfunded account.
|
|
13001
|
+
[
|
|
13002
|
+
/^\s*unauthorized\s*$/i,
|
|
13003
|
+
"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.",
|
|
13004
|
+
"balance"
|
|
12867
13005
|
]
|
|
12868
13006
|
];
|
|
13007
|
+
function errorClass(result) {
|
|
13008
|
+
const { status } = result;
|
|
13009
|
+
const payload = asRecord(result.payload) ?? { error: result.payload };
|
|
13010
|
+
const detail = detailText(payload);
|
|
13011
|
+
if (status === 400 && detail) {
|
|
13012
|
+
for (const [pattern, , cls] of DETAIL_HINTS)
|
|
13013
|
+
if (pattern.test(detail)) return cls;
|
|
13014
|
+
}
|
|
13015
|
+
if (status === 400 || status === 422) return "validation";
|
|
13016
|
+
if (status === 401) return "auth";
|
|
13017
|
+
if (status === 402) return "balance";
|
|
13018
|
+
if (status === 403) return "permission";
|
|
13019
|
+
if (status === 404) return "not_found";
|
|
13020
|
+
if (status === 429) return "rate_limited";
|
|
13021
|
+
if (status === 504) return "timeout";
|
|
13022
|
+
if (status >= 500) return "server";
|
|
13023
|
+
return "other";
|
|
13024
|
+
}
|
|
12869
13025
|
function errorHint(status, detail) {
|
|
12870
13026
|
if (status === 400 && detail) {
|
|
12871
13027
|
for (const [pattern, hint] of DETAIL_HINTS)
|
|
12872
13028
|
if (pattern.test(detail)) return hint;
|
|
12873
|
-
if (/^unauthorized$/i.test(detail.trim()))
|
|
12874
|
-
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.";
|
|
12875
13029
|
}
|
|
12876
13030
|
if (status === 400 || status === 422) return SHAPE_HINT;
|
|
12877
13031
|
if (status === 401) return "The Runpod API key is missing or invalid.";
|