@runpod/mcp-server 4.10.0 → 4.11.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 +32 -0
- package/README.md +2 -11
- package/dist/{chunk-FQJFKWUV.mjs → chunk-24UOECYY.mjs} +19 -1
- package/dist/chunk-24UOECYY.mjs.map +1 -0
- package/dist/{chunk-JTK6RETN.mjs → chunk-4M7QDHK4.mjs} +496 -58
- package/dist/chunk-4M7QDHK4.mjs.map +1 -0
- package/dist/hosted.cjs +510 -56
- package/dist/hosted.cjs.map +1 -1
- package/dist/hosted.mjs +2 -2
- package/dist/http.cjs +510 -56
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +2 -2
- package/dist/specgen/profile.cjs +19 -0
- package/dist/specgen/profile.cjs.map +1 -1
- package/dist/specgen/profile.d.cts +6 -3
- package/dist/specgen/profile.d.ts +6 -3
- package/dist/specgen/profile.mjs +3 -1
- package/dist/stdio.cjs +508 -55
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +508 -55
- package/dist/stdio.mjs.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-FQJFKWUV.mjs.map +0 -1
- package/dist/chunk-JTK6RETN.mjs.map +0 -1
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.
|
|
1186
|
+
var SERVER_VERSION = true ? "4.11.0" : "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;
|
|
@@ -1401,25 +1409,16 @@ async function dispatchGeneratedTool(client, tool, args) {
|
|
|
1401
1409
|
const value = args[param.name];
|
|
1402
1410
|
query[param.name] = param.explode === false && Array.isArray(value) ? value.join(",") : value;
|
|
1403
1411
|
}
|
|
1404
|
-
|
|
1405
|
-
if (
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
}
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
if (tool.hasBody && requestBody !== void 0) {
|
|
1419
|
-
const properties = tool.inputSchema.properties ?? {};
|
|
1420
|
-
const defs = tool.inputSchema.$defs ?? {};
|
|
1421
|
-
requestBody = coerceScalars(requestBody, properties.body, defs);
|
|
1422
|
-
}
|
|
1412
|
+
const sent = tool.hasBody ? bodyAsSent(tool, args.body) : { body: args.body };
|
|
1413
|
+
if (!sent)
|
|
1414
|
+
return {
|
|
1415
|
+
ok: false,
|
|
1416
|
+
status: 400,
|
|
1417
|
+
payload: {
|
|
1418
|
+
error: "The body argument arrived as a string that is not valid JSON. Send body as an object."
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
const requestBody = sent.body;
|
|
1423
1422
|
const verbs = client;
|
|
1424
1423
|
const { data, error, response } = await verbs[tool.method](tool.path, {
|
|
1425
1424
|
params: { path: pathParams, query },
|
|
@@ -1432,6 +1431,32 @@ async function dispatchGeneratedTool(client, tool, args) {
|
|
|
1432
1431
|
payload: data ?? { status: response.status }
|
|
1433
1432
|
};
|
|
1434
1433
|
}
|
|
1434
|
+
function bodyAsSent(tool, body) {
|
|
1435
|
+
let parsed = body;
|
|
1436
|
+
if (typeof parsed === "string") {
|
|
1437
|
+
try {
|
|
1438
|
+
parsed = JSON.parse(parsed);
|
|
1439
|
+
} catch {
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (parsed === void 0) return { body: parsed };
|
|
1444
|
+
const properties = tool.inputSchema.properties ?? {};
|
|
1445
|
+
const defs = tool.inputSchema.$defs ?? {};
|
|
1446
|
+
return { body: coerceScalars(parsed, properties.body, defs) };
|
|
1447
|
+
}
|
|
1448
|
+
function argsAsSent(tool, args) {
|
|
1449
|
+
if (!tool.hasBody || args.body === void 0) return args;
|
|
1450
|
+
const sent = bodyAsSent(tool, args.body);
|
|
1451
|
+
return sent ? { ...args, body: sent.body } : null;
|
|
1452
|
+
}
|
|
1453
|
+
function omittedEmptyQueryArg(tool) {
|
|
1454
|
+
const required = new Set(tool.inputSchema.required ?? []);
|
|
1455
|
+
const query = new Set(
|
|
1456
|
+
tool.params.filter((p2) => p2.location === "query").map((p2) => p2.name)
|
|
1457
|
+
);
|
|
1458
|
+
return (name, value) => query.has(name) && !required.has(name) && isEmptyValue(value);
|
|
1459
|
+
}
|
|
1435
1460
|
function isEmptyValue(value) {
|
|
1436
1461
|
return value === null || value === "" || Array.isArray(value) && !value.length;
|
|
1437
1462
|
}
|
|
@@ -3868,6 +3893,109 @@ var generatedTools = [
|
|
|
3868
3893
|
}
|
|
3869
3894
|
}
|
|
3870
3895
|
},
|
|
3896
|
+
{
|
|
3897
|
+
name: "exec-pod",
|
|
3898
|
+
title: "Run a command in a Pod",
|
|
3899
|
+
operationId: "execPod",
|
|
3900
|
+
description: `Run one command inside a running Pod's container and get stdout, stderr and the exit code back. No SSH key, public IP or exposed port is needed. command is an argv array run without a shell: pass ["sh", "-c", "..."] for pipes, redirects, globs or &&. One call waits at most timeoutSeconds (1-25, default 20). timedOut true means the call stopped waiting, NOT that the command stopped: it keeps running in the container. For anything longer (model downloads, pip installs, builds), start it in the background and log to a file, e.g. ["sh", "-c", "nohup wget -c -O /workspace/m.safetensors URL > /tmp/dl.log 2>&1 &"], then check progress with later calls (ls -la, du -h, tail /tmp/dl.log). stdout and stderr are each capped at 1 MiB (truncated true). Right after a create, the Pod can answer 404 "Pod not found" for about 10 seconds even with the right id, then 409 while its container starts: retry in about 10 seconds, a few times at most. A Pod that is not running answers 409. A host not yet updated for exec answers 503. Reaches only Pods owned by the API key's own account. Commands can change or delete data in the Pod: run write commands only for what the user asked.`,
|
|
3901
|
+
directoryDescription: "Run one command inside a running Pod's container and return stdout, stderr and the exit code. The command is an argv array run without a shell. One call waits at most timeoutSeconds (1-25, default 20); a command still running then keeps running in the container, and the result says timedOut. stdout and stderr are each capped at 1 MiB. A Pod that is not running answers 409, and a host not yet updated for exec answers 503. Reaches only Pods owned by the caller's own account. The command can change or delete data in the Pod.",
|
|
3902
|
+
method: "POST",
|
|
3903
|
+
path: "/v2/pods/{id}/exec",
|
|
3904
|
+
params: [
|
|
3905
|
+
{
|
|
3906
|
+
name: "id",
|
|
3907
|
+
location: "path"
|
|
3908
|
+
}
|
|
3909
|
+
],
|
|
3910
|
+
hasBody: true,
|
|
3911
|
+
annotations: {
|
|
3912
|
+
readOnlyHint: false,
|
|
3913
|
+
destructiveHint: true,
|
|
3914
|
+
idempotentHint: false,
|
|
3915
|
+
openWorldHint: true
|
|
3916
|
+
},
|
|
3917
|
+
inputSchema: {
|
|
3918
|
+
type: "object",
|
|
3919
|
+
properties: {
|
|
3920
|
+
id: {
|
|
3921
|
+
type: "string",
|
|
3922
|
+
description: "Pod identifier"
|
|
3923
|
+
},
|
|
3924
|
+
body: {
|
|
3925
|
+
$ref: "#/$defs/PodExecRequest",
|
|
3926
|
+
type: "object"
|
|
3927
|
+
}
|
|
3928
|
+
},
|
|
3929
|
+
required: ["id", "body"],
|
|
3930
|
+
$defs: {
|
|
3931
|
+
PodExecRequest: {
|
|
3932
|
+
type: "object",
|
|
3933
|
+
additionalProperties: false,
|
|
3934
|
+
required: ["command"],
|
|
3935
|
+
properties: {
|
|
3936
|
+
command: {
|
|
3937
|
+
type: "array",
|
|
3938
|
+
minItems: 1,
|
|
3939
|
+
maxItems: 256,
|
|
3940
|
+
prefixItems: [
|
|
3941
|
+
{
|
|
3942
|
+
type: "string",
|
|
3943
|
+
minLength: 1,
|
|
3944
|
+
maxLength: 4096,
|
|
3945
|
+
pattern: "^[^\\x00]*[^\\t-\\r \x85\xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\\x00][^\\x00]*$"
|
|
3946
|
+
}
|
|
3947
|
+
],
|
|
3948
|
+
items: {
|
|
3949
|
+
type: "string",
|
|
3950
|
+
maxLength: 4096,
|
|
3951
|
+
pattern: "^[^\\x00]*$"
|
|
3952
|
+
},
|
|
3953
|
+
description: 'Command to run, as an argv array of 1-256 arguments of at most 4096 characters each. It is run directly, never through a shell; for pipes, redirects or globbing pass `["sh", "-c", "..."]`.',
|
|
3954
|
+
examples: [
|
|
3955
|
+
["nvidia-smi", "--query-gpu=name,memory.used", "--format=csv"]
|
|
3956
|
+
]
|
|
3957
|
+
},
|
|
3958
|
+
timeoutSeconds: {
|
|
3959
|
+
type: "integer",
|
|
3960
|
+
minimum: 1,
|
|
3961
|
+
maximum: 25,
|
|
3962
|
+
default: 20,
|
|
3963
|
+
description: "How long the call waits for the command to finish, in seconds. A command still running at this point returns with `timedOut: true`.",
|
|
3964
|
+
examples: [10]
|
|
3965
|
+
},
|
|
3966
|
+
cwd: {
|
|
3967
|
+
type: "string",
|
|
3968
|
+
pattern: "^/[^\\x00-\\x1f\\x7f]*$",
|
|
3969
|
+
maxLength: 1024,
|
|
3970
|
+
description: "Absolute working directory for the command, at most 1024 characters and without control characters. Defaults to the image's `WORKDIR`.",
|
|
3971
|
+
examples: ["/workspace"]
|
|
3972
|
+
},
|
|
3973
|
+
env: {
|
|
3974
|
+
type: "object",
|
|
3975
|
+
maxProperties: 64,
|
|
3976
|
+
propertyNames: {
|
|
3977
|
+
type: "string",
|
|
3978
|
+
minLength: 1,
|
|
3979
|
+
maxLength: 256,
|
|
3980
|
+
pattern: "^[^=\\x00]+$"
|
|
3981
|
+
},
|
|
3982
|
+
additionalProperties: {
|
|
3983
|
+
type: "string",
|
|
3984
|
+
maxLength: 16384,
|
|
3985
|
+
pattern: "^[^\\x00]*$"
|
|
3986
|
+
},
|
|
3987
|
+
description: "Up to 64 environment variables added to the container's environment for this command only. A name is 1-256 characters and must not contain `=` or a NUL character; a value is at most 16384 characters and must not contain a NUL character.",
|
|
3988
|
+
examples: [
|
|
3989
|
+
{
|
|
3990
|
+
HF_HOME: "/workspace/hf"
|
|
3991
|
+
}
|
|
3992
|
+
]
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
},
|
|
3871
3999
|
{
|
|
3872
4000
|
name: "get-cluster",
|
|
3873
4001
|
title: "Get cluster",
|
|
@@ -7930,6 +8058,122 @@ var generatedTools = [
|
|
|
7930
8058
|
}
|
|
7931
8059
|
];
|
|
7932
8060
|
|
|
8061
|
+
// src/specgen/validate.ts
|
|
8062
|
+
var import__ = require("ajv/dist/2020.js");
|
|
8063
|
+
function schemaValidationMode(value = process.env.MCP_SCHEMA_VALIDATION) {
|
|
8064
|
+
const mode = value?.trim().toLowerCase();
|
|
8065
|
+
return mode === "off" || mode === "enforce" ? mode : "shadow";
|
|
8066
|
+
}
|
|
8067
|
+
var ajv = new import__.Ajv2020({
|
|
8068
|
+
strict: false,
|
|
8069
|
+
allErrors: true,
|
|
8070
|
+
validateFormats: false
|
|
8071
|
+
});
|
|
8072
|
+
var compiled = /* @__PURE__ */ new WeakMap();
|
|
8073
|
+
function validatorFor(schema) {
|
|
8074
|
+
let validate = compiled.get(schema);
|
|
8075
|
+
if (!validate) {
|
|
8076
|
+
validate = ajv.compile(structuredClone(schema));
|
|
8077
|
+
compiled.set(schema, validate);
|
|
8078
|
+
}
|
|
8079
|
+
return validate;
|
|
8080
|
+
}
|
|
8081
|
+
var MAX_NULL_PASSES = 8;
|
|
8082
|
+
function isNullTypeError(error, data) {
|
|
8083
|
+
return error.keyword === "type" && error.instancePath !== "" && valueAt(data, error.instancePath) === null;
|
|
8084
|
+
}
|
|
8085
|
+
function pointerSegments(pointer) {
|
|
8086
|
+
return pointer.split("/").slice(1).map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
8087
|
+
}
|
|
8088
|
+
function valueAt(data, pointer) {
|
|
8089
|
+
let node = data;
|
|
8090
|
+
for (const key of pointerSegments(pointer)) {
|
|
8091
|
+
if (node === null || typeof node !== "object") return void 0;
|
|
8092
|
+
node = node[key];
|
|
8093
|
+
}
|
|
8094
|
+
return node;
|
|
8095
|
+
}
|
|
8096
|
+
function removeAt(data, pointer) {
|
|
8097
|
+
const segments = pointerSegments(pointer);
|
|
8098
|
+
const last = segments.pop();
|
|
8099
|
+
const parent = valueAt(data, segments.length ? `/${segments.join("/")}` : "");
|
|
8100
|
+
if (last === void 0 || parent === null || typeof parent !== "object")
|
|
8101
|
+
return false;
|
|
8102
|
+
if (Array.isArray(parent)) return false;
|
|
8103
|
+
delete parent[last];
|
|
8104
|
+
return true;
|
|
8105
|
+
}
|
|
8106
|
+
var WRAPPER_KEYWORDS = /* @__PURE__ */ new Set(["if"]);
|
|
8107
|
+
function toViolation(error) {
|
|
8108
|
+
const base = pointerSegments(error.instancePath);
|
|
8109
|
+
let path2 = base.join(".");
|
|
8110
|
+
let message = error.message ?? "is invalid";
|
|
8111
|
+
const params = error.params;
|
|
8112
|
+
if (error.keyword === "required" && typeof params.missingProperty === "string") {
|
|
8113
|
+
path2 = [...base, params.missingProperty].join(".");
|
|
8114
|
+
message = "is required";
|
|
8115
|
+
} else if (error.keyword === "additionalProperties" && typeof params.additionalProperty === "string") {
|
|
8116
|
+
path2 = [...base, params.additionalProperty].join(".");
|
|
8117
|
+
message = "is not an accepted argument here";
|
|
8118
|
+
} else if (error.keyword === "pattern") {
|
|
8119
|
+
message = "is not in an accepted format";
|
|
8120
|
+
}
|
|
8121
|
+
const allowed = error.keyword === "enum" ? params.allowedValues : error.keyword === "const" ? [params.allowedValue] : void 0;
|
|
8122
|
+
return {
|
|
8123
|
+
path: path2,
|
|
8124
|
+
keyword: error.keyword,
|
|
8125
|
+
message,
|
|
8126
|
+
...allowed ? { allowed } : {},
|
|
8127
|
+
schemaPath: error.schemaPath
|
|
8128
|
+
};
|
|
8129
|
+
}
|
|
8130
|
+
function schemaViolations(schema, args, omitted = () => false) {
|
|
8131
|
+
const validate = validatorFor(schema);
|
|
8132
|
+
const data = structuredClone(
|
|
8133
|
+
Object.fromEntries(
|
|
8134
|
+
Object.entries(args).filter(([name, value]) => !omitted(name, value))
|
|
8135
|
+
)
|
|
8136
|
+
);
|
|
8137
|
+
for (let pass = 0; ; pass++) {
|
|
8138
|
+
if (validate(data)) return [];
|
|
8139
|
+
const errors = validate.errors ?? [];
|
|
8140
|
+
let removed = false;
|
|
8141
|
+
if (pass < MAX_NULL_PASSES) {
|
|
8142
|
+
for (const error of errors.filter((e) => isNullTypeError(e, data)))
|
|
8143
|
+
removed = removeAt(data, error.instancePath) || removed;
|
|
8144
|
+
}
|
|
8145
|
+
if (!removed) {
|
|
8146
|
+
return errors.filter((e) => !WRAPPER_KEYWORDS.has(e.keyword)).map(toViolation);
|
|
8147
|
+
}
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
8150
|
+
var INVALID_ARGUMENTS_HINT = "Fix the listed fields and call again.";
|
|
8151
|
+
function invalidArgumentsPayload(violations) {
|
|
8152
|
+
return {
|
|
8153
|
+
error: "Invalid arguments",
|
|
8154
|
+
code: "invalid_arguments",
|
|
8155
|
+
fields: violations.map(({ path: path2, message, allowed }) => ({
|
|
8156
|
+
path: path2,
|
|
8157
|
+
problem: message,
|
|
8158
|
+
...allowed ? { allowed } : {}
|
|
8159
|
+
})),
|
|
8160
|
+
hint: INVALID_ARGUMENTS_HINT
|
|
8161
|
+
};
|
|
8162
|
+
}
|
|
8163
|
+
function logSchemaViolations(tool, mode, violations) {
|
|
8164
|
+
console.error(
|
|
8165
|
+
"schema_violation",
|
|
8166
|
+
JSON.stringify({
|
|
8167
|
+
tool,
|
|
8168
|
+
mode,
|
|
8169
|
+
violations: violations.map(({ keyword, schemaPath }) => ({
|
|
8170
|
+
keyword,
|
|
8171
|
+
schemaPath
|
|
8172
|
+
}))
|
|
8173
|
+
})
|
|
8174
|
+
);
|
|
8175
|
+
}
|
|
8176
|
+
|
|
7933
8177
|
// src/specgen/tools/annotations.ts
|
|
7934
8178
|
var readOnly = {
|
|
7935
8179
|
readOnlyHint: true,
|
|
@@ -8143,6 +8387,8 @@ var DIRECTORY_TOOLS = {
|
|
|
8143
8387
|
"deploy-hub-repo": IN,
|
|
8144
8388
|
"detach-cost-center-resource": IN,
|
|
8145
8389
|
"endpoint-health": IN,
|
|
8390
|
+
// env can carry tokens (HF_TOKEN for a model download), so the directory omits it.
|
|
8391
|
+
"exec-pod": { include: true, omitInputs: ["body.env"] },
|
|
8146
8392
|
"get-capacity": IN,
|
|
8147
8393
|
"get-cluster": IN,
|
|
8148
8394
|
"get-cost-center": IN,
|
|
@@ -8262,6 +8508,21 @@ function directoryOmittedInputs(name) {
|
|
|
8262
8508
|
const decision = DIRECTORY_TOOLS[name];
|
|
8263
8509
|
return decision?.include ? decision.omitInputs ?? [] : [];
|
|
8264
8510
|
}
|
|
8511
|
+
function omittedInputsIn(omit, args) {
|
|
8512
|
+
return omit.filter((key) => {
|
|
8513
|
+
const [parent, child] = key.split(".");
|
|
8514
|
+
if (child === void 0) return parent in args;
|
|
8515
|
+
let value = args[parent];
|
|
8516
|
+
if (typeof value === "string") {
|
|
8517
|
+
try {
|
|
8518
|
+
value = JSON.parse(value);
|
|
8519
|
+
} catch {
|
|
8520
|
+
return false;
|
|
8521
|
+
}
|
|
8522
|
+
}
|
|
8523
|
+
return !!value && typeof value === "object" && child in value;
|
|
8524
|
+
});
|
|
8525
|
+
}
|
|
8265
8526
|
|
|
8266
8527
|
// src/specgen/redact.ts
|
|
8267
8528
|
var REDACTED = "[redacted]";
|
|
@@ -11016,23 +11277,40 @@ function createRunpodClient(options = {}) {
|
|
|
11016
11277
|
// src/specgen/clients/sse.ts
|
|
11017
11278
|
var LOG_STREAM_DEFAULT_WAIT_MS = 5e3;
|
|
11018
11279
|
var LOG_STREAM_MAX_BYTES = 256 * 1024;
|
|
11280
|
+
var LOG_STREAM_CONNECT_TIMEOUT_MS = 1e4;
|
|
11281
|
+
var LOG_SNAPSHOT_MAX_CHARS = 32e3;
|
|
11019
11282
|
function createSseReader(options = {}) {
|
|
11020
11283
|
const apiKey = options.apiKey ?? process.env.RUNPOD_API_KEY;
|
|
11021
11284
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
11285
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? LOG_STREAM_CONNECT_TIMEOUT_MS;
|
|
11022
11286
|
return async (url, opts) => {
|
|
11023
11287
|
if (!apiKey) throw missingKeyError();
|
|
11024
11288
|
const chunks = [];
|
|
11025
11289
|
let bytes = 0;
|
|
11026
11290
|
let truncated = false;
|
|
11027
11291
|
let streamEstablished = false;
|
|
11292
|
+
const connect = new AbortController();
|
|
11293
|
+
const connectTimer = setTimeout(
|
|
11294
|
+
() => connect.abort(
|
|
11295
|
+
new DOMException(
|
|
11296
|
+
`Runpod log stream sent no headers within ${connectTimeoutMs}ms`,
|
|
11297
|
+
"TimeoutError"
|
|
11298
|
+
)
|
|
11299
|
+
),
|
|
11300
|
+
connectTimeoutMs
|
|
11301
|
+
);
|
|
11028
11302
|
try {
|
|
11029
|
-
const response = await createDeadlineFetch(
|
|
11303
|
+
const response = await createDeadlineFetch(
|
|
11304
|
+
fetchImpl,
|
|
11305
|
+
connectTimeoutMs + opts.maxWaitMs
|
|
11306
|
+
)(url, {
|
|
11030
11307
|
method: "GET",
|
|
11031
11308
|
headers: {
|
|
11032
11309
|
Authorization: `Bearer ${apiKey}`,
|
|
11033
11310
|
Accept: "text/event-stream"
|
|
11034
|
-
}
|
|
11035
|
-
|
|
11311
|
+
},
|
|
11312
|
+
signal: connect.signal
|
|
11313
|
+
}).finally(() => clearTimeout(connectTimer));
|
|
11036
11314
|
if (!response.ok) {
|
|
11037
11315
|
const body = await response.text().catch(() => "");
|
|
11038
11316
|
throw new HttpError(
|
|
@@ -11043,14 +11321,31 @@ function createSseReader(options = {}) {
|
|
|
11043
11321
|
}
|
|
11044
11322
|
streamEstablished = true;
|
|
11045
11323
|
if (response.body) {
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
|
|
11324
|
+
const reader = response.body.getReader();
|
|
11325
|
+
let timer;
|
|
11326
|
+
const windowClosed = new Promise((resolve2) => {
|
|
11327
|
+
timer = setTimeout(() => resolve2(null), opts.maxWaitMs);
|
|
11328
|
+
});
|
|
11329
|
+
try {
|
|
11330
|
+
for (; ; ) {
|
|
11331
|
+
const read = reader.read();
|
|
11332
|
+
read.catch(() => {
|
|
11333
|
+
});
|
|
11334
|
+
const next = await Promise.race([read, windowClosed]);
|
|
11335
|
+
if (next === null || next.done) break;
|
|
11336
|
+
const boundedChunk = next.value.subarray(0, opts.maxBytes - bytes);
|
|
11337
|
+
chunks.push(boundedChunk);
|
|
11338
|
+
bytes += boundedChunk.length;
|
|
11339
|
+
if (bytes >= opts.maxBytes) {
|
|
11340
|
+
truncated = true;
|
|
11341
|
+
break;
|
|
11342
|
+
}
|
|
11053
11343
|
}
|
|
11344
|
+
} finally {
|
|
11345
|
+
clearTimeout(timer);
|
|
11346
|
+
void reader.cancel().catch(() => {
|
|
11347
|
+
});
|
|
11348
|
+
reader.releaseLock();
|
|
11054
11349
|
}
|
|
11055
11350
|
}
|
|
11056
11351
|
} catch (err) {
|
|
@@ -11076,7 +11371,80 @@ async function collectLogSnapshot(reader, logsUrl, params) {
|
|
|
11076
11371
|
for await (const event of iterateLogEvents(new Response(raw).body)) {
|
|
11077
11372
|
items.push(event.data);
|
|
11078
11373
|
}
|
|
11079
|
-
return
|
|
11374
|
+
return fitSnapshot(items, truncated);
|
|
11375
|
+
}
|
|
11376
|
+
var serializedLength = (snapshot) => JSON.stringify(snapshot, null, 2).length;
|
|
11377
|
+
var LOG_ENTRY_MAX_CHARS = 16e3;
|
|
11378
|
+
var CLIPPABLE_KEYS = ["line", "raw"];
|
|
11379
|
+
function clipText(text, keep) {
|
|
11380
|
+
const code = text.charCodeAt(keep - 1);
|
|
11381
|
+
if (keep > 0 && code >= 55296 && code <= 56319) keep--;
|
|
11382
|
+
return `${text.slice(0, keep)}\u2026 [clipped ${text.length - keep} chars]`;
|
|
11383
|
+
}
|
|
11384
|
+
function clipEntry(entry) {
|
|
11385
|
+
let clipped = false;
|
|
11386
|
+
const out = { ...entry };
|
|
11387
|
+
for (const key of CLIPPABLE_KEYS) {
|
|
11388
|
+
const text = out[key];
|
|
11389
|
+
if (typeof text === "string" && text.length > LOG_ENTRY_MAX_CHARS) {
|
|
11390
|
+
out[key] = clipText(text, LOG_ENTRY_MAX_CHARS);
|
|
11391
|
+
clipped = true;
|
|
11392
|
+
}
|
|
11393
|
+
}
|
|
11394
|
+
return { entry: out, clipped };
|
|
11395
|
+
}
|
|
11396
|
+
function fitEntry(entry, maxChars) {
|
|
11397
|
+
const size = (e) => serializedLength({ items: [e], count: 1, truncated: false });
|
|
11398
|
+
const out = { ...entry };
|
|
11399
|
+
const keys = Object.keys(out).filter((key) => key !== "ts" && typeof out[key] === "string").sort((a, b) => out[b].length - out[a].length);
|
|
11400
|
+
for (const key of keys) {
|
|
11401
|
+
if (size(out) <= maxChars) break;
|
|
11402
|
+
const text = entry[key];
|
|
11403
|
+
let lo = 0;
|
|
11404
|
+
let hi = Math.min(text.length, LOG_ENTRY_MAX_CHARS);
|
|
11405
|
+
while (lo < hi) {
|
|
11406
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
11407
|
+
if (size({ ...out, [key]: clipText(text, mid) }) <= maxChars)
|
|
11408
|
+
lo = mid;
|
|
11409
|
+
else hi = mid - 1;
|
|
11410
|
+
}
|
|
11411
|
+
out[key] = clipText(text, lo);
|
|
11412
|
+
}
|
|
11413
|
+
return out;
|
|
11414
|
+
}
|
|
11415
|
+
var entryTs = (entry) => entry?.ts;
|
|
11416
|
+
function fitSnapshot(items, truncated, maxChars = LOG_SNAPSHOT_MAX_CHARS) {
|
|
11417
|
+
let cut = truncated;
|
|
11418
|
+
const entries = items.map((item) => {
|
|
11419
|
+
const { entry, clipped } = clipEntry(item);
|
|
11420
|
+
if (clipped) cut = true;
|
|
11421
|
+
return entry;
|
|
11422
|
+
});
|
|
11423
|
+
const snapshot = (n, isCut) => ({
|
|
11424
|
+
items: entries.slice(0, n),
|
|
11425
|
+
count: n,
|
|
11426
|
+
truncated: isCut
|
|
11427
|
+
});
|
|
11428
|
+
const whole = snapshot(entries.length, cut);
|
|
11429
|
+
if (entries.length === 0 || serializedLength(whole) <= maxChars) return whole;
|
|
11430
|
+
if (serializedLength(snapshot(1, true)) > maxChars) {
|
|
11431
|
+
entries[0] = fitEntry(items[0], maxChars);
|
|
11432
|
+
if (entries.length === 1) return snapshot(1, true);
|
|
11433
|
+
}
|
|
11434
|
+
let lo = 1;
|
|
11435
|
+
let hi = entries.length - 1;
|
|
11436
|
+
while (lo < hi) {
|
|
11437
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
11438
|
+
if (serializedLength(snapshot(mid, true)) <= maxChars) lo = mid;
|
|
11439
|
+
else hi = mid - 1;
|
|
11440
|
+
}
|
|
11441
|
+
let keep = lo;
|
|
11442
|
+
const omittedTs = entryTs(entries[keep]);
|
|
11443
|
+
if (omittedTs !== void 0) {
|
|
11444
|
+
while (keep > 0 && entryTs(entries[keep - 1]) === omittedTs) keep--;
|
|
11445
|
+
if (keep === 0) keep = lo;
|
|
11446
|
+
}
|
|
11447
|
+
return snapshot(keep, true);
|
|
11080
11448
|
}
|
|
11081
11449
|
|
|
11082
11450
|
// src/specgen/tools/logs.ts
|
|
@@ -11094,13 +11462,13 @@ var logStreamProperties = {
|
|
|
11094
11462
|
},
|
|
11095
11463
|
since: {
|
|
11096
11464
|
type: "string",
|
|
11097
|
-
description: "RFC3339 timestamp to resume
|
|
11465
|
+
description: "RFC3339 timestamp to resume after (exclusive). Replays container lines only; system lines stream live from the call. When set, `tail` is ignored."
|
|
11098
11466
|
},
|
|
11099
11467
|
maxWaitMs: {
|
|
11100
11468
|
type: "integer",
|
|
11101
11469
|
minimum: 500,
|
|
11102
11470
|
maximum: 3e4,
|
|
11103
|
-
description: "How long to read the stream, in ms (default 5000, max 30000)"
|
|
11471
|
+
description: "How long to read the stream once it opens, in ms (default 5000, max 30000)"
|
|
11104
11472
|
}
|
|
11105
11473
|
};
|
|
11106
11474
|
function baseUrl() {
|
|
@@ -11126,7 +11494,7 @@ var streamPodLogs = {
|
|
|
11126
11494
|
title: "Stream Pod logs",
|
|
11127
11495
|
holdsResponse: true,
|
|
11128
11496
|
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
|
|
11497
|
+
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
11498
|
inputSchema: {
|
|
11131
11499
|
type: "object",
|
|
11132
11500
|
properties: {
|
|
@@ -11148,10 +11516,10 @@ var streamPodLogs = {
|
|
|
11148
11516
|
var streamWorkerLogs = {
|
|
11149
11517
|
name: "stream-worker-logs",
|
|
11150
11518
|
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
|
|
11519
|
+
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
11520
|
holdsResponse: true,
|
|
11153
11521
|
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
|
|
11522
|
+
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
11523
|
inputSchema: {
|
|
11156
11524
|
type: "object",
|
|
11157
11525
|
properties: {
|
|
@@ -11295,6 +11663,7 @@ var listPublicEndpoints = {
|
|
|
11295
11663
|
// src/specgen/tools/knowledge.ts
|
|
11296
11664
|
var import_plugin_knowledge = require("@runpod/plugin-knowledge");
|
|
11297
11665
|
var knowledge = (0, import_plugin_knowledge.loadKnowledge)();
|
|
11666
|
+
var resolveGuideId = (id) => knowledge.aliases?.[id] ?? id;
|
|
11298
11667
|
var hiddenRules = new Set(
|
|
11299
11668
|
(process.env.KNOWLEDGE_HIDDEN_RULES ?? "").split(",").map((id) => id.trim()).filter(Boolean)
|
|
11300
11669
|
);
|
|
@@ -11471,7 +11840,9 @@ var readGuide = {
|
|
|
11471
11840
|
title: "Read guide",
|
|
11472
11841
|
annotations: bundledRead,
|
|
11473
11842
|
recordedArguments: ["id"],
|
|
11474
|
-
recordedGuideKind: (args) => knowledge.guides.find(
|
|
11843
|
+
recordedGuideKind: (args) => knowledge.guides.find(
|
|
11844
|
+
(guide) => guide.id === resolveGuideId(String(args.id).trim())
|
|
11845
|
+
)?.kind,
|
|
11475
11846
|
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
11847
|
inputSchema: {
|
|
11477
11848
|
type: "object",
|
|
@@ -11483,7 +11854,9 @@ var readGuide = {
|
|
|
11483
11854
|
},
|
|
11484
11855
|
handler: async (_ctx, args) => {
|
|
11485
11856
|
const id = String(args.id).trim();
|
|
11486
|
-
const guide = knowledge.guides.find(
|
|
11857
|
+
const guide = knowledge.guides.find(
|
|
11858
|
+
(candidate) => candidate.id === resolveGuideId(id)
|
|
11859
|
+
);
|
|
11487
11860
|
if (!guide) {
|
|
11488
11861
|
const close = knowledge.guides.filter(
|
|
11489
11862
|
(candidate) => candidate.id.includes(id) || id.includes(candidate.id)
|
|
@@ -11638,6 +12011,12 @@ var guideResources = knowledge.guides.map((guide) => ({
|
|
|
11638
12011
|
mimeType: "text/markdown",
|
|
11639
12012
|
text: guide.body
|
|
11640
12013
|
}));
|
|
12014
|
+
var findGuideResource = (resources, uri) => {
|
|
12015
|
+
const id = uri.startsWith(GUIDE_URI_PREFIX) ? resolveGuideId(uri.slice(GUIDE_URI_PREFIX.length)) : void 0;
|
|
12016
|
+
return resources.find(
|
|
12017
|
+
(resource) => resource.uri === uri || resource.uri === GUIDE_URI_PREFIX + id
|
|
12018
|
+
);
|
|
12019
|
+
};
|
|
11641
12020
|
|
|
11642
12021
|
// src/specgen/prose.ts
|
|
11643
12022
|
var ABBREVIATIONS = ["e.g.", "i.e.", "etc.", "vs."];
|
|
@@ -11713,13 +12092,40 @@ function withoutCredentialExamples(schema) {
|
|
|
11713
12092
|
};
|
|
11714
12093
|
return strip(schema);
|
|
11715
12094
|
}
|
|
11716
|
-
function
|
|
11717
|
-
if (!omit.length) return schema;
|
|
12095
|
+
function withoutKeys(schema, omit) {
|
|
11718
12096
|
const properties = { ...schema.properties ?? {} };
|
|
11719
12097
|
for (const key of omit) delete properties[key];
|
|
11720
12098
|
const required = Array.isArray(schema.required) ? schema.required.filter((key) => !omit.includes(key)) : schema.required;
|
|
11721
12099
|
return { ...schema, properties, ...required ? { required } : {} };
|
|
11722
12100
|
}
|
|
12101
|
+
function withoutInputs(schema, omit) {
|
|
12102
|
+
if (!omit.length) return schema;
|
|
12103
|
+
let out = withoutKeys(
|
|
12104
|
+
schema,
|
|
12105
|
+
omit.filter((key) => !key.includes("."))
|
|
12106
|
+
);
|
|
12107
|
+
for (const key of omit.filter((k) => k.includes("."))) {
|
|
12108
|
+
const [parent, child] = key.split(".");
|
|
12109
|
+
const properties = out.properties ?? {};
|
|
12110
|
+
const node = properties[parent];
|
|
12111
|
+
if (!node) continue;
|
|
12112
|
+
const ref = typeof node.$ref === "string" ? node.$ref : void 0;
|
|
12113
|
+
const defName = ref?.startsWith("#/$defs/") ? ref.slice(8) : void 0;
|
|
12114
|
+
const defs = out.$defs ?? {};
|
|
12115
|
+
if (defName && defs[defName]) {
|
|
12116
|
+
out = {
|
|
12117
|
+
...out,
|
|
12118
|
+
$defs: { ...defs, [defName]: withoutKeys(defs[defName], [child]) }
|
|
12119
|
+
};
|
|
12120
|
+
} else {
|
|
12121
|
+
out = {
|
|
12122
|
+
...out,
|
|
12123
|
+
properties: { ...properties, [parent]: withoutKeys(node, [child]) }
|
|
12124
|
+
};
|
|
12125
|
+
}
|
|
12126
|
+
}
|
|
12127
|
+
return out;
|
|
12128
|
+
}
|
|
11723
12129
|
function callShape(schema) {
|
|
11724
12130
|
const defs = schema.$defs ?? {};
|
|
11725
12131
|
const resolve2 = (node) => {
|
|
@@ -11885,6 +12291,7 @@ function captureToolCall(apiKey, event) {
|
|
|
11885
12291
|
server_version: event.serverVersion,
|
|
11886
12292
|
...event.clientName ? { client_name: event.clientName } : {},
|
|
11887
12293
|
...event.guideKind ? { guide_kind: event.guideKind } : {},
|
|
12294
|
+
...event.errorClass ? { error_class: event.errorClass } : {},
|
|
11888
12295
|
...event.blockedBy ? { blocked_by: event.blockedBy } : {},
|
|
11889
12296
|
...event.profile ? { profile: event.profile } : {},
|
|
11890
12297
|
...args
|
|
@@ -12663,7 +13070,7 @@ These tools manage infrastructure only. They do not do SSH sessions, file transf
|
|
|
12663
13070
|
|
|
12664
13071
|
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
13072
|
|
|
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.
|
|
13073
|
+
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
13074
|
|
|
12668
13075
|
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
13076
|
|
|
@@ -12682,6 +13089,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12682
13089
|
const caller = callerId(ctx.apiKey);
|
|
12683
13090
|
const profile = opts.profile ?? "default";
|
|
12684
13091
|
const directory = profile === "directory";
|
|
13092
|
+
const schemaMode = schemaValidationMode();
|
|
12685
13093
|
const alp = directory ? void 0 : opts.alp;
|
|
12686
13094
|
const servedGeneratedTools = directory ? generatedTools.filter((tool) => directoryIncludes(tool.name)) : generatedTools;
|
|
12687
13095
|
const servedCuratedTools = [
|
|
@@ -12724,9 +13132,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12724
13132
|
resourceTemplates: []
|
|
12725
13133
|
}));
|
|
12726
13134
|
server.setRequestHandler(import_types2.ReadResourceRequestSchema, async (request) => {
|
|
12727
|
-
const guide = servedGuides.
|
|
12728
|
-
(candidate) => candidate.uri === request.params.uri
|
|
12729
|
-
);
|
|
13135
|
+
const guide = findGuideResource(servedGuides, request.params.uri);
|
|
12730
13136
|
if (opts.analytics) {
|
|
12731
13137
|
captureResourceCall(ctx.apiKey, {
|
|
12732
13138
|
method: "resources/read",
|
|
@@ -12763,6 +13169,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12763
13169
|
ok: false,
|
|
12764
13170
|
status: 429,
|
|
12765
13171
|
durationMs: 0,
|
|
13172
|
+
errorClass: "rate_limited",
|
|
12766
13173
|
...verdict.reason ? { blockedBy: verdict.reason } : {},
|
|
12767
13174
|
profile,
|
|
12768
13175
|
...opts.analytics
|
|
@@ -12788,7 +13195,8 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12788
13195
|
const generated = servedGeneratedTools.find(
|
|
12789
13196
|
(tool) => tool.name === request.params.name
|
|
12790
13197
|
);
|
|
12791
|
-
const
|
|
13198
|
+
const curatedArgs = curated ? withoutNullOptionals(curated.inputSchema, args) : args;
|
|
13199
|
+
const omitted = directory ? omittedInputsIn(directoryOmittedInputs(request.params.name), args) : [];
|
|
12792
13200
|
const rejected = omitted.length ? {
|
|
12793
13201
|
ok: false,
|
|
12794
13202
|
status: 400,
|
|
@@ -12797,10 +13205,28 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12797
13205
|
}
|
|
12798
13206
|
} : curated && !curated.lenientArguments ? validateArguments(
|
|
12799
13207
|
curated.inputSchema,
|
|
12800
|
-
|
|
12801
|
-
) : null;
|
|
12802
|
-
|
|
12803
|
-
|
|
13208
|
+
curatedArgs
|
|
13209
|
+
) : generated ? validateArguments(generated.inputSchema, args) : null;
|
|
13210
|
+
let schemaRejected = null;
|
|
13211
|
+
if (!rejected && schemaMode !== "off") {
|
|
13212
|
+
const sent = generated ? argsAsSent(generated, args) : null;
|
|
13213
|
+
const violations = curated ? curated.lenientArguments ? [] : schemaViolations(curated.inputSchema, curatedArgs) : generated && sent ? schemaViolations(
|
|
13214
|
+
generated.inputSchema,
|
|
13215
|
+
sent,
|
|
13216
|
+
omittedEmptyQueryArg(generated)
|
|
13217
|
+
) : [];
|
|
13218
|
+
if (violations.length) {
|
|
13219
|
+
logSchemaViolations(request.params.name, schemaMode, violations);
|
|
13220
|
+
if (schemaMode === "enforce")
|
|
13221
|
+
schemaRejected = {
|
|
13222
|
+
ok: false,
|
|
13223
|
+
status: 400,
|
|
13224
|
+
payload: invalidArgumentsPayload(violations)
|
|
13225
|
+
};
|
|
13226
|
+
}
|
|
13227
|
+
}
|
|
13228
|
+
const result = rejected ?? schemaRejected ?? await runTool(
|
|
13229
|
+
() => curated ? curated.handler(ctx, curatedArgs) : dispatchTool(generated, request.params.name, args)
|
|
12804
13230
|
);
|
|
12805
13231
|
logToolCall({
|
|
12806
13232
|
tool: request.params.name,
|
|
@@ -12815,6 +13241,7 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12815
13241
|
ok: result.ok,
|
|
12816
13242
|
status: result.status,
|
|
12817
13243
|
durationMs: Date.now() - startedAt,
|
|
13244
|
+
...result.ok ? {} : { errorClass: errorClass(result) },
|
|
12818
13245
|
// The directory profile records which tool ran, never what was asked.
|
|
12819
13246
|
...directory ? {} : {
|
|
12820
13247
|
args: recordedArgumentValues(curated?.recordedArguments, args),
|
|
@@ -12854,24 +13281,50 @@ function createSpecgenServer(ctx, version = "0.1.0", opts = {}) {
|
|
|
12854
13281
|
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
13282
|
var DETAIL_HINTS = [
|
|
12856
13283
|
[
|
|
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."
|
|
13284
|
+
/no longer any instances available|no instances currently available|not enough free GPUs/i,
|
|
13285
|
+
"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.",
|
|
13286
|
+
"capacity"
|
|
12859
13287
|
],
|
|
12860
13288
|
[
|
|
12861
13289
|
/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."
|
|
13290
|
+
"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.",
|
|
13291
|
+
"balance"
|
|
12863
13292
|
],
|
|
12864
13293
|
[
|
|
12865
13294
|
/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."
|
|
13295
|
+
"Two arguments conflict: send only one of them and leave the other out entirely. Do not change anything else.",
|
|
13296
|
+
"conflicting_args"
|
|
13297
|
+
],
|
|
13298
|
+
// A 400 with a bare "Unauthorized" detail from a write the same key can
|
|
13299
|
+
// otherwise make is upstream's answer for an unfunded account.
|
|
13300
|
+
[
|
|
13301
|
+
/^\s*unauthorized\s*$/i,
|
|
13302
|
+
"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.",
|
|
13303
|
+
"balance"
|
|
12867
13304
|
]
|
|
12868
13305
|
];
|
|
13306
|
+
function errorClass(result) {
|
|
13307
|
+
const { status } = result;
|
|
13308
|
+
const payload = asRecord(result.payload) ?? { error: result.payload };
|
|
13309
|
+
const detail = detailText(payload);
|
|
13310
|
+
if (status === 400 && detail) {
|
|
13311
|
+
for (const [pattern, , cls] of DETAIL_HINTS)
|
|
13312
|
+
if (pattern.test(detail)) return cls;
|
|
13313
|
+
}
|
|
13314
|
+
if (status === 400 || status === 422) return "validation";
|
|
13315
|
+
if (status === 401) return "auth";
|
|
13316
|
+
if (status === 402) return "balance";
|
|
13317
|
+
if (status === 403) return "permission";
|
|
13318
|
+
if (status === 404) return "not_found";
|
|
13319
|
+
if (status === 429) return "rate_limited";
|
|
13320
|
+
if (status === 504) return "timeout";
|
|
13321
|
+
if (status >= 500) return "server";
|
|
13322
|
+
return "other";
|
|
13323
|
+
}
|
|
12869
13324
|
function errorHint(status, detail) {
|
|
12870
13325
|
if (status === 400 && detail) {
|
|
12871
13326
|
for (const [pattern, hint] of DETAIL_HINTS)
|
|
12872
13327
|
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
13328
|
}
|
|
12876
13329
|
if (status === 400 || status === 422) return SHAPE_HINT;
|
|
12877
13330
|
if (status === 401) return "The Runpod API key is missing or invalid.";
|